feat(佈署): 以 npm 裝出 tea-sdlc 指令並產生各平台轉接檔

單一入口 bin/tea-sdlc.js 認四個子指令。第一個位置參數是子指令,其餘 argv 原樣
交出去——既有的 flag 解析拒絕位置參數,所以子指令必須在那之前就被取走。

轉接檔裡沒有路徑,只有一句 tea-sdlc prompt --name <指令名>。正本在哪由 PATH 上
的 tea-sdlc 自己回推:fnm 把 Node 版號寫進全域安裝路徑,寫死路徑的話升一次
Node,七個平台的轉接檔會同時指向不存在的檔案,而且不會有任何錯誤訊息。

prompt 是全專案唯一輸出非 JSON 的路徑,理由只有一個:它的輸出要餵給模型讀。
失敗仍走 envelope——成功是內容,失敗才需要結構。

status 的 ok 不兼差表達環境好壞,健康與否放在 data.healthy:呼叫端要分得出
「status 掛了」與「status 成功查到你環境有問題」。

install 只寫進偵測得到的平台;缺 git/tea 只警告不中止,因為那兩個完全不影響
轉接檔產生,硬擋等於逼使用者為了裝 plugin 先去裝 tea。uninstall 只刪帶產生標記
的檔案,使用者自己寫的同名檔案一律留著並在輸出裡交代。裝哪些指令以 prompts/ 裡
實際存在的正本為準,不是寫死的六個名字——裝出指向不存在正本的轉接檔,使用者只會
看到 PROMPT_NOT_FOUND。

流程正本的 description 前綴在抄進轉接檔之前就檢查:有三個平台關不掉自動觸發,
全靠那句話把 description 窄到不會被誤判,不能等使用者發現誤觸才知道漏了。

議題 #26 #27 #28 #29 #17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 06:59:37 +00:00
co-authored by Claude Opus 5
parent f736d11049
commit 17178f0c4d
16 changed files with 1746 additions and 39 deletions
+80
View File
@@ -0,0 +1,80 @@
/**
* README 的安裝段。
*
* 這一段是使用者照著打的東西,打錯一個字就裝不起來,所以指令要能逐字複製執行,
* 而且不能留下已經不成立的說法——並列一條「照著做不會出現任何指令」的流程等於在說謊。
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { repoRoot } from './helpers/run-script.js';
const readme = () => readFileSync(join(repoRoot, 'README.md'), 'utf8');
const agents = () => readFileSync(join(repoRoot, 'AGENTS.md'), 'utf8');
/** 取出所有 bash 圍欄裡的指令行 */
function commands(text) {
return [...text.matchAll(/```bash\n([\s\S]*?)```/g)]
.flatMap((block) => block[1].split('\n'))
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'));
}
test('安裝以 npm 為唯一建議路徑,指令含完整可複製的 git URL', () => {
const install = commands(readme()).filter((line) => line.startsWith('npm i -g'));
assert.equal(install.length > 0, true, 'README 沒有 npm 安裝指令');
assert.ok(
install.some((line) => line.includes('https://gitea.jsc.idv.tw/plugins/tea-sdlc.git')),
`安裝指令少了完整 git URL:${install.join(' / ')}`,
);
});
test('更新與移除各有完整指令表,順序講清楚先 uninstall 再 npm rm', () => {
const text = readme();
assert.match(text, /npm i -g https:\/\/gitea\.jsc\.idv\.tw\/plugins\/tea-sdlc\.git/);
assert.match(text, /tea-sdlc uninstall/);
assert.match(text, /npm rm -g tea-sdlc/);
// 先砍套件就再也刪不掉那些孤兒轉接檔,順序本身就是內容
assert.ok(
text.indexOf('tea-sdlc uninstall') < text.indexOf('npm rm -g tea-sdlc'),
'uninstall 必須寫在 npm rm -g 之前',
);
});
test('明寫不要用 npm update -g,並說出為什麼', () => {
const text = readme();
assert.match(text, /npm update -g/);
const at = text.indexOf('npm update -g');
assert.match(text.slice(at - 200, at + 200), /不要|別/);
});
test('四個子指令在 README 上都有用途說明', () => {
const text = readme();
for (const name of ['install', 'uninstall', 'prompt', 'status']) {
assert.match(text, new RegExp(`tea-sdlc ${name}`), `README 沒有交代 ${name}`);
}
});
test('七套 marketplace 流程移進附錄保留,內容沒有被刪掉', () => {
const text = readme();
const appendix = text.slice(text.indexOf('## 附錄'));
assert.ok(text.includes('## 附錄'), 'README 沒有附錄');
for (const line of ['claude plugin marketplace add', 'codex plugin marketplace add', 'agy plugin install']) {
assert.ok(appendix.includes(line), `附錄少了 ${line}`);
}
});
test('模組邊界表指得到實際存在的檔案', () => {
const text = agents();
assert.match(text, /`bin\/tea-sdlc\.js`/);
assert.match(text, /`scripts\/install\.js`/);
assert.equal(/\| `install\.js` \|/.test(text), false, '表上還留著已經不存在的根目錄 install.js');
});