agent-dag 1.33.0 → 1.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-dag.js +112 -5
- package/bin/deck.js +22 -7
- package/dist/web/assets/{index-Disgmxp1.js → index-D4UiP6Xm.js} +16 -16
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/server/index.mjs +31 -5
- package/src/server/self-update.mjs +93 -9
package/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agents-deck</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D4UiP6Xm.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-hRjhJVfb.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.2",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server/index.mjs
CHANGED
|
@@ -938,7 +938,11 @@ async function serveStatic(req, res, url) {
|
|
|
938
938
|
// SPA fallback to index.html for client-side routes
|
|
939
939
|
try {
|
|
940
940
|
const idx = await readFile(join(WEB_DIST, "index.html"));
|
|
941
|
-
|
|
941
|
+
// Same no-cache as the normal path above, which this used to omit. It
|
|
942
|
+
// matters more since the deck upgrades itself: index.html is the file
|
|
943
|
+
// naming the hashed bundle, so a heuristically-cached copy sends the tab
|
|
944
|
+
// back to the OLD assets after an update and the reload achieves nothing.
|
|
945
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
942
946
|
res.end(idx);
|
|
943
947
|
} catch {
|
|
944
948
|
send(res, 404, { error: "ui not built. run `pnpm build` or `npm run build`." });
|
|
@@ -1041,17 +1045,39 @@ async function handleUpgrade(_req, res) {
|
|
|
1041
1045
|
|
|
1042
1046
|
// Restart is a two-party act: this half answers before it stops listening, so
|
|
1043
1047
|
// the caller learns it was accepted rather than losing the socket mid-reply.
|
|
1044
|
-
|
|
1048
|
+
//
|
|
1049
|
+
// `{ upgrade: true }` asks for the npx variant: come back through
|
|
1050
|
+
// `npx -y <spec>@latest` instead of re-running the files already here. Granted
|
|
1051
|
+
// only where that is genuinely how this copy updates — the mode is decided from
|
|
1052
|
+
// the install on disk, never from the request.
|
|
1053
|
+
async function handleRestart(req, res) {
|
|
1045
1054
|
if (!_onRestart) return send(res, 501, { ok: false, reason: "unsupervised" });
|
|
1046
1055
|
// Enforced here and not only in the UI. Under --no-persist a restart destroys
|
|
1047
1056
|
// the whole canvas — replayLog has no file to read — and a destructive act
|
|
1048
1057
|
// must not be prevented by a hidden button alone.
|
|
1049
1058
|
if (!_canRestart) return send(res, 409, { ok: false, reason: "no_persist" });
|
|
1059
|
+
|
|
1060
|
+
let wantUpgrade = false;
|
|
1061
|
+
if (req.method === "POST") {
|
|
1062
|
+
const body = await readBody(req).catch(() => null);
|
|
1063
|
+
try { wantUpgrade = JSON.parse(body ?? "")?.upgrade === true; } catch { /* a plain restart */ }
|
|
1064
|
+
}
|
|
1065
|
+
let mode = null;
|
|
1066
|
+
if (wantUpgrade) {
|
|
1067
|
+
const { upgradeBlock, upgradeMode, npxRestartSpec } = await import(
|
|
1068
|
+
pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href
|
|
1069
|
+
);
|
|
1070
|
+
if (upgradeMode(upgradeBlock(PKG_ROOT)) !== "npx" || !npxRestartSpec(PKG_ROOT)) {
|
|
1071
|
+
return send(res, 409, { ok: false, reason: "not_npx" });
|
|
1072
|
+
}
|
|
1073
|
+
mode = "npx";
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1050
1076
|
if (_restarting) return send(res, 202, { ok: true, already: true });
|
|
1051
1077
|
_restarting = true;
|
|
1052
|
-
send(res, 200, { ok: true });
|
|
1078
|
+
send(res, 200, { ok: true, mode });
|
|
1053
1079
|
// Let the response flush before the listener goes away.
|
|
1054
|
-
setTimeout(() => { try { _onRestart(); } catch { _restarting = false; } }, 120).unref();
|
|
1080
|
+
setTimeout(() => { try { _onRestart(mode); } catch { _restarting = false; } }, 120).unref();
|
|
1055
1081
|
}
|
|
1056
1082
|
|
|
1057
1083
|
async function handleQuota(req, res) {
|
|
@@ -1311,7 +1337,7 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
1311
1337
|
if (req.method === "GET" && url.pathname === "/events") return handleSse(req, res);
|
|
1312
1338
|
if (req.method === "GET" && url.pathname === "/api/version") return guard(handleVersion(req, res), res);
|
|
1313
1339
|
if (req.method === "POST" && url.pathname === "/api/upgrade") return guard(handleUpgrade(req, res), res);
|
|
1314
|
-
if (req.method === "POST" && url.pathname === "/api/restart") return handleRestart(req, res);
|
|
1340
|
+
if (req.method === "POST" && url.pathname === "/api/restart") return guard(handleRestart(req, res), res);
|
|
1315
1341
|
if (req.method === "GET" && url.pathname === "/api/quota") return guard(handleQuota(req, res), res);
|
|
1316
1342
|
if (req.method === "GET" && url.pathname === "/api/codex-usage") return guard(handleCodexUsage(req, res), res);
|
|
1317
1343
|
if (req.method === "GET" && url.pathname === "/api/codex-quota") return guard(handleCodexQuota(req, res), res);
|
|
@@ -83,9 +83,66 @@ export function isGitCheckout(pkgRoot) {
|
|
|
83
83
|
try { return existsSync(join(pkgRoot, ".git")); } catch { return false; }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
// ── npx ──────────────────────────────────────────────────────────────────────
|
|
87
|
+
//
|
|
88
|
+
// An npx run lives in ~/.npm/_npx/<hash>/node_modules/<pkg>. The hash is over
|
|
89
|
+
// the SPEC the user typed, so upgrading means fetching a different directory —
|
|
90
|
+
// there is nothing to install over. What there IS, is the spec itself: npm
|
|
91
|
+
// writes it into <hash>/package.json as `_npx.packages`, which is the only
|
|
92
|
+
// record of whether the user typed `ccdeck`, `agent-dag` or `agents-deck`.
|
|
93
|
+
// Re-running the wrong one would work but would leave them on a package they
|
|
94
|
+
// never asked for, so it is worth reading rather than guessing.
|
|
95
|
+
|
|
96
|
+
/** The `_npx/<hash>` directory this package was unpacked into, or null. Pure —
|
|
97
|
+
* path arithmetic only, so both platforms' separators can be tested. */
|
|
98
|
+
export function npxRoot(pkgRoot) {
|
|
99
|
+
if (typeof pkgRoot !== "string") return null;
|
|
100
|
+
const parts = pkgRoot.split(/[\\/]/);
|
|
101
|
+
const i = parts.lastIndexOf("_npx");
|
|
102
|
+
if (i === -1 || i + 1 >= parts.length) return null;
|
|
103
|
+
// Keep the separator the input used: a Windows path must come back as one.
|
|
104
|
+
const sep = pkgRoot.includes("\\") && !pkgRoot.includes("/") ? "\\" : "/";
|
|
105
|
+
return parts.slice(0, i + 2).join(sep);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Package name out of an npm spec, scope intact: `ccdeck@1.2.3` → `ccdeck`,
|
|
109
|
+
* `@scope/pkg` → `@scope/pkg`. Null for anything that is not a plain name —
|
|
110
|
+
* a tarball URL or a git spec is not something to re-run with `@latest`. */
|
|
111
|
+
export function bareSpecName(spec) {
|
|
112
|
+
if (typeof spec !== "string") return null;
|
|
113
|
+
const s = spec.trim();
|
|
114
|
+
if (!s) return null;
|
|
115
|
+
const at = s.lastIndexOf("@");
|
|
116
|
+
const name = at > 0 ? s.slice(0, at) : s;
|
|
117
|
+
return /^@?[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)?$/i.test(name) ? name : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What to hand `npx -y`, read from the cache directory's own metadata. */
|
|
121
|
+
export function npxSpecFromMeta(meta, fallback = "agents-deck") {
|
|
122
|
+
const list = meta && meta._npx && Array.isArray(meta._npx.packages) ? meta._npx.packages : [];
|
|
123
|
+
for (const entry of list) {
|
|
124
|
+
const name = bareSpecName(entry);
|
|
125
|
+
if (name) return `${name}@latest`;
|
|
126
|
+
}
|
|
127
|
+
return `${fallback}@latest`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The same, answered against the filesystem. Null when this is not an npx run. */
|
|
131
|
+
export function npxRestartSpec(pkgRoot, name = "agents-deck") {
|
|
132
|
+
const root = npxRoot(pkgRoot);
|
|
133
|
+
if (!root) return null;
|
|
134
|
+
let meta = null;
|
|
135
|
+
try { meta = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); } catch { /* fall back to the name */ }
|
|
136
|
+
return npxSpecFromMeta(meta, name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The exact line the user can paste, for the way THIS copy was installed. */
|
|
87
140
|
export function upgradeCommand(pkgRoot, name = "agents-deck") {
|
|
88
|
-
|
|
141
|
+
// A checkout is updated by pulling, and the bundle is built, not shipped —
|
|
142
|
+
// so `npm run build` is part of the answer rather than an afterthought.
|
|
143
|
+
if (isGitCheckout(pkgRoot)) return "git pull && npm run build";
|
|
144
|
+
if (isNpxInstall(pkgRoot)) return `npx -y ${npxRestartSpec(pkgRoot, name) ?? `${name}@latest`}`;
|
|
145
|
+
return `npm i -g ${name}@latest`;
|
|
89
146
|
}
|
|
90
147
|
|
|
91
148
|
// ── what npm has ─────────────────────────────────────────────────────────────
|
|
@@ -188,6 +245,23 @@ export function upgradeBlockedReason({ git, npx, writable, optedOut }) {
|
|
|
188
245
|
return null;
|
|
189
246
|
}
|
|
190
247
|
|
|
248
|
+
/**
|
|
249
|
+
* How this copy can update itself, if it can at all.
|
|
250
|
+
*
|
|
251
|
+
* "install" — `npm i -g` here and restart into the new files.
|
|
252
|
+
* "npx" — nothing to install: the supervisor re-runs `npx -y <spec>`,
|
|
253
|
+
* which fetches a NEW cache directory and hands the port to it.
|
|
254
|
+
* null — a checkout, an unwritable prefix, or an explicit opt-out; the
|
|
255
|
+
* user gets the command and does it themselves.
|
|
256
|
+
*
|
|
257
|
+
* Pure, so the policy is one readable expression rather than three conditions
|
|
258
|
+
* spread across the server and the UI.
|
|
259
|
+
*/
|
|
260
|
+
export function upgradeMode(blockedReason) {
|
|
261
|
+
if (blockedReason === null || blockedReason === undefined) return "install";
|
|
262
|
+
return blockedReason === "npx" ? "npx" : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
191
265
|
function dirWritable(p) {
|
|
192
266
|
try { accessSync(p, FS.W_OK); return true; } catch { return false; }
|
|
193
267
|
}
|
|
@@ -278,13 +352,21 @@ export function lastMeaningfulLine(text) {
|
|
|
278
352
|
* registry is unreachable. */
|
|
279
353
|
export async function versionReport({ running, pkgRoot, name = "agents-deck", now = Date.now() }) {
|
|
280
354
|
const installed = installedVersion(pkgRoot);
|
|
281
|
-
//
|
|
282
|
-
//
|
|
355
|
+
// Only an explicit opt-out silences the registry.
|
|
356
|
+
//
|
|
357
|
+
// A checkout used to be excluded here too, on the reasoning that its version
|
|
358
|
+
// leads npm's. That reasoning holds for the COMMAND — telling someone to
|
|
359
|
+
// `npm i -g` over their working copy is wrong — but not for the question.
|
|
360
|
+
// Knowing a release shipped is useful however you would install it, and
|
|
361
|
+
// suppressing the lookup meant `latest` was always null, so the upgrade
|
|
362
|
+
// notice could never appear and the "this is a checkout" explanation had
|
|
363
|
+
// nowhere to render. A checkout that is ahead of npm still says nothing:
|
|
364
|
+
// isOlder decides that, not this.
|
|
283
365
|
const skipRegistry =
|
|
284
366
|
process.env.AGENTS_DECK_NO_UPDATE_CHECK === "1" ||
|
|
285
|
-
process.env.AGENTS_DECK_NO_INSTALL === "1"
|
|
286
|
-
isGitCheckout(pkgRoot);
|
|
367
|
+
process.env.AGENTS_DECK_NO_INSTALL === "1";
|
|
287
368
|
const latest = skipRegistry ? null : await latestOnNpm(name, now);
|
|
369
|
+
const blocked = upgradeBlock(pkgRoot);
|
|
288
370
|
return {
|
|
289
371
|
name,
|
|
290
372
|
running: running ?? null,
|
|
@@ -292,9 +374,11 @@ export async function versionReport({ running, pkgRoot, name = "agents-deck", no
|
|
|
292
374
|
latest,
|
|
293
375
|
notice: pickNotice({ running, installed, latest }),
|
|
294
376
|
command: upgradeCommand(pkgRoot, name),
|
|
295
|
-
// Why
|
|
296
|
-
// instead of leaving a gap the user has to guess about.
|
|
297
|
-
|
|
377
|
+
// Why an in-app `npm i -g` is refused, when it is — so the UI can say so
|
|
378
|
+
// instead of leaving a gap the user has to guess about. "npx" is a refusal
|
|
379
|
+
// of the install, not of the update: upgradeMode says so.
|
|
380
|
+
upgradeBlocked: blocked,
|
|
381
|
+
upgradeMode: upgradeMode(blocked),
|
|
298
382
|
upgrade: upgradeStatus(),
|
|
299
383
|
};
|
|
300
384
|
}
|