vibe-forge 0.8.1 → 0.8.2

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 (51) hide show
  1. package/.claude/commands/configure-vcs.md +102 -102
  2. package/.claude/commands/forge.md +218 -218
  3. package/.claude/hooks/worker-loop.js +220 -217
  4. package/.claude/settings.json +89 -89
  5. package/README.md +149 -191
  6. package/agents/aegis/personality.md +303 -303
  7. package/agents/anvil/personality.md +278 -278
  8. package/agents/architect/personality.md +260 -260
  9. package/agents/crucible/personality.md +362 -362
  10. package/agents/crucible-x/personality.md +210 -210
  11. package/agents/ember/personality.md +293 -293
  12. package/agents/flux/personality.md +248 -248
  13. package/agents/furnace/personality.md +342 -342
  14. package/agents/herald/personality.md +249 -249
  15. package/agents/oracle/personality.md +284 -284
  16. package/agents/pixel/personality.md +140 -140
  17. package/agents/planning-hub/personality.md +473 -473
  18. package/agents/scribe/personality.md +253 -253
  19. package/agents/slag/personality.md +268 -268
  20. package/agents/temper/personality.md +270 -270
  21. package/bin/cli.js +372 -372
  22. package/bin/forge-daemon.sh +477 -477
  23. package/bin/forge-setup.sh +662 -661
  24. package/bin/forge-spawn.sh +164 -164
  25. package/bin/forge.sh +566 -566
  26. package/docs/commands.md +8 -8
  27. package/package.json +77 -77
  28. package/{bin → src}/lib/agents.sh +177 -177
  29. package/{bin → src}/lib/check-aliases.js +50 -50
  30. package/{bin → src}/lib/colors.sh +45 -44
  31. package/{bin → src}/lib/config.sh +347 -347
  32. package/{bin → src}/lib/constants.sh +241 -241
  33. package/{bin → src}/lib/daemon/budgets.sh +107 -107
  34. package/{bin → src}/lib/daemon/dependencies.sh +146 -146
  35. package/{bin → src}/lib/daemon/display.sh +128 -128
  36. package/{bin → src}/lib/daemon/notifications.sh +273 -273
  37. package/{bin → src}/lib/daemon/routing.sh +93 -93
  38. package/{bin → src}/lib/daemon/state.sh +163 -163
  39. package/{bin → src}/lib/daemon/sync.sh +103 -103
  40. package/{bin → src}/lib/database.sh +357 -357
  41. package/{bin → src}/lib/frontmatter.js +106 -106
  42. package/{bin → src}/lib/heimdall-setup.js +113 -113
  43. package/{bin → src}/lib/heimdall.js +265 -265
  44. package/src/lib/index.sh +25 -0
  45. package/{bin → src}/lib/json.sh +264 -264
  46. package/{bin → src}/lib/terminal.js +452 -452
  47. package/{bin → src}/lib/util.sh +126 -126
  48. package/{bin → src}/lib/vcs.js +349 -349
  49. package/{context → templates}/project-context-template.md +122 -122
  50. package/config/task-template.md +0 -159
  51. package/config/templates/handoff-template.md +0 -40
@@ -1,452 +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, 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
- }
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
+ }