vibe-forge 0.4.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.claude/commands/clear-attention.md +63 -63
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -102
  4. package/.claude/commands/forge.md +218 -171
  5. package/.claude/commands/need-help.md +77 -77
  6. package/.claude/commands/update-status.md +64 -64
  7. package/.claude/commands/worker-loop.md +106 -106
  8. package/.claude/hooks/worker-loop.js +217 -187
  9. package/.claude/scripts/setup-worker-loop.sh +45 -45
  10. package/.claude/settings.json +89 -0
  11. package/LICENSE +21 -21
  12. package/README.md +253 -232
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -240
  15. package/agents/architect/personality.md +260 -234
  16. package/agents/crucible/personality.md +362 -309
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -265
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -291
  21. package/agents/herald/personality.md +249 -247
  22. package/agents/loki/personality.md +108 -0
  23. package/agents/oracle/personality.md +284 -0
  24. package/agents/pixel/personality.md +140 -0
  25. package/agents/planning-hub/personality.md +473 -251
  26. package/agents/scribe/personality.md +253 -251
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/temper/personality.md +270 -0
  29. package/bin/cli.js +372 -325
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +507 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
  34. package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
  35. package/bin/dashboard/public/index.html +14 -0
  36. package/bin/dashboard/server.js +645 -0
  37. package/bin/forge-daemon.sh +477 -851
  38. package/bin/forge-setup.sh +661 -645
  39. package/bin/forge-spawn.sh +164 -164
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -387
  42. package/bin/lib/agents.sh +177 -177
  43. package/bin/lib/check-aliases.js +50 -0
  44. package/bin/lib/colors.sh +44 -44
  45. package/bin/lib/config.sh +347 -313
  46. package/bin/lib/constants.sh +241 -206
  47. package/bin/lib/daemon/budgets.sh +107 -0
  48. package/bin/lib/daemon/dependencies.sh +146 -0
  49. package/bin/lib/daemon/display.sh +128 -0
  50. package/bin/lib/daemon/notifications.sh +273 -0
  51. package/bin/lib/daemon/routing.sh +93 -0
  52. package/bin/lib/daemon/state.sh +163 -0
  53. package/bin/lib/daemon/sync.sh +103 -0
  54. package/bin/lib/database.sh +357 -305
  55. package/bin/lib/frontmatter.js +106 -0
  56. package/bin/lib/heimdall-setup.js +113 -0
  57. package/bin/lib/heimdall.js +265 -0
  58. package/bin/lib/json.sh +264 -258
  59. package/bin/lib/terminal.js +452 -446
  60. package/bin/lib/util.sh +126 -126
  61. package/bin/lib/vcs.js +349 -349
  62. package/config/agent-manifest.yaml +237 -243
  63. package/config/agents.json +207 -132
  64. package/config/task-template.md +159 -87
  65. package/config/task-types.yaml +111 -106
  66. package/config/templates/handoff-template.md +40 -0
  67. package/context/agent-overrides/README.md +41 -0
  68. package/context/architecture.md +42 -0
  69. package/context/modern-conventions.md +129 -129
  70. package/context/project-context-template.md +122 -122
  71. package/docs/agents.md +473 -409
  72. package/docs/architecture.md +194 -162
  73. package/docs/commands.md +451 -388
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -50
  76. package/.claude/settings.local.json +0 -33
  77. package/agents/forge-master/capabilities.md +0 -144
  78. package/agents/forge-master/context-template.md +0 -128
  79. package/agents/forge-master/personality.md +0 -138
  80. package/agents/sentinel/personality.md +0 -194
  81. package/context/forge-state.yaml +0 -19
  82. package/docs/TODO.md +0 -150
  83. package/docs/getting-started.md +0 -243
  84. package/docs/npm-publishing.md +0 -95
  85. package/docs/workflows/README.md +0 -32
  86. package/docs/workflows/azure-devops.md +0 -108
  87. package/docs/workflows/bitbucket.md +0 -104
  88. package/docs/workflows/git-only.md +0 -130
  89. package/docs/workflows/gitea.md +0 -168
  90. package/docs/workflows/github.md +0 -103
  91. package/docs/workflows/gitlab.md +0 -105
  92. package/docs/workflows.md +0 -454
  93. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  94. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  95. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  96. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  97. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  98. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  99. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  100. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  101. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  102. package/tasks/completed/CLEAN-001.md +0 -38
  103. package/tasks/completed/CLEAN-003.md +0 -47
  104. package/tasks/completed/CLEAN-004.md +0 -56
  105. package/tasks/completed/CLEAN-005.md +0 -75
  106. package/tasks/completed/CLEAN-006.md +0 -47
  107. package/tasks/completed/CLEAN-007.md +0 -34
  108. package/tasks/completed/CLEAN-008.md +0 -49
  109. package/tasks/completed/CLEAN-012.md +0 -58
  110. package/tasks/completed/CLEAN-013.md +0 -45
  111. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  112. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  113. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  114. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  115. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  116. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  117. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  118. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  119. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  120. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  121. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  122. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  123. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  124. package/tasks/pending/CLEAN-002.md +0 -29
  125. package/tasks/pending/CLEAN-009.md +0 -31
  126. package/tasks/pending/CLEAN-010.md +0 -30
  127. package/tasks/pending/CLEAN-011.md +0 -30
  128. package/tasks/pending/CLEAN-014.md +0 -32
  129. package/tasks/review/task-001.md +0 -78
@@ -1,446 +1,452 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Vibe Forge - Cross-Platform Terminal Detection & Spawning
4
- *
5
- * Detects available terminals and spawns new tabs/windows for worker agents.
6
- *
7
- * Supported terminals:
8
- * Windows: Windows Terminal, PowerShell
9
- * macOS: Terminal.app, iTerm2, Kitty
10
- * Linux: gnome-terminal, konsole, kitty, xterm
11
- *
12
- * Usage:
13
- * node terminal.js detect List available terminals
14
- * node terminal.js spawn <terminal> <title> <command>
15
- */
16
-
17
- const { execSync, spawn } = require('child_process');
18
- const fs = require('fs');
19
- const path = require('path');
20
- const os = require('os');
21
-
22
- // =============================================================================
23
- // Terminal Definitions
24
- // =============================================================================
25
-
26
- const TERMINALS = {
27
- // Windows
28
- 'windows-terminal': {
29
- name: 'Windows Terminal',
30
- platform: 'win32',
31
- detect: () => commandExists('wt') || commandExists('wt.exe'),
32
- spawn: (title, command, opts) => spawnWindowsTerminal(title, command, opts),
33
- },
34
- 'powershell': {
35
- name: 'PowerShell',
36
- platform: 'win32',
37
- detect: () => commandExists('pwsh') || commandExists('powershell'),
38
- spawn: (title, command, opts) => spawnPowerShell(title, command, opts),
39
- },
40
-
41
- // macOS
42
- 'terminal-app': {
43
- name: 'Terminal.app',
44
- platform: 'darwin',
45
- detect: () => fs.existsSync('/System/Applications/Utilities/Terminal.app'),
46
- spawn: (title, command, opts) => spawnTerminalApp(title, command, opts),
47
- },
48
- 'iterm2': {
49
- name: 'iTerm2',
50
- platform: 'darwin',
51
- detect: () => fs.existsSync('/Applications/iTerm.app'),
52
- spawn: (title, command, opts) => spawnITerm2(title, command, opts),
53
- },
54
- 'kitty-mac': {
55
- name: 'Kitty',
56
- platform: 'darwin',
57
- detect: () => commandExists('kitty') || fs.existsSync('/Applications/kitty.app'),
58
- spawn: (title, command, opts) => spawnKitty(title, command, opts),
59
- },
60
-
61
- // Linux
62
- 'gnome-terminal': {
63
- name: 'GNOME Terminal',
64
- platform: 'linux',
65
- detect: () => commandExists('gnome-terminal'),
66
- spawn: (title, command, opts) => spawnGnomeTerminal(title, command, opts),
67
- },
68
- 'konsole': {
69
- name: 'Konsole',
70
- platform: 'linux',
71
- detect: () => commandExists('konsole'),
72
- spawn: (title, command, opts) => spawnKonsole(title, command, opts),
73
- },
74
- 'kitty-linux': {
75
- name: 'Kitty',
76
- platform: 'linux',
77
- detect: () => commandExists('kitty'),
78
- spawn: (title, command, opts) => spawnKitty(title, command, opts),
79
- },
80
- 'xterm': {
81
- name: 'XTerm',
82
- platform: 'linux',
83
- detect: () => commandExists('xterm'),
84
- spawn: (title, command, opts) => spawnXterm(title, command, opts),
85
- },
86
- };
87
-
88
- // =============================================================================
89
- // Detection Helpers
90
- // =============================================================================
91
-
92
- function commandExists(cmd) {
93
- try {
94
- if (os.platform() === 'win32') {
95
- execSync(`where ${cmd}`, { stdio: 'pipe' });
96
- } else {
97
- execSync(`which ${cmd}`, { stdio: 'pipe' });
98
- }
99
- return true;
100
- } catch {
101
- return false;
102
- }
103
- }
104
-
105
- function getPlatform() {
106
- const p = os.platform();
107
- if (p === 'win32') return 'win32';
108
- if (p === 'darwin') return 'darwin';
109
- return 'linux'; // Treat all other Unix-likes as Linux
110
- }
111
-
112
- function detectTerminals() {
113
- const platform = getPlatform();
114
- const available = [];
115
-
116
- for (const [id, terminal] of Object.entries(TERMINALS)) {
117
- if (terminal.platform === platform) {
118
- try {
119
- if (terminal.detect()) {
120
- available.push({
121
- id,
122
- name: terminal.name,
123
- platform: terminal.platform,
124
- });
125
- }
126
- } catch {
127
- // Detection failed, skip this terminal
128
- }
129
- }
130
- }
131
-
132
- return available;
133
- }
134
-
135
- function getBestTerminal() {
136
- const available = detectTerminals();
137
- return available.length > 0 ? available[0] : null;
138
- }
139
-
140
- // =============================================================================
141
- // Spawn Functions - Windows
142
- // =============================================================================
143
-
144
- function spawnWindowsTerminal(title, command, opts = {}) {
145
- const { cwd, tabColor } = opts;
146
-
147
- // Build wt arguments
148
- const args = ['-w', '0', 'new-tab', '--title', title];
149
-
150
- if (tabColor) {
151
- args.push('--tabColor', tabColor);
152
- }
153
-
154
- // Determine bash path
155
- const bashPath = findGitBash();
156
-
157
- if (bashPath) {
158
- args.push(bashPath, '-c', command);
159
- } else {
160
- args.push('bash', '-c', command);
161
- }
162
-
163
- const child = spawn('wt', args, {
164
- detached: true,
165
- stdio: 'ignore',
166
- cwd: cwd || process.cwd(),
167
- shell: true,
168
- });
169
-
170
- child.unref();
171
- return { success: true, terminal: 'windows-terminal' };
172
- }
173
-
174
- function spawnPowerShell(title, command, opts = {}) {
175
- const { cwd } = opts;
176
-
177
- // PowerShell: Start-Process to open new window
178
- // We wrap in bash since the command expects bash
179
- const bashPath = findGitBash();
180
- const bashCmd = bashPath ? `& '${bashPath}' -c '${command.replace(/'/g, "''")}'` : `bash -c '${command.replace(/'/g, "''")}'`;
181
-
182
- const psCommand = `Start-Process pwsh -ArgumentList '-NoExit', '-Command', '${bashCmd.replace(/'/g, "''")}'`;
183
-
184
- const child = spawn('pwsh', ['-Command', psCommand], {
185
- detached: true,
186
- stdio: 'ignore',
187
- cwd: cwd || process.cwd(),
188
- shell: true,
189
- });
190
-
191
- child.unref();
192
- return { success: true, terminal: 'powershell' };
193
- }
194
-
195
- function findGitBash() {
196
- if (os.platform() !== 'win32') return null;
197
-
198
- const paths = [
199
- 'C:\\Program Files\\Git\\bin\\bash.exe',
200
- 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
201
- path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'bin', 'bash.exe'),
202
- ];
203
-
204
- for (const p of paths) {
205
- if (fs.existsSync(p)) {
206
- return p;
207
- }
208
- }
209
-
210
- // Try to find via where command
211
- try {
212
- const gitPath = execSync('where git', { stdio: 'pipe' }).toString().trim().split('\n')[0];
213
- const gitDir = path.dirname(path.dirname(gitPath));
214
- const bashPath = path.join(gitDir, 'bin', 'bash.exe');
215
- if (fs.existsSync(bashPath)) {
216
- return bashPath;
217
- }
218
- } catch {
219
- // Ignore
220
- }
221
-
222
- return null;
223
- }
224
-
225
- // =============================================================================
226
- // Spawn Functions - macOS
227
- // =============================================================================
228
-
229
- function spawnTerminalApp(title, command, opts = {}) {
230
- const { cwd } = opts;
231
- const workDir = cwd || process.cwd();
232
-
233
- // AppleScript to open new Terminal tab
234
- const script = `
235
- tell application "Terminal"
236
- activate
237
- do script "cd '${workDir.replace(/'/g, "'\\''")}' && ${command.replace(/"/g, '\\"')}"
238
- set custom title of front window to "${title}"
239
- end tell
240
- `;
241
-
242
- try {
243
- execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
244
- return { success: true, terminal: 'terminal-app' };
245
- } catch (err) {
246
- return { success: false, error: err.message, terminal: 'terminal-app' };
247
- }
248
- }
249
-
250
- function spawnITerm2(title, command, opts = {}) {
251
- const { cwd } = opts;
252
- const workDir = cwd || process.cwd();
253
-
254
- // AppleScript for iTerm2
255
- const script = `
256
- tell application "iTerm2"
257
- activate
258
- tell current window
259
- create tab with default profile
260
- tell current session
261
- write text "cd '${workDir.replace(/'/g, "'\\''")}' && ${command.replace(/"/g, '\\"')}"
262
- set name to "${title}"
263
- end tell
264
- end tell
265
- end tell
266
- `;
267
-
268
- try {
269
- execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
270
- return { success: true, terminal: 'iterm2' };
271
- } catch (err) {
272
- return { success: false, error: err.message, terminal: 'iterm2' };
273
- }
274
- }
275
-
276
- // =============================================================================
277
- // Spawn Functions - Linux & Cross-Platform
278
- // =============================================================================
279
-
280
- function spawnGnomeTerminal(title, command, opts = {}) {
281
- const { cwd } = opts;
282
-
283
- const child = spawn('gnome-terminal', [
284
- '--tab',
285
- '--title', title,
286
- '--', 'bash', '-c', `${command}; exec bash`
287
- ], {
288
- detached: true,
289
- stdio: 'ignore',
290
- cwd: cwd || process.cwd(),
291
- });
292
-
293
- child.unref();
294
- return { success: true, terminal: 'gnome-terminal' };
295
- }
296
-
297
- function spawnKonsole(title, command, opts = {}) {
298
- const { cwd } = opts;
299
-
300
- const child = spawn('konsole', [
301
- '--new-tab',
302
- '-p', `tabtitle=${title}`,
303
- '-e', 'bash', '-c', `${command}; exec bash`
304
- ], {
305
- detached: true,
306
- stdio: 'ignore',
307
- cwd: cwd || process.cwd(),
308
- });
309
-
310
- child.unref();
311
- return { success: true, terminal: 'konsole' };
312
- }
313
-
314
- function spawnKitty(title, command, opts = {}) {
315
- const { cwd } = opts;
316
-
317
- // Kitty can work in single-instance mode with remote control
318
- // or spawn new windows
319
- const child = spawn('kitty', [
320
- '--title', title,
321
- '--directory', cwd || process.cwd(),
322
- 'bash', '-c', `${command}; exec bash`
323
- ], {
324
- detached: true,
325
- stdio: 'ignore',
326
- });
327
-
328
- child.unref();
329
- return { success: true, terminal: 'kitty' };
330
- }
331
-
332
- function spawnXterm(title, command, opts = {}) {
333
- const { cwd } = opts;
334
-
335
- const child = spawn('xterm', [
336
- '-T', title,
337
- '-e', 'bash', '-c', `cd '${cwd || process.cwd()}' && ${command}; exec bash`
338
- ], {
339
- detached: true,
340
- stdio: 'ignore',
341
- });
342
-
343
- child.unref();
344
- return { success: true, terminal: 'xterm' };
345
- }
346
-
347
- // =============================================================================
348
- // Main Spawn Function
349
- // =============================================================================
350
-
351
- function spawnTerminal(terminalId, title, command, opts = {}) {
352
- const terminal = TERMINALS[terminalId];
353
-
354
- if (!terminal) {
355
- return { success: false, error: `Unknown terminal: ${terminalId}` };
356
- }
357
-
358
- try {
359
- return terminal.spawn(title, command, opts);
360
- } catch (err) {
361
- return { success: false, error: err.message, terminal: terminalId };
362
- }
363
- }
364
-
365
- // =============================================================================
366
- // CLI Interface
367
- // =============================================================================
368
-
369
- function main() {
370
- const args = process.argv.slice(2);
371
- const command = args[0];
372
-
373
- switch (command) {
374
- case 'detect': {
375
- const terminals = detectTerminals();
376
- console.log(JSON.stringify({ platform: getPlatform(), terminals }, null, 2));
377
- break;
378
- }
379
-
380
- case 'best': {
381
- const best = getBestTerminal();
382
- if (best) {
383
- console.log(JSON.stringify(best, null, 2));
384
- } else {
385
- console.log(JSON.stringify({ error: 'No supported terminal found' }));
386
- process.exit(1);
387
- }
388
- break;
389
- }
390
-
391
- case 'spawn': {
392
- const terminalId = args[1];
393
- const title = args[2];
394
- const cmd = args[3];
395
- const cwd = args[4];
396
- const tabColor = args[5];
397
-
398
- if (!terminalId || !title || !cmd) {
399
- console.error('Usage: terminal.js spawn <terminal-id> <title> <command> [cwd] [tabColor]');
400
- process.exit(1);
401
- }
402
-
403
- const result = spawnTerminal(terminalId, title, cmd, { cwd, tabColor });
404
- console.log(JSON.stringify(result));
405
- process.exit(result.success ? 0 : 1);
406
- break;
407
- }
408
-
409
- case 'list': {
410
- const platform = getPlatform();
411
- const all = Object.entries(TERMINALS)
412
- .filter(([, t]) => t.platform === platform)
413
- .map(([id, t]) => ({ id, name: t.name }));
414
- console.log(JSON.stringify({ platform, terminals: all }, null, 2));
415
- break;
416
- }
417
-
418
- default:
419
- console.log(`Vibe Forge Terminal Utility
420
-
421
- Usage:
422
- node terminal.js detect Detect available terminals (JSON)
423
- node terminal.js best Get best available terminal (JSON)
424
- node terminal.js list List all terminals for this platform
425
- node terminal.js spawn <id> <title> <command> [cwd] [tabColor]
426
-
427
- Examples:
428
- node terminal.js detect
429
- node terminal.js spawn windows-terminal "Anvil" "claude --chat"
430
- `);
431
- }
432
- }
433
-
434
- // Export for use as module
435
- module.exports = {
436
- detectTerminals,
437
- getBestTerminal,
438
- spawnTerminal,
439
- TERMINALS,
440
- getPlatform,
441
- };
442
-
443
- // Run CLI if executed directly
444
- if (require.main === module) {
445
- main();
446
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Vibe Forge - Cross-Platform Terminal Detection & Spawning
4
+ *
5
+ * Detects available terminals and spawns new tabs/windows for worker agents.
6
+ *
7
+ * Supported terminals:
8
+ * Windows: Windows Terminal, PowerShell
9
+ * macOS: Terminal.app, iTerm2, Kitty
10
+ * Linux: gnome-terminal, konsole, kitty, xterm
11
+ *
12
+ * Usage:
13
+ * node terminal.js detect List available terminals
14
+ * node terminal.js spawn <terminal> <title> <command>
15
+ */
16
+
17
+ const { execSync, spawn } = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+
22
+ // =============================================================================
23
+ // Terminal Definitions
24
+ // =============================================================================
25
+
26
+ const TERMINALS = {
27
+ // Windows
28
+ 'windows-terminal': {
29
+ name: 'Windows Terminal',
30
+ platform: 'win32',
31
+ detect: () => commandExists('wt') || commandExists('wt.exe'),
32
+ spawn: (title, command, opts) => spawnWindowsTerminal(title, command, opts),
33
+ },
34
+ 'powershell': {
35
+ name: 'PowerShell',
36
+ platform: 'win32',
37
+ detect: () => commandExists('pwsh') || commandExists('powershell'),
38
+ spawn: (title, command, opts) => spawnPowerShell(title, command, opts),
39
+ },
40
+
41
+ // macOS
42
+ 'terminal-app': {
43
+ name: 'Terminal.app',
44
+ platform: 'darwin',
45
+ detect: () => fs.existsSync('/System/Applications/Utilities/Terminal.app'),
46
+ spawn: (title, command, opts) => spawnTerminalApp(title, command, opts),
47
+ },
48
+ 'iterm2': {
49
+ name: 'iTerm2',
50
+ platform: 'darwin',
51
+ detect: () => fs.existsSync('/Applications/iTerm.app'),
52
+ spawn: (title, command, opts) => spawnITerm2(title, command, opts),
53
+ },
54
+ 'kitty-mac': {
55
+ name: 'Kitty',
56
+ platform: 'darwin',
57
+ detect: () => commandExists('kitty') || fs.existsSync('/Applications/kitty.app'),
58
+ spawn: (title, command, opts) => spawnKitty(title, command, opts),
59
+ },
60
+
61
+ // Linux
62
+ 'gnome-terminal': {
63
+ name: 'GNOME Terminal',
64
+ platform: 'linux',
65
+ detect: () => commandExists('gnome-terminal'),
66
+ spawn: (title, command, opts) => spawnGnomeTerminal(title, command, opts),
67
+ },
68
+ 'konsole': {
69
+ name: 'Konsole',
70
+ platform: 'linux',
71
+ detect: () => commandExists('konsole'),
72
+ spawn: (title, command, opts) => spawnKonsole(title, command, opts),
73
+ },
74
+ 'kitty-linux': {
75
+ name: 'Kitty',
76
+ platform: 'linux',
77
+ detect: () => commandExists('kitty'),
78
+ spawn: (title, command, opts) => spawnKitty(title, command, opts),
79
+ },
80
+ 'xterm': {
81
+ name: 'XTerm',
82
+ platform: 'linux',
83
+ detect: () => commandExists('xterm'),
84
+ spawn: (title, command, opts) => spawnXterm(title, command, opts),
85
+ },
86
+ };
87
+
88
+ // =============================================================================
89
+ // Detection Helpers
90
+ // =============================================================================
91
+
92
+ function commandExists(cmd) {
93
+ try {
94
+ if (os.platform() === 'win32') {
95
+ execSync(`where ${cmd}`, { stdio: 'pipe' });
96
+ } else {
97
+ execSync(`which ${cmd}`, { stdio: 'pipe' });
98
+ }
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function getPlatform() {
106
+ const p = os.platform();
107
+ if (p === 'win32') return 'win32';
108
+ if (p === 'darwin') return 'darwin';
109
+ return 'linux'; // Treat all other Unix-likes as Linux
110
+ }
111
+
112
+ function detectTerminals() {
113
+ const platform = getPlatform();
114
+ const available = [];
115
+
116
+ for (const [id, terminal] of Object.entries(TERMINALS)) {
117
+ if (terminal.platform === platform) {
118
+ try {
119
+ if (terminal.detect()) {
120
+ available.push({
121
+ id,
122
+ name: terminal.name,
123
+ platform: terminal.platform,
124
+ });
125
+ }
126
+ } catch {
127
+ // Detection failed, skip this terminal
128
+ }
129
+ }
130
+ }
131
+
132
+ return available;
133
+ }
134
+
135
+ function getBestTerminal() {
136
+ const available = detectTerminals();
137
+ return available.length > 0 ? available[0] : null;
138
+ }
139
+
140
+ // =============================================================================
141
+ // Spawn Functions - Windows
142
+ // =============================================================================
143
+
144
+ function spawnWindowsTerminal(title, command, opts = {}) {
145
+ const { cwd, tabColor, suppressApplicationTitle = true } = opts;
146
+
147
+ // Build wt arguments
148
+ const args = ['-w', '0', 'new-tab', '--title', title];
149
+
150
+ // FORGE-9: Prevent the spawned process from overwriting the tab title
151
+ if (suppressApplicationTitle) {
152
+ args.push('--suppressApplicationTitle');
153
+ }
154
+
155
+ if (tabColor) {
156
+ args.push('--tabColor', tabColor);
157
+ }
158
+
159
+ // Determine bash path
160
+ const bashPath = findGitBash();
161
+
162
+ if (bashPath) {
163
+ args.push(bashPath, '-c', command);
164
+ } else {
165
+ args.push('bash', '-c', command);
166
+ }
167
+
168
+ const child = spawn('wt', args, {
169
+ detached: true,
170
+ stdio: 'ignore',
171
+ cwd: cwd || process.cwd(),
172
+ shell: true,
173
+ });
174
+
175
+ child.unref();
176
+ return { success: true, terminal: 'windows-terminal' };
177
+ }
178
+
179
+ function spawnPowerShell(title, command, opts = {}) {
180
+ const { cwd } = opts;
181
+
182
+ // PowerShell: Start-Process to open new window
183
+ // We wrap in bash since the command expects bash
184
+ const bashPath = findGitBash();
185
+ const bashCmd = bashPath ? `& '${bashPath}' -c '${command.replace(/'/g, "''")}'` : `bash -c '${command.replace(/'/g, "''")}'`;
186
+
187
+ const psCommand = `Start-Process pwsh -ArgumentList '-NoExit', '-Command', '${bashCmd.replace(/'/g, "''")}'`;
188
+
189
+ const child = spawn('pwsh', ['-Command', psCommand], {
190
+ detached: true,
191
+ stdio: 'ignore',
192
+ cwd: cwd || process.cwd(),
193
+ shell: true,
194
+ });
195
+
196
+ child.unref();
197
+ return { success: true, terminal: 'powershell' };
198
+ }
199
+
200
+ function findGitBash() {
201
+ if (os.platform() !== 'win32') return null;
202
+
203
+ const paths = [
204
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
205
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
206
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'bin', 'bash.exe'),
207
+ ];
208
+
209
+ for (const p of paths) {
210
+ if (fs.existsSync(p)) {
211
+ return p;
212
+ }
213
+ }
214
+
215
+ // Try to find via where command
216
+ try {
217
+ const gitPath = execSync('where git', { stdio: 'pipe' }).toString().trim().split('\n')[0];
218
+ const gitDir = path.dirname(path.dirname(gitPath));
219
+ const bashPath = path.join(gitDir, 'bin', 'bash.exe');
220
+ if (fs.existsSync(bashPath)) {
221
+ return bashPath;
222
+ }
223
+ } catch {
224
+ // Ignore
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ // =============================================================================
231
+ // Spawn Functions - macOS
232
+ // =============================================================================
233
+
234
+ function spawnTerminalApp(title, command, opts = {}) {
235
+ const { cwd } = opts;
236
+ const workDir = cwd || process.cwd();
237
+
238
+ // AppleScript to open new Terminal tab
239
+ const script = `
240
+ tell application "Terminal"
241
+ activate
242
+ do script "cd '${workDir.replace(/'/g, "'\\''")}' && ${command.replace(/"/g, '\\"')}"
243
+ set custom title of front window to "${title}"
244
+ end tell
245
+ `;
246
+
247
+ try {
248
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
249
+ return { success: true, terminal: 'terminal-app' };
250
+ } catch (err) {
251
+ return { success: false, error: err.message, terminal: 'terminal-app' };
252
+ }
253
+ }
254
+
255
+ function spawnITerm2(title, command, opts = {}) {
256
+ const { cwd } = opts;
257
+ const workDir = cwd || process.cwd();
258
+
259
+ // AppleScript for iTerm2
260
+ const script = `
261
+ tell application "iTerm2"
262
+ activate
263
+ tell current window
264
+ create tab with default profile
265
+ tell current session
266
+ write text "cd '${workDir.replace(/'/g, "'\\''")}' && ${command.replace(/"/g, '\\"')}"
267
+ set name to "${title}"
268
+ end tell
269
+ end tell
270
+ end tell
271
+ `;
272
+
273
+ try {
274
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
275
+ return { success: true, terminal: 'iterm2' };
276
+ } catch (err) {
277
+ return { success: false, error: err.message, terminal: 'iterm2' };
278
+ }
279
+ }
280
+
281
+ // =============================================================================
282
+ // Spawn Functions - Linux & Cross-Platform
283
+ // =============================================================================
284
+
285
+ function spawnGnomeTerminal(title, command, opts = {}) {
286
+ const { cwd } = opts;
287
+
288
+ const child = spawn('gnome-terminal', [
289
+ '--tab',
290
+ '--title', title,
291
+ '--', 'bash', '-c', `${command}; exec bash`
292
+ ], {
293
+ detached: true,
294
+ stdio: 'ignore',
295
+ cwd: cwd || process.cwd(),
296
+ });
297
+
298
+ child.unref();
299
+ return { success: true, terminal: 'gnome-terminal' };
300
+ }
301
+
302
+ function spawnKonsole(title, command, opts = {}) {
303
+ const { cwd } = opts;
304
+
305
+ const child = spawn('konsole', [
306
+ '--new-tab',
307
+ '-p', `tabtitle=${title}`,
308
+ '-e', 'bash', '-c', `${command}; exec bash`
309
+ ], {
310
+ detached: true,
311
+ stdio: 'ignore',
312
+ cwd: cwd || process.cwd(),
313
+ });
314
+
315
+ child.unref();
316
+ return { success: true, terminal: 'konsole' };
317
+ }
318
+
319
+ function spawnKitty(title, command, opts = {}) {
320
+ const { cwd } = opts;
321
+
322
+ // Kitty can work in single-instance mode with remote control
323
+ // or spawn new windows
324
+ const child = spawn('kitty', [
325
+ '--title', title,
326
+ '--directory', cwd || process.cwd(),
327
+ 'bash', '-c', `${command}; exec bash`
328
+ ], {
329
+ detached: true,
330
+ stdio: 'ignore',
331
+ });
332
+
333
+ child.unref();
334
+ return { success: true, terminal: 'kitty' };
335
+ }
336
+
337
+ function spawnXterm(title, command, opts = {}) {
338
+ const { cwd } = opts;
339
+
340
+ const child = spawn('xterm', [
341
+ '-T', title,
342
+ '-e', 'bash', '-c', `cd '${cwd || process.cwd()}' && ${command}; exec bash`
343
+ ], {
344
+ detached: true,
345
+ stdio: 'ignore',
346
+ });
347
+
348
+ child.unref();
349
+ return { success: true, terminal: 'xterm' };
350
+ }
351
+
352
+ // =============================================================================
353
+ // Main Spawn Function
354
+ // =============================================================================
355
+
356
+ function spawnTerminal(terminalId, title, command, opts = {}) {
357
+ const terminal = TERMINALS[terminalId];
358
+
359
+ if (!terminal) {
360
+ return { success: false, error: `Unknown terminal: ${terminalId}` };
361
+ }
362
+
363
+ try {
364
+ return terminal.spawn(title, command, opts);
365
+ } catch (err) {
366
+ return { success: false, error: err.message, terminal: terminalId };
367
+ }
368
+ }
369
+
370
+ // =============================================================================
371
+ // CLI Interface
372
+ // =============================================================================
373
+
374
+ function main() {
375
+ const args = process.argv.slice(2);
376
+ const command = args[0];
377
+
378
+ switch (command) {
379
+ case 'detect': {
380
+ const terminals = detectTerminals();
381
+ console.log(JSON.stringify({ platform: getPlatform(), terminals }, null, 2));
382
+ break;
383
+ }
384
+
385
+ case 'best': {
386
+ const best = getBestTerminal();
387
+ if (best) {
388
+ console.log(JSON.stringify(best, null, 2));
389
+ } else {
390
+ console.log(JSON.stringify({ error: 'No supported terminal found' }));
391
+ process.exit(1);
392
+ }
393
+ break;
394
+ }
395
+
396
+ case 'spawn': {
397
+ const terminalId = args[1];
398
+ const title = args[2];
399
+ const cmd = args[3];
400
+ const cwd = args[4];
401
+ const tabColor = args[5];
402
+
403
+ if (!terminalId || !title || !cmd) {
404
+ console.error('Usage: terminal.js spawn <terminal-id> <title> <command> [cwd] [tabColor]');
405
+ process.exit(1);
406
+ }
407
+
408
+ const result = spawnTerminal(terminalId, title, cmd, { cwd, tabColor });
409
+ console.log(JSON.stringify(result));
410
+ process.exit(result.success ? 0 : 1);
411
+ break;
412
+ }
413
+
414
+ case 'list': {
415
+ const platform = getPlatform();
416
+ const all = Object.entries(TERMINALS)
417
+ .filter(([, t]) => t.platform === platform)
418
+ .map(([id, t]) => ({ id, name: t.name }));
419
+ console.log(JSON.stringify({ platform, terminals: all }, null, 2));
420
+ break;
421
+ }
422
+
423
+ default:
424
+ console.log(`Vibe Forge Terminal Utility
425
+
426
+ Usage:
427
+ node terminal.js detect Detect available terminals (JSON)
428
+ node terminal.js best Get best available terminal (JSON)
429
+ node terminal.js list List all terminals for this platform
430
+ node terminal.js spawn <id> <title> <command> [cwd] [tabColor]
431
+
432
+ Examples:
433
+ node terminal.js detect
434
+ node terminal.js spawn windows-terminal "Anvil" "claude --chat"
435
+ `);
436
+ }
437
+ }
438
+
439
+ // Export for use as module
440
+ module.exports = {
441
+ detectTerminals,
442
+ getBestTerminal,
443
+ spawnTerminal,
444
+ findGitBash,
445
+ TERMINALS,
446
+ getPlatform,
447
+ };
448
+
449
+ // Run CLI if executed directly
450
+ if (require.main === module) {
451
+ main();
452
+ }