@brawnen/agent-harness-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,711 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { DEFAULT_RUNTIME_DIR, defaultRuntimeRelativePath } from "../lib/runtime-paths.js";
7
+
8
+ const CLI_VERSION = "0.1.0";
9
+ const RULE_MODES = new Set(["base", "full"]);
10
+ const HOSTS = new Set(["auto", "claude-code", "codex", "gemini-cli"]);
11
+ const MODES = new Set(["delivery", "explore", "poc"]);
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const CLI_ROOT = path.resolve(__dirname, "../..");
16
+ const require = createRequire(import.meta.url);
17
+ const PROTOCOL_ROOT = resolveProtocolRoot();
18
+
19
+ function resolveProtocolRoot() {
20
+ try {
21
+ const protocolPackageJson = require.resolve("@brawnen/agent-harness-protocol/package.json");
22
+ return path.dirname(protocolPackageJson);
23
+ } catch {
24
+ return path.resolve(CLI_ROOT, "../protocol");
25
+ }
26
+ }
27
+
28
+ export function runInit(argv) {
29
+ const parsed = parseInitArgs(argv);
30
+ if (!parsed.ok) {
31
+ console.error(parsed.error);
32
+ return 1;
33
+ }
34
+
35
+ const cwd = process.cwd();
36
+ const project = detectProject(cwd);
37
+ const hosts = resolveHosts(cwd, parsed.options.host);
38
+ const actions = [];
39
+
40
+ queueInitActions({
41
+ cwd,
42
+ project,
43
+ hosts,
44
+ options: parsed.options,
45
+ actions
46
+ });
47
+
48
+ printPlan(actions, parsed.options.dryRun, cwd, project, hosts);
49
+
50
+ if (parsed.options.dryRun) {
51
+ return 0;
52
+ }
53
+
54
+ for (const action of actions) {
55
+ if (action.skip) {
56
+ continue;
57
+ }
58
+
59
+ action.run();
60
+ }
61
+
62
+ console.log("");
63
+ console.log("init 完成。");
64
+ return 0;
65
+ }
66
+
67
+ function parseInitArgs(argv) {
68
+ const options = {
69
+ dryRun: false,
70
+ force: false,
71
+ host: "auto",
72
+ mode: "delivery",
73
+ protocolOnly: false,
74
+ rules: "full",
75
+ yes: false
76
+ };
77
+
78
+ for (let index = 0; index < argv.length; index += 1) {
79
+ const arg = argv[index];
80
+
81
+ if (arg === "--dry-run") {
82
+ options.dryRun = true;
83
+ continue;
84
+ }
85
+
86
+ if (arg === "--force") {
87
+ options.force = true;
88
+ continue;
89
+ }
90
+
91
+ if (arg === "--protocol-only") {
92
+ options.protocolOnly = true;
93
+ continue;
94
+ }
95
+
96
+ if (arg === "--yes") {
97
+ options.yes = true;
98
+ continue;
99
+ }
100
+
101
+ if (arg === "--host") {
102
+ const value = argv[index + 1];
103
+ if (!HOSTS.has(value)) {
104
+ return { ok: false, error: "无效的 --host 参数。可选值: auto, claude-code, codex, gemini-cli" };
105
+ }
106
+
107
+ options.host = value;
108
+ index += 1;
109
+ continue;
110
+ }
111
+
112
+ if (arg === "--rules") {
113
+ const value = argv[index + 1];
114
+ if (!RULE_MODES.has(value)) {
115
+ return { ok: false, error: "无效的 --rules 参数。可选值: base, full" };
116
+ }
117
+
118
+ options.rules = value;
119
+ index += 1;
120
+ continue;
121
+ }
122
+
123
+ if (arg === "--mode") {
124
+ const value = argv[index + 1];
125
+ if (!MODES.has(value)) {
126
+ return { ok: false, error: "无效的 --mode 参数。可选值: delivery, explore, poc" };
127
+ }
128
+
129
+ options.mode = value;
130
+ index += 1;
131
+ continue;
132
+ }
133
+
134
+ return { ok: false, error: `未知参数: ${arg}` };
135
+ }
136
+
137
+ return { ok: true, options };
138
+ }
139
+
140
+ function detectProject(cwd) {
141
+ const packageJsonPath = path.join(cwd, "package.json");
142
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
143
+ const pnpmWorkspacePath = path.join(cwd, "pnpm-workspace.yaml");
144
+ const goModPath = path.join(cwd, "go.mod");
145
+ const cargoPath = path.join(cwd, "Cargo.toml");
146
+ const pomPath = path.join(cwd, "pom.xml");
147
+ const gradlePath = path.join(cwd, "build.gradle");
148
+ const gradleKtsPath = path.join(cwd, "build.gradle.kts");
149
+ const pyprojectPath = path.join(cwd, "pyproject.toml");
150
+ const requirementsPath = path.join(cwd, "requirements.txt");
151
+
152
+ const packageJson = readJsonIfExists(packageJsonPath);
153
+ const hasPackageJson = Boolean(packageJson);
154
+ const hasTsconfig = fs.existsSync(tsconfigPath);
155
+ const hasWorkspace = fs.existsSync(pnpmWorkspacePath) || Boolean(packageJson?.workspaces) || fs.existsSync(path.join(cwd, "packages"));
156
+ const hasNext = existsAny(cwd, ["next.config.js", "next.config.mjs", "next.config.cjs", "next.config.ts"]);
157
+ const hasFrontendHints = existsAny(cwd, [
158
+ "vite.config.js",
159
+ "vite.config.mjs",
160
+ "vite.config.ts",
161
+ "src/main.ts",
162
+ "src/main.js",
163
+ "src/App.tsx",
164
+ "src/App.jsx",
165
+ "index.html"
166
+ ]);
167
+
168
+ let projectType = "other";
169
+ const languages = new Set();
170
+
171
+ if (hasWorkspace) {
172
+ projectType = "monorepo";
173
+ } else if (hasNext) {
174
+ projectType = "fullstack";
175
+ } else if (hasPackageJson && hasFrontendHints) {
176
+ projectType = "frontend";
177
+ } else if (hasPackageJson) {
178
+ projectType = "library";
179
+ } else if (fs.existsSync(goModPath) || fs.existsSync(cargoPath) || fs.existsSync(pomPath) || fs.existsSync(gradlePath) || fs.existsSync(gradleKtsPath) || fs.existsSync(pyprojectPath) || fs.existsSync(requirementsPath)) {
180
+ projectType = "backend";
181
+ }
182
+
183
+ if (hasPackageJson) {
184
+ languages.add("javascript");
185
+ languages.add("json");
186
+ }
187
+ if (hasTsconfig) {
188
+ languages.add("typescript");
189
+ }
190
+ if (fs.existsSync(goModPath)) {
191
+ languages.add("go");
192
+ }
193
+ if (fs.existsSync(cargoPath)) {
194
+ languages.add("rust");
195
+ languages.add("toml");
196
+ }
197
+ if (fs.existsSync(pomPath) || fs.existsSync(gradlePath) || fs.existsSync(gradleKtsPath)) {
198
+ languages.add("java");
199
+ }
200
+ if (fs.existsSync(pyprojectPath) || fs.existsSync(requirementsPath)) {
201
+ languages.add("python");
202
+ }
203
+ if (languages.size === 0) {
204
+ languages.add("markdown");
205
+ }
206
+
207
+ return {
208
+ defaultCommands: inferDefaultCommands({
209
+ packageJson,
210
+ hasTsconfig,
211
+ hasGo: fs.existsSync(goModPath),
212
+ hasRust: fs.existsSync(cargoPath),
213
+ hasMaven: fs.existsSync(pomPath),
214
+ hasGradle: fs.existsSync(gradlePath) || fs.existsSync(gradleKtsPath),
215
+ hasPython: fs.existsSync(pyprojectPath) || fs.existsSync(requirementsPath)
216
+ }),
217
+ languages: [...languages],
218
+ projectName: path.basename(cwd),
219
+ projectType
220
+ };
221
+ }
222
+
223
+ function inferDefaultCommands(context) {
224
+ const commands = {};
225
+ const scripts = context.packageJson?.scripts ?? {};
226
+
227
+ if (scripts["type-check"]) {
228
+ commands.type_check = "npm run type-check";
229
+ } else if (context.hasTsconfig) {
230
+ commands.type_check = "npx tsc --noEmit";
231
+ }
232
+
233
+ if (scripts.lint) {
234
+ commands.lint = "npm run lint";
235
+ }
236
+
237
+ if (scripts.test) {
238
+ commands.test = "npm test";
239
+ } else if (context.hasGo) {
240
+ commands.test = "go test ./...";
241
+ }
242
+
243
+ if (scripts.build) {
244
+ commands.build = "npm run build";
245
+ } else if (context.hasGo) {
246
+ commands.build = "go build ./...";
247
+ } else if (context.hasRust) {
248
+ commands.build = "cargo check";
249
+ } else if (context.hasMaven) {
250
+ commands.build = "mvn compile";
251
+ } else if (context.hasGradle) {
252
+ commands.build = "./gradlew compileJava";
253
+ } else if (context.hasPython) {
254
+ commands.build = "python -m compileall .";
255
+ }
256
+
257
+ return commands;
258
+ }
259
+
260
+ function resolveHosts(cwd, explicitHost) {
261
+ if (explicitHost !== "auto") {
262
+ return [explicitHost];
263
+ }
264
+
265
+ const detected = [];
266
+ if (fs.existsSync(path.join(cwd, "CLAUDE.md")) || fs.existsSync(path.join(cwd, ".claude"))) {
267
+ detected.push("claude-code");
268
+ }
269
+ if (fs.existsSync(path.join(cwd, "AGENTS.md"))) {
270
+ detected.push("codex");
271
+ }
272
+ if (fs.existsSync(path.join(cwd, "GEMINI.md"))) {
273
+ detected.push("gemini-cli");
274
+ }
275
+
276
+ return detected.length > 0 ? detected : ["claude-code", "codex", "gemini-cli"];
277
+ }
278
+
279
+ function queueInitActions(context) {
280
+ const { actions, cwd, hosts, options, project } = context;
281
+ const ruleText = readText(path.join(PROTOCOL_ROOT, "rules", `${options.rules}.md`));
282
+
283
+ queueWriteAction({
284
+ actions,
285
+ content: renderHarnessConfig(project, options.mode),
286
+ force: options.force,
287
+ pathName: path.join(cwd, "harness.yaml")
288
+ });
289
+
290
+ queueProtocolTemplates(actions, cwd, options.force);
291
+ queueRulesInjection(actions, cwd, hosts, options, ruleText);
292
+
293
+ if (!options.protocolOnly) {
294
+ queueRuntimeFiles(actions, cwd);
295
+ if (hosts.includes("claude-code")) {
296
+ queueClaudeSettingsMerge(actions, cwd);
297
+ }
298
+ }
299
+ }
300
+
301
+ function queueProtocolTemplates(actions, cwd, force) {
302
+ const templateDir = path.join(PROTOCOL_ROOT, "templates");
303
+ const targetDir = path.join(cwd, DEFAULT_RUNTIME_DIR, "tasks");
304
+ const templateFiles = fs.readdirSync(templateDir).filter((file) => file.endsWith(".md"));
305
+
306
+ for (const file of templateFiles) {
307
+ const source = path.join(templateDir, file);
308
+ const target = path.join(targetDir, file);
309
+
310
+ queueWriteAction({
311
+ actions,
312
+ content: readText(source),
313
+ force,
314
+ pathName: target
315
+ });
316
+ }
317
+ }
318
+
319
+ function queueRulesInjection(actions, cwd, hosts, options, ruleText) {
320
+ const hostFiles = {
321
+ "claude-code": "CLAUDE.md",
322
+ codex: "AGENTS.md",
323
+ "gemini-cli": "GEMINI.md"
324
+ };
325
+
326
+ for (const host of hosts) {
327
+ const targetPath = path.join(cwd, hostFiles[host]);
328
+ const block = buildRulesBlock(ruleText, options.rules);
329
+ const existing = fs.existsSync(targetPath) ? readText(targetPath) : "";
330
+ const hasMarker = containsManagedBlock(existing);
331
+ const nextContent = buildInjectedContent(existing, block, options.force);
332
+
333
+ actions.push({
334
+ description: describeRuleAction(targetPath, hasMarker, options.force),
335
+ relativePath: path.relative(cwd, targetPath),
336
+ run: () => {
337
+ ensureDirectory(path.dirname(targetPath));
338
+ fs.writeFileSync(targetPath, nextContent, "utf8");
339
+ },
340
+ skip: hasMarker && !options.force
341
+ });
342
+ }
343
+ }
344
+
345
+ function queueClaudeSettingsMerge(actions, cwd) {
346
+ const targetPath = path.join(cwd, ".claude", "settings.json");
347
+ const templatePath = path.join(PROTOCOL_ROOT, "adapters", "claude-code", "hooks.json");
348
+ const template = JSON.parse(readText(templatePath));
349
+ const existing = fs.existsSync(targetPath) ? readJson(targetPath) : {};
350
+ const merged = mergeClaudeSettings(existing, template);
351
+ const content = `${JSON.stringify(merged, null, 2)}\n`;
352
+
353
+ actions.push({
354
+ description: fs.existsSync(targetPath) ? "合并 Claude Code hooks" : "创建 Claude Code hooks 配置",
355
+ relativePath: path.relative(cwd, targetPath),
356
+ run: () => {
357
+ ensureDirectory(path.dirname(targetPath));
358
+ fs.writeFileSync(targetPath, content, "utf8");
359
+ },
360
+ skip: false
361
+ });
362
+ }
363
+
364
+ function queueRuntimeFiles(actions, cwd) {
365
+ const runtimeReadme = path.join(cwd, DEFAULT_RUNTIME_DIR, "README.md");
366
+ queueWriteAction({
367
+ actions,
368
+ content: buildRuntimeReadme(),
369
+ force: false,
370
+ pathName: runtimeReadme
371
+ });
372
+
373
+ for (const file of [
374
+ defaultRuntimeRelativePath("state", "tasks", ".gitkeep"),
375
+ defaultRuntimeRelativePath("audit", ".gitkeep"),
376
+ defaultRuntimeRelativePath("reports", ".gitkeep")
377
+ ]) {
378
+ const targetPath = path.join(cwd, file);
379
+ actions.push({
380
+ description: "创建运行时目录占位",
381
+ relativePath: path.relative(cwd, targetPath),
382
+ run: () => {
383
+ ensureDirectory(path.dirname(targetPath));
384
+ if (!fs.existsSync(targetPath)) {
385
+ fs.writeFileSync(targetPath, "", "utf8");
386
+ }
387
+ },
388
+ skip: false
389
+ });
390
+ }
391
+
392
+ queueGitignoreUpdate(actions, cwd);
393
+ }
394
+
395
+ function queueGitignoreUpdate(actions, cwd) {
396
+ const targetPath = path.join(cwd, ".gitignore");
397
+ const entries = [
398
+ "# agent-harness runtime",
399
+ defaultRuntimeRelativePath("state") + "/",
400
+ defaultRuntimeRelativePath("audit") + "/",
401
+ defaultRuntimeRelativePath("reports") + "/"
402
+ ];
403
+ const existing = fs.existsSync(targetPath) ? readText(targetPath) : "";
404
+ const nextContent = mergeGitignore(existing, entries);
405
+
406
+ actions.push({
407
+ description: fs.existsSync(targetPath) ? "更新 .gitignore" : "创建 .gitignore",
408
+ relativePath: path.relative(cwd, targetPath),
409
+ run: () => {
410
+ fs.writeFileSync(targetPath, nextContent, "utf8");
411
+ },
412
+ skip: nextContent === existing
413
+ });
414
+ }
415
+
416
+ function queueWriteAction({ actions, content, force, pathName }) {
417
+ const exists = fs.existsSync(pathName);
418
+ actions.push({
419
+ description: exists ? (force ? "覆盖文件" : "保留现有文件") : "创建文件",
420
+ relativePath: path.relative(process.cwd(), pathName),
421
+ run: () => {
422
+ ensureDirectory(path.dirname(pathName));
423
+ if (!exists || force) {
424
+ fs.writeFileSync(pathName, content, "utf8");
425
+ }
426
+ },
427
+ skip: exists && !force
428
+ });
429
+ }
430
+
431
+ function renderHarnessConfig(project, mode) {
432
+ const config = {
433
+ version: "0.3",
434
+ project_name: path.basename(process.cwd()),
435
+ project_type: project.projectType,
436
+ default_mode: mode,
437
+ allowed_paths: ["**"],
438
+ protected_paths: [".git/**", ".idea/**"],
439
+ default_commands: project.defaultCommands,
440
+ risk_rules: {
441
+ high: {
442
+ path_matches: ["harness.yaml"],
443
+ requires_confirmation: true,
444
+ minimum_evidence: ["diff_summary", "manual_confirmation"],
445
+ reason: "项目协议配置"
446
+ },
447
+ medium: {
448
+ path_matches: ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".harness/**"],
449
+ requires_confirmation: false,
450
+ minimum_evidence: ["diff_summary"],
451
+ reason: "宿主规则与 agent-harness 运行目录"
452
+ },
453
+ low: {
454
+ path_matches: ["docs/**"],
455
+ requires_confirmation: false,
456
+ minimum_evidence: ["diff_summary"],
457
+ reason: "普通文档修改"
458
+ }
459
+ },
460
+ languages: project.languages,
461
+ task_templates: {
462
+ bug: ".harness/tasks/bug.md",
463
+ feature: ".harness/tasks/feature.md",
464
+ explore: ".harness/tasks/explore.md"
465
+ },
466
+ delivery_policy: {
467
+ commit: {
468
+ mode: "explicit_only",
469
+ via: "skill",
470
+ require: ["verify_passed", "report_generated"]
471
+ },
472
+ push: {
473
+ mode: "explicit_only",
474
+ via: "manual",
475
+ require: ["commit_exists"]
476
+ }
477
+ },
478
+ workflow_policy: {
479
+ default_mode: "full",
480
+ lite_allowed_if: {
481
+ single_file: true,
482
+ low_risk: true,
483
+ docs_only: true,
484
+ no_behavior_change: true,
485
+ no_policy_change: true,
486
+ no_output_artifacts: true
487
+ },
488
+ force_full_if: {
489
+ intents: ["bug", "feature", "refactor"],
490
+ multi_file_scope: true,
491
+ config_changed: true,
492
+ protocol_changed: true,
493
+ host_adapter_changed: true,
494
+ output_artifact_required: true,
495
+ high_risk: true,
496
+ override_used: true
497
+ },
498
+ enforcement: {
499
+ mode: "recommend",
500
+ upgrade_only: true
501
+ }
502
+ },
503
+ output_policy: {
504
+ report: {
505
+ required: true,
506
+ format: "json",
507
+ directory: ".harness/reports",
508
+ required_sections: [
509
+ "task_conclusion",
510
+ "actual_scope",
511
+ "verification_evidence",
512
+ "remaining_risks",
513
+ "next_steps"
514
+ ]
515
+ }
516
+ }
517
+ };
518
+
519
+ return `${toYaml(config)}\n`;
520
+ }
521
+
522
+ function toYaml(value, indent = 0) {
523
+ const pad = " ".repeat(indent);
524
+
525
+ if (Array.isArray(value)) {
526
+ return value
527
+ .map((item) => {
528
+ if (isScalar(item)) {
529
+ return `${pad}- ${formatScalar(item)}`;
530
+ }
531
+
532
+ const nested = toYaml(item, indent + 1);
533
+ const lines = nested.split("\n");
534
+ return `${pad}- ${lines[0].trimStart()}\n${lines.slice(1).join("\n")}`;
535
+ })
536
+ .join("\n");
537
+ }
538
+
539
+ return Object.entries(value)
540
+ .map(([key, entry]) => {
541
+ if (isScalar(entry)) {
542
+ return `${pad}${key}: ${formatScalar(entry)}`;
543
+ }
544
+
545
+ if (Array.isArray(entry) && entry.length === 0) {
546
+ return `${pad}${key}: []`;
547
+ }
548
+
549
+ if (!Array.isArray(entry) && Object.keys(entry).length === 0) {
550
+ return `${pad}${key}: {}`;
551
+ }
552
+
553
+ return `${pad}${key}:\n${toYaml(entry, indent + 1)}`;
554
+ })
555
+ .join("\n");
556
+ }
557
+
558
+ function buildRulesBlock(ruleText, rulesMode) {
559
+ return [
560
+ `<!-- agent-harness:start version="${CLI_VERSION}" rules="${rulesMode}" -->`,
561
+ ruleText.trim(),
562
+ "<!-- agent-harness:end -->",
563
+ ""
564
+ ].join("\n");
565
+ }
566
+
567
+ function buildInjectedContent(existing, block, force) {
568
+ if (!existing.trim()) {
569
+ return block;
570
+ }
571
+
572
+ if (!containsManagedBlock(existing)) {
573
+ return `${existing.replace(/\s*$/, "")}\n\n${block}`;
574
+ }
575
+
576
+ if (!force) {
577
+ return existing;
578
+ }
579
+
580
+ return existing.replace(/<!-- agent-harness:start[\s\S]*?<!-- agent-harness:end -->\n?/m, block);
581
+ }
582
+
583
+ function containsManagedBlock(content) {
584
+ return content.includes("<!-- agent-harness:start") && content.includes("<!-- agent-harness:end -->");
585
+ }
586
+
587
+ function describeRuleAction(targetPath, hasMarker, force) {
588
+ if (!fs.existsSync(targetPath)) {
589
+ return "创建宿主规则文件";
590
+ }
591
+
592
+ if (!hasMarker) {
593
+ return "追加宿主规则块";
594
+ }
595
+
596
+ return force ? "覆盖已有宿主规则块" : "保留已有宿主规则块";
597
+ }
598
+
599
+ function mergeClaudeSettings(existing, template) {
600
+ const result = { ...existing };
601
+ result.hooks = { ...(existing.hooks ?? {}) };
602
+
603
+ for (const [hookName, templateEntries] of Object.entries(template.hooks ?? {})) {
604
+ const existingEntries = Array.isArray(result.hooks[hookName]) ? [...result.hooks[hookName]] : [];
605
+ const knownCommands = new Set(
606
+ existingEntries.flatMap((entry) =>
607
+ (entry.hooks ?? []).map((hook) => hook.command).filter(Boolean)
608
+ )
609
+ );
610
+
611
+ for (const templateEntry of templateEntries) {
612
+ const commands = (templateEntry.hooks ?? []).map((hook) => hook.command).filter(Boolean);
613
+ const hasAllCommands = commands.every((command) => knownCommands.has(command));
614
+ if (hasAllCommands) {
615
+ continue;
616
+ }
617
+
618
+ existingEntries.push(templateEntry);
619
+ for (const command of commands) {
620
+ knownCommands.add(command);
621
+ }
622
+ }
623
+
624
+ result.hooks[hookName] = existingEntries;
625
+ }
626
+
627
+ return result;
628
+ }
629
+
630
+ function mergeGitignore(existing, entries) {
631
+ const trimmed = existing.replace(/\s*$/, "");
632
+ const lines = new Set(trimmed ? trimmed.split("\n") : []);
633
+ const missing = entries.filter((entry) => !lines.has(entry));
634
+
635
+ if (missing.length === 0) {
636
+ return existing;
637
+ }
638
+
639
+ const prefix = trimmed ? `${trimmed}\n\n` : "";
640
+ return `${prefix}${missing.join("\n")}\n`;
641
+ }
642
+
643
+ function buildRuntimeReadme() {
644
+ return `# agent-harness
645
+
646
+ 这个目录由 \`agent-harness init\` 生成。
647
+
648
+ - \`tasks/\`:协议模板副本
649
+ - \`state/\`:后续状态持久化目录
650
+ - \`audit/\`:后续审计日志目录
651
+ - \`reports/\`:后续任务报告目录
652
+ `;
653
+ }
654
+
655
+ function printPlan(actions, dryRun, cwd, project, hosts) {
656
+ console.log(`project: ${project.projectName}`);
657
+ console.log(`project_type: ${project.projectType}`);
658
+ console.log(`hosts: ${hosts.join(", ")}`);
659
+ console.log(`target_dir: ${cwd}`);
660
+ console.log("");
661
+ console.log(dryRun ? "计划写入:" : "执行写入:");
662
+
663
+ for (const action of actions) {
664
+ const prefix = action.skip ? "[skip]" : dryRun ? "[dry-run]" : "[write]";
665
+ console.log(`${prefix} ${action.description}: ${action.relativePath}`);
666
+ }
667
+ }
668
+
669
+ function readText(filePath) {
670
+ return fs.readFileSync(filePath, "utf8");
671
+ }
672
+
673
+ function readJson(filePath) {
674
+ return JSON.parse(readText(filePath));
675
+ }
676
+
677
+ function readJsonIfExists(filePath) {
678
+ if (!fs.existsSync(filePath)) {
679
+ return null;
680
+ }
681
+
682
+ try {
683
+ return readJson(filePath);
684
+ } catch {
685
+ return null;
686
+ }
687
+ }
688
+
689
+ function ensureDirectory(directoryPath) {
690
+ fs.mkdirSync(directoryPath, { recursive: true });
691
+ }
692
+
693
+ function existsAny(cwd, names) {
694
+ return names.some((name) => fs.existsSync(path.join(cwd, name)));
695
+ }
696
+
697
+ function isScalar(value) {
698
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
699
+ }
700
+
701
+ function formatScalar(value) {
702
+ if (typeof value === "string") {
703
+ return JSON.stringify(value);
704
+ }
705
+
706
+ if (value === null) {
707
+ return "null";
708
+ }
709
+
710
+ return String(value);
711
+ }