leglas 0.4.1 → 0.6.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 +38 -13
- package/dist/args.d.ts +2 -0
- package/dist/bin.js +861 -166
- package/dist/index.d.ts +1 -1
- package/dist/index.js +860 -166
- package/dist/shell/assets/index-Bfon5OrV.js +14 -0
- package/dist/shell/assets/index-CIdZiWhI.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"];
|
|
@@ -929,29 +952,39 @@ function nextRequest(requests, failed) {
|
|
|
929
952
|
|
|
930
953
|
// ../server/dist/agents.js
|
|
931
954
|
import { spawn } from "child_process";
|
|
932
|
-
import { constants } from "fs";
|
|
955
|
+
import { constants, readdirSync } from "fs";
|
|
933
956
|
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
934
957
|
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
958
|
+
var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
959
|
+
var effortFlag = (effort) => effort === null ? [] : ["--effort", effort];
|
|
960
|
+
var codexEffortConfig = (effort) => effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`];
|
|
961
|
+
var CODEX_WORKSPACE_CONFIG = [
|
|
962
|
+
"-c",
|
|
963
|
+
"sandbox_workspace_write.network_access=true"
|
|
964
|
+
];
|
|
935
965
|
var KNOWN_AGENTS = {
|
|
936
966
|
claude: {
|
|
937
967
|
name: "Claude",
|
|
938
968
|
binary: "claude",
|
|
939
|
-
|
|
969
|
+
efforts: AGENT_EFFORTS,
|
|
970
|
+
args: (prompt, effort = null) => [
|
|
940
971
|
"-p",
|
|
941
972
|
prompt,
|
|
942
973
|
"--output-format",
|
|
943
974
|
"stream-json",
|
|
944
975
|
"--verbose",
|
|
945
976
|
"--permission-mode",
|
|
946
|
-
"acceptEdits"
|
|
977
|
+
"acceptEdits",
|
|
978
|
+
...effortFlag(effort)
|
|
947
979
|
],
|
|
948
|
-
terminalArgs: (prompt) => [
|
|
980
|
+
terminalArgs: (prompt, effort = null) => [
|
|
949
981
|
"-p",
|
|
950
982
|
prompt,
|
|
951
983
|
"--permission-mode",
|
|
952
|
-
"acceptEdits"
|
|
984
|
+
"acceptEdits",
|
|
985
|
+
...effortFlag(effort)
|
|
953
986
|
],
|
|
954
|
-
resumeArgs: (sessionId, prompt) => [
|
|
987
|
+
resumeArgs: (sessionId, prompt, effort = null) => [
|
|
955
988
|
"-p",
|
|
956
989
|
"--resume",
|
|
957
990
|
sessionId,
|
|
@@ -960,8 +993,15 @@ var KNOWN_AGENTS = {
|
|
|
960
993
|
"stream-json",
|
|
961
994
|
"--verbose",
|
|
962
995
|
"--permission-mode",
|
|
963
|
-
"acceptEdits"
|
|
996
|
+
"acceptEdits",
|
|
997
|
+
...effortFlag(effort)
|
|
964
998
|
],
|
|
999
|
+
// Non-interactive Claude cannot approve a Bash call: acceptEdits covers
|
|
1000
|
+
// files, so a command the prompt requires is refused every time with
|
|
1001
|
+
// nobody there to say yes. This allows exactly that command and nothing
|
|
1002
|
+
// wider. Codex needs no equivalent, because workspace-write already lets
|
|
1003
|
+
// it run commands.
|
|
1004
|
+
allowArgs: (command) => ["--allowedTools", `Bash(${command} *)`],
|
|
965
1005
|
// Every stream-json event names its session.
|
|
966
1006
|
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
967
1007
|
authArgs: ["auth", "status"],
|
|
@@ -982,15 +1022,45 @@ var KNOWN_AGENTS = {
|
|
|
982
1022
|
codex: {
|
|
983
1023
|
name: "Codex",
|
|
984
1024
|
binary: "codex",
|
|
985
|
-
|
|
986
|
-
|
|
1025
|
+
efforts: AGENT_EFFORTS,
|
|
1026
|
+
// `--skip-git-repo-check` is what lets Codex run at all in a project the
|
|
1027
|
+
// user never put under version control: without it codex-cli refuses
|
|
1028
|
+
// before it reaches a model, with "Not inside a trusted directory and
|
|
1029
|
+
// --skip-git-repo-check was not specified", and every Codex request in a
|
|
1030
|
+
// non-git project fails for a reason nothing in Leglas explained. The flag
|
|
1031
|
+
// moves that precondition and only that: `-s workspace-write` still
|
|
1032
|
+
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
1033
|
+
// and in a git repository the flag does nothing at all.
|
|
1034
|
+
args: (prompt, effort = null) => [
|
|
1035
|
+
"exec",
|
|
1036
|
+
"--json",
|
|
1037
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
1038
|
+
...codexEffortConfig(effort),
|
|
1039
|
+
"-s",
|
|
1040
|
+
"workspace-write",
|
|
1041
|
+
"--skip-git-repo-check",
|
|
1042
|
+
prompt
|
|
1043
|
+
],
|
|
1044
|
+
terminalArgs: (prompt, effort = null) => [
|
|
1045
|
+
"exec",
|
|
1046
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
1047
|
+
...codexEffortConfig(effort),
|
|
1048
|
+
"-s",
|
|
1049
|
+
"workspace-write",
|
|
1050
|
+
"--skip-git-repo-check",
|
|
1051
|
+
prompt
|
|
1052
|
+
],
|
|
987
1053
|
// 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.
|
|
989
|
-
|
|
1054
|
+
// session's own sandbox, which the first turn set to workspace-write. The
|
|
1055
|
+
// repository check is per invocation, so resume needs the flag of its own.
|
|
1056
|
+
resumeArgs: (sessionId, prompt, effort = null) => [
|
|
990
1057
|
"exec",
|
|
991
1058
|
"resume",
|
|
992
1059
|
sessionId,
|
|
993
1060
|
"--json",
|
|
1061
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
1062
|
+
...codexEffortConfig(effort),
|
|
1063
|
+
"--skip-git-repo-check",
|
|
994
1064
|
prompt
|
|
995
1065
|
],
|
|
996
1066
|
sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
|
|
@@ -1001,8 +1071,14 @@ var KNOWN_AGENTS = {
|
|
|
1001
1071
|
cursor: {
|
|
1002
1072
|
name: "Cursor",
|
|
1003
1073
|
binary: "cursor-agent",
|
|
1004
|
-
|
|
1005
|
-
|
|
1074
|
+
efforts: [],
|
|
1075
|
+
args: (prompt, _effort = null) => [
|
|
1076
|
+
"-p",
|
|
1077
|
+
prompt,
|
|
1078
|
+
"--output-format",
|
|
1079
|
+
"stream-json"
|
|
1080
|
+
],
|
|
1081
|
+
terminalArgs: (prompt, _effort = null) => ["-p", prompt],
|
|
1006
1082
|
authArgs: ["status"],
|
|
1007
1083
|
// UNVERIFIED: cursor-agent was not available on the build machine. The
|
|
1008
1084
|
// reading is deliberately loose, and anything ambiguous stays unknown.
|
|
@@ -1016,11 +1092,15 @@ var KNOWN_AGENTS = {
|
|
|
1016
1092
|
}
|
|
1017
1093
|
};
|
|
1018
1094
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
1019
|
-
function execProbe(binary, args) {
|
|
1095
|
+
function execProbe(binary, args, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
1020
1096
|
return new Promise((resolve) => {
|
|
1021
1097
|
let child;
|
|
1022
1098
|
try {
|
|
1023
|
-
child = spawn(binary, [...args], {
|
|
1099
|
+
child = spawn(binary, [...args], {
|
|
1100
|
+
env: agentEnvironment(),
|
|
1101
|
+
shell: false,
|
|
1102
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1103
|
+
});
|
|
1024
1104
|
} catch {
|
|
1025
1105
|
return resolve(null);
|
|
1026
1106
|
}
|
|
@@ -1029,7 +1109,10 @@ function execProbe(binary, args) {
|
|
|
1029
1109
|
if (stdout.length < 4096)
|
|
1030
1110
|
stdout += chunk.toString();
|
|
1031
1111
|
});
|
|
1032
|
-
const deadline = setTimeout(() =>
|
|
1112
|
+
const deadline = setTimeout(() => {
|
|
1113
|
+
child.kill("SIGKILL");
|
|
1114
|
+
resolve(null);
|
|
1115
|
+
}, timeoutMs);
|
|
1033
1116
|
child.once("error", () => {
|
|
1034
1117
|
clearTimeout(deadline);
|
|
1035
1118
|
resolve(null);
|
|
@@ -1040,8 +1123,50 @@ function execProbe(binary, args) {
|
|
|
1040
1123
|
});
|
|
1041
1124
|
});
|
|
1042
1125
|
}
|
|
1126
|
+
function agentSearchPath(env = process.env, platform = process.platform) {
|
|
1127
|
+
const home = env.HOME ?? env.USERPROFILE ?? "";
|
|
1128
|
+
const npmPrefix = env.NPM_CONFIG_PREFIX;
|
|
1129
|
+
const versionBins = (root, suffix) => {
|
|
1130
|
+
try {
|
|
1131
|
+
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(root, entry.name, ...suffix));
|
|
1132
|
+
} catch {
|
|
1133
|
+
return [];
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
const candidates = [
|
|
1137
|
+
...(env.PATH ?? "").split(delimiter),
|
|
1138
|
+
env.PNPM_HOME,
|
|
1139
|
+
env.NVM_BIN,
|
|
1140
|
+
env.BUN_INSTALL === void 0 ? void 0 : join(env.BUN_INSTALL, "bin"),
|
|
1141
|
+
env.CARGO_HOME === void 0 ? void 0 : join(env.CARGO_HOME, "bin"),
|
|
1142
|
+
npmPrefix === void 0 ? void 0 : platform === "win32" ? npmPrefix : join(npmPrefix, "bin"),
|
|
1143
|
+
home === "" ? void 0 : join(home, ".local", "bin"),
|
|
1144
|
+
home === "" ? void 0 : join(home, ".npm-global", "bin"),
|
|
1145
|
+
home === "" ? void 0 : join(home, ".bun", "bin"),
|
|
1146
|
+
home === "" ? void 0 : join(home, ".cargo", "bin"),
|
|
1147
|
+
home === "" ? void 0 : join(home, ".volta", "bin"),
|
|
1148
|
+
home === "" ? void 0 : join(home, ".asdf", "shims"),
|
|
1149
|
+
home === "" ? void 0 : join(home, ".local", "share", "mise", "shims"),
|
|
1150
|
+
home === "" ? void 0 : join(home, ".local", "share", "pnpm"),
|
|
1151
|
+
home === "" ? void 0 : join(home, "Library", "pnpm"),
|
|
1152
|
+
...home === "" ? [] : versionBins(join(home, ".nvm", "versions", "node"), ["bin"]),
|
|
1153
|
+
...home === "" ? [] : versionBins(join(home, ".local", "share", "fnm", "node-versions"), [
|
|
1154
|
+
"installation",
|
|
1155
|
+
"bin"
|
|
1156
|
+
]),
|
|
1157
|
+
platform === "win32" ? env.APPDATA : void 0,
|
|
1158
|
+
platform === "darwin" ? "/opt/homebrew/bin" : void 0,
|
|
1159
|
+
platform === "darwin" ? "/usr/local/bin" : void 0,
|
|
1160
|
+
platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
|
|
1161
|
+
platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
|
|
1162
|
+
].filter((entry) => typeof entry === "string" && entry !== "");
|
|
1163
|
+
return [...new Set(candidates)].join(delimiter);
|
|
1164
|
+
}
|
|
1165
|
+
function agentEnvironment(env = process.env) {
|
|
1166
|
+
return { ...env, PATH: agentSearchPath(env) };
|
|
1167
|
+
}
|
|
1043
1168
|
async function pathLookup(binary) {
|
|
1044
|
-
const entries = (
|
|
1169
|
+
const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
|
|
1045
1170
|
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
1046
1171
|
for (const entry of entries) {
|
|
1047
1172
|
for (const extension of extensions) {
|
|
@@ -1058,14 +1183,22 @@ async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
|
|
|
1058
1183
|
const entries = Object.entries(KNOWN_AGENTS);
|
|
1059
1184
|
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
1060
1185
|
const available = await lookup(adapter.binary).catch(() => false);
|
|
1061
|
-
if (!available)
|
|
1062
|
-
return {
|
|
1186
|
+
if (!available) {
|
|
1187
|
+
return {
|
|
1188
|
+
id,
|
|
1189
|
+
name: adapter.name,
|
|
1190
|
+
available,
|
|
1191
|
+
auth: "unknown",
|
|
1192
|
+
efforts: adapter.efforts
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1063
1195
|
const result = await probe2(adapter.binary, adapter.authArgs).catch(() => null);
|
|
1064
1196
|
return {
|
|
1065
1197
|
id,
|
|
1066
1198
|
name: adapter.name,
|
|
1067
1199
|
available,
|
|
1068
|
-
auth: result === null ? "unknown" : adapter.authVerdict(result)
|
|
1200
|
+
auth: result === null ? "unknown" : adapter.authVerdict(result),
|
|
1201
|
+
efforts: adapter.efforts
|
|
1069
1202
|
};
|
|
1070
1203
|
}));
|
|
1071
1204
|
}
|
|
@@ -1098,10 +1231,10 @@ function shownCommand(value) {
|
|
|
1098
1231
|
function claudeActivity(event, cwd) {
|
|
1099
1232
|
if (event.type !== "assistant")
|
|
1100
1233
|
return null;
|
|
1101
|
-
const
|
|
1102
|
-
if (
|
|
1234
|
+
const message2 = record(event.message);
|
|
1235
|
+
if (message2 === null || !Array.isArray(message2.content))
|
|
1103
1236
|
return null;
|
|
1104
|
-
for (const rawBlock of
|
|
1237
|
+
for (const rawBlock of message2.content) {
|
|
1105
1238
|
const block = record(rawBlock);
|
|
1106
1239
|
if (block?.type !== "tool_use" || typeof block.name !== "string")
|
|
1107
1240
|
continue;
|
|
@@ -1170,9 +1303,31 @@ function sessionFrom(agent, line) {
|
|
|
1170
1303
|
return null;
|
|
1171
1304
|
return KNOWN_AGENTS[agent].sessionFrom(event);
|
|
1172
1305
|
}
|
|
1306
|
+
function retryFrom(agent, line) {
|
|
1307
|
+
if (agent !== "claude" && agent !== "cursor")
|
|
1308
|
+
return null;
|
|
1309
|
+
let event;
|
|
1310
|
+
try {
|
|
1311
|
+
event = record(JSON.parse(line));
|
|
1312
|
+
} catch {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
if (event === null || event.type !== "system" || event.subtype !== "api_retry")
|
|
1316
|
+
return null;
|
|
1317
|
+
const attempt = typeof event.attempt === "number" ? event.attempt : 1;
|
|
1318
|
+
return {
|
|
1319
|
+
attempt,
|
|
1320
|
+
max: typeof event.max_retries === "number" ? event.max_retries : null,
|
|
1321
|
+
status: typeof event.error_status === "number" ? event.error_status : null,
|
|
1322
|
+
reason: typeof event.error === "string" && event.error !== "" ? event.error.toLowerCase() : null
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1173
1325
|
function isAgentChoice(value) {
|
|
1174
1326
|
return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
1175
1327
|
}
|
|
1328
|
+
function isAgentEffort(value) {
|
|
1329
|
+
return typeof value === "string" && AGENT_EFFORTS.includes(value);
|
|
1330
|
+
}
|
|
1176
1331
|
async function readWatchConfig(cwd) {
|
|
1177
1332
|
try {
|
|
1178
1333
|
const parsed = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
|
|
@@ -1183,14 +1338,28 @@ async function readWatchConfig(cwd) {
|
|
|
1183
1338
|
}
|
|
1184
1339
|
async function readAgentChoice(cwd) {
|
|
1185
1340
|
const config = await readWatchConfig(cwd);
|
|
1341
|
+
const agent = isAgentChoice(config.agent) ? config.agent : null;
|
|
1342
|
+
const efforts = record(config.efforts);
|
|
1186
1343
|
return {
|
|
1187
|
-
agent
|
|
1344
|
+
agent,
|
|
1345
|
+
effort: agent !== null && agent !== "custom" && isAgentEffort(efforts?.[agent]) ? efforts[agent] : null,
|
|
1188
1346
|
run: typeof config.run === "string" && config.run !== "" ? config.run : null
|
|
1189
1347
|
};
|
|
1190
1348
|
}
|
|
1191
1349
|
async function saveAgentChoice(cwd, choice) {
|
|
1192
1350
|
const config = await readWatchConfig(cwd);
|
|
1193
1351
|
config.agent = choice.agent;
|
|
1352
|
+
if (choice.agent !== "custom" && choice.effort !== void 0) {
|
|
1353
|
+
const efforts = record(config.efforts) ?? {};
|
|
1354
|
+
if (choice.effort === null)
|
|
1355
|
+
delete efforts[choice.agent];
|
|
1356
|
+
else
|
|
1357
|
+
efforts[choice.agent] = choice.effort;
|
|
1358
|
+
if (Object.keys(efforts).length === 0)
|
|
1359
|
+
delete config.efforts;
|
|
1360
|
+
else
|
|
1361
|
+
config.efforts = efforts;
|
|
1362
|
+
}
|
|
1194
1363
|
if (choice.run !== void 0)
|
|
1195
1364
|
config.run = choice.run;
|
|
1196
1365
|
const path = join(cwd, WATCH_PATH);
|
|
@@ -1313,8 +1482,8 @@ async function loadConfig(cwd) {
|
|
|
1313
1482
|
exported = module.default;
|
|
1314
1483
|
}
|
|
1315
1484
|
} catch (error) {
|
|
1316
|
-
const
|
|
1317
|
-
return { config: null, errors: [`${label} could not be loaded: ${
|
|
1485
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1486
|
+
return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
|
|
1318
1487
|
}
|
|
1319
1488
|
const result = normalizeConfig(exported);
|
|
1320
1489
|
return {
|
|
@@ -1384,7 +1553,8 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1384
1553
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1385
1554
|
...input.branch === void 0 ? {} : { branch: input.branch },
|
|
1386
1555
|
...input.file === void 0 ? {} : { file: input.file },
|
|
1387
|
-
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
|
|
1556
|
+
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
|
|
1557
|
+
...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
|
|
1388
1558
|
};
|
|
1389
1559
|
const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
|
|
1390
1560
|
if (check.config === null) {
|
|
@@ -1616,17 +1786,220 @@ async function startAppProcess(options) {
|
|
|
1616
1786
|
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
1787
|
}
|
|
1618
1788
|
|
|
1619
|
-
// ../server/dist/
|
|
1620
|
-
|
|
1789
|
+
// ../server/dist/failure.js
|
|
1790
|
+
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
1791
|
+
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
1792
|
+
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;
|
|
1793
|
+
var LIMIT = /\b429\b|rate limit|usage limit|quota exceeded|too many requests/i;
|
|
1794
|
+
var OVERLOADED = /\b(?:503|529)\b|overloaded|service unavailable/i;
|
|
1795
|
+
function fromStatus(status, reason) {
|
|
1796
|
+
if (status === 401 || status === 403 || reason === "authentication_failed")
|
|
1797
|
+
return "not-signed-in";
|
|
1798
|
+
if (status === 429 || reason === "rate_limit")
|
|
1799
|
+
return "provider-limit";
|
|
1800
|
+
if (status === 529 || status === 503 || reason === "overloaded")
|
|
1801
|
+
return "provider-overloaded";
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
function fromLines(lines) {
|
|
1805
|
+
for (const line of [...lines].reverse()) {
|
|
1806
|
+
if (NEEDS_TRUST.test(line))
|
|
1807
|
+
return "needs-trust";
|
|
1808
|
+
if (NOT_SIGNED_IN.test(line))
|
|
1809
|
+
return "not-signed-in";
|
|
1810
|
+
if (LIMIT.test(line))
|
|
1811
|
+
return "provider-limit";
|
|
1812
|
+
if (OVERLOADED.test(line))
|
|
1813
|
+
return "provider-overloaded";
|
|
1814
|
+
}
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
function attempts(retry) {
|
|
1818
|
+
if (retry === null || retry === void 0)
|
|
1819
|
+
return "";
|
|
1820
|
+
const total = retry.max === null ? retry.attempt : Math.max(retry.attempt, retry.max);
|
|
1821
|
+
return ` It retried ${total} times first.`;
|
|
1822
|
+
}
|
|
1823
|
+
function message(code, input) {
|
|
1824
|
+
const agent = input.agent;
|
|
1825
|
+
switch (code) {
|
|
1826
|
+
case "cancelled":
|
|
1827
|
+
return "You stopped this run.";
|
|
1828
|
+
case "stopped":
|
|
1829
|
+
return "Leglas shut down while this was running.";
|
|
1830
|
+
case "missing-agent":
|
|
1831
|
+
return `${agent} could not be started. Its command is not on this machine's PATH any more.`;
|
|
1832
|
+
case "not-signed-in":
|
|
1833
|
+
return `${agent} is not signed in. Sign in to it in a terminal, then run this again.`;
|
|
1834
|
+
case "provider-overloaded":
|
|
1835
|
+
return `${agent}'s provider was overloaded and gave up.${attempts(input.retry)}`;
|
|
1836
|
+
case "provider-limit":
|
|
1837
|
+
return `${agent} reported a rate or usage limit, so nothing ran.`;
|
|
1838
|
+
case "needs-trust":
|
|
1839
|
+
return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
|
|
1840
|
+
case "not-registered":
|
|
1841
|
+
return `${agent} finished without registering the new direction, so nothing reached the rail. Its last output is in the Leglas terminal.`;
|
|
1842
|
+
case "agent-error":
|
|
1843
|
+
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.`;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function classifyFailure(input) {
|
|
1847
|
+
const lines = input.lines ?? [];
|
|
1848
|
+
const error = input.error ?? null;
|
|
1849
|
+
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";
|
|
1850
|
+
return { code, message: message(code, input) };
|
|
1851
|
+
}
|
|
1852
|
+
function sessionShaped(code) {
|
|
1853
|
+
return code === "agent-error";
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// ../server/dist/annotations.js
|
|
1621
1857
|
import { randomBytes } from "crypto";
|
|
1858
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1622
1859
|
import { dirname as dirname4, join as join5 } from "path";
|
|
1860
|
+
var ANNOTATIONS_PATH = ".leglas/annotations.json";
|
|
1861
|
+
var NOTE_CAP = 500;
|
|
1862
|
+
var SELECTOR_CAP = 300;
|
|
1863
|
+
var TEXT_CAP = 120;
|
|
1864
|
+
var TAG_CAP = 40;
|
|
1865
|
+
var CLASS_CAP = 8;
|
|
1866
|
+
var CLASS_LENGTH_CAP = 60;
|
|
1867
|
+
var COVERS_CAP = 8;
|
|
1868
|
+
function isRecord2(value) {
|
|
1869
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1870
|
+
}
|
|
1871
|
+
function text(value, cap) {
|
|
1872
|
+
return typeof value === "string" ? value.trim().slice(0, cap) : "";
|
|
1873
|
+
}
|
|
1874
|
+
function fraction(value) {
|
|
1875
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1876
|
+
return 0.5;
|
|
1877
|
+
return Math.min(1, Math.max(0, value));
|
|
1878
|
+
}
|
|
1879
|
+
function size(value) {
|
|
1880
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
|
|
1881
|
+
}
|
|
1882
|
+
function anchorFrom(value) {
|
|
1883
|
+
if (!isRecord2(value))
|
|
1884
|
+
return null;
|
|
1885
|
+
const selector = text(value["selector"], SELECTOR_CAP);
|
|
1886
|
+
if (selector === "")
|
|
1887
|
+
return null;
|
|
1888
|
+
const rect = isRecord2(value["rect"]) ? value["rect"] : {};
|
|
1889
|
+
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)) : [];
|
|
1890
|
+
const rawRegion = isRecord2(value["region"]) ? value["region"] : null;
|
|
1891
|
+
const region = rawRegion === null ? null : {
|
|
1892
|
+
height: fraction(rawRegion["height"]),
|
|
1893
|
+
width: fraction(rawRegion["width"]),
|
|
1894
|
+
x: fraction(rawRegion["x"]),
|
|
1895
|
+
y: fraction(rawRegion["y"])
|
|
1896
|
+
};
|
|
1897
|
+
const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
|
|
1898
|
+
tag: text(entry["tag"], TAG_CAP) || "element",
|
|
1899
|
+
text: text(entry["text"], TEXT_CAP)
|
|
1900
|
+
})) : [];
|
|
1901
|
+
return {
|
|
1902
|
+
classes,
|
|
1903
|
+
...covers.length === 0 ? {} : { covers },
|
|
1904
|
+
...region === null ? {} : { region },
|
|
1905
|
+
rect: {
|
|
1906
|
+
height: size(rect["height"]),
|
|
1907
|
+
width: size(rect["width"]),
|
|
1908
|
+
x: size(rect["x"]),
|
|
1909
|
+
y: size(rect["y"])
|
|
1910
|
+
},
|
|
1911
|
+
selector,
|
|
1912
|
+
spot: {
|
|
1913
|
+
x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
|
|
1914
|
+
y: fraction(isRecord2(value["spot"]) ? value["spot"]["y"] : void 0)
|
|
1915
|
+
},
|
|
1916
|
+
tag: text(value["tag"], TAG_CAP) || "element",
|
|
1917
|
+
text: text(value["text"], TEXT_CAP),
|
|
1918
|
+
viewport: size(value["viewport"])
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
async function readAnnotations(cwd) {
|
|
1922
|
+
try {
|
|
1923
|
+
const raw = await readFile4(join5(cwd, ANNOTATIONS_PATH), "utf8");
|
|
1924
|
+
const parsed = JSON.parse(raw);
|
|
1925
|
+
if (!Array.isArray(parsed.annotations))
|
|
1926
|
+
return [];
|
|
1927
|
+
return parsed.annotations.flatMap((entry, index) => {
|
|
1928
|
+
if (!isRecord2(entry))
|
|
1929
|
+
return [];
|
|
1930
|
+
const anchor = anchorFrom(entry["anchor"]);
|
|
1931
|
+
const title = text(entry["title"], TAG_CAP * 4);
|
|
1932
|
+
if (anchor === null || title === "")
|
|
1933
|
+
return [];
|
|
1934
|
+
return [
|
|
1935
|
+
{
|
|
1936
|
+
anchor,
|
|
1937
|
+
id: typeof entry["id"] === "string" ? entry["id"] : String(index),
|
|
1938
|
+
note: text(entry["note"], NOTE_CAP),
|
|
1939
|
+
title
|
|
1940
|
+
}
|
|
1941
|
+
];
|
|
1942
|
+
});
|
|
1943
|
+
} catch {
|
|
1944
|
+
return [];
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function write(cwd, annotations) {
|
|
1948
|
+
const path = join5(cwd, ANNOTATIONS_PATH);
|
|
1949
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1950
|
+
await writeFile3(path, `${JSON.stringify({ annotations }, null, 2)}
|
|
1951
|
+
`, "utf8");
|
|
1952
|
+
}
|
|
1953
|
+
async function addAnnotation(cwd, input) {
|
|
1954
|
+
const annotation = { ...input, id: randomBytes(6).toString("base64url") };
|
|
1955
|
+
await write(cwd, [...await readAnnotations(cwd), annotation]);
|
|
1956
|
+
return annotation;
|
|
1957
|
+
}
|
|
1958
|
+
async function removeAnnotations(cwd, ids) {
|
|
1959
|
+
const wanted = new Set(ids);
|
|
1960
|
+
const annotations = await readAnnotations(cwd);
|
|
1961
|
+
const remaining = annotations.filter((entry) => !wanted.has(entry.id));
|
|
1962
|
+
const dropped = annotations.length - remaining.length;
|
|
1963
|
+
if (dropped > 0)
|
|
1964
|
+
await write(cwd, remaining);
|
|
1965
|
+
return dropped;
|
|
1966
|
+
}
|
|
1967
|
+
function annotationsFor(annotations, title) {
|
|
1968
|
+
return annotations.filter((entry) => entry.title === title);
|
|
1969
|
+
}
|
|
1970
|
+
function describeAnchor(anchor) {
|
|
1971
|
+
const where = `about ${anchor.rect.width}\xD7${anchor.rect.height} at (${anchor.rect.x}, ${anchor.rect.y}) in a ${anchor.viewport}px-wide viewport`;
|
|
1972
|
+
if (anchor.region !== void 0) {
|
|
1973
|
+
const covered = (anchor.covers ?? []).map((entry) => entry.text === "" ? `<${entry.tag}>` : `<${entry.tag}> \u201C${entry.text}\u201D`).join(", ");
|
|
1974
|
+
const inside = covered === "" ? "" : ` covering ${covered};`;
|
|
1975
|
+
return `an area inside <${anchor.tag}>;${inside} path ${anchor.selector}; ${where}`;
|
|
1976
|
+
}
|
|
1977
|
+
const parts = [`<${anchor.tag}>`];
|
|
1978
|
+
if (anchor.classes.length > 0)
|
|
1979
|
+
parts.push(`class "${anchor.classes.join(" ")}"`);
|
|
1980
|
+
if (anchor.text !== "")
|
|
1981
|
+
parts.push(`reading \u201C${anchor.text}\u201D`);
|
|
1982
|
+
return `${parts.join(", ")}; path ${anchor.selector}; ${where}`;
|
|
1983
|
+
}
|
|
1984
|
+
function describeAnnotations(annotations) {
|
|
1985
|
+
return annotations.map((annotation, index) => {
|
|
1986
|
+
const said = annotation.note === "" ? "Look at this." : annotation.note;
|
|
1987
|
+
return `${index + 1}. ${said}
|
|
1988
|
+
The element: ${describeAnchor(annotation.anchor)}`;
|
|
1989
|
+
}).join("\n\n");
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// ../server/dist/requests.js
|
|
1993
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1994
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1995
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1623
1996
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1624
|
-
function
|
|
1997
|
+
function variantSlot(url) {
|
|
1625
1998
|
if (!url.startsWith("/"))
|
|
1626
1999
|
return null;
|
|
1627
|
-
const query = url.slice(url.indexOf("?") + 1);
|
|
1628
2000
|
if (!url.includes("?"))
|
|
1629
2001
|
return null;
|
|
2002
|
+
const query = url.slice(url.indexOf("?") + 1);
|
|
1630
2003
|
for (const pair of query.split("&")) {
|
|
1631
2004
|
const [rawKey, rawValue] = pair.split("=");
|
|
1632
2005
|
if (rawKey === void 0 || rawValue === void 0)
|
|
@@ -1637,37 +2010,125 @@ function targetFor(url) {
|
|
|
1637
2010
|
const option = decodeURIComponent(rawValue);
|
|
1638
2011
|
if (!SAFE_SEGMENT.test(surface) || !SAFE_SEGMENT.test(option))
|
|
1639
2012
|
return null;
|
|
1640
|
-
return
|
|
2013
|
+
return { surface, option };
|
|
1641
2014
|
}
|
|
1642
2015
|
return null;
|
|
1643
2016
|
}
|
|
1644
|
-
function
|
|
2017
|
+
function targetFor(url) {
|
|
2018
|
+
const slot = variantSlot(url);
|
|
2019
|
+
return slot === null ? null : `.leglas/variants/${slot.surface}/${slot.option}.tsx`;
|
|
2020
|
+
}
|
|
2021
|
+
function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx -y leglas") {
|
|
1645
2022
|
const target = preview.file ?? targetFor(preview.url);
|
|
1646
2023
|
const cleaned = intent.trim();
|
|
2024
|
+
const asked = changeBlock(cleaned, notes);
|
|
2025
|
+
const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
|
|
2026
|
+
const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand) : replacePrompt(preview, asked, target);
|
|
2027
|
+
return { prompt, target, mode };
|
|
2028
|
+
}
|
|
2029
|
+
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.`;
|
|
2030
|
+
function changeBlock(cleaned, notes) {
|
|
2031
|
+
if (notes.length === 0)
|
|
2032
|
+
return `What to change: ${cleaned}`;
|
|
2033
|
+
const many = notes.length === 1 ? "a note" : `${notes.length} notes`;
|
|
2034
|
+
const lead = cleaned === "" ? `What to change, left as ${many} on the design itself:` : `What to change: ${cleaned}
|
|
2035
|
+
|
|
2036
|
+
And ${many} left on the design itself:`;
|
|
2037
|
+
return `${lead}
|
|
2038
|
+
|
|
2039
|
+
${describeAnnotations(notes)}
|
|
2040
|
+
|
|
2041
|
+
${ANCHORS}`;
|
|
2042
|
+
}
|
|
2043
|
+
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.
|
|
2044
|
+
|
|
2045
|
+
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.
|
|
2046
|
+
|
|
2047
|
+
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.`;
|
|
2048
|
+
function registrationCommand(leglasCommand) {
|
|
2049
|
+
return `${leglasCommand} add`;
|
|
2050
|
+
}
|
|
2051
|
+
function replacePrompt(preview, asked, target) {
|
|
1647
2052
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1648
2053
|
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
1649
|
-
|
|
2054
|
+
return `In this project, change only the "${preview.title}" design direction. ${where}
|
|
2055
|
+
|
|
2056
|
+
${asked}
|
|
2057
|
+
|
|
2058
|
+
${pace}${SCOPE} The direction is already registered, so nothing needs re-registering.`;
|
|
2059
|
+
}
|
|
2060
|
+
function variantPrompt(preview, recorded, asked, target, leglasCommand) {
|
|
2061
|
+
const slot = variantSlot(preview.url);
|
|
2062
|
+
const parent = JSON.stringify(preview.title);
|
|
2063
|
+
const askedFor = JSON.stringify(recorded);
|
|
2064
|
+
const add = registrationCommand(leglasCommand);
|
|
2065
|
+
const source = target === null ? `Find what renders it first.` : `Its source is ${target}.`;
|
|
2066
|
+
const [make, register] = preview.file !== void 0 ? [
|
|
2067
|
+
`Copy that file to a new file beside it and make the change in the copy.`,
|
|
2068
|
+
` ${add} --title "<name>" --file "<the new file>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
2069
|
+
] : slot !== null ? [
|
|
2070
|
+
`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.`,
|
|
2071
|
+
` ${add} --title "<name>" --url "/?v-${slot.surface}=<key>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
2072
|
+
] : [
|
|
2073
|
+
`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.`,
|
|
2074
|
+
` ${add} --title "<name>" --url "<the URL that shows it>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
2075
|
+
];
|
|
2076
|
+
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
2077
|
|
|
1651
|
-
|
|
2078
|
+
${source} ${make}
|
|
1652
2079
|
|
|
1653
|
-
${
|
|
2080
|
+
${asked}
|
|
1654
2081
|
|
|
1655
|
-
|
|
1656
|
-
|
|
2082
|
+
Then register it, which is what puts it on the rail:
|
|
2083
|
+
|
|
2084
|
+
${register}
|
|
2085
|
+
|
|
2086
|
+
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.
|
|
2087
|
+
|
|
2088
|
+
${SCOPE}`;
|
|
1657
2089
|
}
|
|
1658
2090
|
var REQUESTS_PATH = ".leglas/requests.json";
|
|
2091
|
+
var TERMINAL = ["failed", "cancelled"];
|
|
2092
|
+
function isTerminal(status) {
|
|
2093
|
+
return TERMINAL.includes(status);
|
|
2094
|
+
}
|
|
2095
|
+
var FAILURE_CODES = [
|
|
2096
|
+
"cancelled",
|
|
2097
|
+
"stopped",
|
|
2098
|
+
"missing-agent",
|
|
2099
|
+
"not-signed-in",
|
|
2100
|
+
"provider-overloaded",
|
|
2101
|
+
"provider-limit",
|
|
2102
|
+
"needs-trust",
|
|
2103
|
+
"not-registered",
|
|
2104
|
+
"agent-error"
|
|
2105
|
+
];
|
|
2106
|
+
function failureOf(value) {
|
|
2107
|
+
if (typeof value !== "object" || value === null)
|
|
2108
|
+
return null;
|
|
2109
|
+
const entry = value;
|
|
2110
|
+
if (typeof entry.message !== "string" || entry.message === "")
|
|
2111
|
+
return null;
|
|
2112
|
+
if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
|
|
2113
|
+
return null;
|
|
2114
|
+
return { code: entry.code, message: entry.message };
|
|
2115
|
+
}
|
|
1659
2116
|
async function readRequests(cwd) {
|
|
1660
2117
|
try {
|
|
1661
|
-
const raw = await
|
|
2118
|
+
const raw = await readFile5(join6(cwd, REQUESTS_PATH), "utf8");
|
|
1662
2119
|
const parsed = JSON.parse(raw);
|
|
1663
2120
|
if (!Array.isArray(parsed.requests))
|
|
1664
2121
|
return [];
|
|
1665
2122
|
return parsed.requests.map((request, index) => {
|
|
1666
|
-
const entry = request;
|
|
2123
|
+
const { failure: rawFailure, ...entry } = request;
|
|
2124
|
+
const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
|
|
2125
|
+
const failure = isTerminal(status) ? failureOf(rawFailure) : null;
|
|
1667
2126
|
return {
|
|
1668
2127
|
...entry,
|
|
1669
2128
|
id: typeof entry.id === "string" ? entry.id : String(index),
|
|
1670
|
-
status
|
|
2129
|
+
status,
|
|
2130
|
+
mode: entry.mode === "variant" ? "variant" : "replace",
|
|
2131
|
+
...failure === null ? {} : { failure }
|
|
1671
2132
|
};
|
|
1672
2133
|
});
|
|
1673
2134
|
} catch {
|
|
@@ -1675,23 +2136,23 @@ async function readRequests(cwd) {
|
|
|
1675
2136
|
}
|
|
1676
2137
|
}
|
|
1677
2138
|
async function writeQueue(cwd, requests) {
|
|
1678
|
-
const path =
|
|
1679
|
-
await
|
|
1680
|
-
await
|
|
2139
|
+
const path = join6(cwd, REQUESTS_PATH);
|
|
2140
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2141
|
+
await writeFile4(path, `${JSON.stringify({ requests }, null, 2)}
|
|
1681
2142
|
`, "utf8");
|
|
1682
2143
|
}
|
|
1683
2144
|
async function appendRequest(cwd, request) {
|
|
1684
2145
|
await writeQueue(cwd, [
|
|
1685
2146
|
...await readRequests(cwd),
|
|
1686
|
-
{ ...request, id:
|
|
2147
|
+
{ ...request, id: randomBytes2(6).toString("base64url"), status: "queued" }
|
|
1687
2148
|
]);
|
|
1688
2149
|
}
|
|
1689
2150
|
async function collectRequests(cwd) {
|
|
1690
2151
|
const requests = await readRequests(cwd);
|
|
1691
|
-
const collected = requests.map((request) => ({ ...request, status: "picked-up" })
|
|
1692
|
-
if (requests.some((request) => request.status
|
|
2152
|
+
const collected = requests.map((request) => isTerminal(request.status) ? request : { ...request, status: "picked-up" });
|
|
2153
|
+
if (requests.some((request) => request.status === "queued"))
|
|
1693
2154
|
await writeQueue(cwd, collected);
|
|
1694
|
-
return collected;
|
|
2155
|
+
return collected.filter((request) => !isTerminal(request.status));
|
|
1695
2156
|
}
|
|
1696
2157
|
async function markPickedUp(cwd, id) {
|
|
1697
2158
|
const requests = await readRequests(cwd);
|
|
@@ -1700,6 +2161,17 @@ async function markPickedUp(cwd, id) {
|
|
|
1700
2161
|
await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
|
|
1701
2162
|
return true;
|
|
1702
2163
|
}
|
|
2164
|
+
async function markFailed(cwd, id, failure) {
|
|
2165
|
+
const requests = await readRequests(cwd);
|
|
2166
|
+
if (!requests.some((request) => request.id === id))
|
|
2167
|
+
return false;
|
|
2168
|
+
await writeQueue(cwd, requests.map((request) => request.id === id ? {
|
|
2169
|
+
...request,
|
|
2170
|
+
status: failure.code === "cancelled" ? "cancelled" : "failed",
|
|
2171
|
+
failure
|
|
2172
|
+
} : request));
|
|
2173
|
+
return true;
|
|
2174
|
+
}
|
|
1703
2175
|
async function removeRequest(cwd, id) {
|
|
1704
2176
|
const requests = await readRequests(cwd);
|
|
1705
2177
|
const remaining = requests.filter((request) => request.id !== id);
|
|
@@ -1710,7 +2182,7 @@ async function removeRequest(cwd, id) {
|
|
|
1710
2182
|
}
|
|
1711
2183
|
async function clearRequests(cwd) {
|
|
1712
2184
|
const requests = await readRequests(cwd);
|
|
1713
|
-
const pending = requests.filter((request) => request.status
|
|
2185
|
+
const pending = requests.filter((request) => request.status === "queued");
|
|
1714
2186
|
const cleared = requests.length - pending.length;
|
|
1715
2187
|
if (cleared > 0)
|
|
1716
2188
|
await writeQueue(cwd, pending);
|
|
@@ -1719,10 +2191,13 @@ async function clearRequests(cwd) {
|
|
|
1719
2191
|
|
|
1720
2192
|
// ../server/dist/runner.js
|
|
1721
2193
|
import { spawn as nodeSpawn } from "child_process";
|
|
2194
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2195
|
+
import { join as join7 } from "path";
|
|
1722
2196
|
var POLL_MS = 2e3;
|
|
1723
2197
|
var OUTPUT_LINES = 20;
|
|
2198
|
+
var CANCEL_GRACE_MS = 5e3;
|
|
1724
2199
|
var SESSION_TURNS_CAP = 8;
|
|
1725
|
-
function resolveCommand(choice, prompt, sessionId = null) {
|
|
2200
|
+
function resolveCommand(choice, prompt, sessionId = null, registration = null) {
|
|
1726
2201
|
if (choice.agent === null)
|
|
1727
2202
|
return null;
|
|
1728
2203
|
if (choice.agent === "custom") {
|
|
@@ -1734,12 +2209,13 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1734
2209
|
return { agent: "custom", name: "Custom", ...commandFor(parsed.template, prompt), resumed: false };
|
|
1735
2210
|
}
|
|
1736
2211
|
const adapter = KNOWN_AGENTS[choice.agent];
|
|
2212
|
+
const allow = registration !== null && "allowArgs" in adapter ? adapter.allowArgs(registration) : [];
|
|
1737
2213
|
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1738
2214
|
return {
|
|
1739
2215
|
agent: choice.agent,
|
|
1740
2216
|
name: adapter.name,
|
|
1741
2217
|
command: adapter.binary,
|
|
1742
|
-
args: adapter.resumeArgs(sessionId, prompt),
|
|
2218
|
+
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort), ...allow],
|
|
1743
2219
|
resumed: true
|
|
1744
2220
|
};
|
|
1745
2221
|
}
|
|
@@ -1747,7 +2223,7 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1747
2223
|
agent: choice.agent,
|
|
1748
2224
|
name: adapter.name,
|
|
1749
2225
|
command: adapter.binary,
|
|
1750
|
-
args: adapter.args(prompt),
|
|
2226
|
+
args: [...adapter.args(prompt, choice.effort), ...allow],
|
|
1751
2227
|
resumed: false
|
|
1752
2228
|
};
|
|
1753
2229
|
}
|
|
@@ -1770,19 +2246,24 @@ function lineReader(stream, onLine) {
|
|
|
1770
2246
|
return flush;
|
|
1771
2247
|
}
|
|
1772
2248
|
function defaultSpawn(command, args, options) {
|
|
1773
|
-
return nodeSpawn(command, args, options);
|
|
2249
|
+
return nodeSpawn(command, args, { ...options, env: agentEnvironment() });
|
|
1774
2250
|
}
|
|
1775
2251
|
function startRunner(options) {
|
|
1776
2252
|
const spawn4 = options.spawn ?? defaultSpawn;
|
|
1777
2253
|
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1778
2254
|
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1779
2255
|
const failed = /* @__PURE__ */ new Set();
|
|
2256
|
+
const setLater = options.setTimeout ?? ((callback, milliseconds) => {
|
|
2257
|
+
setTimeout(callback, milliseconds).unref?.();
|
|
2258
|
+
});
|
|
1780
2259
|
let state = {
|
|
1781
2260
|
running: false,
|
|
1782
2261
|
requestId: null,
|
|
1783
2262
|
agent: null,
|
|
1784
2263
|
activity: null,
|
|
1785
|
-
startedAt: null
|
|
2264
|
+
startedAt: null,
|
|
2265
|
+
stopping: false,
|
|
2266
|
+
waiting: null
|
|
1786
2267
|
};
|
|
1787
2268
|
let stopped = false;
|
|
1788
2269
|
let ticking = null;
|
|
@@ -1790,15 +2271,30 @@ function startRunner(options) {
|
|
|
1790
2271
|
let active = null;
|
|
1791
2272
|
const sessions = /* @__PURE__ */ new Map();
|
|
1792
2273
|
const idle = () => {
|
|
1793
|
-
state = {
|
|
2274
|
+
state = {
|
|
2275
|
+
running: false,
|
|
2276
|
+
requestId: null,
|
|
2277
|
+
agent: null,
|
|
2278
|
+
activity: null,
|
|
2279
|
+
startedAt: null,
|
|
2280
|
+
stopping: false,
|
|
2281
|
+
waiting: null
|
|
2282
|
+
};
|
|
1794
2283
|
};
|
|
1795
2284
|
const rememberLine = (lines, line) => {
|
|
1796
2285
|
lines.push(line);
|
|
1797
2286
|
if (lines.length > OUTPUT_LINES)
|
|
1798
2287
|
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1799
2288
|
};
|
|
1800
|
-
const reportFailure = (request,
|
|
1801
|
-
|
|
2289
|
+
const reportFailure = async (request, failure, lines) => {
|
|
2290
|
+
await markFailed(options.cwd, request.id, failure).catch(() => {
|
|
2291
|
+
});
|
|
2292
|
+
failed.add(request.id);
|
|
2293
|
+
if (failure.code === "cancelled") {
|
|
2294
|
+
console.error(`Leglas stopped the run for ${request.title}.`);
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
console.error(`Leglas agent failed for ${request.title}: ${failure.message}`);
|
|
1802
2298
|
for (const line of lines)
|
|
1803
2299
|
console.error(` ${line}`);
|
|
1804
2300
|
};
|
|
@@ -1816,19 +2312,31 @@ function startRunner(options) {
|
|
|
1816
2312
|
error: error instanceof Error ? error.message : String(error)
|
|
1817
2313
|
});
|
|
1818
2314
|
}
|
|
1819
|
-
const current = {
|
|
2315
|
+
const current = {
|
|
2316
|
+
child,
|
|
2317
|
+
requestId: request.id,
|
|
2318
|
+
cancelled: false,
|
|
2319
|
+
abandon: () => {
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
1820
2322
|
active = current;
|
|
1821
2323
|
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1822
2324
|
rememberLine(lines, line);
|
|
1823
2325
|
const sessionId = sessionFrom(resolved.agent, line);
|
|
1824
2326
|
if (sessionId !== null)
|
|
1825
2327
|
observed.sessionId = sessionId;
|
|
2328
|
+
const retry = retryFrom(resolved.agent, line);
|
|
2329
|
+
if (retry !== null) {
|
|
2330
|
+
observed.retry = retry;
|
|
2331
|
+
if (active === current)
|
|
2332
|
+
state = { ...state, waiting: retry };
|
|
2333
|
+
}
|
|
1826
2334
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1827
2335
|
if (activity !== null) {
|
|
1828
2336
|
if (activity.startsWith("editing"))
|
|
1829
2337
|
observed.edited = true;
|
|
1830
2338
|
if (active === current)
|
|
1831
|
-
state = { ...state, activity };
|
|
2339
|
+
state = { ...state, activity, waiting: null };
|
|
1832
2340
|
}
|
|
1833
2341
|
});
|
|
1834
2342
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
@@ -1842,6 +2350,7 @@ function startRunner(options) {
|
|
|
1842
2350
|
stderrFlush();
|
|
1843
2351
|
resolve(outcome);
|
|
1844
2352
|
};
|
|
2353
|
+
current.abandon = () => settle({ ok: false, error: "cancelled" });
|
|
1845
2354
|
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1846
2355
|
child.once("close", (code, signal) => {
|
|
1847
2356
|
if (current.cancelled)
|
|
@@ -1855,10 +2364,12 @@ function startRunner(options) {
|
|
|
1855
2364
|
active = null;
|
|
1856
2365
|
});
|
|
1857
2366
|
};
|
|
2367
|
+
const registered = () => readFile6(join7(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
1858
2368
|
const handle = async (request, choice) => {
|
|
1859
2369
|
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1860
2370
|
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1861
|
-
|
|
2371
|
+
const registration = request.mode === "variant" && options.leglasCommand !== void 0 ? registrationCommand(options.leglasCommand) : null;
|
|
2372
|
+
let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null, registration);
|
|
1862
2373
|
if (resolved === null)
|
|
1863
2374
|
return;
|
|
1864
2375
|
const lines = [];
|
|
@@ -1866,7 +2377,7 @@ function startRunner(options) {
|
|
|
1866
2377
|
if (!await markPickedUp(options.cwd, request.id))
|
|
1867
2378
|
return;
|
|
1868
2379
|
if (stopped) {
|
|
1869
|
-
|
|
2380
|
+
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
1870
2381
|
return;
|
|
1871
2382
|
}
|
|
1872
2383
|
state = {
|
|
@@ -1874,25 +2385,47 @@ function startRunner(options) {
|
|
|
1874
2385
|
requestId: request.id,
|
|
1875
2386
|
agent: resolved.name,
|
|
1876
2387
|
activity: null,
|
|
1877
|
-
startedAt: Date.now()
|
|
2388
|
+
startedAt: Date.now(),
|
|
2389
|
+
stopping: false,
|
|
2390
|
+
waiting: null
|
|
2391
|
+
};
|
|
2392
|
+
const observed = {
|
|
2393
|
+
sessionId: null,
|
|
2394
|
+
edited: false,
|
|
2395
|
+
retry: null
|
|
1878
2396
|
};
|
|
1879
|
-
const
|
|
2397
|
+
const before = request.mode === "variant" ? await registered() : null;
|
|
2398
|
+
const agent = resolved.name;
|
|
1880
2399
|
let outcome = await runChild(request, resolved, lines, observed);
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2400
|
+
const verdict = () => classifyFailure({
|
|
2401
|
+
agent,
|
|
2402
|
+
error: outcome.ok ? null : stopped && outcome.error === "cancelled" ? "stopped by shutdown" : outcome.error,
|
|
2403
|
+
exitCode: outcome.ok ? outcome.code : null,
|
|
2404
|
+
lines,
|
|
2405
|
+
retry: observed.retry
|
|
2406
|
+
});
|
|
2407
|
+
let failure = verdict();
|
|
2408
|
+
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
|
|
2409
|
+
// child settling and the retry starting finds no child to cancel, so
|
|
2410
|
+
// nothing says "cancelled". Stopped still means stopped.
|
|
1885
2411
|
!stopped) {
|
|
1886
2412
|
sessions.delete(resolved.agent);
|
|
1887
|
-
const cold = resolveCommand(choice, request.prompt);
|
|
2413
|
+
const cold = resolveCommand(choice, request.prompt, null, registration);
|
|
1888
2414
|
if (cold !== null) {
|
|
1889
2415
|
resolved = cold;
|
|
1890
2416
|
observed.sessionId = null;
|
|
1891
|
-
|
|
2417
|
+
observed.retry = null;
|
|
2418
|
+
state = { ...state, activity: null, waiting: null };
|
|
1892
2419
|
outcome = await runChild(request, resolved, lines, observed);
|
|
2420
|
+
failure = verdict();
|
|
1893
2421
|
}
|
|
1894
2422
|
}
|
|
1895
2423
|
if (outcome.ok && outcome.code === 0) {
|
|
2424
|
+
if (request.mode === "variant" && await registered() === before) {
|
|
2425
|
+
sessions.delete(resolved.agent);
|
|
2426
|
+
await reportFailure(request, classifyFailure({ agent, error: "not-registered" }), lines);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
1896
2429
|
if (observed.sessionId !== null) {
|
|
1897
2430
|
const previous = sessions.get(resolved.agent);
|
|
1898
2431
|
sessions.set(resolved.agent, {
|
|
@@ -1900,12 +2433,14 @@ function startRunner(options) {
|
|
|
1900
2433
|
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1901
2434
|
});
|
|
1902
2435
|
}
|
|
2436
|
+
if (request.mode === "replace" && request.notes !== void 0) {
|
|
2437
|
+
await removeAnnotations(options.cwd, request.notes).catch(() => 0);
|
|
2438
|
+
}
|
|
1903
2439
|
await removeRequest(options.cwd, request.id);
|
|
1904
2440
|
return;
|
|
1905
2441
|
}
|
|
1906
2442
|
sessions.delete(resolved.agent);
|
|
1907
|
-
|
|
1908
|
-
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
2443
|
+
await reportFailure(request, failure, lines);
|
|
1909
2444
|
} finally {
|
|
1910
2445
|
idle();
|
|
1911
2446
|
}
|
|
@@ -1939,12 +2474,23 @@ function startRunner(options) {
|
|
|
1939
2474
|
return false;
|
|
1940
2475
|
if (id !== void 0 && active.requestId !== id)
|
|
1941
2476
|
return false;
|
|
1942
|
-
|
|
1943
|
-
|
|
2477
|
+
const current = active;
|
|
2478
|
+
current.cancelled = true;
|
|
2479
|
+
failed.add(current.requestId);
|
|
2480
|
+
state = { ...state, stopping: true, waiting: null };
|
|
1944
2481
|
try {
|
|
1945
|
-
|
|
2482
|
+
current.child.kill("SIGTERM");
|
|
1946
2483
|
} catch {
|
|
1947
2484
|
}
|
|
2485
|
+
setLater(() => {
|
|
2486
|
+
if (active !== current)
|
|
2487
|
+
return;
|
|
2488
|
+
try {
|
|
2489
|
+
current.child.kill("SIGKILL");
|
|
2490
|
+
} catch {
|
|
2491
|
+
}
|
|
2492
|
+
current.abandon();
|
|
2493
|
+
}, CANCEL_GRACE_MS);
|
|
1948
2494
|
return true;
|
|
1949
2495
|
};
|
|
1950
2496
|
const stop = () => {
|
|
@@ -1969,12 +2515,12 @@ function startRunner(options) {
|
|
|
1969
2515
|
}
|
|
1970
2516
|
|
|
1971
2517
|
// ../server/dist/renames.js
|
|
1972
|
-
import { mkdir as
|
|
1973
|
-
import { dirname as
|
|
2518
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
2519
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1974
2520
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
1975
2521
|
async function readRenames(cwd) {
|
|
1976
2522
|
try {
|
|
1977
|
-
const raw = await
|
|
2523
|
+
const raw = await readFile7(join8(cwd, RENAMES_PATH), "utf8");
|
|
1978
2524
|
const parsed = JSON.parse(raw);
|
|
1979
2525
|
if (parsed.renames === null || typeof parsed.renames !== "object")
|
|
1980
2526
|
return {};
|
|
@@ -1984,9 +2530,9 @@ async function readRenames(cwd) {
|
|
|
1984
2530
|
}
|
|
1985
2531
|
}
|
|
1986
2532
|
async function writeRenames(cwd, renames) {
|
|
1987
|
-
const path =
|
|
1988
|
-
await
|
|
1989
|
-
await
|
|
2533
|
+
const path = join8(cwd, RENAMES_PATH);
|
|
2534
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2535
|
+
await writeFile5(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1990
2536
|
`, "utf8");
|
|
1991
2537
|
}
|
|
1992
2538
|
function resolveTitle(input, titles, renames) {
|
|
@@ -2004,7 +2550,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
2004
2550
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
2005
2551
|
import http2 from "http";
|
|
2006
2552
|
import net3 from "net";
|
|
2007
|
-
import { extname, join as
|
|
2553
|
+
import { extname, join as join9, normalize, relative as relative3 } from "path";
|
|
2008
2554
|
var LEGLAS_PREFIX = "/leglas";
|
|
2009
2555
|
var DEFAULT_PORT = 4100;
|
|
2010
2556
|
var PORT_ATTEMPTS = 20;
|
|
@@ -2077,6 +2623,9 @@ function isTrustedMutation(req) {
|
|
|
2077
2623
|
return false;
|
|
2078
2624
|
}
|
|
2079
2625
|
}
|
|
2626
|
+
function isEnded(request, failedIds) {
|
|
2627
|
+
return isTerminal(request.status) || failedIds.includes(request.id);
|
|
2628
|
+
}
|
|
2080
2629
|
function hasJsonBody(req) {
|
|
2081
2630
|
const contentType = req.headers["content-type"];
|
|
2082
2631
|
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
@@ -2103,7 +2652,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
2103
2652
|
}
|
|
2104
2653
|
function serveFrom(res, dir, relativePath) {
|
|
2105
2654
|
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
2106
|
-
const candidate =
|
|
2655
|
+
const candidate = join9(dir, relative5);
|
|
2107
2656
|
if (!candidate.startsWith(dir))
|
|
2108
2657
|
return false;
|
|
2109
2658
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -2188,7 +2737,7 @@ async function bind(server, requested) {
|
|
|
2188
2737
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
2189
2738
|
}
|
|
2190
2739
|
async function startServer(options) {
|
|
2191
|
-
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
2740
|
+
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
2192
2741
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
2193
2742
|
const proxy = createProxyHandler({ target });
|
|
2194
2743
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
@@ -2207,18 +2756,16 @@ async function startServer(options) {
|
|
|
2207
2756
|
});
|
|
2208
2757
|
return agentsInflight;
|
|
2209
2758
|
};
|
|
2210
|
-
const currentAgents = () => {
|
|
2211
|
-
if (agentsCache === null)
|
|
2759
|
+
const currentAgents = (refresh = false) => {
|
|
2760
|
+
if (refresh || agentsCache === null || Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
|
|
2212
2761
|
return probeAgents();
|
|
2213
|
-
if (Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
|
|
2214
|
-
void probeAgents().catch(() => {
|
|
2215
|
-
});
|
|
2216
2762
|
}
|
|
2217
2763
|
return Promise.resolve(agentsCache.agents);
|
|
2218
2764
|
};
|
|
2219
2765
|
const server = http2.createServer((req, res) => {
|
|
2220
2766
|
const url = req.url ?? "/";
|
|
2221
2767
|
const path = url.split("?")[0] ?? "/";
|
|
2768
|
+
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
2222
2769
|
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
2223
2770
|
return sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
2224
2771
|
}
|
|
@@ -2305,20 +2852,51 @@ async function startServer(options) {
|
|
|
2305
2852
|
} catch {
|
|
2306
2853
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2307
2854
|
}
|
|
2855
|
+
if (parsed.mode !== void 0 && parsed.mode !== "variant" && parsed.mode !== "replace") {
|
|
2856
|
+
return sendJson(res, 400, {
|
|
2857
|
+
ok: false,
|
|
2858
|
+
error: 'mode must be "variant" or "replace".'
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
const mode = parsed.mode === "replace" ? "replace" : "variant";
|
|
2308
2862
|
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
2309
2863
|
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
2310
2864
|
const localTitles = new Set(local.map((entry) => entry.title));
|
|
2311
2865
|
const bootConfig = config?.previews ?? [];
|
|
2312
2866
|
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
2313
2867
|
const preview = [...boot, ...local].find((entry) => entry.title === parsed.title);
|
|
2314
|
-
if (!preview
|
|
2868
|
+
if (!preview) {
|
|
2315
2869
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2316
2870
|
}
|
|
2317
|
-
const
|
|
2871
|
+
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
2872
|
+
if (!parsed.intent?.trim() && notes.length === 0) {
|
|
2873
|
+
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2874
|
+
}
|
|
2875
|
+
const intent = (parsed.intent ?? "").trim();
|
|
2876
|
+
const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
2877
|
+
const sameNotes = (entry) => {
|
|
2878
|
+
const before = [...entry.notes ?? []].sort().join(",");
|
|
2879
|
+
return before === notes.map((note) => note.id).sort().join(",");
|
|
2880
|
+
};
|
|
2881
|
+
if (live.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
2882
|
+
// one forks the direction and the other rewrites it. Only a
|
|
2883
|
+
// genuine repeat is refused.
|
|
2884
|
+
(entry.mode ?? "replace") === mode && sameNotes(entry))) {
|
|
2885
|
+
return sendJson(res, 409, {
|
|
2886
|
+
ok: false,
|
|
2887
|
+
duplicate: true,
|
|
2888
|
+
error: `That exact change to ${preview.title} is already waiting.`
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
const composed = composeRequest(preview, intent, mode, notes, leglasCommand);
|
|
2318
2892
|
void appendRequest(cwd, {
|
|
2319
2893
|
title: preview.title,
|
|
2320
2894
|
url: preview.url,
|
|
2321
|
-
intent
|
|
2895
|
+
intent,
|
|
2896
|
+
// The ids travel with the request so a change made in place can
|
|
2897
|
+
// forget the notes it answered. A fork leaves them where they are:
|
|
2898
|
+
// the direction they point at was not touched.
|
|
2899
|
+
...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
|
|
2322
2900
|
...composed
|
|
2323
2901
|
}).then(() => {
|
|
2324
2902
|
runner?.nudge();
|
|
@@ -2327,10 +2905,14 @@ async function startServer(options) {
|
|
|
2327
2905
|
});
|
|
2328
2906
|
}
|
|
2329
2907
|
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
2330
|
-
return void Promise.all([
|
|
2908
|
+
return void Promise.all([
|
|
2909
|
+
currentAgents(query.get("refresh") === "1"),
|
|
2910
|
+
readAgentChoice(cwd)
|
|
2911
|
+
]).then(([agents, choice]) => sendJson(res, 200, {
|
|
2331
2912
|
agents,
|
|
2332
2913
|
choice: choice.agent,
|
|
2333
|
-
customRun: choice.run
|
|
2914
|
+
customRun: choice.run,
|
|
2915
|
+
effort: choice.effort
|
|
2334
2916
|
}));
|
|
2335
2917
|
}
|
|
2336
2918
|
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
@@ -2358,7 +2940,17 @@ async function startServer(options) {
|
|
|
2358
2940
|
if (parsed.run !== void 0 && typeof parsed.run !== "string") {
|
|
2359
2941
|
return sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
2360
2942
|
}
|
|
2943
|
+
const effort = parsed.effort === null || isAgentEffort(parsed.effort) ? parsed.effort : void 0;
|
|
2944
|
+
if (parsed.effort !== void 0 && effort === void 0) {
|
|
2945
|
+
return sendJson(res, 400, { ok: false, error: "Effort must be a supported level or null." });
|
|
2946
|
+
}
|
|
2361
2947
|
if (parsed.agent === "custom") {
|
|
2948
|
+
if (effort !== void 0) {
|
|
2949
|
+
return sendJson(res, 400, {
|
|
2950
|
+
ok: false,
|
|
2951
|
+
error: "Custom agents manage effort in their own command."
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2362
2954
|
if (typeof parsed.run !== "string") {
|
|
2363
2955
|
return sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
2364
2956
|
}
|
|
@@ -2367,7 +2959,16 @@ async function startServer(options) {
|
|
|
2367
2959
|
return sendJson(res, 400, { ok: false, error: template.error });
|
|
2368
2960
|
return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
2369
2961
|
}
|
|
2370
|
-
|
|
2962
|
+
if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.includes(effort)) {
|
|
2963
|
+
return sendJson(res, 400, {
|
|
2964
|
+
ok: false,
|
|
2965
|
+
error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
return void saveAgentChoice(cwd, {
|
|
2969
|
+
agent: parsed.agent,
|
|
2970
|
+
...effort === void 0 ? {} : { effort }
|
|
2971
|
+
}).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
2371
2972
|
});
|
|
2372
2973
|
}
|
|
2373
2974
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -2394,21 +2995,33 @@ async function startServer(options) {
|
|
|
2394
2995
|
agent: null,
|
|
2395
2996
|
activity: null,
|
|
2396
2997
|
startedAt: null,
|
|
2998
|
+
stopping: false,
|
|
2999
|
+
waiting: null,
|
|
2397
3000
|
failedIds: []
|
|
2398
3001
|
};
|
|
2399
3002
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
2400
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
3003
|
+
requests: requests.map(({ id, title, intent, status, failure }) => ({
|
|
2401
3004
|
id,
|
|
2402
3005
|
title,
|
|
2403
3006
|
intent,
|
|
2404
|
-
|
|
3007
|
+
// The run in flight is the one thing the file cannot know. After
|
|
3008
|
+
// that the file is the record, including across a restart, and the
|
|
3009
|
+
// process-local failed set only covers a request whose verdict
|
|
3010
|
+
// could not be written.
|
|
3011
|
+
status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
|
|
3012
|
+
failure: failure ?? null
|
|
2405
3013
|
})),
|
|
2406
3014
|
agent: {
|
|
2407
3015
|
attached: externallyAttached(),
|
|
2408
3016
|
running: snapshot.running,
|
|
2409
3017
|
name: snapshot.running ? snapshot.agent : null,
|
|
2410
3018
|
activity: snapshot.running ? snapshot.activity : null,
|
|
2411
|
-
startedAt: snapshot.running ? snapshot.startedAt : null
|
|
3019
|
+
startedAt: snapshot.running ? snapshot.startedAt : null,
|
|
3020
|
+
// A stop that has been asked for but not yet obeyed. The card
|
|
3021
|
+
// says so rather than going on describing a live run.
|
|
3022
|
+
stopping: snapshot.running && snapshot.stopping,
|
|
3023
|
+
// Why a run that looks stalled is stalled, while it is stalled.
|
|
3024
|
+
waiting: snapshot.running ? snapshot.waiting : null
|
|
2412
3025
|
}
|
|
2413
3026
|
}));
|
|
2414
3027
|
}
|
|
@@ -2451,8 +3064,8 @@ async function startServer(options) {
|
|
|
2451
3064
|
if (request === void 0) {
|
|
2452
3065
|
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2453
3066
|
}
|
|
2454
|
-
if (!(runner?.snapshot().failedIds
|
|
2455
|
-
return sendJson(res, 400, { ok: false, error: "Only
|
|
3067
|
+
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
3068
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
2456
3069
|
}
|
|
2457
3070
|
try {
|
|
2458
3071
|
if (!await removeRequest(cwd, request.id)) {
|
|
@@ -2463,7 +3076,11 @@ async function startServer(options) {
|
|
|
2463
3076
|
url: request.url,
|
|
2464
3077
|
intent: request.intent,
|
|
2465
3078
|
target: request.target,
|
|
2466
|
-
prompt: request.prompt
|
|
3079
|
+
prompt: request.prompt,
|
|
3080
|
+
// The stored prompt already carries the mode's instructions; the
|
|
3081
|
+
// field travels with it so the queue keeps saying which kind of
|
|
3082
|
+
// change this is.
|
|
3083
|
+
...request.mode === void 0 ? {} : { mode: request.mode }
|
|
2467
3084
|
});
|
|
2468
3085
|
runner?.nudge();
|
|
2469
3086
|
return sendJson(res, 200, { ok: true });
|
|
@@ -2472,6 +3089,65 @@ async function startServer(options) {
|
|
|
2472
3089
|
}
|
|
2473
3090
|
});
|
|
2474
3091
|
}
|
|
3092
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
3093
|
+
return void readAnnotations(cwd).then((annotations) => sendJson(res, 200, { annotations }));
|
|
3094
|
+
}
|
|
3095
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
3096
|
+
if (!hasJsonBody(req)) {
|
|
3097
|
+
return sendJson(res, 400, { ok: false, error: "A note must be JSON." });
|
|
3098
|
+
}
|
|
3099
|
+
let body = "";
|
|
3100
|
+
req.on("data", (chunk) => body += chunk);
|
|
3101
|
+
return void req.on("end", async () => {
|
|
3102
|
+
let parsed;
|
|
3103
|
+
try {
|
|
3104
|
+
parsed = JSON.parse(body || "{}");
|
|
3105
|
+
} catch {
|
|
3106
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
3107
|
+
}
|
|
3108
|
+
if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
|
|
3109
|
+
return sendJson(res, 400, { ok: false, error: "A note needs a direction." });
|
|
3110
|
+
}
|
|
3111
|
+
const anchor = anchorFrom(parsed.anchor);
|
|
3112
|
+
if (anchor === null) {
|
|
3113
|
+
return sendJson(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
3114
|
+
}
|
|
3115
|
+
try {
|
|
3116
|
+
const annotation = await addAnnotation(cwd, {
|
|
3117
|
+
anchor,
|
|
3118
|
+
note: typeof parsed.note === "string" ? parsed.note.trim() : "",
|
|
3119
|
+
title: parsed.title
|
|
3120
|
+
});
|
|
3121
|
+
return sendJson(res, 200, { ok: true, annotation });
|
|
3122
|
+
} catch {
|
|
3123
|
+
return sendJson(res, 500, { ok: false, error: "The note could not be kept." });
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
3128
|
+
if (!hasJsonBody(req)) {
|
|
3129
|
+
return sendJson(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
3130
|
+
}
|
|
3131
|
+
let body = "";
|
|
3132
|
+
req.on("data", (chunk) => body += chunk);
|
|
3133
|
+
return void req.on("end", async () => {
|
|
3134
|
+
let parsed;
|
|
3135
|
+
try {
|
|
3136
|
+
parsed = JSON.parse(body || "{}");
|
|
3137
|
+
} catch {
|
|
3138
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
3139
|
+
}
|
|
3140
|
+
const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
|
|
3141
|
+
if (ids.length === 0) {
|
|
3142
|
+
return sendJson(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
3143
|
+
}
|
|
3144
|
+
try {
|
|
3145
|
+
return sendJson(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
3146
|
+
} catch {
|
|
3147
|
+
return sendJson(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
2475
3151
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2476
3152
|
if (!hasJsonBody(req)) {
|
|
2477
3153
|
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
@@ -2488,8 +3164,9 @@ async function startServer(options) {
|
|
|
2488
3164
|
if (typeof parsed.id !== "string") {
|
|
2489
3165
|
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2490
3166
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
3167
|
+
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
3168
|
+
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
3169
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
2493
3170
|
}
|
|
2494
3171
|
try {
|
|
2495
3172
|
if (!await removeRequest(cwd, parsed.id)) {
|
|
@@ -2564,7 +3241,7 @@ async function startServer(options) {
|
|
|
2564
3241
|
proxy.upgrade(req, socket, head);
|
|
2565
3242
|
});
|
|
2566
3243
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2567
|
-
runner = startRunner({ cwd, externallyAttached });
|
|
3244
|
+
runner = startRunner({ cwd, externallyAttached, leglasCommand });
|
|
2568
3245
|
let closePromise = null;
|
|
2569
3246
|
return {
|
|
2570
3247
|
port,
|
|
@@ -2639,11 +3316,11 @@ function planKeep(options) {
|
|
|
2639
3316
|
}
|
|
2640
3317
|
|
|
2641
3318
|
// src/run-init.ts
|
|
2642
|
-
import { readFile as
|
|
2643
|
-
import { join as
|
|
3319
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
3320
|
+
import { join as join10 } from "path";
|
|
2644
3321
|
async function readIfPresent(path) {
|
|
2645
3322
|
try {
|
|
2646
|
-
return await
|
|
3323
|
+
return await readFile8(path, "utf8");
|
|
2647
3324
|
} catch {
|
|
2648
3325
|
return null;
|
|
2649
3326
|
}
|
|
@@ -2651,18 +3328,18 @@ async function readIfPresent(path) {
|
|
|
2651
3328
|
async function runInit(options, deps) {
|
|
2652
3329
|
const existingConfig = findConfigFile(options.cwd);
|
|
2653
3330
|
const plan = planInit({
|
|
2654
|
-
agents: await readIfPresent(
|
|
3331
|
+
agents: await readIfPresent(join10(options.cwd, "AGENTS.md")),
|
|
2655
3332
|
config: existingConfig === null ? null : "present",
|
|
2656
|
-
gitignore: await readIfPresent(
|
|
3333
|
+
gitignore: await readIfPresent(join10(options.cwd, ".gitignore")),
|
|
2657
3334
|
force: options.force
|
|
2658
3335
|
});
|
|
2659
3336
|
const touched = [];
|
|
2660
|
-
for (const
|
|
2661
|
-
await
|
|
2662
|
-
touched.push(
|
|
3337
|
+
for (const write2 of plan.writes) {
|
|
3338
|
+
await writeFile6(join10(options.cwd, write2.path), write2.contents, "utf8");
|
|
3339
|
+
touched.push(write2.path);
|
|
2663
3340
|
}
|
|
2664
3341
|
if (plan.gitignore !== null) {
|
|
2665
|
-
await
|
|
3342
|
+
await writeFile6(join10(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2666
3343
|
touched.push(".gitignore");
|
|
2667
3344
|
}
|
|
2668
3345
|
if (options.json) {
|
|
@@ -2682,8 +3359,8 @@ async function runInit(options, deps) {
|
|
|
2682
3359
|
|
|
2683
3360
|
// src/run-keep.ts
|
|
2684
3361
|
import { existsSync as existsSync3 } from "fs";
|
|
2685
|
-
import { mkdir as
|
|
2686
|
-
import { dirname as
|
|
3362
|
+
import { mkdir as mkdir6, readFile as readFile9, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
3363
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
2687
3364
|
|
|
2688
3365
|
// src/resolve-title.ts
|
|
2689
3366
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -2727,18 +3404,18 @@ async function runKeep(options, deps) {
|
|
|
2727
3404
|
if (!resolved.ok) return fail(resolved.error);
|
|
2728
3405
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
2729
3406
|
if (!plan.ok) return fail(plan.error);
|
|
2730
|
-
const from =
|
|
2731
|
-
const to =
|
|
3407
|
+
const from = join11(options.cwd, plan.move.from);
|
|
3408
|
+
const to = join11(options.cwd, plan.move.to);
|
|
2732
3409
|
if (!existsSync3(from)) {
|
|
2733
3410
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
2734
3411
|
}
|
|
2735
3412
|
if (existsSync3(to)) {
|
|
2736
3413
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
2737
3414
|
}
|
|
2738
|
-
const source = await
|
|
2739
|
-
await
|
|
2740
|
-
await
|
|
2741
|
-
await rm2(
|
|
3415
|
+
const source = await readFile9(from, "utf8");
|
|
3416
|
+
await mkdir6(dirname7(to), { recursive: true });
|
|
3417
|
+
await writeFile7(to, renameExport(source, plan.exportName), "utf8");
|
|
3418
|
+
await rm2(join11(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
2742
3419
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
2743
3420
|
if (options.json) {
|
|
2744
3421
|
deps.log(
|
|
@@ -2773,11 +3450,11 @@ async function runKeep(options, deps) {
|
|
|
2773
3450
|
|
|
2774
3451
|
// src/run-new.ts
|
|
2775
3452
|
import { existsSync as existsSync4 } from "fs";
|
|
2776
|
-
import { mkdir as
|
|
2777
|
-
import { dirname as
|
|
3453
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
3454
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
2778
3455
|
async function readIfPresent2(path) {
|
|
2779
3456
|
try {
|
|
2780
|
-
return await
|
|
3457
|
+
return await readFile10(path, "utf8");
|
|
2781
3458
|
} catch {
|
|
2782
3459
|
return null;
|
|
2783
3460
|
}
|
|
@@ -2785,19 +3462,19 @@ async function readIfPresent2(path) {
|
|
|
2785
3462
|
async function runNew(options, deps) {
|
|
2786
3463
|
let from;
|
|
2787
3464
|
if (options.from !== void 0) {
|
|
2788
|
-
const contents = await readIfPresent2(
|
|
3465
|
+
const contents = await readIfPresent2(join12(options.cwd, options.from));
|
|
2789
3466
|
if (contents === null) {
|
|
2790
|
-
const
|
|
2791
|
-
if (options.json) deps.log(JSON.stringify({ ok: false, error:
|
|
2792
|
-
else deps.log(
|
|
3467
|
+
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
3468
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
3469
|
+
else deps.log(message2);
|
|
2793
3470
|
return { exitCode: 1, written: [] };
|
|
2794
3471
|
}
|
|
2795
3472
|
from = { path: options.from, contents };
|
|
2796
3473
|
}
|
|
2797
3474
|
const plan = planNew({
|
|
2798
3475
|
surface: options.surface,
|
|
2799
|
-
packageJson: await readIfPresent2(
|
|
2800
|
-
gitignore: await readIfPresent2(
|
|
3476
|
+
packageJson: await readIfPresent2(join12(options.cwd, "package.json")),
|
|
3477
|
+
gitignore: await readIfPresent2(join12(options.cwd, ".gitignore")),
|
|
2801
3478
|
from
|
|
2802
3479
|
});
|
|
2803
3480
|
const fail = (error) => {
|
|
@@ -2813,26 +3490,26 @@ async function runNew(options, deps) {
|
|
|
2813
3490
|
deps.log(JSON.stringify({ ok: true, files: plan.writes, instructions: plan.instructions, previews: plan.previews }));
|
|
2814
3491
|
return { exitCode: 0, written: [] };
|
|
2815
3492
|
}
|
|
2816
|
-
for (const
|
|
2817
|
-
deps.log(`--- ${
|
|
2818
|
-
deps.log(
|
|
3493
|
+
for (const write2 of plan.writes) {
|
|
3494
|
+
deps.log(`--- ${write2.path}`);
|
|
3495
|
+
deps.log(write2.contents);
|
|
2819
3496
|
}
|
|
2820
3497
|
deps.log(plan.instructions);
|
|
2821
3498
|
return { exitCode: 0, written: [] };
|
|
2822
3499
|
}
|
|
2823
|
-
const existing = plan.writes.filter((
|
|
3500
|
+
const existing = plan.writes.filter((write2) => existsSync4(join12(options.cwd, write2.path)));
|
|
2824
3501
|
if (existing.length > 0) {
|
|
2825
3502
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
2826
3503
|
}
|
|
2827
3504
|
const written = [];
|
|
2828
|
-
for (const
|
|
2829
|
-
const target =
|
|
2830
|
-
await
|
|
2831
|
-
await
|
|
2832
|
-
written.push(
|
|
3505
|
+
for (const write2 of plan.writes) {
|
|
3506
|
+
const target = join12(options.cwd, write2.path);
|
|
3507
|
+
await mkdir7(dirname8(target), { recursive: true });
|
|
3508
|
+
await writeFile8(target, write2.contents, "utf8");
|
|
3509
|
+
written.push(write2.path);
|
|
2833
3510
|
}
|
|
2834
3511
|
if (plan.gitignore !== null) {
|
|
2835
|
-
await
|
|
3512
|
+
await writeFile8(join12(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2836
3513
|
written.push(".gitignore");
|
|
2837
3514
|
}
|
|
2838
3515
|
if (options.json) {
|
|
@@ -2853,21 +3530,21 @@ async function runNew(options, deps) {
|
|
|
2853
3530
|
}
|
|
2854
3531
|
|
|
2855
3532
|
// src/run-previews.ts
|
|
2856
|
-
import { readFile as
|
|
2857
|
-
import { join as
|
|
3533
|
+
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
3534
|
+
import { join as join13 } from "path";
|
|
2858
3535
|
function envelope(deps, ok, body) {
|
|
2859
3536
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
2860
3537
|
}
|
|
2861
3538
|
async function ensureIgnored(cwd) {
|
|
2862
|
-
const path =
|
|
3539
|
+
const path = join13(cwd, ".gitignore");
|
|
2863
3540
|
let current = null;
|
|
2864
3541
|
try {
|
|
2865
|
-
current = await
|
|
3542
|
+
current = await readFile11(path, "utf8");
|
|
2866
3543
|
} catch {
|
|
2867
3544
|
current = null;
|
|
2868
3545
|
}
|
|
2869
3546
|
const next = ignoreEntry(current);
|
|
2870
|
-
if (next !== null) await
|
|
3547
|
+
if (next !== null) await writeFile9(path, next, "utf8");
|
|
2871
3548
|
}
|
|
2872
3549
|
async function runAdd(options, deps) {
|
|
2873
3550
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2891,7 +3568,8 @@ async function runAdd(options, deps) {
|
|
|
2891
3568
|
tags: options.preview.tags,
|
|
2892
3569
|
branch: options.preview.branch,
|
|
2893
3570
|
file: options.preview.file,
|
|
2894
|
-
basedOn: options.preview.basedOn
|
|
3571
|
+
basedOn: options.preview.basedOn,
|
|
3572
|
+
askedFor: options.preview.askedFor
|
|
2895
3573
|
},
|
|
2896
3574
|
shared
|
|
2897
3575
|
);
|
|
@@ -2951,6 +3629,7 @@ async function runList(options, deps) {
|
|
|
2951
3629
|
note: preview.note ?? null,
|
|
2952
3630
|
tags: preview.tags,
|
|
2953
3631
|
basedOn: preview.basedOn ?? null,
|
|
3632
|
+
askedFor: preview.askedFor ?? null,
|
|
2954
3633
|
local: preview.local,
|
|
2955
3634
|
branch: preview.branch ?? null,
|
|
2956
3635
|
file: preview.file ?? null
|
|
@@ -3113,23 +3792,23 @@ async function runShow(options, deps) {
|
|
|
3113
3792
|
|
|
3114
3793
|
// src/run-watch.ts
|
|
3115
3794
|
import { spawn as spawn3 } from "child_process";
|
|
3116
|
-
import { mkdir as
|
|
3117
|
-
import { dirname as
|
|
3795
|
+
import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
|
|
3796
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
3118
3797
|
var POLL_MS2 = 2e3;
|
|
3119
3798
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
3120
3799
|
async function saveTemplate(cwd, run3) {
|
|
3121
|
-
const path =
|
|
3800
|
+
const path = join14(cwd, WATCH_PATH);
|
|
3122
3801
|
let config = {};
|
|
3123
3802
|
try {
|
|
3124
|
-
const parsed = JSON.parse(await
|
|
3803
|
+
const parsed = JSON.parse(await readFile12(path, "utf8"));
|
|
3125
3804
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
3126
3805
|
config = parsed;
|
|
3127
3806
|
}
|
|
3128
3807
|
} catch {
|
|
3129
3808
|
}
|
|
3130
3809
|
config.run = run3;
|
|
3131
|
-
await
|
|
3132
|
-
await
|
|
3810
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3811
|
+
await writeFile10(path, `${JSON.stringify(config, null, 2)}
|
|
3133
3812
|
`, "utf8");
|
|
3134
3813
|
}
|
|
3135
3814
|
function spawnAgent(command, args, cwd) {
|
|
@@ -3168,7 +3847,7 @@ async function runWatch(options, deps) {
|
|
|
3168
3847
|
const adapter = KNOWN_AGENTS[saved.agent];
|
|
3169
3848
|
template = {
|
|
3170
3849
|
command: adapter.binary,
|
|
3171
|
-
args: adapter.terminalArgs(PROMPT_TOKEN)
|
|
3850
|
+
args: adapter.terminalArgs(PROMPT_TOKEN, saved.effort)
|
|
3172
3851
|
};
|
|
3173
3852
|
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
3174
3853
|
synthesizedAgent = adapter.name;
|
|
@@ -3215,9 +3894,13 @@ async function runWatch(options, deps) {
|
|
|
3215
3894
|
return;
|
|
3216
3895
|
}
|
|
3217
3896
|
failed.add(request.id);
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3897
|
+
const failure = classifyFailure({
|
|
3898
|
+
agent: (shownCommand2.split(/\s+/)[0] ?? command).split("/").pop() ?? command,
|
|
3899
|
+
error: outcome.ok ? null : outcome.error,
|
|
3900
|
+
exitCode: outcome.ok ? outcome.code : null
|
|
3901
|
+
});
|
|
3902
|
+
await markFailed(options.cwd, request.id, failure);
|
|
3903
|
+
deps.error(` failed ${request.title}: ${failure.message}`);
|
|
3221
3904
|
deps.error(" Left in the queue and not retried.");
|
|
3222
3905
|
};
|
|
3223
3906
|
const tick = async () => {
|
|
@@ -3263,12 +3946,12 @@ async function runWatch(options, deps) {
|
|
|
3263
3946
|
|
|
3264
3947
|
// src/run-classify.ts
|
|
3265
3948
|
import { stat } from "fs/promises";
|
|
3266
|
-
import { join as
|
|
3949
|
+
import { join as join15 } from "path";
|
|
3267
3950
|
async function runClassify(options, deps) {
|
|
3268
3951
|
const declared = await Promise.all(
|
|
3269
3952
|
options.changes.map(async (change) => ({
|
|
3270
3953
|
...change,
|
|
3271
|
-
exists: await stat(
|
|
3954
|
+
exists: await stat(join15(options.cwd, change.path)).then(
|
|
3272
3955
|
() => true,
|
|
3273
3956
|
() => false
|
|
3274
3957
|
)
|
|
@@ -3296,18 +3979,28 @@ async function runClassify(options, deps) {
|
|
|
3296
3979
|
// src/run.ts
|
|
3297
3980
|
import { existsSync as existsSync5 } from "fs";
|
|
3298
3981
|
import { createRequire } from "module";
|
|
3299
|
-
import { basename as basename3, dirname as
|
|
3982
|
+
import { basename as basename3, dirname as dirname10, join as join16, relative as relative4 } from "path";
|
|
3300
3983
|
import { fileURLToPath } from "url";
|
|
3301
3984
|
function findShellDir() {
|
|
3302
|
-
const bundled =
|
|
3303
|
-
if (existsSync5(
|
|
3985
|
+
const bundled = join16(dirname10(fileURLToPath(import.meta.url)), "shell");
|
|
3986
|
+
if (existsSync5(join16(bundled, "index.html"))) return bundled;
|
|
3304
3987
|
try {
|
|
3305
3988
|
const require2 = createRequire(import.meta.url);
|
|
3306
|
-
return
|
|
3989
|
+
return dirname10(require2.resolve("@leglas/shell/dist/index.html"));
|
|
3307
3990
|
} catch {
|
|
3308
3991
|
return null;
|
|
3309
3992
|
}
|
|
3310
3993
|
}
|
|
3994
|
+
function shellWord(value) {
|
|
3995
|
+
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(value)) return value;
|
|
3996
|
+
if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
|
|
3997
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
3998
|
+
}
|
|
3999
|
+
function embeddedLeglasCommand() {
|
|
4000
|
+
const entry = join16(dirname10(fileURLToPath(import.meta.url)), "bin.js");
|
|
4001
|
+
if (!existsSync5(entry)) return "npx -y leglas";
|
|
4002
|
+
return [process.execPath, entry].map(shellWord).join(" ");
|
|
4003
|
+
}
|
|
3311
4004
|
async function run2(options, deps) {
|
|
3312
4005
|
const loaded = await loadConfig(options.cwd);
|
|
3313
4006
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -3337,7 +4030,7 @@ async function run2(options, deps) {
|
|
|
3337
4030
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
3338
4031
|
for (const preview of merged?.previews ?? []) {
|
|
3339
4032
|
if (preview.file !== void 0) {
|
|
3340
|
-
const absolute =
|
|
4033
|
+
const absolute = join16(options.cwd, preview.file);
|
|
3341
4034
|
if (!existsSync5(absolute)) {
|
|
3342
4035
|
worktreeErrors.push(
|
|
3343
4036
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -3348,7 +4041,7 @@ async function run2(options, deps) {
|
|
|
3348
4041
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
3349
4042
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
3350
4043
|
}
|
|
3351
|
-
fileMounts.set(slug,
|
|
4044
|
+
fileMounts.set(slug, dirname10(absolute));
|
|
3352
4045
|
previews.push({
|
|
3353
4046
|
...preview,
|
|
3354
4047
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -3389,6 +4082,7 @@ async function run2(options, deps) {
|
|
|
3389
4082
|
// directory does. Either way saved layout survives a port change.
|
|
3390
4083
|
project: loaded.path ?? options.cwd,
|
|
3391
4084
|
cwd: options.cwd,
|
|
4085
|
+
leglasCommand: embeddedLeglasCommand(),
|
|
3392
4086
|
...options.port === void 0 ? {} : { port: options.port }
|
|
3393
4087
|
});
|
|
3394
4088
|
const url = `${server.url}${LEGLAS_PREFIX}`;
|