Skip to content

Commit e73bbf3

Browse files
committed
feat(runtime): switch agent scripts to local launchers
Run agent and session commands through project-local spawndock scripts and drop the CLI dev dependency to keep runtime bootstrap self-contained. Made-with: Cursor
1 parent fa65456 commit e73bbf3

6 files changed

Lines changed: 146 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ npx -y @spawn-dock/create@beta --token <pairing-token> [project-dir]
2424
- `pnpm run dev` starts Next.js and the SpawnDock tunnel together (no AI agent).
2525
The tunnel startup logs also print the Telegram Mini App deep link when the
2626
bootstrapped `spawndock.config.json` includes it.
27-
- `pnpm run agent` starts Next.js, the tunnel, then the SpawnDock agent launcher (`@spawn-dock/cli`).
27+
- `pnpm run agent` starts Next.js, the tunnel, then the local SpawnDock runtime launcher (`spawndock/agent.mjs`).
2828
- `pnpm run dev:next` starts only the local Next.js server.
2929
- `pnpm run dev:tunnel` starts only the SpawnDock tunnel client.
3030
- `pnpm run agent:session` starts only the SpawnDock AI runtime launcher (`spawn-dock session`, no dev server).

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"private": true,
55
"scripts": {
66
"dev": "node ./spawndock/dev.mjs",
7-
"agent": "spawn-dock agent",
8-
"agent:session": "spawn-dock session",
7+
"agent": "node ./spawndock/agent.mjs",
8+
"agent:session": "node ./spawndock/session.mjs",
99
"dev:next": "node ./spawndock/next.mjs",
1010
"dev:tunnel": "node ./spawndock/tunnel.mjs",
1111
"dev:https": "next dev --experimental-https",
@@ -28,7 +28,6 @@
2828
"ws": "^8.19.0"
2929
},
3030
"devDependencies": {
31-
"@spawn-dock/cli": "0.2.0",
3231
"@spawn-dock/dev-tunnel": "1.0.9",
3332
"@spawn-dock/mcp": "1.0.5",
3433
"@tailwindcss/postcss": "^4.1.17",

pnpm-lock.yaml

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spawndock/agent.mjs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { spawn } from "node:child_process"
2+
3+
import { readSpawndockConfig, resolveConfiguredLocalPort } from "./config.mjs"
4+
import { findAvailablePort, waitForPort } from "./port.mjs"
5+
6+
const config = readSpawndockConfig()
7+
const requestedLocalPort = resolveConfiguredLocalPort(config)
8+
const localPort = await findAvailablePort(requestedLocalPort)
9+
10+
if (localPort !== requestedLocalPort) {
11+
console.warn(
12+
`SpawnDock local port ${requestedLocalPort} is busy, using ${localPort} instead.`
13+
)
14+
}
15+
16+
const sharedEnv = {
17+
...process.env,
18+
SPAWNDOCK_PORT: String(localPort),
19+
}
20+
21+
const children = []
22+
let shuttingDown = false
23+
24+
const stopChildren = (signal = "SIGTERM") => {
25+
shuttingDown = true
26+
for (const child of [...children].reverse()) {
27+
if (!child.killed) {
28+
child.kill(signal)
29+
}
30+
}
31+
}
32+
33+
const onSignal = (signal) => {
34+
stopChildren(signal)
35+
process.exit(signal === "SIGINT" ? 130 : 143)
36+
}
37+
38+
process.once("SIGINT", () => onSignal("SIGINT"))
39+
process.once("SIGTERM", () => onSignal("SIGTERM"))
40+
41+
const spawnChild = (command, args, env = sharedEnv) => {
42+
const child = spawn(command, args, {
43+
cwd: process.cwd(),
44+
env,
45+
stdio: "inherit",
46+
})
47+
48+
children.push(child)
49+
50+
child.on("exit", (code) => {
51+
if (shuttingDown) {
52+
return
53+
}
54+
if (typeof code === "number" && code !== 0) {
55+
stopChildren()
56+
process.exit(code)
57+
}
58+
})
59+
60+
return child
61+
}
62+
63+
const nextChild = spawnChild("node", ["spawndock/next.mjs"])
64+
65+
try {
66+
await waitForPort(localPort, {
67+
isCancelled: () => shuttingDown || nextChild.exitCode !== null,
68+
})
69+
} catch (error) {
70+
stopChildren()
71+
throw error
72+
}
73+
74+
spawnChild("node", ["spawndock/tunnel.mjs"])
75+
76+
const agentChild = spawnChild("node", ["spawndock/session.mjs"], {
77+
...process.env,
78+
SPAWNDOCK_PORT: String(localPort),
79+
})
80+
81+
const exitCode = await new Promise((resolve, reject) => {
82+
agentChild.once("error", reject)
83+
agentChild.once("exit", (code) => resolve(typeof code === "number" ? code : 1))
84+
})
85+
86+
stopChildren()
87+
process.exit(exitCode)

spawndock/config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,12 @@ export const resolveMcpApiKey = (config) => {
4747

4848
throw new Error("Missing mcpApiKey in spawndock.config.json")
4949
}
50+
51+
52+
const resolveRuntimeFromValue = (value) =>
53+
value === "opencode" || value === "codex" || value === "claude" ? value : undefined
54+
55+
export const resolveAgentRuntime = (config, env = process.env) =>
56+
resolveRuntimeFromValue(env.SPAWNDOCK_AGENT_RUNTIME) ??
57+
resolveRuntimeFromValue(config.agentRuntime) ??
58+
"opencode"

spawndock/session.mjs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { spawn } from "node:child_process"
2+
3+
import { readSpawndockConfig, resolveAgentRuntime } from "./config.mjs"
4+
5+
const config = readSpawndockConfig()
6+
const runtime = resolveAgentRuntime(config)
7+
const projectDir = process.cwd()
8+
9+
const buildRuntimeCommand = (selectedRuntime, cwd) => {
10+
if (selectedRuntime === "codex") {
11+
return {
12+
command: "codex",
13+
args: ["-C", cwd, "-s", "workspace-write", "-a", "on-request"],
14+
}
15+
}
16+
17+
if (selectedRuntime === "claude") {
18+
return {
19+
command: "codex",
20+
args: ["sandbox", "linux", "claude"],
21+
}
22+
}
23+
24+
return {
25+
command: "codex",
26+
args: ["sandbox", "linux", "opencode", cwd],
27+
}
28+
}
29+
30+
const spec = buildRuntimeCommand(runtime, projectDir)
31+
console.log(`Launching ${runtime} in ${projectDir}`)
32+
console.log("Filesystem access is constrained to the project working directory on a best-effort basis.")
33+
34+
const child = spawn(spec.command, spec.args, {
35+
cwd: projectDir,
36+
env: process.env,
37+
stdio: "inherit",
38+
})
39+
40+
child.on("exit", (code) => {
41+
process.exit(typeof code === "number" ? code : 1)
42+
})
43+
44+
child.on("error", (error) => {
45+
console.error(error)
46+
process.exit(1)
47+
})

0 commit comments

Comments
 (0)