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:
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user