create-harness-vibe-coding 0.8.17 → 0.8.19

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 (150) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README-CN.md +2 -0
  3. package/README.md +3 -0
  4. package/package.json +5 -2
  5. package/src/generator.js +613 -97
  6. package/src/index.js +556 -309
  7. package/src/prompts.js +18 -0
  8. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  9. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  10. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  11. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  12. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  13. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  14. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  15. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  16. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  17. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  18. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  19. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  20. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  21. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  22. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  23. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  24. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  25. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  26. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  27. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  28. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  29. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  30. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  31. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  32. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  33. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  34. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  35. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  36. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  37. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  38. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  39. package/src/ui/dist/index.html +18 -0
  40. package/src/ui/index.html +17 -0
  41. package/src/ui/package.json +33 -0
  42. package/src/ui/pnpm-lock.yaml +1582 -0
  43. package/src/ui/pnpm-workspace.yaml +2 -0
  44. package/src/ui/src/App.tsx +86 -0
  45. package/src/ui/src/api.ts +93 -0
  46. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  47. package/src/ui/src/components/Footer.tsx +69 -0
  48. package/src/ui/src/components/Header.tsx +175 -0
  49. package/src/ui/src/components/LoadingView.tsx +22 -0
  50. package/src/ui/src/components/RolesRoute.tsx +169 -0
  51. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  52. package/src/ui/src/components/TaskList.tsx +388 -0
  53. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  54. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  55. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  56. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  57. package/src/ui/src/index.css +281 -0
  58. package/src/ui/src/main.tsx +11 -0
  59. package/src/ui/src/types.ts +108 -0
  60. package/src/ui/tsconfig.json +21 -0
  61. package/src/ui/vite.config.ts +19 -0
  62. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  63. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  64. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  65. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  66. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  67. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  68. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  69. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  70. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  71. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  72. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  73. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  74. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  75. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  76. package/src/wf-ui-server/a2a-store.mjs +296 -0
  77. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  78. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  79. package/src/wf-ui-server/runtime-config.mjs +153 -0
  80. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  81. package/src/wf-ui-server/security.mjs +67 -0
  82. package/src/wf-ui-server/server.mjs +849 -0
  83. package/src/wf-ui-server/session-registry.mjs +204 -0
  84. package/src/wf-ui-server/settings.mjs +100 -0
  85. package/src/wf-ui-server/task-parser.mjs +133 -0
  86. package/src/wf-ui-server/terminal-store.mjs +225 -0
  87. package/src/wf-ui-server/token.mjs +41 -0
  88. package/src/wf-ui-server/ws-events.mjs +354 -0
  89. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  90. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  91. package/templates/common/.claude/commands/wf-help.md +5 -0
  92. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  93. package/templates/common/.claude/commands/wf-task-list.md +26 -0
  94. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  95. package/templates/common/.claude/commands/wf-ui.md +26 -0
  96. package/templates/common/.claude/commands/wf-update.md +54 -9
  97. package/templates/common/.claude/rules/ecc/common.md +1 -1
  98. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  99. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  100. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  101. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  102. package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
  103. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  104. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  105. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  106. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  107. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  108. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  109. package/templates/common/.harness-version +105 -47
  110. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  111. package/templates/common/.opencode/commands/wf-help.md +5 -0
  112. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  113. package/templates/common/.opencode/commands/wf-task-list.md +29 -0
  114. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  115. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  116. package/templates/common/.opencode/commands/wf-update.md +54 -9
  117. package/templates/common/CLAUDE.md +8 -6
  118. package/templates/common/Harness/MEMORY.md +11 -0
  119. package/templates/common/Harness/README.md +21 -38
  120. package/templates/common/Harness/a2a/role-graph.json +79 -0
  121. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  122. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  123. package/templates/common/Harness/ownership.manifest.json +142 -2
  124. package/templates/common/Harness/scripts/README.md +134 -0
  125. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  126. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  127. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  128. package/templates/common/Harness/scripts/task-state.mjs +949 -26
  129. package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
  130. package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
  131. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  132. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  133. package/templates/common/Harness/settings.json +35 -0
  134. package/templates/common/Harness/specs/guides/SETUP.md +8 -0
  135. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  136. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +16 -5
  137. package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
  138. package/templates/common/Harness/specs/runtime/subagents.md +6 -0
  139. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  140. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  141. package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
  142. package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
  143. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  144. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  145. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  146. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  147. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  148. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
  149. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  150. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
package/src/index.js CHANGED
@@ -2,90 +2,115 @@
2
2
  import * as p from '@clack/prompts';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { spawnSync } from 'node:child_process';
5
+ import { spawn, spawnSync } from 'node:child_process';
6
6
  import pc from 'picocolors';
7
- import { askConflictPolicy, askOptionalSelections, askProjectName, askTargetDir } from './prompts.js';
7
+ import { askConflictPolicy, askInstallScope, askOptionalSelections, askProjectName, askTargetDir } from './prompts.js';
8
8
  import { generate, getOptionalCatalog } from './generator.js';
9
9
 
10
10
  const UPDATE_SUCCESS_STATUSES = new Set(['up-to-date', 'update-available', 'partial-update']);
11
11
  const UPDATE_FAILURE_STATUSES = new Set(['error', 'offline', 'template-remote', 'downgrade-refused']);
12
12
  const CANONICAL_UPDATE_SOURCE_BASE = 'https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/';
13
-
14
- // ── CLI flags ──────────────────────────────────────────────
15
- const raw = process.argv.slice(2);
13
+
14
+ // ── CLI flags ──────────────────────────────────────────────
15
+ const raw = process.argv.slice(2);
16
+ function extractFlag(args, flag) {
17
+ const idx = args.indexOf(flag);
18
+ if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
19
+ const withEquals = args.find(arg => arg.startsWith(`${flag}=`));
20
+ if (withEquals) return withEquals.slice(flag.length + 1);
21
+ return null;
22
+ }
23
+ function hasFlag(args, flag) {
24
+ return args.includes(flag);
25
+ }
26
+
27
+ // wf-ui subcommand: handle before parseArgs so --project/--port/--open flags
28
+ // never fall through to the scaffold installer.
29
+ if (raw[0] === 'wf-ui') {
30
+ await runWfUi(raw.slice(1));
31
+ process.exit(0);
32
+ }
33
+
16
34
  const parsed = parseArgs(raw);
17
35
  const showHelp = parsed.flags.help || parsed.flags.h;
18
36
  const skipPrompts = parsed.flags.yes || parsed.flags.y;
19
37
  const conflictPolicyProvided = parsed.flags.onConflict !== undefined;
20
-
21
- if (parsed.errors.length > 0) {
22
- for (const err of parsed.errors) {
23
- console.error(pc.red(`Error: ${err}`));
24
- }
25
- process.exit(1);
26
- }
27
-
28
- if (showHelp) {
29
- console.log('');
30
- console.log(' create-harness-vibe-coding');
31
- console.log('');
32
- console.log(' Usage:');
33
- console.log(' npx create-harness-vibe-coding@latest [project-name] [target-dir] [flags]');
34
- console.log('');
35
- console.log(' Arguments:');
36
- console.log(' project-name Name for the new project (default: my-vibe-project)');
37
- console.log(' target-dir Directory to create the project in (default: ./<project-name>)');
38
- console.log('');
39
- console.log(' Flags:');
40
- console.log(' -y, --yes Skip all prompts, use defaults or provided args');
41
- console.log(' -h, --help Show this help');
42
- console.log(' --dry-run Print the planned writes without creating files');
43
- console.log(' --on-conflict <policy> fail, skip, backup, or overwrite (default: fail)');
38
+
39
+ if (parsed.errors.length > 0 && raw[0] !== 'wf-ui') {
40
+ for (const err of parsed.errors) {
41
+ console.error(pc.red(`Error: ${err}`));
42
+ }
43
+ process.exit(1);
44
+ }
45
+
46
+ if (showHelp) {
47
+ console.log('');
48
+ console.log(' create-harness-vibe-coding');
49
+ console.log('');
50
+ console.log(' Usage:');
51
+ console.log(' npx create-harness-vibe-coding@latest [project-name] [target-dir] [flags]');
52
+ console.log('');
53
+ console.log(' Arguments:');
54
+ console.log(' project-name Name for the new project (default: my-vibe-project)');
55
+ console.log(' target-dir Directory to create the project in (default: ./<project-name>)');
56
+ console.log('');
57
+ console.log(' Flags:');
58
+ console.log(' -y, --yes Skip all prompts, use defaults or provided args');
59
+ console.log(' -h, --help Show this help');
60
+ console.log(' --dry-run Print the planned writes without creating files');
61
+ console.log(' --on-conflict <policy> fail, skip, backup, or overwrite (default: fail)');
44
62
  console.log(' --with <id,id> Add optional local workflow skills');
45
63
  console.log(' --without <id,id> Remove optional workflow skills selected by --preset or --with');
46
64
  console.log(' --recommend <id,id> Record recommendation-only external capabilities');
47
65
  console.log(' --preset <name> Add a built-in optional workflow preset');
66
+ console.log(' --install-scope <scope> project or global (default: project)');
67
+ console.log(' --global-dir <dir> Global Harness runtime directory for --install-scope global');
68
+ console.log(' --host-global-dir <dir> Base directory for Claude/Codex/OpenCode global copies');
48
69
  console.log(' --list-options Print optional workflow skills and presets');
49
70
  console.log(' --json Output machine-readable JSON (use with --dry-run for planning)');
50
71
  console.log('');
51
72
  console.log(' Interactive mode offers checkbox selection for optional workflows and external recommendations.');
52
- console.log('');
53
- console.log(' Examples:');
54
- console.log(' npx create-harness-vibe-coding@latest');
55
- console.log(' npx create-harness-vibe-coding@latest -y');
56
- console.log(' npx create-harness-vibe-coding@latest my-project');
57
- console.log(' npx create-harness-vibe-coding@latest my-project ./dist/my-project -y');
58
- console.log(' npx create-harness-vibe-coding@latest legacy ./legacy -y --dry-run');
59
- console.log(' npx create-harness-vibe-coding@latest legacy ./legacy -y --on-conflict skip');
73
+ console.log('');
74
+ console.log(' Examples:');
75
+ console.log(' npx create-harness-vibe-coding@latest');
76
+ console.log(' npx create-harness-vibe-coding@latest -y');
77
+ console.log(' npx create-harness-vibe-coding@latest my-project');
78
+ console.log(' npx create-harness-vibe-coding@latest my-project ./dist/my-project -y');
79
+ console.log(' npx create-harness-vibe-coding@latest legacy ./legacy -y --dry-run');
80
+ console.log(' npx create-harness-vibe-coding@latest legacy ./legacy -y --on-conflict skip');
60
81
  console.log(' npx create-harness-vibe-coding@latest web ./web -y --with ts-react-frontend,ui-ux-review');
61
- console.log(' npx create-harness-vibe-coding@latest web ./web -y --preset web-app');
62
- console.log(' npx create-harness-vibe-coding@latest api ./api -y --preset fullstack --without github-pr-review');
63
- console.log('');
64
- process.exit(0);
65
- }
66
-
67
- if (parsed.flags.listOptions) {
68
- printOptions();
69
- process.exit(0);
70
- }
71
-
72
- // Positional args (non-flag)
73
- const positional = parsed.positionals;
74
- const argName = positional[0];
75
- const argDir = positional[1];
76
- const generationOptions = {
77
- dryRun: Boolean(parsed.flags.dryRun),
78
- onConflict: parsed.flags.onConflict || 'fail',
79
- withOptions: parsed.flags.with || [],
82
+ console.log(' npx create-harness-vibe-coding@latest web ./web -y --preset web-app');
83
+ console.log(' npx create-harness-vibe-coding@latest api ./api -y --preset fullstack --without github-pr-review');
84
+ console.log(' npx create-harness-vibe-coding@latest app ./app -y --install-scope global');
85
+ console.log('');
86
+ process.exit(0);
87
+ }
88
+
89
+ if (parsed.flags.listOptions) {
90
+ printOptions();
91
+ process.exit(0);
92
+ }
93
+
94
+ // Positional args (non-flag)
95
+ const positional = parsed.positionals;
96
+ const argName = positional[0];
97
+ const argDir = positional[1];
98
+ const generationOptions = {
99
+ dryRun: Boolean(parsed.flags.dryRun),
100
+ onConflict: parsed.flags.onConflict || 'fail',
101
+ withOptions: parsed.flags.with || [],
80
102
  withoutOptions: parsed.flags.without || [],
81
103
  externalOptions: parsed.flags.recommend || [],
82
104
  preset: parsed.flags.preset,
105
+ installScope: parsed.flags.installScope || 'project',
106
+ globalDir: parsed.flags.globalDir,
107
+ hostGlobalDir: parsed.flags.hostGlobalDir,
83
108
  json: Boolean(parsed.flags.json),
84
109
  };
85
-
86
- const DEFAULT_NAME = 'my-vibe-project';
87
-
88
- // --json: machine-readable output, no prompts, no decorative output
110
+
111
+ const DEFAULT_NAME = 'my-vibe-project';
112
+
113
+ // --json: machine-readable output, no prompts, no decorative output
89
114
  if (generationOptions.json) {
90
115
  const projectName = argName || DEFAULT_NAME;
91
116
  const targetDir = argDir || `./${projectName}`;
@@ -109,20 +134,22 @@ if (generationOptions.json) {
109
134
  scan,
110
135
  });
111
136
  printJsonResult(result);
112
- // printJsonResult exits with 1 on failure; we only reach here on success
113
- process.exit(0);
114
- }
115
-
116
- console.log('');
117
- console.log(pc.magenta('╔══════════════════════════════════════════╗'));
118
- console.log(pc.magenta('║ create-harness-vibe-coding ║'));
119
- console.log(pc.magenta('║ 0-1 Product Harness Scaffold ║'));
120
- console.log(pc.magenta('╚══════════════════════════════════════════╝'));
121
- console.log('');
122
-
123
- let projectName, targetDir;
124
-
125
- // Non-interactive: positionals provided OR -y/--yes flag set
137
+ // printJsonResult exits with 1 on failure; we only reach here on success
138
+ process.exit(0);
139
+ }
140
+
141
+ console.log('');
142
+ console.log(pc.magenta('╔══════════════════════════════════════════╗'));
143
+ console.log(pc.magenta('║ create-harness-vibe-coding ║'));
144
+ console.log(pc.magenta('║ 0-1 Product Harness Scaffold ║'));
145
+ console.log(pc.magenta('╚══════════════════════════════════════════╝'));
146
+ console.log('');
147
+
148
+ let projectName, targetDir;
149
+
150
+ // wf-ui: server already running, skip the scaffold/update flow
151
+ if (raw[0] !== 'wf-ui') {
152
+ // Non-interactive: positionals provided OR -y/--yes flag set
126
153
  if (argName || skipPrompts) {
127
154
  projectName = argName || DEFAULT_NAME;
128
155
  targetDir = argDir || `./${projectName}`;
@@ -137,47 +164,64 @@ if (argName || skipPrompts) {
137
164
  }
138
165
 
139
166
  console.log(pc.dim('────────────────────────────────────────────'));
140
- console.log(` Project ${pc.green(projectName)}`);
167
+ console.log(` Project ${pc.green(projectName)}`);
141
168
  console.log(` Directory ${pc.green(targetDir)}`);
142
- console.log(` Creates ${pc.cyan('CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, .opencode/, opencode.json, tests/')}`);
143
- if (generationOptions.dryRun) {
169
+ console.log(` Scope ${pc.green(generationOptions.installScope)}`);
170
+ if (generationOptions.globalDir) {
171
+ console.log(` Global dir ${pc.green(generationOptions.globalDir)}`);
172
+ }
173
+ if (generationOptions.hostGlobalDir) {
174
+ console.log(` Host dir ${pc.green(generationOptions.hostGlobalDir)}`);
175
+ }
176
+ const creates = generationOptions.installScope === 'global'
177
+ ? 'project bridge/state/settings + global runtime + Claude/Codex/OpenCode host copies'
178
+ : 'CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, .opencode/, opencode.json, tests/';
179
+ console.log(` Creates ${pc.cyan(creates)}`);
180
+ if (generationOptions.dryRun) {
144
181
  console.log(` Mode ${pc.yellow('dry-run')}`);
145
- }
146
- console.log(` Conflicts ${pc.cyan(generationOptions.onConflict)}`);
147
- if (generationOptions.withOptions.length > 0) {
148
- console.log(` Optional ${pc.cyan(generationOptions.withOptions.join(','))}`);
149
- }
182
+ }
183
+ console.log(` Conflicts ${pc.cyan(generationOptions.onConflict)}`);
184
+ if (generationOptions.withOptions.length > 0) {
185
+ console.log(` Optional ${pc.cyan(generationOptions.withOptions.join(','))}`);
186
+ }
150
187
  if (generationOptions.withoutOptions.length > 0) {
151
188
  console.log(` Without ${pc.cyan(generationOptions.withoutOptions.join(','))}`);
152
189
  }
153
190
  if (generationOptions.externalOptions.length > 0) {
154
191
  console.log(` Recommend ${pc.cyan(generationOptions.externalOptions.join(','))}`);
155
192
  }
156
- if (generationOptions.preset) {
157
- console.log(` Preset ${pc.cyan(generationOptions.preset)}`);
158
- }
159
- if (skipPrompts) {
160
- console.log(` Mode ${pc.dim('non-interactive (-y)')}`);
161
- }
162
- console.log(pc.dim('────────────────────────────────────────────'));
163
- console.log('');
164
-
165
- const result = generate({ projectName, targetDir, ...generationOptions });
166
- printResult(result, targetDir);
167
- } else {
168
- // Interactive mode
169
- try {
170
- projectName = await askProjectName();
171
- } catch {
172
- projectName = 'my-vibe-project';
173
- console.log(pc.dim(` Project: ${projectName} (default)`));
174
- }
175
-
193
+ if (generationOptions.preset) {
194
+ console.log(` Preset ${pc.cyan(generationOptions.preset)}`);
195
+ }
196
+ if (skipPrompts) {
197
+ console.log(` Mode ${pc.dim('non-interactive (-y)')}`);
198
+ }
199
+ console.log(pc.dim('────────────────────────────────────────────'));
200
+ console.log('');
201
+
202
+ const result = generate({ projectName, targetDir, ...generationOptions });
203
+ printResult(result, targetDir);
204
+ } else {
205
+ // Interactive mode
206
+ try {
207
+ projectName = await askProjectName();
208
+ } catch {
209
+ projectName = 'my-vibe-project';
210
+ console.log(pc.dim(` Project: ${projectName} (default)`));
211
+ }
212
+
176
213
  try {
177
214
  targetDir = await askTargetDir(projectName);
178
215
  } catch {
179
- targetDir = `./${projectName}`;
180
- console.log(pc.dim(` Directory: ${targetDir} (default)`));
216
+ targetDir = `./${projectName}`;
217
+ console.log(pc.dim(` Directory: ${targetDir} (default)`));
218
+ }
219
+
220
+ try {
221
+ generationOptions.installScope = await askInstallScope();
222
+ } catch {
223
+ generationOptions.installScope = 'project';
224
+ console.log(pc.dim(' Scope: project (default)'));
181
225
  }
182
226
 
183
227
  const scan = scanTarget(targetDir);
@@ -216,12 +260,22 @@ if (argName || skipPrompts) {
216
260
  console.log(pc.dim(' Optional: none (default)'));
217
261
  }
218
262
  }
219
-
220
- console.log('');
221
- console.log(pc.dim('────────────────────────────────────────────'));
222
- console.log(` Project ${pc.green(projectName)}`);
263
+
264
+ console.log('');
265
+ console.log(pc.dim('────────────────────────────────────────────'));
266
+ console.log(` Project ${pc.green(projectName)}`);
223
267
  console.log(` Directory ${pc.green(targetDir)}`);
224
- console.log(` Creates ${pc.cyan('CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, .opencode/, opencode.json, tests/')}`);
268
+ console.log(` Scope ${pc.green(generationOptions.installScope)}`);
269
+ if (generationOptions.globalDir) {
270
+ console.log(` Global dir ${pc.green(generationOptions.globalDir)}`);
271
+ }
272
+ if (generationOptions.hostGlobalDir) {
273
+ console.log(` Host dir ${pc.green(generationOptions.hostGlobalDir)}`);
274
+ }
275
+ const creates = generationOptions.installScope === 'global'
276
+ ? 'project bridge/state + global runtime + Claude/Codex/OpenCode host copies'
277
+ : 'CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, .opencode/, opencode.json, tests/';
278
+ console.log(` Creates ${pc.cyan(creates)}`);
225
279
  console.log(` Conflicts ${pc.cyan(generationOptions.onConflict)}`);
226
280
  if (generationOptions.withOptions.length > 0) {
227
281
  console.log(` Optional ${pc.cyan(generationOptions.withOptions.join(','))}`);
@@ -229,145 +283,264 @@ if (argName || skipPrompts) {
229
283
  if (generationOptions.externalOptions.length > 0) {
230
284
  console.log(` Recommend ${pc.cyan(generationOptions.externalOptions.join(','))}`);
231
285
  }
232
- console.log(pc.dim('────────────────────────────────────────────'));
233
- console.log('');
234
-
235
- const preview = generate({ projectName, targetDir, ...generationOptions, dryRun: true });
236
- if (!preview.success) {
237
- printResult(preview, targetDir);
238
- }
239
-
240
- console.log(pc.yellow('Planned changes: no files have been written yet.'));
241
- printSummary(preview.summary);
242
- printPlan(preview.plan);
243
- printWarnings(preview);
244
- console.log('');
245
-
246
- if (generationOptions.dryRun) {
247
- process.exit(0);
248
- }
249
-
250
- let proceed = true;
251
- try {
252
- proceed = await p.confirm({
253
- message: 'Confirm generation with this plan?',
254
- initialValue: true,
255
- });
256
- if (p.isCancel(proceed)) proceed = false;
257
- } catch {
258
- proceed = true;
259
- }
260
-
261
- if (!proceed) {
262
- p.cancel('Cancelled');
263
- process.exit(0);
264
- }
265
-
266
- console.log('');
267
- const result = generate({ projectName, targetDir, ...generationOptions });
268
- printResult(result, targetDir);
269
- }
270
-
271
- function printResult(result, targetDir) {
272
- if (result.success) {
273
- if (result.dryRun) {
274
- console.log(pc.yellow('\nDry run: no files or directories were written.'));
275
- printSummary(result.summary);
276
- printPlan(result.plan);
277
- if (result.warnings.length > 0) {
278
- console.log(pc.yellow('\nWarning(s):'));
279
- for (const warning of result.warnings) {
280
- console.log(pc.yellow(` - ${warning}`));
281
- }
282
- }
283
- console.log('');
284
- return;
285
- }
286
-
287
- console.log(pc.green('\nGeneration complete.\n'));
288
- printSummary(result.summary);
289
-
290
- printWarnings(result);
291
-
286
+ console.log(pc.dim('────────────────────────────────────────────'));
287
+ console.log('');
288
+
289
+ const preview = generate({ projectName, targetDir, ...generationOptions, dryRun: true });
290
+ if (!preview.success) {
291
+ printResult(preview, targetDir);
292
+ }
293
+
294
+ console.log(pc.yellow('Planned changes: no files have been written yet.'));
295
+ console.log(pc.bold('Project plan:'));
296
+ printSummary(preview.summary);
297
+ printPlan(preview.plan);
298
+ if (preview.globalPlan) {
299
+ console.log(pc.bold('\nGlobal runtime plan:'));
300
+ console.log(` Directory ${pc.cyan(preview.globalDir)}`);
301
+ printSummary(preview.globalSummary);
302
+ printPlan(preview.globalPlan);
303
+ }
304
+ printHostPlans(preview.hostPlans, preview.hostSummary);
305
+ printWarnings(preview);
306
+ console.log('');
307
+
308
+ if (generationOptions.dryRun) {
309
+ process.exit(0);
310
+ }
311
+
312
+ let proceed = true;
313
+ try {
314
+ proceed = await p.confirm({
315
+ message: 'Confirm generation with this plan?',
316
+ initialValue: true,
317
+ });
318
+ if (p.isCancel(proceed)) proceed = false;
319
+ } catch {
320
+ proceed = true;
321
+ }
322
+
323
+ if (!proceed) {
324
+ p.cancel('Cancelled');
325
+ process.exit(0);
326
+ }
327
+
328
+ console.log('');
329
+ const result = generate({ projectName, targetDir, ...generationOptions });
330
+ printResult(result, targetDir);
331
+ }
332
+
333
+ function printResult(result, targetDir) {
334
+ if (result.success) {
335
+ if (result.dryRun) {
336
+ console.log(pc.yellow('\nDry run: no files or directories were written.'));
337
+ console.log(pc.bold('Project plan:'));
338
+ printSummary(result.summary);
339
+ printPlan(result.plan);
340
+ if (result.globalPlan) {
341
+ console.log(pc.bold('\nGlobal runtime plan:'));
342
+ console.log(` Directory ${pc.cyan(result.globalDir)}`);
343
+ printSummary(result.globalSummary);
344
+ printPlan(result.globalPlan);
345
+ }
346
+ printHostPlans(result.hostPlans, result.hostSummary);
347
+ if (result.warnings.length > 0) {
348
+ console.log(pc.yellow('\nWarning(s):'));
349
+ for (const warning of result.warnings) {
350
+ console.log(pc.yellow(` - ${warning}`));
351
+ }
352
+ }
353
+ console.log('');
354
+ return;
355
+ }
356
+
357
+ console.log(pc.green('\nGeneration complete.\n'));
358
+ console.log(pc.bold('Project install:'));
359
+ printSummary(result.summary);
360
+ if (result.globalSummary) {
361
+ console.log(pc.bold('Global runtime:'));
362
+ console.log(` Directory ${pc.cyan(result.globalDir)}`);
363
+ printSummary(result.globalSummary);
364
+ }
365
+ printHostPlans(result.hostPlans, result.hostSummary, { summaryOnly: true });
366
+
367
+ printWarnings(result);
368
+
292
369
  console.log(pc.bold('Next steps:'));
293
370
  console.log(` ${pc.cyan(`cd ${targetDir}`)}`);
294
- console.log(` ${pc.cyan('claude')} # Start Claude Code`);
295
- console.log(` ${pc.cyan('codex')} # Or start Codex`);
296
- console.log(` ${pc.cyan('opencode')} # Or start OpenCode`);
371
+ console.log(` ${pc.cyan('claude')} # Start Claude Code`);
372
+ console.log(` ${pc.cyan('codex')} # Or start Codex`);
373
+ console.log(` ${pc.cyan('opencode')} # Or start OpenCode`);
297
374
  console.log(` Tell your agent: "${pc.yellow('Read Harness/specs/guides/SETUP.md. Bootstrap this project from idea to first vertical slice.')}"`);
298
- console.log('');
375
+ console.log('');
299
376
  console.log(pc.dim(' Keep Harness/specs/guides/SETUP.md as a setup reference; normal sessions start at CLAUDE.md, with Harness/README.md as the routed workflow router.'));
300
- console.log('');
301
-
302
- } else {
303
- console.log(pc.red('\nGeneration failed:'));
304
- for (const err of result.errors) {
305
- console.log(pc.red(` - ${err}`));
306
- }
307
- process.exit(1);
308
- }
309
- }
310
-
311
- function parseArgs(args) {
312
- const flags = {
313
- with: [],
314
- without: [],
315
- };
316
- const positionals = [];
317
- const errors = [];
318
-
319
- function readValue(flagName, index) {
320
- const value = args[index + 1];
321
- if (!value || value.startsWith('-')) {
322
- errors.push(`${flagName} requires a value`);
323
- return { value: undefined, nextIndex: index };
324
- }
325
- return { value, nextIndex: index + 1 };
326
- }
327
-
328
- function readEqualsValue(flagName, value) {
329
- if (!value || value.startsWith('-')) {
330
- errors.push(`${flagName} requires a value`);
331
- return undefined;
332
- }
333
- return value;
334
- }
335
-
336
- for (let i = 0; i < args.length; i += 1) {
337
- const arg = args[i];
338
-
339
- if (arg === '-h') {
340
- flags.h = true;
341
- } else if (arg === '--help') {
342
- flags.help = true;
343
- } else if (arg === '-y') {
344
- flags.y = true;
345
- } else if (arg === '--yes') {
346
- flags.yes = true;
347
- } else if (arg === '--dry-run') {
348
- flags.dryRun = true;
377
+ console.log('');
378
+
379
+ } else {
380
+ console.log(pc.red('\nGeneration failed:'));
381
+ for (const err of result.errors) {
382
+ console.log(pc.red(` - ${err}`));
383
+ }
384
+ process.exit(1);
385
+ }
386
+ }
387
+ } // wf-ui skip block end — functions below are module-scoped
388
+
389
+ async function runWfUi(args) {
390
+ const projectRoot = extractFlag(args, '--project') || process.cwd();
391
+ const host = extractFlag(args, '--host') || '127.0.0.1';
392
+ const rawPort = extractFlag(args, '--port') || '0';
393
+ const port = Number.parseInt(rawPort, 10);
394
+ const openBrowser = !hasFlag(args, '--no-open');
395
+
396
+ if (host !== '127.0.0.1') {
397
+ console.error('[wf-ui] host must be 127.0.0.1');
398
+ process.exit(1);
399
+ }
400
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
401
+ console.error('[wf-ui] port must be an integer from 0 to 65535');
402
+ process.exit(1);
403
+ }
404
+
405
+ try {
406
+ const serverModule = await import('./wf-ui-server/server.mjs');
407
+ const { SessionRegistry } = await import('./wf-ui-server/session-registry.mjs');
408
+ const { attachEventsWs } = await import('./wf-ui-server/ws-events.mjs');
409
+ const { attachTerminalWs } = await import('./wf-ui-server/ws-terminal.mjs');
410
+ const { warmRuntimeCache } = await import('./wf-ui-server/runtime-detector.mjs');
411
+ const { appendSessionEvent, appendTerminalData, persistSession, recordInputRequest } =
412
+ await import('./wf-ui-server/terminal-store.mjs');
413
+ const registry = new SessionRegistry();
414
+ const terminalHub = {};
415
+ const started = await serverModule.startServer({
416
+ projectRoot,
417
+ host,
418
+ port,
419
+ sessionRegistry: registry,
420
+ terminalHub,
421
+ });
422
+ attachEventsWs(started.server, started.token, projectRoot);
423
+ Object.assign(terminalHub, attachTerminalWs(started.server, started.token, registry, {
424
+ onTerminalInput(session, data) {
425
+ try {
426
+ recordInputRequest(projectRoot, session.sessionId, data);
427
+ } catch {
428
+ // The session may not have been persisted yet; stdout/stderr capture still remains authoritative.
429
+ }
430
+ appendTerminalData(projectRoot, session, data, 'stdin');
431
+ },
432
+ onAttachModeChange(session, attachMode) {
433
+ persistSession(projectRoot, session);
434
+ appendSessionEvent(projectRoot, session, { type: 'session.attach-mode', attachMode });
435
+ },
436
+ onSessionState(session, state) {
437
+ persistSession(projectRoot, session);
438
+ appendSessionEvent(projectRoot, session, { type: 'session.state', state });
439
+ },
440
+ }));
441
+ warmRuntimeCache();
442
+ console.log(`[wf-ui] ${started.url}`);
443
+ if (openBrowser) openLocalUrl(started.url);
444
+ await waitForWfUiShutdown(started.server);
445
+ } catch (err) {
446
+ console.error(`[wf-ui] ${err.message}`);
447
+ process.exit(1);
448
+ }
449
+ }
450
+
451
+ function waitForWfUiShutdown(server) {
452
+ return new Promise((resolve) => {
453
+ let closing = false;
454
+ const shutdown = () => {
455
+ if (closing) return;
456
+ closing = true;
457
+ server.close(() => resolve());
458
+ };
459
+ process.once('SIGINT', shutdown);
460
+ process.once('SIGTERM', shutdown);
461
+ server.once('close', resolve);
462
+ });
463
+ }
464
+
465
+ function openLocalUrl(url) {
466
+ const command = process.platform === 'win32'
467
+ ? { file: 'cmd', args: ['/c', 'start', '', url] }
468
+ : process.platform === 'darwin'
469
+ ? { file: 'open', args: [url] }
470
+ : { file: 'xdg-open', args: [url] };
471
+
472
+ try {
473
+ const child = spawn(command.file, command.args, {
474
+ detached: true,
475
+ stdio: 'ignore',
476
+ windowsHide: true,
477
+ });
478
+ child.unref();
479
+ } catch (err) {
480
+ console.error(`[wf-ui] could not open browser automatically: ${err.message}`);
481
+ }
482
+ }
483
+
484
+ function parseArgs(args) {
485
+ const flags = {
486
+ with: [],
487
+ without: [],
488
+ };
489
+ const positionals = [];
490
+ const errors = [];
491
+
492
+ function readValue(flagName, index) {
493
+ const value = args[index + 1];
494
+ if (!value || value.startsWith('-')) {
495
+ errors.push(`${flagName} requires a value`);
496
+ return { value: undefined, nextIndex: index };
497
+ }
498
+ return { value, nextIndex: index + 1 };
499
+ }
500
+
501
+ function readEqualsValue(flagName, value) {
502
+ if (!value || value.startsWith('-')) {
503
+ errors.push(`${flagName} requires a value`);
504
+ return undefined;
505
+ }
506
+ return value;
507
+ }
508
+
509
+ for (let i = 0; i < args.length; i += 1) {
510
+ const arg = args[i];
511
+
512
+ if (arg === '-h') {
513
+ flags.h = true;
514
+ } else if (arg === '--help') {
515
+ flags.help = true;
516
+ } else if (arg === '-y') {
517
+ flags.y = true;
518
+ } else if (arg === '--yes') {
519
+ flags.yes = true;
520
+ } else if (arg === '--dry-run') {
521
+ flags.dryRun = true;
349
522
  } else if (arg === '--list-options') {
350
523
  flags.listOptions = true;
351
524
  } else if (arg === '--json') {
352
525
  flags.json = true;
353
- } else if (arg === '--on-conflict') {
354
- const parsedValue = readValue('--on-conflict', i);
355
- flags.onConflict = parsedValue.value;
356
- i = parsedValue.nextIndex;
357
- } else if (arg.startsWith('--on-conflict=')) {
358
- flags.onConflict = readEqualsValue('--on-conflict', arg.slice('--on-conflict='.length));
359
- } else if (arg === '--with') {
360
- const parsedValue = readValue('--with', i);
361
- if (parsedValue.value !== undefined) flags.with.push(parsedValue.value);
362
- i = parsedValue.nextIndex;
363
- } else if (arg.startsWith('--with=')) {
364
- const value = readEqualsValue('--with', arg.slice('--with='.length));
365
- if (value !== undefined) flags.with.push(value);
366
- } else if (arg === '--without') {
367
- const parsedValue = readValue('--without', i);
368
- if (parsedValue.value !== undefined) flags.without.push(parsedValue.value);
369
- i = parsedValue.nextIndex;
370
- } else if (arg.startsWith('--without=')) {
526
+ } else if (arg === '--on-conflict') {
527
+ const parsedValue = readValue('--on-conflict', i);
528
+ flags.onConflict = parsedValue.value;
529
+ i = parsedValue.nextIndex;
530
+ } else if (arg.startsWith('--on-conflict=')) {
531
+ flags.onConflict = readEqualsValue('--on-conflict', arg.slice('--on-conflict='.length));
532
+ } else if (arg === '--with') {
533
+ const parsedValue = readValue('--with', i);
534
+ if (parsedValue.value !== undefined) flags.with.push(parsedValue.value);
535
+ i = parsedValue.nextIndex;
536
+ } else if (arg.startsWith('--with=')) {
537
+ const value = readEqualsValue('--with', arg.slice('--with='.length));
538
+ if (value !== undefined) flags.with.push(value);
539
+ } else if (arg === '--without') {
540
+ const parsedValue = readValue('--without', i);
541
+ if (parsedValue.value !== undefined) flags.without.push(parsedValue.value);
542
+ i = parsedValue.nextIndex;
543
+ } else if (arg.startsWith('--without=')) {
371
544
  const value = readEqualsValue('--without', arg.slice('--without='.length));
372
545
  if (value !== undefined) flags.without.push(value);
373
546
  } else if (arg === '--recommend') {
@@ -384,30 +557,48 @@ function parseArgs(args) {
384
557
  flags.recommend.push(value);
385
558
  }
386
559
  } else if (arg === '--preset') {
387
- const parsedValue = readValue('--preset', i);
388
- flags.preset = parsedValue.value;
389
- i = parsedValue.nextIndex;
390
- } else if (arg.startsWith('--preset=')) {
391
- flags.preset = readEqualsValue('--preset', arg.slice('--preset='.length));
392
- } else if (arg.startsWith('-')) {
393
- errors.push(`Unknown flag "${arg}"`);
394
- } else {
395
- positionals.push(arg);
396
- }
397
- }
398
-
399
- return { flags, positionals, errors };
400
- }
401
-
402
- function printOptions() {
403
- const catalog = getOptionalCatalog();
404
-
405
- console.log('');
406
- console.log(pc.bold('Optional workflow skills:'));
407
- for (const skill of catalog.skills) {
408
- console.log(` ${pc.cyan(skill.id)} - ${skill.description}`);
409
- }
410
-
560
+ const parsedValue = readValue('--preset', i);
561
+ flags.preset = parsedValue.value;
562
+ i = parsedValue.nextIndex;
563
+ } else if (arg.startsWith('--preset=')) {
564
+ flags.preset = readEqualsValue('--preset', arg.slice('--preset='.length));
565
+ } else if (arg === '--install-scope') {
566
+ const parsedValue = readValue('--install-scope', i);
567
+ flags.installScope = parsedValue.value;
568
+ i = parsedValue.nextIndex;
569
+ } else if (arg.startsWith('--install-scope=')) {
570
+ flags.installScope = readEqualsValue('--install-scope', arg.slice('--install-scope='.length));
571
+ } else if (arg === '--global-dir') {
572
+ const parsedValue = readValue('--global-dir', i);
573
+ flags.globalDir = parsedValue.value;
574
+ i = parsedValue.nextIndex;
575
+ } else if (arg.startsWith('--global-dir=')) {
576
+ flags.globalDir = readEqualsValue('--global-dir', arg.slice('--global-dir='.length));
577
+ } else if (arg === '--host-global-dir') {
578
+ const parsedValue = readValue('--host-global-dir', i);
579
+ flags.hostGlobalDir = parsedValue.value;
580
+ i = parsedValue.nextIndex;
581
+ } else if (arg.startsWith('--host-global-dir=')) {
582
+ flags.hostGlobalDir = readEqualsValue('--host-global-dir', arg.slice('--host-global-dir='.length));
583
+ } else if (arg.startsWith('-')) {
584
+ errors.push(`Unknown flag "${arg}"`);
585
+ } else {
586
+ positionals.push(arg);
587
+ }
588
+ }
589
+
590
+ return { flags, positionals, errors };
591
+ }
592
+
593
+ function printOptions() {
594
+ const catalog = getOptionalCatalog();
595
+
596
+ console.log('');
597
+ console.log(pc.bold('Optional workflow skills:'));
598
+ for (const skill of catalog.skills) {
599
+ console.log(` ${pc.cyan(skill.id)} - ${skill.description}`);
600
+ }
601
+
411
602
  console.log('');
412
603
  console.log(pc.bold('Presets:'));
413
604
  for (const [name, skills] of Object.entries(catalog.presets)) {
@@ -423,42 +614,55 @@ function printOptions() {
423
614
  }
424
615
  console.log('');
425
616
  }
426
-
427
- function printSummary(summary) {
428
- console.log(` created ${pc.green(summary.created)}`);
429
- console.log(` skipped ${pc.yellow(summary.skipped)}`);
430
- console.log(` backed up ${pc.cyan(summary.backedUp)}`);
431
- console.log(` overwritten ${pc.cyan(summary.overwritten)}`);
432
- console.log(` conflicts ${summary.conflicts > 0 ? pc.red(summary.conflicts) : pc.dim(summary.conflicts)}`);
433
- console.log(` directories ${pc.dim(summary.mkdir)}`);
434
- console.log('');
435
- }
436
-
437
- function printPlan(plan) {
438
- for (const [label, files] of Object.entries(plan)) {
439
- if (!files.length) continue;
440
- console.log(` ${label}:`);
441
- for (const file of files) {
442
- console.log(` - ${file}`);
443
- }
444
- }
445
- }
446
-
447
- function printWarnings(result) {
448
- if (!result.warnings.length) return;
449
-
450
- console.log(pc.yellow('\nWarning(s):'));
451
- for (const warning of result.warnings) {
452
- console.log(pc.yellow(` - ${warning}`));
453
- }
454
- }
455
-
617
+
618
+ function printSummary(summary) {
619
+ console.log(` created ${pc.green(summary.created)}`);
620
+ console.log(` skipped ${pc.yellow(summary.skipped)}`);
621
+ console.log(` backed up ${pc.cyan(summary.backedUp)}`);
622
+ console.log(` overwritten ${pc.cyan(summary.overwritten)}`);
623
+ console.log(` conflicts ${summary.conflicts > 0 ? pc.red(summary.conflicts) : pc.dim(summary.conflicts)}`);
624
+ console.log(` directories ${pc.dim(summary.mkdir)}`);
625
+ console.log('');
626
+ }
627
+
628
+ function printPlan(plan) {
629
+ for (const [label, files] of Object.entries(plan)) {
630
+ if (!files.length) continue;
631
+ console.log(` ${label}:`);
632
+ for (const file of files) {
633
+ console.log(` - ${file}`);
634
+ }
635
+ }
636
+ }
637
+
638
+ function printHostPlans(hostPlans = [], hostSummary = [], { summaryOnly = false } = {}) {
639
+ if (!hostPlans?.length) return;
640
+
641
+ const summariesByHost = new Map((hostSummary || []).map(item => [item.host, item.summary]));
642
+ console.log(pc.bold('\nHost-global copies:'));
643
+ for (const hostPlan of hostPlans) {
644
+ console.log(` ${hostPlan.host} ${pc.cyan(hostPlan.root)}`);
645
+ const summary = summariesByHost.get(hostPlan.host);
646
+ if (summary) printSummary(summary);
647
+ if (!summaryOnly) printPlan(hostPlan.plan);
648
+ }
649
+ }
650
+
651
+ function printWarnings(result) {
652
+ if (!result.warnings.length) return;
653
+
654
+ console.log(pc.yellow('\nWarning(s):'));
655
+ for (const warning of result.warnings) {
656
+ console.log(pc.yellow(` - ${warning}`));
657
+ }
658
+ }
659
+
456
660
  function printJsonResult(result) {
457
661
  // Remove `created` array from output — it is already in the plan, avoid duplication
458
- const { created, ...rest } = result;
459
- console.log(JSON.stringify(rest, null, 2));
460
- if (!result.success) {
461
- process.exit(1);
662
+ const { created, globalCreated, hostCreated, ...rest } = result;
663
+ console.log(JSON.stringify(rest, null, 2));
664
+ if (!result.success) {
665
+ process.exit(1);
462
666
  }
463
667
  }
464
668
 
@@ -641,8 +845,8 @@ function scanTarget(targetDir) {
641
845
  const hasClaude = isDirectory && fs.existsSync(path.join(resolvedDir, 'CLAUDE.md'));
642
846
  const hasAgents = isDirectory && fs.existsSync(path.join(resolvedDir, 'AGENTS.md'));
643
847
  const hasAgentSkills = isDirectory && fs.existsSync(path.join(resolvedDir, '.agents'));
644
- const hasCodex = isDirectory && fs.existsSync(path.join(resolvedDir, '.codex'));
645
- const hasOpencode = isDirectory && (fs.existsSync(path.join(resolvedDir, '.opencode')) || fs.existsSync(path.join(resolvedDir, 'opencode.json')));
848
+ const hasCodex = isDirectory && fs.existsSync(path.join(resolvedDir, '.codex'));
849
+ const hasOpencode = isDirectory && (fs.existsSync(path.join(resolvedDir, '.opencode')) || fs.existsSync(path.join(resolvedDir, 'opencode.json')));
646
850
  const hasDocs = isDirectory && fs.existsSync(path.join(resolvedDir, 'docs'));
647
851
  const hasReadme = isDirectory && fs.existsSync(path.join(resolvedDir, 'README.md'));
648
852
  const hasPackageJson = isDirectory && fs.existsSync(path.join(resolvedDir, 'package.json'));
@@ -660,8 +864,8 @@ function scanTarget(targetDir) {
660
864
  hasClaude,
661
865
  hasAgents,
662
866
  hasAgentSkills,
663
- hasCodex,
664
- hasOpencode,
867
+ hasCodex,
868
+ hasOpencode,
665
869
  hasDocs,
666
870
  hasReadme,
667
871
  hasPackageJson,
@@ -689,8 +893,8 @@ function createJsonScan(scan) {
689
893
  hasClaude: scan.hasClaude,
690
894
  hasAgents: scan.hasAgents,
691
895
  hasAgentSkills: scan.hasAgentSkills,
692
- hasCodex: scan.hasCodex,
693
- hasOpencode: scan.hasOpencode,
896
+ hasCodex: scan.hasCodex,
897
+ hasOpencode: scan.hasOpencode,
694
898
  hasDocs: scan.hasDocs,
695
899
  hasReadme: scan.hasReadme,
696
900
  hasPackageJson: scan.hasPackageJson,
@@ -703,12 +907,44 @@ function createJsonScan(scan) {
703
907
  }
704
908
 
705
909
  function createAgentGuidance(result, { projectName, targetDir, options, scan }) {
706
- const attentionFiles = [...new Set([
910
+ const projectAttentionFiles = [...new Set([
707
911
  ...(result.plan?.conflict || []),
708
912
  ...(result.plan?.skip || []),
709
913
  ])].sort();
710
- const aiMergeRequired = attentionFiles.map(file => createFileGuidance(file));
711
- const hasBlockingConflicts = (result.plan?.conflict || []).length > 0;
914
+ const globalAttentionFiles = [...new Set([
915
+ ...(result.globalPlan?.conflict || []),
916
+ ...(result.globalPlan?.skip || []),
917
+ ])].sort();
918
+ const hostAttentionFiles = (result.hostPlans || []).flatMap(hostPlan => (
919
+ [...new Set([
920
+ ...(hostPlan.plan?.conflict || []),
921
+ ...(hostPlan.plan?.skip || []),
922
+ ])].sort().map(file => ({ host: hostPlan.host, root: hostPlan.root, file }))
923
+ ));
924
+ const aiMergeRequired = [
925
+ ...projectAttentionFiles.map(file => createFileGuidance(file)),
926
+ ...globalAttentionFiles.map(file => {
927
+ const guidance = createFileGuidance(file);
928
+ return {
929
+ ...guidance,
930
+ file: `global:${guidance.file}`,
931
+ scope: 'global-runtime',
932
+ reason: `Global runtime file in ${result.globalDir || 'the selected global directory'} needs review. ${guidance.reason}`,
933
+ };
934
+ }),
935
+ ...hostAttentionFiles.map(({ host, root, file }) => {
936
+ const guidance = createFileGuidance(file);
937
+ return {
938
+ ...guidance,
939
+ file: `host:${host}:${guidance.file}`,
940
+ scope: `host-global:${host}`,
941
+ reason: `Host-global ${host} file in ${root} needs review. ${guidance.reason}`,
942
+ };
943
+ }),
944
+ ];
945
+ const hasBlockingConflicts = (result.plan?.conflict || []).length > 0
946
+ || (result.globalPlan?.conflict || []).length > 0
947
+ || (result.hostPlans || []).some(hostPlan => (hostPlan.plan?.conflict || []).length > 0);
712
948
  const safeMergeCommand = commandFor(projectName, targetDir, {
713
949
  ...options,
714
950
  dryRun: false,
@@ -769,6 +1005,14 @@ function createAgentGuidance(result, { projectName, targetDir, options, scan })
769
1005
  mkdir: result.plan?.mkdir?.length || 0,
770
1006
  backup: result.plan?.backup?.length || 0,
771
1007
  overwrite: result.plan?.overwrite?.length || 0,
1008
+ globalCreate: result.globalPlan?.create?.length || 0,
1009
+ globalMkdir: result.globalPlan?.mkdir?.length || 0,
1010
+ globalBackup: result.globalPlan?.backup?.length || 0,
1011
+ globalOverwrite: result.globalPlan?.overwrite?.length || 0,
1012
+ hostCreate: (result.hostPlans || []).reduce((sum, hostPlan) => sum + (hostPlan.plan?.create?.length || 0), 0),
1013
+ hostMkdir: (result.hostPlans || []).reduce((sum, hostPlan) => sum + (hostPlan.plan?.mkdir?.length || 0), 0),
1014
+ hostBackup: (result.hostPlans || []).reduce((sum, hostPlan) => sum + (hostPlan.plan?.backup?.length || 0), 0),
1015
+ hostOverwrite: (result.hostPlans || []).reduce((sum, hostPlan) => sum + (hostPlan.plan?.overwrite?.length || 0), 0),
772
1016
  },
773
1017
  aiMergeRequired,
774
1018
  next,
@@ -839,6 +1083,9 @@ function commandFor(projectName, targetDir, options) {
839
1083
  if (options.withoutOptions?.length) args.push('--without', options.withoutOptions.join(','));
840
1084
  if (options.externalOptions?.length) args.push('--recommend', options.externalOptions.join(','));
841
1085
  if (options.preset) args.push('--preset', options.preset);
1086
+ if (options.installScope && options.installScope !== 'project') args.push('--install-scope', options.installScope);
1087
+ if (options.globalDir) args.push('--global-dir', options.globalDir);
1088
+ if (options.hostGlobalDir) args.push('--host-global-dir', options.hostGlobalDir);
842
1089
  if (options.json) args.push('--json');
843
1090
 
844
1091
  return args.map(shellQuoteArg).join(' ');
@@ -864,7 +1111,7 @@ function printScan(scan) {
864
1111
  if (scan.hasClaude) console.log(` CLAUDE.md ${pc.yellow('exists')}`);
865
1112
  if (scan.hasAgents) console.log(` AGENTS.md ${pc.yellow('exists')}`);
866
1113
  if (scan.hasAgentSkills) console.log(` .agents/ ${pc.yellow('exists')}`);
867
- if (scan.hasCodex) console.log(` .codex/ ${pc.yellow('exists')}`);
868
- if (scan.hasOpencode) console.log(` opencode ${pc.yellow('exists')} ${pc.dim('(.opencode/ or opencode.json)')}`);
1114
+ if (scan.hasCodex) console.log(` .codex/ ${pc.yellow('exists')}`);
1115
+ if (scan.hasOpencode) console.log(` opencode ${pc.yellow('exists')} ${pc.dim('(.opencode/ or opencode.json)')}`);
869
1116
  if (scan.hasDocs) console.log(` docs/ ${pc.dim('project-owned; Harness stays in Harness/')}`);
870
1117
  }