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/index.js
CHANGED
|
@@ -57,6 +57,7 @@ function parseAdd(rest) {
|
|
|
57
57
|
let branch;
|
|
58
58
|
let file;
|
|
59
59
|
let basedOn;
|
|
60
|
+
let askedFor;
|
|
60
61
|
const tags = [];
|
|
61
62
|
let json = false;
|
|
62
63
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -75,7 +76,9 @@ function parseAdd(rest) {
|
|
|
75
76
|
} else {
|
|
76
77
|
value = argument.slice(equals + 1);
|
|
77
78
|
}
|
|
78
|
-
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on"].includes(
|
|
79
|
+
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on", "--asked-for"].includes(
|
|
80
|
+
flag
|
|
81
|
+
)) {
|
|
79
82
|
return { kind: "error", message: `leglas add does not take ${flag}.` };
|
|
80
83
|
}
|
|
81
84
|
if (value === void 0 || value === "") {
|
|
@@ -87,6 +90,7 @@ function parseAdd(rest) {
|
|
|
87
90
|
else if (flag === "--branch") branch = value;
|
|
88
91
|
else if (flag === "--file") file = value;
|
|
89
92
|
else if (flag === "--based-on") basedOn = value;
|
|
93
|
+
else if (flag === "--asked-for") askedFor = value;
|
|
90
94
|
else tags.push(value);
|
|
91
95
|
}
|
|
92
96
|
if (title === void 0) {
|
|
@@ -100,7 +104,16 @@ function parseAdd(rest) {
|
|
|
100
104
|
}
|
|
101
105
|
return {
|
|
102
106
|
kind: "add",
|
|
103
|
-
preview: {
|
|
107
|
+
preview: {
|
|
108
|
+
title,
|
|
109
|
+
url,
|
|
110
|
+
note,
|
|
111
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
112
|
+
branch,
|
|
113
|
+
file,
|
|
114
|
+
basedOn,
|
|
115
|
+
askedFor
|
|
116
|
+
},
|
|
104
117
|
json
|
|
105
118
|
};
|
|
106
119
|
}
|
|
@@ -600,6 +613,11 @@ direction. Build it directly. A planning or approval step before implementing
|
|
|
600
613
|
costs more than the work itself, and the directions on screen are the thing
|
|
601
614
|
being asked for.
|
|
602
615
|
|
|
616
|
+
A request that says it came from the running Leglas interface has already
|
|
617
|
+
completed exploration, request collection and the live-server check. Follow
|
|
618
|
+
the exact source and registration command in that request directly. Do not
|
|
619
|
+
repeat \`explore\`, \`requests\`, \`list\`, CLI help/version checks or server startup.
|
|
620
|
+
|
|
603
621
|
When asked for design variations, alternatives, or "a few options":
|
|
604
622
|
|
|
605
623
|
1. **Add beside what exists. Never replace it.** Every direction has to render
|
|
@@ -808,6 +826,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
808
826
|
if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
|
|
809
827
|
errors.push(`${at} has a basedOn that is not a direction title.`);
|
|
810
828
|
}
|
|
829
|
+
const askedFor = entry["askedFor"];
|
|
830
|
+
if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
|
|
831
|
+
errors.push(`${at} has an askedFor that is not a change request.`);
|
|
832
|
+
}
|
|
811
833
|
const tags = entry["tags"];
|
|
812
834
|
previews.push({
|
|
813
835
|
title: typeof title === "string" ? title : "",
|
|
@@ -816,7 +838,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
816
838
|
tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
|
|
817
839
|
...typeof branch === "string" ? { branch } : {},
|
|
818
840
|
...typeof file === "string" ? { file } : {},
|
|
819
|
-
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {}
|
|
841
|
+
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
|
|
842
|
+
...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
|
|
820
843
|
});
|
|
821
844
|
});
|
|
822
845
|
const devCommand = source["devCommand"];
|
|
@@ -932,6 +955,10 @@ import { spawn } from "child_process";
|
|
|
932
955
|
import { constants } from "fs";
|
|
933
956
|
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
934
957
|
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
958
|
+
var CODEX_WORKSPACE_CONFIG = [
|
|
959
|
+
"-c",
|
|
960
|
+
"sandbox_workspace_write.network_access=true"
|
|
961
|
+
];
|
|
935
962
|
var KNOWN_AGENTS = {
|
|
936
963
|
claude: {
|
|
937
964
|
name: "Claude",
|
|
@@ -962,6 +989,12 @@ var KNOWN_AGENTS = {
|
|
|
962
989
|
"--permission-mode",
|
|
963
990
|
"acceptEdits"
|
|
964
991
|
],
|
|
992
|
+
// Non-interactive Claude cannot approve a Bash call: acceptEdits covers
|
|
993
|
+
// files, so a command the prompt requires is refused every time with
|
|
994
|
+
// nobody there to say yes. This allows exactly that command and nothing
|
|
995
|
+
// wider. Codex needs no equivalent, because workspace-write already lets
|
|
996
|
+
// it run commands.
|
|
997
|
+
allowArgs: (command) => ["--allowedTools", `Bash(${command} *)`],
|
|
965
998
|
// Every stream-json event names its session.
|
|
966
999
|
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
967
1000
|
authArgs: ["auth", "status"],
|
|
@@ -982,15 +1015,41 @@ var KNOWN_AGENTS = {
|
|
|
982
1015
|
codex: {
|
|
983
1016
|
name: "Codex",
|
|
984
1017
|
binary: "codex",
|
|
985
|
-
|
|
986
|
-
|
|
1018
|
+
// `--skip-git-repo-check` is what lets Codex run at all in a project the
|
|
1019
|
+
// user never put under version control: without it codex-cli refuses
|
|
1020
|
+
// before it reaches a model, with "Not inside a trusted directory and
|
|
1021
|
+
// --skip-git-repo-check was not specified", and every Codex request in a
|
|
1022
|
+
// non-git project fails for a reason nothing in Leglas explained. The flag
|
|
1023
|
+
// moves that precondition and only that: `-s workspace-write` still
|
|
1024
|
+
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
1025
|
+
// and in a git repository the flag does nothing at all.
|
|
1026
|
+
args: (prompt) => [
|
|
1027
|
+
"exec",
|
|
1028
|
+
"--json",
|
|
1029
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
1030
|
+
"-s",
|
|
1031
|
+
"workspace-write",
|
|
1032
|
+
"--skip-git-repo-check",
|
|
1033
|
+
prompt
|
|
1034
|
+
],
|
|
1035
|
+
terminalArgs: (prompt) => [
|
|
1036
|
+
"exec",
|
|
1037
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
1038
|
+
"-s",
|
|
1039
|
+
"workspace-write",
|
|
1040
|
+
"--skip-git-repo-check",
|
|
1041
|
+
prompt
|
|
1042
|
+
],
|
|
987
1043
|
// No sandbox flag here: `codex exec resume` refuses it and inherits the
|
|
988
|
-
// session's own sandbox, which the first turn set to workspace-write.
|
|
1044
|
+
// session's own sandbox, which the first turn set to workspace-write. The
|
|
1045
|
+
// repository check is per invocation, so resume needs the flag of its own.
|
|
989
1046
|
resumeArgs: (sessionId, prompt) => [
|
|
990
1047
|
"exec",
|
|
991
1048
|
"resume",
|
|
992
1049
|
sessionId,
|
|
993
1050
|
"--json",
|
|
1051
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
1052
|
+
"--skip-git-repo-check",
|
|
994
1053
|
prompt
|
|
995
1054
|
],
|
|
996
1055
|
sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
|
|
@@ -1016,7 +1075,7 @@ var KNOWN_AGENTS = {
|
|
|
1016
1075
|
}
|
|
1017
1076
|
};
|
|
1018
1077
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
1019
|
-
function execProbe(binary, args) {
|
|
1078
|
+
function execProbe(binary, args, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
1020
1079
|
return new Promise((resolve) => {
|
|
1021
1080
|
let child;
|
|
1022
1081
|
try {
|
|
@@ -1029,7 +1088,10 @@ function execProbe(binary, args) {
|
|
|
1029
1088
|
if (stdout.length < 4096)
|
|
1030
1089
|
stdout += chunk.toString();
|
|
1031
1090
|
});
|
|
1032
|
-
const deadline = setTimeout(() =>
|
|
1091
|
+
const deadline = setTimeout(() => {
|
|
1092
|
+
child.kill("SIGKILL");
|
|
1093
|
+
resolve(null);
|
|
1094
|
+
}, timeoutMs);
|
|
1033
1095
|
child.once("error", () => {
|
|
1034
1096
|
clearTimeout(deadline);
|
|
1035
1097
|
resolve(null);
|
|
@@ -1098,10 +1160,10 @@ function shownCommand(value) {
|
|
|
1098
1160
|
function claudeActivity(event, cwd) {
|
|
1099
1161
|
if (event.type !== "assistant")
|
|
1100
1162
|
return null;
|
|
1101
|
-
const
|
|
1102
|
-
if (
|
|
1163
|
+
const message2 = record(event.message);
|
|
1164
|
+
if (message2 === null || !Array.isArray(message2.content))
|
|
1103
1165
|
return null;
|
|
1104
|
-
for (const rawBlock of
|
|
1166
|
+
for (const rawBlock of message2.content) {
|
|
1105
1167
|
const block = record(rawBlock);
|
|
1106
1168
|
if (block?.type !== "tool_use" || typeof block.name !== "string")
|
|
1107
1169
|
continue;
|
|
@@ -1170,6 +1232,25 @@ function sessionFrom(agent, line) {
|
|
|
1170
1232
|
return null;
|
|
1171
1233
|
return KNOWN_AGENTS[agent].sessionFrom(event);
|
|
1172
1234
|
}
|
|
1235
|
+
function retryFrom(agent, line) {
|
|
1236
|
+
if (agent !== "claude" && agent !== "cursor")
|
|
1237
|
+
return null;
|
|
1238
|
+
let event;
|
|
1239
|
+
try {
|
|
1240
|
+
event = record(JSON.parse(line));
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
if (event === null || event.type !== "system" || event.subtype !== "api_retry")
|
|
1245
|
+
return null;
|
|
1246
|
+
const attempt = typeof event.attempt === "number" ? event.attempt : 1;
|
|
1247
|
+
return {
|
|
1248
|
+
attempt,
|
|
1249
|
+
max: typeof event.max_retries === "number" ? event.max_retries : null,
|
|
1250
|
+
status: typeof event.error_status === "number" ? event.error_status : null,
|
|
1251
|
+
reason: typeof event.error === "string" && event.error !== "" ? event.error.toLowerCase() : null
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1173
1254
|
function isAgentChoice(value) {
|
|
1174
1255
|
return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
1175
1256
|
}
|
|
@@ -1313,8 +1394,8 @@ async function loadConfig(cwd) {
|
|
|
1313
1394
|
exported = module.default;
|
|
1314
1395
|
}
|
|
1315
1396
|
} catch (error) {
|
|
1316
|
-
const
|
|
1317
|
-
return { config: null, errors: [`${label} could not be loaded: ${
|
|
1397
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1398
|
+
return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
|
|
1318
1399
|
}
|
|
1319
1400
|
const result = normalizeConfig(exported);
|
|
1320
1401
|
return {
|
|
@@ -1333,8 +1414,17 @@ async function readLocalPreviews(cwd) {
|
|
|
1333
1414
|
let raw;
|
|
1334
1415
|
try {
|
|
1335
1416
|
raw = await readFile3(path, "utf8");
|
|
1336
|
-
} catch {
|
|
1337
|
-
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
1419
|
+
if (code === "ENOENT") {
|
|
1420
|
+
return { previews: [], errors: [] };
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
previews: [],
|
|
1424
|
+
errors: [
|
|
1425
|
+
`${LOCAL_PREVIEWS_PATH} could not be read (${code ?? "unknown error"}). Check its permissions and file type; nothing shared is lost.`
|
|
1426
|
+
]
|
|
1427
|
+
};
|
|
1338
1428
|
}
|
|
1339
1429
|
let parsed;
|
|
1340
1430
|
try {
|
|
@@ -1375,7 +1465,8 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1375
1465
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1376
1466
|
...input.branch === void 0 ? {} : { branch: input.branch },
|
|
1377
1467
|
...input.file === void 0 ? {} : { file: input.file },
|
|
1378
|
-
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
|
|
1468
|
+
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
|
|
1469
|
+
...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
|
|
1379
1470
|
};
|
|
1380
1471
|
const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
|
|
1381
1472
|
if (check.config === null) {
|
|
@@ -1607,17 +1698,220 @@ async function startAppProcess(options) {
|
|
|
1607
1698
|
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.`);
|
|
1608
1699
|
}
|
|
1609
1700
|
|
|
1610
|
-
// ../server/dist/
|
|
1611
|
-
|
|
1701
|
+
// ../server/dist/failure.js
|
|
1702
|
+
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
1703
|
+
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
1704
|
+
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;
|
|
1705
|
+
var LIMIT = /\b429\b|rate limit|usage limit|quota exceeded|too many requests/i;
|
|
1706
|
+
var OVERLOADED = /\b(?:503|529)\b|overloaded|service unavailable/i;
|
|
1707
|
+
function fromStatus(status, reason) {
|
|
1708
|
+
if (status === 401 || status === 403 || reason === "authentication_failed")
|
|
1709
|
+
return "not-signed-in";
|
|
1710
|
+
if (status === 429 || reason === "rate_limit")
|
|
1711
|
+
return "provider-limit";
|
|
1712
|
+
if (status === 529 || status === 503 || reason === "overloaded")
|
|
1713
|
+
return "provider-overloaded";
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
function fromLines(lines) {
|
|
1717
|
+
for (const line of [...lines].reverse()) {
|
|
1718
|
+
if (NEEDS_TRUST.test(line))
|
|
1719
|
+
return "needs-trust";
|
|
1720
|
+
if (NOT_SIGNED_IN.test(line))
|
|
1721
|
+
return "not-signed-in";
|
|
1722
|
+
if (LIMIT.test(line))
|
|
1723
|
+
return "provider-limit";
|
|
1724
|
+
if (OVERLOADED.test(line))
|
|
1725
|
+
return "provider-overloaded";
|
|
1726
|
+
}
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
function attempts(retry) {
|
|
1730
|
+
if (retry === null || retry === void 0)
|
|
1731
|
+
return "";
|
|
1732
|
+
const total = retry.max === null ? retry.attempt : Math.max(retry.attempt, retry.max);
|
|
1733
|
+
return ` It retried ${total} times first.`;
|
|
1734
|
+
}
|
|
1735
|
+
function message(code, input) {
|
|
1736
|
+
const agent = input.agent;
|
|
1737
|
+
switch (code) {
|
|
1738
|
+
case "cancelled":
|
|
1739
|
+
return "You stopped this run.";
|
|
1740
|
+
case "stopped":
|
|
1741
|
+
return "Leglas shut down while this was running.";
|
|
1742
|
+
case "missing-agent":
|
|
1743
|
+
return `${agent} could not be started. Its command is not on this machine's PATH any more.`;
|
|
1744
|
+
case "not-signed-in":
|
|
1745
|
+
return `${agent} is not signed in. Sign in to it in a terminal, then run this again.`;
|
|
1746
|
+
case "provider-overloaded":
|
|
1747
|
+
return `${agent}'s provider was overloaded and gave up.${attempts(input.retry)}`;
|
|
1748
|
+
case "provider-limit":
|
|
1749
|
+
return `${agent} reported a rate or usage limit, so nothing ran.`;
|
|
1750
|
+
case "needs-trust":
|
|
1751
|
+
return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
|
|
1752
|
+
case "not-registered":
|
|
1753
|
+
return `${agent} finished without registering the new direction, so nothing reached the rail. Its last output is in the Leglas terminal.`;
|
|
1754
|
+
case "agent-error":
|
|
1755
|
+
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.`;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function classifyFailure(input) {
|
|
1759
|
+
const lines = input.lines ?? [];
|
|
1760
|
+
const error = input.error ?? null;
|
|
1761
|
+
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";
|
|
1762
|
+
return { code, message: message(code, input) };
|
|
1763
|
+
}
|
|
1764
|
+
function sessionShaped(code) {
|
|
1765
|
+
return code === "agent-error";
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// ../server/dist/annotations.js
|
|
1612
1769
|
import { randomBytes } from "crypto";
|
|
1770
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1613
1771
|
import { dirname as dirname4, join as join5 } from "path";
|
|
1772
|
+
var ANNOTATIONS_PATH = ".leglas/annotations.json";
|
|
1773
|
+
var NOTE_CAP = 500;
|
|
1774
|
+
var SELECTOR_CAP = 300;
|
|
1775
|
+
var TEXT_CAP = 120;
|
|
1776
|
+
var TAG_CAP = 40;
|
|
1777
|
+
var CLASS_CAP = 8;
|
|
1778
|
+
var CLASS_LENGTH_CAP = 60;
|
|
1779
|
+
var COVERS_CAP = 8;
|
|
1780
|
+
function isRecord2(value) {
|
|
1781
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1782
|
+
}
|
|
1783
|
+
function text(value, cap) {
|
|
1784
|
+
return typeof value === "string" ? value.trim().slice(0, cap) : "";
|
|
1785
|
+
}
|
|
1786
|
+
function fraction(value) {
|
|
1787
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1788
|
+
return 0.5;
|
|
1789
|
+
return Math.min(1, Math.max(0, value));
|
|
1790
|
+
}
|
|
1791
|
+
function size(value) {
|
|
1792
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
|
|
1793
|
+
}
|
|
1794
|
+
function anchorFrom(value) {
|
|
1795
|
+
if (!isRecord2(value))
|
|
1796
|
+
return null;
|
|
1797
|
+
const selector = text(value["selector"], SELECTOR_CAP);
|
|
1798
|
+
if (selector === "")
|
|
1799
|
+
return null;
|
|
1800
|
+
const rect = isRecord2(value["rect"]) ? value["rect"] : {};
|
|
1801
|
+
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)) : [];
|
|
1802
|
+
const rawRegion = isRecord2(value["region"]) ? value["region"] : null;
|
|
1803
|
+
const region = rawRegion === null ? null : {
|
|
1804
|
+
height: fraction(rawRegion["height"]),
|
|
1805
|
+
width: fraction(rawRegion["width"]),
|
|
1806
|
+
x: fraction(rawRegion["x"]),
|
|
1807
|
+
y: fraction(rawRegion["y"])
|
|
1808
|
+
};
|
|
1809
|
+
const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
|
|
1810
|
+
tag: text(entry["tag"], TAG_CAP) || "element",
|
|
1811
|
+
text: text(entry["text"], TEXT_CAP)
|
|
1812
|
+
})) : [];
|
|
1813
|
+
return {
|
|
1814
|
+
classes,
|
|
1815
|
+
...covers.length === 0 ? {} : { covers },
|
|
1816
|
+
...region === null ? {} : { region },
|
|
1817
|
+
rect: {
|
|
1818
|
+
height: size(rect["height"]),
|
|
1819
|
+
width: size(rect["width"]),
|
|
1820
|
+
x: size(rect["x"]),
|
|
1821
|
+
y: size(rect["y"])
|
|
1822
|
+
},
|
|
1823
|
+
selector,
|
|
1824
|
+
spot: {
|
|
1825
|
+
x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
|
|
1826
|
+
y: fraction(isRecord2(value["spot"]) ? value["spot"]["y"] : void 0)
|
|
1827
|
+
},
|
|
1828
|
+
tag: text(value["tag"], TAG_CAP) || "element",
|
|
1829
|
+
text: text(value["text"], TEXT_CAP),
|
|
1830
|
+
viewport: size(value["viewport"])
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
async function readAnnotations(cwd) {
|
|
1834
|
+
try {
|
|
1835
|
+
const raw = await readFile4(join5(cwd, ANNOTATIONS_PATH), "utf8");
|
|
1836
|
+
const parsed = JSON.parse(raw);
|
|
1837
|
+
if (!Array.isArray(parsed.annotations))
|
|
1838
|
+
return [];
|
|
1839
|
+
return parsed.annotations.flatMap((entry, index) => {
|
|
1840
|
+
if (!isRecord2(entry))
|
|
1841
|
+
return [];
|
|
1842
|
+
const anchor = anchorFrom(entry["anchor"]);
|
|
1843
|
+
const title = text(entry["title"], TAG_CAP * 4);
|
|
1844
|
+
if (anchor === null || title === "")
|
|
1845
|
+
return [];
|
|
1846
|
+
return [
|
|
1847
|
+
{
|
|
1848
|
+
anchor,
|
|
1849
|
+
id: typeof entry["id"] === "string" ? entry["id"] : String(index),
|
|
1850
|
+
note: text(entry["note"], NOTE_CAP),
|
|
1851
|
+
title
|
|
1852
|
+
}
|
|
1853
|
+
];
|
|
1854
|
+
});
|
|
1855
|
+
} catch {
|
|
1856
|
+
return [];
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async function write(cwd, annotations) {
|
|
1860
|
+
const path = join5(cwd, ANNOTATIONS_PATH);
|
|
1861
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1862
|
+
await writeFile3(path, `${JSON.stringify({ annotations }, null, 2)}
|
|
1863
|
+
`, "utf8");
|
|
1864
|
+
}
|
|
1865
|
+
async function addAnnotation(cwd, input) {
|
|
1866
|
+
const annotation = { ...input, id: randomBytes(6).toString("base64url") };
|
|
1867
|
+
await write(cwd, [...await readAnnotations(cwd), annotation]);
|
|
1868
|
+
return annotation;
|
|
1869
|
+
}
|
|
1870
|
+
async function removeAnnotations(cwd, ids) {
|
|
1871
|
+
const wanted = new Set(ids);
|
|
1872
|
+
const annotations = await readAnnotations(cwd);
|
|
1873
|
+
const remaining = annotations.filter((entry) => !wanted.has(entry.id));
|
|
1874
|
+
const dropped = annotations.length - remaining.length;
|
|
1875
|
+
if (dropped > 0)
|
|
1876
|
+
await write(cwd, remaining);
|
|
1877
|
+
return dropped;
|
|
1878
|
+
}
|
|
1879
|
+
function annotationsFor(annotations, title) {
|
|
1880
|
+
return annotations.filter((entry) => entry.title === title);
|
|
1881
|
+
}
|
|
1882
|
+
function describeAnchor(anchor) {
|
|
1883
|
+
const where = `about ${anchor.rect.width}\xD7${anchor.rect.height} at (${anchor.rect.x}, ${anchor.rect.y}) in a ${anchor.viewport}px-wide viewport`;
|
|
1884
|
+
if (anchor.region !== void 0) {
|
|
1885
|
+
const covered = (anchor.covers ?? []).map((entry) => entry.text === "" ? `<${entry.tag}>` : `<${entry.tag}> \u201C${entry.text}\u201D`).join(", ");
|
|
1886
|
+
const inside = covered === "" ? "" : ` covering ${covered};`;
|
|
1887
|
+
return `an area inside <${anchor.tag}>;${inside} path ${anchor.selector}; ${where}`;
|
|
1888
|
+
}
|
|
1889
|
+
const parts = [`<${anchor.tag}>`];
|
|
1890
|
+
if (anchor.classes.length > 0)
|
|
1891
|
+
parts.push(`class "${anchor.classes.join(" ")}"`);
|
|
1892
|
+
if (anchor.text !== "")
|
|
1893
|
+
parts.push(`reading \u201C${anchor.text}\u201D`);
|
|
1894
|
+
return `${parts.join(", ")}; path ${anchor.selector}; ${where}`;
|
|
1895
|
+
}
|
|
1896
|
+
function describeAnnotations(annotations) {
|
|
1897
|
+
return annotations.map((annotation, index) => {
|
|
1898
|
+
const said = annotation.note === "" ? "Look at this." : annotation.note;
|
|
1899
|
+
return `${index + 1}. ${said}
|
|
1900
|
+
The element: ${describeAnchor(annotation.anchor)}`;
|
|
1901
|
+
}).join("\n\n");
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// ../server/dist/requests.js
|
|
1905
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1906
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1907
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1614
1908
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1615
|
-
function
|
|
1909
|
+
function variantSlot(url) {
|
|
1616
1910
|
if (!url.startsWith("/"))
|
|
1617
1911
|
return null;
|
|
1618
|
-
const query = url.slice(url.indexOf("?") + 1);
|
|
1619
1912
|
if (!url.includes("?"))
|
|
1620
1913
|
return null;
|
|
1914
|
+
const query = url.slice(url.indexOf("?") + 1);
|
|
1621
1915
|
for (const pair of query.split("&")) {
|
|
1622
1916
|
const [rawKey, rawValue] = pair.split("=");
|
|
1623
1917
|
if (rawKey === void 0 || rawValue === void 0)
|
|
@@ -1628,37 +1922,125 @@ function targetFor(url) {
|
|
|
1628
1922
|
const option = decodeURIComponent(rawValue);
|
|
1629
1923
|
if (!SAFE_SEGMENT.test(surface) || !SAFE_SEGMENT.test(option))
|
|
1630
1924
|
return null;
|
|
1631
|
-
return
|
|
1925
|
+
return { surface, option };
|
|
1632
1926
|
}
|
|
1633
1927
|
return null;
|
|
1634
1928
|
}
|
|
1635
|
-
function
|
|
1929
|
+
function targetFor(url) {
|
|
1930
|
+
const slot = variantSlot(url);
|
|
1931
|
+
return slot === null ? null : `.leglas/variants/${slot.surface}/${slot.option}.tsx`;
|
|
1932
|
+
}
|
|
1933
|
+
function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx -y leglas") {
|
|
1636
1934
|
const target = preview.file ?? targetFor(preview.url);
|
|
1637
1935
|
const cleaned = intent.trim();
|
|
1936
|
+
const asked = changeBlock(cleaned, notes);
|
|
1937
|
+
const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
|
|
1938
|
+
const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand) : replacePrompt(preview, asked, target);
|
|
1939
|
+
return { prompt, target, mode };
|
|
1940
|
+
}
|
|
1941
|
+
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.`;
|
|
1942
|
+
function changeBlock(cleaned, notes) {
|
|
1943
|
+
if (notes.length === 0)
|
|
1944
|
+
return `What to change: ${cleaned}`;
|
|
1945
|
+
const many = notes.length === 1 ? "a note" : `${notes.length} notes`;
|
|
1946
|
+
const lead = cleaned === "" ? `What to change, left as ${many} on the design itself:` : `What to change: ${cleaned}
|
|
1947
|
+
|
|
1948
|
+
And ${many} left on the design itself:`;
|
|
1949
|
+
return `${lead}
|
|
1950
|
+
|
|
1951
|
+
${describeAnnotations(notes)}
|
|
1952
|
+
|
|
1953
|
+
${ANCHORS}`;
|
|
1954
|
+
}
|
|
1955
|
+
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.
|
|
1956
|
+
|
|
1957
|
+
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.
|
|
1958
|
+
|
|
1959
|
+
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.`;
|
|
1960
|
+
function registrationCommand(leglasCommand) {
|
|
1961
|
+
return `${leglasCommand} add`;
|
|
1962
|
+
}
|
|
1963
|
+
function replacePrompt(preview, asked, target) {
|
|
1638
1964
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1639
1965
|
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
1640
|
-
|
|
1966
|
+
return `In this project, change only the "${preview.title}" design direction. ${where}
|
|
1967
|
+
|
|
1968
|
+
${asked}
|
|
1969
|
+
|
|
1970
|
+
${pace}${SCOPE} The direction is already registered, so nothing needs re-registering.`;
|
|
1971
|
+
}
|
|
1972
|
+
function variantPrompt(preview, recorded, asked, target, leglasCommand) {
|
|
1973
|
+
const slot = variantSlot(preview.url);
|
|
1974
|
+
const parent = JSON.stringify(preview.title);
|
|
1975
|
+
const askedFor = JSON.stringify(recorded);
|
|
1976
|
+
const add = registrationCommand(leglasCommand);
|
|
1977
|
+
const source = target === null ? `Find what renders it first.` : `Its source is ${target}.`;
|
|
1978
|
+
const [make, register] = preview.file !== void 0 ? [
|
|
1979
|
+
`Copy that file to a new file beside it and make the change in the copy.`,
|
|
1980
|
+
` ${add} --title "<name>" --file "<the new file>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1981
|
+
] : slot !== null ? [
|
|
1982
|
+
`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.`,
|
|
1983
|
+
` ${add} --title "<name>" --url "/?v-${slot.surface}=<key>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1984
|
+
] : [
|
|
1985
|
+
`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.`,
|
|
1986
|
+
` ${add} --title "<name>" --url "<the URL that shows it>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1987
|
+
];
|
|
1988
|
+
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.
|
|
1641
1989
|
|
|
1642
|
-
|
|
1990
|
+
${source} ${make}
|
|
1643
1991
|
|
|
1644
|
-
${
|
|
1992
|
+
${asked}
|
|
1993
|
+
|
|
1994
|
+
Then register it, which is what puts it on the rail:
|
|
1995
|
+
|
|
1996
|
+
${register}
|
|
1645
1997
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1998
|
+
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.
|
|
1999
|
+
|
|
2000
|
+
${SCOPE}`;
|
|
1648
2001
|
}
|
|
1649
2002
|
var REQUESTS_PATH = ".leglas/requests.json";
|
|
2003
|
+
var TERMINAL = ["failed", "cancelled"];
|
|
2004
|
+
function isTerminal(status) {
|
|
2005
|
+
return TERMINAL.includes(status);
|
|
2006
|
+
}
|
|
2007
|
+
var FAILURE_CODES = [
|
|
2008
|
+
"cancelled",
|
|
2009
|
+
"stopped",
|
|
2010
|
+
"missing-agent",
|
|
2011
|
+
"not-signed-in",
|
|
2012
|
+
"provider-overloaded",
|
|
2013
|
+
"provider-limit",
|
|
2014
|
+
"needs-trust",
|
|
2015
|
+
"not-registered",
|
|
2016
|
+
"agent-error"
|
|
2017
|
+
];
|
|
2018
|
+
function failureOf(value) {
|
|
2019
|
+
if (typeof value !== "object" || value === null)
|
|
2020
|
+
return null;
|
|
2021
|
+
const entry = value;
|
|
2022
|
+
if (typeof entry.message !== "string" || entry.message === "")
|
|
2023
|
+
return null;
|
|
2024
|
+
if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
|
|
2025
|
+
return null;
|
|
2026
|
+
return { code: entry.code, message: entry.message };
|
|
2027
|
+
}
|
|
1650
2028
|
async function readRequests(cwd) {
|
|
1651
2029
|
try {
|
|
1652
|
-
const raw = await
|
|
2030
|
+
const raw = await readFile5(join6(cwd, REQUESTS_PATH), "utf8");
|
|
1653
2031
|
const parsed = JSON.parse(raw);
|
|
1654
2032
|
if (!Array.isArray(parsed.requests))
|
|
1655
2033
|
return [];
|
|
1656
2034
|
return parsed.requests.map((request, index) => {
|
|
1657
|
-
const entry = request;
|
|
2035
|
+
const { failure: rawFailure, ...entry } = request;
|
|
2036
|
+
const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
|
|
2037
|
+
const failure = isTerminal(status) ? failureOf(rawFailure) : null;
|
|
1658
2038
|
return {
|
|
1659
2039
|
...entry,
|
|
1660
2040
|
id: typeof entry.id === "string" ? entry.id : String(index),
|
|
1661
|
-
status
|
|
2041
|
+
status,
|
|
2042
|
+
mode: entry.mode === "variant" ? "variant" : "replace",
|
|
2043
|
+
...failure === null ? {} : { failure }
|
|
1662
2044
|
};
|
|
1663
2045
|
});
|
|
1664
2046
|
} catch {
|
|
@@ -1666,23 +2048,23 @@ async function readRequests(cwd) {
|
|
|
1666
2048
|
}
|
|
1667
2049
|
}
|
|
1668
2050
|
async function writeQueue(cwd, requests) {
|
|
1669
|
-
const path =
|
|
1670
|
-
await
|
|
1671
|
-
await
|
|
2051
|
+
const path = join6(cwd, REQUESTS_PATH);
|
|
2052
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2053
|
+
await writeFile4(path, `${JSON.stringify({ requests }, null, 2)}
|
|
1672
2054
|
`, "utf8");
|
|
1673
2055
|
}
|
|
1674
2056
|
async function appendRequest(cwd, request) {
|
|
1675
2057
|
await writeQueue(cwd, [
|
|
1676
2058
|
...await readRequests(cwd),
|
|
1677
|
-
{ ...request, id:
|
|
2059
|
+
{ ...request, id: randomBytes2(6).toString("base64url"), status: "queued" }
|
|
1678
2060
|
]);
|
|
1679
2061
|
}
|
|
1680
2062
|
async function collectRequests(cwd) {
|
|
1681
2063
|
const requests = await readRequests(cwd);
|
|
1682
|
-
const collected = requests.map((request) => ({ ...request, status: "picked-up" })
|
|
1683
|
-
if (requests.some((request) => request.status
|
|
2064
|
+
const collected = requests.map((request) => isTerminal(request.status) ? request : { ...request, status: "picked-up" });
|
|
2065
|
+
if (requests.some((request) => request.status === "queued"))
|
|
1684
2066
|
await writeQueue(cwd, collected);
|
|
1685
|
-
return collected;
|
|
2067
|
+
return collected.filter((request) => !isTerminal(request.status));
|
|
1686
2068
|
}
|
|
1687
2069
|
async function markPickedUp(cwd, id) {
|
|
1688
2070
|
const requests = await readRequests(cwd);
|
|
@@ -1691,6 +2073,17 @@ async function markPickedUp(cwd, id) {
|
|
|
1691
2073
|
await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
|
|
1692
2074
|
return true;
|
|
1693
2075
|
}
|
|
2076
|
+
async function markFailed(cwd, id, failure) {
|
|
2077
|
+
const requests = await readRequests(cwd);
|
|
2078
|
+
if (!requests.some((request) => request.id === id))
|
|
2079
|
+
return false;
|
|
2080
|
+
await writeQueue(cwd, requests.map((request) => request.id === id ? {
|
|
2081
|
+
...request,
|
|
2082
|
+
status: failure.code === "cancelled" ? "cancelled" : "failed",
|
|
2083
|
+
failure
|
|
2084
|
+
} : request));
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
1694
2087
|
async function removeRequest(cwd, id) {
|
|
1695
2088
|
const requests = await readRequests(cwd);
|
|
1696
2089
|
const remaining = requests.filter((request) => request.id !== id);
|
|
@@ -1701,7 +2094,7 @@ async function removeRequest(cwd, id) {
|
|
|
1701
2094
|
}
|
|
1702
2095
|
async function clearRequests(cwd) {
|
|
1703
2096
|
const requests = await readRequests(cwd);
|
|
1704
|
-
const pending = requests.filter((request) => request.status
|
|
2097
|
+
const pending = requests.filter((request) => request.status === "queued");
|
|
1705
2098
|
const cleared = requests.length - pending.length;
|
|
1706
2099
|
if (cleared > 0)
|
|
1707
2100
|
await writeQueue(cwd, pending);
|
|
@@ -1710,10 +2103,13 @@ async function clearRequests(cwd) {
|
|
|
1710
2103
|
|
|
1711
2104
|
// ../server/dist/runner.js
|
|
1712
2105
|
import { spawn as nodeSpawn } from "child_process";
|
|
2106
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2107
|
+
import { join as join7 } from "path";
|
|
1713
2108
|
var POLL_MS = 2e3;
|
|
1714
2109
|
var OUTPUT_LINES = 20;
|
|
2110
|
+
var CANCEL_GRACE_MS = 5e3;
|
|
1715
2111
|
var SESSION_TURNS_CAP = 8;
|
|
1716
|
-
function resolveCommand(choice, prompt, sessionId = null) {
|
|
2112
|
+
function resolveCommand(choice, prompt, sessionId = null, registration = null) {
|
|
1717
2113
|
if (choice.agent === null)
|
|
1718
2114
|
return null;
|
|
1719
2115
|
if (choice.agent === "custom") {
|
|
@@ -1725,12 +2121,13 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1725
2121
|
return { agent: "custom", name: "Custom", ...commandFor(parsed.template, prompt), resumed: false };
|
|
1726
2122
|
}
|
|
1727
2123
|
const adapter = KNOWN_AGENTS[choice.agent];
|
|
2124
|
+
const allow = registration !== null && "allowArgs" in adapter ? adapter.allowArgs(registration) : [];
|
|
1728
2125
|
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1729
2126
|
return {
|
|
1730
2127
|
agent: choice.agent,
|
|
1731
2128
|
name: adapter.name,
|
|
1732
2129
|
command: adapter.binary,
|
|
1733
|
-
args: adapter.resumeArgs(sessionId, prompt),
|
|
2130
|
+
args: [...adapter.resumeArgs(sessionId, prompt), ...allow],
|
|
1734
2131
|
resumed: true
|
|
1735
2132
|
};
|
|
1736
2133
|
}
|
|
@@ -1738,7 +2135,7 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1738
2135
|
agent: choice.agent,
|
|
1739
2136
|
name: adapter.name,
|
|
1740
2137
|
command: adapter.binary,
|
|
1741
|
-
args: adapter.args(prompt),
|
|
2138
|
+
args: [...adapter.args(prompt), ...allow],
|
|
1742
2139
|
resumed: false
|
|
1743
2140
|
};
|
|
1744
2141
|
}
|
|
@@ -1768,12 +2165,17 @@ function startRunner(options) {
|
|
|
1768
2165
|
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1769
2166
|
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1770
2167
|
const failed = /* @__PURE__ */ new Set();
|
|
2168
|
+
const setLater = options.setTimeout ?? ((callback, milliseconds) => {
|
|
2169
|
+
setTimeout(callback, milliseconds).unref?.();
|
|
2170
|
+
});
|
|
1771
2171
|
let state = {
|
|
1772
2172
|
running: false,
|
|
1773
2173
|
requestId: null,
|
|
1774
2174
|
agent: null,
|
|
1775
2175
|
activity: null,
|
|
1776
|
-
startedAt: null
|
|
2176
|
+
startedAt: null,
|
|
2177
|
+
stopping: false,
|
|
2178
|
+
waiting: null
|
|
1777
2179
|
};
|
|
1778
2180
|
let stopped = false;
|
|
1779
2181
|
let ticking = null;
|
|
@@ -1781,15 +2183,30 @@ function startRunner(options) {
|
|
|
1781
2183
|
let active = null;
|
|
1782
2184
|
const sessions = /* @__PURE__ */ new Map();
|
|
1783
2185
|
const idle = () => {
|
|
1784
|
-
state = {
|
|
2186
|
+
state = {
|
|
2187
|
+
running: false,
|
|
2188
|
+
requestId: null,
|
|
2189
|
+
agent: null,
|
|
2190
|
+
activity: null,
|
|
2191
|
+
startedAt: null,
|
|
2192
|
+
stopping: false,
|
|
2193
|
+
waiting: null
|
|
2194
|
+
};
|
|
1785
2195
|
};
|
|
1786
2196
|
const rememberLine = (lines, line) => {
|
|
1787
2197
|
lines.push(line);
|
|
1788
2198
|
if (lines.length > OUTPUT_LINES)
|
|
1789
2199
|
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1790
2200
|
};
|
|
1791
|
-
const reportFailure = (request,
|
|
1792
|
-
|
|
2201
|
+
const reportFailure = async (request, failure, lines) => {
|
|
2202
|
+
await markFailed(options.cwd, request.id, failure).catch(() => {
|
|
2203
|
+
});
|
|
2204
|
+
failed.add(request.id);
|
|
2205
|
+
if (failure.code === "cancelled") {
|
|
2206
|
+
console.error(`Leglas stopped the run for ${request.title}.`);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
console.error(`Leglas agent failed for ${request.title}: ${failure.message}`);
|
|
1793
2210
|
for (const line of lines)
|
|
1794
2211
|
console.error(` ${line}`);
|
|
1795
2212
|
};
|
|
@@ -1807,19 +2224,31 @@ function startRunner(options) {
|
|
|
1807
2224
|
error: error instanceof Error ? error.message : String(error)
|
|
1808
2225
|
});
|
|
1809
2226
|
}
|
|
1810
|
-
const current = {
|
|
2227
|
+
const current = {
|
|
2228
|
+
child,
|
|
2229
|
+
requestId: request.id,
|
|
2230
|
+
cancelled: false,
|
|
2231
|
+
abandon: () => {
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
1811
2234
|
active = current;
|
|
1812
2235
|
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1813
2236
|
rememberLine(lines, line);
|
|
1814
2237
|
const sessionId = sessionFrom(resolved.agent, line);
|
|
1815
2238
|
if (sessionId !== null)
|
|
1816
2239
|
observed.sessionId = sessionId;
|
|
2240
|
+
const retry = retryFrom(resolved.agent, line);
|
|
2241
|
+
if (retry !== null) {
|
|
2242
|
+
observed.retry = retry;
|
|
2243
|
+
if (active === current)
|
|
2244
|
+
state = { ...state, waiting: retry };
|
|
2245
|
+
}
|
|
1817
2246
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1818
2247
|
if (activity !== null) {
|
|
1819
2248
|
if (activity.startsWith("editing"))
|
|
1820
2249
|
observed.edited = true;
|
|
1821
2250
|
if (active === current)
|
|
1822
|
-
state = { ...state, activity };
|
|
2251
|
+
state = { ...state, activity, waiting: null };
|
|
1823
2252
|
}
|
|
1824
2253
|
});
|
|
1825
2254
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
@@ -1833,6 +2262,7 @@ function startRunner(options) {
|
|
|
1833
2262
|
stderrFlush();
|
|
1834
2263
|
resolve(outcome);
|
|
1835
2264
|
};
|
|
2265
|
+
current.abandon = () => settle({ ok: false, error: "cancelled" });
|
|
1836
2266
|
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1837
2267
|
child.once("close", (code, signal) => {
|
|
1838
2268
|
if (current.cancelled)
|
|
@@ -1846,10 +2276,12 @@ function startRunner(options) {
|
|
|
1846
2276
|
active = null;
|
|
1847
2277
|
});
|
|
1848
2278
|
};
|
|
2279
|
+
const registered = () => readFile6(join7(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
1849
2280
|
const handle = async (request, choice) => {
|
|
1850
2281
|
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1851
2282
|
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1852
|
-
|
|
2283
|
+
const registration = request.mode === "variant" && options.leglasCommand !== void 0 ? registrationCommand(options.leglasCommand) : null;
|
|
2284
|
+
let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null, registration);
|
|
1853
2285
|
if (resolved === null)
|
|
1854
2286
|
return;
|
|
1855
2287
|
const lines = [];
|
|
@@ -1857,7 +2289,7 @@ function startRunner(options) {
|
|
|
1857
2289
|
if (!await markPickedUp(options.cwd, request.id))
|
|
1858
2290
|
return;
|
|
1859
2291
|
if (stopped) {
|
|
1860
|
-
|
|
2292
|
+
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
1861
2293
|
return;
|
|
1862
2294
|
}
|
|
1863
2295
|
state = {
|
|
@@ -1865,25 +2297,47 @@ function startRunner(options) {
|
|
|
1865
2297
|
requestId: request.id,
|
|
1866
2298
|
agent: resolved.name,
|
|
1867
2299
|
activity: null,
|
|
1868
|
-
startedAt: Date.now()
|
|
2300
|
+
startedAt: Date.now(),
|
|
2301
|
+
stopping: false,
|
|
2302
|
+
waiting: null
|
|
1869
2303
|
};
|
|
1870
|
-
const observed = {
|
|
2304
|
+
const observed = {
|
|
2305
|
+
sessionId: null,
|
|
2306
|
+
edited: false,
|
|
2307
|
+
retry: null
|
|
2308
|
+
};
|
|
2309
|
+
const before = request.mode === "variant" ? await registered() : null;
|
|
2310
|
+
const agent = resolved.name;
|
|
1871
2311
|
let outcome = await runChild(request, resolved, lines, observed);
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2312
|
+
const verdict = () => classifyFailure({
|
|
2313
|
+
agent,
|
|
2314
|
+
error: outcome.ok ? null : stopped && outcome.error === "cancelled" ? "stopped by shutdown" : outcome.error,
|
|
2315
|
+
exitCode: outcome.ok ? outcome.code : null,
|
|
2316
|
+
lines,
|
|
2317
|
+
retry: observed.retry
|
|
2318
|
+
});
|
|
2319
|
+
let failure = verdict();
|
|
2320
|
+
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
|
|
2321
|
+
// child settling and the retry starting finds no child to cancel, so
|
|
2322
|
+
// nothing says "cancelled". Stopped still means stopped.
|
|
1876
2323
|
!stopped) {
|
|
1877
2324
|
sessions.delete(resolved.agent);
|
|
1878
|
-
const cold = resolveCommand(choice, request.prompt);
|
|
2325
|
+
const cold = resolveCommand(choice, request.prompt, null, registration);
|
|
1879
2326
|
if (cold !== null) {
|
|
1880
2327
|
resolved = cold;
|
|
1881
2328
|
observed.sessionId = null;
|
|
1882
|
-
|
|
2329
|
+
observed.retry = null;
|
|
2330
|
+
state = { ...state, activity: null, waiting: null };
|
|
1883
2331
|
outcome = await runChild(request, resolved, lines, observed);
|
|
2332
|
+
failure = verdict();
|
|
1884
2333
|
}
|
|
1885
2334
|
}
|
|
1886
2335
|
if (outcome.ok && outcome.code === 0) {
|
|
2336
|
+
if (request.mode === "variant" && await registered() === before) {
|
|
2337
|
+
sessions.delete(resolved.agent);
|
|
2338
|
+
await reportFailure(request, classifyFailure({ agent, error: "not-registered" }), lines);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
1887
2341
|
if (observed.sessionId !== null) {
|
|
1888
2342
|
const previous = sessions.get(resolved.agent);
|
|
1889
2343
|
sessions.set(resolved.agent, {
|
|
@@ -1891,12 +2345,14 @@ function startRunner(options) {
|
|
|
1891
2345
|
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1892
2346
|
});
|
|
1893
2347
|
}
|
|
2348
|
+
if (request.mode === "replace" && request.notes !== void 0) {
|
|
2349
|
+
await removeAnnotations(options.cwd, request.notes).catch(() => 0);
|
|
2350
|
+
}
|
|
1894
2351
|
await removeRequest(options.cwd, request.id);
|
|
1895
2352
|
return;
|
|
1896
2353
|
}
|
|
1897
2354
|
sessions.delete(resolved.agent);
|
|
1898
|
-
|
|
1899
|
-
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
2355
|
+
await reportFailure(request, failure, lines);
|
|
1900
2356
|
} finally {
|
|
1901
2357
|
idle();
|
|
1902
2358
|
}
|
|
@@ -1930,12 +2386,23 @@ function startRunner(options) {
|
|
|
1930
2386
|
return false;
|
|
1931
2387
|
if (id !== void 0 && active.requestId !== id)
|
|
1932
2388
|
return false;
|
|
1933
|
-
|
|
1934
|
-
|
|
2389
|
+
const current = active;
|
|
2390
|
+
current.cancelled = true;
|
|
2391
|
+
failed.add(current.requestId);
|
|
2392
|
+
state = { ...state, stopping: true, waiting: null };
|
|
1935
2393
|
try {
|
|
1936
|
-
|
|
2394
|
+
current.child.kill("SIGTERM");
|
|
1937
2395
|
} catch {
|
|
1938
2396
|
}
|
|
2397
|
+
setLater(() => {
|
|
2398
|
+
if (active !== current)
|
|
2399
|
+
return;
|
|
2400
|
+
try {
|
|
2401
|
+
current.child.kill("SIGKILL");
|
|
2402
|
+
} catch {
|
|
2403
|
+
}
|
|
2404
|
+
current.abandon();
|
|
2405
|
+
}, CANCEL_GRACE_MS);
|
|
1939
2406
|
return true;
|
|
1940
2407
|
};
|
|
1941
2408
|
const stop = () => {
|
|
@@ -1960,12 +2427,12 @@ function startRunner(options) {
|
|
|
1960
2427
|
}
|
|
1961
2428
|
|
|
1962
2429
|
// ../server/dist/renames.js
|
|
1963
|
-
import { mkdir as
|
|
1964
|
-
import { dirname as
|
|
2430
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
2431
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1965
2432
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
1966
2433
|
async function readRenames(cwd) {
|
|
1967
2434
|
try {
|
|
1968
|
-
const raw = await
|
|
2435
|
+
const raw = await readFile7(join8(cwd, RENAMES_PATH), "utf8");
|
|
1969
2436
|
const parsed = JSON.parse(raw);
|
|
1970
2437
|
if (parsed.renames === null || typeof parsed.renames !== "object")
|
|
1971
2438
|
return {};
|
|
@@ -1975,9 +2442,9 @@ async function readRenames(cwd) {
|
|
|
1975
2442
|
}
|
|
1976
2443
|
}
|
|
1977
2444
|
async function writeRenames(cwd, renames) {
|
|
1978
|
-
const path =
|
|
1979
|
-
await
|
|
1980
|
-
await
|
|
2445
|
+
const path = join8(cwd, RENAMES_PATH);
|
|
2446
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2447
|
+
await writeFile5(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1981
2448
|
`, "utf8");
|
|
1982
2449
|
}
|
|
1983
2450
|
function resolveTitle(input, titles, renames) {
|
|
@@ -1995,7 +2462,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
1995
2462
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1996
2463
|
import http2 from "http";
|
|
1997
2464
|
import net3 from "net";
|
|
1998
|
-
import { extname, join as
|
|
2465
|
+
import { extname, join as join9, normalize, relative as relative3 } from "path";
|
|
1999
2466
|
var LEGLAS_PREFIX = "/leglas";
|
|
2000
2467
|
var DEFAULT_PORT = 4100;
|
|
2001
2468
|
var PORT_ATTEMPTS = 20;
|
|
@@ -2068,6 +2535,9 @@ function isTrustedMutation(req) {
|
|
|
2068
2535
|
return false;
|
|
2069
2536
|
}
|
|
2070
2537
|
}
|
|
2538
|
+
function isEnded(request, failedIds) {
|
|
2539
|
+
return isTerminal(request.status) || failedIds.includes(request.id);
|
|
2540
|
+
}
|
|
2071
2541
|
function hasJsonBody(req) {
|
|
2072
2542
|
const contentType = req.headers["content-type"];
|
|
2073
2543
|
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
@@ -2094,7 +2564,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
2094
2564
|
}
|
|
2095
2565
|
function serveFrom(res, dir, relativePath) {
|
|
2096
2566
|
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
2097
|
-
const candidate =
|
|
2567
|
+
const candidate = join9(dir, relative5);
|
|
2098
2568
|
if (!candidate.startsWith(dir))
|
|
2099
2569
|
return false;
|
|
2100
2570
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -2179,7 +2649,7 @@ async function bind(server, requested) {
|
|
|
2179
2649
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
2180
2650
|
}
|
|
2181
2651
|
async function startServer(options) {
|
|
2182
|
-
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
2652
|
+
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
2183
2653
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
2184
2654
|
const proxy = createProxyHandler({ target });
|
|
2185
2655
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
@@ -2219,13 +2689,23 @@ async function startServer(options) {
|
|
|
2219
2689
|
const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
|
|
2220
2690
|
if (notice !== null)
|
|
2221
2691
|
errors.push(notice);
|
|
2222
|
-
return void readLocalPreviews(cwd).then(({ previews: local }) => {
|
|
2223
|
-
|
|
2692
|
+
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
2693
|
+
if (localErrors.length > 0) {
|
|
2694
|
+
return sendJson(res, 200, {
|
|
2695
|
+
project,
|
|
2696
|
+
devServer: target,
|
|
2697
|
+
previews: boot,
|
|
2698
|
+
errors
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
const localTitles = new Set(local.map((preview) => preview.title));
|
|
2702
|
+
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
2703
|
+
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
2224
2704
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
2225
2705
|
sendJson(res, 200, {
|
|
2226
2706
|
project,
|
|
2227
2707
|
devServer: target,
|
|
2228
|
-
previews: [...
|
|
2708
|
+
previews: [...currentBoot, ...fresh],
|
|
2229
2709
|
errors
|
|
2230
2710
|
});
|
|
2231
2711
|
}).catch(() => sendJson(res, 200, {
|
|
@@ -2235,6 +2715,47 @@ async function startServer(options) {
|
|
|
2235
2715
|
errors
|
|
2236
2716
|
}));
|
|
2237
2717
|
}
|
|
2718
|
+
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
2719
|
+
let body = "";
|
|
2720
|
+
req.on("data", (chunk) => body += chunk);
|
|
2721
|
+
return void req.on("end", async () => {
|
|
2722
|
+
let parsed;
|
|
2723
|
+
try {
|
|
2724
|
+
parsed = JSON.parse(body || "{}");
|
|
2725
|
+
} catch {
|
|
2726
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2727
|
+
}
|
|
2728
|
+
const titles = parsed.titles;
|
|
2729
|
+
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
2730
|
+
return sendJson(res, 400, {
|
|
2731
|
+
ok: false,
|
|
2732
|
+
error: "Body needs a non-empty array of direction titles."
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
const unique = [...new Set(titles)];
|
|
2736
|
+
try {
|
|
2737
|
+
const local = await readLocalPreviews(cwd);
|
|
2738
|
+
if (local.errors.length > 0) {
|
|
2739
|
+
return sendJson(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
2740
|
+
}
|
|
2741
|
+
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
2742
|
+
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
2743
|
+
if (unknown.length > 0) {
|
|
2744
|
+
return sendJson(res, 400, {
|
|
2745
|
+
ok: false,
|
|
2746
|
+
error: "Only machine-local directions can be deleted from the registry."
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
const deleted = await dropLocalPreviews(cwd, unique);
|
|
2750
|
+
return sendJson(res, 200, { ok: true, deleted });
|
|
2751
|
+
} catch {
|
|
2752
|
+
return sendJson(res, 500, {
|
|
2753
|
+
ok: false,
|
|
2754
|
+
error: "The directions could not be deleted from Leglas."
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2238
2759
|
if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
|
|
2239
2760
|
let body = "";
|
|
2240
2761
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -2245,16 +2766,51 @@ async function startServer(options) {
|
|
|
2245
2766
|
} catch {
|
|
2246
2767
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2247
2768
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2769
|
+
if (parsed.mode !== void 0 && parsed.mode !== "variant" && parsed.mode !== "replace") {
|
|
2770
|
+
return sendJson(res, 400, {
|
|
2771
|
+
ok: false,
|
|
2772
|
+
error: 'mode must be "variant" or "replace".'
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
const mode = parsed.mode === "replace" ? "replace" : "variant";
|
|
2776
|
+
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
2777
|
+
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
2778
|
+
const localTitles = new Set(local.map((entry) => entry.title));
|
|
2779
|
+
const bootConfig = config?.previews ?? [];
|
|
2780
|
+
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
2781
|
+
const preview = [...boot, ...local].find((entry) => entry.title === parsed.title);
|
|
2782
|
+
if (!preview) {
|
|
2783
|
+
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2784
|
+
}
|
|
2785
|
+
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
2786
|
+
if (!parsed.intent?.trim() && notes.length === 0) {
|
|
2251
2787
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2252
2788
|
}
|
|
2253
|
-
const
|
|
2789
|
+
const intent = (parsed.intent ?? "").trim();
|
|
2790
|
+
const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
2791
|
+
const sameNotes = (entry) => {
|
|
2792
|
+
const before = [...entry.notes ?? []].sort().join(",");
|
|
2793
|
+
return before === notes.map((note) => note.id).sort().join(",");
|
|
2794
|
+
};
|
|
2795
|
+
if (live.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
2796
|
+
// one forks the direction and the other rewrites it. Only a
|
|
2797
|
+
// genuine repeat is refused.
|
|
2798
|
+
(entry.mode ?? "replace") === mode && sameNotes(entry))) {
|
|
2799
|
+
return sendJson(res, 409, {
|
|
2800
|
+
ok: false,
|
|
2801
|
+
duplicate: true,
|
|
2802
|
+
error: `That exact change to ${preview.title} is already waiting.`
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
const composed = composeRequest(preview, intent, mode, notes, leglasCommand);
|
|
2254
2806
|
void appendRequest(cwd, {
|
|
2255
2807
|
title: preview.title,
|
|
2256
2808
|
url: preview.url,
|
|
2257
|
-
intent
|
|
2809
|
+
intent,
|
|
2810
|
+
// The ids travel with the request so a change made in place can
|
|
2811
|
+
// forget the notes it answered. A fork leaves them where they are:
|
|
2812
|
+
// the direction they point at was not touched.
|
|
2813
|
+
...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
|
|
2258
2814
|
...composed
|
|
2259
2815
|
}).then(() => {
|
|
2260
2816
|
runner?.nudge();
|
|
@@ -2330,21 +2886,33 @@ async function startServer(options) {
|
|
|
2330
2886
|
agent: null,
|
|
2331
2887
|
activity: null,
|
|
2332
2888
|
startedAt: null,
|
|
2889
|
+
stopping: false,
|
|
2890
|
+
waiting: null,
|
|
2333
2891
|
failedIds: []
|
|
2334
2892
|
};
|
|
2335
2893
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
2336
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
2894
|
+
requests: requests.map(({ id, title, intent, status, failure }) => ({
|
|
2337
2895
|
id,
|
|
2338
2896
|
title,
|
|
2339
2897
|
intent,
|
|
2340
|
-
|
|
2898
|
+
// The run in flight is the one thing the file cannot know. After
|
|
2899
|
+
// that the file is the record, including across a restart, and the
|
|
2900
|
+
// process-local failed set only covers a request whose verdict
|
|
2901
|
+
// could not be written.
|
|
2902
|
+
status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
|
|
2903
|
+
failure: failure ?? null
|
|
2341
2904
|
})),
|
|
2342
2905
|
agent: {
|
|
2343
2906
|
attached: externallyAttached(),
|
|
2344
2907
|
running: snapshot.running,
|
|
2345
2908
|
name: snapshot.running ? snapshot.agent : null,
|
|
2346
2909
|
activity: snapshot.running ? snapshot.activity : null,
|
|
2347
|
-
startedAt: snapshot.running ? snapshot.startedAt : null
|
|
2910
|
+
startedAt: snapshot.running ? snapshot.startedAt : null,
|
|
2911
|
+
// A stop that has been asked for but not yet obeyed. The card
|
|
2912
|
+
// says so rather than going on describing a live run.
|
|
2913
|
+
stopping: snapshot.running && snapshot.stopping,
|
|
2914
|
+
// Why a run that looks stalled is stalled, while it is stalled.
|
|
2915
|
+
waiting: snapshot.running ? snapshot.waiting : null
|
|
2348
2916
|
}
|
|
2349
2917
|
}));
|
|
2350
2918
|
}
|
|
@@ -2387,8 +2955,8 @@ async function startServer(options) {
|
|
|
2387
2955
|
if (request === void 0) {
|
|
2388
2956
|
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2389
2957
|
}
|
|
2390
|
-
if (!(runner?.snapshot().failedIds
|
|
2391
|
-
return sendJson(res, 400, { ok: false, error: "Only
|
|
2958
|
+
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
2959
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
2392
2960
|
}
|
|
2393
2961
|
try {
|
|
2394
2962
|
if (!await removeRequest(cwd, request.id)) {
|
|
@@ -2399,7 +2967,11 @@ async function startServer(options) {
|
|
|
2399
2967
|
url: request.url,
|
|
2400
2968
|
intent: request.intent,
|
|
2401
2969
|
target: request.target,
|
|
2402
|
-
prompt: request.prompt
|
|
2970
|
+
prompt: request.prompt,
|
|
2971
|
+
// The stored prompt already carries the mode's instructions; the
|
|
2972
|
+
// field travels with it so the queue keeps saying which kind of
|
|
2973
|
+
// change this is.
|
|
2974
|
+
...request.mode === void 0 ? {} : { mode: request.mode }
|
|
2403
2975
|
});
|
|
2404
2976
|
runner?.nudge();
|
|
2405
2977
|
return sendJson(res, 200, { ok: true });
|
|
@@ -2408,6 +2980,65 @@ async function startServer(options) {
|
|
|
2408
2980
|
}
|
|
2409
2981
|
});
|
|
2410
2982
|
}
|
|
2983
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
2984
|
+
return void readAnnotations(cwd).then((annotations) => sendJson(res, 200, { annotations }));
|
|
2985
|
+
}
|
|
2986
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
2987
|
+
if (!hasJsonBody(req)) {
|
|
2988
|
+
return sendJson(res, 400, { ok: false, error: "A note must be JSON." });
|
|
2989
|
+
}
|
|
2990
|
+
let body = "";
|
|
2991
|
+
req.on("data", (chunk) => body += chunk);
|
|
2992
|
+
return void req.on("end", async () => {
|
|
2993
|
+
let parsed;
|
|
2994
|
+
try {
|
|
2995
|
+
parsed = JSON.parse(body || "{}");
|
|
2996
|
+
} catch {
|
|
2997
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2998
|
+
}
|
|
2999
|
+
if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
|
|
3000
|
+
return sendJson(res, 400, { ok: false, error: "A note needs a direction." });
|
|
3001
|
+
}
|
|
3002
|
+
const anchor = anchorFrom(parsed.anchor);
|
|
3003
|
+
if (anchor === null) {
|
|
3004
|
+
return sendJson(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
3005
|
+
}
|
|
3006
|
+
try {
|
|
3007
|
+
const annotation = await addAnnotation(cwd, {
|
|
3008
|
+
anchor,
|
|
3009
|
+
note: typeof parsed.note === "string" ? parsed.note.trim() : "",
|
|
3010
|
+
title: parsed.title
|
|
3011
|
+
});
|
|
3012
|
+
return sendJson(res, 200, { ok: true, annotation });
|
|
3013
|
+
} catch {
|
|
3014
|
+
return sendJson(res, 500, { ok: false, error: "The note could not be kept." });
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
3019
|
+
if (!hasJsonBody(req)) {
|
|
3020
|
+
return sendJson(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
3021
|
+
}
|
|
3022
|
+
let body = "";
|
|
3023
|
+
req.on("data", (chunk) => body += chunk);
|
|
3024
|
+
return void req.on("end", async () => {
|
|
3025
|
+
let parsed;
|
|
3026
|
+
try {
|
|
3027
|
+
parsed = JSON.parse(body || "{}");
|
|
3028
|
+
} catch {
|
|
3029
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
3030
|
+
}
|
|
3031
|
+
const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
|
|
3032
|
+
if (ids.length === 0) {
|
|
3033
|
+
return sendJson(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
3034
|
+
}
|
|
3035
|
+
try {
|
|
3036
|
+
return sendJson(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
3037
|
+
} catch {
|
|
3038
|
+
return sendJson(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
2411
3042
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2412
3043
|
if (!hasJsonBody(req)) {
|
|
2413
3044
|
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
@@ -2424,8 +3055,9 @@ async function startServer(options) {
|
|
|
2424
3055
|
if (typeof parsed.id !== "string") {
|
|
2425
3056
|
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2426
3057
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
3058
|
+
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
3059
|
+
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
3060
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
2429
3061
|
}
|
|
2430
3062
|
try {
|
|
2431
3063
|
if (!await removeRequest(cwd, parsed.id)) {
|
|
@@ -2500,7 +3132,7 @@ async function startServer(options) {
|
|
|
2500
3132
|
proxy.upgrade(req, socket, head);
|
|
2501
3133
|
});
|
|
2502
3134
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2503
|
-
runner = startRunner({ cwd, externallyAttached });
|
|
3135
|
+
runner = startRunner({ cwd, externallyAttached, leglasCommand });
|
|
2504
3136
|
let closePromise = null;
|
|
2505
3137
|
return {
|
|
2506
3138
|
port,
|
|
@@ -2575,11 +3207,11 @@ function planKeep(options) {
|
|
|
2575
3207
|
}
|
|
2576
3208
|
|
|
2577
3209
|
// src/run-init.ts
|
|
2578
|
-
import { readFile as
|
|
2579
|
-
import { join as
|
|
3210
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
3211
|
+
import { join as join10 } from "path";
|
|
2580
3212
|
async function readIfPresent(path) {
|
|
2581
3213
|
try {
|
|
2582
|
-
return await
|
|
3214
|
+
return await readFile8(path, "utf8");
|
|
2583
3215
|
} catch {
|
|
2584
3216
|
return null;
|
|
2585
3217
|
}
|
|
@@ -2587,18 +3219,18 @@ async function readIfPresent(path) {
|
|
|
2587
3219
|
async function runInit(options, deps) {
|
|
2588
3220
|
const existingConfig = findConfigFile(options.cwd);
|
|
2589
3221
|
const plan = planInit({
|
|
2590
|
-
agents: await readIfPresent(
|
|
3222
|
+
agents: await readIfPresent(join10(options.cwd, "AGENTS.md")),
|
|
2591
3223
|
config: existingConfig === null ? null : "present",
|
|
2592
|
-
gitignore: await readIfPresent(
|
|
3224
|
+
gitignore: await readIfPresent(join10(options.cwd, ".gitignore")),
|
|
2593
3225
|
force: options.force
|
|
2594
3226
|
});
|
|
2595
3227
|
const touched = [];
|
|
2596
|
-
for (const
|
|
2597
|
-
await
|
|
2598
|
-
touched.push(
|
|
3228
|
+
for (const write2 of plan.writes) {
|
|
3229
|
+
await writeFile6(join10(options.cwd, write2.path), write2.contents, "utf8");
|
|
3230
|
+
touched.push(write2.path);
|
|
2599
3231
|
}
|
|
2600
3232
|
if (plan.gitignore !== null) {
|
|
2601
|
-
await
|
|
3233
|
+
await writeFile6(join10(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2602
3234
|
touched.push(".gitignore");
|
|
2603
3235
|
}
|
|
2604
3236
|
if (options.json) {
|
|
@@ -2618,8 +3250,8 @@ async function runInit(options, deps) {
|
|
|
2618
3250
|
|
|
2619
3251
|
// src/run-keep.ts
|
|
2620
3252
|
import { existsSync as existsSync3 } from "fs";
|
|
2621
|
-
import { mkdir as
|
|
2622
|
-
import { dirname as
|
|
3253
|
+
import { mkdir as mkdir6, readFile as readFile9, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
3254
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
2623
3255
|
|
|
2624
3256
|
// src/resolve-title.ts
|
|
2625
3257
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -2663,18 +3295,18 @@ async function runKeep(options, deps) {
|
|
|
2663
3295
|
if (!resolved.ok) return fail(resolved.error);
|
|
2664
3296
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
2665
3297
|
if (!plan.ok) return fail(plan.error);
|
|
2666
|
-
const from =
|
|
2667
|
-
const to =
|
|
3298
|
+
const from = join11(options.cwd, plan.move.from);
|
|
3299
|
+
const to = join11(options.cwd, plan.move.to);
|
|
2668
3300
|
if (!existsSync3(from)) {
|
|
2669
3301
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
2670
3302
|
}
|
|
2671
3303
|
if (existsSync3(to)) {
|
|
2672
3304
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
2673
3305
|
}
|
|
2674
|
-
const source = await
|
|
2675
|
-
await
|
|
2676
|
-
await
|
|
2677
|
-
await rm2(
|
|
3306
|
+
const source = await readFile9(from, "utf8");
|
|
3307
|
+
await mkdir6(dirname7(to), { recursive: true });
|
|
3308
|
+
await writeFile7(to, renameExport(source, plan.exportName), "utf8");
|
|
3309
|
+
await rm2(join11(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
2678
3310
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
2679
3311
|
if (options.json) {
|
|
2680
3312
|
deps.log(
|
|
@@ -2709,11 +3341,11 @@ async function runKeep(options, deps) {
|
|
|
2709
3341
|
|
|
2710
3342
|
// src/run-new.ts
|
|
2711
3343
|
import { existsSync as existsSync4 } from "fs";
|
|
2712
|
-
import { mkdir as
|
|
2713
|
-
import { dirname as
|
|
3344
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
3345
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
2714
3346
|
async function readIfPresent2(path) {
|
|
2715
3347
|
try {
|
|
2716
|
-
return await
|
|
3348
|
+
return await readFile10(path, "utf8");
|
|
2717
3349
|
} catch {
|
|
2718
3350
|
return null;
|
|
2719
3351
|
}
|
|
@@ -2721,19 +3353,19 @@ async function readIfPresent2(path) {
|
|
|
2721
3353
|
async function runNew(options, deps) {
|
|
2722
3354
|
let from;
|
|
2723
3355
|
if (options.from !== void 0) {
|
|
2724
|
-
const contents = await readIfPresent2(
|
|
3356
|
+
const contents = await readIfPresent2(join12(options.cwd, options.from));
|
|
2725
3357
|
if (contents === null) {
|
|
2726
|
-
const
|
|
2727
|
-
if (options.json) deps.log(JSON.stringify({ ok: false, error:
|
|
2728
|
-
else deps.log(
|
|
3358
|
+
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
3359
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
3360
|
+
else deps.log(message2);
|
|
2729
3361
|
return { exitCode: 1, written: [] };
|
|
2730
3362
|
}
|
|
2731
3363
|
from = { path: options.from, contents };
|
|
2732
3364
|
}
|
|
2733
3365
|
const plan = planNew({
|
|
2734
3366
|
surface: options.surface,
|
|
2735
|
-
packageJson: await readIfPresent2(
|
|
2736
|
-
gitignore: await readIfPresent2(
|
|
3367
|
+
packageJson: await readIfPresent2(join12(options.cwd, "package.json")),
|
|
3368
|
+
gitignore: await readIfPresent2(join12(options.cwd, ".gitignore")),
|
|
2737
3369
|
from
|
|
2738
3370
|
});
|
|
2739
3371
|
const fail = (error) => {
|
|
@@ -2749,26 +3381,26 @@ async function runNew(options, deps) {
|
|
|
2749
3381
|
deps.log(JSON.stringify({ ok: true, files: plan.writes, instructions: plan.instructions, previews: plan.previews }));
|
|
2750
3382
|
return { exitCode: 0, written: [] };
|
|
2751
3383
|
}
|
|
2752
|
-
for (const
|
|
2753
|
-
deps.log(`--- ${
|
|
2754
|
-
deps.log(
|
|
3384
|
+
for (const write2 of plan.writes) {
|
|
3385
|
+
deps.log(`--- ${write2.path}`);
|
|
3386
|
+
deps.log(write2.contents);
|
|
2755
3387
|
}
|
|
2756
3388
|
deps.log(plan.instructions);
|
|
2757
3389
|
return { exitCode: 0, written: [] };
|
|
2758
3390
|
}
|
|
2759
|
-
const existing = plan.writes.filter((
|
|
3391
|
+
const existing = plan.writes.filter((write2) => existsSync4(join12(options.cwd, write2.path)));
|
|
2760
3392
|
if (existing.length > 0) {
|
|
2761
3393
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
2762
3394
|
}
|
|
2763
3395
|
const written = [];
|
|
2764
|
-
for (const
|
|
2765
|
-
const target =
|
|
2766
|
-
await
|
|
2767
|
-
await
|
|
2768
|
-
written.push(
|
|
3396
|
+
for (const write2 of plan.writes) {
|
|
3397
|
+
const target = join12(options.cwd, write2.path);
|
|
3398
|
+
await mkdir7(dirname8(target), { recursive: true });
|
|
3399
|
+
await writeFile8(target, write2.contents, "utf8");
|
|
3400
|
+
written.push(write2.path);
|
|
2769
3401
|
}
|
|
2770
3402
|
if (plan.gitignore !== null) {
|
|
2771
|
-
await
|
|
3403
|
+
await writeFile8(join12(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2772
3404
|
written.push(".gitignore");
|
|
2773
3405
|
}
|
|
2774
3406
|
if (options.json) {
|
|
@@ -2789,21 +3421,21 @@ async function runNew(options, deps) {
|
|
|
2789
3421
|
}
|
|
2790
3422
|
|
|
2791
3423
|
// src/run-previews.ts
|
|
2792
|
-
import { readFile as
|
|
2793
|
-
import { join as
|
|
3424
|
+
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
3425
|
+
import { join as join13 } from "path";
|
|
2794
3426
|
function envelope(deps, ok, body) {
|
|
2795
3427
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
2796
3428
|
}
|
|
2797
3429
|
async function ensureIgnored(cwd) {
|
|
2798
|
-
const path =
|
|
3430
|
+
const path = join13(cwd, ".gitignore");
|
|
2799
3431
|
let current = null;
|
|
2800
3432
|
try {
|
|
2801
|
-
current = await
|
|
3433
|
+
current = await readFile11(path, "utf8");
|
|
2802
3434
|
} catch {
|
|
2803
3435
|
current = null;
|
|
2804
3436
|
}
|
|
2805
3437
|
const next = ignoreEntry(current);
|
|
2806
|
-
if (next !== null) await
|
|
3438
|
+
if (next !== null) await writeFile9(path, next, "utf8");
|
|
2807
3439
|
}
|
|
2808
3440
|
async function runAdd(options, deps) {
|
|
2809
3441
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2827,7 +3459,8 @@ async function runAdd(options, deps) {
|
|
|
2827
3459
|
tags: options.preview.tags,
|
|
2828
3460
|
branch: options.preview.branch,
|
|
2829
3461
|
file: options.preview.file,
|
|
2830
|
-
basedOn: options.preview.basedOn
|
|
3462
|
+
basedOn: options.preview.basedOn,
|
|
3463
|
+
askedFor: options.preview.askedFor
|
|
2831
3464
|
},
|
|
2832
3465
|
shared
|
|
2833
3466
|
);
|
|
@@ -2887,6 +3520,7 @@ async function runList(options, deps) {
|
|
|
2887
3520
|
note: preview.note ?? null,
|
|
2888
3521
|
tags: preview.tags,
|
|
2889
3522
|
basedOn: preview.basedOn ?? null,
|
|
3523
|
+
askedFor: preview.askedFor ?? null,
|
|
2890
3524
|
local: preview.local,
|
|
2891
3525
|
branch: preview.branch ?? null,
|
|
2892
3526
|
file: preview.file ?? null
|
|
@@ -3049,23 +3683,23 @@ async function runShow(options, deps) {
|
|
|
3049
3683
|
|
|
3050
3684
|
// src/run-watch.ts
|
|
3051
3685
|
import { spawn as spawn3 } from "child_process";
|
|
3052
|
-
import { mkdir as
|
|
3053
|
-
import { dirname as
|
|
3686
|
+
import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
|
|
3687
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
3054
3688
|
var POLL_MS2 = 2e3;
|
|
3055
3689
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
3056
3690
|
async function saveTemplate(cwd, run3) {
|
|
3057
|
-
const path =
|
|
3691
|
+
const path = join14(cwd, WATCH_PATH);
|
|
3058
3692
|
let config = {};
|
|
3059
3693
|
try {
|
|
3060
|
-
const parsed = JSON.parse(await
|
|
3694
|
+
const parsed = JSON.parse(await readFile12(path, "utf8"));
|
|
3061
3695
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
3062
3696
|
config = parsed;
|
|
3063
3697
|
}
|
|
3064
3698
|
} catch {
|
|
3065
3699
|
}
|
|
3066
3700
|
config.run = run3;
|
|
3067
|
-
await
|
|
3068
|
-
await
|
|
3701
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3702
|
+
await writeFile10(path, `${JSON.stringify(config, null, 2)}
|
|
3069
3703
|
`, "utf8");
|
|
3070
3704
|
}
|
|
3071
3705
|
function spawnAgent(command, args, cwd) {
|
|
@@ -3151,9 +3785,13 @@ async function runWatch(options, deps) {
|
|
|
3151
3785
|
return;
|
|
3152
3786
|
}
|
|
3153
3787
|
failed.add(request.id);
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3788
|
+
const failure = classifyFailure({
|
|
3789
|
+
agent: (shownCommand2.split(/\s+/)[0] ?? command).split("/").pop() ?? command,
|
|
3790
|
+
error: outcome.ok ? null : outcome.error,
|
|
3791
|
+
exitCode: outcome.ok ? outcome.code : null
|
|
3792
|
+
});
|
|
3793
|
+
await markFailed(options.cwd, request.id, failure);
|
|
3794
|
+
deps.error(` failed ${request.title}: ${failure.message}`);
|
|
3157
3795
|
deps.error(" Left in the queue and not retried.");
|
|
3158
3796
|
};
|
|
3159
3797
|
const tick = async () => {
|
|
@@ -3199,12 +3837,12 @@ async function runWatch(options, deps) {
|
|
|
3199
3837
|
|
|
3200
3838
|
// src/run-classify.ts
|
|
3201
3839
|
import { stat } from "fs/promises";
|
|
3202
|
-
import { join as
|
|
3840
|
+
import { join as join15 } from "path";
|
|
3203
3841
|
async function runClassify(options, deps) {
|
|
3204
3842
|
const declared = await Promise.all(
|
|
3205
3843
|
options.changes.map(async (change) => ({
|
|
3206
3844
|
...change,
|
|
3207
|
-
exists: await stat(
|
|
3845
|
+
exists: await stat(join15(options.cwd, change.path)).then(
|
|
3208
3846
|
() => true,
|
|
3209
3847
|
() => false
|
|
3210
3848
|
)
|
|
@@ -3232,18 +3870,28 @@ async function runClassify(options, deps) {
|
|
|
3232
3870
|
// src/run.ts
|
|
3233
3871
|
import { existsSync as existsSync5 } from "fs";
|
|
3234
3872
|
import { createRequire } from "module";
|
|
3235
|
-
import { basename as basename3, dirname as
|
|
3873
|
+
import { basename as basename3, dirname as dirname10, join as join16, relative as relative4 } from "path";
|
|
3236
3874
|
import { fileURLToPath } from "url";
|
|
3237
3875
|
function findShellDir() {
|
|
3238
|
-
const bundled =
|
|
3239
|
-
if (existsSync5(
|
|
3876
|
+
const bundled = join16(dirname10(fileURLToPath(import.meta.url)), "shell");
|
|
3877
|
+
if (existsSync5(join16(bundled, "index.html"))) return bundled;
|
|
3240
3878
|
try {
|
|
3241
3879
|
const require2 = createRequire(import.meta.url);
|
|
3242
|
-
return
|
|
3880
|
+
return dirname10(require2.resolve("@leglas/shell/dist/index.html"));
|
|
3243
3881
|
} catch {
|
|
3244
3882
|
return null;
|
|
3245
3883
|
}
|
|
3246
3884
|
}
|
|
3885
|
+
function shellWord(value) {
|
|
3886
|
+
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(value)) return value;
|
|
3887
|
+
if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
|
|
3888
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
3889
|
+
}
|
|
3890
|
+
function embeddedLeglasCommand() {
|
|
3891
|
+
const entry = join16(dirname10(fileURLToPath(import.meta.url)), "bin.js");
|
|
3892
|
+
if (!existsSync5(entry)) return "npx -y leglas";
|
|
3893
|
+
return [process.execPath, entry].map(shellWord).join(" ");
|
|
3894
|
+
}
|
|
3247
3895
|
async function run2(options, deps) {
|
|
3248
3896
|
const loaded = await loadConfig(options.cwd);
|
|
3249
3897
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -3273,7 +3921,7 @@ async function run2(options, deps) {
|
|
|
3273
3921
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
3274
3922
|
for (const preview of merged?.previews ?? []) {
|
|
3275
3923
|
if (preview.file !== void 0) {
|
|
3276
|
-
const absolute =
|
|
3924
|
+
const absolute = join16(options.cwd, preview.file);
|
|
3277
3925
|
if (!existsSync5(absolute)) {
|
|
3278
3926
|
worktreeErrors.push(
|
|
3279
3927
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -3284,7 +3932,7 @@ async function run2(options, deps) {
|
|
|
3284
3932
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
3285
3933
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
3286
3934
|
}
|
|
3287
|
-
fileMounts.set(slug,
|
|
3935
|
+
fileMounts.set(slug, dirname10(absolute));
|
|
3288
3936
|
previews.push({
|
|
3289
3937
|
...preview,
|
|
3290
3938
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -3325,6 +3973,7 @@ async function run2(options, deps) {
|
|
|
3325
3973
|
// directory does. Either way saved layout survives a port change.
|
|
3326
3974
|
project: loaded.path ?? options.cwd,
|
|
3327
3975
|
cwd: options.cwd,
|
|
3976
|
+
leglasCommand: embeddedLeglasCommand(),
|
|
3328
3977
|
...options.port === void 0 ? {} : { port: options.port }
|
|
3329
3978
|
});
|
|
3330
3979
|
const url = `${server.url}${LEGLAS_PREFIX}`;
|