agentflowctl 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
3
+ import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { builtinAgents } from "./agents/index.js";
5
+ import { stdin, stdout } from "node:process";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { config } from "./config.js";
6
8
  import { advance, loadRepoConfig } from "./engine.js";
7
- import { API_KEY_VARS, probeAgent, resolveAgent, runCommand } from "./runner.js";
8
- import { addWorktree, git, removeWorktree } from "./git.js";
9
- import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
9
+ import { probeAgent, resolveAgent, runCommand } from "./runner.js";
10
+ import { addWorktree, git } from "./git.js";
11
+ import { cleanableRuns, cleanRun } from "./cleanup.js";
12
+ import { describeDetected, detectProjectDefaults } from "./detect.js";
13
+ import { CMD_AGENT, listLogs, localTime, logMark, nextLogFile, renderLog } from "./logs.js";
14
+ import { flowDir, logDir, projectRoot, worktreeDir } from "./paths.js";
10
15
  import { TaskList } from "./schemas.js";
11
16
  import { agentRuns, getRun, listRuns, listSubstitutions, saveRun, usageByAgent } from "./store.js";
12
17
  import { readJsonFile } from "./util.js";
18
+ import { runSetup, SETUP_ADAPTERS } from "./setup.js";
13
19
  import { addAgent, readRawConfig, removeAgent, setAgent, setCycle, writeRawConfig } from "./agentConfig.js";
14
20
  function mustGetRun(id) {
15
21
  const run = getRun(id);
@@ -30,7 +36,7 @@ function printSummary(run) {
30
36
  console.log(`run ${run.id}`);
31
37
  console.log(`階段 ${run.stage}`);
32
38
  console.log(`用量 agent 執行 ${agentRuns(run.id)} / ${run.maxAgentRuns} 次`);
33
- console.log(`agent ${run.cycle.join(" → ")}${run.lastWriter ? `(最後作者:${run.lastWriter})` : ""}`);
39
+ console.log(`agent ${run.cycle.join("、")}${run.lastWriter ? `(最後作者:${run.lastWriter})` : ""}`);
34
40
  console.log(`分支 ${run.branch}`);
35
41
  console.log(`worktree ${worktreeDir(run.id)}`);
36
42
  if (run.prUrl)
@@ -40,7 +46,14 @@ function printSummary(run) {
40
46
  if (run.stage === "failed") {
41
47
  console.log(`失敗於 ${run.failedStage ?? "?"}`);
42
48
  console.log(`原因 ${run.failureReason ?? "?"}`);
43
- console.log(`\n可檢查 ${logDir(run.id)},必要時直接在 worktree 裡修正,再執行 agentflowctl resume ${run.id}`);
49
+ const logs = listLogs(logDir(run.id));
50
+ const last = logs.at(-1);
51
+ const lastFailed = logs.filter((e) => e.footer && !e.footer.ok).at(-1);
52
+ if (last)
53
+ console.log(`最後的 log #${last.seq} ${logMark(last)}(agentflowctl logs ${run.id} ${last.seq})`);
54
+ if (lastFailed && lastFailed !== last)
55
+ console.log(`最近失敗的 log #${lastFailed.seq}(agentflowctl logs ${run.id} ${lastFailed.seq})`);
56
+ console.log(`\n必要時直接在 worktree 裡修正,再執行 agentflowctl resume ${run.id}`);
44
57
  }
45
58
  if (run.stage === "paused") {
46
59
  console.log(`暫停於 ${run.pausedStage ?? "?"}`);
@@ -50,7 +63,7 @@ function printSummary(run) {
50
63
  if (run.stage === "awaiting_approval")
51
64
  console.log(`\n確認計畫後執行 agentflowctl approve ${run.id}`);
52
65
  }
53
- /** 決定輪替順序:指令參數 > flow.config.json > 自動偵測已安裝的 CLI */
66
+ /** 決定參與的 agent:指令參數 > flow.config.json > 自動偵測已安裝的 CLI */
54
67
  async function resolveCycle(flag) {
55
68
  const cfg = loadRepoConfig();
56
69
  const wanted = flag ? flag.split(",").map((s) => s.trim()).filter(Boolean) : cfg.cycle;
@@ -61,17 +74,26 @@ async function resolveCycle(flag) {
61
74
  }
62
75
  return wanted;
63
76
  }
77
+ // 沒有內建 agent:只從 agents 裡定義的挑出已安裝的
78
+ const defined = Object.keys(cfg.agents);
79
+ if (!defined.length)
80
+ throw new Error("還沒有設定任何 agent,請先用 agentflowctl agent setup 互動設定,或用 agent add <name> --adapter <adapter> 新增");
64
81
  const found = [];
65
- for (const name of builtinAgents(cfg.removedAgents))
82
+ for (const name of defined)
66
83
  if (await probeAgent(resolveAgent(cfg, name)))
67
84
  found.push(name);
68
85
  if (!found.length)
69
- throw new Error(`沒有偵測到任何 agent CLI(${builtinAgents(cfg.removedAgents).join("、") || "內建 agent 都已移除"}),可用 agentflowctl doctor 檢查`);
86
+ throw new Error(`設定的 agent(${defined.join("、")})都沒有偵測到已安裝的 CLI,可用 agentflowctl doctor 檢查`);
70
87
  return found;
71
88
  }
72
89
  const program = new Command()
73
90
  .name("agentflowctl")
74
- .description("在專案資料夾內執行的 Agent 開發流程:規格 → 計畫 → TDD 實作 → 驗證 → 審查 → PR");
91
+ .description("在專案資料夾內執行的 Agent 開發流程:規格 → 計畫 → TDD 實作 → 驗證 → 審查 → PR")
92
+ .option("-v, --verbose", "執行時印出 agent 的文字、工具呼叫與專案指令(預設只印階段進度)")
93
+ .hook("preAction", (cmd) => {
94
+ if (cmd.opts().verbose)
95
+ config.verbose = true;
96
+ });
75
97
  program
76
98
  .command("run")
77
99
  .description("在目前的專案建立並執行新的 flow")
@@ -80,7 +102,7 @@ program
80
102
  .option("--base <branch>", "基底分支(預設為目前的分支)")
81
103
  .option("--max-agent-runs <n>", "單一 run 最多執行幾次 agent(預設取 flow.config.json 的 maxAgentRuns)")
82
104
  .option("--manual-plan", "計畫通過 AI 審查後,仍停下來等你確認", false)
83
- .option("--cycle <agents>", "agent 輪替順序,例如 claude,codex,gemini")
105
+ .option("--cycle <agents>", "參與的 agent,例如 claude,codex,gemini(順序不影響分工)")
84
106
  .action(async (opts) => {
85
107
  const requirement = opts.reqFile ? readFileSync(opts.reqFile, "utf8") : opts.req;
86
108
  if (!requirement?.trim())
@@ -94,13 +116,10 @@ program
94
116
  const branch = `flow/${id}`;
95
117
  console.log(`[${id}] 🌿 從 ${base} 建立 worktree(分支 ${branch})`);
96
118
  await addWorktree(root, worktreeDir(id), base, branch);
97
- console.log(`[${id}] 🤝 agent 輪替順序:${cycle.join(" → ")}`);
98
- // 先裝好相依套件:有些 agent 的沙箱不能連網,無法自己安裝
119
+ console.log(`[${id}] 🤝 參與的 agent:${cycle.join("、")}(角色隨機分配)`);
99
120
  const cfg = loadRepoConfig();
100
- const install = await runCommand({ runId: id, cwd: worktreeDir(id), logFile: join(logDir(id), "install.log") }, cfg.install);
101
- if (!install.ok)
102
- console.log(`[${id}] ⚠️ 安裝相依套件失敗,稍後 verify 階段會再試一次(見 ${join(logDir(id), "install.log")})`);
103
121
  const now = new Date().toISOString();
122
+ // worktree 一建好就寫入紀錄:之後在任何地方中斷,都能用 resume 接續或用 clean 清掉
104
123
  const run = saveRun({
105
124
  id,
106
125
  baseBranch: base,
@@ -116,6 +135,12 @@ program
116
135
  createdAt: now,
117
136
  updatedAt: now,
118
137
  });
138
+ for (const line of describeDetected(readRawConfig(configPath()), detectProjectDefaults(root)))
139
+ console.log(`[${id}] ${line}`);
140
+ // 先裝好相依套件:有些 agent 的沙箱不能連網,無法自己安裝
141
+ const install = await runCommand({ runId: id, cwd: worktreeDir(id), logFile: nextLogFile(logDir(id), "setup", "install", CMD_AGENT), stage: "setup", step: "install" }, cfg.install);
142
+ if (!install.ok)
143
+ console.log(`[${id}] ⚠️ 安裝相依套件失敗,稍後 verify 階段會再試一次(agentflowctl logs ${id} ${install.seq})`);
119
144
  await drive(run);
120
145
  });
121
146
  program
@@ -152,14 +177,29 @@ program
152
177
  console.log(`已取消 ${id}`);
153
178
  });
154
179
  program
155
- .command("clean <id>")
156
- .description("移除 run 的 worktree 與紀錄(分支會保留)")
157
- .action(async (id) => {
158
- mustGetRun(id);
159
- if (existsSync(worktreeDir(id)))
160
- await removeWorktree(projectRoot(), worktreeDir(id));
161
- rmSync(runDir(id), { recursive: true, force: true });
162
- console.log(`已清除 ${id},分支仍保留,不需要時可用 git branch -D 刪除`);
180
+ .command("clean [id]")
181
+ .description("移除 run 的 worktree 與紀錄(分支會保留);--all 清掉所有已結束的 run 與中斷留下的孤兒 worktree")
182
+ .option("--all", "清除所有 done、failed 的 run,以及沒有紀錄的 worktree;進行中、暫停、等待核准的不動", false)
183
+ .action(async (id, opts) => {
184
+ if (opts.all === Boolean(id))
185
+ throw new Error("請指定 run id,或使用 --all(兩者擇一)");
186
+ if (id) {
187
+ if (!(await cleanRun(id)))
188
+ throw new Error(`找不到 run:${id}`);
189
+ console.log(`已清除 ${id},分支仍保留,不需要時可用 git branch -D 刪除`);
190
+ return;
191
+ }
192
+ const targets = cleanableRuns();
193
+ if (!targets.length) {
194
+ await git(projectRoot(), "worktree", "prune");
195
+ console.log("沒有可清除的 run");
196
+ return;
197
+ }
198
+ for (const t of targets) {
199
+ await cleanRun(t.id);
200
+ console.log(`已清除 ${t.id}(${t.stage ?? "沒有紀錄,可能是建立時中斷"})`);
201
+ }
202
+ console.log(`\n共清除 ${targets.length} 個,分支仍保留,不需要時可用 git branch -D 刪除`);
163
203
  });
164
204
  program
165
205
  .command("status <id>")
@@ -201,34 +241,33 @@ function applyEdit(edit, done) {
201
241
  for (const c of changes)
202
242
  console.log(` ↳ ${c}`);
203
243
  if (JSON.stringify(before.cycle) !== JSON.stringify(cfg.cycle)) {
204
- console.log(" 已建立的 run 會沿用建立時的輪替順序,不受影響");
244
+ console.log(" 已建立的 run 會沿用建立時參與的 agent,不受影響");
205
245
  }
206
246
  }
207
247
  const agent = program.command("agent").description("管理 agent 與 adapter 設定(寫入 flow.config.json)");
208
248
  agent
209
249
  .command("list")
210
- .description("列出內建與自訂 agent、是否已安裝、在輪替中的位置")
250
+ .description("列出設定的 agent、是否已安裝、是否參與")
211
251
  .action(async () => {
212
252
  const cfg = loadRepoConfig();
213
253
  const cycle = await resolveCycle().catch(() => cfg.cycle ?? []);
214
- const names = [...new Set([...builtinAgents(cfg.removedAgents), ...Object.keys(cfg.agents)])];
254
+ const names = Object.keys(cfg.agents);
255
+ if (!names.length)
256
+ console.log("還沒有設定任何 agent,請用 agent setup 互動設定,或用 agent add <name> --adapter <adapter> 新增");
215
257
  for (const name of names) {
216
258
  const def = resolveAgent(cfg, name);
217
259
  const ok = await probeAgent(def);
218
260
  const pos = cycle.indexOf(name);
219
- const kind = builtinAgents(cfg.removedAgents).includes(name) ? (name in cfg.agents ? "內建(已覆寫)" : "內建") : "自訂";
220
261
  const detail = [
221
262
  `adapter=${def.adapter}`,
222
263
  def.model && `model=${def.model}`,
223
264
  def.extraArgs.length && `extraArgs=${def.extraArgs.join(" ")}`,
224
265
  def.command && `command=${def.command.join(" ")}`,
225
266
  ].filter(Boolean);
226
- console.log(`${ok ? "✅" : "❌"} ${name.padEnd(14)} ${kind.padEnd(8)} ${pos >= 0 ? `輪替 #${pos + 1}` : "不在輪替"} ${detail.join(" ")}`);
267
+ console.log(`${ok ? "✅" : "❌"} ${name.padEnd(14)} ${pos >= 0 ? "參與" : "不參與"} ${detail.join(" ")}`);
227
268
  }
228
- if (cfg.removedAgents.length)
229
- console.log(`\n已移除的內建 agent:${cfg.removedAgents.join("、")}(可用 agent add <name> --adapter <name> 加回)`);
230
269
  console.log(`
231
- 輪替順序:${cycle.length ? cycle.join(" → ") : "(沒有可用的 agent)"}${cfg.cycle ? "" : "(自動偵測)"}`);
270
+ 參與的 agent:${cycle.length ? cycle.join("、") : "(沒有可用的 agent)"}${cfg.cycle ? "" : "(自動偵測)"}`);
232
271
  });
233
272
  agent
234
273
  .command("add <name> [command...]")
@@ -237,7 +276,7 @@ agent
237
276
  .option("--model <model>", "模型名稱")
238
277
  .option("--extra-arg <arg>", "額外參數,可重複;以 - 開頭時寫成 --extra-arg=--sandbox", collect)
239
278
  .action((name, command, opts) => {
240
- applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name};要加進輪替請用 agent cycle`);
279
+ applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name};要讓它參與請用 agent cycle`);
241
280
  });
242
281
  agent
243
282
  .command("set <name> [command...]")
@@ -250,51 +289,71 @@ agent
250
289
  });
251
290
  agent
252
291
  .command("remove <name>")
253
- .description("刪除 agent(含內建的 claude、codex、gemini),一併從輪替移除;內建的可用 agent add 加回")
292
+ .description("刪除 agent,一併從參與的 agent 移除")
254
293
  .action((name) => applyEdit((cfg) => removeAgent(cfg, name), `已移除 ${name}`));
255
294
  agent
256
295
  .command("cycle [names]")
257
- .description("顯示輪替順序,或用逗號分隔設定新的順序")
296
+ .description("顯示參與的 agent,或用逗號分隔設定新的名單")
258
297
  .action(async (names) => {
259
298
  if (!names) {
260
299
  const cfg = loadRepoConfig();
261
- console.log(`${(await resolveCycle()).join(" → ")}${cfg.cycle ? "" : "(自動偵測)"}`);
300
+ console.log(`${(await resolveCycle()).join("、")}${cfg.cycle ? "" : "(自動偵測)"}`);
262
301
  return;
263
302
  }
264
303
  const list = names.split(",").map((s) => s.trim()).filter(Boolean);
265
- applyEdit((cfg) => setCycle(cfg, list), `輪替順序設為 ${list.join(" → ")}`);
304
+ applyEdit((cfg) => setCycle(cfg, list), `參與的 agent 設為 ${list.join("、")}`);
266
305
  const cfg = loadRepoConfig();
267
306
  for (const n of list) {
268
307
  if (!(await probeAgent(resolveAgent(cfg, n))))
269
308
  console.log(`⚠️ ${n} 目前找不到可執行的 CLI,run 會失敗,請先安裝或用 agent set 修正`);
270
309
  }
271
310
  });
272
- program
273
- .command("doctor")
274
- .description("檢查可用的 agent CLI 與目前的輪替設定")
311
+ agent
312
+ .command("setup")
313
+ .description("互動式設定:偵測已安裝的 claude、codex、gemini,逐一選擇要不要加入並設定參與的 agent")
275
314
  .action(async () => {
315
+ if (!stdin.isTTY)
316
+ throw new Error("agent setup 需要互動式終端機,請改用 agent add");
317
+ const detected = {};
318
+ for (const a of SETUP_ADAPTERS)
319
+ detected[a] = await probeAgent({ adapter: a, extraArgs: [] });
320
+ const rl = createInterface({ input: stdin, output: stdout });
321
+ let edit;
322
+ try {
323
+ edit = await runSetup(readRawConfig(configPath()), { ask: (q) => rl.question(q), detected, log: (l) => console.log(l) });
324
+ }
325
+ finally {
326
+ rl.close();
327
+ }
328
+ if (!edit)
329
+ return;
330
+ const result = edit;
331
+ applyEdit(() => result, "設定完成");
332
+ console.log("");
333
+ await doctor();
334
+ });
335
+ /** 檢查設定的 agent 是否已安裝,印出參與的 agent 與主要設定 */
336
+ async function doctor() {
276
337
  const cfg = loadRepoConfig();
277
- const names = [...new Set([...builtinAgents(cfg.removedAgents), ...Object.keys(cfg.agents)])];
338
+ const names = Object.keys(cfg.agents);
339
+ if (!names.length)
340
+ console.log("還沒有設定任何 agent,請用 agent setup 互動設定,或用 agent add <name> --adapter <adapter> 新增");
278
341
  for (const name of names) {
279
342
  const def = resolveAgent(cfg, name);
280
343
  const ok = await probeAgent(def);
281
344
  console.log(`${ok ? "✅" : "❌"} ${name.padEnd(10)} adapter=${def.adapter}${def.model ? ` model=${def.model}` : ""}`);
282
345
  }
283
346
  try {
284
- console.log(`\n輪替順序:${(await resolveCycle()).join(" → ")}`);
347
+ console.log(`\n參與的 agent:${(await resolveCycle()).join("、")}`);
285
348
  }
286
349
  catch (e) {
287
350
  console.log(`\n${e.message}`);
288
351
  }
289
- const leaked = API_KEY_VARS.filter((k) => process.env[k]);
290
- console.log("\n登入方式:訂閱登入(執行 agent 時會移除 API key)");
291
- if (leaked.length) {
292
- console.log(`⚠️ 環境中有 ${leaked.join("、")},agentflowctl 執行 agent 時會移除,但你自己直接執行 CLI 時仍可能改走 API 計費`);
293
- }
294
- console.log(`單一 run 的 agent 執行上限:${cfg.maxAgentRuns} 次`);
352
+ console.log(`\n單一 run 的 agent 執行上限:${cfg.maxAgentRuns} 次`);
295
353
  console.log(`修正策略:${cfg.fixStrategy} 測試與實作分開:${cfg.tddSplit ? "是" : "否"}`);
296
354
  console.log(`程式碼審查人數:${cfg.reviewQuorum} 計畫審查人數:${cfg.planReviewQuorum} 計畫仲裁:${cfg.planArbiter ? "開啟" : "關閉"}`);
297
- });
355
+ }
356
+ program.command("doctor").description("檢查可用的 agent CLI 與目前參與的 agent").action(doctor);
298
357
  program
299
358
  .command("list")
300
359
  .description("列出這個專案的所有 run")
@@ -305,18 +364,29 @@ program
305
364
  }
306
365
  });
307
366
  program
308
- .command("logs <id>")
309
- .description("列出 log 檔,或顯示最新一份")
310
- .option("--latest", "顯示最新一份 log 的內容", false)
311
- .action((id, opts) => {
367
+ .command("logs <id> [seq]")
368
+ .description("列出 log;指定編號(或 --latest)時顯示解析後的內容,最後附上錯誤整理")
369
+ .option("--latest", "顯示最新一份 log", false)
370
+ .option("--raw", "顯示原始內容(agent 的 JSON 行)", false)
371
+ .action((id, seq, opts) => {
312
372
  mustGetRun(id);
313
- const dir = logDir(id);
314
- const files = existsSync(dir) ? readdirSync(dir).sort() : [];
315
- if (!files.length)
373
+ const logs = listLogs(logDir(id));
374
+ if (!logs.length)
316
375
  return console.log("還沒有 log");
317
- if (!opts.latest)
318
- return files.forEach((f) => console.log(join(dir, f)));
319
- console.log(readFileSync(join(dir, files.at(-1)), "utf8"));
376
+ if (!seq && !opts.latest) {
377
+ console.log(" # 結果 階段 步驟 agent 開始時間");
378
+ for (const e of logs) {
379
+ const h = e.header;
380
+ console.log(`${String(e.seq).padStart(3)} ${logMark(e).padEnd(4)} ${(h?.stage ?? "?").padEnd(12)} ${(h?.step ?? "?").padEnd(19)} ${(h?.agent ?? "?").padEnd(10)} ${localTime(h?.startedAt)}`);
381
+ }
382
+ console.log(`\n查看內容:agentflowctl logs ${id} <編號>(加 --raw 看原始 JSON)`);
383
+ return;
384
+ }
385
+ const entry = seq ? logs.find((e) => e.seq === Number(seq)) : logs.at(-1);
386
+ if (!entry)
387
+ throw new Error(`找不到 log #${seq}(共 ${logs.length} 份,可用 agentflowctl logs ${id} 列出)`);
388
+ const text = readFileSync(entry.file, "utf8");
389
+ console.log(opts.raw ? text : renderLog(text, entry.file));
320
390
  });
321
391
  program.parseAsync().catch((err) => {
322
392
  console.error(`錯誤:${err.message}`);
package/dist/config.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export const config = {
2
- /** 指定 Claude Code 使用的模型;未設定則用 Claude Code 預設值 */
3
- model: process.env.AGENTFLOWCTL_MODEL,
4
2
  /** 單一 Agent 執行最多幾輪工具迴圈,避免卡在迴圈裡 */
5
3
  maxTurns: Number(process.env.AGENTFLOWCTL_MAX_TURNS ?? 80),
6
4
  /** 同一個關卡連續失敗幾次後停止 */
7
5
  maxAttempts: Number(process.env.AGENTFLOWCTL_MAX_ATTEMPTS ?? 3),
6
+ /** 終端機是否印出 agent 的文字、工具呼叫與專案指令;預設安靜,-v 或 AGENTFLOWCTL_VERBOSE=1 開啟 */
7
+ verbose: process.env.AGENTFLOWCTL_VERBOSE === "1",
8
8
  };
9
9
  //# sourceMappingURL=config.js.map
package/dist/detect.js ADDED
@@ -0,0 +1,82 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { RepoConfig } from "./schemas.js";
4
+ const LOCKFILES = [
5
+ ["pnpm-lock.yaml", "pnpm"],
6
+ ["yarn.lock", "yarn"],
7
+ ["bun.lock", "bun"],
8
+ ["bun.lockb", "bun"],
9
+ ["package-lock.json", "npm"],
10
+ ];
11
+ /** 不鎖 lockfile:implement 階段 agent 可能新增依賴 */
12
+ const INSTALL = {
13
+ npm: RepoConfig.parse({}).install,
14
+ pnpm: "pnpm install",
15
+ yarn: "yarn install",
16
+ bun: "bun install",
17
+ };
18
+ /** 執行專案內 bin 的前綴,取代預設指令裡的 npx */
19
+ const EXEC = { npm: "npx", pnpm: "pnpm exec", yarn: "yarn", bun: "bunx" };
20
+ /** 每項檢查會找的 script 名稱,依序取第一個存在的 */
21
+ const CHECK_SCRIPTS = {
22
+ typecheck: ["typecheck", "type-check"],
23
+ lint: ["lint"],
24
+ test: ["test"],
25
+ build: ["build"],
26
+ };
27
+ function readPackageJson(root) {
28
+ try {
29
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
30
+ return pkg && typeof pkg === "object" ? pkg : {};
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ }
36
+ function detectManager(root, pkg) {
37
+ if (typeof pkg.packageManager === "string") {
38
+ const name = pkg.packageManager.split("@")[0];
39
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") {
40
+ return { manager: name, source: "package.json 的 packageManager" };
41
+ }
42
+ }
43
+ for (const [file, manager] of LOCKFILES) {
44
+ if (existsSync(join(root, file)))
45
+ return { manager, source: file };
46
+ }
47
+ return { manager: "npm", source: "預設" };
48
+ }
49
+ const withExec = (cmd, manager) => cmd.replace(/^npx /, `${EXEC[manager]} `);
50
+ export function detectProjectDefaults(root) {
51
+ const pkg = readPackageJson(root);
52
+ const { manager, source } = detectManager(root, pkg);
53
+ const defaults = RepoConfig.parse({});
54
+ const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
55
+ const checks = defaults.checks.map(({ name, cmd }) => {
56
+ const script = CHECK_SCRIPTS[name]?.find((s) => typeof scripts[s] === "string");
57
+ return { name, cmd: script ? `${manager} run ${script}` : withExec(cmd, manager) };
58
+ });
59
+ return { manager, source, install: INSTALL[manager], test: withExec(defaults.test, manager), checks };
60
+ }
61
+ /** 補上原始設定裡沒寫的 install、test、checks;不是物件就原樣回傳,交給 schema 報錯 */
62
+ export function withProjectDefaults(raw, detected) {
63
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
64
+ return raw;
65
+ const { install, test, checks } = detected;
66
+ return { install, test, checks, ...raw };
67
+ }
68
+ /** run 開始時印出的說明:只列出這次用了偵測結果的欄位 */
69
+ export function describeDetected(raw, detected) {
70
+ const lines = [];
71
+ if (!("install" in raw))
72
+ lines.push(` install:${detected.install}`);
73
+ if (!("test" in raw))
74
+ lines.push(` test:${detected.test}`);
75
+ if (!("checks" in raw))
76
+ lines.push(...detected.checks.map((c) => ` checks.${c.name}:${c.cmd}`));
77
+ if (!lines.length)
78
+ return [];
79
+ const why = detected.source === "預設" ? "預設" : `依 ${detected.source}`;
80
+ return [`🔧 依專案偵測指令:${detected.manager}(${why})`, ...lines];
81
+ }
82
+ //# sourceMappingURL=detect.js.map