leglas 1.0.0 → 1.1.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/dist/bin.js CHANGED
@@ -2,729 +2,313 @@
2
2
 
3
3
  // src/bin.ts
4
4
  import { spawn as spawn4 } from "child_process";
5
+ import { realpathSync as realpathSync2 } from "fs";
5
6
  import { createRequire as createRequire2 } from "module";
7
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
8
 
7
- // src/args.ts
8
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["--port", "--user-port", "--config"]);
9
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--no-open", "--json"]);
10
- var MIN_SHOW_WIDTH = 320;
11
- var MAX_SHOW_WIDTH = 3840;
12
- function parsePort(flag, raw) {
13
- if (!/^\d+$/.test(raw)) {
14
- return { error: `${flag} needs a number, received ${JSON.stringify(raw)}.` };
15
- }
16
- const port = Number(raw);
17
- if (port < 1 || port > 65535) {
18
- return { error: `${flag} must be between 1 and 65535, received ${port}.` };
19
- }
20
- return port;
9
+ // ../server/dist/log.js
10
+ var DEFAULT_LOG_DIR = "design-log";
11
+ function slugify(value) {
12
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
21
13
  }
22
- function parseNew(rest) {
23
- let surface;
24
- let print = false;
25
- let json = false;
26
- let from;
27
- for (let index = 0; index < rest.length; index += 1) {
28
- const argument = rest[index];
29
- if (argument === "--from" || argument.startsWith("--from=")) {
30
- from = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
31
- if (from === void 0 || from === "") {
32
- return { kind: "error", message: "--from needs a path, for example --from src/Hero.tsx" };
33
- }
14
+ function frameFor(title, requests) {
15
+ let found = null;
16
+ for (const request of requests) {
17
+ if (request.title !== title)
34
18
  continue;
19
+ for (const attachment of request.attachments ?? []) {
20
+ if (attachment.kind === "frame")
21
+ found = attachment;
35
22
  }
36
- if (argument === "--print") {
37
- print = true;
38
- continue;
23
+ }
24
+ return found;
25
+ }
26
+ function askedOf(title, requests) {
27
+ return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
28
+ }
29
+ function composeEntry(input) {
30
+ const slug = `${input.date}-${slugify(input.surface)}`;
31
+ const pictures = [];
32
+ const lines2 = [];
33
+ lines2.push(`# ${input.surface}, ${input.date}`);
34
+ lines2.push("");
35
+ lines2.push(`**${input.won.title}** won and became \`${input.won.to}\`. ${input.previews.length === 1 ? "It was the only direction." : `${input.previews.length} directions were compared.`}`);
36
+ lines2.push("");
37
+ for (const preview of input.previews) {
38
+ const won = preview.title === input.won.title;
39
+ lines2.push(`## ${preview.title}${won ? " \u2014 kept" : ""}`);
40
+ lines2.push("");
41
+ if (preview.note !== void 0 && preview.note.trim() !== "") {
42
+ lines2.push(preview.note.trim());
43
+ lines2.push("");
39
44
  }
40
- if (argument === "--json") {
41
- json = true;
42
- continue;
45
+ const frame = frameFor(preview.title, input.requests);
46
+ if (frame !== null) {
47
+ const name = `${slugify(preview.title)}.png`;
48
+ pictures.push({ from: frame.file, to: name });
49
+ lines2.push(`![${preview.title}](${slug}/${name})`);
50
+ lines2.push("");
43
51
  }
44
- if (argument === "--help" || argument === "-h") return { kind: "help" };
45
- if (argument.startsWith("-")) {
46
- return { kind: "error", message: `leglas new does not take ${argument}.` };
52
+ if (preview.basedOn !== void 0) {
53
+ lines2.push(`A variant of ${preview.basedOn}.`);
54
+ lines2.push("");
47
55
  }
48
- if (surface !== void 0) {
49
- return { kind: "error", message: `leglas new takes one surface name, received ${JSON.stringify(argument)} as well.` };
56
+ const asked = askedOf(preview.title, input.requests);
57
+ if (asked.length > 0) {
58
+ lines2.push("Asked for:");
59
+ lines2.push("");
60
+ for (const words of asked)
61
+ lines2.push(`- ${words}`);
62
+ lines2.push("");
63
+ }
64
+ const notes = input.annotations.filter((note) => note.title === preview.title);
65
+ if (notes.length > 0) {
66
+ lines2.push("Marked on the design:");
67
+ lines2.push("");
68
+ for (const note of notes)
69
+ lines2.push(`- ${note.note}`);
70
+ lines2.push("");
50
71
  }
51
- surface = argument;
52
72
  }
53
- if (surface === void 0) {
54
- return {
55
- kind: "error",
56
- message: "leglas new needs a surface name, for example: npx leglas new hero"
57
- };
73
+ const failed = input.requests.filter((request) => request.status === "failed");
74
+ if (failed.length > 0) {
75
+ lines2.push("## Changes that did not land");
76
+ lines2.push("");
77
+ for (const request of failed) {
78
+ const why = request.failure?.message;
79
+ lines2.push(`- ${request.title}: ${request.intent}${why === void 0 ? "" : ` (${why})`}`);
80
+ }
81
+ lines2.push("");
58
82
  }
59
- return { kind: "new", surface, print, json, from };
83
+ return { slug, markdown: `${lines2.join("\n").trimEnd()}
84
+ `, pictures };
60
85
  }
61
- function parseAdd(rest) {
62
- let title;
63
- let url;
64
- let note;
65
- let branch;
66
- let file;
67
- let basedOn;
68
- let askedFor;
69
- const tags = [];
70
- let json = false;
71
- for (let index = 0; index < rest.length; index += 1) {
72
- const argument = rest[index];
73
- if (argument === "--json") {
74
- json = true;
75
- continue;
86
+
87
+ // ../server/dist/config.js
88
+ var DEFAULT_DEV_SERVER = "http://localhost:3000";
89
+ var DEFAULT_INSTALL_COMMAND = "npm install";
90
+ var IMPLICIT_PREVIEW = { title: "App", url: "/" };
91
+ function isRecord(value) {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
94
+ function isValidOrigin(value) {
95
+ try {
96
+ const url = new URL(value);
97
+ return url.protocol === "http:" || url.protocol === "https:";
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ function isSafeBranch(value) {
103
+ if (value === "" || value.startsWith("-"))
104
+ return false;
105
+ if (value.split("/").some((segment) => segment === "." || segment === ".."))
106
+ return false;
107
+ return /^[A-Za-z0-9._/-]+$/.test(value);
108
+ }
109
+ function isValidPreviewUrl(value) {
110
+ return value.startsWith("/") || isValidOrigin(value);
111
+ }
112
+ function isSafePreviewFile(value) {
113
+ if (value === "" || value.startsWith("/") || value.startsWith("\\"))
114
+ return false;
115
+ if (/^[A-Za-z]:/.test(value))
116
+ return false;
117
+ return !value.split(/[/\\]/).some((segment) => segment === "..");
118
+ }
119
+ function normalizeConfig(raw, options = {}) {
120
+ const requireDevCommand = options.requireDevCommand ?? true;
121
+ const errors = [];
122
+ const source = raw === void 0 || raw === null ? {} : raw;
123
+ if (!isRecord(source)) {
124
+ return { config: null, errors: ["Config must export an object."] };
125
+ }
126
+ const devServer = source["devServer"] ?? DEFAULT_DEV_SERVER;
127
+ if (typeof devServer !== "string" || !isValidOrigin(devServer)) {
128
+ errors.push(`devServer must be an http(s) URL, received ${JSON.stringify(devServer)}.`);
129
+ }
130
+ const rawPreviews = source["previews"] ?? [IMPLICIT_PREVIEW];
131
+ if (!Array.isArray(rawPreviews)) {
132
+ errors.push(`previews must be an array, received ${JSON.stringify(rawPreviews)}.`);
133
+ return { config: null, errors };
134
+ }
135
+ const previews = [];
136
+ const seenTitles = /* @__PURE__ */ new Set();
137
+ rawPreviews.forEach((entry2, index) => {
138
+ const at = `previews[${index}]`;
139
+ if (!isRecord(entry2)) {
140
+ errors.push(`${at} must be an object.`);
141
+ return;
76
142
  }
77
- if (argument === "--help" || argument === "-h") return { kind: "help" };
78
- const equals = argument.indexOf("=");
79
- const flag = equals === -1 ? argument : argument.slice(0, equals);
80
- let value;
81
- if (equals === -1) {
82
- value = rest[index + 1];
83
- index += 1;
143
+ const title = entry2["title"];
144
+ const url = entry2["url"];
145
+ const file = entry2["file"];
146
+ if (typeof title !== "string" || title.trim() === "") {
147
+ errors.push(`${at} needs a title; the rail has nothing to show without one.`);
148
+ } else if (seenTitles.has(title)) {
149
+ errors.push(`${at} repeats the title ${JSON.stringify(title)}; titles must be unique.`);
84
150
  } else {
85
- value = argument.slice(equals + 1);
151
+ seenTitles.add(title);
86
152
  }
87
- if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on", "--asked-for"].includes(
88
- flag
89
- )) {
90
- return { kind: "error", message: `leglas add does not take ${flag}.` };
153
+ if (file !== void 0) {
154
+ if (typeof file !== "string" || !isSafePreviewFile(file)) {
155
+ errors.push(`${at} has an unusable file ${JSON.stringify(file)}; use a path inside the project, like "directions/hero.html".`);
156
+ }
157
+ if (url !== void 0) {
158
+ errors.push(`${at} names a file and a url; a file preview's url is assigned by Leglas.`);
159
+ }
160
+ } else if (typeof url !== "string" || url.trim() === "") {
161
+ errors.push(`${at} needs a url.`);
162
+ } else if (!isValidPreviewUrl(url)) {
163
+ errors.push(`${at} has url ${JSON.stringify(url)}; use a root-relative path ("/pricing") or a full URL.`);
91
164
  }
92
- if (value === void 0 || value === "") {
93
- return { kind: "error", message: `${flag} needs a value.` };
165
+ const branch = entry2["branch"];
166
+ if (branch !== void 0) {
167
+ if (typeof branch !== "string" || !isSafeBranch(branch)) {
168
+ errors.push(`${at} has an unusable branch ${JSON.stringify(branch)}; use a plain git branch name.`);
169
+ } else if (typeof url === "string" && !url.startsWith("/")) {
170
+ errors.push(`${at} names a branch and an absolute url; a branch preview is served by Leglas, so its url must be a path.`);
171
+ }
172
+ if (file !== void 0) {
173
+ errors.push(`${at} names a branch and a file; a file preview is served by Leglas itself and has no checkout.`);
174
+ }
94
175
  }
95
- if (flag === "--title") title = value;
96
- else if (flag === "--url") url = value;
97
- else if (flag === "--note") note = value;
98
- else if (flag === "--branch") branch = value;
99
- else if (flag === "--file") file = value;
100
- else if (flag === "--based-on") basedOn = value;
101
- else if (flag === "--asked-for") askedFor = value;
102
- else tags.push(value);
176
+ const basedOn = entry2["basedOn"];
177
+ if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
178
+ errors.push(`${at} has a basedOn that is not a direction title.`);
179
+ }
180
+ const askedFor = entry2["askedFor"];
181
+ if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
182
+ errors.push(`${at} has an askedFor that is not a change request.`);
183
+ }
184
+ const tags = entry2["tags"];
185
+ previews.push({
186
+ title: typeof title === "string" ? title : "",
187
+ url: typeof url === "string" ? url : "",
188
+ note: typeof entry2["note"] === "string" ? entry2["note"] : void 0,
189
+ tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
190
+ ...typeof branch === "string" ? { branch } : {},
191
+ ...typeof file === "string" ? { file } : {},
192
+ ...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
193
+ ...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
194
+ });
195
+ });
196
+ const devCommand = source["devCommand"];
197
+ if (devCommand !== void 0 && typeof devCommand !== "string") {
198
+ errors.push("devCommand must be a string.");
199
+ } else if (typeof devCommand === "string" && !devCommand.includes("{port}")) {
200
+ errors.push(`devCommand must include {port}, so Leglas can start each checkout on a free port. Received ${JSON.stringify(devCommand)}.`);
103
201
  }
104
- if (title === void 0) {
105
- return { kind: "error", message: "leglas add needs --title, which is how the preview is identified." };
202
+ if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
203
+ errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
106
204
  }
107
- if (url === void 0 && file === void 0) {
108
- return {
109
- kind: "error",
110
- message: "leglas add needs --url (for example --url '/?v-hero=aurora') or --file for a page Leglas serves itself."
111
- };
205
+ const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
206
+ if (typeof logDir !== "string" || logDir.trim() === "") {
207
+ errors.push("logDir must be a non-empty string.");
208
+ }
209
+ const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
210
+ if (typeof installCommand !== "string" || installCommand.trim() === "") {
211
+ errors.push("installCommand must be a non-empty string.");
112
212
  }
213
+ const scanPreviews = source["scanPreviews"] ?? true;
214
+ if (typeof scanPreviews !== "boolean") {
215
+ errors.push("scanPreviews must be a boolean.");
216
+ }
217
+ if (errors.length > 0)
218
+ return { config: null, errors };
113
219
  return {
114
- kind: "add",
115
- preview: {
116
- title,
117
- url,
118
- note,
119
- tags: tags.length > 0 ? tags : void 0,
120
- branch,
121
- file,
122
- basedOn,
123
- askedFor
220
+ config: {
221
+ devServer,
222
+ previews,
223
+ scanPreviews,
224
+ devCommand: typeof devCommand === "string" ? devCommand : void 0,
225
+ installCommand,
226
+ logDir
124
227
  },
125
- json
228
+ errors: []
126
229
  };
127
230
  }
128
- function parseClassify(rest) {
129
- const changes = [];
130
- let json = false;
131
- for (let index = 0; index < rest.length; index += 1) {
132
- const argument = rest[index];
133
- if (argument === "--json") {
134
- json = true;
231
+
232
+ // ../server/dist/agent-command.js
233
+ var WATCH_PATH = ".leglas/watch.json";
234
+ var PROMPT_TOKEN = "{prompt}";
235
+ var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
236
+ function tokenize(template) {
237
+ const tokens = [];
238
+ let current = "";
239
+ let started = false;
240
+ let quote = null;
241
+ for (const character of template) {
242
+ if (quote !== null) {
243
+ if (character === quote)
244
+ quote = null;
245
+ else
246
+ current += character;
135
247
  continue;
136
248
  }
137
- if (argument === "--help" || argument === "-h") return { kind: "help" };
138
- const equals = argument.indexOf("=");
139
- const flag = equals === -1 ? argument : argument.slice(0, equals);
140
- if (flag !== "--change" && flag !== "--rewrite") {
141
- return { kind: "error", message: `leglas classify does not take ${argument}.` };
249
+ if (character === '"' || character === "'") {
250
+ quote = character;
251
+ started = true;
252
+ continue;
142
253
  }
143
- const value = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
144
- if (value === void 0 || value === "") {
145
- return { kind: "error", message: `${flag} needs a path, for example ${flag} package.json` };
254
+ if (/\s/.test(character)) {
255
+ if (started)
256
+ tokens.push(current);
257
+ current = "";
258
+ started = false;
259
+ continue;
146
260
  }
147
- changes.push({ path: value, kind: flag === "--change" ? "change" : "rewrite" });
261
+ current += character;
262
+ started = true;
148
263
  }
149
- if (changes.length === 0) {
150
- return {
151
- kind: "error",
152
- message: "leglas classify needs what the direction will touch, for example: npx leglas classify --change package.json --rewrite src/theme.css"
153
- };
264
+ if (quote !== null) {
265
+ return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
154
266
  }
155
- return { kind: "classify", changes, json };
267
+ if (started)
268
+ tokens.push(current);
269
+ return { ok: true, tokens };
156
270
  }
157
- function parseWatch(rest) {
158
- let run4;
159
- let port;
160
- for (let index = 0; index < rest.length; index += 1) {
161
- const argument = rest[index];
162
- if (argument === "--help" || argument === "-h") return { kind: "help" };
163
- const equals = argument.indexOf("=");
164
- const flag = equals === -1 ? argument : argument.slice(0, equals);
165
- if (flag !== "--run" && flag !== "--port") {
166
- return { kind: "error", message: `leglas watch does not take ${argument}.` };
167
- }
168
- const value = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
169
- if (value === void 0 || value === "") {
170
- return {
171
- kind: "error",
172
- message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' : "--port needs a value."
173
- };
174
- }
175
- if (flag === "--run") {
176
- run4 = value;
177
- continue;
178
- }
179
- const parsed2 = parsePort(flag, value);
180
- if (typeof parsed2 !== "number") return { kind: "error", message: parsed2.error };
181
- port = parsed2;
271
+ function parseTemplate(raw) {
272
+ const tokenized = tokenize(raw);
273
+ if (!tokenized.ok)
274
+ return tokenized;
275
+ const { tokens } = tokenized;
276
+ const [command, ...args] = tokens;
277
+ if (command === void 0) {
278
+ return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
182
279
  }
183
- return { kind: "watch", run: run4, port };
184
- }
185
- function parseArgs(argv) {
186
- if (argv[0] === "new") return parseNew(argv.slice(1));
187
- if (argv[0] === "watch") return parseWatch(argv.slice(1));
188
- if (argv[0] === "add") return parseAdd(argv.slice(1));
189
- if (argv[0] === "classify") return parseClassify(argv.slice(1));
190
- if (argv[0] === "init") {
191
- const rest = argv.slice(1);
192
- const unknown = rest.find((argument) => argument !== "--force" && argument !== "--json");
193
- if (unknown !== void 0) {
194
- return { kind: "error", message: `leglas init does not take ${unknown}.` };
195
- }
196
- return { kind: "init", force: rest.includes("--force"), json: rest.includes("--json") };
280
+ if (tokens.some((token) => token !== PROMPT_TOKEN && token.includes(PROMPT_TOKEN))) {
281
+ return {
282
+ ok: false,
283
+ error: `${PROMPT_TOKEN} must stand as a word of its own, for example: ${EXAMPLE}`
284
+ };
197
285
  }
198
- if (argv[0] === "keep") {
199
- const rest = argv.slice(1);
200
- let title;
201
- let to;
202
- let json = false;
203
- for (let index = 0; index < rest.length; index += 1) {
204
- const argument = rest[index];
205
- if (argument === "--json") {
206
- json = true;
207
- continue;
208
- }
209
- if (argument === "--to" || argument.startsWith("--to=")) {
210
- to = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
211
- if (to === void 0 || to === "") {
212
- return { kind: "error", message: "--to needs a path, for example --to src/components/hero.tsx" };
213
- }
214
- continue;
215
- }
216
- if (argument.startsWith("-")) {
217
- return { kind: "error", message: `leglas keep does not take ${argument}.` };
218
- }
219
- if (title !== void 0) {
220
- return { kind: "error", message: "leglas keep takes one direction title." };
221
- }
222
- title = argument;
223
- }
224
- if (title === void 0) {
225
- return {
226
- kind: "error",
227
- message: 'leglas keep needs a direction title, for example: npx leglas keep "Aurora" --to src/components/hero.tsx'
228
- };
229
- }
230
- if (to === void 0) {
231
- return { kind: "error", message: "leglas keep needs --to, the path the winner should live at." };
232
- }
233
- return { kind: "keep", title, to, json };
286
+ const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
287
+ if (placeholders > 1) {
288
+ return {
289
+ ok: false,
290
+ error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
291
+ };
234
292
  }
235
- if (argv[0] === "explore") {
236
- const rest = argv.slice(1);
237
- let surface;
238
- let count = 3;
239
- let basedOn = null;
240
- let json = false;
241
- for (let index = 0; index < rest.length; index += 1) {
242
- const argument = rest[index];
243
- if (argument === "--json") {
244
- json = true;
245
- continue;
246
- }
247
- if (argument === "--count" || argument.startsWith("--count=")) {
248
- const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
249
- if (raw === void 0 || !/^\d+$/.test(raw)) {
250
- return { kind: "error", message: "--count needs a number, for example --count 6." };
251
- }
252
- count = Number(raw);
253
- continue;
254
- }
255
- if (argument === "--based-on" || argument.startsWith("--based-on=")) {
256
- const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
257
- if (raw === void 0 || raw === "") {
258
- return {
259
- kind: "error",
260
- message: '--based-on needs a direction title, for example --based-on "Aurora".'
261
- };
262
- }
263
- basedOn = raw;
264
- continue;
265
- }
266
- if (argument.startsWith("-")) {
267
- return { kind: "error", message: `leglas explore does not take ${argument}.` };
268
- }
269
- if (surface !== void 0) {
270
- return { kind: "error", message: "leglas explore takes one surface name." };
271
- }
272
- surface = argument;
273
- }
274
- if (surface === void 0) {
275
- return {
276
- kind: "error",
277
- message: "leglas explore needs a surface name, for example: npx leglas explore hero --count 6"
278
- };
279
- }
280
- return { kind: "explore", surface, count, basedOn, json };
293
+ if (command === PROMPT_TOKEN) {
294
+ return {
295
+ ok: false,
296
+ error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
297
+ };
281
298
  }
282
- if (argv[0] === "requests") {
283
- const rest = argv.slice(1);
284
- const unknown = rest.find((argument) => argument !== "--json" && argument !== "--clear");
285
- if (unknown !== void 0) {
286
- return { kind: "error", message: `leglas requests does not take ${unknown}.` };
287
- }
288
- return { kind: "requests", json: rest.includes("--json"), clear: rest.includes("--clear") };
299
+ return { ok: true, template: { command, args } };
300
+ }
301
+ function commandFor(template, prompt) {
302
+ if (!template.args.includes(PROMPT_TOKEN)) {
303
+ return { command: template.command, args: [...template.args, prompt] };
289
304
  }
290
- if (argv[0] === "log") {
291
- const rest = argv.slice(1);
292
- const flags = rest.filter((argument) => argument.startsWith("--"));
293
- const unknown = flags.find((flag) => flag !== "--json");
294
- if (unknown !== void 0) {
295
- return { kind: "error", message: `leglas log does not take ${unknown}.` };
296
- }
297
- const names = rest.filter((argument) => !argument.startsWith("--"));
298
- if (names.length > 1) {
299
- return { kind: "error", message: "leglas log takes one entry at most." };
300
- }
301
- return { kind: "log", entry: names[0] ?? null, json: flags.includes("--json") };
302
- }
303
- if (argv[0] === "list") {
304
- const rest = argv.slice(1);
305
- const unknown = rest.find((argument) => argument !== "--json");
306
- if (unknown !== void 0) {
307
- return { kind: "error", message: `leglas list does not take ${unknown}.` };
308
- }
309
- return { kind: "list", json: rest.includes("--json") };
310
- }
311
- if (argv[0] === "show") {
312
- const rest = argv.slice(1);
313
- let title;
314
- let json = false;
315
- let screenshot = false;
316
- let width = null;
317
- let port = null;
318
- for (let index = 0; index < rest.length; index += 1) {
319
- const argument = rest[index];
320
- if (argument === "--json") {
321
- json = true;
322
- continue;
323
- }
324
- if (argument === "--screenshot") {
325
- screenshot = true;
326
- continue;
327
- }
328
- if (argument === "--width" || argument.startsWith("--width=") || argument === "--port" || argument.startsWith("--port=")) {
329
- const equals = argument.indexOf("=");
330
- const flag = equals === -1 ? argument : argument.slice(0, equals);
331
- const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
332
- if (raw === void 0 || raw === "") {
333
- return { kind: "error", message: `${flag} needs a value.` };
334
- }
335
- if (flag === "--port") {
336
- const parsed2 = parsePort(flag, raw);
337
- if (typeof parsed2 !== "number") return { kind: "error", message: parsed2.error };
338
- port = parsed2;
339
- continue;
340
- }
341
- if (!/^\d+$/.test(raw)) {
342
- return { kind: "error", message: `--width needs a number, received ${JSON.stringify(raw)}.` };
343
- }
344
- width = Number(raw);
345
- if (width < MIN_SHOW_WIDTH || width > MAX_SHOW_WIDTH) {
346
- return {
347
- kind: "error",
348
- message: `--width must be between ${MIN_SHOW_WIDTH} and ${MAX_SHOW_WIDTH}, received ${width}.`
349
- };
350
- }
351
- continue;
352
- }
353
- if (argument.startsWith("-")) {
354
- return { kind: "error", message: `leglas show does not take ${argument}.` };
355
- }
356
- if (title !== void 0) {
357
- return { kind: "error", message: "leglas show takes one direction title." };
358
- }
359
- title = argument;
360
- }
361
- if (title === void 0) {
362
- return {
363
- kind: "error",
364
- message: 'leglas show needs a direction title, for example: npx leglas show "Aurora" --json'
365
- };
366
- }
367
- if (width !== null && !screenshot) {
368
- return { kind: "error", message: "leglas show --width needs --screenshot." };
369
- }
370
- if (port !== null && !screenshot) {
371
- return { kind: "error", message: "leglas show --port needs --screenshot." };
372
- }
373
- return { kind: "show", title, json, screenshot, width, port };
374
- }
375
- const options = {
376
- port: void 0,
377
- userPort: void 0,
378
- configPath: void 0,
379
- open: true,
380
- json: false
305
+ return {
306
+ command: template.command,
307
+ args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
381
308
  };
382
- for (let index = 0; index < argv.length; index += 1) {
383
- const argument = argv[index];
384
- if (argument === "--help" || argument === "-h") return { kind: "help" };
385
- if (argument === "--version" || argument === "-v") return { kind: "version" };
386
- if (BOOLEAN_FLAGS.has(argument)) {
387
- if (argument === "--no-open") options.open = false;
388
- if (argument === "--json") options.json = true;
389
- continue;
390
- }
391
- const equals = argument.indexOf("=");
392
- const flag = equals === -1 ? argument : argument.slice(0, equals);
393
- if (!VALUE_FLAGS.has(flag)) {
394
- return {
395
- kind: "error",
396
- message: argument.startsWith("-") ? `Unknown flag ${argument}. Run leglas --help to see the options.` : `Unexpected argument ${JSON.stringify(argument)}. leglas takes flags only.`
397
- };
398
- }
399
- let value;
400
- if (equals === -1) {
401
- value = argv[index + 1];
402
- index += 1;
403
- } else {
404
- value = argument.slice(equals + 1);
405
- }
406
- if (value === void 0 || value === "" || value.startsWith("--")) {
407
- return { kind: "error", message: `${flag} needs a value.` };
408
- }
409
- if (flag === "--config") {
410
- options.configPath = value;
411
- continue;
412
- }
413
- const port = parsePort(flag, value);
414
- if (typeof port !== "number") return { kind: "error", message: port.error };
415
- if (flag === "--port") options.port = port;
416
- else options.userPort = port;
417
- }
418
- return { kind: "run", options };
419
- }
420
-
421
- // src/run-classify.ts
422
- import { stat as stat4 } from "fs/promises";
423
- import { join as join13 } from "path";
424
-
425
- // ../server/dist/log.js
426
- var DEFAULT_LOG_DIR = "design-log";
427
- function slugify(value) {
428
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
429
- }
430
- function frameFor(title, requests) {
431
- let found = null;
432
- for (const request of requests) {
433
- if (request.title !== title)
434
- continue;
435
- for (const attachment of request.attachments ?? []) {
436
- if (attachment.kind === "frame")
437
- found = attachment;
438
- }
439
- }
440
- return found;
441
- }
442
- function askedOf(title, requests) {
443
- return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
444
309
  }
445
- function composeEntry(input) {
446
- const slug = `${input.date}-${slugify(input.surface)}`;
447
- const pictures = [];
448
- const lines2 = [];
449
- lines2.push(`# ${input.surface}, ${input.date}`);
450
- lines2.push("");
451
- lines2.push(`**${input.won.title}** won and became \`${input.won.to}\`. ${input.previews.length === 1 ? "It was the only direction." : `${input.previews.length} directions were compared.`}`);
452
- lines2.push("");
453
- for (const preview of input.previews) {
454
- const won = preview.title === input.won.title;
455
- lines2.push(`## ${preview.title}${won ? " \u2014 kept" : ""}`);
456
- lines2.push("");
457
- if (preview.note !== void 0 && preview.note.trim() !== "") {
458
- lines2.push(preview.note.trim());
459
- lines2.push("");
460
- }
461
- const frame = frameFor(preview.title, input.requests);
462
- if (frame !== null) {
463
- const name = `${slugify(preview.title)}.png`;
464
- pictures.push({ from: frame.file, to: name });
465
- lines2.push(`![${preview.title}](${slug}/${name})`);
466
- lines2.push("");
467
- }
468
- if (preview.basedOn !== void 0) {
469
- lines2.push(`A variant of ${preview.basedOn}.`);
470
- lines2.push("");
471
- }
472
- const asked = askedOf(preview.title, input.requests);
473
- if (asked.length > 0) {
474
- lines2.push("Asked for:");
475
- lines2.push("");
476
- for (const words of asked)
477
- lines2.push(`- ${words}`);
478
- lines2.push("");
479
- }
480
- const notes = input.annotations.filter((note) => note.title === preview.title);
481
- if (notes.length > 0) {
482
- lines2.push("Marked on the design:");
483
- lines2.push("");
484
- for (const note of notes)
485
- lines2.push(`- ${note.note}`);
486
- lines2.push("");
487
- }
488
- }
489
- const failed = input.requests.filter((request) => request.status === "failed");
490
- if (failed.length > 0) {
491
- lines2.push("## Changes that did not land");
492
- lines2.push("");
493
- for (const request of failed) {
494
- const why = request.failure?.message;
495
- lines2.push(`- ${request.title}: ${request.intent}${why === void 0 ? "" : ` (${why})`}`);
496
- }
497
- lines2.push("");
498
- }
499
- return { slug, markdown: `${lines2.join("\n").trimEnd()}
500
- `, pictures };
501
- }
502
-
503
- // ../server/dist/config.js
504
- var DEFAULT_DEV_SERVER = "http://localhost:3000";
505
- var DEFAULT_INSTALL_COMMAND = "npm install";
506
- var IMPLICIT_PREVIEW = { title: "App", url: "/" };
507
- function isRecord(value) {
508
- return typeof value === "object" && value !== null && !Array.isArray(value);
509
- }
510
- function isValidOrigin(value) {
511
- try {
512
- const url = new URL(value);
513
- return url.protocol === "http:" || url.protocol === "https:";
514
- } catch {
515
- return false;
516
- }
517
- }
518
- function isSafeBranch(value) {
519
- if (value === "" || value.startsWith("-"))
520
- return false;
521
- if (value.split("/").some((segment) => segment === "." || segment === ".."))
522
- return false;
523
- return /^[A-Za-z0-9._/-]+$/.test(value);
524
- }
525
- function isValidPreviewUrl(value) {
526
- return value.startsWith("/") || isValidOrigin(value);
527
- }
528
- function isSafePreviewFile(value) {
529
- if (value === "" || value.startsWith("/") || value.startsWith("\\"))
530
- return false;
531
- if (/^[A-Za-z]:/.test(value))
532
- return false;
533
- return !value.split(/[/\\]/).some((segment) => segment === "..");
534
- }
535
- function normalizeConfig(raw, options = {}) {
536
- const requireDevCommand = options.requireDevCommand ?? true;
537
- const errors = [];
538
- const source = raw === void 0 || raw === null ? {} : raw;
539
- if (!isRecord(source)) {
540
- return { config: null, errors: ["Config must export an object."] };
541
- }
542
- const devServer = source["devServer"] ?? DEFAULT_DEV_SERVER;
543
- if (typeof devServer !== "string" || !isValidOrigin(devServer)) {
544
- errors.push(`devServer must be an http(s) URL, received ${JSON.stringify(devServer)}.`);
545
- }
546
- const rawPreviews = source["previews"] ?? [IMPLICIT_PREVIEW];
547
- if (!Array.isArray(rawPreviews)) {
548
- errors.push(`previews must be an array, received ${JSON.stringify(rawPreviews)}.`);
549
- return { config: null, errors };
550
- }
551
- const previews = [];
552
- const seenTitles = /* @__PURE__ */ new Set();
553
- rawPreviews.forEach((entry, index) => {
554
- const at = `previews[${index}]`;
555
- if (!isRecord(entry)) {
556
- errors.push(`${at} must be an object.`);
557
- return;
558
- }
559
- const title = entry["title"];
560
- const url = entry["url"];
561
- const file = entry["file"];
562
- if (typeof title !== "string" || title.trim() === "") {
563
- errors.push(`${at} needs a title; the rail has nothing to show without one.`);
564
- } else if (seenTitles.has(title)) {
565
- errors.push(`${at} repeats the title ${JSON.stringify(title)}; titles must be unique.`);
566
- } else {
567
- seenTitles.add(title);
568
- }
569
- if (file !== void 0) {
570
- if (typeof file !== "string" || !isSafePreviewFile(file)) {
571
- errors.push(`${at} has an unusable file ${JSON.stringify(file)}; use a path inside the project, like "directions/hero.html".`);
572
- }
573
- if (url !== void 0) {
574
- errors.push(`${at} names a file and a url; a file preview's url is assigned by Leglas.`);
575
- }
576
- } else if (typeof url !== "string" || url.trim() === "") {
577
- errors.push(`${at} needs a url.`);
578
- } else if (!isValidPreviewUrl(url)) {
579
- errors.push(`${at} has url ${JSON.stringify(url)}; use a root-relative path ("/pricing") or a full URL.`);
580
- }
581
- const branch = entry["branch"];
582
- if (branch !== void 0) {
583
- if (typeof branch !== "string" || !isSafeBranch(branch)) {
584
- errors.push(`${at} has an unusable branch ${JSON.stringify(branch)}; use a plain git branch name.`);
585
- } else if (typeof url === "string" && !url.startsWith("/")) {
586
- errors.push(`${at} names a branch and an absolute url; a branch preview is served by Leglas, so its url must be a path.`);
587
- }
588
- if (file !== void 0) {
589
- errors.push(`${at} names a branch and a file; a file preview is served by Leglas itself and has no checkout.`);
590
- }
591
- }
592
- const basedOn = entry["basedOn"];
593
- if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
594
- errors.push(`${at} has a basedOn that is not a direction title.`);
595
- }
596
- const askedFor = entry["askedFor"];
597
- if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
598
- errors.push(`${at} has an askedFor that is not a change request.`);
599
- }
600
- const tags = entry["tags"];
601
- previews.push({
602
- title: typeof title === "string" ? title : "",
603
- url: typeof url === "string" ? url : "",
604
- note: typeof entry["note"] === "string" ? entry["note"] : void 0,
605
- tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
606
- ...typeof branch === "string" ? { branch } : {},
607
- ...typeof file === "string" ? { file } : {},
608
- ...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
609
- ...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
610
- });
611
- });
612
- const devCommand = source["devCommand"];
613
- if (devCommand !== void 0 && typeof devCommand !== "string") {
614
- errors.push("devCommand must be a string.");
615
- } else if (typeof devCommand === "string" && !devCommand.includes("{port}")) {
616
- errors.push(`devCommand must include {port}, so Leglas can start each checkout on a free port. Received ${JSON.stringify(devCommand)}.`);
617
- }
618
- if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
619
- errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
620
- }
621
- const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
622
- if (typeof logDir !== "string" || logDir.trim() === "") {
623
- errors.push("logDir must be a non-empty string.");
624
- }
625
- const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
626
- if (typeof installCommand !== "string" || installCommand.trim() === "") {
627
- errors.push("installCommand must be a non-empty string.");
628
- }
629
- const scanPreviews = source["scanPreviews"] ?? true;
630
- if (typeof scanPreviews !== "boolean") {
631
- errors.push("scanPreviews must be a boolean.");
632
- }
633
- if (errors.length > 0)
634
- return { config: null, errors };
635
- return {
636
- config: {
637
- devServer,
638
- previews,
639
- scanPreviews,
640
- devCommand: typeof devCommand === "string" ? devCommand : void 0,
641
- installCommand,
642
- logDir
643
- },
644
- errors: []
645
- };
646
- }
647
-
648
- // ../server/dist/agent-command.js
649
- var WATCH_PATH = ".leglas/watch.json";
650
- var PROMPT_TOKEN = "{prompt}";
651
- var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
652
- function tokenize(template) {
653
- const tokens = [];
654
- let current = "";
655
- let started = false;
656
- let quote = null;
657
- for (const character of template) {
658
- if (quote !== null) {
659
- if (character === quote)
660
- quote = null;
661
- else
662
- current += character;
663
- continue;
664
- }
665
- if (character === '"' || character === "'") {
666
- quote = character;
667
- started = true;
668
- continue;
669
- }
670
- if (/\s/.test(character)) {
671
- if (started)
672
- tokens.push(current);
673
- current = "";
674
- started = false;
675
- continue;
676
- }
677
- current += character;
678
- started = true;
679
- }
680
- if (quote !== null) {
681
- return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
682
- }
683
- if (started)
684
- tokens.push(current);
685
- return { ok: true, tokens };
686
- }
687
- function parseTemplate(raw) {
688
- const tokenized = tokenize(raw);
689
- if (!tokenized.ok)
690
- return tokenized;
691
- const { tokens } = tokenized;
692
- const [command, ...args] = tokens;
693
- if (command === void 0) {
694
- return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
695
- }
696
- if (tokens.some((token) => token !== PROMPT_TOKEN && token.includes(PROMPT_TOKEN))) {
697
- return {
698
- ok: false,
699
- error: `${PROMPT_TOKEN} must stand as a word of its own, for example: ${EXAMPLE}`
700
- };
701
- }
702
- const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
703
- if (placeholders > 1) {
704
- return {
705
- ok: false,
706
- error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
707
- };
708
- }
709
- if (command === PROMPT_TOKEN) {
710
- return {
711
- ok: false,
712
- error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
713
- };
714
- }
715
- return { ok: true, template: { command, args } };
716
- }
717
- function commandFor(template, prompt) {
718
- if (!template.args.includes(PROMPT_TOKEN)) {
719
- return { command: template.command, args: [...template.args, prompt] };
720
- }
721
- return {
722
- command: template.command,
723
- args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
724
- };
725
- }
726
- function nextRequest(requests, failed) {
727
- return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
310
+ function nextRequest(requests, failed) {
311
+ return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
728
312
  }
729
313
 
730
314
  // ../server/dist/agents.js
@@ -857,13 +441,21 @@ var KNOWN_AGENTS = {
857
441
  name: "Cursor",
858
442
  binary: "cursor-agent",
859
443
  efforts: [],
444
+ // `--trust` on every invocation. Without it, print mode stops at a
445
+ // "Workspace Trust Required" prompt nothing can answer and exits 1 with
446
+ // no events, in any directory Cursor has not been trusted for by hand:
447
+ // measured against cursor-agent 2026.09.02, one second, zero output. The
448
+ // project is the one the user pointed Leglas at, which is the trust the
449
+ // flag grants. It is the only permission the run needs: with it alone,
450
+ // the edit and the shell command in the same run both executed.
860
451
  args: (prompt, _effort = null, _images = []) => [
861
452
  "-p",
862
453
  prompt,
863
454
  "--output-format",
864
- "stream-json"
455
+ "stream-json",
456
+ "--trust"
865
457
  ],
866
- terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt],
458
+ terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt, "--trust"],
867
459
  // `--resume [chatId]` is documented alongside `--continue` in the CLI
868
460
  // parameter reference, and every stream-json event carries the
869
461
  // `session_id` to feed it. Cursor exposes no persistent transport the way
@@ -874,11 +466,16 @@ var KNOWN_AGENTS = {
874
466
  // Images are accepted and ignored, like its other argument builders:
875
467
  // `cursor-agent` documents no flag for them, and the capture paths reach
876
468
  // it as text in the prompt either way.
877
- resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json"],
469
+ resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json", "--trust"],
878
470
  sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
471
+ // Read against cursor-agent 2026.09.02: every event carries the id, a
472
+ // resume in this argument order answers under the same id and remembers
473
+ // the earlier turn.
474
+ activityVerified: true,
879
475
  authArgs: ["status"],
880
- // UNVERIFIED: cursor-agent was not available on the build machine. The
881
- // reading is deliberately loose, and anything ambiguous stays unknown.
476
+ // The signed-in form was read from the CLI: "✓ Logged in as <email>",
477
+ // exit 0. The signed-out form has not been, so that side of the reading
478
+ // stays loose, and anything ambiguous stays unknown.
882
479
  authVerdict: (result2) => {
883
480
  if (/logged in|signed in/i.test(result2.stdout))
884
481
  return "ok";
@@ -931,7 +528,7 @@ function agentSearchPath(env = process.env, platform = process.platform) {
931
528
  const npmPrefix = env.NPM_CONFIG_PREFIX;
932
529
  const versionBins = (root, suffix) => {
933
530
  try {
934
- return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(root, entry.name, ...suffix));
531
+ return readdirSync(root, { withFileTypes: true }).filter((entry2) => entry2.isDirectory()).map((entry2) => join(root, entry2.name, ...suffix));
935
532
  } catch {
936
533
  return [];
937
534
  }
@@ -962,19 +559,19 @@ function agentSearchPath(env = process.env, platform = process.platform) {
962
559
  platform === "darwin" ? "/usr/local/bin" : void 0,
963
560
  platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
964
561
  platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
965
- ].filter((entry) => typeof entry === "string" && entry !== "");
562
+ ].filter((entry2) => typeof entry2 === "string" && entry2 !== "");
966
563
  return [...new Set(candidates)].join(delimiter);
967
564
  }
968
565
  function agentEnvironment(env = process.env) {
969
566
  return { ...env, PATH: agentSearchPath(env) };
970
567
  }
971
568
  async function pathLookup(binary, env = process.env, platform = process.platform) {
972
- const entries = agentSearchPath(env, platform).split(delimiter).filter((entry) => entry !== "");
973
- const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
974
- for (const entry of entries) {
569
+ const entries = agentSearchPath(env, platform).split(delimiter).filter((entry2) => entry2 !== "");
570
+ const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry2) => entry2 !== "") : [""];
571
+ for (const entry2 of entries) {
975
572
  for (const extension of extensions) {
976
573
  try {
977
- await access(join(entry, `${binary}${extension}`), constants.X_OK);
574
+ await access(join(entry2, `${binary}${extension}`), constants.X_OK);
978
575
  return true;
979
576
  } catch {
980
577
  }
@@ -1082,15 +679,15 @@ function cursorActivity(event, cwd) {
1082
679
  const wrapper = record(event.tool_call);
1083
680
  if (wrapper === null)
1084
681
  return null;
1085
- const key = Object.keys(wrapper)[0];
682
+ const key = Object.keys(wrapper).find((name) => name.endsWith("ToolCall"));
1086
683
  if (key === void 0)
1087
684
  return null;
1088
685
  const call = record(wrapper[key]);
1089
686
  const args = record(call?.args);
1090
687
  const tool = key.replace(/ToolCall$/, "");
1091
- if (tool === "write") {
688
+ if (tool === "edit" || tool === "write") {
1092
689
  const path2 = shownPath(args?.path, cwd);
1093
- return path2 === null ? "using write" : `editing ${path2}`;
690
+ return path2 === null ? "editing a file" : `editing ${path2}`;
1094
691
  }
1095
692
  if (tool === "read") {
1096
693
  const path2 = shownPath(args?.path, cwd);
@@ -1418,7 +1015,7 @@ var SHARE_COOKIE = "leglas-share";
1418
1015
  function withoutShareCookie(cookie) {
1419
1016
  if (cookie === void 0)
1420
1017
  return void 0;
1421
- const kept = (Array.isArray(cookie) ? cookie.join("; ") : cookie).split(";").map((entry) => entry.trim()).filter((entry) => entry !== "" && !entry.startsWith(`${SHARE_COOKIE}=`));
1018
+ const kept = (Array.isArray(cookie) ? cookie.join("; ") : cookie).split(";").map((entry2) => entry2.trim()).filter((entry2) => entry2 !== "" && !entry2.startsWith(`${SHARE_COOKIE}=`));
1422
1019
  return kept.length === 0 ? void 0 : kept.join("; ");
1423
1020
  }
1424
1021
  function createProxyHandler(options) {
@@ -1633,8 +1230,8 @@ var HEADLESS_SHELL = [
1633
1230
  ["chrome-headless-shell-linux64", "chrome-headless-shell"],
1634
1231
  ["chrome-headless-shell-win64", "chrome-headless-shell.exe"]
1635
1232
  ];
1636
- function buildNumber(entry) {
1637
- const digits = /(\d+)\s*$/.exec(entry)?.[1];
1233
+ function buildNumber(entry2) {
1234
+ const digits = /(\d+)\s*$/.exec(entry2)?.[1];
1638
1235
  return digits === void 0 ? 0 : Number(digits);
1639
1236
  }
1640
1237
  function cacheRoots(platform, home, readdir5 = readableDirectories) {
@@ -1645,7 +1242,7 @@ function cacheRoots(platform, home, readdir5 = readableDirectories) {
1645
1242
  entries: readdir5(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
1646
1243
  });
1647
1244
  return [
1648
- newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
1245
+ newestFirst(playwright, (entry2) => entry2.startsWith("chromium-") || entry2.startsWith("chromium_headless_shell-")),
1649
1246
  newestFirst(join4(puppeteer, "chrome-headless-shell")),
1650
1247
  newestFirst(join4(puppeteer, "chrome"))
1651
1248
  ];
@@ -1670,7 +1267,7 @@ function findBrowser(search = {}) {
1670
1267
  return candidate;
1671
1268
  }
1672
1269
  const caches = cacheRoots(platform, home, readdir5);
1673
- const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join4(root, entry, ...rest)))));
1270
+ const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry2) => HEADLESS_SHELL.map((rest) => join4(root, entry2, ...rest)))));
1674
1271
  if (shell !== null)
1675
1272
  return shell;
1676
1273
  if (platform === "darwin") {
@@ -1692,15 +1289,15 @@ function findBrowser(search = {}) {
1692
1289
  return installed;
1693
1290
  }
1694
1291
  if (platform === "win32") {
1695
- const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => typeof entry === "string" && entry !== "");
1292
+ const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry2) => typeof entry2 === "string" && entry2 !== "");
1696
1293
  const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) => join4(root, browser))));
1697
1294
  if (installed !== null)
1698
1295
  return installed;
1699
1296
  }
1700
1297
  if (platform === "darwin" || platform === "linux") {
1701
1298
  const playwrightRoot = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
1702
- const playwright = readdir5(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
1703
- const root = join4(playwrightRoot, entry);
1299
+ const playwright = readdir5(playwrightRoot).filter((entry2) => entry2.startsWith("chromium-") || entry2.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry2) => {
1300
+ const root = join4(playwrightRoot, entry2);
1704
1301
  return [
1705
1302
  ...FOR_TESTING.map((rest) => join4(root, ...rest)),
1706
1303
  ...HEADLESS_SHELL.map((rest) => join4(root, ...rest)),
@@ -1716,8 +1313,8 @@ function findBrowser(search = {}) {
1716
1313
  const puppeteerCache = join4(home, ".cache", "puppeteer");
1717
1314
  for (const kind of ["chrome", "chrome-headless-shell"]) {
1718
1315
  const kindRoot = join4(puppeteerCache, kind);
1719
- const found = firstExisting(readdir5(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
1720
- const root = join4(kindRoot, entry);
1316
+ const found = firstExisting(readdir5(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry2) => {
1317
+ const root = join4(kindRoot, entry2);
1721
1318
  return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join4(root, ...rest));
1722
1319
  }));
1723
1320
  if (found !== null)
@@ -1840,12 +1437,12 @@ async function closeOrphan(url, connect) {
1840
1437
  async function reapOrphanedBrowsers(deps = {}) {
1841
1438
  const root = deps.tmpdir ?? osTmpdir();
1842
1439
  const clock = deps.now ?? (() => Date.now());
1843
- const list = deps.list ?? (async (path) => (await readdir(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
1440
+ const list = deps.list ?? (async (path) => (await readdir(path, { withFileTypes: true })).filter((entry2) => entry2.isDirectory()).map((entry2) => entry2.name));
1844
1441
  const read = deps.read ?? ((path) => readFile4(path, "utf8"));
1845
1442
  const alive = deps.alive ?? livePid;
1846
1443
  const profileOf = deps.profile ?? (async (path) => {
1847
- const entry = await stat(path);
1848
- return { createdAt: entry.birthtimeMs, uid: entry.uid };
1444
+ const entry2 = await stat(path);
1445
+ return { createdAt: entry2.birthtimeMs, uid: entry2.uid };
1849
1446
  });
1850
1447
  const currentUid = deps.uid ?? (() => nodeProcess.getuid?.() ?? null);
1851
1448
  const connect = deps.connect ?? connectWebSocket;
@@ -2335,7 +1932,7 @@ function validBox(value) {
2335
1932
  if (typeof value !== "object" || value === null)
2336
1933
  return false;
2337
1934
  const box = value;
2338
- return [box.x, box.y, box.width, box.height].every((entry) => typeof entry === "number" && Number.isFinite(entry));
1935
+ return [box.x, box.y, box.width, box.height].every((entry2) => typeof entry2 === "number" && Number.isFinite(entry2));
2339
1936
  }
2340
1937
  function locatorExpression(focus) {
2341
1938
  return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
@@ -2778,8 +2375,8 @@ function rehomeText(text2, from, to) {
2778
2375
  async function ownCapturesRoot(cwd) {
2779
2376
  const root = join5(cwd, CAPTURES_DIR);
2780
2377
  try {
2781
- const entry = await lstat(root);
2782
- return entry.isDirectory() ? root : null;
2378
+ const entry2 = await lstat(root);
2379
+ return entry2.isDirectory() ? root : null;
2783
2380
  } catch {
2784
2381
  return null;
2785
2382
  }
@@ -2800,7 +2397,7 @@ async function pruneCaptures(cwd, keep) {
2800
2397
  return void await pruneReferences(cwd);
2801
2398
  try {
2802
2399
  const entries = await readdir2(root, { withFileTypes: true });
2803
- await Promise.all(entries.filter((entry) => entry.isDirectory() && !kept.has(entry.name)).map((entry) => rm2(join5(root, entry.name), { recursive: true, force: true })));
2400
+ await Promise.all(entries.filter((entry2) => entry2.isDirectory() && !kept.has(entry2.name)).map((entry2) => rm2(join5(root, entry2.name), { recursive: true, force: true })));
2804
2401
  } catch {
2805
2402
  }
2806
2403
  await pruneReferences(cwd);
@@ -2812,8 +2409,8 @@ async function pruneReferences(cwd) {
2812
2409
  return;
2813
2410
  const entries = await readdir2(references, { withFileTypes: true });
2814
2411
  const old = Date.now() - 60 * 60 * 1e3;
2815
- await Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
2816
- const file = join5(references, entry.name);
2412
+ await Promise.all(entries.filter((entry2) => entry2.isFile()).map(async (entry2) => {
2413
+ const file = join5(references, entry2.name);
2817
2414
  if ((await stat2(file)).mtimeMs < old)
2818
2415
  await unlink(file).catch(() => {
2819
2416
  });
@@ -3096,7 +2693,7 @@ function createBranchRegistry(options) {
3096
2693
  clearInterval(sweepTimer);
3097
2694
  stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
3098
2695
  await Promise.allSettled([...stopping.values()]);
3099
- const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
2696
+ const ready = [...states.entries()].filter((entry2) => entry2[1].status === "ready" && proxies.has(entry2[0]));
3100
2697
  await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
3101
2698
  });
3102
2699
  return stopPromise;
@@ -3204,7 +2801,7 @@ function anchorFrom(value) {
3204
2801
  if (selector === "")
3205
2802
  return null;
3206
2803
  const rect = isRecord2(value["rect"]) ? value["rect"] : {};
3207
- const classes = Array.isArray(value["classes"]) ? value["classes"].filter((entry) => typeof entry === "string").slice(0, CLASS_CAP).map((entry) => entry.slice(0, CLASS_LENGTH_CAP)) : [];
2804
+ const classes = Array.isArray(value["classes"]) ? value["classes"].filter((entry2) => typeof entry2 === "string").slice(0, CLASS_CAP).map((entry2) => entry2.slice(0, CLASS_LENGTH_CAP)) : [];
3208
2805
  const rawRegion = isRecord2(value["region"]) ? value["region"] : null;
3209
2806
  const region = rawRegion === null ? null : {
3210
2807
  height: fraction(rawRegion["height"]),
@@ -3212,9 +2809,9 @@ function anchorFrom(value) {
3212
2809
  x: fraction(rawRegion["x"]),
3213
2810
  y: fraction(rawRegion["y"])
3214
2811
  };
3215
- const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
3216
- tag: text(entry["tag"], TAG_CAP) || "element",
3217
- text: text(entry["text"], TEXT_CAP)
2812
+ const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry2) => ({
2813
+ tag: text(entry2["tag"], TAG_CAP) || "element",
2814
+ text: text(entry2["text"], TEXT_CAP)
3218
2815
  })) : [];
3219
2816
  return {
3220
2817
  classes,
@@ -3242,18 +2839,18 @@ async function readAnnotations(cwd) {
3242
2839
  const parsed2 = JSON.parse(raw);
3243
2840
  if (!Array.isArray(parsed2.annotations))
3244
2841
  return [];
3245
- return parsed2.annotations.flatMap((entry, index) => {
3246
- if (!isRecord2(entry))
2842
+ return parsed2.annotations.flatMap((entry2, index) => {
2843
+ if (!isRecord2(entry2))
3247
2844
  return [];
3248
- const anchor = anchorFrom(entry["anchor"]);
3249
- const title = text(entry["title"], TAG_CAP * 4);
2845
+ const anchor = anchorFrom(entry2["anchor"]);
2846
+ const title = text(entry2["title"], TAG_CAP * 4);
3250
2847
  if (anchor === null || title === "")
3251
2848
  return [];
3252
2849
  return [
3253
2850
  {
3254
2851
  anchor,
3255
- id: typeof entry["id"] === "string" ? entry["id"] : String(index),
3256
- note: text(entry["note"], NOTE_CAP),
2852
+ id: typeof entry2["id"] === "string" ? entry2["id"] : String(index),
2853
+ note: text(entry2["note"], NOTE_CAP),
3257
2854
  title
3258
2855
  }
3259
2856
  ];
@@ -3284,7 +2881,7 @@ async function addAnnotation(cwd, input) {
3284
2881
  async function updateAnnotation(cwd, id, note) {
3285
2882
  return inTurn(async () => {
3286
2883
  const annotations = await readAnnotations(cwd);
3287
- const found = annotations.find((entry) => entry.id === id);
2884
+ const found = annotations.find((entry2) => entry2.id === id);
3288
2885
  if (found === void 0)
3289
2886
  return null;
3290
2887
  const words = text(note, NOTE_CAP);
@@ -3295,7 +2892,7 @@ async function updateAnnotation(cwd, id, note) {
3295
2892
  id: randomBytes2(6).toString("base64url"),
3296
2893
  note: words
3297
2894
  };
3298
- await write(cwd, annotations.map((entry) => entry.id === id ? revised : entry));
2895
+ await write(cwd, annotations.map((entry2) => entry2.id === id ? revised : entry2));
3299
2896
  return revised;
3300
2897
  });
3301
2898
  }
@@ -3303,7 +2900,7 @@ async function removeAnnotations(cwd, ids) {
3303
2900
  return inTurn(async () => {
3304
2901
  const wanted = new Set(ids);
3305
2902
  const annotations = await readAnnotations(cwd);
3306
- const remaining = annotations.filter((entry) => !wanted.has(entry.id));
2903
+ const remaining = annotations.filter((entry2) => !wanted.has(entry2.id));
3307
2904
  const dropped = annotations.length - remaining.length;
3308
2905
  if (dropped > 0)
3309
2906
  await write(cwd, remaining);
@@ -3311,12 +2908,12 @@ async function removeAnnotations(cwd, ids) {
3311
2908
  });
3312
2909
  }
3313
2910
  function annotationsFor(annotations, title) {
3314
- return annotations.filter((entry) => entry.title === title);
2911
+ return annotations.filter((entry2) => entry2.title === title);
3315
2912
  }
3316
2913
  function describeAnchor(anchor) {
3317
2914
  const where = `about ${anchor.rect.width}\xD7${anchor.rect.height} at (${anchor.rect.x}, ${anchor.rect.y}) in a ${anchor.viewport}px-wide viewport`;
3318
2915
  if (anchor.region !== void 0) {
3319
- const covered = (anchor.covers ?? []).map((entry) => entry.text === "" ? `<${entry.tag}>` : `<${entry.tag}> \u201C${entry.text}\u201D`).join(", ");
2916
+ const covered = (anchor.covers ?? []).map((entry2) => entry2.text === "" ? `<${entry2.tag}>` : `<${entry2.tag}> \u201C${entry2.text}\u201D`).join(", ");
3320
2917
  const inside = covered === "" ? "" : ` covering ${covered};`;
3321
2918
  return `an area inside <${anchor.tag}>;${inside} path ${anchor.selector}; ${where}`;
3322
2919
  }
@@ -3368,7 +2965,7 @@ function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx
3368
2965
  const target = preview.file ?? targetFor(preview.url);
3369
2966
  const cleaned = intent.trim();
3370
2967
  const asked = changeBlock(cleaned, notes);
3371
- const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
2968
+ const recorded = cleaned === "" ? notes.map((entry2) => entry2.note).filter((entry2) => entry2 !== "").join("; ") : cleaned;
3372
2969
  const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand, captured) : replacePrompt(preview, asked, target, leglasCommand, captured);
3373
2970
  return { prompt, target, mode };
3374
2971
  }
@@ -3505,12 +3102,12 @@ var FAILURE_CODES = [
3505
3102
  function failureOf(value) {
3506
3103
  if (typeof value !== "object" || value === null)
3507
3104
  return null;
3508
- const entry = value;
3509
- if (typeof entry.message !== "string" || entry.message === "")
3105
+ const entry2 = value;
3106
+ if (typeof entry2.message !== "string" || entry2.message === "")
3510
3107
  return null;
3511
- if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
3108
+ if (entry2.code === void 0 || !FAILURE_CODES.includes(entry2.code))
3512
3109
  return null;
3513
- return { code: entry.code, message: entry.message };
3110
+ return { code: entry2.code, message: entry2.message };
3514
3111
  }
3515
3112
  async function readRequests(cwd) {
3516
3113
  try {
@@ -3520,17 +3117,17 @@ async function readRequests(cwd) {
3520
3117
  return [];
3521
3118
  return parsed2.requests.map((request, index) => {
3522
3119
  const source = typeof request === "object" && request !== null ? request : {};
3523
- const { failure: rawFailure, attachments: rawAttachments, captureNote: rawCaptureNote, compare: rawCompare, references: rawReferences, ...entry } = source;
3524
- const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
3120
+ const { failure: rawFailure, attachments: rawAttachments, captureNote: rawCaptureNote, compare: rawCompare, references: rawReferences, ...entry2 } = source;
3121
+ const status = entry2.status === "picked-up" || entry2.status === "failed" || entry2.status === "cancelled" ? entry2.status : "queued";
3525
3122
  const failure = isTerminal(status) ? failureOf(rawFailure) : null;
3526
- const id = typeof entry.id === "string" && REQUEST_ID.test(entry.id) ? entry.id : String(index);
3123
+ const id = typeof entry2.id === "string" && REQUEST_ID.test(entry2.id) ? entry2.id : String(index);
3527
3124
  const ownFile = new RegExp(`^\\.leglas/captures/${id}/[A-Za-z0-9][A-Za-z0-9_.-]*$`);
3528
3125
  const attachments = Array.isArray(rawAttachments) ? rawAttachments.filter((attachment) => typeof attachment === "object" && attachment !== null && !Array.isArray(attachment) && typeof attachment.file === "string" && ownFile.test(attachment.file) && !attachment.file.includes("..") && ["frame", "note", "compare", "reference"].includes(String(attachment.kind))) : null;
3529
3126
  return {
3530
- ...entry,
3127
+ ...entry2,
3531
3128
  id,
3532
3129
  status,
3533
- mode: entry.mode === "variant" ? "variant" : "replace",
3130
+ mode: entry2.mode === "variant" ? "variant" : "replace",
3534
3131
  ...failure === null ? {} : { failure },
3535
3132
  ...attachments === null || attachments.length === 0 ? {} : { attachments },
3536
3133
  ...typeof rawCaptureNote === "string" ? { captureNote: rawCaptureNote } : {},
@@ -5608,10 +5205,10 @@ function isRecord3(value) {
5608
5205
  return typeof value === "object" && value !== null && !Array.isArray(value);
5609
5206
  }
5610
5207
  function stringArray(value) {
5611
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
5208
+ return Array.isArray(value) && value.every((entry2) => typeof entry2 === "string");
5612
5209
  }
5613
5210
  function stringRecord(value) {
5614
- return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
5211
+ return isRecord3(value) && Object.values(value).every((entry2) => typeof entry2 === "string");
5615
5212
  }
5616
5213
  function layoutFrom(value) {
5617
5214
  if (!isRecord3(value))
@@ -6505,7 +6102,7 @@ async function readRenames(cwd) {
6505
6102
  const parsed2 = JSON.parse(raw);
6506
6103
  if (parsed2.renames === null || typeof parsed2.renames !== "object")
6507
6104
  return {};
6508
- return Object.fromEntries(Object.entries(parsed2.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
6105
+ return Object.fromEntries(Object.entries(parsed2.renames).filter((entry2) => typeof entry2[1] === "string" && entry2[1] !== ""));
6509
6106
  } catch {
6510
6107
  return {};
6511
6108
  }
@@ -7072,86 +6669,670 @@ async function startServer(options) {
7072
6669
  void probeAgents().catch(() => {
7073
6670
  });
7074
6671
  }
7075
- return Promise.resolve(agentsCache.agents);
7076
- };
7077
- const livePreviewDefinitions = async () => {
7078
- const localRead = await readLocalPreviews(cwd).catch(() => null);
7079
- const local = localRead?.errors.length === 0 ? localRead.previews : [];
7080
- const localTitles = new Set(local.map((entry) => entry.title));
7081
- const bootConfig = config?.previews ?? [];
7082
- const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
7083
- const known = new Set(boot.map((entry) => entry.title));
7084
- const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
7085
- return [...boot, ...fresh];
7086
- };
7087
- const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
7088
- void probeAgents().catch(() => {
7089
- });
7090
- const readShareBody = (req, res, run4) => {
7091
- let body = "";
7092
- req.on("data", (chunk) => body += chunk);
7093
- req.on("end", () => {
7094
- const parsed2 = jsonBody(body);
7095
- if (parsed2 === null)
7096
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7097
- void Promise.resolve(run4(parsed2)).then((result2) => {
7098
- if (result2 === void 0) {
7099
- return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
6672
+ return Promise.resolve(agentsCache.agents);
6673
+ };
6674
+ const livePreviewDefinitions = async () => {
6675
+ const localRead = await readLocalPreviews(cwd).catch(() => null);
6676
+ const local = localRead?.errors.length === 0 ? localRead.previews : [];
6677
+ const localTitles = new Set(local.map((entry2) => entry2.title));
6678
+ const bootConfig = config?.previews ?? [];
6679
+ const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry2) => entry2.local !== true || localTitles.has(entry2.title));
6680
+ const known = new Set(boot.map((entry2) => entry2.title));
6681
+ const fresh = local.filter((entry2) => !known.has(entry2.title) && entry2.branch === void 0 && entry2.file === void 0);
6682
+ return [...boot, ...fresh];
6683
+ };
6684
+ const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
6685
+ void probeAgents().catch(() => {
6686
+ });
6687
+ const readShareBody = (req, res, run4) => {
6688
+ let body = "";
6689
+ req.on("data", (chunk) => body += chunk);
6690
+ req.on("end", () => {
6691
+ const parsed2 = jsonBody(body);
6692
+ if (parsed2 === null)
6693
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6694
+ void Promise.resolve(run4(parsed2)).then((result2) => {
6695
+ if (result2 === void 0) {
6696
+ return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
6697
+ }
6698
+ return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
6699
+ });
6700
+ });
6701
+ };
6702
+ const handleRequest = (req, res, context) => {
6703
+ const url = req.url ?? "/";
6704
+ const path = url.split("?")[0] ?? "/";
6705
+ const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
6706
+ if (!context.remote && req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
6707
+ return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
6708
+ }
6709
+ if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
6710
+ return void shares?.viewerConfig(context.grantId ?? "").then((payload) => {
6711
+ if (payload === null) {
6712
+ return sendJson2(res, 403, { ok: false, error: "This link isn't active." });
6713
+ }
6714
+ sendConditionalJson(req, res, payload);
6715
+ });
6716
+ }
6717
+ if (context.remote && path === `${LEGLAS_PREFIX}/api/health`) {
6718
+ const known = liveHealth?.reachable() ?? null;
6719
+ return void (known === null ? probe(target) : Promise.resolve(known)).then((reachable) => sendConditionalJson(req, res, { reachable }));
6720
+ }
6721
+ if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
6722
+ return sendJson2(res, 403, { error: "Not available to viewers." });
6723
+ }
6724
+ if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
6725
+ const updates2 = options.updates;
6726
+ if (updates2 === void 0) {
6727
+ return sendJson2(res, 404, { ok: false, error: "Updates are not available here." });
6728
+ }
6729
+ if (req.method === "GET")
6730
+ return sendJson2(res, 200, updates2.status());
6731
+ if (path.endsWith("/check")) {
6732
+ return void updates2.check({ force: true }).then((status) => sendJson2(res, 200, status));
6733
+ }
6734
+ if (path.endsWith("/install")) {
6735
+ return void updates2.update().then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 409, {
6736
+ ok: false,
6737
+ error: error instanceof Error ? error.message : String(error)
6738
+ }));
6739
+ }
6740
+ if (!hasJsonBody(req))
6741
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6742
+ let body = "";
6743
+ req.on("data", (chunk) => body += chunk);
6744
+ return void req.on("end", () => {
6745
+ const parsed2 = jsonBody(body);
6746
+ if (parsed2 === null)
6747
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6748
+ if (typeof parsed2.version !== "string" || parsed2.version.trim() === "") {
6749
+ return sendJson2(res, 400, { ok: false, error: "Body needs a version." });
6750
+ }
6751
+ void updates2.skip(parsed2.version).then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 400, {
6752
+ ok: false,
6753
+ error: error instanceof Error ? error.message : String(error)
6754
+ }));
6755
+ });
6756
+ }
6757
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "GET") {
6758
+ return void shares?.tunnels().then((tunnels) => sendJson2(res, 200, { share: shares?.status() ?? null, tunnels }));
6759
+ }
6760
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "POST") {
6761
+ if (!hasJsonBody(req)) {
6762
+ return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
6763
+ }
6764
+ let body = "";
6765
+ req.on("data", (chunk) => body += chunk);
6766
+ return void req.on("end", async () => {
6767
+ const parsed2 = jsonBody(body);
6768
+ if (parsed2 === null) {
6769
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6770
+ }
6771
+ const result2 = await shares?.create(parsed2).catch((error) => ({
6772
+ ok: false,
6773
+ status: 500,
6774
+ error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
6775
+ }));
6776
+ if (result2 === void 0) {
6777
+ return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
6778
+ }
6779
+ return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
6780
+ });
6781
+ }
6782
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/update` && req.method === "POST") {
6783
+ if (!hasJsonBody(req)) {
6784
+ return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
6785
+ }
6786
+ let body = "";
6787
+ req.on("data", (chunk) => body += chunk);
6788
+ return void req.on("end", async () => {
6789
+ const parsed2 = jsonBody(body);
6790
+ if (parsed2 === null) {
6791
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6792
+ }
6793
+ const result2 = await shares?.update(parsed2);
6794
+ if (result2 === void 0) {
6795
+ return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
6796
+ }
6797
+ return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
6798
+ });
6799
+ }
6800
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants` && req.method === "POST") {
6801
+ return void readShareBody(req, res, (body) => shares?.createGrant(body));
6802
+ }
6803
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/revoke` && req.method === "POST") {
6804
+ return void readShareBody(req, res, (body) => shares?.revokeGrant(body));
6805
+ }
6806
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/extend` && req.method === "POST") {
6807
+ return void readShareBody(req, res, (body) => shares?.extendGrant(body));
6808
+ }
6809
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/allow` && req.method === "POST") {
6810
+ return void readShareBody(req, res, (body) => shares?.allowRoute(body));
6811
+ }
6812
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/rotate` && req.method === "POST") {
6813
+ return void readShareBody(req, res, () => shares?.rotate());
6814
+ }
6815
+ if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/stop` && req.method === "POST") {
6816
+ return void (shares?.stop() ?? Promise.resolve()).then(() => sendJson2(res, 200, { ok: true }));
6817
+ }
6818
+ if (path === `${LEGLAS_PREFIX}/api/config`) {
6819
+ const boot = config?.previews ?? [];
6820
+ const errors = [...configErrors];
6821
+ const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
6822
+ if (notice !== null)
6823
+ errors.push(notice);
6824
+ return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
6825
+ if (localErrors.length > 0) {
6826
+ return sendConditionalJson(req, res, {
6827
+ project,
6828
+ devServer: target,
6829
+ scanPreviews: config?.scanPreviews ?? true,
6830
+ previews: previewsForConfig(boot),
6831
+ errors,
6832
+ warnings: configWarnings
6833
+ });
6834
+ }
6835
+ const localTitles = new Set(local.map((preview) => preview.title));
6836
+ const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
6837
+ const known = new Set(currentBoot.map((preview) => preview.title));
6838
+ const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
6839
+ sendConditionalJson(req, res, {
6840
+ project,
6841
+ devServer: target,
6842
+ scanPreviews: config?.scanPreviews ?? true,
6843
+ previews: previewsForConfig([...currentBoot, ...fresh]),
6844
+ errors,
6845
+ warnings: configWarnings
6846
+ });
6847
+ }).catch(() => sendConditionalJson(req, res, {
6848
+ project,
6849
+ devServer: target,
6850
+ scanPreviews: config?.scanPreviews ?? true,
6851
+ previews: previewsForConfig(boot),
6852
+ errors,
6853
+ warnings: configWarnings
6854
+ }));
6855
+ }
6856
+ if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
6857
+ let body = "";
6858
+ req.on("data", (chunk) => body += chunk);
6859
+ return void req.on("end", async () => {
6860
+ const parsed2 = jsonBody(body);
6861
+ if (parsed2 === null) {
6862
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6863
+ }
6864
+ if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
6865
+ return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
6866
+ }
6867
+ const preview = (await livePreviewDefinitions()).find((entry2) => entry2.title === parsed2.title);
6868
+ if (preview === void 0) {
6869
+ return sendJson2(res, 404, { ok: false, error: "No such direction." });
6870
+ }
6871
+ if (preview.branch === void 0) {
6872
+ return sendJson2(res, 400, {
6873
+ ok: false,
6874
+ error: `"${preview.title}" is not a branch preview.`
6875
+ });
6876
+ }
6877
+ if (config?.devCommand === void 0) {
6878
+ return sendJson2(res, 400, {
6879
+ ok: false,
6880
+ error: `"${preview.title}" cannot start because the config sets no devCommand.`
6881
+ });
6882
+ }
6883
+ void branches.start(preview.title);
6884
+ const state = branches.state(preview.title);
6885
+ if (state === void 0) {
6886
+ return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
6887
+ }
6888
+ return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
6889
+ });
6890
+ }
6891
+ if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
6892
+ let body = "";
6893
+ req.on("data", (chunk) => body += chunk);
6894
+ return void req.on("end", async () => {
6895
+ const parsed2 = jsonBody(body);
6896
+ if (parsed2 === null) {
6897
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6898
+ }
6899
+ const titles = parsed2.titles;
6900
+ if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
6901
+ return sendJson2(res, 400, {
6902
+ ok: false,
6903
+ error: "Body needs a non-empty array of direction titles."
6904
+ });
6905
+ }
6906
+ const unique = [...new Set(titles)];
6907
+ try {
6908
+ const local = await readLocalPreviews(cwd);
6909
+ if (local.errors.length > 0) {
6910
+ return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
6911
+ }
6912
+ const localTitles = new Set(local.previews.map((preview) => preview.title));
6913
+ const unknown = unique.filter((title) => !localTitles.has(title));
6914
+ if (unknown.length > 0) {
6915
+ return sendJson2(res, 400, {
6916
+ ok: false,
6917
+ error: "Only machine-local directions can be deleted from the registry."
6918
+ });
6919
+ }
6920
+ const deleted = await dropLocalPreviews(cwd, unique);
6921
+ return sendJson2(res, 200, { ok: true, deleted });
6922
+ } catch {
6923
+ return sendJson2(res, 500, {
6924
+ ok: false,
6925
+ error: "The directions could not be deleted from Leglas."
6926
+ });
6927
+ }
6928
+ });
6929
+ }
6930
+ if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
6931
+ const declaredLength = req.headers["content-length"];
6932
+ if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
6933
+ return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
6934
+ }
6935
+ const chunks = [];
6936
+ let bytes = 0;
6937
+ let refused = false;
6938
+ req.on("data", (chunk) => {
6939
+ if (refused)
6940
+ return;
6941
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
6942
+ bytes += buffer.length;
6943
+ if (bytes > REFERENCE_MAX_BYTES) {
6944
+ refused = true;
6945
+ req.pause();
6946
+ res.once("finish", () => req.socket.destroy());
6947
+ sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
6948
+ return;
6949
+ }
6950
+ chunks.push(buffer);
6951
+ });
6952
+ return void req.once("end", async () => {
6953
+ if (refused)
6954
+ return;
6955
+ if (bytes === 0) {
6956
+ return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
6957
+ }
6958
+ const body = Buffer.concat(chunks, bytes);
6959
+ const image = sniffImage(body);
6960
+ if (image === null) {
6961
+ return sendJson2(res, 415, {
6962
+ ok: false,
6963
+ error: "Only PNG, JPEG, WebP and GIF images can be attached."
6964
+ });
6965
+ }
6966
+ const id = newRequestId();
6967
+ const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
6968
+ try {
6969
+ await mkdir8(join12(cwd, REFERENCES_DIR), { recursive: true });
6970
+ await writeFile8(join12(cwd, file), body);
6971
+ void pruneReferences(cwd).catch(() => {
6972
+ });
6973
+ return sendJson2(res, 200, {
6974
+ ok: true,
6975
+ reference: {
6976
+ id,
6977
+ file,
6978
+ name: referenceName(req.headers["x-leglas-filename"]),
6979
+ width: image.width,
6980
+ height: image.height,
6981
+ bytes
6982
+ }
6983
+ });
6984
+ } catch {
6985
+ return sendJson2(res, 500, {
6986
+ ok: false,
6987
+ error: "The image could not be attached."
6988
+ });
6989
+ }
6990
+ });
6991
+ }
6992
+ if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
6993
+ let body = "";
6994
+ req.on("data", (chunk) => body += chunk);
6995
+ return void req.on("end", async () => {
6996
+ const parsed2 = jsonBody(body);
6997
+ if (parsed2 === null) {
6998
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6999
+ }
7000
+ if (parsed2.mode !== void 0 && parsed2.mode !== "variant" && parsed2.mode !== "replace") {
7001
+ return sendJson2(res, 400, {
7002
+ ok: false,
7003
+ error: 'mode must be "variant" or "replace".'
7004
+ });
7005
+ }
7006
+ const mode = parsed2.mode === "replace" ? "replace" : "variant";
7007
+ if (parsed2.references !== void 0 && (!Array.isArray(parsed2.references) || parsed2.references.some((reference) => typeof reference !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
7008
+ return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
7009
+ }
7010
+ const references = parsed2.references ?? [];
7011
+ if (references.length > 0) {
7012
+ const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
7013
+ const gone = references.filter((id2) => !present.has(id2));
7014
+ if (gone.length > 0) {
7015
+ return sendJson2(res, 410, {
7016
+ ok: false,
7017
+ error: gone.length === 1 ? "An attached image is gone: it was pasted over an hour ago and never sent. Attach it again." : "Some attached images are gone: they were pasted over an hour ago and never sent. Attach them again."
7018
+ });
7019
+ }
7020
+ }
7021
+ const width = typeof parsed2.width === "number" && Number.isFinite(parsed2.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed2.width))) : 1440;
7022
+ const previews = await livePreviews();
7023
+ const preview = previews.find((entry2) => entry2.title === parsed2.title);
7024
+ if (!preview) {
7025
+ return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7026
+ }
7027
+ const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
7028
+ if (!parsed2.intent?.trim() && notes.length === 0) {
7029
+ return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7030
+ }
7031
+ const intent = (parsed2.intent ?? "").trim();
7032
+ const live2 = (await readRequests(cwd).catch(() => [])).filter((entry2) => entry2.status === "queued" || entry2.status === "picked-up");
7033
+ const sameNotes = (entry2) => {
7034
+ const before = [...entry2.notes ?? []].sort().join(",");
7035
+ return before === notes.map((note) => note.id).sort().join(",");
7036
+ };
7037
+ const compare = typeof parsed2.compare === "string" && parsed2.compare !== preview.title ? previews.find((entry2) => entry2.title === parsed2.compare) ?? null : null;
7038
+ const sameContext = (entry2) => (entry2.compare ?? null) === (compare?.title ?? null) && [...entry2.references ?? []].sort().join(",") === [...references].sort().join(",");
7039
+ if (live2.some((entry2) => entry2.title === preview.title && entry2.intent === intent && // The same words in the other mode are not the same request:
7040
+ // one forks the direction and the other rewrites it. Only a
7041
+ // genuine repeat is refused.
7042
+ (entry2.mode ?? "replace") === mode && sameNotes(entry2) && sameContext(entry2))) {
7043
+ return sendJson2(res, 409, {
7044
+ ok: false,
7045
+ duplicate: true,
7046
+ error: `That exact change to ${preview.title} is already waiting.`
7047
+ });
7048
+ }
7049
+ const address = server.address();
7050
+ const requestPort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
7051
+ const id = newRequestId();
7052
+ const captured = await attachRequest(cwd, id, {
7053
+ origin: `http://127.0.0.1:${requestPort}`,
7054
+ preview,
7055
+ width,
7056
+ notes,
7057
+ compare,
7058
+ references
7059
+ }, { pool: browserPool });
7060
+ const composed = composeRequest(preview, intent, mode, notes, leglasCommand, captured);
7061
+ try {
7062
+ await appendRequest(cwd, {
7063
+ title: preview.title,
7064
+ url: preview.url,
7065
+ intent,
7066
+ // The ids travel with the request so a change made in place can
7067
+ // forget the notes it answered. A fork leaves them where they are:
7068
+ // the direction they point at was not touched.
7069
+ ...notes.length === 0 ? {} : { notes: notes.map((entry2) => entry2.id) },
7070
+ ...captured.attachments.length === 0 ? {} : { attachments: captured.attachments },
7071
+ ...captured.skipped === null ? {} : { captureNote: captured.skipped },
7072
+ ...compare === null ? {} : { compare: compare.title },
7073
+ ...references.length === 0 ? {} : { references },
7074
+ ...composed
7075
+ }, id);
7076
+ runner?.nudge();
7077
+ return sendJson2(res, 200, {
7078
+ ok: true,
7079
+ ...composed,
7080
+ attachments: captured.attachments
7081
+ });
7082
+ } catch {
7083
+ return sendJson2(res, 200, {
7084
+ ok: true,
7085
+ ...composed,
7086
+ attachments: captured.attachments,
7087
+ queued: false
7088
+ });
7089
+ }
7090
+ });
7091
+ }
7092
+ if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
7093
+ if (!hasJsonBody(req)) {
7094
+ return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
7095
+ }
7096
+ let body = "";
7097
+ req.on("data", (chunk) => body += chunk);
7098
+ return void req.on("end", async () => {
7099
+ const parsed2 = jsonBody(body);
7100
+ if (parsed2 === null) {
7101
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7102
+ }
7103
+ if (typeof parsed2.title !== "string" || parsed2.title === "") {
7104
+ return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
7105
+ }
7106
+ if (parsed2.note !== void 0 && typeof parsed2.note !== "string") {
7107
+ return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
7108
+ }
7109
+ const preview = (await livePreviews()).find((entry2) => entry2.title === parsed2.title);
7110
+ if (preview === void 0) {
7111
+ return sendJson2(res, 404, { ok: false, error: "No such direction." });
7112
+ }
7113
+ const width = typeof parsed2.width === "number" && Number.isFinite(parsed2.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed2.width))) : 1440;
7114
+ const browser = await browserPool.acquire();
7115
+ if (browser === null) {
7116
+ return sendJson2(res, 503, {
7117
+ ok: false,
7118
+ error: browserPool.reason() ?? NO_BROWSER
7119
+ });
7120
+ }
7121
+ const annotations = typeof parsed2.note === "string" ? (await readAnnotations(cwd).catch(() => [])).filter((entry2) => entry2.id === parsed2.note && entry2.title === preview.title) : [];
7122
+ const address = server.address();
7123
+ const capturePort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
7124
+ const controller = new AbortController();
7125
+ const timeoutMarker = /* @__PURE__ */ Symbol("capture timeout");
7126
+ let timedOut2;
7127
+ const timeout = new Promise((resolve5) => {
7128
+ timedOut2 = () => resolve5(timeoutMarker);
7129
+ });
7130
+ const timer = setTimeout(() => {
7131
+ timedOut2();
7132
+ controller.abort();
7133
+ }, CAPTURE_DEADLINE_MS);
7134
+ timer.unref?.();
7135
+ try {
7136
+ const work = capturePage(browser, {
7137
+ url: previewUrl(`http://127.0.0.1:${capturePort}`, preview),
7138
+ width,
7139
+ ...annotations.length === 0 ? {} : {
7140
+ focuses: annotations.map((entry2) => ({
7141
+ selector: entry2.anchor.selector,
7142
+ text: entry2.anchor.text,
7143
+ tag: entry2.anchor.tag,
7144
+ ...entry2.anchor.region === void 0 ? {} : { region: entry2.anchor.region },
7145
+ rect: entry2.anchor.rect
7146
+ }))
7147
+ },
7148
+ timeoutMs: CAPTURE_LOAD_MS,
7149
+ signal: controller.signal
7150
+ });
7151
+ const result2 = await Promise.race([work, timeout]);
7152
+ if (result2 === timeoutMarker) {
7153
+ return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
7154
+ }
7155
+ clearTimeout(timer);
7156
+ const crop = annotations.length > 0 ? result2.crops[0] : null;
7157
+ const shot = crop?.shot ?? result2.frame;
7158
+ const noteSuffix = typeof parsed2.note === "string" ? `-${parsed2.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
7159
+ const name = `${captureSlug(preview.title)}-${result2.frame.width}${noteSuffix}.png`;
7160
+ const relativeFile = `${CAPTURES_DIR}/show/${name}`;
7161
+ await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
7162
+ await writeFile8(join12(cwd, relativeFile), shot.png);
7163
+ return sendJson2(res, 200, {
7164
+ ok: true,
7165
+ file: relativeFile,
7166
+ width: shot.width,
7167
+ height: shot.height,
7168
+ viewport: result2.frame.width,
7169
+ errors: result2.errors,
7170
+ hydration: result2.hydration,
7171
+ cut: result2.cut
7172
+ });
7173
+ } catch (error) {
7174
+ clearTimeout(timer);
7175
+ return sendJson2(res, 502, {
7176
+ ok: false,
7177
+ error: error instanceof Error ? error.message : String(error)
7178
+ });
7100
7179
  }
7101
- return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
7102
7180
  });
7103
- });
7104
- };
7105
- const handleRequest = (req, res, context) => {
7106
- const url = req.url ?? "/";
7107
- const path = url.split("?")[0] ?? "/";
7108
- const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
7109
- if (!context.remote && req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
7110
- return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
7111
7181
  }
7112
- if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
7113
- return void shares?.viewerConfig(context.grantId ?? "").then((payload) => {
7114
- if (payload === null) {
7115
- return sendJson2(res, 403, { ok: false, error: "This link isn't active." });
7182
+ if (path === `${LEGLAS_PREFIX}/api/agents/warm` && req.method === "POST") {
7183
+ return void readAgentChoice(cwd).then((choice) => {
7184
+ if (choice.agent !== null)
7185
+ runner?.prepare(choice.agent);
7186
+ sendJson2(res, 200, { ok: true });
7187
+ }, () => sendJson2(res, 200, { ok: true }));
7188
+ }
7189
+ if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
7190
+ return void Promise.all([
7191
+ currentAgents(query.get("refresh") === "1"),
7192
+ readAgentChoice(cwd)
7193
+ ]).then(([agents, choice]) => sendJson2(res, 200, {
7194
+ agents,
7195
+ choice: choice.agent,
7196
+ customRun: choice.run,
7197
+ effort: choice.effort
7198
+ }));
7199
+ }
7200
+ if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
7201
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
7202
+ return sendJson2(res, 403, {
7203
+ ok: false,
7204
+ error: "The agent choice can only be made from the machine running Leglas."
7205
+ });
7206
+ }
7207
+ if (!hasJsonBody(req)) {
7208
+ return sendJson2(res, 400, { ok: false, error: "Agent choice must be JSON." });
7209
+ }
7210
+ let body = "";
7211
+ req.on("data", (chunk) => body += chunk);
7212
+ return void req.on("end", () => {
7213
+ const parsed2 = jsonBody(body);
7214
+ if (parsed2 === null) {
7215
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7116
7216
  }
7117
- sendConditionalJson(req, res, payload);
7217
+ if (!isKnownAgent(parsed2.agent) && parsed2.agent !== "custom") {
7218
+ return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
7219
+ }
7220
+ if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
7221
+ return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
7222
+ }
7223
+ const effort = parsed2.effort === null || isAgentEffort(parsed2.effort) ? parsed2.effort : void 0;
7224
+ if (parsed2.effort !== void 0 && effort === void 0) {
7225
+ return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
7226
+ }
7227
+ if (parsed2.agent === "custom") {
7228
+ if (effort !== void 0) {
7229
+ return sendJson2(res, 400, {
7230
+ ok: false,
7231
+ error: "Custom agents manage effort in their own command."
7232
+ });
7233
+ }
7234
+ if (typeof parsed2.run !== "string") {
7235
+ return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
7236
+ }
7237
+ const template = parseTemplate(parsed2.run);
7238
+ if (!template.ok)
7239
+ return sendJson2(res, 400, { ok: false, error: template.error });
7240
+ return void saveAgentChoice(cwd, { agent: "custom", run: parsed2.run }).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
7241
+ }
7242
+ if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed2.agent].efforts.includes(effort)) {
7243
+ return sendJson2(res, 400, {
7244
+ ok: false,
7245
+ error: `${KNOWN_AGENTS[parsed2.agent].name} does not expose an effort override.`
7246
+ });
7247
+ }
7248
+ return void saveAgentChoice(cwd, {
7249
+ agent: parsed2.agent,
7250
+ ...effort === void 0 ? {} : { effort }
7251
+ }).then(() => {
7252
+ runner?.prepare(parsed2.agent);
7253
+ sendJson2(res, 200, { ok: true });
7254
+ }, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
7118
7255
  });
7119
7256
  }
7120
- if (context.remote && path === `${LEGLAS_PREFIX}/api/health`) {
7121
- const known = liveHealth?.reachable() ?? null;
7122
- return void (known === null ? probe(target) : Promise.resolve(known)).then((reachable) => sendConditionalJson(req, res, { reachable }));
7123
- }
7124
- if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
7125
- return sendJson2(res, 403, { error: "Not available to viewers." });
7257
+ if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
7258
+ let body = "";
7259
+ req.on("data", (chunk) => body += chunk);
7260
+ return void req.on("end", () => {
7261
+ const parsed2 = jsonBody(body);
7262
+ if (parsed2 === null) {
7263
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7264
+ }
7265
+ if (typeof parsed2.watching !== "boolean") {
7266
+ return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
7267
+ }
7268
+ lastSeen = parsed2.watching ? Date.now() : null;
7269
+ sendJson2(res, 200, { ok: true });
7270
+ });
7126
7271
  }
7127
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "GET") {
7128
- return void shares?.tunnels().then((tunnels) => sendJson2(res, 200, { share: shares?.status() ?? null, tunnels }));
7272
+ if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
7273
+ const snapshot = runner?.snapshot() ?? {
7274
+ running: false,
7275
+ requestId: null,
7276
+ agent: null,
7277
+ activity: null,
7278
+ startedAt: null,
7279
+ stopping: false,
7280
+ waiting: null,
7281
+ failedIds: []
7282
+ };
7283
+ return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
7284
+ requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
7285
+ id,
7286
+ title,
7287
+ intent,
7288
+ // A fork leaves its parent's document alone; the interface keeps
7289
+ // the parent's duplicate verdict on the strength of this.
7290
+ mode,
7291
+ // Which pins this change speaks for. The interface marks them, so
7292
+ // a note already sitting in a prompt an agent holds does not look
7293
+ // like one nobody has read.
7294
+ notes: notes ?? [],
7295
+ // The run in flight is the one thing the file cannot know. After
7296
+ // that the file is the record, including across a restart, and the
7297
+ // process-local failed set only covers a request whose verdict
7298
+ // could not be written.
7299
+ status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
7300
+ failure: failure ?? null
7301
+ })),
7302
+ agent: {
7303
+ attached: externallyAttached(),
7304
+ running: snapshot.running,
7305
+ name: snapshot.running ? snapshot.agent : null,
7306
+ activity: snapshot.running ? snapshot.activity : null,
7307
+ startedAt: snapshot.running ? snapshot.startedAt : null,
7308
+ // A stop that has been asked for but not yet obeyed. The card
7309
+ // says so rather than going on describing a live run.
7310
+ stopping: snapshot.running && snapshot.stopping,
7311
+ // Why a run that looks stalled is stalled, while it is stalled.
7312
+ waiting: snapshot.running ? snapshot.waiting : null
7313
+ }
7314
+ }));
7129
7315
  }
7130
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "POST") {
7316
+ if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
7131
7317
  if (!hasJsonBody(req)) {
7132
- return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
7318
+ return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
7133
7319
  }
7134
7320
  let body = "";
7135
7321
  req.on("data", (chunk) => body += chunk);
7136
- return void req.on("end", async () => {
7322
+ return void req.on("end", () => {
7137
7323
  const parsed2 = jsonBody(body);
7138
7324
  if (parsed2 === null) {
7139
7325
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7140
7326
  }
7141
- const result2 = await shares?.create(parsed2).catch((error) => ({
7142
- ok: false,
7143
- status: 500,
7144
- error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
7145
- }));
7146
- if (result2 === void 0) {
7147
- return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
7327
+ if (parsed2.id !== void 0 && typeof parsed2.id !== "string") {
7328
+ return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
7148
7329
  }
7149
- return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
7330
+ return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed2.id) ?? false });
7150
7331
  });
7151
7332
  }
7152
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/update` && req.method === "POST") {
7333
+ if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
7153
7334
  if (!hasJsonBody(req)) {
7154
- return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
7335
+ return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
7155
7336
  }
7156
7337
  let body = "";
7157
7338
  req.on("data", (chunk) => body += chunk);
@@ -7160,70 +7341,54 @@ async function startServer(options) {
7160
7341
  if (parsed2 === null) {
7161
7342
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7162
7343
  }
7163
- const result2 = await shares?.update(parsed2);
7164
- if (result2 === void 0) {
7165
- return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
7344
+ if (typeof parsed2.id !== "string") {
7345
+ return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
7346
+ }
7347
+ const request = (await readRequests(cwd)).find((entry2) => entry2.id === parsed2.id);
7348
+ if (request === void 0) {
7349
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
7350
+ }
7351
+ if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
7352
+ return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
7353
+ }
7354
+ try {
7355
+ const retryId = newRequestId();
7356
+ const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
7357
+ if (!await removeRequest(cwd, request.id)) {
7358
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
7359
+ }
7360
+ await appendRequest(cwd, {
7361
+ title: request.title,
7362
+ url: request.url,
7363
+ intent: request.intent,
7364
+ target: request.target,
7365
+ // The prompt names the captures by path, and the embedded pipes
7366
+ // are not its only readers: watch, a custom command and
7367
+ // `requests --json` all hand the text over as it stands.
7368
+ prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId),
7369
+ // The stored prompt already carries the mode's instructions;
7370
+ // its notes and visual context travel with the retry too.
7371
+ ...request.mode === void 0 ? {} : { mode: request.mode },
7372
+ ...request.notes === void 0 ? {} : { notes: request.notes },
7373
+ ...attachments.length === 0 ? {} : { attachments },
7374
+ ...request.captureNote === void 0 ? {} : { captureNote: request.captureNote },
7375
+ ...request.compare === void 0 ? {} : { compare: request.compare },
7376
+ ...request.references === void 0 ? {} : { references: request.references }
7377
+ }, retryId);
7378
+ runner?.nudge();
7379
+ return sendJson2(res, 200, { ok: true });
7380
+ } catch {
7381
+ return sendJson2(res, 500, { ok: false, error: "The request could not be retried." });
7166
7382
  }
7167
- return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
7168
7383
  });
7169
7384
  }
7170
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants` && req.method === "POST") {
7171
- return void readShareBody(req, res, (body) => shares?.createGrant(body));
7172
- }
7173
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/revoke` && req.method === "POST") {
7174
- return void readShareBody(req, res, (body) => shares?.revokeGrant(body));
7175
- }
7176
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/extend` && req.method === "POST") {
7177
- return void readShareBody(req, res, (body) => shares?.extendGrant(body));
7178
- }
7179
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/allow` && req.method === "POST") {
7180
- return void readShareBody(req, res, (body) => shares?.allowRoute(body));
7181
- }
7182
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/rotate` && req.method === "POST") {
7183
- return void readShareBody(req, res, () => shares?.rotate());
7184
- }
7185
- if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/stop` && req.method === "POST") {
7186
- return void (shares?.stop() ?? Promise.resolve()).then(() => sendJson2(res, 200, { ok: true }));
7187
- }
7188
- if (path === `${LEGLAS_PREFIX}/api/config`) {
7189
- const boot = config?.previews ?? [];
7190
- const errors = [...configErrors];
7191
- const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
7192
- if (notice !== null)
7193
- errors.push(notice);
7194
- return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
7195
- if (localErrors.length > 0) {
7196
- return sendConditionalJson(req, res, {
7197
- project,
7198
- devServer: target,
7199
- scanPreviews: config?.scanPreviews ?? true,
7200
- previews: previewsForConfig(boot),
7201
- errors,
7202
- warnings: configWarnings
7203
- });
7204
- }
7205
- const localTitles = new Set(local.map((preview) => preview.title));
7206
- const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
7207
- const known = new Set(currentBoot.map((preview) => preview.title));
7208
- const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
7209
- sendConditionalJson(req, res, {
7210
- project,
7211
- devServer: target,
7212
- scanPreviews: config?.scanPreviews ?? true,
7213
- previews: previewsForConfig([...currentBoot, ...fresh]),
7214
- errors,
7215
- warnings: configWarnings
7216
- });
7217
- }).catch(() => sendConditionalJson(req, res, {
7218
- project,
7219
- devServer: target,
7220
- scanPreviews: config?.scanPreviews ?? true,
7221
- previews: previewsForConfig(boot),
7222
- errors,
7223
- warnings: configWarnings
7224
- }));
7385
+ if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
7386
+ return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
7225
7387
  }
7226
- if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
7388
+ if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
7389
+ if (!hasJsonBody(req)) {
7390
+ return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
7391
+ }
7227
7392
  let body = "";
7228
7393
  req.on("data", (chunk) => body += chunk);
7229
7394
  return void req.on("end", async () => {
@@ -7232,33 +7397,28 @@ async function startServer(options) {
7232
7397
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7233
7398
  }
7234
7399
  if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
7235
- return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
7236
- }
7237
- const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed2.title);
7238
- if (preview === void 0) {
7239
- return sendJson2(res, 404, { ok: false, error: "No such direction." });
7400
+ return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
7240
7401
  }
7241
- if (preview.branch === void 0) {
7242
- return sendJson2(res, 400, {
7243
- ok: false,
7244
- error: `"${preview.title}" is not a branch preview.`
7245
- });
7402
+ const anchor = anchorFrom(parsed2.anchor);
7403
+ if (anchor === null) {
7404
+ return sendJson2(res, 400, { ok: false, error: "A note needs something to point at." });
7246
7405
  }
7247
- if (config?.devCommand === void 0) {
7248
- return sendJson2(res, 400, {
7249
- ok: false,
7250
- error: `"${preview.title}" cannot start because the config sets no devCommand.`
7406
+ try {
7407
+ const annotation = await addAnnotation(cwd, {
7408
+ anchor,
7409
+ note: typeof parsed2.note === "string" ? parsed2.note.trim() : "",
7410
+ title: parsed2.title
7251
7411
  });
7412
+ return sendJson2(res, 200, { ok: true, annotation });
7413
+ } catch {
7414
+ return sendJson2(res, 500, { ok: false, error: "The note could not be kept." });
7252
7415
  }
7253
- void branches.start(preview.title);
7254
- const state = branches.state(preview.title);
7255
- if (state === void 0) {
7256
- return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
7257
- }
7258
- return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
7259
7416
  });
7260
7417
  }
7261
- if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
7418
+ if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
7419
+ if (!hasJsonBody(req)) {
7420
+ return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
7421
+ }
7262
7422
  let body = "";
7263
7423
  req.on("data", (chunk) => body += chunk);
7264
7424
  return void req.on("end", async () => {
@@ -7266,100 +7426,27 @@ async function startServer(options) {
7266
7426
  if (parsed2 === null) {
7267
7427
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7268
7428
  }
7269
- const titles = parsed2.titles;
7270
- if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
7271
- return sendJson2(res, 400, {
7272
- ok: false,
7273
- error: "Body needs a non-empty array of direction titles."
7274
- });
7275
- }
7276
- const unique = [...new Set(titles)];
7277
- try {
7278
- const local = await readLocalPreviews(cwd);
7279
- if (local.errors.length > 0) {
7280
- return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
7281
- }
7282
- const localTitles = new Set(local.previews.map((preview) => preview.title));
7283
- const unknown = unique.filter((title) => !localTitles.has(title));
7284
- if (unknown.length > 0) {
7285
- return sendJson2(res, 400, {
7286
- ok: false,
7287
- error: "Only machine-local directions can be deleted from the registry."
7288
- });
7289
- }
7290
- const deleted = await dropLocalPreviews(cwd, unique);
7291
- return sendJson2(res, 200, { ok: true, deleted });
7292
- } catch {
7293
- return sendJson2(res, 500, {
7294
- ok: false,
7295
- error: "The directions could not be deleted from Leglas."
7296
- });
7297
- }
7298
- });
7299
- }
7300
- if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
7301
- const declaredLength = req.headers["content-length"];
7302
- if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
7303
- return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
7304
- }
7305
- const chunks = [];
7306
- let bytes = 0;
7307
- let refused = false;
7308
- req.on("data", (chunk) => {
7309
- if (refused)
7310
- return;
7311
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
7312
- bytes += buffer.length;
7313
- if (bytes > REFERENCE_MAX_BYTES) {
7314
- refused = true;
7315
- req.pause();
7316
- res.once("finish", () => req.socket.destroy());
7317
- sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
7318
- return;
7319
- }
7320
- chunks.push(buffer);
7321
- });
7322
- return void req.once("end", async () => {
7323
- if (refused)
7324
- return;
7325
- if (bytes === 0) {
7326
- return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
7429
+ if (typeof parsed2.id !== "string" || parsed2.id === "") {
7430
+ return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
7327
7431
  }
7328
- const body = Buffer.concat(chunks, bytes);
7329
- const image = sniffImage(body);
7330
- if (image === null) {
7331
- return sendJson2(res, 415, {
7332
- ok: false,
7333
- error: "Only PNG, JPEG, WebP and GIF images can be attached."
7334
- });
7432
+ if (typeof parsed2.note !== "string") {
7433
+ return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
7335
7434
  }
7336
- const id = newRequestId();
7337
- const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
7338
7435
  try {
7339
- await mkdir8(join12(cwd, REFERENCES_DIR), { recursive: true });
7340
- await writeFile8(join12(cwd, file), body);
7341
- void pruneReferences(cwd).catch(() => {
7342
- });
7343
- return sendJson2(res, 200, {
7344
- ok: true,
7345
- reference: {
7346
- id,
7347
- file,
7348
- name: referenceName(req.headers["x-leglas-filename"]),
7349
- width: image.width,
7350
- height: image.height,
7351
- bytes
7352
- }
7353
- });
7354
- } catch {
7355
- return sendJson2(res, 500, {
7356
- ok: false,
7357
- error: "The image could not be attached."
7358
- });
7436
+ const annotation = await updateAnnotation(cwd, parsed2.id, parsed2.note);
7437
+ if (annotation === null) {
7438
+ return sendJson2(res, 404, { ok: false, error: "That note has gone." });
7439
+ }
7440
+ return sendJson2(res, 200, { ok: true, annotation });
7441
+ } catch {
7442
+ return sendJson2(res, 500, { ok: false, error: "The note could not be reworded." });
7359
7443
  }
7360
7444
  });
7361
7445
  }
7362
- if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
7446
+ if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
7447
+ if (!hasJsonBody(req)) {
7448
+ return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
7449
+ }
7363
7450
  let body = "";
7364
7451
  req.on("data", (chunk) => body += chunk);
7365
7452
  return void req.on("end", async () => {
@@ -7367,101 +7454,20 @@ async function startServer(options) {
7367
7454
  if (parsed2 === null) {
7368
7455
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7369
7456
  }
7370
- if (parsed2.mode !== void 0 && parsed2.mode !== "variant" && parsed2.mode !== "replace") {
7371
- return sendJson2(res, 400, {
7372
- ok: false,
7373
- error: 'mode must be "variant" or "replace".'
7374
- });
7375
- }
7376
- const mode = parsed2.mode === "replace" ? "replace" : "variant";
7377
- if (parsed2.references !== void 0 && (!Array.isArray(parsed2.references) || parsed2.references.some((reference) => typeof reference !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
7378
- return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
7379
- }
7380
- const references = parsed2.references ?? [];
7381
- if (references.length > 0) {
7382
- const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
7383
- const gone = references.filter((id2) => !present.has(id2));
7384
- if (gone.length > 0) {
7385
- return sendJson2(res, 410, {
7386
- ok: false,
7387
- error: gone.length === 1 ? "An attached image is gone: it was pasted over an hour ago and never sent. Attach it again." : "Some attached images are gone: they were pasted over an hour ago and never sent. Attach them again."
7388
- });
7389
- }
7390
- }
7391
- const width = typeof parsed2.width === "number" && Number.isFinite(parsed2.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed2.width))) : 1440;
7392
- const previews = await livePreviews();
7393
- const preview = previews.find((entry) => entry.title === parsed2.title);
7394
- if (!preview) {
7395
- return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7396
- }
7397
- const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
7398
- if (!parsed2.intent?.trim() && notes.length === 0) {
7399
- return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7400
- }
7401
- const intent = (parsed2.intent ?? "").trim();
7402
- const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
7403
- const sameNotes = (entry) => {
7404
- const before = [...entry.notes ?? []].sort().join(",");
7405
- return before === notes.map((note) => note.id).sort().join(",");
7406
- };
7407
- const compare = typeof parsed2.compare === "string" && parsed2.compare !== preview.title ? previews.find((entry) => entry.title === parsed2.compare) ?? null : null;
7408
- const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
7409
- if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
7410
- // one forks the direction and the other rewrites it. Only a
7411
- // genuine repeat is refused.
7412
- (entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
7413
- return sendJson2(res, 409, {
7414
- ok: false,
7415
- duplicate: true,
7416
- error: `That exact change to ${preview.title} is already waiting.`
7417
- });
7457
+ const ids = Array.isArray(parsed2.ids) ? parsed2.ids.filter((entry2) => typeof entry2 === "string") : [];
7458
+ if (ids.length === 0) {
7459
+ return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
7418
7460
  }
7419
- const address = server.address();
7420
- const requestPort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
7421
- const id = newRequestId();
7422
- const captured = await attachRequest(cwd, id, {
7423
- origin: `http://127.0.0.1:${requestPort}`,
7424
- preview,
7425
- width,
7426
- notes,
7427
- compare,
7428
- references
7429
- }, { pool: browserPool });
7430
- const composed = composeRequest(preview, intent, mode, notes, leglasCommand, captured);
7431
7461
  try {
7432
- await appendRequest(cwd, {
7433
- title: preview.title,
7434
- url: preview.url,
7435
- intent,
7436
- // The ids travel with the request so a change made in place can
7437
- // forget the notes it answered. A fork leaves them where they are:
7438
- // the direction they point at was not touched.
7439
- ...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
7440
- ...captured.attachments.length === 0 ? {} : { attachments: captured.attachments },
7441
- ...captured.skipped === null ? {} : { captureNote: captured.skipped },
7442
- ...compare === null ? {} : { compare: compare.title },
7443
- ...references.length === 0 ? {} : { references },
7444
- ...composed
7445
- }, id);
7446
- runner?.nudge();
7447
- return sendJson2(res, 200, {
7448
- ok: true,
7449
- ...composed,
7450
- attachments: captured.attachments
7451
- });
7462
+ return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
7452
7463
  } catch {
7453
- return sendJson2(res, 200, {
7454
- ok: true,
7455
- ...composed,
7456
- attachments: captured.attachments,
7457
- queued: false
7458
- });
7464
+ return sendJson2(res, 500, { ok: false, error: "The notes could not be forgotten." });
7459
7465
  }
7460
7466
  });
7461
7467
  }
7462
- if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
7468
+ if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
7463
7469
  if (!hasJsonBody(req)) {
7464
- return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
7470
+ return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
7465
7471
  }
7466
7472
  let body = "";
7467
7473
  req.on("data", (chunk) => body += chunk);
@@ -7470,113 +7476,24 @@ async function startServer(options) {
7470
7476
  if (parsed2 === null) {
7471
7477
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7472
7478
  }
7473
- if (typeof parsed2.title !== "string" || parsed2.title === "") {
7474
- return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
7475
- }
7476
- if (parsed2.note !== void 0 && typeof parsed2.note !== "string") {
7477
- return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
7478
- }
7479
- const preview = (await livePreviews()).find((entry) => entry.title === parsed2.title);
7480
- if (preview === void 0) {
7481
- return sendJson2(res, 404, { ok: false, error: "No such direction." });
7479
+ if (typeof parsed2.id !== "string") {
7480
+ return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
7482
7481
  }
7483
- const width = typeof parsed2.width === "number" && Number.isFinite(parsed2.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed2.width))) : 1440;
7484
- const browser = await browserPool.acquire();
7485
- if (browser === null) {
7486
- return sendJson2(res, 503, {
7487
- ok: false,
7488
- error: browserPool.reason() ?? NO_BROWSER
7489
- });
7482
+ const target2 = (await readRequests(cwd)).find((entry2) => entry2.id === parsed2.id);
7483
+ if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
7484
+ return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
7490
7485
  }
7491
- const annotations = typeof parsed2.note === "string" ? (await readAnnotations(cwd).catch(() => [])).filter((entry) => entry.id === parsed2.note && entry.title === preview.title) : [];
7492
- const address = server.address();
7493
- const capturePort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
7494
- const controller = new AbortController();
7495
- const timeoutMarker = /* @__PURE__ */ Symbol("capture timeout");
7496
- let timedOut;
7497
- const timeout = new Promise((resolve5) => {
7498
- timedOut = () => resolve5(timeoutMarker);
7499
- });
7500
- const timer = setTimeout(() => {
7501
- timedOut();
7502
- controller.abort();
7503
- }, CAPTURE_DEADLINE_MS);
7504
- timer.unref?.();
7505
7486
  try {
7506
- const work = capturePage(browser, {
7507
- url: previewUrl(`http://127.0.0.1:${capturePort}`, preview),
7508
- width,
7509
- ...annotations.length === 0 ? {} : {
7510
- focuses: annotations.map((entry) => ({
7511
- selector: entry.anchor.selector,
7512
- text: entry.anchor.text,
7513
- tag: entry.anchor.tag,
7514
- ...entry.anchor.region === void 0 ? {} : { region: entry.anchor.region },
7515
- rect: entry.anchor.rect
7516
- }))
7517
- },
7518
- timeoutMs: CAPTURE_LOAD_MS,
7519
- signal: controller.signal
7520
- });
7521
- const result2 = await Promise.race([work, timeout]);
7522
- if (result2 === timeoutMarker) {
7523
- return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
7487
+ if (!await removeRequest(cwd, parsed2.id)) {
7488
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
7524
7489
  }
7525
- clearTimeout(timer);
7526
- const crop = annotations.length > 0 ? result2.crops[0] : null;
7527
- const shot = crop?.shot ?? result2.frame;
7528
- const noteSuffix = typeof parsed2.note === "string" ? `-${parsed2.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
7529
- const name = `${captureSlug(preview.title)}-${result2.frame.width}${noteSuffix}.png`;
7530
- const relativeFile = `${CAPTURES_DIR}/show/${name}`;
7531
- await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
7532
- await writeFile8(join12(cwd, relativeFile), shot.png);
7533
- return sendJson2(res, 200, {
7534
- ok: true,
7535
- file: relativeFile,
7536
- width: shot.width,
7537
- height: shot.height,
7538
- viewport: result2.frame.width,
7539
- errors: result2.errors,
7540
- hydration: result2.hydration,
7541
- cut: result2.cut
7542
- });
7543
- } catch (error) {
7544
- clearTimeout(timer);
7545
- return sendJson2(res, 502, {
7546
- ok: false,
7547
- error: error instanceof Error ? error.message : String(error)
7548
- });
7490
+ return sendJson2(res, 200, { ok: true });
7491
+ } catch {
7492
+ return sendJson2(res, 500, { ok: false, error: "The request could not be dismissed." });
7549
7493
  }
7550
7494
  });
7551
7495
  }
7552
- if (path === `${LEGLAS_PREFIX}/api/agents/warm` && req.method === "POST") {
7553
- return void readAgentChoice(cwd).then((choice) => {
7554
- if (choice.agent !== null)
7555
- runner?.prepare(choice.agent);
7556
- sendJson2(res, 200, { ok: true });
7557
- }, () => sendJson2(res, 200, { ok: true }));
7558
- }
7559
- if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
7560
- return void Promise.all([
7561
- currentAgents(query.get("refresh") === "1"),
7562
- readAgentChoice(cwd)
7563
- ]).then(([agents, choice]) => sendJson2(res, 200, {
7564
- agents,
7565
- choice: choice.agent,
7566
- customRun: choice.run,
7567
- effort: choice.effort
7568
- }));
7569
- }
7570
- if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
7571
- if (!isLoopbackAddress(req.socket.remoteAddress)) {
7572
- return sendJson2(res, 403, {
7573
- ok: false,
7574
- error: "The agent choice can only be made from the machine running Leglas."
7575
- });
7576
- }
7577
- if (!hasJsonBody(req)) {
7578
- return sendJson2(res, 400, { ok: false, error: "Agent choice must be JSON." });
7579
- }
7496
+ if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
7580
7497
  let body = "";
7581
7498
  req.on("data", (chunk) => body += chunk);
7582
7499
  return void req.on("end", () => {
@@ -7584,448 +7501,1192 @@ async function startServer(options) {
7584
7501
  if (parsed2 === null) {
7585
7502
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7586
7503
  }
7587
- if (!isKnownAgent(parsed2.agent) && parsed2.agent !== "custom") {
7588
- return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
7589
- }
7590
- if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
7591
- return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
7592
- }
7593
- const effort = parsed2.effort === null || isAgentEffort(parsed2.effort) ? parsed2.effort : void 0;
7594
- if (parsed2.effort !== void 0 && effort === void 0) {
7595
- return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
7596
- }
7597
- if (parsed2.agent === "custom") {
7598
- if (effort !== void 0) {
7599
- return sendJson2(res, 400, {
7600
- ok: false,
7601
- error: "Custom agents manage effort in their own command."
7602
- });
7603
- }
7604
- if (typeof parsed2.run !== "string") {
7605
- return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
7606
- }
7607
- const template = parseTemplate(parsed2.run);
7608
- if (!template.ok)
7609
- return sendJson2(res, 400, { ok: false, error: template.error });
7610
- return void saveAgentChoice(cwd, { agent: "custom", run: parsed2.run }).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
7611
- }
7612
- if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed2.agent].efforts.includes(effort)) {
7613
- return sendJson2(res, 400, {
7614
- ok: false,
7615
- error: `${KNOWN_AGENTS[parsed2.agent].name} does not expose an effort override.`
7616
- });
7504
+ if (parsed2.renames === null || typeof parsed2.renames !== "object") {
7505
+ return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
7617
7506
  }
7618
- return void saveAgentChoice(cwd, {
7619
- agent: parsed2.agent,
7620
- ...effort === void 0 ? {} : { effort }
7621
- }).then(() => {
7622
- runner?.prepare(parsed2.agent);
7623
- sendJson2(res, 200, { ok: true });
7624
- }, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
7507
+ const renames = Object.fromEntries(Object.entries(parsed2.renames).filter((entry2) => typeof entry2[1] === "string" && entry2[1] !== ""));
7508
+ void writeRenames(cwd, renames).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 200, { ok: false }));
7509
+ });
7510
+ }
7511
+ if (path === `${LEGLAS_PREFIX}/api/health`) {
7512
+ return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
7513
+ }
7514
+ if (path.startsWith(`${FILES_PREFIX}/`)) {
7515
+ const rest = path.slice(FILES_PREFIX.length + 1);
7516
+ const slash = rest.indexOf("/");
7517
+ const slug = slash === -1 ? rest : rest.slice(0, slash);
7518
+ let relative6 = slash === -1 ? "" : rest.slice(slash + 1);
7519
+ try {
7520
+ relative6 = decodeURIComponent(relative6);
7521
+ } catch {
7522
+ relative6 = "";
7523
+ }
7524
+ const dir = fileMounts.get(slug);
7525
+ const serveMount = () => {
7526
+ if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
7527
+ return;
7528
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
7529
+ res.end("Leglas: no such preview file.");
7530
+ };
7531
+ if (!context.remote)
7532
+ return serveMount();
7533
+ if (relative6.split("/").some((segment) => segment.startsWith("."))) {
7534
+ return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
7535
+ }
7536
+ return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
7537
+ if (!allowed)
7538
+ return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
7539
+ serveMount();
7625
7540
  });
7626
7541
  }
7627
- if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
7628
- let body = "";
7629
- req.on("data", (chunk) => body += chunk);
7630
- return void req.on("end", () => {
7631
- const parsed2 = jsonBody(body);
7632
- if (parsed2 === null) {
7633
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7634
- }
7635
- if (typeof parsed2.watching !== "boolean") {
7636
- return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
7637
- }
7638
- lastSeen = parsed2.watching ? Date.now() : null;
7639
- sendJson2(res, 200, { ok: true });
7542
+ if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
7543
+ return sendJson2(res, 404, { error: "No such Leglas API path." });
7544
+ }
7545
+ if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
7546
+ if (shellDir !== null && serveShellFile(res, shellDir, path))
7547
+ return;
7548
+ if (shellDir !== null) {
7549
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
7550
+ res.end("Leglas: no such path.");
7551
+ return;
7552
+ }
7553
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
7554
+ res.end(PLACEHOLDER);
7555
+ return;
7556
+ }
7557
+ return proxy.request(req, res, context.publicOrigin);
7558
+ };
7559
+ let port = 0;
7560
+ const server = http4.createServer((req, res) => handleRequest(req, res, {
7561
+ remote: false,
7562
+ publicOrigin: `http://localhost:${port}`
7563
+ }));
7564
+ const sockets = /* @__PURE__ */ new Set();
7565
+ server.on("connection", (socket) => {
7566
+ sockets.add(socket);
7567
+ socket.once("close", () => sockets.delete(socket));
7568
+ });
7569
+ const handleUpgrade = (req, socket, head, context) => {
7570
+ const liveUpgrade = context.remote ? live.upgrade(req, socket, head, { viewer: true }) : live.upgrade(req, socket, head);
7571
+ if (liveUpgrade)
7572
+ return true;
7573
+ const path = (req.url ?? "/").split("?")[0] ?? "/";
7574
+ if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
7575
+ socket.destroy();
7576
+ return false;
7577
+ }
7578
+ proxy.upgrade(req, socket, head);
7579
+ return false;
7580
+ };
7581
+ server.on("upgrade", (req, socket, head) => {
7582
+ handleUpgrade(req, socket, head, { remote: false });
7583
+ });
7584
+ shares = createShareManager({
7585
+ live,
7586
+ previews: livePreviewDefinitions,
7587
+ previewsForConfig,
7588
+ viewerConfig: {
7589
+ project,
7590
+ devServer: target,
7591
+ scanPreviews: config?.scanPreviews ?? true
7592
+ },
7593
+ request: (req, res, context) => handleRequest(req, res, {
7594
+ remote: true,
7595
+ publicOrigin: context.publicOrigin,
7596
+ grantId: context.grantId
7597
+ }),
7598
+ upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
7599
+ ...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
7600
+ ...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
7601
+ });
7602
+ port = await bind2(server, options.port ?? DEFAULT_PORT);
7603
+ options.updates?.setPort(port);
7604
+ const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
7605
+ liveHealth = watchHealth(target, live);
7606
+ await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
7607
+ });
7608
+ await writeServerInfo(cwd, {
7609
+ port,
7610
+ url: `http://localhost:${port}`,
7611
+ pid: process.pid
7612
+ }).catch(() => {
7613
+ });
7614
+ runner = startRunner({
7615
+ cwd,
7616
+ externallyAttached,
7617
+ onChange: () => live.nudge("requests"),
7618
+ leglasCommand,
7619
+ ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
7620
+ ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
7621
+ });
7622
+ options.updates?.onBusy(() => runner?.snapshot().running ?? false);
7623
+ options.updates?.onChange(() => live.nudge("update"));
7624
+ let closePromise = null;
7625
+ return {
7626
+ port,
7627
+ url: `http://localhost:${port}`,
7628
+ close: () => {
7629
+ if (closePromise !== null)
7630
+ return closePromise;
7631
+ closePromise = (async () => {
7632
+ liveFiles.close();
7633
+ liveHealth?.close();
7634
+ await options.updates?.close();
7635
+ await Promise.all([
7636
+ shares?.close() ?? Promise.resolve(),
7637
+ branches.stop(),
7638
+ runner.stop(),
7639
+ browserPool.close(),
7640
+ live.close()
7641
+ ]);
7642
+ await new Promise((done) => {
7643
+ for (const socket of sockets)
7644
+ socket.destroy();
7645
+ sockets.clear();
7646
+ server.closeAllConnections();
7647
+ server.close(() => done());
7648
+ });
7649
+ await removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
7650
+ });
7651
+ })();
7652
+ return closePromise;
7653
+ }
7654
+ };
7655
+ }
7656
+
7657
+ // ../server/dist/update.js
7658
+ import { spawn as spawnChild2 } from "child_process";
7659
+ import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
7660
+ import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as writeFile9 } from "fs/promises";
7661
+ import { homedir as homedir2 } from "os";
7662
+ import { dirname as dirname9, join as join13, posix as posix2, win32 } from "path";
7663
+ var DAY_MS = 24 * 60 * 60 * 1e3;
7664
+ var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
7665
+ var REGISTRY = "https://registry.npmjs.org";
7666
+ var RELEASES = "https://leglas.vercel.app/releases.json";
7667
+ var CHECKOUT_NOTICE = "You run Leglas from a checkout, so pull to update.";
7668
+ var releaseUrl = (version2) => `https://leglas.vercel.app/changelog/#v${version2}`;
7669
+ function normalized(path) {
7670
+ return posix2.normalize(path.replaceAll("\\", "/"));
7671
+ }
7672
+ function realPath(path) {
7673
+ try {
7674
+ return realpathSync(path);
7675
+ } catch {
7676
+ return null;
7677
+ }
7678
+ }
7679
+ var COMMANDS = {
7680
+ npx: { npm: ["npx"], pnpm: ["pnpm", "dlx"], bun: ["bunx"], yarn: ["yarn", "dlx"] },
7681
+ global: { npm: ["npm", "i", "-g"], pnpm: ["pnpm", "add", "-g"], bun: ["bun", "add", "-g"], yarn: ["yarn", "global", "add"] },
7682
+ project: { npm: ["npm", "install"], pnpm: ["pnpm", "up"], bun: ["bun", "update"], yarn: ["yarn", "up"] }
7683
+ };
7684
+ var YARN_CLASSIC = ["yarn", "upgrade"];
7685
+ function commandParts(kind, manager, version2, classic = false) {
7686
+ return [...classic ? YARN_CLASSIC : COMMANDS[kind][manager], `leglas@${version2}`];
7687
+ }
7688
+ function installation(kind, manager, root, classic = false) {
7689
+ return { kind, manager, command: commandParts(kind, manager, "latest", classic).join(" "), ...root === void 0 ? {} : { root } };
7690
+ }
7691
+ function* ancestors(directory) {
7692
+ const paths = /^[a-z]:\//i.test(directory) ? win32 : posix2;
7693
+ let current = directory;
7694
+ while (true) {
7695
+ yield current;
7696
+ const parent = normalized(paths.dirname(current));
7697
+ if (parent === current)
7698
+ return;
7699
+ current = parent;
7700
+ }
7701
+ }
7702
+ function environmentManager(env) {
7703
+ const manager = /^(npm|pnpm|bun|yarn)\//.exec(env.npm_config_user_agent ?? "")?.[1];
7704
+ return manager === "pnpm" || manager === "bun" || manager === "yarn" || manager === "npm" ? manager : null;
7705
+ }
7706
+ function projectInstall(root, exists) {
7707
+ for (const directory of ancestors(root)) {
7708
+ if (exists(posix2.join(directory, "pnpm-lock.yaml")))
7709
+ return installation("project", "pnpm", root);
7710
+ if (exists(posix2.join(directory, "yarn.lock"))) {
7711
+ return installation("project", "yarn", root, !exists(posix2.join(directory, ".yarnrc.yml")));
7712
+ }
7713
+ if (exists(posix2.join(directory, "bun.lock")) || exists(posix2.join(directory, "bun.lockb")))
7714
+ return installation("project", "bun", root);
7715
+ if (exists(posix2.join(directory, "package-lock.json")) || exists(posix2.join(directory, "npm-shrinkwrap.json")))
7716
+ return installation("project", "npm", root);
7717
+ }
7718
+ return installation("project", "npm", root);
7719
+ }
7720
+ function detectInstall(entry2, cwd, exists, realpath5 = realPath, env = process.env) {
7721
+ const path = normalized(entry2);
7722
+ const directory = normalized(realpath5(cwd) ?? cwd);
7723
+ const manager = environmentManager(env);
7724
+ if (path.includes("/_npx/"))
7725
+ return installation("npx", "npm");
7726
+ if (path.includes("/pnpm/") && path.includes("/dlx/"))
7727
+ return installation("npx", "pnpm");
7728
+ if (/\/bunx-[^/]+\//.test(path))
7729
+ return installation("npx", "bun");
7730
+ if (/\/dlx-[^/]+\//.test(path))
7731
+ return installation("npx", "yarn");
7732
+ const berry = /\/\.yarn\/(?:berry\/)?cache\//.test(path) || path.includes("/.yarn/unplugged/");
7733
+ const packageIndex = path.lastIndexOf("/node_modules/leglas/");
7734
+ const cached = /\/(?:tmp|temp|cache|\.cache)\//i.test(path) || path.includes("/Library/Caches/") || /\/var\/folders\/[^/]+\/[^/]+\/T\//.test(path) || [env.TMPDIR, env.TEMP, env.TMP].some((temp) => temp !== void 0 && path.startsWith(`${normalized(temp).replace(/\/$/, "")}/`));
7735
+ if (!berry && packageIndex !== -1 && cached && manager !== null && manager !== "npm") {
7736
+ return installation("npx", manager);
7737
+ }
7738
+ if (berry) {
7739
+ for (const root of ancestors(directory)) {
7740
+ if (exists(posix2.join(root, "yarn.lock")) && exists(posix2.join(root, ".yarnrc.yml")))
7741
+ return installation("project", "yarn", root);
7742
+ }
7743
+ return { kind: "source", manager: "yarn", command: null };
7744
+ }
7745
+ if (packageIndex === -1)
7746
+ return { kind: "source", manager: "npm", command: null };
7747
+ const packageDirectory = path.slice(0, packageIndex + "/node_modules/leglas".length);
7748
+ const windows = /^[a-z]:\//i.test(path) || entry2.startsWith("\\\\");
7749
+ const comparable = (value) => windows ? normalized(value).toLowerCase() : normalized(value);
7750
+ for (const root of ancestors(directory)) {
7751
+ const dependency = realpath5(posix2.join(root, "node_modules/leglas"));
7752
+ if (dependency !== null && comparable(dependency) === comparable(packageDirectory))
7753
+ return projectInstall(root, exists);
7754
+ }
7755
+ if (path.includes("/pnpm/") && /\/store\/[^/]+\/links\//.test(path) && manager === "pnpm")
7756
+ return installation("npx", "pnpm");
7757
+ if (path.includes("/pnpm/"))
7758
+ return installation("global", "pnpm");
7759
+ if (path.includes("/yarn/global/") || path.includes("/.yarn/global/"))
7760
+ return installation("global", "yarn");
7761
+ if (path.includes("/.bun/"))
7762
+ return installation("global", "bun");
7763
+ return installation("global", manager ?? "npm");
7764
+ }
7765
+ function parsedVersion(value) {
7766
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
7767
+ if (match === null)
7768
+ return null;
7769
+ const pre = match[4]?.split(".") ?? [];
7770
+ if (pre.some((part) => /^0\d+$/.test(part)))
7771
+ return null;
7772
+ return { core: match.slice(1, 4).map((part) => BigInt(part)), pre };
7773
+ }
7774
+ function compareVersions(a, b) {
7775
+ const left = parsedVersion(a);
7776
+ const right = parsedVersion(b);
7777
+ if (left === null || right === null)
7778
+ return 0;
7779
+ for (let i = 0; i < 3; i += 1) {
7780
+ if (left.core[i] < right.core[i])
7781
+ return -1;
7782
+ if (left.core[i] > right.core[i])
7783
+ return 1;
7784
+ }
7785
+ if (left.pre.length === 0)
7786
+ return right.pre.length === 0 ? 0 : 1;
7787
+ if (right.pre.length === 0)
7788
+ return -1;
7789
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
7790
+ const l = left.pre[i];
7791
+ const r = right.pre[i];
7792
+ if (l === void 0)
7793
+ return -1;
7794
+ if (r === void 0)
7795
+ return 1;
7796
+ if (l === r)
7797
+ continue;
7798
+ const ln = /^\d+$/.test(l);
7799
+ const rn = /^\d+$/.test(r);
7800
+ if (ln && rn)
7801
+ return BigInt(l) < BigInt(r) ? -1 : 1;
7802
+ if (ln !== rn)
7803
+ return ln ? -1 : 1;
7804
+ return l < r ? -1 : 1;
7805
+ }
7806
+ return 0;
7807
+ }
7808
+ function windowsLine(parts) {
7809
+ return parts.map((part) => {
7810
+ if (part !== "" && !/[\s"&|<>^()]/.test(part))
7811
+ return part;
7812
+ return `"${part.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
7813
+ }).join(" ");
7814
+ }
7815
+ function spawnCommand(parts, platform) {
7816
+ if (platform === "win32")
7817
+ return { file: windowsLine(parts), args: [], shell: true };
7818
+ return { file: parts[0], args: parts.slice(1), shell: false };
7819
+ }
7820
+ function restartCommand(install, argv, latest, port, options) {
7821
+ const rest = [];
7822
+ for (let i = 2; i < argv.length; i += 1) {
7823
+ const argument = argv[i];
7824
+ if (argument === "--port")
7825
+ i += 1;
7826
+ else if (!argument.startsWith("--port=") && argument !== "--no-open")
7827
+ rest.push(argument);
7828
+ }
7829
+ rest.push("--port", String(port), "--no-open");
7830
+ const windows = options.platform === "win32";
7831
+ if (install.kind === "npx") {
7832
+ const runner = [...COMMANDS.npx[install.manager], ...install.manager === "npm" ? ["-y"] : [], `leglas@${latest}`];
7833
+ return spawnCommand([...runner, ...rest], options.platform);
7834
+ }
7835
+ if (install.kind === "global") {
7836
+ if (argv[1] !== void 0 && options.exists(argv[1]))
7837
+ return { file: options.execPath, args: [argv[1], ...rest], shell: false };
7838
+ return spawnCommand(["leglas", ...rest], options.platform);
7839
+ }
7840
+ if (install.kind === "project" && install.root !== void 0) {
7841
+ const shim = (windows ? win32.join : join13)(install.root, "node_modules", ".bin", windows ? "leglas.cmd" : "leglas");
7842
+ return spawnCommand(options.exists(shim) ? [shim, ...rest] : ["yarn", "leglas", ...rest], options.platform);
7843
+ }
7844
+ throw new Error(CHECKOUT_NOTICE);
7845
+ }
7846
+ function object(value) {
7847
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7848
+ }
7849
+ function readState(path) {
7850
+ const empty = { checkedAt: null, latest: null, skipped: null };
7851
+ if (path === null)
7852
+ return empty;
7853
+ try {
7854
+ const value = JSON.parse(readFileSync(path, "utf8"));
7855
+ if (!object(value))
7856
+ return empty;
7857
+ if (value.checkedAt !== null && (typeof value.checkedAt !== "string" || !Number.isFinite(Date.parse(value.checkedAt))))
7858
+ return empty;
7859
+ if (value.skipped !== null && (typeof value.skipped !== "string" || parsedVersion(value.skipped) === null))
7860
+ return empty;
7861
+ let latest = null;
7862
+ if (value.latest !== null) {
7863
+ const release = value.latest;
7864
+ if (!object(release) || typeof release.version !== "string" || parsedVersion(release.version) === null || release.title !== null && typeof release.title !== "string" || typeof release.url !== "string")
7865
+ return empty;
7866
+ latest = { version: release.version, title: release.title, url: releaseUrl(release.version) };
7867
+ }
7868
+ return { checkedAt: value.checkedAt, latest, skipped: value.skipped };
7869
+ } catch {
7870
+ return empty;
7871
+ }
7872
+ }
7873
+ function defaultStatePath(home) {
7874
+ try {
7875
+ return join13(home(), ".leglas", "update.json");
7876
+ } catch {
7877
+ return null;
7878
+ }
7879
+ }
7880
+ function mergeState(current, saved) {
7881
+ const checked = (state) => state.checkedAt === null ? -Infinity : Date.parse(state.checkedAt);
7882
+ const newest = checked(saved) > checked(current) ? saved : current;
7883
+ let skipped = current.skipped;
7884
+ if (saved.skipped !== null && (skipped === null || compareVersions(saved.skipped, skipped) > 0))
7885
+ skipped = saved.skipped;
7886
+ if (skipped !== null && newest.latest !== null && compareVersions(newest.latest.version, skipped) > 0)
7887
+ skipped = null;
7888
+ return { latest: newest.latest, checkedAt: newest.checkedAt, skipped };
7889
+ }
7890
+ async function writeState(path, state) {
7891
+ let temporary = null;
7892
+ try {
7893
+ const directory = dirname9(path);
7894
+ await mkdir9(directory, { recursive: true });
7895
+ if (!(await lstat3(directory)).isDirectory())
7896
+ return;
7897
+ const existing = await lstat3(path).catch(() => null);
7898
+ if (existing !== null && !existing.isFile())
7899
+ return;
7900
+ temporary = await mkdtemp(join13(directory, ".update-"));
7901
+ const file = join13(temporary, "update.json");
7902
+ await writeFile9(file, `${JSON.stringify(state, null, 2)}
7903
+ `, "utf8");
7904
+ await rename3(file, path);
7905
+ } catch {
7906
+ } finally {
7907
+ if (temporary !== null)
7908
+ await rm5(temporary, { recursive: true, force: true }).catch(() => {
7640
7909
  });
7910
+ }
7911
+ }
7912
+ function timedOut(error) {
7913
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
7914
+ }
7915
+ async function registryVersion(fetcher, timeoutMs, registry) {
7916
+ let response;
7917
+ try {
7918
+ response = await fetcher(`${registry}/leglas/latest`, {
7919
+ signal: AbortSignal.timeout(timeoutMs),
7920
+ headers: { accept: "application/json" }
7921
+ });
7922
+ } catch (error) {
7923
+ throw new Error(timedOut(error) ? "npm took too long to answer." : "Could not reach npm.");
7924
+ }
7925
+ if (response.status !== 200)
7926
+ throw new Error(`npm answered ${response.status}.`);
7927
+ let body;
7928
+ try {
7929
+ body = await response.json();
7930
+ } catch (error) {
7931
+ throw new Error(timedOut(error) ? "npm took too long to answer." : "npm's answer made no sense.");
7932
+ }
7933
+ if (!object(body) || typeof body.version !== "string" || parsedVersion(body.version) === null) {
7934
+ throw new Error("npm's answer made no sense.");
7935
+ }
7936
+ return body.version;
7937
+ }
7938
+ async function releaseTitles(fetcher, timeoutMs) {
7939
+ const titles = /* @__PURE__ */ new Map();
7940
+ try {
7941
+ const response = await fetcher(RELEASES, {
7942
+ signal: AbortSignal.timeout(timeoutMs),
7943
+ headers: { accept: "application/json" }
7944
+ });
7945
+ if (response.status !== 200)
7946
+ return titles;
7947
+ const body = await response.json();
7948
+ if (!Array.isArray(body))
7949
+ return titles;
7950
+ for (const entry2 of body) {
7951
+ if (object(entry2) && typeof entry2.version === "string" && typeof entry2.title === "string") {
7952
+ titles.set(entry2.version, entry2.title);
7953
+ }
7641
7954
  }
7642
- if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
7643
- const snapshot = runner?.snapshot() ?? {
7644
- running: false,
7645
- requestId: null,
7646
- agent: null,
7647
- activity: null,
7648
- startedAt: null,
7649
- stopping: false,
7650
- waiting: null,
7651
- failedIds: []
7652
- };
7653
- return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
7654
- requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
7655
- id,
7656
- title,
7657
- intent,
7658
- // A fork leaves its parent's document alone; the interface keeps
7659
- // the parent's duplicate verdict on the strength of this.
7660
- mode,
7661
- // Which pins this change speaks for. The interface marks them, so
7662
- // a note already sitting in a prompt an agent holds does not look
7663
- // like one nobody has read.
7664
- notes: notes ?? [],
7665
- // The run in flight is the one thing the file cannot know. After
7666
- // that the file is the record, including across a restart, and the
7667
- // process-local failed set only covers a request whose verdict
7668
- // could not be written.
7669
- status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
7670
- failure: failure ?? null
7671
- })),
7672
- agent: {
7673
- attached: externallyAttached(),
7674
- running: snapshot.running,
7675
- name: snapshot.running ? snapshot.agent : null,
7676
- activity: snapshot.running ? snapshot.activity : null,
7677
- startedAt: snapshot.running ? snapshot.startedAt : null,
7678
- // A stop that has been asked for but not yet obeyed. The card
7679
- // says so rather than going on describing a live run.
7680
- stopping: snapshot.running && snapshot.stopping,
7681
- // Why a run that looks stalled is stalled, while it is stalled.
7682
- waiting: snapshot.running ? snapshot.waiting : null
7683
- }
7684
- }));
7955
+ } catch {
7956
+ }
7957
+ return titles;
7958
+ }
7959
+ function installerReason(stdout, stderr) {
7960
+ const lines2 = (output2) => output2.split(/\r?\n/).filter((line) => !/^\s+at /.test(line) && !/A complete log of this run|info Visit https:\/\/yarnpkg\.com/.test(line)).map((line) => line.trim()).filter(Boolean);
7961
+ const errors = lines2(stderr);
7962
+ const output = lines2(stdout);
7963
+ const npm = [...errors, ...output].find((line) => /^npm error\s+\S/.test(line) && !/^npm error code\s/.test(line));
7964
+ return npm?.replace(/^npm error\s+/, "") ?? errors[0] ?? output[0] ?? null;
7965
+ }
7966
+ function installVersion(install, version2, deps) {
7967
+ if (install.kind !== "global" && install.kind !== "project")
7968
+ throw new Error(CHECKOUT_NOTICE);
7969
+ const classic = install.command === commandParts("project", "yarn", "latest", true).join(" ");
7970
+ const parts = commandParts(install.kind, install.manager, version2, classic);
7971
+ const command = parts.join(" ");
7972
+ const invocation = spawnCommand(parts, deps.platform);
7973
+ deps.log(`Updating Leglas to ${version2} with ${command}\u2026`);
7974
+ const child = deps.spawn(invocation.file, invocation.args, {
7975
+ stdio: ["ignore", "pipe", "pipe"],
7976
+ shell: invocation.shell,
7977
+ detached: deps.platform !== "win32",
7978
+ env: deps.env,
7979
+ ...install.kind === "project" ? { cwd: install.root } : {}
7980
+ });
7981
+ let settle;
7982
+ let reject;
7983
+ const result2 = new Promise((resolve5, fail) => {
7984
+ settle = resolve5;
7985
+ reject = fail;
7986
+ });
7987
+ let markGone;
7988
+ const gone = new Promise((resolve5) => {
7989
+ markGone = resolve5;
7990
+ });
7991
+ let settled = false;
7992
+ let exited = false;
7993
+ let stopping = false;
7994
+ let stdout = "";
7995
+ let stderr = "";
7996
+ const readOut = (chunk) => {
7997
+ stdout = (stdout + chunk.toString()).slice(-64e3);
7998
+ };
7999
+ const readErr = (chunk) => {
8000
+ stderr = (stderr + chunk.toString()).slice(-64e3);
8001
+ };
8002
+ const finish = (error) => {
8003
+ if (settled)
8004
+ return;
8005
+ settled = true;
8006
+ clearTimeout(timer);
8007
+ if (error === void 0)
8008
+ settle();
8009
+ else
8010
+ reject(error);
8011
+ };
8012
+ const stop = () => {
8013
+ if (stopping || exited)
8014
+ return;
8015
+ stopping = true;
8016
+ try {
8017
+ if (child.pid === void 0)
8018
+ child.kill("SIGKILL");
8019
+ else if (deps.platform === "win32") {
8020
+ const killer = deps.spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", shell: false });
8021
+ killer.once("error", () => child.kill("SIGKILL"));
8022
+ } else
8023
+ deps.kill(-child.pid, "SIGKILL");
8024
+ } catch {
7685
8025
  }
7686
- if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
7687
- if (!hasJsonBody(req)) {
7688
- return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
8026
+ };
8027
+ const timer = setTimeout(() => {
8028
+ finish(new Error(`${command} took longer than five minutes.`));
8029
+ stop();
8030
+ }, INSTALL_DEADLINE_MS);
8031
+ timer.unref?.();
8032
+ child.stdout?.on("data", readOut);
8033
+ child.stderr?.on("data", readErr);
8034
+ const onGone = () => {
8035
+ exited = true;
8036
+ clearTimeout(timer);
8037
+ child.stdout?.off("data", readOut);
8038
+ child.stderr?.off("data", readErr);
8039
+ markGone();
8040
+ };
8041
+ child.once("error", (error) => {
8042
+ finish(error);
8043
+ if (child.pid === void 0)
8044
+ onGone();
8045
+ });
8046
+ child.once("close", (code) => {
8047
+ const reason = installerReason(stdout, stderr);
8048
+ finish(code === 0 ? void 0 : new Error(`${command} exited ${code ?? 1}.${reason === null ? "" : ` ${reason}`}`));
8049
+ onGone();
8050
+ });
8051
+ return { result: result2, gone, stop };
8052
+ }
8053
+ function createUpdateService(input) {
8054
+ const deps = input.deps ?? {};
8055
+ const fetcher = deps.fetch ?? fetch;
8056
+ const now = deps.now ?? Date.now;
8057
+ const timeoutMs = deps.timeoutMs ?? 4e3;
8058
+ const statePath = deps.statePath === void 0 ? defaultStatePath(deps.homedir ?? homedir2) : deps.statePath;
8059
+ const exists = deps.exists ?? existsSync4;
8060
+ const env = deps.env ?? process.env;
8061
+ const install = detectInstall(input.entry, input.cwd, exists, deps.realpath ?? realPath, env);
8062
+ const spawn5 = deps.spawn ?? spawnChild2;
8063
+ const kill = deps.kill ?? process.kill.bind(process);
8064
+ const platform = deps.platform ?? process.platform;
8065
+ const execPath = deps.execPath ?? process.execPath;
8066
+ const log = deps.log ?? console.log;
8067
+ const registry = (env.npm_config_registry || REGISTRY).replace(/\/+$/, "");
8068
+ const argv = [...input.argv];
8069
+ let state = {
8070
+ ...readState(statePath),
8071
+ checkError: null,
8072
+ phase: { status: "idle" }
8073
+ };
8074
+ let checking = null;
8075
+ let writing2 = Promise.resolve();
8076
+ let port = DEFAULT_PORT;
8077
+ let busy = () => false;
8078
+ let restart = null;
8079
+ let installer = null;
8080
+ let pending = null;
8081
+ let waiting = null;
8082
+ let resume = null;
8083
+ let closed = false;
8084
+ let closing = null;
8085
+ const listeners = /* @__PURE__ */ new Set();
8086
+ const change = (next) => {
8087
+ if (Object.entries(next).every(([key, value]) => JSON.stringify(state[key]) === JSON.stringify(value)))
8088
+ return;
8089
+ state = { ...state, ...next };
8090
+ for (const listener of listeners) {
8091
+ try {
8092
+ listener();
8093
+ } catch {
7689
8094
  }
7690
- let body = "";
7691
- req.on("data", (chunk) => body += chunk);
7692
- return void req.on("end", () => {
7693
- const parsed2 = jsonBody(body);
7694
- if (parsed2 === null) {
7695
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7696
- }
7697
- if (parsed2.id !== void 0 && typeof parsed2.id !== "string") {
7698
- return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
7699
- }
7700
- return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed2.id) ?? false });
8095
+ }
8096
+ };
8097
+ const available = () => state.latest !== null && compareVersions(state.latest.version, input.version) > 0;
8098
+ const inFlight = () => ["installing", "waiting", "restarting"].includes(state.phase.status) || installer !== null;
8099
+ const status = () => ({
8100
+ version: input.version,
8101
+ install: { ...install },
8102
+ latest: state.latest === null ? null : { ...state.latest },
8103
+ checkedAt: state.checkedAt,
8104
+ checkError: state.checkError,
8105
+ skipped: state.skipped,
8106
+ available: available(),
8107
+ phase: { ...state.phase },
8108
+ busy: busy()
8109
+ });
8110
+ const persist = () => {
8111
+ if (statePath === null)
8112
+ return Promise.resolve();
8113
+ writing2 = writing2.then(async () => {
8114
+ const merged = mergeState(state, readState(statePath));
8115
+ change(merged);
8116
+ await writeState(statePath, merged);
8117
+ });
8118
+ return writing2;
8119
+ };
8120
+ const waitForIdle = async (version2) => {
8121
+ if (!busy() || closed)
8122
+ return;
8123
+ change({ phase: { status: "waiting", version: version2 } });
8124
+ while (!closed && busy()) {
8125
+ await new Promise((resolve5) => {
8126
+ resume = resolve5;
8127
+ waiting = setTimeout(() => {
8128
+ waiting = null;
8129
+ resume = null;
8130
+ resolve5();
8131
+ }, 1e3);
8132
+ waiting.unref?.();
7701
8133
  });
7702
8134
  }
7703
- if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
7704
- if (!hasJsonBody(req)) {
7705
- return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
8135
+ };
8136
+ const performUpdate = async (version2, handoff) => {
8137
+ try {
8138
+ if (closed)
8139
+ return;
8140
+ if (install.kind !== "npx") {
8141
+ const running = installVersion(install, version2, { spawn: spawn5, kill, platform, env, log });
8142
+ installer = running;
8143
+ void running.gone.then(() => {
8144
+ if (installer === running)
8145
+ installer = null;
8146
+ });
8147
+ await running.result;
7706
8148
  }
7707
- let body = "";
7708
- req.on("data", (chunk) => body += chunk);
7709
- return void req.on("end", async () => {
7710
- const parsed2 = jsonBody(body);
7711
- if (parsed2 === null) {
7712
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7713
- }
7714
- if (typeof parsed2.id !== "string") {
7715
- return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
7716
- }
7717
- const request = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
7718
- if (request === void 0) {
7719
- return sendJson2(res, 404, { ok: false, error: "No such request." });
7720
- }
7721
- if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
7722
- return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
7723
- }
8149
+ await waitForIdle(version2);
8150
+ if (closed)
8151
+ return;
8152
+ change({ phase: { status: "restarting", version: version2 } });
8153
+ log(`Restarting Leglas with ${version2}\u2026`);
8154
+ await handoff(restartCommand(install, argv, version2, port, { execPath, platform, exists }));
8155
+ } catch (error) {
8156
+ if (!closed)
8157
+ change({ phase: { status: "failed", version: version2, reason: error instanceof Error ? error.message : String(error) } });
8158
+ }
8159
+ };
8160
+ return {
8161
+ status,
8162
+ check(options = {}) {
8163
+ if (closed)
8164
+ return Promise.resolve(status());
8165
+ if (checking !== null)
8166
+ return checking;
8167
+ if (!options.force && state.latest !== null && state.checkedAt !== null) {
8168
+ const age = now() - Date.parse(state.checkedAt);
8169
+ if (age >= 0 && age < DAY_MS)
8170
+ return Promise.resolve(status());
8171
+ }
8172
+ if (!inFlight())
8173
+ change({ phase: { status: "checking" } });
8174
+ checking = (async () => {
7724
8175
  try {
7725
- const retryId = newRequestId();
7726
- const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
7727
- if (!await removeRequest(cwd, request.id)) {
7728
- return sendJson2(res, 404, { ok: false, error: "No such request." });
8176
+ const [version2, titles] = await Promise.all([
8177
+ registryVersion(fetcher, timeoutMs, registry),
8178
+ releaseTitles(fetcher, timeoutMs)
8179
+ ]);
8180
+ if (!closed) {
8181
+ change({
8182
+ latest: { version: version2, title: titles.get(version2) ?? null, url: releaseUrl(version2) },
8183
+ checkedAt: new Date(now()).toISOString(),
8184
+ checkError: null,
8185
+ skipped: state.skipped !== null && compareVersions(version2, state.skipped) > 0 ? null : state.skipped
8186
+ });
8187
+ await persist();
7729
8188
  }
7730
- await appendRequest(cwd, {
7731
- title: request.title,
7732
- url: request.url,
7733
- intent: request.intent,
7734
- target: request.target,
7735
- // The prompt names the captures by path, and the embedded pipes
7736
- // are not its only readers: watch, a custom command and
7737
- // `requests --json` all hand the text over as it stands.
7738
- prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId),
7739
- // The stored prompt already carries the mode's instructions;
7740
- // its notes and visual context travel with the retry too.
7741
- ...request.mode === void 0 ? {} : { mode: request.mode },
7742
- ...request.notes === void 0 ? {} : { notes: request.notes },
7743
- ...attachments.length === 0 ? {} : { attachments },
7744
- ...request.captureNote === void 0 ? {} : { captureNote: request.captureNote },
7745
- ...request.compare === void 0 ? {} : { compare: request.compare },
7746
- ...request.references === void 0 ? {} : { references: request.references }
7747
- }, retryId);
7748
- runner?.nudge();
7749
- return sendJson2(res, 200, { ok: true });
7750
- } catch {
7751
- return sendJson2(res, 500, { ok: false, error: "The request could not be retried." });
8189
+ } catch (error) {
8190
+ if (!closed)
8191
+ change({ checkError: error instanceof Error ? error.message : "Could not reach npm." });
8192
+ } finally {
8193
+ if (state.phase.status === "checking")
8194
+ change({ phase: { status: "idle" } });
8195
+ checking = null;
7752
8196
  }
8197
+ return status();
8198
+ })();
8199
+ return checking;
8200
+ },
8201
+ async skip(version2) {
8202
+ if (state.latest?.version !== version2)
8203
+ throw new Error("That is not the newest version.");
8204
+ change({ skipped: version2 });
8205
+ await persist();
8206
+ return status();
8207
+ },
8208
+ async update() {
8209
+ if (closed)
8210
+ throw new Error("Updates are not available here.");
8211
+ if (inFlight())
8212
+ throw new Error("An update is already running.");
8213
+ if (!available())
8214
+ throw new Error("You have the newest version.");
8215
+ if (install.kind === "source")
8216
+ throw new Error(CHECKOUT_NOTICE);
8217
+ if (busy())
8218
+ throw new Error("A change is running. Wait for it to finish.");
8219
+ if (restart === null)
8220
+ throw new Error("Updates are not available here.");
8221
+ const version2 = state.latest.version;
8222
+ const handoff = restart;
8223
+ change({ phase: { status: "installing", version: version2 } });
8224
+ pending = setImmediate(() => {
8225
+ pending = null;
8226
+ void performUpdate(version2, handoff);
7753
8227
  });
8228
+ return status();
8229
+ },
8230
+ notice() {
8231
+ const { latest, skipped } = state;
8232
+ if (!available() || latest === null || latest.version === skipped)
8233
+ return null;
8234
+ const title = latest.title === null ? "" : `: ${latest.title}`;
8235
+ const next = install.kind === "source" ? CHECKOUT_NOTICE : install.kind === "npx" ? `Update from the interface, or start Leglas again with ${install.command}` : `Update from the interface, or run ${install.command}`;
8236
+ return `update ${latest.version} is out, you have ${input.version}${title}
8237
+ ${next}`;
8238
+ },
8239
+ onRestart(handler) {
8240
+ restart = handler;
8241
+ },
8242
+ onBusy(handler) {
8243
+ busy = handler;
8244
+ },
8245
+ setPort(value) {
8246
+ port = value;
8247
+ },
8248
+ onChange(listener) {
8249
+ listeners.add(listener);
8250
+ },
8251
+ close() {
8252
+ if (closing !== null)
8253
+ return closing;
8254
+ closed = true;
8255
+ if (pending !== null)
8256
+ clearImmediate(pending);
8257
+ if (waiting !== null)
8258
+ clearTimeout(waiting);
8259
+ resume?.();
8260
+ listeners.clear();
8261
+ installer?.stop();
8262
+ closing = installer?.gone ?? Promise.resolve();
8263
+ return closing;
8264
+ }
8265
+ };
8266
+ }
8267
+
8268
+ // src/args.ts
8269
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--port", "--user-port", "--config"]);
8270
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--no-open", "--json"]);
8271
+ var MIN_SHOW_WIDTH = 320;
8272
+ var MAX_SHOW_WIDTH = 3840;
8273
+ function parsePort(flag, raw) {
8274
+ if (!/^\d+$/.test(raw)) {
8275
+ return { error: `${flag} needs a number, received ${JSON.stringify(raw)}.` };
8276
+ }
8277
+ const port = Number(raw);
8278
+ if (port < 1 || port > 65535) {
8279
+ return { error: `${flag} must be between 1 and 65535, received ${port}.` };
8280
+ }
8281
+ return port;
8282
+ }
8283
+ function parseNew(rest) {
8284
+ let surface;
8285
+ let print = false;
8286
+ let json = false;
8287
+ let from;
8288
+ for (let index = 0; index < rest.length; index += 1) {
8289
+ const argument = rest[index];
8290
+ if (argument === "--from" || argument.startsWith("--from=")) {
8291
+ from = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
8292
+ if (from === void 0 || from === "") {
8293
+ return { kind: "error", message: "--from needs a path, for example --from src/Hero.tsx" };
8294
+ }
8295
+ continue;
8296
+ }
8297
+ if (argument === "--print") {
8298
+ print = true;
8299
+ continue;
8300
+ }
8301
+ if (argument === "--json") {
8302
+ json = true;
8303
+ continue;
8304
+ }
8305
+ if (argument === "--help" || argument === "-h") return { kind: "help" };
8306
+ if (argument.startsWith("-")) {
8307
+ return { kind: "error", message: `leglas new does not take ${argument}.` };
8308
+ }
8309
+ if (surface !== void 0) {
8310
+ return { kind: "error", message: `leglas new takes one surface name, received ${JSON.stringify(argument)} as well.` };
8311
+ }
8312
+ surface = argument;
8313
+ }
8314
+ if (surface === void 0) {
8315
+ return {
8316
+ kind: "error",
8317
+ message: "leglas new needs a surface name, for example: npx leglas new hero"
8318
+ };
8319
+ }
8320
+ return { kind: "new", surface, print, json, from };
8321
+ }
8322
+ function parseAdd(rest) {
8323
+ let title;
8324
+ let url;
8325
+ let note;
8326
+ let branch;
8327
+ let file;
8328
+ let basedOn;
8329
+ let askedFor;
8330
+ const tags = [];
8331
+ let json = false;
8332
+ for (let index = 0; index < rest.length; index += 1) {
8333
+ const argument = rest[index];
8334
+ if (argument === "--json") {
8335
+ json = true;
8336
+ continue;
8337
+ }
8338
+ if (argument === "--help" || argument === "-h") return { kind: "help" };
8339
+ const equals = argument.indexOf("=");
8340
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
8341
+ let value;
8342
+ if (equals === -1) {
8343
+ value = rest[index + 1];
8344
+ index += 1;
8345
+ } else {
8346
+ value = argument.slice(equals + 1);
8347
+ }
8348
+ if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on", "--asked-for"].includes(
8349
+ flag
8350
+ )) {
8351
+ return { kind: "error", message: `leglas add does not take ${flag}.` };
8352
+ }
8353
+ if (value === void 0 || value === "") {
8354
+ return { kind: "error", message: `${flag} needs a value.` };
8355
+ }
8356
+ if (flag === "--title") title = value;
8357
+ else if (flag === "--url") url = value;
8358
+ else if (flag === "--note") note = value;
8359
+ else if (flag === "--branch") branch = value;
8360
+ else if (flag === "--file") file = value;
8361
+ else if (flag === "--based-on") basedOn = value;
8362
+ else if (flag === "--asked-for") askedFor = value;
8363
+ else tags.push(value);
8364
+ }
8365
+ if (title === void 0) {
8366
+ return { kind: "error", message: "leglas add needs --title, which is how the preview is identified." };
8367
+ }
8368
+ if (url === void 0 && file === void 0) {
8369
+ return {
8370
+ kind: "error",
8371
+ message: "leglas add needs --url (for example --url '/?v-hero=aurora') or --file for a page Leglas serves itself."
8372
+ };
8373
+ }
8374
+ return {
8375
+ kind: "add",
8376
+ preview: {
8377
+ title,
8378
+ url,
8379
+ note,
8380
+ tags: tags.length > 0 ? tags : void 0,
8381
+ branch,
8382
+ file,
8383
+ basedOn,
8384
+ askedFor
8385
+ },
8386
+ json
8387
+ };
8388
+ }
8389
+ function parseClassify(rest) {
8390
+ const changes = [];
8391
+ let json = false;
8392
+ for (let index = 0; index < rest.length; index += 1) {
8393
+ const argument = rest[index];
8394
+ if (argument === "--json") {
8395
+ json = true;
8396
+ continue;
8397
+ }
8398
+ if (argument === "--help" || argument === "-h") return { kind: "help" };
8399
+ const equals = argument.indexOf("=");
8400
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
8401
+ if (flag !== "--change" && flag !== "--rewrite") {
8402
+ return { kind: "error", message: `leglas classify does not take ${argument}.` };
8403
+ }
8404
+ const value = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
8405
+ if (value === void 0 || value === "") {
8406
+ return { kind: "error", message: `${flag} needs a path, for example ${flag} package.json` };
8407
+ }
8408
+ changes.push({ path: value, kind: flag === "--change" ? "change" : "rewrite" });
8409
+ }
8410
+ if (changes.length === 0) {
8411
+ return {
8412
+ kind: "error",
8413
+ message: "leglas classify needs what the direction will touch, for example: npx leglas classify --change package.json --rewrite src/theme.css"
8414
+ };
8415
+ }
8416
+ return { kind: "classify", changes, json };
8417
+ }
8418
+ function parseWatch(rest) {
8419
+ let run4;
8420
+ let port;
8421
+ for (let index = 0; index < rest.length; index += 1) {
8422
+ const argument = rest[index];
8423
+ if (argument === "--help" || argument === "-h") return { kind: "help" };
8424
+ const equals = argument.indexOf("=");
8425
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
8426
+ if (flag !== "--run" && flag !== "--port") {
8427
+ return { kind: "error", message: `leglas watch does not take ${argument}.` };
7754
8428
  }
7755
- if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
7756
- return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
8429
+ const value = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
8430
+ if (value === void 0 || value === "") {
8431
+ return {
8432
+ kind: "error",
8433
+ message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' : "--port needs a value."
8434
+ };
7757
8435
  }
7758
- if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
7759
- if (!hasJsonBody(req)) {
7760
- return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
7761
- }
7762
- let body = "";
7763
- req.on("data", (chunk) => body += chunk);
7764
- return void req.on("end", async () => {
7765
- const parsed2 = jsonBody(body);
7766
- if (parsed2 === null) {
7767
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7768
- }
7769
- if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
7770
- return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
7771
- }
7772
- const anchor = anchorFrom(parsed2.anchor);
7773
- if (anchor === null) {
7774
- return sendJson2(res, 400, { ok: false, error: "A note needs something to point at." });
7775
- }
7776
- try {
7777
- const annotation = await addAnnotation(cwd, {
7778
- anchor,
7779
- note: typeof parsed2.note === "string" ? parsed2.note.trim() : "",
7780
- title: parsed2.title
7781
- });
7782
- return sendJson2(res, 200, { ok: true, annotation });
7783
- } catch {
7784
- return sendJson2(res, 500, { ok: false, error: "The note could not be kept." });
7785
- }
7786
- });
8436
+ if (flag === "--run") {
8437
+ run4 = value;
8438
+ continue;
7787
8439
  }
7788
- if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
7789
- if (!hasJsonBody(req)) {
7790
- return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
8440
+ const parsed2 = parsePort(flag, value);
8441
+ if (typeof parsed2 !== "number") return { kind: "error", message: parsed2.error };
8442
+ port = parsed2;
8443
+ }
8444
+ return { kind: "watch", run: run4, port };
8445
+ }
8446
+ function parseArgs(argv) {
8447
+ if (argv[0] === "new") return parseNew(argv.slice(1));
8448
+ if (argv[0] === "watch") return parseWatch(argv.slice(1));
8449
+ if (argv[0] === "add") return parseAdd(argv.slice(1));
8450
+ if (argv[0] === "classify") return parseClassify(argv.slice(1));
8451
+ if (argv[0] === "init") {
8452
+ const rest = argv.slice(1);
8453
+ const unknown = rest.find((argument) => argument !== "--force" && argument !== "--json");
8454
+ if (unknown !== void 0) {
8455
+ return { kind: "error", message: `leglas init does not take ${unknown}.` };
8456
+ }
8457
+ return { kind: "init", force: rest.includes("--force"), json: rest.includes("--json") };
8458
+ }
8459
+ if (argv[0] === "keep") {
8460
+ const rest = argv.slice(1);
8461
+ let title;
8462
+ let to;
8463
+ let json = false;
8464
+ for (let index = 0; index < rest.length; index += 1) {
8465
+ const argument = rest[index];
8466
+ if (argument === "--json") {
8467
+ json = true;
8468
+ continue;
7791
8469
  }
7792
- let body = "";
7793
- req.on("data", (chunk) => body += chunk);
7794
- return void req.on("end", async () => {
7795
- const parsed2 = jsonBody(body);
7796
- if (parsed2 === null) {
7797
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7798
- }
7799
- if (typeof parsed2.id !== "string" || parsed2.id === "") {
7800
- return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
7801
- }
7802
- if (typeof parsed2.note !== "string") {
7803
- return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
7804
- }
7805
- try {
7806
- const annotation = await updateAnnotation(cwd, parsed2.id, parsed2.note);
7807
- if (annotation === null) {
7808
- return sendJson2(res, 404, { ok: false, error: "That note has gone." });
7809
- }
7810
- return sendJson2(res, 200, { ok: true, annotation });
7811
- } catch {
7812
- return sendJson2(res, 500, { ok: false, error: "The note could not be reworded." });
8470
+ if (argument === "--to" || argument.startsWith("--to=")) {
8471
+ to = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
8472
+ if (to === void 0 || to === "") {
8473
+ return { kind: "error", message: "--to needs a path, for example --to src/components/hero.tsx" };
7813
8474
  }
7814
- });
8475
+ continue;
8476
+ }
8477
+ if (argument.startsWith("-")) {
8478
+ return { kind: "error", message: `leglas keep does not take ${argument}.` };
8479
+ }
8480
+ if (title !== void 0) {
8481
+ return { kind: "error", message: "leglas keep takes one direction title." };
8482
+ }
8483
+ title = argument;
7815
8484
  }
7816
- if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
7817
- if (!hasJsonBody(req)) {
7818
- return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
8485
+ if (title === void 0) {
8486
+ return {
8487
+ kind: "error",
8488
+ message: 'leglas keep needs a direction title, for example: npx leglas keep "Aurora" --to src/components/hero.tsx'
8489
+ };
8490
+ }
8491
+ if (to === void 0) {
8492
+ return { kind: "error", message: "leglas keep needs --to, the path the winner should live at." };
8493
+ }
8494
+ return { kind: "keep", title, to, json };
8495
+ }
8496
+ if (argv[0] === "explore") {
8497
+ const rest = argv.slice(1);
8498
+ let surface;
8499
+ let count = 3;
8500
+ let basedOn = null;
8501
+ let json = false;
8502
+ for (let index = 0; index < rest.length; index += 1) {
8503
+ const argument = rest[index];
8504
+ if (argument === "--json") {
8505
+ json = true;
8506
+ continue;
7819
8507
  }
7820
- let body = "";
7821
- req.on("data", (chunk) => body += chunk);
7822
- return void req.on("end", async () => {
7823
- const parsed2 = jsonBody(body);
7824
- if (parsed2 === null) {
7825
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7826
- }
7827
- const ids = Array.isArray(parsed2.ids) ? parsed2.ids.filter((entry) => typeof entry === "string") : [];
7828
- if (ids.length === 0) {
7829
- return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
8508
+ if (argument === "--count" || argument.startsWith("--count=")) {
8509
+ const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
8510
+ if (raw === void 0 || !/^\d+$/.test(raw)) {
8511
+ return { kind: "error", message: "--count needs a number, for example --count 6." };
7830
8512
  }
7831
- try {
7832
- return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
7833
- } catch {
7834
- return sendJson2(res, 500, { ok: false, error: "The notes could not be forgotten." });
8513
+ count = Number(raw);
8514
+ continue;
8515
+ }
8516
+ if (argument === "--based-on" || argument.startsWith("--based-on=")) {
8517
+ const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
8518
+ if (raw === void 0 || raw === "") {
8519
+ return {
8520
+ kind: "error",
8521
+ message: '--based-on needs a direction title, for example --based-on "Aurora".'
8522
+ };
7835
8523
  }
7836
- });
8524
+ basedOn = raw;
8525
+ continue;
8526
+ }
8527
+ if (argument.startsWith("-")) {
8528
+ return { kind: "error", message: `leglas explore does not take ${argument}.` };
8529
+ }
8530
+ if (surface !== void 0) {
8531
+ return { kind: "error", message: "leglas explore takes one surface name." };
8532
+ }
8533
+ surface = argument;
7837
8534
  }
7838
- if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
7839
- if (!hasJsonBody(req)) {
7840
- return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
8535
+ if (surface === void 0) {
8536
+ return {
8537
+ kind: "error",
8538
+ message: "leglas explore needs a surface name, for example: npx leglas explore hero --count 6"
8539
+ };
8540
+ }
8541
+ return { kind: "explore", surface, count, basedOn, json };
8542
+ }
8543
+ if (argv[0] === "requests") {
8544
+ const rest = argv.slice(1);
8545
+ const unknown = rest.find((argument) => argument !== "--json" && argument !== "--clear");
8546
+ if (unknown !== void 0) {
8547
+ return { kind: "error", message: `leglas requests does not take ${unknown}.` };
8548
+ }
8549
+ return { kind: "requests", json: rest.includes("--json"), clear: rest.includes("--clear") };
8550
+ }
8551
+ if (argv[0] === "log") {
8552
+ const rest = argv.slice(1);
8553
+ const flags = rest.filter((argument) => argument.startsWith("--"));
8554
+ const unknown = flags.find((flag) => flag !== "--json");
8555
+ if (unknown !== void 0) {
8556
+ return { kind: "error", message: `leglas log does not take ${unknown}.` };
8557
+ }
8558
+ const names = rest.filter((argument) => !argument.startsWith("--"));
8559
+ if (names.length > 1) {
8560
+ return { kind: "error", message: "leglas log takes one entry at most." };
8561
+ }
8562
+ return { kind: "log", entry: names[0] ?? null, json: flags.includes("--json") };
8563
+ }
8564
+ if (argv[0] === "list") {
8565
+ const rest = argv.slice(1);
8566
+ const unknown = rest.find((argument) => argument !== "--json");
8567
+ if (unknown !== void 0) {
8568
+ return { kind: "error", message: `leglas list does not take ${unknown}.` };
8569
+ }
8570
+ return { kind: "list", json: rest.includes("--json") };
8571
+ }
8572
+ if (argv[0] === "show") {
8573
+ const rest = argv.slice(1);
8574
+ let title;
8575
+ let json = false;
8576
+ let screenshot = false;
8577
+ let width = null;
8578
+ let port = null;
8579
+ for (let index = 0; index < rest.length; index += 1) {
8580
+ const argument = rest[index];
8581
+ if (argument === "--json") {
8582
+ json = true;
8583
+ continue;
7841
8584
  }
7842
- let body = "";
7843
- req.on("data", (chunk) => body += chunk);
7844
- return void req.on("end", async () => {
7845
- const parsed2 = jsonBody(body);
7846
- if (parsed2 === null) {
7847
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7848
- }
7849
- if (typeof parsed2.id !== "string") {
7850
- return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
7851
- }
7852
- const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
7853
- if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
7854
- return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
8585
+ if (argument === "--screenshot") {
8586
+ screenshot = true;
8587
+ continue;
8588
+ }
8589
+ if (argument === "--width" || argument.startsWith("--width=") || argument === "--port" || argument.startsWith("--port=")) {
8590
+ const equals = argument.indexOf("=");
8591
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
8592
+ const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
8593
+ if (raw === void 0 || raw === "") {
8594
+ return { kind: "error", message: `${flag} needs a value.` };
7855
8595
  }
7856
- try {
7857
- if (!await removeRequest(cwd, parsed2.id)) {
7858
- return sendJson2(res, 404, { ok: false, error: "No such request." });
7859
- }
7860
- return sendJson2(res, 200, { ok: true });
7861
- } catch {
7862
- return sendJson2(res, 500, { ok: false, error: "The request could not be dismissed." });
8596
+ if (flag === "--port") {
8597
+ const parsed2 = parsePort(flag, raw);
8598
+ if (typeof parsed2 !== "number") return { kind: "error", message: parsed2.error };
8599
+ port = parsed2;
8600
+ continue;
7863
8601
  }
7864
- });
7865
- }
7866
- if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
7867
- let body = "";
7868
- req.on("data", (chunk) => body += chunk);
7869
- return void req.on("end", () => {
7870
- const parsed2 = jsonBody(body);
7871
- if (parsed2 === null) {
7872
- return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8602
+ if (!/^\d+$/.test(raw)) {
8603
+ return { kind: "error", message: `--width needs a number, received ${JSON.stringify(raw)}.` };
7873
8604
  }
7874
- if (parsed2.renames === null || typeof parsed2.renames !== "object") {
7875
- return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
8605
+ width = Number(raw);
8606
+ if (width < MIN_SHOW_WIDTH || width > MAX_SHOW_WIDTH) {
8607
+ return {
8608
+ kind: "error",
8609
+ message: `--width must be between ${MIN_SHOW_WIDTH} and ${MAX_SHOW_WIDTH}, received ${width}.`
8610
+ };
7876
8611
  }
7877
- const renames = Object.fromEntries(Object.entries(parsed2.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
7878
- void writeRenames(cwd, renames).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 200, { ok: false }));
7879
- });
7880
- }
7881
- if (path === `${LEGLAS_PREFIX}/api/health`) {
7882
- return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
7883
- }
7884
- if (path.startsWith(`${FILES_PREFIX}/`)) {
7885
- const rest = path.slice(FILES_PREFIX.length + 1);
7886
- const slash = rest.indexOf("/");
7887
- const slug = slash === -1 ? rest : rest.slice(0, slash);
7888
- let relative6 = slash === -1 ? "" : rest.slice(slash + 1);
7889
- try {
7890
- relative6 = decodeURIComponent(relative6);
7891
- } catch {
7892
- relative6 = "";
8612
+ continue;
7893
8613
  }
7894
- const dir = fileMounts.get(slug);
7895
- const serveMount = () => {
7896
- if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
7897
- return;
7898
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
7899
- res.end("Leglas: no such preview file.");
7900
- };
7901
- if (!context.remote)
7902
- return serveMount();
7903
- if (relative6.split("/").some((segment) => segment.startsWith("."))) {
7904
- return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
8614
+ if (argument.startsWith("-")) {
8615
+ return { kind: "error", message: `leglas show does not take ${argument}.` };
7905
8616
  }
7906
- return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
7907
- if (!allowed)
7908
- return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
7909
- serveMount();
7910
- });
8617
+ if (title !== void 0) {
8618
+ return { kind: "error", message: "leglas show takes one direction title." };
8619
+ }
8620
+ title = argument;
7911
8621
  }
7912
- if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
7913
- return sendJson2(res, 404, { error: "No such Leglas API path." });
8622
+ if (title === void 0) {
8623
+ return {
8624
+ kind: "error",
8625
+ message: 'leglas show needs a direction title, for example: npx leglas show "Aurora" --json'
8626
+ };
7914
8627
  }
7915
- if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
7916
- if (shellDir !== null && serveShellFile(res, shellDir, path))
7917
- return;
7918
- if (shellDir !== null) {
7919
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
7920
- res.end("Leglas: no such path.");
7921
- return;
7922
- }
7923
- res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
7924
- res.end(PLACEHOLDER);
7925
- return;
8628
+ if (width !== null && !screenshot) {
8629
+ return { kind: "error", message: "leglas show --width needs --screenshot." };
7926
8630
  }
7927
- return proxy.request(req, res, context.publicOrigin);
7928
- };
7929
- let port = 0;
7930
- const server = http4.createServer((req, res) => handleRequest(req, res, {
7931
- remote: false,
7932
- publicOrigin: `http://localhost:${port}`
7933
- }));
7934
- const sockets = /* @__PURE__ */ new Set();
7935
- server.on("connection", (socket) => {
7936
- sockets.add(socket);
7937
- socket.once("close", () => sockets.delete(socket));
7938
- });
7939
- const handleUpgrade = (req, socket, head, context) => {
7940
- const liveUpgrade = context.remote ? live.upgrade(req, socket, head, { viewer: true }) : live.upgrade(req, socket, head);
7941
- if (liveUpgrade)
7942
- return true;
7943
- const path = (req.url ?? "/").split("?")[0] ?? "/";
7944
- if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
7945
- socket.destroy();
7946
- return false;
8631
+ if (port !== null && !screenshot) {
8632
+ return { kind: "error", message: "leglas show --port needs --screenshot." };
7947
8633
  }
7948
- proxy.upgrade(req, socket, head);
7949
- return false;
8634
+ return { kind: "show", title, json, screenshot, width, port };
8635
+ }
8636
+ const options = {
8637
+ port: void 0,
8638
+ userPort: void 0,
8639
+ configPath: void 0,
8640
+ open: true,
8641
+ json: false
7950
8642
  };
7951
- server.on("upgrade", (req, socket, head) => {
7952
- handleUpgrade(req, socket, head, { remote: false });
7953
- });
7954
- shares = createShareManager({
7955
- live,
7956
- previews: livePreviewDefinitions,
7957
- previewsForConfig,
7958
- viewerConfig: {
7959
- project,
7960
- devServer: target,
7961
- scanPreviews: config?.scanPreviews ?? true
7962
- },
7963
- request: (req, res, context) => handleRequest(req, res, {
7964
- remote: true,
7965
- publicOrigin: context.publicOrigin,
7966
- grantId: context.grantId
7967
- }),
7968
- upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
7969
- ...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
7970
- ...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
7971
- });
7972
- port = await bind2(server, options.port ?? DEFAULT_PORT);
7973
- const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
7974
- liveHealth = watchHealth(target, live);
7975
- await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
7976
- });
7977
- await writeServerInfo(cwd, {
7978
- port,
7979
- url: `http://localhost:${port}`,
7980
- pid: process.pid
7981
- }).catch(() => {
7982
- });
7983
- runner = startRunner({
7984
- cwd,
7985
- externallyAttached,
7986
- onChange: () => live.nudge("requests"),
7987
- leglasCommand,
7988
- ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
7989
- ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
7990
- });
7991
- let closePromise = null;
7992
- return {
7993
- port,
7994
- url: `http://localhost:${port}`,
7995
- close: () => {
7996
- if (closePromise !== null)
7997
- return closePromise;
7998
- closePromise = (async () => {
7999
- liveFiles.close();
8000
- liveHealth?.close();
8001
- await Promise.all([
8002
- shares?.close() ?? Promise.resolve(),
8003
- branches.stop(),
8004
- runner.stop(),
8005
- browserPool.close(),
8006
- live.close()
8007
- ]);
8008
- await new Promise((done) => {
8009
- for (const socket of sockets)
8010
- socket.destroy();
8011
- sockets.clear();
8012
- server.closeAllConnections();
8013
- server.close(() => done());
8014
- });
8015
- await removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
8016
- });
8017
- })();
8018
- return closePromise;
8643
+ for (let index = 0; index < argv.length; index += 1) {
8644
+ const argument = argv[index];
8645
+ if (argument === "--help" || argument === "-h") return { kind: "help" };
8646
+ if (argument === "--version" || argument === "-v") return { kind: "version" };
8647
+ if (BOOLEAN_FLAGS.has(argument)) {
8648
+ if (argument === "--no-open") options.open = false;
8649
+ if (argument === "--json") options.json = true;
8650
+ continue;
8019
8651
  }
8020
- };
8652
+ const equals = argument.indexOf("=");
8653
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
8654
+ if (!VALUE_FLAGS.has(flag)) {
8655
+ return {
8656
+ kind: "error",
8657
+ message: argument.startsWith("-") ? `Unknown flag ${argument}. Run leglas --help to see the options.` : `Unexpected argument ${JSON.stringify(argument)}. leglas takes flags only.`
8658
+ };
8659
+ }
8660
+ let value;
8661
+ if (equals === -1) {
8662
+ value = argv[index + 1];
8663
+ index += 1;
8664
+ } else {
8665
+ value = argument.slice(equals + 1);
8666
+ }
8667
+ if (value === void 0 || value === "" || value.startsWith("--")) {
8668
+ return { kind: "error", message: `${flag} needs a value.` };
8669
+ }
8670
+ if (flag === "--config") {
8671
+ options.configPath = value;
8672
+ continue;
8673
+ }
8674
+ const port = parsePort(flag, value);
8675
+ if (typeof port !== "number") return { kind: "error", message: port.error };
8676
+ if (flag === "--port") options.port = port;
8677
+ else options.userPort = port;
8678
+ }
8679
+ return { kind: "run", options };
8021
8680
  }
8022
8681
 
8023
8682
  // src/run-classify.ts
8683
+ import { stat as stat4 } from "fs/promises";
8684
+ import { join as join14 } from "path";
8024
8685
  async function runClassify(options, deps) {
8025
8686
  const declared = await Promise.all(
8026
8687
  options.changes.map(async (change) => ({
8027
8688
  ...change,
8028
- exists: await stat4(join13(options.cwd, change.path)).then(
8689
+ exists: await stat4(join14(options.cwd, change.path)).then(
8029
8690
  () => true,
8030
8691
  () => false
8031
8692
  )
@@ -8292,8 +8953,8 @@ function runExplore(options, deps) {
8292
8953
  }
8293
8954
 
8294
8955
  // src/run-init.ts
8295
- import { readFile as readFile12, writeFile as writeFile9 } from "fs/promises";
8296
- import { join as join14 } from "path";
8956
+ import { readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
8957
+ import { join as join15 } from "path";
8297
8958
 
8298
8959
  // src/init.ts
8299
8960
  var AGENTS_MARKER_START = "<!-- leglas:start -->";
@@ -8462,18 +9123,18 @@ async function readIfPresent(path) {
8462
9123
  async function runInit(options, deps) {
8463
9124
  const existingConfig = findConfigFile(options.cwd);
8464
9125
  const plan = planInit({
8465
- agents: await readIfPresent(join14(options.cwd, "AGENTS.md")),
9126
+ agents: await readIfPresent(join15(options.cwd, "AGENTS.md")),
8466
9127
  config: existingConfig === null ? null : "present",
8467
- gitignore: await readIfPresent(join14(options.cwd, ".gitignore")),
9128
+ gitignore: await readIfPresent(join15(options.cwd, ".gitignore")),
8468
9129
  force: options.force
8469
9130
  });
8470
9131
  const touched = [];
8471
9132
  for (const write2 of plan.writes) {
8472
- await writeFile9(join14(options.cwd, write2.path), write2.contents, "utf8");
9133
+ await writeFile10(join15(options.cwd, write2.path), write2.contents, "utf8");
8473
9134
  touched.push(write2.path);
8474
9135
  }
8475
9136
  if (plan.gitignore !== null) {
8476
- await writeFile9(join14(options.cwd, ".gitignore"), plan.gitignore, "utf8");
9137
+ await writeFile10(join15(options.cwd, ".gitignore"), plan.gitignore, "utf8");
8477
9138
  touched.push(".gitignore");
8478
9139
  }
8479
9140
  if (options.json) {
@@ -8492,9 +9153,9 @@ async function runInit(options, deps) {
8492
9153
  }
8493
9154
 
8494
9155
  // src/run-keep.ts
8495
- import { existsSync as existsSync4 } from "fs";
8496
- import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
8497
- import { dirname as dirname9, join as join15 } from "path";
9156
+ import { existsSync as existsSync5 } from "fs";
9157
+ import { copyFile as copyFile2, mkdir as mkdir10, readFile as readFile13, rm as rm6, writeFile as writeFile11 } from "fs/promises";
9158
+ import { dirname as dirname10, join as join16 } from "path";
8498
9159
 
8499
9160
  // src/keep.ts
8500
9161
  import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
@@ -8577,7 +9238,7 @@ function renameExport(source, to) {
8577
9238
  );
8578
9239
  }
8579
9240
  async function writeLogEntry(options) {
8580
- const entry = composeEntry({
9241
+ const entry2 = composeEntry({
8581
9242
  surface: options.surface,
8582
9243
  won: options.won,
8583
9244
  previews: options.previews,
@@ -8585,19 +9246,19 @@ async function writeLogEntry(options) {
8585
9246
  annotations: await readAnnotations(options.cwd),
8586
9247
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
8587
9248
  });
8588
- const dir = join15(options.cwd, options.logDir);
8589
- await mkdir9(dir, { recursive: true });
8590
- const file = join15(dir, `${entry.slug}.md`);
8591
- await writeFile10(file, entry.markdown, "utf8");
8592
- if (entry.pictures.length > 0) {
8593
- const pictureDir = join15(dir, entry.slug);
8594
- await mkdir9(pictureDir, { recursive: true });
8595
- for (const picture of entry.pictures) {
8596
- await copyFile2(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
9249
+ const dir = join16(options.cwd, options.logDir);
9250
+ await mkdir10(dir, { recursive: true });
9251
+ const file = join16(dir, `${entry2.slug}.md`);
9252
+ await writeFile11(file, entry2.markdown, "utf8");
9253
+ if (entry2.pictures.length > 0) {
9254
+ const pictureDir = join16(dir, entry2.slug);
9255
+ await mkdir10(pictureDir, { recursive: true });
9256
+ for (const picture of entry2.pictures) {
9257
+ await copyFile2(join16(options.cwd, picture.from), join16(pictureDir, picture.to)).catch(() => {
8597
9258
  });
8598
9259
  }
8599
9260
  }
8600
- return `${options.logDir}/${entry.slug}.md`;
9261
+ return `${options.logDir}/${entry2.slug}.md`;
8601
9262
  }
8602
9263
  async function runKeep(options, deps) {
8603
9264
  const loaded = await loadConfig(options.cwd);
@@ -8616,17 +9277,17 @@ async function runKeep(options, deps) {
8616
9277
  if (!resolved.ok) return fail(resolved.error);
8617
9278
  const plan = planKeep({ title: resolved.title, previews, to: options.to });
8618
9279
  if (!plan.ok) return fail(plan.error);
8619
- const from = join15(options.cwd, plan.move.from);
8620
- const to = join15(options.cwd, plan.move.to);
8621
- if (!existsSync4(from)) {
9280
+ const from = join16(options.cwd, plan.move.from);
9281
+ const to = join16(options.cwd, plan.move.to);
9282
+ if (!existsSync5(from)) {
8622
9283
  return fail(`${plan.move.from} does not exist. Nothing to keep.`);
8623
9284
  }
8624
- if (existsSync4(to)) {
9285
+ if (existsSync5(to)) {
8625
9286
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
8626
9287
  }
8627
9288
  const source = await readFile13(from, "utf8");
8628
- await mkdir9(dirname9(to), { recursive: true });
8629
- await writeFile10(to, renameExport(source, plan.exportName), "utf8");
9289
+ await mkdir10(dirname10(to), { recursive: true });
9290
+ await writeFile11(to, renameExport(source, plan.exportName), "utf8");
8630
9291
  const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
8631
9292
  let logged = null;
8632
9293
  let logError = null;
@@ -8641,7 +9302,7 @@ async function runKeep(options, deps) {
8641
9302
  } catch (error) {
8642
9303
  logError = error instanceof Error ? error.message : String(error);
8643
9304
  }
8644
- await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
9305
+ await rm6(join16(options.cwd, plan.removeDir), { recursive: true, force: true });
8645
9306
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
8646
9307
  if (options.json) {
8647
9308
  deps.log(
@@ -8679,9 +9340,9 @@ async function runKeep(options, deps) {
8679
9340
  }
8680
9341
 
8681
9342
  // src/run-new.ts
8682
- import { existsSync as existsSync5 } from "fs";
8683
- import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8684
- import { dirname as dirname10, join as join16 } from "path";
9343
+ import { existsSync as existsSync6 } from "fs";
9344
+ import { mkdir as mkdir11, readFile as readFile14, writeFile as writeFile12 } from "fs/promises";
9345
+ import { dirname as dirname11, join as join17 } from "path";
8685
9346
  async function readIfPresent2(path) {
8686
9347
  try {
8687
9348
  return await readFile14(path, "utf8");
@@ -8692,7 +9353,7 @@ async function readIfPresent2(path) {
8692
9353
  async function runNew(options, deps) {
8693
9354
  let from;
8694
9355
  if (options.from !== void 0) {
8695
- const contents = await readIfPresent2(join16(options.cwd, options.from));
9356
+ const contents = await readIfPresent2(join17(options.cwd, options.from));
8696
9357
  if (contents === null) {
8697
9358
  const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
8698
9359
  if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
@@ -8703,8 +9364,8 @@ async function runNew(options, deps) {
8703
9364
  }
8704
9365
  const plan = planNew({
8705
9366
  surface: options.surface,
8706
- packageJson: await readIfPresent2(join16(options.cwd, "package.json")),
8707
- gitignore: await readIfPresent2(join16(options.cwd, ".gitignore")),
9367
+ packageJson: await readIfPresent2(join17(options.cwd, "package.json")),
9368
+ gitignore: await readIfPresent2(join17(options.cwd, ".gitignore")),
8708
9369
  from
8709
9370
  });
8710
9371
  const fail = (error) => {
@@ -8727,19 +9388,19 @@ async function runNew(options, deps) {
8727
9388
  deps.log(plan.instructions);
8728
9389
  return { exitCode: 0, written: [] };
8729
9390
  }
8730
- const existing = plan.writes.filter((write2) => existsSync5(join16(options.cwd, write2.path)));
9391
+ const existing = plan.writes.filter((write2) => existsSync6(join17(options.cwd, write2.path)));
8731
9392
  if (existing.length > 0) {
8732
9393
  return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
8733
9394
  }
8734
9395
  const written = [];
8735
9396
  for (const write2 of plan.writes) {
8736
- const target = join16(options.cwd, write2.path);
8737
- await mkdir10(dirname10(target), { recursive: true });
8738
- await writeFile11(target, write2.contents, "utf8");
9397
+ const target = join17(options.cwd, write2.path);
9398
+ await mkdir11(dirname11(target), { recursive: true });
9399
+ await writeFile12(target, write2.contents, "utf8");
8739
9400
  written.push(write2.path);
8740
9401
  }
8741
9402
  if (plan.gitignore !== null) {
8742
- await writeFile11(join16(options.cwd, ".gitignore"), plan.gitignore, "utf8");
9403
+ await writeFile12(join17(options.cwd, ".gitignore"), plan.gitignore, "utf8");
8743
9404
  written.push(".gitignore");
8744
9405
  }
8745
9406
  if (options.json) {
@@ -8761,7 +9422,7 @@ async function runNew(options, deps) {
8761
9422
 
8762
9423
  // src/run-log.ts
8763
9424
  import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
8764
- import { join as join17 } from "path";
9425
+ import { join as join18 } from "path";
8765
9426
  function headline(markdown) {
8766
9427
  const first = markdown.split("\n", 1)[0] ?? "";
8767
9428
  return first.replace(/^#\s*/, "").trim();
@@ -8771,7 +9432,7 @@ async function runLog(options, deps) {
8771
9432
  const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
8772
9433
  let names;
8773
9434
  try {
8774
- names = (await readdir4(join17(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
9435
+ names = (await readdir4(join18(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
8775
9436
  } catch {
8776
9437
  names = [];
8777
9438
  }
@@ -8784,7 +9445,7 @@ async function runLog(options, deps) {
8784
9445
  else deps.error(error);
8785
9446
  return { exitCode: 1 };
8786
9447
  }
8787
- const markdown = await readFile15(join17(options.cwd, dir, found), "utf8");
9448
+ const markdown = await readFile15(join18(options.cwd, dir, found), "utf8");
8788
9449
  if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
8789
9450
  else deps.log(markdown.trimEnd());
8790
9451
  return { exitCode: 0 };
@@ -8792,7 +9453,7 @@ async function runLog(options, deps) {
8792
9453
  const entries = await Promise.all(
8793
9454
  names.map(async (name) => ({
8794
9455
  entry: name.replace(/\.md$/, ""),
8795
- title: headline(await readFile15(join17(options.cwd, dir, name), "utf8")),
9456
+ title: headline(await readFile15(join18(options.cwd, dir, name), "utf8")),
8796
9457
  file: `${dir}/${name}`
8797
9458
  }))
8798
9459
  );
@@ -8804,19 +9465,19 @@ async function runLog(options, deps) {
8804
9465
  deps.log(` No decisions recorded yet. One is written each time you run leglas keep.`);
8805
9466
  return { exitCode: 0 };
8806
9467
  }
8807
- const width = Math.max(...entries.map((entry) => entry.entry.length));
8808
- for (const entry of entries) deps.log(` ${entry.entry.padEnd(width)} ${entry.title}`);
9468
+ const width = Math.max(...entries.map((entry2) => entry2.entry.length));
9469
+ for (const entry2 of entries) deps.log(` ${entry2.entry.padEnd(width)} ${entry2.title}`);
8809
9470
  return { exitCode: 0 };
8810
9471
  }
8811
9472
 
8812
9473
  // src/run-previews.ts
8813
- import { readFile as readFile16, writeFile as writeFile12 } from "fs/promises";
8814
- import { join as join18 } from "path";
9474
+ import { readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
9475
+ import { join as join19 } from "path";
8815
9476
  function envelope(deps, ok, body) {
8816
9477
  deps.log(JSON.stringify({ ok, ...body }));
8817
9478
  }
8818
9479
  async function ensureIgnored(cwd) {
8819
- const path = join18(cwd, ".gitignore");
9480
+ const path = join19(cwd, ".gitignore");
8820
9481
  let current = null;
8821
9482
  try {
8822
9483
  current = await readFile16(path, "utf8");
@@ -8824,7 +9485,7 @@ async function ensureIgnored(cwd) {
8824
9485
  current = null;
8825
9486
  }
8826
9487
  const next = ignoreEntry(current);
8827
- if (next !== null) await writeFile12(path, next, "utf8");
9488
+ if (next !== null) await writeFile13(path, next, "utf8");
8828
9489
  }
8829
9490
  async function runAdd(options, deps) {
8830
9491
  const loaded = await loadConfig(options.cwd);
@@ -9156,12 +9817,12 @@ async function runShow(options, deps) {
9156
9817
 
9157
9818
  // src/run-watch.ts
9158
9819
  import { spawn as spawn3 } from "child_process";
9159
- import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile13 } from "fs/promises";
9160
- import { dirname as dirname11, join as join19 } from "path";
9820
+ import { mkdir as mkdir12, readFile as readFile17, writeFile as writeFile14 } from "fs/promises";
9821
+ import { dirname as dirname12, join as join20 } from "path";
9161
9822
  var POLL_MS2 = 2e3;
9162
9823
  var HEARTBEAT_TIMEOUT_MS = 1e3;
9163
9824
  async function saveTemplate(cwd, run4) {
9164
- const path = join19(cwd, WATCH_PATH);
9825
+ const path = join20(cwd, WATCH_PATH);
9165
9826
  let config = {};
9166
9827
  try {
9167
9828
  const parsed2 = JSON.parse(await readFile17(path, "utf8"));
@@ -9171,8 +9832,8 @@ async function saveTemplate(cwd, run4) {
9171
9832
  } catch {
9172
9833
  }
9173
9834
  config.run = run4;
9174
- await mkdir11(dirname11(path), { recursive: true });
9175
- await writeFile13(path, `${JSON.stringify(config, null, 2)}
9835
+ await mkdir12(dirname12(path), { recursive: true });
9836
+ await writeFile14(path, `${JSON.stringify(config, null, 2)}
9176
9837
  `, "utf8");
9177
9838
  }
9178
9839
  function spawnAgent(command, args, cwd) {
@@ -9310,10 +9971,10 @@ async function runWatch(options, deps) {
9310
9971
  }
9311
9972
 
9312
9973
  // src/run.ts
9313
- import { existsSync as existsSync6 } from "fs";
9974
+ import { existsSync as existsSync7 } from "fs";
9314
9975
  import { realpath as realpath4 } from "fs/promises";
9315
9976
  import { createRequire } from "module";
9316
- import { basename as basename6, dirname as dirname12, join as join20, relative as relative5, resolve as resolve4 } from "path";
9977
+ import { basename as basename6, dirname as dirname13, join as join21, relative as relative5, resolve as resolve4 } from "path";
9317
9978
  import { fileURLToPath } from "url";
9318
9979
 
9319
9980
  // src/dev-server-owner.ts
@@ -9399,11 +10060,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
9399
10060
 
9400
10061
  // src/run.ts
9401
10062
  function findShellDir() {
9402
- const bundled = join20(dirname12(fileURLToPath(import.meta.url)), "shell");
9403
- if (existsSync6(join20(bundled, "index.html"))) return bundled;
10063
+ const bundled = join21(dirname13(fileURLToPath(import.meta.url)), "shell");
10064
+ if (existsSync7(join21(bundled, "index.html"))) return bundled;
9404
10065
  try {
9405
10066
  const require2 = createRequire(import.meta.url);
9406
- return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
10067
+ return dirname13(require2.resolve("@leglas/shell/dist/index.html"));
9407
10068
  } catch {
9408
10069
  return null;
9409
10070
  }
@@ -9414,9 +10075,13 @@ function shellWord(value) {
9414
10075
  return `'${value.replaceAll("'", `'\\''`)}'`;
9415
10076
  }
9416
10077
  function embeddedLeglasCommand() {
9417
- const entry = join20(dirname12(fileURLToPath(import.meta.url)), "bin.js");
9418
- if (!existsSync6(entry)) return "npx -y leglas";
9419
- return [process.execPath, entry].map(shellWord).join(" ");
10078
+ const entry2 = join21(dirname13(fileURLToPath(import.meta.url)), "bin.js");
10079
+ if (!existsSync7(entry2)) return "npx -y leglas";
10080
+ return [process.execPath, entry2].map(shellWord).join(" ");
10081
+ }
10082
+ function skipStartupCheck(env) {
10083
+ const off = (value) => value === void 0 || value === "" || value === "0" || value === "false";
10084
+ return env.CI !== void 0 && env.CI !== "" && env.CI !== "false" || !off(env.LEGLAS_NO_UPDATE_CHECK);
9420
10085
  }
9421
10086
  async function run3(options, deps) {
9422
10087
  const loaded = await loadConfig(options.cwd);
@@ -9446,8 +10111,8 @@ async function run3(options, deps) {
9446
10111
  const fileMounts = /* @__PURE__ */ new Map();
9447
10112
  for (const preview of merged?.previews ?? []) {
9448
10113
  if (preview.file !== void 0) {
9449
- const absolute = join20(options.cwd, preview.file);
9450
- if (!existsSync6(absolute)) {
10114
+ const absolute = join21(options.cwd, preview.file);
10115
+ if (!existsSync7(absolute)) {
9451
10116
  previewErrors.push(
9452
10117
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
9453
10118
  );
@@ -9457,7 +10122,7 @@ async function run3(options, deps) {
9457
10122
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
9458
10123
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
9459
10124
  }
9460
- fileMounts.set(slug, dirname12(absolute));
10125
+ fileMounts.set(slug, dirname13(absolute));
9461
10126
  previews.push({
9462
10127
  ...preview,
9463
10128
  url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
@@ -9468,8 +10133,8 @@ async function run3(options, deps) {
9468
10133
  }
9469
10134
  const config = merged === null ? null : { ...merged, previews };
9470
10135
  const configWarnings = [];
9471
- const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
9472
- () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
10136
+ const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname13(loaded.path)).catch(
10137
+ () => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path))
9473
10138
  );
9474
10139
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
9475
10140
  const serverPromise = startServer({
@@ -9483,6 +10148,7 @@ async function run3(options, deps) {
9483
10148
  project: loaded.path ?? options.cwd,
9484
10149
  cwd: options.cwd,
9485
10150
  leglasCommand: embeddedLeglasCommand(),
10151
+ ...deps.updates === void 0 ? {} : { updates: deps.updates },
9486
10152
  ...options.port === void 0 ? {} : { port: options.port }
9487
10153
  });
9488
10154
  const [server, warning] = await Promise.all([serverPromise, ownerWarning]);
@@ -9532,12 +10198,26 @@ async function run3(options, deps) {
9532
10198
  }
9533
10199
  }
9534
10200
  if (options.open) await deps.open(url);
10201
+ let stopped = false;
10202
+ let updateTimer = null;
10203
+ if (!options.json && deps.updates !== void 0 && !skipStartupCheck(process.env)) {
10204
+ void deps.updates.check().then(() => {
10205
+ if (stopped) return;
10206
+ const line = deps.updates?.notice();
10207
+ if (line !== null && line !== void 0) deps.log(line);
10208
+ });
10209
+ const updates2 = deps.updates;
10210
+ updateTimer = setInterval(() => void updates2.check(), 60 * 6e4);
10211
+ updateTimer.unref?.();
10212
+ }
9535
10213
  return {
9536
10214
  exitCode: 0,
9537
10215
  url,
9538
10216
  devServer,
9539
10217
  previewCount,
9540
10218
  stop: async () => {
10219
+ stopped = true;
10220
+ if (updateTimer !== null) clearInterval(updateTimer);
9541
10221
  await app?.stop().catch(() => {
9542
10222
  });
9543
10223
  await server.close();
@@ -9557,6 +10237,61 @@ function installShutdown(stop, target = process) {
9557
10237
  return shutdown;
9558
10238
  }
9559
10239
 
10240
+ // src/restart.ts
10241
+ function createHandoff() {
10242
+ let transferred = false;
10243
+ function handedOff2() {
10244
+ return transferred;
10245
+ }
10246
+ async function handOff2(command, stop, deps) {
10247
+ transferred = true;
10248
+ let child = null;
10249
+ let cancelled = false;
10250
+ let exited = false;
10251
+ const listeners = SHUTDOWN_SIGNALS.map((signal) => {
10252
+ const listener = () => {
10253
+ if (child === null) cancelled = true;
10254
+ else child.kill(signal);
10255
+ };
10256
+ deps.target.on(signal, listener);
10257
+ return { signal, listener };
10258
+ });
10259
+ const cleanup = () => {
10260
+ for (const { signal, listener } of listeners) deps.target.off(signal, listener);
10261
+ };
10262
+ const exit = (code) => {
10263
+ if (exited) return;
10264
+ exited = true;
10265
+ cleanup();
10266
+ deps.exit(code);
10267
+ };
10268
+ try {
10269
+ await stop();
10270
+ } catch (error) {
10271
+ if (cancelled) return exit(0);
10272
+ cleanup();
10273
+ transferred = false;
10274
+ throw error;
10275
+ }
10276
+ if (cancelled) return exit(0);
10277
+ const failed = (error) => {
10278
+ if (exited) return;
10279
+ const message2 = (error instanceof Error ? error.message : String(error)).replace(/[.!?]+$/, "");
10280
+ process.stderr.write(`Could not start Leglas again: ${message2}. Start it from your terminal.
10281
+ `);
10282
+ exit(1);
10283
+ };
10284
+ try {
10285
+ child = deps.spawn(command.file, command.args, { stdio: "inherit", shell: command.shell });
10286
+ child.once("error", failed);
10287
+ child.once("exit", (code) => exit(code ?? 1));
10288
+ } catch (error) {
10289
+ failed(error);
10290
+ }
10291
+ }
10292
+ return { handOff: handOff2, handedOff: handedOff2 };
10293
+ }
10294
+
9560
10295
  // src/bin.ts
9561
10296
  var HELP = `leglas - compare design directions inside your own running app
9562
10297
 
@@ -9739,12 +10474,32 @@ if (parsed.kind === "new") {
9739
10474
  );
9740
10475
  process.exit(outcome.exitCode);
9741
10476
  }
10477
+ var entry = fileURLToPath2(import.meta.url);
10478
+ try {
10479
+ entry = realpathSync2(entry);
10480
+ } catch {
10481
+ }
10482
+ var updates = createUpdateService({
10483
+ version: version(),
10484
+ entry,
10485
+ argv: process.argv,
10486
+ cwd: process.cwd(),
10487
+ deps: { log: (line) => (parsed.options.json ? process.stderr : process.stdout).write(`${line}
10488
+ `) }
10489
+ });
9742
10490
  var result = await run3(
9743
10491
  { ...parsed.options, cwd: process.cwd() },
9744
10492
  { open: openBrowser, log: (line) => process.stdout.write(`${line}
9745
- `) }
10493
+ `), updates }
9746
10494
  );
10495
+ var { handOff, handedOff } = createHandoff();
10496
+ updates.onRestart((command) => handOff(command, result.stop, {
10497
+ spawn: spawn4,
10498
+ exit: (code) => process.exit(code),
10499
+ target: process
10500
+ }));
9747
10501
  installShutdown(async () => {
10502
+ if (handedOff()) return;
9748
10503
  await result.stop();
9749
10504
  process.exit(0);
9750
10505
  });