create-harness-vibe-coding 0.8.18 → 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 (128) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -0
  3. package/package.json +5 -2
  4. package/src/index.js +383 -267
  5. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  6. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  7. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  8. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  9. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  10. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  11. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  12. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  13. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  14. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  15. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  16. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  17. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  18. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  19. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  20. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  21. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  22. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  23. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  24. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  25. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  26. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  27. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  28. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  29. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  30. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  31. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  32. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  33. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  34. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  35. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  36. package/src/ui/dist/index.html +18 -0
  37. package/src/ui/index.html +17 -0
  38. package/src/ui/package.json +33 -0
  39. package/src/ui/pnpm-lock.yaml +1582 -0
  40. package/src/ui/pnpm-workspace.yaml +2 -0
  41. package/src/ui/src/App.tsx +86 -0
  42. package/src/ui/src/api.ts +93 -0
  43. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  44. package/src/ui/src/components/Footer.tsx +69 -0
  45. package/src/ui/src/components/Header.tsx +175 -0
  46. package/src/ui/src/components/LoadingView.tsx +22 -0
  47. package/src/ui/src/components/RolesRoute.tsx +169 -0
  48. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  49. package/src/ui/src/components/TaskList.tsx +388 -0
  50. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  51. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  52. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  53. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  54. package/src/ui/src/index.css +281 -0
  55. package/src/ui/src/main.tsx +11 -0
  56. package/src/ui/src/types.ts +108 -0
  57. package/src/ui/tsconfig.json +21 -0
  58. package/src/ui/vite.config.ts +19 -0
  59. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  60. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  61. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  62. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  63. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  64. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  65. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  66. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  67. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  68. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  69. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  70. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  71. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  72. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  73. package/src/wf-ui-server/a2a-store.mjs +296 -0
  74. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  75. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  76. package/src/wf-ui-server/runtime-config.mjs +153 -0
  77. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  78. package/src/wf-ui-server/security.mjs +67 -0
  79. package/src/wf-ui-server/server.mjs +849 -0
  80. package/src/wf-ui-server/session-registry.mjs +204 -0
  81. package/src/wf-ui-server/settings.mjs +100 -0
  82. package/src/wf-ui-server/task-parser.mjs +133 -0
  83. package/src/wf-ui-server/terminal-store.mjs +225 -0
  84. package/src/wf-ui-server/token.mjs +41 -0
  85. package/src/wf-ui-server/ws-events.mjs +354 -0
  86. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  87. package/templates/common/.claude/commands/wf-help.md +1 -0
  88. package/templates/common/.claude/commands/wf-task-list.md +2 -0
  89. package/templates/common/.claude/commands/wf-ui.md +26 -0
  90. package/templates/common/.claude/commands/wf-update.md +54 -9
  91. package/templates/common/.claude/rules/ecc/common.md +1 -1
  92. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  93. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  94. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  95. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  96. package/templates/common/.harness-version +63 -39
  97. package/templates/common/.opencode/commands/wf-help.md +1 -0
  98. package/templates/common/.opencode/commands/wf-task-list.md +2 -0
  99. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  100. package/templates/common/.opencode/commands/wf-update.md +54 -9
  101. package/templates/common/CLAUDE.md +1 -1
  102. package/templates/common/Harness/MEMORY.md +2 -0
  103. package/templates/common/Harness/README.md +6 -4
  104. package/templates/common/Harness/a2a/role-graph.json +79 -0
  105. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  106. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  107. package/templates/common/Harness/ownership.manifest.json +57 -2
  108. package/templates/common/Harness/scripts/README.md +134 -0
  109. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  110. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  111. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  112. package/templates/common/Harness/scripts/task-state.mjs +556 -23
  113. package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
  114. package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
  115. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  116. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  117. package/templates/common/Harness/settings.json +35 -0
  118. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
  119. package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
  120. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  121. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  122. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  123. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  124. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  125. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  126. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  127. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  128. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
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
7
  import { askConflictPolicy, askInstallScope, askOptionalSelections, askProjectName, askTargetDir } from './prompts.js';
8
8
  import { generate, getOptionalCatalog } from './generator.js';
@@ -10,37 +10,55 @@ import { generate, getOptionalCatalog } from './generator.js';
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');
@@ -52,35 +70,35 @@ if (showHelp) {
52
70
  console.log(' --json Output machine-readable JSON (use with --dry-run for planning)');
53
71
  console.log('');
54
72
  console.log(' Interactive mode offers checkbox selection for optional workflows and external recommendations.');
55
- console.log('');
56
- console.log(' Examples:');
57
- console.log(' npx create-harness-vibe-coding@latest');
58
- console.log(' npx create-harness-vibe-coding@latest -y');
59
- console.log(' npx create-harness-vibe-coding@latest my-project');
60
- console.log(' npx create-harness-vibe-coding@latest my-project ./dist/my-project -y');
61
- console.log(' npx create-harness-vibe-coding@latest legacy ./legacy -y --dry-run');
62
- 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');
63
81
  console.log(' npx create-harness-vibe-coding@latest web ./web -y --with ts-react-frontend,ui-ux-review');
64
82
  console.log(' npx create-harness-vibe-coding@latest web ./web -y --preset web-app');
65
83
  console.log(' npx create-harness-vibe-coding@latest api ./api -y --preset fullstack --without github-pr-review');
66
84
  console.log(' npx create-harness-vibe-coding@latest app ./app -y --install-scope global');
67
- console.log('');
68
- process.exit(0);
69
- }
70
-
71
- if (parsed.flags.listOptions) {
72
- printOptions();
73
- process.exit(0);
74
- }
75
-
76
- // Positional args (non-flag)
77
- const positional = parsed.positionals;
78
- const argName = positional[0];
79
- const argDir = positional[1];
80
- const generationOptions = {
81
- dryRun: Boolean(parsed.flags.dryRun),
82
- onConflict: parsed.flags.onConflict || 'fail',
83
- withOptions: parsed.flags.with || [],
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 || [],
84
102
  withoutOptions: parsed.flags.without || [],
85
103
  externalOptions: parsed.flags.recommend || [],
86
104
  preset: parsed.flags.preset,
@@ -89,10 +107,10 @@ const generationOptions = {
89
107
  hostGlobalDir: parsed.flags.hostGlobalDir,
90
108
  json: Boolean(parsed.flags.json),
91
109
  };
92
-
93
- const DEFAULT_NAME = 'my-vibe-project';
94
-
95
- // --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
96
114
  if (generationOptions.json) {
97
115
  const projectName = argName || DEFAULT_NAME;
98
116
  const targetDir = argDir || `./${projectName}`;
@@ -116,20 +134,22 @@ if (generationOptions.json) {
116
134
  scan,
117
135
  });
118
136
  printJsonResult(result);
119
- // printJsonResult exits with 1 on failure; we only reach here on success
120
- process.exit(0);
121
- }
122
-
123
- console.log('');
124
- console.log(pc.magenta('╔══════════════════════════════════════════╗'));
125
- console.log(pc.magenta('║ create-harness-vibe-coding ║'));
126
- console.log(pc.magenta('║ 0-1 Product Harness Scaffold ║'));
127
- console.log(pc.magenta('╚══════════════════════════════════════════╝'));
128
- console.log('');
129
-
130
- let projectName, targetDir;
131
-
132
- // 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
133
153
  if (argName || skipPrompts) {
134
154
  projectName = argName || DEFAULT_NAME;
135
155
  targetDir = argDir || `./${projectName}`;
@@ -157,39 +177,39 @@ if (argName || skipPrompts) {
157
177
  ? 'project bridge/state/settings + global runtime + Claude/Codex/OpenCode host copies'
158
178
  : 'CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, .opencode/, opencode.json, tests/';
159
179
  console.log(` Creates ${pc.cyan(creates)}`);
160
- if (generationOptions.dryRun) {
180
+ if (generationOptions.dryRun) {
161
181
  console.log(` Mode ${pc.yellow('dry-run')}`);
162
- }
163
- console.log(` Conflicts ${pc.cyan(generationOptions.onConflict)}`);
164
- if (generationOptions.withOptions.length > 0) {
165
- console.log(` Optional ${pc.cyan(generationOptions.withOptions.join(','))}`);
166
- }
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
+ }
167
187
  if (generationOptions.withoutOptions.length > 0) {
168
188
  console.log(` Without ${pc.cyan(generationOptions.withoutOptions.join(','))}`);
169
189
  }
170
190
  if (generationOptions.externalOptions.length > 0) {
171
191
  console.log(` Recommend ${pc.cyan(generationOptions.externalOptions.join(','))}`);
172
192
  }
173
- if (generationOptions.preset) {
174
- console.log(` Preset ${pc.cyan(generationOptions.preset)}`);
175
- }
176
- if (skipPrompts) {
177
- console.log(` Mode ${pc.dim('non-interactive (-y)')}`);
178
- }
179
- console.log(pc.dim('────────────────────────────────────────────'));
180
- console.log('');
181
-
182
- const result = generate({ projectName, targetDir, ...generationOptions });
183
- printResult(result, targetDir);
184
- } else {
185
- // Interactive mode
186
- try {
187
- projectName = await askProjectName();
188
- } catch {
189
- projectName = 'my-vibe-project';
190
- console.log(pc.dim(` Project: ${projectName} (default)`));
191
- }
192
-
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
+
193
213
  try {
194
214
  targetDir = await askTargetDir(projectName);
195
215
  } catch {
@@ -240,9 +260,9 @@ if (argName || skipPrompts) {
240
260
  console.log(pc.dim(' Optional: none (default)'));
241
261
  }
242
262
  }
243
-
244
- console.log('');
245
- console.log(pc.dim('────────────────────────────────────────────'));
263
+
264
+ console.log('');
265
+ console.log(pc.dim('────────────────────────────────────────────'));
246
266
  console.log(` Project ${pc.green(projectName)}`);
247
267
  console.log(` Directory ${pc.green(targetDir)}`);
248
268
  console.log(` Scope ${pc.green(generationOptions.installScope)}`);
@@ -263,14 +283,14 @@ if (argName || skipPrompts) {
263
283
  if (generationOptions.externalOptions.length > 0) {
264
284
  console.log(` Recommend ${pc.cyan(generationOptions.externalOptions.join(','))}`);
265
285
  }
266
- console.log(pc.dim('────────────────────────────────────────────'));
267
- console.log('');
268
-
269
- const preview = generate({ projectName, targetDir, ...generationOptions, dryRun: true });
270
- if (!preview.success) {
271
- printResult(preview, targetDir);
272
- }
273
-
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
+
274
294
  console.log(pc.yellow('Planned changes: no files have been written yet.'));
275
295
  console.log(pc.bold('Project plan:'));
276
296
  printSummary(preview.summary);
@@ -284,34 +304,34 @@ if (argName || skipPrompts) {
284
304
  printHostPlans(preview.hostPlans, preview.hostSummary);
285
305
  printWarnings(preview);
286
306
  console.log('');
287
-
288
- if (generationOptions.dryRun) {
289
- process.exit(0);
290
- }
291
-
292
- let proceed = true;
293
- try {
294
- proceed = await p.confirm({
295
- message: 'Confirm generation with this plan?',
296
- initialValue: true,
297
- });
298
- if (p.isCancel(proceed)) proceed = false;
299
- } catch {
300
- proceed = true;
301
- }
302
-
303
- if (!proceed) {
304
- p.cancel('Cancelled');
305
- process.exit(0);
306
- }
307
-
308
- console.log('');
309
- const result = generate({ projectName, targetDir, ...generationOptions });
310
- printResult(result, targetDir);
311
- }
312
-
313
- function printResult(result, targetDir) {
314
- if (result.success) {
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) {
315
335
  if (result.dryRun) {
316
336
  console.log(pc.yellow('\nDry run: no files or directories were written.'));
317
337
  console.log(pc.bold('Project plan:'));
@@ -325,14 +345,14 @@ function printResult(result, targetDir) {
325
345
  }
326
346
  printHostPlans(result.hostPlans, result.hostSummary);
327
347
  if (result.warnings.length > 0) {
328
- console.log(pc.yellow('\nWarning(s):'));
329
- for (const warning of result.warnings) {
330
- console.log(pc.yellow(` - ${warning}`));
331
- }
332
- }
333
- console.log('');
334
- return;
335
- }
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
+ }
336
356
 
337
357
  console.log(pc.green('\nGeneration complete.\n'));
338
358
  console.log(pc.bold('Project install:'));
@@ -345,86 +365,182 @@ function printResult(result, targetDir) {
345
365
  printHostPlans(result.hostPlans, result.hostSummary, { summaryOnly: true });
346
366
 
347
367
  printWarnings(result);
348
-
368
+
349
369
  console.log(pc.bold('Next steps:'));
350
370
  console.log(` ${pc.cyan(`cd ${targetDir}`)}`);
351
- console.log(` ${pc.cyan('claude')} # Start Claude Code`);
352
- console.log(` ${pc.cyan('codex')} # Or start Codex`);
353
- 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`);
354
374
  console.log(` Tell your agent: "${pc.yellow('Read Harness/specs/guides/SETUP.md. Bootstrap this project from idea to first vertical slice.')}"`);
355
- console.log('');
375
+ console.log('');
356
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.'));
357
- console.log('');
358
-
359
- } else {
360
- console.log(pc.red('\nGeneration failed:'));
361
- for (const err of result.errors) {
362
- console.log(pc.red(` - ${err}`));
363
- }
364
- process.exit(1);
365
- }
366
- }
367
-
368
- function parseArgs(args) {
369
- const flags = {
370
- with: [],
371
- without: [],
372
- };
373
- const positionals = [];
374
- const errors = [];
375
-
376
- function readValue(flagName, index) {
377
- const value = args[index + 1];
378
- if (!value || value.startsWith('-')) {
379
- errors.push(`${flagName} requires a value`);
380
- return { value: undefined, nextIndex: index };
381
- }
382
- return { value, nextIndex: index + 1 };
383
- }
384
-
385
- function readEqualsValue(flagName, value) {
386
- if (!value || value.startsWith('-')) {
387
- errors.push(`${flagName} requires a value`);
388
- return undefined;
389
- }
390
- return value;
391
- }
392
-
393
- for (let i = 0; i < args.length; i += 1) {
394
- const arg = args[i];
395
-
396
- if (arg === '-h') {
397
- flags.h = true;
398
- } else if (arg === '--help') {
399
- flags.help = true;
400
- } else if (arg === '-y') {
401
- flags.y = true;
402
- } else if (arg === '--yes') {
403
- flags.yes = true;
404
- } else if (arg === '--dry-run') {
405
- 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;
406
522
  } else if (arg === '--list-options') {
407
523
  flags.listOptions = true;
408
524
  } else if (arg === '--json') {
409
525
  flags.json = true;
410
- } else if (arg === '--on-conflict') {
411
- const parsedValue = readValue('--on-conflict', i);
412
- flags.onConflict = parsedValue.value;
413
- i = parsedValue.nextIndex;
414
- } else if (arg.startsWith('--on-conflict=')) {
415
- flags.onConflict = readEqualsValue('--on-conflict', arg.slice('--on-conflict='.length));
416
- } else if (arg === '--with') {
417
- const parsedValue = readValue('--with', i);
418
- if (parsedValue.value !== undefined) flags.with.push(parsedValue.value);
419
- i = parsedValue.nextIndex;
420
- } else if (arg.startsWith('--with=')) {
421
- const value = readEqualsValue('--with', arg.slice('--with='.length));
422
- if (value !== undefined) flags.with.push(value);
423
- } else if (arg === '--without') {
424
- const parsedValue = readValue('--without', i);
425
- if (parsedValue.value !== undefined) flags.without.push(parsedValue.value);
426
- i = parsedValue.nextIndex;
427
- } 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=')) {
428
544
  const value = readEqualsValue('--without', arg.slice('--without='.length));
429
545
  if (value !== undefined) flags.without.push(value);
430
546
  } else if (arg === '--recommend') {
@@ -466,23 +582,23 @@ function parseArgs(args) {
466
582
  flags.hostGlobalDir = readEqualsValue('--host-global-dir', arg.slice('--host-global-dir='.length));
467
583
  } else if (arg.startsWith('-')) {
468
584
  errors.push(`Unknown flag "${arg}"`);
469
- } else {
470
- positionals.push(arg);
471
- }
472
- }
473
-
474
- return { flags, positionals, errors };
475
- }
476
-
477
- function printOptions() {
478
- const catalog = getOptionalCatalog();
479
-
480
- console.log('');
481
- console.log(pc.bold('Optional workflow skills:'));
482
- for (const skill of catalog.skills) {
483
- console.log(` ${pc.cyan(skill.id)} - ${skill.description}`);
484
- }
485
-
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
+
486
602
  console.log('');
487
603
  console.log(pc.bold('Presets:'));
488
604
  for (const [name, skills] of Object.entries(catalog.presets)) {
@@ -498,24 +614,24 @@ function printOptions() {
498
614
  }
499
615
  console.log('');
500
616
  }
501
-
502
- function printSummary(summary) {
503
- console.log(` created ${pc.green(summary.created)}`);
504
- console.log(` skipped ${pc.yellow(summary.skipped)}`);
505
- console.log(` backed up ${pc.cyan(summary.backedUp)}`);
506
- console.log(` overwritten ${pc.cyan(summary.overwritten)}`);
507
- console.log(` conflicts ${summary.conflicts > 0 ? pc.red(summary.conflicts) : pc.dim(summary.conflicts)}`);
508
- console.log(` directories ${pc.dim(summary.mkdir)}`);
509
- console.log('');
510
- }
511
-
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
+
512
628
  function printPlan(plan) {
513
629
  for (const [label, files] of Object.entries(plan)) {
514
630
  if (!files.length) continue;
515
- console.log(` ${label}:`);
516
- for (const file of files) {
517
- console.log(` - ${file}`);
518
- }
631
+ console.log(` ${label}:`);
632
+ for (const file of files) {
633
+ console.log(` - ${file}`);
634
+ }
519
635
  }
520
636
  }
521
637
 
@@ -533,20 +649,20 @@ function printHostPlans(hostPlans = [], hostSummary = [], { summaryOnly = false
533
649
  }
534
650
 
535
651
  function printWarnings(result) {
536
- if (!result.warnings.length) return;
537
-
538
- console.log(pc.yellow('\nWarning(s):'));
539
- for (const warning of result.warnings) {
540
- console.log(pc.yellow(` - ${warning}`));
541
- }
542
- }
543
-
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
+
544
660
  function printJsonResult(result) {
545
661
  // Remove `created` array from output — it is already in the plan, avoid duplication
546
662
  const { created, globalCreated, hostCreated, ...rest } = result;
547
- console.log(JSON.stringify(rest, null, 2));
548
- if (!result.success) {
549
- process.exit(1);
663
+ console.log(JSON.stringify(rest, null, 2));
664
+ if (!result.success) {
665
+ process.exit(1);
550
666
  }
551
667
  }
552
668
 
@@ -729,8 +845,8 @@ function scanTarget(targetDir) {
729
845
  const hasClaude = isDirectory && fs.existsSync(path.join(resolvedDir, 'CLAUDE.md'));
730
846
  const hasAgents = isDirectory && fs.existsSync(path.join(resolvedDir, 'AGENTS.md'));
731
847
  const hasAgentSkills = isDirectory && fs.existsSync(path.join(resolvedDir, '.agents'));
732
- const hasCodex = isDirectory && fs.existsSync(path.join(resolvedDir, '.codex'));
733
- 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')));
734
850
  const hasDocs = isDirectory && fs.existsSync(path.join(resolvedDir, 'docs'));
735
851
  const hasReadme = isDirectory && fs.existsSync(path.join(resolvedDir, 'README.md'));
736
852
  const hasPackageJson = isDirectory && fs.existsSync(path.join(resolvedDir, 'package.json'));
@@ -748,8 +864,8 @@ function scanTarget(targetDir) {
748
864
  hasClaude,
749
865
  hasAgents,
750
866
  hasAgentSkills,
751
- hasCodex,
752
- hasOpencode,
867
+ hasCodex,
868
+ hasOpencode,
753
869
  hasDocs,
754
870
  hasReadme,
755
871
  hasPackageJson,
@@ -777,8 +893,8 @@ function createJsonScan(scan) {
777
893
  hasClaude: scan.hasClaude,
778
894
  hasAgents: scan.hasAgents,
779
895
  hasAgentSkills: scan.hasAgentSkills,
780
- hasCodex: scan.hasCodex,
781
- hasOpencode: scan.hasOpencode,
896
+ hasCodex: scan.hasCodex,
897
+ hasOpencode: scan.hasOpencode,
782
898
  hasDocs: scan.hasDocs,
783
899
  hasReadme: scan.hasReadme,
784
900
  hasPackageJson: scan.hasPackageJson,
@@ -995,7 +1111,7 @@ function printScan(scan) {
995
1111
  if (scan.hasClaude) console.log(` CLAUDE.md ${pc.yellow('exists')}`);
996
1112
  if (scan.hasAgents) console.log(` AGENTS.md ${pc.yellow('exists')}`);
997
1113
  if (scan.hasAgentSkills) console.log(` .agents/ ${pc.yellow('exists')}`);
998
- if (scan.hasCodex) console.log(` .codex/ ${pc.yellow('exists')}`);
999
- 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)')}`);
1000
1116
  if (scan.hasDocs) console.log(` docs/ ${pc.dim('project-owned; Harness stays in Harness/')}`);
1001
1117
  }