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/README.md +7 -1
- package/dist/bin.js +2384 -1629
- package/dist/index.d.ts +1 -1
- package/dist/index.js +143 -65
- package/dist/restart.d.ts +13 -0
- package/dist/run.d.ts +5 -0
- package/dist/shell/assets/index-BzaBzGEv.css +1 -0
- package/dist/shell/assets/index-CKmxj5gW.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-BCvek69-.css +0 -1
- package/dist/shell/assets/index-BjY8sKEQ.js +0 -14
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
|
-
//
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(``);
|
|
50
|
+
lines2.push("");
|
|
43
51
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
if (preview.basedOn !== void 0) {
|
|
53
|
+
lines2.push(`A variant of ${preview.basedOn}.`);
|
|
54
|
+
lines2.push("");
|
|
47
55
|
}
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
83
|
+
return { slug, markdown: `${lines2.join("\n").trimEnd()}
|
|
84
|
+
`, pictures };
|
|
60
85
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
151
|
+
seenTitles.add(title);
|
|
86
152
|
}
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
228
|
+
errors: []
|
|
126
229
|
};
|
|
127
230
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
254
|
+
if (/\s/.test(character)) {
|
|
255
|
+
if (started)
|
|
256
|
+
tokens.push(current);
|
|
257
|
+
current = "";
|
|
258
|
+
started = false;
|
|
259
|
+
continue;
|
|
146
260
|
}
|
|
147
|
-
|
|
261
|
+
current += character;
|
|
262
|
+
started = true;
|
|
148
263
|
}
|
|
149
|
-
if (
|
|
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
|
-
|
|
267
|
+
if (started)
|
|
268
|
+
tokens.push(current);
|
|
269
|
+
return { ok: true, tokens };
|
|
156
270
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
446
|
-
|
|
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(``);
|
|
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
|
-
//
|
|
881
|
-
//
|
|
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((
|
|
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((
|
|
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((
|
|
973
|
-
const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((
|
|
974
|
-
for (const
|
|
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(
|
|
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)
|
|
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 ? "
|
|
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((
|
|
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(
|
|
1637
|
-
const digits = /(\d+)\s*$/.exec(
|
|
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, (
|
|
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((
|
|
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((
|
|
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((
|
|
1703
|
-
const root = join4(playwrightRoot,
|
|
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((
|
|
1720
|
-
const root = join4(kindRoot,
|
|
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((
|
|
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
|
|
1848
|
-
return { createdAt:
|
|
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((
|
|
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
|
|
2782
|
-
return
|
|
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((
|
|
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((
|
|
2816
|
-
const file = join5(references,
|
|
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((
|
|
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((
|
|
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((
|
|
3216
|
-
tag: text(
|
|
3217
|
-
text: text(
|
|
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((
|
|
3246
|
-
if (!isRecord2(
|
|
2842
|
+
return parsed2.annotations.flatMap((entry2, index) => {
|
|
2843
|
+
if (!isRecord2(entry2))
|
|
3247
2844
|
return [];
|
|
3248
|
-
const anchor = anchorFrom(
|
|
3249
|
-
const title = text(
|
|
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
|
|
3256
|
-
note: text(
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
3509
|
-
if (typeof
|
|
3105
|
+
const entry2 = value;
|
|
3106
|
+
if (typeof entry2.message !== "string" || entry2.message === "")
|
|
3510
3107
|
return null;
|
|
3511
|
-
if (
|
|
3108
|
+
if (entry2.code === void 0 || !FAILURE_CODES.includes(entry2.code))
|
|
3512
3109
|
return null;
|
|
3513
|
-
return { code:
|
|
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, ...
|
|
3524
|
-
const status =
|
|
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
|
|
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
|
-
...
|
|
3127
|
+
...entry2,
|
|
3531
3128
|
id,
|
|
3532
3129
|
status,
|
|
3533
|
-
mode:
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
7081
|
-
const bootConfig = config?.previews ?? [];
|
|
7082
|
-
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((
|
|
7083
|
-
const known = new Set(boot.map((
|
|
7084
|
-
const fresh = local.filter((
|
|
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 (
|
|
7113
|
-
return void
|
|
7114
|
-
if (
|
|
7115
|
-
|
|
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
|
-
|
|
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 (
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
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 (
|
|
7128
|
-
|
|
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 (
|
|
7316
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
7131
7317
|
if (!hasJsonBody(req)) {
|
|
7132
|
-
return sendJson2(res,
|
|
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",
|
|
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
|
-
|
|
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
|
|
7330
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed2.id) ?? false });
|
|
7150
7331
|
});
|
|
7151
7332
|
}
|
|
7152
|
-
if (
|
|
7333
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
7153
7334
|
if (!hasJsonBody(req)) {
|
|
7154
|
-
return sendJson2(res, 400, { ok: false, error: "
|
|
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
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
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 (
|
|
7171
|
-
return void
|
|
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/
|
|
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: "
|
|
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
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
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
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
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/
|
|
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
|
-
|
|
7270
|
-
|
|
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
|
-
|
|
7329
|
-
|
|
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
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
}
|
|
7343
|
-
return sendJson2(res, 200, {
|
|
7344
|
-
|
|
7345
|
-
|
|
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/
|
|
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
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
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
|
|
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,
|
|
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/
|
|
7468
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
7463
7469
|
if (!hasJsonBody(req)) {
|
|
7464
|
-
return sendJson2(res, 400, { ok: false, error: "
|
|
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.
|
|
7474
|
-
return sendJson2(res, 400, { ok: false, error: "
|
|
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
|
|
7484
|
-
|
|
7485
|
-
|
|
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
|
-
|
|
7507
|
-
|
|
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
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
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/
|
|
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 (
|
|
7588
|
-
return sendJson2(res, 400, { ok: false, error: "Body needs a
|
|
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
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
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
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
}
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
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
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
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
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
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
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
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
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
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
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
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
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
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
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
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
|
-
|
|
7756
|
-
|
|
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 (
|
|
7759
|
-
|
|
7760
|
-
|
|
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
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
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
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
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 (
|
|
7817
|
-
|
|
7818
|
-
|
|
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
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
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
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
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 (
|
|
7839
|
-
|
|
7840
|
-
|
|
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
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
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
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
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
|
-
|
|
7875
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7895
|
-
|
|
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
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
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 (
|
|
7913
|
-
return
|
|
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 (
|
|
7916
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7949
|
-
|
|
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
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
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(
|
|
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
|
|
8296
|
-
import { join as
|
|
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(
|
|
9126
|
+
agents: await readIfPresent(join15(options.cwd, "AGENTS.md")),
|
|
8466
9127
|
config: existingConfig === null ? null : "present",
|
|
8467
|
-
gitignore: await readIfPresent(
|
|
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
|
|
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
|
|
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
|
|
8496
|
-
import { copyFile as copyFile2, mkdir as
|
|
8497
|
-
import { dirname as
|
|
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
|
|
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 =
|
|
8589
|
-
await
|
|
8590
|
-
const file =
|
|
8591
|
-
await
|
|
8592
|
-
if (
|
|
8593
|
-
const pictureDir =
|
|
8594
|
-
await
|
|
8595
|
-
for (const picture of
|
|
8596
|
-
await copyFile2(
|
|
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}/${
|
|
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 =
|
|
8620
|
-
const to =
|
|
8621
|
-
if (!
|
|
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 (
|
|
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
|
|
8629
|
-
await
|
|
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
|
|
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
|
|
8683
|
-
import { mkdir as
|
|
8684
|
-
import { dirname as
|
|
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(
|
|
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(
|
|
8707
|
-
gitignore: await readIfPresent2(
|
|
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) =>
|
|
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 =
|
|
8737
|
-
await
|
|
8738
|
-
await
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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((
|
|
8808
|
-
for (const
|
|
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
|
|
8814
|
-
import { join as
|
|
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 =
|
|
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
|
|
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
|
|
9160
|
-
import { dirname as
|
|
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 =
|
|
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
|
|
9175
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
9403
|
-
if (
|
|
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
|
|
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
|
|
9418
|
-
if (!
|
|
9419
|
-
return [process.execPath,
|
|
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 =
|
|
9450
|
-
if (!
|
|
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,
|
|
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 :
|
|
9472
|
-
() => resolve4(loaded.path === null ? options.cwd :
|
|
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
|
});
|