@skrupellose/code-helper 0.1.0 → 0.1.2

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
@@ -2,11 +2,13 @@ import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import { FEATURE_KEYS, FEATURE_LABELS } from "./constants.js";
4
4
  import { archiveFeature, listTasks } from "./archive.js";
5
+ import { createCompletionReview } from "./completion.js";
5
6
  import { loadConfig, setFeatureEnabled } from "./config.js";
6
7
  import { runChecks } from "./checks.js";
8
+ import { installHook, listHookInstallations, parseHookTargets, uninstallHook } from "./hooks.js";
7
9
  import { initializeProject } from "./init.js";
8
10
  import { normalizeDroppedPath } from "./input-utils.js";
9
- import { listProjectSkillRegistrations, parseSkillRegistrationTargets, registerProjectSkills, resolveSkillRegistrationTargets, unregisterProjectSkills } from "./skills.js";
11
+ import { formatSkillRegistrationTargetName, listProjectSkillRegistrations, listSupportedSkillRegistrationTargets, parseSkillRegistrationTargets, registerProjectSkills, resolveSkillRegistrationTargets, runSkillsAudit, runSkillsDoctor, unregisterProjectSkills } from "./skills.js";
10
12
  import { canUseInteractiveKeys, promptContinue, promptMultiSelect, promptSelect } from "./terminal-ui.js";
11
13
  import { createManualTestDocument, createPlanWorkbench } from "./workflows.js";
12
14
  /**
@@ -21,9 +23,11 @@ export async function runCli(argv, projectRoot = process.cwd()) {
21
23
  case "menu":
22
24
  return runInteractiveMenu(projectRoot);
23
25
  case "init":
24
- return runInit(projectRoot);
26
+ return runInit(projectRoot, args);
27
+ case "sync-local":
28
+ return runSyncLocal(projectRoot, args);
25
29
  case "check":
26
- return runCheck(projectRoot);
30
+ return runCheck(projectRoot, args);
27
31
  case "features":
28
32
  return runFeatures(projectRoot, args);
29
33
  case "plan":
@@ -32,10 +36,14 @@ export async function runCli(argv, projectRoot = process.cwd()) {
32
36
  return runManualTest(projectRoot, args);
33
37
  case "archive":
34
38
  return runArchive(projectRoot, args);
39
+ case "finish":
40
+ return runFinish(projectRoot, args);
35
41
  case "tasks":
36
42
  return runTasks(projectRoot, args);
37
43
  case "skills":
38
44
  return runSkills(projectRoot, args);
45
+ case "hooks":
46
+ return runHooks(projectRoot, args);
39
47
  case "help":
40
48
  case "--help":
41
49
  case "-h":
@@ -52,24 +60,189 @@ export async function runCli(argv, projectRoot = process.cwd()) {
52
60
  return 1;
53
61
  }
54
62
  }
63
+ /**
64
+ * 主菜单信息架构。
65
+ * 分组按用户完成一次协作任务的常见顺序排列:准备项目、推进任务、维护文档,再管理工具能力。
66
+ */
67
+ const MAIN_MENU_GROUPS = [
68
+ {
69
+ title: "项目准备",
70
+ items: [
71
+ {
72
+ value: "1",
73
+ name: "初始化/刷新项目配置",
74
+ description: "创建或更新工作区、入口索引、规则模板、Skills 和可用 hooks"
75
+ }
76
+ ]
77
+ },
78
+ {
79
+ title: "任务推进",
80
+ items: [
81
+ {
82
+ value: "2",
83
+ name: "生成任务计划",
84
+ description: "根据需求文档生成计划、状态记录和执行记录入口"
85
+ },
86
+ {
87
+ value: "3",
88
+ name: "生成手工测试文档",
89
+ description: "为页面或交互验收生成需要人工执行的测试文档"
90
+ },
91
+ {
92
+ value: "4",
93
+ name: "检查功能完成情况",
94
+ description: "检查当前任务是否满足完成条件,并提示后续动作"
95
+ }
96
+ ]
97
+ },
98
+ {
99
+ title: "项目维护",
100
+ items: [
101
+ {
102
+ value: "5",
103
+ name: "查看任务列表",
104
+ description: "查看 active、archived 和 mixed 状态的任务文档"
105
+ },
106
+ {
107
+ value: "6",
108
+ name: "归档已完成任务",
109
+ description: "将已结束任务的计划、结果和状态文档移动到 archive"
110
+ },
111
+ {
112
+ value: "7",
113
+ name: "检查协作规范",
114
+ description: "检查入口文档、规则目录、计划和归档结构是否完整"
115
+ }
116
+ ]
117
+ },
118
+ {
119
+ title: "工具设置",
120
+ items: [
121
+ {
122
+ value: "8",
123
+ name: "功能管理",
124
+ description: "应用或取消项目级 Skills、Agent hooks 和 Git hook"
125
+ },
126
+ {
127
+ value: "9",
128
+ name: "管理项目 Skills",
129
+ description: "查看、注册、取消注册、检查或分析项目级 Skills"
130
+ },
131
+ {
132
+ value: "10",
133
+ name: "管理 Hooks",
134
+ description: "查看、安装或卸载 code-helper 管理的 Git / Agent hooks"
135
+ }
136
+ ]
137
+ }
138
+ ];
139
+ const MAIN_MENU_NAME_COLUMN_WIDTH = 24;
140
+ /**
141
+ * 导出主菜单分组,供测试锁定菜单分组、命名和说明。
142
+ */
143
+ export function getMainMenuGroups() {
144
+ return MAIN_MENU_GROUPS.map((group) => ({
145
+ title: group.title,
146
+ items: group.items.map((item) => ({ ...item }))
147
+ }));
148
+ }
149
+ /**
150
+ * 渲染主菜单分组标题。
151
+ * 标题使用中文常见的书名号式括号,和功能项形成明确视觉区分,且不依赖 ANSI 样式。
152
+ */
153
+ export function formatMainMenuGroupTitle(title) {
154
+ return `【${title}】`;
155
+ }
156
+ /**
157
+ * 渲染 raw mode 菜单中的单行功能项。
158
+ * 功能名按终端显示宽度补齐,保证说明从稳定列开始,便于快速扫描。
159
+ */
160
+ export function formatMainMenuSelectItemLabel(item) {
161
+ return ` ${item.value.padStart(2, " ")}. ${padMenuText(item.name, MAIN_MENU_NAME_COLUMN_WIDTH)} ${item.description}`;
162
+ }
163
+ /**
164
+ * 渲染数字兜底菜单中的功能项。
165
+ * 数字兜底没有高亮能力,因此把功能名和说明拆成两行,避免长说明挤在同一行。
166
+ */
167
+ export function formatMainMenuTextItemLines(item) {
168
+ return [` ${item.value.padStart(2, " ")}. ${item.name}`, ` ${item.description}`];
169
+ }
170
+ /**
171
+ * 按终端显示宽度补齐文本。
172
+ * 中文字符通常占两个终端列,这里做轻量宽字符判断,避免主菜单说明列明显错位。
173
+ */
174
+ function padMenuText(text, width) {
175
+ const paddingLength = Math.max(width - getMenuTextWidth(text), 0);
176
+ return `${text}${" ".repeat(paddingLength)}`;
177
+ }
178
+ /**
179
+ * 计算菜单文本在常见等宽终端中的显示宽度。
180
+ * 该函数只用于菜单排版,不参与业务逻辑;宽字符范围覆盖中文、日文、韩文和全角符号。
181
+ */
182
+ function getMenuTextWidth(text) {
183
+ return Array.from(text).reduce((width, character) => {
184
+ const codePoint = character.codePointAt(0) ?? 0;
185
+ return width + (isWideMenuCharacter(codePoint) ? 2 : 1);
186
+ }, 0);
187
+ }
188
+ /**
189
+ * 判断字符是否通常按双列宽度显示。
190
+ * 范围参考 Unicode 中常见 CJK 和全角字符区间,避免引入额外依赖。
191
+ */
192
+ function isWideMenuCharacter(codePoint) {
193
+ return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
194
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
195
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
196
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
197
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
198
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
199
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
200
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6));
201
+ }
202
+ /**
203
+ * 构造 raw mode 单选菜单。
204
+ * 分组标题和分组间空行作为 disabled 选项展示,方向键会自动跳过。
205
+ */
206
+ export function buildMainMenuSelectOptions() {
207
+ const options = [];
208
+ for (const [groupIndex, group] of MAIN_MENU_GROUPS.entries()) {
209
+ if (groupIndex > 0) {
210
+ options.push({
211
+ value: `__spacer_${group.title}`,
212
+ label: "",
213
+ disabled: true
214
+ });
215
+ }
216
+ options.push({
217
+ value: `__group_${group.title}`,
218
+ label: formatMainMenuGroupTitle(group.title),
219
+ disabled: true
220
+ });
221
+ for (const item of group.items) {
222
+ options.push({
223
+ value: item.value,
224
+ label: formatMainMenuSelectItemLabel(item)
225
+ });
226
+ }
227
+ }
228
+ options.push({ value: "__spacer_exit", label: "", disabled: true });
229
+ options.push({ value: "0", label: " 0. 退出 关闭 code-helper 菜单" });
230
+ return options;
231
+ }
232
+ /**
233
+ * 根据主菜单数字取回用户可见功能名。
234
+ * 菜单动作回显复用这里的名称,避免旧文案散落在 switch 分支里。
235
+ */
236
+ function getMainMenuItemName(value) {
237
+ return MAIN_MENU_GROUPS.flatMap((group) => group.items).find((item) => item.value === value)?.name ?? value;
238
+ }
55
239
  /**
56
240
  * 无参数时展示交互菜单。
57
241
  * 使用 Node 内置 readline,减少首版运行依赖和安装体积。
58
242
  */
59
243
  async function runInteractiveMenu(projectRoot) {
60
244
  const rl = createInterface({ input, output });
61
- const menuOptions = [
62
- { value: "1", label: "初始化项目" },
63
- { value: "2", label: "项目记忆规则优化" },
64
- { value: "3", label: "项目计划优化" },
65
- { value: "4", label: "生成人工页面测试文档" },
66
- { value: "5", label: "功能开关管理" },
67
- { value: "6", label: "项目规则检查" },
68
- { value: "7", label: "文档归档" },
69
- { value: "8", label: "查看任务状态" },
70
- { value: "9", label: "Skills 管理" },
71
- { value: "0", label: "退出" }
72
- ];
245
+ const menuOptions = buildMainMenuSelectOptions();
73
246
  try {
74
247
  let shouldExit = false;
75
248
  while (!shouldExit) {
@@ -79,78 +252,99 @@ async function runInteractiveMenu(projectRoot) {
79
252
  : await askTextMenu(rl);
80
253
  switch (answer.trim()) {
81
254
  case "1":
82
- await runMenuAction("初始化项目", () => runInit(projectRoot));
83
- await pauseAfterMenuAction(useKeyMenu);
84
- break;
85
- case "2":
86
- await runMenuAction("项目记忆规则优化", async () => {
87
- await runInit(projectRoot);
88
- console.log("已刷新项目记忆规则模板。请根据当前变更定向修改 code-helper-docs/user-rules/ 中的专题规则。");
89
- return 0;
90
- });
255
+ await runMenuAction(getMainMenuItemName(answer), () => runInit(projectRoot));
91
256
  await pauseAfterMenuAction(useKeyMenu);
92
257
  break;
93
- case "3": {
94
- printInputHint("项目计划优化需要需求文档路径,支持直接把文件拖到终端。输入 0 或直接回车返回。");
258
+ case "2": {
259
+ printInputHint("生成任务计划需要需求文档路径,支持直接把文件拖到终端。输入 0 或直接回车返回。");
95
260
  const requirementPath = await askRequiredMenuInput(rl, "请输入或拖拽需求文档路径:");
96
261
  if (requirementPath === undefined) {
97
- console.log("已取消项目计划优化,返回主菜单。");
262
+ console.log("已取消生成任务计划,返回主菜单。");
98
263
  break;
99
264
  }
100
265
  const featureName = await askOptionalMenuInput(rl, "请输入中文功能名称(可留空,默认取需求标题或中文文件名;输入 0 返回):");
101
266
  if (featureName === undefined) {
102
- console.log("已取消项目计划优化,返回主菜单。");
267
+ console.log("已取消生成任务计划,返回主菜单。");
103
268
  break;
104
269
  }
105
- await runMenuAction("项目计划优化", () => runPlan(projectRoot, [normalizeDroppedPath(requirementPath, projectRoot), featureName].filter(Boolean)));
270
+ await runMenuAction(getMainMenuItemName(answer), () => runPlan(projectRoot, [normalizeDroppedPath(requirementPath, projectRoot), featureName].filter(Boolean)));
106
271
  await pauseAfterMenuAction(useKeyMenu);
107
272
  break;
108
273
  }
109
- case "4": {
110
- printInputHint("生成人工页面测试文档需要功能名称。输入 0 或直接回车返回。");
111
- const featureName = await askRequiredMenuInput(rl, "请输入功能名称:");
274
+ case "3": {
275
+ const featureName = await selectTaskFeatureNameForMenu(projectRoot, rl, {
276
+ title: "选择要生成手工测试文档的任务",
277
+ statuses: ["active", "mixed"],
278
+ manualHint: "未找到合适任务或需要新建文档时,可手动输入功能名称。输入 0 或直接回车返回。",
279
+ manualQuestion: "请输入功能名称:"
280
+ });
112
281
  if (featureName === undefined) {
113
- console.log("已取消生成人工页面测试文档,返回主菜单。");
282
+ console.log("已取消生成手工测试文档,返回主菜单。");
114
283
  break;
115
284
  }
116
285
  const title = await askOptionalMenuInput(rl, "请输入测试文档标题(可留空;输入 0 返回):");
117
286
  if (title === undefined) {
118
- console.log("已取消生成人工页面测试文档,返回主菜单。");
287
+ console.log("已取消生成手工测试文档,返回主菜单。");
119
288
  break;
120
289
  }
121
- await runMenuAction("生成人工页面测试文档", () => runManualTest(projectRoot, [featureName, title].filter(Boolean)));
290
+ await runMenuAction(getMainMenuItemName(answer), () => runManualTest(projectRoot, [featureName, title].filter(Boolean)));
122
291
  await pauseAfterMenuAction(useKeyMenu);
123
292
  break;
124
293
  }
125
- case "5":
126
- if (await runFeatureMenu(projectRoot, rl)) {
294
+ case "4":
295
+ {
296
+ const featureName = await selectTaskFeatureNameForMenu(projectRoot, rl, {
297
+ title: "选择要检查完成情况的任务",
298
+ statuses: ["active", "mixed"],
299
+ manualHint: "未找到合适任务或需要兼容旧文档时,可手动输入功能名称。输入 0 或直接回车返回。",
300
+ manualQuestion: "请输入要检查的功能名称:"
301
+ });
302
+ if (featureName === undefined) {
303
+ console.log("已取消检查功能完成情况,返回主菜单。");
304
+ break;
305
+ }
306
+ await runMenuAction(getMainMenuItemName(answer), () => runFinish(projectRoot, [featureName]));
127
307
  await pauseAfterMenuAction(useKeyMenu);
308
+ break;
128
309
  }
129
- break;
130
- case "6":
131
- await runMenuAction("项目规则检查", () => runCheck(projectRoot));
310
+ case "5":
311
+ await runMenuAction(getMainMenuItemName(answer), () => runTasks(projectRoot, []));
132
312
  await pauseAfterMenuAction(useKeyMenu);
133
313
  break;
134
- case "7": {
135
- printInputHint("文档归档需要中文功能名称,例如 订单管理升级。输入 0 或直接回车返回。");
136
- const featureName = await askRequiredMenuInput(rl, "请输入要归档的功能名称:");
314
+ case "6": {
315
+ const featureName = await selectTaskFeatureNameForMenu(projectRoot, rl, {
316
+ title: "选择要归档的任务",
317
+ statuses: ["active", "mixed"],
318
+ manualHint: "未找到合适任务或需要兼容旧文档时,可手动输入功能名称。输入 0 或直接回车返回。",
319
+ manualQuestion: "请输入要归档的功能名称:"
320
+ });
137
321
  if (featureName === undefined) {
138
- console.log("已取消文档归档,返回主菜单。");
322
+ console.log("已取消归档已完成任务,返回主菜单。");
139
323
  break;
140
324
  }
141
- await runMenuAction("文档归档", () => runArchive(projectRoot, [featureName]));
325
+ await runMenuAction(getMainMenuItemName(answer), () => runArchive(projectRoot, [featureName]));
142
326
  await pauseAfterMenuAction(useKeyMenu);
143
327
  break;
144
328
  }
145
- case "8":
146
- await runMenuAction("查看任务状态", () => runTasks(projectRoot, []));
329
+ case "7":
330
+ await runMenuAction(getMainMenuItemName(answer), () => runCheck(projectRoot));
147
331
  await pauseAfterMenuAction(useKeyMenu);
148
332
  break;
333
+ case "8":
334
+ if (await runApplyMenu(projectRoot, rl)) {
335
+ await pauseAfterMenuAction(useKeyMenu);
336
+ }
337
+ break;
149
338
  case "9":
150
339
  if (await runSkillMenu(projectRoot, rl)) {
151
340
  await pauseAfterMenuAction(useKeyMenu);
152
341
  }
153
342
  break;
343
+ case "10":
344
+ if (await runHooksMenu(projectRoot, rl)) {
345
+ await pauseAfterMenuAction(useKeyMenu);
346
+ }
347
+ break;
154
348
  case "0":
155
349
  console.log("已退出 code-helper。");
156
350
  shouldExit = true;
@@ -166,7 +360,127 @@ async function runInteractiveMenu(projectRoot) {
166
360
  }
167
361
  }
168
362
  /**
169
- * 交互式 Skills 管理菜单。
363
+ * 在菜单中选择一个任务功能名。
364
+ * 优先从已有任务文档选择;没有合适任务或用户选择手动输入时,再回退到文本输入。
365
+ */
366
+ async function selectTaskFeatureNameForMenu(projectRoot, rl, options) {
367
+ const tasks = await getSelectableTasks(projectRoot, options.statuses);
368
+ if (tasks.length > 0) {
369
+ const answer = canUseInteractiveKeys(input, output)
370
+ ? await promptSelect(input, output, options.title, buildTaskSelectOptions(tasks, true))
371
+ : await askTextTaskMenu(rl, options.title, tasks);
372
+ if (answer === "__return__") {
373
+ return undefined;
374
+ }
375
+ if (answer !== "__manual__") {
376
+ return tasks[Number.parseInt(answer, 10)]?.featureName;
377
+ }
378
+ }
379
+ else {
380
+ console.log("当前没有发现可选择的活动任务。");
381
+ }
382
+ printInputHint(options.manualHint);
383
+ return askRequiredMenuInput(rl, options.manualQuestion);
384
+ }
385
+ /**
386
+ * 直接命令缺少功能名时,从已有任务中选择。
387
+ * 非 TTY 场景不进入交互,只打印可用任务和正确用法。
388
+ */
389
+ async function selectTaskFeatureNameForCommand(projectRoot, title, statuses) {
390
+ const tasks = await getSelectableTasks(projectRoot, statuses);
391
+ if (tasks.length === 0) {
392
+ console.error("缺少功能名称,且当前没有发现可选择的活动任务。");
393
+ return undefined;
394
+ }
395
+ if (!input.isTTY || !output.isTTY) {
396
+ console.error("缺少功能名称。可用任务:");
397
+ for (const task of tasks) {
398
+ console.error(`- ${task.featureName}(${task.status})`);
399
+ }
400
+ return undefined;
401
+ }
402
+ if (!canUseInteractiveKeys(input, output)) {
403
+ const rl = createInterface({ input, output });
404
+ try {
405
+ const answer = await askTextTaskMenu(rl, title, tasks);
406
+ return resolveTaskSelectionForCommand(answer, tasks, rl);
407
+ }
408
+ finally {
409
+ rl.close();
410
+ }
411
+ }
412
+ const answer = await promptSelect(input, output, title, buildTaskSelectOptions(tasks, true));
413
+ const rl = createInterface({ input, output });
414
+ try {
415
+ return await resolveTaskSelectionForCommand(answer, tasks, rl);
416
+ }
417
+ finally {
418
+ rl.close();
419
+ }
420
+ }
421
+ /**
422
+ * 将任务选择菜单结果解析为功能名。
423
+ * 直接命令也保留手动输入入口,兼容旧文档或尚未生成完整任务记录的场景。
424
+ */
425
+ async function resolveTaskSelectionForCommand(answer, tasks, rl) {
426
+ if (answer === "__return__" || answer === "__manual__") {
427
+ if (answer === "__manual__") {
428
+ return askRequiredMenuInput(rl, "请输入功能名称:");
429
+ }
430
+ return undefined;
431
+ }
432
+ return tasks[Number.parseInt(answer, 10)]?.featureName;
433
+ }
434
+ /**
435
+ * 读取可供动作选择的任务。
436
+ * archived 任务已经结束,不会默认出现在生成手工测试和归档动作中。
437
+ */
438
+ async function getSelectableTasks(projectRoot, statuses) {
439
+ const allowedStatuses = new Set(statuses);
440
+ return (await listTasks(projectRoot)).filter((task) => allowedStatuses.has(task.status));
441
+ }
442
+ /**
443
+ * 为任务选择菜单生成稳定 value。
444
+ * value 使用数组下标,避免功能名中包含特殊字符时影响菜单控制项。
445
+ */
446
+ function buildTaskSelectOptions(tasks, includeManualInput) {
447
+ const options = tasks.map((task, index) => ({
448
+ value: String(index),
449
+ label: `${task.featureName}(${task.status})`
450
+ }));
451
+ if (includeManualInput) {
452
+ options.push({ value: "__manual__", label: "手动输入功能名称" });
453
+ }
454
+ options.push({ value: "__return__", label: "返回" });
455
+ return options;
456
+ }
457
+ /**
458
+ * 非 raw mode 终端下的任务选择菜单。
459
+ * 数字选择任务,M 表示手动输入,0 表示返回。
460
+ */
461
+ async function askTextTaskMenu(rl, title, tasks) {
462
+ console.log(`\n${title}`);
463
+ tasks.forEach((task, index) => {
464
+ console.log(`${index + 1}. ${task.featureName}(${task.status})`);
465
+ });
466
+ console.log("M. 手动输入功能名称");
467
+ console.log("0. 返回");
468
+ const answer = (await askQuestionOrDefault(rl, "请选择任务:", "0")).trim();
469
+ if (answer === "0" || answer === "") {
470
+ return "__return__";
471
+ }
472
+ if (answer.toLowerCase() === "m") {
473
+ return "__manual__";
474
+ }
475
+ const selectedIndex = Number.parseInt(answer, 10);
476
+ if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= tasks.length) {
477
+ return String(selectedIndex - 1);
478
+ }
479
+ console.log("无效选择,返回上一级。");
480
+ return "__return__";
481
+ }
482
+ /**
483
+ * 交互式项目 Skills 管理菜单。
170
484
  * 这里只管理 code-helper 自己的项目级 skill,不触碰用户自定义 skills。
171
485
  */
172
486
  async function runSkillMenu(projectRoot, rl) {
@@ -177,12 +491,15 @@ async function runSkillMenu(projectRoot, rl) {
177
491
  { value: "3", label: "按当前项目取消注册 Skills" },
178
492
  { value: "4", label: "仅注册 Codex" },
179
493
  { value: "5", label: "仅注册 Claude Code" },
180
- { value: "6", label: "注册全部" },
181
- { value: "7", label: "取消注册全部" },
494
+ { value: "6", label: "仅注册 GitHub Copilot" },
495
+ { value: "7", label: "注册全部" },
496
+ { value: "8", label: "取消注册全部" },
497
+ { value: "9", label: "Skills 质量检查" },
498
+ { value: "10", label: "Skills 建议分析" },
182
499
  { value: "0", label: "返回" }
183
500
  ];
184
501
  const answer = useKeyMenu
185
- ? await promptSelect(input, output, "Skills 管理", options)
502
+ ? await promptSelect(input, output, "管理项目 Skills", options)
186
503
  : await askTextSkillMenu(rl);
187
504
  switch (answer.trim()) {
188
505
  case "1":
@@ -201,11 +518,77 @@ async function runSkillMenu(projectRoot, rl) {
201
518
  await runMenuAction("注册 Claude Code 项目级 skills", () => runSkills(projectRoot, ["register", "claudecode"]));
202
519
  return true;
203
520
  case "6":
204
- await runMenuAction("注册全部项目级 skills", () => runSkills(projectRoot, ["register", "all"]));
521
+ await runMenuAction("注册 GitHub Copilot 项目级 skills", () => runSkills(projectRoot, ["register", "githubcopilot"]));
205
522
  return true;
206
523
  case "7":
524
+ await runMenuAction("注册全部项目级 skills", () => runSkills(projectRoot, ["register", "all"]));
525
+ return true;
526
+ case "8":
207
527
  await runMenuAction("取消注册全部项目级 skills", () => runSkills(projectRoot, ["unregister", "all"]));
208
528
  return true;
529
+ case "9":
530
+ await runMenuAction("Skills 质量检查", () => runSkills(projectRoot, ["doctor"]));
531
+ return true;
532
+ case "10":
533
+ await runMenuAction("Skills 建议分析", () => runSkills(projectRoot, ["audit"]));
534
+ return true;
535
+ case "0":
536
+ console.log("已返回主菜单。");
537
+ return false;
538
+ default:
539
+ console.log("无效选择,返回主菜单。");
540
+ return false;
541
+ }
542
+ }
543
+ /**
544
+ * 交互式 Hooks 管理菜单。
545
+ * hooks 安装动作受 gitHooks / agentHooks 开关控制,卸载动作不受开关限制。
546
+ */
547
+ async function runHooksMenu(projectRoot, rl) {
548
+ const useKeyMenu = canUseInteractiveKeys(input, output);
549
+ const options = [
550
+ { value: "1", label: "查看 Hooks 状态" },
551
+ { value: "2", label: "安装 Git pre-commit hook" },
552
+ { value: "3", label: "卸载 Git pre-commit hook" },
553
+ { value: "4", label: "安装 Codex Agent hook" },
554
+ { value: "5", label: "卸载 Codex Agent hook" },
555
+ { value: "6", label: "安装 Claude Code Agent hook" },
556
+ { value: "7", label: "卸载 Claude Code Agent hook" },
557
+ { value: "8", label: "安装全部 Hooks" },
558
+ { value: "9", label: "卸载全部 Hooks" },
559
+ { value: "0", label: "返回" }
560
+ ];
561
+ const answer = useKeyMenu
562
+ ? await promptSelect(input, output, "管理 Hooks", options)
563
+ : await askTextHooksMenu(rl);
564
+ switch (answer.trim()) {
565
+ case "1":
566
+ await runMenuAction("查看 Hooks 状态", () => runHooks(projectRoot, ["list"]));
567
+ return true;
568
+ case "2":
569
+ await runMenuAction("安装 Git pre-commit hook", () => runHooks(projectRoot, ["install", "git"]));
570
+ return true;
571
+ case "3":
572
+ await runMenuAction("卸载 Git pre-commit hook", () => runHooks(projectRoot, ["uninstall", "git"]));
573
+ return true;
574
+ case "4":
575
+ await runMenuAction("安装 Codex Agent hook", () => runHooks(projectRoot, ["install", "codex"]));
576
+ return true;
577
+ case "5":
578
+ await runMenuAction("卸载 Codex Agent hook", () => runHooks(projectRoot, ["uninstall", "codex"]));
579
+ return true;
580
+ case "6":
581
+ await runMenuAction("安装 Claude Code Agent hook", () => runHooks(projectRoot, ["install", "claudecode"]));
582
+ return true;
583
+ case "7":
584
+ await runMenuAction("卸载 Claude Code Agent hook", () => runHooks(projectRoot, ["uninstall", "claudecode"]));
585
+ return true;
586
+ case "8":
587
+ await runMenuAction("安装全部 Hooks", () => runHooks(projectRoot, ["install", "all"]));
588
+ return true;
589
+ case "9":
590
+ await runMenuAction("卸载全部 Hooks", () => runHooks(projectRoot, ["uninstall", "all"]));
591
+ return true;
209
592
  case "0":
210
593
  console.log("已返回主菜单。");
211
594
  return false;
@@ -214,6 +597,308 @@ async function runSkillMenu(projectRoot, rl) {
214
597
  return false;
215
598
  }
216
599
  }
600
+ /**
601
+ * 功能管理菜单。
602
+ * 面向用户的一级入口应直接应用或取消能力,不要求用户理解内部 feature key。
603
+ */
604
+ async function runApplyMenu(projectRoot, rl) {
605
+ const useKeyMenu = canUseInteractiveKeys(input, output);
606
+ const options = [
607
+ { value: "1", label: "应用项目级 Skills" },
608
+ { value: "2", label: "取消项目级 Skills" },
609
+ { value: "3", label: "应用 Agent hooks" },
610
+ { value: "4", label: "取消 Agent hooks" },
611
+ { value: "5", label: "应用 Git hook" },
612
+ { value: "6", label: "取消 Git hook" },
613
+ { value: "7", label: "刷新规则和模板" },
614
+ { value: "8", label: "查看应用状态" },
615
+ { value: "0", label: "返回" }
616
+ ];
617
+ const answer = useKeyMenu
618
+ ? await promptSelect(input, output, "功能管理", options)
619
+ : await askTextApplyMenu(rl);
620
+ switch (answer.trim()) {
621
+ case "1": {
622
+ const selection = await selectSkillTargetsForMenu(projectRoot, rl, "选择要应用 Skills 的 agent 工具");
623
+ if (selection === undefined) {
624
+ console.log("已取消应用项目级 Skills,返回功能管理。");
625
+ return false;
626
+ }
627
+ await runMenuAction(`应用项目级 Skills(${formatTargetList(selection.targets)})`, () => applyProjectSkills(projectRoot, selection.targets));
628
+ return true;
629
+ }
630
+ case "2": {
631
+ const selection = await selectSkillTargetsForMenu(projectRoot, rl, "选择要取消 Skills 的 agent 工具");
632
+ if (selection === undefined) {
633
+ console.log("已取消项目级 Skills 取消操作,返回功能管理。");
634
+ return false;
635
+ }
636
+ await runMenuAction(`取消项目级 Skills(${formatTargetList(selection.targets)})`, () => removeProjectSkills(projectRoot, selection.targets, selection.shouldDisableFeatureAfterRemove));
637
+ return true;
638
+ }
639
+ case "3": {
640
+ const selection = await selectAgentHookTargetsForMenu(projectRoot, rl, "选择要应用 Agent hooks 的 agent 工具");
641
+ if (selection === undefined) {
642
+ console.log("已取消应用 Agent hooks,返回功能管理。");
643
+ return false;
644
+ }
645
+ await runMenuAction(`应用 Agent hooks(${formatAgentHookTargetList(selection.targets)})`, () => applyAgentHooks(projectRoot, selection.targets));
646
+ return true;
647
+ }
648
+ case "4": {
649
+ const selection = await selectAgentHookTargetsForMenu(projectRoot, rl, "选择要取消 Agent hooks 的 agent 工具");
650
+ if (selection === undefined) {
651
+ console.log("已取消 Agent hooks 取消操作,返回功能管理。");
652
+ return false;
653
+ }
654
+ await runMenuAction(`取消 Agent hooks(${formatAgentHookTargetList(selection.targets)})`, () => removeAgentHooks(projectRoot, selection.targets, selection.shouldDisableFeatureAfterRemove));
655
+ return true;
656
+ }
657
+ case "5":
658
+ await runMenuAction("应用 Git hook", () => applyGitHook(projectRoot));
659
+ return true;
660
+ case "6":
661
+ await runMenuAction("取消 Git hook", () => removeGitHook(projectRoot));
662
+ return true;
663
+ case "7":
664
+ await runMenuAction("刷新规则和模板", () => runInit(projectRoot));
665
+ return true;
666
+ case "8":
667
+ await runMenuAction("查看应用状态", () => printApplyStatus(projectRoot));
668
+ return true;
669
+ case "0":
670
+ console.log("已返回主菜单。");
671
+ return false;
672
+ default:
673
+ console.log("无效选择,返回主菜单。");
674
+ return false;
675
+ }
676
+ }
677
+ /**
678
+ * 选择 Skills 应用或取消的 agent 工具目标。
679
+ * 优先提供“按当前项目”默认项;用户也可以显式选择单个 agent 或全部 agent。
680
+ */
681
+ async function selectSkillTargetsForMenu(projectRoot, rl, title) {
682
+ const inferredTargets = await resolveSkillRegistrationTargets(projectRoot);
683
+ const useKeyMenu = canUseInteractiveKeys(input, output);
684
+ if (useKeyMenu) {
685
+ const answer = await promptSelect(input, output, title, buildSkillTargetSelectOptions(inferredTargets));
686
+ return resolveSkillTargetMenuAnswer(answer, inferredTargets);
687
+ }
688
+ const answer = await askTextSkillTargetMenu(rl, title, inferredTargets);
689
+ return resolveSkillTargetMenuAnswer(answer.trim(), inferredTargets);
690
+ }
691
+ /**
692
+ * 选择 Agent hooks 应用或取消的 agent 工具目标。
693
+ * GitHub Copilot 没有可安装的 Agent hook,因此只把 Codex 和 Claude Code 列为可选项。
694
+ */
695
+ async function selectAgentHookTargetsForMenu(projectRoot, rl, title) {
696
+ const inferredSkillTargets = await resolveSkillRegistrationTargets(projectRoot);
697
+ const inferredHookTargets = toAgentHookTargets(inferredSkillTargets);
698
+ if (inferredSkillTargets.length > 0 && inferredHookTargets.length === 0) {
699
+ console.log("当前项目只识别到 GitHub Copilot;GitHub Copilot 不支持 Agent hook,请选择 Codex 或 Claude Code。");
700
+ }
701
+ if (canUseInteractiveKeys(input, output)) {
702
+ const answer = await promptSelect(input, output, title, buildAgentHookTargetSelectOptions(inferredSkillTargets));
703
+ return resolveAgentHookTargetMenuAnswer(answer, inferredSkillTargets);
704
+ }
705
+ const answer = await askTextAgentHookTargetMenu(rl, title, inferredSkillTargets);
706
+ return resolveAgentHookTargetMenuAnswer(answer.trim(), inferredSkillTargets);
707
+ }
708
+ /**
709
+ * raw mode 菜单中的 Skills 目标选项。
710
+ * 默认项放在第一位,让已能识别 agent 工具的项目可以直接回车确认。
711
+ */
712
+ function buildSkillTargetSelectOptions(inferredTargets) {
713
+ const options = [];
714
+ if (inferredTargets.length > 0) {
715
+ options.push({
716
+ value: "default",
717
+ label: `按当前项目(${formatTargetList(inferredTargets)})`
718
+ });
719
+ }
720
+ options.push({ value: "codex", label: "Codex" }, { value: "claudecode", label: "Claude Code" }, { value: "githubcopilot", label: "GitHub Copilot" }, { value: "all", label: "全部" }, { value: "0", label: "返回" });
721
+ return options;
722
+ }
723
+ /**
724
+ * raw mode 菜单中的 Agent hook 目标选项。
725
+ * 默认项只包含支持 Agent hook 的目标,自动忽略 GitHub Copilot。
726
+ */
727
+ function buildAgentHookTargetSelectOptions(inferredSkillTargets) {
728
+ const inferredHookTargets = toAgentHookTargets(inferredSkillTargets);
729
+ const options = [];
730
+ if (inferredHookTargets.length > 0) {
731
+ options.push({
732
+ value: "default",
733
+ label: `按当前项目(${formatAgentHookTargetList(inferredHookTargets)})`
734
+ });
735
+ }
736
+ options.push({ value: "codex", label: "Codex" }, { value: "claudecode", label: "Claude Code" }, { value: "all", label: "全部可用 Agent hooks" }, { value: "0", label: "返回" });
737
+ return options;
738
+ }
739
+ /**
740
+ * 非 raw mode 终端中的 Skills 目标菜单。
741
+ * 除数字外也支持输入 codex、claudecode、githubcopilot、all 和 default。
742
+ */
743
+ async function askTextSkillTargetMenu(rl, title, inferredTargets) {
744
+ console.log(`\n${title}`);
745
+ if (inferredTargets.length > 0) {
746
+ console.log(`D. 按当前项目(${formatTargetList(inferredTargets)})`);
747
+ }
748
+ console.log("1. Codex");
749
+ console.log("2. Claude Code");
750
+ console.log("3. GitHub Copilot");
751
+ console.log("A. 全部");
752
+ console.log("0. 返回");
753
+ console.log("可输入编号或名称,例如:1、codex、githubcopilot、all。");
754
+ return askQuestionOrDefault(rl, "请选择 agent 工具:", "0");
755
+ }
756
+ /**
757
+ * 非 raw mode 终端中的 Agent hook 目标菜单。
758
+ * 菜单不列出 GitHub Copilot,避免用户误以为它支持 Agent hook。
759
+ */
760
+ async function askTextAgentHookTargetMenu(rl, title, inferredSkillTargets) {
761
+ const inferredHookTargets = toAgentHookTargets(inferredSkillTargets);
762
+ console.log(`\n${title}`);
763
+ if (inferredHookTargets.length > 0) {
764
+ console.log(`D. 按当前项目(${formatAgentHookTargetList(inferredHookTargets)})`);
765
+ }
766
+ console.log("1. Codex");
767
+ console.log("2. Claude Code");
768
+ console.log("A. 全部可用 Agent hooks");
769
+ console.log("0. 返回");
770
+ console.log("GitHub Copilot 不支持 Agent hook,因此不在这里安装或取消。");
771
+ console.log("可输入编号或名称,例如:1、codex、claudecode、all。");
772
+ return askQuestionOrDefault(rl, "请选择 agent 工具:", "0");
773
+ }
774
+ /**
775
+ * 把 Skills 菜单答案解析成目标列表。
776
+ * 返回 undefined 表示用户返回;抛错表示输入了不支持的目标。
777
+ */
778
+ function resolveSkillTargetMenuAnswer(answer, inferredTargets) {
779
+ const targets = parseSkillTargetMenuSelection(answer, inferredTargets);
780
+ if (targets.length === 0) {
781
+ return undefined;
782
+ }
783
+ return {
784
+ targets,
785
+ shouldDisableFeatureAfterRemove: isDefaultOrAllTargetAnswer(answer)
786
+ };
787
+ }
788
+ /**
789
+ * 把 Agent hook 菜单答案解析成目标列表。
790
+ * 返回 undefined 表示用户返回;GitHub Copilot 输入会得到明确错误。
791
+ */
792
+ function resolveAgentHookTargetMenuAnswer(answer, inferredSkillTargets) {
793
+ const targets = parseAgentHookTargetMenuSelection(answer, inferredSkillTargets);
794
+ if (targets.length === 0) {
795
+ return undefined;
796
+ }
797
+ return {
798
+ targets,
799
+ shouldDisableFeatureAfterRemove: isDefaultOrAllTargetAnswer(answer)
800
+ };
801
+ }
802
+ /**
803
+ * 解析功能管理中 Skills 目标文本。
804
+ * 该函数导出给单元测试使用,确保非 raw mode 菜单和 raw mode 菜单使用同一套目标规则。
805
+ */
806
+ export function parseSkillTargetMenuSelection(value, inferredTargets = []) {
807
+ const normalizedValue = value.trim().toLowerCase();
808
+ if (normalizedValue === "" || normalizedValue === "0") {
809
+ return [];
810
+ }
811
+ if (normalizedValue === "d" || normalizedValue === "default" || normalizedValue === "current") {
812
+ return [...inferredTargets];
813
+ }
814
+ const targets = new Set();
815
+ const tokens = normalizedValue.split(/[,\s]+/u).filter(Boolean);
816
+ for (const token of tokens) {
817
+ if (token === "a" || token === "all") {
818
+ return listSupportedSkillRegistrationTargets();
819
+ }
820
+ if (token === "1") {
821
+ targets.add("codex");
822
+ continue;
823
+ }
824
+ if (token === "2") {
825
+ targets.add("claudecode");
826
+ continue;
827
+ }
828
+ if (token === "3") {
829
+ targets.add("githubcopilot");
830
+ continue;
831
+ }
832
+ for (const target of parseSkillRegistrationTargets(token)) {
833
+ targets.add(target);
834
+ }
835
+ }
836
+ return [...targets];
837
+ }
838
+ /**
839
+ * 解析功能管理中 Agent hook 目标文本。
840
+ * GitHub Copilot 没有 Agent hook 安装位置,因此输入相关别名时直接给出清晰错误。
841
+ */
842
+ export function parseAgentHookTargetMenuSelection(value, inferredSkillTargets = []) {
843
+ const normalizedValue = value.trim().toLowerCase();
844
+ if (normalizedValue === "" || normalizedValue === "0") {
845
+ return [];
846
+ }
847
+ if (normalizedValue === "d" || normalizedValue === "default" || normalizedValue === "current") {
848
+ return toAgentHookTargets(inferredSkillTargets);
849
+ }
850
+ const targets = new Set();
851
+ const tokens = normalizedValue.split(/[,\s]+/u).filter(Boolean);
852
+ for (const token of tokens) {
853
+ if (token === "a" || token === "all" || token === "agent" || token === "agents") {
854
+ return ["codex", "claudecode"];
855
+ }
856
+ if (token === "1" || token === "codex") {
857
+ targets.add("codex");
858
+ continue;
859
+ }
860
+ if (token === "2" || token === "claudecode" || token === "claude-code" || token === "claude") {
861
+ targets.add("claudecode");
862
+ continue;
863
+ }
864
+ if (token === "3" || token === "githubcopilot" || token === "github-copilot" || token === "copilot" || token === "github") {
865
+ throw new Error("GitHub Copilot 不支持 Agent hook,请选择 Codex、Claude Code 或全部可用 Agent hooks。");
866
+ }
867
+ throw new Error(`不支持的 Agent hook 目标:${token}。当前支持 codex、claudecode 或 all。`);
868
+ }
869
+ return [...targets];
870
+ }
871
+ /**
872
+ * 从 Skills 目标中过滤出支持 Agent hook 的目标。
873
+ * GitHub Copilot 只支持项目级 Skills,不映射到任何 hook。
874
+ */
875
+ function toAgentHookTargets(targets) {
876
+ return targets.filter((target) => target === "codex" || target === "claudecode");
877
+ }
878
+ /**
879
+ * 判断菜单答案是否代表“按当前项目”或“全部”。
880
+ * 这些范围取消后会同步关闭对应功能开关,保持原有菜单行为。
881
+ */
882
+ function isDefaultOrAllTargetAnswer(answer) {
883
+ const normalizedAnswer = answer.trim().toLowerCase();
884
+ return normalizedAnswer === "default"
885
+ || normalizedAnswer === "d"
886
+ || normalizedAnswer === "current"
887
+ || normalizedAnswer === "all"
888
+ || normalizedAnswer === "a";
889
+ }
890
+ /**
891
+ * 格式化 Skills 目标列表,用于菜单动作回显。
892
+ */
893
+ function formatTargetList(targets) {
894
+ return targets.map((target) => formatSkillRegistrationTargetName(target)).join("、");
895
+ }
896
+ /**
897
+ * 格式化 Agent hook 目标列表,用于菜单动作回显。
898
+ */
899
+ function formatAgentHookTargetList(targets) {
900
+ return targets.map((target) => target === "codex" ? "Codex" : "Claude Code").join("、");
901
+ }
217
902
  /**
218
903
  * TTY 菜单动作结束后暂停,避免下一轮菜单清屏导致结果一闪而过。
219
904
  * 非 TTY 兜底模式不暂停,保证管道和脚本执行不会被阻塞。
@@ -254,17 +939,133 @@ function printInputHint(message) {
254
939
  * 初始化命令实现。
255
940
  * 输出所有操作结果,便于用户看清哪些文件被创建、更新或跳过。
256
941
  */
257
- async function runInit(projectRoot) {
258
- const result = await initializeProject({ projectRoot });
942
+ async function runInit(projectRoot, args = []) {
943
+ if (args.length > 1) {
944
+ console.error("init 只接受一个可选 agent 目标。用法:code-helper init [all|codex|claudecode|githubcopilot]");
945
+ return 1;
946
+ }
947
+ const skillRegistrationTargets = args[0] === undefined
948
+ ? await resolveInitSkillRegistrationTargets(projectRoot)
949
+ : parseSkillRegistrationTargets(args[0]);
950
+ const result = await initializeProject({ projectRoot, skillRegistrationTargets });
259
951
  printOperations(result.operations);
260
952
  return 0;
261
953
  }
954
+ /**
955
+ * 本仓库开发后的本地刷新命令。
956
+ * 它先用空目标刷新受控入口、规则模板和 `.code-helper/skills`,避免顺带创建其他 agent 入口或安装 hooks;
957
+ * 再显式注册全部项目级 skills,保持 Codex、Claude Code 和 GitHub Copilot 看到的 skill 内容一致。
958
+ */
959
+ async function runSyncLocal(projectRoot, args = []) {
960
+ if (args.length > 0) {
961
+ console.error("sync-local 不接受参数。用法:code-helper sync-local");
962
+ return 1;
963
+ }
964
+ const initializeResult = await initializeProject({ projectRoot, skillRegistrationTargets: [] });
965
+ await setFeatureEnabled(projectRoot, "skillRegistration", true);
966
+ const targets = listSupportedSkillRegistrationTargets();
967
+ const skillOperations = (await Promise.all(targets.map((target) => registerProjectSkills(projectRoot, target)))).flat();
968
+ const statuses = (await Promise.all(targets.map((target) => listProjectSkillRegistrations(projectRoot, target)))).flat();
969
+ const initializeOperations = initializeResult.operations.filter((operation) => !operation.message.includes("已跳过项目级 skills 注册")
970
+ && !operation.message.includes("已跳过 Agent hooks 安装"));
971
+ printOperations([...initializeOperations, ...skillOperations]);
972
+ printSkillRegistrationStatus(statuses);
973
+ return 0;
974
+ }
975
+ /**
976
+ * 为 init 解析要应用的 agent 工具目标。
977
+ * 已有入口文件可以直接推断;完全无法判断时,交互终端让用户选择,非交互场景保守跳过。
978
+ */
979
+ async function resolveInitSkillRegistrationTargets(projectRoot) {
980
+ const inferredTargets = await resolveSkillRegistrationTargets(projectRoot);
981
+ const canUseTextMenu = Boolean(input.isTTY && output.isTTY);
982
+ if (inferredTargets.length > 0) {
983
+ return inferredTargets;
984
+ }
985
+ if (canUseInteractiveKeys(input, output)) {
986
+ const result = await promptMultiSelect(input, output, "选择 init 要应用的 agent 工具(默认选中 Codex,可用空格调整)", listSupportedSkillRegistrationTargets().map((target) => ({
987
+ value: target,
988
+ label: formatSkillRegistrationTargetName(target),
989
+ checked: target === "codex"
990
+ })));
991
+ const selectedTargets = result.cancelled
992
+ ? []
993
+ : result.options.filter((option) => option.checked).map((option) => option.value);
994
+ if (selectedTargets.length === 0) {
995
+ console.log("未选择 agent 工具,init 将只刷新 code-helper 工作区和规则模板,跳过项目级 skills 与 Agent hooks。");
996
+ }
997
+ return selectedTargets;
998
+ }
999
+ if (canUseTextMenu) {
1000
+ const rl = createInterface({ input, output });
1001
+ try {
1002
+ return await askTextInitTargetMenu(rl);
1003
+ }
1004
+ finally {
1005
+ rl.close();
1006
+ }
1007
+ }
1008
+ console.log("未发现 AGENTS.md、CLAUDE.md 或 GitHub Copilot 入口;非交互模式不会默认全量安装项目级 skills 或 Agent hooks。");
1009
+ console.log("如需应用能力,请改用 `code-helper init codex|claudecode|githubcopilot|all`,或先创建对应入口文件后再运行 init。");
1010
+ return [];
1011
+ }
1012
+ /**
1013
+ * raw mode 不可用但仍是 TTY 时,使用数字输入选择 init 目标。
1014
+ * 空回车或 0 表示跳过,避免用户误入流程后无法退出。
1015
+ */
1016
+ async function askTextInitTargetMenu(rl) {
1017
+ console.log("\n选择 init 要应用的 agent 工具");
1018
+ console.log("1. Codex");
1019
+ console.log("2. Claude Code");
1020
+ console.log("3. GitHub Copilot");
1021
+ console.log("A. 全部");
1022
+ console.log("0. 跳过项目级 skills 与 Agent hooks");
1023
+ console.log("可输入多个编号或名称,例如:1,2 或 codex,claudecode。");
1024
+ const answer = (await askQuestionOrDefault(rl, "请选择 agent 工具:", "0")).trim();
1025
+ const targets = parseInitTargetSelection(answer);
1026
+ if (targets.length === 0) {
1027
+ console.log("未选择 agent 工具,init 将只刷新 code-helper 工作区和规则模板,跳过项目级 skills 与 Agent hooks。");
1028
+ }
1029
+ return targets;
1030
+ }
1031
+ /**
1032
+ * 解析 init 文本兜底菜单的多目标输入。
1033
+ * 同时支持数字、英文目标名和 all,方便 macOS / Windows 终端复制粘贴。
1034
+ */
1035
+ function parseInitTargetSelection(value) {
1036
+ if (value === "" || value === "0") {
1037
+ return [];
1038
+ }
1039
+ const targets = new Set();
1040
+ const tokens = value.toLowerCase().split(/[,\s]+/u).filter(Boolean);
1041
+ for (const token of tokens) {
1042
+ if (token === "a" || token === "all") {
1043
+ return listSupportedSkillRegistrationTargets();
1044
+ }
1045
+ if (token === "1") {
1046
+ targets.add("codex");
1047
+ continue;
1048
+ }
1049
+ if (token === "2") {
1050
+ targets.add("claudecode");
1051
+ continue;
1052
+ }
1053
+ if (token === "3") {
1054
+ targets.add("githubcopilot");
1055
+ continue;
1056
+ }
1057
+ for (const target of parseSkillRegistrationTargets(token)) {
1058
+ targets.add(target);
1059
+ }
1060
+ }
1061
+ return [...targets];
1062
+ }
262
1063
  /**
263
1064
  * 检查命令实现。
264
1065
  * 存在 error 时返回 1,方便 CI 或 hook 使用。
265
1066
  */
266
- async function runCheck(projectRoot) {
267
- const issues = await runChecks(projectRoot);
1067
+ async function runCheck(projectRoot, args = []) {
1068
+ const issues = await runChecks(projectRoot, { writeReport: args.includes("--write-report") });
268
1069
  if (issues.length === 0) {
269
1070
  console.log("code-helper check 通过:未发现协作文档结构问题。");
270
1071
  return 0;
@@ -278,6 +1079,90 @@ async function runCheck(projectRoot) {
278
1079
  }
279
1080
  return issues.some((issue) => issue.level === "error") ? 1 : 0;
280
1081
  }
1082
+ /**
1083
+ * 应用项目级 Skills。
1084
+ * 功能管理菜单已经完成目标选择,这里按显式目标写入对应 agent 的项目级 skills。
1085
+ */
1086
+ async function applyProjectSkills(projectRoot, targets) {
1087
+ await setFeatureEnabled(projectRoot, "skillRegistration", true);
1088
+ const operations = (await Promise.all(targets.map((target) => registerProjectSkills(projectRoot, target)))).flat();
1089
+ const statuses = (await Promise.all(targets.map((target) => listProjectSkillRegistrations(projectRoot, target)))).flat();
1090
+ printOperations(operations);
1091
+ printSkillRegistrationStatus(statuses);
1092
+ return 0;
1093
+ }
1094
+ /**
1095
+ * 取消项目级 Skills。
1096
+ * 只删除目标 agent 下 code-helper 管理的 skills;按当前项目或全部取消时同步关闭后续自动注册。
1097
+ */
1098
+ async function removeProjectSkills(projectRoot, targets, shouldDisableFeatureAfterRemove) {
1099
+ const operations = (await Promise.all(targets.map((target) => unregisterProjectSkills(projectRoot, target)))).flat();
1100
+ const statuses = (await Promise.all(targets.map((target) => listProjectSkillRegistrations(projectRoot, target)))).flat();
1101
+ if (shouldDisableFeatureAfterRemove) {
1102
+ await setFeatureEnabled(projectRoot, "skillRegistration", false);
1103
+ console.log("已关闭后续初始化时的项目级 Skills 自动注册。");
1104
+ }
1105
+ printOperations(operations);
1106
+ printSkillRegistrationStatus(statuses);
1107
+ return 0;
1108
+ }
1109
+ /**
1110
+ * 应用 Agent hooks。
1111
+ * Agent hooks 安装到 Codex / Claude Code 项目级配置,只运行 finish --check-only。
1112
+ */
1113
+ async function applyAgentHooks(projectRoot, targets) {
1114
+ const operations = [];
1115
+ for (const target of targets) {
1116
+ operations.push(await installHook(projectRoot, target));
1117
+ }
1118
+ await setFeatureEnabled(projectRoot, "agentHooks", true);
1119
+ printOperations(operations);
1120
+ printHookInstallationStatus(await listHookInstallations(projectRoot));
1121
+ return 0;
1122
+ }
1123
+ /**
1124
+ * 取消 Agent hooks。
1125
+ * 只卸载目标 agent 的 code-helper hook;按当前项目或全部取消时同步关闭后续安装入口。
1126
+ */
1127
+ async function removeAgentHooks(projectRoot, targets, shouldDisableFeatureAfterRemove) {
1128
+ const operations = [];
1129
+ for (const target of targets) {
1130
+ operations.push(await uninstallHook(projectRoot, target));
1131
+ }
1132
+ if (shouldDisableFeatureAfterRemove) {
1133
+ await setFeatureEnabled(projectRoot, "agentHooks", false);
1134
+ console.log("已关闭 Agent hooks 应用能力。");
1135
+ }
1136
+ printOperations(operations);
1137
+ printHookInstallationStatus(await listHookInstallations(projectRoot));
1138
+ return 0;
1139
+ }
1140
+ /**
1141
+ * 应用 Git pre-commit hook。
1142
+ */
1143
+ async function applyGitHook(projectRoot) {
1144
+ return runHooks(projectRoot, ["install", "git"]);
1145
+ }
1146
+ /**
1147
+ * 取消 Git pre-commit hook。
1148
+ */
1149
+ async function removeGitHook(projectRoot) {
1150
+ const exitCode = await runHooks(projectRoot, ["uninstall", "git"]);
1151
+ await setFeatureEnabled(projectRoot, "gitHooks", false);
1152
+ console.log("已关闭 Git hook 应用能力。");
1153
+ return exitCode;
1154
+ }
1155
+ /**
1156
+ * 查看功能管理状态。
1157
+ */
1158
+ async function printApplyStatus(projectRoot) {
1159
+ console.log("Skills 状态:");
1160
+ await runSkills(projectRoot, ["list"]);
1161
+ console.log("");
1162
+ console.log("Hooks 状态:");
1163
+ await runHooks(projectRoot, ["list"]);
1164
+ return 0;
1165
+ }
281
1166
  /**
282
1167
  * 非交互功能开关命令。
283
1168
  * 支持:features list、features enable <key>、features disable <key>。
@@ -301,38 +1186,6 @@ async function runFeatures(projectRoot, args) {
301
1186
  printFeatureHelp();
302
1187
  return 1;
303
1188
  }
304
- /**
305
- * 交互式功能开关菜单。
306
- * 修改后只保存配置,不自动重写模板;用户可再执行初始化刷新模板。
307
- */
308
- async function runFeatureMenu(projectRoot, rl) {
309
- const config = await loadConfig(projectRoot);
310
- if (canUseInteractiveKeys(input, output)) {
311
- const selectedFeatures = await promptMultiSelect(input, output, "功能开关管理", FEATURE_KEYS.map((feature) => ({
312
- value: feature,
313
- label: `${feature} - ${FEATURE_LABELS[feature]}`,
314
- checked: config.features[feature].enabled
315
- })));
316
- if (selectedFeatures.cancelled) {
317
- console.log("已取消功能开关修改,返回主菜单。");
318
- return false;
319
- }
320
- console.log(`\n▶ 开始:功能开关管理`);
321
- let changedCount = 0;
322
- for (const feature of selectedFeatures.options) {
323
- if (config.features[feature.value].enabled !== feature.checked) {
324
- await setFeatureEnabled(projectRoot, feature.value, feature.checked);
325
- changedCount += 1;
326
- }
327
- }
328
- console.log(`已保存功能开关,变更 ${changedCount} 项。`);
329
- printFeatureList(await loadConfig(projectRoot));
330
- console.log(`✓ 完成:功能开关管理`);
331
- return true;
332
- }
333
- await runTextFeatureMenu(projectRoot, rl);
334
- return false;
335
- }
336
1189
  /**
337
1190
  * 读取必填菜单输入。
338
1191
  * 空回车或输入 0 都表示返回上一级,避免用户误入流程后无法退出。
@@ -355,67 +1208,73 @@ async function askOptionalMenuInput(rl, question) {
355
1208
  }
356
1209
  return answer;
357
1210
  }
358
- /**
359
- * 非 TTY 环境下的数字功能开关菜单。
360
- * 输入 1..N 切换对应功能,输入 0 返回上一级。
361
- */
362
- async function runTextFeatureMenu(projectRoot, rl) {
363
- let shouldReturn = false;
364
- while (!shouldReturn) {
365
- const config = await loadConfig(projectRoot);
366
- console.log("\n功能开关管理");
367
- FEATURE_KEYS.forEach((feature, index) => {
368
- const status = config.features[feature].enabled ? "启用" : "关闭";
369
- console.log(`${index + 1}. ${FEATURE_LABELS[feature]}(${feature}):${status}`);
370
- });
371
- console.log("0. 返回");
372
- const answer = await askQuestionOrDefault(rl, "请输入数字切换功能,或输入 0 返回:", "0");
373
- const selectedIndex = Number.parseInt(answer.trim(), 10);
374
- if (selectedIndex === 0) {
375
- shouldReturn = true;
376
- continue;
377
- }
378
- if (!Number.isInteger(selectedIndex) || selectedIndex < 1 || selectedIndex > FEATURE_KEYS.length) {
379
- console.log("无效选择,请输入列表中的数字。");
380
- continue;
381
- }
382
- const selectedFeature = FEATURE_KEYS[selectedIndex - 1];
383
- const current = config.features[selectedFeature].enabled;
384
- await setFeatureEnabled(projectRoot, selectedFeature, !current);
385
- console.log(`已${current ? "关闭" : "启用"}:${FEATURE_LABELS[selectedFeature]}`);
386
- }
387
- }
388
1211
  /**
389
1212
  * 非 TTY 环境下的文本菜单兜底。
390
1213
  * 当终端不支持 raw mode 时,仍允许用户输入数字选择。
391
1214
  */
392
1215
  async function askTextMenu(rl) {
393
1216
  console.log("\ncode-helper 操作菜单");
394
- console.log("1. 初始化项目");
395
- console.log("2. 项目记忆规则优化");
396
- console.log("3. 项目计划优化");
397
- console.log("4. 生成人工页面测试文档");
398
- console.log("5. 功能开关管理");
399
- console.log("6. 项目规则检查");
400
- console.log("7. 文档归档");
401
- console.log("8. 查看任务状态");
402
- console.log("9. Skills 管理");
403
- console.log("0. 退出");
1217
+ for (const group of MAIN_MENU_GROUPS) {
1218
+ console.log(`\n${formatMainMenuGroupTitle(group.title)}`);
1219
+ for (const item of group.items) {
1220
+ for (const line of formatMainMenuTextItemLines(item)) {
1221
+ console.log(line);
1222
+ }
1223
+ }
1224
+ }
1225
+ console.log("\n 0. 退出");
1226
+ console.log(" 关闭 code-helper 菜单");
404
1227
  return askQuestionOrDefault(rl, "请选择操作:", "0");
405
1228
  }
406
1229
  /**
407
- * 非 TTY 环境下的 Skills 管理菜单。
1230
+ * 非 TTY 环境下的项目 Skills 管理菜单。
408
1231
  * 输入 0 立即返回,避免用户误入子菜单后无法退出。
409
1232
  */
410
1233
  async function askTextSkillMenu(rl) {
411
- console.log("\nSkills 管理");
1234
+ console.log("\n管理项目 Skills");
412
1235
  console.log("1. 查看注册状态");
413
1236
  console.log("2. 按当前项目注册 Skills");
414
1237
  console.log("3. 按当前项目取消注册 Skills");
415
1238
  console.log("4. 仅注册 Codex");
416
1239
  console.log("5. 仅注册 Claude Code");
417
- console.log("6. 注册全部");
418
- console.log("7. 取消注册全部");
1240
+ console.log("6. 仅注册 GitHub Copilot");
1241
+ console.log("7. 注册全部");
1242
+ console.log("8. 取消注册全部");
1243
+ console.log("9. Skills 质量检查");
1244
+ console.log("10. Skills 建议分析");
1245
+ console.log("0. 返回");
1246
+ return askQuestionOrDefault(rl, "请选择操作:", "0");
1247
+ }
1248
+ /**
1249
+ * 非 TTY 环境下的功能管理菜单。
1250
+ */
1251
+ async function askTextApplyMenu(rl) {
1252
+ console.log("\n功能管理");
1253
+ console.log("1. 应用项目级 Skills");
1254
+ console.log("2. 取消项目级 Skills");
1255
+ console.log("3. 应用 Agent hooks");
1256
+ console.log("4. 取消 Agent hooks");
1257
+ console.log("5. 应用 Git hook");
1258
+ console.log("6. 取消 Git hook");
1259
+ console.log("7. 刷新规则和模板");
1260
+ console.log("8. 查看应用状态");
1261
+ console.log("0. 返回");
1262
+ return askQuestionOrDefault(rl, "请选择操作:", "0");
1263
+ }
1264
+ /**
1265
+ * 非 TTY 环境下的 Hooks 管理菜单。
1266
+ */
1267
+ async function askTextHooksMenu(rl) {
1268
+ console.log("\n管理 Hooks");
1269
+ console.log("1. 查看 Hooks 状态");
1270
+ console.log("2. 安装 Git pre-commit hook");
1271
+ console.log("3. 卸载 Git pre-commit hook");
1272
+ console.log("4. 安装 Codex Agent hook");
1273
+ console.log("5. 卸载 Codex Agent hook");
1274
+ console.log("6. 安装 Claude Code Agent hook");
1275
+ console.log("7. 卸载 Claude Code Agent hook");
1276
+ console.log("8. 安装全部 Hooks");
1277
+ console.log("9. 卸载全部 Hooks");
419
1278
  console.log("0. 返回");
420
1279
  return askQuestionOrDefault(rl, "请选择操作:", "0");
421
1280
  }
@@ -479,7 +1338,8 @@ async function runPlan(projectRoot, args) {
479
1338
  * 参数:manual-test <功能名称> [标题]。
480
1339
  */
481
1340
  async function runManualTest(projectRoot, args) {
482
- const [featureName, title] = args;
1341
+ const [rawFeatureName, title] = args;
1342
+ const featureName = rawFeatureName ?? await selectTaskFeatureNameForCommand(projectRoot, "选择要生成手工测试文档的任务", ["active", "mixed"]);
483
1343
  if (!featureName) {
484
1344
  console.error("缺少功能名称。用法:code-helper manual-test <中文功能名> [标题]");
485
1345
  return 1;
@@ -492,15 +1352,57 @@ async function runManualTest(projectRoot, args) {
492
1352
  * 参数:archive <功能名称>。
493
1353
  */
494
1354
  async function runArchive(projectRoot, args) {
495
- const [featureName] = args;
1355
+ const flags = new Set(args.filter((arg) => arg.startsWith("--")));
1356
+ const rawFeatureName = args.find((arg) => !arg.startsWith("--"));
1357
+ const featureName = rawFeatureName ?? await selectTaskFeatureNameForCommand(projectRoot, "选择要归档的任务", ["active", "mixed"]);
496
1358
  if (!featureName) {
497
- console.error("缺少功能名称。用法:code-helper archive <中文功能名>");
1359
+ console.error("缺少功能名称。用法:code-helper archive <中文功能名> [--resolve-mixed]");
498
1360
  return 1;
499
1361
  }
500
- printOperations(await archiveFeature(projectRoot, featureName));
1362
+ printOperations(await archiveFeature(projectRoot, featureName, { resolveMixed: flags.has("--resolve-mixed") }));
501
1363
  await runTasks(projectRoot, []);
502
1364
  return 0;
503
1365
  }
1366
+ /**
1367
+ * 功能完成检查命令。
1368
+ * 参数:finish [中文功能名] [--check-only] [--json]。
1369
+ */
1370
+ async function runFinish(projectRoot, args) {
1371
+ const flags = new Set(args.filter((arg) => arg.startsWith("--")));
1372
+ const rawFeatureName = args.find((arg) => !arg.startsWith("--"));
1373
+ if (rawFeatureName === undefined && flags.has("--check-only") && !canUseInteractiveKeys(input, output)) {
1374
+ printFinishCheckOnlyCandidates(await getSelectableTasks(projectRoot, ["active", "mixed"]));
1375
+ return 0;
1376
+ }
1377
+ const featureName = rawFeatureName ?? await selectTaskFeatureNameForCommand(projectRoot, "选择要检查完成情况的任务", ["active", "mixed"]);
1378
+ if (!featureName) {
1379
+ console.error("缺少功能名称。用法:code-helper finish <中文功能名> [--check-only] [--json]");
1380
+ return 1;
1381
+ }
1382
+ const review = await createCompletionReview(projectRoot, featureName);
1383
+ if (flags.has("--json")) {
1384
+ console.log(JSON.stringify(review, null, 2));
1385
+ return 0;
1386
+ }
1387
+ printCompletionReview(review, flags.has("--check-only"));
1388
+ return 0;
1389
+ }
1390
+ /**
1391
+ * Agent hook 常用 check-only 模式没有明确功能名。
1392
+ * 这时只提示候选任务并返回成功,避免 hook 把正常收尾流程误判为命令失败。
1393
+ */
1394
+ function printFinishCheckOnlyCandidates(tasks) {
1395
+ if (tasks.length === 0) {
1396
+ console.log("功能完成检查:当前没有发现活动任务。");
1397
+ console.log("如果本轮变更形成长期规则,请询问用户是否更新项目记忆。");
1398
+ return;
1399
+ }
1400
+ console.log("功能完成检查:检测到活动任务,请 agent 选择当前任务后运行更精确的检查。");
1401
+ for (const task of tasks) {
1402
+ console.log(`- ${task.featureName}(${task.status})`);
1403
+ }
1404
+ console.log("建议命令:code-helper finish <中文功能名> --check-only");
1405
+ }
504
1406
  /**
505
1407
  * 任务状态列表命令。
506
1408
  * 参数:tasks [--json]。
@@ -528,18 +1430,28 @@ async function runTasks(projectRoot, args) {
528
1430
  }
529
1431
  /**
530
1432
  * 项目级 skills 注册命令。
531
- * 支持:skills list、skills register [all|codex|claudecode]、skills unregister [all|codex|claudecode]。
532
- * register/unregister 不带 target 时按当前项目入口文件推断目标,只有显式 all 才处理两套 agent。
1433
+ * 支持:skills list、skills register [target]、skills unregister [target]、skills doctor、skills audit
1434
+ * register/unregister 不带 target 时按当前项目入口文件推断目标,只有显式 all 才处理全部 agent。
533
1435
  */
534
1436
  async function runSkills(projectRoot, args) {
535
1437
  const [action = "list", rawTarget] = args;
536
- const targets = await resolveTargetsForSkillAction(projectRoot, action, rawTarget);
1438
+ if (action === "help" || action === "--help" || action === "-h") {
1439
+ printSkillsHelp();
1440
+ return 0;
1441
+ }
537
1442
  if (action === "list") {
1443
+ const targets = await resolveTargetsForSkillAction(projectRoot, action, rawTarget);
538
1444
  const statuses = (await Promise.all(targets.map((target) => listProjectSkillRegistrations(projectRoot, target)))).flat();
539
1445
  printSkillRegistrationStatus(statuses);
540
1446
  return 0;
541
1447
  }
542
1448
  if (action === "register") {
1449
+ const targets = await resolveTargetsForSkillAction(projectRoot, action, rawTarget);
1450
+ if (targets.length === 0) {
1451
+ printNoInferredSkillTargets(projectRoot, "注册");
1452
+ return 0;
1453
+ }
1454
+ await setFeatureEnabled(projectRoot, "skillRegistration", true);
543
1455
  const operations = (await Promise.all(targets.map((target) => registerProjectSkills(projectRoot, target)))).flat();
544
1456
  const statuses = (await Promise.all(targets.map((target) => listProjectSkillRegistrations(projectRoot, target)))).flat();
545
1457
  printOperations(operations);
@@ -547,15 +1459,86 @@ async function runSkills(projectRoot, args) {
547
1459
  return 0;
548
1460
  }
549
1461
  if (action === "unregister") {
1462
+ const targets = await resolveTargetsForSkillAction(projectRoot, action, rawTarget);
1463
+ if (targets.length === 0) {
1464
+ printNoInferredSkillTargets(projectRoot, "取消注册");
1465
+ return 0;
1466
+ }
550
1467
  const operations = (await Promise.all(targets.map((target) => unregisterProjectSkills(projectRoot, target)))).flat();
1468
+ if (rawTarget === undefined || rawTarget === "all") {
1469
+ await setFeatureEnabled(projectRoot, "skillRegistration", false);
1470
+ }
551
1471
  const statuses = (await Promise.all(targets.map((target) => listProjectSkillRegistrations(projectRoot, target)))).flat();
552
1472
  printOperations(operations);
553
1473
  printSkillRegistrationStatus(statuses);
554
1474
  return 0;
555
1475
  }
1476
+ if (action === "doctor") {
1477
+ const issues = await runSkillsDoctor(projectRoot);
1478
+ printSkillDoctorIssues(issues);
1479
+ return issues.some((issue) => issue.level === "error") ? 1 : 0;
1480
+ }
1481
+ if (action === "audit") {
1482
+ printSkillAuditRecommendations(await runSkillsAudit(projectRoot));
1483
+ return 0;
1484
+ }
556
1485
  printSkillsHelp();
557
1486
  return 1;
558
1487
  }
1488
+ /**
1489
+ * skills register/unregister 无法从入口文件推断目标时,输出可理解的跳过结果。
1490
+ * 这里不默认处理全部目标,避免在 CI 或新项目里误写入多个 agent 的项目级目录。
1491
+ */
1492
+ function printNoInferredSkillTargets(projectRoot, actionLabel) {
1493
+ printOperations([
1494
+ {
1495
+ path: projectRoot,
1496
+ action: "skipped",
1497
+ message: `未识别到明确的 agent 工具,已跳过项目级 skills ${actionLabel};请显式传入 codex、claudecode、githubcopilot 或 all。`
1498
+ }
1499
+ ]);
1500
+ }
1501
+ /**
1502
+ * Hooks 管理命令。
1503
+ * 支持:hooks list、hooks install <target>、hooks uninstall <target>。
1504
+ */
1505
+ async function runHooks(projectRoot, args) {
1506
+ const [action = "list", rawTarget] = args;
1507
+ if (action === "help" || action === "--help" || action === "-h") {
1508
+ printHooksHelp();
1509
+ return 0;
1510
+ }
1511
+ if (action === "list") {
1512
+ printHookInstallationStatus(await listHookInstallations(projectRoot));
1513
+ return 0;
1514
+ }
1515
+ if (action === "install" || action === "uninstall") {
1516
+ if (rawTarget === undefined) {
1517
+ console.error(`缺少 hooks 目标。用法:code-helper hooks ${action} <git|codex|claudecode|agent|all>`);
1518
+ printHooksHelp();
1519
+ return 1;
1520
+ }
1521
+ const targets = parseHookTargets(rawTarget);
1522
+ const operations = [];
1523
+ for (const target of targets) {
1524
+ if (action === "install") {
1525
+ operations.push(await installHook(projectRoot, target));
1526
+ await setFeatureEnabled(projectRoot, target === "git" ? "gitHooks" : "agentHooks", true);
1527
+ }
1528
+ else {
1529
+ operations.push(await uninstallHook(projectRoot, target));
1530
+ if (target === "git" || rawTarget === undefined || rawTarget === "all" || rawTarget === "agent" || rawTarget === "agents" || rawTarget === "agentHooks") {
1531
+ await setFeatureEnabled(projectRoot, target === "git" ? "gitHooks" : "agentHooks", false);
1532
+ }
1533
+ }
1534
+ }
1535
+ printOperations(operations);
1536
+ printHookInstallationStatus(await listHookInstallations(projectRoot));
1537
+ return 0;
1538
+ }
1539
+ printHooksHelp();
1540
+ return 1;
1541
+ }
559
1542
  /**
560
1543
  * 打印操作结果。
561
1544
  * 路径可能是绝对路径,保留原样方便用户定位。
@@ -565,6 +1548,76 @@ function printOperations(operations) {
565
1548
  console.log(`[${operation.action}] ${operation.path} - ${operation.message}`);
566
1549
  }
567
1550
  }
1551
+ /**
1552
+ * 打印功能完成检查结果。
1553
+ * checkOnly 模式用于 agent hook,输出更强调“下一步必须判断什么”。
1554
+ */
1555
+ function printCompletionReview(review, checkOnly) {
1556
+ console.log(`功能完成检查:${review.featureName}`);
1557
+ console.log(`任务状态:${review.taskStatus}`);
1558
+ console.log(`检查结论:${formatCompletionReviewStatus(review.reviewStatus)}`);
1559
+ console.log(`运行模式:${checkOnly ? "仅检查,不修改文件" : "检查并给出下一步建议"}`);
1560
+ console.log("");
1561
+ console.log("文档状态:");
1562
+ console.log(`- 计划文档:${formatDocumentPresence(review.documents.plan)}`);
1563
+ console.log(`- 实施记录:${formatDocumentPresence(review.documents.result)}`);
1564
+ console.log(`- 状态记录:${formatDocumentPresence(review.documents.status)}`);
1565
+ console.log(`- 手工测试:${formatDocumentPresence(review.documents.manualTest)}`);
1566
+ console.log("");
1567
+ console.log("状态枚举:");
1568
+ console.log(`- 未开始:${review.statusCounts.notStarted}`);
1569
+ console.log(`- 进行中:${review.statusCounts.inProgress}`);
1570
+ console.log(`- 部分完成:${review.statusCounts.partial}`);
1571
+ console.log(`- 被阻塞:${review.statusCounts.blocked}`);
1572
+ console.log(`- 已完成:${review.statusCounts.done}`);
1573
+ console.log("");
1574
+ console.log(`当前执行节点:${review.hasCurrentExecutionNode ? "已存在" : "缺失"}`);
1575
+ console.log(`子计划队列:${review.hasSubPlanQueue ? "已存在" : "缺失"}`);
1576
+ console.log(`建议询问更新记忆:${review.shouldAskMemoryUpdate ? "是" : "否"}`);
1577
+ console.log(`建议询问归档:${review.shouldAskArchive ? "是" : "否"}`);
1578
+ console.log("");
1579
+ console.log("必须确认事项:");
1580
+ if (review.requiredConfirmations.length === 0) {
1581
+ console.log("- 无必须向用户确认的事项。");
1582
+ }
1583
+ else {
1584
+ review.requiredConfirmations.forEach((confirmation, index) => {
1585
+ console.log(`${index + 1}. ${confirmation}`);
1586
+ });
1587
+ }
1588
+ console.log("");
1589
+ console.log("下一步建议:");
1590
+ review.recommendations.forEach((recommendation, index) => {
1591
+ console.log(`${index + 1}. ${recommendation}`);
1592
+ });
1593
+ if (review.changedPaths.length > 0) {
1594
+ console.log("");
1595
+ console.log("检测到的当前变更:");
1596
+ review.changedPaths.forEach((path) => {
1597
+ console.log(`- ${path}`);
1598
+ });
1599
+ }
1600
+ }
1601
+ /**
1602
+ * 把完成检查状态转成中文文案。
1603
+ */
1604
+ function formatCompletionReviewStatus(status) {
1605
+ const labels = {
1606
+ "needs-work": "当前任务仍需继续推进",
1607
+ blocked: "当前任务存在阻塞",
1608
+ "node-review": "需要先补齐当前执行节点",
1609
+ "ready-to-archive": "可在用户确认后归档",
1610
+ archived: "任务已归档,视为已结束",
1611
+ "missing-docs": "缺少必要协作文档"
1612
+ };
1613
+ return labels[status];
1614
+ }
1615
+ /**
1616
+ * 把文档存在状态转成稳定中文输出。
1617
+ */
1618
+ function formatDocumentPresence(document) {
1619
+ return `${document.exists ? "已存在" : "缺失"} - ${document.relativePath}`;
1620
+ }
568
1621
  /**
569
1622
  * 打印功能开关列表。
570
1623
  * key 直接展示给用户,便于配合非交互命令使用。
@@ -585,6 +1638,41 @@ function printSkillRegistrationStatus(statuses) {
585
1638
  console.log(` path: ${status.path}`);
586
1639
  }
587
1640
  }
1641
+ /**
1642
+ * 打印 hooks 安装状态。
1643
+ */
1644
+ function printHookInstallationStatus(statuses) {
1645
+ for (const status of statuses) {
1646
+ console.log(`${status.target}: ${status.installed ? "已安装" : "未安装"} - ${status.label}`);
1647
+ console.log(` 开关:${status.enabled ? "启用" : "关闭"}`);
1648
+ console.log(` path: ${status.path}`);
1649
+ }
1650
+ }
1651
+ /**
1652
+ * 打印 skills doctor 检查结果。
1653
+ * 没有问题时输出明确结论,避免用户误以为空命令失败。
1654
+ */
1655
+ function printSkillDoctorIssues(issues) {
1656
+ if (issues.length === 0) {
1657
+ console.log("skills doctor 通过:未发现项目级 skills 结构问题。");
1658
+ return;
1659
+ }
1660
+ for (const issue of issues) {
1661
+ console.log(`[${issue.level}] ${issue.code}: ${issue.message}`);
1662
+ console.log(` 路径:${issue.path}`);
1663
+ console.log(` 建议:${issue.suggestion}`);
1664
+ }
1665
+ }
1666
+ /**
1667
+ * 打印 skills audit 推荐项。
1668
+ * audit 是建议型命令,始终返回 0。
1669
+ */
1670
+ function printSkillAuditRecommendations(recommendations) {
1671
+ for (const recommendation of recommendations) {
1672
+ console.log(`[${recommendation.priority}] ${recommendation.code}: ${recommendation.message}`);
1673
+ console.log(` 建议:${recommendation.suggestion}`);
1674
+ }
1675
+ }
588
1676
  /**
589
1677
  * 判断字符串是否是合法 FeatureKey。
590
1678
  * 运行时 CLI 参数需要显式校验,不能只依赖 TypeScript 类型。
@@ -608,9 +1696,21 @@ function printFeatureHelp() {
608
1696
  function printSkillsHelp() {
609
1697
  console.log("用法:");
610
1698
  console.log(" code-helper skills list");
611
- console.log(" code-helper skills register [all|codex|claudecode]");
612
- console.log(" code-helper skills unregister [all|codex|claudecode]");
613
- console.log("说明:register/unregister 不带 target 时按当前项目已有 AGENTS.md / CLAUDE.md 自动选择目标。");
1699
+ console.log(" code-helper skills register [all|codex|claudecode|githubcopilot]");
1700
+ console.log(" code-helper skills unregister [all|codex|claudecode|githubcopilot]");
1701
+ console.log(" code-helper skills doctor");
1702
+ console.log(" code-helper skills audit");
1703
+ console.log("说明:register/unregister 不带 target 时按当前项目已有 AGENTS.md / CLAUDE.md / GitHub Copilot 入口自动选择目标;无法识别时会跳过,请显式传 target。");
1704
+ }
1705
+ /**
1706
+ * 打印 hooks 命令帮助。
1707
+ */
1708
+ function printHooksHelp() {
1709
+ console.log("用法:");
1710
+ console.log(" code-helper hooks list");
1711
+ console.log(" code-helper hooks install <git|codex|claudecode|agent|all>");
1712
+ console.log(" code-helper hooks uninstall <git|codex|claudecode|agent|all>");
1713
+ console.log("说明:hooks install 会直接应用对应 hook,并同步内部开关;init 只会安装选中 agent 对应的 Agent hooks,不会安装 Git hook。");
614
1714
  }
615
1715
  /**
616
1716
  * 打印 CLI 帮助。
@@ -621,18 +1721,25 @@ function printHelp() {
621
1721
 
622
1722
  用法:
623
1723
  code-helper 打开交互菜单
624
- code-helper init 初始化项目规则和工作区
625
- code-helper check 检查协作文档结构
626
- code-helper features list 查看功能开关
627
- code-helper features enable <key> 启用功能
628
- code-helper features disable <key> 关闭功能
1724
+ code-helper init [target] 初始化项目规则和工作区,可指定 all|codex|claudecode|githubcopilot
1725
+ code-helper sync-local 刷新本仓库本地模板并注册全部项目级 skills
1726
+ code-helper check [--write-report] 检查协作文档结构
1727
+ code-helper features list 查看高级功能配置
1728
+ code-helper features enable <key> 启用高级功能配置
1729
+ code-helper features disable <key> 关闭高级功能配置
629
1730
  code-helper plan <需求文档> [中文功能名] 生成项目计划文档
630
1731
  code-helper manual-test <中文功能名> [标题] 生成页面手工测试文档
631
- code-helper archive <中文功能名> 将功能文档移动到 archive 并识别为已结束
1732
+ code-helper archive <中文功能名> [--resolve-mixed] 将功能文档移动到 archive 并识别为已结束
1733
+ code-helper finish [中文功能名] 检查当前功能是否完成并提示后续动作
632
1734
  code-helper tasks [--json] 查看 active / archived / mixed 任务
633
1735
  code-helper skills list 查看项目级 skills 注册状态
634
1736
  code-helper skills register [target] 按项目入口或指定 target 注册项目级 skills
635
1737
  code-helper skills unregister [target] 按项目入口或指定 target 取消注册项目级 skills
1738
+ code-helper skills doctor 检查项目级 skills 结构和质量
1739
+ code-helper skills audit 根据项目状态给出 skills 建议
1740
+ code-helper hooks list 查看 Git / Agent hooks 安装状态
1741
+ code-helper hooks install <target> 安装 Git / Agent hooks
1742
+ code-helper hooks uninstall <target> 卸载 code-helper 管理的 hooks
636
1743
  `);
637
1744
  }
638
1745
  /**