workerdeck 0.22.0 → 1.0.0
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/build/cli.mjs +18 -5
- package/build/cli.mjs.map +1 -1
- package/build/{guard-DkovUj7o.mjs → guard-BJNUnO3H.mjs} +1 -26
- package/build/guard-BJNUnO3H.mjs.map +1 -0
- package/build/index.d.mts +36 -327
- package/build/index.mjs +2 -2
- package/build/{instance-iBl_fOW2.mjs → instance-5sdv2Rrw.mjs} +91 -392
- package/build/instance-5sdv2Rrw.mjs.map +1 -0
- package/package.json +4 -4
- package/build/guard-DkovUj7o.mjs.map +0 -1
- package/build/instance-iBl_fOW2.mjs.map +0 -1
package/build/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-
|
|
2
|
+
import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-5sdv2Rrw.mjs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -84,7 +84,6 @@ async function readVersion() {
|
|
|
84
84
|
return "unknown";
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
/** Best-effort: a browser that won't open is a convenience missed, not a failure. */
|
|
88
87
|
function openInBrowser(url) {
|
|
89
88
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
90
89
|
try {
|
|
@@ -97,9 +96,12 @@ function openInBrowser(url) {
|
|
|
97
96
|
child.unref();
|
|
98
97
|
} catch {}
|
|
99
98
|
}
|
|
99
|
+
function line(text) {
|
|
100
|
+
process.stdout.write(`${text}\n`);
|
|
101
|
+
}
|
|
100
102
|
async function main(argv) {
|
|
101
103
|
if (argv[0] === "guard") {
|
|
102
|
-
const { runGuard } = await import("./guard-
|
|
104
|
+
const { runGuard } = await import("./guard-BJNUnO3H.mjs").then((n) => n.t);
|
|
103
105
|
return await runGuard(argv.slice(1));
|
|
104
106
|
}
|
|
105
107
|
const flags = parseArgs(argv);
|
|
@@ -114,9 +116,20 @@ async function main(argv) {
|
|
|
114
116
|
const config = resolveInstanceConfig(flags, await loadConfigFile(flags.config));
|
|
115
117
|
const instance = await startInstance(config);
|
|
116
118
|
if (config.open) openInBrowser(instance.url);
|
|
119
|
+
let shuttingDown = false;
|
|
117
120
|
const shutdown = (signal) => {
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
if (shuttingDown) {
|
|
122
|
+
process.stdout.write(`\n[workerdeck] ${signal} again — terminating now\n`);
|
|
123
|
+
process.exit(130);
|
|
124
|
+
}
|
|
125
|
+
shuttingDown = true;
|
|
126
|
+
line(`\n[workerdeck] ${signal} — shutting down (press again to stop now)`);
|
|
127
|
+
instance.drain({ onProgress: (report) => {
|
|
128
|
+
if (report.working.length > 0) line(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`);
|
|
129
|
+
for (const id of report.awaitingHuman) line(`[workerdeck] session ${id} is waiting on an approval — not waiting for it`);
|
|
130
|
+
if (report.timedOut) line(`[workerdeck] ${report.working.length} session(s) still running — stopping anyway`);
|
|
131
|
+
else if (report.working.length === 0) line("[workerdeck] all turns finished");
|
|
132
|
+
} }).catch(() => void 0).then(() => instance.close()).then(() => process.exit(0)).catch(() => process.exit(1));
|
|
120
133
|
};
|
|
121
134
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
122
135
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
package/build/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { spawn } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigError, loadConfigFile, parseArgs, resolveInstanceConfig } from './config.ts'\nimport { startInstance } from './lib/instance.ts'\n\nconst HELP = `workerdeck — run a workerdeck instance: session gateway + dashboard, one port.\n\nUsage\n workerdeck [options]\n workerdeck guard [options] check whether it is safe to restart an instance\n\nOptions\n -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)\n --host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)\n --auth-key <secret> shared secret, min 12 chars; browsers log in with it,\n services send it as x-workerdeck-key\n (WORKERDECK_AUTH_KEY). Unset = no auth on loopback;\n on any other interface a key is generated instead,\n printed once, and stored in <state-dir>/auth-key for\n later starts to reuse.\n --trust-proxy trust x-forwarded-proto/-host/-for from one reverse\n proxy. Required behind TLS termination, or the session\n cookie loses its Secure flag and the origin check\n computes http:// where the browser says https://.\n --allowed-origin <o> extra origin accepted on browser requests, for when a\n proxy rewrites Host (repeatable)\n --allowed-host <name> extra Host header accepted when running without auth\n (repeatable; loopback names are always accepted)\n --insecure-host <name>\n bind host that may serve without auth — no key demanded,\n none generated — and, while unauthenticated, also\n accepted as a Host header (repeatable; config:\n insecureHosts). Names the host alone, no port.\n --profile <name=dir> Claude config dir a session may run under (repeatable)\n --cwd-root <path> restrict session cwds to this root (repeatable,\n WORKERDECK_CWD_ROOTS as a ':'-separated list)\n --fs-root <path> narrow which host directories /v1/fs serves\n (repeatable, WORKERDECK_FS_ROOTS as a ':'-separated\n list). Reading otherwise follows --cwd-root: a caller\n who may start a session in a tree can already read it\n through the agent. With neither, the routes 404.\n --fs-write also accept writes over /v1/fs. Its own switch because\n an agent's writes go through the permission flow and a\n PUT does not. Every write is still conditional on the\n hash the client last read.\n --state-dir <path> where parked sessions are persisted\n (default: beside the config file, else ~/.workerdeck)\n --no-parking-store keep parked sessions in memory only; a restart drops them\n -c, --config <path> config file (default: ./workerdeck.config.mjs)\n --insecure allow no-auth on a non-loopback address. Only when\n something in front is doing the authenticating.\n --cors-origin <o> browser origin allowed to call /v1 cross-origin, for a\n dashboard served elsewhere (repeatable, exact origin, no\n wildcard; config: corsOrigins). Refused without auth. The\n key is still required — this only permits the call.\n --no-web don't serve the web dashboard (config: web: false).\n /v1 and the auth routes are unchanged; everything else\n 404s. For a gateway reached only from the VS Code\n extension, the phone, or another host's dashboard.\n --open open the dashboard in a browser once it is up\n -h, --help show this\n -v, --version print the version\n\nConfig file\n Options that cannot fit on a command line — \\`authenticate\\`, \\`buildRunnerConfig\\`,\n \\`createEngineRunner\\` are functions — live in workerdeck.config.mjs, which\n default-exports the createWorkerServer options (or a function returning them).\n Flags and env override it. Supplying your own \\`authenticate\\` turns the built-in\n shared-secret auth off entirely.\n\nCredentials\n workerdeck implements no Anthropic auth of its own: the official SDK/CLI\n resolves credentials from the environment, per profile. --auth-key protects this\n gateway, nothing else.\n`\n\nasync function readVersion(): Promise<string> {\n // src/cli.ts and build/cli.mjs are both one level under the package root.\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')\n try {\n const raw = await readFile(pkgPath, 'utf8')\n return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\n/** Best-effort: a browser that won't open is a convenience missed, not a failure. */\nfunction openInBrowser(url: string): void {\n const command =\n process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'\n try {\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n })\n child.on('error', () => {})\n child.unref()\n } catch {\n // ignored\n }\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./lib/guard.ts')\n return await runGuard(argv.slice(1))\n }\n\n const flags = parseArgs(argv)\n if (flags.help) {\n process.stdout.write(HELP)\n return 0\n }\n if (flags.version) {\n process.stdout.write(`${await readVersion()}\\n`)\n return 0\n }\n\n const loaded = await loadConfigFile(flags.config)\n const config = resolveInstanceConfig(flags, loaded)\n const instance = await startInstance(config)\n\n if (config.open) openInBrowser(instance.url)\n\n const shutdown = (signal: string): void => {\n process.stdout.write(`\\n[workerdeck] ${signal} — shutting down\\n`)\n // Parked sessions are already on disk; this is about letting in-flight\n // requests finish rather than dropping sockets on the floor.\n instance\n .close()\n .then(() => process.exit(0))\n .catch(() => process.exit(1))\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n // Resolves only on close; the process stays up serving.\n await instance.closed\n return 0\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n if (code !== 0) process.exit(code)\n })\n .catch((error: unknown) => {\n if (error instanceof ConfigError) {\n process.stderr.write(`[workerdeck] ${error.message}\\n`)\n process.exit(2)\n }\n process.stderr.write(\n `[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n )\n process.exit(1)\n })\n"],"mappings":";;;;;;;AAQA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEb,eAAe,cAA+B;CAE5C,MAAM,UAAU,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,MAAM,eAAe;AACnF,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,SAAQ,KAAK,MAAM,IAAI,CAA0B,WAAW;SACtD;AACN,SAAO;;;;AAKX,SAAS,cAAc,KAAmB;CACxC,MAAM,UACJ,QAAQ,aAAa,WAAW,SAAS,QAAQ,aAAa,UAAU,UAAU;AACpF,KAAI;EACF,MAAM,QAAQ,MAAM,SAAS,CAAC,IAAI,EAAE;GAClC,OAAO;GACP,UAAU;GACV,OAAO,QAAQ,aAAa;GAC7B,CAAC;AACF,QAAM,GAAG,eAAe,GAAG;AAC3B,QAAM,OAAO;SACP;;AAKV,eAAe,KAAK,MAAiC;AACnD,KAAI,KAAK,OAAO,SAAS;EACvB,MAAM,EAAE,aAAa,MAAM,OAAO,wBAAA,MAAA,MAAA,EAAA,EAAA;AAClC,SAAO,MAAM,SAAS,KAAK,MAAM,EAAE,CAAC;;CAGtC,MAAM,QAAQ,UAAU,KAAK;AAC7B,KAAI,MAAM,MAAM;AACd,UAAQ,OAAO,MAAM,KAAK;AAC1B,SAAO;;AAET,KAAI,MAAM,SAAS;AACjB,UAAQ,OAAO,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI;AAChD,SAAO;;CAIT,MAAM,SAAS,sBAAsB,OAAO,MADvB,eAAe,MAAM,OAAO,CACE;CACnD,MAAM,WAAW,MAAM,cAAc,OAAO;AAE5C,KAAI,OAAO,KAAM,eAAc,SAAS,IAAI;CAE5C,MAAM,YAAY,WAAyB;AACzC,UAAQ,OAAO,MAAM,kBAAkB,OAAO,oBAAoB;AAGlE,WACG,OAAO,CACP,WAAW,QAAQ,KAAK,EAAE,CAAC,CAC3B,YAAY,QAAQ,KAAK,EAAE,CAAC;;AAEjC,SAAQ,GAAG,gBAAgB,SAAS,SAAS,CAAC;AAC9C,SAAQ,GAAG,iBAAiB,SAAS,UAAU,CAAC;AAGhD,OAAM,SAAS;AACf,QAAO;;AAGT,KAAK,QAAQ,KAAK,MAAM,EAAE,CAAC,CACxB,MAAM,SAAS;AACd,KAAI,SAAS,EAAG,SAAQ,KAAK,KAAK;EAClC,CACD,OAAO,UAAmB;AACzB,KAAI,iBAAiB,aAAa;AAChC,UAAQ,OAAO,MAAM,gBAAgB,MAAM,QAAQ,IAAI;AACvD,UAAQ,KAAK,EAAE;;AAEjB,SAAQ,OAAO,MACb,gBAAgB,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,MAAM,CAAC,IACzF;AACD,SAAQ,KAAK,EAAE;EACf"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { spawn } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigError, loadConfigFile, parseArgs, resolveInstanceConfig } from './config.ts'\nimport { startInstance } from './lib/instance.ts'\n\nconst HELP = `workerdeck — run a workerdeck instance: session gateway + dashboard, one port.\n\nUsage\n workerdeck [options]\n workerdeck guard [options] check whether it is safe to restart an instance\n\nOptions\n -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)\n --host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)\n --auth-key <secret> shared secret, min 12 chars; browsers log in with it,\n services send it as x-workerdeck-key\n (WORKERDECK_AUTH_KEY). Unset = no auth on loopback;\n on any other interface a key is generated instead,\n printed once, and stored in <state-dir>/auth-key for\n later starts to reuse.\n --trust-proxy trust x-forwarded-proto/-host/-for from one reverse\n proxy. Required behind TLS termination, or the session\n cookie loses its Secure flag and the origin check\n computes http:// where the browser says https://.\n --allowed-origin <o> extra origin accepted on browser requests, for when a\n proxy rewrites Host (repeatable)\n --allowed-host <name> extra Host header accepted when running without auth\n (repeatable; loopback names are always accepted)\n --insecure-host <name>\n bind host that may serve without auth — no key demanded,\n none generated — and, while unauthenticated, also\n accepted as a Host header (repeatable; config:\n insecureHosts). Names the host alone, no port.\n --profile <name=dir> Claude config dir a session may run under (repeatable)\n --cwd-root <path> restrict session cwds to this root (repeatable,\n WORKERDECK_CWD_ROOTS as a ':'-separated list)\n --fs-root <path> narrow which host directories /v1/fs serves\n (repeatable, WORKERDECK_FS_ROOTS as a ':'-separated\n list). Reading otherwise follows --cwd-root: a caller\n who may start a session in a tree can already read it\n through the agent. With neither, the routes 404.\n --fs-write also accept writes over /v1/fs. Its own switch because\n an agent's writes go through the permission flow and a\n PUT does not. Every write is still conditional on the\n hash the client last read.\n --state-dir <path> where parked sessions are persisted\n (default: beside the config file, else ~/.workerdeck)\n --no-parking-store keep parked sessions in memory only; a restart drops them\n -c, --config <path> config file (default: ./workerdeck.config.mjs)\n --insecure allow no-auth on a non-loopback address. Only when\n something in front is doing the authenticating.\n --cors-origin <o> browser origin allowed to call /v1 cross-origin, for a\n dashboard served elsewhere (repeatable, exact origin, no\n wildcard; config: corsOrigins). Refused without auth. The\n key is still required — this only permits the call.\n --no-web don't serve the web dashboard (config: web: false).\n /v1 and the auth routes are unchanged; everything else\n 404s. For a gateway reached only from the VS Code\n extension, the phone, or another host's dashboard.\n --open open the dashboard in a browser once it is up\n -h, --help show this\n -v, --version print the version\n\nConfig file\n Options that cannot fit on a command line — \\`authenticate\\`, \\`buildRunnerConfig\\`,\n \\`createEngineRunner\\` are functions — live in workerdeck.config.mjs, which\n default-exports the createWorkerServer options (or a function returning them).\n Flags and env override it. Supplying your own \\`authenticate\\` turns the built-in\n shared-secret auth off entirely.\n\nCredentials\n workerdeck implements no Anthropic auth of its own: the official SDK/CLI\n resolves credentials from the environment, per profile. --auth-key protects this\n gateway, nothing else.\n`\n\nasync function readVersion(): Promise<string> {\n // src/cli.ts and build/cli.mjs are both one level under the package root.\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')\n try {\n const raw = await readFile(pkgPath, 'utf8')\n return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\nfunction openInBrowser(url: string): void {\n const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'\n try {\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n })\n child.on('error', () => {})\n child.unref()\n } catch {}\n}\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./lib/guard.ts')\n return await runGuard(argv.slice(1))\n }\n\n const flags = parseArgs(argv)\n if (flags.help) {\n process.stdout.write(HELP)\n return 0\n }\n if (flags.version) {\n process.stdout.write(`${await readVersion()}\\n`)\n return 0\n }\n\n const loaded = await loadConfigFile(flags.config)\n const config = resolveInstanceConfig(flags, loaded)\n const instance = await startInstance(config)\n\n if (config.open) {\n openInBrowser(instance.url)\n }\n\n // A second signal must always be able to kill a shutdown that is taking too long. That used to work only by\n // accident — the second call re-entered `instance.close()` and got an immediate callback out of an already-closed\n // http server — so it evaporated the moment close stopped being idempotent-by-luck. Make it a real path.\n let shuttingDown = false\n const shutdown = (signal: string): void => {\n if (shuttingDown) {\n process.stdout.write(`\\n[workerdeck] ${signal} again — terminating now\\n`)\n process.exit(130)\n }\n shuttingDown = true\n line(`\\n[workerdeck] ${signal} — shutting down (press again to stop now)`)\n instance\n .drain({\n onProgress: (report) => {\n if (report.working.length > 0) {\n line(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`)\n }\n // Named, not waited for: nothing about shutting down answers a permission prompt.\n for (const id of report.awaitingHuman) {\n line(`[workerdeck] session ${id} is waiting on an approval — not waiting for it`)\n }\n if (report.timedOut) {\n line(`[workerdeck] ${report.working.length} session(s) still running — stopping anyway`)\n } else if (report.working.length === 0) {\n line('[workerdeck] all turns finished')\n }\n },\n })\n .catch(() => undefined)\n .then(() => instance.close())\n .then(() => process.exit(0))\n .catch(() => process.exit(1))\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n // Resolves only on close: until then the process stays up, serving.\n await instance.closed\n return 0\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n if (code !== 0) {\n process.exit(code)\n }\n })\n .catch((error: unknown) => {\n if (error instanceof ConfigError) {\n process.stderr.write(`[workerdeck] ${error.message}\\n`)\n process.exit(2)\n }\n process.stderr.write(`[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n process.exit(1)\n })\n"],"mappings":";;;;;;;AAQA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEb,eAAe,cAA+B;CAE5C,MAAM,UAAU,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,MAAM,eAAe;AACnF,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,SAAQ,KAAK,MAAM,IAAI,CAA0B,WAAW;SACtD;AACN,SAAO;;;AAIX,SAAS,cAAc,KAAmB;CACxC,MAAM,UAAU,QAAQ,aAAa,WAAW,SAAS,QAAQ,aAAa,UAAU,UAAU;AAClG,KAAI;EACF,MAAM,QAAQ,MAAM,SAAS,CAAC,IAAI,EAAE;GAClC,OAAO;GACP,UAAU;GACV,OAAO,QAAQ,aAAa;GAC7B,CAAC;AACF,QAAM,GAAG,eAAe,GAAG;AAC3B,QAAM,OAAO;SACP;;AAGV,SAAS,KAAK,MAAoB;AAChC,SAAQ,OAAO,MAAM,GAAG,KAAK,IAAI;;AAGnC,eAAe,KAAK,MAAiC;AACnD,KAAI,KAAK,OAAO,SAAS;EACvB,MAAM,EAAE,aAAa,MAAM,OAAO,wBAAA,MAAA,MAAA,EAAA,EAAA;AAClC,SAAO,MAAM,SAAS,KAAK,MAAM,EAAE,CAAC;;CAGtC,MAAM,QAAQ,UAAU,KAAK;AAC7B,KAAI,MAAM,MAAM;AACd,UAAQ,OAAO,MAAM,KAAK;AAC1B,SAAO;;AAET,KAAI,MAAM,SAAS;AACjB,UAAQ,OAAO,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI;AAChD,SAAO;;CAIT,MAAM,SAAS,sBAAsB,OAAO,MADvB,eAAe,MAAM,OAAO,CACE;CACnD,MAAM,WAAW,MAAM,cAAc,OAAO;AAE5C,KAAI,OAAO,KACT,eAAc,SAAS,IAAI;CAM7B,IAAI,eAAe;CACnB,MAAM,YAAY,WAAyB;AACzC,MAAI,cAAc;AAChB,WAAQ,OAAO,MAAM,kBAAkB,OAAO,4BAA4B;AAC1E,WAAQ,KAAK,IAAI;;AAEnB,iBAAe;AACf,OAAK,kBAAkB,OAAO,4CAA4C;AAC1E,WACG,MAAM,EACL,aAAa,WAAW;AACtB,OAAI,OAAO,QAAQ,SAAS,EAC1B,MAAK,4BAA4B,OAAO,QAAQ,OAAO,wCAAwC;AAGjG,QAAK,MAAM,MAAM,OAAO,cACtB,MAAK,wBAAwB,GAAG,iDAAiD;AAEnF,OAAI,OAAO,SACT,MAAK,gBAAgB,OAAO,QAAQ,OAAO,6CAA6C;YAC/E,OAAO,QAAQ,WAAW,EACnC,MAAK,kCAAkC;KAG5C,CAAC,CACD,YAAY,KAAA,EAAU,CACtB,WAAW,SAAS,OAAO,CAAC,CAC5B,WAAW,QAAQ,KAAK,EAAE,CAAC,CAC3B,YAAY,QAAQ,KAAK,EAAE,CAAC;;AAEjC,SAAQ,GAAG,gBAAgB,SAAS,SAAS,CAAC;AAC9C,SAAQ,GAAG,iBAAiB,SAAS,UAAU,CAAC;AAGhD,OAAM,SAAS;AACf,QAAO;;AAGT,KAAK,QAAQ,KAAK,MAAM,EAAE,CAAC,CACxB,MAAM,SAAS;AACd,KAAI,SAAS,EACX,SAAQ,KAAK,KAAK;EAEpB,CACD,OAAO,UAAmB;AACzB,KAAI,iBAAiB,aAAa;AAChC,UAAQ,OAAO,MAAM,gBAAgB,MAAM,QAAQ,IAAI;AACvD,UAAQ,KAAK,EAAE;;AAEjB,SAAQ,OAAO,MAAM,gBAAgB,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,MAAM,CAAC,IAAI;AACjH,SAAQ,KAAK,EAAE;EACf"}
|
|
@@ -13,29 +13,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/lib/guard.ts
|
|
15
15
|
var guard_exports = /* @__PURE__ */ __exportAll({ runGuard: () => runGuard });
|
|
16
|
-
/**
|
|
17
|
-
* Restart guard for a deploy: ask a running instance whether anything would be
|
|
18
|
-
* lost by stopping it, and exit non-zero while the answer is yes.
|
|
19
|
-
*
|
|
20
|
-
* workerdeck guard --wait 300 --allow-parked && launchctl kickstart -k …
|
|
21
|
-
*
|
|
22
|
-
* What a restart costs, and why this is policy and not a server route: an
|
|
23
|
-
* in-flight turn dies with the process (the CLI subprocess and the provider
|
|
24
|
-
* request both go), a pending permission request dies with it, and a running job
|
|
25
|
-
* is left claimed. Two more depend on configuration rather than on state, so each
|
|
26
|
-
* has an opt-out flag that says "I made that durable":
|
|
27
|
-
*
|
|
28
|
-
* --allow-parked parked sessions survive with a durable SessionStore, not
|
|
29
|
-
* with the in-memory default. Note this covers the *session*:
|
|
30
|
-
* a parked job's queue-side record lives in the QueueAdapter,
|
|
31
|
-
* so with the bundled in-memory adapter the woken session
|
|
32
|
-
* completes with no job attached to finish.
|
|
33
|
-
* --allow-queued queued jobs survive only in a durable QueueAdapter; the
|
|
34
|
-
* bundled one loses them with the process.
|
|
35
|
-
*
|
|
36
|
-
* Exit codes: 0 safe to restart, 1 still busy, 2 could not tell (bad URL, auth,
|
|
37
|
-
* or an unexpected response — never treated as safe).
|
|
38
|
-
*/
|
|
39
16
|
const BUSY_STATUSES = new Set([
|
|
40
17
|
"starting",
|
|
41
18
|
"running",
|
|
@@ -155,7 +132,6 @@ async function runGuard(argv) {
|
|
|
155
132
|
};
|
|
156
133
|
}
|
|
157
134
|
};
|
|
158
|
-
/** One look at the server. Returns the reasons a restart would cost something. */
|
|
159
135
|
const inspect = async () => {
|
|
160
136
|
const sessions = await get("/sessions");
|
|
161
137
|
if (!sessions.ok && "status" in sessions) return {
|
|
@@ -176,7 +152,6 @@ async function runGuard(argv) {
|
|
|
176
152
|
};
|
|
177
153
|
const all = listed;
|
|
178
154
|
const reasons = [];
|
|
179
|
-
/** Worth saying out loud, but not worth blocking a deploy over. */
|
|
180
155
|
const notes = [];
|
|
181
156
|
for (const session of all.filter((s) => BUSY_STATUSES.has(s.status))) reasons.push(`session ${session.id} is ${session.status}`);
|
|
182
157
|
const parked = all.filter((s) => s.status === "parked");
|
|
@@ -238,4 +213,4 @@ async function runGuard(argv) {
|
|
|
238
213
|
//#endregion
|
|
239
214
|
export { runGuard as n, guard_exports as t };
|
|
240
215
|
|
|
241
|
-
//# sourceMappingURL=guard-
|
|
216
|
+
//# sourceMappingURL=guard-BJNUnO3H.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-BJNUnO3H.mjs","names":[],"sources":["../src/lib/guard.ts"],"sourcesContent":["import { parseArgs } from 'node:util'\n\nconst BUSY_STATUSES = new Set(['starting', 'running', 'awaiting_approval'])\n\nconst HELP = `usage: workerdeck guard [--url URL] [--token TOKEN] [--header name=value]\n [--wait SECONDS] [--interval SECONDS]\n [--allow-parked] [--allow-queued] [--json]\n\nexit 0 = safe to restart, 1 = busy, 2 = could not tell\n`\n\ntype GetResult =\n | { ok: true; body: Record<string, unknown> }\n | { ok: false; status: 'unreachable' }\n | { ok: false; code?: number; detail: string }\n\ntype Verdict = {\n reasons: string[]\n notes: string[]\n unreachable?: boolean\n error?: string\n sessions?: number\n parked?: number\n}\n\nclass GuardError extends Error {}\n\nexport async function runGuard(argv: string[]): Promise<number> {\n let values: {\n url: string\n token?: string\n header: string[]\n wait: string\n interval: string\n 'allow-parked': boolean\n 'allow-queued': boolean\n json: boolean\n help: boolean\n }\n try {\n values = parseArgs({\n args: argv,\n options: {\n url: {\n type: 'string',\n default: process.env.WORKERDECK_URL ?? 'http://127.0.0.1:8787/v1',\n },\n token: { type: 'string', default: process.env.WORKERDECK_TOKEN },\n header: { type: 'string', multiple: true, default: [] },\n wait: { type: 'string', default: '0' },\n interval: { type: 'string', default: '5' },\n 'allow-parked': { type: 'boolean', default: false },\n 'allow-queued': { type: 'boolean', default: false },\n json: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false },\n },\n }).values as typeof values\n } catch (error) {\n process.stderr.write(`guard: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 2\n }\n\n if (values.help) {\n process.stdout.write(HELP)\n return 0\n }\n\n const base = values.url.replace(/\\/$/, '')\n const headers: Record<string, string> = { accept: 'application/json' }\n if (values.token) {\n headers.authorization = `Bearer ${values.token}`\n }\n\n const seconds = (flag: string, raw: string): number => {\n const value = Number(raw)\n if (!Number.isFinite(value) || value < 0) {\n throw new GuardError(`${flag} must be a non-negative number of seconds`)\n }\n return value\n }\n\n let waitMs: number\n let intervalMs: number\n try {\n for (const entry of values.header) {\n const at = entry.indexOf('=')\n if (at < 1) {\n throw new GuardError(`--header must be name=value, got '${entry}'`)\n }\n headers[entry.slice(0, at).trim()] = entry.slice(at + 1).trim()\n }\n waitMs = seconds('--wait', values.wait) * 1000\n intervalMs = Math.max(seconds('--interval', values.interval), 1) * 1000\n } catch (error) {\n process.stderr.write(`guard: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 2\n }\n\n const get = async (path: string): Promise<GetResult> => {\n let res: Response\n try {\n res = await fetch(base + path, { headers })\n } catch (error) {\n // Nothing listening on the URL we were pointed at, so there is no session to lose.\n const code = (error as { cause?: { code?: string } })?.cause?.code\n if (code === 'ECONNREFUSED') {\n return { ok: false, status: 'unreachable' }\n }\n return { ok: false, detail: String(error instanceof Error ? error.message : error) }\n }\n if (!res.ok) {\n return { ok: false, code: res.status, detail: `HTTP ${res.status}` }\n }\n try {\n return { ok: true, body: (await res.json()) as Record<string, unknown> }\n } catch {\n return { ok: false, code: res.status, detail: 'response was not JSON' }\n }\n }\n\n const inspect = async (): Promise<Verdict> => {\n const sessions = await get('/sessions')\n if (!sessions.ok && 'status' in sessions) {\n return { unreachable: true, reasons: [], notes: [] }\n }\n if (!sessions.ok) {\n return { error: `GET ${base}/sessions → ${sessions.detail}`, reasons: [], notes: [] }\n }\n const listed = sessions.body.sessions\n if (!Array.isArray(listed)) {\n return { error: `GET ${base}/sessions returned no session list`, reasons: [], notes: [] }\n }\n const all = listed as { id: string; status: string }[]\n\n const reasons: string[] = []\n // Worth printing, but never worth blocking a deploy over.\n const notes: string[] = []\n for (const session of all.filter((s) => BUSY_STATUSES.has(s.status))) {\n reasons.push(`session ${session.id} is ${session.status}`)\n }\n const parked = all.filter((s) => s.status === 'parked')\n if (parked.length > 0 && !values['allow-parked']) {\n reasons.push(\n `${parked.length} parked session(s) — pass --allow-parked once the server ` +\n 'runs a durable SessionStore, or they are lost on restart',\n )\n }\n\n // A queue is optional, so a 404 here means this server declares none.\n const queue = await get('/queue')\n if (!queue.ok && !('status' in queue) && queue.code !== 404) {\n return { error: `GET ${base}/queue → ${queue.detail}`, reasons: [], notes: [] }\n }\n const stats = queue.ok ? (queue.body.stats as { running?: number; queued?: number; parked?: number } | undefined) : undefined\n if ((stats?.running ?? 0) > 0) {\n reasons.push(`${stats!.running} job(s) running`)\n }\n if ((stats?.queued ?? 0) > 0 && !values['allow-queued']) {\n reasons.push(\n `${stats!.queued} job(s) queued — pass --allow-queued once the server runs a ` +\n 'durable QueueAdapter, or they are lost on restart',\n )\n }\n if ((stats?.parked ?? 0) > 0 && values['allow-parked']) {\n notes.push(\n `${stats!.parked} parked job(s): their queue-side records are the QueueAdapter's, ` +\n \"not the SessionStore's — with the in-memory adapter they never finish\",\n )\n }\n return { reasons, notes, sessions: all.length, parked: parked.length }\n }\n\n const report = (verdict: string, detail: Verdict): void => {\n if (values.json) {\n process.stdout.write(`${JSON.stringify({ verdict, ...detail })}\\n`)\n return\n }\n const lines = [...detail.reasons.map((reason) => ` - ${reason}`), ...detail.notes.map((note) => ` note: ${note}`)].join('\\n')\n process.stdout.write(`guard: ${verdict}${lines ? `\\n${lines}` : ''}\\n`)\n }\n\n const deadline = Date.now() + waitMs\n for (;;) {\n const result = await inspect()\n if (result.error) {\n report('unknown', { ...result, reasons: [result.error] })\n return 2\n }\n if (result.unreachable) {\n report('safe to restart (nothing listening on the given URL)', result)\n return 0\n }\n if (result.reasons.length === 0) {\n report('safe to restart', result)\n return 0\n }\n if (Date.now() + intervalMs > deadline) {\n report('busy — not safe to restart', result)\n return 1\n }\n report(`busy, waiting up to ${Math.round((deadline - Date.now()) / 1000)}s`, result)\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n}\n"],"mappings":"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"}
|