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,100 @@
|
||||
/**
|
||||
* 指令入口的契約:第一個位置參數是子指令,其餘 argv 原樣交給子指令。
|
||||
*
|
||||
* 入口是唯一收位置參數的地方——既有的 flag 解析明確拒絕位置參數,所以子指令必須
|
||||
* 在交出去之前先被取走。這裡釘住的是「取走」這件事本身,以及取不到、取錯時的說法。
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { repoRoot, manifest, runBin } from './helpers/run-script.js';
|
||||
|
||||
/** 入口必須認得的四個名字,缺一個就有轉接檔或文件會指向不存在的子指令 */
|
||||
const SUBCOMMANDS = ['install', 'uninstall', 'prompt', 'status'];
|
||||
|
||||
|
||||
// ── 子指令解析 ─────────────────────────────────────────────────────
|
||||
|
||||
test('未帶子指令時說清楚缺的是什麼,而不是印出用法就當作成功', async () => {
|
||||
const { code, json } = await runBin([]);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.ok, false);
|
||||
assert.equal(json.error.code, 'MISSING_SUBCOMMAND');
|
||||
// 訊息要列得出可用的名字,使用者才不用回頭翻文件
|
||||
for (const name of SUBCOMMANDS) assert.match(json.error.message, new RegExp(name));
|
||||
});
|
||||
|
||||
test('不認得的子指令與「flag 打錯」分得開', async () => {
|
||||
const { code, json } = await runBin(['instal']);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.error.code, 'UNKNOWN_SUBCOMMAND');
|
||||
assert.match(json.error.message, /instal/);
|
||||
});
|
||||
|
||||
test('四個子指令的名字都被認得,沒有一個掉進 UNKNOWN_SUBCOMMAND', async () => {
|
||||
for (const name of SUBCOMMANDS) {
|
||||
const { json } = await runBin([name, '--no-such-flag']);
|
||||
|
||||
assert.notEqual(
|
||||
json.error?.code,
|
||||
'UNKNOWN_SUBCOMMAND',
|
||||
`子指令 ${name} 沒被入口認得`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('子指令之後的未知 flag 沿用既有的 UNKNOWN_FLAG,不另立一套', async () => {
|
||||
const { code, json } = await runBin(['status', '--verbose']);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.error.code, 'UNKNOWN_FLAG');
|
||||
assert.match(json.error.message, /--verbose/);
|
||||
});
|
||||
|
||||
test('子指令當成位置參數被取走,不會再被 flag 解析當成不認得的參數', async () => {
|
||||
const { json } = await runBin(['status']);
|
||||
|
||||
assert.equal(json.ok, true);
|
||||
});
|
||||
|
||||
|
||||
// ── 打包 ───────────────────────────────────────────────────────────
|
||||
|
||||
test('manifest 可發佈:移除 private、有 bin、有 files 白名單,且沒有 prepare', () => {
|
||||
const pkg = manifest();
|
||||
|
||||
assert.equal(pkg.private, undefined);
|
||||
assert.equal(pkg.bin['tea-sdlc'], 'bin/tea-sdlc.js');
|
||||
assert.ok(Array.isArray(pkg.files) && pkg.files.length > 0);
|
||||
assert.equal(pkg.scripts.prepare, undefined);
|
||||
});
|
||||
|
||||
test('零外部套件這條線沒有因為要發佈而鬆掉', () => {
|
||||
const pkg = manifest();
|
||||
|
||||
assert.equal(pkg.dependencies, undefined);
|
||||
assert.equal(pkg.devDependencies, undefined);
|
||||
});
|
||||
|
||||
test('打包內容以白名單決定:四個正本目錄都在,測試與暫存都不在', () => {
|
||||
const packed = JSON.parse(
|
||||
execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: repoRoot, encoding: 'utf8' }),
|
||||
);
|
||||
const files = packed[0].files.map((file) => file.path);
|
||||
|
||||
for (const dir of ['prompts/', 'scripts/', 'templates/', 'references/', 'bin/']) {
|
||||
assert.ok(
|
||||
files.some((file) => file.startsWith(dir)),
|
||||
`打包內容少了 ${dir}`,
|
||||
);
|
||||
}
|
||||
for (const dir of ['test/', '.tmp/']) {
|
||||
assert.equal(
|
||||
files.some((file) => file.startsWith(dir)),
|
||||
false,
|
||||
`${dir} 不該被發給使用者`,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user