leglas 0.1.1 → 0.3.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/dist/index.js CHANGED
@@ -45,7 +45,7 @@ function parseNew(rest) {
45
45
  if (surface === void 0) {
46
46
  return {
47
47
  kind: "error",
48
- message: "leglas new needs a surface name, for example: leglas new hero"
48
+ message: "leglas new needs a surface name, for example: npx leglas new hero"
49
49
  };
50
50
  }
51
51
  return { kind: "new", surface, print, json, from };
@@ -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
  }
@@ -126,13 +128,42 @@ function parseClassify(rest) {
126
128
  if (changes.length === 0) {
127
129
  return {
128
130
  kind: "error",
129
- message: "leglas classify needs what the direction will touch, for example: leglas classify --change package.json --rewrite src/theme.css"
131
+ message: "leglas classify needs what the direction will touch, for example: npx leglas classify --change package.json --rewrite src/theme.css"
130
132
  };
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") {
@@ -172,7 +203,7 @@ function parseArgs(argv) {
172
203
  if (title === void 0) {
173
204
  return {
174
205
  kind: "error",
175
- message: 'leglas keep needs a direction title, for example: leglas keep "Aurora" --to src/components/hero.tsx'
206
+ message: 'leglas keep needs a direction title, for example: npx leglas keep "Aurora" --to src/components/hero.tsx'
176
207
  };
177
208
  }
178
209
  if (to === void 0) {
@@ -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
  }
@@ -210,10 +253,10 @@ function parseArgs(argv) {
210
253
  if (surface === void 0) {
211
254
  return {
212
255
  kind: "error",
213
- message: "leglas explore needs a surface name, for example: leglas explore hero --count 6"
256
+ message: "leglas explore needs a surface name, for example: npx 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: npx 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/briefs.ts
482
- var ALL_BRIEFS = [
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 chosen = briefsFor(count);
551
- const previews = chosen.map((brief) => ({
552
- title: brief.name,
553
- url: `/?v-${slug}=${brief.slug}`
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.
561
555
 
562
- Each goes in its own file under .leglas/variants/${slug}/, named after its slug, and is listed in the DIRECTIONS map in that folder's switch file. If the surface has no switch file yet, run \`leglas new ${slug}\` first.
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}".
563
557
 
564
- Keep them distinct from each other. The point of exploring several at once is that they disagree; directions that converge on one look waste the exercise. Read each angle's "avoid" line before starting, because it names the obvious reading that collapses the difference.
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 ? ` npx leglas add --title "<name>" --url "/?v-${slug}=<key>" --note "<the idea, one line>"` : ` npx 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 \`npx 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:
565
561
 
566
- Then register them:
562
+ ${register}
567
563
 
568
- ` + commands.map((command) => ` ${command}`).join("\n");
569
- return { previews, commands, instructions };
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}
566
+
567
+ ${mechanics}` };
570
568
  }
571
569
 
572
570
  // src/run-explore.ts
573
571
  function runExplore(options, deps) {
574
- const chosen = briefsFor(options.count);
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 = planBriefs(options.surface, options.count);
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
  }
@@ -633,34 +609,48 @@ When asked for design variations, alternatives, or "a few options":
633
609
  If a direction cannot be additive, because it must change dependencies,
634
610
  change build configuration, or rewrite an existing file's behaviour, ask
635
611
  where it should live before writing it:
636
- \`leglas classify --change package.json --rewrite src/theme.css --json\`.
612
+ \`npx leglas classify --change package.json --rewrite src/theme.css --json\`.
637
613
  When the answer is \`checkout\`, build the direction on its own git branch
638
- and register it with \`leglas add --title "\u2026" --url "/" --branch <branch>\`
614
+ and register it with \`npx 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. It returns distinct
642
- angles to build, each with what to avoid, so several directions genuinely
643
- disagree instead of becoming shades of one idea. Follow those angles rather
644
- than inventing your own variations of the current design.
617
+ 2. Run \`npx 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
- \`leglas new <surface> --from <the component that renders it today>\`. It
624
+ \`npx 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. Put each direction in its own file beside the others in
651
- \`.leglas/variants/<surface>/\`, then list it in the \`DIRECTIONS\` map in
652
- that folder's \`switch\` file.
653
- 5. Register each one so it appears in the interface:
654
- \`leglas add --title "Aurora" --url "/?v-<surface>=aurora" --note "One line on the idea."\`
655
- 6. Tell the user to open the interface, or to run \`leglas\` if it is not
656
- already running.
657
-
658
- When the user asks to change one direction, check \`leglas requests --json\`
628
+ 4. Before building, make sure the interface is up. If Leglas is not
629
+ already running, tell the user to run \`npx leglas\`, and hand them the
630
+ URL now rather than when the set is done: the rail picks up each
631
+ registration 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:
635
+ \`npx leglas add --title "Aurora" --url "/?v-<surface>=aurora" --note "One line on the idea."\`
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.
639
+
640
+ When the user asks to change one direction, check \`npx leglas requests --json\`
659
641
  first: they may have described it from the interface, and the request names the
660
- exact file. Clear the queue with \`leglas requests --clear\` once done.
642
+ exact file. Acknowledge them with \`npx leglas requests --clear\` once done: it
643
+ drops what you collected and reports anything the user typed while you worked,
644
+ which is yours to collect and do next.
645
+
646
+ If the user wants requests handled the moment they are typed, without relaying
647
+ each one, tell them about \`npx leglas watch --run "claude -p {prompt}"\` (any
648
+ agent command works; {prompt} receives the request). It runs in their
649
+ terminal, hands each request to that command as it arrives, and the interface
650
+ shows the request's progress.
661
651
 
662
652
  When the user picks a winner, run
663
- \`leglas keep "<title>" --to <path in real source>\`. It moves that direction
653
+ \`npx leglas keep "<title>" --to <path in real source>\`. It moves that direction
664
654
  out of the ignored directory, deletes the rest of the exploration, and drops
665
655
  them from the rail. Then change their component to use the kept component
666
656
  instead of the switcher.
@@ -671,18 +661,24 @@ Useful to know:
671
661
  ships. Move a direction into real source only when it wins.
672
662
  - If the project has no running app yet, a direction can be a plain HTML
673
663
  file: write it under \`.leglas/pages/\` and register it with
674
- \`leglas add --title "Aurora" --file .leglas/pages/aurora.html\`. Leglas
664
+ \`npx leglas add --title "Aurora" --file .leglas/pages/aurora.html\`. Leglas
675
665
  serves the file itself, so no dev server is needed. Sibling assets in the
676
666
  same directory resolve normally.
677
- - Titles identify previews and must be unique.
678
- - \`leglas list\` shows every direction, shared and local.
667
+ - Titles identify previews and must be unique. The user may rename one in the
668
+ rail, which renames it on their machine only; the commands answer to either
669
+ name, so use whichever they said.
670
+ - \`npx leglas list\` shows every direction, shared and local.
671
+ - \`npx leglas show "<title>" --json\` answers for one of them: the file behind it,
672
+ the variants based on it, what it is being compared against, and anything
673
+ they have asked for that is not done yet. Run it when handed a direction you
674
+ did not register yourself.
679
675
  - Every command accepts \`--json\` and prints one envelope with a stable exit
680
676
  code, so you can drive it without parsing prose.
681
677
 
682
678
  ${AGENTS_MARKER_END}
683
679
  `;
684
680
  var STARTER_CONFIG = `// Previews are URLs of your own app. Add one per direction you want to
685
- // compare, then run \`leglas\`.
681
+ // compare, then run \`npx leglas\`.
686
682
  export default {
687
683
  // Where your dev server is. Override at the command line with --user-port.
688
684
  devServer: "http://localhost:3000",
@@ -808,6 +804,10 @@ function normalizeConfig(raw, options = {}) {
808
804
  errors.push(`${at} names a branch and a file; a file preview is served by Leglas itself and has no checkout.`);
809
805
  }
810
806
  }
807
+ const basedOn = entry["basedOn"];
808
+ if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
809
+ errors.push(`${at} has a basedOn that is not a direction title.`);
810
+ }
811
811
  const tags = entry["tags"];
812
812
  previews.push({
813
813
  title: typeof title === "string" ? title : "",
@@ -815,7 +815,8 @@ function normalizeConfig(raw, options = {}) {
815
815
  note: typeof entry["note"] === "string" ? entry["note"] : void 0,
816
816
  tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
817
817
  ...typeof branch === "string" ? { branch } : {},
818
- ...typeof file === "string" ? { file } : {}
818
+ ...typeof file === "string" ? { file } : {},
819
+ ...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {}
819
820
  });
820
821
  });
821
822
  const devCommand = source["devCommand"];
@@ -881,12 +882,12 @@ function isExplorationFile(path) {
881
882
  }
882
883
  var CHECKOUT_STEPS = [
883
884
  "Build the direction on its own branch: git switch -c <branch>, commit it there, switch back.",
884
- 'Register it: leglas add --title "<title>" --url "/" --branch <branch>.',
885
+ 'Register it: npx leglas add --title "<title>" --url "/" --branch <branch>.',
885
886
  "Make sure the config sets devCommand (with {port}), so Leglas can start the checkout."
886
887
  ];
887
888
  var IN_APP_STEPS = [
888
889
  "Author it additively under .leglas/variants/<surface>/, beside the existing directions.",
889
- 'Register it: leglas add --title "<title>" --url "/?v-<surface>=<direction>".'
890
+ 'Register it: npx leglas add --title "<title>" --url "/?v-<surface>=<direction>".'
890
891
  ];
891
892
  function classifyDirection(input) {
892
893
  const checkout = (reason) => ({ level: "checkout", reason, steps: CHECKOUT_STEPS });
@@ -1019,7 +1020,8 @@ async function addLocalPreview(cwd, input, shared) {
1019
1020
  ...input.note === void 0 ? {} : { note: input.note },
1020
1021
  ...input.tags === void 0 ? {} : { tags: input.tags },
1021
1022
  ...input.branch === void 0 ? {} : { branch: input.branch },
1022
- ...input.file === void 0 ? {} : { file: input.file }
1023
+ ...input.file === void 0 ? {} : { file: input.file },
1024
+ ...input.basedOn === void 0 ? {} : { basedOn: input.basedOn }
1023
1025
  };
1024
1026
  const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
1025
1027
  if (check.config === null) {
@@ -1253,6 +1255,7 @@ async function startAppProcess(options) {
1253
1255
 
1254
1256
  // ../server/dist/requests.js
1255
1257
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1258
+ import { randomBytes } from "crypto";
1256
1259
  import { dirname as dirname3, join as join4 } from "path";
1257
1260
  var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
1258
1261
  function targetFor(url) {
@@ -1291,7 +1294,16 @@ async function readRequests(cwd) {
1291
1294
  try {
1292
1295
  const raw = await readFile3(join4(cwd, REQUESTS_PATH), "utf8");
1293
1296
  const parsed = JSON.parse(raw);
1294
- return Array.isArray(parsed.requests) ? parsed.requests : [];
1297
+ if (!Array.isArray(parsed.requests))
1298
+ return [];
1299
+ return parsed.requests.map((request, index) => {
1300
+ const entry = request;
1301
+ return {
1302
+ ...entry,
1303
+ id: typeof entry.id === "string" ? entry.id : String(index),
1304
+ status: entry.status === "picked-up" ? "picked-up" : "queued"
1305
+ };
1306
+ });
1295
1307
  } catch {
1296
1308
  return [];
1297
1309
  }
@@ -1303,20 +1315,83 @@ async function writeQueue(cwd, requests) {
1303
1315
  `, "utf8");
1304
1316
  }
1305
1317
  async function appendRequest(cwd, request) {
1306
- await writeQueue(cwd, [...await readRequests(cwd), request]);
1318
+ await writeQueue(cwd, [
1319
+ ...await readRequests(cwd),
1320
+ { ...request, id: randomBytes(6).toString("base64url"), status: "queued" }
1321
+ ]);
1322
+ }
1323
+ async function collectRequests(cwd) {
1324
+ const requests = await readRequests(cwd);
1325
+ const collected = requests.map((request) => ({ ...request, status: "picked-up" }));
1326
+ if (requests.some((request) => request.status !== "picked-up"))
1327
+ await writeQueue(cwd, collected);
1328
+ return collected;
1329
+ }
1330
+ async function markPickedUp(cwd, id) {
1331
+ const requests = await readRequests(cwd);
1332
+ if (!requests.some((request) => request.id === id && request.status !== "picked-up"))
1333
+ return false;
1334
+ await writeQueue(cwd, requests.map((request) => request.id === id ? { ...request, status: "picked-up" } : request));
1335
+ return true;
1336
+ }
1337
+ async function removeRequest(cwd, id) {
1338
+ const requests = await readRequests(cwd);
1339
+ const remaining = requests.filter((request) => request.id !== id);
1340
+ if (remaining.length === requests.length)
1341
+ return false;
1342
+ await writeQueue(cwd, remaining);
1343
+ return true;
1307
1344
  }
1308
1345
  async function clearRequests(cwd) {
1309
- await writeQueue(cwd, []);
1346
+ const requests = await readRequests(cwd);
1347
+ const pending = requests.filter((request) => request.status !== "picked-up");
1348
+ const cleared = requests.length - pending.length;
1349
+ if (cleared > 0)
1350
+ await writeQueue(cwd, pending);
1351
+ return { cleared, pending: pending.length };
1352
+ }
1353
+
1354
+ // ../server/dist/renames.js
1355
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1356
+ import { dirname as dirname4, join as join5 } from "path";
1357
+ var RENAMES_PATH = ".leglas/renames.json";
1358
+ async function readRenames(cwd) {
1359
+ try {
1360
+ const raw = await readFile4(join5(cwd, RENAMES_PATH), "utf8");
1361
+ const parsed = JSON.parse(raw);
1362
+ if (parsed.renames === null || typeof parsed.renames !== "object")
1363
+ return {};
1364
+ return Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
1365
+ } catch {
1366
+ return {};
1367
+ }
1368
+ }
1369
+ async function writeRenames(cwd, renames) {
1370
+ const path = join5(cwd, RENAMES_PATH);
1371
+ await mkdir3(dirname4(path), { recursive: true });
1372
+ await writeFile3(path, `${JSON.stringify({ renames }, null, 2)}
1373
+ `, "utf8");
1374
+ }
1375
+ function resolveTitle(input, titles, renames) {
1376
+ if (titles.includes(input))
1377
+ return { ok: true, title: input };
1378
+ const matches = titles.filter((title) => renames[title] === input);
1379
+ if (matches.length === 1 && matches[0] !== void 0)
1380
+ return { ok: true, title: matches[0] };
1381
+ if (matches.length > 1)
1382
+ return { ok: false, reason: "ambiguous", matches };
1383
+ return { ok: false, reason: "unknown" };
1310
1384
  }
1311
1385
 
1312
1386
  // ../server/dist/server.js
1313
1387
  import { createReadStream, existsSync as existsSync2, statSync } from "fs";
1314
1388
  import http2 from "http";
1315
1389
  import net3 from "net";
1316
- import { extname, join as join5, normalize } from "path";
1390
+ import { extname, join as join6, normalize, relative as relative2 } from "path";
1317
1391
  var LEGLAS_PREFIX = "/leglas";
1318
1392
  var DEFAULT_PORT = 4100;
1319
1393
  var PORT_ATTEMPTS = 20;
1394
+ var ATTACHED_WINDOW_MS = 6e3;
1320
1395
  var CONTENT_TYPES = {
1321
1396
  ".css": "text/css; charset=utf-8",
1322
1397
  ".gif": "image/gif",
@@ -1364,8 +1439,8 @@ function probe(target, timeoutMs = 1e3) {
1364
1439
  });
1365
1440
  }
1366
1441
  function serveFrom(res, dir, relativePath) {
1367
- const relative3 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
1368
- const candidate = join5(dir, relative3);
1442
+ const relative4 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
1443
+ const candidate = join6(dir, relative4);
1369
1444
  if (!candidate.startsWith(dir))
1370
1445
  return false;
1371
1446
  if (!existsSync2(candidate) || !statSync(candidate).isFile())
@@ -1378,9 +1453,36 @@ function serveFrom(res, dir, relativePath) {
1378
1453
  return true;
1379
1454
  }
1380
1455
  function serveShellFile(res, shellDir, urlPath) {
1381
- const relative3 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
1382
- const isRoot = relative3 === "" || relative3 === "." || relative3 === "/";
1383
- return serveFrom(res, shellDir, isRoot ? "index.html" : relative3);
1456
+ const relative4 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
1457
+ const isRoot = relative4 === "" || relative4 === "." || relative4 === "/";
1458
+ return serveFrom(res, shellDir, isRoot ? "index.html" : relative4);
1459
+ }
1460
+ function snapshotConfig(cwd) {
1461
+ const path = findConfigFile(cwd);
1462
+ if (path === null)
1463
+ return null;
1464
+ try {
1465
+ return { path, mtimeMs: statSync(path).mtimeMs };
1466
+ } catch {
1467
+ return null;
1468
+ }
1469
+ }
1470
+ function configStalenessNotice(cwd, boot, current) {
1471
+ if (boot === null && current === null)
1472
+ return null;
1473
+ if (boot === null && current !== null) {
1474
+ const label = relative2(cwd, current.path) || current.path;
1475
+ return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
1476
+ }
1477
+ if (boot !== null && current === null) {
1478
+ const label = relative2(cwd, boot.path) || boot.path;
1479
+ return `${label} was removed after Leglas started. Restart leglas to run without it.`;
1480
+ }
1481
+ if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
1482
+ const label = relative2(cwd, current.path) || current.path;
1483
+ return `${label} changed after Leglas started. Restart leglas to pick it up.`;
1484
+ }
1485
+ return null;
1384
1486
  }
1385
1487
  var PLACEHOLDER = `<!doctype html>
1386
1488
  <meta charset="utf-8">
@@ -1390,7 +1492,8 @@ var PLACEHOLDER = `<!doctype html>
1390
1492
  <p>The server is running and proxying your app. The interface has not been
1391
1493
  built into this install yet.</p>
1392
1494
  <p><a href="/leglas/api/config">/leglas/api/config</a> \xB7
1393
- <a href="/leglas/api/health">/leglas/api/health</a></p>
1495
+ <a href="/leglas/api/health">/leglas/api/health</a> \xB7
1496
+ <a href="/leglas/api/requests">/leglas/api/requests</a></p>
1394
1497
  </body>`;
1395
1498
  function listen(server, port) {
1396
1499
  return new Promise((resolve, reject) => {
@@ -1425,28 +1528,45 @@ async function startServer(options) {
1425
1528
  const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map() } = options;
1426
1529
  const target = config?.devServer ?? "http://localhost:3000";
1427
1530
  const proxy = createProxyHandler({ target });
1531
+ const bootConfigSnapshot = snapshotConfig(cwd);
1532
+ let lastSeen = null;
1428
1533
  const server = http2.createServer((req, res) => {
1429
1534
  const url = req.url ?? "/";
1430
1535
  const path = url.split("?")[0] ?? "/";
1431
1536
  if (path === `${LEGLAS_PREFIX}/api/config`) {
1432
- return sendJson(res, 200, {
1537
+ const boot = config?.previews ?? [];
1538
+ const errors = [...configErrors];
1539
+ const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
1540
+ if (notice !== null)
1541
+ errors.push(notice);
1542
+ return void readLocalPreviews(cwd).then(({ previews: local }) => {
1543
+ const known = new Set(boot.map((preview) => preview.title));
1544
+ const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
1545
+ sendJson(res, 200, {
1546
+ project,
1547
+ devServer: target,
1548
+ previews: [...boot, ...fresh],
1549
+ errors
1550
+ });
1551
+ }).catch(() => sendJson(res, 200, {
1433
1552
  project,
1434
1553
  devServer: target,
1435
- previews: config?.previews ?? [],
1436
- errors: configErrors
1437
- });
1554
+ previews: boot,
1555
+ errors
1556
+ }));
1438
1557
  }
1439
1558
  if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
1440
1559
  let body = "";
1441
1560
  req.on("data", (chunk) => body += chunk);
1442
- return void req.on("end", () => {
1561
+ return void req.on("end", async () => {
1443
1562
  let parsed;
1444
1563
  try {
1445
1564
  parsed = JSON.parse(body || "{}");
1446
1565
  } catch {
1447
1566
  return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
1448
1567
  }
1449
- const preview = (config?.previews ?? []).find((entry) => entry.title === parsed.title);
1568
+ const local = await readLocalPreviews(cwd).then((read) => read.previews, () => []);
1569
+ const preview = [...config?.previews ?? [], ...local].find((entry) => entry.title === parsed.title);
1450
1570
  if (!preview || !parsed.intent?.trim()) {
1451
1571
  return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
1452
1572
  }
@@ -1459,6 +1579,46 @@ async function startServer(options) {
1459
1579
  }).then(() => sendJson(res, 200, { ok: true, ...composed })).catch(() => sendJson(res, 200, { ok: true, ...composed, queued: false }));
1460
1580
  });
1461
1581
  }
1582
+ if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
1583
+ let body = "";
1584
+ req.on("data", (chunk) => body += chunk);
1585
+ return void req.on("end", () => {
1586
+ let parsed;
1587
+ try {
1588
+ parsed = JSON.parse(body || "{}");
1589
+ } catch {
1590
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
1591
+ }
1592
+ if (typeof parsed.watching !== "boolean") {
1593
+ return sendJson(res, 400, { ok: false, error: "Body needs a watching boolean." });
1594
+ }
1595
+ lastSeen = parsed.watching ? Date.now() : null;
1596
+ sendJson(res, 200, { ok: true });
1597
+ });
1598
+ }
1599
+ if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
1600
+ return void readRequests(cwd).then((requests) => sendJson(res, 200, {
1601
+ requests: requests.map(({ id, title, intent, status }) => ({ id, title, intent, status })),
1602
+ agent: { attached: lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS }
1603
+ }));
1604
+ }
1605
+ if (path === `${LEGLAS_PREFIX}/api/renames` && req.method === "POST") {
1606
+ let body = "";
1607
+ req.on("data", (chunk) => body += chunk);
1608
+ return void req.on("end", () => {
1609
+ let parsed;
1610
+ try {
1611
+ parsed = JSON.parse(body || "{}");
1612
+ } catch {
1613
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
1614
+ }
1615
+ if (parsed.renames === null || typeof parsed.renames !== "object") {
1616
+ return sendJson(res, 400, { ok: false, error: "Body needs a renames object." });
1617
+ }
1618
+ const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
1619
+ void writeRenames(cwd, renames).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 200, { ok: false }));
1620
+ });
1621
+ }
1462
1622
  if (path === `${LEGLAS_PREFIX}/api/health`) {
1463
1623
  return void probe(target).then((reachable) => sendJson(res, 200, { devServer: target, reachable }));
1464
1624
  }
@@ -1466,21 +1626,28 @@ async function startServer(options) {
1466
1626
  const rest = path.slice(FILES_PREFIX.length + 1);
1467
1627
  const slash = rest.indexOf("/");
1468
1628
  const slug = slash === -1 ? rest : rest.slice(0, slash);
1469
- let relative3 = slash === -1 ? "" : rest.slice(slash + 1);
1629
+ let relative4 = slash === -1 ? "" : rest.slice(slash + 1);
1470
1630
  try {
1471
- relative3 = decodeURIComponent(relative3);
1631
+ relative4 = decodeURIComponent(relative4);
1472
1632
  } catch {
1473
- relative3 = "";
1633
+ relative4 = "";
1474
1634
  }
1475
1635
  const dir = fileMounts.get(slug);
1476
- if (dir !== void 0 && relative3 !== "" && serveFrom(res, dir, relative3))
1636
+ if (dir !== void 0 && relative4 !== "" && serveFrom(res, dir, relative4))
1477
1637
  return;
1478
1638
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
1479
1639
  return res.end("Leglas: no such preview file.");
1480
1640
  }
1641
+ if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
1642
+ return sendJson(res, 404, { error: "No such Leglas API path." });
1643
+ }
1481
1644
  if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
1482
1645
  if (shellDir !== null && serveShellFile(res, shellDir, path))
1483
1646
  return;
1647
+ if (shellDir !== null) {
1648
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
1649
+ return res.end("Leglas: no such path.");
1650
+ }
1484
1651
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
1485
1652
  return res.end(PLACEHOLDER);
1486
1653
  }
@@ -1529,7 +1696,7 @@ function planKeep(options) {
1529
1696
  if (!winner) {
1530
1697
  return {
1531
1698
  ok: false,
1532
- error: `No direction called ${JSON.stringify(options.title)}. Run leglas list to see them.`
1699
+ error: `No direction called ${JSON.stringify(options.title)}. Run npx leglas list to see them.`
1533
1700
  };
1534
1701
  }
1535
1702
  const from = targetFor(winner.url);
@@ -1566,11 +1733,11 @@ function planKeep(options) {
1566
1733
  }
1567
1734
 
1568
1735
  // src/run-init.ts
1569
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1570
- import { join as join6 } from "path";
1736
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
1737
+ import { join as join7 } from "path";
1571
1738
  async function readIfPresent(path) {
1572
1739
  try {
1573
- return await readFile4(path, "utf8");
1740
+ return await readFile5(path, "utf8");
1574
1741
  } catch {
1575
1742
  return null;
1576
1743
  }
@@ -1578,18 +1745,18 @@ async function readIfPresent(path) {
1578
1745
  async function runInit(options, deps) {
1579
1746
  const existingConfig = findConfigFile(options.cwd);
1580
1747
  const plan = planInit({
1581
- agents: await readIfPresent(join6(options.cwd, "AGENTS.md")),
1748
+ agents: await readIfPresent(join7(options.cwd, "AGENTS.md")),
1582
1749
  config: existingConfig === null ? null : "present",
1583
- gitignore: await readIfPresent(join6(options.cwd, ".gitignore")),
1750
+ gitignore: await readIfPresent(join7(options.cwd, ".gitignore")),
1584
1751
  force: options.force
1585
1752
  });
1586
1753
  const touched = [];
1587
1754
  for (const write of plan.writes) {
1588
- await writeFile3(join6(options.cwd, write.path), write.contents, "utf8");
1755
+ await writeFile4(join7(options.cwd, write.path), write.contents, "utf8");
1589
1756
  touched.push(write.path);
1590
1757
  }
1591
1758
  if (plan.gitignore !== null) {
1592
- await writeFile3(join6(options.cwd, ".gitignore"), plan.gitignore, "utf8");
1759
+ await writeFile4(join7(options.cwd, ".gitignore"), plan.gitignore, "utf8");
1593
1760
  touched.push(".gitignore");
1594
1761
  }
1595
1762
  if (options.json) {
@@ -1609,8 +1776,26 @@ async function runInit(options, deps) {
1609
1776
 
1610
1777
  // src/run-keep.ts
1611
1778
  import { existsSync as existsSync3 } from "fs";
1612
- import { mkdir as mkdir3, readFile as readFile5, rm as rm2, writeFile as writeFile4 } from "fs/promises";
1613
- import { dirname as dirname4, join as join7 } from "path";
1779
+ import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
1780
+ import { dirname as dirname5, join as join8 } from "path";
1781
+
1782
+ // src/resolve-title.ts
1783
+ function resolveOrExplain(input, titles, renames) {
1784
+ const resolution = resolveTitle(input, titles, renames);
1785
+ if (resolution.ok) return { ok: true, title: resolution.title };
1786
+ if (resolution.reason === "ambiguous") {
1787
+ return {
1788
+ ok: false,
1789
+ 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.`
1790
+ };
1791
+ }
1792
+ return {
1793
+ ok: false,
1794
+ 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. npx leglas list shows every title.`
1795
+ };
1796
+ }
1797
+
1798
+ // src/run-keep.ts
1614
1799
  function renameExport(source, to) {
1615
1800
  const match = /export function ([A-Za-z0-9_]+)\s*\(/.exec(source);
1616
1801
  if (!match || match[1] === void 0) return source;
@@ -1623,31 +1808,37 @@ async function runKeep(options, deps) {
1623
1808
  const loaded = await loadConfig(options.cwd);
1624
1809
  const local = await readLocalPreviews(options.cwd);
1625
1810
  const previews = [...loaded.config?.previews ?? [], ...local.previews];
1626
- const plan = planKeep({ title: options.title, previews, to: options.to });
1627
1811
  const fail = (error) => {
1628
1812
  if (options.json) deps.log(JSON.stringify({ ok: false, error }));
1629
1813
  else deps.error(error);
1630
1814
  return { exitCode: 1 };
1631
1815
  };
1816
+ const resolved = resolveOrExplain(
1817
+ options.title,
1818
+ previews.map((preview) => preview.title),
1819
+ await readRenames(options.cwd)
1820
+ );
1821
+ if (!resolved.ok) return fail(resolved.error);
1822
+ const plan = planKeep({ title: resolved.title, previews, to: options.to });
1632
1823
  if (!plan.ok) return fail(plan.error);
1633
- const from = join7(options.cwd, plan.move.from);
1634
- const to = join7(options.cwd, plan.move.to);
1824
+ const from = join8(options.cwd, plan.move.from);
1825
+ const to = join8(options.cwd, plan.move.to);
1635
1826
  if (!existsSync3(from)) {
1636
1827
  return fail(`${plan.move.from} does not exist. Nothing to keep.`);
1637
1828
  }
1638
1829
  if (existsSync3(to)) {
1639
1830
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
1640
1831
  }
1641
- const source = await readFile5(from, "utf8");
1642
- await mkdir3(dirname4(to), { recursive: true });
1643
- await writeFile4(to, renameExport(source, plan.exportName), "utf8");
1644
- await rm2(join7(options.cwd, plan.removeDir), { recursive: true, force: true });
1832
+ const source = await readFile6(from, "utf8");
1833
+ await mkdir4(dirname5(to), { recursive: true });
1834
+ await writeFile5(to, renameExport(source, plan.exportName), "utf8");
1835
+ await rm2(join8(options.cwd, plan.removeDir), { recursive: true, force: true });
1645
1836
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
1646
1837
  if (options.json) {
1647
1838
  deps.log(
1648
1839
  JSON.stringify({
1649
1840
  ok: true,
1650
- kept: options.title,
1841
+ kept: resolved.title,
1651
1842
  to: plan.move.to,
1652
1843
  exportName: plan.exportName,
1653
1844
  removed: plan.removeDir,
@@ -1676,11 +1867,11 @@ async function runKeep(options, deps) {
1676
1867
 
1677
1868
  // src/run-new.ts
1678
1869
  import { existsSync as existsSync4 } from "fs";
1679
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
1680
- import { dirname as dirname5, join as join8 } from "path";
1870
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
1871
+ import { dirname as dirname6, join as join9 } from "path";
1681
1872
  async function readIfPresent2(path) {
1682
1873
  try {
1683
- return await readFile6(path, "utf8");
1874
+ return await readFile7(path, "utf8");
1684
1875
  } catch {
1685
1876
  return null;
1686
1877
  }
@@ -1688,7 +1879,7 @@ async function readIfPresent2(path) {
1688
1879
  async function runNew(options, deps) {
1689
1880
  let from;
1690
1881
  if (options.from !== void 0) {
1691
- const contents = await readIfPresent2(join8(options.cwd, options.from));
1882
+ const contents = await readIfPresent2(join9(options.cwd, options.from));
1692
1883
  if (contents === null) {
1693
1884
  const message = `${options.from} does not exist, so there is nothing to use as the baseline.`;
1694
1885
  if (options.json) deps.log(JSON.stringify({ ok: false, error: message }));
@@ -1699,8 +1890,8 @@ async function runNew(options, deps) {
1699
1890
  }
1700
1891
  const plan = planNew({
1701
1892
  surface: options.surface,
1702
- packageJson: await readIfPresent2(join8(options.cwd, "package.json")),
1703
- gitignore: await readIfPresent2(join8(options.cwd, ".gitignore")),
1893
+ packageJson: await readIfPresent2(join9(options.cwd, "package.json")),
1894
+ gitignore: await readIfPresent2(join9(options.cwd, ".gitignore")),
1704
1895
  from
1705
1896
  });
1706
1897
  const fail = (error) => {
@@ -1723,19 +1914,19 @@ async function runNew(options, deps) {
1723
1914
  deps.log(plan.instructions);
1724
1915
  return { exitCode: 0, written: [] };
1725
1916
  }
1726
- const existing = plan.writes.filter((write) => existsSync4(join8(options.cwd, write.path)));
1917
+ const existing = plan.writes.filter((write) => existsSync4(join9(options.cwd, write.path)));
1727
1918
  if (existing.length > 0) {
1728
1919
  return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
1729
1920
  }
1730
1921
  const written = [];
1731
1922
  for (const write of plan.writes) {
1732
- const target = join8(options.cwd, write.path);
1733
- await mkdir4(dirname5(target), { recursive: true });
1734
- await writeFile5(target, write.contents, "utf8");
1923
+ const target = join9(options.cwd, write.path);
1924
+ await mkdir5(dirname6(target), { recursive: true });
1925
+ await writeFile6(target, write.contents, "utf8");
1735
1926
  written.push(write.path);
1736
1927
  }
1737
1928
  if (plan.gitignore !== null) {
1738
- await writeFile5(join8(options.cwd, ".gitignore"), plan.gitignore, "utf8");
1929
+ await writeFile6(join9(options.cwd, ".gitignore"), plan.gitignore, "utf8");
1739
1930
  written.push(".gitignore");
1740
1931
  }
1741
1932
  if (options.json) {
@@ -1750,31 +1941,41 @@ async function runNew(options, deps) {
1750
1941
  deps.log("Then register them so they appear in the interface:");
1751
1942
  deps.log("");
1752
1943
  for (const preview of plan.previews) {
1753
- deps.log(` leglas add --title ${JSON.stringify(preview.title)} --url ${JSON.stringify(preview.url)}`);
1944
+ deps.log(` npx leglas add --title ${JSON.stringify(preview.title)} --url ${JSON.stringify(preview.url)}`);
1754
1945
  }
1755
1946
  return { exitCode: 0, written };
1756
1947
  }
1757
1948
 
1758
1949
  // src/run-previews.ts
1759
- import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
1760
- import { join as join9 } from "path";
1950
+ import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
1951
+ import { join as join10 } from "path";
1761
1952
  function envelope(deps, ok, body) {
1762
1953
  deps.log(JSON.stringify({ ok, ...body }));
1763
1954
  }
1764
1955
  async function ensureIgnored(cwd) {
1765
- const path = join9(cwd, ".gitignore");
1956
+ const path = join10(cwd, ".gitignore");
1766
1957
  let current = null;
1767
1958
  try {
1768
- current = await readFile7(path, "utf8");
1959
+ current = await readFile8(path, "utf8");
1769
1960
  } catch {
1770
1961
  current = null;
1771
1962
  }
1772
1963
  const next = ignoreEntry(current);
1773
- if (next !== null) await writeFile6(path, next, "utf8");
1964
+ if (next !== null) await writeFile7(path, next, "utf8");
1774
1965
  }
1775
1966
  async function runAdd(options, deps) {
1776
1967
  const loaded = await loadConfig(options.cwd);
1777
1968
  const shared = loaded.config?.previews ?? [];
1969
+ if (options.preview.basedOn !== void 0) {
1970
+ const local = await readLocalPreviews(options.cwd);
1971
+ const titles = new Set([...shared, ...local.previews].map((preview) => preview.title));
1972
+ if (!titles.has(options.preview.basedOn)) {
1973
+ const error = `--based-on names ${JSON.stringify(options.preview.basedOn)}, which is not a registered direction. npx leglas list shows what exists.`;
1974
+ if (options.json) envelope(deps, false, { error });
1975
+ else deps.error(error);
1976
+ return { exitCode: 1 };
1977
+ }
1978
+ }
1778
1979
  const outcome = await addLocalPreview(
1779
1980
  options.cwd,
1780
1981
  {
@@ -1783,7 +1984,8 @@ async function runAdd(options, deps) {
1783
1984
  note: options.preview.note,
1784
1985
  tags: options.preview.tags,
1785
1986
  branch: options.preview.branch,
1786
- file: options.preview.file
1987
+ file: options.preview.file,
1988
+ basedOn: options.preview.basedOn
1787
1989
  },
1788
1990
  shared
1789
1991
  );
@@ -1801,6 +2003,7 @@ async function runAdd(options, deps) {
1801
2003
  local: true,
1802
2004
  ...options.preview.branch === void 0 ? {} : { branch: options.preview.branch },
1803
2005
  ...options.preview.file === void 0 ? {} : { file: options.preview.file },
2006
+ note: options.preview.branch === void 0 && options.preview.file === void 0 ? "A running interface picks this up within seconds." : "Restart Leglas to see this preview: branch checkouts and file mounts are built when Leglas starts.",
1804
2007
  ...needsDevCommand ? { warning: "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config." } : {}
1805
2008
  });
1806
2009
  } else {
@@ -1813,7 +2016,11 @@ async function runAdd(options, deps) {
1813
2016
  deps.log(" Add devCommand (with {port}) to the config.");
1814
2017
  deps.log("");
1815
2018
  }
1816
- deps.log("Local to this machine. Restart Leglas to see it, or run leglas list.");
2019
+ if (options.preview.branch === void 0 && options.preview.file === void 0) {
2020
+ deps.log("Local to this machine. A running interface picks it up within seconds.");
2021
+ } else {
2022
+ deps.log("Local to this machine. Restart Leglas to see it, or run npx leglas list.");
2023
+ }
1817
2024
  }
1818
2025
  return { exitCode: 0 };
1819
2026
  }
@@ -1827,9 +2034,17 @@ async function runList(options, deps) {
1827
2034
  ];
1828
2035
  if (options.json) {
1829
2036
  envelope(deps, errors.length === 0, {
2037
+ // The whole record, not a summary of it. What the config holds about a
2038
+ // preview — its note, its tags, the direction it is a variant of — is
2039
+ // exactly what tells an agent why these are being compared, and leaving
2040
+ // it out made the listing thinner than the reference block that points
2041
+ // at it.
1830
2042
  previews: previews.map((preview) => ({
1831
2043
  title: preview.title,
1832
2044
  url: preview.url,
2045
+ note: preview.note ?? null,
2046
+ tags: preview.tags,
2047
+ basedOn: preview.basedOn ?? null,
1833
2048
  local: preview.local,
1834
2049
  branch: preview.branch ?? null,
1835
2050
  file: preview.file ?? null
@@ -1839,7 +2054,7 @@ async function runList(options, deps) {
1839
2054
  return { exitCode: errors.length === 0 ? 0 : 1 };
1840
2055
  }
1841
2056
  if (previews.length === 0) {
1842
- deps.log("No previews yet. Add one with leglas add, or list them in leglas.config.ts.");
2057
+ deps.log("No previews yet. Add one with npx leglas add, or list them in leglas.config.ts.");
1843
2058
  } else {
1844
2059
  const width = Math.max(...previews.map((preview) => preview.title.length));
1845
2060
  for (const preview of previews) {
@@ -1854,12 +2069,19 @@ async function runList(options, deps) {
1854
2069
  }
1855
2070
  async function runRequests(options, deps) {
1856
2071
  if (options.clear) {
1857
- await clearRequests(options.cwd);
1858
- if (options.json) envelope(deps, true, { cleared: true });
1859
- else deps.log("Queue cleared.");
2072
+ const { cleared, pending } = await clearRequests(options.cwd);
2073
+ if (options.json) envelope(deps, true, { cleared, pending });
2074
+ else {
2075
+ deps.log(cleared === 1 ? "Cleared 1 request." : `Cleared ${cleared} requests.`);
2076
+ if (pending > 0) {
2077
+ deps.log(
2078
+ `${pending} arrived while you worked. Run npx leglas requests to collect ${pending === 1 ? "it" : "them"}.`
2079
+ );
2080
+ }
2081
+ }
1860
2082
  return { exitCode: 0 };
1861
2083
  }
1862
- const requests = await readRequests(options.cwd);
2084
+ const requests = await collectRequests(options.cwd);
1863
2085
  if (options.json) {
1864
2086
  envelope(deps, true, { requests });
1865
2087
  return { exitCode: 0 };
@@ -1873,18 +2095,326 @@ async function runRequests(options, deps) {
1873
2095
  if (request.target !== null) deps.log(` ${request.target}`);
1874
2096
  }
1875
2097
  deps.log("");
1876
- deps.log("Run leglas requests --json to get the full prompts, then --clear when done.");
2098
+ deps.log("Run npx leglas requests --json to get the full prompts, then --clear when done.");
1877
2099
  return { exitCode: 0 };
1878
2100
  }
1879
2101
 
2102
+ // src/show.ts
2103
+ function describe(preview) {
2104
+ return {
2105
+ title: preview.title,
2106
+ url: preview.url,
2107
+ note: preview.note ?? null,
2108
+ tags: preview.tags,
2109
+ basedOn: preview.basedOn ?? null,
2110
+ branch: preview.branch ?? null,
2111
+ file: preview.file ?? null,
2112
+ local: preview.local === true,
2113
+ // A file preview names its own source. Everything else is decoded from the
2114
+ // URL, and a URL outside the convention yields nothing rather than a path
2115
+ // that looks authoritative and is not there.
2116
+ target: preview.file ?? targetFor(preview.url)
2117
+ };
2118
+ }
2119
+ function planShow({ title, previews, requests }) {
2120
+ const found = previews.find((preview) => preview.title === title);
2121
+ if (!found) {
2122
+ return {
2123
+ ok: false,
2124
+ error: `No direction called ${JSON.stringify(title)}. Run npx leglas list to see them.`
2125
+ };
2126
+ }
2127
+ const variants = previews.filter((preview) => preview.basedOn === title).map(describe);
2128
+ const variantTitles = new Set(variants.map((variant) => variant.title));
2129
+ return {
2130
+ ok: true,
2131
+ direction: describe(found),
2132
+ variants,
2133
+ // Its own variants are already listed in full, so they are not repeated
2134
+ // here; this is the rest of the comparison.
2135
+ comparedWith: previews.map((preview) => preview.title).filter((other) => other !== title && !variantTitles.has(other)),
2136
+ requests: requests.filter((request) => request.title === title).map((request) => ({
2137
+ id: request.id,
2138
+ intent: request.intent,
2139
+ target: request.target,
2140
+ prompt: request.prompt,
2141
+ status: request.status
2142
+ }))
2143
+ };
2144
+ }
2145
+
2146
+ // src/run-show.ts
2147
+ async function runShow(options, deps) {
2148
+ const loaded = await loadConfig(options.cwd);
2149
+ const local = await readLocalPreviews(options.cwd);
2150
+ const requests = await readRequests(options.cwd);
2151
+ const previews = [
2152
+ ...(loaded.config?.previews ?? []).map((preview) => ({ ...preview, local: false })),
2153
+ ...local.previews
2154
+ ];
2155
+ const resolved = resolveOrExplain(
2156
+ options.title,
2157
+ previews.map((preview) => preview.title),
2158
+ await readRenames(options.cwd)
2159
+ );
2160
+ if (!resolved.ok) {
2161
+ if (options.json) deps.log(JSON.stringify({ ok: false, error: resolved.error }));
2162
+ else deps.error(resolved.error);
2163
+ return { exitCode: 1 };
2164
+ }
2165
+ const plan = planShow({ title: resolved.title, previews, requests });
2166
+ if (!plan.ok) {
2167
+ if (options.json) deps.log(JSON.stringify({ ok: false, error: plan.error }));
2168
+ else deps.error(plan.error);
2169
+ return { exitCode: 1 };
2170
+ }
2171
+ if (options.json) {
2172
+ deps.log(
2173
+ JSON.stringify({
2174
+ ok: true,
2175
+ direction: plan.direction,
2176
+ variants: plan.variants,
2177
+ comparedWith: plan.comparedWith,
2178
+ requests: plan.requests
2179
+ })
2180
+ );
2181
+ return { exitCode: 0 };
2182
+ }
2183
+ const { direction } = plan;
2184
+ deps.log(` ${direction.title}${direction.local ? " (local)" : ""}`);
2185
+ if (direction.note !== null) deps.log(` ${direction.note}`);
2186
+ deps.log("");
2187
+ if (direction.target !== null) deps.log(` file ${direction.target}`);
2188
+ if (direction.branch !== null) deps.log(` branch ${direction.branch}`);
2189
+ deps.log(` url ${direction.url}`);
2190
+ if (direction.tags.length > 0) deps.log(` tags ${direction.tags.join(", ")}`);
2191
+ if (direction.basedOn !== null) deps.log(` variant of ${direction.basedOn}`);
2192
+ if (plan.variants.length > 0) {
2193
+ deps.log(` variants ${plan.variants.map((variant) => variant.title).join(", ")}`);
2194
+ }
2195
+ if (plan.comparedWith.length > 0) {
2196
+ deps.log(` against ${plan.comparedWith.join(", ")}`);
2197
+ }
2198
+ if (plan.requests.length > 0) {
2199
+ deps.log("");
2200
+ deps.log(` Pending, not yet done (${plan.requests.length}):`);
2201
+ for (const request of plan.requests) deps.log(` ${request.status} ${request.intent}`);
2202
+ deps.log("");
2203
+ deps.log(" Run npx leglas requests --json for the full prompts.");
2204
+ }
2205
+ return { exitCode: 0 };
2206
+ }
2207
+
2208
+ // src/watch.ts
2209
+ var WATCH_PATH = ".leglas/watch.json";
2210
+ var PROMPT_TOKEN = "{prompt}";
2211
+ var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
2212
+ function tokenize(template) {
2213
+ const tokens = [];
2214
+ let current = "";
2215
+ let started = false;
2216
+ let quote = null;
2217
+ for (const character of template) {
2218
+ if (quote !== null) {
2219
+ if (character === quote) quote = null;
2220
+ else current += character;
2221
+ continue;
2222
+ }
2223
+ if (character === '"' || character === "'") {
2224
+ quote = character;
2225
+ started = true;
2226
+ continue;
2227
+ }
2228
+ if (/\s/.test(character)) {
2229
+ if (started) tokens.push(current);
2230
+ current = "";
2231
+ started = false;
2232
+ continue;
2233
+ }
2234
+ current += character;
2235
+ started = true;
2236
+ }
2237
+ if (quote !== null) {
2238
+ return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
2239
+ }
2240
+ if (started) tokens.push(current);
2241
+ return { ok: true, tokens };
2242
+ }
2243
+ function parseTemplate(raw) {
2244
+ const tokenized = tokenize(raw);
2245
+ if (!tokenized.ok) return tokenized;
2246
+ const { tokens } = tokenized;
2247
+ const [command, ...args] = tokens;
2248
+ if (command === void 0) {
2249
+ return { ok: false, error: `Watch needs an agent command, for example: ${EXAMPLE}` };
2250
+ }
2251
+ const placeholders = tokens.filter((token) => token === PROMPT_TOKEN).length;
2252
+ if (placeholders === 0) {
2253
+ return {
2254
+ ok: false,
2255
+ error: `The agent command needs ${PROMPT_TOKEN} as a word of its own, for example: ${EXAMPLE}`
2256
+ };
2257
+ }
2258
+ if (placeholders > 1) {
2259
+ return {
2260
+ ok: false,
2261
+ error: `The agent command takes ${PROMPT_TOKEN} once, for example: ${EXAMPLE}`
2262
+ };
2263
+ }
2264
+ if (command === PROMPT_TOKEN) {
2265
+ return {
2266
+ ok: false,
2267
+ error: `The agent command must name a program before ${PROMPT_TOKEN}, for example: ${EXAMPLE}`
2268
+ };
2269
+ }
2270
+ return { ok: true, template: { command, args } };
2271
+ }
2272
+ function commandFor(template, prompt) {
2273
+ return {
2274
+ command: template.command,
2275
+ args: template.args.map((argument) => argument === PROMPT_TOKEN ? prompt : argument)
2276
+ };
2277
+ }
2278
+ function nextRequest(requests, failed) {
2279
+ return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
2280
+ }
2281
+
2282
+ // src/run-watch.ts
2283
+ import { spawn as spawn2 } from "child_process";
2284
+ import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
2285
+ import { dirname as dirname7, join as join11 } from "path";
2286
+ var POLL_MS = 2e3;
2287
+ var HEARTBEAT_TIMEOUT_MS = 1e3;
2288
+ async function readSavedTemplate(cwd) {
2289
+ try {
2290
+ const raw = await readFile9(join11(cwd, WATCH_PATH), "utf8");
2291
+ const parsed = JSON.parse(raw);
2292
+ return typeof parsed.run === "string" && parsed.run !== "" ? parsed.run : null;
2293
+ } catch {
2294
+ return null;
2295
+ }
2296
+ }
2297
+ async function saveTemplate(cwd, run3) {
2298
+ const path = join11(cwd, WATCH_PATH);
2299
+ await mkdir6(dirname7(path), { recursive: true });
2300
+ await writeFile8(path, `${JSON.stringify({ run: run3 }, null, 2)}
2301
+ `, "utf8");
2302
+ }
2303
+ function spawnAgent(command, args, cwd) {
2304
+ return new Promise((resolve) => {
2305
+ let settled = false;
2306
+ const settle = (outcome) => {
2307
+ if (settled) return;
2308
+ settled = true;
2309
+ resolve(outcome);
2310
+ };
2311
+ const child = spawn2(command, args, { cwd, stdio: "inherit" });
2312
+ child.on("error", (error) => settle({ ok: false, error: error.message }));
2313
+ child.on(
2314
+ "close",
2315
+ (code, signal) => settle(
2316
+ signal === null ? { ok: true, code: code ?? 0 } : { ok: false, error: `stopped by ${signal}` }
2317
+ )
2318
+ );
2319
+ });
2320
+ }
2321
+ async function runWatch(options, deps) {
2322
+ const saved = options.run === void 0 ? await readSavedTemplate(options.cwd) : null;
2323
+ const raw = options.run ?? saved;
2324
+ if (raw === null) {
2325
+ deps.error(
2326
+ 'Watch needs an agent command the first time: npx leglas watch --run "claude -p {prompt}"'
2327
+ );
2328
+ return { exitCode: 1 };
2329
+ }
2330
+ const parsed = parseTemplate(raw);
2331
+ if (!parsed.ok) {
2332
+ deps.error(parsed.error);
2333
+ return { exitCode: 1 };
2334
+ }
2335
+ const template = parsed.template;
2336
+ if (options.run !== void 0) await saveTemplate(options.cwd, raw).catch(() => {
2337
+ });
2338
+ const base = `http://localhost:${options.port ?? DEFAULT_PORT}`;
2339
+ const heartbeat = async (watching) => {
2340
+ try {
2341
+ await fetch(`${base}${LEGLAS_PREFIX}/api/watch`, {
2342
+ method: "POST",
2343
+ headers: { "content-type": "application/json" },
2344
+ body: JSON.stringify({ watching }),
2345
+ signal: AbortSignal.timeout(HEARTBEAT_TIMEOUT_MS)
2346
+ });
2347
+ } catch {
2348
+ }
2349
+ };
2350
+ deps.log(`Watching for change requests. Each one runs: ${raw}`);
2351
+ deps.log("Stop with Ctrl-C.");
2352
+ const failed = /* @__PURE__ */ new Set();
2353
+ let stopped = false;
2354
+ let busy = false;
2355
+ let inflight = null;
2356
+ const handle = async (request) => {
2357
+ deps.log("");
2358
+ deps.log(` ${request.title}: ${request.intent}`);
2359
+ if (request.target !== null) deps.log(` ${request.target}`);
2360
+ await markPickedUp(options.cwd, request.id);
2361
+ const { command, args } = commandFor(template, request.prompt);
2362
+ const outcome = await spawnAgent(command, args, options.cwd);
2363
+ if (outcome.ok && outcome.code === 0) {
2364
+ await removeRequest(options.cwd, request.id);
2365
+ deps.log(` done ${request.title}`);
2366
+ return;
2367
+ }
2368
+ failed.add(request.id);
2369
+ deps.error(
2370
+ ` failed ${request.title}: ${outcome.ok ? `${command} exited ${outcome.code}` : outcome.error}`
2371
+ );
2372
+ deps.error(" Left in the queue and not retried.");
2373
+ };
2374
+ const tick = async () => {
2375
+ if (stopped) return;
2376
+ void heartbeat(true);
2377
+ if (busy) return;
2378
+ busy = true;
2379
+ try {
2380
+ const request = nextRequest(await readRequests(options.cwd), failed);
2381
+ if (request !== null && !stopped) {
2382
+ inflight = handle(request);
2383
+ await inflight;
2384
+ }
2385
+ } catch (error) {
2386
+ deps.error(` ! ${error instanceof Error ? error.message : String(error)}`);
2387
+ } finally {
2388
+ inflight = null;
2389
+ busy = false;
2390
+ }
2391
+ };
2392
+ return new Promise((resolve) => {
2393
+ const timer = setInterval(() => void tick(), POLL_MS);
2394
+ const stop = () => {
2395
+ if (stopped) return;
2396
+ stopped = true;
2397
+ clearInterval(timer);
2398
+ process.off("SIGINT", stop);
2399
+ process.off("SIGTERM", stop);
2400
+ void Promise.resolve(inflight).catch(() => {
2401
+ }).then(() => heartbeat(false)).then(() => resolve({ exitCode: 0 }));
2402
+ };
2403
+ process.on("SIGINT", stop);
2404
+ process.on("SIGTERM", stop);
2405
+ options.signal?.addEventListener("abort", stop, { once: true });
2406
+ void tick();
2407
+ });
2408
+ }
2409
+
1880
2410
  // src/run-classify.ts
1881
2411
  import { stat } from "fs/promises";
1882
- import { join as join10 } from "path";
2412
+ import { join as join12 } from "path";
1883
2413
  async function runClassify(options, deps) {
1884
2414
  const declared = await Promise.all(
1885
2415
  options.changes.map(async (change) => ({
1886
2416
  ...change,
1887
- exists: await stat(join10(options.cwd, change.path)).then(
2417
+ exists: await stat(join12(options.cwd, change.path)).then(
1888
2418
  () => true,
1889
2419
  () => false
1890
2420
  )
@@ -1912,14 +2442,14 @@ async function runClassify(options, deps) {
1912
2442
  // src/run.ts
1913
2443
  import { existsSync as existsSync5 } from "fs";
1914
2444
  import { createRequire } from "module";
1915
- import { basename as basename3, dirname as dirname6, join as join11, relative as relative2 } from "path";
2445
+ import { basename as basename3, dirname as dirname8, join as join13, relative as relative3 } from "path";
1916
2446
  import { fileURLToPath } from "url";
1917
2447
  function findShellDir() {
1918
- const bundled = join11(dirname6(fileURLToPath(import.meta.url)), "shell");
1919
- if (existsSync5(join11(bundled, "index.html"))) return bundled;
2448
+ const bundled = join13(dirname8(fileURLToPath(import.meta.url)), "shell");
2449
+ if (existsSync5(join13(bundled, "index.html"))) return bundled;
1920
2450
  try {
1921
2451
  const require2 = createRequire(import.meta.url);
1922
- return dirname6(require2.resolve("@leglas/shell/dist/index.html"));
2452
+ return dirname8(require2.resolve("@leglas/shell/dist/index.html"));
1923
2453
  } catch {
1924
2454
  return null;
1925
2455
  }
@@ -1953,7 +2483,7 @@ async function run2(options, deps) {
1953
2483
  const fileMounts = /* @__PURE__ */ new Map();
1954
2484
  for (const preview of merged?.previews ?? []) {
1955
2485
  if (preview.file !== void 0) {
1956
- const absolute = join11(options.cwd, preview.file);
2486
+ const absolute = join13(options.cwd, preview.file);
1957
2487
  if (!existsSync5(absolute)) {
1958
2488
  worktreeErrors.push(
1959
2489
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
@@ -1964,7 +2494,7 @@ async function run2(options, deps) {
1964
2494
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
1965
2495
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
1966
2496
  }
1967
- fileMounts.set(slug, dirname6(absolute));
2497
+ fileMounts.set(slug, dirname8(absolute));
1968
2498
  previews.push({
1969
2499
  ...preview,
1970
2500
  url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename3(absolute))}`
@@ -2025,7 +2555,7 @@ async function run2(options, deps) {
2025
2555
  })
2026
2556
  );
2027
2557
  } else {
2028
- const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative2(options.cwd, loaded.path) || loaded.path;
2558
+ const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative3(options.cwd, loaded.path) || loaded.path;
2029
2559
  deps.log(`Leglas ${url}`);
2030
2560
  deps.log(
2031
2561
  `app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
@@ -2064,15 +2594,20 @@ async function run2(options, deps) {
2064
2594
  export {
2065
2595
  AGENTS_MARKER_END,
2066
2596
  AGENTS_MARKER_START,
2067
- ALL_BRIEFS,
2597
+ PROMPT_TOKEN,
2598
+ WATCH_PATH,
2068
2599
  baselineFrom,
2069
- briefsFor,
2600
+ commandFor,
2070
2601
  detectFramework,
2602
+ nextRequest,
2071
2603
  parseArgs,
2072
- planBriefs,
2604
+ parseTemplate,
2605
+ planExplore,
2073
2606
  planInit,
2074
2607
  planKeep,
2075
2608
  planNew,
2609
+ planShow,
2610
+ readRequests,
2076
2611
  run2 as run,
2077
2612
  runAdd,
2078
2613
  runClassify,
@@ -2082,5 +2617,7 @@ export {
2082
2617
  runList,
2083
2618
  runNew,
2084
2619
  runRequests,
2620
+ runShow,
2621
+ runWatch,
2085
2622
  surfaceSlug
2086
2623
  };