@ctrl-spc/cli 1.3.1 → 1.3.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.
- package/dist/dispatch.js +77 -35
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { access, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
6
|
import { resolveAgentCommand } from './agents.js';
|
|
6
7
|
function shellQuote(value) {
|
|
7
8
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
-
const
|
|
10
|
+
function agentInvocationCommand(request, executable) {
|
|
11
|
+
const prompt = shellQuote(request.prompt);
|
|
12
|
+
const binary = shellQuote(executable);
|
|
13
|
+
const fresh = `exec ${binary} ${prompt}`;
|
|
14
|
+
if (request.mode === 'start')
|
|
15
|
+
return fresh;
|
|
16
|
+
const resume = request.agent_kind === 'claude'
|
|
17
|
+
? `${binary} --continue ${prompt}`
|
|
18
|
+
: `${binary} resume --last ${prompt}`;
|
|
19
|
+
return `${resume} || ${fresh}`;
|
|
20
|
+
}
|
|
21
|
+
export function agentTerminalScript(request, executable, markerPath) {
|
|
11
22
|
return [
|
|
12
|
-
'#!/bin/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'/bin/
|
|
16
|
-
|
|
23
|
+
'#!/bin/sh',
|
|
24
|
+
`cd ${shellQuote(request.workspace_root)} || exit 72`,
|
|
25
|
+
`: > ${shellQuote(markerPath)} || exit 73`,
|
|
26
|
+
'/bin/rm -f -- "$0"',
|
|
27
|
+
agentInvocationCommand(request, executable),
|
|
17
28
|
'',
|
|
18
29
|
].join('\n');
|
|
19
30
|
}
|
|
@@ -29,46 +40,77 @@ export function agentTerminalCommand(request, executable) {
|
|
|
29
40
|
}
|
|
30
41
|
/** Opens a visible agent conversation. Resume is best-effort and falls back
|
|
31
42
|
* to a fresh task conversation in the same project checkout. */
|
|
32
|
-
export async function launchAgentDispatch(request) {
|
|
33
|
-
if (process.platform !== 'darwin') {
|
|
43
|
+
export async function launchAgentDispatch(request, deps = {}) {
|
|
44
|
+
if ((deps.platform ?? process.platform) !== 'darwin') {
|
|
34
45
|
throw new Error('Automatic agent launch currently requires macOS Terminal.');
|
|
35
46
|
}
|
|
36
|
-
const executable = resolveAgentCommand(request.agent_kind);
|
|
47
|
+
const executable = (deps.resolveExecutable ?? resolveAgentCommand)(request.agent_kind);
|
|
37
48
|
if (!executable)
|
|
38
49
|
throw new Error(`${request.agent_kind} is not installed on this machine.`);
|
|
50
|
+
const timeoutMs = deps.timeoutMs ?? 10_000;
|
|
39
51
|
const directory = await mkdtemp(join(tmpdir(), 'ctrl-spc-dispatch-'));
|
|
40
|
-
const scriptPath = join(directory,
|
|
52
|
+
const scriptPath = join(directory, 'launch.command');
|
|
53
|
+
const markerPath = join(directory, 'launched');
|
|
41
54
|
try {
|
|
42
|
-
await writeFile(scriptPath, agentTerminalScript(request, executable), {
|
|
55
|
+
await writeFile(scriptPath, agentTerminalScript(request, executable, markerPath), {
|
|
43
56
|
encoding: 'utf8',
|
|
44
57
|
flag: 'wx',
|
|
45
58
|
mode: 0o700,
|
|
46
59
|
});
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
50
|
-
});
|
|
51
|
-
let stderr = '';
|
|
52
|
-
child.stderr.on('data', (chunk) => { stderr += String(chunk); });
|
|
53
|
-
child.once('error', reject);
|
|
54
|
-
child.once('exit', (code) => {
|
|
55
|
-
if (code === 0)
|
|
56
|
-
resolve();
|
|
57
|
-
else
|
|
58
|
-
reject(new Error(stderr.trim() || `Terminal launch exited with code ${code ?? 'unknown'}.`));
|
|
59
|
-
});
|
|
60
|
-
});
|
|
60
|
+
await (deps.openTerminal ?? openTerminalCommandFile)(scriptPath, timeoutMs);
|
|
61
|
+
await (deps.waitForAcknowledgement ?? waitForLaunchAcknowledgement)(markerPath, timeoutMs);
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
finally {
|
|
63
64
|
await rm(directory, { recursive: true, force: true }).catch(() => { });
|
|
64
|
-
throw error;
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
}
|
|
67
|
+
export function terminalOpenArguments(scriptPath) {
|
|
68
|
+
return ['-b', 'com.apple.Terminal', scriptPath];
|
|
69
|
+
}
|
|
70
|
+
async function openTerminalCommandFile(scriptPath, timeoutMs) {
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn('/usr/bin/open', terminalOpenArguments(scriptPath), {
|
|
73
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
74
|
+
});
|
|
75
|
+
let stderr = '';
|
|
76
|
+
let settled = false;
|
|
77
|
+
const finish = (error) => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
settled = true;
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
if (error)
|
|
83
|
+
reject(error);
|
|
84
|
+
else
|
|
85
|
+
resolve();
|
|
86
|
+
};
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
child.kill('SIGTERM');
|
|
89
|
+
finish(new Error(`Terminal launch timed out after ${timeoutMs}ms.`));
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
timer.unref();
|
|
92
|
+
child.stderr.on('data', (chunk) => { stderr += String(chunk); });
|
|
93
|
+
child.once('error', (error) => finish(error));
|
|
94
|
+
child.once('exit', (code) => {
|
|
95
|
+
if (code === 0)
|
|
96
|
+
finish();
|
|
97
|
+
else
|
|
98
|
+
finish(new Error(stderr.trim() || `Terminal launch exited with code ${code ?? 'unknown'}.`));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function waitForLaunchAcknowledgement(markerPath, timeoutMs) {
|
|
103
|
+
const deadline = Date.now() + timeoutMs;
|
|
104
|
+
while (Date.now() < deadline) {
|
|
105
|
+
try {
|
|
106
|
+
await access(markerPath);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
await delay(50);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Terminal accepted the launch but did not start the agent within ${timeoutMs}ms.`);
|
|
72
114
|
}
|
|
73
115
|
export function createAgentDispatchController(client, machineId, agents, deps = {}) {
|
|
74
116
|
const claim = deps.claim ?? (async () => {
|