Merge pull request 'docs/npm-deploy-alignment/main' (#39) from docs/npm-deploy-alignment/main into master

Reviewed-on: #39
Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #39.
This commit is contained in:
2026-09-17 07:14:57 +00:00
2 changed files with 75 additions and 8 deletions
+9 -6
View File
@@ -49,13 +49,16 @@ tea-sdlc/
## 前置需求
| 需求 | 用途 | 備註 |
安裝與佈署(`tea-sdlc install`)只需要 Node;其餘是**跑流程指令**時才需要的。
本工具一律不替你安裝任何東西。
| 需求 | 用途 | 缺了會怎樣 |
| --- | --- | --- |
| Node.js ≥ 20 | 執行 `scripts/` 與測試 | 不自動安裝,缺少時腳本印出指引並中止 |
| git | 分支與 commit 操作 | 同上 |
| [`tea`](https://gitea.com/gitea/tea) 並已登入 | Gitea 議題、標籤、Milestone、留言、工時 | `tea login add` |
| 目標 repo 已開啟時間追蹤 | 工時碼錶 | Settings → Advanced Settings → Enable Time Tracker |
| 帳號對目標 repo 的 issues unit 有寫入權 | 建立與更新議題 | Gitea 的 unit 權限獨立於 push 權限 |
| Node.js ≥ 20 | 執行 `tea-sdlc` 與 `scripts/` | 連 `tea-sdlc` 都跑不起來 |
| git | 分支與 commit 操作 | `install` 照樣把轉接檔裝好,只在輸出裡列出缺的東西;流程指令中止並印出安裝指引 |
| [`tea`](https://gitea.com/gitea/tea) 並已登入 | Gitea 議題、標籤、Milestone、留言、工時 | 同上;登入用 `tea login add` |
| 目標 repo 已開啟時間追蹤 | 工時碼錶 | 流程指令中止,並指出 Settings → Advanced Settings → Enable Time Tracker |
| 帳號對目標 repo 的 issues unit 有寫入權 | 建立與更新議題 | 流程指令中止;Gitea 的 unit 權限獨立於 push 權限 |
---
+66 -2
View File
@@ -6,9 +6,9 @@
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { repoRoot } from './helpers/run-script.js';
import { repoRoot, runBin, tmpRoot } from './helpers/run-script.js';
const readme = () => readFileSync(join(repoRoot, 'README.md'), 'utf8');
const agents = () => readFileSync(join(repoRoot, 'AGENTS.md'), 'utf8');
@@ -71,6 +71,57 @@ test('七套 marketplace 流程移進附錄保留,內容沒有被刪掉', () =
}
});
test('prompt 與 status 各自交代了「誰在什麼時候用它」,不是只列出指令', () => {
const text = readme();
const section = text.slice(text.indexOf('## 給 agent 的入口'), text.indexOf('## 附錄'));
// prompt 是 agent 取得流程正本的管道
assert.match(section, /tea-sdlc prompt --name/);
assert.match(section, /agent|模型/);
// status 是使用者查安裝與環境現況的入口
assert.match(section, /tea-sdlc status/);
assert.match(section, /環境/);
});
test('前置需求表分得開「安裝」與「跑流程指令」各自缺什麼會怎樣', () => {
const text = readme();
const section = text.slice(text.indexOf('## 前置需求'), text.indexOf('## 安裝'));
// install 只警告不中止,表上不能再寫成一律中止——那是文件在描述一個不存在的行為
assert.equal(
/缺少時腳本印出指引並中止/.test(section),
false,
'前置需求表還寫著一律中止,但 tea-sdlc install 只警告',
);
assert.match(section, /install/);
});
test('README 裡的 tea-sdlc 指令逐字拿去跑都認得,不會是寫給人看的假指令', async () => {
const invocations = [...readme().matchAll(/```bash\n([\s\S]*?)```/g)]
.flatMap((block) => block[1].split('\n'))
.map((line) => line.trim())
.filter((line) => line.startsWith('tea-sdlc '))
// shell 會把空白後的 # 之後當註解,貼進終端機仍然跑得動,這裡照做
.map((line) => line.replace(/\s+#.*$/, '').trim());
assert.ok(invocations.length >= 4, `README 只找到 ${invocations.length} 個 tea-sdlc 指令`);
for (const line of invocations) {
// 家目錄指到空的暫存:install/uninstall 因此偵測不到任何平台,一個檔都不會寫
const home = mkdtempSync(join(tmpRoot, 'readme-home-'));
const { code, raw } = await runBin(line.split(/\s+/).slice(1), { env: { HOME: home } });
rmSync(home, { recursive: true, force: true });
// 跑成功就沒話說(prompt 印的是原樣 markdown,本來就不是 JSON);
// 失敗的話,理由不可以是「這個指令/參數我不認得」——那代表 README 寫錯了
if (code === 0) continue;
const { error } = JSON.parse(raw.trim().split('\n').at(-1));
for (const wrong of ['UNKNOWN_SUBCOMMAND', 'UNKNOWN_FLAG', 'MISSING_FLAG', 'BAD_PROMPT_NAME']) {
assert.notEqual(error.code, wrong, `README 的「${line}」跑出 ${wrong}:${error.message}`);
}
}
});
test('模組邊界表指得到實際存在的檔案', () => {
const text = agents();
@@ -78,3 +129,16 @@ test('模組邊界表指得到實際存在的檔案', () => {
assert.match(text, /`scripts\/install\.js`/);
assert.equal(/\| `install\.js` \|/.test(text), false, '表上還留著已經不存在的根目錄 install.js');
});
test('指令入口那一列把職責與邊界都寫出來了', () => {
const row = agents().split('\n').find((line) => line.includes('`bin/tea-sdlc.js`'));
assert.ok(row, 'AGENTS.md 模組邊界表沒有指令入口那一列');
const [, , duty, boundary] = row.split('|').map((cell) => cell.trim());
// 職責是子指令解析與 dispatch
assert.match(duty, /子指令/);
assert.match(duty, /dispatch|交出去/);
// 邊界是不含任何平台目錄知識與動作實作
assert.match(boundary, /平台目錄/);
assert.match(boundary, /實作|做事/);
});