svharness 0.14.13 → 0.14.17

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.
Files changed (32) hide show
  1. package/README.md +148 -9
  2. package/assets/codechat-agent.ico +0 -0
  3. package/dist/adapters/codex.js +19 -0
  4. package/dist/adapters/index.js +2 -0
  5. package/dist/commands/doctor/utils.js +2 -0
  6. package/dist/commands/init.js +37 -129
  7. package/dist/commands/shell-integration.js +40 -3
  8. package/dist/commands/wiki.js +127 -0
  9. package/dist/config/index.js +2 -1
  10. package/dist/config/merge-options.js +19 -0
  11. package/dist/config/normalize.js +18 -0
  12. package/dist/core/state.js +39 -0
  13. package/dist/index.js +58 -10
  14. package/dist/lib/agent-launcher.js +14 -16
  15. package/dist/lib/codechat-runner-resolver.js +208 -0
  16. package/dist/lib/wiki/run-wiki-generation.js +161 -0
  17. package/dist/lib/win-registry.js +10 -0
  18. package/dist/utils/validate-args.js +1 -0
  19. package/dist/wiki/repowikiIndexer.js +19 -150
  20. package/dist/wiki/repowikiPrompts.js +296 -0
  21. package/dist/wiki/repowikiStructureNormalize.js +31 -1
  22. package/dist/wiki/wikiTasksWriter.js +59 -22
  23. package/docs/agent-launcher-design.md +127 -26
  24. package/docs/standalone-codechat-ps1.md +123 -0
  25. package/package.json +2 -1
  26. package/scripts/preuninstall.js +45 -5
  27. package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +2 -2
  28. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +1 -1
  29. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +1 -1
  30. package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +15 -6
  31. package/templates/codechat/Start-CodeChat.ps1 +359 -0
  32. package/templates/svharness.config.example.yaml +7 -0
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * `svharness wiki` — standalone wiki generation for any code project.
4
+ *
5
+ * Unlike the wiki embedded in `svharness build`, this command:
6
+ * - Scans source code in-place (no baseline copy).
7
+ * - Defaults output to `./wiki/` (relative to cwd).
8
+ * - Does NOT require a harness; optionally syncs S10_wiki via --update-state.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.runWiki = runWiki;
15
+ const fs_extra_1 = __importDefault(require("fs-extra"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const picocolors_1 = __importDefault(require("picocolors"));
18
+ const version_1 = require("../utils/version");
19
+ const logger_1 = require("../utils/logger");
20
+ const run_wiki_generation_1 = require("../lib/wiki/run-wiki-generation");
21
+ const state_1 = require("../core/state");
22
+ async function runWiki(opts) {
23
+ (0, logger_1.setVerbose)(!!opts.verbose);
24
+ const cwd = opts.cwd ?? process.cwd();
25
+ // 1. Mutual exclusion check
26
+ if (opts.generateWiki && opts.wikiTasksOnly) {
27
+ logger_1.logger.error('--generate-wiki 与 --wiki-tasks-only 不可同时启用');
28
+ return 1;
29
+ }
30
+ // 2. --update-state requires --harness
31
+ if (opts.updateState && !opts.harness) {
32
+ logger_1.logger.error('--update-state 须同时提供 --harness <path>');
33
+ return 1;
34
+ }
35
+ // 3. Resolve paths
36
+ const repoRootFs = node_path_1.default.resolve(cwd, opts.wikiSource ?? '.');
37
+ const outputRoot = node_path_1.default.resolve(cwd, opts.output ?? './wiki');
38
+ const wikiRelPath = node_path_1.default.relative(outputRoot, outputRoot); // always '.'
39
+ const projectName = opts.projectName ?? node_path_1.default.basename(repoRootFs);
40
+ const mode = opts.generateWiki ? 'full' : 'tasks';
41
+ // 4. Source guard
42
+ if (!(await fs_extra_1.default.pathExists(repoRootFs))) {
43
+ logger_1.logger.error(`源码目录不存在:${repoRootFs}`);
44
+ return 1;
45
+ }
46
+ // 5. Display config
47
+ logger_1.logger.info(`源码根:${repoRootFs}`);
48
+ logger_1.logger.info(`输出到:${outputRoot}`);
49
+ logger_1.logger.info(`模式:${mode === 'full' ? '完整生成(generate-wiki)' : '仅任务清单(tasks-only)'}`);
50
+ logger_1.logger.info(`语言:${opts.wikiLang ?? 'zh'}`);
51
+ // 6. Run wiki generation
52
+ const result = await (0, run_wiki_generation_1.runWikiGeneration)({
53
+ repoRootFs,
54
+ wikiOutputRootFs: outputRoot,
55
+ wikiRelPath: '.', // wiki output is directly in outputRoot
56
+ wikiLang: opts.wikiLang,
57
+ wikiModel: opts.wikiModel,
58
+ wikiBaseUrl: opts.wikiBaseUrl,
59
+ wikiApiKey: opts.wikiApiKey,
60
+ projectName,
61
+ cliVersion: (0, version_1.getCliVersion)(),
62
+ mode,
63
+ cwd,
64
+ forceFull: opts.forceFull,
65
+ wikiAudience: opts.harness ? 'agent' : 'human',
66
+ sourceRootRel: '.',
67
+ onLog: (m) => logger_1.logger.info(m),
68
+ });
69
+ // 7. Handle result
70
+ if (result.ok) {
71
+ if (mode === 'full') {
72
+ logger_1.logger.success(`wiki 已完整生成到:${outputRoot}`);
73
+ }
74
+ else {
75
+ logger_1.logger.success(`wiki 任务清单已生成:${result.tasksPath} (共 ${result.pageCount} 页任务)`);
76
+ }
77
+ }
78
+ else {
79
+ if (mode === 'full') {
80
+ logger_1.logger.error('wiki 完整生成失败(已产出部分文件可从 checkpoint 恢复)');
81
+ }
82
+ else {
83
+ logger_1.logger.error('wiki 任务清单生成失败');
84
+ }
85
+ }
86
+ // 8. Optional: sync S10_wiki to harness state
87
+ if (opts.updateState && opts.harness) {
88
+ const harnessRoot = node_path_1.default.resolve(cwd, opts.harness);
89
+ if (!(await fs_extra_1.default.pathExists(node_path_1.default.join(harnessRoot, 'harness.yaml')))) {
90
+ logger_1.logger.warn(`未找到 harness.yaml:${harnessRoot},跳过 state 更新`);
91
+ }
92
+ else {
93
+ // Warn if output doesn't match harness/baseline/wiki
94
+ const expectedOutput = node_path_1.default.resolve(harnessRoot, 'baseline', 'wiki');
95
+ if (node_path_1.default.resolve(outputRoot) !== expectedOutput) {
96
+ logger_1.logger.warn(`--output (${outputRoot}) 不等于 <harness>/baseline/wiki。` +
97
+ 'state 中 S10_wiki 的 tasks_file 路径可能不匹配实际产出位置。');
98
+ }
99
+ await (0, state_1.patchS10WikiPhase)(harnessRoot, {
100
+ wikiPhase: result.wikiPhase,
101
+ wikiSource: result.wikiSource,
102
+ });
103
+ }
104
+ }
105
+ // 9. Next-steps hint
106
+ if (result.ok) {
107
+ logger_1.logger.plain('');
108
+ if (opts.updateState && opts.harness) {
109
+ logger_1.logger.plain(picocolors_1.default.bold('下一步(harness 联动):'));
110
+ logger_1.logger.plain(' - 可执行 svharness doctor --harness <path> 校验 wiki 状态');
111
+ if (mode === 'tasks') {
112
+ logger_1.logger.plain(' - 可用 harness-build-skill-wiki-writer 按 TASKS.md 逐页补写');
113
+ }
114
+ }
115
+ else {
116
+ logger_1.logger.plain(picocolors_1.default.bold('下一步:'));
117
+ if (mode === 'tasks') {
118
+ logger_1.logger.plain(` - 编辑 ${node_path_1.default.join(outputRoot, 'TASKS.md')} 中的 wiki 页面任务`);
119
+ logger_1.logger.plain(' - 或使用 --generate-wiki 直接完整生成');
120
+ }
121
+ else {
122
+ logger_1.logger.plain(` - wiki 文件位于 ${outputRoot}`);
123
+ }
124
+ }
125
+ }
126
+ return result.ok ? 0 : 1;
127
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.saveConfigSection = exports.mergeDoctorOptions = exports.mergeConvertOptions = exports.mergeApplyOptions = exports.mergeBuildOptions = exports.resolveConfigHarnessName = exports.normalizeConfig = exports.resolveConfigPath = exports.loadConfig = exports.CONFIG_SCHEMA_VERSION = exports.DEFAULT_CONFIG_FILENAME = void 0;
3
+ exports.saveConfigSection = exports.mergeWikiOptions = exports.mergeDoctorOptions = exports.mergeConvertOptions = exports.mergeApplyOptions = exports.mergeBuildOptions = exports.resolveConfigHarnessName = exports.normalizeConfig = exports.resolveConfigPath = exports.loadConfig = exports.CONFIG_SCHEMA_VERSION = exports.DEFAULT_CONFIG_FILENAME = void 0;
4
4
  var constants_1 = require("./constants");
5
5
  Object.defineProperty(exports, "DEFAULT_CONFIG_FILENAME", { enumerable: true, get: function () { return constants_1.DEFAULT_CONFIG_FILENAME; } });
6
6
  Object.defineProperty(exports, "CONFIG_SCHEMA_VERSION", { enumerable: true, get: function () { return constants_1.CONFIG_SCHEMA_VERSION; } });
@@ -15,5 +15,6 @@ Object.defineProperty(exports, "mergeBuildOptions", { enumerable: true, get: fun
15
15
  Object.defineProperty(exports, "mergeApplyOptions", { enumerable: true, get: function () { return merge_options_1.mergeApplyOptions; } });
16
16
  Object.defineProperty(exports, "mergeConvertOptions", { enumerable: true, get: function () { return merge_options_1.mergeConvertOptions; } });
17
17
  Object.defineProperty(exports, "mergeDoctorOptions", { enumerable: true, get: function () { return merge_options_1.mergeDoctorOptions; } });
18
+ Object.defineProperty(exports, "mergeWikiOptions", { enumerable: true, get: function () { return merge_options_1.mergeWikiOptions; } });
18
19
  var save_config_1 = require("./save-config");
19
20
  Object.defineProperty(exports, "saveConfigSection", { enumerable: true, get: function () { return save_config_1.saveConfigSection; } });
@@ -4,6 +4,7 @@ exports.mergeBuildOptions = mergeBuildOptions;
4
4
  exports.mergeApplyOptions = mergeApplyOptions;
5
5
  exports.mergeConvertOptions = mergeConvertOptions;
6
6
  exports.mergeDoctorOptions = mergeDoctorOptions;
7
+ exports.mergeWikiOptions = mergeWikiOptions;
7
8
  const normalize_1 = require("./normalize");
8
9
  function getSource(cmd, key) {
9
10
  if (!cmd)
@@ -137,3 +138,21 @@ function mergeDoctorOptions(cli, configSection, defaults, cmd) {
137
138
  verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
138
139
  };
139
140
  }
141
+ function mergeWikiOptions(cli, configSection, defaults, cmd) {
142
+ const cfg = mergeSection(configSection, defaults);
143
+ return {
144
+ wikiSource: pickString('wikiSource', cli.wikiSource, cfg.wikiSource, cmd),
145
+ output: pickString('output', cli.output, cfg.output, cmd),
146
+ generateWiki: pickBool('generateWiki', cli.generateWiki, cfg.generateWiki, undefined, cmd),
147
+ wikiTasksOnly: pickBool('wikiTasksOnly', cli.wikiTasksOnly, cfg.wikiTasksOnly, undefined, cmd),
148
+ wikiLang: pickString('wikiLang', cli.wikiLang, cfg.wikiLang, cmd),
149
+ wikiModel: pickString('wikiModel', cli.wikiModel, cfg.wikiModel, cmd),
150
+ wikiBaseUrl: pickString('wikiBaseUrl', cli.wikiBaseUrl, cfg.wikiBaseUrl, cmd),
151
+ wikiApiKey: pickString('wikiApiKey', cli.wikiApiKey, cfg.wikiApiKey, cmd),
152
+ projectName: pickString('projectName', cli.projectName, cfg.projectName, cmd),
153
+ forceFull: pickBool('forceFull', cli.forceFull, cfg.forceFull, undefined, cmd),
154
+ harness: pickString('harness', cli.harness, cfg.harness, cmd),
155
+ updateState: pickBool('updateState', cli.updateState, cfg.updateState, undefined, cmd),
156
+ verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
157
+ };
158
+ }
@@ -129,6 +129,18 @@ function pickDefaults(raw) {
129
129
  }
130
130
  return s;
131
131
  }
132
+ function pickWikiSection(raw) {
133
+ const s = {};
134
+ for (const k of ['wikiSource', 'output', 'wikiLang', 'wikiModel', 'wikiBaseUrl', 'wikiApiKey', 'projectName', 'harness']) {
135
+ if (raw[k] !== undefined && raw[k] !== null)
136
+ s[k] = String(raw[k]).trim();
137
+ }
138
+ for (const k of ['generateWiki', 'wikiTasksOnly', 'forceFull', 'updateState', 'verbose']) {
139
+ if (raw[k] !== undefined)
140
+ s[k] = Boolean(raw[k]);
141
+ }
142
+ return s;
143
+ }
132
144
  /**
133
145
  * Parse and normalize raw YAML/JSON object into SvharnessConfig.
134
146
  */
@@ -165,5 +177,11 @@ function normalizeConfig(raw, configPath) {
165
177
  }
166
178
  cfg.convert = pickConvertSection(raw.convert);
167
179
  }
180
+ if (raw.wiki !== undefined) {
181
+ if (!isPlainObject(raw.wiki)) {
182
+ throw new Error(`配置 wiki 必须是对象:${configPath}`);
183
+ }
184
+ cfg.wiki = pickWikiSection(raw.wiki);
185
+ }
168
186
  return cfg;
169
187
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildInitialState = buildInitialState;
7
7
  exports.writeStateFile = writeStateFile;
8
+ exports.patchS10WikiPhase = patchS10WikiPhase;
8
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  const js_yaml_1 = __importDefault(require("js-yaml"));
@@ -130,3 +131,41 @@ async function writeStateFile(targetRoot, state) {
130
131
  logger_1.logger.debug(`wrote state file: ${outPath}`);
131
132
  return outPath;
132
133
  }
134
+ /**
135
+ * Patch `phases.S10_wiki` in an existing `.harness-build-state.yaml`.
136
+ *
137
+ * - Inserts or updates `S10_wiki` with the appropriate status/mode fields.
138
+ * - Does NOT modify `current_phase` or `next_action` to avoid disrupting
139
+ * a build that has already progressed past S10.
140
+ * - If the state file does not exist, logs a warning and returns without error.
141
+ */
142
+ async function patchS10WikiPhase(harnessRoot, input) {
143
+ const statePath = node_path_1.default.join(harnessRoot, '.harness-build-state.yaml');
144
+ if (!(await fs_extra_1.default.pathExists(statePath))) {
145
+ logger_1.logger.warn('--update-state 已跳过:未找到 .harness-build-state.yaml');
146
+ return;
147
+ }
148
+ const raw = await fs_extra_1.default.readFile(statePath, 'utf8');
149
+ // Strip comment-only header lines before parsing.
150
+ const body = raw.replace(/^#.*\n/gm, '');
151
+ const state = js_yaml_1.default.load(body);
152
+ const phases = (state.phases ?? {});
153
+ if (input.wikiPhase === 'done') {
154
+ phases.S10_wiki = { status: 'DONE', requires_agent: false, mode: 'full', source: 'cli-full' };
155
+ }
156
+ else {
157
+ phases.S10_wiki = {
158
+ status: 'PENDING',
159
+ requires_agent: true,
160
+ mode: 'outline-only',
161
+ source: input.wikiSource ?? 'cli-tasks',
162
+ tasks_file: 'baseline/wiki/TASKS.md',
163
+ };
164
+ }
165
+ state.phases = phases;
166
+ // Preserve the original header comment block.
167
+ const headerLines = raw.split('\n').filter((l) => l.startsWith('#')).join('\n') + '\n';
168
+ const newBody = js_yaml_1.default.dump(state, { lineWidth: 100, noRefs: true, sortKeys: false });
169
+ await fs_extra_1.default.outputFile(statePath, headerLines + newBody, 'utf8');
170
+ logger_1.logger.info(`已更新 S10_wiki → ${input.wikiPhase.toUpperCase()}(未修改 current_phase)`);
171
+ }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ const apply_1 = require("./commands/apply");
10
10
  const convert_1 = require("./commands/convert");
11
11
  const wizard_1 = require("./commands/wizard");
12
12
  const doctor_1 = require("./commands/doctor");
13
+ const wiki_1 = require("./commands/wiki");
13
14
  const requirements_1 = require("./commands/requirements");
14
15
  const start_agent_1 = require("./commands/start-agent");
15
16
  const shell_integration_1 = require("./commands/shell-integration");
@@ -120,6 +121,20 @@ async function runBuildAction(opts, cmd) {
120
121
  process.exitCode = 1;
121
122
  }
122
123
  }
124
+ /**
125
+ * Attach wiki-specific LLM and source options to a command.
126
+ * Shared by both `build` (for --baseline wiki generation) and standalone `wiki`.
127
+ */
128
+ function attachWikiOptions(cmd) {
129
+ return cmd
130
+ .option('--generate-wiki', '【Wiki】自动生成完整 Wiki 文档')
131
+ .option('--wiki-tasks-only', '【Wiki】仅生成 Wiki 大纲和任务列表(默认模式)')
132
+ .option('--wiki-lang <lang>', '【Wiki】输出语言:zh(默认)| en')
133
+ .option('--wiki-model <model>', '【Wiki】AI 模型名称')
134
+ .option('--wiki-base-url <url>', '【Wiki】OpenAI 兼容 API 地址')
135
+ .option('--wiki-api-key <key>', '【Wiki】API 密钥')
136
+ .option('--wiki-source <path>', '【Wiki】扫描的源码根路径');
137
+ }
123
138
  function attachBuildOptions(cmd) {
124
139
  return cmd
125
140
  .option('--config <path>', `从 YAML/JSON 读取参数(默认可选 ${config_1.DEFAULT_CONFIG_FILENAME})`)
@@ -149,13 +164,6 @@ function attachBuildOptions(cmd) {
149
164
  .option('--force', '强制覆盖已存在的目标目录')
150
165
  .option('-y, --yes', '跳过交互确认,使用默认值')
151
166
  .option('--verbose', '显示详细日志')
152
- .option('--generate-wiki', '【Wiki】自动生成完整 Wiki 文档(需 --baseline)')
153
- .option('--wiki-tasks-only', '【Wiki】仅生成 Wiki 大纲和任务列表(有 --baseline 时为隐式默认)')
154
- .option('--wiki-lang <lang>', '【Wiki】输出语言:zh(默认)| en')
155
- .option('--wiki-model <model>', '【Wiki】AI 模型名称')
156
- .option('--wiki-base-url <url>', '【Wiki】OpenAI 兼容 API 地址')
157
- .option('--wiki-api-key <key>', '【Wiki】API 密钥')
158
- .option('--wiki-source <path>', '【Wiki】扫描的源码根路径')
159
167
  .option('--enable-baseline-extract', '【可选】启用 baseline 自动提取 skills/rules(S61 ENABLED;默认 DISABLED)')
160
168
  .option('--strict-input-confirm', '【可选】禁用 CLI 预声明,恢复 S60/S61/S65 等阶段的 Agent 表单确认');
161
169
  }
@@ -176,7 +184,7 @@ function main() {
176
184
  '║ 为任意 Agent IDE 生成项目本地知识层骨架(harness) ',
177
185
  '╠══════════════════════════════════════════════════════════════════════════',
178
186
  '║ 支持架构:android-compose | android-xml | cpp | web-react | python ',
179
- '║ 支持 Agent:codechat | qoder | cursor | claude-code | opencode | generic ',
187
+ '║ 支持 Agent:codechat | qoder | cursor | claude-code | opencode | codex | generic',
180
188
  '║ 运行要求:Node.js ≥ 18 ',
181
189
  '╚══════════════════════════════════════════════════════════════════════════',
182
190
  '',
@@ -203,13 +211,16 @@ function main() {
203
211
  '│ # 方式五:文档转 Markdown ',
204
212
  '│ svharness convert --input ./docs/*.pdf --output ./docs/md --type requirements',
205
213
  '│ ',
214
+ '│ # 方式六:对当前工程生成 wiki 骨架 ',
215
+ '│ svharness wiki ',
216
+ '│ ',
206
217
  '└──────────────────────────────────────────────────────────────────────────',
207
218
  ' 📚 使用说明文档:https://yesv-desaysv.feishu.cn/docx/OEBwdywTfoncPNxPZ0acClNincg',
208
219
  '',
209
220
  ].join('\n'));
210
- attachBuildOptions(program
221
+ attachWikiOptions(attachBuildOptions(program
211
222
  .command('build')
212
- .description('构建 harness:生成目录骨架、渲染元文件、注入 harness-build skill 与构建规则。')).action(async (opts, cmd) => {
223
+ .description('构建 harness:生成目录骨架、渲染元文件、注入 harness-build skill 与构建规则。'))).action(async (opts, cmd) => {
213
224
  await runBuildAction(opts, cmd);
214
225
  });
215
226
  attachBuildOptions(program
@@ -336,6 +347,43 @@ function main() {
336
347
  process.exitCode = 1;
337
348
  }
338
349
  });
350
+ // Standalone wiki generation for any code project
351
+ attachWikiOptions(program
352
+ .command('wiki')
353
+ .description('对任意代码工程目录生成 baseline wiki(不依赖 harness)'))
354
+ .option('--output <path>', 'wiki 产物输出目录(默认 ./wiki)')
355
+ .option('--project-name <name>', '项目名(用于 TASKS.md 标题;默认取 wiki-source 的目录名)')
356
+ .option('--force-full', 'full 模式:忽略 checkpoint 强制重新生成')
357
+ .option('--harness <path>', '【可选】已有 harness 根目录(仅配合 --update-state 使用)')
358
+ .option('--update-state', '同步 S10_wiki 到 harness 的 .harness-build-state.yaml(须同时提供 --harness)')
359
+ .option('--config <path>', `从配置文件读取 wiki 节(默认 ${config_1.DEFAULT_CONFIG_FILENAME})`)
360
+ .option('--verbose', '显示详细日志')
361
+ .action(async (opts, cmd) => {
362
+ try {
363
+ const loaded = (0, config_1.loadConfig)({ configPath: opts.config });
364
+ const merged = (0, config_1.mergeWikiOptions)(opts, loaded?.config.wiki, loaded?.config.defaults, cmd);
365
+ const exitCode = await (0, wiki_1.runWiki)({
366
+ wikiSource: merged.wikiSource,
367
+ output: merged.output,
368
+ generateWiki: merged.generateWiki,
369
+ wikiTasksOnly: merged.wikiTasksOnly,
370
+ wikiLang: merged.wikiLang,
371
+ wikiModel: merged.wikiModel,
372
+ wikiBaseUrl: merged.wikiBaseUrl,
373
+ wikiApiKey: merged.wikiApiKey,
374
+ projectName: merged.projectName,
375
+ forceFull: merged.forceFull,
376
+ harness: merged.harness,
377
+ updateState: merged.updateState,
378
+ verbose: merged.verbose,
379
+ });
380
+ process.exitCode = exitCode;
381
+ }
382
+ catch (err) {
383
+ logger_1.logger.error(err.message);
384
+ process.exitCode = 1;
385
+ }
386
+ });
339
387
  const requirements = program
340
388
  .command('requirements')
341
389
  .description('S40 需求条目化:结构扫描与 verify 门禁');
@@ -13,6 +13,7 @@ const os_1 = __importDefault(require("os"));
13
13
  const child_process_1 = require("child_process");
14
14
  const logger_1 = require("../utils/logger");
15
15
  const validate_args_1 = require("../utils/validate-args");
16
+ const codechat_runner_resolver_1 = require("./codechat-runner-resolver");
16
17
  const SUPPORTED_START_AGENTS = ['codechat'];
17
18
  const DEFAULT_ENV_TEMPLATE = 'default-claude.env';
18
19
  function homePath(...segments) {
@@ -54,23 +55,21 @@ async function verifyUserEnvWritten(userEnvPath) {
54
55
  throw new Error(`env 写入后校验失败(文件不存在或为空):${userEnvPath}\n` +
55
56
  `请重试 svharness start-agent,或手动创建该文件。`);
56
57
  }
57
- function getCodechatRunnerConfig() {
58
+ function getCodechatEnvConfig() {
58
59
  const platform = process.platform;
59
- if (platform === 'win32') {
60
+ if (platform === 'win32' || platform === 'linux' || platform === 'darwin') {
60
61
  return {
61
- runner: homePath('.codechat', 'cli_app', 'run.bat'),
62
62
  envFile: homePath('.claude', '.env'),
63
63
  envSyncFromRel: path_1.default.join('.claude', '.env'),
64
64
  };
65
65
  }
66
- if (platform === 'linux' || platform === 'darwin') {
67
- return {
68
- runner: homePath('.codechat', 'cli_app', 'run.sh'),
69
- envFile: homePath('.claude', '.env'),
70
- envSyncFromRel: path_1.default.join('.claude', '.env'),
71
- };
66
+ throw new Error(`当前平台 "${platform}" 暂不支持 start-agent codechat。支持:win32、linuxdarwin。`);
67
+ }
68
+ function formatRunnerSourceLog(source, label) {
69
+ if (label) {
70
+ return `${source}: ${label}`;
72
71
  }
73
- throw new Error(`当前平台 "${platform}" 暂不支持 start-agent codechat。支持:win32、linux。`);
72
+ return source;
74
73
  }
75
74
  function normalizeComparable(p) {
76
75
  return path_1.default.resolve(p).replace(/\\/g, '/').toLowerCase();
@@ -206,14 +205,12 @@ async function runStartAgent(opts) {
206
205
  workDir: opts.workDir,
207
206
  positionalWorkdir: opts.positionalWorkdir,
208
207
  });
209
- const config = getCodechatRunnerConfig();
208
+ const config = getCodechatEnvConfig();
210
209
  const syncEnv = opts.syncEnv !== false;
211
210
  const skipPermissions = opts.skipPermissions !== false;
212
211
  await ensureUserEnv(workdir, config, syncEnv);
213
212
  const userEnvFile = resolveUserEnvPath(config);
214
- if (!(await fs_extra_1.default.pathExists(config.runner))) {
215
- throw new Error(`未找到 CodeChat CLI:${config.runner}\n请先安装 CodeChat CLI 到用户目录。`);
216
- }
213
+ const { runner, source, label } = (0, codechat_runner_resolver_1.resolveCodechatRunner)();
217
214
  if (!(await fs_extra_1.default.pathExists(userEnvFile))) {
218
215
  throw new Error(`未找到 env 文件:${userEnvFile}\n` +
219
216
  `请提供 ${path_1.default.join(workdir, config.envSyncFromRel)},或确保内置模板 templates/default-claude.env 可访问。`);
@@ -221,14 +218,15 @@ async function runStartAgent(opts) {
221
218
  await verifyUserEnvWritten(userEnvFile);
222
219
  warnIfNotHarnessProject(workdir);
223
220
  const args = buildRunnerArgs(config, workdir, skipPermissions);
221
+ logger_1.logger.info(`CodeChat CLI: ${runner} (${formatRunnerSourceLog(source, label)})`);
224
222
  logger_1.logger.info(`启动 CodeChat Agent(workdir: ${workdir})`);
225
223
  const result = process.platform === 'win32'
226
- ? (0, child_process_1.spawnSync)(config.runner, args, {
224
+ ? (0, child_process_1.spawnSync)(runner, args, {
227
225
  cwd: workdir,
228
226
  stdio: 'inherit',
229
227
  shell: true,
230
228
  })
231
- : (0, child_process_1.spawnSync)(config.runner, args, {
229
+ : (0, child_process_1.spawnSync)(runner, args, {
232
230
  cwd: workdir,
233
231
  stdio: 'inherit',
234
232
  shell: false,
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CODECHAT_CLI_RUNNER_ENV = void 0;
7
+ exports.getRunnerFileName = getRunnerFileName;
8
+ exports.getStandaloneRunnerPath = getStandaloneRunnerPath;
9
+ exports.parseIdeVersion = parseIdeVersion;
10
+ exports.compareVersionDesc = compareVersionDesc;
11
+ exports.compareIdePluginCandidates = compareIdePluginCandidates;
12
+ exports.getDefaultIdeScanRoots = getDefaultIdeScanRoots;
13
+ exports.scanIdePluginRunners = scanIdePluginRunners;
14
+ exports.resolveCodechatRunner = resolveCodechatRunner;
15
+ const fs_extra_1 = __importDefault(require("fs-extra"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const os_1 = __importDefault(require("os"));
18
+ exports.CODECHAT_CLI_RUNNER_ENV = 'CODECHAT_CLI_RUNNER';
19
+ function canAccessExecutable(filePath) {
20
+ try {
21
+ fs_extra_1.default.accessSync(filePath, fs_extra_1.default.constants.F_OK);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ function homePath(homedir, ...segments) {
29
+ return path_1.default.join(homedir, ...segments);
30
+ }
31
+ /** Runner script file name for the current platform. */
32
+ function getRunnerFileName(platform = process.platform) {
33
+ return platform === 'win32' ? 'run.bat' : 'run.sh';
34
+ }
35
+ /** Standalone CodeChat CLI install path (~/.codechat/cli_app/run.*). */
36
+ function getStandaloneRunnerPath(homedir = os_1.default.homedir(), platform = process.platform) {
37
+ return homePath(homedir, '.codechat', 'cli_app', getRunnerFileName(platform));
38
+ }
39
+ /**
40
+ * Parse version segments from IDE install dir name (e.g. AndroidStudio2024.2 → [2024, 2]).
41
+ */
42
+ function parseIdeVersion(installDirName, dirPrefix) {
43
+ if (!installDirName.startsWith(dirPrefix)) {
44
+ return [0];
45
+ }
46
+ const rest = installDirName.slice(dirPrefix.length);
47
+ const match = rest.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
48
+ if (!match) {
49
+ return [0];
50
+ }
51
+ return [match[1], match[2], match[3]]
52
+ .filter((part) => part !== undefined && part !== '')
53
+ .map(Number);
54
+ }
55
+ function formatIdeLabel(labelPrefix, installDirName, dirPrefix) {
56
+ const versionPart = installDirName.slice(dirPrefix.length);
57
+ return versionPart ? `${labelPrefix} ${versionPart}` : labelPrefix;
58
+ }
59
+ /** Compare version arrays lexicographically (descending — higher version first). */
60
+ function compareVersionDesc(a, b) {
61
+ const len = Math.max(a.length, b.length);
62
+ for (let i = 0; i < len; i++) {
63
+ const av = a[i] ?? 0;
64
+ const bv = b[i] ?? 0;
65
+ if (av !== bv) {
66
+ return bv - av;
67
+ }
68
+ }
69
+ return 0;
70
+ }
71
+ /**
72
+ * Compare two IDE plugin candidates — higher version first, then newer mtime.
73
+ */
74
+ function compareIdePluginCandidates(a, b) {
75
+ const versionCmp = compareVersionDesc(a.version, b.version);
76
+ if (versionCmp !== 0) {
77
+ return versionCmp;
78
+ }
79
+ return b.mtimeMs - a.mtimeMs;
80
+ }
81
+ /** Default IDE plugin scan roots for the current platform. */
82
+ function getDefaultIdeScanRoots(platform = process.platform, homedir = os_1.default.homedir(), appData = process.env.APPDATA) {
83
+ if (platform === 'win32') {
84
+ const roaming = appData ?? homePath(homedir, 'AppData', 'Roaming');
85
+ return [
86
+ { root: path_1.default.join(roaming, 'Google'), dirPrefix: 'AndroidStudio', labelPrefix: 'Android Studio' },
87
+ { root: path_1.default.join(roaming, 'JetBrains'), dirPrefix: 'IntelliJIdea', labelPrefix: 'IntelliJ IDEA' },
88
+ { root: path_1.default.join(roaming, 'JetBrains'), dirPrefix: 'IdeaIC', labelPrefix: 'IntelliJ IDEA Community' },
89
+ ];
90
+ }
91
+ if (platform === 'darwin') {
92
+ const appSupport = homePath(homedir, 'Library', 'Application Support');
93
+ return [
94
+ { root: path_1.default.join(appSupport, 'Google'), dirPrefix: 'AndroidStudio', labelPrefix: 'Android Studio' },
95
+ { root: path_1.default.join(appSupport, 'JetBrains'), dirPrefix: 'IntelliJIdea', labelPrefix: 'IntelliJ IDEA' },
96
+ { root: path_1.default.join(appSupport, 'JetBrains'), dirPrefix: 'IdeaIC', labelPrefix: 'IntelliJ IDEA Community' },
97
+ ];
98
+ }
99
+ // linux and others
100
+ const localShare = homePath(homedir, '.local', 'share');
101
+ return [
102
+ { root: path_1.default.join(localShare, 'JetBrains'), dirPrefix: 'IntelliJIdea', labelPrefix: 'IntelliJ IDEA' },
103
+ { root: path_1.default.join(localShare, 'JetBrains'), dirPrefix: 'IdeaIC', labelPrefix: 'IntelliJ IDEA Community' },
104
+ ];
105
+ }
106
+ /**
107
+ * Scan IDE plugin directories for CodeChat CLI runner scripts.
108
+ */
109
+ function scanIdePluginRunners(options = {}) {
110
+ const platform = options.platform ?? process.platform;
111
+ const runnerFileName = getRunnerFileName(platform);
112
+ const scanRoots = options.ideScanRoots ??
113
+ getDefaultIdeScanRoots(platform, options.homedir ?? os_1.default.homedir(), options.appData);
114
+ const candidates = [];
115
+ for (const { root, dirPrefix, labelPrefix } of scanRoots) {
116
+ if (!fs_extra_1.default.existsSync(root)) {
117
+ continue;
118
+ }
119
+ let entries;
120
+ try {
121
+ entries = fs_extra_1.default.readdirSync(root);
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ for (const entry of entries) {
127
+ if (!entry.startsWith(dirPrefix)) {
128
+ continue;
129
+ }
130
+ const runner = path_1.default.join(root, entry, 'plugins', 'CodeChat', 'cli', runnerFileName);
131
+ if (!canAccessExecutable(runner)) {
132
+ continue;
133
+ }
134
+ let mtimeMs = 0;
135
+ try {
136
+ mtimeMs = fs_extra_1.default.statSync(runner).mtimeMs;
137
+ }
138
+ catch {
139
+ // keep 0
140
+ }
141
+ candidates.push({
142
+ runner: path_1.default.resolve(runner),
143
+ installDirName: entry,
144
+ label: formatIdeLabel(labelPrefix, entry, dirPrefix),
145
+ version: parseIdeVersion(entry, dirPrefix),
146
+ mtimeMs,
147
+ });
148
+ }
149
+ }
150
+ return candidates.sort(compareIdePluginCandidates);
151
+ }
152
+ function pickBestIdeCandidate(candidates) {
153
+ if (candidates.length === 0) {
154
+ return null;
155
+ }
156
+ return candidates[0];
157
+ }
158
+ function buildRunnerNotFoundError(standalonePath, envOverride, scanRoots, platform) {
159
+ const runnerFile = getRunnerFileName(platform);
160
+ const lines = [
161
+ `未找到 CodeChat CLI(${runnerFile})。已尝试:`,
162
+ ` 1. 环境变量 ${exports.CODECHAT_CLI_RUNNER_ENV}${envOverride ? ` → ${envOverride}` : '(未设置)'}`,
163
+ ` 2. 独立安装 → ${standalonePath}`,
164
+ ' 3. IDE 插件目录扫描:',
165
+ ];
166
+ for (const { root, dirPrefix } of scanRoots) {
167
+ lines.push(` ${path_1.default.join(root, `${dirPrefix}*`, 'plugins', 'CodeChat', 'cli', runnerFile)}`);
168
+ }
169
+ lines.push('', '请安装 CodeChat CLI 到 ~/.codechat/cli_app/,或在 Android Studio / IntelliJ IDEA 中安装 CodeChat 插件,', `或设置 ${exports.CODECHAT_CLI_RUNNER_ENV} 指向 run.bat / run.sh 的绝对路径。`);
170
+ return lines.join('\n');
171
+ }
172
+ /**
173
+ * Resolve CodeChat CLI runner with fallback: env → standalone → IDE plugin scan.
174
+ */
175
+ function resolveCodechatRunner(options = {}) {
176
+ const platform = options.platform ?? process.platform;
177
+ const homedir = options.homedir ?? os_1.default.homedir();
178
+ const envRunner = (options.envRunner ?? process.env[exports.CODECHAT_CLI_RUNNER_ENV])?.trim();
179
+ const standalonePath = getStandaloneRunnerPath(homedir, platform);
180
+ const scanRoots = options.ideScanRoots ??
181
+ getDefaultIdeScanRoots(platform, homedir, options.appData);
182
+ if (envRunner) {
183
+ if (!canAccessExecutable(envRunner)) {
184
+ throw new Error(`${exports.CODECHAT_CLI_RUNNER_ENV} 指向的路径不可访问:${envRunner}`);
185
+ }
186
+ return {
187
+ runner: path_1.default.resolve(envRunner),
188
+ source: 'env',
189
+ label: exports.CODECHAT_CLI_RUNNER_ENV,
190
+ };
191
+ }
192
+ if (canAccessExecutable(standalonePath)) {
193
+ return {
194
+ runner: path_1.default.resolve(standalonePath),
195
+ source: 'standalone',
196
+ };
197
+ }
198
+ const ideCandidates = scanIdePluginRunners(options);
199
+ const best = pickBestIdeCandidate(ideCandidates);
200
+ if (best) {
201
+ return {
202
+ runner: best.runner,
203
+ source: 'ide-plugin',
204
+ label: best.label,
205
+ };
206
+ }
207
+ throw new Error(buildRunnerNotFoundError(standalonePath, envRunner, scanRoots, platform));
208
+ }