leglas 0.4.1 → 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 +22 -7
- package/dist/args.d.ts +2 -0
- package/dist/bin.js +726 -140
- package/dist/index.d.ts +1 -1
- package/dist/index.js +725 -140
- 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 +1 -1
- package/dist/shell/assets/index-B1hUXNY3.js +0 -14
- package/dist/shell/assets/index-DCg7pmCj.css +0 -1
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 {
|
|
@@ -1384,7 +1465,8 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1384
1465
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1385
1466
|
...input.branch === void 0 ? {} : { branch: input.branch },
|
|
1386
1467
|
...input.file === void 0 ? {} : { file: input.file },
|
|
1387
|
-
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
|
|
1468
|
+
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
|
|
1469
|
+
...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
|
|
1388
1470
|
};
|
|
1389
1471
|
const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
|
|
1390
1472
|
if (check.config === null) {
|
|
@@ -1616,17 +1698,220 @@ async function startAppProcess(options) {
|
|
|
1616
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.`);
|
|
1617
1699
|
}
|
|
1618
1700
|
|
|
1619
|
-
// ../server/dist/
|
|
1620
|
-
|
|
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
|
|
1621
1769
|
import { randomBytes } from "crypto";
|
|
1770
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1622
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";
|
|
1623
1908
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1624
|
-
function
|
|
1909
|
+
function variantSlot(url) {
|
|
1625
1910
|
if (!url.startsWith("/"))
|
|
1626
1911
|
return null;
|
|
1627
|
-
const query = url.slice(url.indexOf("?") + 1);
|
|
1628
1912
|
if (!url.includes("?"))
|
|
1629
1913
|
return null;
|
|
1914
|
+
const query = url.slice(url.indexOf("?") + 1);
|
|
1630
1915
|
for (const pair of query.split("&")) {
|
|
1631
1916
|
const [rawKey, rawValue] = pair.split("=");
|
|
1632
1917
|
if (rawKey === void 0 || rawValue === void 0)
|
|
@@ -1637,37 +1922,125 @@ function targetFor(url) {
|
|
|
1637
1922
|
const option = decodeURIComponent(rawValue);
|
|
1638
1923
|
if (!SAFE_SEGMENT.test(surface) || !SAFE_SEGMENT.test(option))
|
|
1639
1924
|
return null;
|
|
1640
|
-
return
|
|
1925
|
+
return { surface, option };
|
|
1641
1926
|
}
|
|
1642
1927
|
return null;
|
|
1643
1928
|
}
|
|
1644
|
-
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") {
|
|
1645
1934
|
const target = preview.file ?? targetFor(preview.url);
|
|
1646
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) {
|
|
1647
1964
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1648
1965
|
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
1649
|
-
|
|
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.
|
|
1650
1989
|
|
|
1651
|
-
|
|
1990
|
+
${source} ${make}
|
|
1652
1991
|
|
|
1653
|
-
${
|
|
1992
|
+
${asked}
|
|
1654
1993
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1994
|
+
Then register it, which is what puts it on the rail:
|
|
1995
|
+
|
|
1996
|
+
${register}
|
|
1997
|
+
|
|
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}`;
|
|
1657
2001
|
}
|
|
1658
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
|
+
}
|
|
1659
2028
|
async function readRequests(cwd) {
|
|
1660
2029
|
try {
|
|
1661
|
-
const raw = await
|
|
2030
|
+
const raw = await readFile5(join6(cwd, REQUESTS_PATH), "utf8");
|
|
1662
2031
|
const parsed = JSON.parse(raw);
|
|
1663
2032
|
if (!Array.isArray(parsed.requests))
|
|
1664
2033
|
return [];
|
|
1665
2034
|
return parsed.requests.map((request, index) => {
|
|
1666
|
-
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;
|
|
1667
2038
|
return {
|
|
1668
2039
|
...entry,
|
|
1669
2040
|
id: typeof entry.id === "string" ? entry.id : String(index),
|
|
1670
|
-
status
|
|
2041
|
+
status,
|
|
2042
|
+
mode: entry.mode === "variant" ? "variant" : "replace",
|
|
2043
|
+
...failure === null ? {} : { failure }
|
|
1671
2044
|
};
|
|
1672
2045
|
});
|
|
1673
2046
|
} catch {
|
|
@@ -1675,23 +2048,23 @@ async function readRequests(cwd) {
|
|
|
1675
2048
|
}
|
|
1676
2049
|
}
|
|
1677
2050
|
async function writeQueue(cwd, requests) {
|
|
1678
|
-
const path =
|
|
1679
|
-
await
|
|
1680
|
-
await
|
|
2051
|
+
const path = join6(cwd, REQUESTS_PATH);
|
|
2052
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2053
|
+
await writeFile4(path, `${JSON.stringify({ requests }, null, 2)}
|
|
1681
2054
|
`, "utf8");
|
|
1682
2055
|
}
|
|
1683
2056
|
async function appendRequest(cwd, request) {
|
|
1684
2057
|
await writeQueue(cwd, [
|
|
1685
2058
|
...await readRequests(cwd),
|
|
1686
|
-
{ ...request, id:
|
|
2059
|
+
{ ...request, id: randomBytes2(6).toString("base64url"), status: "queued" }
|
|
1687
2060
|
]);
|
|
1688
2061
|
}
|
|
1689
2062
|
async function collectRequests(cwd) {
|
|
1690
2063
|
const requests = await readRequests(cwd);
|
|
1691
|
-
const collected = requests.map((request) => ({ ...request, status: "picked-up" })
|
|
1692
|
-
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"))
|
|
1693
2066
|
await writeQueue(cwd, collected);
|
|
1694
|
-
return collected;
|
|
2067
|
+
return collected.filter((request) => !isTerminal(request.status));
|
|
1695
2068
|
}
|
|
1696
2069
|
async function markPickedUp(cwd, id) {
|
|
1697
2070
|
const requests = await readRequests(cwd);
|
|
@@ -1700,6 +2073,17 @@ async function markPickedUp(cwd, id) {
|
|
|
1700
2073
|
await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
|
|
1701
2074
|
return true;
|
|
1702
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
|
+
}
|
|
1703
2087
|
async function removeRequest(cwd, id) {
|
|
1704
2088
|
const requests = await readRequests(cwd);
|
|
1705
2089
|
const remaining = requests.filter((request) => request.id !== id);
|
|
@@ -1710,7 +2094,7 @@ async function removeRequest(cwd, id) {
|
|
|
1710
2094
|
}
|
|
1711
2095
|
async function clearRequests(cwd) {
|
|
1712
2096
|
const requests = await readRequests(cwd);
|
|
1713
|
-
const pending = requests.filter((request) => request.status
|
|
2097
|
+
const pending = requests.filter((request) => request.status === "queued");
|
|
1714
2098
|
const cleared = requests.length - pending.length;
|
|
1715
2099
|
if (cleared > 0)
|
|
1716
2100
|
await writeQueue(cwd, pending);
|
|
@@ -1719,10 +2103,13 @@ async function clearRequests(cwd) {
|
|
|
1719
2103
|
|
|
1720
2104
|
// ../server/dist/runner.js
|
|
1721
2105
|
import { spawn as nodeSpawn } from "child_process";
|
|
2106
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2107
|
+
import { join as join7 } from "path";
|
|
1722
2108
|
var POLL_MS = 2e3;
|
|
1723
2109
|
var OUTPUT_LINES = 20;
|
|
2110
|
+
var CANCEL_GRACE_MS = 5e3;
|
|
1724
2111
|
var SESSION_TURNS_CAP = 8;
|
|
1725
|
-
function resolveCommand(choice, prompt, sessionId = null) {
|
|
2112
|
+
function resolveCommand(choice, prompt, sessionId = null, registration = null) {
|
|
1726
2113
|
if (choice.agent === null)
|
|
1727
2114
|
return null;
|
|
1728
2115
|
if (choice.agent === "custom") {
|
|
@@ -1734,12 +2121,13 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1734
2121
|
return { agent: "custom", name: "Custom", ...commandFor(parsed.template, prompt), resumed: false };
|
|
1735
2122
|
}
|
|
1736
2123
|
const adapter = KNOWN_AGENTS[choice.agent];
|
|
2124
|
+
const allow = registration !== null && "allowArgs" in adapter ? adapter.allowArgs(registration) : [];
|
|
1737
2125
|
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1738
2126
|
return {
|
|
1739
2127
|
agent: choice.agent,
|
|
1740
2128
|
name: adapter.name,
|
|
1741
2129
|
command: adapter.binary,
|
|
1742
|
-
args: adapter.resumeArgs(sessionId, prompt),
|
|
2130
|
+
args: [...adapter.resumeArgs(sessionId, prompt), ...allow],
|
|
1743
2131
|
resumed: true
|
|
1744
2132
|
};
|
|
1745
2133
|
}
|
|
@@ -1747,7 +2135,7 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1747
2135
|
agent: choice.agent,
|
|
1748
2136
|
name: adapter.name,
|
|
1749
2137
|
command: adapter.binary,
|
|
1750
|
-
args: adapter.args(prompt),
|
|
2138
|
+
args: [...adapter.args(prompt), ...allow],
|
|
1751
2139
|
resumed: false
|
|
1752
2140
|
};
|
|
1753
2141
|
}
|
|
@@ -1777,12 +2165,17 @@ function startRunner(options) {
|
|
|
1777
2165
|
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1778
2166
|
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1779
2167
|
const failed = /* @__PURE__ */ new Set();
|
|
2168
|
+
const setLater = options.setTimeout ?? ((callback, milliseconds) => {
|
|
2169
|
+
setTimeout(callback, milliseconds).unref?.();
|
|
2170
|
+
});
|
|
1780
2171
|
let state = {
|
|
1781
2172
|
running: false,
|
|
1782
2173
|
requestId: null,
|
|
1783
2174
|
agent: null,
|
|
1784
2175
|
activity: null,
|
|
1785
|
-
startedAt: null
|
|
2176
|
+
startedAt: null,
|
|
2177
|
+
stopping: false,
|
|
2178
|
+
waiting: null
|
|
1786
2179
|
};
|
|
1787
2180
|
let stopped = false;
|
|
1788
2181
|
let ticking = null;
|
|
@@ -1790,15 +2183,30 @@ function startRunner(options) {
|
|
|
1790
2183
|
let active = null;
|
|
1791
2184
|
const sessions = /* @__PURE__ */ new Map();
|
|
1792
2185
|
const idle = () => {
|
|
1793
|
-
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
|
+
};
|
|
1794
2195
|
};
|
|
1795
2196
|
const rememberLine = (lines, line) => {
|
|
1796
2197
|
lines.push(line);
|
|
1797
2198
|
if (lines.length > OUTPUT_LINES)
|
|
1798
2199
|
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1799
2200
|
};
|
|
1800
|
-
const reportFailure = (request,
|
|
1801
|
-
|
|
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}`);
|
|
1802
2210
|
for (const line of lines)
|
|
1803
2211
|
console.error(` ${line}`);
|
|
1804
2212
|
};
|
|
@@ -1816,19 +2224,31 @@ function startRunner(options) {
|
|
|
1816
2224
|
error: error instanceof Error ? error.message : String(error)
|
|
1817
2225
|
});
|
|
1818
2226
|
}
|
|
1819
|
-
const current = {
|
|
2227
|
+
const current = {
|
|
2228
|
+
child,
|
|
2229
|
+
requestId: request.id,
|
|
2230
|
+
cancelled: false,
|
|
2231
|
+
abandon: () => {
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
1820
2234
|
active = current;
|
|
1821
2235
|
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1822
2236
|
rememberLine(lines, line);
|
|
1823
2237
|
const sessionId = sessionFrom(resolved.agent, line);
|
|
1824
2238
|
if (sessionId !== null)
|
|
1825
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
|
+
}
|
|
1826
2246
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1827
2247
|
if (activity !== null) {
|
|
1828
2248
|
if (activity.startsWith("editing"))
|
|
1829
2249
|
observed.edited = true;
|
|
1830
2250
|
if (active === current)
|
|
1831
|
-
state = { ...state, activity };
|
|
2251
|
+
state = { ...state, activity, waiting: null };
|
|
1832
2252
|
}
|
|
1833
2253
|
});
|
|
1834
2254
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
@@ -1842,6 +2262,7 @@ function startRunner(options) {
|
|
|
1842
2262
|
stderrFlush();
|
|
1843
2263
|
resolve(outcome);
|
|
1844
2264
|
};
|
|
2265
|
+
current.abandon = () => settle({ ok: false, error: "cancelled" });
|
|
1845
2266
|
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1846
2267
|
child.once("close", (code, signal) => {
|
|
1847
2268
|
if (current.cancelled)
|
|
@@ -1855,10 +2276,12 @@ function startRunner(options) {
|
|
|
1855
2276
|
active = null;
|
|
1856
2277
|
});
|
|
1857
2278
|
};
|
|
2279
|
+
const registered = () => readFile6(join7(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
1858
2280
|
const handle = async (request, choice) => {
|
|
1859
2281
|
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1860
2282
|
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1861
|
-
|
|
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);
|
|
1862
2285
|
if (resolved === null)
|
|
1863
2286
|
return;
|
|
1864
2287
|
const lines = [];
|
|
@@ -1866,7 +2289,7 @@ function startRunner(options) {
|
|
|
1866
2289
|
if (!await markPickedUp(options.cwd, request.id))
|
|
1867
2290
|
return;
|
|
1868
2291
|
if (stopped) {
|
|
1869
|
-
|
|
2292
|
+
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
1870
2293
|
return;
|
|
1871
2294
|
}
|
|
1872
2295
|
state = {
|
|
@@ -1874,25 +2297,47 @@ function startRunner(options) {
|
|
|
1874
2297
|
requestId: request.id,
|
|
1875
2298
|
agent: resolved.name,
|
|
1876
2299
|
activity: null,
|
|
1877
|
-
startedAt: Date.now()
|
|
2300
|
+
startedAt: Date.now(),
|
|
2301
|
+
stopping: false,
|
|
2302
|
+
waiting: null
|
|
2303
|
+
};
|
|
2304
|
+
const observed = {
|
|
2305
|
+
sessionId: null,
|
|
2306
|
+
edited: false,
|
|
2307
|
+
retry: null
|
|
1878
2308
|
};
|
|
1879
|
-
const
|
|
2309
|
+
const before = request.mode === "variant" ? await registered() : null;
|
|
2310
|
+
const agent = resolved.name;
|
|
1880
2311
|
let outcome = await runChild(request, resolved, lines, observed);
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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.
|
|
1885
2323
|
!stopped) {
|
|
1886
2324
|
sessions.delete(resolved.agent);
|
|
1887
|
-
const cold = resolveCommand(choice, request.prompt);
|
|
2325
|
+
const cold = resolveCommand(choice, request.prompt, null, registration);
|
|
1888
2326
|
if (cold !== null) {
|
|
1889
2327
|
resolved = cold;
|
|
1890
2328
|
observed.sessionId = null;
|
|
1891
|
-
|
|
2329
|
+
observed.retry = null;
|
|
2330
|
+
state = { ...state, activity: null, waiting: null };
|
|
1892
2331
|
outcome = await runChild(request, resolved, lines, observed);
|
|
2332
|
+
failure = verdict();
|
|
1893
2333
|
}
|
|
1894
2334
|
}
|
|
1895
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
|
+
}
|
|
1896
2341
|
if (observed.sessionId !== null) {
|
|
1897
2342
|
const previous = sessions.get(resolved.agent);
|
|
1898
2343
|
sessions.set(resolved.agent, {
|
|
@@ -1900,12 +2345,14 @@ function startRunner(options) {
|
|
|
1900
2345
|
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1901
2346
|
});
|
|
1902
2347
|
}
|
|
2348
|
+
if (request.mode === "replace" && request.notes !== void 0) {
|
|
2349
|
+
await removeAnnotations(options.cwd, request.notes).catch(() => 0);
|
|
2350
|
+
}
|
|
1903
2351
|
await removeRequest(options.cwd, request.id);
|
|
1904
2352
|
return;
|
|
1905
2353
|
}
|
|
1906
2354
|
sessions.delete(resolved.agent);
|
|
1907
|
-
|
|
1908
|
-
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
2355
|
+
await reportFailure(request, failure, lines);
|
|
1909
2356
|
} finally {
|
|
1910
2357
|
idle();
|
|
1911
2358
|
}
|
|
@@ -1939,12 +2386,23 @@ function startRunner(options) {
|
|
|
1939
2386
|
return false;
|
|
1940
2387
|
if (id !== void 0 && active.requestId !== id)
|
|
1941
2388
|
return false;
|
|
1942
|
-
|
|
1943
|
-
|
|
2389
|
+
const current = active;
|
|
2390
|
+
current.cancelled = true;
|
|
2391
|
+
failed.add(current.requestId);
|
|
2392
|
+
state = { ...state, stopping: true, waiting: null };
|
|
1944
2393
|
try {
|
|
1945
|
-
|
|
2394
|
+
current.child.kill("SIGTERM");
|
|
1946
2395
|
} catch {
|
|
1947
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);
|
|
1948
2406
|
return true;
|
|
1949
2407
|
};
|
|
1950
2408
|
const stop = () => {
|
|
@@ -1969,12 +2427,12 @@ function startRunner(options) {
|
|
|
1969
2427
|
}
|
|
1970
2428
|
|
|
1971
2429
|
// ../server/dist/renames.js
|
|
1972
|
-
import { mkdir as
|
|
1973
|
-
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";
|
|
1974
2432
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
1975
2433
|
async function readRenames(cwd) {
|
|
1976
2434
|
try {
|
|
1977
|
-
const raw = await
|
|
2435
|
+
const raw = await readFile7(join8(cwd, RENAMES_PATH), "utf8");
|
|
1978
2436
|
const parsed = JSON.parse(raw);
|
|
1979
2437
|
if (parsed.renames === null || typeof parsed.renames !== "object")
|
|
1980
2438
|
return {};
|
|
@@ -1984,9 +2442,9 @@ async function readRenames(cwd) {
|
|
|
1984
2442
|
}
|
|
1985
2443
|
}
|
|
1986
2444
|
async function writeRenames(cwd, renames) {
|
|
1987
|
-
const path =
|
|
1988
|
-
await
|
|
1989
|
-
await
|
|
2445
|
+
const path = join8(cwd, RENAMES_PATH);
|
|
2446
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2447
|
+
await writeFile5(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1990
2448
|
`, "utf8");
|
|
1991
2449
|
}
|
|
1992
2450
|
function resolveTitle(input, titles, renames) {
|
|
@@ -2004,7 +2462,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
2004
2462
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
2005
2463
|
import http2 from "http";
|
|
2006
2464
|
import net3 from "net";
|
|
2007
|
-
import { extname, join as
|
|
2465
|
+
import { extname, join as join9, normalize, relative as relative3 } from "path";
|
|
2008
2466
|
var LEGLAS_PREFIX = "/leglas";
|
|
2009
2467
|
var DEFAULT_PORT = 4100;
|
|
2010
2468
|
var PORT_ATTEMPTS = 20;
|
|
@@ -2077,6 +2535,9 @@ function isTrustedMutation(req) {
|
|
|
2077
2535
|
return false;
|
|
2078
2536
|
}
|
|
2079
2537
|
}
|
|
2538
|
+
function isEnded(request, failedIds) {
|
|
2539
|
+
return isTerminal(request.status) || failedIds.includes(request.id);
|
|
2540
|
+
}
|
|
2080
2541
|
function hasJsonBody(req) {
|
|
2081
2542
|
const contentType = req.headers["content-type"];
|
|
2082
2543
|
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
@@ -2103,7 +2564,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
2103
2564
|
}
|
|
2104
2565
|
function serveFrom(res, dir, relativePath) {
|
|
2105
2566
|
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
2106
|
-
const candidate =
|
|
2567
|
+
const candidate = join9(dir, relative5);
|
|
2107
2568
|
if (!candidate.startsWith(dir))
|
|
2108
2569
|
return false;
|
|
2109
2570
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -2188,7 +2649,7 @@ async function bind(server, requested) {
|
|
|
2188
2649
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
2189
2650
|
}
|
|
2190
2651
|
async function startServer(options) {
|
|
2191
|
-
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;
|
|
2192
2653
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
2193
2654
|
const proxy = createProxyHandler({ target });
|
|
2194
2655
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
@@ -2305,20 +2766,51 @@ async function startServer(options) {
|
|
|
2305
2766
|
} catch {
|
|
2306
2767
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2307
2768
|
}
|
|
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";
|
|
2308
2776
|
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
2309
2777
|
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
2310
2778
|
const localTitles = new Set(local.map((entry) => entry.title));
|
|
2311
2779
|
const bootConfig = config?.previews ?? [];
|
|
2312
2780
|
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
2313
2781
|
const preview = [...boot, ...local].find((entry) => entry.title === parsed.title);
|
|
2314
|
-
if (!preview
|
|
2782
|
+
if (!preview) {
|
|
2315
2783
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2316
2784
|
}
|
|
2317
|
-
const
|
|
2785
|
+
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
2786
|
+
if (!parsed.intent?.trim() && notes.length === 0) {
|
|
2787
|
+
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2788
|
+
}
|
|
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);
|
|
2318
2806
|
void appendRequest(cwd, {
|
|
2319
2807
|
title: preview.title,
|
|
2320
2808
|
url: preview.url,
|
|
2321
|
-
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) },
|
|
2322
2814
|
...composed
|
|
2323
2815
|
}).then(() => {
|
|
2324
2816
|
runner?.nudge();
|
|
@@ -2394,21 +2886,33 @@ async function startServer(options) {
|
|
|
2394
2886
|
agent: null,
|
|
2395
2887
|
activity: null,
|
|
2396
2888
|
startedAt: null,
|
|
2889
|
+
stopping: false,
|
|
2890
|
+
waiting: null,
|
|
2397
2891
|
failedIds: []
|
|
2398
2892
|
};
|
|
2399
2893
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
2400
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
2894
|
+
requests: requests.map(({ id, title, intent, status, failure }) => ({
|
|
2401
2895
|
id,
|
|
2402
2896
|
title,
|
|
2403
2897
|
intent,
|
|
2404
|
-
|
|
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
|
|
2405
2904
|
})),
|
|
2406
2905
|
agent: {
|
|
2407
2906
|
attached: externallyAttached(),
|
|
2408
2907
|
running: snapshot.running,
|
|
2409
2908
|
name: snapshot.running ? snapshot.agent : null,
|
|
2410
2909
|
activity: snapshot.running ? snapshot.activity : null,
|
|
2411
|
-
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
|
|
2412
2916
|
}
|
|
2413
2917
|
}));
|
|
2414
2918
|
}
|
|
@@ -2451,8 +2955,8 @@ async function startServer(options) {
|
|
|
2451
2955
|
if (request === void 0) {
|
|
2452
2956
|
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2453
2957
|
}
|
|
2454
|
-
if (!(runner?.snapshot().failedIds
|
|
2455
|
-
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." });
|
|
2456
2960
|
}
|
|
2457
2961
|
try {
|
|
2458
2962
|
if (!await removeRequest(cwd, request.id)) {
|
|
@@ -2463,7 +2967,11 @@ async function startServer(options) {
|
|
|
2463
2967
|
url: request.url,
|
|
2464
2968
|
intent: request.intent,
|
|
2465
2969
|
target: request.target,
|
|
2466
|
-
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 }
|
|
2467
2975
|
});
|
|
2468
2976
|
runner?.nudge();
|
|
2469
2977
|
return sendJson(res, 200, { ok: true });
|
|
@@ -2472,6 +2980,65 @@ async function startServer(options) {
|
|
|
2472
2980
|
}
|
|
2473
2981
|
});
|
|
2474
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
|
+
}
|
|
2475
3042
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2476
3043
|
if (!hasJsonBody(req)) {
|
|
2477
3044
|
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
@@ -2488,8 +3055,9 @@ async function startServer(options) {
|
|
|
2488
3055
|
if (typeof parsed.id !== "string") {
|
|
2489
3056
|
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2490
3057
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
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." });
|
|
2493
3061
|
}
|
|
2494
3062
|
try {
|
|
2495
3063
|
if (!await removeRequest(cwd, parsed.id)) {
|
|
@@ -2564,7 +3132,7 @@ async function startServer(options) {
|
|
|
2564
3132
|
proxy.upgrade(req, socket, head);
|
|
2565
3133
|
});
|
|
2566
3134
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2567
|
-
runner = startRunner({ cwd, externallyAttached });
|
|
3135
|
+
runner = startRunner({ cwd, externallyAttached, leglasCommand });
|
|
2568
3136
|
let closePromise = null;
|
|
2569
3137
|
return {
|
|
2570
3138
|
port,
|
|
@@ -2639,11 +3207,11 @@ function planKeep(options) {
|
|
|
2639
3207
|
}
|
|
2640
3208
|
|
|
2641
3209
|
// src/run-init.ts
|
|
2642
|
-
import { readFile as
|
|
2643
|
-
import { join as
|
|
3210
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
3211
|
+
import { join as join10 } from "path";
|
|
2644
3212
|
async function readIfPresent(path) {
|
|
2645
3213
|
try {
|
|
2646
|
-
return await
|
|
3214
|
+
return await readFile8(path, "utf8");
|
|
2647
3215
|
} catch {
|
|
2648
3216
|
return null;
|
|
2649
3217
|
}
|
|
@@ -2651,18 +3219,18 @@ async function readIfPresent(path) {
|
|
|
2651
3219
|
async function runInit(options, deps) {
|
|
2652
3220
|
const existingConfig = findConfigFile(options.cwd);
|
|
2653
3221
|
const plan = planInit({
|
|
2654
|
-
agents: await readIfPresent(
|
|
3222
|
+
agents: await readIfPresent(join10(options.cwd, "AGENTS.md")),
|
|
2655
3223
|
config: existingConfig === null ? null : "present",
|
|
2656
|
-
gitignore: await readIfPresent(
|
|
3224
|
+
gitignore: await readIfPresent(join10(options.cwd, ".gitignore")),
|
|
2657
3225
|
force: options.force
|
|
2658
3226
|
});
|
|
2659
3227
|
const touched = [];
|
|
2660
|
-
for (const
|
|
2661
|
-
await
|
|
2662
|
-
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);
|
|
2663
3231
|
}
|
|
2664
3232
|
if (plan.gitignore !== null) {
|
|
2665
|
-
await
|
|
3233
|
+
await writeFile6(join10(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2666
3234
|
touched.push(".gitignore");
|
|
2667
3235
|
}
|
|
2668
3236
|
if (options.json) {
|
|
@@ -2682,8 +3250,8 @@ async function runInit(options, deps) {
|
|
|
2682
3250
|
|
|
2683
3251
|
// src/run-keep.ts
|
|
2684
3252
|
import { existsSync as existsSync3 } from "fs";
|
|
2685
|
-
import { mkdir as
|
|
2686
|
-
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";
|
|
2687
3255
|
|
|
2688
3256
|
// src/resolve-title.ts
|
|
2689
3257
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -2727,18 +3295,18 @@ async function runKeep(options, deps) {
|
|
|
2727
3295
|
if (!resolved.ok) return fail(resolved.error);
|
|
2728
3296
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
2729
3297
|
if (!plan.ok) return fail(plan.error);
|
|
2730
|
-
const from =
|
|
2731
|
-
const to =
|
|
3298
|
+
const from = join11(options.cwd, plan.move.from);
|
|
3299
|
+
const to = join11(options.cwd, plan.move.to);
|
|
2732
3300
|
if (!existsSync3(from)) {
|
|
2733
3301
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
2734
3302
|
}
|
|
2735
3303
|
if (existsSync3(to)) {
|
|
2736
3304
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
2737
3305
|
}
|
|
2738
|
-
const source = await
|
|
2739
|
-
await
|
|
2740
|
-
await
|
|
2741
|
-
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 });
|
|
2742
3310
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
2743
3311
|
if (options.json) {
|
|
2744
3312
|
deps.log(
|
|
@@ -2773,11 +3341,11 @@ async function runKeep(options, deps) {
|
|
|
2773
3341
|
|
|
2774
3342
|
// src/run-new.ts
|
|
2775
3343
|
import { existsSync as existsSync4 } from "fs";
|
|
2776
|
-
import { mkdir as
|
|
2777
|
-
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";
|
|
2778
3346
|
async function readIfPresent2(path) {
|
|
2779
3347
|
try {
|
|
2780
|
-
return await
|
|
3348
|
+
return await readFile10(path, "utf8");
|
|
2781
3349
|
} catch {
|
|
2782
3350
|
return null;
|
|
2783
3351
|
}
|
|
@@ -2785,19 +3353,19 @@ async function readIfPresent2(path) {
|
|
|
2785
3353
|
async function runNew(options, deps) {
|
|
2786
3354
|
let from;
|
|
2787
3355
|
if (options.from !== void 0) {
|
|
2788
|
-
const contents = await readIfPresent2(
|
|
3356
|
+
const contents = await readIfPresent2(join12(options.cwd, options.from));
|
|
2789
3357
|
if (contents === null) {
|
|
2790
|
-
const
|
|
2791
|
-
if (options.json) deps.log(JSON.stringify({ ok: false, error:
|
|
2792
|
-
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);
|
|
2793
3361
|
return { exitCode: 1, written: [] };
|
|
2794
3362
|
}
|
|
2795
3363
|
from = { path: options.from, contents };
|
|
2796
3364
|
}
|
|
2797
3365
|
const plan = planNew({
|
|
2798
3366
|
surface: options.surface,
|
|
2799
|
-
packageJson: await readIfPresent2(
|
|
2800
|
-
gitignore: await readIfPresent2(
|
|
3367
|
+
packageJson: await readIfPresent2(join12(options.cwd, "package.json")),
|
|
3368
|
+
gitignore: await readIfPresent2(join12(options.cwd, ".gitignore")),
|
|
2801
3369
|
from
|
|
2802
3370
|
});
|
|
2803
3371
|
const fail = (error) => {
|
|
@@ -2813,26 +3381,26 @@ async function runNew(options, deps) {
|
|
|
2813
3381
|
deps.log(JSON.stringify({ ok: true, files: plan.writes, instructions: plan.instructions, previews: plan.previews }));
|
|
2814
3382
|
return { exitCode: 0, written: [] };
|
|
2815
3383
|
}
|
|
2816
|
-
for (const
|
|
2817
|
-
deps.log(`--- ${
|
|
2818
|
-
deps.log(
|
|
3384
|
+
for (const write2 of plan.writes) {
|
|
3385
|
+
deps.log(`--- ${write2.path}`);
|
|
3386
|
+
deps.log(write2.contents);
|
|
2819
3387
|
}
|
|
2820
3388
|
deps.log(plan.instructions);
|
|
2821
3389
|
return { exitCode: 0, written: [] };
|
|
2822
3390
|
}
|
|
2823
|
-
const existing = plan.writes.filter((
|
|
3391
|
+
const existing = plan.writes.filter((write2) => existsSync4(join12(options.cwd, write2.path)));
|
|
2824
3392
|
if (existing.length > 0) {
|
|
2825
3393
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
2826
3394
|
}
|
|
2827
3395
|
const written = [];
|
|
2828
|
-
for (const
|
|
2829
|
-
const target =
|
|
2830
|
-
await
|
|
2831
|
-
await
|
|
2832
|
-
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);
|
|
2833
3401
|
}
|
|
2834
3402
|
if (plan.gitignore !== null) {
|
|
2835
|
-
await
|
|
3403
|
+
await writeFile8(join12(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2836
3404
|
written.push(".gitignore");
|
|
2837
3405
|
}
|
|
2838
3406
|
if (options.json) {
|
|
@@ -2853,21 +3421,21 @@ async function runNew(options, deps) {
|
|
|
2853
3421
|
}
|
|
2854
3422
|
|
|
2855
3423
|
// src/run-previews.ts
|
|
2856
|
-
import { readFile as
|
|
2857
|
-
import { join as
|
|
3424
|
+
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
3425
|
+
import { join as join13 } from "path";
|
|
2858
3426
|
function envelope(deps, ok, body) {
|
|
2859
3427
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
2860
3428
|
}
|
|
2861
3429
|
async function ensureIgnored(cwd) {
|
|
2862
|
-
const path =
|
|
3430
|
+
const path = join13(cwd, ".gitignore");
|
|
2863
3431
|
let current = null;
|
|
2864
3432
|
try {
|
|
2865
|
-
current = await
|
|
3433
|
+
current = await readFile11(path, "utf8");
|
|
2866
3434
|
} catch {
|
|
2867
3435
|
current = null;
|
|
2868
3436
|
}
|
|
2869
3437
|
const next = ignoreEntry(current);
|
|
2870
|
-
if (next !== null) await
|
|
3438
|
+
if (next !== null) await writeFile9(path, next, "utf8");
|
|
2871
3439
|
}
|
|
2872
3440
|
async function runAdd(options, deps) {
|
|
2873
3441
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2891,7 +3459,8 @@ async function runAdd(options, deps) {
|
|
|
2891
3459
|
tags: options.preview.tags,
|
|
2892
3460
|
branch: options.preview.branch,
|
|
2893
3461
|
file: options.preview.file,
|
|
2894
|
-
basedOn: options.preview.basedOn
|
|
3462
|
+
basedOn: options.preview.basedOn,
|
|
3463
|
+
askedFor: options.preview.askedFor
|
|
2895
3464
|
},
|
|
2896
3465
|
shared
|
|
2897
3466
|
);
|
|
@@ -2951,6 +3520,7 @@ async function runList(options, deps) {
|
|
|
2951
3520
|
note: preview.note ?? null,
|
|
2952
3521
|
tags: preview.tags,
|
|
2953
3522
|
basedOn: preview.basedOn ?? null,
|
|
3523
|
+
askedFor: preview.askedFor ?? null,
|
|
2954
3524
|
local: preview.local,
|
|
2955
3525
|
branch: preview.branch ?? null,
|
|
2956
3526
|
file: preview.file ?? null
|
|
@@ -3113,23 +3683,23 @@ async function runShow(options, deps) {
|
|
|
3113
3683
|
|
|
3114
3684
|
// src/run-watch.ts
|
|
3115
3685
|
import { spawn as spawn3 } from "child_process";
|
|
3116
|
-
import { mkdir as
|
|
3117
|
-
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";
|
|
3118
3688
|
var POLL_MS2 = 2e3;
|
|
3119
3689
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
3120
3690
|
async function saveTemplate(cwd, run3) {
|
|
3121
|
-
const path =
|
|
3691
|
+
const path = join14(cwd, WATCH_PATH);
|
|
3122
3692
|
let config = {};
|
|
3123
3693
|
try {
|
|
3124
|
-
const parsed = JSON.parse(await
|
|
3694
|
+
const parsed = JSON.parse(await readFile12(path, "utf8"));
|
|
3125
3695
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
3126
3696
|
config = parsed;
|
|
3127
3697
|
}
|
|
3128
3698
|
} catch {
|
|
3129
3699
|
}
|
|
3130
3700
|
config.run = run3;
|
|
3131
|
-
await
|
|
3132
|
-
await
|
|
3701
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3702
|
+
await writeFile10(path, `${JSON.stringify(config, null, 2)}
|
|
3133
3703
|
`, "utf8");
|
|
3134
3704
|
}
|
|
3135
3705
|
function spawnAgent(command, args, cwd) {
|
|
@@ -3215,9 +3785,13 @@ async function runWatch(options, deps) {
|
|
|
3215
3785
|
return;
|
|
3216
3786
|
}
|
|
3217
3787
|
failed.add(request.id);
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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}`);
|
|
3221
3795
|
deps.error(" Left in the queue and not retried.");
|
|
3222
3796
|
};
|
|
3223
3797
|
const tick = async () => {
|
|
@@ -3263,12 +3837,12 @@ async function runWatch(options, deps) {
|
|
|
3263
3837
|
|
|
3264
3838
|
// src/run-classify.ts
|
|
3265
3839
|
import { stat } from "fs/promises";
|
|
3266
|
-
import { join as
|
|
3840
|
+
import { join as join15 } from "path";
|
|
3267
3841
|
async function runClassify(options, deps) {
|
|
3268
3842
|
const declared = await Promise.all(
|
|
3269
3843
|
options.changes.map(async (change) => ({
|
|
3270
3844
|
...change,
|
|
3271
|
-
exists: await stat(
|
|
3845
|
+
exists: await stat(join15(options.cwd, change.path)).then(
|
|
3272
3846
|
() => true,
|
|
3273
3847
|
() => false
|
|
3274
3848
|
)
|
|
@@ -3296,18 +3870,28 @@ async function runClassify(options, deps) {
|
|
|
3296
3870
|
// src/run.ts
|
|
3297
3871
|
import { existsSync as existsSync5 } from "fs";
|
|
3298
3872
|
import { createRequire } from "module";
|
|
3299
|
-
import { basename as basename3, dirname as
|
|
3873
|
+
import { basename as basename3, dirname as dirname10, join as join16, relative as relative4 } from "path";
|
|
3300
3874
|
import { fileURLToPath } from "url";
|
|
3301
3875
|
function findShellDir() {
|
|
3302
|
-
const bundled =
|
|
3303
|
-
if (existsSync5(
|
|
3876
|
+
const bundled = join16(dirname10(fileURLToPath(import.meta.url)), "shell");
|
|
3877
|
+
if (existsSync5(join16(bundled, "index.html"))) return bundled;
|
|
3304
3878
|
try {
|
|
3305
3879
|
const require2 = createRequire(import.meta.url);
|
|
3306
|
-
return
|
|
3880
|
+
return dirname10(require2.resolve("@leglas/shell/dist/index.html"));
|
|
3307
3881
|
} catch {
|
|
3308
3882
|
return null;
|
|
3309
3883
|
}
|
|
3310
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
|
+
}
|
|
3311
3895
|
async function run2(options, deps) {
|
|
3312
3896
|
const loaded = await loadConfig(options.cwd);
|
|
3313
3897
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -3337,7 +3921,7 @@ async function run2(options, deps) {
|
|
|
3337
3921
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
3338
3922
|
for (const preview of merged?.previews ?? []) {
|
|
3339
3923
|
if (preview.file !== void 0) {
|
|
3340
|
-
const absolute =
|
|
3924
|
+
const absolute = join16(options.cwd, preview.file);
|
|
3341
3925
|
if (!existsSync5(absolute)) {
|
|
3342
3926
|
worktreeErrors.push(
|
|
3343
3927
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -3348,7 +3932,7 @@ async function run2(options, deps) {
|
|
|
3348
3932
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
3349
3933
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
3350
3934
|
}
|
|
3351
|
-
fileMounts.set(slug,
|
|
3935
|
+
fileMounts.set(slug, dirname10(absolute));
|
|
3352
3936
|
previews.push({
|
|
3353
3937
|
...preview,
|
|
3354
3938
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -3389,6 +3973,7 @@ async function run2(options, deps) {
|
|
|
3389
3973
|
// directory does. Either way saved layout survives a port change.
|
|
3390
3974
|
project: loaded.path ?? options.cwd,
|
|
3391
3975
|
cwd: options.cwd,
|
|
3976
|
+
leglasCommand: embeddedLeglasCommand(),
|
|
3392
3977
|
...options.port === void 0 ? {} : { port: options.port }
|
|
3393
3978
|
});
|
|
3394
3979
|
const url = `${server.url}${LEGLAS_PREFIX}`;
|