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/bin.js
CHANGED
|
@@ -63,6 +63,7 @@ function parseAdd(rest) {
|
|
|
63
63
|
let branch;
|
|
64
64
|
let file;
|
|
65
65
|
let basedOn;
|
|
66
|
+
let askedFor;
|
|
66
67
|
const tags = [];
|
|
67
68
|
let json = false;
|
|
68
69
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -81,7 +82,9 @@ function parseAdd(rest) {
|
|
|
81
82
|
} else {
|
|
82
83
|
value = argument.slice(equals + 1);
|
|
83
84
|
}
|
|
84
|
-
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on"].includes(
|
|
85
|
+
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on", "--asked-for"].includes(
|
|
86
|
+
flag
|
|
87
|
+
)) {
|
|
85
88
|
return { kind: "error", message: `leglas add does not take ${flag}.` };
|
|
86
89
|
}
|
|
87
90
|
if (value === void 0 || value === "") {
|
|
@@ -93,6 +96,7 @@ function parseAdd(rest) {
|
|
|
93
96
|
else if (flag === "--branch") branch = value;
|
|
94
97
|
else if (flag === "--file") file = value;
|
|
95
98
|
else if (flag === "--based-on") basedOn = value;
|
|
99
|
+
else if (flag === "--asked-for") askedFor = value;
|
|
96
100
|
else tags.push(value);
|
|
97
101
|
}
|
|
98
102
|
if (title === void 0) {
|
|
@@ -106,7 +110,16 @@ function parseAdd(rest) {
|
|
|
106
110
|
}
|
|
107
111
|
return {
|
|
108
112
|
kind: "add",
|
|
109
|
-
preview: {
|
|
113
|
+
preview: {
|
|
114
|
+
title,
|
|
115
|
+
url,
|
|
116
|
+
note,
|
|
117
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
118
|
+
branch,
|
|
119
|
+
file,
|
|
120
|
+
basedOn,
|
|
121
|
+
askedFor
|
|
122
|
+
},
|
|
110
123
|
json
|
|
111
124
|
};
|
|
112
125
|
}
|
|
@@ -353,7 +366,7 @@ function parseArgs(argv) {
|
|
|
353
366
|
|
|
354
367
|
// src/run-classify.ts
|
|
355
368
|
import { stat } from "fs/promises";
|
|
356
|
-
import { join as
|
|
369
|
+
import { join as join10 } from "path";
|
|
357
370
|
|
|
358
371
|
// ../server/dist/config.js
|
|
359
372
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
@@ -448,6 +461,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
448
461
|
if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
|
|
449
462
|
errors.push(`${at} has a basedOn that is not a direction title.`);
|
|
450
463
|
}
|
|
464
|
+
const askedFor = entry["askedFor"];
|
|
465
|
+
if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
|
|
466
|
+
errors.push(`${at} has an askedFor that is not a change request.`);
|
|
467
|
+
}
|
|
451
468
|
const tags = entry["tags"];
|
|
452
469
|
previews.push({
|
|
453
470
|
title: typeof title === "string" ? title : "",
|
|
@@ -456,7 +473,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
456
473
|
tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
|
|
457
474
|
...typeof branch === "string" ? { branch } : {},
|
|
458
475
|
...typeof file === "string" ? { file } : {},
|
|
459
|
-
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {}
|
|
476
|
+
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
|
|
477
|
+
...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
|
|
460
478
|
});
|
|
461
479
|
});
|
|
462
480
|
const devCommand = source["devCommand"];
|
|
@@ -569,29 +587,39 @@ function nextRequest(requests, failed) {
|
|
|
569
587
|
|
|
570
588
|
// ../server/dist/agents.js
|
|
571
589
|
import { spawn } from "child_process";
|
|
572
|
-
import { constants } from "fs";
|
|
590
|
+
import { constants, readdirSync } from "fs";
|
|
573
591
|
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
574
592
|
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
593
|
+
var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
594
|
+
var effortFlag = (effort) => effort === null ? [] : ["--effort", effort];
|
|
595
|
+
var codexEffortConfig = (effort) => effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`];
|
|
596
|
+
var CODEX_WORKSPACE_CONFIG = [
|
|
597
|
+
"-c",
|
|
598
|
+
"sandbox_workspace_write.network_access=true"
|
|
599
|
+
];
|
|
575
600
|
var KNOWN_AGENTS = {
|
|
576
601
|
claude: {
|
|
577
602
|
name: "Claude",
|
|
578
603
|
binary: "claude",
|
|
579
|
-
|
|
604
|
+
efforts: AGENT_EFFORTS,
|
|
605
|
+
args: (prompt, effort = null) => [
|
|
580
606
|
"-p",
|
|
581
607
|
prompt,
|
|
582
608
|
"--output-format",
|
|
583
609
|
"stream-json",
|
|
584
610
|
"--verbose",
|
|
585
611
|
"--permission-mode",
|
|
586
|
-
"acceptEdits"
|
|
612
|
+
"acceptEdits",
|
|
613
|
+
...effortFlag(effort)
|
|
587
614
|
],
|
|
588
|
-
terminalArgs: (prompt) => [
|
|
615
|
+
terminalArgs: (prompt, effort = null) => [
|
|
589
616
|
"-p",
|
|
590
617
|
prompt,
|
|
591
618
|
"--permission-mode",
|
|
592
|
-
"acceptEdits"
|
|
619
|
+
"acceptEdits",
|
|
620
|
+
...effortFlag(effort)
|
|
593
621
|
],
|
|
594
|
-
resumeArgs: (sessionId, prompt) => [
|
|
622
|
+
resumeArgs: (sessionId, prompt, effort = null) => [
|
|
595
623
|
"-p",
|
|
596
624
|
"--resume",
|
|
597
625
|
sessionId,
|
|
@@ -600,8 +628,15 @@ var KNOWN_AGENTS = {
|
|
|
600
628
|
"stream-json",
|
|
601
629
|
"--verbose",
|
|
602
630
|
"--permission-mode",
|
|
603
|
-
"acceptEdits"
|
|
631
|
+
"acceptEdits",
|
|
632
|
+
...effortFlag(effort)
|
|
604
633
|
],
|
|
634
|
+
// Non-interactive Claude cannot approve a Bash call: acceptEdits covers
|
|
635
|
+
// files, so a command the prompt requires is refused every time with
|
|
636
|
+
// nobody there to say yes. This allows exactly that command and nothing
|
|
637
|
+
// wider. Codex needs no equivalent, because workspace-write already lets
|
|
638
|
+
// it run commands.
|
|
639
|
+
allowArgs: (command) => ["--allowedTools", `Bash(${command} *)`],
|
|
605
640
|
// Every stream-json event names its session.
|
|
606
641
|
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
607
642
|
authArgs: ["auth", "status"],
|
|
@@ -622,15 +657,45 @@ var KNOWN_AGENTS = {
|
|
|
622
657
|
codex: {
|
|
623
658
|
name: "Codex",
|
|
624
659
|
binary: "codex",
|
|
625
|
-
|
|
626
|
-
|
|
660
|
+
efforts: AGENT_EFFORTS,
|
|
661
|
+
// `--skip-git-repo-check` is what lets Codex run at all in a project the
|
|
662
|
+
// user never put under version control: without it codex-cli refuses
|
|
663
|
+
// before it reaches a model, with "Not inside a trusted directory and
|
|
664
|
+
// --skip-git-repo-check was not specified", and every Codex request in a
|
|
665
|
+
// non-git project fails for a reason nothing in Leglas explained. The flag
|
|
666
|
+
// moves that precondition and only that: `-s workspace-write` still
|
|
667
|
+
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
668
|
+
// and in a git repository the flag does nothing at all.
|
|
669
|
+
args: (prompt, effort = null) => [
|
|
670
|
+
"exec",
|
|
671
|
+
"--json",
|
|
672
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
673
|
+
...codexEffortConfig(effort),
|
|
674
|
+
"-s",
|
|
675
|
+
"workspace-write",
|
|
676
|
+
"--skip-git-repo-check",
|
|
677
|
+
prompt
|
|
678
|
+
],
|
|
679
|
+
terminalArgs: (prompt, effort = null) => [
|
|
680
|
+
"exec",
|
|
681
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
682
|
+
...codexEffortConfig(effort),
|
|
683
|
+
"-s",
|
|
684
|
+
"workspace-write",
|
|
685
|
+
"--skip-git-repo-check",
|
|
686
|
+
prompt
|
|
687
|
+
],
|
|
627
688
|
// No sandbox flag here: `codex exec resume` refuses it and inherits the
|
|
628
|
-
// session's own sandbox, which the first turn set to workspace-write.
|
|
629
|
-
|
|
689
|
+
// session's own sandbox, which the first turn set to workspace-write. The
|
|
690
|
+
// repository check is per invocation, so resume needs the flag of its own.
|
|
691
|
+
resumeArgs: (sessionId, prompt, effort = null) => [
|
|
630
692
|
"exec",
|
|
631
693
|
"resume",
|
|
632
694
|
sessionId,
|
|
633
695
|
"--json",
|
|
696
|
+
...CODEX_WORKSPACE_CONFIG,
|
|
697
|
+
...codexEffortConfig(effort),
|
|
698
|
+
"--skip-git-repo-check",
|
|
634
699
|
prompt
|
|
635
700
|
],
|
|
636
701
|
sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
|
|
@@ -641,8 +706,14 @@ var KNOWN_AGENTS = {
|
|
|
641
706
|
cursor: {
|
|
642
707
|
name: "Cursor",
|
|
643
708
|
binary: "cursor-agent",
|
|
644
|
-
|
|
645
|
-
|
|
709
|
+
efforts: [],
|
|
710
|
+
args: (prompt, _effort = null) => [
|
|
711
|
+
"-p",
|
|
712
|
+
prompt,
|
|
713
|
+
"--output-format",
|
|
714
|
+
"stream-json"
|
|
715
|
+
],
|
|
716
|
+
terminalArgs: (prompt, _effort = null) => ["-p", prompt],
|
|
646
717
|
authArgs: ["status"],
|
|
647
718
|
// UNVERIFIED: cursor-agent was not available on the build machine. The
|
|
648
719
|
// reading is deliberately loose, and anything ambiguous stays unknown.
|
|
@@ -656,11 +727,15 @@ var KNOWN_AGENTS = {
|
|
|
656
727
|
}
|
|
657
728
|
};
|
|
658
729
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
659
|
-
function execProbe(binary, args) {
|
|
730
|
+
function execProbe(binary, args, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
660
731
|
return new Promise((resolve) => {
|
|
661
732
|
let child;
|
|
662
733
|
try {
|
|
663
|
-
child = spawn(binary, [...args], {
|
|
734
|
+
child = spawn(binary, [...args], {
|
|
735
|
+
env: agentEnvironment(),
|
|
736
|
+
shell: false,
|
|
737
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
738
|
+
});
|
|
664
739
|
} catch {
|
|
665
740
|
return resolve(null);
|
|
666
741
|
}
|
|
@@ -669,7 +744,10 @@ function execProbe(binary, args) {
|
|
|
669
744
|
if (stdout.length < 4096)
|
|
670
745
|
stdout += chunk.toString();
|
|
671
746
|
});
|
|
672
|
-
const deadline = setTimeout(() =>
|
|
747
|
+
const deadline = setTimeout(() => {
|
|
748
|
+
child.kill("SIGKILL");
|
|
749
|
+
resolve(null);
|
|
750
|
+
}, timeoutMs);
|
|
673
751
|
child.once("error", () => {
|
|
674
752
|
clearTimeout(deadline);
|
|
675
753
|
resolve(null);
|
|
@@ -680,8 +758,50 @@ function execProbe(binary, args) {
|
|
|
680
758
|
});
|
|
681
759
|
});
|
|
682
760
|
}
|
|
761
|
+
function agentSearchPath(env = process.env, platform = process.platform) {
|
|
762
|
+
const home = env.HOME ?? env.USERPROFILE ?? "";
|
|
763
|
+
const npmPrefix = env.NPM_CONFIG_PREFIX;
|
|
764
|
+
const versionBins = (root, suffix) => {
|
|
765
|
+
try {
|
|
766
|
+
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(root, entry.name, ...suffix));
|
|
767
|
+
} catch {
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
const candidates = [
|
|
772
|
+
...(env.PATH ?? "").split(delimiter),
|
|
773
|
+
env.PNPM_HOME,
|
|
774
|
+
env.NVM_BIN,
|
|
775
|
+
env.BUN_INSTALL === void 0 ? void 0 : join(env.BUN_INSTALL, "bin"),
|
|
776
|
+
env.CARGO_HOME === void 0 ? void 0 : join(env.CARGO_HOME, "bin"),
|
|
777
|
+
npmPrefix === void 0 ? void 0 : platform === "win32" ? npmPrefix : join(npmPrefix, "bin"),
|
|
778
|
+
home === "" ? void 0 : join(home, ".local", "bin"),
|
|
779
|
+
home === "" ? void 0 : join(home, ".npm-global", "bin"),
|
|
780
|
+
home === "" ? void 0 : join(home, ".bun", "bin"),
|
|
781
|
+
home === "" ? void 0 : join(home, ".cargo", "bin"),
|
|
782
|
+
home === "" ? void 0 : join(home, ".volta", "bin"),
|
|
783
|
+
home === "" ? void 0 : join(home, ".asdf", "shims"),
|
|
784
|
+
home === "" ? void 0 : join(home, ".local", "share", "mise", "shims"),
|
|
785
|
+
home === "" ? void 0 : join(home, ".local", "share", "pnpm"),
|
|
786
|
+
home === "" ? void 0 : join(home, "Library", "pnpm"),
|
|
787
|
+
...home === "" ? [] : versionBins(join(home, ".nvm", "versions", "node"), ["bin"]),
|
|
788
|
+
...home === "" ? [] : versionBins(join(home, ".local", "share", "fnm", "node-versions"), [
|
|
789
|
+
"installation",
|
|
790
|
+
"bin"
|
|
791
|
+
]),
|
|
792
|
+
platform === "win32" ? env.APPDATA : void 0,
|
|
793
|
+
platform === "darwin" ? "/opt/homebrew/bin" : void 0,
|
|
794
|
+
platform === "darwin" ? "/usr/local/bin" : void 0,
|
|
795
|
+
platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
|
|
796
|
+
platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
|
|
797
|
+
].filter((entry) => typeof entry === "string" && entry !== "");
|
|
798
|
+
return [...new Set(candidates)].join(delimiter);
|
|
799
|
+
}
|
|
800
|
+
function agentEnvironment(env = process.env) {
|
|
801
|
+
return { ...env, PATH: agentSearchPath(env) };
|
|
802
|
+
}
|
|
683
803
|
async function pathLookup(binary) {
|
|
684
|
-
const entries = (
|
|
804
|
+
const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
|
|
685
805
|
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
686
806
|
for (const entry of entries) {
|
|
687
807
|
for (const extension of extensions) {
|
|
@@ -698,14 +818,22 @@ async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
|
|
|
698
818
|
const entries = Object.entries(KNOWN_AGENTS);
|
|
699
819
|
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
700
820
|
const available = await lookup(adapter.binary).catch(() => false);
|
|
701
|
-
if (!available)
|
|
702
|
-
return {
|
|
821
|
+
if (!available) {
|
|
822
|
+
return {
|
|
823
|
+
id,
|
|
824
|
+
name: adapter.name,
|
|
825
|
+
available,
|
|
826
|
+
auth: "unknown",
|
|
827
|
+
efforts: adapter.efforts
|
|
828
|
+
};
|
|
829
|
+
}
|
|
703
830
|
const result2 = await probe2(adapter.binary, adapter.authArgs).catch(() => null);
|
|
704
831
|
return {
|
|
705
832
|
id,
|
|
706
833
|
name: adapter.name,
|
|
707
834
|
available,
|
|
708
|
-
auth: result2 === null ? "unknown" : adapter.authVerdict(result2)
|
|
835
|
+
auth: result2 === null ? "unknown" : adapter.authVerdict(result2),
|
|
836
|
+
efforts: adapter.efforts
|
|
709
837
|
};
|
|
710
838
|
}));
|
|
711
839
|
}
|
|
@@ -738,10 +866,10 @@ function shownCommand(value) {
|
|
|
738
866
|
function claudeActivity(event, cwd) {
|
|
739
867
|
if (event.type !== "assistant")
|
|
740
868
|
return null;
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
869
|
+
const message2 = record(event.message);
|
|
870
|
+
if (message2 === null || !Array.isArray(message2.content))
|
|
743
871
|
return null;
|
|
744
|
-
for (const rawBlock of
|
|
872
|
+
for (const rawBlock of message2.content) {
|
|
745
873
|
const block = record(rawBlock);
|
|
746
874
|
if (block?.type !== "tool_use" || typeof block.name !== "string")
|
|
747
875
|
continue;
|
|
@@ -810,9 +938,31 @@ function sessionFrom(agent, line) {
|
|
|
810
938
|
return null;
|
|
811
939
|
return KNOWN_AGENTS[agent].sessionFrom(event);
|
|
812
940
|
}
|
|
941
|
+
function retryFrom(agent, line) {
|
|
942
|
+
if (agent !== "claude" && agent !== "cursor")
|
|
943
|
+
return null;
|
|
944
|
+
let event;
|
|
945
|
+
try {
|
|
946
|
+
event = record(JSON.parse(line));
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
if (event === null || event.type !== "system" || event.subtype !== "api_retry")
|
|
951
|
+
return null;
|
|
952
|
+
const attempt = typeof event.attempt === "number" ? event.attempt : 1;
|
|
953
|
+
return {
|
|
954
|
+
attempt,
|
|
955
|
+
max: typeof event.max_retries === "number" ? event.max_retries : null,
|
|
956
|
+
status: typeof event.error_status === "number" ? event.error_status : null,
|
|
957
|
+
reason: typeof event.error === "string" && event.error !== "" ? event.error.toLowerCase() : null
|
|
958
|
+
};
|
|
959
|
+
}
|
|
813
960
|
function isAgentChoice(value) {
|
|
814
961
|
return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
815
962
|
}
|
|
963
|
+
function isAgentEffort(value) {
|
|
964
|
+
return typeof value === "string" && AGENT_EFFORTS.includes(value);
|
|
965
|
+
}
|
|
816
966
|
async function readWatchConfig(cwd) {
|
|
817
967
|
try {
|
|
818
968
|
const parsed2 = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
|
|
@@ -823,14 +973,28 @@ async function readWatchConfig(cwd) {
|
|
|
823
973
|
}
|
|
824
974
|
async function readAgentChoice(cwd) {
|
|
825
975
|
const config = await readWatchConfig(cwd);
|
|
976
|
+
const agent = isAgentChoice(config.agent) ? config.agent : null;
|
|
977
|
+
const efforts = record(config.efforts);
|
|
826
978
|
return {
|
|
827
|
-
agent
|
|
979
|
+
agent,
|
|
980
|
+
effort: agent !== null && agent !== "custom" && isAgentEffort(efforts?.[agent]) ? efforts[agent] : null,
|
|
828
981
|
run: typeof config.run === "string" && config.run !== "" ? config.run : null
|
|
829
982
|
};
|
|
830
983
|
}
|
|
831
984
|
async function saveAgentChoice(cwd, choice) {
|
|
832
985
|
const config = await readWatchConfig(cwd);
|
|
833
986
|
config.agent = choice.agent;
|
|
987
|
+
if (choice.agent !== "custom" && choice.effort !== void 0) {
|
|
988
|
+
const efforts = record(config.efforts) ?? {};
|
|
989
|
+
if (choice.effort === null)
|
|
990
|
+
delete efforts[choice.agent];
|
|
991
|
+
else
|
|
992
|
+
efforts[choice.agent] = choice.effort;
|
|
993
|
+
if (Object.keys(efforts).length === 0)
|
|
994
|
+
delete config.efforts;
|
|
995
|
+
else
|
|
996
|
+
config.efforts = efforts;
|
|
997
|
+
}
|
|
834
998
|
if (choice.run !== void 0)
|
|
835
999
|
config.run = choice.run;
|
|
836
1000
|
const path = join(cwd, WATCH_PATH);
|
|
@@ -953,8 +1117,8 @@ async function loadConfig(cwd) {
|
|
|
953
1117
|
exported = module.default;
|
|
954
1118
|
}
|
|
955
1119
|
} catch (error) {
|
|
956
|
-
const
|
|
957
|
-
return { config: null, errors: [`${label} could not be loaded: ${
|
|
1120
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1121
|
+
return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
|
|
958
1122
|
}
|
|
959
1123
|
const result2 = normalizeConfig(exported);
|
|
960
1124
|
return {
|
|
@@ -1024,7 +1188,8 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1024
1188
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1025
1189
|
...input.branch === void 0 ? {} : { branch: input.branch },
|
|
1026
1190
|
...input.file === void 0 ? {} : { file: input.file },
|
|
1027
|
-
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
|
|
1191
|
+
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
|
|
1192
|
+
...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
|
|
1028
1193
|
};
|
|
1029
1194
|
const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
|
|
1030
1195
|
if (check.config === null) {
|
|
@@ -1256,17 +1421,220 @@ async function startAppProcess(options) {
|
|
|
1256
1421
|
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.`);
|
|
1257
1422
|
}
|
|
1258
1423
|
|
|
1259
|
-
// ../server/dist/
|
|
1260
|
-
|
|
1424
|
+
// ../server/dist/failure.js
|
|
1425
|
+
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
1426
|
+
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
1427
|
+
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;
|
|
1428
|
+
var LIMIT = /\b429\b|rate limit|usage limit|quota exceeded|too many requests/i;
|
|
1429
|
+
var OVERLOADED = /\b(?:503|529)\b|overloaded|service unavailable/i;
|
|
1430
|
+
function fromStatus(status, reason) {
|
|
1431
|
+
if (status === 401 || status === 403 || reason === "authentication_failed")
|
|
1432
|
+
return "not-signed-in";
|
|
1433
|
+
if (status === 429 || reason === "rate_limit")
|
|
1434
|
+
return "provider-limit";
|
|
1435
|
+
if (status === 529 || status === 503 || reason === "overloaded")
|
|
1436
|
+
return "provider-overloaded";
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
function fromLines(lines) {
|
|
1440
|
+
for (const line of [...lines].reverse()) {
|
|
1441
|
+
if (NEEDS_TRUST.test(line))
|
|
1442
|
+
return "needs-trust";
|
|
1443
|
+
if (NOT_SIGNED_IN.test(line))
|
|
1444
|
+
return "not-signed-in";
|
|
1445
|
+
if (LIMIT.test(line))
|
|
1446
|
+
return "provider-limit";
|
|
1447
|
+
if (OVERLOADED.test(line))
|
|
1448
|
+
return "provider-overloaded";
|
|
1449
|
+
}
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
function attempts(retry) {
|
|
1453
|
+
if (retry === null || retry === void 0)
|
|
1454
|
+
return "";
|
|
1455
|
+
const total = retry.max === null ? retry.attempt : Math.max(retry.attempt, retry.max);
|
|
1456
|
+
return ` It retried ${total} times first.`;
|
|
1457
|
+
}
|
|
1458
|
+
function message(code, input) {
|
|
1459
|
+
const agent = input.agent;
|
|
1460
|
+
switch (code) {
|
|
1461
|
+
case "cancelled":
|
|
1462
|
+
return "You stopped this run.";
|
|
1463
|
+
case "stopped":
|
|
1464
|
+
return "Leglas shut down while this was running.";
|
|
1465
|
+
case "missing-agent":
|
|
1466
|
+
return `${agent} could not be started. Its command is not on this machine's PATH any more.`;
|
|
1467
|
+
case "not-signed-in":
|
|
1468
|
+
return `${agent} is not signed in. Sign in to it in a terminal, then run this again.`;
|
|
1469
|
+
case "provider-overloaded":
|
|
1470
|
+
return `${agent}'s provider was overloaded and gave up.${attempts(input.retry)}`;
|
|
1471
|
+
case "provider-limit":
|
|
1472
|
+
return `${agent} reported a rate or usage limit, so nothing ran.`;
|
|
1473
|
+
case "needs-trust":
|
|
1474
|
+
return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
|
|
1475
|
+
case "not-registered":
|
|
1476
|
+
return `${agent} finished without registering the new direction, so nothing reached the rail. Its last output is in the Leglas terminal.`;
|
|
1477
|
+
case "agent-error":
|
|
1478
|
+
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.`;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function classifyFailure(input) {
|
|
1482
|
+
const lines = input.lines ?? [];
|
|
1483
|
+
const error = input.error ?? null;
|
|
1484
|
+
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";
|
|
1485
|
+
return { code, message: message(code, input) };
|
|
1486
|
+
}
|
|
1487
|
+
function sessionShaped(code) {
|
|
1488
|
+
return code === "agent-error";
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ../server/dist/annotations.js
|
|
1261
1492
|
import { randomBytes } from "crypto";
|
|
1493
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1262
1494
|
import { dirname as dirname4, join as join5 } from "path";
|
|
1495
|
+
var ANNOTATIONS_PATH = ".leglas/annotations.json";
|
|
1496
|
+
var NOTE_CAP = 500;
|
|
1497
|
+
var SELECTOR_CAP = 300;
|
|
1498
|
+
var TEXT_CAP = 120;
|
|
1499
|
+
var TAG_CAP = 40;
|
|
1500
|
+
var CLASS_CAP = 8;
|
|
1501
|
+
var CLASS_LENGTH_CAP = 60;
|
|
1502
|
+
var COVERS_CAP = 8;
|
|
1503
|
+
function isRecord2(value) {
|
|
1504
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1505
|
+
}
|
|
1506
|
+
function text(value, cap) {
|
|
1507
|
+
return typeof value === "string" ? value.trim().slice(0, cap) : "";
|
|
1508
|
+
}
|
|
1509
|
+
function fraction(value) {
|
|
1510
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1511
|
+
return 0.5;
|
|
1512
|
+
return Math.min(1, Math.max(0, value));
|
|
1513
|
+
}
|
|
1514
|
+
function size(value) {
|
|
1515
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
|
|
1516
|
+
}
|
|
1517
|
+
function anchorFrom(value) {
|
|
1518
|
+
if (!isRecord2(value))
|
|
1519
|
+
return null;
|
|
1520
|
+
const selector = text(value["selector"], SELECTOR_CAP);
|
|
1521
|
+
if (selector === "")
|
|
1522
|
+
return null;
|
|
1523
|
+
const rect = isRecord2(value["rect"]) ? value["rect"] : {};
|
|
1524
|
+
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)) : [];
|
|
1525
|
+
const rawRegion = isRecord2(value["region"]) ? value["region"] : null;
|
|
1526
|
+
const region = rawRegion === null ? null : {
|
|
1527
|
+
height: fraction(rawRegion["height"]),
|
|
1528
|
+
width: fraction(rawRegion["width"]),
|
|
1529
|
+
x: fraction(rawRegion["x"]),
|
|
1530
|
+
y: fraction(rawRegion["y"])
|
|
1531
|
+
};
|
|
1532
|
+
const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
|
|
1533
|
+
tag: text(entry["tag"], TAG_CAP) || "element",
|
|
1534
|
+
text: text(entry["text"], TEXT_CAP)
|
|
1535
|
+
})) : [];
|
|
1536
|
+
return {
|
|
1537
|
+
classes,
|
|
1538
|
+
...covers.length === 0 ? {} : { covers },
|
|
1539
|
+
...region === null ? {} : { region },
|
|
1540
|
+
rect: {
|
|
1541
|
+
height: size(rect["height"]),
|
|
1542
|
+
width: size(rect["width"]),
|
|
1543
|
+
x: size(rect["x"]),
|
|
1544
|
+
y: size(rect["y"])
|
|
1545
|
+
},
|
|
1546
|
+
selector,
|
|
1547
|
+
spot: {
|
|
1548
|
+
x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
|
|
1549
|
+
y: fraction(isRecord2(value["spot"]) ? value["spot"]["y"] : void 0)
|
|
1550
|
+
},
|
|
1551
|
+
tag: text(value["tag"], TAG_CAP) || "element",
|
|
1552
|
+
text: text(value["text"], TEXT_CAP),
|
|
1553
|
+
viewport: size(value["viewport"])
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
async function readAnnotations(cwd) {
|
|
1557
|
+
try {
|
|
1558
|
+
const raw = await readFile4(join5(cwd, ANNOTATIONS_PATH), "utf8");
|
|
1559
|
+
const parsed2 = JSON.parse(raw);
|
|
1560
|
+
if (!Array.isArray(parsed2.annotations))
|
|
1561
|
+
return [];
|
|
1562
|
+
return parsed2.annotations.flatMap((entry, index) => {
|
|
1563
|
+
if (!isRecord2(entry))
|
|
1564
|
+
return [];
|
|
1565
|
+
const anchor = anchorFrom(entry["anchor"]);
|
|
1566
|
+
const title = text(entry["title"], TAG_CAP * 4);
|
|
1567
|
+
if (anchor === null || title === "")
|
|
1568
|
+
return [];
|
|
1569
|
+
return [
|
|
1570
|
+
{
|
|
1571
|
+
anchor,
|
|
1572
|
+
id: typeof entry["id"] === "string" ? entry["id"] : String(index),
|
|
1573
|
+
note: text(entry["note"], NOTE_CAP),
|
|
1574
|
+
title
|
|
1575
|
+
}
|
|
1576
|
+
];
|
|
1577
|
+
});
|
|
1578
|
+
} catch {
|
|
1579
|
+
return [];
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
async function write(cwd, annotations) {
|
|
1583
|
+
const path = join5(cwd, ANNOTATIONS_PATH);
|
|
1584
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1585
|
+
await writeFile3(path, `${JSON.stringify({ annotations }, null, 2)}
|
|
1586
|
+
`, "utf8");
|
|
1587
|
+
}
|
|
1588
|
+
async function addAnnotation(cwd, input) {
|
|
1589
|
+
const annotation = { ...input, id: randomBytes(6).toString("base64url") };
|
|
1590
|
+
await write(cwd, [...await readAnnotations(cwd), annotation]);
|
|
1591
|
+
return annotation;
|
|
1592
|
+
}
|
|
1593
|
+
async function removeAnnotations(cwd, ids) {
|
|
1594
|
+
const wanted = new Set(ids);
|
|
1595
|
+
const annotations = await readAnnotations(cwd);
|
|
1596
|
+
const remaining = annotations.filter((entry) => !wanted.has(entry.id));
|
|
1597
|
+
const dropped = annotations.length - remaining.length;
|
|
1598
|
+
if (dropped > 0)
|
|
1599
|
+
await write(cwd, remaining);
|
|
1600
|
+
return dropped;
|
|
1601
|
+
}
|
|
1602
|
+
function annotationsFor(annotations, title) {
|
|
1603
|
+
return annotations.filter((entry) => entry.title === title);
|
|
1604
|
+
}
|
|
1605
|
+
function describeAnchor(anchor) {
|
|
1606
|
+
const where = `about ${anchor.rect.width}\xD7${anchor.rect.height} at (${anchor.rect.x}, ${anchor.rect.y}) in a ${anchor.viewport}px-wide viewport`;
|
|
1607
|
+
if (anchor.region !== void 0) {
|
|
1608
|
+
const covered = (anchor.covers ?? []).map((entry) => entry.text === "" ? `<${entry.tag}>` : `<${entry.tag}> \u201C${entry.text}\u201D`).join(", ");
|
|
1609
|
+
const inside = covered === "" ? "" : ` covering ${covered};`;
|
|
1610
|
+
return `an area inside <${anchor.tag}>;${inside} path ${anchor.selector}; ${where}`;
|
|
1611
|
+
}
|
|
1612
|
+
const parts = [`<${anchor.tag}>`];
|
|
1613
|
+
if (anchor.classes.length > 0)
|
|
1614
|
+
parts.push(`class "${anchor.classes.join(" ")}"`);
|
|
1615
|
+
if (anchor.text !== "")
|
|
1616
|
+
parts.push(`reading \u201C${anchor.text}\u201D`);
|
|
1617
|
+
return `${parts.join(", ")}; path ${anchor.selector}; ${where}`;
|
|
1618
|
+
}
|
|
1619
|
+
function describeAnnotations(annotations) {
|
|
1620
|
+
return annotations.map((annotation, index) => {
|
|
1621
|
+
const said = annotation.note === "" ? "Look at this." : annotation.note;
|
|
1622
|
+
return `${index + 1}. ${said}
|
|
1623
|
+
The element: ${describeAnchor(annotation.anchor)}`;
|
|
1624
|
+
}).join("\n\n");
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// ../server/dist/requests.js
|
|
1628
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1629
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1630
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1263
1631
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1264
|
-
function
|
|
1632
|
+
function variantSlot(url) {
|
|
1265
1633
|
if (!url.startsWith("/"))
|
|
1266
1634
|
return null;
|
|
1267
|
-
const query = url.slice(url.indexOf("?") + 1);
|
|
1268
1635
|
if (!url.includes("?"))
|
|
1269
1636
|
return null;
|
|
1637
|
+
const query = url.slice(url.indexOf("?") + 1);
|
|
1270
1638
|
for (const pair of query.split("&")) {
|
|
1271
1639
|
const [rawKey, rawValue] = pair.split("=");
|
|
1272
1640
|
if (rawKey === void 0 || rawValue === void 0)
|
|
@@ -1277,37 +1645,125 @@ function targetFor(url) {
|
|
|
1277
1645
|
const option = decodeURIComponent(rawValue);
|
|
1278
1646
|
if (!SAFE_SEGMENT.test(surface) || !SAFE_SEGMENT.test(option))
|
|
1279
1647
|
return null;
|
|
1280
|
-
return
|
|
1648
|
+
return { surface, option };
|
|
1281
1649
|
}
|
|
1282
1650
|
return null;
|
|
1283
1651
|
}
|
|
1284
|
-
function
|
|
1652
|
+
function targetFor(url) {
|
|
1653
|
+
const slot = variantSlot(url);
|
|
1654
|
+
return slot === null ? null : `.leglas/variants/${slot.surface}/${slot.option}.tsx`;
|
|
1655
|
+
}
|
|
1656
|
+
function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx -y leglas") {
|
|
1285
1657
|
const target = preview.file ?? targetFor(preview.url);
|
|
1286
1658
|
const cleaned = intent.trim();
|
|
1659
|
+
const asked = changeBlock(cleaned, notes);
|
|
1660
|
+
const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
|
|
1661
|
+
const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand) : replacePrompt(preview, asked, target);
|
|
1662
|
+
return { prompt, target, mode };
|
|
1663
|
+
}
|
|
1664
|
+
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.`;
|
|
1665
|
+
function changeBlock(cleaned, notes) {
|
|
1666
|
+
if (notes.length === 0)
|
|
1667
|
+
return `What to change: ${cleaned}`;
|
|
1668
|
+
const many = notes.length === 1 ? "a note" : `${notes.length} notes`;
|
|
1669
|
+
const lead = cleaned === "" ? `What to change, left as ${many} on the design itself:` : `What to change: ${cleaned}
|
|
1670
|
+
|
|
1671
|
+
And ${many} left on the design itself:`;
|
|
1672
|
+
return `${lead}
|
|
1673
|
+
|
|
1674
|
+
${describeAnnotations(notes)}
|
|
1675
|
+
|
|
1676
|
+
${ANCHORS}`;
|
|
1677
|
+
}
|
|
1678
|
+
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.
|
|
1679
|
+
|
|
1680
|
+
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.
|
|
1681
|
+
|
|
1682
|
+
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.`;
|
|
1683
|
+
function registrationCommand(leglasCommand) {
|
|
1684
|
+
return `${leglasCommand} add`;
|
|
1685
|
+
}
|
|
1686
|
+
function replacePrompt(preview, asked, target) {
|
|
1287
1687
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1288
1688
|
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
1289
|
-
|
|
1689
|
+
return `In this project, change only the "${preview.title}" design direction. ${where}
|
|
1690
|
+
|
|
1691
|
+
${asked}
|
|
1290
1692
|
|
|
1291
|
-
|
|
1693
|
+
${pace}${SCOPE} The direction is already registered, so nothing needs re-registering.`;
|
|
1694
|
+
}
|
|
1695
|
+
function variantPrompt(preview, recorded, asked, target, leglasCommand) {
|
|
1696
|
+
const slot = variantSlot(preview.url);
|
|
1697
|
+
const parent = JSON.stringify(preview.title);
|
|
1698
|
+
const askedFor = JSON.stringify(recorded);
|
|
1699
|
+
const add = registrationCommand(leglasCommand);
|
|
1700
|
+
const source = target === null ? `Find what renders it first.` : `Its source is ${target}.`;
|
|
1701
|
+
const [make, register] = preview.file !== void 0 ? [
|
|
1702
|
+
`Copy that file to a new file beside it and make the change in the copy.`,
|
|
1703
|
+
` ${add} --title "<name>" --file "<the new file>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1704
|
+
] : slot !== null ? [
|
|
1705
|
+
`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.`,
|
|
1706
|
+
` ${add} --title "<name>" --url "/?v-${slot.surface}=<key>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1707
|
+
] : [
|
|
1708
|
+
`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.`,
|
|
1709
|
+
` ${add} --title "<name>" --url "<the URL that shows it>" --based-on ${parent} --note "<what this direction is, one line>" --asked-for ${askedFor}`
|
|
1710
|
+
];
|
|
1711
|
+
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.
|
|
1712
|
+
|
|
1713
|
+
${source} ${make}
|
|
1714
|
+
|
|
1715
|
+
${asked}
|
|
1716
|
+
|
|
1717
|
+
Then register it, which is what puts it on the rail:
|
|
1718
|
+
|
|
1719
|
+
${register}
|
|
1292
1720
|
|
|
1293
|
-
|
|
1721
|
+
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.
|
|
1294
1722
|
|
|
1295
|
-
|
|
1296
|
-
return { prompt, target };
|
|
1723
|
+
${SCOPE}`;
|
|
1297
1724
|
}
|
|
1298
1725
|
var REQUESTS_PATH = ".leglas/requests.json";
|
|
1726
|
+
var TERMINAL = ["failed", "cancelled"];
|
|
1727
|
+
function isTerminal(status) {
|
|
1728
|
+
return TERMINAL.includes(status);
|
|
1729
|
+
}
|
|
1730
|
+
var FAILURE_CODES = [
|
|
1731
|
+
"cancelled",
|
|
1732
|
+
"stopped",
|
|
1733
|
+
"missing-agent",
|
|
1734
|
+
"not-signed-in",
|
|
1735
|
+
"provider-overloaded",
|
|
1736
|
+
"provider-limit",
|
|
1737
|
+
"needs-trust",
|
|
1738
|
+
"not-registered",
|
|
1739
|
+
"agent-error"
|
|
1740
|
+
];
|
|
1741
|
+
function failureOf(value) {
|
|
1742
|
+
if (typeof value !== "object" || value === null)
|
|
1743
|
+
return null;
|
|
1744
|
+
const entry = value;
|
|
1745
|
+
if (typeof entry.message !== "string" || entry.message === "")
|
|
1746
|
+
return null;
|
|
1747
|
+
if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
|
|
1748
|
+
return null;
|
|
1749
|
+
return { code: entry.code, message: entry.message };
|
|
1750
|
+
}
|
|
1299
1751
|
async function readRequests(cwd) {
|
|
1300
1752
|
try {
|
|
1301
|
-
const raw = await
|
|
1753
|
+
const raw = await readFile5(join6(cwd, REQUESTS_PATH), "utf8");
|
|
1302
1754
|
const parsed2 = JSON.parse(raw);
|
|
1303
1755
|
if (!Array.isArray(parsed2.requests))
|
|
1304
1756
|
return [];
|
|
1305
1757
|
return parsed2.requests.map((request, index) => {
|
|
1306
|
-
const entry = request;
|
|
1758
|
+
const { failure: rawFailure, ...entry } = request;
|
|
1759
|
+
const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
|
|
1760
|
+
const failure = isTerminal(status) ? failureOf(rawFailure) : null;
|
|
1307
1761
|
return {
|
|
1308
1762
|
...entry,
|
|
1309
1763
|
id: typeof entry.id === "string" ? entry.id : String(index),
|
|
1310
|
-
status
|
|
1764
|
+
status,
|
|
1765
|
+
mode: entry.mode === "variant" ? "variant" : "replace",
|
|
1766
|
+
...failure === null ? {} : { failure }
|
|
1311
1767
|
};
|
|
1312
1768
|
});
|
|
1313
1769
|
} catch {
|
|
@@ -1315,23 +1771,23 @@ async function readRequests(cwd) {
|
|
|
1315
1771
|
}
|
|
1316
1772
|
}
|
|
1317
1773
|
async function writeQueue(cwd, requests) {
|
|
1318
|
-
const path =
|
|
1319
|
-
await
|
|
1320
|
-
await
|
|
1774
|
+
const path = join6(cwd, REQUESTS_PATH);
|
|
1775
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
1776
|
+
await writeFile4(path, `${JSON.stringify({ requests }, null, 2)}
|
|
1321
1777
|
`, "utf8");
|
|
1322
1778
|
}
|
|
1323
1779
|
async function appendRequest(cwd, request) {
|
|
1324
1780
|
await writeQueue(cwd, [
|
|
1325
1781
|
...await readRequests(cwd),
|
|
1326
|
-
{ ...request, id:
|
|
1782
|
+
{ ...request, id: randomBytes2(6).toString("base64url"), status: "queued" }
|
|
1327
1783
|
]);
|
|
1328
1784
|
}
|
|
1329
1785
|
async function collectRequests(cwd) {
|
|
1330
1786
|
const requests = await readRequests(cwd);
|
|
1331
|
-
const collected = requests.map((request) => ({ ...request, status: "picked-up" })
|
|
1332
|
-
if (requests.some((request) => request.status
|
|
1787
|
+
const collected = requests.map((request) => isTerminal(request.status) ? request : { ...request, status: "picked-up" });
|
|
1788
|
+
if (requests.some((request) => request.status === "queued"))
|
|
1333
1789
|
await writeQueue(cwd, collected);
|
|
1334
|
-
return collected;
|
|
1790
|
+
return collected.filter((request) => !isTerminal(request.status));
|
|
1335
1791
|
}
|
|
1336
1792
|
async function markPickedUp(cwd, id) {
|
|
1337
1793
|
const requests = await readRequests(cwd);
|
|
@@ -1340,6 +1796,17 @@ async function markPickedUp(cwd, id) {
|
|
|
1340
1796
|
await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
|
|
1341
1797
|
return true;
|
|
1342
1798
|
}
|
|
1799
|
+
async function markFailed(cwd, id, failure) {
|
|
1800
|
+
const requests = await readRequests(cwd);
|
|
1801
|
+
if (!requests.some((request) => request.id === id))
|
|
1802
|
+
return false;
|
|
1803
|
+
await writeQueue(cwd, requests.map((request) => request.id === id ? {
|
|
1804
|
+
...request,
|
|
1805
|
+
status: failure.code === "cancelled" ? "cancelled" : "failed",
|
|
1806
|
+
failure
|
|
1807
|
+
} : request));
|
|
1808
|
+
return true;
|
|
1809
|
+
}
|
|
1343
1810
|
async function removeRequest(cwd, id) {
|
|
1344
1811
|
const requests = await readRequests(cwd);
|
|
1345
1812
|
const remaining = requests.filter((request) => request.id !== id);
|
|
@@ -1350,7 +1817,7 @@ async function removeRequest(cwd, id) {
|
|
|
1350
1817
|
}
|
|
1351
1818
|
async function clearRequests(cwd) {
|
|
1352
1819
|
const requests = await readRequests(cwd);
|
|
1353
|
-
const pending = requests.filter((request) => request.status
|
|
1820
|
+
const pending = requests.filter((request) => request.status === "queued");
|
|
1354
1821
|
const cleared = requests.length - pending.length;
|
|
1355
1822
|
if (cleared > 0)
|
|
1356
1823
|
await writeQueue(cwd, pending);
|
|
@@ -1359,10 +1826,13 @@ async function clearRequests(cwd) {
|
|
|
1359
1826
|
|
|
1360
1827
|
// ../server/dist/runner.js
|
|
1361
1828
|
import { spawn as nodeSpawn } from "child_process";
|
|
1829
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1830
|
+
import { join as join7 } from "path";
|
|
1362
1831
|
var POLL_MS = 2e3;
|
|
1363
1832
|
var OUTPUT_LINES = 20;
|
|
1833
|
+
var CANCEL_GRACE_MS = 5e3;
|
|
1364
1834
|
var SESSION_TURNS_CAP = 8;
|
|
1365
|
-
function resolveCommand(choice, prompt, sessionId = null) {
|
|
1835
|
+
function resolveCommand(choice, prompt, sessionId = null, registration = null) {
|
|
1366
1836
|
if (choice.agent === null)
|
|
1367
1837
|
return null;
|
|
1368
1838
|
if (choice.agent === "custom") {
|
|
@@ -1374,12 +1844,13 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1374
1844
|
return { agent: "custom", name: "Custom", ...commandFor(parsed2.template, prompt), resumed: false };
|
|
1375
1845
|
}
|
|
1376
1846
|
const adapter = KNOWN_AGENTS[choice.agent];
|
|
1847
|
+
const allow = registration !== null && "allowArgs" in adapter ? adapter.allowArgs(registration) : [];
|
|
1377
1848
|
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1378
1849
|
return {
|
|
1379
1850
|
agent: choice.agent,
|
|
1380
1851
|
name: adapter.name,
|
|
1381
1852
|
command: adapter.binary,
|
|
1382
|
-
args: adapter.resumeArgs(sessionId, prompt),
|
|
1853
|
+
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort), ...allow],
|
|
1383
1854
|
resumed: true
|
|
1384
1855
|
};
|
|
1385
1856
|
}
|
|
@@ -1387,7 +1858,7 @@ function resolveCommand(choice, prompt, sessionId = null) {
|
|
|
1387
1858
|
agent: choice.agent,
|
|
1388
1859
|
name: adapter.name,
|
|
1389
1860
|
command: adapter.binary,
|
|
1390
|
-
args: adapter.args(prompt),
|
|
1861
|
+
args: [...adapter.args(prompt, choice.effort), ...allow],
|
|
1391
1862
|
resumed: false
|
|
1392
1863
|
};
|
|
1393
1864
|
}
|
|
@@ -1410,19 +1881,24 @@ function lineReader(stream, onLine) {
|
|
|
1410
1881
|
return flush;
|
|
1411
1882
|
}
|
|
1412
1883
|
function defaultSpawn(command, args, options) {
|
|
1413
|
-
return nodeSpawn(command, args, options);
|
|
1884
|
+
return nodeSpawn(command, args, { ...options, env: agentEnvironment() });
|
|
1414
1885
|
}
|
|
1415
1886
|
function startRunner(options) {
|
|
1416
1887
|
const spawn5 = options.spawn ?? defaultSpawn;
|
|
1417
1888
|
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1418
1889
|
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1419
1890
|
const failed = /* @__PURE__ */ new Set();
|
|
1891
|
+
const setLater = options.setTimeout ?? ((callback, milliseconds) => {
|
|
1892
|
+
setTimeout(callback, milliseconds).unref?.();
|
|
1893
|
+
});
|
|
1420
1894
|
let state = {
|
|
1421
1895
|
running: false,
|
|
1422
1896
|
requestId: null,
|
|
1423
1897
|
agent: null,
|
|
1424
1898
|
activity: null,
|
|
1425
|
-
startedAt: null
|
|
1899
|
+
startedAt: null,
|
|
1900
|
+
stopping: false,
|
|
1901
|
+
waiting: null
|
|
1426
1902
|
};
|
|
1427
1903
|
let stopped = false;
|
|
1428
1904
|
let ticking = null;
|
|
@@ -1430,15 +1906,30 @@ function startRunner(options) {
|
|
|
1430
1906
|
let active = null;
|
|
1431
1907
|
const sessions = /* @__PURE__ */ new Map();
|
|
1432
1908
|
const idle = () => {
|
|
1433
|
-
state = {
|
|
1909
|
+
state = {
|
|
1910
|
+
running: false,
|
|
1911
|
+
requestId: null,
|
|
1912
|
+
agent: null,
|
|
1913
|
+
activity: null,
|
|
1914
|
+
startedAt: null,
|
|
1915
|
+
stopping: false,
|
|
1916
|
+
waiting: null
|
|
1917
|
+
};
|
|
1434
1918
|
};
|
|
1435
1919
|
const rememberLine = (lines, line) => {
|
|
1436
1920
|
lines.push(line);
|
|
1437
1921
|
if (lines.length > OUTPUT_LINES)
|
|
1438
1922
|
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1439
1923
|
};
|
|
1440
|
-
const reportFailure = (request,
|
|
1441
|
-
|
|
1924
|
+
const reportFailure = async (request, failure, lines) => {
|
|
1925
|
+
await markFailed(options.cwd, request.id, failure).catch(() => {
|
|
1926
|
+
});
|
|
1927
|
+
failed.add(request.id);
|
|
1928
|
+
if (failure.code === "cancelled") {
|
|
1929
|
+
console.error(`Leglas stopped the run for ${request.title}.`);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
console.error(`Leglas agent failed for ${request.title}: ${failure.message}`);
|
|
1442
1933
|
for (const line of lines)
|
|
1443
1934
|
console.error(` ${line}`);
|
|
1444
1935
|
};
|
|
@@ -1456,19 +1947,31 @@ function startRunner(options) {
|
|
|
1456
1947
|
error: error instanceof Error ? error.message : String(error)
|
|
1457
1948
|
});
|
|
1458
1949
|
}
|
|
1459
|
-
const current = {
|
|
1950
|
+
const current = {
|
|
1951
|
+
child,
|
|
1952
|
+
requestId: request.id,
|
|
1953
|
+
cancelled: false,
|
|
1954
|
+
abandon: () => {
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1460
1957
|
active = current;
|
|
1461
1958
|
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1462
1959
|
rememberLine(lines, line);
|
|
1463
1960
|
const sessionId = sessionFrom(resolved.agent, line);
|
|
1464
1961
|
if (sessionId !== null)
|
|
1465
1962
|
observed.sessionId = sessionId;
|
|
1963
|
+
const retry = retryFrom(resolved.agent, line);
|
|
1964
|
+
if (retry !== null) {
|
|
1965
|
+
observed.retry = retry;
|
|
1966
|
+
if (active === current)
|
|
1967
|
+
state = { ...state, waiting: retry };
|
|
1968
|
+
}
|
|
1466
1969
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1467
1970
|
if (activity !== null) {
|
|
1468
1971
|
if (activity.startsWith("editing"))
|
|
1469
1972
|
observed.edited = true;
|
|
1470
1973
|
if (active === current)
|
|
1471
|
-
state = { ...state, activity };
|
|
1974
|
+
state = { ...state, activity, waiting: null };
|
|
1472
1975
|
}
|
|
1473
1976
|
});
|
|
1474
1977
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
@@ -1482,6 +1985,7 @@ function startRunner(options) {
|
|
|
1482
1985
|
stderrFlush();
|
|
1483
1986
|
resolve(outcome);
|
|
1484
1987
|
};
|
|
1988
|
+
current.abandon = () => settle({ ok: false, error: "cancelled" });
|
|
1485
1989
|
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1486
1990
|
child.once("close", (code, signal) => {
|
|
1487
1991
|
if (current.cancelled)
|
|
@@ -1495,10 +1999,12 @@ function startRunner(options) {
|
|
|
1495
1999
|
active = null;
|
|
1496
2000
|
});
|
|
1497
2001
|
};
|
|
2002
|
+
const registered = () => readFile6(join7(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
1498
2003
|
const handle = async (request, choice) => {
|
|
1499
2004
|
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1500
2005
|
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1501
|
-
|
|
2006
|
+
const registration = request.mode === "variant" && options.leglasCommand !== void 0 ? registrationCommand(options.leglasCommand) : null;
|
|
2007
|
+
let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null, registration);
|
|
1502
2008
|
if (resolved === null)
|
|
1503
2009
|
return;
|
|
1504
2010
|
const lines = [];
|
|
@@ -1506,7 +2012,7 @@ function startRunner(options) {
|
|
|
1506
2012
|
if (!await markPickedUp(options.cwd, request.id))
|
|
1507
2013
|
return;
|
|
1508
2014
|
if (stopped) {
|
|
1509
|
-
|
|
2015
|
+
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
1510
2016
|
return;
|
|
1511
2017
|
}
|
|
1512
2018
|
state = {
|
|
@@ -1514,25 +2020,47 @@ function startRunner(options) {
|
|
|
1514
2020
|
requestId: request.id,
|
|
1515
2021
|
agent: resolved.name,
|
|
1516
2022
|
activity: null,
|
|
1517
|
-
startedAt: Date.now()
|
|
2023
|
+
startedAt: Date.now(),
|
|
2024
|
+
stopping: false,
|
|
2025
|
+
waiting: null
|
|
2026
|
+
};
|
|
2027
|
+
const observed = {
|
|
2028
|
+
sessionId: null,
|
|
2029
|
+
edited: false,
|
|
2030
|
+
retry: null
|
|
1518
2031
|
};
|
|
1519
|
-
const
|
|
2032
|
+
const before = request.mode === "variant" ? await registered() : null;
|
|
2033
|
+
const agent = resolved.name;
|
|
1520
2034
|
let outcome = await runChild(request, resolved, lines, observed);
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2035
|
+
const verdict = () => classifyFailure({
|
|
2036
|
+
agent,
|
|
2037
|
+
error: outcome.ok ? null : stopped && outcome.error === "cancelled" ? "stopped by shutdown" : outcome.error,
|
|
2038
|
+
exitCode: outcome.ok ? outcome.code : null,
|
|
2039
|
+
lines,
|
|
2040
|
+
retry: observed.retry
|
|
2041
|
+
});
|
|
2042
|
+
let failure = verdict();
|
|
2043
|
+
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
|
|
2044
|
+
// child settling and the retry starting finds no child to cancel, so
|
|
2045
|
+
// nothing says "cancelled". Stopped still means stopped.
|
|
1525
2046
|
!stopped) {
|
|
1526
2047
|
sessions.delete(resolved.agent);
|
|
1527
|
-
const cold = resolveCommand(choice, request.prompt);
|
|
2048
|
+
const cold = resolveCommand(choice, request.prompt, null, registration);
|
|
1528
2049
|
if (cold !== null) {
|
|
1529
2050
|
resolved = cold;
|
|
1530
2051
|
observed.sessionId = null;
|
|
1531
|
-
|
|
2052
|
+
observed.retry = null;
|
|
2053
|
+
state = { ...state, activity: null, waiting: null };
|
|
1532
2054
|
outcome = await runChild(request, resolved, lines, observed);
|
|
2055
|
+
failure = verdict();
|
|
1533
2056
|
}
|
|
1534
2057
|
}
|
|
1535
2058
|
if (outcome.ok && outcome.code === 0) {
|
|
2059
|
+
if (request.mode === "variant" && await registered() === before) {
|
|
2060
|
+
sessions.delete(resolved.agent);
|
|
2061
|
+
await reportFailure(request, classifyFailure({ agent, error: "not-registered" }), lines);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
1536
2064
|
if (observed.sessionId !== null) {
|
|
1537
2065
|
const previous = sessions.get(resolved.agent);
|
|
1538
2066
|
sessions.set(resolved.agent, {
|
|
@@ -1540,12 +2068,14 @@ function startRunner(options) {
|
|
|
1540
2068
|
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1541
2069
|
});
|
|
1542
2070
|
}
|
|
2071
|
+
if (request.mode === "replace" && request.notes !== void 0) {
|
|
2072
|
+
await removeAnnotations(options.cwd, request.notes).catch(() => 0);
|
|
2073
|
+
}
|
|
1543
2074
|
await removeRequest(options.cwd, request.id);
|
|
1544
2075
|
return;
|
|
1545
2076
|
}
|
|
1546
2077
|
sessions.delete(resolved.agent);
|
|
1547
|
-
|
|
1548
|
-
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
2078
|
+
await reportFailure(request, failure, lines);
|
|
1549
2079
|
} finally {
|
|
1550
2080
|
idle();
|
|
1551
2081
|
}
|
|
@@ -1579,12 +2109,23 @@ function startRunner(options) {
|
|
|
1579
2109
|
return false;
|
|
1580
2110
|
if (id !== void 0 && active.requestId !== id)
|
|
1581
2111
|
return false;
|
|
1582
|
-
|
|
1583
|
-
|
|
2112
|
+
const current = active;
|
|
2113
|
+
current.cancelled = true;
|
|
2114
|
+
failed.add(current.requestId);
|
|
2115
|
+
state = { ...state, stopping: true, waiting: null };
|
|
1584
2116
|
try {
|
|
1585
|
-
|
|
2117
|
+
current.child.kill("SIGTERM");
|
|
1586
2118
|
} catch {
|
|
1587
2119
|
}
|
|
2120
|
+
setLater(() => {
|
|
2121
|
+
if (active !== current)
|
|
2122
|
+
return;
|
|
2123
|
+
try {
|
|
2124
|
+
current.child.kill("SIGKILL");
|
|
2125
|
+
} catch {
|
|
2126
|
+
}
|
|
2127
|
+
current.abandon();
|
|
2128
|
+
}, CANCEL_GRACE_MS);
|
|
1588
2129
|
return true;
|
|
1589
2130
|
};
|
|
1590
2131
|
const stop = () => {
|
|
@@ -1609,12 +2150,12 @@ function startRunner(options) {
|
|
|
1609
2150
|
}
|
|
1610
2151
|
|
|
1611
2152
|
// ../server/dist/renames.js
|
|
1612
|
-
import { mkdir as
|
|
1613
|
-
import { dirname as
|
|
2153
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
2154
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1614
2155
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
1615
2156
|
async function readRenames(cwd) {
|
|
1616
2157
|
try {
|
|
1617
|
-
const raw = await
|
|
2158
|
+
const raw = await readFile7(join8(cwd, RENAMES_PATH), "utf8");
|
|
1618
2159
|
const parsed2 = JSON.parse(raw);
|
|
1619
2160
|
if (parsed2.renames === null || typeof parsed2.renames !== "object")
|
|
1620
2161
|
return {};
|
|
@@ -1624,9 +2165,9 @@ async function readRenames(cwd) {
|
|
|
1624
2165
|
}
|
|
1625
2166
|
}
|
|
1626
2167
|
async function writeRenames(cwd, renames) {
|
|
1627
|
-
const path =
|
|
1628
|
-
await
|
|
1629
|
-
await
|
|
2168
|
+
const path = join8(cwd, RENAMES_PATH);
|
|
2169
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2170
|
+
await writeFile5(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1630
2171
|
`, "utf8");
|
|
1631
2172
|
}
|
|
1632
2173
|
function resolveTitle(input, titles, renames) {
|
|
@@ -1644,7 +2185,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
1644
2185
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1645
2186
|
import http2 from "http";
|
|
1646
2187
|
import net3 from "net";
|
|
1647
|
-
import { extname, join as
|
|
2188
|
+
import { extname, join as join9, normalize, relative as relative3 } from "path";
|
|
1648
2189
|
var LEGLAS_PREFIX = "/leglas";
|
|
1649
2190
|
var DEFAULT_PORT = 4100;
|
|
1650
2191
|
var PORT_ATTEMPTS = 20;
|
|
@@ -1717,6 +2258,9 @@ function isTrustedMutation(req) {
|
|
|
1717
2258
|
return false;
|
|
1718
2259
|
}
|
|
1719
2260
|
}
|
|
2261
|
+
function isEnded(request, failedIds) {
|
|
2262
|
+
return isTerminal(request.status) || failedIds.includes(request.id);
|
|
2263
|
+
}
|
|
1720
2264
|
function hasJsonBody(req) {
|
|
1721
2265
|
const contentType = req.headers["content-type"];
|
|
1722
2266
|
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
@@ -1743,7 +2287,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
1743
2287
|
}
|
|
1744
2288
|
function serveFrom(res, dir, relativePath) {
|
|
1745
2289
|
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
1746
|
-
const candidate =
|
|
2290
|
+
const candidate = join9(dir, relative5);
|
|
1747
2291
|
if (!candidate.startsWith(dir))
|
|
1748
2292
|
return false;
|
|
1749
2293
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -1828,7 +2372,7 @@ async function bind(server, requested) {
|
|
|
1828
2372
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
1829
2373
|
}
|
|
1830
2374
|
async function startServer(options) {
|
|
1831
|
-
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
2375
|
+
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
1832
2376
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
1833
2377
|
const proxy = createProxyHandler({ target });
|
|
1834
2378
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
@@ -1847,18 +2391,16 @@ async function startServer(options) {
|
|
|
1847
2391
|
});
|
|
1848
2392
|
return agentsInflight;
|
|
1849
2393
|
};
|
|
1850
|
-
const currentAgents = () => {
|
|
1851
|
-
if (agentsCache === null)
|
|
2394
|
+
const currentAgents = (refresh = false) => {
|
|
2395
|
+
if (refresh || agentsCache === null || Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
|
|
1852
2396
|
return probeAgents();
|
|
1853
|
-
if (Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
|
|
1854
|
-
void probeAgents().catch(() => {
|
|
1855
|
-
});
|
|
1856
2397
|
}
|
|
1857
2398
|
return Promise.resolve(agentsCache.agents);
|
|
1858
2399
|
};
|
|
1859
2400
|
const server = http2.createServer((req, res) => {
|
|
1860
2401
|
const url = req.url ?? "/";
|
|
1861
2402
|
const path = url.split("?")[0] ?? "/";
|
|
2403
|
+
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
1862
2404
|
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
1863
2405
|
return sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
1864
2406
|
}
|
|
@@ -1945,20 +2487,51 @@ async function startServer(options) {
|
|
|
1945
2487
|
} catch {
|
|
1946
2488
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1947
2489
|
}
|
|
2490
|
+
if (parsed2.mode !== void 0 && parsed2.mode !== "variant" && parsed2.mode !== "replace") {
|
|
2491
|
+
return sendJson(res, 400, {
|
|
2492
|
+
ok: false,
|
|
2493
|
+
error: 'mode must be "variant" or "replace".'
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
const mode = parsed2.mode === "replace" ? "replace" : "variant";
|
|
1948
2497
|
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
1949
2498
|
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
1950
2499
|
const localTitles = new Set(local.map((entry) => entry.title));
|
|
1951
2500
|
const bootConfig = config?.previews ?? [];
|
|
1952
2501
|
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
1953
2502
|
const preview = [...boot, ...local].find((entry) => entry.title === parsed2.title);
|
|
1954
|
-
if (!preview
|
|
2503
|
+
if (!preview) {
|
|
1955
2504
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
1956
2505
|
}
|
|
1957
|
-
const
|
|
2506
|
+
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
2507
|
+
if (!parsed2.intent?.trim() && notes.length === 0) {
|
|
2508
|
+
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
2509
|
+
}
|
|
2510
|
+
const intent = (parsed2.intent ?? "").trim();
|
|
2511
|
+
const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
2512
|
+
const sameNotes = (entry) => {
|
|
2513
|
+
const before = [...entry.notes ?? []].sort().join(",");
|
|
2514
|
+
return before === notes.map((note) => note.id).sort().join(",");
|
|
2515
|
+
};
|
|
2516
|
+
if (live.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
2517
|
+
// one forks the direction and the other rewrites it. Only a
|
|
2518
|
+
// genuine repeat is refused.
|
|
2519
|
+
(entry.mode ?? "replace") === mode && sameNotes(entry))) {
|
|
2520
|
+
return sendJson(res, 409, {
|
|
2521
|
+
ok: false,
|
|
2522
|
+
duplicate: true,
|
|
2523
|
+
error: `That exact change to ${preview.title} is already waiting.`
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
const composed = composeRequest(preview, intent, mode, notes, leglasCommand);
|
|
1958
2527
|
void appendRequest(cwd, {
|
|
1959
2528
|
title: preview.title,
|
|
1960
2529
|
url: preview.url,
|
|
1961
|
-
intent
|
|
2530
|
+
intent,
|
|
2531
|
+
// The ids travel with the request so a change made in place can
|
|
2532
|
+
// forget the notes it answered. A fork leaves them where they are:
|
|
2533
|
+
// the direction they point at was not touched.
|
|
2534
|
+
...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
|
|
1962
2535
|
...composed
|
|
1963
2536
|
}).then(() => {
|
|
1964
2537
|
runner?.nudge();
|
|
@@ -1967,10 +2540,14 @@ async function startServer(options) {
|
|
|
1967
2540
|
});
|
|
1968
2541
|
}
|
|
1969
2542
|
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
1970
|
-
return void Promise.all([
|
|
2543
|
+
return void Promise.all([
|
|
2544
|
+
currentAgents(query.get("refresh") === "1"),
|
|
2545
|
+
readAgentChoice(cwd)
|
|
2546
|
+
]).then(([agents, choice]) => sendJson(res, 200, {
|
|
1971
2547
|
agents,
|
|
1972
2548
|
choice: choice.agent,
|
|
1973
|
-
customRun: choice.run
|
|
2549
|
+
customRun: choice.run,
|
|
2550
|
+
effort: choice.effort
|
|
1974
2551
|
}));
|
|
1975
2552
|
}
|
|
1976
2553
|
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
@@ -1998,7 +2575,17 @@ async function startServer(options) {
|
|
|
1998
2575
|
if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
|
|
1999
2576
|
return sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
2000
2577
|
}
|
|
2578
|
+
const effort = parsed2.effort === null || isAgentEffort(parsed2.effort) ? parsed2.effort : void 0;
|
|
2579
|
+
if (parsed2.effort !== void 0 && effort === void 0) {
|
|
2580
|
+
return sendJson(res, 400, { ok: false, error: "Effort must be a supported level or null." });
|
|
2581
|
+
}
|
|
2001
2582
|
if (parsed2.agent === "custom") {
|
|
2583
|
+
if (effort !== void 0) {
|
|
2584
|
+
return sendJson(res, 400, {
|
|
2585
|
+
ok: false,
|
|
2586
|
+
error: "Custom agents manage effort in their own command."
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2002
2589
|
if (typeof parsed2.run !== "string") {
|
|
2003
2590
|
return sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
2004
2591
|
}
|
|
@@ -2007,7 +2594,16 @@ async function startServer(options) {
|
|
|
2007
2594
|
return sendJson(res, 400, { ok: false, error: template.error });
|
|
2008
2595
|
return void saveAgentChoice(cwd, { agent: "custom", run: parsed2.run }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
2009
2596
|
}
|
|
2010
|
-
|
|
2597
|
+
if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed2.agent].efforts.includes(effort)) {
|
|
2598
|
+
return sendJson(res, 400, {
|
|
2599
|
+
ok: false,
|
|
2600
|
+
error: `${KNOWN_AGENTS[parsed2.agent].name} does not expose an effort override.`
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
return void saveAgentChoice(cwd, {
|
|
2604
|
+
agent: parsed2.agent,
|
|
2605
|
+
...effort === void 0 ? {} : { effort }
|
|
2606
|
+
}).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
2011
2607
|
});
|
|
2012
2608
|
}
|
|
2013
2609
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -2034,21 +2630,33 @@ async function startServer(options) {
|
|
|
2034
2630
|
agent: null,
|
|
2035
2631
|
activity: null,
|
|
2036
2632
|
startedAt: null,
|
|
2633
|
+
stopping: false,
|
|
2634
|
+
waiting: null,
|
|
2037
2635
|
failedIds: []
|
|
2038
2636
|
};
|
|
2039
2637
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
2040
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
2638
|
+
requests: requests.map(({ id, title, intent, status, failure }) => ({
|
|
2041
2639
|
id,
|
|
2042
2640
|
title,
|
|
2043
2641
|
intent,
|
|
2044
|
-
|
|
2642
|
+
// The run in flight is the one thing the file cannot know. After
|
|
2643
|
+
// that the file is the record, including across a restart, and the
|
|
2644
|
+
// process-local failed set only covers a request whose verdict
|
|
2645
|
+
// could not be written.
|
|
2646
|
+
status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
|
|
2647
|
+
failure: failure ?? null
|
|
2045
2648
|
})),
|
|
2046
2649
|
agent: {
|
|
2047
2650
|
attached: externallyAttached(),
|
|
2048
2651
|
running: snapshot.running,
|
|
2049
2652
|
name: snapshot.running ? snapshot.agent : null,
|
|
2050
2653
|
activity: snapshot.running ? snapshot.activity : null,
|
|
2051
|
-
startedAt: snapshot.running ? snapshot.startedAt : null
|
|
2654
|
+
startedAt: snapshot.running ? snapshot.startedAt : null,
|
|
2655
|
+
// A stop that has been asked for but not yet obeyed. The card
|
|
2656
|
+
// says so rather than going on describing a live run.
|
|
2657
|
+
stopping: snapshot.running && snapshot.stopping,
|
|
2658
|
+
// Why a run that looks stalled is stalled, while it is stalled.
|
|
2659
|
+
waiting: snapshot.running ? snapshot.waiting : null
|
|
2052
2660
|
}
|
|
2053
2661
|
}));
|
|
2054
2662
|
}
|
|
@@ -2091,8 +2699,8 @@ async function startServer(options) {
|
|
|
2091
2699
|
if (request === void 0) {
|
|
2092
2700
|
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2093
2701
|
}
|
|
2094
|
-
if (!(runner?.snapshot().failedIds
|
|
2095
|
-
return sendJson(res, 400, { ok: false, error: "Only
|
|
2702
|
+
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
2703
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
2096
2704
|
}
|
|
2097
2705
|
try {
|
|
2098
2706
|
if (!await removeRequest(cwd, request.id)) {
|
|
@@ -2103,7 +2711,11 @@ async function startServer(options) {
|
|
|
2103
2711
|
url: request.url,
|
|
2104
2712
|
intent: request.intent,
|
|
2105
2713
|
target: request.target,
|
|
2106
|
-
prompt: request.prompt
|
|
2714
|
+
prompt: request.prompt,
|
|
2715
|
+
// The stored prompt already carries the mode's instructions; the
|
|
2716
|
+
// field travels with it so the queue keeps saying which kind of
|
|
2717
|
+
// change this is.
|
|
2718
|
+
...request.mode === void 0 ? {} : { mode: request.mode }
|
|
2107
2719
|
});
|
|
2108
2720
|
runner?.nudge();
|
|
2109
2721
|
return sendJson(res, 200, { ok: true });
|
|
@@ -2112,6 +2724,65 @@ async function startServer(options) {
|
|
|
2112
2724
|
}
|
|
2113
2725
|
});
|
|
2114
2726
|
}
|
|
2727
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
2728
|
+
return void readAnnotations(cwd).then((annotations) => sendJson(res, 200, { annotations }));
|
|
2729
|
+
}
|
|
2730
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
2731
|
+
if (!hasJsonBody(req)) {
|
|
2732
|
+
return sendJson(res, 400, { ok: false, error: "A note must be JSON." });
|
|
2733
|
+
}
|
|
2734
|
+
let body = "";
|
|
2735
|
+
req.on("data", (chunk) => body += chunk);
|
|
2736
|
+
return void req.on("end", async () => {
|
|
2737
|
+
let parsed2;
|
|
2738
|
+
try {
|
|
2739
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2740
|
+
} catch {
|
|
2741
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2742
|
+
}
|
|
2743
|
+
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
2744
|
+
return sendJson(res, 400, { ok: false, error: "A note needs a direction." });
|
|
2745
|
+
}
|
|
2746
|
+
const anchor = anchorFrom(parsed2.anchor);
|
|
2747
|
+
if (anchor === null) {
|
|
2748
|
+
return sendJson(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
const annotation = await addAnnotation(cwd, {
|
|
2752
|
+
anchor,
|
|
2753
|
+
note: typeof parsed2.note === "string" ? parsed2.note.trim() : "",
|
|
2754
|
+
title: parsed2.title
|
|
2755
|
+
});
|
|
2756
|
+
return sendJson(res, 200, { ok: true, annotation });
|
|
2757
|
+
} catch {
|
|
2758
|
+
return sendJson(res, 500, { ok: false, error: "The note could not be kept." });
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
2763
|
+
if (!hasJsonBody(req)) {
|
|
2764
|
+
return sendJson(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
2765
|
+
}
|
|
2766
|
+
let body = "";
|
|
2767
|
+
req.on("data", (chunk) => body += chunk);
|
|
2768
|
+
return void req.on("end", async () => {
|
|
2769
|
+
let parsed2;
|
|
2770
|
+
try {
|
|
2771
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2772
|
+
} catch {
|
|
2773
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2774
|
+
}
|
|
2775
|
+
const ids = Array.isArray(parsed2.ids) ? parsed2.ids.filter((entry) => typeof entry === "string") : [];
|
|
2776
|
+
if (ids.length === 0) {
|
|
2777
|
+
return sendJson(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
2778
|
+
}
|
|
2779
|
+
try {
|
|
2780
|
+
return sendJson(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
2781
|
+
} catch {
|
|
2782
|
+
return sendJson(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2115
2786
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2116
2787
|
if (!hasJsonBody(req)) {
|
|
2117
2788
|
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
@@ -2128,8 +2799,9 @@ async function startServer(options) {
|
|
|
2128
2799
|
if (typeof parsed2.id !== "string") {
|
|
2129
2800
|
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2130
2801
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2802
|
+
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
2803
|
+
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
2804
|
+
return sendJson(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
2133
2805
|
}
|
|
2134
2806
|
try {
|
|
2135
2807
|
if (!await removeRequest(cwd, parsed2.id)) {
|
|
@@ -2204,7 +2876,7 @@ async function startServer(options) {
|
|
|
2204
2876
|
proxy.upgrade(req, socket, head);
|
|
2205
2877
|
});
|
|
2206
2878
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2207
|
-
runner = startRunner({ cwd, externallyAttached });
|
|
2879
|
+
runner = startRunner({ cwd, externallyAttached, leglasCommand });
|
|
2208
2880
|
let closePromise = null;
|
|
2209
2881
|
return {
|
|
2210
2882
|
port,
|
|
@@ -2229,7 +2901,7 @@ async function runClassify(options, deps) {
|
|
|
2229
2901
|
const declared = await Promise.all(
|
|
2230
2902
|
options.changes.map(async (change) => ({
|
|
2231
2903
|
...change,
|
|
2232
|
-
exists: await stat(
|
|
2904
|
+
exists: await stat(join10(options.cwd, change.path)).then(
|
|
2233
2905
|
() => true,
|
|
2234
2906
|
() => false
|
|
2235
2907
|
)
|
|
@@ -2494,8 +3166,8 @@ function runExplore(options, deps) {
|
|
|
2494
3166
|
}
|
|
2495
3167
|
|
|
2496
3168
|
// src/run-init.ts
|
|
2497
|
-
import { readFile as
|
|
2498
|
-
import { join as
|
|
3169
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
3170
|
+
import { join as join11 } from "path";
|
|
2499
3171
|
|
|
2500
3172
|
// src/init.ts
|
|
2501
3173
|
var AGENTS_MARKER_START = "<!-- leglas:start -->";
|
|
@@ -2513,6 +3185,11 @@ direction. Build it directly. A planning or approval step before implementing
|
|
|
2513
3185
|
costs more than the work itself, and the directions on screen are the thing
|
|
2514
3186
|
being asked for.
|
|
2515
3187
|
|
|
3188
|
+
A request that says it came from the running Leglas interface has already
|
|
3189
|
+
completed exploration, request collection and the live-server check. Follow
|
|
3190
|
+
the exact source and registration command in that request directly. Do not
|
|
3191
|
+
repeat \`explore\`, \`requests\`, \`list\`, CLI help/version checks or server startup.
|
|
3192
|
+
|
|
2516
3193
|
When asked for design variations, alternatives, or "a few options":
|
|
2517
3194
|
|
|
2518
3195
|
1. **Add beside what exists. Never replace it.** Every direction has to render
|
|
@@ -2628,7 +3305,7 @@ ${AGENTS_SECTION}`
|
|
|
2628
3305
|
// src/run-init.ts
|
|
2629
3306
|
async function readIfPresent(path) {
|
|
2630
3307
|
try {
|
|
2631
|
-
return await
|
|
3308
|
+
return await readFile8(path, "utf8");
|
|
2632
3309
|
} catch {
|
|
2633
3310
|
return null;
|
|
2634
3311
|
}
|
|
@@ -2636,18 +3313,18 @@ async function readIfPresent(path) {
|
|
|
2636
3313
|
async function runInit(options, deps) {
|
|
2637
3314
|
const existingConfig = findConfigFile(options.cwd);
|
|
2638
3315
|
const plan = planInit({
|
|
2639
|
-
agents: await readIfPresent(
|
|
3316
|
+
agents: await readIfPresent(join11(options.cwd, "AGENTS.md")),
|
|
2640
3317
|
config: existingConfig === null ? null : "present",
|
|
2641
|
-
gitignore: await readIfPresent(
|
|
3318
|
+
gitignore: await readIfPresent(join11(options.cwd, ".gitignore")),
|
|
2642
3319
|
force: options.force
|
|
2643
3320
|
});
|
|
2644
3321
|
const touched = [];
|
|
2645
|
-
for (const
|
|
2646
|
-
await
|
|
2647
|
-
touched.push(
|
|
3322
|
+
for (const write2 of plan.writes) {
|
|
3323
|
+
await writeFile6(join11(options.cwd, write2.path), write2.contents, "utf8");
|
|
3324
|
+
touched.push(write2.path);
|
|
2648
3325
|
}
|
|
2649
3326
|
if (plan.gitignore !== null) {
|
|
2650
|
-
await
|
|
3327
|
+
await writeFile6(join11(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2651
3328
|
touched.push(".gitignore");
|
|
2652
3329
|
}
|
|
2653
3330
|
if (options.json) {
|
|
@@ -2667,8 +3344,8 @@ async function runInit(options, deps) {
|
|
|
2667
3344
|
|
|
2668
3345
|
// src/run-keep.ts
|
|
2669
3346
|
import { existsSync as existsSync3 } from "fs";
|
|
2670
|
-
import { mkdir as
|
|
2671
|
-
import { dirname as
|
|
3347
|
+
import { mkdir as mkdir6, readFile as readFile9, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
3348
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
2672
3349
|
|
|
2673
3350
|
// src/keep.ts
|
|
2674
3351
|
import { basename as basename2, extname as extname2, normalize as normalize2 } from "path";
|
|
@@ -2767,18 +3444,18 @@ async function runKeep(options, deps) {
|
|
|
2767
3444
|
if (!resolved.ok) return fail(resolved.error);
|
|
2768
3445
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
2769
3446
|
if (!plan.ok) return fail(plan.error);
|
|
2770
|
-
const from =
|
|
2771
|
-
const to =
|
|
3447
|
+
const from = join12(options.cwd, plan.move.from);
|
|
3448
|
+
const to = join12(options.cwd, plan.move.to);
|
|
2772
3449
|
if (!existsSync3(from)) {
|
|
2773
3450
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
2774
3451
|
}
|
|
2775
3452
|
if (existsSync3(to)) {
|
|
2776
3453
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
2777
3454
|
}
|
|
2778
|
-
const source = await
|
|
2779
|
-
await
|
|
2780
|
-
await
|
|
2781
|
-
await rm2(
|
|
3455
|
+
const source = await readFile9(from, "utf8");
|
|
3456
|
+
await mkdir6(dirname7(to), { recursive: true });
|
|
3457
|
+
await writeFile7(to, renameExport(source, plan.exportName), "utf8");
|
|
3458
|
+
await rm2(join12(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
2782
3459
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
2783
3460
|
if (options.json) {
|
|
2784
3461
|
deps.log(
|
|
@@ -2813,11 +3490,11 @@ async function runKeep(options, deps) {
|
|
|
2813
3490
|
|
|
2814
3491
|
// src/run-new.ts
|
|
2815
3492
|
import { existsSync as existsSync4 } from "fs";
|
|
2816
|
-
import { mkdir as
|
|
2817
|
-
import { dirname as
|
|
3493
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
3494
|
+
import { dirname as dirname8, join as join13 } from "path";
|
|
2818
3495
|
async function readIfPresent2(path) {
|
|
2819
3496
|
try {
|
|
2820
|
-
return await
|
|
3497
|
+
return await readFile10(path, "utf8");
|
|
2821
3498
|
} catch {
|
|
2822
3499
|
return null;
|
|
2823
3500
|
}
|
|
@@ -2825,19 +3502,19 @@ async function readIfPresent2(path) {
|
|
|
2825
3502
|
async function runNew(options, deps) {
|
|
2826
3503
|
let from;
|
|
2827
3504
|
if (options.from !== void 0) {
|
|
2828
|
-
const contents = await readIfPresent2(
|
|
3505
|
+
const contents = await readIfPresent2(join13(options.cwd, options.from));
|
|
2829
3506
|
if (contents === null) {
|
|
2830
|
-
const
|
|
2831
|
-
if (options.json) deps.log(JSON.stringify({ ok: false, error:
|
|
2832
|
-
else deps.log(
|
|
3507
|
+
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
3508
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
3509
|
+
else deps.log(message2);
|
|
2833
3510
|
return { exitCode: 1, written: [] };
|
|
2834
3511
|
}
|
|
2835
3512
|
from = { path: options.from, contents };
|
|
2836
3513
|
}
|
|
2837
3514
|
const plan = planNew({
|
|
2838
3515
|
surface: options.surface,
|
|
2839
|
-
packageJson: await readIfPresent2(
|
|
2840
|
-
gitignore: await readIfPresent2(
|
|
3516
|
+
packageJson: await readIfPresent2(join13(options.cwd, "package.json")),
|
|
3517
|
+
gitignore: await readIfPresent2(join13(options.cwd, ".gitignore")),
|
|
2841
3518
|
from
|
|
2842
3519
|
});
|
|
2843
3520
|
const fail = (error) => {
|
|
@@ -2853,26 +3530,26 @@ async function runNew(options, deps) {
|
|
|
2853
3530
|
deps.log(JSON.stringify({ ok: true, files: plan.writes, instructions: plan.instructions, previews: plan.previews }));
|
|
2854
3531
|
return { exitCode: 0, written: [] };
|
|
2855
3532
|
}
|
|
2856
|
-
for (const
|
|
2857
|
-
deps.log(`--- ${
|
|
2858
|
-
deps.log(
|
|
3533
|
+
for (const write2 of plan.writes) {
|
|
3534
|
+
deps.log(`--- ${write2.path}`);
|
|
3535
|
+
deps.log(write2.contents);
|
|
2859
3536
|
}
|
|
2860
3537
|
deps.log(plan.instructions);
|
|
2861
3538
|
return { exitCode: 0, written: [] };
|
|
2862
3539
|
}
|
|
2863
|
-
const existing = plan.writes.filter((
|
|
3540
|
+
const existing = plan.writes.filter((write2) => existsSync4(join13(options.cwd, write2.path)));
|
|
2864
3541
|
if (existing.length > 0) {
|
|
2865
3542
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
2866
3543
|
}
|
|
2867
3544
|
const written = [];
|
|
2868
|
-
for (const
|
|
2869
|
-
const target =
|
|
2870
|
-
await
|
|
2871
|
-
await
|
|
2872
|
-
written.push(
|
|
3545
|
+
for (const write2 of plan.writes) {
|
|
3546
|
+
const target = join13(options.cwd, write2.path);
|
|
3547
|
+
await mkdir7(dirname8(target), { recursive: true });
|
|
3548
|
+
await writeFile8(target, write2.contents, "utf8");
|
|
3549
|
+
written.push(write2.path);
|
|
2873
3550
|
}
|
|
2874
3551
|
if (plan.gitignore !== null) {
|
|
2875
|
-
await
|
|
3552
|
+
await writeFile8(join13(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
2876
3553
|
written.push(".gitignore");
|
|
2877
3554
|
}
|
|
2878
3555
|
if (options.json) {
|
|
@@ -2893,21 +3570,21 @@ async function runNew(options, deps) {
|
|
|
2893
3570
|
}
|
|
2894
3571
|
|
|
2895
3572
|
// src/run-previews.ts
|
|
2896
|
-
import { readFile as
|
|
2897
|
-
import { join as
|
|
3573
|
+
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
3574
|
+
import { join as join14 } from "path";
|
|
2898
3575
|
function envelope(deps, ok, body) {
|
|
2899
3576
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
2900
3577
|
}
|
|
2901
3578
|
async function ensureIgnored(cwd) {
|
|
2902
|
-
const path =
|
|
3579
|
+
const path = join14(cwd, ".gitignore");
|
|
2903
3580
|
let current = null;
|
|
2904
3581
|
try {
|
|
2905
|
-
current = await
|
|
3582
|
+
current = await readFile11(path, "utf8");
|
|
2906
3583
|
} catch {
|
|
2907
3584
|
current = null;
|
|
2908
3585
|
}
|
|
2909
3586
|
const next = ignoreEntry(current);
|
|
2910
|
-
if (next !== null) await
|
|
3587
|
+
if (next !== null) await writeFile9(path, next, "utf8");
|
|
2911
3588
|
}
|
|
2912
3589
|
async function runAdd(options, deps) {
|
|
2913
3590
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2931,7 +3608,8 @@ async function runAdd(options, deps) {
|
|
|
2931
3608
|
tags: options.preview.tags,
|
|
2932
3609
|
branch: options.preview.branch,
|
|
2933
3610
|
file: options.preview.file,
|
|
2934
|
-
basedOn: options.preview.basedOn
|
|
3611
|
+
basedOn: options.preview.basedOn,
|
|
3612
|
+
askedFor: options.preview.askedFor
|
|
2935
3613
|
},
|
|
2936
3614
|
shared
|
|
2937
3615
|
);
|
|
@@ -2991,6 +3669,7 @@ async function runList(options, deps) {
|
|
|
2991
3669
|
note: preview.note ?? null,
|
|
2992
3670
|
tags: preview.tags,
|
|
2993
3671
|
basedOn: preview.basedOn ?? null,
|
|
3672
|
+
askedFor: preview.askedFor ?? null,
|
|
2994
3673
|
local: preview.local,
|
|
2995
3674
|
branch: preview.branch ?? null,
|
|
2996
3675
|
file: preview.file ?? null
|
|
@@ -3153,23 +3832,23 @@ async function runShow(options, deps) {
|
|
|
3153
3832
|
|
|
3154
3833
|
// src/run-watch.ts
|
|
3155
3834
|
import { spawn as spawn3 } from "child_process";
|
|
3156
|
-
import { mkdir as
|
|
3157
|
-
import { dirname as
|
|
3835
|
+
import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
|
|
3836
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
3158
3837
|
var POLL_MS2 = 2e3;
|
|
3159
3838
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
3160
3839
|
async function saveTemplate(cwd, run3) {
|
|
3161
|
-
const path =
|
|
3840
|
+
const path = join15(cwd, WATCH_PATH);
|
|
3162
3841
|
let config = {};
|
|
3163
3842
|
try {
|
|
3164
|
-
const parsed2 = JSON.parse(await
|
|
3843
|
+
const parsed2 = JSON.parse(await readFile12(path, "utf8"));
|
|
3165
3844
|
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
3166
3845
|
config = parsed2;
|
|
3167
3846
|
}
|
|
3168
3847
|
} catch {
|
|
3169
3848
|
}
|
|
3170
3849
|
config.run = run3;
|
|
3171
|
-
await
|
|
3172
|
-
await
|
|
3850
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3851
|
+
await writeFile10(path, `${JSON.stringify(config, null, 2)}
|
|
3173
3852
|
`, "utf8");
|
|
3174
3853
|
}
|
|
3175
3854
|
function spawnAgent(command, args, cwd) {
|
|
@@ -3208,7 +3887,7 @@ async function runWatch(options, deps) {
|
|
|
3208
3887
|
const adapter = KNOWN_AGENTS[saved.agent];
|
|
3209
3888
|
template = {
|
|
3210
3889
|
command: adapter.binary,
|
|
3211
|
-
args: adapter.terminalArgs(PROMPT_TOKEN)
|
|
3890
|
+
args: adapter.terminalArgs(PROMPT_TOKEN, saved.effort)
|
|
3212
3891
|
};
|
|
3213
3892
|
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
3214
3893
|
synthesizedAgent = adapter.name;
|
|
@@ -3255,9 +3934,13 @@ async function runWatch(options, deps) {
|
|
|
3255
3934
|
return;
|
|
3256
3935
|
}
|
|
3257
3936
|
failed.add(request.id);
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3937
|
+
const failure = classifyFailure({
|
|
3938
|
+
agent: (shownCommand2.split(/\s+/)[0] ?? command).split("/").pop() ?? command,
|
|
3939
|
+
error: outcome.ok ? null : outcome.error,
|
|
3940
|
+
exitCode: outcome.ok ? outcome.code : null
|
|
3941
|
+
});
|
|
3942
|
+
await markFailed(options.cwd, request.id, failure);
|
|
3943
|
+
deps.error(` failed ${request.title}: ${failure.message}`);
|
|
3261
3944
|
deps.error(" Left in the queue and not retried.");
|
|
3262
3945
|
};
|
|
3263
3946
|
const tick = async () => {
|
|
@@ -3304,18 +3987,28 @@ async function runWatch(options, deps) {
|
|
|
3304
3987
|
// src/run.ts
|
|
3305
3988
|
import { existsSync as existsSync5 } from "fs";
|
|
3306
3989
|
import { createRequire } from "module";
|
|
3307
|
-
import { basename as basename3, dirname as
|
|
3990
|
+
import { basename as basename3, dirname as dirname10, join as join16, relative as relative4 } from "path";
|
|
3308
3991
|
import { fileURLToPath } from "url";
|
|
3309
3992
|
function findShellDir() {
|
|
3310
|
-
const bundled =
|
|
3311
|
-
if (existsSync5(
|
|
3993
|
+
const bundled = join16(dirname10(fileURLToPath(import.meta.url)), "shell");
|
|
3994
|
+
if (existsSync5(join16(bundled, "index.html"))) return bundled;
|
|
3312
3995
|
try {
|
|
3313
3996
|
const require2 = createRequire(import.meta.url);
|
|
3314
|
-
return
|
|
3997
|
+
return dirname10(require2.resolve("@leglas/shell/dist/index.html"));
|
|
3315
3998
|
} catch {
|
|
3316
3999
|
return null;
|
|
3317
4000
|
}
|
|
3318
4001
|
}
|
|
4002
|
+
function shellWord(value) {
|
|
4003
|
+
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(value)) return value;
|
|
4004
|
+
if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
|
|
4005
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
4006
|
+
}
|
|
4007
|
+
function embeddedLeglasCommand() {
|
|
4008
|
+
const entry = join16(dirname10(fileURLToPath(import.meta.url)), "bin.js");
|
|
4009
|
+
if (!existsSync5(entry)) return "npx -y leglas";
|
|
4010
|
+
return [process.execPath, entry].map(shellWord).join(" ");
|
|
4011
|
+
}
|
|
3319
4012
|
async function run2(options, deps) {
|
|
3320
4013
|
const loaded = await loadConfig(options.cwd);
|
|
3321
4014
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -3345,7 +4038,7 @@ async function run2(options, deps) {
|
|
|
3345
4038
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
3346
4039
|
for (const preview of merged?.previews ?? []) {
|
|
3347
4040
|
if (preview.file !== void 0) {
|
|
3348
|
-
const absolute =
|
|
4041
|
+
const absolute = join16(options.cwd, preview.file);
|
|
3349
4042
|
if (!existsSync5(absolute)) {
|
|
3350
4043
|
worktreeErrors.push(
|
|
3351
4044
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -3356,7 +4049,7 @@ async function run2(options, deps) {
|
|
|
3356
4049
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
3357
4050
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
3358
4051
|
}
|
|
3359
|
-
fileMounts.set(slug,
|
|
4052
|
+
fileMounts.set(slug, dirname10(absolute));
|
|
3360
4053
|
previews.push({
|
|
3361
4054
|
...preview,
|
|
3362
4055
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -3397,6 +4090,7 @@ async function run2(options, deps) {
|
|
|
3397
4090
|
// directory does. Either way saved layout survives a port change.
|
|
3398
4091
|
project: loaded.path ?? options.cwd,
|
|
3399
4092
|
cwd: options.cwd,
|
|
4093
|
+
leglasCommand: embeddedLeglasCommand(),
|
|
3400
4094
|
...options.port === void 0 ? {} : { port: options.port }
|
|
3401
4095
|
});
|
|
3402
4096
|
const url = `${server.url}${LEGLAS_PREFIX}`;
|
|
@@ -3502,6 +4196,7 @@ Options for add
|
|
|
3502
4196
|
--branch <name> Back the preview with a checkout of this git branch
|
|
3503
4197
|
--file <path> Preview a plain HTML file served by Leglas itself
|
|
3504
4198
|
--based-on <title> The direction this is a variant of; groups the family
|
|
4199
|
+
--asked-for <text> The change that was asked for, in the words that were typed
|
|
3505
4200
|
`;
|
|
3506
4201
|
function version() {
|
|
3507
4202
|
const require2 = createRequire2(import.meta.url);
|