以 npm 佈署並提供 tea-sdlc 指令 #25
Notifications
Due Date
No due date set.
Blocks
#17 以 install.js 產生並移除各平台轉接檔
plugins/tea-sdlc
Reference: plugins/tea-sdlc#25
Reference in New Issue
Block a user
母議題
#1 — tea-sdlc:以 tea 驅動 SDLC 全流程的跨平台指令組
Problem Statement
使用者要在一台新機器上用到這六個流程指令,目前得為七個 agent 平台各學一套安裝流程:Claude Code 走 marketplace、Antigravity 得先 clone 再給本地路徑、OpenCode 得手動
cp -r到 skills 目錄。裝完之後其實什麼都沒有——skills/是空的,真正會生效的轉接檔要靠尚未實作的install.js產生。更根本的是,這七套流程都只解決「把檔案放到哪」,沒有一套解決「放進去的檔案要指向哪個正本」。議題 #1 原本的答案是「路徑於產生時替換為絕對路徑」,但那個答案在用版本管理器管 Node 的機器上是壞的:fnm 把 Node 版號寫進全域安裝路徑(
.../node-versions/v26.7.0/installation/lib/node_modules),升一次 Node,七個平台的轉接檔會同時指向不存在的檔案,而使用者不會收到任何錯誤——只會發現/sdlc-plan不動了。Solution
用 npm 當佈署管道,並讓轉接檔不再持有路徑。
使用者跑一次
npm i -g https://gitea.jsc.idv.tw/plugins/tea-sdlc.git,得到一個tea-sdlc指令;跑一次tea-sdlc install,六個流程指令就在他裝了的每個平台上可用。轉接檔裡沒有路徑,只有一句「執行tea-sdlc prompt --name sdlc-plan並遵照其輸出」——正本在哪由 PATH 上的tea-sdlc自己回推,Node 升級、換版本管理器、改 npm prefix 都不影響它。這個設計讓更新變成兩件粗細不同的事:改流程正本、腳本、模板或規則,只要重跑
npm i -g,下一次叫用就讀到新的,不必重新佈署;只有指令數量或轉接檔模板本身變了,才需要再跑一次tea-sdlc install。因此不需要update子指令。本工作包負責 npm 打包與
tea-sdlc指令本身(prompt、status、薄殼),轉接檔的產生與移除留在 #17。User Stories
tea-sdlc,以便佈署、移除、查狀態都從同一個地方進去。tea-sdlc status就知道目前版本、正本位置與環境缺什麼,以便在出事前先知道。status順便告訴我 Gitea 登入還有沒有效,以便不用等到跑流程指令才發現 token 過期。status在環境不健康時指令本身仍算成功,以便分辨「查詢失敗」與「成功查到問題」這兩件下一步完全不同的事。tea-sdlc prompt --name <指令名>取得流程正本原文,以便照著執行而不必知道檔案放在哪。npm link開發,改動 working tree 立即生效,以便不必每改一行就重裝。status告訴我目前tea-sdlc實際解析到哪個目錄,以便確認自己是不是在 link 模式。.tmp/不會被打包給使用者。npm update -g然後得到一個沒有真的更新的結果。skills/有內容時還查得到。--dry-run,以便先看清楚會發生什麼再決定。tea-sdlc不在 PATH 上時,agent 停下來叫我安裝,而不是自己憑空實作一套看起來像那麼回事、卻完全沒碰 Gitea 的流程。prompt的原樣輸出有自己的斷言,以便不必為了它而稀釋其餘子指令的單行 JSON 契約。範圍邊界
本工作包做:npm 打包設定、
bin/tea-sdlc.js薄殼、prompt與status兩個子指令、runBin()測試 helper、README 的安裝/更新/移除段落。本工作包不做:轉接檔的產生與移除(
install.js的實作、平台偵測、勾選介面、產生標記的寫入),那些是 #17。本工作包只提供install/uninstall兩個子指令的薄殼 dispatch 與 flag 解析,動作實作由 #17 補上。因此本工作包阻擋 #17:#17 的轉接檔內容是「執行
tea-sdlc prompt --name …」,那條驗收標準要能真的驗過,prompt必須先存在。Implementation Decisions
佈署管道
以 git URL 安裝,不發佈到任何 registry:
npm i -g https://gitea.jsc.idv.tw/plugins/tea-sdlc.git。版本以 git tag 指定(…git#v0.1.0)。選這條是因為 README 其餘七個平台的安裝指令本來就都用同一個 git URL,多一條佈署管道不該多一套憑證與 registry 設定;日後若需要真 semver,升級到 Gitea 自架 npm registry 只是補publishConfig,不是重寫。只支援全域安裝,明確拒絕 npx。npx 的套件落在會被 npm 自動清掉的快取目錄,雖然本設計的轉接檔不持有路徑、不會因此斷裂,但 npx 每次叫用重新解析套件的行為會讓「正本是哪一版」無法回答,與
status的職責直接衝突。套件內容
package.json移除private: true、新增bin、新增files白名單(涵蓋prompts/scripts/templates/references/skills/install.js/bin/README.md/AGENTS.md)。選白名單而非.npmignore黑名單,是因為兩者的失敗模式不對稱:白名單漏掉東西會在安裝當下被既有的PLUGIN_LAYOUT_BROKEN檢查立刻炸出來,黑名單漏掉排除則是沉默地把測試檔案發給使用者。不新增
preparescript:本專案零外部套件、無建置步驟,加了只是多一個會在 git 安裝時跑的未定義行為。「零外部套件」這條約束不變,dependencies與devDependencies一律不得出現。指令介面
單一 bin
tea-sdlc,入口是薄殼bin/tea-sdlc.js,只負責取第一個位置參數當子指令、把其餘 argv 交給lib.js的parseFlags()、再 dispatch 到install.js的具名 export。動作實作不放進薄殼,因為install.js在模組邊界上的職責是「平台偵測與轉接檔產生,唯一知道各平台目錄結構的地方」,把參數解析與狀態比對塞進去會讓它變成兩件事。薄殼複用
lib.js的parseFlags與main,因此 bin 的輸入輸出契約與既有十五支腳本自動一致:只接受具名 flag、不認得的 flag 一律拒絕、輸出單行 JSON{ok, data, error:{code, message}}、exit 0 或 1。注意parseFlags明確拒絕位置參數,所以子指令必須由薄殼在交棒前自行取走,而prompt的目標以--name指定,不是位置參數。子指令四個:
install、uninstall(薄殼由本工作包提供,動作屬 #17)、prompt、status。沒有update子指令,理由見「Further Notes」。prompt的契約tea-sdlc prompt --name <指令名> [--adapter-version <版本>]成功時把對應的流程正本原樣印到 stdout,exit 0。這是全專案唯一輸出非 JSON 的子指令,例外的理由寫死成一句:它的輸出要餵給模型讀。把幾百行 markdown 包進單行 JSON、再逼模型反跳脫,只會增加它讀錯的機率;JSON envelope 的價值是可程式化判斷成敗,而這條路徑的成功就是內容本身。失敗時仍走 JSON envelope + exit 1(
PROMPT_NOT_FOUND)——成功是內容,失敗才需要結構。--adapter-version由轉接檔帶入自己被產生時的版本。與套件版本不符時,在輸出的 markdown 最前面加一行警告,指示使用者重跑tea-sdlc install,其後照常輸出正本原文。選這個機制而非只靠status,是因為轉接檔過時不會壞掉——它只是繼續指向一個指令名稱,症狀是「新加的指令怎麼沒出現」這種查不出來的東西;而警告出現在模型每次叫用必定會讀到的位置,比任何被動機制都可靠。status的契約回報四類事實:版本與正本位置(
readlink -f $(command -v tea-sdlc),順帶揭露是不是npm link)、環境第一層(node/git/tea是否在 PATH)、環境第二層(Gitea 登入是否有效,會打網路)、各平台轉接檔現況(偵測到哪些平台、裝了幾份、有沒有被改過)。第二層要查:
status存在的理由就是「在出事前告訴我現在是什麼狀態」,而 token 失效是實務上最常見的故障,一次GET /user很便宜,為它多開一個--deep旗標是把判斷推回給使用者。data形狀:{version, root, linked, healthy, environment:{node, git, tea, login}, platforms:[{name, dir, detected, adapters:{expected, present, stale}}]}。ok與healthy分離:ok在既有契約裡的意思是「這支腳本跑成功了」,不兼差表達環境健康與否;環境不健康時ok仍為true,健康旗標是data.healthy。混用會讓呼叫端分不出「status 掛了」和「status 成功查到你環境有問題」,而這兩件事的下一步完全不同。前置檢查的分工
install只跑四層前置檢查的第一層(執行環境),且只警告不中止。缺tea完全不影響轉接檔產生,硬擋等於逼使用者為了裝 plugin 先去裝 tea;但完全不查也不行——安裝是一次性動作,使用者裝完就走,沒有警告他會以為一切就緒。第三、四層(repo issues 寫入權、時間追蹤)在安裝當下沒有--repo可查,無從執行。lib.js的checkEnvironment()不加查tea-sdlc自己:那段檢查碼要能執行,tea-sdlc必然已經跑起來了,查它是自證的廢話。真正會出事的時刻是「轉接檔被讀到、但指令不存在」,那一刻連 Node 都還沒啟動,任何程式內的檢查都攔不到——只能靠轉接檔本身的純文字指引(#17)。status則要查,因為那正是它的職責。沒有
--link旗標轉接檔不持有路徑,
npm link直接換掉 PATH 上的tea-sdlc,working tree 自動成為正本,不需要任何旗標配合。留一個什麼都不做的旗標比沒有它更糟。「link 模式要可見」這個需求由status的root/linked欄位滿足,而且比旗標誠實:它報的是現況,不是安裝當下宣告的意圖。錯誤碼
沿用
ScriptError。新增MISSING_SUBCOMMAND、UNKNOWN_SUBCOMMAND、PROMPT_NOT_FOUND。文件
README 的安裝段改寫為三段,且移除那段要明寫順序反直覺之處:安裝=
npm i -g <git url>後tea-sdlc install;更新=重跑同樣那兩行,並明寫「不要用npm update -g,git 相依之下它不保證重新解析 ref」;移除=先tea-sdlc uninstall再npm rm -g tea-sdlc(順序反了就沒有指令可以拿來刪轉接檔了)。既有七套 marketplace 安裝流程收進附錄「其他安裝方式」,不刪除——那些機制本身有用(自動更新、plugin 清單),等
skills/有內容時會回來。但也不與 npm 並列為「兩種都行」,因為現在skills/是空的,照那些流程裝完不會出現任何指令。AGENTS.md的模組邊界表新增bin/一列(職責:子指令解析與 dispatch;邊界:不含任何平台目錄知識與動作實作),隨本工作包的 PR 一起改——文件描述現況,議題描述意圖,在bin/還不存在時先寫進邊界表會讓讀它的 AI 助理去找一個不存在的目錄。Testing Decisions
什麼是好的測試:只測外部行為,不測實作細節。對本工作包而言,外部行為就是「給定一個子指令與一組 flag,
tea-sdlc印出什麼、exit code 是多少」。不測install.js的具名 export、不測薄殼的 dispatch 表、不測 JSON 內部的建構過程。接縫:全專案維持兩個接縫,都在行程邊界,本工作包新增其中第二個。
scripts/*.js的 CLI 邊界,helperrunScript()。bin/tea-sdlc.js的 CLI 邊界,helperrunBin()。選在這裡而非函式層,理由與既有接縫相同:它是七個平台共用的實際呼叫方式,測到的東西就是使用者真正會執行的東西,且不會因內部重構而破碎。
為什麼另寫
runBin()而不擴充runScript():runScript()的路徑寫死在scripts/底下,而它的parseSingleLine()是刻意在輸出不是恰好一行時主動拋錯的——那段斷言的存在本身就是在測「單行 JSON」這個契約。給它加一個raw: true旗標等於在十五支腳本的契約上開一個只有 bin 要用的後門。兩個 helper 各自持有各自的契約斷言,互不污染:install/uninstall/status斷言單行 JSON,prompt斷言原樣 markdown。外部相依的隔離:
status的第二層檢查走lib.js既有的單一 HTTP 出口,測試沿用TEA_SDLC_API_BASE指向本機 stub server。command -v類的 PATH 探測沿用既有的pathWithOnly(),精確造出「缺 git」「缺 tea」的情境。測試對象與順序
prompt的輸出形狀——多行原樣 markdown、非 JSON、exit 0;--name指向不存在的指令時回單行 JSON + exit 1。這是被叫用頻率最高、也是唯一破格的契約。prompt --adapter-version的版本比對——相符時輸出與不帶該 flag 時完全一致;不符時第一行為警告、其後逐字等於正本原文。status的輸出契約——單行 JSON;登入失效時ok仍為true而data.healthy為false(這條專門守住ok與healthy的分離);缺git/tea時對應欄位為 false。npm pack --dry-run的檔案清單斷言prompts/、scripts/、templates/、references/在內,test/、.tmp/不在內。Prior art:
test/script-contract.test.js已用子行程 + stub server 驗過腳本契約與四層前置檢查;helperrun-script.js、stub-gitea.js、temp-repo.js可直接沿用其模式。測試執行器維持 Node 內建node:test+node:assert,暫存一律寫到.tmp/。驗收標準
npm i -g <git url>後 PATH 上出現tea-sdlc指令package.json已移除private、新增bin、新增files白名單,且未新增prepare、未出現任何dependencies/devDependenciesnpm pack --dry-run的清單包含prompts/、scripts/、templates/、references/,不包含test/、.tmp/parseFlags處理;缺子指令、未知子指令、未知 flag 各回對應錯誤碼與 exit 1install/uninstall/status輸出單行 JSON{ok, data, error:{code, message}}tea-sdlc prompt --name <指令名>原樣輸出流程正本、exit 0;名稱不存在時回單行 JSONPROMPT_NOT_FOUND+ exit 1--adapter-version與套件版本不符時,輸出第一行為重跑tea-sdlc install的警告,其後為正本原文tea-sdlc status回報 version、root、linked、healthy、environment(node/git/tea/login);platforms因平台偵測屬 #17 的模組邊界,改由 #17 交付status的ok仍為true,健康狀態反映在data.healthystatus的root在npm link情境下指向 working tree 且linked為trueinstall缺少node/git/tea時印出警告但不中止runBin()helper,install/uninstall/status斷言單行 JSON、prompt斷言原樣 markdown,且runScript()的既有斷言未被放寬prompt與status;更新/移除的完整指令表(含「不要用npm update -g」與「移除要先tea-sdlc uninstall再npm rm -g」)因需install/uninstall實際可用,改由 #17 交付AGENTS.md模組邊界表新增bin/一列Out of Scope
publishConfig即可)。update子指令。postinstall之類在 npm 安裝過程中自動佈署的機制。Issue.time_estimate的填寫(等 #8 的估算機制建立後回填)。Further Notes
為什麼沒有
update子指令。轉接檔不持有路徑也不持有內容副本,所以更新被拆成粗細不同的兩件事:改prompts/、scripts/、templates/、references/時,npm i -g <git url>之後下一次叫用就讀到新的,完全不必重新佈署;只有指令數量或轉接檔模板本身變了,才需要再跑一次tea-sdlc install。一個語義等同「重跑 install」的update只會讓使用者猶豫該用哪個。重跑
install的預設值(實作屬 #17,但決策源自本工作包的更新語義):偵測到既有轉接檔時,預設只勾已經裝過的平台;完全沒有既有轉接檔時才預設全勾。這讓「重跑 install」天然就是更新動作,不會因為使用者半年後裝了新平台就把轉接檔擴散過去——帶擴散副作用的更新動作會讓人不敢隨手重跑,然後轉接檔就一直過時。這條取代了 #17 原本的「預設全勾」。fnm 的實測(Q6 選擇改採指令叫用的證據):本機
npm root -g為/root/.local/share/fnm/node-versions/v26.7.0/installation/lib/node_modules,Node 版號寫在路徑裡,且 fnm 為每個 Node 版本開獨立的全域根——升級 Node 之後那條路徑不只失效,套件根本不在新的全域根裡。PATH 可用性的實測(指令叫用機制的前提):目前 shell 與
bash -lc都找得到全域 bin,但走的是不同的 fnm shim 根(/run/user/0/fnm_multishells/…與~/.local/state/fnm_multishells/…);那些 per-session 目錄全是指向~/.local/share/fnm/aliases/default的 symlink,所以舊 session 留下的目錄仍可用。唯一找不到的是env -i的裸環境。這個破口的失敗時機在任何 Node 程式啟動之前,因此只能由 #17 的轉接檔第三段純文字承接。與 #17 的相依:本工作包 blocks #17。#17 的轉接檔內容是「執行
tea-sdlc prompt --name …」,那條驗收標準要能真的驗過,prompt必須先存在。與 #1 的關係:議題 #1 的「跨平台佈署」段仍記載原本的絕對路徑替換方案,已被本工作包與 #17 取代。#1 正在實作中,其契約內容不在本工作包動它。
十五條驗收標準勾了十四條。#26 / #27 / #28 / #29 / #30 已全部合併並關閉,#17 的轉接檔產生與移除亦已交付。
未勾選:「
npm i -g <git url>後 PATH 上出現tea-sdlc指令」。套件這一側是好的,但這條路徑目前在本 Gitea 上跑不起來,原因在伺服器端:
Gitea 主機上有一支 hook script 會呼叫
curl,而該主機沒有安裝 curl,因此所有git clone/git fetch(git pack 傳輸)一律失敗;git ls-remote與git push不走這條路徑,所以仍然正常。這也是本 repo 的本地工作副本當初「clone 失效、改以 archive 取得」的原因。套件本身已驗證可用,用 npm 實際產生的 tarball 全域安裝:
npm i -g ./tea-sdlc-0.0.1.tgz→ PATH 上出現tea-sdlc,root落在node_modules底下、linked為false,tea-sdlc prompt --name sdlc-plan從安裝副本讀得到正本(代表files白名單沒漏東西)。npm i -g <本地目錄>→linked為true且root指向 working tree。可用的繞道(走 Gitea 的 archive 端點,不經 git pack 傳輸,實測可裝起來):
真正的修法是在 Gitea 主機上補裝
curl(或修掉那支 hook)。修好之後這條驗收標準即可直接驗證並勾選。最後一條「
npm i -g <git url>後 PATH 上出現tea-sdlc指令」已驗證通過,十五條全數達成。先前擋住驗證的兩個問題都已解決:
1. Gitea 主機缺
curl(伺服器端)。 主機上一支在 upload-pack 期間執行的 script 會呼叫curl,而主機沒有安裝,導致全站所有git clone/git fetch失敗(ls-remote與push不走該路徑,因此正常)。已由管理者修復。2. README 的安裝指令少了
git+前綴(本專案)。 npm 只有看到git+https://才會當成 git repo;純https://….git會被歸類成遠端壓縮檔,以TAR_BAD_ARCHIVE: Unrecognized archive format失敗。已於 #43 修正,並加測試釘住「指向.git的npm i -g一律要帶git+前綴」。註:本票與 #26 的驗收標準原文寫的都是不帶
git+的形式,實際正確寫法是npm i -g git+https://gitea.jsc.idv.tw/plugins/tea-sdlc.git。端到端驗證(從合併後的 master 全新安裝,逐字照 README 執行):
子工作包 #26 / #27 / #28 / #29 / #30 均已合併關閉;轉接檔的產生與移除由 #17 交付。