@algolia/wizard 0.8.0-rc.58.45 → 0.8.0-rc.59.47

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/main.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/ui/App.tsx
7
- import { Box as Box14, Text as Text14, useApp, useInput as useInput6, useWindowSize as useWindowSize7 } from "ink";
7
+ import { Box as Box13, Text as Text13, useApp, useInput as useInput6, useWindowSize as useWindowSize7 } from "ink";
8
8
 
9
9
  // src/core/store.ts
10
10
  import { create } from "zustand";
@@ -497,9 +497,9 @@ function Notices() {
497
497
  }
498
498
 
499
499
  // src/ui/PromptInput.tsx
500
- import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
500
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
501
501
  import TextInput from "ink-text-input";
502
- import { useState as useState5 } from "react";
502
+ import { useState as useState4 } from "react";
503
503
 
504
504
  // src/ui/NextAction.tsx
505
505
  import { Box as Box3, Text as Text3 } from "ink";
@@ -525,14 +525,13 @@ function NextAction({
525
525
  }
526
526
 
527
527
  // src/ui/SelectPrompt.tsx
528
- import { Box as Box5, Text as Text5, useInput, useWindowSize as useWindowSize4 } from "ink";
529
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
530
-
531
- // src/ui/ScrollView.tsx
532
- import { Box as Box4, Text as Text4, measureElement as measureElement2, useWindowSize as useWindowSize3 } from "ink";
533
- import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
534
- import { jsxs as jsxs3 } from "react/jsx-runtime";
535
- var INDICATOR_ROWS = 2;
528
+ import { Box as Box4, Text as Text4, measureElement as measureElement2, useInput, useWindowSize as useWindowSize3 } from "ink";
529
+ import { useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
530
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
531
+ var CANCEL = "cancel";
532
+ var ARROW_WIDTH = 4;
533
+ var COLUMN_GAP = 2;
534
+ var BAR_PADDING = 2;
536
535
  function fittedWidth(node, columns) {
537
536
  let left = 0;
538
537
  for (let n = node; n; n = n.parentNode) {
@@ -540,89 +539,6 @@ function fittedWidth(node, columns) {
540
539
  }
541
540
  return Math.max(Math.min(measureElement2(node).width, columns - left), 0);
542
541
  }
543
- function useScrollWindow({
544
- itemCount,
545
- rowHeight = 1,
546
- followBottom = false
547
- }) {
548
- const viewportRef = useRef2(null);
549
- const { columns } = useWindowSize3();
550
- const [size, setSize] = useState3(
551
- null
552
- );
553
- useLayoutEffect(() => {
554
- if (!viewportRef.current) return;
555
- const width = fittedWidth(viewportRef.current, columns);
556
- const { height } = measureElement2(viewportRef.current);
557
- setSize(
558
- (prev) => prev?.width === width && prev.height === height ? prev : { width, height }
559
- );
560
- });
561
- const capacity = size === null || itemCount * rowHeight <= size.height ? itemCount : Math.max(Math.floor((size.height - INDICATOR_ROWS) / rowHeight), 1);
562
- const maxOffset = Math.max(itemCount - capacity, 0);
563
- const [offset, setOffset] = useState3(0);
564
- const prevMaxOffsetRef = useRef2(0);
565
- useLayoutEffect(() => {
566
- const wasAtBottom = offset >= prevMaxOffsetRef.current;
567
- prevMaxOffsetRef.current = maxOffset;
568
- setOffset(
569
- (o) => followBottom && wasAtBottom ? maxOffset : Math.min(o, maxOffset)
570
- );
571
- }, [maxOffset, followBottom]);
572
- const scrollBy = useCallback(
573
- (delta) => {
574
- setOffset((o) => Math.min(Math.max(o + delta, 0), maxOffset));
575
- },
576
- [maxOffset]
577
- );
578
- const revealIndex = useCallback(
579
- (index) => {
580
- setOffset((o) => {
581
- if (index < o) return index;
582
- if (index >= o + capacity) {
583
- return Math.min(index - capacity + 1, maxOffset);
584
- }
585
- return o;
586
- });
587
- },
588
- [capacity, maxOffset]
589
- );
590
- const visibleCount = Math.min(capacity, Math.max(itemCount - offset, 0));
591
- return {
592
- viewportRef,
593
- width: size?.width ?? columns,
594
- offset,
595
- capacity,
596
- maxOffset,
597
- hiddenAbove: Math.min(offset, itemCount),
598
- hiddenBelow: Math.max(itemCount - offset - visibleCount, 0),
599
- scrollBy,
600
- revealIndex
601
- };
602
- }
603
- function ScrollView({ scroll, children }) {
604
- return /* @__PURE__ */ jsxs3(Box4, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
605
- scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
606
- "\u2191 ",
607
- scroll.hiddenAbove,
608
- " more"
609
- ] }),
610
- children,
611
- scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
612
- "\u2193 ",
613
- scroll.hiddenBelow,
614
- " more"
615
- ] })
616
- ] });
617
- }
618
-
619
- // src/ui/SelectPrompt.tsx
620
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
621
- var CANCEL = "cancel";
622
- var ARROW_WIDTH = 4;
623
- var COLUMN_GAP = 2;
624
- var BAR_PADDING = 2;
625
- var ROW_HEIGHT = 3;
626
542
  function SelectPrompt({
627
543
  options,
628
544
  onSelect,
@@ -636,10 +552,10 @@ function SelectPrompt({
636
552
  secondary,
637
553
  defaultSelectedIndex = 0
638
554
  }) {
639
- const [index, setIndex] = useState4(
555
+ const [index, setIndex] = useState3(
640
556
  () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
641
557
  );
642
- const [checked, setChecked] = useState4(() => /* @__PURE__ */ new Set());
558
+ const [checked, setChecked] = useState3(() => /* @__PURE__ */ new Set());
643
559
  const hasCancel = Boolean(multi || cancelable);
644
560
  const rows = hasCancel ? [...options, "Cancel"] : options;
645
561
  const cancelIndex = hasCancel ? options.length : -1;
@@ -647,14 +563,14 @@ function SelectPrompt({
647
563
  if (rows.length > 1) hints.push({ key: "[\u2191] [\u2193]", label: "move" });
648
564
  if (multi) hints.push({ key: "[space]", label: "select" });
649
565
  hints.push({ key: "[enter]", label: "confirm" });
650
- const containerRef = useRef3(null);
651
- const { columns } = useWindowSize4();
652
- const [width, setWidth] = useState4(columns);
653
- useLayoutEffect2(() => {
654
- if (!containerRef.current) return;
655
- const measured = fittedWidth(containerRef.current, columns);
656
- setWidth((prev) => prev === measured ? prev : measured);
657
- });
566
+ const containerRef = useRef2(null);
567
+ const { columns } = useWindowSize3();
568
+ const [width, setWidth] = useState3(columns);
569
+ useLayoutEffect(() => {
570
+ if (containerRef.current) {
571
+ setWidth(fittedWidth(containerRef.current, columns));
572
+ }
573
+ }, [columns]);
658
574
  const inner = Math.max(width - BAR_PADDING, 0);
659
575
  const labelWidth = Math.min(
660
576
  ARROW_WIDTH + (multi ? 2 : 0) + Math.max(0, ...rows.map((opt) => opt.length)) + COLUMN_GAP,
@@ -670,15 +586,6 @@ function SelectPrompt({
670
586
  const barWidth = Math.min(labelWidth + badgeWidth + BAR_PADDING, width);
671
587
  const barLabelWidth = Math.max(barWidth - BAR_PADDING - badgeWidth, 0);
672
588
  const textWidth = inner - labelWidth;
673
- const scroll = useScrollWindow({
674
- itemCount: rows.length,
675
- rowHeight: ROW_HEIGHT
676
- });
677
- const { revealIndex } = scroll;
678
- useLayoutEffect2(() => {
679
- revealIndex(index);
680
- }, [index, revealIndex]);
681
- const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
682
589
  useInput((input, key) => {
683
590
  if (rows.length === 0) return;
684
591
  if (key.upArrow || input === "k") {
@@ -702,65 +609,62 @@ function SelectPrompt({
702
609
  }
703
610
  }
704
611
  });
705
- return /* @__PURE__ */ jsx4(Box5, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, width, children: [
706
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
707
- error && /* @__PURE__ */ jsx4(Text5, { color: COLORS.danger, children: error }),
708
- messages?.map((m, i) => /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
709
- table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
710
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
711
- question && /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: question }),
712
- helpText && /* @__PURE__ */ jsx4(Text5, { color: COLORS.dim, children: helpText })
713
- ] })
612
+ return /* @__PURE__ */ jsx4(Box4, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 1, width, children: [
613
+ error && /* @__PURE__ */ jsx4(Text4, { color: COLORS.danger, children: error }),
614
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: m }, `msg-${i}`)),
615
+ table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
616
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
617
+ question && /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: question }),
618
+ helpText && /* @__PURE__ */ jsx4(Text4, { color: COLORS.dim, children: helpText })
714
619
  ] }),
715
- /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
716
- const i = scroll.offset + visibleIndex;
620
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: rows.map((option, i) => {
717
621
  const highlighted = i === index;
718
622
  const isCancel = i === cancelIndex;
719
623
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
720
624
  const sec = isCancel ? void 0 : secondary?.[i];
721
625
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
722
- const label = /* @__PURE__ */ jsxs4(Text5, { color: labelColor, wrap: "truncate", children: [
626
+ const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, wrap: "truncate", children: [
723
627
  highlighted ? "\u276F " : " ",
724
628
  bullet,
725
629
  option
726
630
  ] });
727
631
  const isText = sec?.kind === "text";
728
- return /* @__PURE__ */ jsxs4(
729
- Box5,
632
+ return /* @__PURE__ */ jsxs3(
633
+ Box4,
730
634
  {
731
635
  width: isText ? "100%" : barWidth,
732
636
  paddingX: 1,
733
637
  paddingY: 1,
734
638
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
735
639
  children: [
736
- /* @__PURE__ */ jsx4(Box5, { width: isText ? labelWidth : barLabelWidth, children: label }),
737
- isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box5, { width: textWidth, children: /* @__PURE__ */ jsx4(
738
- Text5,
640
+ /* @__PURE__ */ jsx4(Box4, { width: isText ? labelWidth : barLabelWidth, children: label }),
641
+ isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box4, { width: textWidth, children: /* @__PURE__ */ jsx4(
642
+ Text4,
739
643
  {
740
644
  wrap: "truncate",
741
645
  color: highlighted ? COLORS.primary : COLORS.muted,
742
646
  children: sec.value
743
647
  }
744
648
  ) }),
745
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box5, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text5, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
649
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box4, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
746
650
  ]
747
651
  },
748
652
  `row-${i}`
749
653
  );
750
654
  }) }),
751
- /* @__PURE__ */ jsx4(Box5, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text5, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs4(Text5, { children: [
655
+ /* @__PURE__ */ jsx4(Text4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
752
656
  i > 0 ? " " : "",
753
- /* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, children: key }),
754
- /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
657
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: key }),
658
+ /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
755
659
  " ",
756
660
  label
757
661
  ] })
758
- ] }, label)) }) })
662
+ ] }, label)) })
759
663
  ] }) });
760
664
  }
761
665
 
762
666
  // src/ui/PromptInput.tsx
763
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
667
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
764
668
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
765
669
  function EnterToContinuePrompt({
766
670
  question,
@@ -771,10 +675,10 @@ function EnterToContinuePrompt({
771
675
  if (key.return) onDecide(true);
772
676
  else if (key.escape) onDecide(false);
773
677
  });
774
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, children: [
775
- messages?.map((m, i) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
776
- question && /* @__PURE__ */ jsx5(Text6, { color: COLORS.primary, children: question }),
777
- /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
678
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
679
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
680
+ question && /* @__PURE__ */ jsx5(Text5, { color: COLORS.primary, children: question }),
681
+ /* @__PURE__ */ jsxs4(Box5, { gap: 1, flexDirection: "column", children: [
778
682
  /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
779
683
  /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
780
684
  ] })
@@ -782,13 +686,13 @@ function EnterToContinuePrompt({
782
686
  }
783
687
  function PromptInput() {
784
688
  const { phase, inputReq, submitInput } = useWizard();
785
- const [draft, setDraft] = useState5("");
689
+ const [draft, setDraft] = useState4("");
786
690
  if (phase === "done" || phase === "error") {
787
- return /* @__PURE__ */ jsx5(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text6, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
691
+ return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
788
692
  }
789
693
  if (phase !== "awaitingInput" || !inputReq) return null;
790
694
  if (inputReq.promptType === "multipleChoice") {
791
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
695
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
792
696
  SelectPrompt,
793
697
  {
794
698
  question: inputReq.prompt,
@@ -805,7 +709,7 @@ function PromptInput() {
805
709
  ) });
806
710
  }
807
711
  if (inputReq.promptType === "multiSelect") {
808
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
712
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
809
713
  SelectPrompt,
810
714
  {
811
715
  multi: true,
@@ -820,7 +724,7 @@ function PromptInput() {
820
724
  ) });
821
725
  }
822
726
  if (inputReq.promptType === "notice") {
823
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
727
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
824
728
  SelectPrompt,
825
729
  {
826
730
  question: inputReq.prompt,
@@ -842,7 +746,7 @@ function PromptInput() {
842
746
  }
843
747
  if (inputReq.promptType === "acceptReject") {
844
748
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
845
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
749
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
846
750
  SelectPrompt,
847
751
  {
848
752
  question: inputReq.prompt,
@@ -853,11 +757,11 @@ function PromptInput() {
853
757
  }
854
758
  ) });
855
759
  }
856
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
857
- inputReq.error && /* @__PURE__ */ jsx5(Text6, { color: COLORS.danger, children: inputReq.error }),
858
- inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
859
- /* @__PURE__ */ jsxs5(Box6, { children: [
860
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.primary, children: [
760
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
761
+ inputReq.error && /* @__PURE__ */ jsx5(Text5, { color: COLORS.danger, children: inputReq.error }),
762
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
763
+ /* @__PURE__ */ jsxs4(Box5, { children: [
764
+ /* @__PURE__ */ jsxs4(Text5, { color: COLORS.primary, children: [
861
765
  inputReq.prompt,
862
766
  " "
863
767
  ] }),
@@ -879,7 +783,7 @@ function PromptInput() {
879
783
  // src/ui/Welcome.tsx
880
784
  import { dirname as dirname2, join as join3 } from "node:path";
881
785
  import { fileURLToPath } from "node:url";
882
- import { Box as Box7, Spacer, Text as Text7, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
786
+ import { Box as Box6, Spacer, Text as Text6, useInput as useInput3, useWindowSize as useWindowSize4 } from "ink";
883
787
 
884
788
  // src/ui/copy/welcome.ts
885
789
  var sidebarItems = [
@@ -892,12 +796,12 @@ var sidebarItems = [
892
796
  description: "push 100 records to Algolia in seconds"
893
797
  },
894
798
  {
895
- title: "detect your framework",
896
- description: "React, Vue, Angular, Vanilla JS"
799
+ title: "detect your stack",
800
+ description: "React, Vue, Angular, Rails, Django, Laravel & more"
897
801
  },
898
802
  {
899
803
  title: "scaffold a search UI",
900
- description: "a styled InstantSearch component, wired into your app"
804
+ description: "a styled InstantSearch UI, wired into your app or templates"
901
805
  },
902
806
  {
903
807
  title: "ship it",
@@ -907,27 +811,27 @@ var sidebarItems = [
907
811
 
908
812
  // src/ui/Welcome.tsx
909
813
  import Image, { InkPictureProvider } from "ink-picture";
910
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
814
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
911
815
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
912
816
  function SidebarItem({
913
817
  title,
914
818
  description
915
819
  }) {
916
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
917
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, children: [
918
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.success, children: "\u2192" }),
919
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.strong, bold: true, children: title })
820
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
821
+ /* @__PURE__ */ jsxs5(Box6, { gap: 1, children: [
822
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.success, children: "\u2192" }),
823
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.strong, bold: true, children: title })
920
824
  ] }),
921
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 2, children: [
825
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", gap: 2, children: [
922
826
  /* @__PURE__ */ jsx6(Spacer, {}),
923
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: description })
827
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: description })
924
828
  ] })
925
829
  ] });
926
830
  }
927
831
  function Welcome() {
928
832
  const confirmStart = useWizard((s) => s.confirmStart);
929
833
  const openLearnMore = useWizard((s) => s.openLearnMore);
930
- const { rows } = useWindowSize5();
834
+ const { rows } = useWindowSize4();
931
835
  useInput3((input, key) => {
932
836
  if (key.return) confirmStart();
933
837
  else if (input === "i") openLearnMore();
@@ -946,15 +850,15 @@ function Welcome() {
946
850
  if (rows < 30) {
947
851
  layout = scales["small"];
948
852
  }
949
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
853
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
950
854
  /* @__PURE__ */ jsx6(
951
- Box7,
855
+ Box6,
952
856
  {
953
857
  paddingY: layout.main.padding.y,
954
858
  paddingX: layout.main.padding.x,
955
859
  flexDirection: "column",
956
860
  justifyContent: "center",
957
- children: /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 2, children: [
861
+ children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 2, children: [
958
862
  /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
959
863
  Image,
960
864
  {
@@ -966,16 +870,16 @@ function Welcome() {
966
870
  protocol: "halfBlock"
967
871
  }
968
872
  ) }),
969
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
970
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
873
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
874
+ /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
971
875
  /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
972
876
  /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
973
877
  ] })
974
878
  ] })
975
879
  }
976
880
  ),
977
- /* @__PURE__ */ jsxs6(
978
- Box7,
881
+ /* @__PURE__ */ jsxs5(
882
+ Box6,
979
883
  {
980
884
  backgroundColor: COLORS.bg.sidebar,
981
885
  width: 40,
@@ -985,7 +889,7 @@ function Welcome() {
985
889
  flexDirection: "column",
986
890
  justifyContent: "center",
987
891
  children: [
988
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
892
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
989
893
  sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
990
894
  ]
991
895
  }
@@ -995,7 +899,7 @@ function Welcome() {
995
899
 
996
900
  // src/ui/LearnMore.tsx
997
901
  import { Fragment as Fragment2 } from "react";
998
- import { Box as Box8, Text as Text8, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
902
+ import { Box as Box7, Text as Text7, useInput as useInput4, useWindowSize as useWindowSize5 } from "ink";
999
903
 
1000
904
  // src/ui/copy/learn-more.ts
1001
905
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -1003,7 +907,7 @@ var accessItems = [
1003
907
  {
1004
908
  tag: "READ",
1005
909
  title: "Project files",
1006
- description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
910
+ description: "reads your dependency manifests (package.json\u2026), configs & source to detect your stack. Read-only; nothing is uploaded."
1007
911
  },
1008
912
  {
1009
913
  tag: "WRITE",
@@ -1032,7 +936,7 @@ var policyLinks = [
1032
936
  ];
1033
937
 
1034
938
  // src/ui/LearnMore.tsx
1035
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
939
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1036
940
  var TAG_COLORS = {
1037
941
  READ: COLORS.success,
1038
942
  WRITE: COLORS.badge,
@@ -1048,25 +952,25 @@ function NeverLine({
1048
952
  }) {
1049
953
  const used = segments.reduce((n, s) => n + s.text.length, 0);
1050
954
  const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
1051
- return /* @__PURE__ */ jsxs7(Text8, { children: [
1052
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: "\u2502" }),
955
+ return /* @__PURE__ */ jsxs6(Text7, { children: [
956
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" }),
1053
957
  " ".repeat(NEVER_BOX_PAD_X),
1054
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text8, { color: s.color, bold: s.bold, children: s.text }, i)),
958
+ segments.map((s, i) => /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: s.bold, children: s.text }, i)),
1055
959
  " ".repeat(rightPad),
1056
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: "\u2502" })
960
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" })
1057
961
  ] });
1058
962
  }
1059
963
  function LearnMore() {
1060
964
  const confirmStart = useWizard((s) => s.confirmStart);
1061
965
  const backToHome = useWizard((s) => s.backToHome);
1062
- const { columns } = useWindowSize6();
966
+ const { columns } = useWindowSize5();
1063
967
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1064
968
  useInput4((_input, key) => {
1065
969
  if (key.escape) backToHome();
1066
970
  else if (key.return) confirmStart();
1067
971
  });
1068
- return /* @__PURE__ */ jsxs7(
1069
- Box8,
972
+ return /* @__PURE__ */ jsxs6(
973
+ Box7,
1070
974
  {
1071
975
  flexDirection: "column",
1072
976
  paddingX: PADDING_X,
@@ -1074,20 +978,20 @@ function LearnMore() {
1074
978
  width: "100%",
1075
979
  gap: 1,
1076
980
  children: [
1077
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1078
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: accessIntro }),
1079
- /* @__PURE__ */ jsx7(Box8, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", marginTop: 1, children: [
1080
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1081
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1082
- /* @__PURE__ */ jsx7(Box8, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text8, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1083
- /* @__PURE__ */ jsx7(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text8, { children: [
1084
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: item.title }),
1085
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
981
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
982
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: accessIntro }),
983
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
984
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
985
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, marginTop: 1, children: [
986
+ /* @__PURE__ */ jsx7(Box7, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text7, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
987
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs6(Text7, { children: [
988
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: item.title }),
989
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1086
990
  ] }) })
1087
991
  ] })
1088
992
  ] }, item.tag)) }),
1089
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
1090
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
993
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
994
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1091
995
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1092
996
  /* @__PURE__ */ jsx7(
1093
997
  NeverLine,
@@ -1096,7 +1000,7 @@ function LearnMore() {
1096
1000
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1097
1001
  }
1098
1002
  ),
1099
- neverItems.map((item) => /* @__PURE__ */ jsxs7(Fragment2, { children: [
1003
+ neverItems.map((item) => /* @__PURE__ */ jsxs6(Fragment2, { children: [
1100
1004
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1101
1005
  /* @__PURE__ */ jsx7(
1102
1006
  NeverLine,
@@ -1111,23 +1015,23 @@ function LearnMore() {
1111
1015
  )
1112
1016
  ] }, item)),
1113
1017
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1114
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1018
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1115
1019
  ] }),
1116
- /* @__PURE__ */ jsx7(Box8, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1117
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1118
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.accent, children: link.url })
1020
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
1021
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1022
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.accent, children: link.url })
1119
1023
  ] }, link.label)) }),
1120
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1121
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1122
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "[" }),
1123
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.primary, children: "esc" }),
1124
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "] back" })
1024
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1025
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
1026
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
1027
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "esc" }),
1028
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "] back" })
1125
1029
  ] }),
1126
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1127
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "[" }),
1128
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.primary, children: "enter" }),
1129
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "]" }),
1130
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.success, bold: true, children: "start wizard" })
1030
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
1031
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
1032
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "enter" }),
1033
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "]" }),
1034
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.success, bold: true, children: "start wizard" })
1131
1035
  ] })
1132
1036
  ] })
1133
1037
  ]
@@ -1136,10 +1040,10 @@ function LearnMore() {
1136
1040
  }
1137
1041
 
1138
1042
  // src/ui/Sidebar.tsx
1139
- import { Box as Box11, Text as Text11 } from "ink";
1043
+ import { Box as Box10, Text as Text10 } from "ink";
1140
1044
 
1141
1045
  // src/ui/Steps.tsx
1142
- import { Box as Box9, Text as Text9 } from "ink";
1046
+ import { Box as Box8, Text as Text8 } from "ink";
1143
1047
  import Spinner from "ink-spinner";
1144
1048
 
1145
1049
  // src/core/persistence.ts
@@ -1168,11 +1072,11 @@ async function clearWorkflowState(workflowId) {
1168
1072
  }
1169
1073
 
1170
1074
  // src/ui/Steps.tsx
1171
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1075
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1172
1076
  function Steps() {
1173
1077
  const { steps } = useWizard();
1174
1078
  const visibleSteps = steps.filter(isStepVisible);
1175
- return /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { color: COLORS.status[s.status], children: [
1079
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status[s.status], children: [
1176
1080
  s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1177
1081
  " ",
1178
1082
  s.title
@@ -1182,7 +1086,7 @@ function CurrentStep() {
1182
1086
  const { steps } = useWizard();
1183
1087
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1184
1088
  if (!currentStep) return null;
1185
- return /* @__PURE__ */ jsxs8(Text9, { color: COLORS.status.running, children: [
1089
+ return /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status.running, children: [
1186
1090
  /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1187
1091
  " ",
1188
1092
  ` ${currentStep.title}`
@@ -1190,19 +1094,19 @@ function CurrentStep() {
1190
1094
  }
1191
1095
 
1192
1096
  // src/ui/Progress.tsx
1193
- import { Box as Box10, Text as Text10 } from "ink";
1194
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1097
+ import { Box as Box9, Text as Text9 } from "ink";
1098
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1195
1099
  function Progress() {
1196
1100
  const { steps, currentStepIndex } = useWizard();
1197
1101
  const visibleSteps = steps.filter(isStepVisible);
1198
1102
  if (visibleSteps.length === 0) return null;
1199
1103
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1200
1104
  const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
1201
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1202
- /* @__PURE__ */ jsx9(Text10, { color: COLORS.muted, children: "STEP" }),
1203
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: activeStepNumber }),
1204
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: "/" }),
1205
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: visibleSteps.length })
1105
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1106
+ /* @__PURE__ */ jsx9(Text9, { color: COLORS.muted, children: "STEP" }),
1107
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: activeStepNumber }),
1108
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "/" }),
1109
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: visibleSteps.length })
1206
1110
  ] });
1207
1111
  }
1208
1112
 
@@ -1213,10 +1117,10 @@ var sidebarCommands = [
1213
1117
  ];
1214
1118
 
1215
1119
  // src/ui/Sidebar.tsx
1216
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1120
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1217
1121
  function Sidebar() {
1218
- return /* @__PURE__ */ jsxs10(
1219
- Box11,
1122
+ return /* @__PURE__ */ jsxs9(
1123
+ Box10,
1220
1124
  {
1221
1125
  backgroundColor: "#14171E",
1222
1126
  width: 30,
@@ -1225,16 +1129,16 @@ function Sidebar() {
1225
1129
  flexDirection: "column",
1226
1130
  justifyContent: "space-between",
1227
1131
  children: [
1228
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", gap: 1, children: [
1229
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.muted, children: "PROGRESS" }),
1132
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1133
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "PROGRESS" }),
1230
1134
  /* @__PURE__ */ jsx10(Steps, {})
1231
1135
  ] }),
1232
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", gap: 1, children: [
1136
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1233
1137
  /* @__PURE__ */ jsx10(Progress, {}),
1234
- /* @__PURE__ */ jsx10(Box11, { flexDirection: "column", children: sidebarCommands.map((c) => {
1235
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1236
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1237
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.muted, children: c.description })
1138
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: sidebarCommands.map((c) => {
1139
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1140
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1141
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: c.description })
1238
1142
  ] });
1239
1143
  }) })
1240
1144
  ] })
@@ -1244,12 +1148,12 @@ function Sidebar() {
1244
1148
  }
1245
1149
 
1246
1150
  // src/ui/Ribbon.tsx
1247
- import { Box as Box12, Text as Text12 } from "ink";
1248
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1151
+ import { Box as Box11, Text as Text11 } from "ink";
1152
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1249
1153
  function Ribbon() {
1250
1154
  const firstCommand = sidebarCommands[0];
1251
- return /* @__PURE__ */ jsxs11(
1252
- Box12,
1155
+ return /* @__PURE__ */ jsxs10(
1156
+ Box11,
1253
1157
  {
1254
1158
  backgroundColor: "#14171E",
1255
1159
  flexDirection: "row",
@@ -1259,9 +1163,9 @@ function Ribbon() {
1259
1163
  children: [
1260
1164
  /* @__PURE__ */ jsx11(Progress, {}),
1261
1165
  /* @__PURE__ */ jsx11(CurrentStep, {}),
1262
- /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1263
- /* @__PURE__ */ jsx11(Text12, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1264
- /* @__PURE__ */ jsx11(Text12, { color: COLORS.muted, children: firstCommand.description })
1166
+ /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1167
+ /* @__PURE__ */ jsx11(Text11, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1168
+ /* @__PURE__ */ jsx11(Text11, { color: COLORS.muted, children: firstCommand.description })
1265
1169
  ] })
1266
1170
  ]
1267
1171
  }
@@ -1272,8 +1176,9 @@ function Ribbon() {
1272
1176
  import { useState as useState6 } from "react";
1273
1177
 
1274
1178
  // src/ui/Logs.tsx
1275
- import { Box as Box13, Text as Text13, useInput as useInput5 } from "ink";
1276
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1179
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
1180
+ import { Box as Box12, Text as Text12, measureElement as measureElement3, useInput as useInput5, useWindowSize as useWindowSize6 } from "ink";
1181
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1277
1182
  var KIND_COLOR = {
1278
1183
  tool: COLORS.primary,
1279
1184
  prompt: COLORS.badge
@@ -1303,32 +1208,75 @@ function formatTimestamp(ms) {
1303
1208
  }
1304
1209
  function Logs() {
1305
1210
  const logs = useWizard((s) => s.logs);
1306
- const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1211
+ const { rows, columns } = useWindowSize6();
1212
+ const viewportRef = useRef3(null);
1213
+ const [viewportHeight, setViewportHeight] = useState5(0);
1214
+ const [viewportWidth, setViewportWidth] = useState5(0);
1215
+ const [scrollOffset, setScrollOffset] = useState5(0);
1216
+ const prevMaxOffsetRef = useRef3(0);
1217
+ useLayoutEffect2(() => {
1218
+ if (!viewportRef.current) return;
1219
+ const { width, height } = measureElement3(viewportRef.current);
1220
+ setViewportHeight(height);
1221
+ setViewportWidth(width);
1222
+ }, [rows, columns, logs.length === 0]);
1223
+ let capacity = viewportHeight;
1224
+ for (let i = 0; i < 2; i++) {
1225
+ const hasAbove = scrollOffset > 0;
1226
+ const hasBelow = scrollOffset + capacity < logs.length;
1227
+ capacity = Math.max(
1228
+ viewportHeight - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0),
1229
+ 0
1230
+ );
1231
+ }
1232
+ const capacityAtBottom = logs.length > viewportHeight ? Math.max(viewportHeight - 1, 0) : viewportHeight;
1233
+ const maxOffset = Math.max(logs.length - capacityAtBottom, 0);
1234
+ useLayoutEffect2(() => {
1235
+ const wasAtBottom = scrollOffset >= prevMaxOffsetRef.current;
1236
+ prevMaxOffsetRef.current = maxOffset;
1237
+ setScrollOffset((o) => wasAtBottom ? maxOffset : Math.min(o, maxOffset));
1238
+ }, [maxOffset]);
1307
1239
  useInput5((_input, key) => {
1308
- if (key.upArrow) scroll.scrollBy(-1);
1309
- else if (key.downArrow) scroll.scrollBy(1);
1240
+ if (!key.upArrow && !key.downArrow) return;
1241
+ setScrollOffset(
1242
+ (o) => key.upArrow ? Math.max(o - 1, 0) : Math.min(o + 1, maxOffset)
1243
+ );
1310
1244
  });
1311
- const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1312
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1313
- logs.length === 0 && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: "No logs yet." }),
1314
- /* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
1315
- const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1316
- const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1317
- const rawPreview = rawInputText(entry.input);
1318
- const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1319
- const gaps = (partCount - 1) * ROW_GAP;
1320
- let budget = scroll.width - timestamp.length - durationText.length - gaps;
1321
- const name = truncate2(entry.name, budget);
1322
- budget -= name.length;
1323
- const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1324
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: ROW_GAP, children: [
1325
- /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: timestamp }),
1326
- /* @__PURE__ */ jsx12(Text13, { color: logNameColor(entry), wrap: "truncate", children: name }),
1327
- preview && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, wrap: "truncate", children: preview }),
1328
- durationText && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: durationText })
1329
- ] }, entry.id);
1330
- }) }),
1331
- /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1245
+ const visible = logs.slice(scrollOffset, scrollOffset + capacity);
1246
+ const hiddenAbove = scrollOffset;
1247
+ const hiddenBelow = logs.length - scrollOffset - visible.length;
1248
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1249
+ logs.length === 0 && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "No logs yet." }),
1250
+ /* @__PURE__ */ jsxs11(Box12, { ref: viewportRef, flexDirection: "column", flexGrow: 1, children: [
1251
+ hiddenAbove > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
1252
+ "\u2191 ",
1253
+ hiddenAbove,
1254
+ " more"
1255
+ ] }),
1256
+ visible.map((entry) => {
1257
+ const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1258
+ const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1259
+ const rawPreview = rawInputText(entry.input);
1260
+ const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1261
+ const gaps = (partCount - 1) * ROW_GAP;
1262
+ let budget = viewportWidth - timestamp.length - durationText.length - gaps;
1263
+ const name = truncate2(entry.name, budget);
1264
+ budget -= name.length;
1265
+ const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1266
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: ROW_GAP, children: [
1267
+ /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: timestamp }),
1268
+ /* @__PURE__ */ jsx12(Text12, { color: logNameColor(entry), wrap: "truncate", children: name }),
1269
+ preview && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, wrap: "truncate", children: preview }),
1270
+ durationText && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: durationText })
1271
+ ] }, entry.id);
1272
+ }),
1273
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
1274
+ "\u2193 ",
1275
+ hiddenBelow,
1276
+ " more"
1277
+ ] })
1278
+ ] }),
1279
+ /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1332
1280
  ] });
1333
1281
  }
1334
1282
 
@@ -1520,7 +1468,7 @@ function track(event, payload) {
1520
1468
  }
1521
1469
 
1522
1470
  // src/ui/App.tsx
1523
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1471
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1524
1472
  function App() {
1525
1473
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1526
1474
  const { exit } = useApp();
@@ -1563,44 +1511,36 @@ function App() {
1563
1511
  const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1564
1512
  const flexDirection = columns > 90 ? "row" : "column";
1565
1513
  const showSidebar = flexDirection === "row";
1566
- return /* @__PURE__ */ jsxs13(
1567
- Box14,
1514
+ return /* @__PURE__ */ jsxs12(
1515
+ Box13,
1568
1516
  {
1569
1517
  backgroundColor: COLORS.bg.main,
1570
1518
  flexDirection: "row",
1571
1519
  width: columns,
1572
1520
  minHeight: rows,
1573
1521
  children: [
1574
- mainWindowVisible && // Ink sizes the root by width only, so without a cap the scrolling
1575
- // lists in here grow to their content instead of windowing (see
1576
- // `useScrollWindow`). The home screens below stay uncapped: they are
1577
- // long static copy that would be clipped rather than windowed.
1578
- /* @__PURE__ */ jsxs13(
1579
- Box14,
1522
+ mainWindowVisible && /* @__PURE__ */ jsxs12(
1523
+ Box13,
1580
1524
  {
1581
1525
  flexDirection,
1582
1526
  width: "100%",
1583
- maxHeight: rows,
1584
1527
  justifyContent: "space-between",
1585
1528
  children: [
1586
1529
  showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : (
1587
- /* Fill the space the sidebar/ribbon leaves width beside the
1588
- sidebar, height above the ribbon. The height matters even
1589
- stacked: it is what the prompt's scrolling list measures itself
1590
- against (see SelectPrompt). */
1591
- /* @__PURE__ */ jsxs13(
1592
- Box14,
1530
+ /* Fill the width beside the sidebar; row layout only (would grow vertically when stacked). */
1531
+ /* @__PURE__ */ jsxs12(
1532
+ Box13,
1593
1533
  {
1594
1534
  flexDirection: "column",
1595
1535
  paddingX: 4,
1596
1536
  paddingY: 2,
1597
1537
  width: showSidebar ? 70 : "100%",
1598
- flexGrow: 1,
1538
+ flexGrow: showSidebar ? 1 : 0,
1599
1539
  children: [
1600
1540
  /* @__PURE__ */ jsx13(Notices, {}),
1601
1541
  /* @__PURE__ */ jsx13(PromptInput, {}),
1602
- phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1603
- phase === "error" && error && /* @__PURE__ */ jsx13(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text14, { color: COLORS.status.error, children: [
1542
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1543
+ phase === "error" && error && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text13, { color: COLORS.status.error, children: [
1604
1544
  "\u2716 ",
1605
1545
  error
1606
1546
  ] }) })
@@ -2221,15 +2161,282 @@ function writeCredentialsTool(ctx) {
2221
2161
  // src/lib/tools/searchFiles.ts
2222
2162
  import { tool as tool7 } from "ai";
2223
2163
  import z10 from "zod";
2224
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2164
+ import { readdir as readdir3, readFile as readFile8 } from "node:fs/promises";
2165
+ import { join as join10 } from "node:path";
2166
+
2167
+ // src/lib/languages.ts
2168
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
2169
+ import { existsSync as existsSync2 } from "node:fs";
2170
+ import { join as join9 } from "node:path";
2171
+
2172
+ // src/lib/tools/utils/packageManager.ts
2173
+ import { readFile as readFile6 } from "node:fs/promises";
2174
+ import { existsSync } from "node:fs";
2225
2175
  import { join as join8 } from "node:path";
2176
+ var LOCKFILES = [
2177
+ ["pnpm-lock.yaml", "pnpm"],
2178
+ ["yarn.lock", "yarn"],
2179
+ ["bun.lockb", "bun"],
2180
+ ["bun.lock", "bun"],
2181
+ ["package-lock.json", "npm"]
2182
+ ];
2183
+ async function readPackageJson(cwd = process.cwd()) {
2184
+ return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2185
+ }
2186
+ function packageManagerFrom(pkg) {
2187
+ return pkg.packageManager?.split("@")[0] ?? "npm";
2188
+ }
2189
+ function packageManagerFromLockfile(cwd) {
2190
+ return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2191
+ }
2192
+ async function detectPackageManager(cwd) {
2193
+ try {
2194
+ const pkg = await readPackageJson(cwd);
2195
+ if (pkg.packageManager) return packageManagerFrom(pkg);
2196
+ } catch {
2197
+ }
2198
+ return packageManagerFromLockfile(cwd) ?? "npm";
2199
+ }
2200
+
2201
+ // src/lib/shell.ts
2202
+ function shellQuote(value) {
2203
+ return "'" + value.replace(/'/g, "'\\''") + "'";
2204
+ }
2205
+
2206
+ // src/lib/languages.ts
2207
+ var ENTRYPOINT_TOKEN = "{entrypoint}";
2208
+ var INGEST_DIR = ".algolia-wizard";
2209
+ var LANGUAGE_PROFILES = {
2210
+ javascript: {
2211
+ id: "javascript",
2212
+ displayName: "JavaScript/TypeScript",
2213
+ aliases: [
2214
+ "javascript",
2215
+ "js",
2216
+ "typescript",
2217
+ "ts",
2218
+ "node",
2219
+ "nodejs",
2220
+ "node.js",
2221
+ "bun",
2222
+ "deno",
2223
+ "ecmascript",
2224
+ "jsx",
2225
+ "tsx"
2226
+ ],
2227
+ manifests: ["package.json"],
2228
+ // The concrete npm-family manager is resolved by detectPackageManager (it
2229
+ // honours the package.json `packageManager` field, which lockfiles can't
2230
+ // express), so one spec covers all four and `resolveToolchain` rewrites the
2231
+ // binary below.
2232
+ packageManagers: [
2233
+ {
2234
+ id: "npm",
2235
+ dependency: { mode: "agent-declares", file: "package.json" },
2236
+ installSteps: [{ argv: ["npm", "install"] }],
2237
+ ingest: {
2238
+ kind: "auto",
2239
+ argv: ["node", ENTRYPOINT_TOKEN],
2240
+ entrypointExtensions: [".mjs", ".cjs", ".js"]
2241
+ }
2242
+ }
2243
+ ],
2244
+ sdk: { packageName: "algoliasearch", versionPin: "^5", docKey: "js" },
2245
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.mjs`,
2246
+ // package.json scripts are repo-defined, so they're resolved at run time by
2247
+ // repoVerification rather than listed here.
2248
+ verification: [],
2249
+ envReadInstruction: "Read them from `process.env`.",
2250
+ skipDirs: ["node_modules", "dist", "build", "coverage", ".next", "out"]
2251
+ }
2252
+ };
2253
+ var DEFAULT_LANGUAGE_ID = "javascript";
2254
+ var JAVASCRIPT = "javascript";
2255
+ var CURATED_LANGUAGES = Object.values(
2256
+ LANGUAGE_PROFILES
2257
+ ).map((profile) => profile.displayName);
2258
+ function isBackendLanguage(profile) {
2259
+ return profile.id !== JAVASCRIPT;
2260
+ }
2261
+ function normalizeLanguageName(name) {
2262
+ return name.toLowerCase().trim().replace(/[\s_-]+/g, "");
2263
+ }
2264
+ var ALIAS_TO_ID = /* @__PURE__ */ new Map();
2265
+ for (const profile of Object.values(LANGUAGE_PROFILES)) {
2266
+ for (const alias of [profile.id, profile.displayName, ...profile.aliases]) {
2267
+ ALIAS_TO_ID.set(normalizeLanguageName(alias), profile.id);
2268
+ }
2269
+ }
2270
+ function resolveLanguageProfile(name) {
2271
+ const id = ALIAS_TO_ID.get(normalizeLanguageName(name));
2272
+ return id ? LANGUAGE_PROFILES[id] : void 0;
2273
+ }
2274
+ function isSameLanguage(a, b) {
2275
+ const x = resolveLanguageProfile(a);
2276
+ const y = resolveLanguageProfile(b);
2277
+ if (x && y) return x.id === y.id;
2278
+ if (x || y) return false;
2279
+ const folded = normalizeLanguageName(a);
2280
+ return folded !== "" && folded === normalizeLanguageName(b);
2281
+ }
2282
+ var BASE_SKIP_DIRS = ["node_modules", ".git", "dist"];
2283
+ var ALL_SKIP_DIRS = /* @__PURE__ */ new Set([
2284
+ ...BASE_SKIP_DIRS,
2285
+ ...Object.values(LANGUAGE_PROFILES).flatMap((p) => p.skipDirs)
2286
+ ]);
2287
+ var ALLOWED_BINARIES = new Set(
2288
+ Object.values(LANGUAGE_PROFILES).flatMap((profile) => [
2289
+ ...profile.packageManagers.flatMap((pm) => [
2290
+ ...pm.installSteps.map((s) => s.argv[0]),
2291
+ ...pm.ingest.kind === "auto" ? [pm.ingest.argv[0]] : []
2292
+ ]),
2293
+ ...profile.verification.map((v) => v.argv[0])
2294
+ ])
2295
+ );
2296
+ var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
2297
+ function isWorktreeRelativeCommand(command) {
2298
+ return command.includes("/");
2299
+ }
2300
+ function withCommand(argv, command) {
2301
+ return [command, ...argv.slice(1)];
2302
+ }
2303
+ function resolveDeclaredManifest(root, packageManager) {
2304
+ const { dependency } = packageManager;
2305
+ if (dependency.mode !== "agent-declares" || !dependency.alternatives?.length) {
2306
+ return packageManager;
2307
+ }
2308
+ const present = [dependency.file, ...dependency.alternatives].find(
2309
+ (file) => existsSync2(join9(root, file))
2310
+ );
2311
+ if (!present || present === dependency.file) return packageManager;
2312
+ return { ...packageManager, dependency: { ...dependency, file: present } };
2313
+ }
2314
+ async function manifestPresent(root, manifest, listing) {
2315
+ if (!manifest.startsWith("*.")) return existsSync2(join9(root, manifest));
2316
+ if (!listing.entries) {
2317
+ const entries = await readdir2(root).catch(() => []);
2318
+ listing.entries = Array.isArray(entries) ? entries : [];
2319
+ }
2320
+ const suffix = manifest.slice(1);
2321
+ return listing.entries.some((e) => e.endsWith(suffix));
2322
+ }
2323
+ async function profileManifestPresent(root, profile, listing) {
2324
+ for (const manifest of profile.manifests) {
2325
+ if (await manifestPresent(root, manifest, listing)) return true;
2326
+ }
2327
+ return false;
2328
+ }
2329
+ async function detectProfilesFromManifests(root) {
2330
+ const listing = {};
2331
+ const found = [];
2332
+ for (const profile of Object.values(LANGUAGE_PROFILES)) {
2333
+ if (await profileManifestPresent(root, profile, listing)) found.push(profile);
2334
+ }
2335
+ return found;
2336
+ }
2337
+ async function hasProfileManifest(root, profile) {
2338
+ return profileManifestPresent(root, profile, {});
2339
+ }
2340
+ async function pickIngestionCandidates(root, confirmedNames) {
2341
+ const confirmed3 = confirmedNames.map((name) => resolveLanguageProfile(name)).filter((p) => p !== void 0);
2342
+ const onDisk = await detectProfilesFromManifests(root);
2343
+ const onDiskIds = new Set(onDisk.map((p) => p.id));
2344
+ const candidates = [
2345
+ ...new Map(
2346
+ confirmed3.filter((p) => onDiskIds.has(p.id)).map((p) => [p.id, p])
2347
+ ).values()
2348
+ ];
2349
+ return { candidates, confirmed: confirmed3, onDisk };
2350
+ }
2351
+ async function resolveToolchain(root, profile) {
2352
+ const signals = (pm) => [
2353
+ ...pm.lockfiles ?? [],
2354
+ ...pm.detectFiles ?? []
2355
+ ];
2356
+ const matched = profile.packageManagers.find(
2357
+ (pm) => signals(pm).some((f) => existsSync2(join9(root, f)))
2358
+ );
2359
+ const fallback = profile.packageManagers.find((pm) => signals(pm).length === 0) ?? profile.packageManagers[0];
2360
+ const packageManager = resolveDeclaredManifest(root, matched ?? fallback);
2361
+ let { installSteps, ingest } = packageManager;
2362
+ installSteps = installSteps.map(
2363
+ (step) => isWorktreeRelativeCommand(step.argv[0]) ? { ...step, argv: withCommand(step.argv, join9(root, step.argv[0])) } : step
2364
+ );
2365
+ if (ingest.kind === "auto" && isWorktreeRelativeCommand(ingest.argv[0])) {
2366
+ ingest = {
2367
+ ...ingest,
2368
+ argv: withCommand(ingest.argv, join9(root, ingest.argv[0]))
2369
+ };
2370
+ }
2371
+ if (profile.id === "javascript") {
2372
+ const pm = await detectPackageManager(root);
2373
+ if (JS_PACKAGE_MANAGERS.has(pm)) {
2374
+ installSteps = installSteps.map((step) => ({
2375
+ ...step,
2376
+ argv: withCommand(step.argv, pm)
2377
+ }));
2378
+ if (pm === "bun" && ingest.kind === "auto") {
2379
+ ingest = { ...ingest, argv: withCommand(ingest.argv, "bun") };
2380
+ }
2381
+ }
2382
+ }
2383
+ return { profile, packageManager, installSteps, ingest };
2384
+ }
2385
+ function resolveIngestArgv(ingest, entrypoint) {
2386
+ if (ingest.kind !== "auto") {
2387
+ throw new Error("resolveIngestArgv called for a manual-run toolchain");
2388
+ }
2389
+ return ingest.argv.map(
2390
+ (part) => part === ENTRYPOINT_TOKEN ? entrypoint : part
2391
+ );
2392
+ }
2393
+ function describeIngestCommand(ingest, entrypoint) {
2394
+ if (ingest.kind !== "auto") return ingest.runCommand;
2395
+ return ingest.argv.map((part) => part === ENTRYPOINT_TOKEN ? shellQuote(entrypoint) : part).join(" ");
2396
+ }
2397
+ function ingestScriptDir(profile) {
2398
+ const parts = profile.ingestEntrypointExample.split("/");
2399
+ return parts.slice(0, -1).join("/") || ".";
2400
+ }
2401
+ function localSourceLimitation(root, profile) {
2402
+ const caveat = profile.localSourceCaveat;
2403
+ if (!caveat) return void 0;
2404
+ return existsSync2(join9(root, caveat.unless)) ? void 0 : caveat.message;
2405
+ }
2406
+ async function missingBuildTask(root, toolchain) {
2407
+ const { ingest, packageManager } = toolchain;
2408
+ if (ingest.kind !== "manual" || !ingest.requiresBuildTask) return void 0;
2409
+ if (packageManager.dependency.mode !== "agent-declares") return void 0;
2410
+ const buildFile = join9(root, packageManager.dependency.file);
2411
+ const contents = await readFile7(buildFile, "utf8").catch(() => void 0);
2412
+ if (contents === void 0) return void 0;
2413
+ return contents.includes(ingest.requiresBuildTask) ? void 0 : ingest.requiresBuildTask;
2414
+ }
2415
+ function sdkVersionPin(profile, packageManager) {
2416
+ return packageManager.sdkVersionPin ?? profile.sdk.versionPin;
2417
+ }
2418
+ function dependencyInstruction(toolchain) {
2419
+ const { profile, packageManager } = toolchain;
2420
+ const { packageName } = profile.sdk;
2421
+ const versionPin = sdkVersionPin(profile, packageManager);
2422
+ const also = profile.sdk.alsoRequires ? ` ${profile.sdk.alsoRequires}` : "";
2423
+ switch (packageManager.dependency.mode) {
2424
+ case "wizard-installs":
2425
+ return `The wizard installs ${packageName} ${versionPin} in the worktree after you finish \u2014 import it directly and do not edit dependency manifests for it.${also}`;
2426
+ case "code-imports":
2427
+ return `Import ${packageName} in the script; the wizard resolves and fetches it in the worktree after you finish. Do not edit dependency manifests by hand.${also}`;
2428
+ case "agent-declares":
2429
+ return `Declare ${packageName} ${versionPin} in "${packageManager.dependency.file}" (create the file if needed), plus any other dependency your script imports; the wizard installs them in the worktree after you finish.${also}`;
2430
+ }
2431
+ }
2432
+
2433
+ // src/lib/tools/searchFiles.ts
2226
2434
  var MAX_QUERY_LENGTH = 1e3;
2227
2435
  async function walkFiles(dir) {
2228
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2229
2436
  const out = [];
2230
- for (const e of await readdir2(dir, { withFileTypes: true })) {
2231
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2232
- const full = join8(dir, e.name);
2437
+ for (const e of await readdir3(dir, { withFileTypes: true })) {
2438
+ if (e.name.startsWith(".") || ALL_SKIP_DIRS.has(e.name)) continue;
2439
+ const full = join10(dir, e.name);
2233
2440
  if (e.isDirectory()) out.push(...await walkFiles(full));
2234
2441
  else if (e.isFile()) out.push(full);
2235
2442
  }
@@ -2262,7 +2469,7 @@ function searchFilesTool(ctx) {
2262
2469
  for (const file of await walkFiles(resolved.target)) {
2263
2470
  let content;
2264
2471
  try {
2265
- content = await readFile6(file, "utf8");
2472
+ content = await readFile8(file, "utf8");
2266
2473
  } catch {
2267
2474
  continue;
2268
2475
  }
@@ -2286,88 +2493,144 @@ function searchFilesTool(ctx) {
2286
2493
  import { tool as tool8 } from "ai";
2287
2494
  import z11 from "zod";
2288
2495
 
2496
+ // src/lib/tools/repoVerification.ts
2497
+ import { existsSync as existsSync3 } from "node:fs";
2498
+ import { join as join11 } from "node:path";
2499
+
2289
2500
  // src/lib/tools/utils/runCommand.ts
2290
2501
  import { spawn as spawn2 } from "node:child_process";
2291
- function runCommand(command, args, cwd) {
2502
+ var INSTALL_TIMEOUT_MS = 15 * 6e4;
2503
+ var INGEST_TIMEOUT_MS = 15 * 6e4;
2504
+ var VERIFY_TIMEOUT_MS = 10 * 6e4;
2505
+ var KILL_GRACE_MS = 5e3;
2506
+ function runCommand(command, args, options = {}) {
2507
+ const { cwd, env, timeoutMs = VERIFY_TIMEOUT_MS } = options;
2292
2508
  return new Promise((resolve4) => {
2293
2509
  let output = "";
2510
+ let settled = false;
2294
2511
  const child = spawn2(command, args, {
2295
2512
  cwd,
2296
- stdio: ["ignore", "pipe", "pipe"]
2513
+ shell: false,
2514
+ stdio: ["ignore", "pipe", "pipe"],
2515
+ ...env ? { env: { ...process.env, ...env } } : {}
2297
2516
  });
2517
+ const settle = (result) => {
2518
+ if (settled) return;
2519
+ settled = true;
2520
+ clearTimeout(timer);
2521
+ resolve4(result);
2522
+ };
2523
+ const timer = setTimeout(() => {
2524
+ child.kill("SIGTERM");
2525
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
2526
+ const seconds = Math.round(timeoutMs / 1e3);
2527
+ settle({
2528
+ code: 1,
2529
+ output: `${output}
2530
+ Timed out after ${seconds}s: ${command} ${args.join(" ")}`.trim(),
2531
+ timedOut: true
2532
+ });
2533
+ }, timeoutMs);
2298
2534
  child.stdout?.on("data", (d) => output += d);
2299
2535
  child.stderr?.on("data", (d) => output += d);
2300
2536
  child.on(
2301
2537
  "error",
2302
- (err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
2538
+ (err) => settle({
2539
+ code: 1,
2540
+ output: `Failed to run ${command}: ${err.message}`,
2541
+ timedOut: false
2542
+ })
2543
+ );
2544
+ child.on(
2545
+ "close",
2546
+ (code) => settle({ code: code ?? 1, output, timedOut: false })
2303
2547
  );
2304
- child.on("close", (code) => resolve4({ code: code ?? 1, output }));
2305
2548
  });
2306
2549
  }
2307
2550
 
2308
- // src/lib/tools/utils/packageManager.ts
2309
- import { readFile as readFile7 } from "node:fs/promises";
2310
- import { existsSync } from "node:fs";
2311
- import { join as join9 } from "node:path";
2312
- var LOCKFILES = [
2313
- ["pnpm-lock.yaml", "pnpm"],
2314
- ["yarn.lock", "yarn"],
2315
- ["bun.lockb", "bun"],
2316
- ["bun.lock", "bun"],
2317
- ["package-lock.json", "npm"]
2318
- ];
2319
- async function readPackageJson(cwd = process.cwd()) {
2320
- return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
2321
- }
2322
- function packageManagerFrom(pkg) {
2323
- return pkg.packageManager?.split("@")[0] ?? "npm";
2324
- }
2325
- function packageManagerFromLockfile(cwd) {
2326
- return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
2327
- }
2328
- async function detectPackageManager(cwd) {
2329
- try {
2330
- const pkg = await readPackageJson(cwd);
2331
- if (pkg.packageManager) return packageManagerFrom(pkg);
2332
- } catch {
2333
- }
2334
- return packageManagerFromLockfile(cwd) ?? "npm";
2335
- }
2336
-
2337
2551
  // src/lib/tools/repoVerification.ts
2338
2552
  var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
2339
- async function runRepoVerificationCheck() {
2553
+ async function runCheck(command, binary, args) {
2554
+ const { code, output } = await runCommand(binary, args, {
2555
+ timeoutMs: VERIFY_TIMEOUT_MS
2556
+ });
2557
+ return { command, exitCode: code, ok: code === 0, output: output.trim() };
2558
+ }
2559
+ async function javascriptChecks() {
2340
2560
  let pkg;
2341
2561
  try {
2342
2562
  pkg = await readPackageJson();
2343
2563
  } catch (err) {
2344
- const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2345
- return { ok: false, checks: [], limitation };
2564
+ return {
2565
+ limitation: `Could not read package.json to detect verification conventions: ${err.message}`
2566
+ };
2346
2567
  }
2347
2568
  const scripts = pkg.scripts ?? {};
2348
2569
  const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
2349
2570
  if (present.length === 0) {
2350
- const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2351
- return { ok: false, checks: [], limitation };
2571
+ return {
2572
+ limitation: `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`
2573
+ };
2352
2574
  }
2353
2575
  const pm = await detectPackageManager(process.cwd());
2354
2576
  const checks = [];
2355
2577
  for (const script of present) {
2356
- const command = `${pm} run ${script}`;
2357
- const { code, output } = await runCommand(pm, ["run", script]);
2358
- checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
2578
+ checks.push(
2579
+ await runCheck(`${pm} run ${script}`, pm, ["run", script])
2580
+ );
2359
2581
  }
2360
- return { ok: checks.every((c) => c.ok), checks };
2582
+ return { checks };
2583
+ }
2584
+ async function registryChecks(id) {
2585
+ const profile = LANGUAGE_PROFILES[id];
2586
+ const runnable = profile.verification.filter(
2587
+ (spec) => !spec.requiresFile || existsSync3(join11(process.cwd(), spec.requiresFile))
2588
+ );
2589
+ if (runnable.length === 0) {
2590
+ return {
2591
+ limitation: `No mechanical verification available for ${profile.displayName} in this repo.`
2592
+ };
2593
+ }
2594
+ const checks = [];
2595
+ for (const spec of runnable) {
2596
+ checks.push(
2597
+ await runCheck(spec.argv.join(" "), spec.argv[0], [...spec.argv.slice(1)])
2598
+ );
2599
+ }
2600
+ return { checks };
2601
+ }
2602
+ async function runRepoVerificationCheck(languages = [DEFAULT_LANGUAGE_ID]) {
2603
+ const ids = [...new Set(languages)];
2604
+ if (ids.length === 0) ids.push(DEFAULT_LANGUAGE_ID);
2605
+ const checks = [];
2606
+ const limitations = [];
2607
+ for (const id of ids) {
2608
+ const result = id === JAVASCRIPT ? await javascriptChecks() : await registryChecks(id);
2609
+ if ("checks" in result) checks.push(...result.checks);
2610
+ else limitations.push(result.limitation);
2611
+ }
2612
+ if (checks.length === 0) {
2613
+ return {
2614
+ ok: false,
2615
+ checks: [],
2616
+ limitation: limitations.join(" ") || "No verification checks available."
2617
+ };
2618
+ }
2619
+ return {
2620
+ ok: checks.every((c) => c.ok),
2621
+ checks,
2622
+ ...limitations.length ? { limitation: limitations.join(" ") } : {}
2623
+ };
2361
2624
  }
2362
2625
 
2363
2626
  // src/lib/tools/verifyImplementation.ts
2364
- function verifyImplementationTool() {
2627
+ function verifyImplementationTool(ctx) {
2365
2628
  return tool8({
2366
- description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
2629
+ description: "Run the repo's mechanical verification checks for generated implementation changes. Uses the conventions of the repo's languages (package.json lint/typecheck/check scripts for JavaScript, the equivalent compile/analyze command elsewhere) and returns structured pass/fail evidence for the verifier to interpret.",
2367
2630
  inputSchema: z11.object(),
2368
2631
  execute: async () => {
2369
- logger.info("called verifyImplementation tool");
2370
- return runRepoVerificationCheck();
2632
+ logger.info({ languages: ctx.languages }, "called verifyImplementation tool");
2633
+ return runRepoVerificationCheck(ctx.languages);
2371
2634
  }
2372
2635
  });
2373
2636
  }
@@ -2493,12 +2756,17 @@ var DEFAULT_TOOL_LIMITS = {
2493
2756
  read: 20,
2494
2757
  match: 100
2495
2758
  };
2496
- function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
2759
+ function createToolContext({
2760
+ limits = DEFAULT_TOOL_LIMITS,
2761
+ cwd = process.cwd(),
2762
+ languages = [DEFAULT_LANGUAGE_ID]
2763
+ } = {}) {
2497
2764
  return {
2498
2765
  root: cwd,
2499
2766
  cwd,
2500
2767
  limits,
2501
- counts: { list: 0, search: 0, read: 0 }
2768
+ counts: { list: 0, search: 0, read: 0 },
2769
+ languages: languages.length ? languages : [DEFAULT_LANGUAGE_ID]
2502
2770
  };
2503
2771
  }
2504
2772
 
@@ -2535,7 +2803,7 @@ function createTools(ctx, { output, tools }) {
2535
2803
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2536
2804
  verifyImplementation: withLogging(
2537
2805
  "verifyImplementation",
2538
- verifyImplementationTool()
2806
+ verifyImplementationTool(ctx)
2539
2807
  ),
2540
2808
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2541
2809
  notifyUser: withLogging("notifyUser", notifyUserTool())
@@ -2571,7 +2839,7 @@ async function runAgent(req) {
2571
2839
  baseURL: PROXY_BASE_URL,
2572
2840
  fetch: proxyFetch
2573
2841
  });
2574
- const toolContext = createToolContext();
2842
+ const toolContext = createToolContext({ languages: req.languages });
2575
2843
  const readTools = ["readFile", "searchFiles", "listFiles"];
2576
2844
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2577
2845
  const instructions = [
@@ -2662,8 +2930,11 @@ var detectLanguageSchema = z16.object({
2662
2930
  var detectLanguage = () => runAgent({
2663
2931
  instructions: [
2664
2932
  "Analyze the codebase and determine the programming languages and frameworks used",
2933
+ "Start from the dependency manifests: package.json.",
2934
+ "List the language that owns the backend/data code first \u2014 that is the one an ingestion script will be written in.",
2665
2935
  "If a superset language is found, exclude the subset language. TS-over-JS.",
2666
2936
  "If a meta-framework is used, exclude the framework. Next-over-React.",
2937
+ "Frameworks include backend and server-rendering frameworks (e.g. Rails, Django, Laravel, Symfony, Spring Boot, ASP.NET Core, Flask, Gin, Ktor) as well as frontend ones (React, Vue, Angular, Svelte) and mobile ones (Flutter, SwiftUI).",
2667
2938
  "Return the exact version",
2668
2939
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
2669
2940
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -2712,6 +2983,7 @@ var MODE_CONFIG = {
2712
2983
  "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
2713
2984
  "For each entity, return its name, the file path(s) where it is defined, and its indexable attribute keys (the fields a user would search or filter on).",
2714
2985
  "Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
2986
+ "Entities live wherever the stack keeps them: TypeScript interfaces or a Prisma schema.",
2715
2987
  "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
2716
2988
  "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
2717
2989
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -2723,8 +2995,9 @@ var MODE_CONFIG = {
2723
2995
  instructions: [
2724
2996
  "Analyze the codebase to determine the single best location to add search UI functionality.",
2725
2997
  "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
2726
- "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
2727
- 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
2998
+ "It may be a client-side component or a server-rendered template \u2014 return whichever the project actually renders its UI from (e.g. /layouts/header.tsx, app/views/layouts/application.html.erb, templates/base.html, resources/views/layouts/app.blade.php, templates/base.html.twig).",
2999
+ "Return one file path as searchImplementationAnalysis.",
3000
+ 'Use as few tools as possible, but do not guess. If the project renders no UI at all (an API-only service), say "unknown".',
2728
3001
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2729
3002
  "When done, call reportStatus"
2730
3003
  ],
@@ -2733,7 +3006,7 @@ var MODE_CONFIG = {
2733
3006
  verification: {
2734
3007
  instructions: [
2735
3008
  "Analyze the codebase to determine which code-quality tools are available to validate changes.",
2736
- "Look at package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
3009
+ "Look at the dependency manifest and config files for the project's languages: package.json scripts with tsconfig/eslint/prettier.",
2737
3010
  'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
2738
3011
  "Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
2739
3012
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -2761,7 +3034,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2761
3034
  // package.json
2762
3035
  var package_default = {
2763
3036
  name: "@algolia/wizard",
2764
- version: "0.8.0-rc.58.45",
3037
+ version: "0.8.0-rc.59.47",
2765
3038
  description: "Magically implement Algolia functionality in your codebase",
2766
3039
  type: "module",
2767
3040
  engines: {
@@ -2783,7 +3056,7 @@ var package_default = {
2783
3056
  prepare: "husky",
2784
3057
  prepublishOnly: "pnpm build",
2785
3058
  reset: "tsx ./scripts/reset-state.ts",
2786
- "test:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
3059
+ "test:toolchains": "tsx ./scripts/verify-toolchains.ts",
2787
3060
  "test:tools": "tsx ./tool-evals/toolEval.ts",
2788
3061
  test: "vitest",
2789
3062
  typecheck: "tsc --noEmit -p tsconfig.json"
@@ -2864,82 +3137,185 @@ function parseEntries(raw) {
2864
3137
  return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
2865
3138
  }
2866
3139
  var summarize = (entries) => entries.length ? entries.map((e) => e.name).join(", ") : "none";
2867
- async function askList(ctx, prompt, { required = false } = {}) {
3140
+
3141
+ // src/actions/confirmLanguage.ts
3142
+ import z19 from "zod";
3143
+ var confirmLanguageSchema = z19.object({
3144
+ languages: detectLanguageSchema.shape.languages
3145
+ });
3146
+ var OTHER_OPTION = "Other";
3147
+ function confirmed(languages) {
3148
+ track("AI Wizard Language Confirmed", { languages });
3149
+ return { languages };
3150
+ }
3151
+ async function askOtherLanguage(ctx) {
3152
+ let prompt = "enter the language for your ingestion script";
2868
3153
  for (; ; ) {
2869
3154
  const answer = await ctx.requestUserInput({
2870
3155
  prompt,
2871
3156
  promptType: "textInput",
2872
- options: [],
2873
- helpText: 'Comma-separated, e.g. "TypeScript, Node".'
3157
+ options: []
2874
3158
  });
2875
3159
  if (typeof answer !== "string") {
2876
- throw new Error("askList received an unexpected non-text result");
3160
+ throw new Error("confirmLanguage received an unexpected non-text result");
2877
3161
  }
2878
- const entries = parseEntries(answer);
2879
- if (entries.length || !required) return entries;
2880
- prompt = "Please enter at least one entry:";
3162
+ const name = parseEntries(answer)[0]?.name;
3163
+ if (name) return name;
3164
+ prompt = "please enter a language name:";
2881
3165
  }
2882
3166
  }
2883
-
2884
- // src/actions/confirmLanguage.ts
2885
- import z19 from "zod";
2886
- var confirmLanguageSchema = z19.object({
2887
- languages: detectLanguageSchema.shape.languages
2888
- });
2889
3167
  async function confirmLanguage(ctx) {
2890
3168
  const detected = ctx.getStepOutput("project-scan");
2891
- const answer = await ctx.requestUserInput({
2892
- prompt: "Did we detect your language(s) correctly?",
2893
- promptType: "acceptReject",
2894
- options: ["Yes", "No"],
2895
- messages: [`Languages: ${summarize(detected.languages)}`]
2896
- });
2897
- const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
2898
- required: true
2899
- });
2900
- track("AI Wizard Language Confirmed", {
2901
- languages
3169
+ const detectedLanguages = detected.languages ?? [];
3170
+ const others = (chosen) => detectedLanguages.filter((l) => !isSameLanguage(l.name, chosen));
3171
+ const primary = detectedLanguages[0];
3172
+ if (primary) {
3173
+ const accepted = await ctx.requestUserInput({
3174
+ prompt: `Write the ingestion script in ${primary.name}?`,
3175
+ promptType: "acceptReject",
3176
+ options: [`Confirm ${primary.name}`, "Use a different language"],
3177
+ secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0],
3178
+ messages: detectedLanguages.length > 1 ? [`Detected: ${summarize(detectedLanguages)}`] : []
3179
+ });
3180
+ if (accepted === true) return confirmed(detectedLanguages);
3181
+ }
3182
+ const options = [...CURATED_LANGUAGES];
3183
+ for (const language of detectedLanguages) {
3184
+ if (!options.some((o) => isSameLanguage(o, language.name))) {
3185
+ options.push(language.name);
3186
+ }
3187
+ }
3188
+ options.push(OTHER_OPTION);
3189
+ const detectedFor = (option) => detectedLanguages.find((l) => isSameLanguage(option, l.name));
3190
+ const secondary = options.map(
3191
+ (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
3192
+ );
3193
+ const defaultSelectedIndex = Math.max(
3194
+ options.findIndex((o) => detectedFor(o)),
3195
+ 0
3196
+ );
3197
+ const selection = await ctx.requestUserInput({
3198
+ prompt: "select the language for your ingestion script",
3199
+ promptType: "multipleChoice",
3200
+ options,
3201
+ secondary,
3202
+ defaultSelectedIndex
2902
3203
  });
2903
- return { languages };
3204
+ if (typeof selection !== "string") {
3205
+ throw new Error("confirmLanguage received an unexpected non-text result");
3206
+ }
3207
+ const name = selection === OTHER_OPTION ? await askOtherLanguage(ctx) : selection;
3208
+ const version = detectedFor(name)?.version ?? "unknown";
3209
+ return confirmed([{ name, version }, ...others(name)]);
2904
3210
  }
2905
3211
 
2906
3212
  // src/actions/confirmFramework.ts
2907
3213
  import z20 from "zod";
2908
- var confirmFrameworkSchema = z20.object({
2909
- frameworks: detectLanguageSchema.shape.frameworks
2910
- });
2911
- var CURATED_FRAMEWORKS = [
2912
- "Next.js",
2913
- "React",
2914
- "Vue",
2915
- "Angular",
2916
- "Svelte",
2917
- "Vanilla JS"
3214
+
3215
+ // src/lib/frameworks.ts
3216
+ var BACKEND_ONLY_FRAMEWORK = "Backend only / API";
3217
+ var FRAMEWORKS = [
3218
+ // Frontend — InstantSearch component flavors.
3219
+ { name: "Next.js", strategy: "react", aliases: ["next", "nextjs"] },
3220
+ { name: "React", strategy: "react", aliases: ["reactjs"] },
3221
+ { name: "Vue", strategy: "vue", aliases: ["vuejs", "nuxt", "nuxtjs"] },
3222
+ { name: "Angular", strategy: "angular", aliases: ["angularjs"] },
3223
+ // No Svelte InstantSearch flavor exists, so it uses InstantSearch.js.
3224
+ { name: "Svelte", strategy: "js", aliases: ["sveltekit"] },
3225
+ {
3226
+ name: "Vanilla JS",
3227
+ strategy: "js",
3228
+ aliases: ["vanilla", "javascript", "js", "astro", "vite"]
3229
+ },
3230
+ // Backend — Algolia's official framework integrations. Server-rendered
3231
+ // templates get InstantSearch.js from a CDN.
3232
+ {
3233
+ name: "Rails",
3234
+ strategy: "cdn-template",
3235
+ aliases: ["rubyonrails", "ruby on rails", "erb"]
3236
+ },
3237
+ { name: "Django", strategy: "cdn-template", aliases: ["jinja", "jinja2"] },
3238
+ { name: "Laravel", strategy: "cdn-template", aliases: ["blade"] },
3239
+ { name: "Symfony", strategy: "cdn-template", aliases: ["twig"] },
3240
+ // Mobile — Algolia ships InstantSearch iOS/Android and Dart clients, but the
3241
+ // wizard can't scaffold a native UI, so it points at the docs instead.
3242
+ { name: "Flutter", strategy: "none", aliases: [] },
3243
+ { name: "iOS", strategy: "none", aliases: ["swiftui", "uikit"] },
3244
+ { name: "Android", strategy: "none", aliases: ["jetpack compose", "compose"] },
3245
+ { name: BACKEND_ONLY_FRAMEWORK, strategy: "cdn-template", aliases: [] }
2918
3246
  ];
2919
- var OTHER_OPTION = "Other";
3247
+ var CURATED_FRAMEWORKS = FRAMEWORKS.map(
3248
+ (f) => f.name
3249
+ );
2920
3250
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2921
- var FRAMEWORK_ALIASES = {
2922
- next: "nextjs",
2923
- nextjs: "nextjs",
2924
- react: "react",
2925
- reactjs: "react",
2926
- vue: "vue",
2927
- vuejs: "vue",
2928
- angular: "angular",
2929
- angularjs: "angular",
2930
- svelte: "svelte",
2931
- sveltekit: "svelte",
2932
- vanillajs: "vanillajs",
2933
- vanilla: "vanillajs",
2934
- javascript: "vanillajs",
2935
- js: "vanillajs"
2936
- };
2937
- var isSameFramework = (a, b) => {
2938
- const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
2939
- const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
3251
+ var ALIAS_TO_NAME = /* @__PURE__ */ new Map();
3252
+ for (const framework of FRAMEWORKS) {
3253
+ for (const alias of [framework.name, ...framework.aliases]) {
3254
+ ALIAS_TO_NAME.set(normalize(alias), framework.name);
3255
+ }
3256
+ }
3257
+ var STRATEGY_BY_NAME = new Map(FRAMEWORKS.map((f) => [f.name, f.strategy]));
3258
+ function canonicalFrameworkName(name) {
3259
+ return ALIAS_TO_NAME.get(normalize(name));
3260
+ }
3261
+ function isSameFramework(a, b) {
3262
+ const x = canonicalFrameworkName(a) ?? normalize(a);
3263
+ const y = canonicalFrameworkName(b) ?? normalize(b);
2940
3264
  return x !== "" && x === y;
2941
- };
2942
- function confirmed(name, version) {
3265
+ }
3266
+ function resolveSearchStrategy(frameworkName, hasJavaScriptInStack) {
3267
+ const canonical = frameworkName ? canonicalFrameworkName(frameworkName) : void 0;
3268
+ const strategy = canonical ? STRATEGY_BY_NAME.get(canonical) : void 0;
3269
+ if (strategy) return strategy;
3270
+ return hasJavaScriptInStack ? "js" : "cdn-template";
3271
+ }
3272
+ function searchDocKey(strategy) {
3273
+ return strategy === "cdn-template" ? "templates" : strategy;
3274
+ }
3275
+ function bundlesJavaScript(strategy) {
3276
+ return strategy !== "cdn-template" && strategy !== "none";
3277
+ }
3278
+ function canScaffoldSearchUI(strategy) {
3279
+ return strategy !== "none";
3280
+ }
3281
+ var ENV_PREFIXES = [
3282
+ { aliases: ["next", "nextjs"], prefix: "NEXT_PUBLIC_" },
3283
+ { aliases: ["nuxt", "nuxtjs"], prefix: "NUXT_PUBLIC_" },
3284
+ { aliases: ["astro"], prefix: "PUBLIC_" },
3285
+ { aliases: ["vite"], prefix: "VITE_" }
3286
+ ];
3287
+ var DEFAULT_ENV_PREFIX = "PUBLIC_";
3288
+ function publicEnvPrefix(frameworkNames, strategy) {
3289
+ if (!bundlesJavaScript(strategy)) return "";
3290
+ const present = new Set(frameworkNames.map(normalize));
3291
+ for (const { aliases, prefix } of ENV_PREFIXES) {
3292
+ if (aliases.some((alias) => present.has(alias))) return prefix;
3293
+ }
3294
+ return DEFAULT_ENV_PREFIX;
3295
+ }
3296
+ function describeSearchTarget(strategy, frameworkName) {
3297
+ switch (strategy) {
3298
+ case "react":
3299
+ return "React (react-instantsearch)";
3300
+ case "vue":
3301
+ return "Vue (vue-instantsearch)";
3302
+ case "angular":
3303
+ return "Angular (angular-instantsearch)";
3304
+ case "js":
3305
+ return "plain JavaScript (InstantSearch.js)";
3306
+ case "cdn-template":
3307
+ return `${frameworkName ?? "server-rendered"} templates (InstantSearch.js via CDN)`;
3308
+ case "none":
3309
+ return frameworkName ?? "a native mobile app";
3310
+ }
3311
+ }
3312
+
3313
+ // src/actions/confirmFramework.ts
3314
+ var confirmFrameworkSchema = z20.object({
3315
+ frameworks: detectLanguageSchema.shape.frameworks
3316
+ });
3317
+ var OTHER_OPTION2 = "Other";
3318
+ function confirmed2(name, version) {
2943
3319
  const frameworks = [{ name, version: version ?? "unknown" }];
2944
3320
  track("AI Wizard Frontend Framework Confirmed", { frameworks });
2945
3321
  return { frameworks };
@@ -2967,7 +3343,7 @@ async function confirmFramework(ctx) {
2967
3343
  for (const fw of detectedFrameworks) {
2968
3344
  if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
2969
3345
  }
2970
- options.push(OTHER_OPTION);
3346
+ options.push(OTHER_OPTION2);
2971
3347
  const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
2972
3348
  const primary = detectedFrameworks[0];
2973
3349
  if (primary) {
@@ -2977,7 +3353,7 @@ async function confirmFramework(ctx) {
2977
3353
  options: [`Confirm ${primary.name}`, "Use a different framework"],
2978
3354
  secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
2979
3355
  });
2980
- if (accepted === true) return confirmed(primary.name, primary.version);
3356
+ if (accepted === true) return confirmed2(primary.name, primary.version);
2981
3357
  }
2982
3358
  const secondary = options.map(
2983
3359
  (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
@@ -2987,7 +3363,7 @@ async function confirmFramework(ctx) {
2987
3363
  0
2988
3364
  );
2989
3365
  const selection = await ctx.requestUserInput({
2990
- prompt: "select a framework",
3366
+ prompt: "select the framework that renders your UI",
2991
3367
  promptType: "multipleChoice",
2992
3368
  options,
2993
3369
  secondary,
@@ -2996,10 +3372,10 @@ async function confirmFramework(ctx) {
2996
3372
  if (typeof selection !== "string") {
2997
3373
  throw new Error("confirmFramework received an unexpected non-text result");
2998
3374
  }
2999
- if (selection === OTHER_OPTION) {
3000
- return confirmed(await askOtherFramework(ctx));
3375
+ if (selection === OTHER_OPTION2) {
3376
+ return confirmed2(await askOtherFramework(ctx));
3001
3377
  }
3002
- return confirmed(selection, detectedFor(selection)?.version);
3378
+ return confirmed2(selection, detectedFor(selection)?.version);
3003
3379
  }
3004
3380
 
3005
3381
  // src/actions/promptUser.ts
@@ -3092,15 +3468,15 @@ async function confirmEntities(ctx) {
3092
3468
  onSubmit: () => {
3093
3469
  }
3094
3470
  });
3095
- const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3096
- if (confirmed2.length === 0) {
3471
+ const confirmed3 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3472
+ if (confirmed3.length === 0) {
3097
3473
  throw new Error("User cancelled entity selection \u2014 analysis halted.");
3098
3474
  }
3099
- ctx.setUserInput("confirmedEntities", confirmed2);
3475
+ ctx.setUserInput("confirmedEntities", confirmed3);
3100
3476
  track("AI Wizard Entities Confirmed", {
3101
- entities: toEntitySummary(confirmed2)
3477
+ entities: toEntitySummary(confirmed3)
3102
3478
  });
3103
- return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
3479
+ return { ingestionAnalysis: entities, confirmedEntities: confirmed3 };
3104
3480
  }
3105
3481
 
3106
3482
  // src/actions/review.ts
@@ -3124,7 +3500,7 @@ ${JSON.stringify(s.output, null, 2)}`
3124
3500
  }
3125
3501
  function formatReviewSummary(result) {
3126
3502
  const nextStepLines = result.nextSteps.map((step) => {
3127
- const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3503
+ const isIngestCommand = step.includes("algolia-wizard/");
3128
3504
  const isWorktreeCommand = step.includes("/worktrees/");
3129
3505
  return {
3130
3506
  text: `\u2192 ${step}`,
@@ -3166,13 +3542,14 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3166
3542
  import z24 from "zod";
3167
3543
 
3168
3544
  // src/lib/worktree.ts
3169
- import { execFile, spawn as spawn3 } from "node:child_process";
3170
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3545
+ import { execFile } from "node:child_process";
3546
+ import { existsSync as existsSync4 } from "node:fs";
3547
+ import { copyFile, mkdir as mkdir6, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3171
3548
  import {
3172
3549
  basename as basename2,
3173
3550
  dirname as dirname7,
3174
3551
  isAbsolute as isAbsolute2,
3175
- join as join10,
3552
+ join as join12,
3176
3553
  relative as relative2,
3177
3554
  resolve as resolve3
3178
3555
  } from "node:path";
@@ -3206,8 +3583,8 @@ async function isWorkingTreeDirty(repoRoot) {
3206
3583
  return out.trim().length > 0;
3207
3584
  }
3208
3585
  async function pruneOldWorktrees(repoRoot) {
3209
- const dir = join10(stateDir(repoRoot), "worktrees");
3210
- const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3586
+ const dir = join12(stateDir(repoRoot), "worktrees");
3587
+ const stale = (await readdir4(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3211
3588
  for (const slug of stale) {
3212
3589
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
3213
3590
  try {
@@ -3217,7 +3594,7 @@ async function pruneOldWorktrees(repoRoot) {
3217
3594
  "worktree",
3218
3595
  "remove",
3219
3596
  "--force",
3220
- join10(dir, slug)
3597
+ join12(dir, slug)
3221
3598
  ]);
3222
3599
  await git(["-C", repoRoot, "branch", "-D", branch]);
3223
3600
  } catch (err) {
@@ -3231,43 +3608,55 @@ async function pruneOldWorktrees(repoRoot) {
3231
3608
  async function createWorktree(repoRoot) {
3232
3609
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3233
3610
  const dirSlug = branch.replace(/\//g, "-");
3234
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3611
+ const path = join12(stateDir(repoRoot), "worktrees", dirSlug);
3235
3612
  await git(["-C", repoRoot, "worktree", "prune"]);
3236
3613
  await pruneOldWorktrees(repoRoot);
3237
3614
  await mkdir6(dirname7(path), { recursive: true });
3238
3615
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
3239
3616
  return { path, branch };
3240
3617
  }
3241
- async function installWorktreeDeps(worktreePath) {
3242
- try {
3243
- await readPackageJson(worktreePath);
3244
- } catch {
3245
- return { ok: true, output: "no package.json; skipped install" };
3246
- }
3247
- const pm = await detectPackageManager(worktreePath);
3248
- return new Promise((resolve4) => {
3249
- let output = "";
3250
- const child = spawn3(pm, ["install"], {
3251
- cwd: worktreePath,
3252
- stdio: ["ignore", "pipe", "pipe"]
3253
- });
3254
- child.stdout?.on("data", (d) => output += d);
3255
- child.stderr?.on("data", (d) => output += d);
3256
- child.on(
3257
- "error",
3258
- (err) => resolve4({
3259
- ok: false,
3260
- output: `Failed to run ${pm} install: ${err.message}`
3261
- })
3262
- );
3263
- child.on(
3264
- "close",
3265
- (code) => resolve4({ ok: code === 0, output: output.trim() })
3266
- );
3618
+ async function spawnStep(worktreePath, argv) {
3619
+ const { code, output } = await runCommand(argv[0], [...argv.slice(1)], {
3620
+ cwd: worktreePath,
3621
+ timeoutMs: INSTALL_TIMEOUT_MS
3267
3622
  });
3623
+ return { ok: code === 0, output: output.trim() };
3624
+ }
3625
+ async function installWorktreeDeps(worktreePath, toolchain) {
3626
+ const { profile, installSteps, packageManager } = toolchain;
3627
+ const declared = packageManager.dependency.mode === "agent-declares" ? packageManager.dependency.file : void 0;
3628
+ const haveSomethingToInstall = await hasProfileManifest(worktreePath, profile) || declared !== void 0 && existsSync4(join12(worktreePath, declared));
3629
+ if (!haveSomethingToInstall) {
3630
+ return {
3631
+ ok: true,
3632
+ output: `no ${profile.displayName} manifest; skipped install`
3633
+ };
3634
+ }
3635
+ if (installSteps.length === 0) {
3636
+ return {
3637
+ ok: true,
3638
+ output: `${profile.displayName} (${toolchain.packageManager.id}) has no wizard-run install step`
3639
+ };
3640
+ }
3641
+ const outputs = [];
3642
+ for (const step of installSteps) {
3643
+ if (step.requiresFile && !existsSync4(join12(worktreePath, step.requiresFile)))
3644
+ continue;
3645
+ const result = await spawnStep(worktreePath, step.argv);
3646
+ if (result.output) outputs.push(result.output);
3647
+ if (result.ok) continue;
3648
+ if (step.optional) {
3649
+ logger.warn(
3650
+ { step: step.argv.join(" "), output: result.output },
3651
+ "installWorktreeDeps: optional install step failed; continuing"
3652
+ );
3653
+ continue;
3654
+ }
3655
+ return { ok: false, output: outputs.join("\n").trim() };
3656
+ }
3657
+ return { ok: true, output: outputs.join("\n").trim() };
3268
3658
  }
3269
- var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3270
- function validateIngestEntrypoint(worktreePath, entrypoint) {
3659
+ function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
3271
3660
  if (!entrypoint || entrypoint.startsWith("-")) {
3272
3661
  return {
3273
3662
  ok: false,
@@ -3282,18 +3671,29 @@ function validateIngestEntrypoint(worktreePath, entrypoint) {
3282
3671
  reason: `entrypoint "${entrypoint}" resolves outside the worktree`
3283
3672
  };
3284
3673
  }
3674
+ if (allowedExtensions?.length && !allowedExtensions.some((ext) => entrypoint.endsWith(ext))) {
3675
+ return {
3676
+ ok: false,
3677
+ reason: `entrypoint "${entrypoint}" is not one of ${allowedExtensions.join(", ")}`
3678
+ };
3679
+ }
3285
3680
  return { ok: true, target };
3286
3681
  }
3287
- async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3288
- if (!INGEST_RUNTIMES.includes(runtime)) {
3682
+ async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
3683
+ const { ingest, profile, packageManager } = toolchain;
3684
+ if (ingest.kind !== "auto") {
3289
3685
  return {
3290
3686
  ran: false,
3291
3687
  ok: false,
3292
3688
  output: "",
3293
- reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
3689
+ reason: `${profile.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
3294
3690
  };
3295
3691
  }
3296
- const validated = validateIngestEntrypoint(worktreePath, entrypoint);
3692
+ const validated = validateIngestEntrypoint(
3693
+ worktreePath,
3694
+ entrypoint,
3695
+ ingest.entrypointExtensions
3696
+ );
3297
3697
  if (!validated.ok) {
3298
3698
  return { ran: false, ok: false, output: "", reason: validated.reason };
3299
3699
  }
@@ -3314,29 +3714,13 @@ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3314
3714
  reason: `entrypoint "${entrypoint}" does not exist`
3315
3715
  };
3316
3716
  }
3317
- return new Promise((resolveRun) => {
3318
- let output = "";
3319
- const child = spawn3(runtime, [entrypoint], {
3320
- cwd: worktreePath,
3321
- shell: false,
3322
- stdio: ["ignore", "pipe", "pipe"],
3323
- env: { ...process.env, ...env }
3324
- });
3325
- child.stdout?.on("data", (d) => output += d);
3326
- child.stderr?.on("data", (d) => output += d);
3327
- child.on(
3328
- "error",
3329
- (err) => resolveRun({
3330
- ran: true,
3331
- ok: false,
3332
- output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
3333
- })
3334
- );
3335
- child.on(
3336
- "close",
3337
- (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
3338
- );
3717
+ const argv = resolveIngestArgv(ingest, entrypoint);
3718
+ const { code, output } = await runCommand(argv[0], argv.slice(1), {
3719
+ cwd: worktreePath,
3720
+ env,
3721
+ timeoutMs: INGEST_TIMEOUT_MS
3339
3722
  });
3723
+ return { ran: true, ok: code === 0, output: output.trim() };
3340
3724
  }
3341
3725
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
3342
3726
  const trimmed = sourcePath.trim();
@@ -3351,8 +3735,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3351
3735
  } catch {
3352
3736
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3353
3737
  }
3354
- const relPath = join10(ingestDir, basename2(source));
3355
- const dest = join10(worktreePath, relPath);
3738
+ const relPath = join12(ingestDir, basename2(source));
3739
+ const dest = join12(worktreePath, relPath);
3356
3740
  try {
3357
3741
  await mkdir6(dirname7(dest), { recursive: true });
3358
3742
  await copyFile(source, dest);
@@ -3368,10 +3752,10 @@ function hasEnvVar(content, name) {
3368
3752
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3369
3753
  }
3370
3754
  async function writeSearchEnvValues(worktreePath, vars) {
3371
- const target = join10(worktreePath, ".env");
3755
+ const target = join12(worktreePath, ".env");
3372
3756
  let existing = "";
3373
3757
  try {
3374
- existing = await readFile8(target, "utf8");
3758
+ existing = await readFile9(target, "utf8");
3375
3759
  } catch (err) {
3376
3760
  if (err.code !== "ENOENT") throw err;
3377
3761
  }
@@ -3488,69 +3872,33 @@ async function resolveSearchOnlyKey(index) {
3488
3872
  }
3489
3873
 
3490
3874
  // src/lib/algoliaDocs.ts
3491
- import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3492
- import { dirname as dirname8, join as join11 } from "node:path";
3875
+ import { readFileSync, existsSync as existsSync5 } from "node:fs";
3876
+ import { dirname as dirname8, join as join13 } from "node:path";
3493
3877
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3494
- var DOCS_SUBPATH = join11("docs", "algolia-sdk");
3878
+ var DOCS_SUBPATH = join13("docs", "algolia-sdk");
3495
3879
  function findDocsDir() {
3496
3880
  let dir = dirname8(fileURLToPath2(import.meta.url));
3497
3881
  for (; ; ) {
3498
- const candidate = join11(dir, DOCS_SUBPATH);
3499
- if (existsSync2(candidate)) return candidate;
3882
+ const candidate = join13(dir, DOCS_SUBPATH);
3883
+ if (existsSync5(candidate)) return candidate;
3500
3884
  const parent = dirname8(dir);
3501
3885
  if (parent === dir) return void 0;
3502
3886
  dir = parent;
3503
3887
  }
3504
3888
  }
3505
- function loadAlgoliaDoc(language) {
3506
- const docsDir = findDocsDir();
3507
- if (!docsDir) {
3508
- logger.warn(
3509
- "algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
3510
- );
3511
- return "";
3512
- }
3513
- const files = readdirSync(docsDir).filter((f) => f.includes(language));
3514
- if (files.length === 0) {
3515
- logger.warn(
3516
- { language },
3517
- "algoliaDocs: no SDK reference found for language; skipping"
3518
- );
3519
- return "";
3520
- }
3521
- return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3522
- }
3523
- function getNamedDoc(name, language) {
3889
+ function getNamedDoc(name, key) {
3524
3890
  const docsDir = findDocsDir();
3525
3891
  if (!docsDir) {
3526
3892
  logger.warn("docs/algolia-sdk not found");
3527
3893
  return "";
3528
3894
  }
3529
- const file = join11(docsDir, `${name}-${language}.md`);
3530
- if (!existsSync2(file)) {
3531
- logger.warn({ name, language }, "named SDK reference not found");
3895
+ const file = join13(docsDir, `${name}-${key}.md`);
3896
+ if (!existsSync5(file)) {
3897
+ logger.warn({ name, key }, "named SDK reference not found");
3532
3898
  return "";
3533
3899
  }
3534
3900
  return readFileSync(file, "utf8").trim();
3535
3901
  }
3536
- function getFrameworkSpecificDoc(frameworks) {
3537
- const fw = frameworks.map((f) => f.toLowerCase());
3538
- if (fw.includes("vue") || fw.includes("nuxt")) {
3539
- return loadAlgoliaDoc("vue");
3540
- }
3541
- if (fw.includes("react") || fw.includes("next.js")) {
3542
- return loadAlgoliaDoc("react");
3543
- }
3544
- if (fw.includes("angular")) {
3545
- return loadAlgoliaDoc("angular");
3546
- }
3547
- return loadAlgoliaDoc("js");
3548
- }
3549
-
3550
- // src/lib/shell.ts
3551
- function shellQuote(value) {
3552
- return "'" + value.replace(/'/g, "'\\''") + "'";
3553
- }
3554
3902
 
3555
3903
  // src/actions/implement.ts
3556
3904
  var implementSchema = z24.object({
@@ -3585,12 +3933,11 @@ var implementSchema = z24.object({
3585
3933
  });
3586
3934
  var implementationOutputSchema = z24.object({
3587
3935
  summary: z24.string(),
3588
- // Ingestion only: how to run the generated script, as a structured pair the
3589
- // wizard turns into an argv (`<runtime> <entrypoint>`) never a free-form
3590
- // command string. `runtime` is constrained to an allowlisted interpreter and
3591
- // `entrypoint` is validated to a worktree-relative path before execution, so
3592
- // the agent cannot inject extra commands or swap the interpreter.
3593
- runtime: z24.enum(INGEST_RUNTIMES).optional(),
3936
+ // Ingestion only: the script the wizard should run, as a bare path — never a
3937
+ // command string, and never the interpreter. The command comes from the
3938
+ // resolved language toolchain (a registry constant); this path is validated to
3939
+ // a worktree-relative file with a runnable extension and substituted into it.
3940
+ // So the agent contributes no part of the command that gets executed.
3594
3941
  entrypoint: z24.string().optional()
3595
3942
  });
3596
3943
  var verificationOutputSchema = z24.object({
@@ -3600,47 +3947,11 @@ var verificationOutputSchema = z24.object({
3600
3947
  });
3601
3948
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3602
3949
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
3603
- var INGEST_DIR = ".algolia-wizard";
3604
- function detectUiFramework(language) {
3605
- const names = language.frameworks.map((f) => f.name.toLowerCase());
3606
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3607
- if (names.some((n) => n.includes("react") || n.includes("next")))
3608
- return "React";
3609
- if (names.some((n) => n.includes("angular"))) return "Angular";
3610
- return "JavaScript";
3611
- }
3612
- function frameworksForDoc(framework) {
3613
- switch (framework) {
3614
- case "React":
3615
- return ["react"];
3616
- case "Vue":
3617
- return ["vue"];
3618
- case "Angular":
3619
- return ["angular"];
3620
- case "JavaScript":
3621
- return [];
3622
- }
3623
- }
3624
- function publicEnvPrefix(language) {
3625
- const frameworkNames = language.frameworks.map(
3626
- (framework) => framework.name.toLowerCase()
3950
+ function buildSearchEnvVars(language, strategy, appId, searchKey) {
3951
+ const prefix = publicEnvPrefix(
3952
+ language.frameworks.map((framework) => framework.name),
3953
+ strategy
3627
3954
  );
3628
- if (frameworkNames.some((name) => name.includes("next"))) {
3629
- return "NEXT_PUBLIC_";
3630
- }
3631
- if (frameworkNames.some((name) => name.includes("nuxt"))) {
3632
- return "NUXT_PUBLIC_";
3633
- }
3634
- if (frameworkNames.some((name) => name.includes("astro"))) {
3635
- return "PUBLIC_";
3636
- }
3637
- if (frameworkNames.some((name) => name.includes("vite"))) {
3638
- return "VITE_";
3639
- }
3640
- return "PUBLIC_";
3641
- }
3642
- function searchEnvVars(language, appId, searchKey) {
3643
- const prefix = publicEnvPrefix(language);
3644
3955
  return [
3645
3956
  {
3646
3957
  name: `${prefix}ALGOLIA_APP_ID`,
@@ -3652,6 +3963,38 @@ function searchEnvVars(language, appId, searchKey) {
3652
3963
  }
3653
3964
  ];
3654
3965
  }
3966
+ async function resolveIngestionProfile(ctx, language, repoRoot) {
3967
+ const { candidates, confirmed: confirmed3, onDisk } = await pickIngestionCandidates(
3968
+ repoRoot,
3969
+ language.languages.map((l) => l.name)
3970
+ );
3971
+ if (candidates.length === 0) {
3972
+ const chosen = confirmed3[0] ?? onDisk[0] ?? LANGUAGE_PROFILES[DEFAULT_LANGUAGE_ID];
3973
+ logger.warn(
3974
+ {
3975
+ confirmed: language.languages.map((l) => l.name),
3976
+ onDisk: onDisk.map((p) => p.id),
3977
+ chosen: chosen.id
3978
+ },
3979
+ "implement: no confirmed language matched a manifest on disk; falling back"
3980
+ );
3981
+ return chosen;
3982
+ }
3983
+ if (candidates.length === 1) return candidates[0];
3984
+ const backends = candidates.filter(isBackendLanguage);
3985
+ if (backends.length === 1) return backends[0];
3986
+ if (backends.length === 0) return candidates[0];
3987
+ if (isBackendLanguage(candidates[0])) return candidates[0];
3988
+ const options = backends.map((p) => p.displayName);
3989
+ const selection = await ctx.requestUserInput({
3990
+ prompt: "Which language should the ingestion script use?",
3991
+ promptType: "multipleChoice",
3992
+ options,
3993
+ defaultSelectedIndex: 0
3994
+ });
3995
+ const picked = typeof selection === "string" ? backends.find((p) => p.displayName === selection) : void 0;
3996
+ return picked ?? backends[0];
3997
+ }
3655
3998
  function baseInstructions(input) {
3656
3999
  return [
3657
4000
  `Target Algolia index: ${input.targetIndex}`,
@@ -3679,37 +4022,48 @@ function sourceSpecificInstructions(input) {
3679
4022
  generated: [
3680
4023
  "No real data source exists; use sample records for each confirmed entity.",
3681
4024
  "Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
3682
- "In the script, read and parse each returned file path at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) instead of inlining the records as literals.",
4025
+ "In the script, read and parse each returned JSON file path at runtime using the idiomatic file read for the language you are writing in (the SDK reference above shows one) instead of inlining the records as literals.",
3683
4026
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
3684
4027
  ]
3685
4028
  };
3686
4029
  return byLine[input.ingestionSource];
3687
4030
  }
3688
4031
  function ingestionInstructions(input) {
4032
+ const { ingestionProfile: profile, toolchain } = input;
4033
+ const { ingest } = toolchain;
4034
+ const extensions = ingest.entrypointExtensions.join(", ");
4035
+ const runInstruction = ingest.kind === "auto" ? `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard runs it with \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so it must be a plain path with no flags or arguments and must run as-is under that command.` : `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard does NOT run ${profile.displayName} ${toolchain.packageManager.id} projects itself \u2014 it tells the developer to run \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so also add whatever build configuration that command needs${ingest.kind === "manual" && ingest.requiresBuildTask ? `, including a "${ingest.requiresBuildTask}" task in "${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"}" that runs the script` : ""}.`;
3689
4036
  return [
3690
4037
  ...input.confirmed && input.confirmed.length ? [
3691
- `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
4038
+ `Write the ingestion script in ${profile.displayName}, at "${ingestScriptDir(profile)}/" in the repo.`,
3692
4039
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3693
- `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
3694
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4040
+ `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. ${profile.envReadInstruction} The wizard sets these when it runs the script.`,
4041
+ `Use the official Algolia ${profile.displayName} client (${profile.sdk.packageName}). Do not use the raw HTTP API, and do not use a client for another language.`,
3695
4042
  "After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
3696
- getNamedDoc("save-records", "js"),
3697
- 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4043
+ getNamedDoc("save-records", profile.sdk.docKey),
4044
+ dependencyInstruction(toolchain),
3698
4045
  "The summary should be extremely concise.",
3699
- `Return how to run the script as two fields, not a command string: "runtime" (one of ${INGEST_RUNTIMES.join(", ")}) and "entrypoint" (the script path relative to the worktree root, e.g. "${input.ingestDir}/ingest.mjs"). The wizard runs \`<runtime> <entrypoint>\` directly, so the entrypoint must be a plain path with no flags or arguments. Write a script one of those interpreters can run as-is.`,
4046
+ runInstruction,
3700
4047
  ...sourceSpecificInstructions(input)
3701
4048
  ] : []
3702
4049
  ];
3703
4050
  }
3704
4051
  function searchInstructions(input) {
3705
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4052
+ const doc = getNamedDoc(
4053
+ "instantsearch-setup",
4054
+ searchDocKey(input.searchStrategy)
4055
+ );
4056
+ const isTemplate = input.searchStrategy === "cdn-template";
4057
+ const placement = isTemplate ? input.searchLocation ? `Add the search UI to the server-rendered template at "${input.searchLocation}" \u2014 ideally a shared layout, so it is reachable across the app.` : `This project has no shared template to host the UI, so create a standalone page at "${input.ingestDir}/search-demo.html" the developer can open directly, and add a TODO explaining how to move the snippet into their own layout.` : `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app.`;
3706
4058
  return [
3707
4059
  "Implement an in-app Algolia search experience.",
3708
- `Build the search UI for ${input.uiFramework}.`,
3709
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4060
+ `Build the search UI for ${describeSearchTarget(input.searchStrategy, input.frameworkName)}.`,
4061
+ "Follow the Algolia reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3710
4062
  doc,
3711
- `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
3712
- "Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
4063
+ placement,
4064
+ `It needs at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
4065
+ isTemplate ? "Load InstantSearch from a CDN with script tags as shown in the reference. Do not add JavaScript package dependencies, a bundler, or a build step." : 'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
4066
+ isTemplate ? "Read the App ID and search-only API key from server-side configuration/environment and render them into the page (e.g. as data- attributes the script reads); never hardcode them, and never put a write/admin key in HTML." : "Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
3713
4067
  // appId always resolves (loadActiveProfile throws otherwise); only the
3714
4068
  // search-only key is best-effort and can fall back to a placeholder.
3715
4069
  `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
@@ -3717,20 +4071,22 @@ function searchInstructions(input) {
3717
4071
  // resolved app id / search-only key into ".env" under these exact names
3718
4072
  // right after this step, so a renamed prefix here would leave the code
3719
4073
  // reading a var the wizard never wrote.
3720
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3721
- 'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
4074
+ `Use exactly these env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3722
4075
  "The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
3723
4076
  ];
3724
4077
  }
3725
4078
  function verificationInstructions(input) {
4079
+ const protectedDirs = [
4080
+ .../* @__PURE__ */ new Set([input.ingestDir, ingestScriptDir(input.ingestionProfile)])
4081
+ ];
3726
4082
  return [
3727
4083
  "Verify the Algolia implementation changes in the current worktree.",
3728
4084
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
3729
- "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
4085
+ `Call verifyImplementation at least once; it runs the mechanical checks available for this repo's languages (${input.verificationLanguages.join(", ")}) and returns per-check results plus an aggregate ok.`,
3730
4086
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
3731
4087
  "Do not make speculative fixes when verifyImplementation cannot run, no checks exist, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
3732
4088
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
3733
- `Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
4089
+ `Do not modify ${protectedDirs.map((dir) => `"${dir}/"`).join(" or ")} unless verifyImplementation reports an actionable issue in its files.`,
3734
4090
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
3735
4091
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
3736
4092
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -3739,14 +4095,17 @@ function verificationInstructions(input) {
3739
4095
  var IMPLEMENT_CONFIG = {
3740
4096
  ingestion: {
3741
4097
  title: "Algolia ingestion",
4098
+ label: "Ingestion",
3742
4099
  buildInstructions: ingestionInstructions
3743
4100
  },
3744
4101
  search: {
3745
4102
  title: "Algolia search",
4103
+ label: "Search",
3746
4104
  buildInstructions: searchInstructions
3747
4105
  },
3748
4106
  verification: {
3749
4107
  title: "Algolia verification",
4108
+ label: "Verification",
3750
4109
  buildInstructions: verificationInstructions
3751
4110
  }
3752
4111
  };
@@ -3778,11 +4137,10 @@ function buildAgentInstructions(useCase, input, extraInstructions = []) {
3778
4137
  ];
3779
4138
  }
3780
4139
  function formatSummary(useCase, summary) {
3781
- const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3782
- return `${label}: ${summary}`;
4140
+ return `${IMPLEMENT_CONFIG[useCase].label}: ${summary}`;
3783
4141
  }
3784
- function buildIngestCommand(worktree, runtime, entrypoint) {
3785
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4142
+ function buildIngestCommand(worktree, toolchain, entrypoint) {
4143
+ return `cd ${shellQuote(worktree)} && ${describeIngestCommand(toolchain.ingest, entrypoint)}`;
3786
4144
  }
3787
4145
  function parseIngestRecordCount(output) {
3788
4146
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
@@ -3864,7 +4222,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3864
4222
  await confirmDirtyWorkingTree(ctx, repoRoot);
3865
4223
  }
3866
4224
  const normalized = normalizeFindingPaths(findings);
3867
- const confirmed2 = normalized.confirmedEntities;
4225
+ const confirmed3 = normalized.confirmedEntities;
3868
4226
  const searchLocation = normalized.searchImplementationAnalysis;
3869
4227
  let appId;
3870
4228
  let searchKey;
@@ -3902,31 +4260,66 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3902
4260
  );
3903
4261
  }
3904
4262
  }
4263
+ const ingestionProfile = await resolveIngestionProfile(
4264
+ ctx,
4265
+ language,
4266
+ worktree
4267
+ );
4268
+ const toolchain = await resolveToolchain(worktree, ingestionProfile);
4269
+ const verificationLanguages = [
4270
+ .../* @__PURE__ */ new Set([
4271
+ ingestionProfile.id,
4272
+ ...(await detectProfilesFromManifests(worktree)).map((p) => p.id)
4273
+ ])
4274
+ ];
4275
+ const frameworkName = language.frameworks[0]?.name;
4276
+ const searchStrategy = resolveSearchStrategy(
4277
+ frameworkName,
4278
+ verificationLanguages.includes(JAVASCRIPT)
4279
+ );
4280
+ logger.info(
4281
+ {
4282
+ language: ingestionProfile.id,
4283
+ packageManager: toolchain.packageManager.id,
4284
+ ingest: toolchain.ingest.kind,
4285
+ framework: frameworkName,
4286
+ searchStrategy
4287
+ },
4288
+ "implement: resolved ingestion toolchain and search strategy"
4289
+ );
3905
4290
  const input = {
3906
4291
  findings: normalized,
3907
- confirmed: confirmed2,
4292
+ confirmed: confirmed3,
3908
4293
  searchLocation,
3909
4294
  targetIndex,
3910
4295
  language,
3911
4296
  appId,
3912
4297
  searchKey,
3913
- searchEnvVars: searchEnvVars(language, appId, searchKey),
4298
+ searchEnvVars: buildSearchEnvVars(
4299
+ language,
4300
+ searchStrategy,
4301
+ appId,
4302
+ searchKey
4303
+ ),
3914
4304
  ingestDir: INGEST_DIR,
3915
4305
  ingestionSource,
3916
4306
  uploadFilePath,
3917
- // language.frameworks already prefers the confirm-framework step output,
3918
- // so the user's confirmed stack (not just raw detection) picks the flavor.
3919
- uiFramework: detectUiFramework(language)
4307
+ searchStrategy,
4308
+ frameworkName,
4309
+ ingestionProfile,
4310
+ toolchain,
4311
+ verificationLanguages
3920
4312
  };
4313
+ const searchToolchain = !bundlesJavaScript(searchStrategy) ? void 0 : ingestionProfile.id === JAVASCRIPT ? toolchain : await resolveToolchain(worktree, LANGUAGE_PROFILES[JAVASCRIPT]);
4314
+ const toolchainForUseCase = (useCase) => useCase === "search" ? searchToolchain : toolchain;
3921
4315
  const summaries = [];
3922
4316
  if (uploadWarning) summaries.push(uploadWarning);
3923
4317
  let agentRuns = 0;
3924
- let ingestRuntime;
3925
4318
  let ingestEntrypoint;
3926
4319
  let ingestScriptRan = false;
3927
4320
  let ingestRecordCount;
3928
4321
  let ingestDurationMs;
3929
- let installFailed = false;
4322
+ const failedInstalls = /* @__PURE__ */ new Set();
3930
4323
  let ingestOutcomeMessage;
3931
4324
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
3932
4325
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -3940,16 +4333,19 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3940
4333
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
3941
4334
  outputSchema: implementationOutputSchema
3942
4335
  });
4336
+ const useCaseToolchain = toolchainForUseCase(currentUseCase);
4337
+ if (!useCaseToolchain) return result;
3943
4338
  ctx.notify({
3944
4339
  messages: [`Installing dependencies for ${currentUseCase}\u2026`]
3945
4340
  });
3946
4341
  const installLogId = ctx.logStart("installWorktreeDeps", {
3947
- useCase: currentUseCase
4342
+ useCase: currentUseCase,
4343
+ language: useCaseToolchain.profile.id
3948
4344
  });
3949
- const install = await installWorktreeDeps(worktree);
4345
+ const install = await installWorktreeDeps(worktree, useCaseToolchain);
3950
4346
  ctx.logEnd(installLogId, install.ok ? "success" : "error");
3951
4347
  if (!install.ok) {
3952
- installFailed = true;
4348
+ failedInstalls.add(useCaseToolchain.profile.displayName);
3953
4349
  logger.warn(
3954
4350
  { useCase: currentUseCase, output: install.output },
3955
4351
  "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
@@ -3963,15 +4359,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3963
4359
  return runAgent({
3964
4360
  instructions: buildAgentInstructions("verification", input),
3965
4361
  tools: toolsForUseCase("verification"),
3966
- outputSchema: verificationOutputSchema
4362
+ outputSchema: verificationOutputSchema,
4363
+ // So verifyImplementation runs this repo's checks, not just npm scripts.
4364
+ languages: input.verificationLanguages
3967
4365
  });
3968
4366
  }
3969
4367
  if (useCases.includes("ingestion")) {
3970
- const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4368
+ const { summary, entrypoint } = await runImplementationUseCase("ingestion");
3971
4369
  summaries.push(formatSummary("ingestion", summary));
3972
- ingestRuntime = runtime;
3973
4370
  ingestEntrypoint = entrypoint;
3974
- if (ingestRuntime && ingestEntrypoint && !installFailed) {
4371
+ if (ingestEntrypoint && toolchain.ingest.kind === "auto" && failedInstalls.size === 0) {
3975
4372
  ctx.clearNotices();
3976
4373
  const runNow = await ctx.requestUserInput({
3977
4374
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
@@ -3983,13 +4380,13 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3983
4380
  const profile = await loadActiveProfile();
3984
4381
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
3985
4382
  const scriptLogId = ctx.logStart("runIngestScript", {
3986
- runtime: ingestRuntime,
4383
+ language: ingestionProfile.id,
3987
4384
  entrypoint: ingestEntrypoint
3988
4385
  });
3989
4386
  const startedAt = Date.now();
3990
4387
  const run2 = await runIngestScript(
3991
4388
  worktree,
3992
- ingestRuntime,
4389
+ toolchain,
3993
4390
  ingestEntrypoint,
3994
4391
  {
3995
4392
  [APP_ID_VAR]: profile.appId,
@@ -4003,7 +4400,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4003
4400
  ingestRecordCount = parseIngestRecordCount(run2.output);
4004
4401
  if (ingestRecordCount != null) {
4005
4402
  track("AI Wizard Ingest Successful", {
4006
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4403
+ entity_name: confirmed3?.map((e) => e.name).join(", ") || "unknown",
4007
4404
  record_count: ingestRecordCount,
4008
4405
  duration_ms: ingestDurationMs
4009
4406
  });
@@ -4016,7 +4413,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4016
4413
  outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
4017
4414
  logger.warn(
4018
4415
  {
4019
- runtime: ingestRuntime,
4416
+ language: ingestionProfile.id,
4020
4417
  entrypoint: ingestEntrypoint,
4021
4418
  reason: run2.reason
4022
4419
  },
@@ -4039,7 +4436,7 @@ ${run2.output}` : status;
4039
4436
  outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
4040
4437
  logger.warn(
4041
4438
  {
4042
- runtime: ingestRuntime,
4439
+ language: ingestionProfile.id,
4043
4440
  entrypoint: ingestEntrypoint,
4044
4441
  output: run2.output
4045
4442
  },
@@ -4056,10 +4453,28 @@ ${run2.output}` : status;
4056
4453
  }
4057
4454
  }
4058
4455
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4059
- if (ingestRuntime && ingestEntrypoint) {
4456
+ if (ingestEntrypoint) {
4060
4457
  commandMessages.push(
4061
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4458
+ `Ingestion command: ${buildIngestCommand(worktree, toolchain, ingestEntrypoint)}`
4062
4459
  );
4460
+ if (toolchain.ingest.kind === "manual") {
4461
+ commandMessages.push(
4462
+ `The wizard does not run ${ingestionProfile.displayName} ${toolchain.packageManager.id} projects \u2014 run the command above yourself to ingest.`
4463
+ );
4464
+ const missingTask = await missingBuildTask(worktree, toolchain);
4465
+ if (missingTask) {
4466
+ const warning = `\u26A0\uFE0F The command above needs a "${missingTask}" task, which is not in ${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"} \u2014 add it before running, or run the script through your IDE instead.`;
4467
+ commandMessages.push(warning);
4468
+ summaries.push(warning);
4469
+ }
4470
+ }
4471
+ }
4472
+ if (ingestionSource === "local") {
4473
+ const limitation = localSourceLimitation(worktree, ingestionProfile);
4474
+ if (limitation) {
4475
+ commandMessages.push(`\u26A0\uFE0F ${limitation}`);
4476
+ summaries.push(`\u26A0\uFE0F ${limitation}`);
4477
+ }
4063
4478
  }
4064
4479
  await ctx.requestUserInput({
4065
4480
  // No question being asked here, just an acknowledgement — the
@@ -4070,7 +4485,20 @@ ${run2.output}` : status;
4070
4485
  messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4071
4486
  });
4072
4487
  }
4073
- if (useCases.includes("search")) {
4488
+ const skipSearch = useCases.includes("search") && !canScaffoldSearchUI(input.searchStrategy);
4489
+ if (skipSearch) {
4490
+ const target = describeSearchTarget(
4491
+ input.searchStrategy,
4492
+ input.frameworkName
4493
+ );
4494
+ summaries.push(
4495
+ `Search UI skipped: the wizard can't scaffold a native search UI for ${target}. Your records are in the "${targetIndex}" index \u2014 build the UI with Algolia's mobile InstantSearch libraries (https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/ios/ for iOS, .../android for Android).`
4496
+ );
4497
+ track("AI Wizard Search UI Skipped", {
4498
+ framework: input.frameworkName ?? "unknown"
4499
+ });
4500
+ }
4501
+ if (useCases.includes("search") && !skipSearch) {
4074
4502
  let extraInstructions = [];
4075
4503
  const preSearchFiles = new Set(await listChangedFiles(worktree));
4076
4504
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
@@ -4141,9 +4569,9 @@ ${run2.output}` : status;
4141
4569
  "implement: agent reported success but no files changed in the worktree"
4142
4570
  );
4143
4571
  }
4144
- if (installFailed) {
4572
+ if (failedInstalls.size > 0) {
4145
4573
  summaries.push(
4146
- '\u26A0\uFE0F Dependency install in the worktree failed. Run your package manager install in the worktree before the command below, or it will fail with "Cannot find module".'
4574
+ `\u26A0\uFE0F Dependency install in the worktree failed. Install the ${[...failedInstalls].join(" and ")} dependencies in the worktree before the command below, or it will fail on a missing package.`
4147
4575
  );
4148
4576
  }
4149
4577
  return {
@@ -4151,10 +4579,10 @@ ${run2.output}` : status;
4151
4579
  filesChanged,
4152
4580
  summary: summaries.join("\n\n"),
4153
4581
  worktreePath: worktree,
4154
- ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
4582
+ ...useCases.includes("ingestion") && ingestEntrypoint ? {
4155
4583
  ingestCommand: buildIngestCommand(
4156
4584
  worktree,
4157
- ingestRuntime,
4585
+ toolchain,
4158
4586
  ingestEntrypoint
4159
4587
  ),
4160
4588
  ingestScriptRan,
@@ -4483,20 +4911,20 @@ function parseCliArgs(argv) {
4483
4911
  }
4484
4912
 
4485
4913
  // src/lib/resetState.ts
4486
- import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4487
- import { join as join12 } from "node:path";
4914
+ import { readdir as readdir5, rm as rm2 } from "node:fs/promises";
4915
+ import { join as join14 } from "node:path";
4488
4916
  var KEEP = ["wizard.log"];
4489
4917
  async function resetProjectState() {
4490
4918
  const dir = stateDir();
4491
4919
  let entries;
4492
4920
  try {
4493
- entries = await readdir4(dir);
4921
+ entries = await readdir5(dir);
4494
4922
  } catch {
4495
4923
  return { dir, removed: [] };
4496
4924
  }
4497
4925
  const targets = entries.filter((name) => !KEEP.includes(name));
4498
4926
  await Promise.all(
4499
- targets.map((name) => rm2(join12(dir, name), { recursive: true, force: true }))
4927
+ targets.map((name) => rm2(join14(dir, name), { recursive: true, force: true }))
4500
4928
  );
4501
4929
  return { dir, removed: targets };
4502
4930
  }