leglas 0.3.0 → 0.4.1
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 +60 -8
- package/dist/bin.js +1063 -211
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1063 -207
- package/dist/shell/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/shell/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/shell/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/shell/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/shell/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/shell/assets/index-B1hUXNY3.js +14 -0
- package/dist/shell/assets/index-DCg7pmCj.css +1 -0
- package/dist/shell/index.html +2 -2
- package/dist/watch.d.ts +2 -45
- package/package.json +1 -1
- package/dist/shell/assets/index-Bl87Vq9G.js +0 -10
- package/dist/shell/assets/index-dMGYwlE1.css +0 -1
- package/dist/shell/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 +0 -0
- package/dist/shell/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 +0 -0
- package/dist/shell/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
- package/dist/shell/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
- package/dist/shell/assets/manrope-vietnamese-wght-normal-usUDDRr7.woff2 +0 -0
package/dist/index.js
CHANGED
|
@@ -845,6 +845,360 @@ function normalizeConfig(raw, options = {}) {
|
|
|
845
845
|
};
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
+
// ../server/dist/agent-command.js
|
|
849
|
+
var WATCH_PATH = ".leglas/watch.json";
|
|
850
|
+
var PROMPT_TOKEN = "{prompt}";
|
|
851
|
+
var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
852
|
+
function tokenize(template) {
|
|
853
|
+
const tokens = [];
|
|
854
|
+
let current = "";
|
|
855
|
+
let started = false;
|
|
856
|
+
let quote = null;
|
|
857
|
+
for (const character of template) {
|
|
858
|
+
if (quote !== null) {
|
|
859
|
+
if (character === quote)
|
|
860
|
+
quote = null;
|
|
861
|
+
else
|
|
862
|
+
current += character;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (character === '"' || character === "'") {
|
|
866
|
+
quote = character;
|
|
867
|
+
started = true;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (/\s/.test(character)) {
|
|
871
|
+
if (started)
|
|
872
|
+
tokens.push(current);
|
|
873
|
+
current = "";
|
|
874
|
+
started = false;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
current += character;
|
|
878
|
+
started = true;
|
|
879
|
+
}
|
|
880
|
+
if (quote !== null) {
|
|
881
|
+
return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
|
|
882
|
+
}
|
|
883
|
+
if (started)
|
|
884
|
+
tokens.push(current);
|
|
885
|
+
return { ok: true, tokens };
|
|
886
|
+
}
|
|
887
|
+
function parseTemplate(raw) {
|
|
888
|
+
const tokenized = tokenize(raw);
|
|
889
|
+
if (!tokenized.ok)
|
|
890
|
+
return tokenized;
|
|
891
|
+
const { tokens } = tokenized;
|
|
892
|
+
const [command, ...args] = tokens;
|
|
893
|
+
if (command === void 0) {
|
|
894
|
+
return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
|
|
895
|
+
}
|
|
896
|
+
if (tokens.some((token) => token !== PROMPT_TOKEN && token.includes(PROMPT_TOKEN))) {
|
|
897
|
+
return {
|
|
898
|
+
ok: false,
|
|
899
|
+
error: `${PROMPT_TOKEN} must stand as a word of its own, for example: ${EXAMPLE}`
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
|
|
903
|
+
if (placeholders > 1) {
|
|
904
|
+
return {
|
|
905
|
+
ok: false,
|
|
906
|
+
error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (command === PROMPT_TOKEN) {
|
|
910
|
+
return {
|
|
911
|
+
ok: false,
|
|
912
|
+
error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
return { ok: true, template: { command, args } };
|
|
916
|
+
}
|
|
917
|
+
function commandFor(template, prompt) {
|
|
918
|
+
if (!template.args.includes(PROMPT_TOKEN)) {
|
|
919
|
+
return { command: template.command, args: [...template.args, prompt] };
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
command: template.command,
|
|
923
|
+
args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function nextRequest(requests, failed) {
|
|
927
|
+
return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// ../server/dist/agents.js
|
|
931
|
+
import { spawn } from "child_process";
|
|
932
|
+
import { constants } from "fs";
|
|
933
|
+
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
934
|
+
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
935
|
+
var KNOWN_AGENTS = {
|
|
936
|
+
claude: {
|
|
937
|
+
name: "Claude",
|
|
938
|
+
binary: "claude",
|
|
939
|
+
args: (prompt) => [
|
|
940
|
+
"-p",
|
|
941
|
+
prompt,
|
|
942
|
+
"--output-format",
|
|
943
|
+
"stream-json",
|
|
944
|
+
"--verbose",
|
|
945
|
+
"--permission-mode",
|
|
946
|
+
"acceptEdits"
|
|
947
|
+
],
|
|
948
|
+
terminalArgs: (prompt) => [
|
|
949
|
+
"-p",
|
|
950
|
+
prompt,
|
|
951
|
+
"--permission-mode",
|
|
952
|
+
"acceptEdits"
|
|
953
|
+
],
|
|
954
|
+
resumeArgs: (sessionId, prompt) => [
|
|
955
|
+
"-p",
|
|
956
|
+
"--resume",
|
|
957
|
+
sessionId,
|
|
958
|
+
prompt,
|
|
959
|
+
"--output-format",
|
|
960
|
+
"stream-json",
|
|
961
|
+
"--verbose",
|
|
962
|
+
"--permission-mode",
|
|
963
|
+
"acceptEdits"
|
|
964
|
+
],
|
|
965
|
+
// Every stream-json event names its session.
|
|
966
|
+
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
967
|
+
authArgs: ["auth", "status"],
|
|
968
|
+
// `claude auth status` prints JSON with a loggedIn boolean. Only that
|
|
969
|
+
// field decides; any other shape stays unknown.
|
|
970
|
+
authVerdict: (result) => {
|
|
971
|
+
try {
|
|
972
|
+
const parsed = record(JSON.parse(result.stdout));
|
|
973
|
+
if (parsed?.loggedIn === true)
|
|
974
|
+
return "ok";
|
|
975
|
+
if (parsed?.loggedIn === false)
|
|
976
|
+
return "signed-out";
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
return "unknown";
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
codex: {
|
|
983
|
+
name: "Codex",
|
|
984
|
+
binary: "codex",
|
|
985
|
+
args: (prompt) => ["exec", "--json", "-s", "workspace-write", prompt],
|
|
986
|
+
terminalArgs: (prompt) => ["exec", "-s", "workspace-write", prompt],
|
|
987
|
+
// 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
|
+
resumeArgs: (sessionId, prompt) => [
|
|
990
|
+
"exec",
|
|
991
|
+
"resume",
|
|
992
|
+
sessionId,
|
|
993
|
+
"--json",
|
|
994
|
+
prompt
|
|
995
|
+
],
|
|
996
|
+
sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
|
|
997
|
+
authArgs: ["login", "status"],
|
|
998
|
+
// `codex login status` exits 0 when logged in and nonzero when not.
|
|
999
|
+
authVerdict: (result) => result.code === 0 ? "ok" : "signed-out"
|
|
1000
|
+
},
|
|
1001
|
+
cursor: {
|
|
1002
|
+
name: "Cursor",
|
|
1003
|
+
binary: "cursor-agent",
|
|
1004
|
+
args: (prompt) => ["-p", prompt, "--output-format", "stream-json"],
|
|
1005
|
+
terminalArgs: (prompt) => ["-p", prompt],
|
|
1006
|
+
authArgs: ["status"],
|
|
1007
|
+
// UNVERIFIED: cursor-agent was not available on the build machine. The
|
|
1008
|
+
// reading is deliberately loose, and anything ambiguous stays unknown.
|
|
1009
|
+
authVerdict: (result) => {
|
|
1010
|
+
if (/logged in|signed in/i.test(result.stdout))
|
|
1011
|
+
return "ok";
|
|
1012
|
+
if (result.code !== 0 || /not logged in|log in|sign in/i.test(result.stdout))
|
|
1013
|
+
return "signed-out";
|
|
1014
|
+
return "unknown";
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
1019
|
+
function execProbe(binary, args) {
|
|
1020
|
+
return new Promise((resolve) => {
|
|
1021
|
+
let child;
|
|
1022
|
+
try {
|
|
1023
|
+
child = spawn(binary, [...args], { shell: false, stdio: ["ignore", "pipe", "ignore"] });
|
|
1024
|
+
} catch {
|
|
1025
|
+
return resolve(null);
|
|
1026
|
+
}
|
|
1027
|
+
let stdout = "";
|
|
1028
|
+
child.stdout?.on("data", (chunk) => {
|
|
1029
|
+
if (stdout.length < 4096)
|
|
1030
|
+
stdout += chunk.toString();
|
|
1031
|
+
});
|
|
1032
|
+
const deadline = setTimeout(() => child.kill("SIGKILL"), PROBE_TIMEOUT_MS);
|
|
1033
|
+
child.once("error", () => {
|
|
1034
|
+
clearTimeout(deadline);
|
|
1035
|
+
resolve(null);
|
|
1036
|
+
});
|
|
1037
|
+
child.once("close", (code, signal) => {
|
|
1038
|
+
clearTimeout(deadline);
|
|
1039
|
+
resolve(signal !== null ? null : { code: code ?? 0, stdout });
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
async function pathLookup(binary) {
|
|
1044
|
+
const entries = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
|
|
1045
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
1046
|
+
for (const entry of entries) {
|
|
1047
|
+
for (const extension of extensions) {
|
|
1048
|
+
try {
|
|
1049
|
+
await access(join(entry, `${binary}${extension}`), constants.X_OK);
|
|
1050
|
+
return true;
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
|
|
1058
|
+
const entries = Object.entries(KNOWN_AGENTS);
|
|
1059
|
+
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
1060
|
+
const available = await lookup(adapter.binary).catch(() => false);
|
|
1061
|
+
if (!available)
|
|
1062
|
+
return { id, name: adapter.name, available, auth: "unknown" };
|
|
1063
|
+
const result = await probe2(adapter.binary, adapter.authArgs).catch(() => null);
|
|
1064
|
+
return {
|
|
1065
|
+
id,
|
|
1066
|
+
name: adapter.name,
|
|
1067
|
+
available,
|
|
1068
|
+
auth: result === null ? "unknown" : adapter.authVerdict(result)
|
|
1069
|
+
};
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
function record(value) {
|
|
1073
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1074
|
+
}
|
|
1075
|
+
function shownPath(value, cwd) {
|
|
1076
|
+
if (typeof value !== "string" || value === "")
|
|
1077
|
+
return null;
|
|
1078
|
+
if (!isAbsolute(value))
|
|
1079
|
+
return value;
|
|
1080
|
+
return relative(cwd, value) || ".";
|
|
1081
|
+
}
|
|
1082
|
+
function shownCommand(value) {
|
|
1083
|
+
let command = Array.isArray(value) ? value.filter((part) => typeof part === "string").join(" ") : typeof value === "string" ? value : "";
|
|
1084
|
+
command = command.trim();
|
|
1085
|
+
const wrapped = /^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+([\s\S]*)$/.exec(command);
|
|
1086
|
+
if (wrapped?.[1] !== void 0) {
|
|
1087
|
+
command = wrapped[1].trim();
|
|
1088
|
+
const quote = command[0];
|
|
1089
|
+
if ((quote === "'" || quote === '"') && command.endsWith(quote) && command.length > 1) {
|
|
1090
|
+
command = command.slice(1, -1);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
command = (command.split("\n")[0] ?? "").replace(/\s+/g, " ").trim();
|
|
1094
|
+
if (command === "")
|
|
1095
|
+
return null;
|
|
1096
|
+
return command.length > 48 ? `${command.slice(0, 47)}\u2026` : command;
|
|
1097
|
+
}
|
|
1098
|
+
function claudeActivity(event, cwd) {
|
|
1099
|
+
if (event.type !== "assistant")
|
|
1100
|
+
return null;
|
|
1101
|
+
const message = record(event.message);
|
|
1102
|
+
if (message === null || !Array.isArray(message.content))
|
|
1103
|
+
return null;
|
|
1104
|
+
for (const rawBlock of message.content) {
|
|
1105
|
+
const block = record(rawBlock);
|
|
1106
|
+
if (block?.type !== "tool_use" || typeof block.name !== "string")
|
|
1107
|
+
continue;
|
|
1108
|
+
const input = record(block.input);
|
|
1109
|
+
if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name)) {
|
|
1110
|
+
const path = shownPath(input?.file_path ?? input?.notebook_path, cwd);
|
|
1111
|
+
return path === null ? `using ${block.name}` : `editing ${path}`;
|
|
1112
|
+
}
|
|
1113
|
+
if (block.name === "Read") {
|
|
1114
|
+
const path = shownPath(input?.file_path ?? input?.path, cwd);
|
|
1115
|
+
return path === null ? "using Read" : `reading ${path}`;
|
|
1116
|
+
}
|
|
1117
|
+
if (block.name === "Bash") {
|
|
1118
|
+
const command = shownCommand(input?.command);
|
|
1119
|
+
return command === null ? "running a command" : `running ${command}`;
|
|
1120
|
+
}
|
|
1121
|
+
if (block.name === "Grep" || block.name === "Glob")
|
|
1122
|
+
return "searching the project";
|
|
1123
|
+
return `using ${block.name}`;
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
function codexActivity(event, cwd) {
|
|
1128
|
+
if (event.type !== "item.started" && event.type !== "item.completed")
|
|
1129
|
+
return null;
|
|
1130
|
+
const item = record(event.item);
|
|
1131
|
+
if (item === null)
|
|
1132
|
+
return null;
|
|
1133
|
+
if (item.type === "command_execution") {
|
|
1134
|
+
const command = shownCommand(item.command);
|
|
1135
|
+
return command === null ? "running a command" : `running ${command}`;
|
|
1136
|
+
}
|
|
1137
|
+
if (item.type !== "file_change")
|
|
1138
|
+
return null;
|
|
1139
|
+
const first = Array.isArray(item.changes) ? record(item.changes[0]) : null;
|
|
1140
|
+
const path = shownPath(first?.path ?? item.path, cwd);
|
|
1141
|
+
return path === null ? null : `editing ${path}`;
|
|
1142
|
+
}
|
|
1143
|
+
function activityFrom(agent, line, cwd = process.cwd()) {
|
|
1144
|
+
let event;
|
|
1145
|
+
try {
|
|
1146
|
+
event = record(JSON.parse(line));
|
|
1147
|
+
} catch {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
if (event === null)
|
|
1151
|
+
return null;
|
|
1152
|
+
if (agent === "claude")
|
|
1153
|
+
return claudeActivity(event, cwd);
|
|
1154
|
+
if (agent === "codex")
|
|
1155
|
+
return codexActivity(event, cwd);
|
|
1156
|
+
if (agent === "cursor")
|
|
1157
|
+
return claudeActivity(event, cwd);
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
function sessionFrom(agent, line) {
|
|
1161
|
+
if (agent !== "claude" && agent !== "codex")
|
|
1162
|
+
return null;
|
|
1163
|
+
let event;
|
|
1164
|
+
try {
|
|
1165
|
+
event = record(JSON.parse(line));
|
|
1166
|
+
} catch {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
if (event === null)
|
|
1170
|
+
return null;
|
|
1171
|
+
return KNOWN_AGENTS[agent].sessionFrom(event);
|
|
1172
|
+
}
|
|
1173
|
+
function isAgentChoice(value) {
|
|
1174
|
+
return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
1175
|
+
}
|
|
1176
|
+
async function readWatchConfig(cwd) {
|
|
1177
|
+
try {
|
|
1178
|
+
const parsed = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
|
|
1179
|
+
return record(parsed) ?? {};
|
|
1180
|
+
} catch {
|
|
1181
|
+
return {};
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function readAgentChoice(cwd) {
|
|
1185
|
+
const config = await readWatchConfig(cwd);
|
|
1186
|
+
return {
|
|
1187
|
+
agent: isAgentChoice(config.agent) ? config.agent : null,
|
|
1188
|
+
run: typeof config.run === "string" && config.run !== "" ? config.run : null
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
async function saveAgentChoice(cwd, choice) {
|
|
1192
|
+
const config = await readWatchConfig(cwd);
|
|
1193
|
+
config.agent = choice.agent;
|
|
1194
|
+
if (choice.run !== void 0)
|
|
1195
|
+
config.run = choice.run;
|
|
1196
|
+
const path = join(cwd, WATCH_PATH);
|
|
1197
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1198
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
1199
|
+
`, "utf8");
|
|
1200
|
+
}
|
|
1201
|
+
|
|
848
1202
|
// ../server/dist/classify.js
|
|
849
1203
|
var MANIFESTS = /* @__PURE__ */ new Set([
|
|
850
1204
|
"package.json",
|
|
@@ -912,7 +1266,7 @@ function classifyDirection(input) {
|
|
|
912
1266
|
|
|
913
1267
|
// ../server/dist/find-config.js
|
|
914
1268
|
import { existsSync } from "fs";
|
|
915
|
-
import { dirname, join, parse } from "path";
|
|
1269
|
+
import { dirname as dirname2, join as join2, parse } from "path";
|
|
916
1270
|
var CONFIG_BASENAMES = [
|
|
917
1271
|
"leglas.config.ts",
|
|
918
1272
|
"leglas.config.mjs",
|
|
@@ -924,13 +1278,13 @@ function findConfigFile(startDir) {
|
|
|
924
1278
|
let dir = startDir;
|
|
925
1279
|
for (; ; ) {
|
|
926
1280
|
for (const basename4 of CONFIG_BASENAMES) {
|
|
927
|
-
const candidate =
|
|
1281
|
+
const candidate = join2(dir, basename4);
|
|
928
1282
|
if (existsSync(candidate))
|
|
929
1283
|
return candidate;
|
|
930
1284
|
}
|
|
931
1285
|
if (dir === root)
|
|
932
1286
|
return null;
|
|
933
|
-
const parent =
|
|
1287
|
+
const parent = dirname2(dir);
|
|
934
1288
|
if (parent === dir)
|
|
935
1289
|
return null;
|
|
936
1290
|
dir = parent;
|
|
@@ -938,19 +1292,19 @@ function findConfigFile(startDir) {
|
|
|
938
1292
|
}
|
|
939
1293
|
|
|
940
1294
|
// ../server/dist/load-config.js
|
|
941
|
-
import { readFile } from "fs/promises";
|
|
942
|
-
import { relative } from "path";
|
|
1295
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1296
|
+
import { relative as relative2 } from "path";
|
|
943
1297
|
import { pathToFileURL } from "url";
|
|
944
1298
|
async function loadConfig(cwd) {
|
|
945
1299
|
const path = findConfigFile(cwd);
|
|
946
1300
|
if (path === null) {
|
|
947
1301
|
return { ...normalizeConfig(void 0), path: null };
|
|
948
1302
|
}
|
|
949
|
-
const label =
|
|
1303
|
+
const label = relative2(cwd, path) || path;
|
|
950
1304
|
let exported;
|
|
951
1305
|
try {
|
|
952
1306
|
if (path.endsWith(".json")) {
|
|
953
|
-
exported = JSON.parse(await
|
|
1307
|
+
exported = JSON.parse(await readFile2(path, "utf8"));
|
|
954
1308
|
} else {
|
|
955
1309
|
const module = await import(pathToFileURL(path).href);
|
|
956
1310
|
if (!("default" in module)) {
|
|
@@ -971,16 +1325,25 @@ async function loadConfig(cwd) {
|
|
|
971
1325
|
}
|
|
972
1326
|
|
|
973
1327
|
// ../server/dist/local-previews.js
|
|
974
|
-
import { mkdir, readFile as
|
|
975
|
-
import { dirname as
|
|
1328
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1329
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
976
1330
|
var LOCAL_PREVIEWS_PATH = ".leglas/previews.json";
|
|
977
1331
|
async function readLocalPreviews(cwd) {
|
|
978
|
-
const path =
|
|
1332
|
+
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
979
1333
|
let raw;
|
|
980
1334
|
try {
|
|
981
|
-
raw = await
|
|
982
|
-
} catch {
|
|
983
|
-
|
|
1335
|
+
raw = await readFile3(path, "utf8");
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
1338
|
+
if (code === "ENOENT") {
|
|
1339
|
+
return { previews: [], errors: [] };
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
previews: [],
|
|
1343
|
+
errors: [
|
|
1344
|
+
`${LOCAL_PREVIEWS_PATH} could not be read (${code ?? "unknown error"}). Check its permissions and file type; nothing shared is lost.`
|
|
1345
|
+
]
|
|
1346
|
+
};
|
|
984
1347
|
}
|
|
985
1348
|
let parsed;
|
|
986
1349
|
try {
|
|
@@ -1027,9 +1390,9 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1027
1390
|
if (check.config === null) {
|
|
1028
1391
|
return { ok: false, error: check.errors.join(" ") };
|
|
1029
1392
|
}
|
|
1030
|
-
const path =
|
|
1031
|
-
await
|
|
1032
|
-
await
|
|
1393
|
+
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
1394
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
1395
|
+
await writeFile2(path, `${JSON.stringify({ previews: [...existing.previews.map(toStored), candidate] }, null, 2)}
|
|
1033
1396
|
`, "utf8");
|
|
1034
1397
|
return { ok: true };
|
|
1035
1398
|
}
|
|
@@ -1042,9 +1405,9 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
1042
1405
|
const keep = existing.previews.filter((preview) => !titles.includes(preview.title));
|
|
1043
1406
|
if (keep.length === existing.previews.length)
|
|
1044
1407
|
return 0;
|
|
1045
|
-
const path =
|
|
1046
|
-
await
|
|
1047
|
-
await
|
|
1408
|
+
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
1409
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
1410
|
+
await writeFile2(path, `${JSON.stringify({ previews: keep.map(toStored) }, null, 2)}
|
|
1048
1411
|
`, "utf8");
|
|
1049
1412
|
return existing.previews.length - keep.length;
|
|
1050
1413
|
}
|
|
@@ -1113,10 +1476,10 @@ ${headers}\r
|
|
|
1113
1476
|
}
|
|
1114
1477
|
|
|
1115
1478
|
// ../server/dist/worktree.js
|
|
1116
|
-
import { execFile, spawn } from "child_process";
|
|
1479
|
+
import { execFile, spawn as spawn2 } from "child_process";
|
|
1117
1480
|
import { rm } from "fs/promises";
|
|
1118
1481
|
import net2 from "net";
|
|
1119
|
-
import { join as
|
|
1482
|
+
import { join as join4 } from "path";
|
|
1120
1483
|
import { promisify } from "util";
|
|
1121
1484
|
var run = promisify(execFile);
|
|
1122
1485
|
var WORKTREES_DIR = ".leglas/worktrees";
|
|
@@ -1153,7 +1516,7 @@ function answers(port) {
|
|
|
1153
1516
|
}
|
|
1154
1517
|
async function startWorktree(options) {
|
|
1155
1518
|
const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
|
|
1156
|
-
const path =
|
|
1519
|
+
const path = join4(options.cwd, WORKTREES_DIR, worktreeSlug(options.branch));
|
|
1157
1520
|
const log = options.onLog ?? (() => {
|
|
1158
1521
|
});
|
|
1159
1522
|
await rm(path, { recursive: true, force: true });
|
|
@@ -1214,7 +1577,7 @@ async function startAppProcess(options) {
|
|
|
1214
1577
|
const port = await freePort();
|
|
1215
1578
|
let child;
|
|
1216
1579
|
try {
|
|
1217
|
-
child =
|
|
1580
|
+
child = spawn2(substitutePort(options.devCommand, port), {
|
|
1218
1581
|
cwd: options.cwd,
|
|
1219
1582
|
shell: true,
|
|
1220
1583
|
// Own process group, so stopping kills the shell and whatever it spawned
|
|
@@ -1254,9 +1617,9 @@ async function startAppProcess(options) {
|
|
|
1254
1617
|
}
|
|
1255
1618
|
|
|
1256
1619
|
// ../server/dist/requests.js
|
|
1257
|
-
import { mkdir as
|
|
1620
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1258
1621
|
import { randomBytes } from "crypto";
|
|
1259
|
-
import { dirname as
|
|
1622
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
1260
1623
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1261
1624
|
function targetFor(url) {
|
|
1262
1625
|
if (!url.startsWith("/"))
|
|
@@ -1282,17 +1645,20 @@ function composeRequest(preview, intent) {
|
|
|
1282
1645
|
const target = preview.file ?? targetFor(preview.url);
|
|
1283
1646
|
const cleaned = intent.trim();
|
|
1284
1647
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1648
|
+
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
1285
1649
|
const prompt = `In this project, change only the "${preview.title}" design direction. ${where}
|
|
1286
1650
|
|
|
1287
1651
|
What to change: ${cleaned}
|
|
1288
1652
|
|
|
1653
|
+
${pace}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.
|
|
1654
|
+
|
|
1289
1655
|
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. The direction is already registered, so nothing needs re-registering. Keep the change additive: do not rewrite shared components that other directions rely on.`;
|
|
1290
1656
|
return { prompt, target };
|
|
1291
1657
|
}
|
|
1292
1658
|
var REQUESTS_PATH = ".leglas/requests.json";
|
|
1293
1659
|
async function readRequests(cwd) {
|
|
1294
1660
|
try {
|
|
1295
|
-
const raw = await
|
|
1661
|
+
const raw = await readFile4(join5(cwd, REQUESTS_PATH), "utf8");
|
|
1296
1662
|
const parsed = JSON.parse(raw);
|
|
1297
1663
|
if (!Array.isArray(parsed.requests))
|
|
1298
1664
|
return [];
|
|
@@ -1309,9 +1675,9 @@ async function readRequests(cwd) {
|
|
|
1309
1675
|
}
|
|
1310
1676
|
}
|
|
1311
1677
|
async function writeQueue(cwd, requests) {
|
|
1312
|
-
const path =
|
|
1313
|
-
await
|
|
1314
|
-
await
|
|
1678
|
+
const path = join5(cwd, REQUESTS_PATH);
|
|
1679
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1680
|
+
await writeFile3(path, `${JSON.stringify({ requests }, null, 2)}
|
|
1315
1681
|
`, "utf8");
|
|
1316
1682
|
}
|
|
1317
1683
|
async function appendRequest(cwd, request) {
|
|
@@ -1351,13 +1717,264 @@ async function clearRequests(cwd) {
|
|
|
1351
1717
|
return { cleared, pending: pending.length };
|
|
1352
1718
|
}
|
|
1353
1719
|
|
|
1720
|
+
// ../server/dist/runner.js
|
|
1721
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
1722
|
+
var POLL_MS = 2e3;
|
|
1723
|
+
var OUTPUT_LINES = 20;
|
|
1724
|
+
var SESSION_TURNS_CAP = 8;
|
|
1725
|
+
function resolveCommand(choice, prompt, sessionId = null) {
|
|
1726
|
+
if (choice.agent === null)
|
|
1727
|
+
return null;
|
|
1728
|
+
if (choice.agent === "custom") {
|
|
1729
|
+
if (choice.run === null)
|
|
1730
|
+
return null;
|
|
1731
|
+
const parsed = parseTemplate(choice.run);
|
|
1732
|
+
if (!parsed.ok)
|
|
1733
|
+
return null;
|
|
1734
|
+
return { agent: "custom", name: "Custom", ...commandFor(parsed.template, prompt), resumed: false };
|
|
1735
|
+
}
|
|
1736
|
+
const adapter = KNOWN_AGENTS[choice.agent];
|
|
1737
|
+
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1738
|
+
return {
|
|
1739
|
+
agent: choice.agent,
|
|
1740
|
+
name: adapter.name,
|
|
1741
|
+
command: adapter.binary,
|
|
1742
|
+
args: adapter.resumeArgs(sessionId, prompt),
|
|
1743
|
+
resumed: true
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
agent: choice.agent,
|
|
1748
|
+
name: adapter.name,
|
|
1749
|
+
command: adapter.binary,
|
|
1750
|
+
args: adapter.args(prompt),
|
|
1751
|
+
resumed: false
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function lineReader(stream, onLine) {
|
|
1755
|
+
let buffered = "";
|
|
1756
|
+
const flush = () => {
|
|
1757
|
+
if (buffered === "")
|
|
1758
|
+
return;
|
|
1759
|
+
onLine(buffered.replace(/\r$/, ""));
|
|
1760
|
+
buffered = "";
|
|
1761
|
+
};
|
|
1762
|
+
stream.on("data", (chunk) => {
|
|
1763
|
+
buffered += chunk.toString();
|
|
1764
|
+
const lines = buffered.split("\n");
|
|
1765
|
+
buffered = lines.pop() ?? "";
|
|
1766
|
+
for (const line of lines)
|
|
1767
|
+
onLine(line.replace(/\r$/, ""));
|
|
1768
|
+
});
|
|
1769
|
+
stream.on("end", flush);
|
|
1770
|
+
return flush;
|
|
1771
|
+
}
|
|
1772
|
+
function defaultSpawn(command, args, options) {
|
|
1773
|
+
return nodeSpawn(command, args, options);
|
|
1774
|
+
}
|
|
1775
|
+
function startRunner(options) {
|
|
1776
|
+
const spawn4 = options.spawn ?? defaultSpawn;
|
|
1777
|
+
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1778
|
+
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1779
|
+
const failed = /* @__PURE__ */ new Set();
|
|
1780
|
+
let state = {
|
|
1781
|
+
running: false,
|
|
1782
|
+
requestId: null,
|
|
1783
|
+
agent: null,
|
|
1784
|
+
activity: null,
|
|
1785
|
+
startedAt: null
|
|
1786
|
+
};
|
|
1787
|
+
let stopped = false;
|
|
1788
|
+
let ticking = null;
|
|
1789
|
+
let stopPromise = null;
|
|
1790
|
+
let active = null;
|
|
1791
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1792
|
+
const idle = () => {
|
|
1793
|
+
state = { running: false, requestId: null, agent: null, activity: null, startedAt: null };
|
|
1794
|
+
};
|
|
1795
|
+
const rememberLine = (lines, line) => {
|
|
1796
|
+
lines.push(line);
|
|
1797
|
+
if (lines.length > OUTPUT_LINES)
|
|
1798
|
+
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1799
|
+
};
|
|
1800
|
+
const reportFailure = (request, error, lines) => {
|
|
1801
|
+
console.error(`Leglas agent failed for ${request.title}: ${error}`);
|
|
1802
|
+
for (const line of lines)
|
|
1803
|
+
console.error(` ${line}`);
|
|
1804
|
+
};
|
|
1805
|
+
const runChild = (request, resolved, lines, observed) => {
|
|
1806
|
+
let child;
|
|
1807
|
+
try {
|
|
1808
|
+
child = spawn4(resolved.command, resolved.args, {
|
|
1809
|
+
cwd: options.cwd,
|
|
1810
|
+
shell: false,
|
|
1811
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1812
|
+
});
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
return Promise.resolve({
|
|
1815
|
+
ok: false,
|
|
1816
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const current = { child, requestId: request.id, cancelled: false };
|
|
1820
|
+
active = current;
|
|
1821
|
+
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1822
|
+
rememberLine(lines, line);
|
|
1823
|
+
const sessionId = sessionFrom(resolved.agent, line);
|
|
1824
|
+
if (sessionId !== null)
|
|
1825
|
+
observed.sessionId = sessionId;
|
|
1826
|
+
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1827
|
+
if (activity !== null) {
|
|
1828
|
+
if (activity.startsWith("editing"))
|
|
1829
|
+
observed.edited = true;
|
|
1830
|
+
if (active === current)
|
|
1831
|
+
state = { ...state, activity };
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
1835
|
+
return new Promise((resolve) => {
|
|
1836
|
+
let settled = false;
|
|
1837
|
+
const settle = (outcome) => {
|
|
1838
|
+
if (settled)
|
|
1839
|
+
return;
|
|
1840
|
+
settled = true;
|
|
1841
|
+
stdoutFlush();
|
|
1842
|
+
stderrFlush();
|
|
1843
|
+
resolve(outcome);
|
|
1844
|
+
};
|
|
1845
|
+
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1846
|
+
child.once("close", (code, signal) => {
|
|
1847
|
+
if (current.cancelled)
|
|
1848
|
+
return settle({ ok: false, error: "cancelled" });
|
|
1849
|
+
if (signal !== null)
|
|
1850
|
+
return settle({ ok: false, error: `stopped by ${signal}` });
|
|
1851
|
+
settle({ ok: true, code: code ?? 0 });
|
|
1852
|
+
});
|
|
1853
|
+
}).finally(() => {
|
|
1854
|
+
if (active === current)
|
|
1855
|
+
active = null;
|
|
1856
|
+
});
|
|
1857
|
+
};
|
|
1858
|
+
const handle = async (request, choice) => {
|
|
1859
|
+
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1860
|
+
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1861
|
+
let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null);
|
|
1862
|
+
if (resolved === null)
|
|
1863
|
+
return;
|
|
1864
|
+
const lines = [];
|
|
1865
|
+
try {
|
|
1866
|
+
if (!await markPickedUp(options.cwd, request.id))
|
|
1867
|
+
return;
|
|
1868
|
+
if (stopped) {
|
|
1869
|
+
failed.add(request.id);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
state = {
|
|
1873
|
+
running: true,
|
|
1874
|
+
requestId: request.id,
|
|
1875
|
+
agent: resolved.name,
|
|
1876
|
+
activity: null,
|
|
1877
|
+
startedAt: Date.now()
|
|
1878
|
+
};
|
|
1879
|
+
const observed = { sessionId: null, edited: false };
|
|
1880
|
+
let outcome = await runChild(request, resolved, lines, observed);
|
|
1881
|
+
const cancelled = !outcome.ok && outcome.error === "cancelled";
|
|
1882
|
+
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && !cancelled && // Not redundant with the line above: a stop that lands between the
|
|
1883
|
+
// first child settling and the retry starting finds no child to
|
|
1884
|
+
// cancel, so nothing says "cancelled". Stopped still means stopped.
|
|
1885
|
+
!stopped) {
|
|
1886
|
+
sessions.delete(resolved.agent);
|
|
1887
|
+
const cold = resolveCommand(choice, request.prompt);
|
|
1888
|
+
if (cold !== null) {
|
|
1889
|
+
resolved = cold;
|
|
1890
|
+
observed.sessionId = null;
|
|
1891
|
+
state = { ...state, activity: null };
|
|
1892
|
+
outcome = await runChild(request, resolved, lines, observed);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
if (outcome.ok && outcome.code === 0) {
|
|
1896
|
+
if (observed.sessionId !== null) {
|
|
1897
|
+
const previous = sessions.get(resolved.agent);
|
|
1898
|
+
sessions.set(resolved.agent, {
|
|
1899
|
+
id: observed.sessionId,
|
|
1900
|
+
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
await removeRequest(options.cwd, request.id);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
sessions.delete(resolved.agent);
|
|
1907
|
+
failed.add(request.id);
|
|
1908
|
+
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
1909
|
+
} finally {
|
|
1910
|
+
idle();
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
const tick = async () => {
|
|
1914
|
+
if (stopped)
|
|
1915
|
+
return;
|
|
1916
|
+
const choice = await readAgentChoice(options.cwd);
|
|
1917
|
+
if (choice.agent === null || stopped)
|
|
1918
|
+
return;
|
|
1919
|
+
if (options.externallyAttached())
|
|
1920
|
+
return;
|
|
1921
|
+
const request = nextRequest(await readRequests(options.cwd), failed);
|
|
1922
|
+
if (request !== null && !stopped)
|
|
1923
|
+
await handle(request, choice);
|
|
1924
|
+
};
|
|
1925
|
+
const schedule = () => {
|
|
1926
|
+
if (stopped || ticking !== null)
|
|
1927
|
+
return;
|
|
1928
|
+
const task = tick();
|
|
1929
|
+
ticking = task;
|
|
1930
|
+
void task.catch((error) => console.error(`Leglas runner: ${error instanceof Error ? error.message : String(error)}`)).finally(() => {
|
|
1931
|
+
if (ticking === task)
|
|
1932
|
+
ticking = null;
|
|
1933
|
+
});
|
|
1934
|
+
};
|
|
1935
|
+
const timer = setEvery(schedule, POLL_MS);
|
|
1936
|
+
schedule();
|
|
1937
|
+
const cancel = (id) => {
|
|
1938
|
+
if (active === null || active.cancelled)
|
|
1939
|
+
return false;
|
|
1940
|
+
if (id !== void 0 && active.requestId !== id)
|
|
1941
|
+
return false;
|
|
1942
|
+
active.cancelled = true;
|
|
1943
|
+
failed.add(active.requestId);
|
|
1944
|
+
try {
|
|
1945
|
+
active.child.kill("SIGTERM");
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
return true;
|
|
1949
|
+
};
|
|
1950
|
+
const stop = () => {
|
|
1951
|
+
if (stopPromise !== null)
|
|
1952
|
+
return stopPromise;
|
|
1953
|
+
stopped = true;
|
|
1954
|
+
clearEvery(timer);
|
|
1955
|
+
cancel();
|
|
1956
|
+
stopPromise = Promise.resolve(ticking).catch(() => {
|
|
1957
|
+
}).then(() => {
|
|
1958
|
+
});
|
|
1959
|
+
return stopPromise;
|
|
1960
|
+
};
|
|
1961
|
+
return {
|
|
1962
|
+
stop,
|
|
1963
|
+
snapshot: () => ({ ...state, failedIds: [...failed] }),
|
|
1964
|
+
cancel,
|
|
1965
|
+
// schedule already refuses to overlap a tick in flight, so a nudge during
|
|
1966
|
+
// a run costs nothing and a nudge between runs starts the next one now.
|
|
1967
|
+
nudge: schedule
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1354
1971
|
// ../server/dist/renames.js
|
|
1355
|
-
import { mkdir as
|
|
1356
|
-
import { dirname as
|
|
1972
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1973
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1357
1974
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
1358
1975
|
async function readRenames(cwd) {
|
|
1359
1976
|
try {
|
|
1360
|
-
const raw = await
|
|
1977
|
+
const raw = await readFile5(join6(cwd, RENAMES_PATH), "utf8");
|
|
1361
1978
|
const parsed = JSON.parse(raw);
|
|
1362
1979
|
if (parsed.renames === null || typeof parsed.renames !== "object")
|
|
1363
1980
|
return {};
|
|
@@ -1367,9 +1984,9 @@ async function readRenames(cwd) {
|
|
|
1367
1984
|
}
|
|
1368
1985
|
}
|
|
1369
1986
|
async function writeRenames(cwd, renames) {
|
|
1370
|
-
const path =
|
|
1371
|
-
await
|
|
1372
|
-
await
|
|
1987
|
+
const path = join6(cwd, RENAMES_PATH);
|
|
1988
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
1989
|
+
await writeFile4(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1373
1990
|
`, "utf8");
|
|
1374
1991
|
}
|
|
1375
1992
|
function resolveTitle(input, titles, renames) {
|
|
@@ -1387,7 +2004,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
1387
2004
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1388
2005
|
import http2 from "http";
|
|
1389
2006
|
import net3 from "net";
|
|
1390
|
-
import { extname, join as
|
|
2007
|
+
import { extname, join as join7, normalize, relative as relative3 } from "path";
|
|
1391
2008
|
var LEGLAS_PREFIX = "/leglas";
|
|
1392
2009
|
var DEFAULT_PORT = 4100;
|
|
1393
2010
|
var PORT_ATTEMPTS = 20;
|
|
@@ -1418,6 +2035,52 @@ function sendJson(res, status, body) {
|
|
|
1418
2035
|
});
|
|
1419
2036
|
res.end(payload);
|
|
1420
2037
|
}
|
|
2038
|
+
function isKnownAgent(value) {
|
|
2039
|
+
return typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
2040
|
+
}
|
|
2041
|
+
function isAllowedMutationHost(hostname) {
|
|
2042
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
2043
|
+
if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1")
|
|
2044
|
+
return true;
|
|
2045
|
+
if (bare.endsWith(".local"))
|
|
2046
|
+
return true;
|
|
2047
|
+
if (!net3.isIPv4(bare))
|
|
2048
|
+
return false;
|
|
2049
|
+
const [first, second] = bare.split(".").map(Number);
|
|
2050
|
+
return first === 10 || first === 172 && second !== void 0 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
2051
|
+
}
|
|
2052
|
+
function isLoopbackAddress(address) {
|
|
2053
|
+
if (address === void 0)
|
|
2054
|
+
return false;
|
|
2055
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1" || address.startsWith("127.");
|
|
2056
|
+
}
|
|
2057
|
+
function isTrustedMutation(req) {
|
|
2058
|
+
if (!isLoopbackAddress(req.socket.remoteAddress))
|
|
2059
|
+
return false;
|
|
2060
|
+
if (typeof req.headers.host !== "string")
|
|
2061
|
+
return false;
|
|
2062
|
+
let host;
|
|
2063
|
+
try {
|
|
2064
|
+
host = new URL(`http://${req.headers.host}`);
|
|
2065
|
+
} catch {
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
if (!isAllowedMutationHost(host.hostname))
|
|
2069
|
+
return false;
|
|
2070
|
+
const rawOrigin = req.headers.origin;
|
|
2071
|
+
if (rawOrigin === void 0)
|
|
2072
|
+
return true;
|
|
2073
|
+
try {
|
|
2074
|
+
const origin = new URL(rawOrigin);
|
|
2075
|
+
return origin.protocol === "http:" && origin.host === host.host;
|
|
2076
|
+
} catch {
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function hasJsonBody(req) {
|
|
2081
|
+
const contentType = req.headers["content-type"];
|
|
2082
|
+
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
2083
|
+
}
|
|
1421
2084
|
function probe(target, timeoutMs = 1e3) {
|
|
1422
2085
|
return new Promise((resolve) => {
|
|
1423
2086
|
let url;
|
|
@@ -1439,8 +2102,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
1439
2102
|
});
|
|
1440
2103
|
}
|
|
1441
2104
|
function serveFrom(res, dir, relativePath) {
|
|
1442
|
-
const
|
|
1443
|
-
const candidate =
|
|
2105
|
+
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
2106
|
+
const candidate = join7(dir, relative5);
|
|
1444
2107
|
if (!candidate.startsWith(dir))
|
|
1445
2108
|
return false;
|
|
1446
2109
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -1453,9 +2116,9 @@ function serveFrom(res, dir, relativePath) {
|
|
|
1453
2116
|
return true;
|
|
1454
2117
|
}
|
|
1455
2118
|
function serveShellFile(res, shellDir, urlPath) {
|
|
1456
|
-
const
|
|
1457
|
-
const isRoot =
|
|
1458
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
2119
|
+
const relative5 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
2120
|
+
const isRoot = relative5 === "" || relative5 === "." || relative5 === "/";
|
|
2121
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative5);
|
|
1459
2122
|
}
|
|
1460
2123
|
function snapshotConfig(cwd) {
|
|
1461
2124
|
const path = findConfigFile(cwd);
|
|
@@ -1471,15 +2134,15 @@ function configStalenessNotice(cwd, boot, current) {
|
|
|
1471
2134
|
if (boot === null && current === null)
|
|
1472
2135
|
return null;
|
|
1473
2136
|
if (boot === null && current !== null) {
|
|
1474
|
-
const label =
|
|
2137
|
+
const label = relative3(cwd, current.path) || current.path;
|
|
1475
2138
|
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
1476
2139
|
}
|
|
1477
2140
|
if (boot !== null && current === null) {
|
|
1478
|
-
const label =
|
|
2141
|
+
const label = relative3(cwd, boot.path) || boot.path;
|
|
1479
2142
|
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
1480
2143
|
}
|
|
1481
2144
|
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
1482
|
-
const label =
|
|
2145
|
+
const label = relative3(cwd, current.path) || current.path;
|
|
1483
2146
|
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
1484
2147
|
}
|
|
1485
2148
|
return null;
|
|
@@ -1525,27 +2188,63 @@ async function bind(server, requested) {
|
|
|
1525
2188
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
1526
2189
|
}
|
|
1527
2190
|
async function startServer(options) {
|
|
1528
|
-
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map() } = options;
|
|
2191
|
+
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
1529
2192
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
1530
2193
|
const proxy = createProxyHandler({ target });
|
|
1531
2194
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
1532
2195
|
let lastSeen = null;
|
|
2196
|
+
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
2197
|
+
let runner = null;
|
|
2198
|
+
let agentsCache = null;
|
|
2199
|
+
let agentsInflight = null;
|
|
2200
|
+
const AGENTS_FRESH_MS = 3e4;
|
|
2201
|
+
const probeAgents = () => {
|
|
2202
|
+
agentsInflight ??= detect().then((agents) => {
|
|
2203
|
+
agentsCache = { at: Date.now(), agents };
|
|
2204
|
+
return agents;
|
|
2205
|
+
}).finally(() => {
|
|
2206
|
+
agentsInflight = null;
|
|
2207
|
+
});
|
|
2208
|
+
return agentsInflight;
|
|
2209
|
+
};
|
|
2210
|
+
const currentAgents = () => {
|
|
2211
|
+
if (agentsCache === null)
|
|
2212
|
+
return probeAgents();
|
|
2213
|
+
if (Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
|
|
2214
|
+
void probeAgents().catch(() => {
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
return Promise.resolve(agentsCache.agents);
|
|
2218
|
+
};
|
|
1533
2219
|
const server = http2.createServer((req, res) => {
|
|
1534
2220
|
const url = req.url ?? "/";
|
|
1535
2221
|
const path = url.split("?")[0] ?? "/";
|
|
2222
|
+
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
2223
|
+
return sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
2224
|
+
}
|
|
1536
2225
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
1537
2226
|
const boot = config?.previews ?? [];
|
|
1538
2227
|
const errors = [...configErrors];
|
|
1539
2228
|
const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
|
|
1540
2229
|
if (notice !== null)
|
|
1541
2230
|
errors.push(notice);
|
|
1542
|
-
return void readLocalPreviews(cwd).then(({ previews: local }) => {
|
|
1543
|
-
|
|
2231
|
+
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
2232
|
+
if (localErrors.length > 0) {
|
|
2233
|
+
return sendJson(res, 200, {
|
|
2234
|
+
project,
|
|
2235
|
+
devServer: target,
|
|
2236
|
+
previews: boot,
|
|
2237
|
+
errors
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
const localTitles = new Set(local.map((preview) => preview.title));
|
|
2241
|
+
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
2242
|
+
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
1544
2243
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
1545
2244
|
sendJson(res, 200, {
|
|
1546
2245
|
project,
|
|
1547
2246
|
devServer: target,
|
|
1548
|
-
previews: [...
|
|
2247
|
+
previews: [...currentBoot, ...fresh],
|
|
1549
2248
|
errors
|
|
1550
2249
|
});
|
|
1551
2250
|
}).catch(() => sendJson(res, 200, {
|
|
@@ -1555,6 +2254,47 @@ async function startServer(options) {
|
|
|
1555
2254
|
errors
|
|
1556
2255
|
}));
|
|
1557
2256
|
}
|
|
2257
|
+
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
2258
|
+
let body = "";
|
|
2259
|
+
req.on("data", (chunk) => body += chunk);
|
|
2260
|
+
return void req.on("end", async () => {
|
|
2261
|
+
let parsed;
|
|
2262
|
+
try {
|
|
2263
|
+
parsed = JSON.parse(body || "{}");
|
|
2264
|
+
} catch {
|
|
2265
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2266
|
+
}
|
|
2267
|
+
const titles = parsed.titles;
|
|
2268
|
+
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
2269
|
+
return sendJson(res, 400, {
|
|
2270
|
+
ok: false,
|
|
2271
|
+
error: "Body needs a non-empty array of direction titles."
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
const unique = [...new Set(titles)];
|
|
2275
|
+
try {
|
|
2276
|
+
const local = await readLocalPreviews(cwd);
|
|
2277
|
+
if (local.errors.length > 0) {
|
|
2278
|
+
return sendJson(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
2279
|
+
}
|
|
2280
|
+
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
2281
|
+
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
2282
|
+
if (unknown.length > 0) {
|
|
2283
|
+
return sendJson(res, 400, {
|
|
2284
|
+
ok: false,
|
|
2285
|
+
error: "Only machine-local directions can be deleted from the registry."
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
const deleted = await dropLocalPreviews(cwd, unique);
|
|
2289
|
+
return sendJson(res, 200, { ok: true, deleted });
|
|
2290
|
+
} catch {
|
|
2291
|
+
return sendJson(res, 500, {
|
|
2292
|
+
ok: false,
|
|
2293
|
+
error: "The directions could not be deleted from Leglas."
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
1558
2298
|
if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
|
|
1559
2299
|
let body = "";
|
|
1560
2300
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -1565,8 +2305,12 @@ async function startServer(options) {
|
|
|
1565
2305
|
} catch {
|
|
1566
2306
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1567
2307
|
}
|
|
1568
|
-
const
|
|
1569
|
-
const
|
|
2308
|
+
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
2309
|
+
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
2310
|
+
const localTitles = new Set(local.map((entry) => entry.title));
|
|
2311
|
+
const bootConfig = config?.previews ?? [];
|
|
2312
|
+
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
2313
|
+
const preview = [...boot, ...local].find((entry) => entry.title === parsed.title);
|
|
1570
2314
|
if (!preview || !parsed.intent?.trim()) {
|
|
1571
2315
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
1572
2316
|
}
|
|
@@ -1576,7 +2320,54 @@ async function startServer(options) {
|
|
|
1576
2320
|
url: preview.url,
|
|
1577
2321
|
intent: parsed.intent.trim(),
|
|
1578
2322
|
...composed
|
|
1579
|
-
}).then(() =>
|
|
2323
|
+
}).then(() => {
|
|
2324
|
+
runner?.nudge();
|
|
2325
|
+
sendJson(res, 200, { ok: true, ...composed });
|
|
2326
|
+
}).catch(() => sendJson(res, 200, { ok: true, ...composed, queued: false }));
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
2330
|
+
return void Promise.all([currentAgents(), readAgentChoice(cwd)]).then(([agents, choice]) => sendJson(res, 200, {
|
|
2331
|
+
agents,
|
|
2332
|
+
choice: choice.agent,
|
|
2333
|
+
customRun: choice.run
|
|
2334
|
+
}));
|
|
2335
|
+
}
|
|
2336
|
+
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
2337
|
+
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
2338
|
+
return sendJson(res, 403, {
|
|
2339
|
+
ok: false,
|
|
2340
|
+
error: "The agent choice can only be made from the machine running Leglas."
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
if (!hasJsonBody(req)) {
|
|
2344
|
+
return sendJson(res, 400, { ok: false, error: "Agent choice must be JSON." });
|
|
2345
|
+
}
|
|
2346
|
+
let body = "";
|
|
2347
|
+
req.on("data", (chunk) => body += chunk);
|
|
2348
|
+
return void req.on("end", () => {
|
|
2349
|
+
let parsed;
|
|
2350
|
+
try {
|
|
2351
|
+
parsed = JSON.parse(body || "{}");
|
|
2352
|
+
} catch {
|
|
2353
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2354
|
+
}
|
|
2355
|
+
if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
|
|
2356
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a known agent." });
|
|
2357
|
+
}
|
|
2358
|
+
if (parsed.run !== void 0 && typeof parsed.run !== "string") {
|
|
2359
|
+
return sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
2360
|
+
}
|
|
2361
|
+
if (parsed.agent === "custom") {
|
|
2362
|
+
if (typeof parsed.run !== "string") {
|
|
2363
|
+
return sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
2364
|
+
}
|
|
2365
|
+
const template = parseTemplate(parsed.run);
|
|
2366
|
+
if (!template.ok)
|
|
2367
|
+
return sendJson(res, 400, { ok: false, error: template.error });
|
|
2368
|
+
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
|
+
}
|
|
2370
|
+
return void saveAgentChoice(cwd, { agent: parsed.agent }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
1580
2371
|
});
|
|
1581
2372
|
}
|
|
1582
2373
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -1597,11 +2388,119 @@ async function startServer(options) {
|
|
|
1597
2388
|
});
|
|
1598
2389
|
}
|
|
1599
2390
|
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
2391
|
+
const snapshot = runner?.snapshot() ?? {
|
|
2392
|
+
running: false,
|
|
2393
|
+
requestId: null,
|
|
2394
|
+
agent: null,
|
|
2395
|
+
activity: null,
|
|
2396
|
+
startedAt: null,
|
|
2397
|
+
failedIds: []
|
|
2398
|
+
};
|
|
1600
2399
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
1601
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
1602
|
-
|
|
2400
|
+
requests: requests.map(({ id, title, intent, status }) => ({
|
|
2401
|
+
id,
|
|
2402
|
+
title,
|
|
2403
|
+
intent,
|
|
2404
|
+
status: snapshot.running && snapshot.requestId === id ? "running" : snapshot.failedIds.includes(id) ? "failed" : status
|
|
2405
|
+
})),
|
|
2406
|
+
agent: {
|
|
2407
|
+
attached: externallyAttached(),
|
|
2408
|
+
running: snapshot.running,
|
|
2409
|
+
name: snapshot.running ? snapshot.agent : null,
|
|
2410
|
+
activity: snapshot.running ? snapshot.activity : null,
|
|
2411
|
+
startedAt: snapshot.running ? snapshot.startedAt : null
|
|
2412
|
+
}
|
|
1603
2413
|
}));
|
|
1604
2414
|
}
|
|
2415
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
2416
|
+
if (!hasJsonBody(req)) {
|
|
2417
|
+
return sendJson(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
|
|
2418
|
+
}
|
|
2419
|
+
let body = "";
|
|
2420
|
+
req.on("data", (chunk) => body += chunk);
|
|
2421
|
+
return void req.on("end", () => {
|
|
2422
|
+
let parsed;
|
|
2423
|
+
try {
|
|
2424
|
+
parsed = JSON.parse(body || "{}");
|
|
2425
|
+
} catch {
|
|
2426
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2427
|
+
}
|
|
2428
|
+
if (parsed.id !== void 0 && typeof parsed.id !== "string") {
|
|
2429
|
+
return sendJson(res, 400, { ok: false, error: "The request id must be a string." });
|
|
2430
|
+
}
|
|
2431
|
+
return sendJson(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
2435
|
+
if (!hasJsonBody(req)) {
|
|
2436
|
+
return sendJson(res, 400, { ok: false, error: "Retry must be JSON." });
|
|
2437
|
+
}
|
|
2438
|
+
let body = "";
|
|
2439
|
+
req.on("data", (chunk) => body += chunk);
|
|
2440
|
+
return void req.on("end", async () => {
|
|
2441
|
+
let parsed;
|
|
2442
|
+
try {
|
|
2443
|
+
parsed = JSON.parse(body || "{}");
|
|
2444
|
+
} catch {
|
|
2445
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2446
|
+
}
|
|
2447
|
+
if (typeof parsed.id !== "string") {
|
|
2448
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2449
|
+
}
|
|
2450
|
+
const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
2451
|
+
if (request === void 0) {
|
|
2452
|
+
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2453
|
+
}
|
|
2454
|
+
if (!(runner?.snapshot().failedIds.includes(request.id) ?? false)) {
|
|
2455
|
+
return sendJson(res, 400, { ok: false, error: "Only a failed request can be retried." });
|
|
2456
|
+
}
|
|
2457
|
+
try {
|
|
2458
|
+
if (!await removeRequest(cwd, request.id)) {
|
|
2459
|
+
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2460
|
+
}
|
|
2461
|
+
await appendRequest(cwd, {
|
|
2462
|
+
title: request.title,
|
|
2463
|
+
url: request.url,
|
|
2464
|
+
intent: request.intent,
|
|
2465
|
+
target: request.target,
|
|
2466
|
+
prompt: request.prompt
|
|
2467
|
+
});
|
|
2468
|
+
runner?.nudge();
|
|
2469
|
+
return sendJson(res, 200, { ok: true });
|
|
2470
|
+
} catch {
|
|
2471
|
+
return sendJson(res, 500, { ok: false, error: "The request could not be retried." });
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2476
|
+
if (!hasJsonBody(req)) {
|
|
2477
|
+
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
2478
|
+
}
|
|
2479
|
+
let body = "";
|
|
2480
|
+
req.on("data", (chunk) => body += chunk);
|
|
2481
|
+
return void req.on("end", async () => {
|
|
2482
|
+
let parsed;
|
|
2483
|
+
try {
|
|
2484
|
+
parsed = JSON.parse(body || "{}");
|
|
2485
|
+
} catch {
|
|
2486
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2487
|
+
}
|
|
2488
|
+
if (typeof parsed.id !== "string") {
|
|
2489
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2490
|
+
}
|
|
2491
|
+
if (!(runner?.snapshot().failedIds.includes(parsed.id) ?? false)) {
|
|
2492
|
+
return sendJson(res, 400, { ok: false, error: "Only a failed request can be dismissed." });
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
if (!await removeRequest(cwd, parsed.id)) {
|
|
2496
|
+
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2497
|
+
}
|
|
2498
|
+
return sendJson(res, 200, { ok: true });
|
|
2499
|
+
} catch {
|
|
2500
|
+
return sendJson(res, 500, { ok: false, error: "The request could not be dismissed." });
|
|
2501
|
+
}
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
1605
2504
|
if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
|
|
1606
2505
|
let body = "";
|
|
1607
2506
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -1626,14 +2525,14 @@ async function startServer(options) {
|
|
|
1626
2525
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
1627
2526
|
const slash = rest.indexOf("/");
|
|
1628
2527
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
1629
|
-
let
|
|
2528
|
+
let relative5 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
1630
2529
|
try {
|
|
1631
|
-
|
|
2530
|
+
relative5 = decodeURIComponent(relative5);
|
|
1632
2531
|
} catch {
|
|
1633
|
-
|
|
2532
|
+
relative5 = "";
|
|
1634
2533
|
}
|
|
1635
2534
|
const dir = fileMounts.get(slug);
|
|
1636
|
-
if (dir !== void 0 &&
|
|
2535
|
+
if (dir !== void 0 && relative5 !== "" && serveFrom(res, dir, relative5))
|
|
1637
2536
|
return;
|
|
1638
2537
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
1639
2538
|
return res.end("Leglas: no such preview file.");
|
|
@@ -1665,16 +2564,23 @@ async function startServer(options) {
|
|
|
1665
2564
|
proxy.upgrade(req, socket, head);
|
|
1666
2565
|
});
|
|
1667
2566
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2567
|
+
runner = startRunner({ cwd, externallyAttached });
|
|
2568
|
+
let closePromise = null;
|
|
1668
2569
|
return {
|
|
1669
2570
|
port,
|
|
1670
2571
|
url: `http://localhost:${port}`,
|
|
1671
|
-
close: () =>
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2572
|
+
close: () => {
|
|
2573
|
+
if (closePromise !== null)
|
|
2574
|
+
return closePromise;
|
|
2575
|
+
closePromise = runner.stop().then(() => new Promise((done) => {
|
|
2576
|
+
for (const socket of sockets)
|
|
2577
|
+
socket.destroy();
|
|
2578
|
+
sockets.clear();
|
|
2579
|
+
server.closeAllConnections();
|
|
2580
|
+
server.close(() => done());
|
|
2581
|
+
}));
|
|
2582
|
+
return closePromise;
|
|
2583
|
+
}
|
|
1678
2584
|
};
|
|
1679
2585
|
}
|
|
1680
2586
|
|
|
@@ -1733,11 +2639,11 @@ function planKeep(options) {
|
|
|
1733
2639
|
}
|
|
1734
2640
|
|
|
1735
2641
|
// src/run-init.ts
|
|
1736
|
-
import { readFile as
|
|
1737
|
-
import { join as
|
|
2642
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
2643
|
+
import { join as join8 } from "path";
|
|
1738
2644
|
async function readIfPresent(path) {
|
|
1739
2645
|
try {
|
|
1740
|
-
return await
|
|
2646
|
+
return await readFile6(path, "utf8");
|
|
1741
2647
|
} catch {
|
|
1742
2648
|
return null;
|
|
1743
2649
|
}
|
|
@@ -1745,18 +2651,18 @@ async function readIfPresent(path) {
|
|
|
1745
2651
|
async function runInit(options, deps) {
|
|
1746
2652
|
const existingConfig = findConfigFile(options.cwd);
|
|
1747
2653
|
const plan = planInit({
|
|
1748
|
-
agents: await readIfPresent(
|
|
2654
|
+
agents: await readIfPresent(join8(options.cwd, "AGENTS.md")),
|
|
1749
2655
|
config: existingConfig === null ? null : "present",
|
|
1750
|
-
gitignore: await readIfPresent(
|
|
2656
|
+
gitignore: await readIfPresent(join8(options.cwd, ".gitignore")),
|
|
1751
2657
|
force: options.force
|
|
1752
2658
|
});
|
|
1753
2659
|
const touched = [];
|
|
1754
2660
|
for (const write of plan.writes) {
|
|
1755
|
-
await
|
|
2661
|
+
await writeFile5(join8(options.cwd, write.path), write.contents, "utf8");
|
|
1756
2662
|
touched.push(write.path);
|
|
1757
2663
|
}
|
|
1758
2664
|
if (plan.gitignore !== null) {
|
|
1759
|
-
await
|
|
2665
|
+
await writeFile5(join8(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
1760
2666
|
touched.push(".gitignore");
|
|
1761
2667
|
}
|
|
1762
2668
|
if (options.json) {
|
|
@@ -1776,8 +2682,8 @@ async function runInit(options, deps) {
|
|
|
1776
2682
|
|
|
1777
2683
|
// src/run-keep.ts
|
|
1778
2684
|
import { existsSync as existsSync3 } from "fs";
|
|
1779
|
-
import { mkdir as
|
|
1780
|
-
import { dirname as
|
|
2685
|
+
import { mkdir as mkdir5, readFile as readFile7, rm as rm2, writeFile as writeFile6 } from "fs/promises";
|
|
2686
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1781
2687
|
|
|
1782
2688
|
// src/resolve-title.ts
|
|
1783
2689
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -1821,18 +2727,18 @@ async function runKeep(options, deps) {
|
|
|
1821
2727
|
if (!resolved.ok) return fail(resolved.error);
|
|
1822
2728
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
1823
2729
|
if (!plan.ok) return fail(plan.error);
|
|
1824
|
-
const from =
|
|
1825
|
-
const to =
|
|
2730
|
+
const from = join9(options.cwd, plan.move.from);
|
|
2731
|
+
const to = join9(options.cwd, plan.move.to);
|
|
1826
2732
|
if (!existsSync3(from)) {
|
|
1827
2733
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
1828
2734
|
}
|
|
1829
2735
|
if (existsSync3(to)) {
|
|
1830
2736
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
1831
2737
|
}
|
|
1832
|
-
const source = await
|
|
1833
|
-
await
|
|
1834
|
-
await
|
|
1835
|
-
await rm2(
|
|
2738
|
+
const source = await readFile7(from, "utf8");
|
|
2739
|
+
await mkdir5(dirname6(to), { recursive: true });
|
|
2740
|
+
await writeFile6(to, renameExport(source, plan.exportName), "utf8");
|
|
2741
|
+
await rm2(join9(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
1836
2742
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
1837
2743
|
if (options.json) {
|
|
1838
2744
|
deps.log(
|
|
@@ -1867,11 +2773,11 @@ async function runKeep(options, deps) {
|
|
|
1867
2773
|
|
|
1868
2774
|
// src/run-new.ts
|
|
1869
2775
|
import { existsSync as existsSync4 } from "fs";
|
|
1870
|
-
import { mkdir as
|
|
1871
|
-
import { dirname as
|
|
2776
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
2777
|
+
import { dirname as dirname7, join as join10 } from "path";
|
|
1872
2778
|
async function readIfPresent2(path) {
|
|
1873
2779
|
try {
|
|
1874
|
-
return await
|
|
2780
|
+
return await readFile8(path, "utf8");
|
|
1875
2781
|
} catch {
|
|
1876
2782
|
return null;
|
|
1877
2783
|
}
|
|
@@ -1879,7 +2785,7 @@ async function readIfPresent2(path) {
|
|
|
1879
2785
|
async function runNew(options, deps) {
|
|
1880
2786
|
let from;
|
|
1881
2787
|
if (options.from !== void 0) {
|
|
1882
|
-
const contents = await readIfPresent2(
|
|
2788
|
+
const contents = await readIfPresent2(join10(options.cwd, options.from));
|
|
1883
2789
|
if (contents === null) {
|
|
1884
2790
|
const message = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
1885
2791
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message }));
|
|
@@ -1890,8 +2796,8 @@ async function runNew(options, deps) {
|
|
|
1890
2796
|
}
|
|
1891
2797
|
const plan = planNew({
|
|
1892
2798
|
surface: options.surface,
|
|
1893
|
-
packageJson: await readIfPresent2(
|
|
1894
|
-
gitignore: await readIfPresent2(
|
|
2799
|
+
packageJson: await readIfPresent2(join10(options.cwd, "package.json")),
|
|
2800
|
+
gitignore: await readIfPresent2(join10(options.cwd, ".gitignore")),
|
|
1895
2801
|
from
|
|
1896
2802
|
});
|
|
1897
2803
|
const fail = (error) => {
|
|
@@ -1914,19 +2820,19 @@ async function runNew(options, deps) {
|
|
|
1914
2820
|
deps.log(plan.instructions);
|
|
1915
2821
|
return { exitCode: 0, written: [] };
|
|
1916
2822
|
}
|
|
1917
|
-
const existing = plan.writes.filter((write) => existsSync4(
|
|
2823
|
+
const existing = plan.writes.filter((write) => existsSync4(join10(options.cwd, write.path)));
|
|
1918
2824
|
if (existing.length > 0) {
|
|
1919
2825
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
1920
2826
|
}
|
|
1921
2827
|
const written = [];
|
|
1922
2828
|
for (const write of plan.writes) {
|
|
1923
|
-
const target =
|
|
1924
|
-
await
|
|
1925
|
-
await
|
|
2829
|
+
const target = join10(options.cwd, write.path);
|
|
2830
|
+
await mkdir6(dirname7(target), { recursive: true });
|
|
2831
|
+
await writeFile7(target, write.contents, "utf8");
|
|
1926
2832
|
written.push(write.path);
|
|
1927
2833
|
}
|
|
1928
2834
|
if (plan.gitignore !== null) {
|
|
1929
|
-
await
|
|
2835
|
+
await writeFile7(join10(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
1930
2836
|
written.push(".gitignore");
|
|
1931
2837
|
}
|
|
1932
2838
|
if (options.json) {
|
|
@@ -1947,21 +2853,21 @@ async function runNew(options, deps) {
|
|
|
1947
2853
|
}
|
|
1948
2854
|
|
|
1949
2855
|
// src/run-previews.ts
|
|
1950
|
-
import { readFile as
|
|
1951
|
-
import { join as
|
|
2856
|
+
import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
|
|
2857
|
+
import { join as join11 } from "path";
|
|
1952
2858
|
function envelope(deps, ok, body) {
|
|
1953
2859
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
1954
2860
|
}
|
|
1955
2861
|
async function ensureIgnored(cwd) {
|
|
1956
|
-
const path =
|
|
2862
|
+
const path = join11(cwd, ".gitignore");
|
|
1957
2863
|
let current = null;
|
|
1958
2864
|
try {
|
|
1959
|
-
current = await
|
|
2865
|
+
current = await readFile9(path, "utf8");
|
|
1960
2866
|
} catch {
|
|
1961
2867
|
current = null;
|
|
1962
2868
|
}
|
|
1963
2869
|
const next = ignoreEntry(current);
|
|
1964
|
-
if (next !== null) await
|
|
2870
|
+
if (next !== null) await writeFile8(path, next, "utf8");
|
|
1965
2871
|
}
|
|
1966
2872
|
async function runAdd(options, deps) {
|
|
1967
2873
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2205,99 +3111,25 @@ async function runShow(options, deps) {
|
|
|
2205
3111
|
return { exitCode: 0 };
|
|
2206
3112
|
}
|
|
2207
3113
|
|
|
2208
|
-
// src/watch.ts
|
|
2209
|
-
var WATCH_PATH = ".leglas/watch.json";
|
|
2210
|
-
var PROMPT_TOKEN = "{prompt}";
|
|
2211
|
-
var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
2212
|
-
function tokenize(template) {
|
|
2213
|
-
const tokens = [];
|
|
2214
|
-
let current = "";
|
|
2215
|
-
let started = false;
|
|
2216
|
-
let quote = null;
|
|
2217
|
-
for (const character of template) {
|
|
2218
|
-
if (quote !== null) {
|
|
2219
|
-
if (character === quote) quote = null;
|
|
2220
|
-
else current += character;
|
|
2221
|
-
continue;
|
|
2222
|
-
}
|
|
2223
|
-
if (character === '"' || character === "'") {
|
|
2224
|
-
quote = character;
|
|
2225
|
-
started = true;
|
|
2226
|
-
continue;
|
|
2227
|
-
}
|
|
2228
|
-
if (/\s/.test(character)) {
|
|
2229
|
-
if (started) tokens.push(current);
|
|
2230
|
-
current = "";
|
|
2231
|
-
started = false;
|
|
2232
|
-
continue;
|
|
2233
|
-
}
|
|
2234
|
-
current += character;
|
|
2235
|
-
started = true;
|
|
2236
|
-
}
|
|
2237
|
-
if (quote !== null) {
|
|
2238
|
-
return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
|
|
2239
|
-
}
|
|
2240
|
-
if (started) tokens.push(current);
|
|
2241
|
-
return { ok: true, tokens };
|
|
2242
|
-
}
|
|
2243
|
-
function parseTemplate(raw) {
|
|
2244
|
-
const tokenized = tokenize(raw);
|
|
2245
|
-
if (!tokenized.ok) return tokenized;
|
|
2246
|
-
const { tokens } = tokenized;
|
|
2247
|
-
const [command, ...args] = tokens;
|
|
2248
|
-
if (command === void 0) {
|
|
2249
|
-
return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
|
|
2250
|
-
}
|
|
2251
|
-
const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
|
|
2252
|
-
if (placeholders === 0) {
|
|
2253
|
-
return {
|
|
2254
|
-
ok: false,
|
|
2255
|
-
error: `The agent command needs ${PROMPT_TOKEN} as a word of its own, for example: ${EXAMPLE}`
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
if (placeholders > 1) {
|
|
2259
|
-
return {
|
|
2260
|
-
ok: false,
|
|
2261
|
-
error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
if (command === PROMPT_TOKEN) {
|
|
2265
|
-
return {
|
|
2266
|
-
ok: false,
|
|
2267
|
-
error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
return { ok: true, template: { command, args } };
|
|
2271
|
-
}
|
|
2272
|
-
function commandFor(template, prompt) {
|
|
2273
|
-
return {
|
|
2274
|
-
command: template.command,
|
|
2275
|
-
args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
|
|
2276
|
-
};
|
|
2277
|
-
}
|
|
2278
|
-
function nextRequest(requests, failed) {
|
|
2279
|
-
return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
3114
|
// src/run-watch.ts
|
|
2283
|
-
import { spawn as
|
|
2284
|
-
import { mkdir as
|
|
2285
|
-
import { dirname as
|
|
2286
|
-
var
|
|
3115
|
+
import { spawn as spawn3 } from "child_process";
|
|
3116
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
|
|
3117
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
3118
|
+
var POLL_MS2 = 2e3;
|
|
2287
3119
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
2288
|
-
async function
|
|
3120
|
+
async function saveTemplate(cwd, run3) {
|
|
3121
|
+
const path = join12(cwd, WATCH_PATH);
|
|
3122
|
+
let config = {};
|
|
2289
3123
|
try {
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
3124
|
+
const parsed = JSON.parse(await readFile10(path, "utf8"));
|
|
3125
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
3126
|
+
config = parsed;
|
|
3127
|
+
}
|
|
2293
3128
|
} catch {
|
|
2294
|
-
return null;
|
|
2295
3129
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
2300
|
-
await writeFile8(path, `${JSON.stringify({ run: run3 }, null, 2)}
|
|
3130
|
+
config.run = run3;
|
|
3131
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
3132
|
+
await writeFile9(path, `${JSON.stringify(config, null, 2)}
|
|
2301
3133
|
`, "utf8");
|
|
2302
3134
|
}
|
|
2303
3135
|
function spawnAgent(command, args, cwd) {
|
|
@@ -2308,7 +3140,7 @@ function spawnAgent(command, args, cwd) {
|
|
|
2308
3140
|
settled = true;
|
|
2309
3141
|
resolve(outcome);
|
|
2310
3142
|
};
|
|
2311
|
-
const child =
|
|
3143
|
+
const child = spawn3(command, args, { cwd, stdio: "inherit" });
|
|
2312
3144
|
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
2313
3145
|
child.on(
|
|
2314
3146
|
"close",
|
|
@@ -2319,21 +3151,34 @@ function spawnAgent(command, args, cwd) {
|
|
|
2319
3151
|
});
|
|
2320
3152
|
}
|
|
2321
3153
|
async function runWatch(options, deps) {
|
|
2322
|
-
const saved = options.run === void 0 ? await
|
|
2323
|
-
const raw = options.run ?? saved;
|
|
2324
|
-
|
|
3154
|
+
const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
|
|
3155
|
+
const raw = options.run ?? saved.run;
|
|
3156
|
+
let template;
|
|
3157
|
+
let shownCommand2;
|
|
3158
|
+
let synthesizedAgent = null;
|
|
3159
|
+
if (raw !== null) {
|
|
3160
|
+
const parsed = parseTemplate(raw);
|
|
3161
|
+
if (!parsed.ok) {
|
|
3162
|
+
deps.error(parsed.error);
|
|
3163
|
+
return { exitCode: 1 };
|
|
3164
|
+
}
|
|
3165
|
+
template = parsed.template;
|
|
3166
|
+
shownCommand2 = raw;
|
|
3167
|
+
} else if (saved.agent !== null && saved.agent !== "custom") {
|
|
3168
|
+
const adapter = KNOWN_AGENTS[saved.agent];
|
|
3169
|
+
template = {
|
|
3170
|
+
command: adapter.binary,
|
|
3171
|
+
args: adapter.terminalArgs(PROMPT_TOKEN)
|
|
3172
|
+
};
|
|
3173
|
+
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
3174
|
+
synthesizedAgent = adapter.name;
|
|
3175
|
+
} else {
|
|
2325
3176
|
deps.error(
|
|
2326
|
-
'Watch needs an agent command the first time:
|
|
3177
|
+
'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
|
|
2327
3178
|
);
|
|
2328
3179
|
return { exitCode: 1 };
|
|
2329
3180
|
}
|
|
2330
|
-
|
|
2331
|
-
if (!parsed.ok) {
|
|
2332
|
-
deps.error(parsed.error);
|
|
2333
|
-
return { exitCode: 1 };
|
|
2334
|
-
}
|
|
2335
|
-
const template = parsed.template;
|
|
2336
|
-
if (options.run !== void 0) await saveTemplate(options.cwd, raw).catch(() => {
|
|
3181
|
+
if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
|
|
2337
3182
|
});
|
|
2338
3183
|
const base = `http://localhost:${options.port ?? DEFAULT_PORT}`;
|
|
2339
3184
|
const heartbeat = async (watching) => {
|
|
@@ -2347,11 +3192,15 @@ async function runWatch(options, deps) {
|
|
|
2347
3192
|
} catch {
|
|
2348
3193
|
}
|
|
2349
3194
|
};
|
|
2350
|
-
|
|
3195
|
+
if (synthesizedAgent !== null) {
|
|
3196
|
+
deps.log(`Using ${synthesizedAgent}, chosen in the interface.`);
|
|
3197
|
+
}
|
|
3198
|
+
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
2351
3199
|
deps.log("Stop with Ctrl-C.");
|
|
2352
3200
|
const failed = /* @__PURE__ */ new Set();
|
|
2353
3201
|
let stopped = false;
|
|
2354
3202
|
let busy = false;
|
|
3203
|
+
let announced = false;
|
|
2355
3204
|
let inflight = null;
|
|
2356
3205
|
const handle = async (request) => {
|
|
2357
3206
|
deps.log("");
|
|
@@ -2373,7 +3222,12 @@ async function runWatch(options, deps) {
|
|
|
2373
3222
|
};
|
|
2374
3223
|
const tick = async () => {
|
|
2375
3224
|
if (stopped) return;
|
|
2376
|
-
|
|
3225
|
+
if (announced) {
|
|
3226
|
+
void heartbeat(true);
|
|
3227
|
+
} else {
|
|
3228
|
+
await heartbeat(true);
|
|
3229
|
+
announced = true;
|
|
3230
|
+
}
|
|
2377
3231
|
if (busy) return;
|
|
2378
3232
|
busy = true;
|
|
2379
3233
|
try {
|
|
@@ -2390,7 +3244,7 @@ async function runWatch(options, deps) {
|
|
|
2390
3244
|
}
|
|
2391
3245
|
};
|
|
2392
3246
|
return new Promise((resolve) => {
|
|
2393
|
-
const timer = setInterval(() => void tick(),
|
|
3247
|
+
const timer = setInterval(() => void tick(), POLL_MS2);
|
|
2394
3248
|
const stop = () => {
|
|
2395
3249
|
if (stopped) return;
|
|
2396
3250
|
stopped = true;
|
|
@@ -2409,12 +3263,12 @@ async function runWatch(options, deps) {
|
|
|
2409
3263
|
|
|
2410
3264
|
// src/run-classify.ts
|
|
2411
3265
|
import { stat } from "fs/promises";
|
|
2412
|
-
import { join as
|
|
3266
|
+
import { join as join13 } from "path";
|
|
2413
3267
|
async function runClassify(options, deps) {
|
|
2414
3268
|
const declared = await Promise.all(
|
|
2415
3269
|
options.changes.map(async (change) => ({
|
|
2416
3270
|
...change,
|
|
2417
|
-
exists: await stat(
|
|
3271
|
+
exists: await stat(join13(options.cwd, change.path)).then(
|
|
2418
3272
|
() => true,
|
|
2419
3273
|
() => false
|
|
2420
3274
|
)
|
|
@@ -2442,14 +3296,14 @@ async function runClassify(options, deps) {
|
|
|
2442
3296
|
// src/run.ts
|
|
2443
3297
|
import { existsSync as existsSync5 } from "fs";
|
|
2444
3298
|
import { createRequire } from "module";
|
|
2445
|
-
import { basename as basename3, dirname as
|
|
3299
|
+
import { basename as basename3, dirname as dirname9, join as join14, relative as relative4 } from "path";
|
|
2446
3300
|
import { fileURLToPath } from "url";
|
|
2447
3301
|
function findShellDir() {
|
|
2448
|
-
const bundled =
|
|
2449
|
-
if (existsSync5(
|
|
3302
|
+
const bundled = join14(dirname9(fileURLToPath(import.meta.url)), "shell");
|
|
3303
|
+
if (existsSync5(join14(bundled, "index.html"))) return bundled;
|
|
2450
3304
|
try {
|
|
2451
3305
|
const require2 = createRequire(import.meta.url);
|
|
2452
|
-
return
|
|
3306
|
+
return dirname9(require2.resolve("@leglas/shell/dist/index.html"));
|
|
2453
3307
|
} catch {
|
|
2454
3308
|
return null;
|
|
2455
3309
|
}
|
|
@@ -2483,7 +3337,7 @@ async function run2(options, deps) {
|
|
|
2483
3337
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
2484
3338
|
for (const preview of merged?.previews ?? []) {
|
|
2485
3339
|
if (preview.file !== void 0) {
|
|
2486
|
-
const absolute =
|
|
3340
|
+
const absolute = join14(options.cwd, preview.file);
|
|
2487
3341
|
if (!existsSync5(absolute)) {
|
|
2488
3342
|
worktreeErrors.push(
|
|
2489
3343
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -2494,7 +3348,7 @@ async function run2(options, deps) {
|
|
|
2494
3348
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
2495
3349
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
2496
3350
|
}
|
|
2497
|
-
fileMounts.set(slug,
|
|
3351
|
+
fileMounts.set(slug, dirname9(absolute));
|
|
2498
3352
|
previews.push({
|
|
2499
3353
|
...preview,
|
|
2500
3354
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -2555,7 +3409,7 @@ async function run2(options, deps) {
|
|
|
2555
3409
|
})
|
|
2556
3410
|
);
|
|
2557
3411
|
} else {
|
|
2558
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
3412
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative4(options.cwd, loaded.path) || loaded.path;
|
|
2559
3413
|
deps.log(`Leglas ${url}`);
|
|
2560
3414
|
deps.log(
|
|
2561
3415
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|
|
@@ -2594,6 +3448,8 @@ async function run2(options, deps) {
|
|
|
2594
3448
|
export {
|
|
2595
3449
|
AGENTS_MARKER_END,
|
|
2596
3450
|
AGENTS_MARKER_START,
|
|
3451
|
+
DEFAULT_PORT,
|
|
3452
|
+
LEGLAS_PREFIX,
|
|
2597
3453
|
PROMPT_TOKEN,
|
|
2598
3454
|
WATCH_PATH,
|
|
2599
3455
|
baselineFrom,
|