leglas 0.1.1 → 0.2.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/LICENSE +21 -0
- package/README.md +276 -300
- package/dist/args.d.ts +11 -0
- package/dist/bin.js +683 -184
- package/dist/explore.d.ts +25 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +665 -182
- package/dist/resolve-title.d.ts +19 -0
- package/dist/run-explore.d.ts +6 -5
- package/dist/run-show.d.ts +18 -0
- package/dist/run-watch.d.ts +23 -0
- package/dist/shell/assets/index-dMGYwlE1.css +1 -0
- package/dist/shell/assets/index-r7sGM7WL.js +10 -0
- package/dist/shell/favicon.svg +48 -0
- package/dist/shell/index.html +3 -2
- package/dist/show.d.ts +59 -0
- package/dist/watch.d.ts +45 -0
- package/package.json +9 -3
- package/dist/briefs.d.ts +0 -32
- package/dist/shell/assets/index-D-nej-Uo.js +0 -9
- package/dist/shell/assets/index-DcVuoqfz.css +0 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ function parseAdd(rest) {
|
|
|
56
56
|
let note;
|
|
57
57
|
let branch;
|
|
58
58
|
let file;
|
|
59
|
+
let basedOn;
|
|
59
60
|
const tags = [];
|
|
60
61
|
let json = false;
|
|
61
62
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -74,7 +75,7 @@ function parseAdd(rest) {
|
|
|
74
75
|
} else {
|
|
75
76
|
value = argument.slice(equals + 1);
|
|
76
77
|
}
|
|
77
|
-
if (!["--title", "--url", "--note", "--tag", "--branch", "--file"].includes(flag)) {
|
|
78
|
+
if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on"].includes(flag)) {
|
|
78
79
|
return { kind: "error", message: `leglas add does not take ${flag}.` };
|
|
79
80
|
}
|
|
80
81
|
if (value === void 0 || value === "") {
|
|
@@ -85,6 +86,7 @@ function parseAdd(rest) {
|
|
|
85
86
|
else if (flag === "--note") note = value;
|
|
86
87
|
else if (flag === "--branch") branch = value;
|
|
87
88
|
else if (flag === "--file") file = value;
|
|
89
|
+
else if (flag === "--based-on") basedOn = value;
|
|
88
90
|
else tags.push(value);
|
|
89
91
|
}
|
|
90
92
|
if (title === void 0) {
|
|
@@ -98,7 +100,7 @@ function parseAdd(rest) {
|
|
|
98
100
|
}
|
|
99
101
|
return {
|
|
100
102
|
kind: "add",
|
|
101
|
-
preview: { title, url, note, tags: tags.length > 0 ? tags : void 0, branch, file },
|
|
103
|
+
preview: { title, url, note, tags: tags.length > 0 ? tags : void 0, branch, file, basedOn },
|
|
102
104
|
json
|
|
103
105
|
};
|
|
104
106
|
}
|
|
@@ -131,8 +133,37 @@ function parseClassify(rest) {
|
|
|
131
133
|
}
|
|
132
134
|
return { kind: "classify", changes, json };
|
|
133
135
|
}
|
|
136
|
+
function parseWatch(rest) {
|
|
137
|
+
let run3;
|
|
138
|
+
let port;
|
|
139
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
140
|
+
const argument = rest[index];
|
|
141
|
+
if (argument === "--help" || argument === "-h") return { kind: "help" };
|
|
142
|
+
const equals = argument.indexOf("=");
|
|
143
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
144
|
+
if (flag !== "--run" && flag !== "--port") {
|
|
145
|
+
return { kind: "error", message: `leglas watch does not take ${argument}.` };
|
|
146
|
+
}
|
|
147
|
+
const value = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
148
|
+
if (value === void 0 || value === "") {
|
|
149
|
+
return {
|
|
150
|
+
kind: "error",
|
|
151
|
+
message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' : "--port needs a value."
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (flag === "--run") {
|
|
155
|
+
run3 = value;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const parsed = parsePort(flag, value);
|
|
159
|
+
if (typeof parsed !== "number") return { kind: "error", message: parsed.error };
|
|
160
|
+
port = parsed;
|
|
161
|
+
}
|
|
162
|
+
return { kind: "watch", run: run3, port };
|
|
163
|
+
}
|
|
134
164
|
function parseArgs(argv) {
|
|
135
165
|
if (argv[0] === "new") return parseNew(argv.slice(1));
|
|
166
|
+
if (argv[0] === "watch") return parseWatch(argv.slice(1));
|
|
136
167
|
if (argv[0] === "add") return parseAdd(argv.slice(1));
|
|
137
168
|
if (argv[0] === "classify") return parseClassify(argv.slice(1));
|
|
138
169
|
if (argv[0] === "init") {
|
|
@@ -184,6 +215,7 @@ function parseArgs(argv) {
|
|
|
184
215
|
const rest = argv.slice(1);
|
|
185
216
|
let surface;
|
|
186
217
|
let count = 3;
|
|
218
|
+
let basedOn = null;
|
|
187
219
|
let json = false;
|
|
188
220
|
for (let index = 0; index < rest.length; index += 1) {
|
|
189
221
|
const argument = rest[index];
|
|
@@ -199,6 +231,17 @@ function parseArgs(argv) {
|
|
|
199
231
|
count = Number(raw);
|
|
200
232
|
continue;
|
|
201
233
|
}
|
|
234
|
+
if (argument === "--based-on" || argument.startsWith("--based-on=")) {
|
|
235
|
+
const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
|
|
236
|
+
if (raw === void 0 || raw === "") {
|
|
237
|
+
return {
|
|
238
|
+
kind: "error",
|
|
239
|
+
message: '--based-on needs a direction title, for example --based-on "Aurora".'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
basedOn = raw;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
202
245
|
if (argument.startsWith("-")) {
|
|
203
246
|
return { kind: "error", message: `leglas explore does not take ${argument}.` };
|
|
204
247
|
}
|
|
@@ -213,7 +256,7 @@ function parseArgs(argv) {
|
|
|
213
256
|
message: "leglas explore needs a surface name, for example: leglas explore hero --count 6"
|
|
214
257
|
};
|
|
215
258
|
}
|
|
216
|
-
return { kind: "explore", surface, count, json };
|
|
259
|
+
return { kind: "explore", surface, count, basedOn, json };
|
|
217
260
|
}
|
|
218
261
|
if (argv[0] === "requests") {
|
|
219
262
|
const rest = argv.slice(1);
|
|
@@ -231,6 +274,31 @@ function parseArgs(argv) {
|
|
|
231
274
|
}
|
|
232
275
|
return { kind: "list", json: rest.includes("--json") };
|
|
233
276
|
}
|
|
277
|
+
if (argv[0] === "show") {
|
|
278
|
+
const rest = argv.slice(1);
|
|
279
|
+
let title;
|
|
280
|
+
let json = false;
|
|
281
|
+
for (const argument of rest) {
|
|
282
|
+
if (argument === "--json") {
|
|
283
|
+
json = true;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (argument.startsWith("-")) {
|
|
287
|
+
return { kind: "error", message: `leglas show does not take ${argument}.` };
|
|
288
|
+
}
|
|
289
|
+
if (title !== void 0) {
|
|
290
|
+
return { kind: "error", message: "leglas show takes one direction title." };
|
|
291
|
+
}
|
|
292
|
+
title = argument;
|
|
293
|
+
}
|
|
294
|
+
if (title === void 0) {
|
|
295
|
+
return {
|
|
296
|
+
kind: "error",
|
|
297
|
+
message: 'leglas show needs a direction title, for example: leglas show "Aurora" --json'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return { kind: "show", title, json };
|
|
301
|
+
}
|
|
234
302
|
const options = {
|
|
235
303
|
port: void 0,
|
|
236
304
|
userPort: void 0,
|
|
@@ -478,132 +546,40 @@ Rewriting your component automatically is how a tool breaks a codebase it does n
|
|
|
478
546
|
};
|
|
479
547
|
}
|
|
480
548
|
|
|
481
|
-
// src/
|
|
482
|
-
|
|
483
|
-
{
|
|
484
|
-
slug: "quiet",
|
|
485
|
-
name: "Quiet",
|
|
486
|
-
brief: "Reduce until almost nothing is left. Generous whitespace, a single focal element, and typography carrying the whole hierarchy. Remove decoration rather than softening it.",
|
|
487
|
-
avoid: "Adding a subtle gradient or a lighter shade and calling the result minimal."
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
slug: "image-led",
|
|
491
|
-
name: "Image-led",
|
|
492
|
-
brief: "Let imagery be the page. Full-bleed visual, text as a restrained overlay, and a composition that follows the artwork rather than sitting beside it.",
|
|
493
|
-
avoid: "Keeping the existing layout and enlarging the picture inside it."
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
slug: "kinetic",
|
|
497
|
-
name: "Kinetic",
|
|
498
|
-
brief: "Motion carries the hierarchy. Something continuous and ambient, with elements arriving in a deliberate sequence. Honour prefers-reduced-motion with a still composition that still works.",
|
|
499
|
-
avoid: "A fade-in on scroll bolted onto the current design."
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
slug: "editorial",
|
|
503
|
-
name: "Editorial",
|
|
504
|
-
brief: "Compose it like a magazine spread. Asymmetric grid, large display type with tight leading, rules and captions, imagery treated as a plate rather than a background.",
|
|
505
|
-
avoid: "A centred headline above a centred paragraph."
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
slug: "dense",
|
|
509
|
-
name: "Dense",
|
|
510
|
-
brief: "Information forward. Tighter rhythm, smaller type, several entry points visible at once, and the confidence that the reader wants more rather than less.",
|
|
511
|
-
avoid: "The same layout with the padding reduced."
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
slug: "high-contrast",
|
|
515
|
-
name: "High contrast",
|
|
516
|
-
brief: "Commit to a hard palette: near-black against one saturated accent, or the whole thing inverted. Define shapes with edges rather than gradients.",
|
|
517
|
-
avoid: "Darkening the existing palette by a few steps."
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
slug: "material",
|
|
521
|
-
name: "Material",
|
|
522
|
-
brief: "Give it depth and surface. Layered planes, grain or noise, shadow used structurally to stack elements, a sense that the parts are physical objects.",
|
|
523
|
-
avoid: "One drop shadow on an otherwise flat card."
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
slug: "type-led",
|
|
527
|
-
name: "Type-led",
|
|
528
|
-
brief: "Remove imagery entirely. Build the composition from letterforms: extreme scale contrast, a second typeface earning its place, text as the visual itself.",
|
|
529
|
-
avoid: "Keeping the image and setting the headline larger."
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
slug: "playful",
|
|
533
|
-
name: "Playful",
|
|
534
|
-
brief: "Deliberate imperfection. Rotation, overlap, irregular or hand-made elements, and one colour that ought not to work but does.",
|
|
535
|
-
avoid: "Increasing the border radius and little else."
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
slug: "systemic",
|
|
539
|
-
name: "Systemic",
|
|
540
|
-
brief: "Make the structure visible. Modular blocks on a stated grid, consistent module sizes, alignment itself as the aesthetic.",
|
|
541
|
-
avoid: "Adding borders around the sections that already exist."
|
|
542
|
-
}
|
|
543
|
-
];
|
|
544
|
-
function briefsFor(count) {
|
|
545
|
-
if (!Number.isFinite(count) || count <= 0) return [];
|
|
546
|
-
return ALL_BRIEFS.slice(0, Math.min(Math.floor(count), ALL_BRIEFS.length));
|
|
547
|
-
}
|
|
548
|
-
function planBriefs(surface, count) {
|
|
549
|
+
// src/explore.ts
|
|
550
|
+
function planExplore(surface, count, basedOn = null) {
|
|
549
551
|
const slug = surfaceSlug(surface);
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const commands = chosen.map(
|
|
556
|
-
(brief) => `leglas add --title ${JSON.stringify(brief.name)} --url ${JSON.stringify(
|
|
557
|
-
`/?v-${slug}=${brief.slug}`
|
|
558
|
-
)} --note ${JSON.stringify(`${brief.brief.split(".")[0]}.`)}`
|
|
559
|
-
);
|
|
560
|
-
const instructions = `Build ${chosen.length} direction${chosen.length === 1 ? "" : "s"} for "${surface}", one per angle below.
|
|
552
|
+
const goal = basedOn === null ? `Build ${count} design directions for "${surface}".
|
|
553
|
+
|
|
554
|
+
The set exists to be chosen from, and the choice only means something if the directions genuinely disagree: ${count} variants of one idea would make it empty. What counts as different is yours to decide, and the strongest sets disagree about more than styling.
|
|
555
|
+
|
|
556
|
+
One trap, seen every time this goes wrong: a set collapses toward whichever direction is built first. Decide all ${count} before building any, and if two would read as the same direction at a glance, replace one of them.` : `Build ${count} variations of the "${basedOn}" direction for "${surface}".
|
|
561
557
|
|
|
562
|
-
|
|
558
|
+
The set exists to pick a variant of a direction already chosen, so every variation must stay recognisably that direction. The trap here is drift: change enough and the comparison stops being about the variant. Vary each one deliberately and hold everything else still; if a variation grows into a new direction, it belongs in its own exploration instead.`;
|
|
559
|
+
const register = basedOn === null ? ` leglas add --title "<name>" --url "/?v-${slug}=<key>" --note "<the idea, one line>"` : ` leglas add --title "<name>" --url "/?v-${slug}=<key>" --based-on ${JSON.stringify(basedOn)} --note "<the idea, one line>"`;
|
|
560
|
+
const mechanics = `Each one is its own file under .leglas/variants/${slug}/, listed in the DIRECTIONS map in that folder's switch file. If there is no switch file yet, run \`leglas new ${slug}\` first. Register each one the moment it renders, not the set at the end. The interface picks a registration up within seconds, so whoever asked watches the set fill in:
|
|
563
561
|
|
|
564
|
-
|
|
562
|
+
${register}
|
|
565
563
|
|
|
566
|
-
|
|
564
|
+
The title and note are what the user judges from in the rail, so name each one for its idea rather than numbering it.`;
|
|
565
|
+
return { surface, slug, count, basedOn, instructions: `${goal}
|
|
567
566
|
|
|
568
|
-
|
|
569
|
-
return { previews, commands, instructions };
|
|
567
|
+
${mechanics}` };
|
|
570
568
|
}
|
|
571
569
|
|
|
572
570
|
// src/run-explore.ts
|
|
573
571
|
function runExplore(options, deps) {
|
|
574
|
-
|
|
575
|
-
if (chosen.length === 0) {
|
|
572
|
+
if (!Number.isFinite(options.count) || options.count <= 0) {
|
|
576
573
|
deps.log(
|
|
577
574
|
options.json ? JSON.stringify({ ok: false, error: "Ask for at least one direction." }) : "Ask for at least one direction, for example --count 4."
|
|
578
575
|
);
|
|
579
576
|
return { exitCode: 1 };
|
|
580
577
|
}
|
|
581
|
-
const plan =
|
|
578
|
+
const plan = planExplore(options.surface, Math.floor(options.count), options.basedOn);
|
|
582
579
|
if (options.json) {
|
|
583
|
-
deps.log(
|
|
584
|
-
JSON.stringify({
|
|
585
|
-
ok: true,
|
|
586
|
-
surface: options.surface,
|
|
587
|
-
directions: chosen,
|
|
588
|
-
previews: plan.previews,
|
|
589
|
-
commands: plan.commands,
|
|
590
|
-
instructions: plan.instructions
|
|
591
|
-
})
|
|
592
|
-
);
|
|
580
|
+
deps.log(JSON.stringify({ ok: true, ...plan }));
|
|
593
581
|
return { exitCode: 0 };
|
|
594
582
|
}
|
|
595
|
-
if (options.count > ALL_BRIEFS.length) {
|
|
596
|
-
deps.log(
|
|
597
|
-
`Asked for ${options.count}; there are ${ALL_BRIEFS.length} distinct angles, so ${ALL_BRIEFS.length} follow.`
|
|
598
|
-
);
|
|
599
|
-
deps.log("");
|
|
600
|
-
}
|
|
601
|
-
for (const brief of chosen) {
|
|
602
|
-
deps.log(`${brief.name}`);
|
|
603
|
-
deps.log(` ${brief.brief}`);
|
|
604
|
-
deps.log(` Avoid: ${brief.avoid}`);
|
|
605
|
-
deps.log("");
|
|
606
|
-
}
|
|
607
583
|
deps.log(plan.instructions);
|
|
608
584
|
return { exitCode: 0 };
|
|
609
585
|
}
|
|
@@ -638,27 +614,39 @@ When asked for design variations, alternatives, or "a few options":
|
|
|
638
614
|
and register it with \`leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
639
615
|
(the config needs \`devCommand\` with \`{port}\`). Everything below is the
|
|
640
616
|
ordinary, in-app path.
|
|
641
|
-
2. Run \`leglas explore <surface> --count <n>\` first
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
617
|
+
2. Run \`leglas explore <surface> --count <n>\` first, adding
|
|
618
|
+
\`--based-on "<title>"\` when the user wants variations of a direction they
|
|
619
|
+
already like. It prints what the set needs and how to register it. In
|
|
620
|
+
short: new directions must genuinely disagree with each other, variants of
|
|
621
|
+
one must not, and either way decide the whole set before building any of
|
|
622
|
+
it. The designs themselves are yours.
|
|
645
623
|
3. If the surface has no switcher yet, run
|
|
646
624
|
\`leglas new <surface> --from <the component that renders it today>\`. It
|
|
647
625
|
writes one under \`.leglas/variants/<surface>/\` and prints the single line
|
|
648
626
|
to add where that surface renders. \`--from\` makes the baseline re-export
|
|
649
627
|
the real component rather than copying it, so it stays live.
|
|
650
|
-
4.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
628
|
+
4. Before building, make sure the interface is up. If \`leglas\` is not
|
|
629
|
+
already running, tell the user to run it, and hand them the URL now
|
|
630
|
+
rather than when the set is done: the rail picks up each registration
|
|
631
|
+
within seconds, so they get to watch the exploration fill in.
|
|
632
|
+
5. Build one direction at a time: its own file beside the others in
|
|
633
|
+
\`.leglas/variants/<surface>/\`, listed in the \`DIRECTIONS\` map in that
|
|
634
|
+
folder's \`switch\` file, then registered the moment it renders:
|
|
654
635
|
\`leglas add --title "Aurora" --url "/?v-<surface>=aurora" --note "One line on the idea."\`
|
|
655
|
-
|
|
656
|
-
|
|
636
|
+
Register each direction as it lands, never the whole set at the end. To
|
|
637
|
+
the user watching the rail, a batch at the end is minutes of nothing and
|
|
638
|
+
then everything at once.
|
|
657
639
|
|
|
658
640
|
When the user asks to change one direction, check \`leglas requests --json\`
|
|
659
641
|
first: they may have described it from the interface, and the request names the
|
|
660
642
|
exact file. Clear the queue with \`leglas requests --clear\` once done.
|
|
661
643
|
|
|
644
|
+
If the user wants requests handled the moment they are typed, without relaying
|
|
645
|
+
each one, tell them about \`leglas watch --run "claude -p {prompt}"\` (any
|
|
646
|
+
agent command works; {prompt} receives the request). It runs in their
|
|
647
|
+
terminal, hands each request to that command as it arrives, and the interface
|
|
648
|
+
shows the request's progress.
|
|
649
|
+
|
|
662
650
|
When the user picks a winner, run
|
|
663
651
|
\`leglas keep "<title>" --to <path in real source>\`. It moves that direction
|
|
664
652
|
out of the ignored directory, deletes the rest of the exploration, and drops
|
|
@@ -674,8 +662,14 @@ Useful to know:
|
|
|
674
662
|
\`leglas add --title "Aurora" --file .leglas/pages/aurora.html\`. Leglas
|
|
675
663
|
serves the file itself, so no dev server is needed. Sibling assets in the
|
|
676
664
|
same directory resolve normally.
|
|
677
|
-
- Titles identify previews and must be unique.
|
|
665
|
+
- Titles identify previews and must be unique. The user may rename one in the
|
|
666
|
+
rail, which renames it on their machine only; the commands answer to either
|
|
667
|
+
name, so use whichever they said.
|
|
678
668
|
- \`leglas list\` shows every direction, shared and local.
|
|
669
|
+
- \`leglas show "<title>" --json\` answers for one of them: the file behind it,
|
|
670
|
+
the variants based on it, what it is being compared against, and anything
|
|
671
|
+
they have asked for that is not done yet. Run it when handed a direction you
|
|
672
|
+
did not register yourself.
|
|
679
673
|
- Every command accepts \`--json\` and prints one envelope with a stable exit
|
|
680
674
|
code, so you can drive it without parsing prose.
|
|
681
675
|
|
|
@@ -808,6 +802,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
808
802
|
errors.push(`${at} names a branch and a file; a file preview is served by Leglas itself and has no checkout.`);
|
|
809
803
|
}
|
|
810
804
|
}
|
|
805
|
+
const basedOn = entry["basedOn"];
|
|
806
|
+
if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
|
|
807
|
+
errors.push(`${at} has a basedOn that is not a direction title.`);
|
|
808
|
+
}
|
|
811
809
|
const tags = entry["tags"];
|
|
812
810
|
previews.push({
|
|
813
811
|
title: typeof title === "string" ? title : "",
|
|
@@ -815,7 +813,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
815
813
|
note: typeof entry["note"] === "string" ? entry["note"] : void 0,
|
|
816
814
|
tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
|
|
817
815
|
...typeof branch === "string" ? { branch } : {},
|
|
818
|
-
...typeof file === "string" ? { file } : {}
|
|
816
|
+
...typeof file === "string" ? { file } : {},
|
|
817
|
+
...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {}
|
|
819
818
|
});
|
|
820
819
|
});
|
|
821
820
|
const devCommand = source["devCommand"];
|
|
@@ -1019,7 +1018,8 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1019
1018
|
...input.note === void 0 ? {} : { note: input.note },
|
|
1020
1019
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1021
1020
|
...input.branch === void 0 ? {} : { branch: input.branch },
|
|
1022
|
-
...input.file === void 0 ? {} : { file: input.file }
|
|
1021
|
+
...input.file === void 0 ? {} : { file: input.file },
|
|
1022
|
+
...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
|
|
1023
1023
|
};
|
|
1024
1024
|
const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
|
|
1025
1025
|
if (check.config === null) {
|
|
@@ -1253,6 +1253,7 @@ async function startAppProcess(options) {
|
|
|
1253
1253
|
|
|
1254
1254
|
// ../server/dist/requests.js
|
|
1255
1255
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1256
|
+
import { randomBytes } from "crypto";
|
|
1256
1257
|
import { dirname as dirname3, join as join4 } from "path";
|
|
1257
1258
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
1258
1259
|
function targetFor(url) {
|
|
@@ -1291,7 +1292,16 @@ async function readRequests(cwd) {
|
|
|
1291
1292
|
try {
|
|
1292
1293
|
const raw = await readFile3(join4(cwd, REQUESTS_PATH), "utf8");
|
|
1293
1294
|
const parsed = JSON.parse(raw);
|
|
1294
|
-
|
|
1295
|
+
if (!Array.isArray(parsed.requests))
|
|
1296
|
+
return [];
|
|
1297
|
+
return parsed.requests.map((request, index) => {
|
|
1298
|
+
const entry = request;
|
|
1299
|
+
return {
|
|
1300
|
+
...entry,
|
|
1301
|
+
id: typeof entry.id === "string" ? entry.id : String(index),
|
|
1302
|
+
status: entry.status === "picked-up" ? "picked-up" : "queued"
|
|
1303
|
+
};
|
|
1304
|
+
});
|
|
1295
1305
|
} catch {
|
|
1296
1306
|
return [];
|
|
1297
1307
|
}
|
|
@@ -1303,20 +1313,78 @@ async function writeQueue(cwd, requests) {
|
|
|
1303
1313
|
`, "utf8");
|
|
1304
1314
|
}
|
|
1305
1315
|
async function appendRequest(cwd, request) {
|
|
1306
|
-
await writeQueue(cwd, [
|
|
1316
|
+
await writeQueue(cwd, [
|
|
1317
|
+
...await readRequests(cwd),
|
|
1318
|
+
{ ...request, id: randomBytes(6).toString("base64url"), status: "queued" }
|
|
1319
|
+
]);
|
|
1320
|
+
}
|
|
1321
|
+
async function collectRequests(cwd) {
|
|
1322
|
+
const requests = await readRequests(cwd);
|
|
1323
|
+
const collected = requests.map((request) => ({ ...request, status: "picked-up" }));
|
|
1324
|
+
if (requests.some((request) => request.status !== "picked-up"))
|
|
1325
|
+
await writeQueue(cwd, collected);
|
|
1326
|
+
return collected;
|
|
1327
|
+
}
|
|
1328
|
+
async function markPickedUp(cwd, id) {
|
|
1329
|
+
const requests = await readRequests(cwd);
|
|
1330
|
+
if (!requests.some((request) => request.id === id && request.status !== "picked-up"))
|
|
1331
|
+
return false;
|
|
1332
|
+
await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
async function removeRequest(cwd, id) {
|
|
1336
|
+
const requests = await readRequests(cwd);
|
|
1337
|
+
const remaining = requests.filter((request) => request.id !== id);
|
|
1338
|
+
if (remaining.length === requests.length)
|
|
1339
|
+
return false;
|
|
1340
|
+
await writeQueue(cwd, remaining);
|
|
1341
|
+
return true;
|
|
1307
1342
|
}
|
|
1308
1343
|
async function clearRequests(cwd) {
|
|
1309
1344
|
await writeQueue(cwd, []);
|
|
1310
1345
|
}
|
|
1311
1346
|
|
|
1347
|
+
// ../server/dist/renames.js
|
|
1348
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1349
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
1350
|
+
var RENAMES_PATH = ".leglas/renames.json";
|
|
1351
|
+
async function readRenames(cwd) {
|
|
1352
|
+
try {
|
|
1353
|
+
const raw = await readFile4(join5(cwd, RENAMES_PATH), "utf8");
|
|
1354
|
+
const parsed = JSON.parse(raw);
|
|
1355
|
+
if (parsed.renames === null || typeof parsed.renames !== "object")
|
|
1356
|
+
return {};
|
|
1357
|
+
return Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
1358
|
+
} catch {
|
|
1359
|
+
return {};
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function writeRenames(cwd, renames) {
|
|
1363
|
+
const path = join5(cwd, RENAMES_PATH);
|
|
1364
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1365
|
+
await writeFile3(path, `${JSON.stringify({ renames }, null, 2)}
|
|
1366
|
+
`, "utf8");
|
|
1367
|
+
}
|
|
1368
|
+
function resolveTitle(input, titles, renames) {
|
|
1369
|
+
if (titles.includes(input))
|
|
1370
|
+
return { ok: true, title: input };
|
|
1371
|
+
const matches = titles.filter((title) => renames[title] === input);
|
|
1372
|
+
if (matches.length === 1 && matches[0] !== void 0)
|
|
1373
|
+
return { ok: true, title: matches[0] };
|
|
1374
|
+
if (matches.length > 1)
|
|
1375
|
+
return { ok: false, reason: "ambiguous", matches };
|
|
1376
|
+
return { ok: false, reason: "unknown" };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1312
1379
|
// ../server/dist/server.js
|
|
1313
1380
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1314
1381
|
import http2 from "http";
|
|
1315
1382
|
import net3 from "net";
|
|
1316
|
-
import { extname, join as
|
|
1383
|
+
import { extname, join as join6, normalize } from "path";
|
|
1317
1384
|
var LEGLAS_PREFIX = "/leglas";
|
|
1318
1385
|
var DEFAULT_PORT = 4100;
|
|
1319
1386
|
var PORT_ATTEMPTS = 20;
|
|
1387
|
+
var ATTACHED_WINDOW_MS = 6e3;
|
|
1320
1388
|
var CONTENT_TYPES = {
|
|
1321
1389
|
".css": "text/css; charset=utf-8",
|
|
1322
1390
|
".gif": "image/gif",
|
|
@@ -1365,7 +1433,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
1365
1433
|
}
|
|
1366
1434
|
function serveFrom(res, dir, relativePath) {
|
|
1367
1435
|
const relative3 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
1368
|
-
const candidate =
|
|
1436
|
+
const candidate = join6(dir, relative3);
|
|
1369
1437
|
if (!candidate.startsWith(dir))
|
|
1370
1438
|
return false;
|
|
1371
1439
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -1390,7 +1458,8 @@ var PLACEHOLDER = `<!doctype html>
|
|
|
1390
1458
|
<p>The server is running and proxying your app. The interface has not been
|
|
1391
1459
|
built into this install yet.</p>
|
|
1392
1460
|
<p><a href="/leglas/api/config">/leglas/api/config</a> \xB7
|
|
1393
|
-
<a href="/leglas/api/health">/leglas/api/health</a
|
|
1461
|
+
<a href="/leglas/api/health">/leglas/api/health</a> \xB7
|
|
1462
|
+
<a href="/leglas/api/requests">/leglas/api/requests</a></p>
|
|
1394
1463
|
</body>`;
|
|
1395
1464
|
function listen(server, port) {
|
|
1396
1465
|
return new Promise((resolve, reject) => {
|
|
@@ -1425,28 +1494,40 @@ async function startServer(options) {
|
|
|
1425
1494
|
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map() } = options;
|
|
1426
1495
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
1427
1496
|
const proxy = createProxyHandler({ target });
|
|
1497
|
+
let lastSeen = null;
|
|
1428
1498
|
const server = http2.createServer((req, res) => {
|
|
1429
1499
|
const url = req.url ?? "/";
|
|
1430
1500
|
const path = url.split("?")[0] ?? "/";
|
|
1431
1501
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
1432
|
-
|
|
1502
|
+
const boot = config?.previews ?? [];
|
|
1503
|
+
return void readLocalPreviews(cwd).then(({ previews: local }) => {
|
|
1504
|
+
const known = new Set(boot.map((preview) => preview.title));
|
|
1505
|
+
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
1506
|
+
sendJson(res, 200, {
|
|
1507
|
+
project,
|
|
1508
|
+
devServer: target,
|
|
1509
|
+
previews: [...boot, ...fresh],
|
|
1510
|
+
errors: configErrors
|
|
1511
|
+
});
|
|
1512
|
+
}).catch(() => sendJson(res, 200, {
|
|
1433
1513
|
project,
|
|
1434
1514
|
devServer: target,
|
|
1435
|
-
previews:
|
|
1515
|
+
previews: boot,
|
|
1436
1516
|
errors: configErrors
|
|
1437
|
-
});
|
|
1517
|
+
}));
|
|
1438
1518
|
}
|
|
1439
1519
|
if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
|
|
1440
1520
|
let body = "";
|
|
1441
1521
|
req.on("data", (chunk) => body += chunk);
|
|
1442
|
-
return void req.on("end", () => {
|
|
1522
|
+
return void req.on("end", async () => {
|
|
1443
1523
|
let parsed;
|
|
1444
1524
|
try {
|
|
1445
1525
|
parsed = JSON.parse(body || "{}");
|
|
1446
1526
|
} catch {
|
|
1447
1527
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1448
1528
|
}
|
|
1449
|
-
const
|
|
1529
|
+
const local = await readLocalPreviews(cwd).then((read) => read.previews, () => []);
|
|
1530
|
+
const preview = [...config?.previews ?? [], ...local].find((entry) => entry.title === parsed.title);
|
|
1450
1531
|
if (!preview || !parsed.intent?.trim()) {
|
|
1451
1532
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
1452
1533
|
}
|
|
@@ -1459,6 +1540,46 @@ async function startServer(options) {
|
|
|
1459
1540
|
}).then(() => sendJson(res, 200, { ok: true, ...composed })).catch(() => sendJson(res, 200, { ok: true, ...composed, queued: false }));
|
|
1460
1541
|
});
|
|
1461
1542
|
}
|
|
1543
|
+
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
1544
|
+
let body = "";
|
|
1545
|
+
req.on("data", (chunk) => body += chunk);
|
|
1546
|
+
return void req.on("end", () => {
|
|
1547
|
+
let parsed;
|
|
1548
|
+
try {
|
|
1549
|
+
parsed = JSON.parse(body || "{}");
|
|
1550
|
+
} catch {
|
|
1551
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof parsed.watching !== "boolean") {
|
|
1554
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a watching boolean." });
|
|
1555
|
+
}
|
|
1556
|
+
lastSeen = parsed.watching ? Date.now() : null;
|
|
1557
|
+
sendJson(res, 200, { ok: true });
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
1561
|
+
return void readRequests(cwd).then((requests) => sendJson(res, 200, {
|
|
1562
|
+
requests: requests.map(({ id, title, intent, status }) => ({ id, title, intent, status })),
|
|
1563
|
+
agent: { attached: lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS }
|
|
1564
|
+
}));
|
|
1565
|
+
}
|
|
1566
|
+
if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
|
|
1567
|
+
let body = "";
|
|
1568
|
+
req.on("data", (chunk) => body += chunk);
|
|
1569
|
+
return void req.on("end", () => {
|
|
1570
|
+
let parsed;
|
|
1571
|
+
try {
|
|
1572
|
+
parsed = JSON.parse(body || "{}");
|
|
1573
|
+
} catch {
|
|
1574
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
1575
|
+
}
|
|
1576
|
+
if (parsed.renames === null || typeof parsed.renames !== "object") {
|
|
1577
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a renames object." });
|
|
1578
|
+
}
|
|
1579
|
+
const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
1580
|
+
void writeRenames(cwd, renames).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 200, { ok: false }));
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1462
1583
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
1463
1584
|
return void probe(target).then((reachable) => sendJson(res, 200, { devServer: target, reachable }));
|
|
1464
1585
|
}
|
|
@@ -1566,11 +1687,11 @@ function planKeep(options) {
|
|
|
1566
1687
|
}
|
|
1567
1688
|
|
|
1568
1689
|
// src/run-init.ts
|
|
1569
|
-
import { readFile as
|
|
1570
|
-
import { join as
|
|
1690
|
+
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1691
|
+
import { join as join7 } from "path";
|
|
1571
1692
|
async function readIfPresent(path) {
|
|
1572
1693
|
try {
|
|
1573
|
-
return await
|
|
1694
|
+
return await readFile5(path, "utf8");
|
|
1574
1695
|
} catch {
|
|
1575
1696
|
return null;
|
|
1576
1697
|
}
|
|
@@ -1578,18 +1699,18 @@ async function readIfPresent(path) {
|
|
|
1578
1699
|
async function runInit(options, deps) {
|
|
1579
1700
|
const existingConfig = findConfigFile(options.cwd);
|
|
1580
1701
|
const plan = planInit({
|
|
1581
|
-
agents: await readIfPresent(
|
|
1702
|
+
agents: await readIfPresent(join7(options.cwd, "AGENTS.md")),
|
|
1582
1703
|
config: existingConfig === null ? null : "present",
|
|
1583
|
-
gitignore: await readIfPresent(
|
|
1704
|
+
gitignore: await readIfPresent(join7(options.cwd, ".gitignore")),
|
|
1584
1705
|
force: options.force
|
|
1585
1706
|
});
|
|
1586
1707
|
const touched = [];
|
|
1587
1708
|
for (const write of plan.writes) {
|
|
1588
|
-
await
|
|
1709
|
+
await writeFile4(join7(options.cwd, write.path), write.contents, "utf8");
|
|
1589
1710
|
touched.push(write.path);
|
|
1590
1711
|
}
|
|
1591
1712
|
if (plan.gitignore !== null) {
|
|
1592
|
-
await
|
|
1713
|
+
await writeFile4(join7(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
1593
1714
|
touched.push(".gitignore");
|
|
1594
1715
|
}
|
|
1595
1716
|
if (options.json) {
|
|
@@ -1609,8 +1730,26 @@ async function runInit(options, deps) {
|
|
|
1609
1730
|
|
|
1610
1731
|
// src/run-keep.ts
|
|
1611
1732
|
import { existsSync as existsSync3 } from "fs";
|
|
1612
|
-
import { mkdir as
|
|
1613
|
-
import { dirname as
|
|
1733
|
+
import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
1734
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1735
|
+
|
|
1736
|
+
// src/resolve-title.ts
|
|
1737
|
+
function resolveOrExplain(input, titles, renames) {
|
|
1738
|
+
const resolution = resolveTitle(input, titles, renames);
|
|
1739
|
+
if (resolution.ok) return { ok: true, title: resolution.title };
|
|
1740
|
+
if (resolution.reason === "ambiguous") {
|
|
1741
|
+
return {
|
|
1742
|
+
ok: false,
|
|
1743
|
+
error: `More than one direction is called ${JSON.stringify(input)} on this machine: ${resolution.matches.join(", ")}. Name the one you mean by its title in the config.`
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
ok: false,
|
|
1748
|
+
error: `No direction called ${JSON.stringify(input)}. Renaming one in the rail only renames it here, and it still answers to its title in the config, which its reference block quotes. leglas list shows every title.`
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/run-keep.ts
|
|
1614
1753
|
function renameExport(source, to) {
|
|
1615
1754
|
const match = /export function ([A-Za-z0-9_]+)\s*\(/.exec(source);
|
|
1616
1755
|
if (!match || match[1] === void 0) return source;
|
|
@@ -1623,31 +1762,37 @@ async function runKeep(options, deps) {
|
|
|
1623
1762
|
const loaded = await loadConfig(options.cwd);
|
|
1624
1763
|
const local = await readLocalPreviews(options.cwd);
|
|
1625
1764
|
const previews = [...loaded.config?.previews ?? [], ...local.previews];
|
|
1626
|
-
const plan = planKeep({ title: options.title, previews, to: options.to });
|
|
1627
1765
|
const fail = (error) => {
|
|
1628
1766
|
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
1629
1767
|
else deps.error(error);
|
|
1630
1768
|
return { exitCode: 1 };
|
|
1631
1769
|
};
|
|
1770
|
+
const resolved = resolveOrExplain(
|
|
1771
|
+
options.title,
|
|
1772
|
+
previews.map((preview) => preview.title),
|
|
1773
|
+
await readRenames(options.cwd)
|
|
1774
|
+
);
|
|
1775
|
+
if (!resolved.ok) return fail(resolved.error);
|
|
1776
|
+
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
1632
1777
|
if (!plan.ok) return fail(plan.error);
|
|
1633
|
-
const from =
|
|
1634
|
-
const to =
|
|
1778
|
+
const from = join8(options.cwd, plan.move.from);
|
|
1779
|
+
const to = join8(options.cwd, plan.move.to);
|
|
1635
1780
|
if (!existsSync3(from)) {
|
|
1636
1781
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
1637
1782
|
}
|
|
1638
1783
|
if (existsSync3(to)) {
|
|
1639
1784
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
1640
1785
|
}
|
|
1641
|
-
const source = await
|
|
1642
|
-
await
|
|
1643
|
-
await
|
|
1644
|
-
await rm2(
|
|
1786
|
+
const source = await readFile6(from, "utf8");
|
|
1787
|
+
await mkdir4(dirname5(to), { recursive: true });
|
|
1788
|
+
await writeFile5(to, renameExport(source, plan.exportName), "utf8");
|
|
1789
|
+
await rm2(join8(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
1645
1790
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
1646
1791
|
if (options.json) {
|
|
1647
1792
|
deps.log(
|
|
1648
1793
|
JSON.stringify({
|
|
1649
1794
|
ok: true,
|
|
1650
|
-
kept:
|
|
1795
|
+
kept: resolved.title,
|
|
1651
1796
|
to: plan.move.to,
|
|
1652
1797
|
exportName: plan.exportName,
|
|
1653
1798
|
removed: plan.removeDir,
|
|
@@ -1676,11 +1821,11 @@ async function runKeep(options, deps) {
|
|
|
1676
1821
|
|
|
1677
1822
|
// src/run-new.ts
|
|
1678
1823
|
import { existsSync as existsSync4 } from "fs";
|
|
1679
|
-
import { mkdir as
|
|
1680
|
-
import { dirname as
|
|
1824
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
|
|
1825
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1681
1826
|
async function readIfPresent2(path) {
|
|
1682
1827
|
try {
|
|
1683
|
-
return await
|
|
1828
|
+
return await readFile7(path, "utf8");
|
|
1684
1829
|
} catch {
|
|
1685
1830
|
return null;
|
|
1686
1831
|
}
|
|
@@ -1688,7 +1833,7 @@ async function readIfPresent2(path) {
|
|
|
1688
1833
|
async function runNew(options, deps) {
|
|
1689
1834
|
let from;
|
|
1690
1835
|
if (options.from !== void 0) {
|
|
1691
|
-
const contents = await readIfPresent2(
|
|
1836
|
+
const contents = await readIfPresent2(join9(options.cwd, options.from));
|
|
1692
1837
|
if (contents === null) {
|
|
1693
1838
|
const message = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
1694
1839
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message }));
|
|
@@ -1699,8 +1844,8 @@ async function runNew(options, deps) {
|
|
|
1699
1844
|
}
|
|
1700
1845
|
const plan = planNew({
|
|
1701
1846
|
surface: options.surface,
|
|
1702
|
-
packageJson: await readIfPresent2(
|
|
1703
|
-
gitignore: await readIfPresent2(
|
|
1847
|
+
packageJson: await readIfPresent2(join9(options.cwd, "package.json")),
|
|
1848
|
+
gitignore: await readIfPresent2(join9(options.cwd, ".gitignore")),
|
|
1704
1849
|
from
|
|
1705
1850
|
});
|
|
1706
1851
|
const fail = (error) => {
|
|
@@ -1723,19 +1868,19 @@ async function runNew(options, deps) {
|
|
|
1723
1868
|
deps.log(plan.instructions);
|
|
1724
1869
|
return { exitCode: 0, written: [] };
|
|
1725
1870
|
}
|
|
1726
|
-
const existing = plan.writes.filter((write) => existsSync4(
|
|
1871
|
+
const existing = plan.writes.filter((write) => existsSync4(join9(options.cwd, write.path)));
|
|
1727
1872
|
if (existing.length > 0) {
|
|
1728
1873
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
1729
1874
|
}
|
|
1730
1875
|
const written = [];
|
|
1731
1876
|
for (const write of plan.writes) {
|
|
1732
|
-
const target =
|
|
1733
|
-
await
|
|
1734
|
-
await
|
|
1877
|
+
const target = join9(options.cwd, write.path);
|
|
1878
|
+
await mkdir5(dirname6(target), { recursive: true });
|
|
1879
|
+
await writeFile6(target, write.contents, "utf8");
|
|
1735
1880
|
written.push(write.path);
|
|
1736
1881
|
}
|
|
1737
1882
|
if (plan.gitignore !== null) {
|
|
1738
|
-
await
|
|
1883
|
+
await writeFile6(join9(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
1739
1884
|
written.push(".gitignore");
|
|
1740
1885
|
}
|
|
1741
1886
|
if (options.json) {
|
|
@@ -1756,25 +1901,35 @@ async function runNew(options, deps) {
|
|
|
1756
1901
|
}
|
|
1757
1902
|
|
|
1758
1903
|
// src/run-previews.ts
|
|
1759
|
-
import { readFile as
|
|
1760
|
-
import { join as
|
|
1904
|
+
import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
1905
|
+
import { join as join10 } from "path";
|
|
1761
1906
|
function envelope(deps, ok, body) {
|
|
1762
1907
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
1763
1908
|
}
|
|
1764
1909
|
async function ensureIgnored(cwd) {
|
|
1765
|
-
const path =
|
|
1910
|
+
const path = join10(cwd, ".gitignore");
|
|
1766
1911
|
let current = null;
|
|
1767
1912
|
try {
|
|
1768
|
-
current = await
|
|
1913
|
+
current = await readFile8(path, "utf8");
|
|
1769
1914
|
} catch {
|
|
1770
1915
|
current = null;
|
|
1771
1916
|
}
|
|
1772
1917
|
const next = ignoreEntry(current);
|
|
1773
|
-
if (next !== null) await
|
|
1918
|
+
if (next !== null) await writeFile7(path, next, "utf8");
|
|
1774
1919
|
}
|
|
1775
1920
|
async function runAdd(options, deps) {
|
|
1776
1921
|
const loaded = await loadConfig(options.cwd);
|
|
1777
1922
|
const shared = loaded.config?.previews ?? [];
|
|
1923
|
+
if (options.preview.basedOn !== void 0) {
|
|
1924
|
+
const local = await readLocalPreviews(options.cwd);
|
|
1925
|
+
const titles = new Set([...shared, ...local.previews].map((preview) => preview.title));
|
|
1926
|
+
if (!titles.has(options.preview.basedOn)) {
|
|
1927
|
+
const error = `--based-on names ${JSON.stringify(options.preview.basedOn)}, which is not a registered direction. leglas list shows what exists.`;
|
|
1928
|
+
if (options.json) envelope(deps, false, { error });
|
|
1929
|
+
else deps.error(error);
|
|
1930
|
+
return { exitCode: 1 };
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1778
1933
|
const outcome = await addLocalPreview(
|
|
1779
1934
|
options.cwd,
|
|
1780
1935
|
{
|
|
@@ -1783,7 +1938,8 @@ async function runAdd(options, deps) {
|
|
|
1783
1938
|
note: options.preview.note,
|
|
1784
1939
|
tags: options.preview.tags,
|
|
1785
1940
|
branch: options.preview.branch,
|
|
1786
|
-
file: options.preview.file
|
|
1941
|
+
file: options.preview.file,
|
|
1942
|
+
basedOn: options.preview.basedOn
|
|
1787
1943
|
},
|
|
1788
1944
|
shared
|
|
1789
1945
|
);
|
|
@@ -1813,7 +1969,11 @@ async function runAdd(options, deps) {
|
|
|
1813
1969
|
deps.log(" Add devCommand (with {port}) to the config.");
|
|
1814
1970
|
deps.log("");
|
|
1815
1971
|
}
|
|
1816
|
-
|
|
1972
|
+
if (options.preview.branch === void 0 && options.preview.file === void 0) {
|
|
1973
|
+
deps.log("Local to this machine. A running interface picks it up within seconds.");
|
|
1974
|
+
} else {
|
|
1975
|
+
deps.log("Local to this machine. Restart Leglas to see it, or run leglas list.");
|
|
1976
|
+
}
|
|
1817
1977
|
}
|
|
1818
1978
|
return { exitCode: 0 };
|
|
1819
1979
|
}
|
|
@@ -1827,9 +1987,17 @@ async function runList(options, deps) {
|
|
|
1827
1987
|
];
|
|
1828
1988
|
if (options.json) {
|
|
1829
1989
|
envelope(deps, errors.length === 0, {
|
|
1990
|
+
// The whole record, not a summary of it. What the config holds about a
|
|
1991
|
+
// preview — its note, its tags, the direction it is a variant of — is
|
|
1992
|
+
// exactly what tells an agent why these are being compared, and leaving
|
|
1993
|
+
// it out made the listing thinner than the reference block that points
|
|
1994
|
+
// at it.
|
|
1830
1995
|
previews: previews.map((preview) => ({
|
|
1831
1996
|
title: preview.title,
|
|
1832
1997
|
url: preview.url,
|
|
1998
|
+
note: preview.note ?? null,
|
|
1999
|
+
tags: preview.tags,
|
|
2000
|
+
basedOn: preview.basedOn ?? null,
|
|
1833
2001
|
local: preview.local,
|
|
1834
2002
|
branch: preview.branch ?? null,
|
|
1835
2003
|
file: preview.file ?? null
|
|
@@ -1859,7 +2027,7 @@ async function runRequests(options, deps) {
|
|
|
1859
2027
|
else deps.log("Queue cleared.");
|
|
1860
2028
|
return { exitCode: 0 };
|
|
1861
2029
|
}
|
|
1862
|
-
const requests = await
|
|
2030
|
+
const requests = await collectRequests(options.cwd);
|
|
1863
2031
|
if (options.json) {
|
|
1864
2032
|
envelope(deps, true, { requests });
|
|
1865
2033
|
return { exitCode: 0 };
|
|
@@ -1877,14 +2045,322 @@ async function runRequests(options, deps) {
|
|
|
1877
2045
|
return { exitCode: 0 };
|
|
1878
2046
|
}
|
|
1879
2047
|
|
|
2048
|
+
// src/show.ts
|
|
2049
|
+
function describe(preview) {
|
|
2050
|
+
return {
|
|
2051
|
+
title: preview.title,
|
|
2052
|
+
url: preview.url,
|
|
2053
|
+
note: preview.note ?? null,
|
|
2054
|
+
tags: preview.tags,
|
|
2055
|
+
basedOn: preview.basedOn ?? null,
|
|
2056
|
+
branch: preview.branch ?? null,
|
|
2057
|
+
file: preview.file ?? null,
|
|
2058
|
+
local: preview.local === true,
|
|
2059
|
+
// A file preview names its own source. Everything else is decoded from the
|
|
2060
|
+
// URL, and a URL outside the convention yields nothing rather than a path
|
|
2061
|
+
// that looks authoritative and is not there.
|
|
2062
|
+
target: preview.file ?? targetFor(preview.url)
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
function planShow({ title, previews, requests }) {
|
|
2066
|
+
const found = previews.find((preview) => preview.title === title);
|
|
2067
|
+
if (!found) {
|
|
2068
|
+
return {
|
|
2069
|
+
ok: false,
|
|
2070
|
+
error: `No direction called ${JSON.stringify(title)}. Run leglas list to see them.`
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
const variants = previews.filter((preview) => preview.basedOn === title).map(describe);
|
|
2074
|
+
const variantTitles = new Set(variants.map((variant) => variant.title));
|
|
2075
|
+
return {
|
|
2076
|
+
ok: true,
|
|
2077
|
+
direction: describe(found),
|
|
2078
|
+
variants,
|
|
2079
|
+
// Its own variants are already listed in full, so they are not repeated
|
|
2080
|
+
// here; this is the rest of the comparison.
|
|
2081
|
+
comparedWith: previews.map((preview) => preview.title).filter((other) => other !== title && !variantTitles.has(other)),
|
|
2082
|
+
requests: requests.filter((request) => request.title === title).map((request) => ({
|
|
2083
|
+
id: request.id,
|
|
2084
|
+
intent: request.intent,
|
|
2085
|
+
target: request.target,
|
|
2086
|
+
prompt: request.prompt,
|
|
2087
|
+
status: request.status
|
|
2088
|
+
}))
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/run-show.ts
|
|
2093
|
+
async function runShow(options, deps) {
|
|
2094
|
+
const loaded = await loadConfig(options.cwd);
|
|
2095
|
+
const local = await readLocalPreviews(options.cwd);
|
|
2096
|
+
const requests = await readRequests(options.cwd);
|
|
2097
|
+
const previews = [
|
|
2098
|
+
...(loaded.config?.previews ?? []).map((preview) => ({ ...preview, local: false })),
|
|
2099
|
+
...local.previews
|
|
2100
|
+
];
|
|
2101
|
+
const resolved = resolveOrExplain(
|
|
2102
|
+
options.title,
|
|
2103
|
+
previews.map((preview) => preview.title),
|
|
2104
|
+
await readRenames(options.cwd)
|
|
2105
|
+
);
|
|
2106
|
+
if (!resolved.ok) {
|
|
2107
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error: resolved.error }));
|
|
2108
|
+
else deps.error(resolved.error);
|
|
2109
|
+
return { exitCode: 1 };
|
|
2110
|
+
}
|
|
2111
|
+
const plan = planShow({ title: resolved.title, previews, requests });
|
|
2112
|
+
if (!plan.ok) {
|
|
2113
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error: plan.error }));
|
|
2114
|
+
else deps.error(plan.error);
|
|
2115
|
+
return { exitCode: 1 };
|
|
2116
|
+
}
|
|
2117
|
+
if (options.json) {
|
|
2118
|
+
deps.log(
|
|
2119
|
+
JSON.stringify({
|
|
2120
|
+
ok: true,
|
|
2121
|
+
direction: plan.direction,
|
|
2122
|
+
variants: plan.variants,
|
|
2123
|
+
comparedWith: plan.comparedWith,
|
|
2124
|
+
requests: plan.requests
|
|
2125
|
+
})
|
|
2126
|
+
);
|
|
2127
|
+
return { exitCode: 0 };
|
|
2128
|
+
}
|
|
2129
|
+
const { direction } = plan;
|
|
2130
|
+
deps.log(` ${direction.title}${direction.local ? " (local)" : ""}`);
|
|
2131
|
+
if (direction.note !== null) deps.log(` ${direction.note}`);
|
|
2132
|
+
deps.log("");
|
|
2133
|
+
if (direction.target !== null) deps.log(` file ${direction.target}`);
|
|
2134
|
+
if (direction.branch !== null) deps.log(` branch ${direction.branch}`);
|
|
2135
|
+
deps.log(` url ${direction.url}`);
|
|
2136
|
+
if (direction.tags.length > 0) deps.log(` tags ${direction.tags.join(", ")}`);
|
|
2137
|
+
if (direction.basedOn !== null) deps.log(` variant of ${direction.basedOn}`);
|
|
2138
|
+
if (plan.variants.length > 0) {
|
|
2139
|
+
deps.log(` variants ${plan.variants.map((variant) => variant.title).join(", ")}`);
|
|
2140
|
+
}
|
|
2141
|
+
if (plan.comparedWith.length > 0) {
|
|
2142
|
+
deps.log(` against ${plan.comparedWith.join(", ")}`);
|
|
2143
|
+
}
|
|
2144
|
+
if (plan.requests.length > 0) {
|
|
2145
|
+
deps.log("");
|
|
2146
|
+
deps.log(` Pending, not yet done (${plan.requests.length}):`);
|
|
2147
|
+
for (const request of plan.requests) deps.log(` ${request.status} ${request.intent}`);
|
|
2148
|
+
deps.log("");
|
|
2149
|
+
deps.log(" Run leglas requests --json for the full prompts.");
|
|
2150
|
+
}
|
|
2151
|
+
return { exitCode: 0 };
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/watch.ts
|
|
2155
|
+
var WATCH_PATH = ".leglas/watch.json";
|
|
2156
|
+
var PROMPT_TOKEN = "{prompt}";
|
|
2157
|
+
var EXAMPLE = `leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
2158
|
+
function tokenize(template) {
|
|
2159
|
+
const tokens = [];
|
|
2160
|
+
let current = "";
|
|
2161
|
+
let started = false;
|
|
2162
|
+
let quote = null;
|
|
2163
|
+
for (const character of template) {
|
|
2164
|
+
if (quote !== null) {
|
|
2165
|
+
if (character === quote) quote = null;
|
|
2166
|
+
else current += character;
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
if (character === '"' || character === "'") {
|
|
2170
|
+
quote = character;
|
|
2171
|
+
started = true;
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
if (/\s/.test(character)) {
|
|
2175
|
+
if (started) tokens.push(current);
|
|
2176
|
+
current = "";
|
|
2177
|
+
started = false;
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
current += character;
|
|
2181
|
+
started = true;
|
|
2182
|
+
}
|
|
2183
|
+
if (quote !== null) {
|
|
2184
|
+
return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
|
|
2185
|
+
}
|
|
2186
|
+
if (started) tokens.push(current);
|
|
2187
|
+
return { ok: true, tokens };
|
|
2188
|
+
}
|
|
2189
|
+
function parseTemplate(raw) {
|
|
2190
|
+
const tokenized = tokenize(raw);
|
|
2191
|
+
if (!tokenized.ok) return tokenized;
|
|
2192
|
+
const { tokens } = tokenized;
|
|
2193
|
+
const [command, ...args] = tokens;
|
|
2194
|
+
if (command === void 0) {
|
|
2195
|
+
return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
|
|
2196
|
+
}
|
|
2197
|
+
const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
|
|
2198
|
+
if (placeholders === 0) {
|
|
2199
|
+
return {
|
|
2200
|
+
ok: false,
|
|
2201
|
+
error: `The agent command needs ${PROMPT_TOKEN} as a word of its own, for example: ${EXAMPLE}`
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
if (placeholders > 1) {
|
|
2205
|
+
return {
|
|
2206
|
+
ok: false,
|
|
2207
|
+
error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
if (command === PROMPT_TOKEN) {
|
|
2211
|
+
return {
|
|
2212
|
+
ok: false,
|
|
2213
|
+
error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
return { ok: true, template: { command, args } };
|
|
2217
|
+
}
|
|
2218
|
+
function commandFor(template, prompt) {
|
|
2219
|
+
return {
|
|
2220
|
+
command: template.command,
|
|
2221
|
+
args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
function nextRequest(requests, failed) {
|
|
2225
|
+
return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// src/run-watch.ts
|
|
2229
|
+
import { spawn as spawn2 } from "child_process";
|
|
2230
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
|
|
2231
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
2232
|
+
var POLL_MS = 2e3;
|
|
2233
|
+
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
2234
|
+
async function readSavedTemplate(cwd) {
|
|
2235
|
+
try {
|
|
2236
|
+
const raw = await readFile9(join11(cwd, WATCH_PATH), "utf8");
|
|
2237
|
+
const parsed = JSON.parse(raw);
|
|
2238
|
+
return typeof parsed.run === "string" && parsed.run !== "" ? parsed.run : null;
|
|
2239
|
+
} catch {
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async function saveTemplate(cwd, run3) {
|
|
2244
|
+
const path = join11(cwd, WATCH_PATH);
|
|
2245
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
2246
|
+
await writeFile8(path, `${JSON.stringify({ run: run3 }, null, 2)}
|
|
2247
|
+
`, "utf8");
|
|
2248
|
+
}
|
|
2249
|
+
function spawnAgent(command, args, cwd) {
|
|
2250
|
+
return new Promise((resolve) => {
|
|
2251
|
+
let settled = false;
|
|
2252
|
+
const settle = (outcome) => {
|
|
2253
|
+
if (settled) return;
|
|
2254
|
+
settled = true;
|
|
2255
|
+
resolve(outcome);
|
|
2256
|
+
};
|
|
2257
|
+
const child = spawn2(command, args, { cwd, stdio: "inherit" });
|
|
2258
|
+
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
2259
|
+
child.on(
|
|
2260
|
+
"close",
|
|
2261
|
+
(code, signal) => settle(
|
|
2262
|
+
signal === null ? { ok: true, code: code ?? 0 } : { ok: false, error: `stopped by ${signal}` }
|
|
2263
|
+
)
|
|
2264
|
+
);
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
async function runWatch(options, deps) {
|
|
2268
|
+
const saved = options.run === void 0 ? await readSavedTemplate(options.cwd) : null;
|
|
2269
|
+
const raw = options.run ?? saved;
|
|
2270
|
+
if (raw === null) {
|
|
2271
|
+
deps.error(
|
|
2272
|
+
'Watch needs an agent command the first time: leglas watch --run "claude -p {prompt}"'
|
|
2273
|
+
);
|
|
2274
|
+
return { exitCode: 1 };
|
|
2275
|
+
}
|
|
2276
|
+
const parsed = parseTemplate(raw);
|
|
2277
|
+
if (!parsed.ok) {
|
|
2278
|
+
deps.error(parsed.error);
|
|
2279
|
+
return { exitCode: 1 };
|
|
2280
|
+
}
|
|
2281
|
+
const template = parsed.template;
|
|
2282
|
+
if (options.run !== void 0) await saveTemplate(options.cwd, raw).catch(() => {
|
|
2283
|
+
});
|
|
2284
|
+
const base = `http://localhost:${options.port ?? DEFAULT_PORT}`;
|
|
2285
|
+
const heartbeat = async (watching) => {
|
|
2286
|
+
try {
|
|
2287
|
+
await fetch(`${base}${LEGLAS_PREFIX}/api/watch`, {
|
|
2288
|
+
method: "POST",
|
|
2289
|
+
headers: { "content-type": "application/json" },
|
|
2290
|
+
body: JSON.stringify({ watching }),
|
|
2291
|
+
signal: AbortSignal.timeout(HEARTBEAT_TIMEOUT_MS)
|
|
2292
|
+
});
|
|
2293
|
+
} catch {
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
deps.log(`Watching for change requests. Each one runs: ${raw}`);
|
|
2297
|
+
deps.log("Stop with Ctrl-C.");
|
|
2298
|
+
const failed = /* @__PURE__ */ new Set();
|
|
2299
|
+
let stopped = false;
|
|
2300
|
+
let busy = false;
|
|
2301
|
+
let inflight = null;
|
|
2302
|
+
const handle = async (request) => {
|
|
2303
|
+
deps.log("");
|
|
2304
|
+
deps.log(` ${request.title}: ${request.intent}`);
|
|
2305
|
+
if (request.target !== null) deps.log(` ${request.target}`);
|
|
2306
|
+
await markPickedUp(options.cwd, request.id);
|
|
2307
|
+
const { command, args } = commandFor(template, request.prompt);
|
|
2308
|
+
const outcome = await spawnAgent(command, args, options.cwd);
|
|
2309
|
+
if (outcome.ok && outcome.code === 0) {
|
|
2310
|
+
await removeRequest(options.cwd, request.id);
|
|
2311
|
+
deps.log(` done ${request.title}`);
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
failed.add(request.id);
|
|
2315
|
+
deps.error(
|
|
2316
|
+
` failed ${request.title}: ${outcome.ok ? `${command} exited ${outcome.code}` : outcome.error}`
|
|
2317
|
+
);
|
|
2318
|
+
deps.error(" Left in the queue and not retried.");
|
|
2319
|
+
};
|
|
2320
|
+
const tick = async () => {
|
|
2321
|
+
if (stopped) return;
|
|
2322
|
+
void heartbeat(true);
|
|
2323
|
+
if (busy) return;
|
|
2324
|
+
busy = true;
|
|
2325
|
+
try {
|
|
2326
|
+
const request = nextRequest(await readRequests(options.cwd), failed);
|
|
2327
|
+
if (request !== null && !stopped) {
|
|
2328
|
+
inflight = handle(request);
|
|
2329
|
+
await inflight;
|
|
2330
|
+
}
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
deps.error(` ! ${error instanceof Error ? error.message : String(error)}`);
|
|
2333
|
+
} finally {
|
|
2334
|
+
inflight = null;
|
|
2335
|
+
busy = false;
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
return new Promise((resolve) => {
|
|
2339
|
+
const timer = setInterval(() => void tick(), POLL_MS);
|
|
2340
|
+
const stop = () => {
|
|
2341
|
+
if (stopped) return;
|
|
2342
|
+
stopped = true;
|
|
2343
|
+
clearInterval(timer);
|
|
2344
|
+
process.off("SIGINT", stop);
|
|
2345
|
+
process.off("SIGTERM", stop);
|
|
2346
|
+
void Promise.resolve(inflight).catch(() => {
|
|
2347
|
+
}).then(() => heartbeat(false)).then(() => resolve({ exitCode: 0 }));
|
|
2348
|
+
};
|
|
2349
|
+
process.on("SIGINT", stop);
|
|
2350
|
+
process.on("SIGTERM", stop);
|
|
2351
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
2352
|
+
void tick();
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
1880
2356
|
// src/run-classify.ts
|
|
1881
2357
|
import { stat } from "fs/promises";
|
|
1882
|
-
import { join as
|
|
2358
|
+
import { join as join12 } from "path";
|
|
1883
2359
|
async function runClassify(options, deps) {
|
|
1884
2360
|
const declared = await Promise.all(
|
|
1885
2361
|
options.changes.map(async (change) => ({
|
|
1886
2362
|
...change,
|
|
1887
|
-
exists: await stat(
|
|
2363
|
+
exists: await stat(join12(options.cwd, change.path)).then(
|
|
1888
2364
|
() => true,
|
|
1889
2365
|
() => false
|
|
1890
2366
|
)
|
|
@@ -1912,14 +2388,14 @@ async function runClassify(options, deps) {
|
|
|
1912
2388
|
// src/run.ts
|
|
1913
2389
|
import { existsSync as existsSync5 } from "fs";
|
|
1914
2390
|
import { createRequire } from "module";
|
|
1915
|
-
import { basename as basename3, dirname as
|
|
2391
|
+
import { basename as basename3, dirname as dirname8, join as join13, relative as relative2 } from "path";
|
|
1916
2392
|
import { fileURLToPath } from "url";
|
|
1917
2393
|
function findShellDir() {
|
|
1918
|
-
const bundled =
|
|
1919
|
-
if (existsSync5(
|
|
2394
|
+
const bundled = join13(dirname8(fileURLToPath(import.meta.url)), "shell");
|
|
2395
|
+
if (existsSync5(join13(bundled, "index.html"))) return bundled;
|
|
1920
2396
|
try {
|
|
1921
2397
|
const require2 = createRequire(import.meta.url);
|
|
1922
|
-
return
|
|
2398
|
+
return dirname8(require2.resolve("@leglas/shell/dist/index.html"));
|
|
1923
2399
|
} catch {
|
|
1924
2400
|
return null;
|
|
1925
2401
|
}
|
|
@@ -1953,7 +2429,7 @@ async function run2(options, deps) {
|
|
|
1953
2429
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
1954
2430
|
for (const preview of merged?.previews ?? []) {
|
|
1955
2431
|
if (preview.file !== void 0) {
|
|
1956
|
-
const absolute =
|
|
2432
|
+
const absolute = join13(options.cwd, preview.file);
|
|
1957
2433
|
if (!existsSync5(absolute)) {
|
|
1958
2434
|
worktreeErrors.push(
|
|
1959
2435
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -1964,7 +2440,7 @@ async function run2(options, deps) {
|
|
|
1964
2440
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
1965
2441
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
1966
2442
|
}
|
|
1967
|
-
fileMounts.set(slug,
|
|
2443
|
+
fileMounts.set(slug, dirname8(absolute));
|
|
1968
2444
|
previews.push({
|
|
1969
2445
|
...preview,
|
|
1970
2446
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
|
|
@@ -2064,15 +2540,20 @@ async function run2(options, deps) {
|
|
|
2064
2540
|
export {
|
|
2065
2541
|
AGENTS_MARKER_END,
|
|
2066
2542
|
AGENTS_MARKER_START,
|
|
2067
|
-
|
|
2543
|
+
PROMPT_TOKEN,
|
|
2544
|
+
WATCH_PATH,
|
|
2068
2545
|
baselineFrom,
|
|
2069
|
-
|
|
2546
|
+
commandFor,
|
|
2070
2547
|
detectFramework,
|
|
2548
|
+
nextRequest,
|
|
2071
2549
|
parseArgs,
|
|
2072
|
-
|
|
2550
|
+
parseTemplate,
|
|
2551
|
+
planExplore,
|
|
2073
2552
|
planInit,
|
|
2074
2553
|
planKeep,
|
|
2075
2554
|
planNew,
|
|
2555
|
+
planShow,
|
|
2556
|
+
readRequests,
|
|
2076
2557
|
run2 as run,
|
|
2077
2558
|
runAdd,
|
|
2078
2559
|
runClassify,
|
|
@@ -2082,5 +2563,7 @@ export {
|
|
|
2082
2563
|
runList,
|
|
2083
2564
|
runNew,
|
|
2084
2565
|
runRequests,
|
|
2566
|
+
runShow,
|
|
2567
|
+
runWatch,
|
|
2085
2568
|
surfaceSlug
|
|
2086
2569
|
};
|