workerdeck 0.0.0 → 0.7.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/LICENSE +21 -0
- package/README.md +100 -9
- package/build/cli.d.mts +1 -0
- package/build/cli.mjs +131 -0
- package/build/cli.mjs.map +1 -0
- package/build/guard-D3VN855w.mjs +241 -0
- package/build/guard-D3VN855w.mjs.map +1 -0
- package/build/index.d.mts +453 -0
- package/build/index.mjs +3 -0
- package/build/instance-DbIqlX-e.mjs +1586 -0
- package/build/instance-DbIqlX-e.mjs.map +1 -0
- package/package.json +55 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tobias Strebitzer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,16 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WorkerDeck
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The turnkey [WorkerDeck](https://github.com/workerdeck/workerdeck) instance: the
|
|
4
|
+
session gateway **and** the full dashboard, on one port, with nothing to clone.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
```bash
|
|
7
|
+
npx workerdeck
|
|
8
|
+
```
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
README and nothing else.
|
|
10
|
+
That serves `http://127.0.0.1:8787` — dashboard at the root, API under `/v1` — and persists parked
|
|
11
|
+
sessions under `~/.workerdeck` so a restart doesn't drop them.
|
|
11
12
|
|
|
12
|
-
The
|
|
13
|
+
The `@workerdeck/*` packages are the libraries you embed in your own app. This one is the
|
|
14
|
+
service you run.
|
|
13
15
|
|
|
14
|
-
##
|
|
16
|
+
## Protecting it
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx workerdeck --auth-key "$SECRET" --host 0.0.0.0
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
One secret, two transports:
|
|
23
|
+
|
|
24
|
+
- **Browsers** get a login page, trade the secret for an `HttpOnly` session cookie, and use that
|
|
25
|
+
for everything afterwards — including the WebSocket attach.
|
|
26
|
+
- **Services** send the same secret as an `x-workerdeck-key` header (or
|
|
27
|
+
`Authorization: Bearer`).
|
|
28
|
+
|
|
29
|
+
The cookie is not a convenience. A browser cannot set a header on a WebSocket handshake at all, so
|
|
30
|
+
a cookie is the only credential a tab can present when it attaches to a session — and a cookie only
|
|
31
|
+
rides requests to the origin that set it. That is why the dashboard and the API share a port.
|
|
32
|
+
|
|
33
|
+
Neither transport authenticates *who* the person is; the secret is a door key. Put an
|
|
34
|
+
identity-aware proxy in front if you need more than that.
|
|
35
|
+
|
|
36
|
+
**Without `--auth-key`, the instance refuses to bind anything but a loopback address.** An
|
|
37
|
+
unauthenticated gateway on a routable interface is a Claude Code shell for anyone who can reach the
|
|
38
|
+
port. `--insecure` overrides that, for when something in front is doing the authenticating.
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
| Flag | Env | Default |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `--port <n>` | `WORKERDECK_PORT` | `8787` |
|
|
45
|
+
| `--host <addr>` | `WORKERDECK_HOST` | `127.0.0.1` |
|
|
46
|
+
| `--auth-key <secret>` | `WORKERDECK_AUTH_KEY` | none (no auth) |
|
|
47
|
+
| `--cwd-root <path>` (repeatable) | `WORKERDECK_CWD_ROOTS` (`:`-separated) | unrestricted |
|
|
48
|
+
| `--fs-root <path>` (repeatable) | `WORKERDECK_FS_ROOTS` (`:`-separated) | narrows `/v1/fs`; unset, reading follows `--cwd-root` |
|
|
49
|
+
| `--fs-write` | — | off (browse and read only) |
|
|
50
|
+
| `--profile <name=dir>` (repeatable) | — | auto-detected from `~/.claude` |
|
|
51
|
+
| `--state-dir <path>` | `WORKERDECK_STATE_DIR` | beside the config file, else `~/.workerdeck` |
|
|
52
|
+
| `--no-parking-store` | — | durable parking on |
|
|
53
|
+
| `--config <path>` | — | `./workerdeck.config.mjs` |
|
|
54
|
+
| `--insecure`, `--open`, `--help`, `--version` | | |
|
|
55
|
+
|
|
56
|
+
Precedence is narrowest-wins: flags > env > config file > defaults.
|
|
57
|
+
|
|
58
|
+
## The config file
|
|
59
|
+
|
|
60
|
+
`authenticate`, `buildRunnerConfig` and `createEngineRunner` are functions, so they can't come from
|
|
61
|
+
a flag. `workerdeck.config.mjs` default-exports the
|
|
62
|
+
[`createWorkerServer` options](https://workerdeck.github.io/workerdeck/docs/reference/server/)
|
|
63
|
+
(or a function, sync or async, returning them):
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
export default {
|
|
67
|
+
allowedCwdRoots: ['/srv/projects'],
|
|
68
|
+
profiles: [{ name: 'me', configDir: '/home/me/.claude' }],
|
|
69
|
+
requireApiKey: true,
|
|
70
|
+
buildRunnerConfig: (req) => ({ ...req, env: { ...process.env, CI: '1' } }),
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Supplying your own `authenticate` turns the built-in shared-secret auth **off entirely** — one
|
|
75
|
+
hook, one scheme, rather than two paths where only one got audited. If you take it over, you own
|
|
76
|
+
finding a credential the browser can actually present. There's a fuller example in
|
|
77
|
+
[`examples/workerdeck.config.mjs`](https://github.com/workerdeck/workerdeck/blob/master/examples/workerdeck.config.mjs).
|
|
78
|
+
|
|
79
|
+
## Restart guard
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx workerdeck guard --wait 300 --allow-parked && systemctl restart workerdeck
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Exits `0` when a restart is safe, `1` while a session is mid-turn, awaiting an approval, or parked
|
|
86
|
+
without durability behind it, and `2` when it couldn't tell — never treating "couldn't tell" as
|
|
87
|
+
safe. `--allow-parked` and `--allow-queued` are you asserting that the `SessionStore` and the
|
|
88
|
+
`QueueAdapter` respectively are durable; they're separate decisions. Point `--url` at any instance
|
|
89
|
+
and authenticate with `--token` or `--header name=value`.
|
|
90
|
+
|
|
91
|
+
## Credentials
|
|
92
|
+
|
|
93
|
+
WorkerDeck implements **no Anthropic auth**. The official SDK/CLI resolves credentials from the
|
|
94
|
+
operator's environment, per profile — `--auth-key` protects this gateway and nothing else. See the
|
|
95
|
+
project's
|
|
96
|
+
[Auth & Anthropic's terms](https://github.com/workerdeck/workerdeck#auth--anthropics-terms).
|
|
97
|
+
|
|
98
|
+
## Mounting
|
|
99
|
+
|
|
100
|
+
The dashboard's assets resolve from an absolute `/assets/...`, so it must be served at a **domain
|
|
101
|
+
root**, not a subpath. A dedicated vhost reverse-proxying to the instance is the intended shape.
|
|
102
|
+
|
|
103
|
+
The dashboard itself is [`@workerdeck/web`](https://www.npmjs.com/package/@workerdeck/web),
|
|
104
|
+
a normal dependency of this package. Point `webRoot` in the config file at your own build to serve
|
|
105
|
+
a fork instead.
|
|
15
106
|
|
|
16
107
|
MIT
|
package/build/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/build/cli.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-DbIqlX-e.mjs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
const HELP = `workerdeck — run a workerdeck instance: session gateway + dashboard, one port.
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
workerdeck [options]
|
|
12
|
+
workerdeck guard [options] check whether it is safe to restart an instance
|
|
13
|
+
|
|
14
|
+
Options
|
|
15
|
+
-p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)
|
|
16
|
+
--host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)
|
|
17
|
+
--auth-key <secret> shared secret, min 12 chars; browsers log in with it,
|
|
18
|
+
services send it as x-workerdeck-key
|
|
19
|
+
(WORKERDECK_AUTH_KEY). Unset = no auth on loopback;
|
|
20
|
+
on any other interface a key is generated instead,
|
|
21
|
+
printed once, and stored in <state-dir>/auth-key for
|
|
22
|
+
later starts to reuse.
|
|
23
|
+
--trust-proxy trust x-forwarded-proto/-host/-for from one reverse
|
|
24
|
+
proxy. Required behind TLS termination, or the session
|
|
25
|
+
cookie loses its Secure flag and the origin check
|
|
26
|
+
computes http:// where the browser says https://.
|
|
27
|
+
--allowed-origin <o> extra origin accepted on browser requests, for when a
|
|
28
|
+
proxy rewrites Host (repeatable)
|
|
29
|
+
--allowed-host <name> extra Host header accepted when running without auth
|
|
30
|
+
(repeatable; loopback names are always accepted)
|
|
31
|
+
--insecure-host <name>
|
|
32
|
+
bind host that may serve without auth — no key demanded,
|
|
33
|
+
none generated — and, while unauthenticated, also
|
|
34
|
+
accepted as a Host header (repeatable; config:
|
|
35
|
+
insecureHosts). Names the host alone, no port.
|
|
36
|
+
--profile <name=dir> Claude config dir a session may run under (repeatable)
|
|
37
|
+
--cwd-root <path> restrict session cwds to this root (repeatable,
|
|
38
|
+
WORKERDECK_CWD_ROOTS as a ':'-separated list)
|
|
39
|
+
--fs-root <path> narrow which host directories /v1/fs serves
|
|
40
|
+
(repeatable, WORKERDECK_FS_ROOTS as a ':'-separated
|
|
41
|
+
list). Reading otherwise follows --cwd-root: a caller
|
|
42
|
+
who may start a session in a tree can already read it
|
|
43
|
+
through the agent. With neither, the routes 404.
|
|
44
|
+
--fs-write also accept writes over /v1/fs. Its own switch because
|
|
45
|
+
an agent's writes go through the permission flow and a
|
|
46
|
+
PUT does not. Every write is still conditional on the
|
|
47
|
+
hash the client last read.
|
|
48
|
+
--state-dir <path> where parked sessions are persisted
|
|
49
|
+
(default: beside the config file, else ~/.workerdeck)
|
|
50
|
+
--no-parking-store keep parked sessions in memory only; a restart drops them
|
|
51
|
+
-c, --config <path> config file (default: ./workerdeck.config.mjs)
|
|
52
|
+
--insecure allow no-auth on a non-loopback address. Only when
|
|
53
|
+
something in front is doing the authenticating.
|
|
54
|
+
--open open the dashboard in a browser once it is up
|
|
55
|
+
-h, --help show this
|
|
56
|
+
-v, --version print the version
|
|
57
|
+
|
|
58
|
+
Config file
|
|
59
|
+
Options that cannot fit on a command line — \`authenticate\`, \`buildRunnerConfig\`,
|
|
60
|
+
\`createEngineRunner\` are functions — live in workerdeck.config.mjs, which
|
|
61
|
+
default-exports the createWorkerServer options (or a function returning them).
|
|
62
|
+
Flags and env override it. Supplying your own \`authenticate\` turns the built-in
|
|
63
|
+
shared-secret auth off entirely.
|
|
64
|
+
|
|
65
|
+
Credentials
|
|
66
|
+
workerdeck implements no Anthropic auth of its own: the official SDK/CLI
|
|
67
|
+
resolves credentials from the environment, per profile. --auth-key protects this
|
|
68
|
+
gateway, nothing else.
|
|
69
|
+
`;
|
|
70
|
+
async function readVersion() {
|
|
71
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
72
|
+
try {
|
|
73
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
74
|
+
return JSON.parse(raw).version ?? "unknown";
|
|
75
|
+
} catch {
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Best-effort: a browser that won't open is a convenience missed, not a failure. */
|
|
80
|
+
function openInBrowser(url) {
|
|
81
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
82
|
+
try {
|
|
83
|
+
const child = spawn(command, [url], {
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
detached: true,
|
|
86
|
+
shell: process.platform === "win32"
|
|
87
|
+
});
|
|
88
|
+
child.on("error", () => {});
|
|
89
|
+
child.unref();
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
async function main(argv) {
|
|
93
|
+
if (argv[0] === "guard") {
|
|
94
|
+
const { runGuard } = await import("./guard-D3VN855w.mjs").then((n) => n.t);
|
|
95
|
+
return await runGuard(argv.slice(1));
|
|
96
|
+
}
|
|
97
|
+
const flags = parseArgs(argv);
|
|
98
|
+
if (flags.help) {
|
|
99
|
+
process.stdout.write(HELP);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (flags.version) {
|
|
103
|
+
process.stdout.write(`${await readVersion()}\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const config = resolveInstanceConfig(flags, await loadConfigFile(flags.config));
|
|
107
|
+
const instance = await startInstance(config);
|
|
108
|
+
if (config.open) openInBrowser(instance.url);
|
|
109
|
+
const shutdown = (signal) => {
|
|
110
|
+
process.stdout.write(`\n[workerdeck] ${signal} — shutting down\n`);
|
|
111
|
+
instance.close().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
112
|
+
};
|
|
113
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
114
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
115
|
+
await instance.closed;
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
main(process.argv.slice(2)).then((code) => {
|
|
119
|
+
if (code !== 0) process.exit(code);
|
|
120
|
+
}).catch((error) => {
|
|
121
|
+
if (error instanceof ConfigError) {
|
|
122
|
+
process.stderr.write(`[workerdeck] ${error.message}\n`);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
process.stderr.write(`[workerdeck] ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
128
|
+
//#endregion
|
|
129
|
+
export {};
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=cli.mjs.map
|
|
@@ -0,0 +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 './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 --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('./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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+Db,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"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __exportAll = (all, no_symbols) => {
|
|
5
|
+
let target = {};
|
|
6
|
+
for (var name in all) __defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
return target;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/guard.ts
|
|
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
|
+
const BUSY_STATUSES = new Set([
|
|
40
|
+
"starting",
|
|
41
|
+
"running",
|
|
42
|
+
"awaiting_approval"
|
|
43
|
+
]);
|
|
44
|
+
const HELP = `usage: workerdeck guard [--url URL] [--token TOKEN] [--header name=value]
|
|
45
|
+
[--wait SECONDS] [--interval SECONDS]
|
|
46
|
+
[--allow-parked] [--allow-queued] [--json]
|
|
47
|
+
|
|
48
|
+
exit 0 = safe to restart, 1 = busy, 2 = could not tell
|
|
49
|
+
`;
|
|
50
|
+
var GuardError = class extends Error {};
|
|
51
|
+
async function runGuard(argv) {
|
|
52
|
+
let values;
|
|
53
|
+
try {
|
|
54
|
+
values = parseArgs({
|
|
55
|
+
args: argv,
|
|
56
|
+
options: {
|
|
57
|
+
url: {
|
|
58
|
+
type: "string",
|
|
59
|
+
default: process.env.WORKERDECK_URL ?? "http://127.0.0.1:8787/v1"
|
|
60
|
+
},
|
|
61
|
+
token: {
|
|
62
|
+
type: "string",
|
|
63
|
+
default: process.env.WORKERDECK_TOKEN
|
|
64
|
+
},
|
|
65
|
+
header: {
|
|
66
|
+
type: "string",
|
|
67
|
+
multiple: true,
|
|
68
|
+
default: []
|
|
69
|
+
},
|
|
70
|
+
wait: {
|
|
71
|
+
type: "string",
|
|
72
|
+
default: "0"
|
|
73
|
+
},
|
|
74
|
+
interval: {
|
|
75
|
+
type: "string",
|
|
76
|
+
default: "5"
|
|
77
|
+
},
|
|
78
|
+
"allow-parked": {
|
|
79
|
+
type: "boolean",
|
|
80
|
+
default: false
|
|
81
|
+
},
|
|
82
|
+
"allow-queued": {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
default: false
|
|
85
|
+
},
|
|
86
|
+
json: {
|
|
87
|
+
type: "boolean",
|
|
88
|
+
default: false
|
|
89
|
+
},
|
|
90
|
+
help: {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
default: false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}).values;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
process.stderr.write(`guard: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
if (values.help) {
|
|
101
|
+
process.stdout.write(HELP);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const base = values.url.replace(/\/$/, "");
|
|
105
|
+
const headers = { accept: "application/json" };
|
|
106
|
+
if (values.token) headers.authorization = `Bearer ${values.token}`;
|
|
107
|
+
const seconds = (flag, raw) => {
|
|
108
|
+
const value = Number(raw);
|
|
109
|
+
if (!Number.isFinite(value) || value < 0) throw new GuardError(`${flag} must be a non-negative number of seconds`);
|
|
110
|
+
return value;
|
|
111
|
+
};
|
|
112
|
+
let waitMs;
|
|
113
|
+
let intervalMs;
|
|
114
|
+
try {
|
|
115
|
+
for (const entry of values.header) {
|
|
116
|
+
const at = entry.indexOf("=");
|
|
117
|
+
if (at < 1) throw new GuardError(`--header must be name=value, got '${entry}'`);
|
|
118
|
+
headers[entry.slice(0, at).trim()] = entry.slice(at + 1).trim();
|
|
119
|
+
}
|
|
120
|
+
waitMs = seconds("--wait", values.wait) * 1e3;
|
|
121
|
+
intervalMs = Math.max(seconds("--interval", values.interval), 1) * 1e3;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
process.stderr.write(`guard: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
const get = async (path) => {
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetch(base + path, { headers });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error?.cause?.code === "ECONNREFUSED") return {
|
|
132
|
+
ok: false,
|
|
133
|
+
status: "unreachable"
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
detail: String(error instanceof Error ? error.message : error)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!res.ok) return {
|
|
141
|
+
ok: false,
|
|
142
|
+
code: res.status,
|
|
143
|
+
detail: `HTTP ${res.status}`
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
body: await res.json()
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
code: res.status,
|
|
154
|
+
detail: "response was not JSON"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
/** One look at the server. Returns the reasons a restart would cost something. */
|
|
159
|
+
const inspect = async () => {
|
|
160
|
+
const sessions = await get("/sessions");
|
|
161
|
+
if (!sessions.ok && "status" in sessions) return {
|
|
162
|
+
unreachable: true,
|
|
163
|
+
reasons: [],
|
|
164
|
+
notes: []
|
|
165
|
+
};
|
|
166
|
+
if (!sessions.ok) return {
|
|
167
|
+
error: `GET ${base}/sessions → ${sessions.detail}`,
|
|
168
|
+
reasons: [],
|
|
169
|
+
notes: []
|
|
170
|
+
};
|
|
171
|
+
const listed = sessions.body.sessions;
|
|
172
|
+
if (!Array.isArray(listed)) return {
|
|
173
|
+
error: `GET ${base}/sessions returned no session list`,
|
|
174
|
+
reasons: [],
|
|
175
|
+
notes: []
|
|
176
|
+
};
|
|
177
|
+
const all = listed;
|
|
178
|
+
const reasons = [];
|
|
179
|
+
/** Worth saying out loud, but not worth blocking a deploy over. */
|
|
180
|
+
const notes = [];
|
|
181
|
+
for (const session of all.filter((s) => BUSY_STATUSES.has(s.status))) reasons.push(`session ${session.id} is ${session.status}`);
|
|
182
|
+
const parked = all.filter((s) => s.status === "parked");
|
|
183
|
+
if (parked.length > 0 && !values["allow-parked"]) reasons.push(`${parked.length} parked session(s) — pass --allow-parked once the server runs a durable SessionStore, or they are lost on restart`);
|
|
184
|
+
const queue = await get("/queue");
|
|
185
|
+
if (!queue.ok && !("status" in queue) && queue.code !== 404) return {
|
|
186
|
+
error: `GET ${base}/queue → ${queue.detail}`,
|
|
187
|
+
reasons: [],
|
|
188
|
+
notes: []
|
|
189
|
+
};
|
|
190
|
+
const stats = queue.ok ? queue.body.stats : void 0;
|
|
191
|
+
if ((stats?.running ?? 0) > 0) reasons.push(`${stats.running} job(s) running`);
|
|
192
|
+
if ((stats?.queued ?? 0) > 0 && !values["allow-queued"]) reasons.push(`${stats.queued} job(s) queued — pass --allow-queued once the server runs a durable QueueAdapter, or they are lost on restart`);
|
|
193
|
+
if ((stats?.parked ?? 0) > 0 && values["allow-parked"]) notes.push(`${stats.parked} parked job(s): their queue-side records are the QueueAdapter's, not the SessionStore's — with the in-memory adapter they never finish`);
|
|
194
|
+
return {
|
|
195
|
+
reasons,
|
|
196
|
+
notes,
|
|
197
|
+
sessions: all.length,
|
|
198
|
+
parked: parked.length
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
const report = (verdict, detail) => {
|
|
202
|
+
if (values.json) {
|
|
203
|
+
process.stdout.write(`${JSON.stringify({
|
|
204
|
+
verdict,
|
|
205
|
+
...detail
|
|
206
|
+
})}\n`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const lines = [...detail.reasons.map((reason) => ` - ${reason}`), ...detail.notes.map((note) => ` note: ${note}`)].join("\n");
|
|
210
|
+
process.stdout.write(`guard: ${verdict}${lines ? `\n${lines}` : ""}\n`);
|
|
211
|
+
};
|
|
212
|
+
const deadline = Date.now() + waitMs;
|
|
213
|
+
for (;;) {
|
|
214
|
+
const result = await inspect();
|
|
215
|
+
if (result.error) {
|
|
216
|
+
report("unknown", {
|
|
217
|
+
...result,
|
|
218
|
+
reasons: [result.error]
|
|
219
|
+
});
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
if (result.unreachable) {
|
|
223
|
+
report("safe to restart (nothing listening on the given URL)", result);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (result.reasons.length === 0) {
|
|
227
|
+
report("safe to restart", result);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
if (Date.now() + intervalMs > deadline) {
|
|
231
|
+
report("busy — not safe to restart", result);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
report(`busy, waiting up to ${Math.round((deadline - Date.now()) / 1e3)}s`, result);
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
export { runGuard as n, guard_exports as t };
|
|
240
|
+
|
|
241
|
+
//# sourceMappingURL=guard-D3VN855w.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-D3VN855w.mjs","names":[],"sources":["../src/guard.ts"],"sourcesContent":["import { parseArgs } from 'node:util'\n\n/**\n * Restart guard for a deploy: ask a running instance whether anything would be\n * lost by stopping it, and exit non-zero while the answer is yes.\n *\n * workerdeck guard --wait 300 --allow-parked && launchctl kickstart -k …\n *\n * What a restart costs, and why this is policy and not a server route: an\n * in-flight turn dies with the process (the CLI subprocess and the provider\n * request both go), a pending permission request dies with it, and a running job\n * is left claimed. Two more depend on configuration rather than on state, so each\n * has an opt-out flag that says \"I made that durable\":\n *\n * --allow-parked parked sessions survive with a durable SessionStore, not\n * with the in-memory default. Note this covers the *session*:\n * a parked job's queue-side record lives in the QueueAdapter,\n * so with the bundled in-memory adapter the woken session\n * completes with no job attached to finish.\n * --allow-queued queued jobs survive only in a durable QueueAdapter; the\n * bundled one loses them with the process.\n *\n * Exit codes: 0 safe to restart, 1 still busy, 2 could not tell (bad URL, auth,\n * or an unexpected response — never treated as safe).\n */\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 // The token covers the common `Authorization: Bearer` case, including this\n // CLI's own --auth-key; --header covers hosts whose `authenticate` hook reads\n // something else.\n if (values.token) headers.authorization = `Bearer ${values.token}`\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) throw new GuardError(`--header must be name=value, got '${entry}'`)\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: there is no session to lose.\n const code = (error as { cause?: { code?: string } })?.cause?.code\n if (code === 'ECONNREFUSED') return { ok: false, status: 'unreachable' }\n return { ok: false, detail: String(error instanceof Error ? error.message : error) }\n }\n if (!res.ok) return { ok: false, code: res.status, detail: `HTTP ${res.status}` }\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 /** One look at the server. Returns the reasons a restart would cost something. */\n const inspect = async (): Promise<Verdict> => {\n const sessions = await get('/sessions')\n if (!sessions.ok && 'status' in sessions) return { unreachable: true, reasons: [], notes: [] }\n if (!sessions.ok) return { error: `GET ${base}/sessions → ${sessions.detail}`, reasons: [], notes: [] }\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 saying out loud, but not 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: 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\n ? (queue.body.stats as { running?: number; queued?: number; parked?: number } | undefined)\n : undefined\n if ((stats?.running ?? 0) > 0) reasons.push(`${stats!.running} job(s) running`)\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 // Not a blocking reason: the operator has already said parks are durable.\n // But session durability is not job durability, and only one of the two is theirs.\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 = [\n ...detail.reasons.map((reason) => ` - ${reason}`),\n ...detail.notes.map((note) => ` note: ${note}`),\n ].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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"}
|