vibe-forge 0.8.1 → 0.8.3
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.
- package/.claude/commands/configure-vcs.md +102 -102
- package/.claude/commands/forge.md +218 -218
- package/.claude/hooks/worker-loop.js +220 -217
- package/.claude/settings.json +89 -89
- package/README.md +149 -191
- package/agents/aegis/personality.md +303 -303
- package/agents/anvil/personality.md +278 -278
- package/agents/architect/personality.md +260 -260
- package/agents/crucible/personality.md +362 -362
- package/agents/crucible-x/personality.md +210 -210
- package/agents/ember/personality.md +293 -293
- package/agents/flux/personality.md +248 -248
- package/agents/furnace/personality.md +342 -342
- package/agents/herald/personality.md +249 -249
- package/agents/oracle/personality.md +284 -284
- package/agents/pixel/personality.md +140 -140
- package/agents/planning-hub/personality.md +473 -473
- package/agents/scribe/personality.md +253 -253
- package/agents/slag/personality.md +268 -268
- package/agents/temper/personality.md +270 -270
- package/bin/cli.js +372 -372
- package/bin/forge-daemon.sh +477 -477
- package/bin/forge-setup.sh +662 -661
- package/bin/forge-spawn.sh +164 -164
- package/bin/forge.sh +566 -566
- package/docs/commands.md +8 -8
- package/package.json +77 -77
- package/{bin → src}/lib/agents.sh +177 -177
- package/{bin → src}/lib/check-aliases.js +50 -50
- package/{bin → src}/lib/colors.sh +45 -44
- package/{bin → src}/lib/config.sh +347 -347
- package/{bin → src}/lib/constants.sh +241 -241
- package/{bin → src}/lib/daemon/budgets.sh +107 -107
- package/{bin → src}/lib/daemon/dependencies.sh +146 -146
- package/{bin → src}/lib/daemon/display.sh +128 -128
- package/{bin → src}/lib/daemon/notifications.sh +273 -273
- package/{bin → src}/lib/daemon/routing.sh +93 -93
- package/{bin → src}/lib/daemon/state.sh +163 -163
- package/{bin → src}/lib/daemon/sync.sh +103 -103
- package/{bin → src}/lib/database.sh +357 -357
- package/{bin → src}/lib/frontmatter.js +106 -106
- package/{bin → src}/lib/heimdall-setup.js +113 -113
- package/{bin → src}/lib/heimdall.js +265 -265
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +264 -264
- package/{bin → src}/lib/terminal.js +452 -452
- package/{bin → src}/lib/util.sh +126 -126
- package/{bin → src}/lib/vcs.js +349 -349
- package/{context → templates}/project-context-template.md +122 -122
- package/config/task-template.md +0 -159
- 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
|
+
}
|