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/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin.ts
|
|
4
|
-
import { spawn as
|
|
4
|
+
import { spawn as spawn4 } from "child_process";
|
|
5
5
|
import { createRequire as createRequire2 } from "module";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -353,7 +353,7 @@ function parseArgs(argv) {
|
|
|
353
353
|
|
|
354
354
|
// src/run-classify.ts
|
|
355
355
|
import { stat } from "fs/promises";
|
|
356
|
-
import { join as
|
|
356
|
+
import { join as join8 } from "path";
|
|
357
357
|
|
|
358
358
|
// ../server/dist/config.js
|
|
359
359
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
@@ -485,6 +485,360 @@ function normalizeConfig(raw, options = {}) {
|
|
|
485
485
|
};
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
// ../server/dist/agent-command.js
|
|
489
|
+
var WATCH_PATH = ".leglas/watch.json";
|
|
490
|
+
var PROMPT_TOKEN = "{prompt}";
|
|
491
|
+
var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
492
|
+
function tokenize(template) {
|
|
493
|
+
const tokens = [];
|
|
494
|
+
let current = "";
|
|
495
|
+
let started = false;
|
|
496
|
+
let quote = null;
|
|
497
|
+
for (const character of template) {
|
|
498
|
+
if (quote !== null) {
|
|
499
|
+
if (character === quote)
|
|
500
|
+
quote = null;
|
|
501
|
+
else
|
|
502
|
+
current += character;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (character === '"' || character === "'") {
|
|
506
|
+
quote = character;
|
|
507
|
+
started = true;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (/\s/.test(character)) {
|
|
511
|
+
if (started)
|
|
512
|
+
tokens.push(current);
|
|
513
|
+
current = "";
|
|
514
|
+
started = false;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
current += character;
|
|
518
|
+
started = true;
|
|
519
|
+
}
|
|
520
|
+
if (quote !== null) {
|
|
521
|
+
return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
|
|
522
|
+
}
|
|
523
|
+
if (started)
|
|
524
|
+
tokens.push(current);
|
|
525
|
+
return { ok: true, tokens };
|
|
526
|
+
}
|
|
527
|
+
function parseTemplate(raw) {
|
|
528
|
+
const tokenized = tokenize(raw);
|
|
529
|
+
if (!tokenized.ok)
|
|
530
|
+
return tokenized;
|
|
531
|
+
const { tokens } = tokenized;
|
|
532
|
+
const [command, ...args] = tokens;
|
|
533
|
+
if (command === void 0) {
|
|
534
|
+
return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
|
|
535
|
+
}
|
|
536
|
+
if (tokens.some((token) => token !== PROMPT_TOKEN && token.includes(PROMPT_TOKEN))) {
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
error: `${PROMPT_TOKEN} must stand as a word of its own, for example: ${EXAMPLE}`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
|
|
543
|
+
if (placeholders > 1) {
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (command === PROMPT_TOKEN) {
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return { ok: true, template: { command, args } };
|
|
556
|
+
}
|
|
557
|
+
function commandFor(template, prompt) {
|
|
558
|
+
if (!template.args.includes(PROMPT_TOKEN)) {
|
|
559
|
+
return { command: template.command, args: [...template.args, prompt] };
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
command: template.command,
|
|
563
|
+
args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function nextRequest(requests, failed) {
|
|
567
|
+
return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ../server/dist/agents.js
|
|
571
|
+
import { spawn } from "child_process";
|
|
572
|
+
import { constants } from "fs";
|
|
573
|
+
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
574
|
+
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
575
|
+
var KNOWN_AGENTS = {
|
|
576
|
+
claude: {
|
|
577
|
+
name: "Claude",
|
|
578
|
+
binary: "claude",
|
|
579
|
+
args: (prompt) => [
|
|
580
|
+
"-p",
|
|
581
|
+
prompt,
|
|
582
|
+
"--output-format",
|
|
583
|
+
"stream-json",
|
|
584
|
+
"--verbose",
|
|
585
|
+
"--permission-mode",
|
|
586
|
+
"acceptEdits"
|
|
587
|
+
],
|
|
588
|
+
terminalArgs: (prompt) => [
|
|
589
|
+
"-p",
|
|
590
|
+
prompt,
|
|
591
|
+
"--permission-mode",
|
|
592
|
+
"acceptEdits"
|
|
593
|
+
],
|
|
594
|
+
resumeArgs: (sessionId, prompt) => [
|
|
595
|
+
"-p",
|
|
596
|
+
"--resume",
|
|
597
|
+
sessionId,
|
|
598
|
+
prompt,
|
|
599
|
+
"--output-format",
|
|
600
|
+
"stream-json",
|
|
601
|
+
"--verbose",
|
|
602
|
+
"--permission-mode",
|
|
603
|
+
"acceptEdits"
|
|
604
|
+
],
|
|
605
|
+
// Every stream-json event names its session.
|
|
606
|
+
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
607
|
+
authArgs: ["auth", "status"],
|
|
608
|
+
// `claude auth status` prints JSON with a loggedIn boolean. Only that
|
|
609
|
+
// field decides; any other shape stays unknown.
|
|
610
|
+
authVerdict: (result2) => {
|
|
611
|
+
try {
|
|
612
|
+
const parsed2 = record(JSON.parse(result2.stdout));
|
|
613
|
+
if (parsed2?.loggedIn === true)
|
|
614
|
+
return "ok";
|
|
615
|
+
if (parsed2?.loggedIn === false)
|
|
616
|
+
return "signed-out";
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
return "unknown";
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
codex: {
|
|
623
|
+
name: "Codex",
|
|
624
|
+
binary: "codex",
|
|
625
|
+
args: (prompt) => ["exec", "--json", "-s", "workspace-write", prompt],
|
|
626
|
+
terminalArgs: (prompt) => ["exec", "-s", "workspace-write", prompt],
|
|
627
|
+
// 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
|
+
resumeArgs: (sessionId, prompt) => [
|
|
630
|
+
"exec",
|
|
631
|
+
"resume",
|
|
632
|
+
sessionId,
|
|
633
|
+
"--json",
|
|
634
|
+
prompt
|
|
635
|
+
],
|
|
636
|
+
sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
|
|
637
|
+
authArgs: ["login", "status"],
|
|
638
|
+
// `codex login status` exits 0 when logged in and nonzero when not.
|
|
639
|
+
authVerdict: (result2) => result2.code === 0 ? "ok" : "signed-out"
|
|
640
|
+
},
|
|
641
|
+
cursor: {
|
|
642
|
+
name: "Cursor",
|
|
643
|
+
binary: "cursor-agent",
|
|
644
|
+
args: (prompt) => ["-p", prompt, "--output-format", "stream-json"],
|
|
645
|
+
terminalArgs: (prompt) => ["-p", prompt],
|
|
646
|
+
authArgs: ["status"],
|
|
647
|
+
// UNVERIFIED: cursor-agent was not available on the build machine. The
|
|
648
|
+
// reading is deliberately loose, and anything ambiguous stays unknown.
|
|
649
|
+
authVerdict: (result2) => {
|
|
650
|
+
if (/logged in|signed in/i.test(result2.stdout))
|
|
651
|
+
return "ok";
|
|
652
|
+
if (result2.code !== 0 || /not logged in|log in|sign in/i.test(result2.stdout))
|
|
653
|
+
return "signed-out";
|
|
654
|
+
return "unknown";
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
659
|
+
function execProbe(binary, args) {
|
|
660
|
+
return new Promise((resolve) => {
|
|
661
|
+
let child;
|
|
662
|
+
try {
|
|
663
|
+
child = spawn(binary, [...args], { shell: false, stdio: ["ignore", "pipe", "ignore"] });
|
|
664
|
+
} catch {
|
|
665
|
+
return resolve(null);
|
|
666
|
+
}
|
|
667
|
+
let stdout = "";
|
|
668
|
+
child.stdout?.on("data", (chunk) => {
|
|
669
|
+
if (stdout.length < 4096)
|
|
670
|
+
stdout += chunk.toString();
|
|
671
|
+
});
|
|
672
|
+
const deadline = setTimeout(() => child.kill("SIGKILL"), PROBE_TIMEOUT_MS);
|
|
673
|
+
child.once("error", () => {
|
|
674
|
+
clearTimeout(deadline);
|
|
675
|
+
resolve(null);
|
|
676
|
+
});
|
|
677
|
+
child.once("close", (code, signal) => {
|
|
678
|
+
clearTimeout(deadline);
|
|
679
|
+
resolve(signal !== null ? null : { code: code ?? 0, stdout });
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
async function pathLookup(binary) {
|
|
684
|
+
const entries = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
|
|
685
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
686
|
+
for (const entry of entries) {
|
|
687
|
+
for (const extension of extensions) {
|
|
688
|
+
try {
|
|
689
|
+
await access(join(entry, `${binary}${extension}`), constants.X_OK);
|
|
690
|
+
return true;
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
|
|
698
|
+
const entries = Object.entries(KNOWN_AGENTS);
|
|
699
|
+
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
700
|
+
const available = await lookup(adapter.binary).catch(() => false);
|
|
701
|
+
if (!available)
|
|
702
|
+
return { id, name: adapter.name, available, auth: "unknown" };
|
|
703
|
+
const result2 = await probe2(adapter.binary, adapter.authArgs).catch(() => null);
|
|
704
|
+
return {
|
|
705
|
+
id,
|
|
706
|
+
name: adapter.name,
|
|
707
|
+
available,
|
|
708
|
+
auth: result2 === null ? "unknown" : adapter.authVerdict(result2)
|
|
709
|
+
};
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
function record(value) {
|
|
713
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
714
|
+
}
|
|
715
|
+
function shownPath(value, cwd) {
|
|
716
|
+
if (typeof value !== "string" || value === "")
|
|
717
|
+
return null;
|
|
718
|
+
if (!isAbsolute(value))
|
|
719
|
+
return value;
|
|
720
|
+
return relative(cwd, value) || ".";
|
|
721
|
+
}
|
|
722
|
+
function shownCommand(value) {
|
|
723
|
+
let command = Array.isArray(value) ? value.filter((part) => typeof part === "string").join(" ") : typeof value === "string" ? value : "";
|
|
724
|
+
command = command.trim();
|
|
725
|
+
const wrapped = /^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+([\s\S]*)$/.exec(command);
|
|
726
|
+
if (wrapped?.[1] !== void 0) {
|
|
727
|
+
command = wrapped[1].trim();
|
|
728
|
+
const quote = command[0];
|
|
729
|
+
if ((quote === "'" || quote === '"') && command.endsWith(quote) && command.length > 1) {
|
|
730
|
+
command = command.slice(1, -1);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
command = (command.split("\n")[0] ?? "").replace(/\s+/g, " ").trim();
|
|
734
|
+
if (command === "")
|
|
735
|
+
return null;
|
|
736
|
+
return command.length > 48 ? `${command.slice(0, 47)}\u2026` : command;
|
|
737
|
+
}
|
|
738
|
+
function claudeActivity(event, cwd) {
|
|
739
|
+
if (event.type !== "assistant")
|
|
740
|
+
return null;
|
|
741
|
+
const message = record(event.message);
|
|
742
|
+
if (message === null || !Array.isArray(message.content))
|
|
743
|
+
return null;
|
|
744
|
+
for (const rawBlock of message.content) {
|
|
745
|
+
const block = record(rawBlock);
|
|
746
|
+
if (block?.type !== "tool_use" || typeof block.name !== "string")
|
|
747
|
+
continue;
|
|
748
|
+
const input = record(block.input);
|
|
749
|
+
if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name)) {
|
|
750
|
+
const path = shownPath(input?.file_path ?? input?.notebook_path, cwd);
|
|
751
|
+
return path === null ? `using ${block.name}` : `editing ${path}`;
|
|
752
|
+
}
|
|
753
|
+
if (block.name === "Read") {
|
|
754
|
+
const path = shownPath(input?.file_path ?? input?.path, cwd);
|
|
755
|
+
return path === null ? "using Read" : `reading ${path}`;
|
|
756
|
+
}
|
|
757
|
+
if (block.name === "Bash") {
|
|
758
|
+
const command = shownCommand(input?.command);
|
|
759
|
+
return command === null ? "running a command" : `running ${command}`;
|
|
760
|
+
}
|
|
761
|
+
if (block.name === "Grep" || block.name === "Glob")
|
|
762
|
+
return "searching the project";
|
|
763
|
+
return `using ${block.name}`;
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
function codexActivity(event, cwd) {
|
|
768
|
+
if (event.type !== "item.started" && event.type !== "item.completed")
|
|
769
|
+
return null;
|
|
770
|
+
const item = record(event.item);
|
|
771
|
+
if (item === null)
|
|
772
|
+
return null;
|
|
773
|
+
if (item.type === "command_execution") {
|
|
774
|
+
const command = shownCommand(item.command);
|
|
775
|
+
return command === null ? "running a command" : `running ${command}`;
|
|
776
|
+
}
|
|
777
|
+
if (item.type !== "file_change")
|
|
778
|
+
return null;
|
|
779
|
+
const first = Array.isArray(item.changes) ? record(item.changes[0]) : null;
|
|
780
|
+
const path = shownPath(first?.path ?? item.path, cwd);
|
|
781
|
+
return path === null ? null : `editing ${path}`;
|
|
782
|
+
}
|
|
783
|
+
function activityFrom(agent, line, cwd = process.cwd()) {
|
|
784
|
+
let event;
|
|
785
|
+
try {
|
|
786
|
+
event = record(JSON.parse(line));
|
|
787
|
+
} catch {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
if (event === null)
|
|
791
|
+
return null;
|
|
792
|
+
if (agent === "claude")
|
|
793
|
+
return claudeActivity(event, cwd);
|
|
794
|
+
if (agent === "codex")
|
|
795
|
+
return codexActivity(event, cwd);
|
|
796
|
+
if (agent === "cursor")
|
|
797
|
+
return claudeActivity(event, cwd);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
function sessionFrom(agent, line) {
|
|
801
|
+
if (agent !== "claude" && agent !== "codex")
|
|
802
|
+
return null;
|
|
803
|
+
let event;
|
|
804
|
+
try {
|
|
805
|
+
event = record(JSON.parse(line));
|
|
806
|
+
} catch {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
if (event === null)
|
|
810
|
+
return null;
|
|
811
|
+
return KNOWN_AGENTS[agent].sessionFrom(event);
|
|
812
|
+
}
|
|
813
|
+
function isAgentChoice(value) {
|
|
814
|
+
return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
815
|
+
}
|
|
816
|
+
async function readWatchConfig(cwd) {
|
|
817
|
+
try {
|
|
818
|
+
const parsed2 = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
|
|
819
|
+
return record(parsed2) ?? {};
|
|
820
|
+
} catch {
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function readAgentChoice(cwd) {
|
|
825
|
+
const config = await readWatchConfig(cwd);
|
|
826
|
+
return {
|
|
827
|
+
agent: isAgentChoice(config.agent) ? config.agent : null,
|
|
828
|
+
run: typeof config.run === "string" && config.run !== "" ? config.run : null
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
async function saveAgentChoice(cwd, choice) {
|
|
832
|
+
const config = await readWatchConfig(cwd);
|
|
833
|
+
config.agent = choice.agent;
|
|
834
|
+
if (choice.run !== void 0)
|
|
835
|
+
config.run = choice.run;
|
|
836
|
+
const path = join(cwd, WATCH_PATH);
|
|
837
|
+
await mkdir(dirname(path), { recursive: true });
|
|
838
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
839
|
+
`, "utf8");
|
|
840
|
+
}
|
|
841
|
+
|
|
488
842
|
// ../server/dist/classify.js
|
|
489
843
|
var MANIFESTS = /* @__PURE__ */ new Set([
|
|
490
844
|
"package.json",
|
|
@@ -552,7 +906,7 @@ function classifyDirection(input) {
|
|
|
552
906
|
|
|
553
907
|
// ../server/dist/find-config.js
|
|
554
908
|
import { existsSync } from "fs";
|
|
555
|
-
import { dirname, join, parse } from "path";
|
|
909
|
+
import { dirname as dirname2, join as join2, parse } from "path";
|
|
556
910
|
var CONFIG_BASENAMES = [
|
|
557
911
|
"leglas.config.ts",
|
|
558
912
|
"leglas.config.mjs",
|
|
@@ -564,13 +918,13 @@ function findConfigFile(startDir) {
|
|
|
564
918
|
let dir = startDir;
|
|
565
919
|
for (; ; ) {
|
|
566
920
|
for (const basename4 of CONFIG_BASENAMES) {
|
|
567
|
-
const candidate =
|
|
921
|
+
const candidate = join2(dir, basename4);
|
|
568
922
|
if (existsSync(candidate))
|
|
569
923
|
return candidate;
|
|
570
924
|
}
|
|
571
925
|
if (dir === root)
|
|
572
926
|
return null;
|
|
573
|
-
const parent =
|
|
927
|
+
const parent = dirname2(dir);
|
|
574
928
|
if (parent === dir)
|
|
575
929
|
return null;
|
|
576
930
|
dir = parent;
|
|
@@ -578,19 +932,19 @@ function findConfigFile(startDir) {
|
|
|
578
932
|
}
|
|
579
933
|
|
|
580
934
|
// ../server/dist/load-config.js
|
|
581
|
-
import { readFile } from "fs/promises";
|
|
582
|
-
import { relative } from "path";
|
|
935
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
936
|
+
import { relative as relative2 } from "path";
|
|
583
937
|
import { pathToFileURL } from "url";
|
|
584
938
|
async function loadConfig(cwd) {
|
|
585
939
|
const path = findConfigFile(cwd);
|
|
586
940
|
if (path === null) {
|
|
587
941
|
return { ...normalizeConfig(void 0), path: null };
|
|
588
942
|
}
|
|
589
|
-
const label =
|
|
943
|
+
const label = relative2(cwd, path) || path;
|
|
590
944
|
let exported;
|
|
591
945
|
try {
|
|
592
946
|
if (path.endsWith(".json")) {
|
|
593
|
-
exported = JSON.parse(await
|
|
947
|
+
exported = JSON.parse(await readFile2(path, "utf8"));
|
|
594
948
|
} else {
|
|
595
949
|
const module = await import(pathToFileURL(path).href);
|
|
596
950
|
if (!("default" in module)) {
|
|
@@ -611,16 +965,25 @@ async function loadConfig(cwd) {
|
|
|
611
965
|
}
|
|
612
966
|
|
|
613
967
|
// ../server/dist/local-previews.js
|
|
614
|
-
import { mkdir, readFile as
|
|
615
|
-
import { dirname as
|
|
968
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
969
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
616
970
|
var LOCAL_PREVIEWS_PATH = ".leglas/previews.json";
|
|
617
971
|
async function readLocalPreviews(cwd) {
|
|
618
|
-
const path =
|
|
972
|
+
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
619
973
|
let raw;
|
|
620
974
|
try {
|
|
621
|
-
raw = await
|
|
622
|
-
} catch {
|
|
623
|
-
|
|
975
|
+
raw = await readFile3(path, "utf8");
|
|
976
|
+
} catch (error) {
|
|
977
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
978
|
+
if (code === "ENOENT") {
|
|
979
|
+
return { previews: [], errors: [] };
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
previews: [],
|
|
983
|
+
errors: [
|
|
984
|
+
`${LOCAL_PREVIEWS_PATH} could not be read (${code ?? "unknown error"}). Check its permissions and file type; nothing shared is lost.`
|
|
985
|
+
]
|
|
986
|
+
};
|
|
624
987
|
}
|
|
625
988
|
let parsed2;
|
|
626
989
|
try {
|
|
@@ -667,9 +1030,9 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
667
1030
|
if (check.config === null) {
|
|
668
1031
|
return { ok: false, error: check.errors.join(" ") };
|
|
669
1032
|
}
|
|
670
|
-
const path =
|
|
671
|
-
await
|
|
672
|
-
await
|
|
1033
|
+
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
1034
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
1035
|
+
await writeFile2(path, `${JSON.stringify({ previews: [...existing.previews.map(toStored), candidate] }, null, 2)}
|
|
673
1036
|
`, "utf8");
|
|
674
1037
|
return { ok: true };
|
|
675
1038
|
}
|
|
@@ -682,9 +1045,9 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
682
1045
|
const keep = existing.previews.filter((preview) => !titles.includes(preview.title));
|
|
683
1046
|
if (keep.length === existing.previews.length)
|
|
684
1047
|
return 0;
|
|
685
|
-
const path =
|
|
686
|
-
await
|
|
687
|
-
await
|
|
1048
|
+
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
1049
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
1050
|
+
await writeFile2(path, `${JSON.stringify({ previews: keep.map(toStored) }, null, 2)}
|
|
688
1051
|
`, "utf8");
|
|
689
1052
|
return existing.previews.length - keep.length;
|
|
690
1053
|
}
|
|
@@ -753,10 +1116,10 @@ ${headers}\r
|
|
|
753
1116
|
}
|
|
754
1117
|
|
|
755
1118
|
// ../server/dist/worktree.js
|
|
756
|
-
import { execFile, spawn } from "child_process";
|
|
1119
|
+
import { execFile, spawn as spawn2 } from "child_process";
|
|
757
1120
|
import { rm } from "fs/promises";
|
|
758
1121
|
import net2 from "net";
|
|
759
|
-
import { join as
|
|
1122
|
+
import { join as join4 } from "path";
|
|
760
1123
|
import { promisify } from "util";
|
|
761
1124
|
var run = promisify(execFile);
|
|
762
1125
|
var WORKTREES_DIR = ".leglas/worktrees";
|
|
@@ -793,7 +1156,7 @@ function answers(port) {
|
|
|
793
1156
|
}
|
|
794
1157
|
async function startWorktree(options) {
|
|
795
1158
|
const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
|
|
796
|
-
const path =
|
|
1159
|
+
const path = join4(options.cwd, WORKTREES_DIR, worktreeSlug(options.branch));
|
|
797
1160
|
const log = options.onLog ?? (() => {
|
|
798
1161
|
});
|
|
799
1162
|
await rm(path, { recursive: true, force: true });
|
|
@@ -854,7 +1217,7 @@ async function startAppProcess(options) {
|
|
|
854
1217
|
const port = await freePort();
|
|
855
1218
|
let child;
|
|
856
1219
|
try {
|
|
857
|
-
child =
|
|
1220
|
+
child = spawn2(substitutePort(options.devCommand, port), {
|
|
858
1221
|
cwd: options.cwd,
|
|
859
1222
|
shell: true,
|
|
860
1223
|
// Own process group, so stopping kills the shell and whatever it spawned
|
|
@@ -894,9 +1257,9 @@ async function startAppProcess(options) {
|
|
|
894
1257
|
}
|
|
895
1258
|
|
|
896
1259
|
// ../server/dist/requests.js
|
|
897
|
-
import { mkdir as
|
|
1260
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
898
1261
|
import { randomBytes } from "crypto";
|
|
899
|
-
import { dirname as
|
|
1262
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
900
1263
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
901
1264
|
function targetFor(url) {
|
|
902
1265
|
if (!url.startsWith("/"))
|
|
@@ -922,17 +1285,20 @@ function composeRequest(preview, intent) {
|
|
|
922
1285
|
const target = preview.file ?? targetFor(preview.url);
|
|
923
1286
|
const cleaned = intent.trim();
|
|
924
1287
|
const where = target === null ? `The direction is titled "${preview.title}" and renders at ${preview.url}. Find what produces it.` : `It lives at ${target}.`;
|
|
1288
|
+
const pace = target === null ? `Once found, make the change and finish. ` : `Make the change in that file and finish. `;
|
|
925
1289
|
const prompt = `In this project, change only the "${preview.title}" design direction. ${where}
|
|
926
1290
|
|
|
927
1291
|
What to change: ${cleaned}
|
|
928
1292
|
|
|
1293
|
+
${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.
|
|
1294
|
+
|
|
929
1295
|
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.`;
|
|
930
1296
|
return { prompt, target };
|
|
931
1297
|
}
|
|
932
1298
|
var REQUESTS_PATH = ".leglas/requests.json";
|
|
933
1299
|
async function readRequests(cwd) {
|
|
934
1300
|
try {
|
|
935
|
-
const raw = await
|
|
1301
|
+
const raw = await readFile4(join5(cwd, REQUESTS_PATH), "utf8");
|
|
936
1302
|
const parsed2 = JSON.parse(raw);
|
|
937
1303
|
if (!Array.isArray(parsed2.requests))
|
|
938
1304
|
return [];
|
|
@@ -949,9 +1315,9 @@ async function readRequests(cwd) {
|
|
|
949
1315
|
}
|
|
950
1316
|
}
|
|
951
1317
|
async function writeQueue(cwd, requests) {
|
|
952
|
-
const path =
|
|
953
|
-
await
|
|
954
|
-
await
|
|
1318
|
+
const path = join5(cwd, REQUESTS_PATH);
|
|
1319
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1320
|
+
await writeFile3(path, `${JSON.stringify({ requests }, null, 2)}
|
|
955
1321
|
`, "utf8");
|
|
956
1322
|
}
|
|
957
1323
|
async function appendRequest(cwd, request) {
|
|
@@ -991,13 +1357,264 @@ async function clearRequests(cwd) {
|
|
|
991
1357
|
return { cleared, pending: pending.length };
|
|
992
1358
|
}
|
|
993
1359
|
|
|
1360
|
+
// ../server/dist/runner.js
|
|
1361
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
1362
|
+
var POLL_MS = 2e3;
|
|
1363
|
+
var OUTPUT_LINES = 20;
|
|
1364
|
+
var SESSION_TURNS_CAP = 8;
|
|
1365
|
+
function resolveCommand(choice, prompt, sessionId = null) {
|
|
1366
|
+
if (choice.agent === null)
|
|
1367
|
+
return null;
|
|
1368
|
+
if (choice.agent === "custom") {
|
|
1369
|
+
if (choice.run === null)
|
|
1370
|
+
return null;
|
|
1371
|
+
const parsed2 = parseTemplate(choice.run);
|
|
1372
|
+
if (!parsed2.ok)
|
|
1373
|
+
return null;
|
|
1374
|
+
return { agent: "custom", name: "Custom", ...commandFor(parsed2.template, prompt), resumed: false };
|
|
1375
|
+
}
|
|
1376
|
+
const adapter = KNOWN_AGENTS[choice.agent];
|
|
1377
|
+
if (sessionId !== null && "resumeArgs" in adapter) {
|
|
1378
|
+
return {
|
|
1379
|
+
agent: choice.agent,
|
|
1380
|
+
name: adapter.name,
|
|
1381
|
+
command: adapter.binary,
|
|
1382
|
+
args: adapter.resumeArgs(sessionId, prompt),
|
|
1383
|
+
resumed: true
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
agent: choice.agent,
|
|
1388
|
+
name: adapter.name,
|
|
1389
|
+
command: adapter.binary,
|
|
1390
|
+
args: adapter.args(prompt),
|
|
1391
|
+
resumed: false
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function lineReader(stream, onLine) {
|
|
1395
|
+
let buffered = "";
|
|
1396
|
+
const flush = () => {
|
|
1397
|
+
if (buffered === "")
|
|
1398
|
+
return;
|
|
1399
|
+
onLine(buffered.replace(/\r$/, ""));
|
|
1400
|
+
buffered = "";
|
|
1401
|
+
};
|
|
1402
|
+
stream.on("data", (chunk) => {
|
|
1403
|
+
buffered += chunk.toString();
|
|
1404
|
+
const lines = buffered.split("\n");
|
|
1405
|
+
buffered = lines.pop() ?? "";
|
|
1406
|
+
for (const line of lines)
|
|
1407
|
+
onLine(line.replace(/\r$/, ""));
|
|
1408
|
+
});
|
|
1409
|
+
stream.on("end", flush);
|
|
1410
|
+
return flush;
|
|
1411
|
+
}
|
|
1412
|
+
function defaultSpawn(command, args, options) {
|
|
1413
|
+
return nodeSpawn(command, args, options);
|
|
1414
|
+
}
|
|
1415
|
+
function startRunner(options) {
|
|
1416
|
+
const spawn5 = options.spawn ?? defaultSpawn;
|
|
1417
|
+
const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
1418
|
+
const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
|
|
1419
|
+
const failed = /* @__PURE__ */ new Set();
|
|
1420
|
+
let state = {
|
|
1421
|
+
running: false,
|
|
1422
|
+
requestId: null,
|
|
1423
|
+
agent: null,
|
|
1424
|
+
activity: null,
|
|
1425
|
+
startedAt: null
|
|
1426
|
+
};
|
|
1427
|
+
let stopped = false;
|
|
1428
|
+
let ticking = null;
|
|
1429
|
+
let stopPromise = null;
|
|
1430
|
+
let active = null;
|
|
1431
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1432
|
+
const idle = () => {
|
|
1433
|
+
state = { running: false, requestId: null, agent: null, activity: null, startedAt: null };
|
|
1434
|
+
};
|
|
1435
|
+
const rememberLine = (lines, line) => {
|
|
1436
|
+
lines.push(line);
|
|
1437
|
+
if (lines.length > OUTPUT_LINES)
|
|
1438
|
+
lines.splice(0, lines.length - OUTPUT_LINES);
|
|
1439
|
+
};
|
|
1440
|
+
const reportFailure = (request, error, lines) => {
|
|
1441
|
+
console.error(`Leglas agent failed for ${request.title}: ${error}`);
|
|
1442
|
+
for (const line of lines)
|
|
1443
|
+
console.error(` ${line}`);
|
|
1444
|
+
};
|
|
1445
|
+
const runChild = (request, resolved, lines, observed) => {
|
|
1446
|
+
let child;
|
|
1447
|
+
try {
|
|
1448
|
+
child = spawn5(resolved.command, resolved.args, {
|
|
1449
|
+
cwd: options.cwd,
|
|
1450
|
+
shell: false,
|
|
1451
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1452
|
+
});
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
return Promise.resolve({
|
|
1455
|
+
ok: false,
|
|
1456
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
const current = { child, requestId: request.id, cancelled: false };
|
|
1460
|
+
active = current;
|
|
1461
|
+
const stdoutFlush = lineReader(child.stdout, (line) => {
|
|
1462
|
+
rememberLine(lines, line);
|
|
1463
|
+
const sessionId = sessionFrom(resolved.agent, line);
|
|
1464
|
+
if (sessionId !== null)
|
|
1465
|
+
observed.sessionId = sessionId;
|
|
1466
|
+
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
1467
|
+
if (activity !== null) {
|
|
1468
|
+
if (activity.startsWith("editing"))
|
|
1469
|
+
observed.edited = true;
|
|
1470
|
+
if (active === current)
|
|
1471
|
+
state = { ...state, activity };
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines, line));
|
|
1475
|
+
return new Promise((resolve) => {
|
|
1476
|
+
let settled = false;
|
|
1477
|
+
const settle = (outcome) => {
|
|
1478
|
+
if (settled)
|
|
1479
|
+
return;
|
|
1480
|
+
settled = true;
|
|
1481
|
+
stdoutFlush();
|
|
1482
|
+
stderrFlush();
|
|
1483
|
+
resolve(outcome);
|
|
1484
|
+
};
|
|
1485
|
+
child.once("error", (error) => settle({ ok: false, error: error.message }));
|
|
1486
|
+
child.once("close", (code, signal) => {
|
|
1487
|
+
if (current.cancelled)
|
|
1488
|
+
return settle({ ok: false, error: "cancelled" });
|
|
1489
|
+
if (signal !== null)
|
|
1490
|
+
return settle({ ok: false, error: `stopped by ${signal}` });
|
|
1491
|
+
settle({ ok: true, code: code ?? 0 });
|
|
1492
|
+
});
|
|
1493
|
+
}).finally(() => {
|
|
1494
|
+
if (active === current)
|
|
1495
|
+
active = null;
|
|
1496
|
+
});
|
|
1497
|
+
};
|
|
1498
|
+
const handle = async (request, choice) => {
|
|
1499
|
+
const session = choice.agent !== null ? sessions.get(choice.agent) ?? null : null;
|
|
1500
|
+
const continuable = session !== null && session.turns < SESSION_TURNS_CAP;
|
|
1501
|
+
let resolved = resolveCommand(choice, request.prompt, continuable ? session.id : null);
|
|
1502
|
+
if (resolved === null)
|
|
1503
|
+
return;
|
|
1504
|
+
const lines = [];
|
|
1505
|
+
try {
|
|
1506
|
+
if (!await markPickedUp(options.cwd, request.id))
|
|
1507
|
+
return;
|
|
1508
|
+
if (stopped) {
|
|
1509
|
+
failed.add(request.id);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
state = {
|
|
1513
|
+
running: true,
|
|
1514
|
+
requestId: request.id,
|
|
1515
|
+
agent: resolved.name,
|
|
1516
|
+
activity: null,
|
|
1517
|
+
startedAt: Date.now()
|
|
1518
|
+
};
|
|
1519
|
+
const observed = { sessionId: null, edited: false };
|
|
1520
|
+
let outcome = await runChild(request, resolved, lines, observed);
|
|
1521
|
+
const cancelled = !outcome.ok && outcome.error === "cancelled";
|
|
1522
|
+
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && !cancelled && // Not redundant with the line above: a stop that lands between the
|
|
1523
|
+
// first child settling and the retry starting finds no child to
|
|
1524
|
+
// cancel, so nothing says "cancelled". Stopped still means stopped.
|
|
1525
|
+
!stopped) {
|
|
1526
|
+
sessions.delete(resolved.agent);
|
|
1527
|
+
const cold = resolveCommand(choice, request.prompt);
|
|
1528
|
+
if (cold !== null) {
|
|
1529
|
+
resolved = cold;
|
|
1530
|
+
observed.sessionId = null;
|
|
1531
|
+
state = { ...state, activity: null };
|
|
1532
|
+
outcome = await runChild(request, resolved, lines, observed);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (outcome.ok && outcome.code === 0) {
|
|
1536
|
+
if (observed.sessionId !== null) {
|
|
1537
|
+
const previous = sessions.get(resolved.agent);
|
|
1538
|
+
sessions.set(resolved.agent, {
|
|
1539
|
+
id: observed.sessionId,
|
|
1540
|
+
turns: resolved.resumed && previous?.id === observed.sessionId ? previous.turns + 1 : 1
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
await removeRequest(options.cwd, request.id);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
sessions.delete(resolved.agent);
|
|
1547
|
+
failed.add(request.id);
|
|
1548
|
+
reportFailure(request, outcome.ok ? `${resolved.command} exited ${outcome.code}` : outcome.error, lines);
|
|
1549
|
+
} finally {
|
|
1550
|
+
idle();
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
const tick = async () => {
|
|
1554
|
+
if (stopped)
|
|
1555
|
+
return;
|
|
1556
|
+
const choice = await readAgentChoice(options.cwd);
|
|
1557
|
+
if (choice.agent === null || stopped)
|
|
1558
|
+
return;
|
|
1559
|
+
if (options.externallyAttached())
|
|
1560
|
+
return;
|
|
1561
|
+
const request = nextRequest(await readRequests(options.cwd), failed);
|
|
1562
|
+
if (request !== null && !stopped)
|
|
1563
|
+
await handle(request, choice);
|
|
1564
|
+
};
|
|
1565
|
+
const schedule = () => {
|
|
1566
|
+
if (stopped || ticking !== null)
|
|
1567
|
+
return;
|
|
1568
|
+
const task = tick();
|
|
1569
|
+
ticking = task;
|
|
1570
|
+
void task.catch((error) => console.error(`Leglas runner: ${error instanceof Error ? error.message : String(error)}`)).finally(() => {
|
|
1571
|
+
if (ticking === task)
|
|
1572
|
+
ticking = null;
|
|
1573
|
+
});
|
|
1574
|
+
};
|
|
1575
|
+
const timer = setEvery(schedule, POLL_MS);
|
|
1576
|
+
schedule();
|
|
1577
|
+
const cancel = (id) => {
|
|
1578
|
+
if (active === null || active.cancelled)
|
|
1579
|
+
return false;
|
|
1580
|
+
if (id !== void 0 && active.requestId !== id)
|
|
1581
|
+
return false;
|
|
1582
|
+
active.cancelled = true;
|
|
1583
|
+
failed.add(active.requestId);
|
|
1584
|
+
try {
|
|
1585
|
+
active.child.kill("SIGTERM");
|
|
1586
|
+
} catch {
|
|
1587
|
+
}
|
|
1588
|
+
return true;
|
|
1589
|
+
};
|
|
1590
|
+
const stop = () => {
|
|
1591
|
+
if (stopPromise !== null)
|
|
1592
|
+
return stopPromise;
|
|
1593
|
+
stopped = true;
|
|
1594
|
+
clearEvery(timer);
|
|
1595
|
+
cancel();
|
|
1596
|
+
stopPromise = Promise.resolve(ticking).catch(() => {
|
|
1597
|
+
}).then(() => {
|
|
1598
|
+
});
|
|
1599
|
+
return stopPromise;
|
|
1600
|
+
};
|
|
1601
|
+
return {
|
|
1602
|
+
stop,
|
|
1603
|
+
snapshot: () => ({ ...state, failedIds: [...failed] }),
|
|
1604
|
+
cancel,
|
|
1605
|
+
// schedule already refuses to overlap a tick in flight, so a nudge during
|
|
1606
|
+
// a run costs nothing and a nudge between runs starts the next one now.
|
|
1607
|
+
nudge: schedule
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
994
1611
|
// ../server/dist/renames.js
|
|
995
|
-
import { mkdir as
|
|
996
|
-
import { dirname as
|
|
1612
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1613
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
997
1614
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
998
1615
|
async function readRenames(cwd) {
|
|
999
1616
|
try {
|
|
1000
|
-
const raw = await
|
|
1617
|
+
const raw = await readFile5(join6(cwd, RENAMES_PATH), "utf8");
|
|
1001
1618
|
const parsed2 = JSON.parse(raw);
|
|
1002
1619
|
if (parsed2.renames === null || typeof parsed2.renames !== "object")
|
|
1003
1620
|
return {};
|
|
@@ -1007,9 +1624,9 @@ async function readRenames(cwd) {
|
|
|
1007
1624
|
}
|
|
1008
1625
|
}
|
|
1009
1626
|
async function writeRenames(cwd, renames) {
|
|
1010
|
-
const path =
|
|
1011
|
-
await
|
|
1012
|
-
await
|
|
1627
|
+
const path = join6(cwd, RENAMES_PATH);
|
|
1628
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
1629
|
+
await writeFile4(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1013
1630
|
`, "utf8");
|
|
1014
1631
|
}
|
|
1015
1632
|
function resolveTitle(input, titles, renames) {
|
|
@@ -1027,7 +1644,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
1027
1644
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1028
1645
|
import http2 from "http";
|
|
1029
1646
|
import net3 from "net";
|
|
1030
|
-
import { extname, join as
|
|
1647
|
+
import { extname, join as join7, normalize, relative as relative3 } from "path";
|
|
1031
1648
|
var LEGLAS_PREFIX = "/leglas";
|
|
1032
1649
|
var DEFAULT_PORT = 4100;
|
|
1033
1650
|
var PORT_ATTEMPTS = 20;
|
|
@@ -1058,6 +1675,52 @@ function sendJson(res, status, body) {
|
|
|
1058
1675
|
});
|
|
1059
1676
|
res.end(payload);
|
|
1060
1677
|
}
|
|
1678
|
+
function isKnownAgent(value) {
|
|
1679
|
+
return typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
|
|
1680
|
+
}
|
|
1681
|
+
function isAllowedMutationHost(hostname) {
|
|
1682
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1683
|
+
if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1")
|
|
1684
|
+
return true;
|
|
1685
|
+
if (bare.endsWith(".local"))
|
|
1686
|
+
return true;
|
|
1687
|
+
if (!net3.isIPv4(bare))
|
|
1688
|
+
return false;
|
|
1689
|
+
const [first, second] = bare.split(".").map(Number);
|
|
1690
|
+
return first === 10 || first === 172 && second !== void 0 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
1691
|
+
}
|
|
1692
|
+
function isLoopbackAddress(address) {
|
|
1693
|
+
if (address === void 0)
|
|
1694
|
+
return false;
|
|
1695
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1" || address.startsWith("127.");
|
|
1696
|
+
}
|
|
1697
|
+
function isTrustedMutation(req) {
|
|
1698
|
+
if (!isLoopbackAddress(req.socket.remoteAddress))
|
|
1699
|
+
return false;
|
|
1700
|
+
if (typeof req.headers.host !== "string")
|
|
1701
|
+
return false;
|
|
1702
|
+
let host;
|
|
1703
|
+
try {
|
|
1704
|
+
host = new URL(`http://${req.headers.host}`);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
if (!isAllowedMutationHost(host.hostname))
|
|
1709
|
+
return false;
|
|
1710
|
+
const rawOrigin = req.headers.origin;
|
|
1711
|
+
if (rawOrigin === void 0)
|
|
1712
|
+
return true;
|
|
1713
|
+
try {
|
|
1714
|
+
const origin = new URL(rawOrigin);
|
|
1715
|
+
return origin.protocol === "http:" && origin.host === host.host;
|
|
1716
|
+
} catch {
|
|
1717
|
+
return false;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function hasJsonBody(req) {
|
|
1721
|
+
const contentType = req.headers["content-type"];
|
|
1722
|
+
return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
1723
|
+
}
|
|
1061
1724
|
function probe(target, timeoutMs = 1e3) {
|
|
1062
1725
|
return new Promise((resolve) => {
|
|
1063
1726
|
let url;
|
|
@@ -1079,8 +1742,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
1079
1742
|
});
|
|
1080
1743
|
}
|
|
1081
1744
|
function serveFrom(res, dir, relativePath) {
|
|
1082
|
-
const
|
|
1083
|
-
const candidate =
|
|
1745
|
+
const relative5 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
1746
|
+
const candidate = join7(dir, relative5);
|
|
1084
1747
|
if (!candidate.startsWith(dir))
|
|
1085
1748
|
return false;
|
|
1086
1749
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -1093,9 +1756,9 @@ function serveFrom(res, dir, relativePath) {
|
|
|
1093
1756
|
return true;
|
|
1094
1757
|
}
|
|
1095
1758
|
function serveShellFile(res, shellDir, urlPath) {
|
|
1096
|
-
const
|
|
1097
|
-
const isRoot =
|
|
1098
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
1759
|
+
const relative5 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
1760
|
+
const isRoot = relative5 === "" || relative5 === "." || relative5 === "/";
|
|
1761
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative5);
|
|
1099
1762
|
}
|
|
1100
1763
|
function snapshotConfig(cwd) {
|
|
1101
1764
|
const path = findConfigFile(cwd);
|
|
@@ -1111,15 +1774,15 @@ function configStalenessNotice(cwd, boot, current) {
|
|
|
1111
1774
|
if (boot === null && current === null)
|
|
1112
1775
|
return null;
|
|
1113
1776
|
if (boot === null && current !== null) {
|
|
1114
|
-
const label =
|
|
1777
|
+
const label = relative3(cwd, current.path) || current.path;
|
|
1115
1778
|
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
1116
1779
|
}
|
|
1117
1780
|
if (boot !== null && current === null) {
|
|
1118
|
-
const label =
|
|
1781
|
+
const label = relative3(cwd, boot.path) || boot.path;
|
|
1119
1782
|
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
1120
1783
|
}
|
|
1121
1784
|
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
1122
|
-
const label =
|
|
1785
|
+
const label = relative3(cwd, current.path) || current.path;
|
|
1123
1786
|
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
1124
1787
|
}
|
|
1125
1788
|
return null;
|
|
@@ -1165,27 +1828,63 @@ async function bind(server, requested) {
|
|
|
1165
1828
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
1166
1829
|
}
|
|
1167
1830
|
async function startServer(options) {
|
|
1168
|
-
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map() } = options;
|
|
1831
|
+
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
1169
1832
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
1170
1833
|
const proxy = createProxyHandler({ target });
|
|
1171
1834
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
1172
1835
|
let lastSeen = null;
|
|
1836
|
+
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
1837
|
+
let runner = null;
|
|
1838
|
+
let agentsCache = null;
|
|
1839
|
+
let agentsInflight = null;
|
|
1840
|
+
const AGENTS_FRESH_MS = 3e4;
|
|
1841
|
+
const probeAgents = () => {
|
|
1842
|
+
agentsInflight ??= detect().then((agents) => {
|
|
1843
|
+
agentsCache = { at: Date.now(), agents };
|
|
1844
|
+
return agents;
|
|
1845
|
+
}).finally(() => {
|
|
1846
|
+
agentsInflight = null;
|
|
1847
|
+
});
|
|
1848
|
+
return agentsInflight;
|
|
1849
|
+
};
|
|
1850
|
+
const currentAgents = () => {
|
|
1851
|
+
if (agentsCache === null)
|
|
1852
|
+
return probeAgents();
|
|
1853
|
+
if (Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
|
|
1854
|
+
void probeAgents().catch(() => {
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
return Promise.resolve(agentsCache.agents);
|
|
1858
|
+
};
|
|
1173
1859
|
const server = http2.createServer((req, res) => {
|
|
1174
1860
|
const url = req.url ?? "/";
|
|
1175
1861
|
const path = url.split("?")[0] ?? "/";
|
|
1862
|
+
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
1863
|
+
return sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
1864
|
+
}
|
|
1176
1865
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
1177
1866
|
const boot = config?.previews ?? [];
|
|
1178
1867
|
const errors = [...configErrors];
|
|
1179
1868
|
const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
|
|
1180
1869
|
if (notice !== null)
|
|
1181
1870
|
errors.push(notice);
|
|
1182
|
-
return void readLocalPreviews(cwd).then(({ previews: local }) => {
|
|
1183
|
-
|
|
1871
|
+
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
1872
|
+
if (localErrors.length > 0) {
|
|
1873
|
+
return sendJson(res, 200, {
|
|
1874
|
+
project,
|
|
1875
|
+
devServer: target,
|
|
1876
|
+
previews: boot,
|
|
1877
|
+
errors
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
const localTitles = new Set(local.map((preview) => preview.title));
|
|
1881
|
+
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
1882
|
+
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
1184
1883
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
1185
1884
|
sendJson(res, 200, {
|
|
1186
1885
|
project,
|
|
1187
1886
|
devServer: target,
|
|
1188
|
-
previews: [...
|
|
1887
|
+
previews: [...currentBoot, ...fresh],
|
|
1189
1888
|
errors
|
|
1190
1889
|
});
|
|
1191
1890
|
}).catch(() => sendJson(res, 200, {
|
|
@@ -1195,6 +1894,47 @@ async function startServer(options) {
|
|
|
1195
1894
|
errors
|
|
1196
1895
|
}));
|
|
1197
1896
|
}
|
|
1897
|
+
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
1898
|
+
let body = "";
|
|
1899
|
+
req.on("data", (chunk) => body += chunk);
|
|
1900
|
+
return void req.on("end", async () => {
|
|
1901
|
+
let parsed2;
|
|
1902
|
+
try {
|
|
1903
|
+
parsed2 = JSON.parse(body || "{}");
|
|
1904
|
+
} catch {
|
|
1905
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1906
|
+
}
|
|
1907
|
+
const titles = parsed2.titles;
|
|
1908
|
+
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
1909
|
+
return sendJson(res, 400, {
|
|
1910
|
+
ok: false,
|
|
1911
|
+
error: "Body needs a non-empty array of direction titles."
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
const unique = [...new Set(titles)];
|
|
1915
|
+
try {
|
|
1916
|
+
const local = await readLocalPreviews(cwd);
|
|
1917
|
+
if (local.errors.length > 0) {
|
|
1918
|
+
return sendJson(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
1919
|
+
}
|
|
1920
|
+
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
1921
|
+
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
1922
|
+
if (unknown.length > 0) {
|
|
1923
|
+
return sendJson(res, 400, {
|
|
1924
|
+
ok: false,
|
|
1925
|
+
error: "Only machine-local directions can be deleted from the registry."
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
const deleted = await dropLocalPreviews(cwd, unique);
|
|
1929
|
+
return sendJson(res, 200, { ok: true, deleted });
|
|
1930
|
+
} catch {
|
|
1931
|
+
return sendJson(res, 500, {
|
|
1932
|
+
ok: false,
|
|
1933
|
+
error: "The directions could not be deleted from Leglas."
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1198
1938
|
if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
|
|
1199
1939
|
let body = "";
|
|
1200
1940
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -1205,8 +1945,12 @@ async function startServer(options) {
|
|
|
1205
1945
|
} catch {
|
|
1206
1946
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1207
1947
|
}
|
|
1208
|
-
const
|
|
1209
|
-
const
|
|
1948
|
+
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
1949
|
+
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
1950
|
+
const localTitles = new Set(local.map((entry) => entry.title));
|
|
1951
|
+
const bootConfig = config?.previews ?? [];
|
|
1952
|
+
const boot = localRead === null || localRead.errors.length > 0 ? bootConfig : bootConfig.filter((entry) => entry.local !== true || localTitles.has(entry.title));
|
|
1953
|
+
const preview = [...boot, ...local].find((entry) => entry.title === parsed2.title);
|
|
1210
1954
|
if (!preview || !parsed2.intent?.trim()) {
|
|
1211
1955
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
1212
1956
|
}
|
|
@@ -1216,7 +1960,54 @@ async function startServer(options) {
|
|
|
1216
1960
|
url: preview.url,
|
|
1217
1961
|
intent: parsed2.intent.trim(),
|
|
1218
1962
|
...composed
|
|
1219
|
-
}).then(() =>
|
|
1963
|
+
}).then(() => {
|
|
1964
|
+
runner?.nudge();
|
|
1965
|
+
sendJson(res, 200, { ok: true, ...composed });
|
|
1966
|
+
}).catch(() => sendJson(res, 200, { ok: true, ...composed, queued: false }));
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
1970
|
+
return void Promise.all([currentAgents(), readAgentChoice(cwd)]).then(([agents, choice]) => sendJson(res, 200, {
|
|
1971
|
+
agents,
|
|
1972
|
+
choice: choice.agent,
|
|
1973
|
+
customRun: choice.run
|
|
1974
|
+
}));
|
|
1975
|
+
}
|
|
1976
|
+
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
1977
|
+
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
1978
|
+
return sendJson(res, 403, {
|
|
1979
|
+
ok: false,
|
|
1980
|
+
error: "The agent choice can only be made from the machine running Leglas."
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
if (!hasJsonBody(req)) {
|
|
1984
|
+
return sendJson(res, 400, { ok: false, error: "Agent choice must be JSON." });
|
|
1985
|
+
}
|
|
1986
|
+
let body = "";
|
|
1987
|
+
req.on("data", (chunk) => body += chunk);
|
|
1988
|
+
return void req.on("end", () => {
|
|
1989
|
+
let parsed2;
|
|
1990
|
+
try {
|
|
1991
|
+
parsed2 = JSON.parse(body || "{}");
|
|
1992
|
+
} catch {
|
|
1993
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1994
|
+
}
|
|
1995
|
+
if (!isKnownAgent(parsed2.agent) && parsed2.agent !== "custom") {
|
|
1996
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a known agent." });
|
|
1997
|
+
}
|
|
1998
|
+
if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
|
|
1999
|
+
return sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
2000
|
+
}
|
|
2001
|
+
if (parsed2.agent === "custom") {
|
|
2002
|
+
if (typeof parsed2.run !== "string") {
|
|
2003
|
+
return sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
2004
|
+
}
|
|
2005
|
+
const template = parseTemplate(parsed2.run);
|
|
2006
|
+
if (!template.ok)
|
|
2007
|
+
return sendJson(res, 400, { ok: false, error: template.error });
|
|
2008
|
+
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
|
+
}
|
|
2010
|
+
return void saveAgentChoice(cwd, { agent: parsed2.agent }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
1220
2011
|
});
|
|
1221
2012
|
}
|
|
1222
2013
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -1237,11 +2028,119 @@ async function startServer(options) {
|
|
|
1237
2028
|
});
|
|
1238
2029
|
}
|
|
1239
2030
|
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
2031
|
+
const snapshot = runner?.snapshot() ?? {
|
|
2032
|
+
running: false,
|
|
2033
|
+
requestId: null,
|
|
2034
|
+
agent: null,
|
|
2035
|
+
activity: null,
|
|
2036
|
+
startedAt: null,
|
|
2037
|
+
failedIds: []
|
|
2038
|
+
};
|
|
1240
2039
|
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
1241
|
-
requests: requests.map(({ id, title, intent, status }) => ({
|
|
1242
|
-
|
|
2040
|
+
requests: requests.map(({ id, title, intent, status }) => ({
|
|
2041
|
+
id,
|
|
2042
|
+
title,
|
|
2043
|
+
intent,
|
|
2044
|
+
status: snapshot.running && snapshot.requestId === id ? "running" : snapshot.failedIds.includes(id) ? "failed" : status
|
|
2045
|
+
})),
|
|
2046
|
+
agent: {
|
|
2047
|
+
attached: externallyAttached(),
|
|
2048
|
+
running: snapshot.running,
|
|
2049
|
+
name: snapshot.running ? snapshot.agent : null,
|
|
2050
|
+
activity: snapshot.running ? snapshot.activity : null,
|
|
2051
|
+
startedAt: snapshot.running ? snapshot.startedAt : null
|
|
2052
|
+
}
|
|
1243
2053
|
}));
|
|
1244
2054
|
}
|
|
2055
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
2056
|
+
if (!hasJsonBody(req)) {
|
|
2057
|
+
return sendJson(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
|
|
2058
|
+
}
|
|
2059
|
+
let body = "";
|
|
2060
|
+
req.on("data", (chunk) => body += chunk);
|
|
2061
|
+
return void req.on("end", () => {
|
|
2062
|
+
let parsed2;
|
|
2063
|
+
try {
|
|
2064
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2065
|
+
} catch {
|
|
2066
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2067
|
+
}
|
|
2068
|
+
if (parsed2.id !== void 0 && typeof parsed2.id !== "string") {
|
|
2069
|
+
return sendJson(res, 400, { ok: false, error: "The request id must be a string." });
|
|
2070
|
+
}
|
|
2071
|
+
return sendJson(res, 200, { ok: true, cancelled: runner?.cancel(parsed2.id) ?? false });
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
2075
|
+
if (!hasJsonBody(req)) {
|
|
2076
|
+
return sendJson(res, 400, { ok: false, error: "Retry must be JSON." });
|
|
2077
|
+
}
|
|
2078
|
+
let body = "";
|
|
2079
|
+
req.on("data", (chunk) => body += chunk);
|
|
2080
|
+
return void req.on("end", async () => {
|
|
2081
|
+
let parsed2;
|
|
2082
|
+
try {
|
|
2083
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2084
|
+
} catch {
|
|
2085
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2086
|
+
}
|
|
2087
|
+
if (typeof parsed2.id !== "string") {
|
|
2088
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2089
|
+
}
|
|
2090
|
+
const request = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
2091
|
+
if (request === void 0) {
|
|
2092
|
+
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2093
|
+
}
|
|
2094
|
+
if (!(runner?.snapshot().failedIds.includes(request.id) ?? false)) {
|
|
2095
|
+
return sendJson(res, 400, { ok: false, error: "Only a failed request can be retried." });
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
if (!await removeRequest(cwd, request.id)) {
|
|
2099
|
+
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2100
|
+
}
|
|
2101
|
+
await appendRequest(cwd, {
|
|
2102
|
+
title: request.title,
|
|
2103
|
+
url: request.url,
|
|
2104
|
+
intent: request.intent,
|
|
2105
|
+
target: request.target,
|
|
2106
|
+
prompt: request.prompt
|
|
2107
|
+
});
|
|
2108
|
+
runner?.nudge();
|
|
2109
|
+
return sendJson(res, 200, { ok: true });
|
|
2110
|
+
} catch {
|
|
2111
|
+
return sendJson(res, 500, { ok: false, error: "The request could not be retried." });
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
2116
|
+
if (!hasJsonBody(req)) {
|
|
2117
|
+
return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
2118
|
+
}
|
|
2119
|
+
let body = "";
|
|
2120
|
+
req.on("data", (chunk) => body += chunk);
|
|
2121
|
+
return void req.on("end", async () => {
|
|
2122
|
+
let parsed2;
|
|
2123
|
+
try {
|
|
2124
|
+
parsed2 = JSON.parse(body || "{}");
|
|
2125
|
+
} catch {
|
|
2126
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
2127
|
+
}
|
|
2128
|
+
if (typeof parsed2.id !== "string") {
|
|
2129
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
|
|
2130
|
+
}
|
|
2131
|
+
if (!(runner?.snapshot().failedIds.includes(parsed2.id) ?? false)) {
|
|
2132
|
+
return sendJson(res, 400, { ok: false, error: "Only a failed request can be dismissed." });
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
if (!await removeRequest(cwd, parsed2.id)) {
|
|
2136
|
+
return sendJson(res, 404, { ok: false, error: "No such request." });
|
|
2137
|
+
}
|
|
2138
|
+
return sendJson(res, 200, { ok: true });
|
|
2139
|
+
} catch {
|
|
2140
|
+
return sendJson(res, 500, { ok: false, error: "The request could not be dismissed." });
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
1245
2144
|
if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
|
|
1246
2145
|
let body = "";
|
|
1247
2146
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -1266,14 +2165,14 @@ async function startServer(options) {
|
|
|
1266
2165
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
1267
2166
|
const slash = rest.indexOf("/");
|
|
1268
2167
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
1269
|
-
let
|
|
2168
|
+
let relative5 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
1270
2169
|
try {
|
|
1271
|
-
|
|
2170
|
+
relative5 = decodeURIComponent(relative5);
|
|
1272
2171
|
} catch {
|
|
1273
|
-
|
|
2172
|
+
relative5 = "";
|
|
1274
2173
|
}
|
|
1275
2174
|
const dir = fileMounts.get(slug);
|
|
1276
|
-
if (dir !== void 0 &&
|
|
2175
|
+
if (dir !== void 0 && relative5 !== "" && serveFrom(res, dir, relative5))
|
|
1277
2176
|
return;
|
|
1278
2177
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
1279
2178
|
return res.end("Leglas: no such preview file.");
|
|
@@ -1305,16 +2204,23 @@ async function startServer(options) {
|
|
|
1305
2204
|
proxy.upgrade(req, socket, head);
|
|
1306
2205
|
});
|
|
1307
2206
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
2207
|
+
runner = startRunner({ cwd, externallyAttached });
|
|
2208
|
+
let closePromise = null;
|
|
1308
2209
|
return {
|
|
1309
2210
|
port,
|
|
1310
2211
|
url: `http://localhost:${port}`,
|
|
1311
|
-
close: () =>
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
2212
|
+
close: () => {
|
|
2213
|
+
if (closePromise !== null)
|
|
2214
|
+
return closePromise;
|
|
2215
|
+
closePromise = runner.stop().then(() => new Promise((done) => {
|
|
2216
|
+
for (const socket of sockets)
|
|
2217
|
+
socket.destroy();
|
|
2218
|
+
sockets.clear();
|
|
2219
|
+
server.closeAllConnections();
|
|
2220
|
+
server.close(() => done());
|
|
2221
|
+
}));
|
|
2222
|
+
return closePromise;
|
|
2223
|
+
}
|
|
1318
2224
|
};
|
|
1319
2225
|
}
|
|
1320
2226
|
|
|
@@ -1323,7 +2229,7 @@ async function runClassify(options, deps) {
|
|
|
1323
2229
|
const declared = await Promise.all(
|
|
1324
2230
|
options.changes.map(async (change) => ({
|
|
1325
2231
|
...change,
|
|
1326
|
-
exists: await stat(
|
|
2232
|
+
exists: await stat(join8(options.cwd, change.path)).then(
|
|
1327
2233
|
() => true,
|
|
1328
2234
|
() => false
|
|
1329
2235
|
)
|
|
@@ -1588,8 +2494,8 @@ function runExplore(options, deps) {
|
|
|
1588
2494
|
}
|
|
1589
2495
|
|
|
1590
2496
|
// src/run-init.ts
|
|
1591
|
-
import { readFile as
|
|
1592
|
-
import { join as
|
|
2497
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
2498
|
+
import { join as join9 } from "path";
|
|
1593
2499
|
|
|
1594
2500
|
// src/init.ts
|
|
1595
2501
|
var AGENTS_MARKER_START = "<!-- leglas:start -->";
|
|
@@ -1722,7 +2628,7 @@ ${AGENTS_SECTION}`
|
|
|
1722
2628
|
// src/run-init.ts
|
|
1723
2629
|
async function readIfPresent(path) {
|
|
1724
2630
|
try {
|
|
1725
|
-
return await
|
|
2631
|
+
return await readFile6(path, "utf8");
|
|
1726
2632
|
} catch {
|
|
1727
2633
|
return null;
|
|
1728
2634
|
}
|
|
@@ -1730,18 +2636,18 @@ async function readIfPresent(path) {
|
|
|
1730
2636
|
async function runInit(options, deps) {
|
|
1731
2637
|
const existingConfig = findConfigFile(options.cwd);
|
|
1732
2638
|
const plan = planInit({
|
|
1733
|
-
agents: await readIfPresent(
|
|
2639
|
+
agents: await readIfPresent(join9(options.cwd, "AGENTS.md")),
|
|
1734
2640
|
config: existingConfig === null ? null : "present",
|
|
1735
|
-
gitignore: await readIfPresent(
|
|
2641
|
+
gitignore: await readIfPresent(join9(options.cwd, ".gitignore")),
|
|
1736
2642
|
force: options.force
|
|
1737
2643
|
});
|
|
1738
2644
|
const touched = [];
|
|
1739
2645
|
for (const write of plan.writes) {
|
|
1740
|
-
await
|
|
2646
|
+
await writeFile5(join9(options.cwd, write.path), write.contents, "utf8");
|
|
1741
2647
|
touched.push(write.path);
|
|
1742
2648
|
}
|
|
1743
2649
|
if (plan.gitignore !== null) {
|
|
1744
|
-
await
|
|
2650
|
+
await writeFile5(join9(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
1745
2651
|
touched.push(".gitignore");
|
|
1746
2652
|
}
|
|
1747
2653
|
if (options.json) {
|
|
@@ -1761,8 +2667,8 @@ async function runInit(options, deps) {
|
|
|
1761
2667
|
|
|
1762
2668
|
// src/run-keep.ts
|
|
1763
2669
|
import { existsSync as existsSync3 } from "fs";
|
|
1764
|
-
import { mkdir as
|
|
1765
|
-
import { dirname as
|
|
2670
|
+
import { mkdir as mkdir5, readFile as readFile7, rm as rm2, writeFile as writeFile6 } from "fs/promises";
|
|
2671
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
1766
2672
|
|
|
1767
2673
|
// src/keep.ts
|
|
1768
2674
|
import { basename as basename2, extname as extname2, normalize as normalize2 } from "path";
|
|
@@ -1861,18 +2767,18 @@ async function runKeep(options, deps) {
|
|
|
1861
2767
|
if (!resolved.ok) return fail(resolved.error);
|
|
1862
2768
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
1863
2769
|
if (!plan.ok) return fail(plan.error);
|
|
1864
|
-
const from =
|
|
1865
|
-
const to =
|
|
2770
|
+
const from = join10(options.cwd, plan.move.from);
|
|
2771
|
+
const to = join10(options.cwd, plan.move.to);
|
|
1866
2772
|
if (!existsSync3(from)) {
|
|
1867
2773
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
1868
2774
|
}
|
|
1869
2775
|
if (existsSync3(to)) {
|
|
1870
2776
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
1871
2777
|
}
|
|
1872
|
-
const source = await
|
|
1873
|
-
await
|
|
1874
|
-
await
|
|
1875
|
-
await rm2(
|
|
2778
|
+
const source = await readFile7(from, "utf8");
|
|
2779
|
+
await mkdir5(dirname6(to), { recursive: true });
|
|
2780
|
+
await writeFile6(to, renameExport(source, plan.exportName), "utf8");
|
|
2781
|
+
await rm2(join10(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
1876
2782
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
1877
2783
|
if (options.json) {
|
|
1878
2784
|
deps.log(
|
|
@@ -1907,11 +2813,11 @@ async function runKeep(options, deps) {
|
|
|
1907
2813
|
|
|
1908
2814
|
// src/run-new.ts
|
|
1909
2815
|
import { existsSync as existsSync4 } from "fs";
|
|
1910
|
-
import { mkdir as
|
|
1911
|
-
import { dirname as
|
|
2816
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
2817
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
1912
2818
|
async function readIfPresent2(path) {
|
|
1913
2819
|
try {
|
|
1914
|
-
return await
|
|
2820
|
+
return await readFile8(path, "utf8");
|
|
1915
2821
|
} catch {
|
|
1916
2822
|
return null;
|
|
1917
2823
|
}
|
|
@@ -1919,7 +2825,7 @@ async function readIfPresent2(path) {
|
|
|
1919
2825
|
async function runNew(options, deps) {
|
|
1920
2826
|
let from;
|
|
1921
2827
|
if (options.from !== void 0) {
|
|
1922
|
-
const contents = await readIfPresent2(
|
|
2828
|
+
const contents = await readIfPresent2(join11(options.cwd, options.from));
|
|
1923
2829
|
if (contents === null) {
|
|
1924
2830
|
const message = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
1925
2831
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message }));
|
|
@@ -1930,8 +2836,8 @@ async function runNew(options, deps) {
|
|
|
1930
2836
|
}
|
|
1931
2837
|
const plan = planNew({
|
|
1932
2838
|
surface: options.surface,
|
|
1933
|
-
packageJson: await readIfPresent2(
|
|
1934
|
-
gitignore: await readIfPresent2(
|
|
2839
|
+
packageJson: await readIfPresent2(join11(options.cwd, "package.json")),
|
|
2840
|
+
gitignore: await readIfPresent2(join11(options.cwd, ".gitignore")),
|
|
1935
2841
|
from
|
|
1936
2842
|
});
|
|
1937
2843
|
const fail = (error) => {
|
|
@@ -1954,19 +2860,19 @@ async function runNew(options, deps) {
|
|
|
1954
2860
|
deps.log(plan.instructions);
|
|
1955
2861
|
return { exitCode: 0, written: [] };
|
|
1956
2862
|
}
|
|
1957
|
-
const existing = plan.writes.filter((write) => existsSync4(
|
|
2863
|
+
const existing = plan.writes.filter((write) => existsSync4(join11(options.cwd, write.path)));
|
|
1958
2864
|
if (existing.length > 0) {
|
|
1959
2865
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
1960
2866
|
}
|
|
1961
2867
|
const written = [];
|
|
1962
2868
|
for (const write of plan.writes) {
|
|
1963
|
-
const target =
|
|
1964
|
-
await
|
|
1965
|
-
await
|
|
2869
|
+
const target = join11(options.cwd, write.path);
|
|
2870
|
+
await mkdir6(dirname7(target), { recursive: true });
|
|
2871
|
+
await writeFile7(target, write.contents, "utf8");
|
|
1966
2872
|
written.push(write.path);
|
|
1967
2873
|
}
|
|
1968
2874
|
if (plan.gitignore !== null) {
|
|
1969
|
-
await
|
|
2875
|
+
await writeFile7(join11(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
1970
2876
|
written.push(".gitignore");
|
|
1971
2877
|
}
|
|
1972
2878
|
if (options.json) {
|
|
@@ -1987,21 +2893,21 @@ async function runNew(options, deps) {
|
|
|
1987
2893
|
}
|
|
1988
2894
|
|
|
1989
2895
|
// src/run-previews.ts
|
|
1990
|
-
import { readFile as
|
|
1991
|
-
import { join as
|
|
2896
|
+
import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
|
|
2897
|
+
import { join as join12 } from "path";
|
|
1992
2898
|
function envelope(deps, ok, body) {
|
|
1993
2899
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
1994
2900
|
}
|
|
1995
2901
|
async function ensureIgnored(cwd) {
|
|
1996
|
-
const path =
|
|
2902
|
+
const path = join12(cwd, ".gitignore");
|
|
1997
2903
|
let current = null;
|
|
1998
2904
|
try {
|
|
1999
|
-
current = await
|
|
2905
|
+
current = await readFile9(path, "utf8");
|
|
2000
2906
|
} catch {
|
|
2001
2907
|
current = null;
|
|
2002
2908
|
}
|
|
2003
2909
|
const next = ignoreEntry(current);
|
|
2004
|
-
if (next !== null) await
|
|
2910
|
+
if (next !== null) await writeFile8(path, next, "utf8");
|
|
2005
2911
|
}
|
|
2006
2912
|
async function runAdd(options, deps) {
|
|
2007
2913
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -2246,100 +3152,24 @@ async function runShow(options, deps) {
|
|
|
2246
3152
|
}
|
|
2247
3153
|
|
|
2248
3154
|
// src/run-watch.ts
|
|
2249
|
-
import { spawn as
|
|
2250
|
-
import { mkdir as
|
|
2251
|
-
import { dirname as
|
|
2252
|
-
|
|
2253
|
-
// src/watch.ts
|
|
2254
|
-
var WATCH_PATH = ".leglas/watch.json";
|
|
2255
|
-
var PROMPT_TOKEN = "{prompt}";
|
|
2256
|
-
var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
2257
|
-
function tokenize(template) {
|
|
2258
|
-
const tokens = [];
|
|
2259
|
-
let current = "";
|
|
2260
|
-
let started = false;
|
|
2261
|
-
let quote = null;
|
|
2262
|
-
for (const character of template) {
|
|
2263
|
-
if (quote !== null) {
|
|
2264
|
-
if (character === quote) quote = null;
|
|
2265
|
-
else current += character;
|
|
2266
|
-
continue;
|
|
2267
|
-
}
|
|
2268
|
-
if (character === '"' || character === "'") {
|
|
2269
|
-
quote = character;
|
|
2270
|
-
started = true;
|
|
2271
|
-
continue;
|
|
2272
|
-
}
|
|
2273
|
-
if (/\s/.test(character)) {
|
|
2274
|
-
if (started) tokens.push(current);
|
|
2275
|
-
current = "";
|
|
2276
|
-
started = false;
|
|
2277
|
-
continue;
|
|
2278
|
-
}
|
|
2279
|
-
current += character;
|
|
2280
|
-
started = true;
|
|
2281
|
-
}
|
|
2282
|
-
if (quote !== null) {
|
|
2283
|
-
return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
|
|
2284
|
-
}
|
|
2285
|
-
if (started) tokens.push(current);
|
|
2286
|
-
return { ok: true, tokens };
|
|
2287
|
-
}
|
|
2288
|
-
function parseTemplate(raw) {
|
|
2289
|
-
const tokenized = tokenize(raw);
|
|
2290
|
-
if (!tokenized.ok) return tokenized;
|
|
2291
|
-
const { tokens } = tokenized;
|
|
2292
|
-
const [command, ...args] = tokens;
|
|
2293
|
-
if (command === void 0) {
|
|
2294
|
-
return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
|
|
2295
|
-
}
|
|
2296
|
-
const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
|
|
2297
|
-
if (placeholders === 0) {
|
|
2298
|
-
return {
|
|
2299
|
-
ok: false,
|
|
2300
|
-
error: `The agent command needs ${PROMPT_TOKEN} as a word of its own, for example: ${EXAMPLE}`
|
|
2301
|
-
};
|
|
2302
|
-
}
|
|
2303
|
-
if (placeholders > 1) {
|
|
2304
|
-
return {
|
|
2305
|
-
ok: false,
|
|
2306
|
-
error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
|
|
2307
|
-
};
|
|
2308
|
-
}
|
|
2309
|
-
if (command === PROMPT_TOKEN) {
|
|
2310
|
-
return {
|
|
2311
|
-
ok: false,
|
|
2312
|
-
error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
|
|
2313
|
-
};
|
|
2314
|
-
}
|
|
2315
|
-
return { ok: true, template: { command, args } };
|
|
2316
|
-
}
|
|
2317
|
-
function commandFor(template, prompt) {
|
|
2318
|
-
return {
|
|
2319
|
-
command: template.command,
|
|
2320
|
-
args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
function nextRequest(requests, failed) {
|
|
2324
|
-
return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
// src/run-watch.ts
|
|
2328
|
-
var POLL_MS = 2e3;
|
|
3155
|
+
import { spawn as spawn3 } from "child_process";
|
|
3156
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
|
|
3157
|
+
import { dirname as dirname8, join as join13 } from "path";
|
|
3158
|
+
var POLL_MS2 = 2e3;
|
|
2329
3159
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
2330
|
-
async function
|
|
3160
|
+
async function saveTemplate(cwd, run3) {
|
|
3161
|
+
const path = join13(cwd, WATCH_PATH);
|
|
3162
|
+
let config = {};
|
|
2331
3163
|
try {
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
3164
|
+
const parsed2 = JSON.parse(await readFile10(path, "utf8"));
|
|
3165
|
+
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
3166
|
+
config = parsed2;
|
|
3167
|
+
}
|
|
2335
3168
|
} catch {
|
|
2336
|
-
return null;
|
|
2337
3169
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
2342
|
-
await writeFile8(path, `${JSON.stringify({ run: run3 }, null, 2)}
|
|
3170
|
+
config.run = run3;
|
|
3171
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
3172
|
+
await writeFile9(path, `${JSON.stringify(config, null, 2)}
|
|
2343
3173
|
`, "utf8");
|
|
2344
3174
|
}
|
|
2345
3175
|
function spawnAgent(command, args, cwd) {
|
|
@@ -2350,7 +3180,7 @@ function spawnAgent(command, args, cwd) {
|
|
|
2350
3180
|
settled = true;
|
|
2351
3181
|
resolve(outcome);
|
|
2352
3182
|
};
|
|
2353
|
-
const child =
|
|
3183
|
+
const child = spawn3(command, args, { cwd, stdio: "inherit" });
|
|
2354
3184
|
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
2355
3185
|
child.on(
|
|
2356
3186
|
"close",
|
|
@@ -2361,21 +3191,34 @@ function spawnAgent(command, args, cwd) {
|
|
|
2361
3191
|
});
|
|
2362
3192
|
}
|
|
2363
3193
|
async function runWatch(options, deps) {
|
|
2364
|
-
const saved = options.run === void 0 ? await
|
|
2365
|
-
const raw = options.run ?? saved;
|
|
2366
|
-
|
|
3194
|
+
const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
|
|
3195
|
+
const raw = options.run ?? saved.run;
|
|
3196
|
+
let template;
|
|
3197
|
+
let shownCommand2;
|
|
3198
|
+
let synthesizedAgent = null;
|
|
3199
|
+
if (raw !== null) {
|
|
3200
|
+
const parsed2 = parseTemplate(raw);
|
|
3201
|
+
if (!parsed2.ok) {
|
|
3202
|
+
deps.error(parsed2.error);
|
|
3203
|
+
return { exitCode: 1 };
|
|
3204
|
+
}
|
|
3205
|
+
template = parsed2.template;
|
|
3206
|
+
shownCommand2 = raw;
|
|
3207
|
+
} else if (saved.agent !== null && saved.agent !== "custom") {
|
|
3208
|
+
const adapter = KNOWN_AGENTS[saved.agent];
|
|
3209
|
+
template = {
|
|
3210
|
+
command: adapter.binary,
|
|
3211
|
+
args: adapter.terminalArgs(PROMPT_TOKEN)
|
|
3212
|
+
};
|
|
3213
|
+
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
3214
|
+
synthesizedAgent = adapter.name;
|
|
3215
|
+
} else {
|
|
2367
3216
|
deps.error(
|
|
2368
|
-
'Watch needs an agent command the first time:
|
|
3217
|
+
'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
|
|
2369
3218
|
);
|
|
2370
3219
|
return { exitCode: 1 };
|
|
2371
3220
|
}
|
|
2372
|
-
|
|
2373
|
-
if (!parsed2.ok) {
|
|
2374
|
-
deps.error(parsed2.error);
|
|
2375
|
-
return { exitCode: 1 };
|
|
2376
|
-
}
|
|
2377
|
-
const template = parsed2.template;
|
|
2378
|
-
if (options.run !== void 0) await saveTemplate(options.cwd, raw).catch(() => {
|
|
3221
|
+
if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
|
|
2379
3222
|
});
|
|
2380
3223
|
const base = `http://localhost:${options.port ?? DEFAULT_PORT}`;
|
|
2381
3224
|
const heartbeat = async (watching) => {
|
|
@@ -2389,11 +3232,15 @@ async function runWatch(options, deps) {
|
|
|
2389
3232
|
} catch {
|
|
2390
3233
|
}
|
|
2391
3234
|
};
|
|
2392
|
-
|
|
3235
|
+
if (synthesizedAgent !== null) {
|
|
3236
|
+
deps.log(`Using ${synthesizedAgent}, chosen in the interface.`);
|
|
3237
|
+
}
|
|
3238
|
+
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
2393
3239
|
deps.log("Stop with Ctrl-C.");
|
|
2394
3240
|
const failed = /* @__PURE__ */ new Set();
|
|
2395
3241
|
let stopped = false;
|
|
2396
3242
|
let busy = false;
|
|
3243
|
+
let announced = false;
|
|
2397
3244
|
let inflight = null;
|
|
2398
3245
|
const handle = async (request) => {
|
|
2399
3246
|
deps.log("");
|
|
@@ -2415,7 +3262,12 @@ async function runWatch(options, deps) {
|
|
|
2415
3262
|
};
|
|
2416
3263
|
const tick = async () => {
|
|
2417
3264
|
if (stopped) return;
|
|
2418
|
-
|
|
3265
|
+
if (announced) {
|
|
3266
|
+
void heartbeat(true);
|
|
3267
|
+
} else {
|
|
3268
|
+
await heartbeat(true);
|
|
3269
|
+
announced = true;
|
|
3270
|
+
}
|
|
2419
3271
|
if (busy) return;
|
|
2420
3272
|
busy = true;
|
|
2421
3273
|
try {
|
|
@@ -2432,7 +3284,7 @@ async function runWatch(options, deps) {
|
|
|
2432
3284
|
}
|
|
2433
3285
|
};
|
|
2434
3286
|
return new Promise((resolve) => {
|
|
2435
|
-
const timer = setInterval(() => void tick(),
|
|
3287
|
+
const timer = setInterval(() => void tick(), POLL_MS2);
|
|
2436
3288
|
const stop = () => {
|
|
2437
3289
|
if (stopped) return;
|
|
2438
3290
|
stopped = true;
|
|
@@ -2452,14 +3304,14 @@ async function runWatch(options, deps) {
|
|
|
2452
3304
|
// src/run.ts
|
|
2453
3305
|
import { existsSync as existsSync5 } from "fs";
|
|
2454
3306
|
import { createRequire } from "module";
|
|
2455
|
-
import { basename as basename3, dirname as
|
|
3307
|
+
import { basename as basename3, dirname as dirname9, join as join14, relative as relative4 } from "path";
|
|
2456
3308
|
import { fileURLToPath } from "url";
|
|
2457
3309
|
function findShellDir() {
|
|
2458
|
-
const bundled =
|
|
2459
|
-
if (existsSync5(
|
|
3310
|
+
const bundled = join14(dirname9(fileURLToPath(import.meta.url)), "shell");
|
|
3311
|
+
if (existsSync5(join14(bundled, "index.html"))) return bundled;
|
|
2460
3312
|
try {
|
|
2461
3313
|
const require2 = createRequire(import.meta.url);
|
|
2462
|
-
return
|
|
3314
|
+
return dirname9(require2.resolve("@leglas/shell/dist/index.html"));
|
|
2463
3315
|
} catch {
|
|
2464
3316
|
return null;
|
|
2465
3317
|
}
|
|
@@ -2493,7 +3345,7 @@ async function run2(options, deps) {
|
|
|
2493
3345
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
2494
3346
|
for (const preview of merged?.previews ?? []) {
|
|
2495
3347
|
if (preview.file !== void 0) {
|
|
2496
|
-
const absolute =
|
|
3348
|
+
const absolute = join14(options.cwd, preview.file);
|
|
2497
3349
|
if (!existsSync5(absolute)) {
|
|
2498
3350
|
worktreeErrors.push(
|
|
2499
3351
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -2504,7 +3356,7 @@ async function run2(options, deps) {
|
|
|
2504
3356
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
2505
3357
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
2506
3358
|
}
|
|
2507
|
-
fileMounts.set(slug,
|
|
3359
|
+
fileMounts.set(slug, dirname9(absolute));
|
|
2508
3360
|
previews.push({
|
|
2509
3361
|
...preview,
|
|
2510
3362
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -2565,7 +3417,7 @@ async function run2(options, deps) {
|
|
|
2565
3417
|
})
|
|
2566
3418
|
);
|
|
2567
3419
|
} else {
|
|
2568
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
3420
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative4(options.cwd, loaded.path) || loaded.path;
|
|
2569
3421
|
deps.log(`Leglas ${url}`);
|
|
2570
3422
|
deps.log(
|
|
2571
3423
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|
|
@@ -2659,7 +3511,7 @@ function version() {
|
|
|
2659
3511
|
async function openBrowser(url) {
|
|
2660
3512
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2661
3513
|
try {
|
|
2662
|
-
|
|
3514
|
+
spawn4(command, [url], { detached: true, stdio: "ignore" }).unref();
|
|
2663
3515
|
} catch {
|
|
2664
3516
|
}
|
|
2665
3517
|
}
|