cubefarm 0.3.0 → 0.3.1
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/assets/{ToyWorld-pDE0OP-F.js → ToyWorld-CM2m1Ytr.js} +2 -2
- package/dist/assets/{index-CDs_MaRq.js → index-BCFM-9fU.js} +2 -2
- package/dist/assets/{rapier-CEtMYVKP.js → rapier-BLchqNuA.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/index.js +2 -2
- package/dist-server/index.js.map +2 -2
- package/dist-server/ptyHost.js +6 -6
- package/dist-server/ptyHost.js.map +2 -2
- package/package.json +1 -1
package/dist-server/ptyHost.js
CHANGED
|
@@ -71,12 +71,12 @@ function killTree(pid) {
|
|
|
71
71
|
function start(m) {
|
|
72
72
|
let proc;
|
|
73
73
|
try {
|
|
74
|
-
proc = nodePty.spawn(m.file, m.args, { name: "xterm-256color", cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env });
|
|
74
|
+
proc = nodePty.spawn(m.file, m.args, { name: "xterm-256color", cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env, useConptyDll: true });
|
|
75
75
|
} catch (err) {
|
|
76
76
|
send({ op: "failed", id: m.id, error: err.message });
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
|
-
const h = { proc, meta: m.meta, attached: true, backlog: [], bytes: 0, exit: null };
|
|
79
|
+
const h = { proc, meta: m.meta, attached: true, backlog: [], bytes: 0, exit: null, killed: false };
|
|
80
80
|
held.set(m.id, h);
|
|
81
81
|
send({ op: "spawned", id: m.id, pid: proc.pid });
|
|
82
82
|
proc.onData((data) => {
|
|
@@ -87,10 +87,9 @@ function start(m) {
|
|
|
87
87
|
});
|
|
88
88
|
proc.onExit(({ exitCode }) => {
|
|
89
89
|
h.exit = exitCode;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
90
|
+
quietly(() => proc.kill());
|
|
91
|
+
if (h.attached && office) send({ op: "exit", id: m.id, code: exitCode });
|
|
92
|
+
if (h.attached && office || h.killed) held.delete(m.id);
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
function attach(id) {
|
|
@@ -172,6 +171,7 @@ function connected(sock) {
|
|
|
172
171
|
if (h) h.meta = m.meta;
|
|
173
172
|
return;
|
|
174
173
|
case "kill":
|
|
174
|
+
if (h) h.killed = true;
|
|
175
175
|
if (h?.exit === null) killTree(h.proc.pid);
|
|
176
176
|
else if (h) held.delete(m.id);
|
|
177
177
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../server/ptyHost.ts", "../server/ptyProtocol.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'node:fs';\nimport http from 'node:http';\nimport net from 'node:net';\nimport path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport * as nodePty from '@lydell/node-pty';\nimport { encode, PTY_PROTOCOL, readMessages, type FromHost, type HeldPty, type ToHost } from './ptyProtocol.ts';\n\n// The office's terminal keeper: a small process of its own that runs the agents' CLIs in pseudo-terminals, so they\n// keep working while the office restarts (an update, a crash, a code change in development). The office connects over\n// a local socket (one office at a time), starts and drives CLIs through it and, when it comes back, picks up the ones\n// still running. The CLIs' hooks come in here too and wait while the office is away, so none is lost and none fails.\n//\n// node ptyHost.js launch <socket> <secret file> starts a keeper outside the caller's process tree, then exits\n// node ptyHost.js serve <socket> <secret file> is the keeper\n\n/** Output kept per terminal while no office is listening. */\nconst BACKLOG_BYTES = 2 * 1024 * 1024;\n/** A hook waits this long for an office (the CLIs are told 120 s). */\nconst HOOK_WAIT_MS = 110_000;\n/** With no office for this long, nobody is coming back: the terminals are stopped. */\nconst ORPHAN_MS = 10 * 60_000;\n/** Nothing left to keep and no office: the keeper goes. */\nconst IDLE_EXIT_MS = 60_000;\n\nconst [mode, socketPath, secretFile] = process.argv.slice(2);\n\nif (mode === 'launch') {\n // A detached child whose parent exits at once: the office's process tree (which the launcher kills on a restart)\n // doesn't include it, and on Windows nothing ties it to a console window.\n spawn(process.execPath, [...process.execArgv, process.argv[1], 'serve', socketPath, secretFile], { detached: true, stdio: 'ignore', windowsHide: true }).unref();\n process.exit(0);\n}\n\nconst logFile = path.join(path.dirname(secretFile), 'pty-host.log');\nconst note = (text: string) => fs.appendFile(logFile, `${new Date().toISOString()} ${text}\\n`, () => undefined);\nprocess.on('uncaughtException', (err) => note(`uncaught: ${err.stack ?? err}`));\nfor (const sig of ['SIGINT', 'SIGHUP', 'SIGBREAK'] as const) process.on(sig, () => undefined); // the office's console isn't ours\n\nconst secret = fs.readFileSync(secretFile, 'utf8').trim();\n\ninterface Held {\n proc: nodePty.IPty;\n meta: unknown;\n /** An office is taking this terminal's output right now. */\n attached: boolean;\n backlog: string[];\n bytes: number;\n exit: number | null;\n}\nconst held = new Map<string, Held>();\nlet office: net.Socket | null = null;\nlet officeLeft = Date.now();\nlet hooksOpen = false; // the connected office has picked up its terminals and takes hooks\n\nconst send = (m: FromHost) => {\n if (office && !office.destroyed) office.write(encode(m));\n};\n\nfunction killTree(pid: number) {\n if (process.platform === 'win32') {\n spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).once('error', () => undefined);\n return;\n }\n try {\n process.kill(-pid, 'SIGTERM'); // the CLI leads its own session: take its MCP servers and tools with it\n } catch {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // already gone\n }\n }\n}\n\n// ---------- terminals ----------\n\nfunction start(m: Extract<ToHost, { op: 'spawn' }>) {\n let proc: nodePty.IPty;\n try {\n proc = nodePty.spawn(m.file, m.args, { name: 'xterm-256color', cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env });\n } catch (err) {\n send({ op: 'failed', id: m.id, error: (err as Error).message });\n return;\n }\n const h: Held = { proc, meta: m.meta, attached: true, backlog: [], bytes: 0, exit: null };\n held.set(m.id, h);\n send({ op: 'spawned', id: m.id, pid: proc.pid });\n proc.onData((data) => {\n if (h.attached && office) return send({ op: 'data', id: m.id, data });\n h.backlog.push(data);\n h.bytes += data.length;\n while (h.bytes > BACKLOG_BYTES && h.backlog.length > 1) h.bytes -= h.backlog.shift()!.length;\n });\n proc.onExit(({ exitCode }) => {\n h.exit = exitCode;\n if (h.attached && office) {\n send({ op: 'exit', id: m.id, code: exitCode });\n held.delete(m.id);\n }\n });\n}\n\nfunction attach(id: string) {\n const h = held.get(id);\n if (!h) return send({ op: 'exit', id, code: -1 });\n h.attached = true;\n if (h.backlog.length) send({ op: 'data', id, data: h.backlog.join('') });\n h.backlog = [];\n h.bytes = 0;\n if (h.exit !== null) {\n send({ op: 'exit', id, code: h.exit });\n held.delete(id);\n }\n}\n\nfunction stopAll() {\n for (const h of held.values()) if (h.exit === null) killTree(h.proc.pid);\n}\n\n// ---------- hooks: CLIs call here, the office answers ----------\n\ninterface Waiting {\n token: string;\n body: unknown;\n res: http.ServerResponse;\n timer: NodeJS.Timeout;\n sent: boolean;\n}\nconst waiting = new Map<number, Waiting>();\nlet nextRid = 1;\n\nfunction answer(rid: number, reply: unknown) {\n const w = waiting.get(rid);\n if (!w) return;\n waiting.delete(rid);\n clearTimeout(w.timer);\n if (!w.res.writableEnded) w.res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(reply ?? {}));\n}\n\n/** Hooks that waited for an office go to the one that's ready (again, if the office before it went without answering). */\nfunction passHooks() {\n if (!office || !hooksOpen) return;\n for (const [rid, w] of waiting) {\n if (w.sent) continue;\n w.sent = true;\n send({ op: 'hook', rid, token: w.token, body: w.body });\n }\n}\n\nconst hooks = http.createServer((req, res) => {\n const token = req.method === 'POST' ? req.url?.match(/^\\/api\\/hooks\\/([\\w-]+)$/)?.[1] : undefined;\n if (!token) return void res.writeHead(404).end();\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size <= 64 * 1024 * 1024) chunks.push(c);\n });\n req.on('end', () => {\n let body: unknown = {};\n try {\n body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');\n } catch {\n // not JSON: an empty hook\n }\n const rid = nextRid++;\n waiting.set(rid, { token, body, res, timer: setTimeout(() => answer(rid, {}), HOOK_WAIT_MS), sent: false });\n passHooks();\n });\n});\n\n// ---------- the office ----------\n\nfunction connected(sock: net.Socket) {\n let helloed = false;\n readMessages<ToHost>(sock, (m) => {\n if (!helloed) {\n if (m.op !== 'hello' || m.secret !== secret) return void sock.destroy();\n helloed = true;\n if (office && office !== sock) office.destroy();\n office = sock;\n hooksOpen = false;\n const ptys: HeldPty[] = [...held].map(([id, h]) => ({ id, pid: h.proc.pid, meta: h.meta, exit: h.exit }));\n send({ op: 'ready', version: PTY_PROTOCOL, hookPort: (hooks.address() as net.AddressInfo).port, ptys, pid: process.pid });\n return;\n }\n const h = 'id' in m ? held.get(m.id) : undefined;\n switch (m.op) {\n case 'spawn':\n return start(m);\n case 'attach':\n return attach(m.id);\n case 'write':\n return void (h && h.exit === null && quietly(() => h.proc.write(m.data)));\n case 'resize':\n return void (h && h.exit === null && quietly(() => h.proc.resize(m.cols, m.rows)));\n case 'meta':\n if (h) h.meta = m.meta;\n return;\n case 'kill':\n if (h?.exit === null) killTree(h.proc.pid);\n else if (h) held.delete(m.id);\n return;\n case 'hooksReady':\n hooksOpen = true;\n return passHooks();\n case 'hookReply':\n return answer(m.rid, m.answer);\n case 'shutdown':\n stopAll();\n setTimeout(() => process.exit(0), 3000);\n return;\n }\n });\n sock.on('error', () => undefined);\n sock.on('close', () => {\n if (office !== sock) return;\n office = null;\n officeLeft = Date.now();\n for (const h of held.values()) h.attached = false;\n for (const w of waiting.values()) w.sent = false; // unanswered: the next office gets them\n });\n}\n\n/** A pseudo-terminal whose CLI just died throws on writes and resizes before it reports the exit. */\nfunction quietly(fn: () => void) {\n try {\n fn();\n } catch {\n // its exit is on the way\n }\n}\n\nconst server = net.createServer(connected);\nserver.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code !== 'EADDRINUSE' || process.platform === 'win32') {\n note(`cannot listen on ${socketPath}: ${err.message}`);\n process.exit(1);\n }\n // A Unix socket file left behind: take it over, unless a keeper still answers on it.\n const probe = net.connect(socketPath, () => {\n probe.destroy();\n process.exit(0);\n });\n probe.on('error', () => {\n fs.rmSync(socketPath, { force: true });\n server.listen(socketPath);\n });\n});\n\nhooks.listen(0, '127.0.0.1', () => server.listen(socketPath));\n\nlet orphaned = false;\nsetInterval(() => {\n if (office) return void (orphaned = false);\n const away = Date.now() - officeLeft;\n if (away > ORPHAN_MS) {\n for (const [id, h] of held) if (h.exit !== null) held.delete(id);\n if (held.size && !orphaned) {\n orphaned = true;\n note(`no office for ${Math.round(away / 60_000)} min: stopping ${held.size} terminal(s)`);\n stopAll();\n }\n }\n if (held.size === 0 && away > IDLE_EXIT_MS) process.exit(0);\n}, 10_000).unref();\n", "import crypto from 'node:crypto';\nimport path from 'node:path';\nimport type { Socket } from 'node:net';\n\n// What the office and its terminal keeper (ptyHost.ts) say to each other over their local socket: one JSON message\n// per line. Kept free of the rest of the server so the keeper stays small and starts fast.\n\n/** Bumped when the messages change: an office that finds a keeper on another version replaces it. */\nexport const PTY_PROTOCOL = 1;\n\n/** A terminal the keeper holds, as it reports it to an office that (re)connects. */\nexport interface HeldPty {\n id: string;\n pid: number;\n /** What the office stored with it (who it belongs to, its session). */\n meta: unknown;\n /** The CLI has exited (while no office was listening): its exit code. */\n exit: number | null;\n}\n\nexport type ToHost =\n | { op: 'hello'; secret: string; version: number }\n | { op: 'hooksReady' } // the office has picked up its terminals: hooks that waited for it can come in now\n | { op: 'spawn'; id: string; file: string; args: string[]; cwd: string; env: Record<string, string>; cols: number; rows: number; meta: unknown }\n | { op: 'attach'; id: string } // an office picking up a terminal it found: what it missed, then everything live\n | { op: 'write'; id: string; data: string }\n | { op: 'resize'; id: string; cols: number; rows: number }\n | { op: 'meta'; id: string; meta: unknown }\n | { op: 'kill'; id: string }\n | { op: 'hookReply'; rid: number; answer: unknown }\n | { op: 'shutdown' }; // stop every terminal and exit (a keeper on another version)\n\nexport type FromHost =\n | { op: 'ready'; version: number; hookPort: number; ptys: HeldPty[]; pid?: number }\n | { op: 'spawned'; id: string; pid: number }\n | { op: 'failed'; id: string; error: string }\n | { op: 'data'; id: string; data: string }\n | { op: 'exit'; id: string; code: number }\n | { op: 'hook'; rid: number; token: string; body: unknown };\n\n/** The keeper's socket for an office home: a named pipe on Windows, a Unix socket in the home elsewhere. */\nexport function hostSocket(home: string, platform = process.platform): string {\n if (platform !== 'win32') return path.join(home, 'pty.sock');\n const tag = crypto.createHash('sha1').update(path.resolve(home).toLowerCase()).digest('hex').slice(0, 12);\n return `\\\\\\\\.\\\\pipe\\\\cubefarm-pty-${tag}`;\n}\n\nexport const encode = (m: ToHost | FromHost) => `${JSON.stringify(m)}\\n`;\n\n/** Calls back with each message a socket sends (newline-delimited JSON); a line that isn't JSON is skipped. */\nexport function readMessages<T>(sock: Socket, onMessage: (m: T) => void) {\n let buf = '';\n sock.setEncoding('utf8');\n sock.on('data', (chunk: string) => {\n buf += chunk;\n let nl: number;\n while ((nl = buf.indexOf('\\n')) >= 0) {\n const line = buf.slice(0, nl);\n buf = buf.slice(nl + 1);\n if (!line) continue;\n let m: T;\n try {\n m = JSON.parse(line) as T;\n } catch {\n continue;\n }\n onMessage(m);\n }\n });\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,SAAS;AAChB,OAAO,UAAU;AACjB,SAAS,aAAa;AACtB,YAAY,aAAa;;;ACGlB,IAAM,eAAe;AAuCrB,IAAM,SAAS,CAAC,MAAyB,GAAG,KAAK,UAAU,CAAC,CAAC;AAAA;AAG7D,SAAS,aAAgB,MAAc,WAA2B;AACvE,MAAI,MAAM;AACV,OAAK,YAAY,MAAM;AACvB,OAAK,GAAG,QAAQ,CAAC,UAAkB;AACjC,WAAO;AACP,QAAI;AACJ,YAAQ,KAAK,IAAI,QAAQ,IAAI,MAAM,GAAG;AACpC,YAAM,OAAO,IAAI,MAAM,GAAG,EAAE;AAC5B,YAAM,IAAI,MAAM,KAAK,CAAC;AACtB,UAAI,CAAC,KAAM;AACX,UAAI;AACJ,UAAI;AACF,YAAI,KAAK,MAAM,IAAI;AAAA,MACrB,QAAQ;AACN;AAAA,MACF;AACA,gBAAU,CAAC;AAAA,IACb;AAAA,EACF,CAAC;AACH;;;ADpDA,IAAM,gBAAgB,IAAI,OAAO;AAEjC,IAAM,eAAe;AAErB,IAAM,YAAY,KAAK;AAEvB,IAAM,eAAe;AAErB,IAAM,CAAC,MAAM,YAAY,UAAU,IAAI,QAAQ,KAAK,MAAM,CAAC;AAE3D,IAAI,SAAS,UAAU;AAGrB,QAAM,QAAQ,UAAU,CAAC,GAAG,QAAQ,UAAU,QAAQ,KAAK,CAAC,GAAG,SAAS,YAAY,UAAU,GAAG,EAAE,UAAU,MAAM,OAAO,UAAU,aAAa,KAAK,CAAC,EAAE,MAAM;AAC/J,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,UAAU,KAAK,KAAK,KAAK,QAAQ,UAAU,GAAG,cAAc;AAClE,IAAM,OAAO,CAAC,SAAiB,GAAG,WAAW,SAAS,IAAG,oBAAI,KAAK,GAAE,YAAY,CAAC,IAAI,IAAI;AAAA,GAAM,MAAM,MAAS;AAC9G,QAAQ,GAAG,qBAAqB,CAAC,QAAQ,KAAK,aAAa,IAAI,SAAS,GAAG,EAAE,CAAC;AAC9E,WAAW,OAAO,CAAC,UAAU,UAAU,UAAU,EAAY,SAAQ,GAAG,KAAK,MAAM,MAAS;AAE5F,IAAM,SAAS,GAAG,aAAa,YAAY,MAAM,EAAE,KAAK;
|
|
4
|
+
"sourcesContent": ["import fs from 'node:fs';\nimport http from 'node:http';\nimport net from 'node:net';\nimport path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport * as nodePty from '@lydell/node-pty';\nimport { encode, PTY_PROTOCOL, readMessages, type FromHost, type HeldPty, type ToHost } from './ptyProtocol.ts';\n\n// The office's terminal keeper: a small process of its own that runs the agents' CLIs in pseudo-terminals, so they\n// keep working while the office restarts (an update, a crash, a code change in development). The office connects over\n// a local socket (one office at a time), starts and drives CLIs through it and, when it comes back, picks up the ones\n// still running. The CLIs' hooks come in here too and wait while the office is away, so none is lost and none fails.\n//\n// node ptyHost.js launch <socket> <secret file> starts a keeper outside the caller's process tree, then exits\n// node ptyHost.js serve <socket> <secret file> is the keeper\n\n/** Output kept per terminal while no office is listening. */\nconst BACKLOG_BYTES = 2 * 1024 * 1024;\n/** A hook waits this long for an office (the CLIs are told 120 s). */\nconst HOOK_WAIT_MS = 110_000;\n/** With no office for this long, nobody is coming back: the terminals are stopped. */\nconst ORPHAN_MS = 10 * 60_000;\n/** Nothing left to keep and no office: the keeper goes. */\nconst IDLE_EXIT_MS = 60_000;\n\nconst [mode, socketPath, secretFile] = process.argv.slice(2);\n\nif (mode === 'launch') {\n // A detached child whose parent exits at once: the office's process tree (which the launcher kills on a restart)\n // doesn't include it, and on Windows nothing ties it to a console window.\n spawn(process.execPath, [...process.execArgv, process.argv[1], 'serve', socketPath, secretFile], { detached: true, stdio: 'ignore', windowsHide: true }).unref();\n process.exit(0);\n}\n\nconst logFile = path.join(path.dirname(secretFile), 'pty-host.log');\nconst note = (text: string) => fs.appendFile(logFile, `${new Date().toISOString()} ${text}\\n`, () => undefined);\nprocess.on('uncaughtException', (err) => note(`uncaught: ${err.stack ?? err}`));\nfor (const sig of ['SIGINT', 'SIGHUP', 'SIGBREAK'] as const) process.on(sig, () => undefined); // the office's console isn't ours\n\nconst secret = fs.readFileSync(secretFile, 'utf8').trim();\n\ninterface Held {\n proc: nodePty.IPty;\n meta: unknown;\n /** An office is taking this terminal's output right now. */\n attached: boolean;\n backlog: string[];\n bytes: number;\n exit: number | null;\n /** The office stopped it: nobody needs to hear how it ended. */\n killed: boolean;\n}\nconst held = new Map<string, Held>();\nlet office: net.Socket | null = null;\nlet officeLeft = Date.now();\nlet hooksOpen = false; // the connected office has picked up its terminals and takes hooks\n\nconst send = (m: FromHost) => {\n if (office && !office.destroyed) office.write(encode(m));\n};\n\nfunction killTree(pid: number) {\n if (process.platform === 'win32') {\n spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).once('error', () => undefined);\n return;\n }\n try {\n process.kill(-pid, 'SIGTERM'); // the CLI leads its own session: take its MCP servers and tools with it\n } catch {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // already gone\n }\n }\n}\n\n// ---------- terminals ----------\n\nfunction start(m: Extract<ToHost, { op: 'spawn' }>) {\n let proc: nodePty.IPty;\n try {\n // Windows' own console host lingers after each CLI exits (one conhost.exe per terminal, forever in a keeper that\n // outlives restarts); the one node-pty ships goes with its terminal.\n proc = nodePty.spawn(m.file, m.args, { name: 'xterm-256color', cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env, useConptyDll: true });\n } catch (err) {\n send({ op: 'failed', id: m.id, error: (err as Error).message });\n return;\n }\n const h: Held = { proc, meta: m.meta, attached: true, backlog: [], bytes: 0, exit: null, killed: false };\n held.set(m.id, h);\n send({ op: 'spawned', id: m.id, pid: proc.pid });\n proc.onData((data) => {\n if (h.attached && office) return send({ op: 'data', id: m.id, data });\n h.backlog.push(data);\n h.bytes += data.length;\n while (h.bytes > BACKLOG_BYTES && h.backlog.length > 1) h.bytes -= h.backlog.shift()!.length;\n });\n proc.onExit(({ exitCode }) => {\n h.exit = exitCode;\n quietly(() => proc.kill()); // on Windows the terminal's console host (conhost.exe) outlives the CLI until then\n if (h.attached && office) send({ op: 'exit', id: m.id, code: exitCode });\n if ((h.attached && office) || h.killed) held.delete(m.id);\n });\n}\n\nfunction attach(id: string) {\n const h = held.get(id);\n if (!h) return send({ op: 'exit', id, code: -1 });\n h.attached = true;\n if (h.backlog.length) send({ op: 'data', id, data: h.backlog.join('') });\n h.backlog = [];\n h.bytes = 0;\n if (h.exit !== null) {\n send({ op: 'exit', id, code: h.exit });\n held.delete(id);\n }\n}\n\nfunction stopAll() {\n for (const h of held.values()) if (h.exit === null) killTree(h.proc.pid);\n}\n\n// ---------- hooks: CLIs call here, the office answers ----------\n\ninterface Waiting {\n token: string;\n body: unknown;\n res: http.ServerResponse;\n timer: NodeJS.Timeout;\n sent: boolean;\n}\nconst waiting = new Map<number, Waiting>();\nlet nextRid = 1;\n\nfunction answer(rid: number, reply: unknown) {\n const w = waiting.get(rid);\n if (!w) return;\n waiting.delete(rid);\n clearTimeout(w.timer);\n if (!w.res.writableEnded) w.res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(reply ?? {}));\n}\n\n/** Hooks that waited for an office go to the one that's ready (again, if the office before it went without answering). */\nfunction passHooks() {\n if (!office || !hooksOpen) return;\n for (const [rid, w] of waiting) {\n if (w.sent) continue;\n w.sent = true;\n send({ op: 'hook', rid, token: w.token, body: w.body });\n }\n}\n\nconst hooks = http.createServer((req, res) => {\n const token = req.method === 'POST' ? req.url?.match(/^\\/api\\/hooks\\/([\\w-]+)$/)?.[1] : undefined;\n if (!token) return void res.writeHead(404).end();\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size <= 64 * 1024 * 1024) chunks.push(c);\n });\n req.on('end', () => {\n let body: unknown = {};\n try {\n body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');\n } catch {\n // not JSON: an empty hook\n }\n const rid = nextRid++;\n waiting.set(rid, { token, body, res, timer: setTimeout(() => answer(rid, {}), HOOK_WAIT_MS), sent: false });\n passHooks();\n });\n});\n\n// ---------- the office ----------\n\nfunction connected(sock: net.Socket) {\n let helloed = false;\n readMessages<ToHost>(sock, (m) => {\n if (!helloed) {\n if (m.op !== 'hello' || m.secret !== secret) return void sock.destroy();\n helloed = true;\n if (office && office !== sock) office.destroy();\n office = sock;\n hooksOpen = false;\n const ptys: HeldPty[] = [...held].map(([id, h]) => ({ id, pid: h.proc.pid, meta: h.meta, exit: h.exit }));\n send({ op: 'ready', version: PTY_PROTOCOL, hookPort: (hooks.address() as net.AddressInfo).port, ptys, pid: process.pid });\n return;\n }\n const h = 'id' in m ? held.get(m.id) : undefined;\n switch (m.op) {\n case 'spawn':\n return start(m);\n case 'attach':\n return attach(m.id);\n case 'write':\n return void (h && h.exit === null && quietly(() => h.proc.write(m.data)));\n case 'resize':\n return void (h && h.exit === null && quietly(() => h.proc.resize(m.cols, m.rows)));\n case 'meta':\n if (h) h.meta = m.meta;\n return;\n case 'kill':\n if (h) h.killed = true;\n if (h?.exit === null) killTree(h.proc.pid);\n else if (h) held.delete(m.id);\n return;\n case 'hooksReady':\n hooksOpen = true;\n return passHooks();\n case 'hookReply':\n return answer(m.rid, m.answer);\n case 'shutdown':\n stopAll();\n setTimeout(() => process.exit(0), 3000);\n return;\n }\n });\n sock.on('error', () => undefined);\n sock.on('close', () => {\n if (office !== sock) return;\n office = null;\n officeLeft = Date.now();\n for (const h of held.values()) h.attached = false;\n for (const w of waiting.values()) w.sent = false; // unanswered: the next office gets them\n });\n}\n\n/** A pseudo-terminal whose CLI just died throws on writes and resizes before it reports the exit. */\nfunction quietly(fn: () => void) {\n try {\n fn();\n } catch {\n // its exit is on the way\n }\n}\n\nconst server = net.createServer(connected);\nserver.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code !== 'EADDRINUSE' || process.platform === 'win32') {\n note(`cannot listen on ${socketPath}: ${err.message}`);\n process.exit(1);\n }\n // A Unix socket file left behind: take it over, unless a keeper still answers on it.\n const probe = net.connect(socketPath, () => {\n probe.destroy();\n process.exit(0);\n });\n probe.on('error', () => {\n fs.rmSync(socketPath, { force: true });\n server.listen(socketPath);\n });\n});\n\nhooks.listen(0, '127.0.0.1', () => server.listen(socketPath));\n\nlet orphaned = false;\nsetInterval(() => {\n if (office) return void (orphaned = false);\n const away = Date.now() - officeLeft;\n if (away > ORPHAN_MS) {\n for (const [id, h] of held) if (h.exit !== null) held.delete(id);\n if (held.size && !orphaned) {\n orphaned = true;\n note(`no office for ${Math.round(away / 60_000)} min: stopping ${held.size} terminal(s)`);\n stopAll();\n }\n }\n if (held.size === 0 && away > IDLE_EXIT_MS) process.exit(0);\n}, 10_000).unref();\n", "import crypto from 'node:crypto';\nimport path from 'node:path';\nimport type { Socket } from 'node:net';\n\n// What the office and its terminal keeper (ptyHost.ts) say to each other over their local socket: one JSON message\n// per line. Kept free of the rest of the server so the keeper stays small and starts fast.\n\n/** Bumped when the messages change: an office that finds a keeper on another version replaces it. */\nexport const PTY_PROTOCOL = 1;\n\n/** A terminal the keeper holds, as it reports it to an office that (re)connects. */\nexport interface HeldPty {\n id: string;\n pid: number;\n /** What the office stored with it (who it belongs to, its session). */\n meta: unknown;\n /** The CLI has exited (while no office was listening): its exit code. */\n exit: number | null;\n}\n\nexport type ToHost =\n | { op: 'hello'; secret: string; version: number }\n | { op: 'hooksReady' } // the office has picked up its terminals: hooks that waited for it can come in now\n | { op: 'spawn'; id: string; file: string; args: string[]; cwd: string; env: Record<string, string>; cols: number; rows: number; meta: unknown }\n | { op: 'attach'; id: string } // an office picking up a terminal it found: what it missed, then everything live\n | { op: 'write'; id: string; data: string }\n | { op: 'resize'; id: string; cols: number; rows: number }\n | { op: 'meta'; id: string; meta: unknown }\n | { op: 'kill'; id: string }\n | { op: 'hookReply'; rid: number; answer: unknown }\n | { op: 'shutdown' }; // stop every terminal and exit (a keeper on another version)\n\nexport type FromHost =\n | { op: 'ready'; version: number; hookPort: number; ptys: HeldPty[]; pid?: number }\n | { op: 'spawned'; id: string; pid: number }\n | { op: 'failed'; id: string; error: string }\n | { op: 'data'; id: string; data: string }\n | { op: 'exit'; id: string; code: number }\n | { op: 'hook'; rid: number; token: string; body: unknown };\n\n/** The keeper's socket for an office home: a named pipe on Windows, a Unix socket in the home elsewhere. */\nexport function hostSocket(home: string, platform = process.platform): string {\n if (platform !== 'win32') return path.join(home, 'pty.sock');\n const tag = crypto.createHash('sha1').update(path.resolve(home).toLowerCase()).digest('hex').slice(0, 12);\n return `\\\\\\\\.\\\\pipe\\\\cubefarm-pty-${tag}`;\n}\n\nexport const encode = (m: ToHost | FromHost) => `${JSON.stringify(m)}\\n`;\n\n/** Calls back with each message a socket sends (newline-delimited JSON); a line that isn't JSON is skipped. */\nexport function readMessages<T>(sock: Socket, onMessage: (m: T) => void) {\n let buf = '';\n sock.setEncoding('utf8');\n sock.on('data', (chunk: string) => {\n buf += chunk;\n let nl: number;\n while ((nl = buf.indexOf('\\n')) >= 0) {\n const line = buf.slice(0, nl);\n buf = buf.slice(nl + 1);\n if (!line) continue;\n let m: T;\n try {\n m = JSON.parse(line) as T;\n } catch {\n continue;\n }\n onMessage(m);\n }\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["h"]
|
|
7
7
|
}
|