leglas 0.4.0 → 0.5.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 +27 -9
- package/dist/args.d.ts +2 -0
- package/dist/bin.js +797 -147
- package/dist/index.d.ts +1 -1
- package/dist/index.js +796 -147
- package/dist/shell/assets/index-B7_ncDLZ.js +14 -0
- package/dist/shell/assets/index-f2TzxtQm.css +1 -0
- package/dist/shell/index.html +2 -2
- package/package.json +3 -3
- package/dist/shell/assets/index-BQHDjn0q.css +0 -1
- package/dist/shell/assets/index-T-uR7kT7.js +0 -14
package/dist/bin.js
CHANGED
|
@@ -63,6 +63,7 @@ function parseAdd(rest) {
|
|
|
63
63
|
let branch;
|
|
64
64
|
let file;
|
|
65
65
|
let basedOn;
|
|
66
|
+
let askedFor;
|
|
66
67
|
const tags = [];
|
|
67
68
|
let json = false;
|
|
68
69
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -81,7 +82,9 @@ function parseAdd(rest) {
|
|
|
81
82
|
} else {
|
|
82
83
|
value = argument.slice(equals + 1);
|
|
83
84
|
}
|
|
84
|
-
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on"].includes(
|
|
85
|
+
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on", "--asked-for"].includes(
|
|
86
|
+
flag
|
|
87
|
+
)) {
|
|
85
88
|
return { kind: "error", message: `leglas add does not take ${flag}.` };
|
|
86
89
|
}
|
|
87
90
|
if (value === void 0 || value === "") {
|
|
@@ -93,6 +96,7 @@ function parseAdd(rest) {
|
|
|
93
96
|
else if (flag === "--branch") branch = value;
|
|
94
97
|
else if (flag === "--file") file = value;
|
|
95
98
|
else if (flag === "--based-on") basedOn = value;
|
|
99
|
+
else if (flag === "--asked-for") askedFor = value;
|
|
96
100
|
else tags.push(value);
|
|
97
101
|
}
|
|
98
102
|
if (title === void 0) {
|
|
@@ -106,7 +110,16 @@ function parseAdd(rest) {
|
|
|
106
110
|
}
|
|
107
111
|
return {
|
|
108
112
|
kind: "add",
|
|
109
|
-
preview: {
|
|
113
|
+
preview: {
|
|
114
|
+
title,
|
|
115
|
+
url,
|
|
116
|
+
note,
|
|
117
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
118
|
+
branch,
|
|
119
|
+
file,
|
|
120
|
+
basedOn,
|
|
121
|
+
askedFor
|
|
122
|
+
},
|
|
110
123
|
json
|
|
111
124
|
};
|
|
112
125
|
}
|
|
@@ -353,7 +366,7 @@ function parseArgs(argv) {
|
|
|
353
366
|
|
|
354
367
|
// src/run-classify.ts
|
|
355
368
|
import { stat } from "fs/promises";
|
|
356
|
-
import { join as
|
|
369
|
+
import { join as join10 } from "path";
|
|
357
370
|
|
|
358
371
|
// ../server/dist/config.js
|
|
359
372
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
@@ -448,6 +461,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
448
461
|
if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
|
|
449
462
|
errors.push(`${at} has a basedOn that is not a direction title.`);
|
|
450
463
|
}
|
|
464
|
+
const askedFor = entry["askedFor"];
|
|
465
|
+
if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
|
|
466
|
+
errors.push(`${at} has an askedFor that is not a change request.`);
|
|
467
|
+
}
|
|
451
468
|
const tags = entry["tags"];
|
|
452
469
|
previews.push({
|
|
453
470
|
title: typeof title === "string" ? title : "",
|
|
@@ -456,7 +473,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
456
473
|
tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
|
|
457
474
|
...typeof branch === "string" ? { branch } : {},
|
|
458
475
|
...typeof file === "string" ? { file } : {},
|
|
459
|
-
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {}
|
|
476
|
+
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
|
|
477
|
+
...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
|
|
460
478
|
});
|
|
461
479
|
});
|
|
462
480
|
const devCommand = source["devCommand"];
|
|
@@ -572,6 +590,10 @@ import { spawn } from "child_process";
|
|
|
572
590
|
import { constants } from "fs";
|
|
573
591
|
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
574
592
|
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
593
|
+
var CODEX_WORKSPACE_CONFIG = [
|
|
594
|
+
"-c",
|
|
595
|
+
"sandbox_workspace_write.network_access=true"
|
|
596
|
+
];
|
|
575
597
|
var KNOWN_AGENTS = {
|
|
576
598
|
claude: {
|
|
577
599
|
name: "Claude",
|
|
@@ -602,6 +624,12 @@ var KNOWN_AGENTS = {
|
|
|
602
624
|
"--permission-mode",
|
|
603
625
|
"acceptEdits"
|
|
604
626
|
],
|
|
627
|
+
// Non-interactive Claude cannot approve a Bash call: acceptEdits covers
|
|
628
|
+
// files, so a command the prompt requires is refused every time with
|
|
629
|
+
// nobody there to say yes. This allows exactly that command and nothing
|
|
630
|
+
// wider. Codex needs no equivalent, because workspace-write already lets
|
|
631
|
+
// it run commands.
|
|
632
|
+
allowArgs: (command) => ["--allowedTools", `Bash(${command} *)`],
|
|
605
633
|
// Every stream-json event names its session.
|
|
606
634
|
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
607
635
|
authArgs: ["auth", "status"],
|
|
@@ -622,15 +650,41 @@ var KNOWN_AGENTS = {
|
|
|
622
650
|
codex: {
|
|
623
651
|
name: "Codex",
|
|
624
652
|
binary: "codex",
|
|
625
|
-
|
|
626
|
-
|
|
653
|
+
// `--skip-git-repo-check` is what lets Codex run at all in a project the
|
|
654
|
+
// user never put under version control: without it codex-cli refuses
|
|
655
|
+
// before it reaches a model, with "Not inside a trusted directory and
|
|
656
|
+
// --skip-git-repo-check was not specified", and every Codex request in a
|
|
657
|
+
// non-git project fails for a reason nothing in Leglas explained. The flag
|
|
658
|
+
// moves that precondition and only that: `-s workspace-write` still
|
|
659
|
+
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
660
|
+
// and in a git repository the flag does nothing at all.
|
|
661
|
+
args: (prompt) => [
|
|
662
|
+
"exec",
|
|
663
|
+
"--json",
|
|
664
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
665
|
+
"-s",
|
|
666
|
+
"workspace-write",
|
|
667
|
+
"--skip-git-repo-check",
|
|
668
|
+
prompt
|
|
669
|
+
],
|
|
670
|
+
terminalArgs: (prompt) => [
|
|
671
|
+
"exec",
|
|
672
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
673
|
+
"-s",
|
|
674
|
+
"workspace-write",
|
|
675
|
+
"--skip-git-repo-check",
|
|
676
|
+
prompt
|
|
677
|
+
],
|
|
627
678
|
// No sandbox flag here: `codex exec resume` refuses it and inherits the
|
|
628
|
-
// session's own sandbox, which the first turn set to workspace-write.
|
|
679
|
+
// session's own sandbox, which the first turn set to workspace-write. The
|
|
680
|
+
// repository check is per invocation, so resume needs the flag of its own.
|
|
629
681
|
resumeArgs: (sessionId, prompt) => [
|
|
630
682
|
"exec",
|
|
631
683
|
"resume",
|
|
632
684
|
sessionId,
|
|
633
685
|
"--json",
|
|
686
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
687
|
+
"--skip-git-repo-check",
|
|
634
688
|
prompt
|
|
635
689
|
],
|
|
636
690
|
sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
|
|
@@ -656,7 +710,7 @@ var KNOWN_AGENTS = {
|
|
|
656
710
|
}
|
|
657
711
|
};
|
|
658
712
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
659
|
-
function execProbe(binary, args) {
|
|
713
|
+
function execProbe(binary, args, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
660
714
|
return new Promise((resolve) => {
|
|
661
715
|
let child;
|
|
662
716
|
try {
|
|
@@ -669,7 +723,10 @@ function execProbe(binary, args) {
|
|
|
669
723
|
if (stdout.length < 4096)
|
|
670
724
|
stdout += chunk.toString();
|
|
671
725
|
});
|
|
672
|
-
const deadline = setTimeout(() =>
|
|
726
|
+
const deadline = setTimeout(() => {
|
|
727
|
+
child.kill("SIGKILL");
|
|
728
|
+
resolve(null);
|
|
729
|
+
}, timeoutMs);
|
|
673
730
|
child.once("error", () => {
|
|
674
731
|
clearTimeout(deadline);
|
|
675
732
|
resolve(null);
|
|
@@ -738,10 +795,10 @@ function shownCommand(value) {
|
|
|
738
795
|
function claudeActivity(event, cwd) {
|
|
739
796
|
if (event.type !== "assistant")
|
|
740
797
|
return null;
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
798
|
+
const message2 = record(event.message);
|
|
799
|
+
if (message2 === null || !Array.isArray(message2.content))
|
|
743
800
|
return null;
|
|
744
|
-
for (const rawBlock of
|
|
801
|
+
for (const rawBlock of message2.content) {
|
|
745
802
|
const block = record(rawBlock);
|
|
746
803
|
if (block?.type !== "tool_use" || typeof block.name !== "string")
|
|
747
804
|
continue;
|
|
@@ -810,6 +867,25 @@ function sessionFrom(agent, line) {
|
|
|
810
867
|
return null;
|
|
811
868
|
return KNOWN_AGENTS[agent].sessionFrom(event);
|
|
812
869
|
}
|
|
870
|
+
function retryFrom(agent, line) {
|
|
871
|
+
if (agent !== "claude" && agent !== "cursor")
|
|
872
|
+
return null;
|
|
873
|
+
let event;
|
|
874
|
+
try {
|
|
875
|
+
event = record(JSON.parse(line));
|
|
876
|
+
} catch {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
if (event === null || event.type !== "system" || event.subtype !== "api_retry")
|
|
880
|
+
return null;
|
|
881
|
+
const attempt = typeof event.attempt === "number" ? event.attempt : 1;
|
|
882
|
+
return {
|
|
883
|
+
attempt,
|
|
884
|
+
max: typeof event.max_retries === "number" ? event.max_retries : null,
|
|
885
|
+
status: typeof event.error_status === "number" ? event.error_status : null,
|
|
886
|
+
reason: typeof event.error === "string" && event.error !== "" ? event.error.toLowerCase() : null
|
|
887
|
+
};
|
|
888
|
+
}
|
|
813
889
|
function isAgentChoice(value) {
|
|
814
890
|
return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
815
891
|
}
|
|
@@ -953,8 +1029,8 @@ async function loadConfig(cwd) {
|
|
|
953
1029
|
exported = module.default;
|
|
954
1030
|
}
|
|
955
1031
|
} catch (error) {
|
|
956
|
-
const
|
|
957
|
-
return { config: null, errors: [`${label} could not be loaded: ${
|
|
1032
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1033
|
+
return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
|
|
958
1034
|
}
|
|
959
1035
|
const result2 = normalizeConfig(exported);
|
|
960
1036
|
return {
|
|
@@ -973,8 +1049,17 @@ async function readLocalPreviews(cwd) {
|
|
|
973
1049
|
let raw;
|
|
974
1050
|
try {
|
|
975
1051
|
raw = await readFile3(path, "utf8");
|
|
976
|
-
} catch {
|
|
977
|
-
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
1054
|
+
if (code === "ENOENT") {
|
|
1055
|
+
return { previews: [], errors: [] };
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
previews: [],
|
|
1059
|
+
errors: [
|
|
1060
|
+
`${LOCAL_PREVIEWS_PATH} could not be read (${code ?? "unknown error"}). Check its permissions and file type; nothing shared is lost.`
|
|
1061
|
+
]
|
|
1062
|
+
};
|
|
978
1063
|
}
|
|
979
1064
|
let parsed2;
|
|
980
1065
|
try {
|
|
@@ -1015,7 +1100,8 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1015
1100
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1016
1101
|
...input.branch === void 0 ? {} : { branch: input.branch },
|
|
1017
1102
|
...input.file === void 0 ? {} : { file: input.file },
|
|
1018
|
-
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
|
|
1103
|
+
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
|
|
1104
|
+
...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
|
|
1019
1105
|
};
|
|
1020
1106
|
const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
|
|
1021
1107
|
if (check.config === null) {
|
|
@@ -1247,17 +1333,220 @@ async function startAppProcess(options) {
|
|
|
1247
1333
|
throw new Error(`${options.label} did not start within ${Math.round(readyTimeoutMs / 1e3)}s. Check that its dev command serves the port it is given.`);
|
|
1248
1334
|
}
|
|
1249
1335
|
|
|
1250
|
-
// ../server/dist/
|
|
1251
|
-
|
|
1336
|
+
// ../server/dist/failure.js
|
|
1337
|
+
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
1338
|
+
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
1339
|
+
var NOT_SIGNED_IN = /not logged in|not signed in|please (?:re-?)?(?:run|sign|log)\s*in|\/login\b|invalid api key|unauthorized|authentication_failed|\b401\b/i;
|
|
1340
|
+
var LIMIT = /\b429\b|rate limit|usage limit|quota exceeded|too many requests/i;
|
|
1341
|
+
var OVERLOADED = /\b(?:503|529)\b|overloaded|service unavailable/i;
|
|
1342
|
+
function fromStatus(status, reason) {
|
|
1343
|
+
if (status === 401 || status === 403 || reason === "authentication_failed")
|
|
1344
|
+
return "not-signed-in";
|
|
1345
|
+
if (status === 429 || reason === "rate_limit")
|
|
1346
|
+
return "provider-limit";
|
|
1347
|
+
if (status === 529 || status === 503 || reason === "overloaded")
|
|
1348
|
+
return "provider-overloaded";
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
function fromLines(lines) {
|
|
1352
|
+
for (const line of [...lines].reverse()) {
|
|
1353
|
+
if (NEEDS_TRUST.test(line))
|
|
1354
|
+
return "needs-trust";
|
|
1355
|
+
if (NOT_SIGNED_IN.test(line))
|
|
1356
|
+
return "not-signed-in";
|
|
1357
|
+
if (LIMIT.test(line))
|
|
1358
|
+
return "provider-limit";
|
|
1359
|
+
if (OVERLOADED.test(line))
|
|
1360
|
+
return "provider-overloaded";
|
|
1361
|
+
}
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
function attempts(retry) {
|
|
1365
|
+
if (retry === null || retry === void 0)
|
|
1366
|
+
return "";
|
|
1367
|
+
const total = retry.max === null ? retry.attempt : Math.max(retry.attempt, retry.max);
|
|
1368
|
+
return ` It retried ${total} times first.`;
|
|
1369
|
+
}
|
|
1370
|
+
function message(code, input) {
|
|
1371
|
+
const agent = input.agent;
|
|
1372
|
+
switch (code) {
|
|
1373
|
+
case "cancelled":
|
|
1374
|
+
return "You stopped this run.";
|
|
1375
|
+
case "stopped":
|
|
1376
|
+
return "Leglas shut down while this was running.";
|
|
1377
|
+
case "missing-agent":
|
|
1378
|
+
return `${agent} could not be started. Its command is not on this machine's PATH any more.`;
|
|
1379
|
+
case "not-signed-in":
|
|
1380
|
+
return `${agent} is not signed in. Sign in to it in a terminal, then run this again.`;
|
|
1381
|
+
case "provider-overloaded":
|
|
1382
|
+
return `${agent}'s provider was overloaded and gave up.${attempts(input.retry)}`;
|
|
1383
|
+
case "provider-limit":
|
|
1384
|
+
return `${agent} reported a rate or usage limit, so nothing ran.`;
|
|
1385
|
+
case "needs-trust":
|
|
1386
|
+
return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
|
|
1387
|
+
case "not-registered":
|
|
1388
|
+
return `${agent} finished without registering the new direction, so nothing reached the rail. Its last output is in the Leglas terminal.`;
|
|
1389
|
+
case "agent-error":
|
|
1390
|
+
return input.exitCode === null || input.exitCode === void 0 ? `${agent} stopped without finishing. Its last output is in the Leglas terminal.` : `${agent} exited with code ${input.exitCode}. Its last output is in the Leglas terminal.`;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function classifyFailure(input) {
|
|
1394
|
+
const lines = input.lines ?? [];
|
|
1395
|
+
const error = input.error ?? null;
|
|
1396
|
+
const code = error === "cancelled" ? "cancelled" : error === "not-registered" ? "not-registered" : error !== null && /^stopped by /.test(error) ? "stopped" : error !== null && MISSING_BINARY.test(error) ? "missing-agent" : (error !== null ? fromLines([error]) : null) ?? fromStatus(input.retry?.status ?? null, input.retry?.reason ?? null) ?? fromLines(lines) ?? "agent-error";
|
|
1397
|
+
return { code, message: message(code, input) };
|
|
1398
|
+
}
|
|
1399
|
+
function sessionShaped(code) {
|
|
1400
|
+
return code === "agent-error";
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// ../server/dist/annotations.js
|
|
1252
1404
|
import { randomBytes } from "crypto";
|
|
1405
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1253
1406
|
import { dirname as dirname4, join as join5 } from "path";
|
|
1407
|
+
var ANNOTATIONS_PATH = ".leglas/annotations.json";
|
|
1408
|
+
var NOTE_CAP = 500;
|
|
1409
|
+
var SELECTOR_CAP = 300;
|
|
1410
|
+
var TEXT_CAP = 120;
|
|
1411
|
+
var TAG_CAP = 40;
|
|
1412
|
+
var CLASS_CAP = 8;
|
|
1413
|
+
var CLASS_LENGTH_CAP = 60;
|
|
1414
|
+
var COVERS_CAP = 8;
|
|
1415
|
+
function isRecord2(value) {
|
|
1416
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1417
|
+
}
|
|
1418
|
+
function text(value, cap) {
|
|
1419
|
+
return typeof value === "string" ? value.trim().slice(0, cap) : "";
|
|
1420
|
+
}
|
|
1421
|
+
function fraction(value) {
|
|
1422
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1423
|
+
return 0.5;
|
|
1424
|
+
return Math.min(1, Math.max(0, value));
|
|
1425
|
+
}
|
|
1426
|
+
function size(value) {
|
|
1427
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
|
|
1428
|
+
}
|
|
1429
|
+
function anchorFrom(value) {
|
|
1430
|
+
if (!isRecord2(value))
|
|
1431
|
+
return null;
|
|
1432
|
+
const selector = text(value["selector"], SELECTOR_CAP);
|
|
1433
|
+
if (selector === "")
|
|
1434
|
+
return null;
|
|
1435
|
+
const rect = isRecord2(value["rect"]) ? value["rect"] : {};
|
|
1436
|
+
const classes = Array.isArray(value["classes"]) ? value["classes"].filter((entry) => typeof entry === "string").slice(0, CLASS_CAP).map((entry) => entry.slice(0, CLASS_LENGTH_CAP)) : [];
|
|
1437
|
+
const rawRegion = isRecord2(value["region"]) ? value["region"] : null;
|
|
1438
|
+
const region = rawRegion === null ? null : {
|
|
1439
|
+
height: fraction(rawRegion["height"]),
|
|
1440
|
+
width: fraction(rawRegion["width"]),
|
|
1441
|
+
x: fraction(rawRegion["x"]),
|
|
1442
|
+
y: fraction(rawRegion["y"])
|
|
1443
|
+
};
|
|
1444
|
+
const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
|
|
1445
|
+
tag: text(entry["tag"], TAG_CAP) || "element",
|
|
1446
|
+
text: text(entry["text"], TEXT_CAP)
|
|
1447
|
+
})) : [];
|
|
1448
|
+
return {
|
|
1449
|
+
classes,
|
|
1450
|
+
...covers.length === 0 ? {} : { covers },
|
|
1451
|
+
...region === null ? {} : { region },
|
|
1452
|
+
rect: {
|
|
1453
|
+
height: size(rect["height"]),
|
|
1454
|
+
width: size(rect["width"]),
|
|
1455
|
+
x: size(rect["x"]),
|
|
1456
|
+
y: size(rect["y"])
|
|
1457
|
+
},
|
|
1458
|
+
selector,
|
|
1459
|
+
spot: {
|
|
1460
|
+
x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
|
|
1461
|
+
y: fraction(isRecord2(value["spot"]) ? value["spot"]["y"] : void 0)
|
|
1462
|
+
},
|
|
1463
|
+
tag: text(value["tag"], TAG_CAP) || "element",
|
|
1464
|
+
text: text(value["text"], TEXT_CAP),
|
|
1465
|
+
viewport: size(value["viewport"])
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
async function readAnnotations(cwd) {
|
|
1469
|
+
try {
|
|
1470
|
+
const raw = await readFile4(join5(cwd, ANNOTATIONS_PATH), "utf8");
|
|
1471
|
+
const parsed2 = JSON.parse(raw);
|
|
1472
|
+
if (!Array.isArray(parsed2.annotations))
|
|
1473
|
+
return [];
|
|
1474
|
+
return parsed2.annotations.flatMap((entry, index) => {
|
|
1475
|
+
if (!isRecord2(entry))
|
|
1476
|
+
return [];
|
|
1477
|
+
const anchor = anchorFrom(entry["anchor"]);
|
|
1478
|
+
const title = text(entry["title"], TAG_CAP * 4);
|
|
1479
|
+
if (anchor === null || title === "")
|
|
1480
|
+
return [];
|
|
1481
|
+
return [
|
|
1482
|
+
{
|
|
1483
|
+
anchor,
|
|
1484
|
+
id: typeof entry["id"] === "string" ? entry["id"] : String(index),
|
|
1485
|
+
note: text(entry["note"], NOTE_CAP),
|
|
1486
|
+
title
|
|
1487
|
+
}
|
|
1488
|
+
];
|
|
1489
|
+
});
|
|
1490
|
+
} catch {
|
|
1491
|
+
return [];
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async function write(cwd, annotations) {
|
|
1495
|
+
const path = join5(cwd, ANNOTATIONS_PATH);
|
|
1496
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1497
|
+
await writeFile3(path, `${JSON.stringify({ annotations }, null, 2)}
|
|
1498
|
+
`, "utf8");
|
|
1499
|
+
}
|
|
1500
|
+
async function addAnnotation(cwd, input) {
|
|
1501
|
+
const annotation = { ...input, id: randomBytes(6).toString("base64url") };
|
|
1502
|
+
await write(cwd, [...await readAnnotations(cwd), annotation]);
|
|
1503
|
+
return annotation;
|
|
1504
|
+
}
|
|
1505
|
+
async function removeAnnotations(cwd, ids) {
|
|
1506
|
+
const wanted = new Set(ids);
|
|
1507
|
+
const annotations = await readAnnotations(cwd);
|
|
1508
|
+
const remaining = annotations.filter((entry) => !wanted.has(entry.id));
|
|
1509
|
+
const dropped = annotations.length - remaining.length;
|
|
1510
|
+
if (dropped > 0)
|
|
1511
|
+
await write(cwd, remaining);
|
|
1512
|
+
return dropped;
|
|
1513
|
+
}
|
|
1514
|
+
function annotationsFor(annotations, title) {
|
|
1515
|
+
return annotations.filter((entry) => entry.title === title);
|
|
1516
|
+
}
|
|
1517
|
+
function describeAnchor(anchor) {
|
|
1518
|
+
const where = `about ${anchor.rect.width}\xD7${anchor.rect.height} at (${anchor.rect.x}, ${anchor.rect.y}) in a ${anchor.viewport}px-wide viewport`;
|
|
1519
|
+
if (anchor.region !== void 0) {
|
|
1520
|
+
const covered = (anchor.covers ?? []).map((entry) => entry.text === "" ? `<${entry.tag}>` : `<${entry.tag}> \u201C${entry.text}\u201D`).join(", ");
|
|
1521
|
+
const inside = covered === "" ? "" : ` covering ${covered};`;
|
|
1522
|
+
return `an area inside <${anchor.tag}>;${inside} path ${anchor.selector}; ${where}`;
|
|
1523
|
+
}
|
|
1524
|
+
const parts = [`<${anchor.tag}>`];
|
|
1525
|
+
if (anchor.classes.length > 0)
|
|
1526
|
+
parts.push(`class "${anchor.classes.join(" ")}"`);
|
|
1527
|
+
if (anchor.text !== "")
|
|
1528
|
+
parts.push(`reading \u201C${anchor.text}\u201D`);
|
|
1529
|
+
return `${parts.join(", ")}; path ${anchor.selector}; ${where}`;
|
|
1530
|
+
}
|
|
1531
|
+
function describeAnnotations(annotations) {
|
|
1532
|
+
return annotations.map((annotation, index) => {
|
|
1533
|
+
const said = annotation.note === "" ? "Look at this." : annotation.note;
|
|
1534
|
+
return `${index + 1}. ${said}
|
|
1535
|
+
The element: ${describeAnchor(annotation.anchor)}`;
|
|
1536
|
+
}).join("\n\n");
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// ../server/dist/requests.js
|
|
1540
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1541
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1542
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1254
1543
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1255
|
-
function
|
|
1544
|
+
function variantSlot(url) {
|
|
1256
1545
|
if (!url.startsWith("/"))
|
|
1257
1546
|
return null;
|
|
1258
|
-
const query = url.slice(url.indexOf("?") + 1);
|
|
1259
1547
|
if (!url.includes("?"))
|
|
1260
1548
|
return null;
|
|
1549
|
+
const query = url.slice(url.indexOf("?") + 1);
|
|
1261
1550
|
for (const pair of query.split("&")) {
|
|
1262
1551
|
const [rawKey, rawValue] = pair.split("=");
|
|
1263
1552
|
if (rawKey === void 0 || rawValue === void 0)
|
|
@@ -1268,37 +1557,125 @@ function targetFor(url) {
|
|
|
1268
1557
|
const option = decodeURIComponent(rawValue);
|
|
1269
1558
|
if (!SAFE_SEGMENT.test(surface) || !SAFE_SEGMENT.test(option))
|
|
1270
1559
|
return null;
|
|
1271
|
-
return
|
|
1560
|
+
return { surface, option };
|
|
1272
1561
|
}
|
|
1273
1562
|
return null;
|
|
1274
1563
|
}
|
|
1275
|
-
function
|
|
1564
|
+
function targetFor(url) {
|
|
1565
|
+
const slot = variantSlot(url);
|
|
1566
|
+
return slot === null ? null : `.leglas/variants/${slot.surface}/${slot.option}.tsx`;
|
|
1567
|
+
}
|
|
1568
|
+
function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx -y leglas") {
|
|
1276
1569
|
const target = preview.file ?? targetFor(preview.url);
|
|
1277
1570
|
const cleaned = intent.trim();
|
|
1571
|
+
const asked = changeBlock(cleaned, notes);
|
|
1572
|
+
const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
|
|
1573
|
+
const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand) : replacePrompt(preview, asked, target);
|
|
1574
|
+
return { prompt, target, mode };
|
|
1575
|
+
}
|
|
1576
|
+
var ANCHORS = `Each path and rectangle was recorded when the note was left, against the design as it looked then. Trust the element's own words first, then its tag and classes, then the path, and treat the rectangle as a hint about where on the page to look rather than a fact.`;
|
|
1577
|
+
function changeBlock(cleaned, notes) {
|
|
1578
|
+
if (notes.length === 0)
|
|
1579
|
+
return `What to change: ${cleaned}`;
|
|
1580
|
+
const many = notes.length === 1 ? "a note" : `${notes.length} notes`;
|
|
1581
|
+
const lead = cleaned === "" ? `What to change, left as ${many} on the design itself:` : `What to change: ${cleaned}
|
|
1582
|
+
|
|
1583
|
+
And ${many} left on the design itself:`;
|
|
1584
|
+
return `${lead}
|
|
1585
|
+
|
|
1586
|
+
${describeAnnotations(notes)}
|
|
1587
|
+
|
|
1588
|
+
${ANCHORS}`;
|
|
1589
|
+
}
|
|
1590
|
+
var SCOPE = `This request came from the running Leglas interface. Request collection, direction discovery and the live-server check are already complete. Do not run Leglas explore, requests, list, show, help or version commands, do not inspect package caches, and do not start or restart the app or Leglas. Use the existing live preview if visual inspection is useful.
|
|
1591
|
+
|
|
1592
|
+
This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
|
|
1593
|
+
|
|
1594
|
+
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on.`;
|
|
1595
|
+
function registrationCommand(leglasCommand) {
|
|
1596
|
+
return `${leglasCommand} add`;
|
|
1597
|
+
}
|
|
1598
|
+
function replacePrompt(preview, asked, target) {
|
|
1278
1599
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1279
1600
|
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
1280
|
-
|
|
1601
|
+
return `In this project, change only the "${preview.title}" design direction. ${where}
|
|
1602
|
+
|
|
1603
|
+
${asked}
|
|
1281
1604
|
|
|
1282
|
-
|
|
1605
|
+
${pace}${SCOPE} The direction is already registered, so nothing needs re-registering.`;
|
|
1606
|
+
}
|
|
1607
|
+
function variantPrompt(preview, recorded, asked, target, leglasCommand) {
|
|
1608
|
+
const slot = variantSlot(preview.url);
|
|
1609
|
+
const parent = JSON.stringify(preview.title);
|
|
1610
|
+
const askedFor = JSON.stringify(recorded);
|
|
1611
|
+
const add = registrationCommand(leglasCommand);
|
|
1612
|
+
const source = target === null ? `Find what renders it first.` : `Its source is ${target}.`;
|
|
1613
|
+
const [make, register] = preview.file !== void 0 ? [
|
|
1614
|
+
`Copy that file to a new file beside it and make the change in the copy.`,
|
|
1615
|
+
` ${add} --title "<name>" --file "<the new file>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1616
|
+
] : slot !== null ? [
|
|
1617
|
+
`Copy that file to a new one in the same folder and make the change in the copy. The new file's name without its extension is its key, and that key has to be listed in the DIRECTIONS map in .leglas/variants/${slot.surface}/switch.tsx or its URL will not resolve.`,
|
|
1618
|
+
` ${add} --title "<name>" --url "/?v-${slot.surface}=<key>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1619
|
+
] : [
|
|
1620
|
+
`Copy its source rather than editing it, and make the change in the copy. Add the new direction the way this project already switches between them; if it has a Leglas branch point, that is the DIRECTIONS map in .leglas/variants/<surface>/switch.tsx.`,
|
|
1621
|
+
` ${add} --title "<name>" --url "<the URL that shows it>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1622
|
+
];
|
|
1623
|
+
return `In this project, add a new design direction based on the "${preview.title}" direction. Leave "${preview.title}" itself exactly as it is: it is the thing the new one will be compared against.
|
|
1283
1624
|
|
|
1284
|
-
${
|
|
1625
|
+
${source} ${make}
|
|
1285
1626
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1627
|
+
${asked}
|
|
1628
|
+
|
|
1629
|
+
Then register it, which is what puts it on the rail:
|
|
1630
|
+
|
|
1631
|
+
${register}
|
|
1632
|
+
|
|
1633
|
+
Name it for its idea rather than numbering it, and keep the name short enough to read in a narrow rail. Pass --asked-for exactly as given above; it is the user's own words and the interface shows them. Registering it is the last step; finish there.
|
|
1634
|
+
|
|
1635
|
+
${SCOPE}`;
|
|
1288
1636
|
}
|
|
1289
1637
|
var REQUESTS_PATH = ".leglas/requests.json";
|
|
1638
|
+
var TERMINAL = ["failed", "cancelled"];
|
|
1639
|
+
function isTerminal(status) {
|
|
1640
|
+
return TERMINAL.includes(status);
|
|
1641
|
+
}
|
|
1642
|
+
var FAILURE_CODES = [
|
|
1643
|
+
"cancelled",
|
|
1644
|
+
"stopped",
|
|
1645
|
+
"missing-agent",
|
|
1646
|
+
"not-signed-in",
|
|
1647
|
+
"provider-overloaded",
|
|
1648
|
+
"provider-limit",
|
|
1649
|
+
"needs-trust",
|
|
1650
|
+
"not-registered",
|
|
1651
|
+
"agent-error"
|
|
1652
|
+
];
|
|
1653
|
+
function failureOf(value) {
|
|
1654
|
+
if (typeof value !== "object" || value === null)
|
|
1655
|
+
return null;
|
|
1656
|
+
const entry = value;
|
|
1657
|
+
if (typeof entry.message !== "string" || entry.message === "")
|
|
1658
|
+
return null;
|
|
1659
|
+
if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
|
|
1660
|
+
return null;
|
|
1661
|
+
return { code: entry.code, message: entry.message };
|
|
1662
|
+
}
|
|
1290
1663
|
async function readRequests(cwd) {
|
|
1291
1664
|
try {
|
|
1292
|
-
const raw = await
|
|
1665
|
+
const raw = await readFile5(join6(cwd, REQUESTS_PATH), "utf8");
|
|
1293
1666
|
const parsed2 = JSON.parse(raw);
|
|
1294
1667
|
if (!Array.isArray(parsed2.requests))
|
|
1295
1668
|
return [];
|
|
1296
1669
|
return parsed2.requests.map((request, index) => {
|
|
1297
|
-
const entry = request;
|
|
1670
|
+
const { failure: rawFailure, ...entry } = request;
|
|
1671
|
+
const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
|
|
1672
|
+
const failure = isTerminal(status) ? failureOf(rawFailure) : null;
|
|
1298
1673
|
return {
|
|
1299
1674
|
...entry,
|
|
1300
1675
|
id: typeof entry.id === "string" ? entry.id : String(index),
|
|
1301
|
-
status
|
|
1676
|
+
status,
|
|
1677
|
+
mode: entry.mode === "variant" ? "variant" : "replace",
|
|
1678
|
+
...failure === null ? {} : { failure }
|
|
1302
1679
|
};
|
|
1303
1680
|
});
|
|
1304
1681
|
} catch {
|
|
@@ -1306,23 +1683,23 @@ async function readRequests(cwd) {
|
|
|
1306
1683
|
}
|
|
1307
1684
|
}
|
|
1308
1685
|
async function writeQueue(cwd, requests) {
|
|
1309
|
-
const path =
|
|
1310
|
-
await
|
|
1311
|
-
await
|
|
1686
|
+
const path = join6(cwd, REQUESTS_PATH);
|
|
1687
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
1688
|
+
await writeFile4(path, `${JSON.stringify({ requests }, null, 2)}
|
|
1312
1689
|
`, "utf8");
|
|
1313
1690
|
}
|
|
1314
1691
|
async function appendRequest(cwd, request) {
|
|
1315
1692
|
await writeQueue(cwd, [
|
|
1316
1693
|
...await readRequests(cwd),
|
|
1317
|
-
{ ...request, id:
|
|
1694
|
+
{ ...request, id: randomBytes2(6).toString("base64url"), status: "queued" }
|
|
1318
1695
|
]);
|
|
1319
1696
|
}
|
|
1320
1697
|
async function collectRequests(cwd) {
|
|
1321
1698
|
const requests = await readRequests(cwd);
|
|
1322
|
-
const collected = requests.map((request) => ({ ...request, status: "picked-up" })
|
|
1323
|
-
if (requests.some((request) => request.status
|
|
1699
|
+
const collected = requests.map((request) => isTerminal(request.status) ? request : { ...request, status: "picked-up" });
|
|
1700
|
+
if (requests.some((request) => request.status === "queued"))
|
|
1324
1701
|
await writeQueue(cwd, collected);
|
|
1325
|
-
return collected;
|
|
1702
|
+
return collected.filter((request) => !isTerminal(request.status));
|
|
1326
1703
|
}
|
|
1327
1704
|
async function markPickedUp(cwd, id) {
|
|
1328
1705
|
const requests = await readRequests(cwd);
|
|
@@ -1331,6 +1708,17 @@ async function markPickedUp(cwd, id) {
|
|
|
1331
1708
|
await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
|
|
1332
1709
|
return true;
|
|
1333
1710
|
}
|
|
1711
|
+
async function markFailed(cwd, id, failure) {
|
|
1712
|
+
const requests = await readRequests(cwd);
|
|
1713
|
+
if (!requests.some((request) => request.id === id))
|
|
1714
|
+
return false;
|
|
1715
|
+
await writeQueue(cwd, requests.map((request) => request.id === id ? {
|
|
1716
|
+
...request,
|
|
1717
|
+
status: failure.code === "cancelled" ? "cancelled" : "failed",
|
|
1718
|
+
failure
|
|
1719
|
+
} : request));
|
|
1720
|
+
return true;
|
|
1721
|
+
}
|
|
1334
1722
|
async function removeRequest(cwd, id) {
|
|
1335
1723
|
const requests = await readRequests(cwd);
|
|
1336
1724
|
const remaining = requests.filter((request) => request.id !== id);
|
|
@@ -1341,7 +1729,7 @@ async function removeRequest(cwd, id) {
|
|
|
1341
1729
|
}
|
|
1342
1730
|
async function clearRequests(cwd) {
|
|
1343
1731
|
const requests = await readRequests(cwd);
|
|
1344
|
-
const pending = requests.filter((request) => request.status
|
|
1732
|
+
const pending = requests.filter((request) => request.status === "queued");
|
|
1345
1733
|
const cleared = requests.length - pending.length;
|
|
1346
1734
|
if (cleared > 0)
|
|
1347
1735
|
await writeQueue(cwd, pending);
|
|
@@ -1350,10 +1738,13 @@ async function clearRequests(cwd) {
|
|
|
1350
1738
|
|
|
1351
1739
|
// ../server/dist/runner.js
|
|
1352
1740
|
import { spawn as nodeSpawn } from "child_process";
|
|
1741
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1742
|
+
import { join as join7 } from "path";
|
|
1353
1743
|
var POLL_MS = 2e3;
|
|
1354
1744
|
var OUTPUT_LINES = 20;
|
|
1745
|
+
var CANCEL_GRACE_MS = 5e3;
|
|
1355
1746
|
var SESSION_TURNS_CAP = 8;
|
|
1356
|
-
function resolveCommand(choice, prompt, sessionId = null) {
|
|
1747
|
+
function resolveCommand(choice, prompt, sessionId = null, registration = null) {
|
|
1357
1748
|
if (choice.agent === null)
|
|
1358
1749
|
return null;
|
|
1359
1750
|
if (choice.agent === "custom") {
|
|
@@ -1365,12 +1756,13 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1365
1756
|
return { agent: "custom", name: "Custom", ...commandFor(parsed2.template, prompt), resumed: false };
|
|
1366
1757
|
}
|
|
1367
1758
|
const adapter = KNOWN_AGENTS[choice.agent];
|
|
1759
|
+
const allow = registration !== null && "allowArgs" in adapter ? adapter.allowArgs(registration) : [];
|
|
1368
1760
|
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1369
1761
|
return {
|
|
1370
1762
|
agent: choice.agent,
|
|
1371
1763
|
name: adapter.name,
|
|
1372
1764
|
command: adapter.binary,
|
|
1373
|
-
args: adapter.resumeArgs(sessionId, prompt),
|
|
1765
|
+
args: [...adapter.resumeArgs(sessionId, prompt), ...allow],
|
|
1374
1766
|
resumed: true
|
|
1375
1767
|
};
|
|
1376
1768
|
}
|
|
@@ -1378,7 +1770,7 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1378
1770
|
agent: choice.agent,
|
|
1379
1771
|
name: adapter.name,
|
|
1380
1772
|
command: adapter.binary,
|
|
1381
|
-
args: adapter.args(prompt),
|
|
1773
|
+
args: [...adapter.args(prompt), ...allow],
|
|
1382
1774
|
resumed: false
|
|
1383
1775
|
};
|
|
1384
1776
|
}
|
|
@@ -1408,12 +1800,17 @@ function startRunner(options) {
|
|
|
1408
1800
|
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1409
1801
|
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1410
1802
|
const failed = /* @__PURE__ */ new Set();
|
|
1803
|
+
const setLater = options.setTimeout ?? ((callback, milliseconds) => {
|
|
1804
|
+
setTimeout(callback, milliseconds).unref?.();
|
|
1805
|
+
});
|
|
1411
1806
|
let state = {
|
|
1412
1807
|
running: false,
|
|
1413
1808
|
requestId: null,
|
|
1414
1809
|
agent: null,
|
|
1415
1810
|
activity: null,
|
|
1416
|
-
startedAt: null
|
|
1811
|
+
startedAt: null,
|
|
1812
|
+
stopping: false,
|
|
1813
|
+
waiting: null
|
|
1417
1814
|
};
|
|
1418
1815
|
let stopped = false;
|
|
1419
1816
|
let ticking = null;
|
|
@@ -1421,15 +1818,30 @@ function startRunner(options) {
|
|
|
1421
1818
|
let active = null;
|
|
1422
1819
|
const sessions = /* @__PURE__ */ new Map();
|
|
1423
1820
|
const idle = () => {
|
|
1424
|
-
state = {
|
|
1821
|
+
state = {
|
|
1822
|
+
running: false,
|
|
1823
|
+
requestId: null,
|
|
1824
|
+
agent: null,
|
|
1825
|
+
activity: null,
|
|
1826
|
+
startedAt: null,
|
|
1827
|
+
stopping: false,
|
|
1828
|
+
waiting: null
|
|
1829
|
+
};
|
|
1425
1830
|
};
|
|
1426
1831
|
const rememberLine = (lines, line) => {
|
|
1427
1832
|
lines.push(line);
|
|
1428
1833
|
if (lines.length > OUTPUT_LINES)
|
|
1429
1834
|
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1430
1835
|
};
|
|
1431
|
-
const reportFailure = (request,
|
|
1432
|
-
|
|
1836
|
+
const reportFailure = async (request, failure, lines) => {
|
|
1837
|
+
await markFailed(options.cwd, request.id, failure).catch(() => {
|
|
1838
|
+
});
|
|
1839
|
+
failed.add(request.id);
|
|
1840
|
+
if (failure.code === "cancelled") {
|
|
1841
|
+
console.error(`Leglas stopped the run for ${request.title}.`);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
console.error(`Leglas agent failed for ${request.title}: ${failure.message}`);
|
|
1433
1845
|
for (const line of lines)
|
|
1434
1846
|
console.error(` ${line}`);
|
|
1435
1847
|
};
|
|
@@ -1447,19 +1859,31 @@ function startRunner(options) {
|
|
|
1447
1859
|
error: error instanceof Error ? error.message : String(error)
|
|
1448
1860
|
});
|
|
1449
1861
|
}
|
|
1450
|
-
const current = {
|
|
1862
|
+
const current = {
|
|
1863
|
+
child,
|
|
1864
|
+
requestId: request.id,
|
|
1865
|
+
cancelled: false,
|
|
1866
|
+
abandon: () => {
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1451
1869
|
active = current;
|
|
1452
1870
|
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1453
1871
|
rememberLine(lines, line);
|
|
1454
1872
|
const sessionId = sessionFrom(resolved.agent, line);
|
|
1455
1873
|
if (sessionId !== null)
|
|
1456
1874
|
observed.sessionId = sessionId;
|
|
1875
|
+
const retry = retryFrom(resolved.agent, line);
|
|
1876
|
+
if (retry !== null) {
|
|
1877
|
+
observed.retry = retry;
|
|
1878
|
+
if (active === current)
|
|
1879
|
+
state = { ...state, waiting: retry };
|
|
1880
|
+
}
|
|
1457
1881
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1458
1882
|
if (activity !== null) {
|
|
1459
1883
|
if (activity.startsWith("editing"))
|
|
1460
1884
|
observed.edited = true;
|
|
1461
1885
|
if (active === current)
|
|
1462
|
-
state = { ...state, activity };
|
|
1886
|
+
state = { ...state, activity, waiting: null };
|
|
1463
1887
|
}
|
|
1464
1888
|
});
|
|
1465
1889
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
@@ -1473,6 +1897,7 @@ function startRunner(options) {
|
|
|
1473
1897
|
stderrFlush();
|
|
1474
1898
|
resolve(outcome);
|
|
1475
1899
|
};
|
|
1900
|
+
current.abandon = () => settle({ ok: false, error: "cancelled" });
|
|
1476
1901
|
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1477
1902
|
child.once("close", (code, signal) => {
|
|
1478
1903
|
if (current.cancelled)
|
|
@@ -1486,10 +1911,12 @@ function startRunner(options) {
|
|
|
1486
1911
|
active = null;
|
|
1487
1912
|
});
|
|
1488
1913
|
};
|
|
1914
|
+
const registered = () => readFile6(join7(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
1489
1915
|
const handle = async (request, choice) => {
|
|
1490
1916
|
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1491
1917
|
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1492
|
-
|
|
1918
|
+
const registration = request.mode === "variant" && options.leglasCommand !== void 0 ? registrationCommand(options.leglasCommand) : null;
|
|
1919
|
+
let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null, registration);
|
|
1493
1920
|
if (resolved === null)
|
|
1494
1921
|
return;
|
|
1495
1922
|
const lines = [];
|
|
@@ -1497,7 +1924,7 @@ function startRunner(options) {
|
|
|
1497
1924
|
if (!await markPickedUp(options.cwd, request.id))
|
|
1498
1925
|
return;
|
|
1499
1926
|
if (stopped) {
|
|
1500
|
-
|
|
1927
|
+
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
1501
1928
|
return;
|
|
1502
1929
|
}
|
|
1503
1930
|
state = {
|
|
@@ -1505,25 +1932,47 @@ function startRunner(options) {
|
|
|
1505
1932
|
requestId: request.id,
|
|
1506
1933
|
agent: resolved.name,
|
|
1507
1934
|
activity: null,
|
|
1508
|
-
startedAt: Date.now()
|
|
1935
|
+
startedAt: Date.now(),
|
|
1936
|
+
stopping: false,
|
|
1937
|
+
waiting: null
|
|
1938
|
+
};
|
|
1939
|
+
const observed = {
|
|
1940
|
+
sessionId: null,
|
|
1941
|
+
edited: false,
|
|
1942
|
+
retry: null
|
|
1509
1943
|
};
|
|
1510
|
-
const
|
|
1944
|
+
const before = request.mode === "variant" ? await registered() : null;
|
|
1945
|
+
const agent = resolved.name;
|
|
1511
1946
|
let outcome = await runChild(request, resolved, lines, observed);
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1947
|
+
const verdict = () => classifyFailure({
|
|
1948
|
+
agent,
|
|
1949
|
+
error: outcome.ok ? null : stopped && outcome.error === "cancelled" ? "stopped by shutdown" : outcome.error,
|
|
1950
|
+
exitCode: outcome.ok ? outcome.code : null,
|
|
1951
|
+
lines,
|
|
1952
|
+
retry: observed.retry
|
|
1953
|
+
});
|
|
1954
|
+
let failure = verdict();
|
|
1955
|
+
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && sessionShaped(failure.code) && // Not redundant with the verdict: a stop that lands between the first
|
|
1956
|
+
// child settling and the retry starting finds no child to cancel, so
|
|
1957
|
+
// nothing says "cancelled". Stopped still means stopped.
|
|
1516
1958
|
!stopped) {
|
|
1517
1959
|
sessions.delete(resolved.agent);
|
|
1518
|
-
const cold = resolveCommand(choice, request.prompt);
|
|
1960
|
+
const cold = resolveCommand(choice, request.prompt, null, registration);
|
|
1519
1961
|
if (cold !== null) {
|
|
1520
1962
|
resolved = cold;
|
|
1521
1963
|
observed.sessionId = null;
|
|
1522
|
-
|
|
1964
|
+
observed.retry = null;
|
|
1965
|
+
state = { ...state, activity: null, waiting: null };
|
|
1523
1966
|
outcome = await runChild(request, resolved, lines, observed);
|
|
1967
|
+
failure = verdict();
|
|
1524
1968
|
}
|
|
1525
1969
|
}
|
|
1526
1970
|
if (outcome.ok && outcome.code === 0) {
|
|
1971
|
+
if (request.mode === "variant" && await registered() === before) {
|
|
1972
|
+
sessions.delete(resolved.agent);
|
|
1973
|
+
await reportFailure(request, classifyFailure({ agent, error: "not-registered" }), lines);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1527
1976
|
if (observed.sessionId !== null) {
|
|
1528
1977
|
const previous = sessions.get(resolved.agent);
|
|
1529
1978
|
sessions.set(resolved.agent, {
|
|
@@ -1531,12 +1980,14 @@ function startRunner(options) {
|
|
|
1531
1980
|
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1532
1981
|
});
|
|
1533
1982
|
}
|
|
1983
|
+
if (request.mode === "replace" && request.notes !== void 0) {
|
|
1984
|
+
await removeAnnotations(options.cwd, request.notes).catch(() => 0);
|
|
1985
|
+
}
|
|
1534
1986
|
await removeRequest(options.cwd, request.id);
|
|
1535
1987
|
return;
|
|
1536
1988
|
}
|
|
1537
1989
|
sessions.delete(resolved.agent);
|
|
1538
|
-
|
|
1539
|
-
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
1990
|
+
await reportFailure(request, failure, lines);
|
|
1540
1991
|
} finally {
|
|
1541
1992
|
idle();
|
|
1542
1993
|
}
|
|
@@ -1570,12 +2021,23 @@ function startRunner(options) {
|
|
|
1570
2021
|
return false;
|
|
1571
2022
|
if (id !== void 0 && active.requestId !== id)
|
|
1572
2023
|
return false;
|
|
1573
|
-
|
|
1574
|
-
|
|
2024
|
+
const current = active;
|
|
2025
|
+
current.cancelled = true;
|
|
2026
|
+
failed.add(current.requestId);
|
|
2027
|
+
state = { ...state, stopping: true, waiting: null };
|
|
1575
2028
|
try {
|
|
1576
|
-
|
|
2029
|
+
current.child.kill("SIGTERM");
|
|
1577
2030
|
} catch {
|
|
1578
2031
|
}
|
|
2032
|
+
setLater(() => {
|
|
2033
|
+
if (active !== current)
|
|
2034
|
+
return;
|
|
2035
|
+
try {
|
|
2036
|
+
current.child.kill("SIGKILL");
|
|
2037
|
+
} catch {
|
|
2038
|
+
}
|
|
2039
|
+
current.abandon();
|
|
2040
|
+
}, CANCEL_GRACE_MS);
|
|
1579
2041
|
return true;
|
|
1580
2042
|
};
|
|
1581
2043
|
const stop = () => {
|
|
@@ -1600,12 +2062,12 @@ function startRunner(options) {
|
|
|
1600
2062
|
}
|
|
1601
2063
|
|
|
1602
2064
|
// ../server/dist/renames.js
|
|
1603
|
-
import { mkdir as
|
|
1604
|
-
import { dirname as
|
|
2065
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
2066
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1605
2067
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
1606
2068
|
async function readRenames(cwd) {
|
|
1607
2069
|
try {
|
|
1608
|
-
const raw = await
|
|
2070
|
+
const raw = await readFile7(join8(cwd, RENAMES_PATH), "utf8");
|
|
1609
2071
|
const parsed2 = JSON.parse(raw);
|
|
1610
2072
|
if (parsed2.renames === null || typeof parsed2.renames !== "object")
|
|
1611
2073
|
return {};
|
|
@@ -1615,9 +2077,9 @@ async function readRenames(cwd) {
|
|
|
1615
2077
|
}
|
|
1616
2078
|
}
|
|
1617
2079
|
async function writeRenames(cwd, renames) {
|
|
1618
|
-
const path =
|
|
1619
|
-
await
|
|
1620
|
-
await
|
|
2080
|
+
const path = join8(cwd, RENAMES_PATH);
|
|
2081
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2082
|
+
await writeFile5(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1621
2083
|
`, "utf8");
|
|
1622
2084
|
}
|
|
1623
2085
|
function resolveTitle(input, titles, renames) {
|
|
@@ -1635,7 +2097,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
1635
2097
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1636
2098
|
import http2 from "http";
|
|
1637
2099
|
import net3 from "net";
|
|
1638
|
-
import { extname, join as
|
|
2100
|
+
import { extname, join as join9, normalize, relative as relative3 } from "path";
|
|
1639
2101
|
var LEGLAS_PREFIX = "/leglas";
|
|
1640
2102
|
var DEFAULT_PORT = 4100;
|
|
1641
2103
|
var PORT_ATTEMPTS = 20;
|
|
@@ -1708,6 +2170,9 @@ function isTrustedMutation(req) {
|
|
|
1708
2170
|
return false;
|
|
1709
2171
|
}
|
|
1710
2172
|
}
|
|
2173
|
+
function isEnded(request, failedIds) {
|
|
2174
|
+
return isTerminal(request.status) || failedIds.includes(request.id);
|
|
2175
|
+
}
|
|
1711
2176
|
function hasJsonBody(req) {
|
|
1712
2177
|
const contentType = req.headers["content-type"];
|
|
1713
2178
|
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
@@ -1734,7 +2199,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
1734
2199
|
}
|
|
1735
2200
|
function serveFrom(res, dir, relativePath) {
|
|
1736
2201
|
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
1737
|
-
const candidate =
|
|
2202
|
+
const candidate = join9(dir, relative5);
|
|
1738
2203
|
if (!candidate.startsWith(dir))
|
|
1739
2204
|
return false;
|
|
1740
2205
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -1819,7 +2284,7 @@ async function bind(server, requested) {
|
|
|
1819
2284
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
1820
2285
|
}
|
|
1821
2286
|
async function startServer(options) {
|
|
1822
|
-
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
2287
|
+
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
1823
2288
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
1824
2289
|
const proxy = createProxyHandler({ target });
|
|
1825
2290
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
@@ -1859,13 +2324,23 @@ async function startServer(options) {
|
|
|
1859
2324
|
const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
|
|
1860
2325
|
if (notice !== null)
|
|
1861
2326
|
errors.push(notice);
|
|
1862
|
-
return void readLocalPreviews(cwd).then(({ previews: local }) => {
|
|
1863
|
-
|
|
2327
|
+
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
2328
|
+
if (localErrors.length > 0) {
|
|
2329
|
+
return sendJson(res, 200, {
|
|
2330
|
+
project,
|
|
2331
|
+
devServer: target,
|
|
2332
|
+
previews: boot,
|
|
2333
|
+
errors
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
const localTitles = new Set(local.map((preview) => preview.title));
|
|
2337
|
+
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
2338
|
+
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
1864
2339
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
1865
2340
|
sendJson(res, 200, {
|
|
1866
2341
|
project,
|
|
1867
2342
|
devServer: target,
|
|
1868
|
-
previews: [...
|
|
2343
|
+
previews: [...currentBoot, ...fresh],
|
|
1869
2344
|
errors
|
|
1870
2345
|
});
|
|
1871
2346
|
}).catch(() => sendJson(res, 200, {
|
|
@@ -1875,6 +2350,47 @@ async function startServer(options) {
|
|
|
1875
2350
|
errors
|
|
1876
2351
|
}));
|
|
1877
2352
|
}
|
|
2353
|
+
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
2354
|
+
let body = "";
|
|
2355
|
+
req.on("data", (chunk) => body += chunk);
|
|
2356
|
+
return void req.on("end", async () => {
|
|
2357
|
+
let parsed2;
|
|
2358
|
+
try {
|
|
2359
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2360
|
+
} catch {
|
|
2361
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2362
|
+
}
|
|
2363
|
+
const titles = parsed2.titles;
|
|
2364
|
+
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
2365
|
+
return sendJson(res, 400, {
|
|
2366
|
+
ok: false,
|
|
2367
|
+
error: "Body needs a non-empty array of direction titles."
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
const unique = [...new Set(titles)];
|
|
2371
|
+
try {
|
|
2372
|
+
const local = await readLocalPreviews(cwd);
|
|
2373
|
+
if (local.errors.length > 0) {
|
|
2374
|
+
return sendJson(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
2375
|
+
}
|
|
2376
|
+
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
2377
|
+
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
2378
|
+
if (unknown.length > 0) {
|
|
2379
|
+
return sendJson(res, 400, {
|
|
2380
|
+
ok: false,
|
|
2381
|
+
error: "Only machine-local directions can be deleted from the registry."
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
const deleted = await dropLocalPreviews(cwd, unique);
|
|
2385
|
+
return sendJson(res, 200, { ok: true, deleted });
|
|
2386
|
+
} catch {
|
|
2387
|
+
return sendJson(res, 500, {
|
|
2388
|
+
ok: false,
|
|
2389
|
+
error: "The directions could not be deleted from Leglas."
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
1878
2394
|
if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
|
|
1879
2395
|
let body = "";
|
|
1880
2396
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -1885,16 +2401,51 @@ async function startServer(options) {
|
|
|
1885
2401
|
} catch {
|
|
1886
2402
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1887
2403
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2404
|
+
if (parsed2.mode !== void 0 && parsed2.mode !== "variant" && parsed2.mode !== "replace") {
|
|
2405
|
+
return sendJson(res, 400, {
|
|
2406
|
+
ok: false,
|
|
2407
|
+
error: 'mode must be "variant" or "replace".'
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
const mode = parsed2.mode === "replace" ? "replace" : "variant";
|
|
2411
|
+
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
2412
|
+
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
2413
|
+
const localTitles = new Set(local.map((entry) => entry.title));
|
|
2414
|
+
const bootConfig = config?.previews ?? [];
|
|
2415
|
+
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
2416
|
+
const preview = [...boot, ...local].find((entry) => entry.title === parsed2.title);
|
|
2417
|
+
if (!preview) {
|
|
2418
|
+
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2419
|
+
}
|
|
2420
|
+
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
2421
|
+
if (!parsed2.intent?.trim() && notes.length === 0) {
|
|
1891
2422
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
1892
2423
|
}
|
|
1893
|
-
const
|
|
2424
|
+
const intent = (parsed2.intent ?? "").trim();
|
|
2425
|
+
const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
2426
|
+
const sameNotes = (entry) => {
|
|
2427
|
+
const before = [...entry.notes ?? []].sort().join(",");
|
|
2428
|
+
return before === notes.map((note) => note.id).sort().join(",");
|
|
2429
|
+
};
|
|
2430
|
+
if (live.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
2431
|
+
// one forks the direction and the other rewrites it. Only a
|
|
2432
|
+
// genuine repeat is refused.
|
|
2433
|
+
(entry.mode ?? "replace") === mode && sameNotes(entry))) {
|
|
2434
|
+
return sendJson(res, 409, {
|
|
2435
|
+
ok: false,
|
|
2436
|
+
duplicate: true,
|
|
2437
|
+
error: `That exact change to ${preview.title} is already waiting.`
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
const composed = composeRequest(preview, intent, mode, notes, leglasCommand);
|
|
1894
2441
|
void appendRequest(cwd, {
|
|
1895
2442
|
title: preview.title,
|
|
1896
2443
|
url: preview.url,
|
|
1897
|
-
intent
|
|
2444
|
+
intent,
|
|
2445
|
+
// The ids travel with the request so a change made in place can
|
|
2446
|
+
// forget the notes it answered. A fork leaves them where they are:
|
|
2447
|
+
// the direction they point at was not touched.
|
|
2448
|
+
...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
|
|
1898
2449
|
...composed
|
|
1899
2450
|
}).then(() => {
|
|
1900
2451
|
runner?.nudge();
|
|
@@ -1970,21 +2521,33 @@ async function startServer(options) {
|
|
|
1970
2521
|
agent: null,
|
|
1971
2522
|
activity: null,
|
|
1972
2523
|
startedAt: null,
|
|
2524
|
+
stopping: false,
|
|
2525
|
+
waiting: null,
|
|
1973
2526
|
failedIds: []
|
|
1974
2527
|
};
|
|
1975
2528
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
1976
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
2529
|
+
requests: requests.map(({ id, title, intent, status, failure }) => ({
|
|
1977
2530
|
id,
|
|
1978
2531
|
title,
|
|
1979
2532
|
intent,
|
|
1980
|
-
|
|
2533
|
+
// The run in flight is the one thing the file cannot know. After
|
|
2534
|
+
// that the file is the record, including across a restart, and the
|
|
2535
|
+
// process-local failed set only covers a request whose verdict
|
|
2536
|
+
// could not be written.
|
|
2537
|
+
status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
|
|
2538
|
+
failure: failure ?? null
|
|
1981
2539
|
})),
|
|
1982
2540
|
agent: {
|
|
1983
2541
|
attached: externallyAttached(),
|
|
1984
2542
|
running: snapshot.running,
|
|
1985
2543
|
name: snapshot.running ? snapshot.agent : null,
|
|
1986
2544
|
activity: snapshot.running ? snapshot.activity : null,
|
|
1987
|
-
startedAt: snapshot.running ? snapshot.startedAt : null
|
|
2545
|
+
startedAt: snapshot.running ? snapshot.startedAt : null,
|
|
2546
|
+
// A stop that has been asked for but not yet obeyed. The card
|
|
2547
|
+
// says so rather than going on describing a live run.
|
|
2548
|
+
stopping: snapshot.running && snapshot.stopping,
|
|
2549
|
+
// Why a run that looks stalled is stalled, while it is stalled.
|
|
2550
|
+
waiting: snapshot.running ? snapshot.waiting : null
|
|
1988
2551
|
}
|
|
1989
2552
|
}));
|
|
1990
2553
|
}
|
|
@@ -2027,8 +2590,8 @@ async function startServer(options) {
|
|
|
2027
2590
|
if (request === void 0) {
|
|
2028
2591
|
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2029
2592
|
}
|
|
2030
|
-
if (!(runner?.snapshot().failedIds
|
|
2031
|
-
return sendJson(res, 400, { ok: false, error: "Only
|
|
2593
|
+
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
2594
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
2032
2595
|
}
|
|
2033
2596
|
try {
|
|
2034
2597
|
if (!await removeRequest(cwd, request.id)) {
|
|
@@ -2039,7 +2602,11 @@ async function startServer(options) {
|
|
|
2039
2602
|
url: request.url,
|
|
2040
2603
|
intent: request.intent,
|
|
2041
2604
|
target: request.target,
|
|
2042
|
-
prompt: request.prompt
|
|
2605
|
+
prompt: request.prompt,
|
|
2606
|
+
// The stored prompt already carries the mode's instructions; the
|
|
2607
|
+
// field travels with it so the queue keeps saying which kind of
|
|
2608
|
+
// change this is.
|
|
2609
|
+
...request.mode === void 0 ? {} : { mode: request.mode }
|
|
2043
2610
|
});
|
|
2044
2611
|
runner?.nudge();
|
|
2045
2612
|
return sendJson(res, 200, { ok: true });
|
|
@@ -2048,6 +2615,65 @@ async function startServer(options) {
|
|
|
2048
2615
|
}
|
|
2049
2616
|
});
|
|
2050
2617
|
}
|
|
2618
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
2619
|
+
return void readAnnotations(cwd).then((annotations) => sendJson(res, 200, { annotations }));
|
|
2620
|
+
}
|
|
2621
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
2622
|
+
if (!hasJsonBody(req)) {
|
|
2623
|
+
return sendJson(res, 400, { ok: false, error: "A note must be JSON." });
|
|
2624
|
+
}
|
|
2625
|
+
let body = "";
|
|
2626
|
+
req.on("data", (chunk) => body += chunk);
|
|
2627
|
+
return void req.on("end", async () => {
|
|
2628
|
+
let parsed2;
|
|
2629
|
+
try {
|
|
2630
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2631
|
+
} catch {
|
|
2632
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2633
|
+
}
|
|
2634
|
+
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
2635
|
+
return sendJson(res, 400, { ok: false, error: "A note needs a direction." });
|
|
2636
|
+
}
|
|
2637
|
+
const anchor = anchorFrom(parsed2.anchor);
|
|
2638
|
+
if (anchor === null) {
|
|
2639
|
+
return sendJson(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
2640
|
+
}
|
|
2641
|
+
try {
|
|
2642
|
+
const annotation = await addAnnotation(cwd, {
|
|
2643
|
+
anchor,
|
|
2644
|
+
note: typeof parsed2.note === "string" ? parsed2.note.trim() : "",
|
|
2645
|
+
title: parsed2.title
|
|
2646
|
+
});
|
|
2647
|
+
return sendJson(res, 200, { ok: true, annotation });
|
|
2648
|
+
} catch {
|
|
2649
|
+
return sendJson(res, 500, { ok: false, error: "The note could not be kept." });
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
2654
|
+
if (!hasJsonBody(req)) {
|
|
2655
|
+
return sendJson(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
2656
|
+
}
|
|
2657
|
+
let body = "";
|
|
2658
|
+
req.on("data", (chunk) => body += chunk);
|
|
2659
|
+
return void req.on("end", async () => {
|
|
2660
|
+
let parsed2;
|
|
2661
|
+
try {
|
|
2662
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2663
|
+
} catch {
|
|
2664
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2665
|
+
}
|
|
2666
|
+
const ids = Array.isArray(parsed2.ids) ? parsed2.ids.filter((entry) => typeof entry === "string") : [];
|
|
2667
|
+
if (ids.length === 0) {
|
|
2668
|
+
return sendJson(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
2669
|
+
}
|
|
2670
|
+
try {
|
|
2671
|
+
return sendJson(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
2672
|
+
} catch {
|
|
2673
|
+
return sendJson(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
2674
|
+
}
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2051
2677
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2052
2678
|
if (!hasJsonBody(req)) {
|
|
2053
2679
|
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
@@ -2064,8 +2690,9 @@ async function startServer(options) {
|
|
|
2064
2690
|
if (typeof parsed2.id !== "string") {
|
|
2065
2691
|
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2066
2692
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2693
|
+
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
2694
|
+
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
2695
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
2069
2696
|
}
|
|
2070
2697
|
try {
|
|
2071
2698
|
if (!await removeRequest(cwd, parsed2.id)) {
|
|
@@ -2140,7 +2767,7 @@ async function startServer(options) {
|
|
|
2140
2767
|
proxy.upgrade(req, socket, head);
|
|
2141
2768
|
});
|
|
2142
2769
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2143
|
-
runner = startRunner({ cwd, externallyAttached });
|
|
2770
|
+
runner = startRunner({ cwd, externallyAttached, leglasCommand });
|
|
2144
2771
|
let closePromise = null;
|
|
2145
2772
|
return {
|
|
2146
2773
|
port,
|
|
@@ -2165,7 +2792,7 @@ async function runClassify(options, deps) {
|
|
|
2165
2792
|
const declared = await Promise.all(
|
|
2166
2793
|
options.changes.map(async (change) => ({
|
|
2167
2794
|
...change,
|
|
2168
|
-
exists: await stat(
|
|
2795
|
+
exists: await stat(join10(options.cwd, change.path)).then(
|
|
2169
2796
|
() => true,
|
|
2170
2797
|
() => false
|
|
2171
2798
|
)
|
|
@@ -2430,8 +3057,8 @@ function runExplore(options, deps) {
|
|
|
2430
3057
|
}
|
|
2431
3058
|
|
|
2432
3059
|
// src/run-init.ts
|
|
2433
|
-
import { readFile as
|
|
2434
|
-
import { join as
|
|
3060
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
3061
|
+
import { join as join11 } from "path";
|
|
2435
3062
|
|
|
2436
3063
|
// src/init.ts
|
|
2437
3064
|
var AGENTS_MARKER_START = "<!-- leglas:start -->";
|
|
@@ -2449,6 +3076,11 @@ direction. Build it directly. A planning or approval step before implementing
|
|
|
2449
3076
|
costs more than the work itself, and the directions on screen are the thing
|
|
2450
3077
|
being asked for.
|
|
2451
3078
|
|
|
3079
|
+
A request that says it came from the running Leglas interface has already
|
|
3080
|
+
completed exploration, request collection and the live-server check. Follow
|
|
3081
|
+
the exact source and registration command in that request directly. Do not
|
|
3082
|
+
repeat \`explore\`, \`requests\`, \`list\`, CLI help/version checks or server startup.
|
|
3083
|
+
|
|
2452
3084
|
When asked for design variations, alternatives, or "a few options":
|
|
2453
3085
|
|
|
2454
3086
|
1. **Add beside what exists. Never replace it.** Every direction has to render
|
|
@@ -2564,7 +3196,7 @@ ${AGENTS_SECTION}`
|
|
|
2564
3196
|
// src/run-init.ts
|
|
2565
3197
|
async function readIfPresent(path) {
|
|
2566
3198
|
try {
|
|
2567
|
-
return await
|
|
3199
|
+
return await readFile8(path, "utf8");
|
|
2568
3200
|
} catch {
|
|
2569
3201
|
return null;
|
|
2570
3202
|
}
|
|
@@ -2572,18 +3204,18 @@ async function readIfPresent(path) {
|
|
|
2572
3204
|
async function runInit(options, deps) {
|
|
2573
3205
|
const existingConfig = findConfigFile(options.cwd);
|
|
2574
3206
|
const plan = planInit({
|
|
2575
|
-
agents: await readIfPresent(
|
|
3207
|
+
agents: await readIfPresent(join11(options.cwd, "AGENTS.md")),
|
|
2576
3208
|
config: existingConfig === null ? null : "present",
|
|
2577
|
-
gitignore: await readIfPresent(
|
|
3209
|
+
gitignore: await readIfPresent(join11(options.cwd, ".gitignore")),
|
|
2578
3210
|
force: options.force
|
|
2579
3211
|
});
|
|
2580
3212
|
const touched = [];
|
|
2581
|
-
for (const
|
|
2582
|
-
await
|
|
2583
|
-
touched.push(
|
|
3213
|
+
for (const write2 of plan.writes) {
|
|
3214
|
+
await writeFile6(join11(options.cwd, write2.path), write2.contents, "utf8");
|
|
3215
|
+
touched.push(write2.path);
|
|
2584
3216
|
}
|
|
2585
3217
|
if (plan.gitignore !== null) {
|
|
2586
|
-
await
|
|
3218
|
+
await writeFile6(join11(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2587
3219
|
touched.push(".gitignore");
|
|
2588
3220
|
}
|
|
2589
3221
|
if (options.json) {
|
|
@@ -2603,8 +3235,8 @@ async function runInit(options, deps) {
|
|
|
2603
3235
|
|
|
2604
3236
|
// src/run-keep.ts
|
|
2605
3237
|
import { existsSync as existsSync3 } from "fs";
|
|
2606
|
-
import { mkdir as
|
|
2607
|
-
import { dirname as
|
|
3238
|
+
import { mkdir as mkdir6, readFile as readFile9, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
3239
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
2608
3240
|
|
|
2609
3241
|
// src/keep.ts
|
|
2610
3242
|
import { basename as basename2, extname as extname2, normalize as normalize2 } from "path";
|
|
@@ -2703,18 +3335,18 @@ async function runKeep(options, deps) {
|
|
|
2703
3335
|
if (!resolved.ok) return fail(resolved.error);
|
|
2704
3336
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
2705
3337
|
if (!plan.ok) return fail(plan.error);
|
|
2706
|
-
const from =
|
|
2707
|
-
const to =
|
|
3338
|
+
const from = join12(options.cwd, plan.move.from);
|
|
3339
|
+
const to = join12(options.cwd, plan.move.to);
|
|
2708
3340
|
if (!existsSync3(from)) {
|
|
2709
3341
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
2710
3342
|
}
|
|
2711
3343
|
if (existsSync3(to)) {
|
|
2712
3344
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
2713
3345
|
}
|
|
2714
|
-
const source = await
|
|
2715
|
-
await
|
|
2716
|
-
await
|
|
2717
|
-
await rm2(
|
|
3346
|
+
const source = await readFile9(from, "utf8");
|
|
3347
|
+
await mkdir6(dirname7(to), { recursive: true });
|
|
3348
|
+
await writeFile7(to, renameExport(source, plan.exportName), "utf8");
|
|
3349
|
+
await rm2(join12(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
2718
3350
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
2719
3351
|
if (options.json) {
|
|
2720
3352
|
deps.log(
|
|
@@ -2749,11 +3381,11 @@ async function runKeep(options, deps) {
|
|
|
2749
3381
|
|
|
2750
3382
|
// src/run-new.ts
|
|
2751
3383
|
import { existsSync as existsSync4 } from "fs";
|
|
2752
|
-
import { mkdir as
|
|
2753
|
-
import { dirname as
|
|
3384
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
3385
|
+
import { dirname as dirname8, join as join13 } from "path";
|
|
2754
3386
|
async function readIfPresent2(path) {
|
|
2755
3387
|
try {
|
|
2756
|
-
return await
|
|
3388
|
+
return await readFile10(path, "utf8");
|
|
2757
3389
|
} catch {
|
|
2758
3390
|
return null;
|
|
2759
3391
|
}
|
|
@@ -2761,19 +3393,19 @@ async function readIfPresent2(path) {
|
|
|
2761
3393
|
async function runNew(options, deps) {
|
|
2762
3394
|
let from;
|
|
2763
3395
|
if (options.from !== void 0) {
|
|
2764
|
-
const contents = await readIfPresent2(
|
|
3396
|
+
const contents = await readIfPresent2(join13(options.cwd, options.from));
|
|
2765
3397
|
if (contents === null) {
|
|
2766
|
-
const
|
|
2767
|
-
if (options.json) deps.log(JSON.stringify({ ok: false, error:
|
|
2768
|
-
else deps.log(
|
|
3398
|
+
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
3399
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
3400
|
+
else deps.log(message2);
|
|
2769
3401
|
return { exitCode: 1, written: [] };
|
|
2770
3402
|
}
|
|
2771
3403
|
from = { path: options.from, contents };
|
|
2772
3404
|
}
|
|
2773
3405
|
const plan = planNew({
|
|
2774
3406
|
surface: options.surface,
|
|
2775
|
-
packageJson: await readIfPresent2(
|
|
2776
|
-
gitignore: await readIfPresent2(
|
|
3407
|
+
packageJson: await readIfPresent2(join13(options.cwd, "package.json")),
|
|
3408
|
+
gitignore: await readIfPresent2(join13(options.cwd, ".gitignore")),
|
|
2777
3409
|
from
|
|
2778
3410
|
});
|
|
2779
3411
|
const fail = (error) => {
|
|
@@ -2789,26 +3421,26 @@ async function runNew(options, deps) {
|
|
|
2789
3421
|
deps.log(JSON.stringify({ ok: true, files: plan.writes, instructions: plan.instructions, previews: plan.previews }));
|
|
2790
3422
|
return { exitCode: 0, written: [] };
|
|
2791
3423
|
}
|
|
2792
|
-
for (const
|
|
2793
|
-
deps.log(`--- ${
|
|
2794
|
-
deps.log(
|
|
3424
|
+
for (const write2 of plan.writes) {
|
|
3425
|
+
deps.log(`--- ${write2.path}`);
|
|
3426
|
+
deps.log(write2.contents);
|
|
2795
3427
|
}
|
|
2796
3428
|
deps.log(plan.instructions);
|
|
2797
3429
|
return { exitCode: 0, written: [] };
|
|
2798
3430
|
}
|
|
2799
|
-
const existing = plan.writes.filter((
|
|
3431
|
+
const existing = plan.writes.filter((write2) => existsSync4(join13(options.cwd, write2.path)));
|
|
2800
3432
|
if (existing.length > 0) {
|
|
2801
3433
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
2802
3434
|
}
|
|
2803
3435
|
const written = [];
|
|
2804
|
-
for (const
|
|
2805
|
-
const target =
|
|
2806
|
-
await
|
|
2807
|
-
await
|
|
2808
|
-
written.push(
|
|
3436
|
+
for (const write2 of plan.writes) {
|
|
3437
|
+
const target = join13(options.cwd, write2.path);
|
|
3438
|
+
await mkdir7(dirname8(target), { recursive: true });
|
|
3439
|
+
await writeFile8(target, write2.contents, "utf8");
|
|
3440
|
+
written.push(write2.path);
|
|
2809
3441
|
}
|
|
2810
3442
|
if (plan.gitignore !== null) {
|
|
2811
|
-
await
|
|
3443
|
+
await writeFile8(join13(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2812
3444
|
written.push(".gitignore");
|
|
2813
3445
|
}
|
|
2814
3446
|
if (options.json) {
|
|
@@ -2829,21 +3461,21 @@ async function runNew(options, deps) {
|
|
|
2829
3461
|
}
|
|
2830
3462
|
|
|
2831
3463
|
// src/run-previews.ts
|
|
2832
|
-
import { readFile as
|
|
2833
|
-
import { join as
|
|
3464
|
+
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
3465
|
+
import { join as join14 } from "path";
|
|
2834
3466
|
function envelope(deps, ok, body) {
|
|
2835
3467
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
2836
3468
|
}
|
|
2837
3469
|
async function ensureIgnored(cwd) {
|
|
2838
|
-
const path =
|
|
3470
|
+
const path = join14(cwd, ".gitignore");
|
|
2839
3471
|
let current = null;
|
|
2840
3472
|
try {
|
|
2841
|
-
current = await
|
|
3473
|
+
current = await readFile11(path, "utf8");
|
|
2842
3474
|
} catch {
|
|
2843
3475
|
current = null;
|
|
2844
3476
|
}
|
|
2845
3477
|
const next = ignoreEntry(current);
|
|
2846
|
-
if (next !== null) await
|
|
3478
|
+
if (next !== null) await writeFile9(path, next, "utf8");
|
|
2847
3479
|
}
|
|
2848
3480
|
async function runAdd(options, deps) {
|
|
2849
3481
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2867,7 +3499,8 @@ async function runAdd(options, deps) {
|
|
|
2867
3499
|
tags: options.preview.tags,
|
|
2868
3500
|
branch: options.preview.branch,
|
|
2869
3501
|
file: options.preview.file,
|
|
2870
|
-
basedOn: options.preview.basedOn
|
|
3502
|
+
basedOn: options.preview.basedOn,
|
|
3503
|
+
askedFor: options.preview.askedFor
|
|
2871
3504
|
},
|
|
2872
3505
|
shared
|
|
2873
3506
|
);
|
|
@@ -2927,6 +3560,7 @@ async function runList(options, deps) {
|
|
|
2927
3560
|
note: preview.note ?? null,
|
|
2928
3561
|
tags: preview.tags,
|
|
2929
3562
|
basedOn: preview.basedOn ?? null,
|
|
3563
|
+
askedFor: preview.askedFor ?? null,
|
|
2930
3564
|
local: preview.local,
|
|
2931
3565
|
branch: preview.branch ?? null,
|
|
2932
3566
|
file: preview.file ?? null
|
|
@@ -3089,23 +3723,23 @@ async function runShow(options, deps) {
|
|
|
3089
3723
|
|
|
3090
3724
|
// src/run-watch.ts
|
|
3091
3725
|
import { spawn as spawn3 } from "child_process";
|
|
3092
|
-
import { mkdir as
|
|
3093
|
-
import { dirname as
|
|
3726
|
+
import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
|
|
3727
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
3094
3728
|
var POLL_MS2 = 2e3;
|
|
3095
3729
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
3096
3730
|
async function saveTemplate(cwd, run3) {
|
|
3097
|
-
const path =
|
|
3731
|
+
const path = join15(cwd, WATCH_PATH);
|
|
3098
3732
|
let config = {};
|
|
3099
3733
|
try {
|
|
3100
|
-
const parsed2 = JSON.parse(await
|
|
3734
|
+
const parsed2 = JSON.parse(await readFile12(path, "utf8"));
|
|
3101
3735
|
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
3102
3736
|
config = parsed2;
|
|
3103
3737
|
}
|
|
3104
3738
|
} catch {
|
|
3105
3739
|
}
|
|
3106
3740
|
config.run = run3;
|
|
3107
|
-
await
|
|
3108
|
-
await
|
|
3741
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3742
|
+
await writeFile10(path, `${JSON.stringify(config, null, 2)}
|
|
3109
3743
|
`, "utf8");
|
|
3110
3744
|
}
|
|
3111
3745
|
function spawnAgent(command, args, cwd) {
|
|
@@ -3191,9 +3825,13 @@ async function runWatch(options, deps) {
|
|
|
3191
3825
|
return;
|
|
3192
3826
|
}
|
|
3193
3827
|
failed.add(request.id);
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3828
|
+
const failure = classifyFailure({
|
|
3829
|
+
agent: (shownCommand2.split(/\s+/)[0] ?? command).split("/").pop() ?? command,
|
|
3830
|
+
error: outcome.ok ? null : outcome.error,
|
|
3831
|
+
exitCode: outcome.ok ? outcome.code : null
|
|
3832
|
+
});
|
|
3833
|
+
await markFailed(options.cwd, request.id, failure);
|
|
3834
|
+
deps.error(` failed ${request.title}: ${failure.message}`);
|
|
3197
3835
|
deps.error(" Left in the queue and not retried.");
|
|
3198
3836
|
};
|
|
3199
3837
|
const tick = async () => {
|
|
@@ -3240,18 +3878,28 @@ async function runWatch(options, deps) {
|
|
|
3240
3878
|
// src/run.ts
|
|
3241
3879
|
import { existsSync as existsSync5 } from "fs";
|
|
3242
3880
|
import { createRequire } from "module";
|
|
3243
|
-
import { basename as basename3, dirname as
|
|
3881
|
+
import { basename as basename3, dirname as dirname10, join as join16, relative as relative4 } from "path";
|
|
3244
3882
|
import { fileURLToPath } from "url";
|
|
3245
3883
|
function findShellDir() {
|
|
3246
|
-
const bundled =
|
|
3247
|
-
if (existsSync5(
|
|
3884
|
+
const bundled = join16(dirname10(fileURLToPath(import.meta.url)), "shell");
|
|
3885
|
+
if (existsSync5(join16(bundled, "index.html"))) return bundled;
|
|
3248
3886
|
try {
|
|
3249
3887
|
const require2 = createRequire(import.meta.url);
|
|
3250
|
-
return
|
|
3888
|
+
return dirname10(require2.resolve("@leglas/shell/dist/index.html"));
|
|
3251
3889
|
} catch {
|
|
3252
3890
|
return null;
|
|
3253
3891
|
}
|
|
3254
3892
|
}
|
|
3893
|
+
function shellWord(value) {
|
|
3894
|
+
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(value)) return value;
|
|
3895
|
+
if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
|
|
3896
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
3897
|
+
}
|
|
3898
|
+
function embeddedLeglasCommand() {
|
|
3899
|
+
const entry = join16(dirname10(fileURLToPath(import.meta.url)), "bin.js");
|
|
3900
|
+
if (!existsSync5(entry)) return "npx -y leglas";
|
|
3901
|
+
return [process.execPath, entry].map(shellWord).join(" ");
|
|
3902
|
+
}
|
|
3255
3903
|
async function run2(options, deps) {
|
|
3256
3904
|
const loaded = await loadConfig(options.cwd);
|
|
3257
3905
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -3281,7 +3929,7 @@ async function run2(options, deps) {
|
|
|
3281
3929
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
3282
3930
|
for (const preview of merged?.previews ?? []) {
|
|
3283
3931
|
if (preview.file !== void 0) {
|
|
3284
|
-
const absolute =
|
|
3932
|
+
const absolute = join16(options.cwd, preview.file);
|
|
3285
3933
|
if (!existsSync5(absolute)) {
|
|
3286
3934
|
worktreeErrors.push(
|
|
3287
3935
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -3292,7 +3940,7 @@ async function run2(options, deps) {
|
|
|
3292
3940
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
3293
3941
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
3294
3942
|
}
|
|
3295
|
-
fileMounts.set(slug,
|
|
3943
|
+
fileMounts.set(slug, dirname10(absolute));
|
|
3296
3944
|
previews.push({
|
|
3297
3945
|
...preview,
|
|
3298
3946
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -3333,6 +3981,7 @@ async function run2(options, deps) {
|
|
|
3333
3981
|
// directory does. Either way saved layout survives a port change.
|
|
3334
3982
|
project: loaded.path ?? options.cwd,
|
|
3335
3983
|
cwd: options.cwd,
|
|
3984
|
+
leglasCommand: embeddedLeglasCommand(),
|
|
3336
3985
|
...options.port === void 0 ? {} : { port: options.port }
|
|
3337
3986
|
});
|
|
3338
3987
|
const url = `${server.url}${LEGLAS_PREFIX}`;
|
|
@@ -3438,6 +4087,7 @@ Options for add
|
|
|
3438
4087
|
--branch <name> Back the preview with a checkout of this git branch
|
|
3439
4088
|
--file <path> Preview a plain HTML file served by Leglas itself
|
|
3440
4089
|
--based-on <title> The direction this is a variant of; groups the family
|
|
4090
|
+
--asked-for <text> The change that was asked for, in the words that were typed
|
|
3441
4091
|
`;
|
|
3442
4092
|
function version() {
|
|
3443
4093
|
const require2 = createRequire2(import.meta.url);
|