@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.
Files changed (2) hide show
  1. package/dist/dispatch.js +77 -35
  2. 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
- export function agentTerminalScript(request, executable) {
10
- const command = agentTerminalCommand(request, executable);
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/zsh',
13
- 'script_path="$0"',
14
- '/bin/rm -f -- "$script_path"',
15
- '/bin/rmdir -- "${script_path%/*}" 2>/dev/null || true',
16
- `exec /bin/zsh -lc ${shellQuote(command)}`,
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, `ctrl-spc-${request.id}.command`);
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 new Promise((resolve, reject) => {
48
- const child = spawn('/usr/bin/open', ['-a', 'Terminal', scriptPath], {
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
- catch (error) {
63
+ finally {
63
64
  await rm(directory, { recursive: true, force: true }).catch(() => { });
64
- throw error;
65
65
  }
66
- // The script removes itself after Terminal opens it. This fallback handles
67
- // cases where Terminal accepts the file but never executes it.
68
- const cleanup = setTimeout(() => {
69
- void rm(directory, { recursive: true, force: true });
70
- }, 60_000);
71
- cleanup.unref();
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "CTRL+SPC CLI — per-machine agent for browser login, project linking, presence, and the local MCP server.",
5
5
  "engines": {
6
6
  "node": ">=22"