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