@cychien/ezdesign 0.9.2 → 0.11.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/README.md +209 -550
- package/dist/cli.mjs +2640 -655
- package/dist/constants.d.ts +8 -4
- package/dist/extension/g/0.11.0-75c3fbebc3/dialog.js +1 -0
- package/dist/extension/g/0.11.0-75c3fbebc3/media.js +1 -0
- package/dist/extension/g/0.11.0-75c3fbebc3/page.js +44 -0
- package/dist/extension/g/0.11.0-75c3fbebc3/worker.js +57 -0
- package/dist/extension/manifest.json +15 -12
- package/dist/home.css +4706 -0
- package/dist/home.js +440 -0
- package/dist/native-host.mjs +33804 -115
- package/dist/overlay.css +5 -1
- package/dist/overlay.js +73 -22
- package/dist/shell.css +297 -1
- package/dist/shell.js +1230 -358
- package/dist/video-digest.js +16388 -0
- package/dist/vite.mjs +5 -1
- package/package.json +4 -3
- package/skills/ezdesign/SKILL.md +7 -3
- package/skills/ezdesign/reference/start.md +2 -1
- package/dist/extension/g/0.9.2-d9aee83dae/dialog.js +0 -1
- package/dist/extension/g/0.9.2-d9aee83dae/media.js +0 -1
- package/dist/extension/g/0.9.2-d9aee83dae/page.js +0 -39
- package/dist/extension/g/0.9.2-d9aee83dae/worker.js +0 -57
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/dialog.css +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/dialog.html +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/icon-128.png +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/icon-16.png +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/icon-32.png +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/icon-48.png +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/media.html +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/setup.css +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/setup.html +0 -0
- /package/dist/extension/g/{0.9.2-d9aee83dae → 0.11.0-75c3fbebc3}/setup.js +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
6
|
+
import { dirname as dirname11, join as join27, resolve as resolve6 } from "node:path";
|
|
6
7
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
8
|
import open2, { apps } from "open";
|
|
8
9
|
|
|
@@ -30,6 +31,10 @@ var NO_UPDATE_CHECK_ENV = `${ENV_PREFIX}_NO_UPDATE_CHECK`;
|
|
|
30
31
|
var DESIGN_HARNESS_ENV = `${ENV_PREFIX}_DESIGN_HARNESS`;
|
|
31
32
|
var API_URL_ENV = `${ENV_PREFIX}_API_URL`;
|
|
32
33
|
var NO_IDLE_STOP_ENV = `${ENV_PREFIX}_NO_IDLE_STOP`;
|
|
34
|
+
var PROJECTS_FILE = join(DATA_DIR, "projects.json");
|
|
35
|
+
var HOME_PATH = `${URL_PREFIX}/home`;
|
|
36
|
+
var LOGIN_ENV_VAR = `${ENV_PREFIX}_LOGIN_ENV`;
|
|
37
|
+
var LOGIN_ENV_CACHE = join(DATA_DIR, "login-env.json");
|
|
33
38
|
function envFlagOn(raw) {
|
|
34
39
|
return raw !== void 0 && raw !== "" && raw !== "0" && raw.toLowerCase() !== "false";
|
|
35
40
|
}
|
|
@@ -138,7 +143,6 @@ function controlPortRange() {
|
|
|
138
143
|
function assertControlPortEnv() {
|
|
139
144
|
controlPortRange();
|
|
140
145
|
}
|
|
141
|
-
var MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
142
146
|
var ATTACHMENT_GRACE_MS = 24 * 60 * 6e4;
|
|
143
147
|
var UPDATE_CHECK_TTL_MS = 4 * 60 * 6e4;
|
|
144
148
|
var POLL_TIMEOUT_MS = 5e4;
|
|
@@ -147,6 +151,10 @@ var IDLE_STOP_MS = 30 * 6e4;
|
|
|
147
151
|
var IDLE_CHECK_MS = 6e4;
|
|
148
152
|
var SHELL_RETRY_MS = 1e3;
|
|
149
153
|
var SHELL_RETURN_MS = 2e3;
|
|
154
|
+
var DEV_SERVER_READY_MS = 6e4;
|
|
155
|
+
var DEV_SERVER_PORT_SCAN_AFTER_MS = 3e3;
|
|
156
|
+
var DEV_SERVER_STOP_GRACE_MS = 3e3;
|
|
157
|
+
var DEV_SERVER_LOG_LINES = 2e3;
|
|
150
158
|
|
|
151
159
|
// src/shell-quote.ts
|
|
152
160
|
function shellQuote(arg) {
|
|
@@ -233,6 +241,7 @@ var Request = z.discriminatedUnion("t", [
|
|
|
233
241
|
capture: CaptureId,
|
|
234
242
|
kind: z.enum(["image", "video"]),
|
|
235
243
|
url: HttpUrl,
|
|
244
|
+
as: z.enum(["file", "hls"]).optional(),
|
|
236
245
|
referer: z.string().max(16384).nullable(),
|
|
237
246
|
userAgent: z.string().max(1024),
|
|
238
247
|
context: CaptureContext
|
|
@@ -244,7 +253,7 @@ var Request = z.discriminatedUnion("t", [
|
|
|
244
253
|
t: z.literal("upload"),
|
|
245
254
|
id: RequestId,
|
|
246
255
|
capture: CaptureId,
|
|
247
|
-
kind: z.enum(["image", "screenshot", "frame"]),
|
|
256
|
+
kind: z.enum(["image", "screenshot", "frame", "video"]),
|
|
248
257
|
size: z.number().int().nonnegative(),
|
|
249
258
|
context: CaptureContext
|
|
250
259
|
}),
|
|
@@ -290,6 +299,13 @@ var Request = z.discriminatedUnion("t", [
|
|
|
290
299
|
exclude: z.array(z.string().max(256)).max(200),
|
|
291
300
|
limit: z.number().int().min(1).max(50)
|
|
292
301
|
}),
|
|
302
|
+
z.object({
|
|
303
|
+
t: z.literal("probe"),
|
|
304
|
+
id: RequestId,
|
|
305
|
+
sources: z.array(z.object({ url: HttpUrl, as: z.enum(["file", "hls"]) })).min(1).max(20),
|
|
306
|
+
referer: z.string().max(16384).nullable(),
|
|
307
|
+
userAgent: z.string().max(1024)
|
|
308
|
+
}),
|
|
293
309
|
/** Aborts the request `ref` (a `download` or `read`), which then fails `cancelled`. */
|
|
294
310
|
z.object({ t: z.literal("cancel"), ref: RequestId })
|
|
295
311
|
]);
|
|
@@ -324,7 +340,11 @@ var ReplyValue = {
|
|
|
324
340
|
read: z.object({ bytes: z.number().int().nonnegative() }),
|
|
325
341
|
commit: z.object({ item: WireItem, merged: z.boolean() }),
|
|
326
342
|
discard: Done,
|
|
327
|
-
suggest: z.object({ tags: z.array(z.object({ name: z.string(), uses: z.number().int() })).max(50) })
|
|
343
|
+
suggest: z.object({ tags: z.array(z.object({ name: z.string(), uses: z.number().int() })).max(50) }),
|
|
344
|
+
probe: z.object({
|
|
345
|
+
durations: z.array(z.number().nonnegative().nullable()),
|
|
346
|
+
masters: z.array(z.boolean())
|
|
347
|
+
})
|
|
328
348
|
};
|
|
329
349
|
var WireError = z.discriminatedUnion("code", [
|
|
330
350
|
/** The ranges do not meet. `host` lets the extension say which side is old. */
|
|
@@ -369,6 +389,24 @@ function writeAtomic(path, data, mode) {
|
|
|
369
389
|
renameSync(temp, path);
|
|
370
390
|
}
|
|
371
391
|
|
|
392
|
+
// src/self-node.ts
|
|
393
|
+
var RUN_AS_NODE = "ELECTRON_RUN_AS_NODE";
|
|
394
|
+
var onElectron = Boolean(process.versions.electron) || process.env[RUN_AS_NODE] === "1";
|
|
395
|
+
var SELF_NODE = {
|
|
396
|
+
execPath: process.execPath,
|
|
397
|
+
env: onElectron ? { [RUN_AS_NODE]: "1" } : {}
|
|
398
|
+
};
|
|
399
|
+
function scrubRunAsNode(env = process.env) {
|
|
400
|
+
delete env[RUN_AS_NODE];
|
|
401
|
+
}
|
|
402
|
+
function selfNodeCommandLine(args, runtime = SELF_NODE) {
|
|
403
|
+
const env = Object.entries(runtime.env).map(([k, v]) => `${k}=${shellQuote(v)}`);
|
|
404
|
+
return [...env, ...[runtime.execPath, ...args].map(shellQuote)].join(" ");
|
|
405
|
+
}
|
|
406
|
+
function selfNodeEnv(base = process.env, runtime = SELF_NODE) {
|
|
407
|
+
return { ...base, ...runtime.env };
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
// src/update-check.ts
|
|
373
411
|
import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
374
412
|
import { dirname } from "node:path";
|
|
@@ -538,9 +576,10 @@ function chooseHostNode(current, recorded) {
|
|
|
538
576
|
}
|
|
539
577
|
function hostWrapperScript(options) {
|
|
540
578
|
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
579
|
+
const env = Object.entries(options.nodeEnv ?? {}).map(([k, v]) => `${k}=${q(v)} `).join("");
|
|
541
580
|
return [
|
|
542
581
|
"#!/bin/sh",
|
|
543
|
-
|
|
582
|
+
`${env}EZDESIGN_DATA_DIR=${q(options.dataDir)} exec ${q(options.node)} ${q(options.host)} "$@"`,
|
|
544
583
|
""
|
|
545
584
|
].join("\n");
|
|
546
585
|
}
|
|
@@ -567,8 +606,8 @@ function resolvePlacement(options) {
|
|
|
567
606
|
}
|
|
568
607
|
function probeNode(path) {
|
|
569
608
|
if (path === process.execPath) return { path, version: process.version };
|
|
570
|
-
const
|
|
571
|
-
const version =
|
|
609
|
+
const run4 = spawnSync(path, ["-p", "process.version"], { encoding: "utf8", timeout: 1e4, env: { [RUN_AS_NODE]: "1" } });
|
|
610
|
+
const version = run4.status === 0 ? run4.stdout.trim() : "";
|
|
572
611
|
return { path, version: /^v\d+\.\d+\.\d+/.test(version) ? version : null };
|
|
573
612
|
}
|
|
574
613
|
async function installCapture(options) {
|
|
@@ -593,7 +632,8 @@ async function installCapture(options) {
|
|
|
593
632
|
mkdirSync3(at.hostDir, { recursive: true });
|
|
594
633
|
const host = join4(at.hostDir, HOST_BUNDLE);
|
|
595
634
|
writeAtomic(host, readFileSync3(bundle));
|
|
596
|
-
|
|
635
|
+
const nodeEnv = choice.node.path === SELF_NODE.execPath ? SELF_NODE.env : {};
|
|
636
|
+
writeAtomic(at.wrapper, hostWrapperScript({ node: choice.node.path, host, dataDir: at.dataDir, nodeEnv }), 493);
|
|
597
637
|
const extensionGeneration = installExtension(join4(options.distDir, "extension"), at.extensionDir);
|
|
598
638
|
const manifest = `${JSON.stringify(hostManifest(at.wrapper, ALLOWED_ORIGINS), null, 2)}
|
|
599
639
|
`;
|
|
@@ -783,13 +823,13 @@ function forget(key, file) {
|
|
|
783
823
|
}
|
|
784
824
|
|
|
785
825
|
// src/daemon.ts
|
|
786
|
-
import { createHash as createHash8, randomBytes as
|
|
826
|
+
import { createHash as createHash8, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
787
827
|
import { EventEmitter } from "node:events";
|
|
788
|
-
import {
|
|
828
|
+
import { mkdirSync as mkdirSync17, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
|
|
789
829
|
import { createServer } from "node:http";
|
|
790
|
-
import {
|
|
830
|
+
import { dirname as dirname9, join as join25 } from "node:path";
|
|
791
831
|
import { fileURLToPath } from "node:url";
|
|
792
|
-
import
|
|
832
|
+
import express4, { Router as Router8 } from "express";
|
|
793
833
|
|
|
794
834
|
// src/app-proxy.ts
|
|
795
835
|
import {
|
|
@@ -818,6 +858,23 @@ var MAX_FRAME_TITLE = 80;
|
|
|
818
858
|
var DEFAULT_FRAME_WIDTH = 1280;
|
|
819
859
|
var MIN_FRAME_SIZE = { width: 240, height: 240 };
|
|
820
860
|
var MAX_FRAME_SIZE = { width: 3840, height: 12e3 };
|
|
861
|
+
var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
862
|
+
var IMAGE_TYPES = {
|
|
863
|
+
"image/png": "png",
|
|
864
|
+
"image/jpeg": "jpg",
|
|
865
|
+
"image/gif": "gif",
|
|
866
|
+
"image/webp": "webp",
|
|
867
|
+
"image/avif": "avif",
|
|
868
|
+
"image/svg+xml": "svg"
|
|
869
|
+
};
|
|
870
|
+
var ASSET_SHAPE = new RegExp(`^[0-9a-f]{12}\\.(${Object.values(IMAGE_TYPES).join("|")})$`);
|
|
871
|
+
function asAssetName(raw) {
|
|
872
|
+
return typeof raw === "string" && ASSET_SHAPE.test(raw) ? raw : null;
|
|
873
|
+
}
|
|
874
|
+
function mimeOfAsset(name) {
|
|
875
|
+
const ext = name.slice(name.indexOf(".") + 1);
|
|
876
|
+
return Object.keys(IMAGE_TYPES).find((mime) => IMAGE_TYPES[mime] === ext);
|
|
877
|
+
}
|
|
821
878
|
|
|
822
879
|
// src/canvas/frame-document.ts
|
|
823
880
|
var FRAME_HOST = "localhost";
|
|
@@ -825,6 +882,24 @@ var FRAME_ROUTE = "/frames";
|
|
|
825
882
|
function frameUrl(frameOrigin, canvasId2, frameId2, hash) {
|
|
826
883
|
return `${frameOrigin}${URL_PREFIX}${FRAME_ROUTE}/${canvasId2}/${frameId2}?rev=${hash}`;
|
|
827
884
|
}
|
|
885
|
+
var ASSETS_DIR = "frames/assets";
|
|
886
|
+
function imageFrameHtml(title, asset2) {
|
|
887
|
+
const text = title.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
888
|
+
return [
|
|
889
|
+
"<!doctype html>",
|
|
890
|
+
"<html>",
|
|
891
|
+
"<head>",
|
|
892
|
+
'<meta charset="utf-8">',
|
|
893
|
+
`<title>${text}</title>`,
|
|
894
|
+
"<style>html,body{margin:0;height:100%}img{display:block;width:100%;height:100%;object-fit:contain}</style>",
|
|
895
|
+
"</head>",
|
|
896
|
+
"<body>",
|
|
897
|
+
`<img src="assets/${asset2}" alt="${text}">`,
|
|
898
|
+
"</body>",
|
|
899
|
+
"</html>",
|
|
900
|
+
""
|
|
901
|
+
].join("\n");
|
|
902
|
+
}
|
|
828
903
|
function isFrameHost(host) {
|
|
829
904
|
if (!host) return false;
|
|
830
905
|
return host.replace(/:\d+$/, "").toLowerCase() === FRAME_HOST;
|
|
@@ -946,16 +1021,47 @@ function wantsHtml(accept) {
|
|
|
946
1021
|
// src/app-proxy.ts
|
|
947
1022
|
var NO_APP = `<!doctype html><meta charset="utf-8"><title>ezdesign</title>
|
|
948
1023
|
<p>No dev server is attached to this review. Run <code>ezdesign <url></code> in the project to attach one.</p>`;
|
|
1024
|
+
var UNREACHABLE_HEADER = "x-ezdesign-unreachable";
|
|
1025
|
+
var unreachable = (origin) => `<!doctype html><meta charset="utf-8"><title>\u9023\u4E0D\u4E0A App</title>
|
|
1026
|
+
<style>
|
|
1027
|
+
/* The shell's dev-server prompt, in its tokens' values: this page is served without the shell's CSS. */
|
|
1028
|
+
body { margin: 0; background: #faf9f6; -webkit-font-smoothing: antialiased;
|
|
1029
|
+
color: #171717; font: 13.5px/1.55 -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans TC', sans-serif; }
|
|
1030
|
+
main { display: grid; width: min(520px, calc(100% - 48px)); margin: max(48px, 12vh) auto 48px; }
|
|
1031
|
+
h1 { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.4; }
|
|
1032
|
+
p { margin: 6px 0 18px; color: #676767; }
|
|
1033
|
+
code { font: 12.5px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
1034
|
+
button { justify-self: start; height: 32px; padding: 0 14px; border: 1px solid #171717; border-radius: 7px;
|
|
1035
|
+
background: #171717; color: #fff; box-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.14);
|
|
1036
|
+
font: 600 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans TC', sans-serif; cursor: pointer; }
|
|
1037
|
+
button:hover { opacity: 0.88; }
|
|
1038
|
+
</style>
|
|
1039
|
+
<main>
|
|
1040
|
+
<h1>\u9023\u4E0D\u4E0A App</h1>
|
|
1041
|
+
<p><code>${origin.replace(/^https?:\/\//, "")}</code> \u6C92\u6709\u56DE\u61C9\uFF0Cdev server \u53EF\u80FD\u505C\u4E86\u3002</p>
|
|
1042
|
+
<button type="button" onclick="location.reload()">\u91CD\u65B0\u6574\u7406</button>
|
|
1043
|
+
</main>
|
|
1044
|
+
<script>
|
|
1045
|
+
setInterval(() => fetch(location.href, { method: 'HEAD', cache: 'no-store' })
|
|
1046
|
+
.then((r) => { if (!r.headers.has('${UNREACHABLE_HEADER}')) location.reload() }, () => {}), 1500)
|
|
1047
|
+
</script>`;
|
|
949
1048
|
var AppProxy = class {
|
|
950
1049
|
constructor(shellPath, onRequest) {
|
|
951
1050
|
this.shellPath = shellPath;
|
|
952
1051
|
this.onRequest = onRequest;
|
|
953
1052
|
const router = () => this.current.kind === "attached" ? this.current.origin : void 0;
|
|
1053
|
+
const error = (_err, req, res) => {
|
|
1054
|
+
if (!("writeHead" in res) || res.headersSent) return void res.end();
|
|
1055
|
+
const origin = this.current.kind === "attached" ? this.current.origin : "";
|
|
1056
|
+
res.writeHead(502, { "content-type": wantsHtml(req.headers.accept) ? "text/html; charset=utf-8" : "text/plain", [UNREACHABLE_HEADER]: "1" });
|
|
1057
|
+
res.end(wantsHtml(req.headers.accept) ? unreachable(origin) : "");
|
|
1058
|
+
};
|
|
954
1059
|
this.htmlProxy = createProxyMiddleware({
|
|
955
1060
|
router,
|
|
956
1061
|
changeOrigin: true,
|
|
957
1062
|
selfHandleResponse: true,
|
|
958
1063
|
on: {
|
|
1064
|
+
error,
|
|
959
1065
|
proxyRes: responseInterceptor(async (buffer, proxyRes, _req, res) => {
|
|
960
1066
|
res.removeHeader("content-security-policy");
|
|
961
1067
|
res.removeHeader("x-frame-options");
|
|
@@ -965,7 +1071,7 @@ var AppProxy = class {
|
|
|
965
1071
|
})
|
|
966
1072
|
}
|
|
967
1073
|
});
|
|
968
|
-
this.rawProxy = createProxyMiddleware({ router, changeOrigin: true, ws: true });
|
|
1074
|
+
this.rawProxy = createProxyMiddleware({ router, changeOrigin: true, ws: true, on: { error } });
|
|
969
1075
|
}
|
|
970
1076
|
current = { kind: "none" };
|
|
971
1077
|
upgraded = /* @__PURE__ */ new Set();
|
|
@@ -1101,7 +1207,7 @@ var CANVAS_TOOLS = {
|
|
|
1101
1207
|
"Add one frame to an open canvas: a complete, self-contained HTML document (<!doctype html>, its own <head> and styles) rendered in its own iframe at a real viewport width.",
|
|
1102
1208
|
"Scripts and CDN assets are allowed. No CSS is shared between frames.",
|
|
1103
1209
|
"width is the viewport width the page is designed for (390 phone, 768 tablet, 1280 desktop; default 1280); height defaults to a common screen for that width.",
|
|
1104
|
-
"Leave position out unless the user asked for a particular arrangement: the frame is then placed where it overlaps nothing, the frames of one turn side by side in the order you add them, in a row under the earlier ones. After creation a frame's position is the user's
|
|
1210
|
+
"Leave position out unless the user asked for a particular arrangement: the frame is then placed where it overlaps nothing, the frames of one turn side by side in the order you add them, in a row under the earlier ones. After creation a frame's position is the user's and its width is fixed; its height can follow its content through update_frame.",
|
|
1105
1211
|
"Call once per frame, as soon as it is ready: the user watches frames appear."
|
|
1106
1212
|
].join("\n"),
|
|
1107
1213
|
input: {
|
|
@@ -1128,16 +1234,18 @@ var CANVAS_TOOLS = {
|
|
|
1128
1234
|
}),
|
|
1129
1235
|
update_frame: tool({
|
|
1130
1236
|
description: [
|
|
1131
|
-
"Replace a frame's HTML document and/or
|
|
1237
|
+
"Replace a frame's HTML document, retitle it, and/or change its height.",
|
|
1132
1238
|
"Pass expectedHash - the hash read_canvas, add_frame or your last update_frame returned - so an edit you have not seen is never overwritten: if the file changed since, nothing is written and the error names its current hash.",
|
|
1133
1239
|
"For a small change you may instead edit the frame's file directly; the user's canvas updates either way.",
|
|
1134
|
-
"
|
|
1240
|
+
"Pass height whenever the content needs a taller or shorter frame; frames below it move down to make room. Never remove a frame and add it again to resize it: that loses its place and the annotations made on it.",
|
|
1241
|
+
"A frame's position belongs to the user and its width is the viewport it was designed for; neither can be changed here."
|
|
1135
1242
|
].join("\n"),
|
|
1136
1243
|
input: {
|
|
1137
1244
|
canvasId,
|
|
1138
1245
|
frameId,
|
|
1139
1246
|
html: z2.string().optional().describe("The new complete HTML document."),
|
|
1140
1247
|
title: z2.string().optional().describe("A new label for the frame."),
|
|
1248
|
+
height: px("A new viewport height in CSS px, to fit the content."),
|
|
1141
1249
|
expectedHash: z2.string().optional().describe("The hash of the version you are replacing.")
|
|
1142
1250
|
},
|
|
1143
1251
|
run: (port, args) => map(
|
|
@@ -1146,9 +1254,10 @@ var CANVAS_TOOLS = {
|
|
|
1146
1254
|
frameId: args.frameId,
|
|
1147
1255
|
...args.html !== void 0 ? { html: args.html } : {},
|
|
1148
1256
|
...args.title !== void 0 ? { title: args.title } : {},
|
|
1257
|
+
...args.height !== void 0 ? { height: args.height } : {},
|
|
1149
1258
|
...args.expectedHash !== void 0 ? { expectedHash: args.expectedHash } : {}
|
|
1150
1259
|
}),
|
|
1151
|
-
|
|
1260
|
+
listing
|
|
1152
1261
|
)
|
|
1153
1262
|
}),
|
|
1154
1263
|
remove_frame: tool({
|
|
@@ -1163,8 +1272,8 @@ function isCanvasTool(permissionName) {
|
|
|
1163
1272
|
return permissionName.startsWith(prefix) && CANVAS_TOOL_NAMES.includes(permissionName.slice(prefix.length));
|
|
1164
1273
|
}
|
|
1165
1274
|
function runCanvasTool(port, name, args) {
|
|
1166
|
-
const
|
|
1167
|
-
const outcome =
|
|
1275
|
+
const run4 = CANVAS_TOOLS[name].run;
|
|
1276
|
+
const outcome = run4(port, args);
|
|
1168
1277
|
if (!outcome.ok) return { isError: true, content: [{ type: "text", text: canvasProblemMessage(outcome.problem) }] };
|
|
1169
1278
|
const value = outcome.value;
|
|
1170
1279
|
return { content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value };
|
|
@@ -1220,7 +1329,7 @@ var CanvasMcp = class {
|
|
|
1220
1329
|
server.registerTool(
|
|
1221
1330
|
name,
|
|
1222
1331
|
// Strict, so `x` or `width` on an existing frame is refused rather than
|
|
1223
|
-
// silently ignored: a frame's position is the user's, its
|
|
1332
|
+
// silently ignored: a frame's position is the user's, its width fixed.
|
|
1224
1333
|
{ description: spec.description, inputSchema: z2.object(spec.input).strict() },
|
|
1225
1334
|
(args) => runCanvasTool(this.port, name, args)
|
|
1226
1335
|
);
|
|
@@ -1258,6 +1367,11 @@ function canvasProblemMessage(problem) {
|
|
|
1258
1367
|
return `BAD_NAME: give a non-empty name of at most ${problem.max} characters.`;
|
|
1259
1368
|
case "BAD_RECT":
|
|
1260
1369
|
return `BAD_RECT: ${problem.reason}`;
|
|
1370
|
+
case "IMAGE_UNSUPPORTED":
|
|
1371
|
+
case "IMAGE_TOO_LARGE":
|
|
1372
|
+
case "IMAGE_EMPTY":
|
|
1373
|
+
case "ASSET_NOT_FOUND":
|
|
1374
|
+
return `${problem.code}: only the user pastes images onto a canvas.`;
|
|
1261
1375
|
}
|
|
1262
1376
|
}
|
|
1263
1377
|
|
|
@@ -1290,7 +1404,7 @@ function canvasBrief(input) {
|
|
|
1290
1404
|
`The review shell has these tabs: ${tabs.join(", ") || "none"}.` + (shownArchived.length ? ` Archived: ${shownArchived.map((c) => `"${c.name}" (${c.id})`).join(", ")}.` : "") + (more > 0 ? ` ${more} more: call list_canvases.` : ""),
|
|
1291
1405
|
...where ? [where] : [],
|
|
1292
1406
|
`Items with anchor.frame were made inside a canvas frame; anchor.source is then a line of that frame's file under ${CANVASES_DIR}/.`,
|
|
1293
|
-
"A frame's HTML is yours: change it with update_frame or by editing that file. Its position is the user's
|
|
1407
|
+
"A frame's HTML is yours: change it with update_frame or by editing that file. Its position is the user's and its width is fixed; when the content needs a different height, pass height to update_frame rather than adding the frame again. Never edit canvas.json or layout.json.",
|
|
1294
1408
|
`To show the user designs, mockups or alternatives side by side, put each one in a frame on a canvas. Canvas tools (${CANVAS_SERVER_NAME}): ${CANVAS_TOOL_NAMES.join(", ")}.`,
|
|
1295
1409
|
target ? `Add new frames to canvas "${target.name}" (${target.id}). Call create_canvas only when the user asks for a new canvas or this one is full.` : "No canvas is open: create one with create_canvas for new frames.",
|
|
1296
1410
|
""
|
|
@@ -1298,7 +1412,7 @@ function canvasBrief(input) {
|
|
|
1298
1412
|
}
|
|
1299
1413
|
|
|
1300
1414
|
// src/canvas/routes.ts
|
|
1301
|
-
import { Router } from "express";
|
|
1415
|
+
import express2, { Router } from "express";
|
|
1302
1416
|
function problemText(problem) {
|
|
1303
1417
|
switch (problem.code) {
|
|
1304
1418
|
case "CANVAS_NOT_FOUND":
|
|
@@ -1321,19 +1435,32 @@ function problemText(problem) {
|
|
|
1321
1435
|
return `a name is 1 to ${problem.max} characters`;
|
|
1322
1436
|
case "BAD_RECT":
|
|
1323
1437
|
return `bad frame geometry: ${problem.reason}`;
|
|
1438
|
+
case "IMAGE_UNSUPPORTED":
|
|
1439
|
+
return `${problem.mime || "this file"} is not an image a frame can show`;
|
|
1440
|
+
case "IMAGE_TOO_LARGE":
|
|
1441
|
+
return `the image is ${problem.bytes} bytes; the limit is ${problem.limit}`;
|
|
1442
|
+
case "IMAGE_EMPTY":
|
|
1443
|
+
return "the image is empty";
|
|
1444
|
+
case "ASSET_NOT_FOUND":
|
|
1445
|
+
return `no image ${problem.asset} on canvas ${problem.canvasId}`;
|
|
1324
1446
|
}
|
|
1325
1447
|
}
|
|
1326
1448
|
function statusOf(problem) {
|
|
1327
1449
|
switch (problem.code) {
|
|
1328
1450
|
case "CANVAS_NOT_FOUND":
|
|
1329
1451
|
case "FRAME_NOT_FOUND":
|
|
1452
|
+
case "ASSET_NOT_FOUND":
|
|
1330
1453
|
return 404;
|
|
1331
1454
|
case "CANVAS_ARCHIVED":
|
|
1332
1455
|
case "FRAME_CONFLICT":
|
|
1333
1456
|
return 409;
|
|
1334
1457
|
case "HTML_TOO_LARGE":
|
|
1458
|
+
case "IMAGE_TOO_LARGE":
|
|
1335
1459
|
return 413;
|
|
1460
|
+
case "IMAGE_UNSUPPORTED":
|
|
1461
|
+
return 415;
|
|
1336
1462
|
case "HTML_EMPTY":
|
|
1463
|
+
case "IMAGE_EMPTY":
|
|
1337
1464
|
case "NOT_A_DOCUMENT":
|
|
1338
1465
|
case "TOO_MANY_FRAMES":
|
|
1339
1466
|
case "BAD_NAME":
|
|
@@ -1411,6 +1538,31 @@ function parseDuplicate(raw) {
|
|
|
1411
1538
|
if (!source || !copy || at === null) return null;
|
|
1412
1539
|
return { from: source, id: copy, ...at ? { at } : {} };
|
|
1413
1540
|
}
|
|
1541
|
+
function rect(raw) {
|
|
1542
|
+
const at = point(raw);
|
|
1543
|
+
const { width, height } = raw ?? {};
|
|
1544
|
+
return at && finite(width) && finite(height) ? { ...at, width, height } : null;
|
|
1545
|
+
}
|
|
1546
|
+
function parseImageFrame(raw) {
|
|
1547
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
1548
|
+
const { id: rawId, asset: rawAsset, title, rect: rawRect } = raw;
|
|
1549
|
+
const id = asFrameId(rawId);
|
|
1550
|
+
const asset2 = asAssetName(rawAsset);
|
|
1551
|
+
const at = rect(rawRect);
|
|
1552
|
+
if (!id || !asset2 || !at || typeof title !== "string") return null;
|
|
1553
|
+
return { id, asset: asset2, title, rect: at };
|
|
1554
|
+
}
|
|
1555
|
+
var rawImage = express2.raw({ type: () => true, limit: MAX_IMAGE_BYTES });
|
|
1556
|
+
function receiveImage(req, res, then) {
|
|
1557
|
+
if (Number(req.get("content-length")) > MAX_IMAGE_BYTES) {
|
|
1558
|
+
return void res.status(413).json({ error: "the image is too large", code: "IMAGE_TOO_LARGE" });
|
|
1559
|
+
}
|
|
1560
|
+
rawImage(req, res, (err) => {
|
|
1561
|
+
if (!err) return then(Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
|
|
1562
|
+
const status = err.status === 413 ? 413 : 400;
|
|
1563
|
+
res.status(status).json({ error: status === 413 ? "the image is too large" : "unreadable image body" });
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1414
1566
|
function canvasApiRouter(host) {
|
|
1415
1567
|
const api = Router();
|
|
1416
1568
|
const { user, read: read4 } = host;
|
|
@@ -1440,15 +1592,28 @@ function canvasApiRouter(host) {
|
|
|
1440
1592
|
answer(res, user.delete(req.params.canvasId), () => ({ ok: true }));
|
|
1441
1593
|
});
|
|
1442
1594
|
api.post("/canvases/:canvasId/ops", (req, res) => {
|
|
1443
|
-
const
|
|
1444
|
-
if (!
|
|
1445
|
-
answer(res, user.applyLayoutOps(req.params.canvasId,
|
|
1595
|
+
const step2 = parseLayoutStep(req.body);
|
|
1596
|
+
if (!step2) return res.status(400).json({ error: "not a layout step" });
|
|
1597
|
+
answer(res, user.applyLayoutOps(req.params.canvasId, step2), (wire) => wire);
|
|
1446
1598
|
});
|
|
1447
1599
|
api.post("/canvases/:canvasId/frames", (req, res) => {
|
|
1448
1600
|
const input = parseDuplicate(req.body);
|
|
1449
1601
|
if (!input) return res.status(400).json({ error: "not a duplicate" });
|
|
1450
1602
|
answer(res, user.duplicate(req.params.canvasId, input), (wire) => wire);
|
|
1451
1603
|
});
|
|
1604
|
+
api.post("/canvases/:canvasId/assets", (req, res) => {
|
|
1605
|
+
const mime = (req.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
1606
|
+
receiveImage(
|
|
1607
|
+
req,
|
|
1608
|
+
res,
|
|
1609
|
+
(bytes) => answer(res, user.addAsset(req.params.canvasId, mime, bytes), (asset2) => ({ asset: asset2 }))
|
|
1610
|
+
);
|
|
1611
|
+
});
|
|
1612
|
+
api.post("/canvases/:canvasId/image-frames", (req, res) => {
|
|
1613
|
+
const input = parseImageFrame(req.body);
|
|
1614
|
+
if (!input) return res.status(400).json({ error: "not an image frame" });
|
|
1615
|
+
answer(res, user.addImageFrame(req.params.canvasId, input), (wire) => wire);
|
|
1616
|
+
});
|
|
1452
1617
|
api.put("/view", (req, res) => {
|
|
1453
1618
|
const view = parseView(req.body);
|
|
1454
1619
|
if (!view) return res.status(400).json({ error: "not a view" });
|
|
@@ -1467,6 +1632,20 @@ function frameHost(read4, shellOrigin) {
|
|
|
1467
1632
|
}
|
|
1468
1633
|
const [canvasRaw, frameRaw, extra] = framePath ? req.path.slice(FRAMES_PATH.length).split("/") : [];
|
|
1469
1634
|
const canvasId2 = asCanvasId(canvasRaw);
|
|
1635
|
+
const assetName = frameRaw === "assets" ? asAssetName(extra) : null;
|
|
1636
|
+
const image = req.method === "GET" && canvasId2 && assetName ? read4.asset(canvasId2, assetName) : null;
|
|
1637
|
+
if (image) {
|
|
1638
|
+
res.sendFile(image.path, {
|
|
1639
|
+
// The project's `.ezdesign/` is a dot directory.
|
|
1640
|
+
dotfiles: "allow",
|
|
1641
|
+
headers: {
|
|
1642
|
+
"content-type": image.mime,
|
|
1643
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
1644
|
+
"x-content-type-options": "nosniff"
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1470
1649
|
const frameId2 = asFrameId(frameRaw);
|
|
1471
1650
|
const doc = req.method === "GET" && canvasId2 && frameId2 && extra === void 0 ? read4.frameDocument(canvasId2, frameId2) : null;
|
|
1472
1651
|
if (!doc) {
|
|
@@ -1526,12 +1705,27 @@ function normalizeRect(raw) {
|
|
|
1526
1705
|
height: clamp(Math.round(raw.height), MIN_FRAME_SIZE.height, MAX_FRAME_SIZE.height)
|
|
1527
1706
|
};
|
|
1528
1707
|
}
|
|
1708
|
+
function growDown(rect3, height, others) {
|
|
1709
|
+
const next = normalizeRect({ ...rect3, height });
|
|
1710
|
+
const growth = next.height - rect3.height;
|
|
1711
|
+
if (growth <= 0) return { rect: next, moved: [] };
|
|
1712
|
+
const spanned = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width;
|
|
1713
|
+
const pushers = [rect3];
|
|
1714
|
+
const moved = [];
|
|
1715
|
+
const below = others.filter(([, r]) => r.y >= rect3.y + rect3.height).sort((a, b) => a[1].y - b[1].y);
|
|
1716
|
+
for (const [id, r] of below) {
|
|
1717
|
+
if (!pushers.some((p) => spanned(p, r))) continue;
|
|
1718
|
+
pushers.push(r);
|
|
1719
|
+
moved.push([id, { ...r, y: r.y + growth }]);
|
|
1720
|
+
}
|
|
1721
|
+
return { rect: next, moved };
|
|
1722
|
+
}
|
|
1529
1723
|
function freeAlongRow(from, size, existing, limit) {
|
|
1530
1724
|
let x = from.x;
|
|
1531
1725
|
for (; ; ) {
|
|
1532
1726
|
if (x + size.width > limit) return null;
|
|
1533
|
-
const
|
|
1534
|
-
const hits = existing.filter((r) => overlaps(
|
|
1727
|
+
const rect3 = { x, y: from.y, width: size.width, height: size.height };
|
|
1728
|
+
const hits = existing.filter((r) => overlaps(rect3, r));
|
|
1535
1729
|
if (!hits.length) return { x, y: from.y };
|
|
1536
1730
|
x = Math.max(...hits.map((r) => r.x + r.width + GAP));
|
|
1537
1731
|
}
|
|
@@ -1582,12 +1776,12 @@ var FrameFileWatcher = class {
|
|
|
1582
1776
|
}
|
|
1583
1777
|
/** `<canvasId>/frames/<frameId>.html`, both ids through `asCanvasId` /
|
|
1584
1778
|
* `asFrameId`; anything else - temp files, manifests, dotfiles - ignored. */
|
|
1585
|
-
schedule(
|
|
1586
|
-
if (
|
|
1587
|
-
const
|
|
1588
|
-
if (
|
|
1589
|
-
const canvasId2 = asCanvasId(
|
|
1590
|
-
const file = /^(.*)\.html$/.exec(
|
|
1779
|
+
schedule(relative3) {
|
|
1780
|
+
if (relative3 === null) return;
|
|
1781
|
+
const parts2 = String(relative3).split(/[\\/]/);
|
|
1782
|
+
if (parts2.length !== 3 || parts2[1] !== "frames") return;
|
|
1783
|
+
const canvasId2 = asCanvasId(parts2[0]);
|
|
1784
|
+
const file = /^(.*)\.html$/.exec(parts2[2]);
|
|
1591
1785
|
const frameId2 = file ? asFrameId(file[1]) : null;
|
|
1592
1786
|
if (!canvasId2 || !frameId2) return;
|
|
1593
1787
|
const key = `${canvasId2}/${frameId2}`;
|
|
@@ -1682,8 +1876,10 @@ var Workspace = class {
|
|
|
1682
1876
|
archive: (canvasId2) => this.archive(canvasId2),
|
|
1683
1877
|
reopen: (canvasId2) => this.reopen(canvasId2),
|
|
1684
1878
|
delete: (canvasId2) => this.delete(canvasId2),
|
|
1685
|
-
applyLayoutOps: (canvasId2,
|
|
1879
|
+
applyLayoutOps: (canvasId2, step2) => this.applyLayoutOps(canvasId2, step2),
|
|
1686
1880
|
duplicate: (canvasId2, input) => this.duplicate(canvasId2, input),
|
|
1881
|
+
addAsset: (canvasId2, mime, bytes) => this.addAsset(canvasId2, mime, bytes),
|
|
1882
|
+
addImageFrame: (canvasId2, input) => this.addImageFrame(canvasId2, input),
|
|
1687
1883
|
setView: (view) => this.setView(view)
|
|
1688
1884
|
};
|
|
1689
1885
|
}
|
|
@@ -1703,7 +1899,11 @@ var Workspace = class {
|
|
|
1703
1899
|
const frame = this.liveFrame(ref.canvasId, ref.frameId);
|
|
1704
1900
|
return held && frame ? { canvas: held.manifest.name, frame: frame.title } : null;
|
|
1705
1901
|
},
|
|
1706
|
-
frameDocument: (canvasId2, frameId2) => this.frameDocument(canvasId2, frameId2)
|
|
1902
|
+
frameDocument: (canvasId2, frameId2) => this.frameDocument(canvasId2, frameId2),
|
|
1903
|
+
asset: (canvasId2, name) => {
|
|
1904
|
+
const path = join6(this.dir(canvasId2), ASSETS_DIR, name);
|
|
1905
|
+
return this.canvases.has(canvasId2) && existsSync5(path) ? { path, mime: mimeOfAsset(name) } : null;
|
|
1906
|
+
}
|
|
1707
1907
|
};
|
|
1708
1908
|
}
|
|
1709
1909
|
// ---------------------------------------------------------------- lifecycle
|
|
@@ -1899,14 +2099,30 @@ var Workspace = class {
|
|
|
1899
2099
|
return;
|
|
1900
2100
|
}
|
|
1901
2101
|
const old = this.now() - UNNAMED_FILE_GRACE_MS;
|
|
1902
|
-
|
|
1903
|
-
if (named.has(`frames/${name}`)) continue;
|
|
1904
|
-
const path = join6(framesDir, name);
|
|
2102
|
+
const unlinkOld = (path) => {
|
|
1905
2103
|
try {
|
|
1906
2104
|
if (statSync(path).mtimeMs < old) unlinkSync(path);
|
|
1907
2105
|
} catch {
|
|
1908
2106
|
}
|
|
2107
|
+
};
|
|
2108
|
+
for (const name of files) {
|
|
2109
|
+
if (named.has(`frames/${name}`) || `frames/${name}` === ASSETS_DIR) continue;
|
|
2110
|
+
unlinkOld(join6(framesDir, name));
|
|
2111
|
+
}
|
|
2112
|
+
const shown = /* @__PURE__ */ new Set();
|
|
2113
|
+
for (const frame of held.manifest.frames) {
|
|
2114
|
+
try {
|
|
2115
|
+
const html = readFileSync5(join6(this.dir(held.manifest.id), frame.file), "utf8");
|
|
2116
|
+
for (const m of html.matchAll(/assets\/([0-9a-f]{12}\.\w+)/g)) shown.add(m[1]);
|
|
2117
|
+
} catch {
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
let assets = [];
|
|
2121
|
+
try {
|
|
2122
|
+
assets = readdirSync2(join6(this.dir(held.manifest.id), ASSETS_DIR));
|
|
2123
|
+
} catch {
|
|
1909
2124
|
}
|
|
2125
|
+
for (const name of assets) if (!shown.has(name)) unlinkOld(join6(this.dir(held.manifest.id), ASSETS_DIR, name));
|
|
1910
2126
|
}
|
|
1911
2127
|
frameDocument(canvasId2, frameId2) {
|
|
1912
2128
|
const held = this.canvases.get(canvasId2);
|
|
@@ -2064,16 +2280,16 @@ var Workspace = class {
|
|
|
2064
2280
|
return ok(void 0);
|
|
2065
2281
|
}
|
|
2066
2282
|
// ----------------------------------------------------------- board edits
|
|
2067
|
-
applyLayoutOps(canvasId2,
|
|
2283
|
+
applyLayoutOps(canvasId2, step2) {
|
|
2068
2284
|
const writable = this.writable(canvasId2);
|
|
2069
2285
|
if (!writable.ok) return writable;
|
|
2070
2286
|
const held = writable.value;
|
|
2071
2287
|
const id = held.manifest.id;
|
|
2072
|
-
if (held.recentOps.includes(
|
|
2288
|
+
if (held.recentOps.includes(step2.opId)) return ok(this.wire(id));
|
|
2073
2289
|
let movedRects = false;
|
|
2074
2290
|
let movedFrames = false;
|
|
2075
2291
|
const live3 = (frameId2) => held.manifest.frames.find((f) => f.id === frameId2 && f.deletedAt === void 0);
|
|
2076
|
-
for (const op of
|
|
2292
|
+
for (const op of step2.ops) {
|
|
2077
2293
|
if (op.t === "move") {
|
|
2078
2294
|
for (const { id: frameId2, x, y } of op.frames) {
|
|
2079
2295
|
const was = held.layout.rects[frameId2];
|
|
@@ -2101,7 +2317,7 @@ var Workspace = class {
|
|
|
2101
2317
|
}
|
|
2102
2318
|
if (movedRects) this.writeLayout(held);
|
|
2103
2319
|
if (movedFrames) this.writeManifest(held);
|
|
2104
|
-
held.recentOps = [...held.recentOps,
|
|
2320
|
+
held.recentOps = [...held.recentOps, step2.opId].slice(-RECENT_OPS);
|
|
2105
2321
|
if (movedRects || movedFrames) this.opts.onChange({ kind: "updated", canvasId: id });
|
|
2106
2322
|
return ok(this.wire(id));
|
|
2107
2323
|
}
|
|
@@ -2125,10 +2341,10 @@ var Workspace = class {
|
|
|
2125
2341
|
}
|
|
2126
2342
|
const sourceRect = held.layout.rects[source.id] ?? NO_RECT;
|
|
2127
2343
|
const at = input.at ?? placeBeside(sourceRect, live3.map((f) => held.layout.rects[f.id] ?? NO_RECT));
|
|
2128
|
-
const
|
|
2344
|
+
const rect3 = normalizeRect({ ...at, width: sourceRect.width, height: sourceRect.height });
|
|
2129
2345
|
const file = `frames/${input.id}.html`;
|
|
2130
2346
|
writeAtomic(join6(this.dir(id), file), bytes.toString("utf8"));
|
|
2131
|
-
held.layout.rects[input.id] =
|
|
2347
|
+
held.layout.rects[input.id] = rect3;
|
|
2132
2348
|
this.writeLayout(held);
|
|
2133
2349
|
const title = cleanName(`${source.title} \u526F\u672C`, MAX_FRAME_TITLE) ?? source.title;
|
|
2134
2350
|
held.manifest.frames.push({ id: input.id, title, file, hash: sha1(bytes), createdAt: this.now() });
|
|
@@ -2136,6 +2352,47 @@ var Workspace = class {
|
|
|
2136
2352
|
this.opts.onChange({ kind: "updated", canvasId: id });
|
|
2137
2353
|
return ok(this.wire(id));
|
|
2138
2354
|
}
|
|
2355
|
+
addAsset(canvasId2, mime, bytes) {
|
|
2356
|
+
const writable = this.writable(canvasId2);
|
|
2357
|
+
if (!writable.ok) return writable;
|
|
2358
|
+
const ext = IMAGE_TYPES[mime];
|
|
2359
|
+
if (!ext) return { ok: false, problem: { code: "IMAGE_UNSUPPORTED", mime } };
|
|
2360
|
+
if (!bytes.length) return { ok: false, problem: { code: "IMAGE_EMPTY" } };
|
|
2361
|
+
if (bytes.length > MAX_IMAGE_BYTES) {
|
|
2362
|
+
return { ok: false, problem: { code: "IMAGE_TOO_LARGE", bytes: bytes.length, limit: MAX_IMAGE_BYTES } };
|
|
2363
|
+
}
|
|
2364
|
+
const name = `${sha1(bytes)}.${ext}`;
|
|
2365
|
+
const path = join6(this.dir(writable.value.manifest.id), ASSETS_DIR, name);
|
|
2366
|
+
if (!existsSync5(path)) {
|
|
2367
|
+
mkdirSync5(join6(this.dir(writable.value.manifest.id), ASSETS_DIR), { recursive: true });
|
|
2368
|
+
writeAtomic(path, bytes);
|
|
2369
|
+
}
|
|
2370
|
+
return ok(name);
|
|
2371
|
+
}
|
|
2372
|
+
addImageFrame(canvasId2, input) {
|
|
2373
|
+
const writable = this.writable(canvasId2);
|
|
2374
|
+
if (!writable.ok) return writable;
|
|
2375
|
+
const held = writable.value;
|
|
2376
|
+
const id = held.manifest.id;
|
|
2377
|
+
if (held.manifest.frames.some((f) => f.id === input.id)) return ok(this.wire(id));
|
|
2378
|
+
const title = cleanName(input.title, MAX_FRAME_TITLE);
|
|
2379
|
+
if (title === null) return { ok: false, problem: { code: "BAD_NAME", max: MAX_FRAME_TITLE } };
|
|
2380
|
+
if (!asAssetName(input.asset) || !existsSync5(join6(this.dir(id), ASSETS_DIR, input.asset))) {
|
|
2381
|
+
return { ok: false, problem: { code: "ASSET_NOT_FOUND", canvasId: id, asset: input.asset } };
|
|
2382
|
+
}
|
|
2383
|
+
if (held.manifest.frames.filter((f) => f.deletedAt === void 0).length >= MAX_FRAMES_PER_CANVAS) {
|
|
2384
|
+
return { ok: false, problem: { code: "TOO_MANY_FRAMES", limit: MAX_FRAMES_PER_CANVAS } };
|
|
2385
|
+
}
|
|
2386
|
+
const html = imageFrameHtml(title, input.asset);
|
|
2387
|
+
const file = `frames/${input.id}.html`;
|
|
2388
|
+
writeAtomic(join6(this.dir(id), file), html);
|
|
2389
|
+
held.layout.rects[input.id] = normalizeRect(input.rect);
|
|
2390
|
+
this.writeLayout(held);
|
|
2391
|
+
held.manifest.frames.push({ id: input.id, title, file, hash: sha1(html), createdAt: this.now() });
|
|
2392
|
+
this.writeManifest(held);
|
|
2393
|
+
this.opts.onChange({ kind: "updated", canvasId: id });
|
|
2394
|
+
return ok(this.wire(id));
|
|
2395
|
+
}
|
|
2139
2396
|
// ------------------------------------------------------------ agent edits
|
|
2140
2397
|
frameOf(held, frame) {
|
|
2141
2398
|
return this.project(held).frames.find((f) => f.id === frame.id);
|
|
@@ -2157,7 +2414,7 @@ var Workspace = class {
|
|
|
2157
2414
|
const rectOf = (frameId2) => held.layout.rects[frameId2] ?? NO_RECT;
|
|
2158
2415
|
const turn = this.turnFrames.filter((frameId2) => live3.some((f) => f.id === frameId2)).map(rectOf);
|
|
2159
2416
|
const at = input.position ?? autoPlace(live3.map((f) => rectOf(f.id)), turn, normalizeRect({ x: 0, y: 0, ...size }));
|
|
2160
|
-
const
|
|
2417
|
+
const rect3 = normalizeRect({ ...at, ...size });
|
|
2161
2418
|
let id;
|
|
2162
2419
|
do
|
|
2163
2420
|
id = randomBytes3(6).toString("hex");
|
|
@@ -2165,7 +2422,7 @@ var Workspace = class {
|
|
|
2165
2422
|
const file = `frames/${id}.html`;
|
|
2166
2423
|
const frame = { id, title, file, hash: sha1(input.html), createdAt: this.now() };
|
|
2167
2424
|
writeAtomic(join6(this.dir(held.manifest.id), file), input.html);
|
|
2168
|
-
held.layout.rects[id] =
|
|
2425
|
+
held.layout.rects[id] = rect3;
|
|
2169
2426
|
this.writeLayout(held);
|
|
2170
2427
|
held.manifest.frames.push(frame);
|
|
2171
2428
|
this.turnFrames.push(id);
|
|
@@ -2202,15 +2459,29 @@ var Workspace = class {
|
|
|
2202
2459
|
writeAtomic(path, input.html);
|
|
2203
2460
|
hash = sha1(input.html);
|
|
2204
2461
|
}
|
|
2205
|
-
|
|
2462
|
+
const resized = input.height === void 0 ? false : this.resizeFrame(held, frame.id, input.height);
|
|
2463
|
+
const rewritten = hash !== frame.hash || title !== void 0 && title !== frame.title;
|
|
2464
|
+
if (rewritten) {
|
|
2206
2465
|
frame.hash = hash;
|
|
2207
2466
|
if (title !== void 0) frame.title = title;
|
|
2208
2467
|
this.writeManifest(held);
|
|
2209
2468
|
this.served.delete(`${held.manifest.id}/${frame.id}`);
|
|
2210
|
-
this.opts.onChange({ kind: "updated", canvasId: held.manifest.id });
|
|
2211
2469
|
}
|
|
2470
|
+
if (rewritten || resized) this.opts.onChange({ kind: "updated", canvasId: held.manifest.id });
|
|
2212
2471
|
return ok(this.frameOf(held, frame));
|
|
2213
2472
|
}
|
|
2473
|
+
/** True if anything moved. */
|
|
2474
|
+
resizeFrame(held, frameId2, height) {
|
|
2475
|
+
const rects = held.layout.rects;
|
|
2476
|
+
const rect3 = rects[frameId2] ?? NO_RECT;
|
|
2477
|
+
const others = held.manifest.frames.filter((f) => f.deletedAt === void 0 && f.id !== frameId2).map((f) => [f.id, rects[f.id] ?? NO_RECT]);
|
|
2478
|
+
const grown = growDown(rect3, height, others);
|
|
2479
|
+
if (grown.rect.height === rect3.height) return false;
|
|
2480
|
+
rects[frameId2] = grown.rect;
|
|
2481
|
+
for (const [id, moved] of grown.moved) rects[id] = moved;
|
|
2482
|
+
this.writeLayout(held);
|
|
2483
|
+
return true;
|
|
2484
|
+
}
|
|
2214
2485
|
removeFrame(canvasId2, frameId2) {
|
|
2215
2486
|
const writable = this.writable(canvasId2);
|
|
2216
2487
|
if (!writable.ok) return writable;
|
|
@@ -2267,6 +2538,119 @@ function refuseCrossSite(sessionOrigin) {
|
|
|
2267
2538
|
};
|
|
2268
2539
|
}
|
|
2269
2540
|
|
|
2541
|
+
// src/library/policy.ts
|
|
2542
|
+
var VIDEO_CAP_BYTES = 2e8;
|
|
2543
|
+
var THUMBNAIL_LONG_SIDE = 400;
|
|
2544
|
+
var TAG_MAX_LENGTH = 64;
|
|
2545
|
+
function normalizeTag(raw) {
|
|
2546
|
+
const name = raw.normalize("NFC").trim().replace(/\s+/g, " ");
|
|
2547
|
+
if (name === "" || name.length > TAG_MAX_LENGTH || /[,,]/.test(name)) return null;
|
|
2548
|
+
return lowercaseInitial(name);
|
|
2549
|
+
}
|
|
2550
|
+
function lowercaseInitial(name) {
|
|
2551
|
+
const [first = "", second = ""] = name;
|
|
2552
|
+
if (second !== second.toLowerCase()) return name;
|
|
2553
|
+
return first.toLowerCase() + name.slice(first.length);
|
|
2554
|
+
}
|
|
2555
|
+
function tagKey(name) {
|
|
2556
|
+
return name.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/attachment-policy.ts
|
|
2560
|
+
var MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
2561
|
+
function isVideoMime(mime) {
|
|
2562
|
+
return mime.startsWith("video/");
|
|
2563
|
+
}
|
|
2564
|
+
function attachmentByteLimit(mime) {
|
|
2565
|
+
return isVideoMime(mime) ? VIDEO_CAP_BYTES : MAX_ATTACHMENT_BYTES;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// src/attachment-wire.ts
|
|
2569
|
+
var MAX_SHEETS = 6;
|
|
2570
|
+
var MAX_SHEET_BYTES = 4 * 1024 * 1024;
|
|
2571
|
+
var MAX_DIGEST_BYTES = 10 * 1024 * 1024;
|
|
2572
|
+
var SETTLE_BODY_LIMIT = Math.ceil(MAX_DIGEST_BYTES * 4 / 3) + 256 * 1024;
|
|
2573
|
+
var FAILURES = {
|
|
2574
|
+
"unsupported-browser": true,
|
|
2575
|
+
"unsupported-codec": true,
|
|
2576
|
+
unreadable: true,
|
|
2577
|
+
crashed: true
|
|
2578
|
+
};
|
|
2579
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
2580
|
+
var positive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
2581
|
+
var count = (n) => Number.isInteger(n) && n >= 0;
|
|
2582
|
+
function parseVideoSettle(raw) {
|
|
2583
|
+
if (typeof raw !== "object" || raw === null) return "a settle is an object";
|
|
2584
|
+
const r = raw;
|
|
2585
|
+
if (r.state === "failed") {
|
|
2586
|
+
if (typeof r.reason !== "string" || !Object.hasOwn(FAILURES, r.reason)) return "unknown failure reason";
|
|
2587
|
+
if (typeof r.detail !== "string") return "detail must be a string";
|
|
2588
|
+
return { state: "failed", reason: r.reason, detail: r.detail.slice(0, 500) };
|
|
2589
|
+
}
|
|
2590
|
+
if (r.state !== "ready") return "state must be ready or failed";
|
|
2591
|
+
const { durationMs, width, height, fps, audio } = r;
|
|
2592
|
+
if (![durationMs, width, height, fps].every(positive)) return "durationMs, width, height and fps must be positive";
|
|
2593
|
+
if (typeof audio !== "boolean") return "audio must be a boolean";
|
|
2594
|
+
const w = width;
|
|
2595
|
+
const h = height;
|
|
2596
|
+
if (!Array.isArray(r.sheets) || r.sheets.length < 1 || r.sheets.length > MAX_SHEETS) {
|
|
2597
|
+
return `sheets must hold 1 to ${MAX_SHEETS} images`;
|
|
2598
|
+
}
|
|
2599
|
+
const sheets = [];
|
|
2600
|
+
let total = 0;
|
|
2601
|
+
for (const sheet of r.sheets) {
|
|
2602
|
+
if (typeof sheet !== "string" || !BASE64.test(sheet)) return "a sheet must be base64";
|
|
2603
|
+
const bytes = new Uint8Array(Buffer.from(sheet, "base64"));
|
|
2604
|
+
if (bytes.byteLength > MAX_SHEET_BYTES) return "a sheet is too large";
|
|
2605
|
+
if (bytes[0] !== 255 || bytes[1] !== 216 || bytes[2] !== 255) return "a sheet must be a JPEG";
|
|
2606
|
+
total += bytes.byteLength;
|
|
2607
|
+
sheets.push(bytes);
|
|
2608
|
+
}
|
|
2609
|
+
if (total > MAX_DIGEST_BYTES) return "the sheets together are too large";
|
|
2610
|
+
if (!Array.isArray(r.motion)) return "motion must be a list";
|
|
2611
|
+
const motion = [];
|
|
2612
|
+
const used = /* @__PURE__ */ new Set();
|
|
2613
|
+
let previous = -1;
|
|
2614
|
+
for (const raw2 of r.motion) {
|
|
2615
|
+
if (typeof raw2 !== "object" || raw2 === null) return "a stretch is an object";
|
|
2616
|
+
const m = raw2;
|
|
2617
|
+
if (!count(m.fromMs) || !count(m.toMs) || m.toMs < m.fromMs) {
|
|
2618
|
+
return "a stretch needs fromMs <= toMs";
|
|
2619
|
+
}
|
|
2620
|
+
if (m.fromMs < previous) return "stretches must be in time order";
|
|
2621
|
+
previous = m.fromMs;
|
|
2622
|
+
if (!insideFrame(m.region, w, h)) return "a stretch region must lie inside the frame";
|
|
2623
|
+
if (!count(m.frames) || !count(m.shown) || m.shown > m.frames) {
|
|
2624
|
+
return "a stretch needs shown <= frames";
|
|
2625
|
+
}
|
|
2626
|
+
if (m.sheet === null) {
|
|
2627
|
+
if (m.shown !== 0) return "a stretch without a sheet shows nothing";
|
|
2628
|
+
} else {
|
|
2629
|
+
if (!Number.isInteger(m.sheet) || m.sheet < 2 || m.sheet > sheets.length) {
|
|
2630
|
+
return "a stretch names a sheet that is not there";
|
|
2631
|
+
}
|
|
2632
|
+
if (used.has(m.sheet)) return "two stretches name one sheet";
|
|
2633
|
+
used.add(m.sheet);
|
|
2634
|
+
}
|
|
2635
|
+
const { x, y, width: rw, height: rh } = m.region;
|
|
2636
|
+
motion.push({
|
|
2637
|
+
fromMs: m.fromMs,
|
|
2638
|
+
toMs: m.toMs,
|
|
2639
|
+
region: { x, y, width: rw, height: rh },
|
|
2640
|
+
frames: m.frames,
|
|
2641
|
+
shown: m.shown,
|
|
2642
|
+
sheet: m.sheet
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
if (used.size !== sheets.length - 1) return "every sheet past the first belongs to a stretch";
|
|
2646
|
+
return { state: "ready", durationMs, width: w, height: h, fps, audio, motion, sheets };
|
|
2647
|
+
}
|
|
2648
|
+
function insideFrame(raw, width, height) {
|
|
2649
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
2650
|
+
const { x, y, width: w, height: h } = raw;
|
|
2651
|
+
return count(x) && count(y) && Number.isInteger(w) && Number.isInteger(h) && w > 0 && h > 0 && x + w <= width && y + h <= height;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2270
2654
|
// src/session-directory.ts
|
|
2271
2655
|
var SessionDirectory = class {
|
|
2272
2656
|
constructor(make, records, log = () => {
|
|
@@ -2289,16 +2673,15 @@ var SessionDirectory = class {
|
|
|
2289
2673
|
if (runtime.endedByUser && !req.reopen) return { ok: false, reason: "ended-by-user" };
|
|
2290
2674
|
runtime.reopen();
|
|
2291
2675
|
}
|
|
2292
|
-
if (req.origin) {
|
|
2293
|
-
|
|
2294
|
-
if (holder && holder !== runtime) holder.detachApp();
|
|
2295
|
-
runtime.attachApp(
|
|
2296
|
-
{ kind: "attached", origin: req.origin, attachedAt: req.now },
|
|
2297
|
-
{ userAttached: true }
|
|
2298
|
-
);
|
|
2299
|
-
}
|
|
2676
|
+
if (req.origin) this.attachOrigin(runtime, req.origin, { userAttached: true, now: req.now });
|
|
2677
|
+
this.records.used?.(req.project, req.now);
|
|
2300
2678
|
return { ok: true, runtime };
|
|
2301
2679
|
}
|
|
2680
|
+
attachOrigin(runtime, origin, opts) {
|
|
2681
|
+
const holder = this.byOrigin(origin);
|
|
2682
|
+
if (holder && holder !== runtime) holder.detachApp();
|
|
2683
|
+
runtime.attachApp({ kind: "attached", origin, attachedAt: opts.now }, { userAttached: opts.userAttached });
|
|
2684
|
+
}
|
|
2302
2685
|
/** `GET /control/sessions/find?project=`: the live session, or the persisted
|
|
2303
2686
|
* active one woken up - a poll that reconnects after an idle exit still finds
|
|
2304
2687
|
* its queued feedback. Null for an unknown or ended project. */
|
|
@@ -2381,15 +2764,28 @@ import {
|
|
|
2381
2764
|
mkdirSync as mkdirSync8,
|
|
2382
2765
|
readFileSync as readFileSync8,
|
|
2383
2766
|
readdirSync as readdirSync4,
|
|
2384
|
-
renameSync as
|
|
2385
|
-
rmSync as
|
|
2767
|
+
renameSync as renameSync4,
|
|
2768
|
+
rmSync as rmSync6,
|
|
2386
2769
|
writeFileSync as writeFileSync6
|
|
2387
2770
|
} from "node:fs";
|
|
2388
2771
|
import { join as join9 } from "node:path";
|
|
2389
2772
|
|
|
2390
2773
|
// src/store.ts
|
|
2391
2774
|
import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
|
|
2392
|
-
import {
|
|
2775
|
+
import { once } from "node:events";
|
|
2776
|
+
import {
|
|
2777
|
+
constants as fsConstants,
|
|
2778
|
+
copyFileSync,
|
|
2779
|
+
createWriteStream,
|
|
2780
|
+
mkdirSync as mkdirSync7,
|
|
2781
|
+
readFileSync as readFileSync7,
|
|
2782
|
+
readdirSync as readdirSync3,
|
|
2783
|
+
renameSync as renameSync3,
|
|
2784
|
+
rmSync as rmSync5,
|
|
2785
|
+
statSync as statSync2,
|
|
2786
|
+
unlinkSync as unlinkSync2,
|
|
2787
|
+
writeFileSync as writeFileSync5
|
|
2788
|
+
} from "node:fs";
|
|
2393
2789
|
import { join as join8 } from "node:path";
|
|
2394
2790
|
|
|
2395
2791
|
// src/preferences.ts
|
|
@@ -2428,6 +2824,13 @@ function rememberAnnouncementSeen(id, file = PREFERENCES_FILE) {
|
|
|
2428
2824
|
write(file, { ...prefs, announcementsSeen: next });
|
|
2429
2825
|
return next;
|
|
2430
2826
|
}
|
|
2827
|
+
function rememberedLastAgent(file = PREFERENCES_FILE) {
|
|
2828
|
+
return read(file).lastAgent;
|
|
2829
|
+
}
|
|
2830
|
+
function rememberLastAgent(command, file = PREFERENCES_FILE) {
|
|
2831
|
+
const prefs = read(file);
|
|
2832
|
+
if (prefs.lastAgent !== command) write(file, { ...prefs, lastAgent: command });
|
|
2833
|
+
}
|
|
2431
2834
|
function write(file, prefs) {
|
|
2432
2835
|
mkdirSync6(dirname4(file), { recursive: true });
|
|
2433
2836
|
writeFileSync4(file, JSON.stringify(prefs, null, 2));
|
|
@@ -2482,6 +2885,30 @@ function dedupeName(name, taken) {
|
|
|
2482
2885
|
if (!taken.has(candidate)) return candidate;
|
|
2483
2886
|
}
|
|
2484
2887
|
}
|
|
2888
|
+
function isPendingVideo(a) {
|
|
2889
|
+
return a.video?.state === "pending";
|
|
2890
|
+
}
|
|
2891
|
+
function sheetFileName(id, sheet) {
|
|
2892
|
+
return `${id}.sheet-${sheet}.jpg`;
|
|
2893
|
+
}
|
|
2894
|
+
function sheetNumbers(video) {
|
|
2895
|
+
return [1, ...video.motion.flatMap((m) => m.sheet === null ? [] : [m.sheet])];
|
|
2896
|
+
}
|
|
2897
|
+
function attachmentFiles(a) {
|
|
2898
|
+
const own = `${a.id}-${a.name}`;
|
|
2899
|
+
return a.video?.state === "ready" ? [own, ...sheetNumbers(a.video).map((n) => sheetFileName(a.id, n))] : [own];
|
|
2900
|
+
}
|
|
2901
|
+
function statusOf2(a) {
|
|
2902
|
+
const video = a.video;
|
|
2903
|
+
if (!video || video.state === "ready") return { state: "ready" };
|
|
2904
|
+
if (video.state === "failed") return { state: "failed", reason: video.reason };
|
|
2905
|
+
return { state: "pending" };
|
|
2906
|
+
}
|
|
2907
|
+
function digestOf(settle) {
|
|
2908
|
+
if (settle.state === "failed") return settle;
|
|
2909
|
+
const { sheets: _, ...frames } = settle;
|
|
2910
|
+
return frames;
|
|
2911
|
+
}
|
|
2485
2912
|
function isProjectSession(value) {
|
|
2486
2913
|
const record = value;
|
|
2487
2914
|
return typeof record?.project === "string" && typeof record.app?.kind === "string" && record.targetOrigin === void 0;
|
|
@@ -2623,6 +3050,7 @@ var SessionStore = class {
|
|
|
2623
3050
|
* the one the user just asked for. Rounds on *other* elements are left alone,
|
|
2624
3051
|
* which is what lets a button and a heading be explored at once. */
|
|
2625
3052
|
startExplore(round) {
|
|
3053
|
+
if (round.attachments?.some(isPendingVideo)) throw new Error("a video still being digested cannot start an explore");
|
|
2626
3054
|
const fresh = {
|
|
2627
3055
|
...round,
|
|
2628
3056
|
status: "generating",
|
|
@@ -2736,6 +3164,7 @@ var SessionStore = class {
|
|
|
2736
3164
|
/** Move the queue (+ optional note and its files) into a sealed batch awaiting
|
|
2737
3165
|
* agent pickup. Attachment bytes stay put - the batch now references them. */
|
|
2738
3166
|
sendBatch(note, attachments = [], references = [], skills = [], view) {
|
|
3167
|
+
if (this.holdsPendingVideo(attachments)) throw new Error("a video still being digested cannot be sent");
|
|
2739
3168
|
const items = this.annotations;
|
|
2740
3169
|
if (items.length === 0 && !note?.trim() && attachments.length === 0 && references.length === 0 && skills.length === 0) {
|
|
2741
3170
|
return null;
|
|
@@ -2809,13 +3238,42 @@ var SessionStore = class {
|
|
|
2809
3238
|
attachmentPath(a) {
|
|
2810
3239
|
return join8(this.attachmentsDir, `${a.id}-${a.name}`);
|
|
2811
3240
|
}
|
|
3241
|
+
sheetPath(a, sheet) {
|
|
3242
|
+
return join8(this.attachmentsDir, sheetFileName(a.id, sheet));
|
|
3243
|
+
}
|
|
2812
3244
|
addAttachment(rawName, mime, bytes) {
|
|
2813
|
-
return this.storeAttachment(rawName, mime, bytes.byteLength, (path) => writeFileSync5(path, bytes));
|
|
3245
|
+
return this.storeAttachment(newId(), rawName, mime, bytes.byteLength, (path) => writeFileSync5(path, bytes));
|
|
3246
|
+
}
|
|
3247
|
+
async receiveAttachment(rawName, mime, body, limit) {
|
|
3248
|
+
const id = newId();
|
|
3249
|
+
mkdirSync7(this.attachmentsDir, { recursive: true });
|
|
3250
|
+
const part = join8(this.attachmentsDir, `${id}.part`);
|
|
3251
|
+
const out = createWriteStream(part);
|
|
3252
|
+
let size = 0;
|
|
3253
|
+
try {
|
|
3254
|
+
for await (const chunk of body) {
|
|
3255
|
+
size += chunk.byteLength;
|
|
3256
|
+
if (size > limit) break;
|
|
3257
|
+
if (!out.write(chunk)) await once(out, "drain");
|
|
3258
|
+
}
|
|
3259
|
+
out.end();
|
|
3260
|
+
await once(out, "close");
|
|
3261
|
+
} catch (error) {
|
|
3262
|
+
out.destroy();
|
|
3263
|
+
rmSync5(part, { force: true });
|
|
3264
|
+
throw error;
|
|
3265
|
+
}
|
|
3266
|
+
if (size > limit || size === 0) {
|
|
3267
|
+
rmSync5(part, { force: true });
|
|
3268
|
+
return size === 0 ? "empty" : "too-large";
|
|
3269
|
+
}
|
|
3270
|
+
return this.storeAttachment(id, rawName, mime, size, (path) => renameSync3(part, path));
|
|
2814
3271
|
}
|
|
2815
3272
|
/** A copy, not a link to the library's file: a session owns its attachments
|
|
2816
3273
|
* outright, and the sweep deletes them. A clone where the filesystem has one. */
|
|
2817
3274
|
copyAttachment(title, mime, from, origin) {
|
|
2818
3275
|
return this.storeAttachment(
|
|
3276
|
+
newId(),
|
|
2819
3277
|
libraryAttachmentName(title, mime),
|
|
2820
3278
|
mime,
|
|
2821
3279
|
statSync2(from).size,
|
|
@@ -2823,16 +3281,17 @@ var SessionStore = class {
|
|
|
2823
3281
|
origin
|
|
2824
3282
|
);
|
|
2825
3283
|
}
|
|
2826
|
-
storeAttachment(rawName, mime, size, write3, origin) {
|
|
3284
|
+
storeAttachment(id, rawName, mime, size, write3, origin) {
|
|
2827
3285
|
const index = this.attachmentIndex;
|
|
2828
3286
|
const taken = new Set(Object.values(index).map((a) => a.name));
|
|
2829
3287
|
const attachment = {
|
|
2830
|
-
id
|
|
3288
|
+
id,
|
|
2831
3289
|
name: dedupeName(sanitizeAttachmentName(rawName, mime), taken),
|
|
2832
3290
|
mime,
|
|
2833
3291
|
size,
|
|
2834
3292
|
createdAt: Date.now(),
|
|
2835
|
-
...origin ? { origin } : {}
|
|
3293
|
+
...origin ? { origin } : {},
|
|
3294
|
+
...isVideoMime(mime) ? { video: { state: "pending" } } : {}
|
|
2836
3295
|
};
|
|
2837
3296
|
mkdirSync7(this.attachmentsDir, { recursive: true });
|
|
2838
3297
|
write3(this.attachmentPath(attachment));
|
|
@@ -2851,6 +3310,31 @@ var SessionStore = class {
|
|
|
2851
3310
|
}
|
|
2852
3311
|
return found;
|
|
2853
3312
|
}
|
|
3313
|
+
get pendingVideoIds() {
|
|
3314
|
+
return Object.values(this.attachmentIndex).flatMap((a) => isPendingVideo(a) ? [a.id] : []);
|
|
3315
|
+
}
|
|
3316
|
+
holdsPendingVideo(extra = []) {
|
|
3317
|
+
return [...extra, ...this.annotations.flatMap((a) => a.attachments ?? [])].some(isPendingVideo);
|
|
3318
|
+
}
|
|
3319
|
+
settleVideo(id, settle) {
|
|
3320
|
+
const index = this.attachmentIndex;
|
|
3321
|
+
const entry = index[id];
|
|
3322
|
+
if (!entry) return { state: "gone" };
|
|
3323
|
+
if (!isPendingVideo(entry)) return statusOf2(entry);
|
|
3324
|
+
if (settle.state === "ready") {
|
|
3325
|
+
settle.sheets.forEach((bytes, i) => writeFileSync5(this.sheetPath(entry, i + 1), bytes));
|
|
3326
|
+
}
|
|
3327
|
+
const settled = { ...entry, video: digestOf(settle) };
|
|
3328
|
+
this.writeJson("attachments.json", { ...index, [id]: settled });
|
|
3329
|
+
const queue = this.annotations;
|
|
3330
|
+
if (queue.some((a) => a.attachments?.some((x) => x.id === id))) {
|
|
3331
|
+
this.writeJson(
|
|
3332
|
+
"queue.json",
|
|
3333
|
+
queue.map((a) => a.attachments ? { ...a, attachments: a.attachments.map((x) => x.id === id ? settled : x) } : a)
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
return statusOf2(settled);
|
|
3337
|
+
}
|
|
2854
3338
|
removeAttachment(id) {
|
|
2855
3339
|
const index = this.attachmentIndex;
|
|
2856
3340
|
const target = index[id];
|
|
@@ -2865,9 +3349,11 @@ var SessionStore = class {
|
|
|
2865
3349
|
if (!list2?.length) return;
|
|
2866
3350
|
const index = this.attachmentIndex;
|
|
2867
3351
|
for (const a of list2) {
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3352
|
+
for (const name of attachmentFiles(a)) {
|
|
3353
|
+
try {
|
|
3354
|
+
unlinkSync2(join8(this.attachmentsDir, name));
|
|
3355
|
+
} catch {
|
|
3356
|
+
}
|
|
2871
3357
|
}
|
|
2872
3358
|
delete index[a.id];
|
|
2873
3359
|
}
|
|
@@ -2896,7 +3382,7 @@ var SessionStore = class {
|
|
|
2896
3382
|
const index = this.attachmentIndex;
|
|
2897
3383
|
const doomed = Object.values(index).filter((a) => !referenced.has(a.id) && a.createdAt < cutoff);
|
|
2898
3384
|
this.dropAttachments(doomed);
|
|
2899
|
-
const kept = new Set(Object.values(this.attachmentIndex).
|
|
3385
|
+
const kept = new Set(Object.values(this.attachmentIndex).flatMap(attachmentFiles));
|
|
2900
3386
|
let entries3;
|
|
2901
3387
|
try {
|
|
2902
3388
|
entries3 = readdirSync3(this.attachmentsDir);
|
|
@@ -2957,6 +3443,18 @@ var SessionStore = class {
|
|
|
2957
3443
|
setAgent(command) {
|
|
2958
3444
|
this.patchSession({ agent: command });
|
|
2959
3445
|
}
|
|
3446
|
+
setAgentBy(by) {
|
|
3447
|
+
const { agentDeclined: _agentDeclined, ...rest } = this.session;
|
|
3448
|
+
this.writeJson("session.json", { ...rest, agentBy: by });
|
|
3449
|
+
}
|
|
3450
|
+
declineAgent() {
|
|
3451
|
+
this.patchSession({ agentDeclined: true });
|
|
3452
|
+
}
|
|
3453
|
+
/** A restore brings back only an agent still connected when the daemon stopped. */
|
|
3454
|
+
clearAgent() {
|
|
3455
|
+
const { agent: _agent, agentBy: _agentBy, ...rest } = this.session;
|
|
3456
|
+
this.writeJson("session.json", rest);
|
|
3457
|
+
}
|
|
2960
3458
|
/** Remember one of the user's agent-config picks - the model, and anything else
|
|
2961
3459
|
* the agent offers - so the next session opens on it. */
|
|
2962
3460
|
setAgentConfig(configId, value) {
|
|
@@ -2967,7 +3465,7 @@ var SessionStore = class {
|
|
|
2967
3465
|
* The picks go with it - they described that agent's options, and the next one
|
|
2968
3466
|
* need not have them. */
|
|
2969
3467
|
end(by) {
|
|
2970
|
-
const { agent: _agent, agentConfig: _agentConfig, ...rest } = this.session;
|
|
3468
|
+
const { agent: _agent, agentBy: _agentBy, agentDeclined: _agentDeclined, agentConfig: _agentConfig, ...rest } = this.session;
|
|
2971
3469
|
this.writeJson("session.json", { ...rest, state: "ended", endedBy: by });
|
|
2972
3470
|
}
|
|
2973
3471
|
/** This review's conversations, oldest first, with one guaranteed to exist.
|
|
@@ -3401,19 +3899,19 @@ var TRANSIENT = /\.(staging|prev)-\d+$/;
|
|
|
3401
3899
|
function recover(sessionsDir, entries3) {
|
|
3402
3900
|
for (const name of entries3) {
|
|
3403
3901
|
const path = join9(sessionsDir, name);
|
|
3404
|
-
if (/\.staging-\d+$/.test(name))
|
|
3902
|
+
if (/\.staging-\d+$/.test(name)) rmSync6(path, { recursive: true, force: true });
|
|
3405
3903
|
const prev = /^(.*)\.prev-\d+$/.exec(name);
|
|
3406
3904
|
if (!prev) continue;
|
|
3407
3905
|
const original = join9(sessionsDir, prev[1]);
|
|
3408
|
-
if (existsSync6(original))
|
|
3409
|
-
else
|
|
3906
|
+
if (existsSync6(original)) rmSync6(path, { recursive: true, force: true });
|
|
3907
|
+
else renameSync4(path, original);
|
|
3410
3908
|
}
|
|
3411
3909
|
}
|
|
3412
3910
|
function writeMerged(sessionsDir, merged, hadExisting) {
|
|
3413
3911
|
const { key } = merged.files;
|
|
3414
3912
|
const target = join9(sessionsDir, key);
|
|
3415
3913
|
const staging = join9(sessionsDir, `${key}.staging-${process.pid}`);
|
|
3416
|
-
|
|
3914
|
+
rmSync6(staging, { recursive: true, force: true });
|
|
3417
3915
|
const attachments = join9(staging, "attachments");
|
|
3418
3916
|
mkdirSync8(attachments, { recursive: true });
|
|
3419
3917
|
if (hadExisting) {
|
|
@@ -3426,27 +3924,27 @@ function writeMerged(sessionsDir, merged, hadExisting) {
|
|
|
3426
3924
|
const from = join9(sessionsDir, copy.sourceKey, "attachments", `${copy.from.id}-${copy.from.name}`);
|
|
3427
3925
|
if (existsSync6(from)) copyFileSync2(from, join9(attachments, `${copy.to.id}-${copy.to.name}`));
|
|
3428
3926
|
}
|
|
3429
|
-
for (const [
|
|
3927
|
+
for (const [field3, file] of Object.entries(FILES)) {
|
|
3430
3928
|
writeFileSync6(
|
|
3431
3929
|
join9(staging, file),
|
|
3432
|
-
JSON.stringify(merged.files[
|
|
3930
|
+
JSON.stringify(merged.files[field3], null, 2)
|
|
3433
3931
|
);
|
|
3434
3932
|
}
|
|
3435
3933
|
writeFileSync6(join9(staging, "session.json"), JSON.stringify(merged.files.session, null, 2));
|
|
3436
3934
|
if (hadExisting) {
|
|
3437
3935
|
const prev = join9(sessionsDir, `${key}.prev-${process.pid}`);
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3936
|
+
renameSync4(target, prev);
|
|
3937
|
+
renameSync4(staging, target);
|
|
3938
|
+
rmSync6(prev, { recursive: true, force: true });
|
|
3441
3939
|
} else {
|
|
3442
|
-
|
|
3940
|
+
renameSync4(staging, target);
|
|
3443
3941
|
}
|
|
3444
3942
|
}
|
|
3445
3943
|
function moveToLegacy(sessionsDir, legacyDir, key) {
|
|
3446
3944
|
mkdirSync8(legacyDir, { recursive: true });
|
|
3447
3945
|
let dest = join9(legacyDir, key);
|
|
3448
3946
|
for (let n = 2; existsSync6(dest); n++) dest = join9(legacyDir, `${key}-${n}`);
|
|
3449
|
-
|
|
3947
|
+
renameSync4(join9(sessionsDir, key), dest);
|
|
3450
3948
|
}
|
|
3451
3949
|
function migrateLegacySessions(sessionsDir = SESSIONS_DIR, legacyDir = LEGACY_SESSIONS_DIR) {
|
|
3452
3950
|
const report2 = { merged: /* @__PURE__ */ new Map(), skipped: [] };
|
|
@@ -3550,12 +4048,12 @@ function fieldsFromSchema(schema) {
|
|
|
3550
4048
|
customs.push({ key, questionId, ...text ? { text } : {} });
|
|
3551
4049
|
continue;
|
|
3552
4050
|
}
|
|
3553
|
-
const
|
|
3554
|
-
if (!
|
|
4051
|
+
const field3 = fieldFrom(key, property);
|
|
4052
|
+
if (!field3) {
|
|
3555
4053
|
if (required.has(key)) return null;
|
|
3556
4054
|
continue;
|
|
3557
4055
|
}
|
|
3558
|
-
fields.push(required.has(key) ?
|
|
4056
|
+
fields.push(required.has(key) ? field3 : { ...field3, optional: true });
|
|
3559
4057
|
}
|
|
3560
4058
|
for (const { questionId, ...custom } of customs) {
|
|
3561
4059
|
const owner = fields.find((f) => f.key === questionId);
|
|
@@ -3645,49 +4143,49 @@ function validateAnswers(fields, raw) {
|
|
|
3645
4143
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
3646
4144
|
const given = raw;
|
|
3647
4145
|
const answers = {};
|
|
3648
|
-
for (const
|
|
3649
|
-
const custom =
|
|
4146
|
+
for (const field3 of fields) {
|
|
4147
|
+
const custom = field3.kind === "select" || field3.kind === "multiselect" ? field3.custom : void 0;
|
|
3650
4148
|
const typed = custom ? given[custom.key] : void 0;
|
|
3651
4149
|
if (typed !== void 0 && typed !== null && typed !== "") {
|
|
3652
4150
|
if (typeof typed !== "string") return null;
|
|
3653
4151
|
answers[custom.key] = typed;
|
|
3654
4152
|
}
|
|
3655
|
-
const value = given[
|
|
4153
|
+
const value = given[field3.key];
|
|
3656
4154
|
if (value === void 0 || value === null || value === "") {
|
|
3657
|
-
if (
|
|
4155
|
+
if (field3.optional || typed) continue;
|
|
3658
4156
|
return null;
|
|
3659
4157
|
}
|
|
3660
|
-
const checked = checkAnswer(
|
|
4158
|
+
const checked = checkAnswer(field3, value);
|
|
3661
4159
|
if (checked === null) return null;
|
|
3662
|
-
answers[
|
|
4160
|
+
answers[field3.key] = checked;
|
|
3663
4161
|
}
|
|
3664
4162
|
return answers;
|
|
3665
4163
|
}
|
|
3666
|
-
function checkAnswer(
|
|
3667
|
-
switch (
|
|
4164
|
+
function checkAnswer(field3, value) {
|
|
4165
|
+
switch (field3.kind) {
|
|
3668
4166
|
case "select":
|
|
3669
|
-
return typeof value === "string" &&
|
|
4167
|
+
return typeof value === "string" && field3.options.some((o) => o.id === value) ? value : null;
|
|
3670
4168
|
case "multiselect": {
|
|
3671
4169
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return null;
|
|
3672
4170
|
const ids = value;
|
|
3673
4171
|
if (new Set(ids).size !== ids.length) return null;
|
|
3674
|
-
if (!ids.every((id) =>
|
|
3675
|
-
if (
|
|
3676
|
-
if (
|
|
4172
|
+
if (!ids.every((id) => field3.options.some((o) => o.id === id))) return null;
|
|
4173
|
+
if (field3.min !== void 0 && ids.length < field3.min) return null;
|
|
4174
|
+
if (field3.max !== void 0 && ids.length > field3.max) return null;
|
|
3677
4175
|
return ids;
|
|
3678
4176
|
}
|
|
3679
4177
|
case "text": {
|
|
3680
4178
|
if (typeof value !== "string") return null;
|
|
3681
|
-
if (
|
|
3682
|
-
if (
|
|
3683
|
-
if (
|
|
4179
|
+
if (field3.min !== void 0 && value.length < field3.min) return null;
|
|
4180
|
+
if (field3.max !== void 0 && value.length > field3.max) return null;
|
|
4181
|
+
if (field3.pattern !== void 0 && !safeMatch(field3.pattern, value)) return null;
|
|
3684
4182
|
return value;
|
|
3685
4183
|
}
|
|
3686
4184
|
case "number": {
|
|
3687
4185
|
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
3688
|
-
if (
|
|
3689
|
-
if (
|
|
3690
|
-
if (
|
|
4186
|
+
if (field3.integer && !Number.isInteger(value)) return null;
|
|
4187
|
+
if (field3.min !== void 0 && value < field3.min) return null;
|
|
4188
|
+
if (field3.max !== void 0 && value > field3.max) return null;
|
|
3691
4189
|
return value;
|
|
3692
4190
|
}
|
|
3693
4191
|
case "boolean":
|
|
@@ -3726,7 +4224,7 @@ function configValueName(option, value) {
|
|
|
3726
4224
|
|
|
3727
4225
|
// src/usage-limit.ts
|
|
3728
4226
|
import { spawn as spawn2 } from "node:child_process";
|
|
3729
|
-
import { mkdirSync as mkdirSync9, mkdtempSync, readFileSync as readFileSync9, rmSync as
|
|
4227
|
+
import { mkdirSync as mkdirSync9, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3730
4228
|
import { tmpdir } from "node:os";
|
|
3731
4229
|
import { join as join10 } from "node:path";
|
|
3732
4230
|
|
|
@@ -3739,7 +4237,7 @@ var CLIENT = { name: "ezdesign", version: "1" };
|
|
|
3739
4237
|
function askCodex(binary, method, params) {
|
|
3740
4238
|
const child = spawn(binary, ["app-server"], { stdio: ["pipe", "pipe", "ignore"] });
|
|
3741
4239
|
const timer = setTimeout(() => child.kill("SIGKILL"), TIMEOUT_MS);
|
|
3742
|
-
return new Promise((
|
|
4240
|
+
return new Promise((resolve7) => {
|
|
3743
4241
|
let answer2;
|
|
3744
4242
|
let rest = "";
|
|
3745
4243
|
const send = (message2) => {
|
|
@@ -3764,8 +4262,8 @@ function askCodex(binary, method, params) {
|
|
|
3764
4262
|
}
|
|
3765
4263
|
}
|
|
3766
4264
|
});
|
|
3767
|
-
child.on("error", () =>
|
|
3768
|
-
child.on("close", () =>
|
|
4265
|
+
child.on("error", () => resolve7(void 0));
|
|
4266
|
+
child.on("close", () => resolve7(answer2));
|
|
3769
4267
|
send({ jsonrpc: "2.0", id: INITIALIZE, method: "initialize", params: { clientInfo: CLIENT } });
|
|
3770
4268
|
}).finally(() => clearTimeout(timer));
|
|
3771
4269
|
}
|
|
@@ -3902,25 +4400,25 @@ async function readClaudeLimit(binary) {
|
|
|
3902
4400
|
});
|
|
3903
4401
|
const timer = setTimeout(() => child.kill("SIGKILL"), READ_TIMEOUT_MS);
|
|
3904
4402
|
try {
|
|
3905
|
-
return await new Promise((
|
|
4403
|
+
return await new Promise((resolve7) => {
|
|
3906
4404
|
let out = "";
|
|
3907
4405
|
child.stdout.setEncoding("utf8");
|
|
3908
4406
|
child.stdout.on("data", (chunk) => out += chunk);
|
|
3909
|
-
child.on("error", () =>
|
|
4407
|
+
child.on("error", () => resolve7(void 0));
|
|
3910
4408
|
child.on("close", () => {
|
|
3911
4409
|
try {
|
|
3912
4410
|
const answer2 = JSON.parse(out).result;
|
|
3913
|
-
|
|
4411
|
+
resolve7(
|
|
3914
4412
|
typeof answer2 === "string" ? claudeLimitFromUsageText(answer2) ?? void 0 : void 0
|
|
3915
4413
|
);
|
|
3916
4414
|
} catch {
|
|
3917
|
-
|
|
4415
|
+
resolve7(void 0);
|
|
3918
4416
|
}
|
|
3919
4417
|
});
|
|
3920
4418
|
});
|
|
3921
4419
|
} finally {
|
|
3922
4420
|
clearTimeout(timer);
|
|
3923
|
-
|
|
4421
|
+
rmSync7(cwd, { recursive: true, force: true });
|
|
3924
4422
|
}
|
|
3925
4423
|
}
|
|
3926
4424
|
var lastRead = /* @__PURE__ */ new Map();
|
|
@@ -3935,12 +4433,13 @@ async function readLimit(command, now = Date.now()) {
|
|
|
3935
4433
|
}
|
|
3936
4434
|
var READ_TIMEOUT_MS = 6e4;
|
|
3937
4435
|
|
|
3938
|
-
// src/
|
|
4436
|
+
// src/child-groups.ts
|
|
3939
4437
|
import { execFileSync } from "node:child_process";
|
|
3940
|
-
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, rmSync as
|
|
4438
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
3941
4439
|
import { join as join11 } from "node:path";
|
|
3942
4440
|
var FILE2 = join11(DATA_DIR, "agent-children.json");
|
|
3943
4441
|
var live2 = /* @__PURE__ */ new Map();
|
|
4442
|
+
process.on("exit", () => killTrackedGroups());
|
|
3944
4443
|
function read3() {
|
|
3945
4444
|
try {
|
|
3946
4445
|
return JSON.parse(readFileSync10(FILE2, "utf8"));
|
|
@@ -3967,7 +4466,7 @@ function running(pid) {
|
|
|
3967
4466
|
return false;
|
|
3968
4467
|
}
|
|
3969
4468
|
}
|
|
3970
|
-
function
|
|
4469
|
+
function isStillTheProcess(pid, command) {
|
|
3971
4470
|
try {
|
|
3972
4471
|
const actual = execFileSync("ps", ["-o", "command=", "-p", String(pid)], {
|
|
3973
4472
|
encoding: "utf8",
|
|
@@ -3988,17 +4487,17 @@ function killGroup(pid, signal = "SIGKILL") {
|
|
|
3988
4487
|
}
|
|
3989
4488
|
}
|
|
3990
4489
|
}
|
|
3991
|
-
function
|
|
4490
|
+
function trackGroup(pid, command) {
|
|
3992
4491
|
live2.set(pid, command);
|
|
3993
4492
|
write2();
|
|
3994
4493
|
}
|
|
3995
|
-
function
|
|
4494
|
+
function untrackGroup(pid) {
|
|
3996
4495
|
if (live2.delete(pid)) write2();
|
|
3997
4496
|
}
|
|
3998
|
-
function
|
|
4497
|
+
function killTrackedGroups() {
|
|
3999
4498
|
for (const pid of live2.keys()) killGroup(pid);
|
|
4000
4499
|
}
|
|
4001
|
-
function
|
|
4500
|
+
function reapOrphanedGroups() {
|
|
4002
4501
|
const record = read3();
|
|
4003
4502
|
if (!record) {
|
|
4004
4503
|
write2();
|
|
@@ -4007,17 +4506,17 @@ function reapOrphanedAgents() {
|
|
|
4007
4506
|
if (record.daemonPid !== process.pid && running(record.daemonPid)) return 0;
|
|
4008
4507
|
let killed = 0;
|
|
4009
4508
|
for (const { pid, command } of record.children ?? []) {
|
|
4010
|
-
if (pid === process.pid || !
|
|
4509
|
+
if (pid === process.pid || !isStillTheProcess(pid, command)) continue;
|
|
4011
4510
|
killGroup(pid);
|
|
4012
4511
|
killed++;
|
|
4013
4512
|
}
|
|
4014
4513
|
write2();
|
|
4015
4514
|
return killed;
|
|
4016
4515
|
}
|
|
4017
|
-
function
|
|
4516
|
+
function clearGroupRecord() {
|
|
4018
4517
|
live2.clear();
|
|
4019
4518
|
try {
|
|
4020
|
-
|
|
4519
|
+
rmSync8(FILE2, { force: true });
|
|
4021
4520
|
} catch {
|
|
4022
4521
|
}
|
|
4023
4522
|
}
|
|
@@ -4044,8 +4543,8 @@ var MessageQueue = class {
|
|
|
4044
4543
|
next() {
|
|
4045
4544
|
if (this.queued.length) return Promise.resolve(this.queued.shift());
|
|
4046
4545
|
if (this.retired) return Promise.resolve(RETIRED);
|
|
4047
|
-
return new Promise((
|
|
4048
|
-
this.waiting =
|
|
4546
|
+
return new Promise((resolve7) => {
|
|
4547
|
+
this.waiting = resolve7;
|
|
4049
4548
|
});
|
|
4050
4549
|
}
|
|
4051
4550
|
/** Wakes the reader and answers every later read. Retiring rather than
|
|
@@ -4114,7 +4613,6 @@ var FEED_CAP = 100;
|
|
|
4114
4613
|
function trimTitle(title, cwd) {
|
|
4115
4614
|
return title.replaceAll(`${cwd}/`, "");
|
|
4116
4615
|
}
|
|
4117
|
-
process.on("exit", () => killTrackedAgents());
|
|
4118
4616
|
var AcpAgent = class {
|
|
4119
4617
|
constructor(opts) {
|
|
4120
4618
|
this.opts = opts;
|
|
@@ -4126,7 +4624,7 @@ var AcpAgent = class {
|
|
|
4126
4624
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4127
4625
|
env: { ...process.env, ...opts.env }
|
|
4128
4626
|
});
|
|
4129
|
-
if (this.child.pid)
|
|
4627
|
+
if (this.child.pid) trackGroup(this.child.pid, opts.command);
|
|
4130
4628
|
this.child.stderr?.on("data", (chunk) => {
|
|
4131
4629
|
this.stderrTail = [...this.stderrTail, chunk.toString()].slice(-20);
|
|
4132
4630
|
});
|
|
@@ -4320,10 +4818,10 @@ var AcpAgent = class {
|
|
|
4320
4818
|
}
|
|
4321
4819
|
settle(id, outcome) {
|
|
4322
4820
|
if (!this.ask || this.ask.id !== id || !this.askResolve) return false;
|
|
4323
|
-
const
|
|
4821
|
+
const resolve7 = this.askResolve;
|
|
4324
4822
|
this.ask = null;
|
|
4325
4823
|
this.askResolve = null;
|
|
4326
|
-
|
|
4824
|
+
resolve7(outcome);
|
|
4327
4825
|
this.opts.onChange();
|
|
4328
4826
|
return true;
|
|
4329
4827
|
}
|
|
@@ -4518,11 +5016,11 @@ var AcpAgent = class {
|
|
|
4518
5016
|
killGroup(pid, "SIGTERM");
|
|
4519
5017
|
setTimeout(() => {
|
|
4520
5018
|
killGroup(pid);
|
|
4521
|
-
|
|
5019
|
+
untrackGroup(pid);
|
|
4522
5020
|
}, 3e3).unref();
|
|
4523
5021
|
}
|
|
4524
5022
|
untrack() {
|
|
4525
|
-
if (this.child.pid !== void 0)
|
|
5023
|
+
if (this.child.pid !== void 0) untrackGroup(this.child.pid);
|
|
4526
5024
|
}
|
|
4527
5025
|
fail(message2) {
|
|
4528
5026
|
if (this.state === "exited") return;
|
|
@@ -4538,10 +5036,10 @@ ${tail}` : message2;
|
|
|
4538
5036
|
}
|
|
4539
5037
|
/** A question nobody can answer any more must not hang the agent's request. */
|
|
4540
5038
|
settleAsk() {
|
|
4541
|
-
const
|
|
5039
|
+
const resolve7 = this.askResolve;
|
|
4542
5040
|
this.ask = null;
|
|
4543
5041
|
this.askResolve = null;
|
|
4544
|
-
|
|
5042
|
+
resolve7?.(null);
|
|
4545
5043
|
}
|
|
4546
5044
|
turnEnded(stopReason, epoch) {
|
|
4547
5045
|
if (epoch !== this.epoch || this.state !== "working") return;
|
|
@@ -4557,19 +5055,19 @@ ${tail}` : message2;
|
|
|
4557
5055
|
* was settled by a shutdown. One at a time by protocol shape: the agent
|
|
4558
5056
|
* blocks on the request, so a second cannot arrive while one is pending. */
|
|
4559
5057
|
pendAsk(ask) {
|
|
4560
|
-
return new Promise((
|
|
5058
|
+
return new Promise((resolve7) => {
|
|
4561
5059
|
this.ask = { id: `ask-${++this.askSeq}`, ...ask };
|
|
4562
|
-
this.askResolve =
|
|
5060
|
+
this.askResolve = resolve7;
|
|
4563
5061
|
this.opts.onChange();
|
|
4564
5062
|
});
|
|
4565
5063
|
}
|
|
4566
5064
|
async requestPermission(params) {
|
|
4567
5065
|
const toolName = params.toolCall.name ?? params.toolCall.title ?? "";
|
|
4568
5066
|
if (this.opts.ownTool?.(toolName)) {
|
|
4569
|
-
const
|
|
4570
|
-
if (
|
|
5067
|
+
const once2 = params.options.find((o) => o.kind === "allow_once");
|
|
5068
|
+
if (once2) return { outcome: { outcome: "selected", optionId: once2.optionId } };
|
|
4571
5069
|
}
|
|
4572
|
-
const
|
|
5070
|
+
const field3 = {
|
|
4573
5071
|
key: "option",
|
|
4574
5072
|
kind: "select",
|
|
4575
5073
|
options: params.options.map((o) => ({ id: o.optionId, name: o.name, hint: o.kind }))
|
|
@@ -4577,7 +5075,7 @@ ${tail}` : message2;
|
|
|
4577
5075
|
const outcome = await this.pendAsk({
|
|
4578
5076
|
kind: "permission",
|
|
4579
5077
|
title: params.toolCall.title ?? "The agent needs a decision",
|
|
4580
|
-
fields: [
|
|
5078
|
+
fields: [field3]
|
|
4581
5079
|
});
|
|
4582
5080
|
const optionId = outcome && "answers" in outcome ? outcome.answers.option : void 0;
|
|
4583
5081
|
if (typeof optionId !== "string") return { outcome: { outcome: "cancelled" } };
|
|
@@ -4729,8 +5227,8 @@ ${tail}` : message2;
|
|
|
4729
5227
|
this.canFork = !!init.agentCapabilities?.sessionCapabilities?.fork;
|
|
4730
5228
|
this.ctx = ctx;
|
|
4731
5229
|
await this.openSession();
|
|
4732
|
-
await new Promise((
|
|
4733
|
-
this.finish =
|
|
5230
|
+
await new Promise((resolve7) => {
|
|
5231
|
+
this.finish = resolve7;
|
|
4734
5232
|
});
|
|
4735
5233
|
});
|
|
4736
5234
|
}
|
|
@@ -4822,109 +5320,1378 @@ function worthListing(chat, index, all) {
|
|
|
4822
5320
|
return !!chat.title || chat.current || index === all.length - 1;
|
|
4823
5321
|
}
|
|
4824
5322
|
|
|
4825
|
-
// src/
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5323
|
+
// src/dev-server/index.ts
|
|
5324
|
+
import { readFileSync as readFileSync13, realpathSync as realpathSync2 } from "node:fs";
|
|
5325
|
+
import { join as join15, relative as relative2 } from "node:path";
|
|
5326
|
+
|
|
5327
|
+
// src/login-env.ts
|
|
5328
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
5329
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
5330
|
+
import { accessSync, constants, readFileSync as readFileSync11 } from "node:fs";
|
|
5331
|
+
import { delimiter, join as join13 } from "node:path";
|
|
5332
|
+
var LOGIN_SHELL_TIMEOUT_MS = 1e4;
|
|
5333
|
+
var PROCESS_LOCAL_VARS = /* @__PURE__ */ new Set(["_", "SHLVL", "PWD", "OLDPWD", LOGIN_ENV_VAR, RUN_AS_NODE]);
|
|
5334
|
+
var adopted = null;
|
|
5335
|
+
function adoptLoginEnvironment() {
|
|
5336
|
+
adopted ??= resolveLoginEnvironment(process.env, LOGIN_ENV_CACHE);
|
|
5337
|
+
return adopted;
|
|
5338
|
+
}
|
|
5339
|
+
async function resolveLoginEnvironment(env, cacheFile) {
|
|
5340
|
+
const request2 = env[LOGIN_ENV_VAR];
|
|
5341
|
+
if (!request2) return { kind: "inherited" };
|
|
5342
|
+
if (request2 === "resolved") return { kind: "already-resolved" };
|
|
5343
|
+
const shell = env.SHELL || "/bin/zsh";
|
|
5344
|
+
const home = env.HOME ?? "";
|
|
5345
|
+
const cached = readCache(cacheFile, shell, home);
|
|
5346
|
+
const fresh = dumpLoginShell(shell, env).then((dumped2) => {
|
|
5347
|
+
if (dumped2.env) cacheDump(cacheFile, { shell, home, env: dumped2.env });
|
|
5348
|
+
return dumped2;
|
|
5349
|
+
});
|
|
5350
|
+
if (cached) {
|
|
5351
|
+
merge(env, cached);
|
|
5352
|
+
void fresh.catch(() => {
|
|
5353
|
+
});
|
|
5354
|
+
return { kind: "cached" };
|
|
4845
5355
|
}
|
|
5356
|
+
const started = Date.now();
|
|
5357
|
+
const dumped = await fresh;
|
|
5358
|
+
if (!dumped.env) return { kind: "failed", shell, error: dumped.error };
|
|
5359
|
+
merge(env, dumped.env);
|
|
5360
|
+
return { kind: "resolved", shell, ms: Date.now() - started };
|
|
4846
5361
|
}
|
|
4847
|
-
function
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
const tags = scanTags(html);
|
|
4852
|
-
if (!tags) return { code: "not-one-root", roots: 0 };
|
|
4853
|
-
for (const tag of tags.names) {
|
|
4854
|
-
if (FORBIDDEN_TAGS.includes(tag)) return { code: "forbidden-tag", tag };
|
|
5362
|
+
function cacheDump(file, cache) {
|
|
5363
|
+
try {
|
|
5364
|
+
writeAtomic(file, JSON.stringify(cache), 384);
|
|
5365
|
+
} catch {
|
|
4855
5366
|
}
|
|
4856
|
-
const handler = /\son([a-z]+)\s*=/i.exec(html);
|
|
4857
|
-
if (handler) return { code: "event-handler", attribute: `on${handler[1].toLowerCase()}` };
|
|
4858
|
-
if (/javascript:/i.test(html)) return { code: "javascript-url" };
|
|
4859
|
-
if (tags.roots !== 1) return { code: "not-one-root", roots: tags.roots };
|
|
4860
|
-
return null;
|
|
4861
5367
|
}
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
"
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
"meta",
|
|
4873
|
-
"param",
|
|
4874
|
-
"source",
|
|
4875
|
-
"track",
|
|
4876
|
-
"wbr"
|
|
4877
|
-
]);
|
|
4878
|
-
function scanTags(html) {
|
|
4879
|
-
const names2 = [];
|
|
4880
|
-
let depth = 0;
|
|
4881
|
-
let roots = 0;
|
|
4882
|
-
let i = 0;
|
|
4883
|
-
while (i < html.length) {
|
|
4884
|
-
const lt = html.indexOf("<", i);
|
|
4885
|
-
if (lt === -1) break;
|
|
4886
|
-
if (html.startsWith("<!--", lt)) {
|
|
4887
|
-
const end2 = html.indexOf("-->", lt + 4);
|
|
4888
|
-
i = end2 === -1 ? html.length : end2 + 3;
|
|
4889
|
-
continue;
|
|
4890
|
-
}
|
|
4891
|
-
if (html.startsWith("<!", lt) || html.startsWith("<?", lt)) {
|
|
4892
|
-
const end2 = html.indexOf(">", lt);
|
|
4893
|
-
i = end2 === -1 ? html.length : end2 + 1;
|
|
4894
|
-
continue;
|
|
4895
|
-
}
|
|
4896
|
-
const tag = /^<(\/?)([a-zA-Z][a-zA-Z0-9-]*)/.exec(html.slice(lt, lt + 64));
|
|
4897
|
-
if (!tag) {
|
|
4898
|
-
i = lt + 1;
|
|
4899
|
-
continue;
|
|
4900
|
-
}
|
|
4901
|
-
const end = closingBracket(html, lt);
|
|
4902
|
-
if (end === -1) break;
|
|
4903
|
-
const closing = tag[1] === "/";
|
|
4904
|
-
const name = tag[2].toLowerCase();
|
|
4905
|
-
names2.push(name);
|
|
4906
|
-
const selfClosing = html[end - 1] === "/";
|
|
4907
|
-
if (closing) {
|
|
4908
|
-
if (depth === 0) return null;
|
|
4909
|
-
depth -= 1;
|
|
4910
|
-
} else if (!selfClosing && !VOID_TAGS.has(name)) {
|
|
4911
|
-
if (depth === 0) roots += 1;
|
|
4912
|
-
depth += 1;
|
|
4913
|
-
} else if (depth === 0) {
|
|
4914
|
-
roots += 1;
|
|
4915
|
-
}
|
|
4916
|
-
i = end + 1;
|
|
4917
|
-
if (!closing && RAW_TEXT2.has(name) && !selfClosing) {
|
|
4918
|
-
const close = html.toLowerCase().indexOf(`</${name}`, i);
|
|
4919
|
-
if (close === -1) break;
|
|
4920
|
-
i = close;
|
|
4921
|
-
}
|
|
5368
|
+
function merge(env, dump) {
|
|
5369
|
+
for (const [key, value] of Object.entries(dump)) if (!PROCESS_LOCAL_VARS.has(key)) env[key] = value;
|
|
5370
|
+
env[LOGIN_ENV_VAR] = "resolved";
|
|
5371
|
+
}
|
|
5372
|
+
function readCache(file, shell, home) {
|
|
5373
|
+
try {
|
|
5374
|
+
const cache = JSON.parse(readFileSync11(file, "utf8"));
|
|
5375
|
+
return cache.shell === shell && cache.home === home && cache.env && typeof cache.env === "object" ? cache.env : null;
|
|
5376
|
+
} catch {
|
|
5377
|
+
return null;
|
|
4922
5378
|
}
|
|
4923
|
-
return { names: names2, roots };
|
|
4924
5379
|
}
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
5380
|
+
function dumpLoginShell(shell, env) {
|
|
5381
|
+
const marker = randomBytes5(8).toString("hex");
|
|
5382
|
+
const { file, args } = loginShellInvocation(shell, marker);
|
|
5383
|
+
return new Promise((resolve7) => {
|
|
5384
|
+
const minimal = { HOME: env.HOME ?? "", USER: env.USER ?? "", LOGNAME: env.LOGNAME ?? "", SHELL: shell, TERM: "dumb" };
|
|
5385
|
+
const child = spawn4(file, args, { env: minimal, stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
5386
|
+
let stdout = "";
|
|
5387
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
5388
|
+
const timer = setTimeout(() => {
|
|
5389
|
+
try {
|
|
5390
|
+
process.kill(-child.pid, "SIGKILL");
|
|
5391
|
+
} catch {
|
|
5392
|
+
}
|
|
5393
|
+
}, LOGIN_SHELL_TIMEOUT_MS);
|
|
5394
|
+
child.on("error", (error) => {
|
|
5395
|
+
clearTimeout(timer);
|
|
5396
|
+
resolve7({ error: error.message });
|
|
5397
|
+
});
|
|
5398
|
+
child.on("close", () => {
|
|
5399
|
+
clearTimeout(timer);
|
|
5400
|
+
const dump = parseEnvDump(stdout, marker);
|
|
5401
|
+
resolve7(dump ? { env: dump } : { error: `${shell} printed no environment within ${LOGIN_SHELL_TIMEOUT_MS / 1e3} s` });
|
|
5402
|
+
});
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
function loginShellInvocation(shell, marker) {
|
|
5406
|
+
return { file: shell, args: ["-i", "-l", "-c", `printf %s ${marker}; /usr/bin/env -0; printf %s ${marker}`] };
|
|
5407
|
+
}
|
|
5408
|
+
function parseEnvDump(stdout, marker) {
|
|
5409
|
+
const first = stdout.indexOf(marker);
|
|
5410
|
+
const last = stdout.lastIndexOf(marker);
|
|
5411
|
+
if (first === -1 || last === first) return null;
|
|
5412
|
+
const dump = {};
|
|
5413
|
+
for (const entry of stdout.slice(first + marker.length, last).split("\0")) {
|
|
5414
|
+
const eq = entry.indexOf("=");
|
|
5415
|
+
if (eq > 0) dump[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
5416
|
+
}
|
|
5417
|
+
return dump;
|
|
5418
|
+
}
|
|
5419
|
+
function findExecutable(binary, path) {
|
|
5420
|
+
for (const dir of (path ?? "").split(delimiter).filter(Boolean)) {
|
|
5421
|
+
const candidate = join13(dir, binary);
|
|
5422
|
+
try {
|
|
5423
|
+
accessSync(candidate, constants.X_OK);
|
|
5424
|
+
return candidate;
|
|
5425
|
+
} catch {
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
return null;
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
// src/dev-server/detect.ts
|
|
5432
|
+
import { readFileSync as readFileSync12, statSync as statSync4 } from "node:fs";
|
|
5433
|
+
import { join as join14 } from "node:path";
|
|
5434
|
+
var LOCKFILES_BY_PRECEDENCE = [
|
|
5435
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
5436
|
+
{ file: "yarn.lock", manager: "yarn" },
|
|
5437
|
+
{ file: "bun.lock", manager: "bun" },
|
|
5438
|
+
{ file: "bun.lockb", manager: "bun" },
|
|
5439
|
+
{ file: "package-lock.json", manager: "npm" }
|
|
5440
|
+
];
|
|
5441
|
+
function readProjectFacts(root) {
|
|
5442
|
+
const isFile = (name) => statSync4(join14(root, name), { throwIfNoEntry: false })?.isFile() === true;
|
|
5443
|
+
return {
|
|
5444
|
+
packageJson: readPackageJson(join14(root, "package.json")),
|
|
5445
|
+
lockfiles: LOCKFILES_BY_PRECEDENCE.filter((l) => isFile(l.file)).map((l) => l.file),
|
|
5446
|
+
pnpmWorkspace: isFile("pnpm-workspace.yaml")
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
function readPackageJson(file) {
|
|
5450
|
+
let pkg2;
|
|
5451
|
+
try {
|
|
5452
|
+
pkg2 = JSON.parse(readFileSync12(file, "utf8"));
|
|
5453
|
+
} catch {
|
|
5454
|
+
return null;
|
|
5455
|
+
}
|
|
5456
|
+
if (!pkg2 || typeof pkg2 !== "object" || Array.isArray(pkg2)) return null;
|
|
5457
|
+
const { scripts, packageManager, workspaces } = pkg2;
|
|
5458
|
+
const named = scripts && typeof scripts === "object" ? Object.entries(scripts).filter((e) => typeof e[1] === "string") : [];
|
|
5459
|
+
return {
|
|
5460
|
+
scripts: Object.fromEntries(named),
|
|
5461
|
+
...typeof packageManager === "string" ? { packageManager } : {},
|
|
5462
|
+
workspaces: Array.isArray(workspaces) || !!workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
var MANAGERS = ["pnpm", "yarn", "bun", "npm"];
|
|
5466
|
+
var SERVER_LOOKING = /(^|[:_-])(dev|start|serve)([:_-]|$)/;
|
|
5467
|
+
function detectDevCommand(facts) {
|
|
5468
|
+
const pkg2 = facts.packageJson;
|
|
5469
|
+
if (!pkg2) return { kind: "undecided", reason: "no-package-json", serverScripts: [] };
|
|
5470
|
+
const script = "dev" in pkg2.scripts ? "dev" : "start" in pkg2.scripts ? "start" : null;
|
|
5471
|
+
if (!script) {
|
|
5472
|
+
const serverScripts = Object.keys(pkg2.scripts).filter((name) => SERVER_LOOKING.test(name));
|
|
5473
|
+
const atRoot = pkg2.workspaces || facts.pnpmWorkspace;
|
|
5474
|
+
return { kind: "undecided", reason: atRoot ? "monorepo-root" : "no-script", serverScripts };
|
|
5475
|
+
}
|
|
5476
|
+
const fromField = MANAGERS.find((m) => pkg2.packageManager?.split("@")[0] === m);
|
|
5477
|
+
if (fromField) return { kind: "command", command: { manager: fromField, script, managerFrom: "packageManager" } };
|
|
5478
|
+
const lock = LOCKFILES_BY_PRECEDENCE.find((l) => facts.lockfiles.includes(l.file));
|
|
5479
|
+
if (lock) return { kind: "command", command: { manager: lock.manager, script, managerFrom: "lockfile" } };
|
|
5480
|
+
return { kind: "command", command: { manager: "npm", script, managerFrom: "default" } };
|
|
5481
|
+
}
|
|
5482
|
+
function commandLine(command) {
|
|
5483
|
+
switch (command.manager) {
|
|
5484
|
+
case "npm":
|
|
5485
|
+
return command.script === "start" ? "npm start" : "npm run dev";
|
|
5486
|
+
case "bun":
|
|
5487
|
+
return `bun run ${command.script}`;
|
|
5488
|
+
default:
|
|
5489
|
+
return `${command.manager} ${command.script}`;
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
function requiredTools(command) {
|
|
5493
|
+
return command.manager === "bun" ? ["bun"] : [command.manager, "node"];
|
|
5494
|
+
}
|
|
5495
|
+
function toolsForLine(line2) {
|
|
5496
|
+
const first = line2.trim().split(/\s+/)[0] ?? "";
|
|
5497
|
+
if (!/^[\w.+-]+$/.test(first)) return [];
|
|
5498
|
+
return ["npm", "npx", "pnpm", "yarn"].includes(first) ? [first, "node"] : [first];
|
|
5499
|
+
}
|
|
5500
|
+
var parts = (...items) => items.map((item) => typeof item === "string" ? { text: item } : item);
|
|
5501
|
+
var run = (command) => ({ command });
|
|
5502
|
+
var INSTALL = {
|
|
5503
|
+
node: parts("\u5F9E https://nodejs.org \u5B89\u88DD Node.js\uFF0C\u6216\u57F7\u884C ", run("brew install node")),
|
|
5504
|
+
npm: parts("npm \u96A8 Node.js \u4E00\u8D77\u5B89\u88DD\uFF1A\u5F9E https://nodejs.org \u5B89\u88DD Node.js"),
|
|
5505
|
+
pnpm: parts("\u57F7\u884C ", run("corepack enable pnpm"), "\uFF0C\u6216 ", run("npm install -g pnpm")),
|
|
5506
|
+
yarn: parts("\u57F7\u884C ", run("corepack enable yarn"), "\uFF0C\u6216 ", run("npm install -g yarn")),
|
|
5507
|
+
bun: parts("\u57F7\u884C ", run("curl -fsSL https://bun.sh/install | bash"), "\uFF0C\u6216 ", run("brew install oven-sh/bun/bun"))
|
|
5508
|
+
};
|
|
5509
|
+
function missingTools(tools, findOnPath) {
|
|
5510
|
+
return tools.filter((tool2) => findOnPath(tool2) === null).map((tool2) => ({ tool: tool2, install: INSTALL[tool2] ?? parts("\u5B89\u88DD ", run(tool2), "\uFF0C\u4E26\u78BA\u8A8D\u767B\u5165 shell \u7684 PATH \u627E\u5F97\u5230\u5B83") }));
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
// src/dev-server/lifecycle.ts
|
|
5514
|
+
var INITIAL = { state: { phase: "idle" }, lastAttempt: 0, lastRunId: 0 };
|
|
5515
|
+
function step(machine, event) {
|
|
5516
|
+
const state = machine.state;
|
|
5517
|
+
const to = (next, effects = [], counters = {}) => ({
|
|
5518
|
+
machine: { ...machine, ...counters, state: next },
|
|
5519
|
+
effects,
|
|
5520
|
+
stateChanged: next !== state
|
|
5521
|
+
});
|
|
5522
|
+
const same = (effects = []) => ({ machine, effects, stateChanged: false });
|
|
5523
|
+
const current = (runId) => liveRun(state)?.id === runId;
|
|
5524
|
+
const resolveAfresh = (now, by) => {
|
|
5525
|
+
const attempt = machine.lastAttempt + 1;
|
|
5526
|
+
return to({ phase: "resolving", since: now, by, attempt }, [{ type: "resolve", attempt }], { lastAttempt: attempt });
|
|
5527
|
+
};
|
|
5528
|
+
const restartRun = (now, by) => {
|
|
5529
|
+
if (state.phase === "stopping") return to({ ...state, then: { kind: "restart", by, since: now } });
|
|
5530
|
+
const run4 = liveRun(state);
|
|
5531
|
+
return to({ phase: "stopping", run: run4, then: { kind: "restart", by, since: now } }, [
|
|
5532
|
+
{ type: "kill", runId: run4.id },
|
|
5533
|
+
...detachShown(state)
|
|
5534
|
+
]);
|
|
5535
|
+
};
|
|
5536
|
+
const crash = (run4, cause, tail, exit = {}) => to(
|
|
5537
|
+
{
|
|
5538
|
+
phase: "crashed",
|
|
5539
|
+
command: run4.command,
|
|
5540
|
+
cause,
|
|
5541
|
+
...exit.code !== void 0 && exit.code !== null ? { exitCode: exit.code } : {},
|
|
5542
|
+
...exit.signal ? { signal: exit.signal } : {},
|
|
5543
|
+
tail
|
|
5544
|
+
},
|
|
5545
|
+
[...detachShown(state), NO_RESUME]
|
|
5546
|
+
);
|
|
5547
|
+
switch (event.type) {
|
|
5548
|
+
case "ensure":
|
|
5549
|
+
return RESTARTABLE.has(state.phase) ? resolveAfresh(event.now, event.by) : same();
|
|
5550
|
+
case "restart":
|
|
5551
|
+
if (liveRun(state)) return restartRun(event.now, event.by);
|
|
5552
|
+
return RESTARTABLE.has(state.phase) ? resolveAfresh(event.now, event.by) : same();
|
|
5553
|
+
case "confirmed":
|
|
5554
|
+
return liveRun(state) ? restartRun(event.now, event.by) : resolveAfresh(event.now, event.by);
|
|
5555
|
+
case "stop": {
|
|
5556
|
+
if (state.phase === "stopping") return to({ ...state, then: { kind: "stopped" } }, [NO_RESUME]);
|
|
5557
|
+
const run4 = liveRun(state);
|
|
5558
|
+
if (run4) {
|
|
5559
|
+
return to({ phase: "stopping", run: run4, then: { kind: "stopped" } }, [
|
|
5560
|
+
{ type: "kill", runId: run4.id },
|
|
5561
|
+
...detachShown(state),
|
|
5562
|
+
NO_RESUME
|
|
5563
|
+
]);
|
|
5564
|
+
}
|
|
5565
|
+
if (state.phase === "resolving" || state.phase === "needs-command") return to({ phase: "stopped" }, [NO_RESUME]);
|
|
5566
|
+
if (state.phase === "blocked") return to({ phase: "stopped", command: state.command }, [NO_RESUME]);
|
|
5567
|
+
return same();
|
|
5568
|
+
}
|
|
5569
|
+
case "resolved": {
|
|
5570
|
+
if (state.phase !== "resolving" || state.attempt !== event.attempt) return same();
|
|
5571
|
+
const r = event.resolution;
|
|
5572
|
+
switch (r.kind) {
|
|
5573
|
+
case "run": {
|
|
5574
|
+
const run4 = { id: machine.lastRunId + 1, command: r.command, startedAt: event.now, by: state.by };
|
|
5575
|
+
return to(
|
|
5576
|
+
{ phase: "starting", since: state.since, run: run4 },
|
|
5577
|
+
[
|
|
5578
|
+
{ type: "spawn", run: run4 },
|
|
5579
|
+
{ type: "arm-timers", runId: run4.id },
|
|
5580
|
+
{ type: "persist-resume", resume: { by: state.by } }
|
|
5581
|
+
],
|
|
5582
|
+
{ lastRunId: run4.id }
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
case "external":
|
|
5586
|
+
return to({ phase: "external", origin: r.origin }, [{ type: "attach", origin: r.origin }]);
|
|
5587
|
+
case "blocked":
|
|
5588
|
+
return to({ phase: "blocked", command: r.command, missing: r.missing });
|
|
5589
|
+
case "undecided":
|
|
5590
|
+
return to({ phase: "needs-command", reason: r.reason, serverScripts: r.serverScripts }, [{ type: "ask-agent" }]);
|
|
5591
|
+
}
|
|
5592
|
+
return same();
|
|
5593
|
+
}
|
|
5594
|
+
case "origins": {
|
|
5595
|
+
if (!current(event.runId) || !event.origins.length) return same();
|
|
5596
|
+
if (state.phase === "starting") {
|
|
5597
|
+
const origin = event.origins[0];
|
|
5598
|
+
return to({ phase: "running", run: state.run, origin, origins: event.origins }, [{ type: "attach", origin }]);
|
|
5599
|
+
}
|
|
5600
|
+
if (state.phase === "running" && event.origins.length > state.origins.length) {
|
|
5601
|
+
return to({ ...state, origins: event.origins });
|
|
5602
|
+
}
|
|
5603
|
+
return same();
|
|
5604
|
+
}
|
|
5605
|
+
case "scan-due":
|
|
5606
|
+
return state.phase === "starting" && current(event.runId) ? same([{ type: "scan-ports", runId: event.runId }]) : same();
|
|
5607
|
+
case "port-found":
|
|
5608
|
+
if (state.phase !== "starting" || !current(event.runId)) return same();
|
|
5609
|
+
return to({ phase: "running", run: state.run, origin: event.origin, origins: [event.origin] }, [
|
|
5610
|
+
{ type: "attach", origin: event.origin }
|
|
5611
|
+
]);
|
|
5612
|
+
case "ready-timeout": {
|
|
5613
|
+
if (state.phase !== "starting" || !current(event.runId)) return same();
|
|
5614
|
+
const crashed = crash(state.run, "no-url", event.tail);
|
|
5615
|
+
return { ...crashed, effects: [{ type: "kill", runId: event.runId }, ...crashed.effects] };
|
|
5616
|
+
}
|
|
5617
|
+
case "exited": {
|
|
5618
|
+
if (!current(event.runId)) return same();
|
|
5619
|
+
if (state.phase === "stopping") {
|
|
5620
|
+
return state.then.kind === "stopped" ? to({ phase: "stopped", command: state.run.command }) : resolveAfresh(state.then.since, state.then.by);
|
|
5621
|
+
}
|
|
5622
|
+
if (state.phase === "running" && event.code === 0) {
|
|
5623
|
+
return same([{ type: "confirm-external", runId: event.runId, origin: state.origin, tail: event.tail }]);
|
|
5624
|
+
}
|
|
5625
|
+
return crash(liveRun(state), "exited", event.tail, { code: event.code, signal: event.signal });
|
|
5626
|
+
}
|
|
5627
|
+
case "external-confirmed":
|
|
5628
|
+
if (state.phase !== "running" || !current(event.runId)) return same();
|
|
5629
|
+
return to({ phase: "external", origin: event.origin }, [{ type: "attach", origin: event.origin }, NO_RESUME]);
|
|
5630
|
+
case "external-gone":
|
|
5631
|
+
if (state.phase !== "running" || !current(event.runId)) return same();
|
|
5632
|
+
return crash(state.run, "exited", event.tail, { code: 0 });
|
|
5633
|
+
case "spawn-failed":
|
|
5634
|
+
if (state.phase !== "starting" || !current(event.runId)) return same();
|
|
5635
|
+
return crash(state.run, "spawn", [event.error]);
|
|
5636
|
+
case "ask-again":
|
|
5637
|
+
if (state.phase !== "needs-command" || state.proposal?.state === "asking") return same();
|
|
5638
|
+
return to({ phase: "needs-command", reason: state.reason, serverScripts: state.serverScripts }, [{ type: "ask-agent" }]);
|
|
5639
|
+
case "proposal":
|
|
5640
|
+
return state.phase === "needs-command" ? to({ ...state, proposal: event.proposal }) : same();
|
|
5641
|
+
case "command-forgotten":
|
|
5642
|
+
return state.phase === "blocked" || state.phase === "crashed" || state.phase === "stopped" ? to({ phase: "stopped" }) : same();
|
|
5643
|
+
case "origin-picked":
|
|
5644
|
+
if (state.phase !== "running" || state.origin === event.origin || !state.origins.includes(event.origin)) return same();
|
|
5645
|
+
return to({ ...state, origin: event.origin }, [{ type: "attach", origin: event.origin }]);
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
var NO_RESUME = { type: "persist-resume", resume: null };
|
|
5649
|
+
var RESTARTABLE = /* @__PURE__ */ new Set(["idle", "stopped", "crashed", "blocked"]);
|
|
5650
|
+
function liveRun(state) {
|
|
5651
|
+
return state.phase === "starting" || state.phase === "running" || state.phase === "stopping" ? state.run : null;
|
|
5652
|
+
}
|
|
5653
|
+
function detachShown(state) {
|
|
5654
|
+
return state.phase === "running" ? [{ type: "detach", origin: state.origin }] : [];
|
|
5655
|
+
}
|
|
5656
|
+
var commandWire = (command) => ({ line: command.line, cwd: command.cwd, source: command.source });
|
|
5657
|
+
function toWire(state) {
|
|
5658
|
+
switch (state.phase) {
|
|
5659
|
+
case "idle":
|
|
5660
|
+
return { phase: "idle" };
|
|
5661
|
+
case "resolving":
|
|
5662
|
+
return { phase: "starting", since: state.since };
|
|
5663
|
+
case "starting":
|
|
5664
|
+
return { phase: "starting", command: commandWire(state.run.command), since: state.since };
|
|
5665
|
+
case "running":
|
|
5666
|
+
return {
|
|
5667
|
+
phase: "running",
|
|
5668
|
+
command: commandWire(state.run.command),
|
|
5669
|
+
origin: state.origin,
|
|
5670
|
+
origins: [...state.origins],
|
|
5671
|
+
since: state.run.startedAt
|
|
5672
|
+
};
|
|
5673
|
+
case "stopping":
|
|
5674
|
+
return state.then.kind === "restart" ? { phase: "starting", command: commandWire(state.run.command), since: state.then.since } : { phase: "stopped", command: commandWire(state.run.command) };
|
|
5675
|
+
case "external":
|
|
5676
|
+
return { phase: "external", origin: state.origin };
|
|
5677
|
+
case "needs-command":
|
|
5678
|
+
return {
|
|
5679
|
+
phase: "needs-command",
|
|
5680
|
+
reason: state.reason,
|
|
5681
|
+
serverScripts: [...state.serverScripts],
|
|
5682
|
+
...state.proposal ? { proposal: state.proposal } : {}
|
|
5683
|
+
};
|
|
5684
|
+
case "blocked":
|
|
5685
|
+
return { phase: "blocked", command: commandWire(state.command), missing: [...state.missing] };
|
|
5686
|
+
case "stopped":
|
|
5687
|
+
return { phase: "stopped", ...state.command ? { command: commandWire(state.command) } : {} };
|
|
5688
|
+
case "crashed":
|
|
5689
|
+
return {
|
|
5690
|
+
phase: "crashed",
|
|
5691
|
+
command: commandWire(state.command),
|
|
5692
|
+
cause: state.cause,
|
|
5693
|
+
...state.exitCode !== void 0 ? { exitCode: state.exitCode } : {},
|
|
5694
|
+
...state.signal ? { signal: state.signal } : {},
|
|
5695
|
+
tail: [...state.tail]
|
|
5696
|
+
};
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
|
|
5700
|
+
// src/dev-server/process.ts
|
|
5701
|
+
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
5702
|
+
import { promisify } from "node:util";
|
|
5703
|
+
|
|
5704
|
+
// src/dev-server/urls.ts
|
|
5705
|
+
var ANSI = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
|
|
5706
|
+
function stripAnsi(text) {
|
|
5707
|
+
return text.replace(ANSI, "");
|
|
5708
|
+
}
|
|
5709
|
+
var URL_IN_TEXT = /\bhttps?:\/\/[^\s'"<>`)]+/g;
|
|
5710
|
+
var LOCAL_HOSTS = {
|
|
5711
|
+
localhost: "localhost",
|
|
5712
|
+
"127.0.0.1": "127.0.0.1",
|
|
5713
|
+
"[::1]": "[::1]",
|
|
5714
|
+
"0.0.0.0": "localhost",
|
|
5715
|
+
"[::]": "localhost"
|
|
5716
|
+
};
|
|
5717
|
+
function localOriginsIn(line2) {
|
|
5718
|
+
const found = [];
|
|
5719
|
+
for (const [raw] of stripAnsi(line2).matchAll(URL_IN_TEXT)) {
|
|
5720
|
+
let url;
|
|
5721
|
+
try {
|
|
5722
|
+
url = new URL(raw.replace(/[.,;:!?\]}]+$/, ""));
|
|
5723
|
+
} catch {
|
|
5724
|
+
continue;
|
|
5725
|
+
}
|
|
5726
|
+
const host = LOCAL_HOSTS[url.hostname];
|
|
5727
|
+
if (!host) continue;
|
|
5728
|
+
found.push(`${url.protocol}//${host}${url.port ? `:${url.port}` : ""}`);
|
|
5729
|
+
}
|
|
5730
|
+
return found;
|
|
5731
|
+
}
|
|
5732
|
+
var OriginScanner = class {
|
|
5733
|
+
partial = "";
|
|
5734
|
+
seen = [];
|
|
5735
|
+
push(chunk) {
|
|
5736
|
+
const text = this.partial + chunk;
|
|
5737
|
+
const parts2 = text.split(/\r?\n/);
|
|
5738
|
+
this.partial = parts2.pop() ?? "";
|
|
5739
|
+
return parts2.map((line2) => this.take(line2));
|
|
5740
|
+
}
|
|
5741
|
+
flush() {
|
|
5742
|
+
const rest = this.partial;
|
|
5743
|
+
this.partial = "";
|
|
5744
|
+
return rest ? [this.take(rest)] : [];
|
|
5745
|
+
}
|
|
5746
|
+
get origins() {
|
|
5747
|
+
return this.seen;
|
|
5748
|
+
}
|
|
5749
|
+
take(line2) {
|
|
5750
|
+
const visible = stripAnsi(line2).split("\r").at(-1) ?? "";
|
|
5751
|
+
for (const origin of localOriginsIn(visible)) if (!this.seen.includes(origin)) this.seen.push(origin);
|
|
5752
|
+
return visible;
|
|
5753
|
+
}
|
|
5754
|
+
};
|
|
5755
|
+
function parseListeningPorts(lsofOutput) {
|
|
5756
|
+
const ports = /* @__PURE__ */ new Set();
|
|
5757
|
+
for (const line2 of lsofOutput.split("\n")) {
|
|
5758
|
+
if (!line2.startsWith("n")) continue;
|
|
5759
|
+
const port = Number(line2.slice(line2.lastIndexOf(":") + 1));
|
|
5760
|
+
if (Number.isInteger(port) && port > 0 && port < 65536) ports.add(port);
|
|
5761
|
+
}
|
|
5762
|
+
return [...ports].sort((a, b) => a - b);
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
// src/dev-server/process.ts
|
|
5766
|
+
var DEV_SERVER_ENV = { BROWSER: "none" };
|
|
5767
|
+
var ManagedProcess = class _ManagedProcess {
|
|
5768
|
+
constructor(pid) {
|
|
5769
|
+
this.pid = pid;
|
|
5770
|
+
this.closed = new Promise((resolve7) => this.markClosed = resolve7);
|
|
5771
|
+
}
|
|
5772
|
+
scanner = new OriginScanner();
|
|
5773
|
+
exitedAt = null;
|
|
5774
|
+
/** Settles once the leader has exited and its pipes are drained. */
|
|
5775
|
+
closed;
|
|
5776
|
+
markClosed;
|
|
5777
|
+
/** Never throws: a shell that cannot start reports through `spawnFailed`. */
|
|
5778
|
+
static launch(spec, log, listener) {
|
|
5779
|
+
let child;
|
|
5780
|
+
try {
|
|
5781
|
+
child = spawn5(spec.line, {
|
|
5782
|
+
shell: true,
|
|
5783
|
+
detached: true,
|
|
5784
|
+
cwd: spec.absoluteCwd,
|
|
5785
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5786
|
+
env: { ...process.env, ...DEV_SERVER_ENV }
|
|
5787
|
+
});
|
|
5788
|
+
} catch (error) {
|
|
5789
|
+
const proc2 = new _ManagedProcess(void 0);
|
|
5790
|
+
proc2.markClosed();
|
|
5791
|
+
queueMicrotask(() => listener.spawnFailed(error instanceof Error ? error.message : String(error)));
|
|
5792
|
+
return proc2;
|
|
5793
|
+
}
|
|
5794
|
+
const proc = new _ManagedProcess(child.pid);
|
|
5795
|
+
const pid = child.pid;
|
|
5796
|
+
if (pid) trackGroup(pid, spec.line);
|
|
5797
|
+
const take = (chunk) => {
|
|
5798
|
+
const lines = proc.scanner.push(chunk.toString());
|
|
5799
|
+
if (!lines.length) return;
|
|
5800
|
+
log.push(lines);
|
|
5801
|
+
listener.output(lines, proc.scanner.origins);
|
|
5802
|
+
};
|
|
5803
|
+
child.stdout?.on("data", take);
|
|
5804
|
+
child.stderr?.on("data", take);
|
|
5805
|
+
child.on("exit", () => {
|
|
5806
|
+
if (!pid) return;
|
|
5807
|
+
try {
|
|
5808
|
+
process.kill(-pid, "SIGTERM");
|
|
5809
|
+
} catch {
|
|
5810
|
+
}
|
|
5811
|
+
});
|
|
5812
|
+
child.on("error", (error) => {
|
|
5813
|
+
if (pid) untrackGroup(pid);
|
|
5814
|
+
if (proc.exitedAt !== null) return;
|
|
5815
|
+
proc.exitedAt = Date.now();
|
|
5816
|
+
proc.markClosed();
|
|
5817
|
+
listener.spawnFailed(error.message);
|
|
5818
|
+
});
|
|
5819
|
+
child.on("close", (code, signal) => {
|
|
5820
|
+
if (pid) untrackGroup(pid);
|
|
5821
|
+
if (proc.exitedAt !== null) return;
|
|
5822
|
+
const rest = proc.scanner.flush();
|
|
5823
|
+
if (rest.length) {
|
|
5824
|
+
log.push(rest);
|
|
5825
|
+
listener.output(rest, proc.scanner.origins);
|
|
5826
|
+
}
|
|
5827
|
+
proc.exitedAt = Date.now();
|
|
5828
|
+
proc.markClosed();
|
|
5829
|
+
listener.exited(code, signal);
|
|
5830
|
+
});
|
|
5831
|
+
return proc;
|
|
5832
|
+
}
|
|
5833
|
+
get running() {
|
|
5834
|
+
return this.pid !== void 0 && this.exitedAt === null;
|
|
5835
|
+
}
|
|
5836
|
+
async stop(graceMs) {
|
|
5837
|
+
const pid = this.pid;
|
|
5838
|
+
if (!this.running || pid === void 0) return;
|
|
5839
|
+
killGroup(pid, graceMs > 0 ? "SIGTERM" : "SIGKILL");
|
|
5840
|
+
const hard = setTimeout(() => killGroup(pid, "SIGKILL"), graceMs);
|
|
5841
|
+
hard.unref();
|
|
5842
|
+
await this.closed;
|
|
5843
|
+
clearTimeout(hard);
|
|
5844
|
+
}
|
|
5845
|
+
async listeningOrigins() {
|
|
5846
|
+
if (!this.running || this.pid === void 0) return [];
|
|
5847
|
+
const pids = (await run2("pgrep", ["-g", String(this.pid)])).split(/\s+/).filter(Boolean);
|
|
5848
|
+
if (!pids.length) return [];
|
|
5849
|
+
const listening = await run2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(","), "-F", "pn"]);
|
|
5850
|
+
return parseListeningPorts(listening).map((port) => `http://localhost:${port}`);
|
|
5851
|
+
}
|
|
5852
|
+
};
|
|
5853
|
+
var execFileAsync = promisify(execFile);
|
|
5854
|
+
async function run2(file, args) {
|
|
5855
|
+
try {
|
|
5856
|
+
return (await execFileAsync(file, args, { encoding: "utf8", timeout: 5e3 })).stdout;
|
|
5857
|
+
} catch {
|
|
5858
|
+
return "";
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
var LogRing = class {
|
|
5862
|
+
constructor(cap = DEV_SERVER_LOG_LINES) {
|
|
5863
|
+
this.cap = cap;
|
|
5864
|
+
}
|
|
5865
|
+
lines = [];
|
|
5866
|
+
dropped = 0;
|
|
5867
|
+
push(lines) {
|
|
5868
|
+
this.lines.push(...lines);
|
|
5869
|
+
const over = this.lines.length - this.cap;
|
|
5870
|
+
if (over > 0) {
|
|
5871
|
+
this.lines.splice(0, over);
|
|
5872
|
+
this.dropped += over;
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
tail(count2) {
|
|
5876
|
+
return this.lines.slice(-count2);
|
|
5877
|
+
}
|
|
5878
|
+
clear() {
|
|
5879
|
+
this.lines = [];
|
|
5880
|
+
this.dropped = 0;
|
|
5881
|
+
}
|
|
5882
|
+
wire() {
|
|
5883
|
+
return { lines: [...this.lines], dropped: this.dropped };
|
|
5884
|
+
}
|
|
5885
|
+
};
|
|
5886
|
+
async function originAnswers(origin, timeoutMs = 1500) {
|
|
5887
|
+
try {
|
|
5888
|
+
const res = await fetch(origin, { redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
|
|
5889
|
+
await res.body?.cancel();
|
|
5890
|
+
return true;
|
|
5891
|
+
} catch {
|
|
5892
|
+
return false;
|
|
5893
|
+
}
|
|
5894
|
+
}
|
|
5895
|
+
async function listenerCwd(origin) {
|
|
5896
|
+
let port;
|
|
5897
|
+
try {
|
|
5898
|
+
const url = new URL(origin);
|
|
5899
|
+
port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
5900
|
+
} catch {
|
|
5901
|
+
return { kind: "unknown" };
|
|
5902
|
+
}
|
|
5903
|
+
const pid = (await run2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"])).split(/\s+/).find(Boolean);
|
|
5904
|
+
if (!pid) return { kind: "unknown" };
|
|
5905
|
+
const cwd = (await run2("lsof", ["-a", "-p", pid, "-d", "cwd", "-Fn"])).split("\n").find((l) => l.startsWith("n"));
|
|
5906
|
+
return cwd ? { kind: "known", path: cwd.slice(1) } : { kind: "unknown" };
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5909
|
+
// src/dev-server/proposal.ts
|
|
5910
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
5911
|
+
import { mkdirSync as mkdirSync11, realpathSync, statSync as statSync5 } from "node:fs";
|
|
5912
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5913
|
+
var PROPOSAL_TIMEOUT_MS = 18e4;
|
|
5914
|
+
var MAX_LINE = 500;
|
|
5915
|
+
async function askAgentForCommand(req) {
|
|
5916
|
+
try {
|
|
5917
|
+
return await askOnce(req);
|
|
5918
|
+
} catch (error) {
|
|
5919
|
+
return { kind: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
function askOnce(req) {
|
|
5923
|
+
mkdirSync11(ACP_NPM_PREFIX, { recursive: true });
|
|
5924
|
+
return new Promise((settle) => {
|
|
5925
|
+
let asked = false;
|
|
5926
|
+
let done = false;
|
|
5927
|
+
const finish = (outcome) => {
|
|
5928
|
+
if (done) return;
|
|
5929
|
+
done = true;
|
|
5930
|
+
clearTimeout(timer);
|
|
5931
|
+
req.signal.removeEventListener("abort", aborted);
|
|
5932
|
+
agent.stop();
|
|
5933
|
+
settle(outcome);
|
|
5934
|
+
};
|
|
5935
|
+
const agent = new AcpAgent({
|
|
5936
|
+
command: req.agent,
|
|
5937
|
+
launch: launchCommand(req.agent, ACP_NPM_PREFIX),
|
|
5938
|
+
cwd: req.project,
|
|
5939
|
+
onChange: () => {
|
|
5940
|
+
const snap = agent.snapshot();
|
|
5941
|
+
if (snap.ask) agent.decline(snap.ask.id);
|
|
5942
|
+
if (snap.state === "idle" && !asked) {
|
|
5943
|
+
asked = true;
|
|
5944
|
+
agent.prompt(proposalPrompt(req));
|
|
5945
|
+
}
|
|
5946
|
+
},
|
|
5947
|
+
onTurnEnd: (reply) => finish(fromReply(reply, req)),
|
|
5948
|
+
onExit: (error) => finish({ kind: "failed", error: error ?? "the agent exited before it answered" })
|
|
5949
|
+
});
|
|
5950
|
+
const timer = setTimeout(() => finish({ kind: "failed", error: "the agent did not answer in time" }), PROPOSAL_TIMEOUT_MS);
|
|
5951
|
+
const aborted = () => finish({ kind: "failed", error: "no longer needed" });
|
|
5952
|
+
req.signal.addEventListener("abort", aborted, { once: true });
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
var REASONS = {
|
|
5956
|
+
"no-package-json": "There is no package.json at the project root.",
|
|
5957
|
+
"no-script": 'package.json has neither a "dev" nor a "start" script.',
|
|
5958
|
+
"monorepo-root": 'This is a workspace root with neither a "dev" nor a "start" script.'
|
|
5959
|
+
};
|
|
5960
|
+
function proposingAgent(preferred, installed) {
|
|
5961
|
+
const usable = (command) => {
|
|
5962
|
+
const profile = agentProfileFor(command);
|
|
5963
|
+
return !profile || installed(profile);
|
|
5964
|
+
};
|
|
5965
|
+
return [...preferred, ...AGENT_PROFILES.map((p) => p.command)].find((c) => !!c && usable(c)) ?? null;
|
|
5966
|
+
}
|
|
5967
|
+
function proposalPrompt(req) {
|
|
5968
|
+
const scripts = Object.entries(req.facts.packageJson?.scripts ?? {});
|
|
5969
|
+
return [
|
|
5970
|
+
"ezdesign wants to start this project's local web dev server so the user can review the page.",
|
|
5971
|
+
`Please propose the command that starts this project's dev server, from ${req.project}.`,
|
|
5972
|
+
"",
|
|
5973
|
+
`Its own rules could not decide: ${REASONS[req.undecided.reason]}`,
|
|
5974
|
+
scripts.length ? `Scripts in package.json:
|
|
5975
|
+
${scripts.map(([name, line2]) => `- ${name}: ${line2}`).join("\n")}` : "",
|
|
5976
|
+
req.facts.lockfiles.length ? `Lockfiles: ${req.facts.lockfiles.join(", ")}` : "No lockfile.",
|
|
5977
|
+
"",
|
|
5978
|
+
"Read files to decide; do not run anything or change anything - every such request will be declined.",
|
|
5979
|
+
"Choose the one web app a designer would want to see, and the command that serves it with live reload.",
|
|
5980
|
+
"End your answer with exactly one fenced json block:",
|
|
5981
|
+
"```json",
|
|
5982
|
+
'{"command": "pnpm --filter web dev", "cwd": ".", "reason": "one sentence"}',
|
|
5983
|
+
"```",
|
|
5984
|
+
"`cwd` is relative to the project root. If nothing here serves a web page, answer",
|
|
5985
|
+
'`{"command": null, "reason": "one sentence"}` instead.'
|
|
5986
|
+
].filter((line2, i, all) => line2 || all[i - 1]).join("\n");
|
|
5987
|
+
}
|
|
5988
|
+
function parseProposalReply(reply) {
|
|
5989
|
+
const blocks = [...reply.matchAll(/```(?:json)?\s*\n([\s\S]*?)```/g)];
|
|
5990
|
+
const last = blocks.at(-1)?.[1];
|
|
5991
|
+
if (!last) return null;
|
|
5992
|
+
let parsed;
|
|
5993
|
+
try {
|
|
5994
|
+
parsed = JSON.parse(last);
|
|
5995
|
+
} catch {
|
|
5996
|
+
return null;
|
|
5997
|
+
}
|
|
5998
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
5999
|
+
const { command, cwd, reason } = parsed;
|
|
6000
|
+
const why = typeof reason === "string" ? reason.slice(0, MAX_LINE) : "";
|
|
6001
|
+
if (command === null) return { kind: "none", reason: why };
|
|
6002
|
+
if (typeof command !== "string") return null;
|
|
6003
|
+
const line2 = command.trim();
|
|
6004
|
+
if (!line2 || line2.length > MAX_LINE || /[\0\r\n]/.test(line2)) return null;
|
|
6005
|
+
const dir = typeof cwd === "string" && cwd.trim() ? cwd.trim() : ".";
|
|
6006
|
+
if (isAbsolute(dir)) return null;
|
|
6007
|
+
return { kind: "command", line: line2, cwd: dir, reason: why };
|
|
6008
|
+
}
|
|
6009
|
+
function fromReply(reply, req) {
|
|
6010
|
+
const parsed = parseProposalReply(reply);
|
|
6011
|
+
if (!parsed) return { kind: "failed", error: "the agent did not answer with a command" };
|
|
6012
|
+
if (parsed.kind === "none") return { kind: "none-found", reason: parsed.reason };
|
|
6013
|
+
const cwd = projectRelativePath(req.project, parsed.cwd);
|
|
6014
|
+
if (cwd === null) return { kind: "failed", error: `the agent named a folder outside the project: ${parsed.cwd}` };
|
|
6015
|
+
return {
|
|
6016
|
+
kind: "proposed",
|
|
6017
|
+
proposal: { id: randomBytes6(8).toString("hex"), agent: req.agent, line: parsed.line, cwd, reason: parsed.reason }
|
|
6018
|
+
};
|
|
6019
|
+
}
|
|
6020
|
+
function projectRelativePath(project, cwd) {
|
|
6021
|
+
if (isAbsolute(cwd)) return null;
|
|
6022
|
+
try {
|
|
6023
|
+
const root = realpathSync(project);
|
|
6024
|
+
const target = realpathSync(resolve(root, cwd));
|
|
6025
|
+
if (!statSync5(target).isDirectory()) return null;
|
|
6026
|
+
const rel = relative(root, target);
|
|
6027
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep) && !isAbsolute(rel) ? rel : null;
|
|
6028
|
+
} catch {
|
|
6029
|
+
return null;
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
// src/dev-server/routes.ts
|
|
6034
|
+
import { Router as Router2 } from "express";
|
|
6035
|
+
function initiator(req) {
|
|
6036
|
+
return req.body?.host === "desktop" ? "desktop" : "browser";
|
|
6037
|
+
}
|
|
6038
|
+
var field = (req, name) => req.body?.[name];
|
|
6039
|
+
function devServerApiRouter(devServer) {
|
|
6040
|
+
const router = Router2();
|
|
6041
|
+
router.get("/dev-server/log", (_req, res) => {
|
|
6042
|
+
res.set("cache-control", "no-store").json(devServer.logWire());
|
|
6043
|
+
});
|
|
6044
|
+
router.post("/dev-server/start", (req, res) => {
|
|
6045
|
+
devServer.ensure(initiator(req));
|
|
6046
|
+
res.status(202).json({ ok: true });
|
|
6047
|
+
});
|
|
6048
|
+
router.post("/dev-server/restart", (req, res) => {
|
|
6049
|
+
devServer.restart(initiator(req));
|
|
6050
|
+
res.status(202).json({ ok: true });
|
|
6051
|
+
});
|
|
6052
|
+
router.post("/dev-server/stop", async (_req, res) => {
|
|
6053
|
+
if (devServer.external) return void res.status(409).json({ error: "this dev server was not started here" });
|
|
6054
|
+
await devServer.stop();
|
|
6055
|
+
res.json({ ok: true });
|
|
6056
|
+
});
|
|
6057
|
+
router.post("/dev-server/command", (req, res) => {
|
|
6058
|
+
const line2 = field(req, "line");
|
|
6059
|
+
const cwd = field(req, "cwd") ?? "";
|
|
6060
|
+
if (typeof line2 !== "string" || typeof cwd !== "string") {
|
|
6061
|
+
return void res.status(400).json({ error: "line and cwd must be strings" });
|
|
6062
|
+
}
|
|
6063
|
+
const proposalId = field(req, "proposalId");
|
|
6064
|
+
const choice = { line: line2, cwd, ...typeof proposalId === "string" ? { proposalId } : {} };
|
|
6065
|
+
const outcome = devServer.saveCommandAndRun(choice, initiator(req));
|
|
6066
|
+
if (outcome === "invalid-line") return void res.status(400).json({ error: "the command must be one line of 1-500 characters" });
|
|
6067
|
+
if (outcome === "invalid-cwd") return void res.status(400).json({ error: "the folder must be inside the project" });
|
|
6068
|
+
res.status(202).json({ ok: true });
|
|
6069
|
+
});
|
|
6070
|
+
router.delete("/dev-server/command", (_req, res) => {
|
|
6071
|
+
devServer.forgetCommand();
|
|
6072
|
+
res.json({ ok: true });
|
|
6073
|
+
});
|
|
6074
|
+
router.post("/dev-server/propose", (_req, res) => {
|
|
6075
|
+
devServer.askAgentAgain();
|
|
6076
|
+
res.status(202).json({ ok: true });
|
|
6077
|
+
});
|
|
6078
|
+
router.post("/dev-server/origin", (req, res) => {
|
|
6079
|
+
const origin = field(req, "origin");
|
|
6080
|
+
if (typeof origin !== "string" || !devServer.pickOrigin(origin)) {
|
|
6081
|
+
return void res.status(404).json({ error: "not an origin this dev server printed" });
|
|
6082
|
+
}
|
|
6083
|
+
res.json({ ok: true });
|
|
6084
|
+
});
|
|
6085
|
+
return router;
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
// src/dev-server/index.ts
|
|
6089
|
+
function isTheAgentsProposal(offered, proposalId, line2, cwd) {
|
|
6090
|
+
return !!offered && proposalId === offered.id && offered.line === line2 && offered.cwd === cwd;
|
|
6091
|
+
}
|
|
6092
|
+
var CRASH_TAIL_LINES = 30;
|
|
6093
|
+
var PORT_SCAN_EVERY_MS = 1e3;
|
|
6094
|
+
var MAX_LINE2 = 500;
|
|
6095
|
+
var DevServer = class {
|
|
6096
|
+
constructor(deps) {
|
|
6097
|
+
this.deps = deps;
|
|
6098
|
+
}
|
|
6099
|
+
machine = INITIAL;
|
|
6100
|
+
live = null;
|
|
6101
|
+
log = new LogRing();
|
|
6102
|
+
proposal = null;
|
|
6103
|
+
startPreview = null;
|
|
6104
|
+
snapshot() {
|
|
6105
|
+
const wire = toWire(this.machine.state);
|
|
6106
|
+
if (wire.phase !== "idle" && wire.phase !== "stopped" || wire.command) return wire;
|
|
6107
|
+
const { command } = this.upcoming();
|
|
6108
|
+
return command ? { ...wire, command } : wire;
|
|
6109
|
+
}
|
|
6110
|
+
summary() {
|
|
6111
|
+
const wire = toWire(this.machine.state);
|
|
6112
|
+
return "origin" in wire ? { phase: wire.phase, origin: wire.origin } : { phase: wire.phase };
|
|
6113
|
+
}
|
|
6114
|
+
offersApp() {
|
|
6115
|
+
return this.machine.state.phase !== "idle" || this.upcoming().runnable;
|
|
6116
|
+
}
|
|
6117
|
+
logWire() {
|
|
6118
|
+
return this.log.wire();
|
|
6119
|
+
}
|
|
6120
|
+
/** Idempotent: a no-op while starting, running or external; from anywhere
|
|
6121
|
+
* else it reuses (D7), runs, or waits for a command. Never throws and never
|
|
6122
|
+
* waits - progress arrives through `onChange`. */
|
|
6123
|
+
ensure(by) {
|
|
6124
|
+
this.dispatch({ type: "ensure", now: Date.now(), by });
|
|
6125
|
+
}
|
|
6126
|
+
restart(by) {
|
|
6127
|
+
this.dispatch({ type: "restart", now: Date.now(), by });
|
|
6128
|
+
}
|
|
6129
|
+
get managedBy() {
|
|
6130
|
+
const state = this.machine.state;
|
|
6131
|
+
return state.phase === "starting" || state.phase === "running" || state.phase === "stopping" ? state.run.by : null;
|
|
6132
|
+
}
|
|
6133
|
+
get external() {
|
|
6134
|
+
return this.machine.state.phase === "external";
|
|
6135
|
+
}
|
|
6136
|
+
/** The daemon is exiting: the group goes, gracefully, and what the record
|
|
6137
|
+
* says to resume stays for the next daemon. */
|
|
6138
|
+
async release() {
|
|
6139
|
+
if (this.live?.process.running) await this.live.process.stop(DEV_SERVER_STOP_GRACE_MS);
|
|
6140
|
+
}
|
|
6141
|
+
/** Resolves once the process group is gone (or at once when there was none).
|
|
6142
|
+
* A server this daemon did not start is left alone (D7). */
|
|
6143
|
+
async stop() {
|
|
6144
|
+
const live3 = this.live;
|
|
6145
|
+
this.dispatch({ type: "stop" });
|
|
6146
|
+
if (live3?.process.running) await live3.process.closed;
|
|
6147
|
+
}
|
|
6148
|
+
saveCommandAndRun(choice, by) {
|
|
6149
|
+
const line2 = choice.line.trim();
|
|
6150
|
+
if (!line2 || line2.length > MAX_LINE2 || /[\0\r\n]/.test(line2)) return "invalid-line";
|
|
6151
|
+
const cwd = projectRelativePath(this.deps.project, choice.cwd.trim() || ".");
|
|
6152
|
+
if (cwd === null) return "invalid-cwd";
|
|
6153
|
+
const source = isTheAgentsProposal(this.proposal?.live, choice.proposalId, line2, cwd) ? "agent" : "user";
|
|
6154
|
+
this.writeRecord({ ...this.readRecord(), command: { line: line2, cwd, source, decidedAt: Date.now() } });
|
|
6155
|
+
this.dispatch({ type: "confirmed", now: Date.now(), by });
|
|
6156
|
+
return "started";
|
|
6157
|
+
}
|
|
6158
|
+
askAgentAgain() {
|
|
6159
|
+
this.dispatch({ type: "ask-again" });
|
|
6160
|
+
}
|
|
6161
|
+
forgetCommand() {
|
|
6162
|
+
const { command: _, ...rest } = this.readRecord();
|
|
6163
|
+
this.writeRecord(rest);
|
|
6164
|
+
this.startPreview = null;
|
|
6165
|
+
this.dispatch({ type: "command-forgotten" });
|
|
6166
|
+
this.deps.onChange();
|
|
6167
|
+
}
|
|
6168
|
+
pickOrigin(origin) {
|
|
6169
|
+
const state = this.machine.state;
|
|
6170
|
+
if (state.phase !== "running" || !state.origins.includes(origin)) return false;
|
|
6171
|
+
this.dispatch({ type: "origin-picked", origin });
|
|
6172
|
+
return true;
|
|
6173
|
+
}
|
|
6174
|
+
resume() {
|
|
6175
|
+
const resume = this.readRecord().resume;
|
|
6176
|
+
if (resume) this.ensure(resume.by);
|
|
6177
|
+
}
|
|
6178
|
+
forgetResume() {
|
|
6179
|
+
const record = this.readRecord();
|
|
6180
|
+
if (!record.resume) return;
|
|
6181
|
+
const { resume: _, ...rest } = record;
|
|
6182
|
+
this.writeRecord(rest);
|
|
6183
|
+
}
|
|
6184
|
+
dispatch(event) {
|
|
6185
|
+
const { machine, effects, stateChanged } = step(this.machine, event);
|
|
6186
|
+
if (this.proposal && machine.state.phase !== "needs-command") {
|
|
6187
|
+
this.proposal.abort.abort();
|
|
6188
|
+
this.proposal = null;
|
|
6189
|
+
}
|
|
6190
|
+
this.machine = machine;
|
|
6191
|
+
if (stateChanged) this.startPreview = null;
|
|
6192
|
+
for (const effect of effects) this.perform(effect);
|
|
6193
|
+
if (stateChanged) this.deps.onChange();
|
|
6194
|
+
}
|
|
6195
|
+
dispatchAfterThisStep(event) {
|
|
6196
|
+
queueMicrotask(() => this.dispatch(event));
|
|
6197
|
+
}
|
|
6198
|
+
reportAlreadyExited(runId) {
|
|
6199
|
+
this.dispatchAfterThisStep({ type: "exited", runId, code: null, signal: null, tail: this.log.tail(CRASH_TAIL_LINES) });
|
|
6200
|
+
}
|
|
6201
|
+
perform(effect) {
|
|
6202
|
+
switch (effect.type) {
|
|
6203
|
+
case "resolve": {
|
|
6204
|
+
const { attempt } = effect;
|
|
6205
|
+
void this.resolve().then((resolution) => this.dispatch({ type: "resolved", attempt, resolution, now: Date.now() }));
|
|
6206
|
+
return;
|
|
6207
|
+
}
|
|
6208
|
+
case "spawn": {
|
|
6209
|
+
const { run: run4 } = effect;
|
|
6210
|
+
const runId = run4.id;
|
|
6211
|
+
this.log.clear();
|
|
6212
|
+
this.log.push([`$ ${run4.command.line}`]);
|
|
6213
|
+
const spec = { line: run4.command.line, absoluteCwd: join15(this.deps.project, run4.command.cwd) };
|
|
6214
|
+
const managed = ManagedProcess.launch(spec, this.log, {
|
|
6215
|
+
output: (_, origins) => this.dispatch({ type: "origins", runId, origins }),
|
|
6216
|
+
exited: (code, signal) => this.dispatch({ type: "exited", runId, code, signal, tail: this.log.tail(CRASH_TAIL_LINES) }),
|
|
6217
|
+
spawnFailed: (error) => this.dispatch({ type: "spawn-failed", runId, error })
|
|
6218
|
+
});
|
|
6219
|
+
this.live = { runId, process: managed };
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
case "arm-timers": {
|
|
6223
|
+
const { runId } = effect;
|
|
6224
|
+
setTimeout(() => this.dispatch({ type: "scan-due", runId }), DEV_SERVER_PORT_SCAN_AFTER_MS).unref();
|
|
6225
|
+
setTimeout(
|
|
6226
|
+
() => this.dispatch({ type: "ready-timeout", runId, tail: this.log.tail(CRASH_TAIL_LINES) }),
|
|
6227
|
+
DEV_SERVER_READY_MS
|
|
6228
|
+
).unref();
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6231
|
+
case "scan-ports": {
|
|
6232
|
+
const { runId } = effect;
|
|
6233
|
+
const managed = this.live?.runId === runId ? this.live.process : null;
|
|
6234
|
+
if (!managed) return;
|
|
6235
|
+
void (async () => {
|
|
6236
|
+
for (const origin of await managed.listeningOrigins()) {
|
|
6237
|
+
if (await originAnswers(origin)) return this.dispatch({ type: "port-found", runId, origin });
|
|
6238
|
+
}
|
|
6239
|
+
setTimeout(() => this.dispatch({ type: "scan-due", runId }), PORT_SCAN_EVERY_MS).unref();
|
|
6240
|
+
})();
|
|
6241
|
+
return;
|
|
6242
|
+
}
|
|
6243
|
+
case "kill": {
|
|
6244
|
+
const { runId } = effect;
|
|
6245
|
+
const managed = this.live?.runId === runId ? this.live.process : null;
|
|
6246
|
+
if (managed?.running) void managed.stop(DEV_SERVER_STOP_GRACE_MS);
|
|
6247
|
+
else this.reportAlreadyExited(runId);
|
|
6248
|
+
return;
|
|
6249
|
+
}
|
|
6250
|
+
case "confirm-external": {
|
|
6251
|
+
const { runId, origin, tail } = effect;
|
|
6252
|
+
void originAnswers(origin).then(
|
|
6253
|
+
(alive) => this.dispatch(alive ? { type: "external-confirmed", runId, origin } : { type: "external-gone", runId, tail })
|
|
6254
|
+
);
|
|
6255
|
+
return;
|
|
6256
|
+
}
|
|
6257
|
+
case "attach":
|
|
6258
|
+
return this.deps.attach(effect.origin);
|
|
6259
|
+
case "detach":
|
|
6260
|
+
return this.deps.detachLeavingRecord(effect.origin);
|
|
6261
|
+
case "ask-agent": {
|
|
6262
|
+
this.proposal?.abort.abort();
|
|
6263
|
+
this.proposal = null;
|
|
6264
|
+
const agent = this.deps.proposingAgent();
|
|
6265
|
+
if (!agent) return;
|
|
6266
|
+
const asking = { live: null, abort: new AbortController() };
|
|
6267
|
+
this.proposal = asking;
|
|
6268
|
+
this.dispatchAfterThisStep({ type: "proposal", proposal: { state: "asking", agent } });
|
|
6269
|
+
const facts = readProjectFacts(this.deps.project);
|
|
6270
|
+
const undecided = detectDevCommand(facts);
|
|
6271
|
+
if (undecided.kind !== "undecided") return;
|
|
6272
|
+
void askAgentForCommand({ agent, project: this.deps.project, facts, undecided, signal: asking.abort.signal }).then(
|
|
6273
|
+
(outcome) => {
|
|
6274
|
+
if (this.proposal !== asking) return;
|
|
6275
|
+
if (outcome.kind === "proposed") asking.live = outcome.proposal;
|
|
6276
|
+
this.dispatch({
|
|
6277
|
+
type: "proposal",
|
|
6278
|
+
proposal: outcome.kind === "proposed" ? { state: "proposed", ...outcome.proposal } : outcome.kind === "none-found" ? { state: "none-found", agent, reason: outcome.reason } : { state: "failed", agent, error: outcome.error }
|
|
6279
|
+
});
|
|
6280
|
+
}
|
|
6281
|
+
);
|
|
6282
|
+
return;
|
|
6283
|
+
}
|
|
6284
|
+
case "persist-resume": {
|
|
6285
|
+
const { resume: _, ...rest } = this.readRecord();
|
|
6286
|
+
this.writeRecord(effect.resume ? { ...rest, resume: effect.resume } : rest);
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
async resolve() {
|
|
6292
|
+
await this.deps.environmentReady();
|
|
6293
|
+
const recorded = this.deps.userAttachedOrigin();
|
|
6294
|
+
if (recorded && !this.deps.heldElsewhere(recorded) && await originAnswers(recorded)) {
|
|
6295
|
+
const cwd = await listenerCwd(recorded);
|
|
6296
|
+
if (cwd.kind === "unknown" || this.contains(cwd.path)) return { kind: "external", origin: recorded };
|
|
6297
|
+
}
|
|
6298
|
+
const stored = this.storedCommand();
|
|
6299
|
+
let command;
|
|
6300
|
+
let tools;
|
|
6301
|
+
if (stored) {
|
|
6302
|
+
command = stored;
|
|
6303
|
+
tools = toolsForLine(stored.line);
|
|
6304
|
+
} else {
|
|
6305
|
+
const detection = detectDevCommand(readProjectFacts(this.deps.project));
|
|
6306
|
+
if (detection.kind === "undecided") {
|
|
6307
|
+
return { kind: "undecided", reason: detection.reason, serverScripts: detection.serverScripts };
|
|
6308
|
+
}
|
|
6309
|
+
command = { line: commandLine(detection.command), cwd: "", source: "detected" };
|
|
6310
|
+
tools = requiredTools(detection.command);
|
|
6311
|
+
}
|
|
6312
|
+
const missing = missingTools(tools, (binary) => findExecutable(binary, process.env.PATH));
|
|
6313
|
+
return missing.length ? { kind: "blocked", command, missing } : { kind: "run", command };
|
|
6314
|
+
}
|
|
6315
|
+
contains(path) {
|
|
6316
|
+
try {
|
|
6317
|
+
return projectRelativePath(this.deps.project, relative2(realpathSync2(this.deps.project), path)) !== null;
|
|
6318
|
+
} catch {
|
|
6319
|
+
return false;
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
upcoming() {
|
|
6323
|
+
if (this.startPreview) return this.startPreview;
|
|
6324
|
+
const stored = this.storedCommand();
|
|
6325
|
+
if (stored) return this.startPreview = { command: stored, runnable: true };
|
|
6326
|
+
const detection = detectDevCommand(readProjectFacts(this.deps.project));
|
|
6327
|
+
this.startPreview = detection.kind === "command" ? { command: { line: commandLine(detection.command), cwd: "", source: "detected" }, runnable: true } : { command: null, runnable: detection.reason === "monorepo-root" || detection.serverScripts.length > 0 };
|
|
6328
|
+
return this.startPreview;
|
|
6329
|
+
}
|
|
6330
|
+
readRecord() {
|
|
6331
|
+
try {
|
|
6332
|
+
return JSON.parse(readFileSync13(join15(this.deps.dir, RECORD_FILE), "utf8"));
|
|
6333
|
+
} catch {
|
|
6334
|
+
return {};
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
writeRecord(record) {
|
|
6338
|
+
writeAtomic(join15(this.deps.dir, RECORD_FILE), `${JSON.stringify(record, null, 2)}
|
|
6339
|
+
`);
|
|
6340
|
+
}
|
|
6341
|
+
storedCommand() {
|
|
6342
|
+
const stored = this.readRecord().command;
|
|
6343
|
+
return stored ? { line: stored.line, cwd: stored.cwd, source: stored.source } : null;
|
|
6344
|
+
}
|
|
6345
|
+
};
|
|
6346
|
+
var RECORD_FILE = "dev-server.json";
|
|
6347
|
+
|
|
6348
|
+
// src/desktop-wire.ts
|
|
6349
|
+
var DESKTOP_CONTRACT = 1;
|
|
6350
|
+
var DESKTOP_HEADER = "x-ezdesign-desktop";
|
|
6351
|
+
|
|
6352
|
+
// src/projects/book.ts
|
|
6353
|
+
import { existsSync as existsSync8, readFileSync as readFileSync14 } from "node:fs";
|
|
6354
|
+
import { basename } from "node:path";
|
|
6355
|
+
var ProjectBook = class {
|
|
6356
|
+
constructor(sessions, file = PROJECTS_FILE) {
|
|
6357
|
+
this.file = file;
|
|
6358
|
+
this.entries = readBook(file);
|
|
6359
|
+
this.withSession = new Map([...sessions].map((s) => [s.project, { createdAt: s.createdAt }]));
|
|
6360
|
+
}
|
|
6361
|
+
entries;
|
|
6362
|
+
withSession;
|
|
6363
|
+
markUsed(project, now) {
|
|
6364
|
+
if (!this.withSession.has(project)) this.withSession.set(project, { createdAt: now });
|
|
6365
|
+
this.update(project, { usedAt: now });
|
|
6366
|
+
}
|
|
6367
|
+
add(project, now) {
|
|
6368
|
+
this.update(project, { addedAt: now });
|
|
6369
|
+
}
|
|
6370
|
+
hide(project, now) {
|
|
6371
|
+
if (this.isListed(project)) this.update(project, { hiddenAt: now });
|
|
6372
|
+
}
|
|
6373
|
+
isListed(project) {
|
|
6374
|
+
return this.entries.has(project) || this.withSession.has(project);
|
|
6375
|
+
}
|
|
6376
|
+
list() {
|
|
6377
|
+
return mergeProjects(this.entries, this.withSession, (project) => existsSync8(project));
|
|
6378
|
+
}
|
|
6379
|
+
update(project, change) {
|
|
6380
|
+
this.entries.set(project, { ...this.entries.get(project), ...change });
|
|
6381
|
+
const file = { version: 1, projects: Object.fromEntries(this.entries) };
|
|
6382
|
+
writeAtomic(this.file, `${JSON.stringify(file, null, 2)}
|
|
6383
|
+
`);
|
|
6384
|
+
}
|
|
6385
|
+
};
|
|
6386
|
+
var isHidden = (entry) => entry.hiddenAt !== void 0 && entry.hiddenAt >= Math.max(entry.usedAt ?? 0, entry.addedAt ?? 0);
|
|
6387
|
+
var byName = new Intl.Collator("en", { sensitivity: "base", numeric: true });
|
|
6388
|
+
function mergeProjects(entries3, sessions, exists) {
|
|
6389
|
+
const rows = [];
|
|
6390
|
+
for (const project of /* @__PURE__ */ new Set([...entries3.keys(), ...sessions.keys()])) {
|
|
6391
|
+
const entry = entries3.get(project) ?? {};
|
|
6392
|
+
if (isHidden(entry)) continue;
|
|
6393
|
+
const usedAt = entry.usedAt ?? entry.addedAt ?? sessions.get(project)?.createdAt ?? null;
|
|
6394
|
+
rows.push({ project, name: basename(project), usedAt, exists: exists(project) });
|
|
6395
|
+
}
|
|
6396
|
+
return rows.sort((a, b) => byName.compare(a.name, b.name) || byName.compare(a.project, b.project));
|
|
6397
|
+
}
|
|
6398
|
+
function readBook(file) {
|
|
6399
|
+
try {
|
|
6400
|
+
const parsed = JSON.parse(readFileSync14(file, "utf8"));
|
|
6401
|
+
return new Map(Object.entries(parsed.projects ?? {}));
|
|
6402
|
+
} catch {
|
|
6403
|
+
return /* @__PURE__ */ new Map();
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
|
|
6407
|
+
// src/projects/hub.ts
|
|
6408
|
+
import { statSync as statSync6 } from "node:fs";
|
|
6409
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
6410
|
+
var ProjectHub = class {
|
|
6411
|
+
constructor(deps) {
|
|
6412
|
+
this.deps = deps;
|
|
6413
|
+
}
|
|
6414
|
+
listeners = /* @__PURE__ */ new Set();
|
|
6415
|
+
touched = false;
|
|
6416
|
+
list() {
|
|
6417
|
+
const runtimes = new Map(this.deps.sessions.list().map((r) => [r.project, r]));
|
|
6418
|
+
const projects = this.deps.book.list().map((row) => {
|
|
6419
|
+
const runtime = runtimes.get(row.project);
|
|
6420
|
+
return {
|
|
6421
|
+
...row,
|
|
6422
|
+
session: runtime ? { devServer: runtime.devServer.summary(), agentWorking: runtime.agentWorking } : null
|
|
6423
|
+
};
|
|
6424
|
+
});
|
|
6425
|
+
return { version: this.deps.version, projects, account: this.deps.account() };
|
|
6426
|
+
}
|
|
6427
|
+
add(path) {
|
|
6428
|
+
if (!isAbsolute2(path) || !statSync6(path, { throwIfNoEntry: false })?.isDirectory()) {
|
|
6429
|
+
return { error: "not a folder on this computer" };
|
|
6430
|
+
}
|
|
6431
|
+
const project = this.deps.projectRoot(path);
|
|
6432
|
+
this.deps.book.add(project, Date.now());
|
|
6433
|
+
this.changed();
|
|
6434
|
+
return this.list().projects.find((p) => p.project === project);
|
|
6435
|
+
}
|
|
6436
|
+
hide(project) {
|
|
6437
|
+
this.deps.book.hide(project, Date.now());
|
|
6438
|
+
this.changed();
|
|
6439
|
+
}
|
|
6440
|
+
/** Opens the review and connects its agent; the dev server waits for the user to start it. */
|
|
6441
|
+
async open(project, by) {
|
|
6442
|
+
if (!this.deps.book.isListed(project)) return { ok: false, status: 404, error: "not a project on the list" };
|
|
6443
|
+
await this.deps.restored();
|
|
6444
|
+
const opened = await this.deps.sessions.open({ project, reopen: true, now: Date.now() });
|
|
6445
|
+
if (!opened.ok) return { ok: false, status: 404, error: "the review could not be opened" };
|
|
6446
|
+
opened.runtime.ensureAgent(this.deps.defaultAgent(), by);
|
|
6447
|
+
this.changed();
|
|
6448
|
+
return { project, shellUrl: opened.runtime.shellUrl() };
|
|
6449
|
+
}
|
|
6450
|
+
async stop(project) {
|
|
6451
|
+
const runtime = this.deps.sessions.list().find((r) => r.project === project);
|
|
6452
|
+
if (runtime && !runtime.devServer.external) await runtime.devServer.stop();
|
|
6453
|
+
}
|
|
6454
|
+
async stopManagedBy(by) {
|
|
6455
|
+
const targets = this.deps.sessions.list().filter((r) => r.devServer.managedBy === by);
|
|
6456
|
+
await Promise.all(targets.map((r) => r.devServer.stop()));
|
|
6457
|
+
return targets.length;
|
|
6458
|
+
}
|
|
6459
|
+
disconnectAgentsOf(by) {
|
|
6460
|
+
return this.deps.sessions.list().filter((r) => r.disconnectAgentOf(by)).length;
|
|
6461
|
+
}
|
|
6462
|
+
changed() {
|
|
6463
|
+
for (const listener of this.listeners) listener();
|
|
6464
|
+
}
|
|
6465
|
+
subscribe(listener) {
|
|
6466
|
+
this.listeners.add(listener);
|
|
6467
|
+
this.touched = true;
|
|
6468
|
+
return () => this.listeners.delete(listener);
|
|
6469
|
+
}
|
|
6470
|
+
takeBusy() {
|
|
6471
|
+
const busy = this.touched || this.listeners.size > 0;
|
|
6472
|
+
this.touched = false;
|
|
6473
|
+
return busy;
|
|
6474
|
+
}
|
|
6475
|
+
};
|
|
6476
|
+
|
|
6477
|
+
// src/projects/routes.ts
|
|
6478
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
6479
|
+
import { join as join16 } from "node:path";
|
|
6480
|
+
import { Router as Router3 } from "express";
|
|
6481
|
+
|
|
6482
|
+
// src/version-header.ts
|
|
6483
|
+
var VERSION_HEADER = "x-ezdesign-version";
|
|
6484
|
+
|
|
6485
|
+
// src/version.ts
|
|
6486
|
+
function versionMismatchBody(cliVersion, daemonVersion) {
|
|
6487
|
+
return {
|
|
6488
|
+
error: cliVersion ? `this ${APP_NAME} CLI is v${cliVersion} but the daemon is running v${daemonVersion}` : `this ${APP_NAME} CLI predates the running daemon (v${daemonVersion})`,
|
|
6489
|
+
hint: `re-run as \`npx -y ${NPM_PACKAGE}@latest <url>\` to bring both to the current version`
|
|
6490
|
+
};
|
|
6491
|
+
}
|
|
6492
|
+
function versionGate(daemonVersion) {
|
|
6493
|
+
return (req, res, next) => {
|
|
6494
|
+
const cliVersion = req.get(VERSION_HEADER) || void 0;
|
|
6495
|
+
if (cliVersion === daemonVersion) return next();
|
|
6496
|
+
res.status(409).json(versionMismatchBody(cliVersion, daemonVersion));
|
|
6497
|
+
};
|
|
6498
|
+
}
|
|
6499
|
+
function desktopContractGate(served = DESKTOP_CONTRACT) {
|
|
6500
|
+
return (req, res, next) => {
|
|
6501
|
+
const asked = Number(req.get(DESKTOP_HEADER));
|
|
6502
|
+
if (Number.isInteger(asked) && asked >= 1 && asked <= served) return next();
|
|
6503
|
+
res.status(409).json({ error: `this ${APP_NAME} daemon serves desktop contracts up to ${served}` });
|
|
6504
|
+
};
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6507
|
+
// src/projects/routes.ts
|
|
6508
|
+
var field2 = (req, name) => req.body?.[name];
|
|
6509
|
+
function projectsApiRouter(hub, opts) {
|
|
6510
|
+
const router = Router3();
|
|
6511
|
+
router.use("/projects", refuseCrossSite(opts.origin));
|
|
6512
|
+
router.get("/projects/events", (req, res) => {
|
|
6513
|
+
res.set({ "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
6514
|
+
res.flushHeaders();
|
|
6515
|
+
const push = () => res.write(`data: ${JSON.stringify(hub.list())}
|
|
6516
|
+
|
|
6517
|
+
`);
|
|
6518
|
+
push();
|
|
6519
|
+
const unsubscribe = hub.subscribe(push);
|
|
6520
|
+
req.on("close", unsubscribe);
|
|
6521
|
+
});
|
|
6522
|
+
router.use("/projects", versionGate(opts.version));
|
|
6523
|
+
router.get("/projects", (_req, res) => {
|
|
6524
|
+
res.set("cache-control", "no-store").json(hub.list());
|
|
6525
|
+
});
|
|
6526
|
+
router.post("/projects/open", async (req, res) => {
|
|
6527
|
+
const project = field2(req, "project");
|
|
6528
|
+
if (typeof project !== "string") return void res.status(400).json({ error: "project required" });
|
|
6529
|
+
const opened = await hub.open(project, field2(req, "host") === "desktop" ? "desktop" : "browser");
|
|
6530
|
+
if ("ok" in opened) return void res.status(opened.status).json({ error: opened.error });
|
|
6531
|
+
res.json(opened);
|
|
6532
|
+
});
|
|
6533
|
+
router.post("/projects/stop", async (req, res) => {
|
|
6534
|
+
const project = field2(req, "project");
|
|
6535
|
+
if (typeof project !== "string") return void res.status(400).json({ error: "project required" });
|
|
6536
|
+
await hub.stop(project);
|
|
6537
|
+
res.json({ ok: true });
|
|
6538
|
+
});
|
|
6539
|
+
router.post("/projects", (req, res) => {
|
|
6540
|
+
const path = field2(req, "path");
|
|
6541
|
+
if (typeof path !== "string") return void res.status(400).json({ error: "path required" });
|
|
6542
|
+
const added = hub.add(path.trim());
|
|
6543
|
+
if ("error" in added) return void res.status(400).json(added);
|
|
6544
|
+
res.json(added);
|
|
6545
|
+
});
|
|
6546
|
+
router.post("/projects/hide", (req, res) => {
|
|
6547
|
+
const project = field2(req, "project");
|
|
6548
|
+
if (typeof project !== "string") return void res.status(400).json({ error: "project required" });
|
|
6549
|
+
hub.hide(project);
|
|
6550
|
+
res.json({ ok: true });
|
|
6551
|
+
});
|
|
6552
|
+
return router;
|
|
6553
|
+
}
|
|
6554
|
+
var homeHtml = (version, icon) => `<!doctype html>
|
|
6555
|
+
<html lang="en">
|
|
6556
|
+
<head>
|
|
6557
|
+
<meta charset="utf-8">
|
|
6558
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6559
|
+
<title>${APP_NAME}</title>
|
|
6560
|
+
<link rel="icon" href="${icon}">
|
|
6561
|
+
<link rel="stylesheet" href="${URL_PREFIX}/home.css">
|
|
6562
|
+
</head>
|
|
6563
|
+
<body>
|
|
6564
|
+
<div id="ez-home"></div>
|
|
6565
|
+
<script src="${URL_PREFIX}/home.js" data-version="${version}"></script>
|
|
6566
|
+
</body>
|
|
6567
|
+
</html>`;
|
|
6568
|
+
function homePageRouter(opts) {
|
|
6569
|
+
const router = Router3();
|
|
6570
|
+
router.get(HOME_PATH, (_req, res) => res.type("html").set("cache-control", "no-store").send(homeHtml(opts.version, opts.icon)));
|
|
6571
|
+
for (const file of ["home.js", "home.css"]) {
|
|
6572
|
+
router.get(`${URL_PREFIX}/${file}`, (_req, res) => {
|
|
6573
|
+
res.type(file.endsWith(".css") ? "text/css" : "text/javascript").set("cache-control", "no-store").send(readFileSync15(join16(opts.distDir, file)));
|
|
6574
|
+
});
|
|
6575
|
+
}
|
|
6576
|
+
return router;
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
// src/project-root.ts
|
|
6580
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
6581
|
+
import { dirname as dirname5, join as join17, parse, resolve as resolve2 } from "node:path";
|
|
6582
|
+
function projectRoot(from) {
|
|
6583
|
+
let dir = resolve2(from);
|
|
6584
|
+
const { root } = parse(dir);
|
|
6585
|
+
for (; ; ) {
|
|
6586
|
+
if (existsSync9(join17(dir, ".git")) || existsSync9(join17(dir, "package.json"))) return dir;
|
|
6587
|
+
if (dir === root) return resolve2(from);
|
|
6588
|
+
dir = dirname5(dir);
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
|
|
6592
|
+
// src/explore.ts
|
|
6593
|
+
var MAX_VARIANT_HTML = 32 * 1024;
|
|
6594
|
+
var MAX_VARIANT_NAME = 40;
|
|
6595
|
+
var FORBIDDEN_TAGS = ["script", "iframe", "object", "embed", "link", "meta", "base"];
|
|
6596
|
+
function variantProblemMessage(problem) {
|
|
6597
|
+
switch (problem.code) {
|
|
6598
|
+
case "empty":
|
|
6599
|
+
return "html is empty. Send the markup for one element.";
|
|
6600
|
+
case "too-long":
|
|
6601
|
+
return `html is ${problem.length} bytes; the limit is ${MAX_VARIANT_HTML}. Send one element's markup, not a page.`;
|
|
6602
|
+
case "name-too-long":
|
|
6603
|
+
return `name is ${problem.length} characters; the limit is ${MAX_VARIANT_NAME}. A short label, not a description.`;
|
|
6604
|
+
case "forbidden-tag":
|
|
6605
|
+
return `html contains <${problem.tag}>, which a variant may not use. A variant is markup and CSS only - no scripts, no embeds, no external resources.`;
|
|
6606
|
+
case "event-handler":
|
|
6607
|
+
return `html sets the inline handler ${problem.attribute}. A variant is markup and CSS only - no scripts.`;
|
|
6608
|
+
case "javascript-url":
|
|
6609
|
+
return "html contains a javascript: url. A variant is markup and CSS only - no scripts.";
|
|
6610
|
+
case "not-one-root":
|
|
6611
|
+
return problem.roots === 0 ? "html has no element at its top level. A variant is one root element, optionally with a <style> block inside it." : `html has ${problem.roots} top-level elements. A variant is one root element - wrap them in a single container.`;
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
function checkVariant(name, html) {
|
|
6615
|
+
if (name.length > MAX_VARIANT_NAME) return { code: "name-too-long", length: name.length };
|
|
6616
|
+
if (!html.trim()) return { code: "empty" };
|
|
6617
|
+
if (html.length > MAX_VARIANT_HTML) return { code: "too-long", length: html.length };
|
|
6618
|
+
const tags = scanTags(html);
|
|
6619
|
+
if (!tags) return { code: "not-one-root", roots: 0 };
|
|
6620
|
+
for (const tag of tags.names) {
|
|
6621
|
+
if (FORBIDDEN_TAGS.includes(tag)) return { code: "forbidden-tag", tag };
|
|
6622
|
+
}
|
|
6623
|
+
const handler = /\son([a-z]+)\s*=/i.exec(html);
|
|
6624
|
+
if (handler) return { code: "event-handler", attribute: `on${handler[1].toLowerCase()}` };
|
|
6625
|
+
if (/javascript:/i.test(html)) return { code: "javascript-url" };
|
|
6626
|
+
if (tags.roots !== 1) return { code: "not-one-root", roots: tags.roots };
|
|
6627
|
+
return null;
|
|
6628
|
+
}
|
|
6629
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
6630
|
+
"area",
|
|
6631
|
+
"base",
|
|
6632
|
+
"br",
|
|
6633
|
+
"col",
|
|
6634
|
+
"embed",
|
|
6635
|
+
"hr",
|
|
6636
|
+
"img",
|
|
6637
|
+
"input",
|
|
6638
|
+
"link",
|
|
6639
|
+
"meta",
|
|
6640
|
+
"param",
|
|
6641
|
+
"source",
|
|
6642
|
+
"track",
|
|
6643
|
+
"wbr"
|
|
6644
|
+
]);
|
|
6645
|
+
function scanTags(html) {
|
|
6646
|
+
const names2 = [];
|
|
6647
|
+
let depth = 0;
|
|
6648
|
+
let roots = 0;
|
|
6649
|
+
let i = 0;
|
|
6650
|
+
while (i < html.length) {
|
|
6651
|
+
const lt = html.indexOf("<", i);
|
|
6652
|
+
if (lt === -1) break;
|
|
6653
|
+
if (html.startsWith("<!--", lt)) {
|
|
6654
|
+
const end2 = html.indexOf("-->", lt + 4);
|
|
6655
|
+
i = end2 === -1 ? html.length : end2 + 3;
|
|
6656
|
+
continue;
|
|
6657
|
+
}
|
|
6658
|
+
if (html.startsWith("<!", lt) || html.startsWith("<?", lt)) {
|
|
6659
|
+
const end2 = html.indexOf(">", lt);
|
|
6660
|
+
i = end2 === -1 ? html.length : end2 + 1;
|
|
6661
|
+
continue;
|
|
6662
|
+
}
|
|
6663
|
+
const tag = /^<(\/?)([a-zA-Z][a-zA-Z0-9-]*)/.exec(html.slice(lt, lt + 64));
|
|
6664
|
+
if (!tag) {
|
|
6665
|
+
i = lt + 1;
|
|
6666
|
+
continue;
|
|
6667
|
+
}
|
|
6668
|
+
const end = closingBracket(html, lt);
|
|
6669
|
+
if (end === -1) break;
|
|
6670
|
+
const closing = tag[1] === "/";
|
|
6671
|
+
const name = tag[2].toLowerCase();
|
|
6672
|
+
names2.push(name);
|
|
6673
|
+
const selfClosing = html[end - 1] === "/";
|
|
6674
|
+
if (closing) {
|
|
6675
|
+
if (depth === 0) return null;
|
|
6676
|
+
depth -= 1;
|
|
6677
|
+
} else if (!selfClosing && !VOID_TAGS.has(name)) {
|
|
6678
|
+
if (depth === 0) roots += 1;
|
|
6679
|
+
depth += 1;
|
|
6680
|
+
} else if (depth === 0) {
|
|
6681
|
+
roots += 1;
|
|
6682
|
+
}
|
|
6683
|
+
i = end + 1;
|
|
6684
|
+
if (!closing && RAW_TEXT2.has(name) && !selfClosing) {
|
|
6685
|
+
const close = html.toLowerCase().indexOf(`</${name}`, i);
|
|
6686
|
+
if (close === -1) break;
|
|
6687
|
+
i = close;
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
return { names: names2, roots };
|
|
6691
|
+
}
|
|
6692
|
+
var RAW_TEXT2 = /* @__PURE__ */ new Set(["style", "script", "textarea", "title"]);
|
|
6693
|
+
function closingBracket(html, from) {
|
|
6694
|
+
let quote = null;
|
|
4928
6695
|
for (let i = from + 1; i < html.length; i += 1) {
|
|
4929
6696
|
const ch = html[i];
|
|
4930
6697
|
if (quote) {
|
|
@@ -4972,7 +6739,7 @@ function point2(v) {
|
|
|
4972
6739
|
const r = typeof rel === "object" && rel !== null ? rel : {};
|
|
4973
6740
|
return { x: px2, y: py, rel: { x: num(r.x) ?? 0, y: num(r.y) ?? 0 } };
|
|
4974
6741
|
}
|
|
4975
|
-
function
|
|
6742
|
+
function rect2(v) {
|
|
4976
6743
|
if (typeof v !== "object" || v === null) return void 0;
|
|
4977
6744
|
const { x, y, width, height } = v;
|
|
4978
6745
|
const vals = [num(x), num(y), num(width), num(height)];
|
|
@@ -5008,7 +6775,7 @@ function sanitizeAnchor(raw) {
|
|
|
5008
6775
|
if (page) out.page = page;
|
|
5009
6776
|
const p = point2(v.point);
|
|
5010
6777
|
if (p) out.point = p;
|
|
5011
|
-
const r =
|
|
6778
|
+
const r = rect2(v.rect);
|
|
5012
6779
|
if (r) out.rect = r;
|
|
5013
6780
|
const vp = viewport(v.viewport);
|
|
5014
6781
|
if (vp) out.viewport = vp;
|
|
@@ -5185,8 +6952,8 @@ function parseBatchView(raw) {
|
|
|
5185
6952
|
}
|
|
5186
6953
|
|
|
5187
6954
|
// src/truth.ts
|
|
5188
|
-
import { existsSync as
|
|
5189
|
-
import { join as
|
|
6955
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
6956
|
+
import { join as join18, resolve as resolve3 } from "node:path";
|
|
5190
6957
|
var PRODUCT_MD = "PRODUCT.md";
|
|
5191
6958
|
var DESIGN_MD = "DESIGN.md";
|
|
5192
6959
|
function designHarnessEnabled(env = process.env) {
|
|
@@ -5201,8 +6968,8 @@ function effortLine(effort) {
|
|
|
5201
6968
|
var FILE_NAME = { product: PRODUCT_MD, design: DESIGN_MD };
|
|
5202
6969
|
function truthState(project, enabled, seen = false) {
|
|
5203
6970
|
return {
|
|
5204
|
-
product:
|
|
5205
|
-
design:
|
|
6971
|
+
product: existsSync10(join18(project, PRODUCT_MD)),
|
|
6972
|
+
design: existsSync10(join18(project, DESIGN_MD)),
|
|
5206
6973
|
enabled,
|
|
5207
6974
|
seen
|
|
5208
6975
|
};
|
|
@@ -5234,10 +7001,10 @@ function harnessReady(truth) {
|
|
|
5234
7001
|
return truth.enabled && truth.product && truth.design;
|
|
5235
7002
|
}
|
|
5236
7003
|
function skillDir(fromDir) {
|
|
5237
|
-
return
|
|
7004
|
+
return resolve3(fromDir, "..", "skills", "ezdesign");
|
|
5238
7005
|
}
|
|
5239
7006
|
function skillReference(skill, name) {
|
|
5240
|
-
return
|
|
7007
|
+
return join18(skill, "reference", `${name}.md`);
|
|
5241
7008
|
}
|
|
5242
7009
|
var names = (files) => files.map((f) => `\`${FILE_NAME[f]}\``);
|
|
5243
7010
|
function truthFilesZh(files) {
|
|
@@ -5255,7 +7022,7 @@ function creationSteps(missing, skill) {
|
|
|
5255
7022
|
steps.push("stack");
|
|
5256
7023
|
if (missing.includes("design")) steps.push("design");
|
|
5257
7024
|
steps.push("components");
|
|
5258
|
-
return steps.map((
|
|
7025
|
+
return steps.map((step2) => `read \`${skillReference(skill, step2)}\` and follow it`);
|
|
5259
7026
|
}
|
|
5260
7027
|
function makeReading(skill, effort = "high") {
|
|
5261
7028
|
const files = ["make", "baseline", ...effort === "low" ? [] : ["principles"]].map(
|
|
@@ -5505,55 +7272,90 @@ function toAgentAttachment(a, files) {
|
|
|
5505
7272
|
mime: a.mime,
|
|
5506
7273
|
size: a.size,
|
|
5507
7274
|
path: files.attachmentPath(a),
|
|
5508
|
-
...a.origin ? { origin: a.origin } : {}
|
|
7275
|
+
...a.origin ? { origin: a.origin } : {},
|
|
7276
|
+
...a.video && a.video.state !== "pending" ? { video: toAgentVideo(a, a.video, files) } : {}
|
|
7277
|
+
};
|
|
7278
|
+
}
|
|
7279
|
+
var READ_FRAMES = "A recording you cannot open: do not read `path`. Read `overview` first, then the `sheet` of each stretch of `motion` you need. A sheet is a grid of frames in time order, left to right, each labelled with its time in seconds; a motion sheet is cropped to `region` (pixels of the video) and starts with the frame before the motion, so consecutive labels give you its timing and easing. `shown` < `frames` means only that many of its changed frames are on the sheet, evenly spaced.";
|
|
7280
|
+
var READ_AUDIO = " It has sound you cannot hear: if it may matter, ask the user what is said.";
|
|
7281
|
+
var UNREADABLE = {
|
|
7282
|
+
"unsupported-browser": () => "the browser the review ran in cannot decode video",
|
|
7283
|
+
"unsupported-codec": (detail) => `that browser cannot decode its codec (${detail})`,
|
|
7284
|
+
unreadable: (detail) => `its file could not be read as a video (${detail})`,
|
|
7285
|
+
crashed: () => "the browser stopped while taking them"
|
|
7286
|
+
};
|
|
7287
|
+
var seconds = (ms) => Math.round(ms) / 1e3;
|
|
7288
|
+
function toAgentVideo(a, video, files) {
|
|
7289
|
+
if (video.state === "failed") {
|
|
7290
|
+
return {
|
|
7291
|
+
read: `No frames could be taken from this video: ${UNREADABLE[video.reason](video.detail)}. Do not try to read \`path\`. Tell the user, and ask them what it shows.`,
|
|
7292
|
+
unreadable: video.reason
|
|
7293
|
+
};
|
|
7294
|
+
}
|
|
7295
|
+
return {
|
|
7296
|
+
read: READ_FRAMES + (video.audio ? READ_AUDIO : ""),
|
|
7297
|
+
duration: seconds(video.durationMs),
|
|
7298
|
+
width: video.width,
|
|
7299
|
+
height: video.height,
|
|
7300
|
+
fps: video.fps,
|
|
7301
|
+
audio: video.audio,
|
|
7302
|
+
overview: files.sheetPath(a, 1),
|
|
7303
|
+
motion: video.motion.map((m) => ({
|
|
7304
|
+
from: seconds(m.fromMs),
|
|
7305
|
+
to: seconds(m.toMs),
|
|
7306
|
+
region: m.region,
|
|
7307
|
+
frames: m.frames,
|
|
7308
|
+
shown: m.shown,
|
|
7309
|
+
...m.sheet === null ? {} : { sheet: files.sheetPath(a, m.sheet) }
|
|
7310
|
+
}))
|
|
5509
7311
|
};
|
|
5510
7312
|
}
|
|
5511
7313
|
function toAgentAttachments(list2, files) {
|
|
5512
7314
|
return list2?.length ? list2.map((a) => toAgentAttachment(a, files)) : void 0;
|
|
5513
7315
|
}
|
|
5514
7316
|
function anchorParts(anchor, textMax, withViewport, frames) {
|
|
5515
|
-
const
|
|
5516
|
-
if (anchor.source)
|
|
5517
|
-
if (anchor.components?.length)
|
|
5518
|
-
if (anchor.section)
|
|
5519
|
-
if (anchor.text)
|
|
7317
|
+
const parts2 = [];
|
|
7318
|
+
if (anchor.source) parts2.push(anchor.source);
|
|
7319
|
+
if (anchor.components?.length) parts2.push(`<${anchor.components.join(" \u2190 ")}>`);
|
|
7320
|
+
if (anchor.section) parts2.push(`[section: ${anchor.section}]`);
|
|
7321
|
+
if (anchor.text) parts2.push(`"${truncate(anchor.text, textMax)}"`);
|
|
5520
7322
|
if (anchor.contains?.length) {
|
|
5521
|
-
|
|
7323
|
+
parts2.push(`[framed ${anchor.contains.length}: ${anchor.contains.join("; ")}]`);
|
|
5522
7324
|
}
|
|
5523
7325
|
if (withViewport && anchor.viewport) {
|
|
5524
7326
|
const { width, height, preset } = anchor.viewport;
|
|
5525
|
-
|
|
7327
|
+
parts2.push(preset ? `@${preset} ${width}x${height}` : `@${width}x${height}`);
|
|
5526
7328
|
}
|
|
5527
7329
|
if (anchor.frame) {
|
|
5528
7330
|
const named = frames?.(anchor.frame);
|
|
5529
|
-
|
|
7331
|
+
parts2.push(
|
|
5530
7332
|
named ? `[canvas frame "${named.frame}" on canvas "${named.canvas}"]` : `[canvas frame ${anchor.frame.canvasId}/${anchor.frame.frameId}]`
|
|
5531
7333
|
);
|
|
5532
7334
|
}
|
|
5533
|
-
if (
|
|
5534
|
-
return
|
|
7335
|
+
if (parts2.length === 0 && anchor.selector) parts2.push(anchor.selector);
|
|
7336
|
+
return parts2;
|
|
5535
7337
|
}
|
|
5536
7338
|
function referenceLabel(reference, frames) {
|
|
5537
7339
|
return anchorParts(reference.anchor, 40, false, frames).join(" \xB7 ") || reference.label;
|
|
5538
7340
|
}
|
|
5539
7341
|
function toAgentItem(a, files, frames) {
|
|
5540
|
-
const
|
|
5541
|
-
if (a.kind === "page")
|
|
7342
|
+
const parts2 = anchorParts(a.anchor, a.kind === "text" ? 60 : 40, true, frames);
|
|
7343
|
+
if (a.kind === "page") parts2.unshift(`[page ${a.anchor.page ?? ""}]`.trim());
|
|
5542
7344
|
if (a.kind === "point") {
|
|
5543
7345
|
const rel = a.anchor.point?.rel;
|
|
5544
7346
|
const at = rel ? ` ${Math.round(rel.x * 100)}%/${Math.round(rel.y * 100)}%` : "";
|
|
5545
|
-
|
|
7347
|
+
parts2.unshift(`[pin${at}]`);
|
|
5546
7348
|
}
|
|
5547
7349
|
a.references?.forEach(
|
|
5548
|
-
(r) =>
|
|
7350
|
+
(r) => parts2.push(`[ref ${r.n}: ${r.variant ? "chosen variant \xB7 " : ""}${referenceLabel(r, frames)}]`)
|
|
5549
7351
|
);
|
|
5550
|
-
a.attachments?.forEach((x, i) =>
|
|
7352
|
+
a.attachments?.forEach((x, i) => parts2.push(`[file ${i + 1}: ${x.name}${x.video ? ", video" : ""}]`));
|
|
5551
7353
|
const attachments = toAgentAttachments(a.attachments, files);
|
|
5552
7354
|
return {
|
|
5553
7355
|
id: a.id,
|
|
5554
7356
|
kind: a.kind,
|
|
5555
7357
|
comment: a.comment,
|
|
5556
|
-
label:
|
|
7358
|
+
label: parts2.join(" \xB7 "),
|
|
5557
7359
|
anchor: a.anchor,
|
|
5558
7360
|
...attachments ? { attachments } : {},
|
|
5559
7361
|
...a.references?.length ? { references: a.references } : {}
|
|
@@ -5717,103 +7519,218 @@ function register(server, sink) {
|
|
|
5717
7519
|
}
|
|
5718
7520
|
);
|
|
5719
7521
|
}
|
|
5720
|
-
|
|
5721
|
-
// src/skills.ts
|
|
5722
|
-
import { readFileSync as
|
|
5723
|
-
import { homedir as homedir4 } from "node:os";
|
|
5724
|
-
import { join as
|
|
5725
|
-
var SKILL_DIRS = [".claude/skills", ".agents/skills"];
|
|
5726
|
-
function readFrontmatter(path) {
|
|
5727
|
-
let text;
|
|
7522
|
+
|
|
7523
|
+
// src/skills.ts
|
|
7524
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync6, statSync as statSync7 } from "node:fs";
|
|
7525
|
+
import { homedir as homedir4 } from "node:os";
|
|
7526
|
+
import { join as join19 } from "node:path";
|
|
7527
|
+
var SKILL_DIRS = [".claude/skills", ".agents/skills"];
|
|
7528
|
+
function readFrontmatter(path) {
|
|
7529
|
+
let text;
|
|
7530
|
+
try {
|
|
7531
|
+
text = readFileSync16(path, "utf8");
|
|
7532
|
+
} catch {
|
|
7533
|
+
return null;
|
|
7534
|
+
}
|
|
7535
|
+
if (!text.startsWith("---")) return null;
|
|
7536
|
+
const end = text.indexOf("\n---", 3);
|
|
7537
|
+
if (end === -1) return null;
|
|
7538
|
+
const fields = {};
|
|
7539
|
+
for (const line2 of text.slice(3, end).split("\n")) {
|
|
7540
|
+
const match = /^(name|description|user-invocable):\s*(.*)$/.exec(line2);
|
|
7541
|
+
if (!match) continue;
|
|
7542
|
+
const value = match[2].trim().replace(/^['"]|['"]$/g, "");
|
|
7543
|
+
if (value) fields[match[1]] = value;
|
|
7544
|
+
}
|
|
7545
|
+
return fields;
|
|
7546
|
+
}
|
|
7547
|
+
function entries2(dir) {
|
|
7548
|
+
try {
|
|
7549
|
+
return readdirSync6(dir, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && (e.isDirectory() || e.isSymbolicLink() && isDir(join19(dir, e.name)))).map((e) => e.name);
|
|
7550
|
+
} catch {
|
|
7551
|
+
return [];
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
function isDir(path) {
|
|
7555
|
+
try {
|
|
7556
|
+
return statSync7(path).isDirectory();
|
|
7557
|
+
} catch {
|
|
7558
|
+
return false;
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
function readSkill(dir, name, source) {
|
|
7562
|
+
const fields = readFrontmatter(join19(dir, name, "SKILL.md"));
|
|
7563
|
+
if (!fields) return null;
|
|
7564
|
+
if (fields["user-invocable"]?.toLowerCase() === "false") return null;
|
|
7565
|
+
return { name, description: fields.description ?? "", source };
|
|
7566
|
+
}
|
|
7567
|
+
function listSkills(project, home = homedir4()) {
|
|
7568
|
+
const found = /* @__PURE__ */ new Map();
|
|
7569
|
+
const roots = [
|
|
7570
|
+
[home, "user"],
|
|
7571
|
+
[project, "project"]
|
|
7572
|
+
];
|
|
7573
|
+
for (const [root, source] of roots) {
|
|
7574
|
+
for (const dir of SKILL_DIRS) {
|
|
7575
|
+
const container = join19(root, dir);
|
|
7576
|
+
for (const name of entries2(container)) {
|
|
7577
|
+
const skill = readSkill(container, name, source);
|
|
7578
|
+
if (skill) found.set(skill.name, skill);
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
const plugins = join19(project, ".claude/plugins");
|
|
7583
|
+
for (const plugin of entries2(plugins)) {
|
|
7584
|
+
const container = join19(plugins, plugin, "skills");
|
|
7585
|
+
for (const name of entries2(container)) {
|
|
7586
|
+
const skill = readSkill(container, name, "plugin");
|
|
7587
|
+
if (skill) found.set(`${plugin}:${name}`, { ...skill, name: `${plugin}:${name}` });
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
7591
|
+
}
|
|
7592
|
+
function skillPrefix(agent) {
|
|
7593
|
+
return agentBrandFor(agent) === "claude" ? "/" : "$";
|
|
7594
|
+
}
|
|
7595
|
+
function spendSkillMarkers(text, skills, agent) {
|
|
7596
|
+
return text.replace(/\[skill (\d+)\]/g, (whole, n) => {
|
|
7597
|
+
const name = skills[Number(n) - 1];
|
|
7598
|
+
return name === void 0 ? whole : `${skillPrefix(agent)}${name}`;
|
|
7599
|
+
});
|
|
7600
|
+
}
|
|
7601
|
+
|
|
7602
|
+
// src/registry.ts
|
|
7603
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
7604
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7605
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync13, openSync as openSync2, readFileSync as readFileSync17, realpathSync as realpathSync4, rmSync as rmSync10, writeFileSync as writeFileSync9 } from "node:fs";
|
|
7606
|
+
import { join as join21 } from "node:path";
|
|
7607
|
+
|
|
7608
|
+
// src/installer.ts
|
|
7609
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
7610
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readdirSync as readdirSync7, realpathSync as realpathSync3, rmSync as rmSync9 } from "node:fs";
|
|
7611
|
+
import { dirname as dirname6, join as join20, resolve as resolve4, sep as sep2 } from "node:path";
|
|
7612
|
+
function npmCli() {
|
|
7613
|
+
const bin = dirname6(process.execPath);
|
|
7614
|
+
for (const candidate of [
|
|
7615
|
+
join20(bin, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
7616
|
+
join20(bin, "node_modules", "npm", "bin", "npm-cli.js")
|
|
7617
|
+
]) {
|
|
7618
|
+
if (existsSync11(candidate)) return candidate;
|
|
7619
|
+
}
|
|
7620
|
+
return null;
|
|
7621
|
+
}
|
|
7622
|
+
function npmErrorSummary(stderr) {
|
|
7623
|
+
const lines = stderr.split("\n").map((l) => l.trim());
|
|
7624
|
+
const own = lines.filter((l) => /^npm (error|ERR!)/.test(l)).map((l) => l.replace(/^npm (error|ERR!)\s*/, "")).filter((l) => l && !/^A complete log of this run/.test(l));
|
|
7625
|
+
if (own.length) return own.slice(0, 2).join(" ");
|
|
7626
|
+
return lines.filter(Boolean).at(-1) ?? "";
|
|
7627
|
+
}
|
|
7628
|
+
function run3(label, command, args, opts = {}) {
|
|
7629
|
+
return new Promise((resolve7, reject) => {
|
|
7630
|
+
const child = spawn6(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: opts.shell, env: opts.env });
|
|
7631
|
+
let stdout = "";
|
|
7632
|
+
let stderr = "";
|
|
7633
|
+
child.stdout.on("data", (c) => stdout += c);
|
|
7634
|
+
child.stderr.on("data", (c) => stderr += c);
|
|
7635
|
+
child.on("error", (err) => reject(new Error(`${label} could not start: ${err.message}`)));
|
|
7636
|
+
child.on("close", (code) => {
|
|
7637
|
+
if (code === 0) return resolve7({ stdout, stderr });
|
|
7638
|
+
const why = npmErrorSummary(stderr);
|
|
7639
|
+
reject(new Error(`${label} failed${why ? `: ${why}` : ` (exit ${code})`}`));
|
|
7640
|
+
});
|
|
7641
|
+
});
|
|
7642
|
+
}
|
|
7643
|
+
async function npm(label, args) {
|
|
7644
|
+
const cli = npmCli();
|
|
7645
|
+
if (cli) {
|
|
7646
|
+
await run3(label, process.execPath, [cli, ...args]);
|
|
7647
|
+
return;
|
|
7648
|
+
}
|
|
7649
|
+
await run3(label, process.platform === "win32" ? "npm.cmd" : "npm", args.map(shellQuote), {
|
|
7650
|
+
shell: true
|
|
7651
|
+
});
|
|
7652
|
+
}
|
|
7653
|
+
function packageSpec(version) {
|
|
7654
|
+
return `${NPM_PACKAGE}@${version}`;
|
|
7655
|
+
}
|
|
7656
|
+
function installedPackageRoot(version) {
|
|
7657
|
+
return join20(VERSIONS_DIR, version, "node_modules", NPM_PACKAGE);
|
|
7658
|
+
}
|
|
7659
|
+
async function verifyInstall(pkgRoot, version) {
|
|
7660
|
+
const cliEntry2 = join20(pkgRoot, "dist", "cli.mjs");
|
|
7661
|
+
if (!existsSync11(cliEntry2)) return false;
|
|
7662
|
+
try {
|
|
7663
|
+
const { stdout } = await run3(`${APP_NAME} --version`, SELF_NODE.execPath, [cliEntry2, "--version"], {
|
|
7664
|
+
env: selfNodeEnv()
|
|
7665
|
+
});
|
|
7666
|
+
return stdout.trim() === version;
|
|
7667
|
+
} catch {
|
|
7668
|
+
return false;
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7671
|
+
async function installVersion(version) {
|
|
7672
|
+
const pkgRoot = installedPackageRoot(version);
|
|
7673
|
+
if (await verifyInstall(pkgRoot, version)) return pkgRoot;
|
|
7674
|
+
const prefix = join20(VERSIONS_DIR, version);
|
|
7675
|
+
mkdirSync12(prefix, { recursive: true });
|
|
7676
|
+
await npm(`npm install ${packageSpec(version)}`, [
|
|
7677
|
+
"install",
|
|
7678
|
+
"--prefix",
|
|
7679
|
+
prefix,
|
|
7680
|
+
"--no-audit",
|
|
7681
|
+
"--no-fund",
|
|
7682
|
+
"--no-package-lock",
|
|
7683
|
+
"--loglevel=error",
|
|
7684
|
+
packageSpec(version)
|
|
7685
|
+
]);
|
|
7686
|
+
if (!await verifyInstall(pkgRoot, version)) {
|
|
7687
|
+
throw new Error(`the installed ${packageSpec(version)} does not run as v${version}`);
|
|
7688
|
+
}
|
|
7689
|
+
return pkgRoot;
|
|
7690
|
+
}
|
|
7691
|
+
function realOrResolved(path) {
|
|
5728
7692
|
try {
|
|
5729
|
-
|
|
7693
|
+
return realpathSync3(path);
|
|
5730
7694
|
} catch {
|
|
5731
|
-
return
|
|
5732
|
-
}
|
|
5733
|
-
if (!text.startsWith("---")) return null;
|
|
5734
|
-
const end = text.indexOf("\n---", 3);
|
|
5735
|
-
if (end === -1) return null;
|
|
5736
|
-
const fields = {};
|
|
5737
|
-
for (const line2 of text.slice(3, end).split("\n")) {
|
|
5738
|
-
const match = /^(name|description|user-invocable):\s*(.*)$/.exec(line2);
|
|
5739
|
-
if (!match) continue;
|
|
5740
|
-
const value = match[2].trim().replace(/^['"]|['"]$/g, "");
|
|
5741
|
-
if (value) fields[match[1]] = value;
|
|
7695
|
+
return resolve4(path);
|
|
5742
7696
|
}
|
|
5743
|
-
return fields;
|
|
5744
7697
|
}
|
|
5745
|
-
function
|
|
7698
|
+
function pruneInstalledVersions(runningFrom, root = VERSIONS_DIR) {
|
|
7699
|
+
let versions;
|
|
5746
7700
|
try {
|
|
5747
|
-
|
|
7701
|
+
versions = readdirSync7(root);
|
|
5748
7702
|
} catch {
|
|
5749
|
-
return
|
|
5750
|
-
}
|
|
5751
|
-
}
|
|
5752
|
-
function readSkill(dir, name, source) {
|
|
5753
|
-
const fields = readFrontmatter(join14(dir, name, "SKILL.md"));
|
|
5754
|
-
if (!fields) return null;
|
|
5755
|
-
if (fields["user-invocable"]?.toLowerCase() === "false") return null;
|
|
5756
|
-
return { name, description: fields.description ?? "", source };
|
|
5757
|
-
}
|
|
5758
|
-
function listSkills(project, home = homedir4()) {
|
|
5759
|
-
const found = /* @__PURE__ */ new Map();
|
|
5760
|
-
const roots = [
|
|
5761
|
-
[home, "user"],
|
|
5762
|
-
[project, "project"]
|
|
5763
|
-
];
|
|
5764
|
-
for (const [root, source] of roots) {
|
|
5765
|
-
for (const dir of SKILL_DIRS) {
|
|
5766
|
-
const container = join14(root, dir);
|
|
5767
|
-
for (const name of entries2(container)) {
|
|
5768
|
-
const skill = readSkill(container, name, source);
|
|
5769
|
-
if (skill) found.set(skill.name, skill);
|
|
5770
|
-
}
|
|
5771
|
-
}
|
|
7703
|
+
return;
|
|
5772
7704
|
}
|
|
5773
|
-
const
|
|
5774
|
-
for (const
|
|
5775
|
-
const
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
if (skill) found.set(`${plugin}:${name}`, { ...skill, name: `${plugin}:${name}` });
|
|
5779
|
-
}
|
|
7705
|
+
const running2 = realOrResolved(runningFrom);
|
|
7706
|
+
for (const version of versions) {
|
|
7707
|
+
const dir = join20(root, version);
|
|
7708
|
+
if (running2.startsWith(realOrResolved(dir) + sep2)) continue;
|
|
7709
|
+
rmSync9(dir, { recursive: true, force: true });
|
|
5780
7710
|
}
|
|
5781
|
-
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
5782
|
-
}
|
|
5783
|
-
function skillPrefix(agent) {
|
|
5784
|
-
return agentBrandFor(agent) === "claude" ? "/" : "$";
|
|
5785
|
-
}
|
|
5786
|
-
function spendSkillMarkers(text, skills, agent) {
|
|
5787
|
-
return text.replace(/\[skill (\d+)\]/g, (whole, n) => {
|
|
5788
|
-
const name = skills[Number(n) - 1];
|
|
5789
|
-
return name === void 0 ? whole : `${skillPrefix(agent)}${name}`;
|
|
5790
|
-
});
|
|
5791
7711
|
}
|
|
5792
7712
|
|
|
5793
7713
|
// src/registry.ts
|
|
5794
|
-
import { spawn as spawn4 } from "node:child_process";
|
|
5795
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
5796
|
-
import { mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as readFileSync12, realpathSync, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5797
7714
|
function readRegistry() {
|
|
5798
7715
|
try {
|
|
5799
|
-
return JSON.parse(
|
|
7716
|
+
return JSON.parse(readFileSync17(REGISTRY_FILE, "utf8"));
|
|
5800
7717
|
} catch {
|
|
5801
7718
|
return null;
|
|
5802
7719
|
}
|
|
5803
7720
|
}
|
|
5804
7721
|
function writeRegistry(info) {
|
|
5805
|
-
|
|
7722
|
+
mkdirSync13(DATA_DIR, { recursive: true });
|
|
5806
7723
|
writeFileSync9(REGISTRY_FILE, JSON.stringify(info, null, 2));
|
|
5807
7724
|
}
|
|
5808
7725
|
function clearRegistry(pid) {
|
|
5809
7726
|
if (pid !== void 0 && readRegistry()?.pid !== pid) return;
|
|
5810
|
-
|
|
7727
|
+
rmSync10(REGISTRY_FILE, { force: true });
|
|
5811
7728
|
}
|
|
5812
7729
|
var ownDataDirId;
|
|
5813
7730
|
function dataDirId() {
|
|
5814
7731
|
if (ownDataDirId === void 0) {
|
|
5815
|
-
|
|
5816
|
-
const real =
|
|
7732
|
+
mkdirSync13(DATA_DIR, { recursive: true });
|
|
7733
|
+
const real = realpathSync4.native(DATA_DIR);
|
|
5817
7734
|
ownDataDirId = createHash4("sha256").update(real).digest("hex").slice(0, 16);
|
|
5818
7735
|
}
|
|
5819
7736
|
return ownDataDirId;
|
|
@@ -5829,7 +7746,7 @@ async function probeDaemon(port, pid) {
|
|
|
5829
7746
|
if (!res.ok) return null;
|
|
5830
7747
|
const body = await res.json();
|
|
5831
7748
|
if (body.ok !== true || body.pid !== pid || !servesThisDataDir(body)) return null;
|
|
5832
|
-
return { version: body.version ?? "unknown" };
|
|
7749
|
+
return { version: body.version ?? "unknown", ...typeof body.desktop === "number" ? { desktopContract: body.desktop } : {} };
|
|
5833
7750
|
} catch {
|
|
5834
7751
|
return null;
|
|
5835
7752
|
}
|
|
@@ -5838,7 +7755,7 @@ async function findRunningDaemon() {
|
|
|
5838
7755
|
const info = readRegistry();
|
|
5839
7756
|
if (!info) return null;
|
|
5840
7757
|
const probed = await probeDaemon(info.port, info.pid);
|
|
5841
|
-
if (probed) return { ...info,
|
|
7758
|
+
if (probed) return { ...info, ...probed };
|
|
5842
7759
|
clearRegistry(info.pid);
|
|
5843
7760
|
return null;
|
|
5844
7761
|
}
|
|
@@ -5860,18 +7777,20 @@ async function stopDaemon(daemon) {
|
|
|
5860
7777
|
await new Promise((r) => setTimeout(r, 50));
|
|
5861
7778
|
}
|
|
5862
7779
|
}
|
|
5863
|
-
function launchDaemon(cliEntry2, args = []) {
|
|
5864
|
-
|
|
7780
|
+
function launchDaemon(cliEntry2, args = [], launch = {}) {
|
|
7781
|
+
mkdirSync13(DATA_DIR, { recursive: true });
|
|
5865
7782
|
const log = openSync2(DAEMON_LOG, "a");
|
|
5866
|
-
const
|
|
7783
|
+
const runtime = launch.runtime ?? SELF_NODE;
|
|
7784
|
+
const child = spawn7(runtime.execPath, [cliEntry2, "__daemon", ...args], {
|
|
5867
7785
|
detached: true,
|
|
5868
|
-
stdio: ["ignore", log, log]
|
|
7786
|
+
stdio: ["ignore", log, log],
|
|
7787
|
+
env: selfNodeEnv({ ...process.env, ...launch.env }, runtime)
|
|
5869
7788
|
});
|
|
5870
7789
|
child.unref();
|
|
5871
7790
|
return child;
|
|
5872
7791
|
}
|
|
5873
|
-
async function spawnDaemon(
|
|
5874
|
-
launchDaemon(
|
|
7792
|
+
async function spawnDaemon(launch) {
|
|
7793
|
+
launchDaemon(launch.cliEntry, [], launch);
|
|
5875
7794
|
const deadline = Date.now() + 8e3;
|
|
5876
7795
|
while (Date.now() < deadline) {
|
|
5877
7796
|
await new Promise((r) => setTimeout(r, 150));
|
|
@@ -5881,18 +7800,29 @@ async function spawnDaemon(cliEntry2) {
|
|
|
5881
7800
|
throw new Error(`daemon failed to start within 8s (see ${DAEMON_LOG})`);
|
|
5882
7801
|
}
|
|
5883
7802
|
async function ensureDaemon(cliEntry2, version) {
|
|
7803
|
+
return ensureDaemonWhere(() => ({ cliEntry: cliEntry2 }), (running2) => running2.version === version);
|
|
7804
|
+
}
|
|
7805
|
+
async function ensureDaemonWhere(launch, accepts) {
|
|
5884
7806
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
5885
|
-
const running2 = await findRunningDaemon() ?? await spawnDaemon(
|
|
5886
|
-
if (running2
|
|
7807
|
+
const running2 = await findRunningDaemon() ?? await spawnDaemon(await launch());
|
|
7808
|
+
if (accepts(running2)) return running2;
|
|
5887
7809
|
await stopDaemon(running2);
|
|
5888
7810
|
}
|
|
5889
7811
|
throw new Error(`a daemon running another version keeps taking over (see ${DAEMON_LOG})`);
|
|
5890
7812
|
}
|
|
7813
|
+
async function resolveDaemon(cliEntry2, version) {
|
|
7814
|
+
const running2 = await findRunningDaemon();
|
|
7815
|
+
if (running2 && isNewer(running2.version, version)) {
|
|
7816
|
+
const entry = join21(installedPackageRoot(running2.version), "dist", "cli.mjs");
|
|
7817
|
+
if (existsSync12(entry)) return { kind: "newer", daemon: running2, cliEntry: entry };
|
|
7818
|
+
}
|
|
7819
|
+
return { kind: "ours", daemon: await ensureDaemon(cliEntry2, version) };
|
|
7820
|
+
}
|
|
5891
7821
|
|
|
5892
7822
|
// src/library/index.ts
|
|
5893
7823
|
import { createHash as createHash6 } from "node:crypto";
|
|
5894
|
-
import { createReadStream, mkdirSync as
|
|
5895
|
-
import { resolve as
|
|
7824
|
+
import { createReadStream, mkdirSync as mkdirSync15, writeFileSync as writeFileSync10 } from "node:fs";
|
|
7825
|
+
import { resolve as resolve5 } from "node:path";
|
|
5896
7826
|
|
|
5897
7827
|
// src/library/internal/errors.ts
|
|
5898
7828
|
var LibraryError = class extends Error {
|
|
@@ -5921,13 +7851,13 @@ function asLibraryError(error) {
|
|
|
5921
7851
|
}
|
|
5922
7852
|
|
|
5923
7853
|
// src/library/internal/files.ts
|
|
5924
|
-
import { createHash as createHash5, randomBytes as
|
|
5925
|
-
import { existsSync as
|
|
7854
|
+
import { createHash as createHash5, randomBytes as randomBytes8 } from "node:crypto";
|
|
7855
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync14, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync11 } from "node:fs";
|
|
5926
7856
|
import { open } from "node:fs/promises";
|
|
5927
|
-
import { dirname as
|
|
7857
|
+
import { dirname as dirname7, join as join22 } from "node:path";
|
|
5928
7858
|
|
|
5929
7859
|
// src/library/internal/ulid.ts
|
|
5930
|
-
import { randomBytes as
|
|
7860
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
5931
7861
|
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
5932
7862
|
var RANDOM_MAX = (1n << 80n) - 1n;
|
|
5933
7863
|
var lastTime = -1;
|
|
@@ -5945,7 +7875,7 @@ function ulid(now = Date.now()) {
|
|
|
5945
7875
|
lastRandom += 1n;
|
|
5946
7876
|
} else {
|
|
5947
7877
|
lastTime = Math.max(now, lastTime);
|
|
5948
|
-
lastRandom = BigInt(`0x${
|
|
7878
|
+
lastRandom = BigInt(`0x${randomBytes7(10).toString("hex")}`) >> 1n;
|
|
5949
7879
|
}
|
|
5950
7880
|
return encode(BigInt(lastTime), 10) + encode(lastRandom, 16);
|
|
5951
7881
|
}
|
|
@@ -5954,15 +7884,15 @@ function ulid(now = Date.now()) {
|
|
|
5954
7884
|
function libraryPaths(root) {
|
|
5955
7885
|
return {
|
|
5956
7886
|
root,
|
|
5957
|
-
db:
|
|
5958
|
-
blobs:
|
|
5959
|
-
thumbs:
|
|
7887
|
+
db: join22(root, "library.db"),
|
|
7888
|
+
blobs: join22(root, "blobs"),
|
|
7889
|
+
thumbs: join22(root, "thumbs"),
|
|
5960
7890
|
// Under the root so sealing a stage into `blobs/` is a rename on one filesystem.
|
|
5961
|
-
staging:
|
|
7891
|
+
staging: join22(root, "staging")
|
|
5962
7892
|
};
|
|
5963
7893
|
}
|
|
5964
7894
|
function contentPath(dir, sha256, ext) {
|
|
5965
|
-
return
|
|
7895
|
+
return join22(dir, sha256.slice(0, 2), `${sha256}.${ext}`);
|
|
5966
7896
|
}
|
|
5967
7897
|
var SNIFF_BYTES = 4096;
|
|
5968
7898
|
var ascii = (bytes, start, end) => String.fromCharCode(...bytes.subarray(start, end));
|
|
@@ -6005,10 +7935,10 @@ var StagedFile = class {
|
|
|
6005
7935
|
thumbPath;
|
|
6006
7936
|
framePath;
|
|
6007
7937
|
constructor(dir, name) {
|
|
6008
|
-
this.partPath =
|
|
6009
|
-
this.sealedPath =
|
|
6010
|
-
this.thumbPath =
|
|
6011
|
-
this.framePath =
|
|
7938
|
+
this.partPath = join22(dir, `${name}.part`);
|
|
7939
|
+
this.sealedPath = join22(dir, `${name}.bytes`);
|
|
7940
|
+
this.thumbPath = join22(dir, `${name}.thumb.webp`);
|
|
7941
|
+
this.framePath = join22(dir, `${name}.frame.png`);
|
|
6012
7942
|
}
|
|
6013
7943
|
get bytes() {
|
|
6014
7944
|
return this.written;
|
|
@@ -6038,7 +7968,7 @@ var StagedFile = class {
|
|
|
6038
7968
|
await this.handle.sync();
|
|
6039
7969
|
await this.handle.close();
|
|
6040
7970
|
this.handle = null;
|
|
6041
|
-
|
|
7971
|
+
renameSync5(this.partPath, this.sealedPath);
|
|
6042
7972
|
return this.hash.digest("hex");
|
|
6043
7973
|
}
|
|
6044
7974
|
remove() {
|
|
@@ -6047,7 +7977,7 @@ var StagedFile = class {
|
|
|
6047
7977
|
});
|
|
6048
7978
|
this.handle = null;
|
|
6049
7979
|
}
|
|
6050
|
-
for (const path of [this.partPath, this.sealedPath, this.thumbPath, this.framePath])
|
|
7980
|
+
for (const path of [this.partPath, this.sealedPath, this.thumbPath, this.framePath]) rmSync11(path, { force: true });
|
|
6051
7981
|
}
|
|
6052
7982
|
};
|
|
6053
7983
|
function isAlive(pid) {
|
|
@@ -6061,22 +7991,22 @@ function isAlive(pid) {
|
|
|
6061
7991
|
var unreleased = /* @__PURE__ */ new Set();
|
|
6062
7992
|
var exitHookInstalled = false;
|
|
6063
7993
|
function sweepDeadStaging(paths) {
|
|
6064
|
-
for (const name of
|
|
7994
|
+
for (const name of readdirSync8(paths.staging)) {
|
|
6065
7995
|
const pid = Number(name.split("-")[0]);
|
|
6066
7996
|
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid && !isAlive(pid)) {
|
|
6067
|
-
|
|
7997
|
+
rmSync11(join22(paths.staging, name), { recursive: true, force: true });
|
|
6068
7998
|
}
|
|
6069
7999
|
}
|
|
6070
8000
|
}
|
|
6071
8001
|
function claimStaging(paths) {
|
|
6072
|
-
const dir =
|
|
6073
|
-
|
|
8002
|
+
const dir = join22(paths.staging, `${process.pid}-${randomBytes8(4).toString("hex")}`);
|
|
8003
|
+
mkdirSync14(dir);
|
|
6074
8004
|
const area = {
|
|
6075
8005
|
dir,
|
|
6076
8006
|
create: () => new StagedFile(dir, ulid()),
|
|
6077
8007
|
release: () => {
|
|
6078
8008
|
unreleased.delete(area);
|
|
6079
|
-
|
|
8009
|
+
rmSync11(dir, { recursive: true, force: true });
|
|
6080
8010
|
}
|
|
6081
8011
|
};
|
|
6082
8012
|
unreleased.add(area);
|
|
@@ -6089,12 +8019,12 @@ function claimStaging(paths) {
|
|
|
6089
8019
|
return area;
|
|
6090
8020
|
}
|
|
6091
8021
|
function placeContent(from, target) {
|
|
6092
|
-
if (
|
|
6093
|
-
|
|
8022
|
+
if (existsSync13(target)) {
|
|
8023
|
+
rmSync11(from, { force: true });
|
|
6094
8024
|
return "existed";
|
|
6095
8025
|
}
|
|
6096
|
-
|
|
6097
|
-
|
|
8026
|
+
mkdirSync14(dirname7(target), { recursive: true });
|
|
8027
|
+
renameSync5(from, target);
|
|
6098
8028
|
return "placed";
|
|
6099
8029
|
}
|
|
6100
8030
|
|
|
@@ -6128,24 +8058,6 @@ function pngSize(bytes) {
|
|
|
6128
8058
|
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
6129
8059
|
}
|
|
6130
8060
|
|
|
6131
|
-
// src/library/policy.ts
|
|
6132
|
-
var VIDEO_CAP_BYTES = 2e8;
|
|
6133
|
-
var THUMBNAIL_LONG_SIDE = 400;
|
|
6134
|
-
var TAG_MAX_LENGTH = 64;
|
|
6135
|
-
function normalizeTag(raw) {
|
|
6136
|
-
const name = raw.normalize("NFC").trim().replace(/\s+/g, " ");
|
|
6137
|
-
if (name === "" || name.length > TAG_MAX_LENGTH || /[,,]/.test(name)) return null;
|
|
6138
|
-
return lowercaseInitial(name);
|
|
6139
|
-
}
|
|
6140
|
-
function lowercaseInitial(name) {
|
|
6141
|
-
const [first = "", second = ""] = name;
|
|
6142
|
-
if (second !== second.toLowerCase()) return name;
|
|
6143
|
-
return first.toLowerCase() + name.slice(first.length);
|
|
6144
|
-
}
|
|
6145
|
-
function tagKey(name) {
|
|
6146
|
-
return name.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
6147
|
-
}
|
|
6148
|
-
|
|
6149
8061
|
// src/library/internal/store.ts
|
|
6150
8062
|
var SCHEMA = { epoch: 1, version: 2 };
|
|
6151
8063
|
var NODE_FLOOR = "22.13.0";
|
|
@@ -6270,14 +8182,14 @@ async function openStore(sqlite, file) {
|
|
|
6270
8182
|
}
|
|
6271
8183
|
}
|
|
6272
8184
|
var OPEN_PATIENCE_MS = 5e3;
|
|
6273
|
-
async function patiently(
|
|
8185
|
+
async function patiently(step2) {
|
|
6274
8186
|
const deadline = Date.now() + OPEN_PATIENCE_MS;
|
|
6275
8187
|
for (; ; ) {
|
|
6276
8188
|
try {
|
|
6277
|
-
return
|
|
8189
|
+
return step2();
|
|
6278
8190
|
} catch (error) {
|
|
6279
8191
|
if (!isBusy(error) || Date.now() > deadline) throw error;
|
|
6280
|
-
await new Promise((
|
|
8192
|
+
await new Promise((resolve7) => setTimeout(resolve7, 10 + Math.random() * 40));
|
|
6281
8193
|
}
|
|
6282
8194
|
}
|
|
6283
8195
|
}
|
|
@@ -6546,7 +8458,7 @@ var SUGGEST_LIMIT = 8;
|
|
|
6546
8458
|
var FIND_LIMIT = 50;
|
|
6547
8459
|
var clamp2 = (value, min, max) => Math.min(max, Math.max(min, Math.floor(value)));
|
|
6548
8460
|
async function openLibrary(options = {}) {
|
|
6549
|
-
const paths = libraryPaths(
|
|
8461
|
+
const paths = libraryPaths(resolve5(options.dir ?? libraryDir()));
|
|
6550
8462
|
const sqlite = await loadSqlite();
|
|
6551
8463
|
ensureLayout(paths);
|
|
6552
8464
|
const store = await openStore(sqlite, paths.db);
|
|
@@ -6560,7 +8472,7 @@ async function openLibrary(options = {}) {
|
|
|
6560
8472
|
}
|
|
6561
8473
|
function ensureLayout(paths) {
|
|
6562
8474
|
try {
|
|
6563
|
-
for (const dir of [paths.root, paths.blobs, paths.thumbs, paths.staging])
|
|
8475
|
+
for (const dir of [paths.root, paths.blobs, paths.thumbs, paths.staging]) mkdirSync15(dir, { recursive: true });
|
|
6564
8476
|
} catch (error) {
|
|
6565
8477
|
throw asLibraryError(error);
|
|
6566
8478
|
}
|
|
@@ -6881,12 +8793,22 @@ var Stage = class {
|
|
|
6881
8793
|
};
|
|
6882
8794
|
|
|
6883
8795
|
// src/library-routes.ts
|
|
6884
|
-
import
|
|
8796
|
+
import express3, { Router as Router4 } from "express";
|
|
6885
8797
|
var PAGE_LIMIT = 60;
|
|
6886
8798
|
var MAX_TAGS = 100;
|
|
6887
8799
|
var MAX_NOTE = 1e4;
|
|
6888
8800
|
var ITEM_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
6889
8801
|
var MAX_PASTE_BYTES = 64 * 1024 * 1024;
|
|
8802
|
+
function storedFileOptions(mediaType) {
|
|
8803
|
+
return {
|
|
8804
|
+
dotfiles: "allow",
|
|
8805
|
+
headers: {
|
|
8806
|
+
"content-type": mediaType,
|
|
8807
|
+
"x-content-type-options": "nosniff",
|
|
8808
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox"
|
|
8809
|
+
}
|
|
8810
|
+
};
|
|
8811
|
+
}
|
|
6890
8812
|
var itemUrl = (id, file, sha256) => `${URL_PREFIX}/api/library/items/${id}/${file}?v=${sha256.slice(0, 16)}`;
|
|
6891
8813
|
function storedOriginal(item) {
|
|
6892
8814
|
return item.content.type === "file" ? item.content : item.content.frame;
|
|
@@ -6898,10 +8820,19 @@ function libraryAttachables(library, ids) {
|
|
|
6898
8820
|
const file = item && storedOriginal(item);
|
|
6899
8821
|
if (!item || !file) return { missing: id };
|
|
6900
8822
|
const { pageUrl, pageTitle, mediaUrl } = item.source;
|
|
8823
|
+
const { content } = item;
|
|
6901
8824
|
ok2.push({
|
|
6902
8825
|
title: pageTitle || hostOf(pageUrl),
|
|
6903
8826
|
file,
|
|
6904
|
-
origin: {
|
|
8827
|
+
origin: {
|
|
8828
|
+
libraryItemId: item.id,
|
|
8829
|
+
pageUrl,
|
|
8830
|
+
pageTitle,
|
|
8831
|
+
mediaUrl,
|
|
8832
|
+
note: item.note,
|
|
8833
|
+
tags: [...item.tags],
|
|
8834
|
+
...content.type === "link" ? { videoFrame: { reason: content.reason, playbackTime: content.playbackTime } } : {}
|
|
8835
|
+
}
|
|
6905
8836
|
});
|
|
6906
8837
|
}
|
|
6907
8838
|
return { ok: ok2 };
|
|
@@ -7005,7 +8936,7 @@ function refuse(res, status, code, error) {
|
|
|
7005
8936
|
res.status(status).json({ error, code });
|
|
7006
8937
|
}
|
|
7007
8938
|
function libraryApiRouter(host) {
|
|
7008
|
-
const router =
|
|
8939
|
+
const router = Router4();
|
|
7009
8940
|
const withLibrary = (handle) => (req, res, next) => {
|
|
7010
8941
|
host.library().then(
|
|
7011
8942
|
(library) => {
|
|
@@ -7053,17 +8984,10 @@ function libraryApiRouter(host) {
|
|
|
7053
8984
|
res.sendFile(
|
|
7054
8985
|
stored.path,
|
|
7055
8986
|
{
|
|
7056
|
-
|
|
7057
|
-
dotfiles: "allow",
|
|
8987
|
+
...storedFileOptions(stored.mediaType),
|
|
7058
8988
|
// Content-addressed: a url names its bytes for good.
|
|
7059
8989
|
maxAge: req.query.v ? "1y" : 0,
|
|
7060
|
-
immutable: Boolean(req.query.v)
|
|
7061
|
-
headers: {
|
|
7062
|
-
"content-type": stored.mediaType,
|
|
7063
|
-
"x-content-type-options": "nosniff",
|
|
7064
|
-
// A saved SVG opened on its own must not run on the session origin.
|
|
7065
|
-
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox"
|
|
7066
|
-
}
|
|
8990
|
+
immutable: Boolean(req.query.v)
|
|
7067
8991
|
},
|
|
7068
8992
|
(error) => {
|
|
7069
8993
|
if (error && !res.headersSent) refuse(res, 404, "not-found", `the ${file} of ${req.params.id} is missing`);
|
|
@@ -7073,7 +8997,7 @@ function libraryApiRouter(host) {
|
|
|
7073
8997
|
})
|
|
7074
8998
|
);
|
|
7075
8999
|
}
|
|
7076
|
-
router.post("/library/items",
|
|
9000
|
+
router.post("/library/items", express3.raw({ type: "multipart/form-data", limit: MAX_PASTE_BYTES }), (req, res, next) => {
|
|
7077
9001
|
void (async () => {
|
|
7078
9002
|
const paste = await parsePaste(req);
|
|
7079
9003
|
if (!paste) return refuse(res, 400, "bad-request", "a paste is a multipart form of original, thumbnail and meta");
|
|
@@ -7118,15 +9042,15 @@ function lazyLibrary(open3) {
|
|
|
7118
9042
|
}
|
|
7119
9043
|
|
|
7120
9044
|
// src/extension-routes.ts
|
|
7121
|
-
import { Router as
|
|
9045
|
+
import { Router as Router5 } from "express";
|
|
7122
9046
|
|
|
7123
9047
|
// src/capture-seen.ts
|
|
7124
|
-
import { mkdirSync as
|
|
7125
|
-
import { dirname as
|
|
7126
|
-
var seenFile = (dataDir) =>
|
|
9048
|
+
import { mkdirSync as mkdirSync16, readFileSync as readFileSync18 } from "node:fs";
|
|
9049
|
+
import { dirname as dirname8, join as join23 } from "node:path";
|
|
9050
|
+
var seenFile = (dataDir) => join23(nativeHostDir(dataDir), "extension-seen.json");
|
|
7127
9051
|
function readExtensionSeen(dataDir) {
|
|
7128
9052
|
try {
|
|
7129
|
-
const { version, at } = JSON.parse(
|
|
9053
|
+
const { version, at } = JSON.parse(readFileSync18(seenFile(dataDir), "utf8"));
|
|
7130
9054
|
return typeof version === "string" && typeof at === "number" ? { version, at } : null;
|
|
7131
9055
|
} catch {
|
|
7132
9056
|
return null;
|
|
@@ -7135,7 +9059,7 @@ function readExtensionSeen(dataDir) {
|
|
|
7135
9059
|
|
|
7136
9060
|
// src/extension-routes.ts
|
|
7137
9061
|
function extensionApiRouter(placement) {
|
|
7138
|
-
const router =
|
|
9062
|
+
const router = Router5();
|
|
7139
9063
|
router.get("/extension/status", (_req, res) => {
|
|
7140
9064
|
res.set("cache-control", "no-store").json({ greeted: readExtensionSeen(placement.dataDir) });
|
|
7141
9065
|
});
|
|
@@ -7155,9 +9079,9 @@ function extensionApiRouter(placement) {
|
|
|
7155
9079
|
}
|
|
7156
9080
|
|
|
7157
9081
|
// src/announcement-routes.ts
|
|
7158
|
-
import { Router as
|
|
9082
|
+
import { Router as Router6 } from "express";
|
|
7159
9083
|
function announcementApiRouter() {
|
|
7160
|
-
const router =
|
|
9084
|
+
const router = Router6();
|
|
7161
9085
|
router.get("/announcements/seen", (_req, res) => {
|
|
7162
9086
|
res.set("cache-control", "no-store").json({ seen: seenAnnouncements() });
|
|
7163
9087
|
});
|
|
@@ -7170,7 +9094,7 @@ function announcementApiRouter() {
|
|
|
7170
9094
|
}
|
|
7171
9095
|
|
|
7172
9096
|
// src/account.ts
|
|
7173
|
-
import { createHash as createHash7, randomBytes as
|
|
9097
|
+
import { createHash as createHash7, randomBytes as randomBytes9 } from "node:crypto";
|
|
7174
9098
|
var DEFAULT_API_URL = "https://api.ezdesign.dev";
|
|
7175
9099
|
var ACCOUNT_CALLBACK_PATH = `${URL_PREFIX}/account/callback`;
|
|
7176
9100
|
var CLIENT_ID = APP_NAME;
|
|
@@ -7208,8 +9132,8 @@ var Account = class {
|
|
|
7208
9132
|
}
|
|
7209
9133
|
authorizeUrl(redirectUri) {
|
|
7210
9134
|
this.dropStale();
|
|
7211
|
-
const verifier = base64url(
|
|
7212
|
-
const state = base64url(
|
|
9135
|
+
const verifier = base64url(randomBytes9(32));
|
|
9136
|
+
const state = base64url(randomBytes9(16));
|
|
7213
9137
|
this.pendingByState.set(state, { verifier, redirectUri, at: this.now() });
|
|
7214
9138
|
const url = new URL(`${this.deps.baseUrl}/authorize`);
|
|
7215
9139
|
url.search = new URLSearchParams({
|
|
@@ -7288,9 +9212,9 @@ var Account = class {
|
|
|
7288
9212
|
};
|
|
7289
9213
|
|
|
7290
9214
|
// src/account-routes.ts
|
|
7291
|
-
import { Router as
|
|
9215
|
+
import { Router as Router7 } from "express";
|
|
7292
9216
|
function accountApiRouter(host) {
|
|
7293
|
-
const router =
|
|
9217
|
+
const router = Router7();
|
|
7294
9218
|
router.get("/account/login", (_req, res) => {
|
|
7295
9219
|
res.set("cache-control", "no-store").redirect(303, host.account.authorizeUrl(host.callbackUrl()));
|
|
7296
9220
|
});
|
|
@@ -7357,111 +9281,8 @@ ${page.close ? "<script>window.close()</script>" : ""}
|
|
|
7357
9281
|
</html>`;
|
|
7358
9282
|
}
|
|
7359
9283
|
|
|
7360
|
-
// src/installer.ts
|
|
7361
|
-
import { spawn as spawn5 } from "node:child_process";
|
|
7362
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync15, readdirSync as readdirSync8, realpathSync as realpathSync2, rmSync as rmSync10 } from "node:fs";
|
|
7363
|
-
import { dirname as dirname7, join as join17, resolve as resolve3, sep } from "node:path";
|
|
7364
|
-
function npmCli() {
|
|
7365
|
-
const bin = dirname7(process.execPath);
|
|
7366
|
-
for (const candidate of [
|
|
7367
|
-
join17(bin, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
7368
|
-
join17(bin, "node_modules", "npm", "bin", "npm-cli.js")
|
|
7369
|
-
]) {
|
|
7370
|
-
if (existsSync10(candidate)) return candidate;
|
|
7371
|
-
}
|
|
7372
|
-
return null;
|
|
7373
|
-
}
|
|
7374
|
-
function npmErrorSummary(stderr) {
|
|
7375
|
-
const lines = stderr.split("\n").map((l) => l.trim());
|
|
7376
|
-
const own = lines.filter((l) => /^npm (error|ERR!)/.test(l)).map((l) => l.replace(/^npm (error|ERR!)\s*/, "")).filter((l) => l && !/^A complete log of this run/.test(l));
|
|
7377
|
-
if (own.length) return own.slice(0, 2).join(" ");
|
|
7378
|
-
return lines.filter(Boolean).at(-1) ?? "";
|
|
7379
|
-
}
|
|
7380
|
-
function run(label, command, args, opts = {}) {
|
|
7381
|
-
return new Promise((resolve6, reject) => {
|
|
7382
|
-
const child = spawn5(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: opts.shell });
|
|
7383
|
-
let stdout = "";
|
|
7384
|
-
let stderr = "";
|
|
7385
|
-
child.stdout.on("data", (c) => stdout += c);
|
|
7386
|
-
child.stderr.on("data", (c) => stderr += c);
|
|
7387
|
-
child.on("error", (err) => reject(new Error(`${label} could not start: ${err.message}`)));
|
|
7388
|
-
child.on("close", (code) => {
|
|
7389
|
-
if (code === 0) return resolve6({ stdout, stderr });
|
|
7390
|
-
const why = npmErrorSummary(stderr);
|
|
7391
|
-
reject(new Error(`${label} failed${why ? `: ${why}` : ` (exit ${code})`}`));
|
|
7392
|
-
});
|
|
7393
|
-
});
|
|
7394
|
-
}
|
|
7395
|
-
async function npm(label, args) {
|
|
7396
|
-
const cli = npmCli();
|
|
7397
|
-
if (cli) {
|
|
7398
|
-
await run(label, process.execPath, [cli, ...args]);
|
|
7399
|
-
return;
|
|
7400
|
-
}
|
|
7401
|
-
await run(label, process.platform === "win32" ? "npm.cmd" : "npm", args.map(shellQuote), {
|
|
7402
|
-
shell: true
|
|
7403
|
-
});
|
|
7404
|
-
}
|
|
7405
|
-
function packageSpec(version) {
|
|
7406
|
-
return `${NPM_PACKAGE}@${version}`;
|
|
7407
|
-
}
|
|
7408
|
-
function installedPackageRoot(version) {
|
|
7409
|
-
return join17(VERSIONS_DIR, version, "node_modules", NPM_PACKAGE);
|
|
7410
|
-
}
|
|
7411
|
-
async function verifyInstall(pkgRoot, version) {
|
|
7412
|
-
const cliEntry2 = join17(pkgRoot, "dist", "cli.mjs");
|
|
7413
|
-
if (!existsSync10(cliEntry2)) return false;
|
|
7414
|
-
try {
|
|
7415
|
-
const { stdout } = await run(`${APP_NAME} --version`, process.execPath, [cliEntry2, "--version"]);
|
|
7416
|
-
return stdout.trim() === version;
|
|
7417
|
-
} catch {
|
|
7418
|
-
return false;
|
|
7419
|
-
}
|
|
7420
|
-
}
|
|
7421
|
-
async function installVersion(version) {
|
|
7422
|
-
const pkgRoot = installedPackageRoot(version);
|
|
7423
|
-
if (await verifyInstall(pkgRoot, version)) return pkgRoot;
|
|
7424
|
-
const prefix = join17(VERSIONS_DIR, version);
|
|
7425
|
-
mkdirSync15(prefix, { recursive: true });
|
|
7426
|
-
await npm(`npm install ${packageSpec(version)}`, [
|
|
7427
|
-
"install",
|
|
7428
|
-
"--prefix",
|
|
7429
|
-
prefix,
|
|
7430
|
-
"--no-audit",
|
|
7431
|
-
"--no-fund",
|
|
7432
|
-
"--no-package-lock",
|
|
7433
|
-
"--loglevel=error",
|
|
7434
|
-
packageSpec(version)
|
|
7435
|
-
]);
|
|
7436
|
-
if (!await verifyInstall(pkgRoot, version)) {
|
|
7437
|
-
throw new Error(`the installed ${packageSpec(version)} does not run as v${version}`);
|
|
7438
|
-
}
|
|
7439
|
-
return pkgRoot;
|
|
7440
|
-
}
|
|
7441
|
-
function realOrResolved(path) {
|
|
7442
|
-
try {
|
|
7443
|
-
return realpathSync2(path);
|
|
7444
|
-
} catch {
|
|
7445
|
-
return resolve3(path);
|
|
7446
|
-
}
|
|
7447
|
-
}
|
|
7448
|
-
function pruneInstalledVersions(runningFrom, root = VERSIONS_DIR) {
|
|
7449
|
-
let versions;
|
|
7450
|
-
try {
|
|
7451
|
-
versions = readdirSync8(root);
|
|
7452
|
-
} catch {
|
|
7453
|
-
return;
|
|
7454
|
-
}
|
|
7455
|
-
const running2 = realOrResolved(runningFrom);
|
|
7456
|
-
for (const version of versions) {
|
|
7457
|
-
const dir = join17(root, version);
|
|
7458
|
-
if (running2.startsWith(realOrResolved(dir) + sep)) continue;
|
|
7459
|
-
rmSync10(dir, { recursive: true, force: true });
|
|
7460
|
-
}
|
|
7461
|
-
}
|
|
7462
|
-
|
|
7463
9284
|
// src/updater.ts
|
|
7464
|
-
import { join as
|
|
9285
|
+
import { join as join24 } from "node:path";
|
|
7465
9286
|
var Updater = class {
|
|
7466
9287
|
constructor(deps) {
|
|
7467
9288
|
this.deps = deps;
|
|
@@ -7513,7 +9334,7 @@ var Updater = class {
|
|
|
7513
9334
|
this.set("installing");
|
|
7514
9335
|
const pkgRoot = await this.deps.install(target);
|
|
7515
9336
|
this.set("handing-over");
|
|
7516
|
-
await this.deps.handover(
|
|
9337
|
+
await this.deps.handover(join24(pkgRoot, "dist", "cli.mjs"));
|
|
7517
9338
|
await this.deps.retire();
|
|
7518
9339
|
} catch (err) {
|
|
7519
9340
|
this.error = err instanceof Error ? err.message : String(err);
|
|
@@ -7522,48 +9343,27 @@ var Updater = class {
|
|
|
7522
9343
|
}
|
|
7523
9344
|
};
|
|
7524
9345
|
|
|
7525
|
-
// src/version.ts
|
|
7526
|
-
var VERSION_HEADER = `x-${APP_NAME}-version`;
|
|
7527
|
-
function versionMismatchBody(cliVersion, daemonVersion) {
|
|
7528
|
-
return {
|
|
7529
|
-
error: cliVersion ? `this ${APP_NAME} CLI is v${cliVersion} but the daemon is running v${daemonVersion}` : `this ${APP_NAME} CLI predates the running daemon (v${daemonVersion})`,
|
|
7530
|
-
hint: `re-run as \`npx -y ${NPM_PACKAGE}@latest <url>\` to bring both to the current version`
|
|
7531
|
-
};
|
|
7532
|
-
}
|
|
7533
|
-
function versionGate(daemonVersion) {
|
|
7534
|
-
return (req, res, next) => {
|
|
7535
|
-
const cliVersion = req.get(VERSION_HEADER) || void 0;
|
|
7536
|
-
if (cliVersion === daemonVersion) return next();
|
|
7537
|
-
res.status(409).json(versionMismatchBody(cliVersion, daemonVersion));
|
|
7538
|
-
};
|
|
7539
|
-
}
|
|
7540
|
-
|
|
7541
9346
|
// src/daemon.ts
|
|
7542
9347
|
function agentInstalled(profile, env = process.env) {
|
|
7543
|
-
return (env.PATH
|
|
7544
|
-
try {
|
|
7545
|
-
accessSync(join19(dir, profile.binary), constants.X_OK);
|
|
7546
|
-
return true;
|
|
7547
|
-
} catch {
|
|
7548
|
-
return false;
|
|
7549
|
-
}
|
|
7550
|
-
});
|
|
9348
|
+
return findExecutable(profile.binary, env.PATH) !== null;
|
|
7551
9349
|
}
|
|
7552
|
-
|
|
9350
|
+
function defaultAgent() {
|
|
9351
|
+
return proposingAgent([rememberedLastAgent()], (p) => agentInstalled(p));
|
|
9352
|
+
}
|
|
9353
|
+
var distDir = dirname9(fileURLToPath(import.meta.url));
|
|
7553
9354
|
var SKILL_DIR = skillDir(distDir);
|
|
7554
|
-
var
|
|
7555
|
-
var SELF_CLI = [process.execPath, ...process.execArgv, process.argv[1] ?? ""].filter(Boolean).map(shellWord).join(" ");
|
|
9355
|
+
var SELF_CLI = selfNodeCommandLine([...process.execArgv, process.argv[1] ?? ""].filter(Boolean));
|
|
7556
9356
|
function inspireAccess() {
|
|
7557
9357
|
return readInspireToken() ? { cli: SELF_CLI } : "unavailable";
|
|
7558
9358
|
}
|
|
7559
|
-
var asset = (name) =>
|
|
9359
|
+
var asset = (name) => readFileSync19(join25(distDir, name));
|
|
7560
9360
|
var overlayKeyed = null;
|
|
7561
9361
|
function overlayAssetKey() {
|
|
7562
|
-
const files = ["overlay.js", "overlay.css"].map((name) =>
|
|
9362
|
+
const files = ["overlay.js", "overlay.css"].map((name) => join25(distDir, name));
|
|
7563
9363
|
let stamp;
|
|
7564
9364
|
try {
|
|
7565
9365
|
stamp = files.map((file) => {
|
|
7566
|
-
const stat =
|
|
9366
|
+
const stat = statSync8(file);
|
|
7567
9367
|
return `${stat.mtimeMs}:${stat.size}`;
|
|
7568
9368
|
}).join("|");
|
|
7569
9369
|
} catch {
|
|
@@ -7571,7 +9371,7 @@ function overlayAssetKey() {
|
|
|
7571
9371
|
}
|
|
7572
9372
|
if (overlayKeyed?.stamp !== stamp) {
|
|
7573
9373
|
const hash = createHash8("sha1");
|
|
7574
|
-
for (const file of files) hash.update(
|
|
9374
|
+
for (const file of files) hash.update(readFileSync19(file));
|
|
7575
9375
|
overlayKeyed = { stamp, key: hash.digest("hex").slice(0, 12) };
|
|
7576
9376
|
}
|
|
7577
9377
|
return overlayKeyed.key;
|
|
@@ -7585,7 +9385,7 @@ function decodeAttachmentName(raw) {
|
|
|
7585
9385
|
}
|
|
7586
9386
|
}
|
|
7587
9387
|
var SHELL_ICON = `data:image/svg+xml,${encodeURIComponent(brandMarkSvg())}`;
|
|
7588
|
-
var shellHtml = (version) => `<!doctype html>
|
|
9388
|
+
var shellHtml = (version, app) => `<!doctype html>
|
|
7589
9389
|
<html lang="en">
|
|
7590
9390
|
<head>
|
|
7591
9391
|
<meta charset="utf-8">
|
|
@@ -7596,11 +9396,11 @@ var shellHtml = (version) => `<!doctype html>
|
|
|
7596
9396
|
</head>
|
|
7597
9397
|
<body>
|
|
7598
9398
|
<div id="ez-shell"></div>
|
|
7599
|
-
<script src="${URL_PREFIX}/shell.js" data-version="${version}"></script>
|
|
9399
|
+
<script src="${URL_PREFIX}/shell.js" data-version="${version}" data-app="${app ?? ""}"></script>
|
|
7600
9400
|
</body>
|
|
7601
9401
|
</html>`;
|
|
7602
9402
|
function listen(app, port) {
|
|
7603
|
-
return new Promise((
|
|
9403
|
+
return new Promise((resolve7, reject) => {
|
|
7604
9404
|
const server = createServer(app);
|
|
7605
9405
|
server.once("error", reject);
|
|
7606
9406
|
server.listen(port, "127.0.0.1", () => {
|
|
@@ -7608,7 +9408,7 @@ function listen(app, port) {
|
|
|
7608
9408
|
server.on("error", (err) => {
|
|
7609
9409
|
console.error(`server on port ${port} failed: ${err.message}`);
|
|
7610
9410
|
});
|
|
7611
|
-
|
|
9411
|
+
resolve7(server);
|
|
7612
9412
|
});
|
|
7613
9413
|
});
|
|
7614
9414
|
}
|
|
@@ -7659,9 +9459,10 @@ function acpPrompt(feedback, skills, agent, truth, history = [], effort = "high"
|
|
|
7659
9459
|
] : [],
|
|
7660
9460
|
"Each item resolves to source: trust `anchor.source` (file:line) when present, else",
|
|
7661
9461
|
"use `anchor.components` / `anchor.section` / `anchor.selector` / `anchor.text`.",
|
|
7662
|
-
"`[file n]` in a comment is `attachments[n-1]` (read the file at `path
|
|
7663
|
-
"`origin` is a reference from their library:
|
|
7664
|
-
"
|
|
9462
|
+
"`[file n]` in a comment is `attachments[n-1]` (read the file at `path`, or, for a video,",
|
|
9463
|
+
"what its `video.read` says); one with an `origin` is a reference from their library:",
|
|
9464
|
+
"`origin.pageUrl` is where it was captured, `origin.note` what they wrote about it, and",
|
|
9465
|
+
"`origin.videoFrame` that the file is one still of a video the library kept only as a link;",
|
|
7665
9466
|
"`[ref n]` names the entry in `references` whose `n` matches.",
|
|
7666
9467
|
"A reference carrying a `variant` is one the user chose in an explore round. Its html is",
|
|
7667
9468
|
"the look they settled on, not code to paste: it is a stand-in that rendered in a shadow",
|
|
@@ -7707,7 +9508,8 @@ function explorePrompt(round, capture, files, truth, earlier, language) {
|
|
|
7707
9508
|
round.direction ? `They asked for: ${round.direction}` : "They gave no direction: they want to see how it could be arranged to read more easily and look cleaner. Range across genuinely different arrangements - what leads, what recedes, how it is grouped, how much it shows - rather than varying one thing.",
|
|
7708
9509
|
...attachments?.length ? [
|
|
7709
9510
|
"",
|
|
7710
|
-
"`[file n]` in that direction is `attachments[n-1]` here - read the file at `path
|
|
9511
|
+
"`[file n]` in that direction is `attachments[n-1]` here - read the file at `path`, or,",
|
|
9512
|
+
"for a video, what its `video.read` says:",
|
|
7711
9513
|
"",
|
|
7712
9514
|
"```json",
|
|
7713
9515
|
JSON.stringify(attachments, null, 2),
|
|
@@ -7879,13 +9681,32 @@ function configChangeNote(option, value) {
|
|
|
7879
9681
|
return `\u5DF2\u5207\u63DB${label}\uFF1A${configValueName(option, value)}`;
|
|
7880
9682
|
}
|
|
7881
9683
|
var SessionRuntime = class {
|
|
7882
|
-
constructor(project, version, updater, library, account) {
|
|
9684
|
+
constructor(project, version, updater, library, account, services) {
|
|
7883
9685
|
this.project = project;
|
|
7884
9686
|
this.version = version;
|
|
7885
9687
|
this.updater = updater;
|
|
7886
9688
|
this.library = library;
|
|
7887
9689
|
this.account = account;
|
|
9690
|
+
this.services = services;
|
|
7888
9691
|
this.store = new SessionStore(project);
|
|
9692
|
+
this.devServer = new DevServer({
|
|
9693
|
+
project,
|
|
9694
|
+
dir: this.store.dir,
|
|
9695
|
+
userAttachedOrigin: () => {
|
|
9696
|
+
const app = this.store.app;
|
|
9697
|
+
return app.kind === "attached" ? app.origin : null;
|
|
9698
|
+
},
|
|
9699
|
+
heldElsewhere: (origin) => services.heldElsewhere(this, origin),
|
|
9700
|
+
attach: (origin) => services.attachOrigin(this, origin),
|
|
9701
|
+
detachLeavingRecord: (origin) => this.detachDevServerApp(origin),
|
|
9702
|
+
environmentReady: services.environmentReady,
|
|
9703
|
+
proposingAgent: () => proposingAgent([this.store.session.agent, rememberedLastAgent()], (p) => agentInstalled(p)),
|
|
9704
|
+
onChange: () => {
|
|
9705
|
+
this.showAppWhenServed();
|
|
9706
|
+
this.broadcast();
|
|
9707
|
+
services.projects.changed();
|
|
9708
|
+
}
|
|
9709
|
+
});
|
|
7889
9710
|
this.appProxy = new AppProxy(`${URL_PREFIX}/shell`, (req) => {
|
|
7890
9711
|
this.pageCookies = req.headers.cookie ?? null;
|
|
7891
9712
|
});
|
|
@@ -7894,7 +9715,7 @@ var SessionRuntime = class {
|
|
|
7894
9715
|
view: this.store,
|
|
7895
9716
|
frameOrigin: () => `http://${FRAME_HOST}:${this.port}`,
|
|
7896
9717
|
overlay: () => ({ assets: overlayAssetKey(), shellOrigin: this.shellOrigin }),
|
|
7897
|
-
hasApp: () => this.app.kind === "attached",
|
|
9718
|
+
hasApp: () => this.app.kind === "attached" || this.devServer.offersApp(),
|
|
7898
9719
|
onChange: (change) => this.canvasChanged(change),
|
|
7899
9720
|
// eslint-disable-next-line no-console
|
|
7900
9721
|
log: (message2) => console.error(message2)
|
|
@@ -7914,6 +9735,7 @@ var SessionRuntime = class {
|
|
|
7914
9735
|
this.bus.setMaxListeners(50);
|
|
7915
9736
|
}
|
|
7916
9737
|
store;
|
|
9738
|
+
devServer;
|
|
7917
9739
|
bus = new EventEmitter();
|
|
7918
9740
|
/** The explore rounds this session is taking variants for, and the tool the
|
|
7919
9741
|
* agent sends them through. */
|
|
@@ -7937,7 +9759,7 @@ var SessionRuntime = class {
|
|
|
7937
9759
|
* Held in memory only, and handed out only for the token in the agent's environment. */
|
|
7938
9760
|
pageCookies = null;
|
|
7939
9761
|
pageStorage = null;
|
|
7940
|
-
pageStateToken =
|
|
9762
|
+
pageStateToken = randomBytes10(32).toString("base64url");
|
|
7941
9763
|
/** The dev-server proxy and the live attachment. */
|
|
7942
9764
|
appProxy;
|
|
7943
9765
|
port = 0;
|
|
@@ -7972,6 +9794,8 @@ var SessionRuntime = class {
|
|
|
7972
9794
|
agentProgress = null;
|
|
7973
9795
|
/** SPIKE: the ACP-driven agent, when this session owns one. */
|
|
7974
9796
|
acp = null;
|
|
9797
|
+
/** Who connected `acp` from a shell, so the app's quit disconnects only its own. */
|
|
9798
|
+
acpBy = null;
|
|
7975
9799
|
/** Since the last idle check. Born touched: a session just opened is in use. */
|
|
7976
9800
|
touched = true;
|
|
7977
9801
|
/** Release the port and detach everyone attached, so the origin can be handed
|
|
@@ -7979,11 +9803,11 @@ var SessionRuntime = class {
|
|
|
7979
9803
|
async stop() {
|
|
7980
9804
|
this.acp?.stop();
|
|
7981
9805
|
this.acp = null;
|
|
7982
|
-
for (const
|
|
9806
|
+
for (const resolve7 of this.pollWaiters) resolve7(null);
|
|
7983
9807
|
this.pollWaiters.clear();
|
|
7984
9808
|
for (const client2 of this.sseClients) client2.end();
|
|
7985
9809
|
this.sseClients.clear();
|
|
7986
|
-
const closed = new Promise((
|
|
9810
|
+
const closed = new Promise((resolve7) => this.server.close(() => resolve7()));
|
|
7987
9811
|
this.server.closeAllConnections();
|
|
7988
9812
|
this.appProxy.detach();
|
|
7989
9813
|
this.canvases.close();
|
|
@@ -8032,10 +9856,66 @@ var SessionRuntime = class {
|
|
|
8032
9856
|
}
|
|
8033
9857
|
setRestoreOnStart(on) {
|
|
8034
9858
|
this.store.setRestoreOnStart(on);
|
|
9859
|
+
if (!on) this.devServer.forgetResume();
|
|
9860
|
+
}
|
|
9861
|
+
detachDevServerApp(origin) {
|
|
9862
|
+
if (this.app.kind !== "attached" || this.app.origin !== origin) return;
|
|
9863
|
+
this.appProxy.detach();
|
|
9864
|
+
this.forgetPageState();
|
|
9865
|
+
this.canvases.appChanged({ show: false, create: this.shellClients.size > 0 });
|
|
9866
|
+
}
|
|
9867
|
+
lastDevServerPhase = "idle";
|
|
9868
|
+
showAppWhenServed() {
|
|
9869
|
+
const phase = this.devServer.snapshot().phase;
|
|
9870
|
+
const served = (p) => p === "running" || p === "external";
|
|
9871
|
+
if (served(phase) && !served(this.lastDevServerPhase)) {
|
|
9872
|
+
this.canvases.appChanged({ show: true, create: this.shellClients.size > 0 });
|
|
9873
|
+
}
|
|
9874
|
+
this.lastDevServerPhase = phase;
|
|
9875
|
+
}
|
|
9876
|
+
/** Opening the project: the agent it was on, else `fallback`, unless they disconnected it here. */
|
|
9877
|
+
ensureAgent(fallback, by) {
|
|
9878
|
+
const live3 = !!this.acp && this.acp.snapshot().state !== "exited";
|
|
9879
|
+
const { agent, agentDeclined } = this.store.session;
|
|
9880
|
+
if (live3 || agentDeclined || this.pollWaiters.size > 0) return;
|
|
9881
|
+
const command = agent ?? this.store.currentChat.agent ?? fallback;
|
|
9882
|
+
if (command) this.connectAgent(command, by);
|
|
9883
|
+
}
|
|
9884
|
+
/** A shell's pick. The chat carries on when this agent can resume it; otherwise,
|
|
9885
|
+
* as with a switch, a new chat begins. */
|
|
9886
|
+
connectAgent(command, by) {
|
|
9887
|
+
const live3 = !!this.acp && this.acp.snapshot().state !== "exited";
|
|
9888
|
+
const resumes = !live3 && (this.store.currentChat.agent ?? command) === command;
|
|
9889
|
+
if (!(resumes ? this.attachAcpAgent(command) : this.switchAcpAgent(command))) return false;
|
|
9890
|
+
this.acpBy = by;
|
|
9891
|
+
this.store.setAgentBy(by);
|
|
9892
|
+
this.broadcast();
|
|
9893
|
+
return true;
|
|
9894
|
+
}
|
|
9895
|
+
/** A restore does not bring it back. Asked for by the user, it stays off until they connect one. */
|
|
9896
|
+
disconnectAgent(opts) {
|
|
9897
|
+
this.acp?.stop();
|
|
9898
|
+
this.acp = null;
|
|
9899
|
+
this.acpBy = null;
|
|
9900
|
+
this.agentBusy = false;
|
|
9901
|
+
this.activeBatch = null;
|
|
9902
|
+
this.activeSkills = [];
|
|
9903
|
+
this.agentProgress = null;
|
|
9904
|
+
this.store.clearAgent();
|
|
9905
|
+
if (opts.declined) this.store.declineAgent();
|
|
9906
|
+
this.broadcast();
|
|
9907
|
+
}
|
|
9908
|
+
/** The app's quit: what its windows connected goes with them. */
|
|
9909
|
+
disconnectAgentOf(by) {
|
|
9910
|
+
if (this.acpBy !== by) return false;
|
|
9911
|
+
this.disconnectAgent({ declined: false });
|
|
9912
|
+
return true;
|
|
8035
9913
|
}
|
|
8036
9914
|
resumeAgent() {
|
|
8037
|
-
const agent = this.store.session
|
|
8038
|
-
if (agent)
|
|
9915
|
+
const { agent, agentBy } = this.store.session;
|
|
9916
|
+
if (!agent) return;
|
|
9917
|
+
this.restoreAcpAgent(agent);
|
|
9918
|
+
this.acpBy = agentBy ?? null;
|
|
8039
9919
|
}
|
|
8040
9920
|
/** Off the live agent, not the record: the record holds what was asked for. */
|
|
8041
9921
|
get acpCommand() {
|
|
@@ -8075,6 +9955,10 @@ var SessionRuntime = class {
|
|
|
8075
9955
|
...this.app.kind === "attached" ? { app: this.app.origin } : {},
|
|
8076
9956
|
canvas: this.canvases.read.snapshot(this.agentHasCanvasTools),
|
|
8077
9957
|
annotations: this.store.annotations,
|
|
9958
|
+
...(() => {
|
|
9959
|
+
const pending = this.store.pendingVideoIds;
|
|
9960
|
+
return pending.length ? { pendingVideos: pending } : {};
|
|
9961
|
+
})(),
|
|
8078
9962
|
conversation: this.store.visibleConversation,
|
|
8079
9963
|
agentOnline: this.pollWaiters.size > 0 || !!this.acp && this.acp.snapshot().state !== "exited",
|
|
8080
9964
|
agentBusy: this.agentBusy,
|
|
@@ -8093,7 +9977,10 @@ var SessionRuntime = class {
|
|
|
8093
9977
|
...update ? { update } : {},
|
|
8094
9978
|
account: this.account.snapshot(),
|
|
8095
9979
|
...live3.length ? { explores: live3 } : {},
|
|
8096
|
-
...this.canExplore ? { canExplore: true } : {}
|
|
9980
|
+
...this.canExplore ? { canExplore: true } : {},
|
|
9981
|
+
devServer: this.devServer.snapshot(),
|
|
9982
|
+
appTab: this.app.kind === "attached" || this.devServer.offersApp(),
|
|
9983
|
+
project: this.project
|
|
8097
9984
|
};
|
|
8098
9985
|
}
|
|
8099
9986
|
/** The chat list as the shell draws it: newest first, because that is the one
|
|
@@ -8112,7 +9999,16 @@ var SessionRuntime = class {
|
|
|
8112
9999
|
|
|
8113
10000
|
`;
|
|
8114
10001
|
for (const res of this.sseClients) res.write(data);
|
|
10002
|
+
if (this.agentWorking !== this.listedAgentWorking) {
|
|
10003
|
+
this.listedAgentWorking = this.agentWorking;
|
|
10004
|
+
this.services.projects.changed();
|
|
10005
|
+
}
|
|
10006
|
+
}
|
|
10007
|
+
get agentWorking() {
|
|
10008
|
+
return this.agentBusy || !!this.acp?.snapshot().cancelling;
|
|
8115
10009
|
}
|
|
10010
|
+
/** What the project list last heard, so it hears of a turn only as it starts and ends. */
|
|
10011
|
+
listedAgentWorking = false;
|
|
8116
10012
|
/** The workspace wrote, or a frame's file was found changed. Shell clients get
|
|
8117
10013
|
* the one canvas that changed as a named event - the snapshot never carries
|
|
8118
10014
|
* frames, so a streaming turn does not resend forty of them per token - and
|
|
@@ -8210,7 +10106,7 @@ var SessionRuntime = class {
|
|
|
8210
10106
|
return this.acp.snapshot().agent === command;
|
|
8211
10107
|
}
|
|
8212
10108
|
this.store.setAgent(command);
|
|
8213
|
-
|
|
10109
|
+
mkdirSync17(ACP_NPM_PREFIX, { recursive: true });
|
|
8214
10110
|
this.acp = new AcpAgent({
|
|
8215
10111
|
command,
|
|
8216
10112
|
launch: launchCommand(command, ACP_NPM_PREFIX),
|
|
@@ -8672,7 +10568,7 @@ var SessionRuntime = class {
|
|
|
8672
10568
|
wakePollers() {
|
|
8673
10569
|
const waiters = [...this.pollWaiters];
|
|
8674
10570
|
this.pollWaiters.clear();
|
|
8675
|
-
for (const
|
|
10571
|
+
for (const resolve7 of waiters) resolve7(this.pollOutcome());
|
|
8676
10572
|
this.broadcast();
|
|
8677
10573
|
}
|
|
8678
10574
|
/** Immediate poll outcome, or null when the agent should keep waiting. */
|
|
@@ -8707,11 +10603,11 @@ var SessionRuntime = class {
|
|
|
8707
10603
|
this.agentProgress = null;
|
|
8708
10604
|
const immediate = this.pollOutcome();
|
|
8709
10605
|
if (immediate) return immediate;
|
|
8710
|
-
return new Promise((
|
|
10606
|
+
return new Promise((resolve7) => {
|
|
8711
10607
|
const waiter = (r) => {
|
|
8712
10608
|
clearTimeout(timer);
|
|
8713
10609
|
gone.removeEventListener("abort", leave);
|
|
8714
|
-
|
|
10610
|
+
resolve7(r);
|
|
8715
10611
|
};
|
|
8716
10612
|
const leave = () => {
|
|
8717
10613
|
this.pollWaiters.delete(waiter);
|
|
@@ -8725,36 +10621,51 @@ var SessionRuntime = class {
|
|
|
8725
10621
|
});
|
|
8726
10622
|
}
|
|
8727
10623
|
apiRouter() {
|
|
8728
|
-
const api =
|
|
10624
|
+
const api = Router8();
|
|
8729
10625
|
api.use(refuseCrossSite(() => this.shellOrigin));
|
|
8730
10626
|
api.use((_req, _res, next) => {
|
|
8731
10627
|
this.touch();
|
|
8732
10628
|
next();
|
|
8733
10629
|
});
|
|
8734
|
-
api.post(
|
|
8735
|
-
"/
|
|
8736
|
-
|
|
8737
|
-
(req
|
|
8738
|
-
|
|
8739
|
-
if (
|
|
8740
|
-
|
|
10630
|
+
api.post("/attachments", (req, res, next) => {
|
|
10631
|
+
const mime = (req.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
|
|
10632
|
+
const limit = attachmentByteLimit(mime);
|
|
10633
|
+
if (Number(req.get("content-length")) > limit) return res.status(413).json({ error: "attachment is too large" });
|
|
10634
|
+
this.store.receiveAttachment(decodeAttachmentName(req.get("x-ez-name")), mime, req, limit).then((stored) => {
|
|
10635
|
+
if (stored === "too-large") return res.status(413).json({ error: "attachment is too large" });
|
|
10636
|
+
if (stored === "empty") return res.status(400).json({ error: "attachment body is empty" });
|
|
10637
|
+
if (stored.video) this.broadcast();
|
|
10638
|
+
res.json(stored);
|
|
10639
|
+
}, next);
|
|
10640
|
+
});
|
|
10641
|
+
api.get("/attachments/:id/content", (req, res) => {
|
|
10642
|
+
const entry = this.store.attachmentIndex[req.params.id];
|
|
10643
|
+
if (!entry) return res.status(404).json({ error: "attachment not found" });
|
|
10644
|
+
res.sendFile(
|
|
10645
|
+
this.store.attachmentPath(entry),
|
|
10646
|
+
storedFileOptions(entry.mime),
|
|
10647
|
+
// A Range reader drops requests it no longer needs, so a stream cut short
|
|
10648
|
+
// after the headers went out is the reader's choice, not a failure.
|
|
10649
|
+
(error) => {
|
|
10650
|
+
if (error && !res.headersSent) res.status(404).json({ error: "attachment not found" });
|
|
8741
10651
|
}
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
);
|
|
10652
|
+
);
|
|
10653
|
+
});
|
|
10654
|
+
api.put("/attachments/:id/video", express4.json({ limit: SETTLE_BODY_LIMIT }), (req, res) => {
|
|
10655
|
+
const settle = parseVideoSettle(req.body);
|
|
10656
|
+
if (typeof settle === "string") return res.status(400).json({ error: settle });
|
|
10657
|
+
const status = this.store.settleVideo(req.params.id, settle);
|
|
10658
|
+
if (status.state !== "gone") this.broadcast();
|
|
10659
|
+
res.status(status.state === "gone" ? 404 : 200).json(status);
|
|
10660
|
+
});
|
|
8751
10661
|
api.post(CANVAS_MCP_ROUTE, ...this.canvasMcp.handlers());
|
|
8752
|
-
api.use(
|
|
10662
|
+
api.use(express4.json({ limit: "2mb" }));
|
|
8753
10663
|
api.post(MCP_ROUTE, this.exploreMcp.handler());
|
|
8754
10664
|
api.use(canvasApiRouter({ user: this.canvases.user, read: this.canvases.read }));
|
|
8755
10665
|
api.use(libraryApiRouter({ library: this.library }));
|
|
8756
10666
|
api.use(extensionApiRouter({ distDir, version: this.version, dataDir: libraryHome() }));
|
|
8757
10667
|
api.use(announcementApiRouter());
|
|
10668
|
+
api.use(devServerApiRouter(this.devServer));
|
|
8758
10669
|
api.use(
|
|
8759
10670
|
accountApiRouter({ account: this.account, callbackUrl: () => `${this.shellOrigin}${ACCOUNT_CALLBACK_PATH}` })
|
|
8760
10671
|
);
|
|
@@ -8838,6 +10749,7 @@ var SessionRuntime = class {
|
|
|
8838
10749
|
case "referenced":
|
|
8839
10750
|
return res.status(409).json({ error: "attachment belongs to queued or sent feedback" });
|
|
8840
10751
|
default:
|
|
10752
|
+
this.broadcast();
|
|
8841
10753
|
return res.json({ ok: true });
|
|
8842
10754
|
}
|
|
8843
10755
|
});
|
|
@@ -8851,11 +10763,11 @@ var SessionRuntime = class {
|
|
|
8851
10763
|
return res.status(404).json({ error: `\u7D20\u6750 ${found.missing} \u5DF2\u7D93\u4E0D\u5728\u7D20\u6750\u5EAB\u88E1\u4E86` });
|
|
8852
10764
|
}
|
|
8853
10765
|
const pick = newId();
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
({ title, file, origin }) => this.store.copyAttachment(title, file.mediaType, file.path, { pick, ...origin })
|
|
8857
|
-
)
|
|
10766
|
+
const copied = found.ok.map(
|
|
10767
|
+
({ title, file, origin }) => this.store.copyAttachment(title, file.mediaType, file.path, { pick, ...origin })
|
|
8858
10768
|
);
|
|
10769
|
+
if (copied.some((a) => a.video)) this.broadcast();
|
|
10770
|
+
res.json(copied);
|
|
8859
10771
|
},
|
|
8860
10772
|
(error) => error instanceof LibraryError ? res.status(503).json({ error: error.message }) : next(error)
|
|
8861
10773
|
).catch(next);
|
|
@@ -8906,6 +10818,7 @@ var SessionRuntime = class {
|
|
|
8906
10818
|
if (!ids) return res.status(400).json({ error: "attachments must be an array of ids" });
|
|
8907
10819
|
const files = this.store.getAttachments(ids);
|
|
8908
10820
|
if (!files) return res.status(400).json({ error: "unknown attachment id" });
|
|
10821
|
+
if (this.store.holdsPendingVideo(files)) return res.status(409).json({ error: "\u5F71\u7247\u9084\u5728\u8655\u7406\uFF0C\u8655\u7406\u5B8C\u624D\u80FD\u4EA4\u7D66 agent" });
|
|
8909
10822
|
const refs = parseReferences(req.body?.references);
|
|
8910
10823
|
if (!refs) return res.status(400).json({ error: "references must be an array of anchors" });
|
|
8911
10824
|
const skills = Array.isArray(req.body?.skills) ? req.body.skills.filter((v) => typeof v === "string") : [];
|
|
@@ -8972,12 +10885,17 @@ var SessionRuntime = class {
|
|
|
8972
10885
|
const id = String(req.body?.id ?? "");
|
|
8973
10886
|
const profile = AGENT_PROFILES.find((p) => p.id === id);
|
|
8974
10887
|
if (!profile) return res.status(400).json({ error: "unknown agent" });
|
|
8975
|
-
if (!this.acp) {
|
|
8976
|
-
return res.status(409).json({ error: "
|
|
10888
|
+
if (!this.acp && this.pollWaiters.size > 0) {
|
|
10889
|
+
return res.status(409).json({ error: "another agent is driving this review" });
|
|
8977
10890
|
}
|
|
8978
|
-
if (!this.
|
|
10891
|
+
if (!this.connectAgent(profile.command, initiator(req))) {
|
|
8979
10892
|
return res.status(409).json({ error: "that agent cannot be started right now" });
|
|
8980
10893
|
}
|
|
10894
|
+
rememberLastAgent(profile.command);
|
|
10895
|
+
res.json({ ok: true });
|
|
10896
|
+
});
|
|
10897
|
+
api.post("/acp/disconnect", (_req, res) => {
|
|
10898
|
+
this.disconnectAgent({ declined: true });
|
|
8981
10899
|
res.json({ ok: true });
|
|
8982
10900
|
});
|
|
8983
10901
|
api.get("/acp/skills", (_req, res) => {
|
|
@@ -9025,6 +10943,7 @@ var SessionRuntime = class {
|
|
|
9025
10943
|
if (!ids) return res.status(400).json({ error: "attachments must be an array of ids" });
|
|
9026
10944
|
const files = this.store.getAttachments(ids);
|
|
9027
10945
|
if (!files) return res.status(400).json({ error: "unknown attachment id" });
|
|
10946
|
+
if (files.some((f) => f.video?.state === "pending")) return res.status(409).json({ error: "\u5F71\u7247\u9084\u5728\u8655\u7406\uFF0C\u8655\u7406\u5B8C\u624D\u80FD\u4EA4\u7D66 agent" });
|
|
9028
10947
|
const refs = parseReferences(req.body?.references);
|
|
9029
10948
|
if (!refs) return res.status(400).json({ error: "references must be an array of anchors" });
|
|
9030
10949
|
const round = await this.startExplore({
|
|
@@ -9187,11 +11106,14 @@ var SessionRuntime = class {
|
|
|
9187
11106
|
return api;
|
|
9188
11107
|
}
|
|
9189
11108
|
async start() {
|
|
9190
|
-
const app =
|
|
11109
|
+
const app = express4();
|
|
9191
11110
|
app.use(frameHost(this.canvases.read, () => this.shellOrigin));
|
|
9192
|
-
const rk =
|
|
9193
|
-
rk.get(
|
|
9194
|
-
|
|
11111
|
+
const rk = Router8();
|
|
11112
|
+
rk.get(
|
|
11113
|
+
"/shell",
|
|
11114
|
+
(_req, res) => res.type("html").send(shellHtml(this.version, this.app.kind === "attached" ? this.app.origin : null))
|
|
11115
|
+
);
|
|
11116
|
+
for (const file of ["overlay.js", "overlay.css", "shell.js", "shell.css", "events-worker.js", "video-digest.js"]) {
|
|
9195
11117
|
const type = file.endsWith(".css") ? "text/css" : "text/javascript";
|
|
9196
11118
|
const keyed = file.startsWith("overlay.");
|
|
9197
11119
|
rk.get(
|
|
@@ -9204,7 +11126,7 @@ var SessionRuntime = class {
|
|
|
9204
11126
|
}
|
|
9205
11127
|
rk.use(
|
|
9206
11128
|
"/announcements",
|
|
9207
|
-
|
|
11129
|
+
express4.static(join25(distDir, "announcements"), { immutable: true, maxAge: "1y", fallthrough: false })
|
|
9208
11130
|
);
|
|
9209
11131
|
rk.get(ACCOUNT_CALLBACK_PATH.slice(URL_PREFIX.length), crossSiteAccountCallback(this.account));
|
|
9210
11132
|
rk.use("/api", this.apiRouter());
|
|
@@ -9222,6 +11144,7 @@ var SessionRuntime = class {
|
|
|
9222
11144
|
if (this.port === preferred) this.shellReturnDeadline = Date.now() + SHELL_RETURN_MS;
|
|
9223
11145
|
this.store.setPort(this.port);
|
|
9224
11146
|
this.canvases.start();
|
|
11147
|
+
this.devServer.resume();
|
|
9225
11148
|
return this.port;
|
|
9226
11149
|
}
|
|
9227
11150
|
/** Whether a shell is open on this session, once any left open across a
|
|
@@ -9289,17 +11212,23 @@ async function handoverTo(cliEntry2) {
|
|
|
9289
11212
|
throw new Error(`new daemon did not come up within 20s (see ${DAEMON_LOG})`);
|
|
9290
11213
|
}
|
|
9291
11214
|
async function daemonMain(version, opts = {}) {
|
|
11215
|
+
scrubRunAsNode();
|
|
11216
|
+
const environment = adoptLoginEnvironment();
|
|
9292
11217
|
const range = controlPortRange();
|
|
9293
11218
|
if (opts.succeed === void 0) {
|
|
9294
|
-
const [
|
|
9295
|
-
if (
|
|
11219
|
+
const [adopted2] = await ownDaemonsInRange(range);
|
|
11220
|
+
if (adopted2) return adopt(adopted2);
|
|
9296
11221
|
}
|
|
9297
11222
|
let sessions;
|
|
11223
|
+
let projectsChanged = () => {
|
|
11224
|
+
};
|
|
9298
11225
|
const broadcastAll = () => {
|
|
9299
11226
|
for (const s of sessions.list()) s.broadcast();
|
|
11227
|
+
projectsChanged();
|
|
9300
11228
|
};
|
|
9301
11229
|
let stopping = false;
|
|
9302
11230
|
let boundAt = 0n;
|
|
11231
|
+
const releaseDevServers = () => Promise.all(sessions.list().map((s) => s.devServer.release()));
|
|
9303
11232
|
const updater = new Updater({
|
|
9304
11233
|
current: version,
|
|
9305
11234
|
latestVersion: updateChecksDisabled() ? async () => null : () => latestVersion(),
|
|
@@ -9307,16 +11236,42 @@ async function daemonMain(version, opts = {}) {
|
|
|
9307
11236
|
handover: handoverTo,
|
|
9308
11237
|
// Exit is the release: it frees every port at once and takes the agent
|
|
9309
11238
|
// children with it, which is what the successor is waiting for.
|
|
9310
|
-
retire: async () =>
|
|
11239
|
+
retire: async () => {
|
|
11240
|
+
await releaseDevServers();
|
|
11241
|
+
process.exit(0);
|
|
11242
|
+
},
|
|
9311
11243
|
onChange: broadcastAll
|
|
9312
11244
|
});
|
|
9313
11245
|
const library = lazyLibrary(() => openLibrary({ dir: libraryDir(libraryHome()) }));
|
|
9314
11246
|
const account = new Account({ baseUrl: apiBaseUrl(), onChange: broadcastAll });
|
|
11247
|
+
let controlPort = 0;
|
|
11248
|
+
const controlOrigin = () => `http://127.0.0.1:${controlPort}`;
|
|
11249
|
+
const book = new ProjectBook(listPersistedSessions());
|
|
11250
|
+
const projects = new ProjectHub({
|
|
11251
|
+
sessions: { list: () => sessions.list(), open: (req) => sessions.open(req) },
|
|
11252
|
+
book,
|
|
11253
|
+
version,
|
|
11254
|
+
account: () => account.snapshot(),
|
|
11255
|
+
defaultAgent,
|
|
11256
|
+
projectRoot,
|
|
11257
|
+
restored: () => restored
|
|
11258
|
+
});
|
|
11259
|
+
projectsChanged = () => projects.changed();
|
|
11260
|
+
const services = {
|
|
11261
|
+
projects,
|
|
11262
|
+
attachOrigin: (runtime, origin) => sessions.attachOrigin(runtime, origin, { userAttached: false, now: Date.now() }),
|
|
11263
|
+
heldElsewhere: (runtime, origin) => {
|
|
11264
|
+
const holder = sessions.byOrigin(origin);
|
|
11265
|
+
return !!holder && holder !== runtime;
|
|
11266
|
+
},
|
|
11267
|
+
environmentReady: () => environment
|
|
11268
|
+
};
|
|
9315
11269
|
sessions = new SessionDirectory(
|
|
9316
|
-
(project) => new SessionRuntime(project, version, updater, library, account),
|
|
11270
|
+
(project) => new SessionRuntime(project, version, updater, library, account, services),
|
|
9317
11271
|
{
|
|
9318
11272
|
restorable: listRestorableSessions,
|
|
9319
11273
|
get: persistedSession,
|
|
11274
|
+
used: (project, now) => book.markUsed(project, now),
|
|
9320
11275
|
migrate: () => {
|
|
9321
11276
|
const { merged } = migrateLegacySessions();
|
|
9322
11277
|
if (merged.size) {
|
|
@@ -9327,8 +11282,8 @@ async function daemonMain(version, opts = {}) {
|
|
|
9327
11282
|
// eslint-disable-next-line no-console
|
|
9328
11283
|
(message2) => console.error(message2)
|
|
9329
11284
|
);
|
|
9330
|
-
const control =
|
|
9331
|
-
control.use(
|
|
11285
|
+
const control = express4();
|
|
11286
|
+
control.use(express4.json());
|
|
9332
11287
|
control.get("/control/health", (_req, res) => {
|
|
9333
11288
|
if (stopping) return res.status(503).json({ ok: false, stopping: true });
|
|
9334
11289
|
res.json({
|
|
@@ -9337,12 +11292,32 @@ async function daemonMain(version, opts = {}) {
|
|
|
9337
11292
|
version,
|
|
9338
11293
|
pid: process.pid,
|
|
9339
11294
|
dataDirId: dataDirId(),
|
|
9340
|
-
boundAt: String(boundAt)
|
|
11295
|
+
boundAt: String(boundAt),
|
|
11296
|
+
desktop: DESKTOP_CONTRACT
|
|
9341
11297
|
});
|
|
9342
11298
|
});
|
|
11299
|
+
control.use("/control/desktop", refuseCrossSite(controlOrigin), desktopContractGate());
|
|
11300
|
+
control.post("/control/desktop/open", async (req, res) => {
|
|
11301
|
+
const path = req.body?.path;
|
|
11302
|
+
if (typeof path !== "string") return res.status(400).json({ error: "path required" });
|
|
11303
|
+
const added = projects.add(path);
|
|
11304
|
+
if ("error" in added) return res.status(400).json(added);
|
|
11305
|
+
const opened = await projects.open(added.project, "desktop");
|
|
11306
|
+
if ("ok" in opened) return res.status(opened.status).json({ error: opened.error });
|
|
11307
|
+
res.json(opened);
|
|
11308
|
+
});
|
|
11309
|
+
control.post("/control/desktop/quit", async (_req, res) => {
|
|
11310
|
+
projects.disconnectAgentsOf("desktop");
|
|
11311
|
+
res.json({ stopped: await projects.stopManagedBy("desktop") });
|
|
11312
|
+
});
|
|
11313
|
+
control.use(homePageRouter({ version, distDir, icon: SHELL_ICON }));
|
|
11314
|
+
control.use(`${URL_PREFIX}/api`, projectsApiRouter(projects, { version, origin: controlOrigin }));
|
|
11315
|
+
control.get(ACCOUNT_CALLBACK_PATH, crossSiteAccountCallback(account));
|
|
11316
|
+
control.use(`${URL_PREFIX}/api/account`, refuseCrossSite(controlOrigin));
|
|
11317
|
+
control.use(`${URL_PREFIX}/api`, accountApiRouter({ account, callbackUrl: () => `${controlOrigin()}${ACCOUNT_CALLBACK_PATH}` }));
|
|
9343
11318
|
let ready;
|
|
9344
|
-
const restored = new Promise((
|
|
9345
|
-
ready =
|
|
11319
|
+
const restored = new Promise((resolve7) => {
|
|
11320
|
+
ready = resolve7;
|
|
9346
11321
|
});
|
|
9347
11322
|
control.use("/control/sessions", (_req, _res, next) => {
|
|
9348
11323
|
restored.then(() => next(), next);
|
|
@@ -9360,7 +11335,7 @@ async function daemonMain(version, opts = {}) {
|
|
|
9360
11335
|
);
|
|
9361
11336
|
});
|
|
9362
11337
|
control.post("/control/sessions", async (req, res) => {
|
|
9363
|
-
const { url, reopen, project, agent } = req.body ?? {};
|
|
11338
|
+
const { url, reopen, project, agent, connectAgent, devServer } = req.body ?? {};
|
|
9364
11339
|
if (typeof project !== "string" || !project) {
|
|
9365
11340
|
return res.status(400).json({ error: "missing project" });
|
|
9366
11341
|
}
|
|
@@ -9386,11 +11361,14 @@ async function daemonMain(version, opts = {}) {
|
|
|
9386
11361
|
}
|
|
9387
11362
|
const runtime = opened.runtime;
|
|
9388
11363
|
runtime.touch();
|
|
11364
|
+
if (devServer === true && !parsed) runtime.devServer.ensure("cli");
|
|
9389
11365
|
if (typeof agent === "string" && agent) {
|
|
9390
11366
|
if (!runtime.attachAcpAgent(agent) && !runtime.switchAcpAgent(agent)) {
|
|
9391
11367
|
return res.status(409).json({ error: "that agent cannot be started right now" });
|
|
9392
11368
|
}
|
|
9393
11369
|
runtime.broadcast();
|
|
11370
|
+
} else if (connectAgent === true) {
|
|
11371
|
+
runtime.ensureAgent(defaultAgent(), "cli");
|
|
9394
11372
|
}
|
|
9395
11373
|
res.json({
|
|
9396
11374
|
port: runtime.port,
|
|
@@ -9414,16 +11392,17 @@ async function daemonMain(version, opts = {}) {
|
|
|
9414
11392
|
...runtime.app.kind === "attached" ? { app: runtime.app.origin } : {}
|
|
9415
11393
|
});
|
|
9416
11394
|
});
|
|
9417
|
-
control.post("/control/stop", (_req, res) => {
|
|
11395
|
+
control.post("/control/stop", refuseCrossSite(controlOrigin), (_req, res) => {
|
|
9418
11396
|
stopping = true;
|
|
9419
11397
|
clearRegistry(process.pid);
|
|
9420
11398
|
res.json({ ok: true });
|
|
9421
|
-
|
|
11399
|
+
void releaseDevServers().then(() => process.exit(0));
|
|
9422
11400
|
});
|
|
9423
11401
|
const controlServer = await listenOn(control, [...range.ports, 0]);
|
|
9424
11402
|
boundAt = process.hrtime.bigint();
|
|
9425
11403
|
const controlAddress = controlServer.address();
|
|
9426
11404
|
const port = typeof controlAddress === "object" && controlAddress ? controlAddress.port : 0;
|
|
11405
|
+
controlPort = port;
|
|
9427
11406
|
if (opts.succeed === void 0) {
|
|
9428
11407
|
const before = (a, b) => {
|
|
9429
11408
|
const [x, y] = [a.boundAt ?? 0n, b.boundAt ?? 0n];
|
|
@@ -9437,14 +11416,16 @@ async function daemonMain(version, opts = {}) {
|
|
|
9437
11416
|
}
|
|
9438
11417
|
}
|
|
9439
11418
|
writeRegistry({ port, pid: process.pid, startedAt: Date.now() });
|
|
9440
|
-
const reaped =
|
|
11419
|
+
const reaped = reapOrphanedGroups();
|
|
9441
11420
|
if (reaped > 0) {
|
|
9442
|
-
console.error(`reaped ${reaped}
|
|
11421
|
+
console.error(`reaped ${reaped} process group(s) left by a killed daemon`);
|
|
9443
11422
|
}
|
|
9444
11423
|
const cleanup = () => {
|
|
9445
11424
|
clearRegistry(process.pid);
|
|
9446
|
-
|
|
9447
|
-
|
|
11425
|
+
void releaseDevServers().then(() => {
|
|
11426
|
+
clearGroupRecord();
|
|
11427
|
+
process.exit(0);
|
|
11428
|
+
});
|
|
9448
11429
|
};
|
|
9449
11430
|
process.on("SIGTERM", cleanup);
|
|
9450
11431
|
process.on("SIGINT", cleanup);
|
|
@@ -9452,9 +11433,10 @@ async function daemonMain(version, opts = {}) {
|
|
|
9452
11433
|
if (!predecessorGone) {
|
|
9453
11434
|
console.error(`predecessor pid ${opts.succeed} is still running; restoring sessions anyway`);
|
|
9454
11435
|
}
|
|
11436
|
+
await environment;
|
|
9455
11437
|
await sessions.restore({ migrate: predecessorGone });
|
|
9456
11438
|
ready();
|
|
9457
|
-
if (predecessorGone) pruneInstalledVersions(
|
|
11439
|
+
if (predecessorGone) pruneInstalledVersions(join25(distDir, "cli.mjs"));
|
|
9458
11440
|
void refreshCaptureInstall({ distDir, version, dataDir: libraryHome() }).catch(
|
|
9459
11441
|
(err) => console.error(`capture extension refresh failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
9460
11442
|
);
|
|
@@ -9463,7 +11445,7 @@ async function daemonMain(version, opts = {}) {
|
|
|
9463
11445
|
if (!envFlagOn(process.env[NO_IDLE_STOP_ENV])) {
|
|
9464
11446
|
const idle = new IdleCount(IDLE_STOP_MS / IDLE_CHECK_MS);
|
|
9465
11447
|
setInterval(() => {
|
|
9466
|
-
const busy = sessions.list().filter((s) => s.takeBusy()).length > 0;
|
|
11448
|
+
const busy = [sessions.list().filter((s) => s.takeBusy()).length > 0, projects.takeBusy()].some(Boolean);
|
|
9467
11449
|
if (idle.observe(busy) && sessions.size > 0) {
|
|
9468
11450
|
sessions.retireIdle();
|
|
9469
11451
|
cleanup();
|
|
@@ -9474,8 +11456,8 @@ async function daemonMain(version, opts = {}) {
|
|
|
9474
11456
|
}
|
|
9475
11457
|
|
|
9476
11458
|
// src/inspire.ts
|
|
9477
|
-
import { existsSync as
|
|
9478
|
-
import { dirname as
|
|
11459
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync18, writeFileSync as writeFileSync11 } from "node:fs";
|
|
11460
|
+
import { dirname as dirname10, join as join26 } from "node:path";
|
|
9479
11461
|
var DEFAULT_INSPIRE_URL = "https://inspire.ezdesign.dev";
|
|
9480
11462
|
var INSPIRE_URL_ENV = `${APP_NAME.toUpperCase()}_INSPIRE_URL`;
|
|
9481
11463
|
var SEARCH_TIMEOUT_MS = 15e3;
|
|
@@ -9490,17 +11472,17 @@ function apiUrl(path, base = inspireBaseUrl()) {
|
|
|
9490
11472
|
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
9491
11473
|
}
|
|
9492
11474
|
function inspirationCacheDir(dataDir = DATA_DIR) {
|
|
9493
|
-
return
|
|
11475
|
+
return join26(dataDir, "cache", "inspiration");
|
|
9494
11476
|
}
|
|
9495
11477
|
function imageCachePath(entryId, image, index = 0, dataDir = DATA_DIR) {
|
|
9496
11478
|
const position = image.position ?? index;
|
|
9497
|
-
const
|
|
11479
|
+
const parts2 = [image.kind, image.viewport ?? void 0, String(position)].filter(
|
|
9498
11480
|
(p) => typeof p === "string" && p !== ""
|
|
9499
11481
|
);
|
|
9500
|
-
return
|
|
11482
|
+
return join26(inspirationCacheDir(dataDir), entryId, `${parts2.join("-")}.png`);
|
|
9501
11483
|
}
|
|
9502
11484
|
function sourceCachePath(entryId, source, dataDir = DATA_DIR) {
|
|
9503
|
-
return
|
|
11485
|
+
return join26(inspirationCacheDir(dataDir), entryId, `source.${source.kind}`);
|
|
9504
11486
|
}
|
|
9505
11487
|
var slugify = (raw) => raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
9506
11488
|
function entryTitle(entry) {
|
|
@@ -9575,7 +11557,7 @@ async function download(url, path, fetchImpl) {
|
|
|
9575
11557
|
try {
|
|
9576
11558
|
const res = await fetchImpl(url, { signal: AbortSignal.timeout(IMAGE_TIMEOUT_MS) });
|
|
9577
11559
|
if (!res.ok) return false;
|
|
9578
|
-
|
|
11560
|
+
mkdirSync18(dirname10(path), { recursive: true });
|
|
9579
11561
|
writeFileSync11(path, Buffer.from(await res.arrayBuffer()));
|
|
9580
11562
|
return true;
|
|
9581
11563
|
} catch {
|
|
@@ -9598,12 +11580,12 @@ async function cacheFiles(results, opts = {}) {
|
|
|
9598
11580
|
);
|
|
9599
11581
|
for (const [index, image] of ordered2) {
|
|
9600
11582
|
const path = imageCachePath(entry.id, image, index, dataDir);
|
|
9601
|
-
if (
|
|
11583
|
+
if (existsSync14(path) || await download(image.url, path, fetchImpl)) images.push(path);
|
|
9602
11584
|
}
|
|
9603
11585
|
const sources = [];
|
|
9604
11586
|
for (const source of entry.sources ?? []) {
|
|
9605
11587
|
const path = sourceCachePath(entry.id, source, dataDir);
|
|
9606
|
-
if (await download(source.url, path, fetchImpl) ||
|
|
11588
|
+
if (await download(source.url, path, fetchImpl) || existsSync14(path)) sources.push(path);
|
|
9607
11589
|
}
|
|
9608
11590
|
files.set(entry.id, { images, sources });
|
|
9609
11591
|
}
|
|
@@ -9652,22 +11634,9 @@ function parseSearchArgs(args) {
|
|
|
9652
11634
|
return { query, tags, limit, json: args.includes("--json") };
|
|
9653
11635
|
}
|
|
9654
11636
|
|
|
9655
|
-
// src/project-root.ts
|
|
9656
|
-
import { existsSync as existsSync12 } from "node:fs";
|
|
9657
|
-
import { dirname as dirname10, join as join21, parse, resolve as resolve4 } from "node:path";
|
|
9658
|
-
function projectRoot(from) {
|
|
9659
|
-
let dir = resolve4(from);
|
|
9660
|
-
const { root } = parse(dir);
|
|
9661
|
-
for (; ; ) {
|
|
9662
|
-
if (existsSync12(join21(dir, ".git")) || existsSync12(join21(dir, "package.json"))) return dir;
|
|
9663
|
-
if (dir === root) return resolve4(from);
|
|
9664
|
-
dir = dirname10(dir);
|
|
9665
|
-
}
|
|
9666
|
-
}
|
|
9667
|
-
|
|
9668
11637
|
// src/cli.ts
|
|
9669
11638
|
var cliEntry = fileURLToPath2(import.meta.url);
|
|
9670
|
-
var pkg = JSON.parse(
|
|
11639
|
+
var pkg = JSON.parse(readFileSync21(join27(dirname11(cliEntry), "../package.json"), "utf8"));
|
|
9671
11640
|
var EXTENSION_HELP = ` ${APP_NAME} extension install [--user-data-dir <dir>] | uninstall | status
|
|
9672
11641
|
install the native host the Chrome capture extension saves through, and
|
|
9673
11642
|
print the folder to load in chrome://extensions. Saving needs no daemon
|
|
@@ -9678,14 +11647,18 @@ var EXTENSION_HELP = ` ${APP_NAME} extension install [--user-data-dir <dir>] |
|
|
|
9678
11647
|
var HELP = `${APP_NAME} - annotate your live dev app, feedback flows to your local agent
|
|
9679
11648
|
|
|
9680
11649
|
Usage:
|
|
9681
|
-
${APP_NAME} [<url>] [--reopen] [--agent <name|command>]
|
|
11650
|
+
${APP_NAME} [<url>] [--reopen] [--agent <name|command> | --no-agent]
|
|
9682
11651
|
open (or resume) this project's review session. With <url>, that dev
|
|
9683
11652
|
server is on its App tab (a dev server that moved ports keeps the
|
|
9684
|
-
conversation); without one,
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
11653
|
+
conversation); without one, its App tab offers to start the project's
|
|
11654
|
+
dev server, or the shell opens on its canvases when there is none.
|
|
11655
|
+
The session drives its agent over ACP: it spawns it, feedback goes
|
|
11656
|
+
straight to it, and its stream, questions and permission prompts all
|
|
11657
|
+
surface in the review shell - no terminal needed. Without --agent it
|
|
11658
|
+
connects the agent this project had, else your last one, else the first
|
|
11659
|
+
installed of claude, codex and gemini; one you disconnected in the shell
|
|
11660
|
+
stays off. --agent picks one: claude, codex, gemini, or any ACP agent
|
|
11661
|
+
command line. --no-agent connects none, for an agent working it with poll.
|
|
9689
11662
|
${APP_NAME} poll [<url>] [--agent-reply <msg>]
|
|
9690
11663
|
block until the user sends feedback; prints JSON, then exits
|
|
9691
11664
|
${APP_NAME} progress [<url>] <msg>
|
|
@@ -9820,7 +11793,9 @@ async function cmdOpen(rawUrl, flags, agentArg) {
|
|
|
9820
11793
|
);
|
|
9821
11794
|
}
|
|
9822
11795
|
}
|
|
9823
|
-
const
|
|
11796
|
+
const resolved = await resolveDaemon(cliEntry, pkg.version);
|
|
11797
|
+
if (resolved.kind === "newer") return reexecAs(resolved.cliEntry);
|
|
11798
|
+
const daemon = resolved.daemon;
|
|
9824
11799
|
const project = projectRoot(process.cwd());
|
|
9825
11800
|
const res = await controlFetch(daemon.port, "/control/sessions", {
|
|
9826
11801
|
method: "POST",
|
|
@@ -9828,7 +11803,7 @@ async function cmdOpen(rawUrl, flags, agentArg) {
|
|
|
9828
11803
|
...target ? { url: target.href } : {},
|
|
9829
11804
|
reopen: flags.has("--reopen"),
|
|
9830
11805
|
project,
|
|
9831
|
-
...agentArg ? { agent: agentCommand(agentArg) } : {}
|
|
11806
|
+
...agentArg ? { agent: agentCommand(agentArg) } : { connectAgent: !flags.has("--no-agent") }
|
|
9832
11807
|
})
|
|
9833
11808
|
});
|
|
9834
11809
|
const body = await res.json();
|
|
@@ -9839,10 +11814,19 @@ async function cmdOpen(rawUrl, flags, agentArg) {
|
|
|
9839
11814
|
console.log(`shell: ${body.shellUrl}${body.shellOpen ? " (already open in your browser)" : ""}`);
|
|
9840
11815
|
if (body.agent) {
|
|
9841
11816
|
console.log(`agent: ${body.agent} (ACP) - review runs in the shell`);
|
|
9842
|
-
} else {
|
|
11817
|
+
} else if (flags.has("--no-agent")) {
|
|
9843
11818
|
console.log(`next: run \`${APP_NAME} poll\` in this project and wait for feedback`);
|
|
11819
|
+
} else {
|
|
11820
|
+
console.log(`next: pick an agent in the shell, or run again with --agent claude|codex|gemini`);
|
|
9844
11821
|
}
|
|
9845
11822
|
}
|
|
11823
|
+
function reexecAs(cliEntry2) {
|
|
11824
|
+
const { status } = spawnSync2(SELF_NODE.execPath, [cliEntry2, ...process.argv.slice(2)], {
|
|
11825
|
+
stdio: "inherit",
|
|
11826
|
+
env: selfNodeEnv()
|
|
11827
|
+
});
|
|
11828
|
+
process.exit(status ?? 1);
|
|
11829
|
+
}
|
|
9846
11830
|
async function cmdPoll(handle, reply) {
|
|
9847
11831
|
let port = await findSessionPort(handle);
|
|
9848
11832
|
if (reply?.trim()) {
|
|
@@ -10030,7 +12014,7 @@ async function cmdExtension(args) {
|
|
|
10030
12014
|
if (rest[i] !== "--user-data-dir") fail(`unknown option: ${rest[i]}`, `${APP_NAME} extension install [--user-data-dir <dir>]`);
|
|
10031
12015
|
const dir = rest[++i];
|
|
10032
12016
|
if (!dir) fail("missing value for --user-data-dir");
|
|
10033
|
-
userDataDirs.push(
|
|
12017
|
+
userDataDirs.push(resolve6(dir));
|
|
10034
12018
|
}
|
|
10035
12019
|
const placement = { distDir: dirname11(cliEntry), version: pkg.version };
|
|
10036
12020
|
try {
|
|
@@ -10117,6 +12101,7 @@ async function main() {
|
|
|
10117
12101
|
const agentIdx = argv.indexOf("--agent");
|
|
10118
12102
|
const agentArg = agentIdx >= 0 ? argv[agentIdx + 1] : void 0;
|
|
10119
12103
|
if (agentIdx >= 0 && !agentArg) fail("missing value for --agent", "e.g. --agent claude");
|
|
12104
|
+
if (agentArg && flags.has("--no-agent")) fail("--agent and --no-agent cannot be used together");
|
|
10120
12105
|
return cmdOpen(first === void 0 || first.startsWith("--") ? void 0 : first, flags, agentArg);
|
|
10121
12106
|
}
|
|
10122
12107
|
}
|