@ctrl-spc/cli 1.3.0 → 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 +81 -11
  2. package/package.json +1 -1
package/dist/dispatch.js CHANGED
@@ -1,10 +1,32 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { access, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { setTimeout as delay } from 'node:timers/promises';
2
6
  import { resolveAgentCommand } from './agents.js';
3
7
  function shellQuote(value) {
4
8
  return `'${value.replaceAll("'", "'\\''")}'`;
5
9
  }
6
- function appleScriptString(value) {
7
- return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
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) {
22
+ return [
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),
28
+ '',
29
+ ].join('\n');
8
30
  }
9
31
  export function agentTerminalCommand(request, executable) {
10
32
  const prompt = shellQuote(request.prompt);
@@ -18,30 +40,78 @@ export function agentTerminalCommand(request, executable) {
18
40
  }
19
41
  /** Opens a visible agent conversation. Resume is best-effort and falls back
20
42
  * to a fresh task conversation in the same project checkout. */
21
- export async function launchAgentDispatch(request) {
22
- if (process.platform !== 'darwin') {
43
+ export async function launchAgentDispatch(request, deps = {}) {
44
+ if ((deps.platform ?? process.platform) !== 'darwin') {
23
45
  throw new Error('Automatic agent launch currently requires macOS Terminal.');
24
46
  }
25
- const executable = resolveAgentCommand(request.agent_kind);
47
+ const executable = (deps.resolveExecutable ?? resolveAgentCommand)(request.agent_kind);
26
48
  if (!executable)
27
49
  throw new Error(`${request.agent_kind} is not installed on this machine.`);
28
- const command = agentTerminalCommand(request, executable);
29
- const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script ${appleScriptString(command)}`;
50
+ const timeoutMs = deps.timeoutMs ?? 10_000;
51
+ const directory = await mkdtemp(join(tmpdir(), 'ctrl-spc-dispatch-'));
52
+ const scriptPath = join(directory, 'launch.command');
53
+ const markerPath = join(directory, 'launched');
54
+ try {
55
+ await writeFile(scriptPath, agentTerminalScript(request, executable, markerPath), {
56
+ encoding: 'utf8',
57
+ flag: 'wx',
58
+ mode: 0o700,
59
+ });
60
+ await (deps.openTerminal ?? openTerminalCommandFile)(scriptPath, timeoutMs);
61
+ await (deps.waitForAcknowledgement ?? waitForLaunchAcknowledgement)(markerPath, timeoutMs);
62
+ }
63
+ finally {
64
+ await rm(directory, { recursive: true, force: true }).catch(() => { });
65
+ }
66
+ }
67
+ export function terminalOpenArguments(scriptPath) {
68
+ return ['-b', 'com.apple.Terminal', scriptPath];
69
+ }
70
+ async function openTerminalCommandFile(scriptPath, timeoutMs) {
30
71
  await new Promise((resolve, reject) => {
31
- const child = spawn('/usr/bin/osascript', ['-e', script], {
72
+ const child = spawn('/usr/bin/open', terminalOpenArguments(scriptPath), {
32
73
  stdio: ['ignore', 'ignore', 'pipe'],
33
74
  });
34
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();
35
92
  child.stderr.on('data', (chunk) => { stderr += String(chunk); });
36
- child.once('error', reject);
93
+ child.once('error', (error) => finish(error));
37
94
  child.once('exit', (code) => {
38
95
  if (code === 0)
39
- resolve();
96
+ finish();
40
97
  else
41
- reject(new Error(stderr.trim() || `Terminal launch exited with code ${code ?? 'unknown'}.`));
98
+ finish(new Error(stderr.trim() || `Terminal launch exited with code ${code ?? 'unknown'}.`));
42
99
  });
43
100
  });
44
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.`);
114
+ }
45
115
  export function createAgentDispatchController(client, machineId, agents, deps = {}) {
46
116
  const claim = deps.claim ?? (async () => {
47
117
  const { data, error } = await client.rpc('claim_agent_dispatch_request', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cli",
3
- "version": "1.3.0",
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"