@algolia/wizard 0.9.0-rc.85.78 → 0.9.0-rc.87.83

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 Box15, Text as Text15, useApp, useInput as useInput6, useWindowSize as useWindowSize8 } from "ink";
7
+ import { Box as Box16, Text as Text16, useApp, useInput as useInput7, useWindowSize as useWindowSize8 } from "ink";
8
8
 
9
9
  // src/core/store.ts
10
10
  import { create } from "zustand";
@@ -13,6 +13,47 @@ import { nanoid } from "nanoid";
13
13
  // src/lib/algoliaCli.ts
14
14
  import { spawn } from "node:child_process";
15
15
  import { z } from "zod";
16
+
17
+ // src/lib/logger.ts
18
+ import pino from "pino";
19
+ import { join as join2, dirname } from "node:path";
20
+ import { devNull } from "node:os";
21
+ import { mkdirSync, openSync, closeSync } from "node:fs";
22
+
23
+ // src/core/constants.ts
24
+ import { homedir } from "node:os";
25
+ import { join, resolve } from "node:path";
26
+ function rootDir() {
27
+ return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
28
+ }
29
+ function projectSlug(cwd = process.cwd()) {
30
+ return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
31
+ }
32
+ function stateDir(cwd = process.cwd()) {
33
+ return join(rootDir(), projectSlug(cwd));
34
+ }
35
+
36
+ // src/lib/logger.ts
37
+ var STDERR_FD = 2;
38
+ function resolveDest() {
39
+ const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
40
+ try {
41
+ mkdirSync(dirname(target), { recursive: true });
42
+ closeSync(openSync(target, "a"));
43
+ return target;
44
+ } catch {
45
+ return STDERR_FD;
46
+ }
47
+ }
48
+ function logDestination() {
49
+ return pino.destination({ dest: resolveDest(), sync: false });
50
+ }
51
+ var logger = pino(
52
+ { level: process.env.LOG_LEVEL ?? "info" },
53
+ logDestination()
54
+ );
55
+
56
+ // src/lib/algoliaCli.ts
16
57
  function npxArgs(args) {
17
58
  return ["--yes", "@algolia/cli@latest", ...args];
18
59
  }
@@ -89,6 +130,7 @@ function runAlgoliaCli(args, { onOutput } = {}) {
89
130
  },
90
131
  (err) => {
91
132
  useWizard.getState().logEnd(logId, "error");
133
+ logger.warn({ err, args }, "Algolia CLI command failed");
92
134
  throw err;
93
135
  }
94
136
  );
@@ -146,45 +188,6 @@ function refreshAuthToken() {
146
188
  return inFlightRefresh;
147
189
  }
148
190
 
149
- // src/lib/logger.ts
150
- import pino from "pino";
151
- import { join as join2, dirname } from "node:path";
152
- import { devNull } from "node:os";
153
- import { mkdirSync, openSync, closeSync } from "node:fs";
154
-
155
- // src/core/constants.ts
156
- import { homedir } from "node:os";
157
- import { join, resolve } from "node:path";
158
- function rootDir() {
159
- return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
160
- }
161
- function projectSlug(cwd = process.cwd()) {
162
- return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
163
- }
164
- function stateDir(cwd = process.cwd()) {
165
- return join(rootDir(), projectSlug(cwd));
166
- }
167
-
168
- // src/lib/logger.ts
169
- var STDERR_FD = 2;
170
- function resolveDest() {
171
- const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
172
- try {
173
- mkdirSync(dirname(target), { recursive: true });
174
- closeSync(openSync(target, "a"));
175
- return target;
176
- } catch {
177
- return STDERR_FD;
178
- }
179
- }
180
- function logDestination() {
181
- return pino.destination({ dest: resolveDest(), sync: false });
182
- }
183
- var logger = pino(
184
- { level: process.env.LOG_LEVEL ?? "info" },
185
- logDestination()
186
- );
187
-
188
191
  // src/lib/proxyFetch.ts
189
192
  var PROXY_BASE_URL = process.env.PROXY_BASE_URL ?? "https://proxy-624203421261.us-east4.run.app";
190
193
  var PROXY_AUTH_REJECTED_HEADER = "x-wizard-proxy-auth";
@@ -614,10 +617,13 @@ function Notices() {
614
617
  }
615
618
 
616
619
  // src/ui/PromptInput.tsx
617
- import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
620
+ import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
618
621
  import TextInput from "ink-text-input";
619
622
  import { useState as useState5 } from "react";
620
623
 
624
+ // src/ui/CommandApproval.tsx
625
+ import { Box as Box5, Text as Text5, useInput } from "ink";
626
+
621
627
  // src/ui/NextAction.tsx
622
628
  import { Box as Box4, Text as Text4 } from "ink";
623
629
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -641,14 +647,69 @@ function NextAction({
641
647
  ] });
642
648
  }
643
649
 
650
+ // src/ui/CommandApproval.tsx
651
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
652
+ function CommandApproval({
653
+ command,
654
+ onDecide
655
+ }) {
656
+ useInput((input, key) => {
657
+ if (key.return) onDecide("approve");
658
+ else if (key.escape) onDecide("reject");
659
+ else if (input.toLowerCase() === "a") onDecide("always");
660
+ });
661
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
662
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, bold: true, children: "Run this command?" }),
663
+ /* @__PURE__ */ jsxs4(
664
+ Box5,
665
+ {
666
+ flexDirection: "column",
667
+ paddingLeft: 2,
668
+ borderStyle: "single",
669
+ borderColor: COLORS.success,
670
+ borderTop: false,
671
+ borderBottom: false,
672
+ borderRight: false,
673
+ gap: 1,
674
+ children: [
675
+ /* @__PURE__ */ jsxs4(Box5, { children: [
676
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "$ " }),
677
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.strong, wrap: "wrap", children: command.command })
678
+ ] }),
679
+ /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
680
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "in:" }),
681
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, wrap: "wrap", children: command.cwd })
682
+ ] }),
683
+ command.explanation && /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
684
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "why:" }),
685
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.accent, wrap: "wrap", children: command.explanation })
686
+ ] })
687
+ ]
688
+ }
689
+ ),
690
+ /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
691
+ /* @__PURE__ */ jsx4(NextAction, { action: "approve", keyHint: "enter" }),
692
+ /* @__PURE__ */ jsx4(NextAction, { action: "reject", keyHint: "esc", hierarchy: "secondary" }),
693
+ /* @__PURE__ */ jsx4(
694
+ NextAction,
695
+ {
696
+ action: "approve, and don't ask again for this command",
697
+ keyHint: "a",
698
+ hierarchy: "secondary"
699
+ }
700
+ )
701
+ ] })
702
+ ] });
703
+ }
704
+
644
705
  // src/ui/SelectPrompt.tsx
645
- import { Box as Box6, Text as Text6, useInput, useWindowSize as useWindowSize5 } from "ink";
706
+ import { Box as Box7, Text as Text7, useInput as useInput2, useWindowSize as useWindowSize5 } from "ink";
646
707
  import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
647
708
 
648
709
  // src/ui/ScrollView.tsx
649
- import { Box as Box5, Text as Text5, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
710
+ import { Box as Box6, Text as Text6, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
650
711
  import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
651
- import { jsxs as jsxs4 } from "react/jsx-runtime";
712
+ import { jsxs as jsxs5 } from "react/jsx-runtime";
652
713
  var INDICATOR_ROWS = 2;
653
714
  function fittedWidth(node, columns) {
654
715
  let left = 0;
@@ -718,14 +779,14 @@ function useScrollWindow({
718
779
  };
719
780
  }
720
781
  function ScrollView({ scroll, children }) {
721
- return /* @__PURE__ */ jsxs4(Box5, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
722
- scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
782
+ return /* @__PURE__ */ jsxs5(Box6, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
783
+ scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
723
784
  "\u2191 ",
724
785
  scroll.hiddenAbove,
725
786
  " more"
726
787
  ] }),
727
788
  children,
728
- scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
789
+ scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
729
790
  "\u2193 ",
730
791
  scroll.hiddenBelow,
731
792
  " more"
@@ -734,7 +795,7 @@ function ScrollView({ scroll, children }) {
734
795
  }
735
796
 
736
797
  // src/ui/SelectPrompt.tsx
737
- import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
798
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
738
799
  var CANCEL = "cancel";
739
800
  var ARROW_WIDTH = 4;
740
801
  var COLUMN_GAP = 2;
@@ -796,7 +857,7 @@ function SelectPrompt({
796
857
  revealIndex(index);
797
858
  }, [index, revealIndex]);
798
859
  const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
799
- useInput((input, key) => {
860
+ useInput2((input, key) => {
800
861
  if (rows.length === 0) return;
801
862
  if (key.upArrow || input === "k") {
802
863
  setIndex((i) => (i - 1 + rows.length) % rows.length);
@@ -819,56 +880,56 @@ function SelectPrompt({
819
880
  }
820
881
  }
821
882
  });
822
- return /* @__PURE__ */ jsx4(Box6, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, width, children: [
823
- /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
824
- error && /* @__PURE__ */ jsx4(Text6, { color: COLORS.danger, children: error }),
825
- messages?.map((m, i) => /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
826
- table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
827
- /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
828
- question && /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: question }),
829
- helpText && /* @__PURE__ */ jsx4(Text6, { color: COLORS.dim, children: helpText })
883
+ return /* @__PURE__ */ jsx5(Box7, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, width, children: [
884
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
885
+ error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: error }),
886
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
887
+ table && /* @__PURE__ */ jsx5(Table, { columns: table.columns, rows: table.rows }),
888
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
889
+ question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: question }),
890
+ helpText && /* @__PURE__ */ jsx5(Text7, { color: COLORS.dim, children: helpText })
830
891
  ] })
831
892
  ] }),
832
- /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
893
+ /* @__PURE__ */ jsx5(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
833
894
  const i = scroll.offset + visibleIndex;
834
895
  const highlighted = i === index;
835
896
  const isCancel = i === cancelIndex;
836
897
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
837
898
  const sec = isCancel ? void 0 : secondary?.[i];
838
899
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
839
- const label = /* @__PURE__ */ jsxs5(Text6, { color: labelColor, wrap: "truncate", children: [
900
+ const label = /* @__PURE__ */ jsxs6(Text7, { color: labelColor, wrap: "truncate", children: [
840
901
  highlighted ? "\u276F " : " ",
841
902
  bullet,
842
903
  option
843
904
  ] });
844
905
  const isText = sec?.kind === "text";
845
- return /* @__PURE__ */ jsxs5(
846
- Box6,
906
+ return /* @__PURE__ */ jsxs6(
907
+ Box7,
847
908
  {
848
909
  width: isText ? "100%" : barWidth,
849
910
  paddingX: 1,
850
911
  paddingY: 1,
851
912
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
852
913
  children: [
853
- /* @__PURE__ */ jsx4(Box6, { width: isText ? labelWidth : barLabelWidth, children: label }),
854
- isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box6, { width: textWidth, children: /* @__PURE__ */ jsx4(
855
- Text6,
914
+ /* @__PURE__ */ jsx5(Box7, { width: isText ? labelWidth : barLabelWidth, children: label }),
915
+ isText && textWidth > 0 && /* @__PURE__ */ jsx5(Box7, { width: textWidth, children: /* @__PURE__ */ jsx5(
916
+ Text7,
856
917
  {
857
918
  wrap: "truncate",
858
919
  color: highlighted ? COLORS.primary : COLORS.muted,
859
920
  children: sec.value
860
921
  }
861
922
  ) }),
862
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box6, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text6, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
923
+ sec?.kind === "badge" && /* @__PURE__ */ jsx5(Box7, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text7, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
863
924
  ]
864
925
  },
865
926
  `row-${i}`
866
927
  );
867
928
  }) }),
868
- /* @__PURE__ */ jsx4(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text6, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs5(Text6, { children: [
929
+ /* @__PURE__ */ jsx5(Box7, { flexShrink: 0, children: /* @__PURE__ */ jsx5(Text7, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs6(Text7, { children: [
869
930
  i > 0 ? " " : "",
870
- /* @__PURE__ */ jsx4(Text6, { color: COLORS.primary, children: key }),
871
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
931
+ /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: key }),
932
+ /* @__PURE__ */ jsxs6(Text7, { color: COLORS.dim, children: [
872
933
  " ",
873
934
  label
874
935
  ] })
@@ -877,23 +938,23 @@ function SelectPrompt({
877
938
  }
878
939
 
879
940
  // src/ui/PromptInput.tsx
880
- import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
941
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
881
942
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
882
943
  function EnterToContinuePrompt({
883
944
  question,
884
945
  messages,
885
946
  onDecide
886
947
  }) {
887
- useInput2((_input, key) => {
948
+ useInput3((_input, key) => {
888
949
  if (key.return) onDecide(true);
889
950
  else if (key.escape) onDecide(false);
890
951
  });
891
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, children: [
892
- messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
893
- question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: question }),
894
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
895
- /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
896
- /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
952
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
953
+ messages?.map((m, i) => /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: m }, `msg-${i}`)),
954
+ question && /* @__PURE__ */ jsx6(Text8, { color: COLORS.primary, children: question }),
955
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
956
+ /* @__PURE__ */ jsx6(NextAction, { action: "continue", keyHint: "enter" }),
957
+ /* @__PURE__ */ jsx6(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
897
958
  ] })
898
959
  ] });
899
960
  }
@@ -901,11 +962,11 @@ function PromptInput() {
901
962
  const { phase, inputReq, submitInput } = useWizard();
902
963
  const [draft, setDraft] = useState5("");
903
964
  if (phase === "done" || phase === "error") {
904
- return /* @__PURE__ */ jsx5(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text7, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
965
+ return /* @__PURE__ */ jsx6(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text8, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
905
966
  }
906
967
  if (phase !== "awaitingInput" || !inputReq) return null;
907
968
  if (inputReq.promptType === "multipleChoice") {
908
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
969
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
909
970
  SelectPrompt,
910
971
  {
911
972
  question: inputReq.prompt,
@@ -922,7 +983,7 @@ function PromptInput() {
922
983
  ) });
923
984
  }
924
985
  if (inputReq.promptType === "multiSelect") {
925
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
986
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
926
987
  SelectPrompt,
927
988
  {
928
989
  multi: true,
@@ -937,7 +998,7 @@ function PromptInput() {
937
998
  ) });
938
999
  }
939
1000
  if (inputReq.promptType === "notice") {
940
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
1001
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
941
1002
  SelectPrompt,
942
1003
  {
943
1004
  question: inputReq.prompt,
@@ -948,7 +1009,7 @@ function PromptInput() {
948
1009
  ) });
949
1010
  }
950
1011
  if (inputReq.promptType === "enterToContinue") {
951
- return /* @__PURE__ */ jsx5(
1012
+ return /* @__PURE__ */ jsx6(
952
1013
  EnterToContinuePrompt,
953
1014
  {
954
1015
  question: inputReq.prompt,
@@ -957,9 +1018,12 @@ function PromptInput() {
957
1018
  }
958
1019
  );
959
1020
  }
1021
+ if (inputReq.promptType === "commandApproval" && inputReq.command) {
1022
+ return /* @__PURE__ */ jsx6(CommandApproval, { command: inputReq.command, onDecide: submitInput });
1023
+ }
960
1024
  if (inputReq.promptType === "acceptReject") {
961
1025
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
962
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
1026
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
963
1027
  SelectPrompt,
964
1028
  {
965
1029
  question: inputReq.prompt,
@@ -970,15 +1034,15 @@ function PromptInput() {
970
1034
  }
971
1035
  ) });
972
1036
  }
973
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
974
- inputReq.error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: inputReq.error }),
975
- inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
976
- /* @__PURE__ */ jsxs6(Box7, { children: [
977
- /* @__PURE__ */ jsxs6(Text7, { color: COLORS.primary, children: [
1037
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1038
+ inputReq.error && /* @__PURE__ */ jsx6(Text8, { color: COLORS.danger, children: inputReq.error }),
1039
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: m }, `msg-${i}`)),
1040
+ /* @__PURE__ */ jsxs7(Box8, { children: [
1041
+ /* @__PURE__ */ jsxs7(Text8, { color: COLORS.primary, children: [
978
1042
  inputReq.prompt,
979
1043
  " "
980
1044
  ] }),
981
- /* @__PURE__ */ jsx5(
1045
+ /* @__PURE__ */ jsx6(
982
1046
  TextInput,
983
1047
  {
984
1048
  value: draft,
@@ -996,7 +1060,7 @@ function PromptInput() {
996
1060
  // src/ui/Welcome.tsx
997
1061
  import { dirname as dirname2, join as join3 } from "node:path";
998
1062
  import { fileURLToPath } from "node:url";
999
- import { Box as Box8, Spacer, Text as Text8, useInput as useInput3, useWindowSize as useWindowSize6 } from "ink";
1063
+ import { Box as Box9, Spacer, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
1000
1064
 
1001
1065
  // src/ui/copy/welcome.ts
1002
1066
  var sidebarItems = [
@@ -1009,12 +1073,12 @@ var sidebarItems = [
1009
1073
  description: "push 100 records to Algolia in seconds"
1010
1074
  },
1011
1075
  {
1012
- title: "detect your framework",
1013
- description: "React, Vue, Angular, Vanilla JS"
1076
+ title: "detect your stack",
1077
+ description: "whatever language and framework you already use"
1014
1078
  },
1015
1079
  {
1016
1080
  title: "scaffold a search UI",
1017
- description: "a styled InstantSearch component, wired into your app"
1081
+ description: "a search box and results, wired into your app"
1018
1082
  },
1019
1083
  {
1020
1084
  title: "ship it",
@@ -1024,20 +1088,20 @@ var sidebarItems = [
1024
1088
 
1025
1089
  // src/ui/Welcome.tsx
1026
1090
  import Image, { InkPictureProvider } from "ink-picture";
1027
- import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
1091
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
1028
1092
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
1029
1093
  function SidebarItem({
1030
1094
  title,
1031
1095
  description
1032
1096
  }) {
1033
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1034
- /* @__PURE__ */ jsxs7(Box8, { gap: 1, children: [
1035
- /* @__PURE__ */ jsx6(Text8, { color: COLORS.success, children: "\u2192" }),
1036
- /* @__PURE__ */ jsx6(Text8, { color: COLORS.strong, bold: true, children: title })
1097
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
1098
+ /* @__PURE__ */ jsxs8(Box9, { gap: 1, children: [
1099
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, children: "\u2192" }),
1100
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: title })
1037
1101
  ] }),
1038
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 2, children: [
1039
- /* @__PURE__ */ jsx6(Spacer, {}),
1040
- /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: description })
1102
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 2, children: [
1103
+ /* @__PURE__ */ jsx7(Spacer, {}),
1104
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: description })
1041
1105
  ] })
1042
1106
  ] });
1043
1107
  }
@@ -1045,7 +1109,7 @@ function Welcome() {
1045
1109
  const confirmStart = useWizard((s) => s.confirmStart);
1046
1110
  const openLearnMore = useWizard((s) => s.openLearnMore);
1047
1111
  const { rows } = useWindowSize6();
1048
- useInput3((input, key) => {
1112
+ useInput4((input, key) => {
1049
1113
  if (key.return) confirmStart();
1050
1114
  else if (input === "i") openLearnMore();
1051
1115
  });
@@ -1063,16 +1127,16 @@ function Welcome() {
1063
1127
  if (rows < 30) {
1064
1128
  layout = scales["small"];
1065
1129
  }
1066
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1067
- /* @__PURE__ */ jsx6(
1068
- Box8,
1130
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1131
+ /* @__PURE__ */ jsx7(
1132
+ Box9,
1069
1133
  {
1070
1134
  paddingY: layout.main.padding.y,
1071
1135
  paddingX: layout.main.padding.x,
1072
1136
  flexDirection: "column",
1073
1137
  justifyContent: "center",
1074
- children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
1075
- /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
1138
+ children: /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", gap: 2, children: [
1139
+ /* @__PURE__ */ jsx7(InkPictureProvider, { children: /* @__PURE__ */ jsx7(
1076
1140
  Image,
1077
1141
  {
1078
1142
  src: IMAGE_PATH,
@@ -1083,16 +1147,16 @@ function Welcome() {
1083
1147
  protocol: "halfBlock"
1084
1148
  }
1085
1149
  ) }),
1086
- /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1087
- /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
1088
- /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
1089
- /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
1150
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1151
+ /* @__PURE__ */ jsxs8(Box9, { gap: 1, flexDirection: "column", children: [
1152
+ /* @__PURE__ */ jsx7(NextAction, { action: "start wizard", keyHint: "enter" }),
1153
+ /* @__PURE__ */ jsx7(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
1090
1154
  ] })
1091
1155
  ] })
1092
1156
  }
1093
1157
  ),
1094
- /* @__PURE__ */ jsxs7(
1095
- Box8,
1158
+ /* @__PURE__ */ jsxs8(
1159
+ Box9,
1096
1160
  {
1097
1161
  backgroundColor: COLORS.bg.sidebar,
1098
1162
  width: 40,
@@ -1102,8 +1166,8 @@ function Welcome() {
1102
1166
  flexDirection: "column",
1103
1167
  justifyContent: "center",
1104
1168
  children: [
1105
- /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1106
- sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
1169
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1170
+ sidebarItems.map((i, idx) => /* @__PURE__ */ jsx7(SidebarItem, { title: i.title, description: i.description }, idx))
1107
1171
  ]
1108
1172
  }
1109
1173
  )
@@ -1112,7 +1176,7 @@ function Welcome() {
1112
1176
 
1113
1177
  // src/ui/LearnMore.tsx
1114
1178
  import { Fragment as Fragment2 } from "react";
1115
- import { Box as Box9, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize7 } from "ink";
1179
+ import { Box as Box10, Text as Text10, useInput as useInput5, useWindowSize as useWindowSize7 } from "ink";
1116
1180
 
1117
1181
  // src/ui/copy/learn-more.ts
1118
1182
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -1120,12 +1184,17 @@ var accessItems = [
1120
1184
  {
1121
1185
  tag: "READ",
1122
1186
  title: "Project files",
1123
- description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
1187
+ description: "reads manifests, configs & source to detect your stack. Read-only; nothing is uploaded."
1124
1188
  },
1125
1189
  {
1126
1190
  tag: "WRITE",
1127
1191
  title: "Code changes",
1128
- description: "creates & edits files (search UI, config). Shown as a diff first \u2014 nothing lands without your approval."
1192
+ description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1193
+ },
1194
+ {
1195
+ tag: "EXEC",
1196
+ title: "Setup commands",
1197
+ description: "runs dependency installs, the ingestion script & your own checks. Every command is shown in full and needs your OK; its output is shown as-is, so a command that prints a secret will display it."
1129
1198
  },
1130
1199
  {
1131
1200
  tag: "NET",
@@ -1135,13 +1204,13 @@ var accessItems = [
1135
1204
  {
1136
1205
  tag: "KEY",
1137
1206
  title: "Credentials",
1138
- description: "saves your Admin API key to .env and adds it to .gitignore."
1207
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1139
1208
  }
1140
1209
  ];
1141
1210
  var neverItems = [
1142
1211
  "Send your source code to a model or third party",
1143
1212
  "Commit or push to git",
1144
- "Touch files outside your project directory"
1213
+ "Run a command you haven't approved"
1145
1214
  ];
1146
1215
  var policyLinks = [
1147
1216
  { label: "Terms", url: "https://www.algolia.com/policies/terms" },
@@ -1149,10 +1218,11 @@ var policyLinks = [
1149
1218
  ];
1150
1219
 
1151
1220
  // src/ui/LearnMore.tsx
1152
- import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
1221
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1153
1222
  var TAG_COLORS = {
1154
1223
  READ: COLORS.success,
1155
1224
  WRITE: COLORS.badge,
1225
+ EXEC: COLORS.danger,
1156
1226
  NET: COLORS.accent,
1157
1227
  KEY: COLORS.muted
1158
1228
  };
@@ -1165,12 +1235,12 @@ function NeverLine({
1165
1235
  }) {
1166
1236
  const used = segments.reduce((n, s) => n + s.text.length, 0);
1167
1237
  const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
1168
- return /* @__PURE__ */ jsxs8(Text9, { children: [
1169
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" }),
1238
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
1239
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: "\u2502" }),
1170
1240
  " ".repeat(NEVER_BOX_PAD_X),
1171
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text9, { color: s.color, bold: s.bold, children: s.text }, i)),
1241
+ segments.map((s, i) => /* @__PURE__ */ jsx8(Text10, { color: s.color, bold: s.bold, children: s.text }, i)),
1172
1242
  " ".repeat(rightPad),
1173
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" })
1243
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: "\u2502" })
1174
1244
  ] });
1175
1245
  }
1176
1246
  function LearnMore() {
@@ -1178,12 +1248,12 @@ function LearnMore() {
1178
1248
  const backToHome = useWizard((s) => s.backToHome);
1179
1249
  const { columns } = useWindowSize7();
1180
1250
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1181
- useInput4((_input, key) => {
1251
+ useInput5((_input, key) => {
1182
1252
  if (key.escape) backToHome();
1183
1253
  else if (key.return) confirmStart();
1184
1254
  });
1185
- return /* @__PURE__ */ jsxs8(
1186
- Box9,
1255
+ return /* @__PURE__ */ jsxs9(
1256
+ Box10,
1187
1257
  {
1188
1258
  flexDirection: "column",
1189
1259
  paddingX: PADDING_X,
@@ -1191,31 +1261,31 @@ function LearnMore() {
1191
1261
  width: "100%",
1192
1262
  gap: 1,
1193
1263
  children: [
1194
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1195
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: accessIntro }),
1196
- /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginTop: 1, children: [
1197
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1198
- /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1199
- /* @__PURE__ */ jsx7(Box9, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text9, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1200
- /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { children: [
1201
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: item.title }),
1202
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1264
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1265
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: accessIntro }),
1266
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginTop: 1, children: [
1267
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1268
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1269
+ /* @__PURE__ */ jsx8(Box10, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text10, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1270
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text10, { children: [
1271
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: item.title }),
1272
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1203
1273
  ] }) })
1204
1274
  ] })
1205
1275
  ] }, item.tag)) }),
1206
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
1207
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1208
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1209
- /* @__PURE__ */ jsx7(
1276
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
1277
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1278
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1279
+ /* @__PURE__ */ jsx8(
1210
1280
  NeverLine,
1211
1281
  {
1212
1282
  width: dividerWidth,
1213
1283
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1214
1284
  }
1215
1285
  ),
1216
- neverItems.map((item) => /* @__PURE__ */ jsxs8(Fragment2, { children: [
1217
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1218
- /* @__PURE__ */ jsx7(
1286
+ neverItems.map((item) => /* @__PURE__ */ jsxs9(Fragment2, { children: [
1287
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1288
+ /* @__PURE__ */ jsx8(
1219
1289
  NeverLine,
1220
1290
  {
1221
1291
  width: dividerWidth,
@@ -1227,24 +1297,24 @@ function LearnMore() {
1227
1297
  }
1228
1298
  )
1229
1299
  ] }, item)),
1230
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1231
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1300
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1301
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1232
1302
  ] }),
1233
- /* @__PURE__ */ jsx7(Box9, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1234
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1235
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.accent, children: link.url })
1303
+ /* @__PURE__ */ jsx8(Box10, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1304
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1305
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.accent, children: link.url })
1236
1306
  ] }, link.label)) }),
1237
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1238
- /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1239
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1240
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "esc" }),
1241
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "] back" })
1307
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1308
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1309
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "[" }),
1310
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "esc" }),
1311
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "] back" })
1242
1312
  ] }),
1243
- /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1244
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1245
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "enter" }),
1246
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "]" }),
1247
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, bold: true, children: "start wizard" })
1313
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1314
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "[" }),
1315
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "enter" }),
1316
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "]" }),
1317
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.success, bold: true, children: "start wizard" })
1248
1318
  ] })
1249
1319
  ] })
1250
1320
  ]
@@ -1253,10 +1323,10 @@ function LearnMore() {
1253
1323
  }
1254
1324
 
1255
1325
  // src/ui/Sidebar.tsx
1256
- import { Box as Box12, Text as Text12 } from "ink";
1326
+ import { Box as Box13, Text as Text13 } from "ink";
1257
1327
 
1258
1328
  // src/ui/Steps.tsx
1259
- import { Box as Box10, Text as Text10 } from "ink";
1329
+ import { Box as Box11, Text as Text11 } from "ink";
1260
1330
  import Spinner from "ink-spinner";
1261
1331
 
1262
1332
  // src/core/persistence.ts
@@ -1285,12 +1355,12 @@ async function clearWorkflowState(workflowId) {
1285
1355
  }
1286
1356
 
1287
1357
  // src/ui/Steps.tsx
1288
- import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1358
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1289
1359
  function Steps() {
1290
1360
  const { steps } = useWizard();
1291
1361
  const visibleSteps = steps.filter(isStepVisible);
1292
- return /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status[s.status], children: [
1293
- s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1362
+ return /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs10(Text11, { color: COLORS.status[s.status], children: [
1363
+ s.status === "running" ? /* @__PURE__ */ jsx9(Spinner, { type: "dots" }) : MARKER[s.status],
1294
1364
  " ",
1295
1365
  s.title
1296
1366
  ] }) }, s.id)) });
@@ -1299,27 +1369,27 @@ function CurrentStep() {
1299
1369
  const { steps } = useWizard();
1300
1370
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1301
1371
  if (!currentStep) return null;
1302
- return /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.running, children: [
1303
- /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1372
+ return /* @__PURE__ */ jsxs10(Text11, { color: COLORS.status.running, children: [
1373
+ /* @__PURE__ */ jsx9(Spinner, { type: "dots" }),
1304
1374
  " ",
1305
1375
  ` ${currentStep.title}`
1306
1376
  ] });
1307
1377
  }
1308
1378
 
1309
1379
  // src/ui/Progress.tsx
1310
- import { Box as Box11, Text as Text11 } from "ink";
1311
- import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1380
+ import { Box as Box12, Text as Text12 } from "ink";
1381
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1312
1382
  function Progress() {
1313
1383
  const { steps, currentStepIndex } = useWizard();
1314
1384
  const visibleSteps = steps.filter(isStepVisible);
1315
1385
  if (visibleSteps.length === 0) return null;
1316
1386
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1317
1387
  const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
1318
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1319
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "STEP" }),
1320
- /* @__PURE__ */ jsx9(Text11, { bold: true, children: activeStepNumber }),
1321
- /* @__PURE__ */ jsx9(Text11, { bold: true, children: "/" }),
1322
- /* @__PURE__ */ jsx9(Text11, { bold: true, children: visibleSteps.length })
1388
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1389
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "STEP" }),
1390
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: activeStepNumber }),
1391
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: "/" }),
1392
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: visibleSteps.length })
1323
1393
  ] });
1324
1394
  }
1325
1395
 
@@ -1330,10 +1400,10 @@ var sidebarCommands = [
1330
1400
  ];
1331
1401
 
1332
1402
  // src/ui/Sidebar.tsx
1333
- import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1403
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1334
1404
  function Sidebar() {
1335
- return /* @__PURE__ */ jsxs11(
1336
- Box12,
1405
+ return /* @__PURE__ */ jsxs12(
1406
+ Box13,
1337
1407
  {
1338
1408
  backgroundColor: "#14171E",
1339
1409
  width: 30,
@@ -1342,16 +1412,16 @@ function Sidebar() {
1342
1412
  flexDirection: "column",
1343
1413
  justifyContent: "space-between",
1344
1414
  children: [
1345
- /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1346
- /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "PROGRESS" }),
1347
- /* @__PURE__ */ jsx10(Steps, {})
1415
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
1416
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: "PROGRESS" }),
1417
+ /* @__PURE__ */ jsx11(Steps, {})
1348
1418
  ] }),
1349
- /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1350
- /* @__PURE__ */ jsx10(Progress, {}),
1351
- /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: sidebarCommands.map((c) => {
1352
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1353
- /* @__PURE__ */ jsx10(Text12, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1354
- /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: c.description })
1419
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
1420
+ /* @__PURE__ */ jsx11(Progress, {}),
1421
+ /* @__PURE__ */ jsx11(Box13, { flexDirection: "column", children: sidebarCommands.map((c) => {
1422
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1423
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1424
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: c.description })
1355
1425
  ] });
1356
1426
  }) })
1357
1427
  ] })
@@ -1361,12 +1431,12 @@ function Sidebar() {
1361
1431
  }
1362
1432
 
1363
1433
  // src/ui/Ribbon.tsx
1364
- import { Box as Box13, Text as Text13 } from "ink";
1365
- import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1434
+ import { Box as Box14, Text as Text14 } from "ink";
1435
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1366
1436
  function Ribbon() {
1367
1437
  const firstCommand = sidebarCommands[0];
1368
- return /* @__PURE__ */ jsxs12(
1369
- Box13,
1438
+ return /* @__PURE__ */ jsxs13(
1439
+ Box14,
1370
1440
  {
1371
1441
  backgroundColor: "#14171E",
1372
1442
  flexDirection: "row",
@@ -1374,11 +1444,11 @@ function Ribbon() {
1374
1444
  paddingX: 2,
1375
1445
  paddingY: 1,
1376
1446
  children: [
1377
- /* @__PURE__ */ jsx11(Progress, {}),
1378
- /* @__PURE__ */ jsx11(CurrentStep, {}),
1379
- /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1380
- /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1381
- /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: firstCommand.description })
1447
+ /* @__PURE__ */ jsx12(Progress, {}),
1448
+ /* @__PURE__ */ jsx12(CurrentStep, {}),
1449
+ /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: 1, children: [
1450
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1451
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: firstCommand.description })
1382
1452
  ] })
1383
1453
  ]
1384
1454
  }
@@ -1389,8 +1459,8 @@ function Ribbon() {
1389
1459
  import { useState as useState6 } from "react";
1390
1460
 
1391
1461
  // src/ui/Logs.tsx
1392
- import { Box as Box14, Text as Text14, useInput as useInput5 } from "ink";
1393
- import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1462
+ import { Box as Box15, Text as Text15, useInput as useInput6 } from "ink";
1463
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1394
1464
  var KIND_COLOR = {
1395
1465
  tool: COLORS.primary,
1396
1466
  prompt: COLORS.badge
@@ -1421,14 +1491,14 @@ function formatTimestamp(ms) {
1421
1491
  function Logs() {
1422
1492
  const logs = useWizard((s) => s.logs);
1423
1493
  const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1424
- useInput5((_input, key) => {
1494
+ useInput6((_input, key) => {
1425
1495
  if (key.upArrow) scroll.scrollBy(-1);
1426
1496
  else if (key.downArrow) scroll.scrollBy(1);
1427
1497
  });
1428
1498
  const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1429
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1430
- logs.length === 0 && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "No logs yet." }),
1431
- /* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
1499
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1500
+ logs.length === 0 && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: "No logs yet." }),
1501
+ /* @__PURE__ */ jsx13(ScrollView, { scroll, children: visible.map((entry) => {
1432
1502
  const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1433
1503
  const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1434
1504
  const rawPreview = rawInputText(entry.input);
@@ -1438,14 +1508,14 @@ function Logs() {
1438
1508
  const name = truncate2(entry.name, budget);
1439
1509
  budget -= name.length;
1440
1510
  const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1441
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: ROW_GAP, children: [
1442
- /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: timestamp }),
1443
- /* @__PURE__ */ jsx12(Text14, { color: logNameColor(entry), wrap: "truncate", children: name }),
1444
- preview && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, wrap: "truncate", children: preview }),
1445
- durationText && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: durationText })
1511
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "row", gap: ROW_GAP, children: [
1512
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: timestamp }),
1513
+ /* @__PURE__ */ jsx13(Text15, { color: logNameColor(entry), wrap: "truncate", children: name }),
1514
+ preview && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, wrap: "truncate", children: preview }),
1515
+ durationText && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: durationText })
1446
1516
  ] }, entry.id);
1447
1517
  }) }),
1448
- /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1518
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1449
1519
  ] });
1450
1520
  }
1451
1521
 
@@ -1637,7 +1707,7 @@ function track(event, payload) {
1637
1707
  }
1638
1708
 
1639
1709
  // src/ui/App.tsx
1640
- import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1710
+ import { jsx as jsx14, jsxs as jsxs15 } from "react/jsx-runtime";
1641
1711
  function App() {
1642
1712
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1643
1713
  const { exit } = useApp();
@@ -1645,7 +1715,7 @@ function App() {
1645
1715
  const [showLogs, setShowLogs] = useState6(false);
1646
1716
  const finished = phase === "done" || phase === "error";
1647
1717
  const currentStep = steps[currentStepIndex];
1648
- useInput6(
1718
+ useInput7(
1649
1719
  (_input, key) => {
1650
1720
  if (key.return) {
1651
1721
  exit();
@@ -1653,7 +1723,7 @@ function App() {
1653
1723
  },
1654
1724
  { isActive: finished }
1655
1725
  );
1656
- useInput6((_input, key) => {
1726
+ useInput7((_input, key) => {
1657
1727
  if (phase === "idle" || phase === "authenticating") return;
1658
1728
  if (key.tab) {
1659
1729
  setShowLogs(!showLogs);
@@ -1664,8 +1734,8 @@ function App() {
1664
1734
  });
1665
1735
  }
1666
1736
  });
1667
- const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1668
- useInput6((_input, key) => {
1737
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
1738
+ useInput7((_input, key) => {
1669
1739
  if (escOwnedElsewhere) return;
1670
1740
  if (key.escape) {
1671
1741
  track("AI Wizard Interaction", {
@@ -1684,8 +1754,8 @@ function App() {
1684
1754
  /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1685
1755
  the whole screen, and the scrolling throws off its cursor arithmetic —
1686
1756
  flicker and leftover rows. */
1687
- /* @__PURE__ */ jsxs14(
1688
- Box15,
1757
+ /* @__PURE__ */ jsxs15(
1758
+ Box16,
1689
1759
  {
1690
1760
  backgroundColor: COLORS.bg.main,
1691
1761
  flexDirection: "row",
@@ -1693,16 +1763,16 @@ function App() {
1693
1763
  height: scrollsPastViewport ? void 0 : rows,
1694
1764
  overflow: scrollsPastViewport ? "visible" : "hidden",
1695
1765
  children: [
1696
- mainWindowVisible && /* @__PURE__ */ jsxs14(
1697
- Box15,
1766
+ mainWindowVisible && /* @__PURE__ */ jsxs15(
1767
+ Box16,
1698
1768
  {
1699
1769
  flexDirection,
1700
1770
  width: "100%",
1701
1771
  maxHeight: rows,
1702
1772
  justifyContent: "space-between",
1703
1773
  children: [
1704
- showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : /* @__PURE__ */ jsxs14(
1705
- Box15,
1774
+ showLogs ? /* @__PURE__ */ jsx14(Logs, {}) : /* @__PURE__ */ jsxs15(
1775
+ Box16,
1706
1776
  {
1707
1777
  flexDirection: "column",
1708
1778
  paddingX: 4,
@@ -1710,26 +1780,26 @@ function App() {
1710
1780
  width: showSidebar ? 70 : "100%",
1711
1781
  flexGrow: 1,
1712
1782
  children: [
1713
- phase === "authenticating" && /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginBottom: 1, children: [
1714
- /* @__PURE__ */ jsx13(Text15, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1715
- /* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1783
+ phase === "authenticating" && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginBottom: 1, children: [
1784
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1785
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1716
1786
  ] }),
1717
- /* @__PURE__ */ jsx13(CliOutput, {}),
1718
- /* @__PURE__ */ jsx13(Notices, {}),
1719
- /* @__PURE__ */ jsx13(PromptInput, {}),
1720
- phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1721
- phase === "error" && error && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text15, { color: COLORS.status.error, children: [
1787
+ /* @__PURE__ */ jsx14(CliOutput, {}),
1788
+ /* @__PURE__ */ jsx14(Notices, {}),
1789
+ /* @__PURE__ */ jsx14(PromptInput, {}),
1790
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx14(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx14(CurrentStep, {}) }),
1791
+ phase === "error" && error && /* @__PURE__ */ jsx14(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text16, { color: COLORS.status.error, children: [
1722
1792
  "\u2716 ",
1723
1793
  error
1724
1794
  ] }) })
1725
1795
  ]
1726
1796
  }
1727
1797
  ),
1728
- showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1798
+ showSidebar ? /* @__PURE__ */ jsx14(Sidebar, {}) : /* @__PURE__ */ jsx14(Ribbon, {})
1729
1799
  ]
1730
1800
  }
1731
1801
  ),
1732
- phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1802
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx14(LearnMore, {}) : /* @__PURE__ */ jsx14(Welcome, {}))
1733
1803
  ]
1734
1804
  }
1735
1805
  )
@@ -1746,7 +1816,8 @@ var configFile = () => join5(stateDir(), "config.json");
1746
1816
  var DEFAULT_CONFIG = {
1747
1817
  version: 1,
1748
1818
  aiConsent: false,
1749
- workflowsRun: []
1819
+ workflowsRun: [],
1820
+ searchApiKeys: {}
1750
1821
  };
1751
1822
  async function loadConfig() {
1752
1823
  try {
@@ -1765,6 +1836,38 @@ async function recordWorkflowRun(workflowId, completedAt) {
1765
1836
  config.workflowsRun.push({ workflowId, completedAt });
1766
1837
  await saveConfig(config);
1767
1838
  }
1839
+ function isStoredSearchKey(value) {
1840
+ if (typeof value !== "object" || value === null) return false;
1841
+ const { appId, key } = value;
1842
+ return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
1843
+ }
1844
+ function storedSearchKeys(config) {
1845
+ const stored = config.searchApiKeys;
1846
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
1847
+ return {};
1848
+ }
1849
+ return stored;
1850
+ }
1851
+ async function getStoredSearchKey(index, appId) {
1852
+ const entry = storedSearchKeys(await loadConfig())[index];
1853
+ if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
1854
+ return entry.key;
1855
+ }
1856
+ async function storeSearchKey(index, appId, key) {
1857
+ const config = await loadConfig();
1858
+ config.searchApiKeys = {
1859
+ ...storedSearchKeys(config),
1860
+ [index]: { appId, key }
1861
+ };
1862
+ await saveConfig(config);
1863
+ }
1864
+ async function forgetSearchKey(index) {
1865
+ const config = await loadConfig();
1866
+ const remaining = { ...storedSearchKeys(config) };
1867
+ delete remaining[index];
1868
+ config.searchApiKeys = remaining;
1869
+ await saveConfig(config);
1870
+ }
1768
1871
 
1769
1872
  // src/core/orchestrator.ts
1770
1873
  function defineStep(step) {
@@ -1809,7 +1912,7 @@ async function ensureConsent() {
1809
1912
  if (config.aiConsent) return;
1810
1913
  const store = useWizard.getState();
1811
1914
  const answer = await store.requestUserInput({
1812
- prompt: "Wizard will make AI-authored changes to this repository.",
1915
+ prompt: "Wizard will make AI-authored changes to this repository, and will propose shell commands to set it up. You approve each command before it runs.",
1813
1916
  promptType: "enterToContinue",
1814
1917
  options: []
1815
1918
  });
@@ -2357,7 +2460,6 @@ import { dirname as dirname5 } from "node:path";
2357
2460
 
2358
2461
  // src/lib/algoliaApiKey.ts
2359
2462
  import { z as z11 } from "zod";
2360
- var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
2361
2463
  var WRITE_ACLS = [
2362
2464
  "addObject",
2363
2465
  "deleteObject",
@@ -2371,41 +2473,40 @@ var apiKeySchema = z11.object({
2371
2473
  acl: z11.array(z11.string()).default([]),
2372
2474
  indexes: z11.array(z11.string()).default([])
2373
2475
  });
2374
- var apiKeyListSchema = z11.object({
2375
- items: z11.array(apiKeySchema).optional(),
2376
- keys: z11.array(apiKeySchema).optional()
2377
- }).transform((o) => o.items ?? o.keys ?? []);
2476
+ var apiKeyListSchema = z11.union([
2477
+ z11.array(apiKeySchema),
2478
+ z11.object({
2479
+ items: z11.array(apiKeySchema).optional(),
2480
+ keys: z11.array(apiKeySchema).optional()
2481
+ })
2482
+ ]).transform((o) => Array.isArray(o) ? o : o.items ?? o.keys ?? []);
2483
+ function parseCliJson(schema, stdout, command) {
2484
+ let raw;
2485
+ try {
2486
+ raw = JSON.parse(stdout);
2487
+ } catch {
2488
+ throw new Error(`Algolia CLI \`${command}\` returned unreadable JSON.`);
2489
+ }
2490
+ const parsed = schema.safeParse(raw);
2491
+ if (!parsed.success) {
2492
+ throw new Error(
2493
+ `Algolia CLI \`${command}\` returned JSON in an unexpected shape.`
2494
+ );
2495
+ }
2496
+ return parsed.data;
2497
+ }
2378
2498
  var createdKeySchema = z11.object({
2379
2499
  key: z11.string().min(1).optional(),
2380
2500
  value: z11.string().min(1).optional()
2381
- });
2382
- function canReuse(key, index) {
2383
- return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
2384
- }
2385
- async function createSearchKey(index) {
2386
- const stdout = await runAlgoliaCli([
2387
- "apikeys",
2388
- "create",
2389
- "--indices",
2390
- index,
2391
- "--acl",
2392
- "search,browse",
2393
- "--description",
2394
- `wizard search-only key for ${index}`,
2395
- "-o",
2396
- "json"
2397
- ]);
2398
- const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
2399
- const created = key ?? value;
2400
- if (!created) throw new Error("apikeys create returned no key value");
2401
- return created;
2402
- }
2501
+ }).transform((o) => o.key ?? o.value);
2403
2502
  function canReuseForWrites(key, index) {
2404
2503
  return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2405
2504
  }
2406
2505
  async function resolveWriteKey(index) {
2407
2506
  const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2408
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key2) => canReuseForWrites(key2, index))?.value;
2507
+ const existing = parseCliJson(apiKeyListSchema, stdout, "apikeys list").find(
2508
+ (key) => canReuseForWrites(key, index)
2509
+ )?.value;
2409
2510
  if (existing) {
2410
2511
  logger.info({ index }, "reusing existing write API key");
2411
2512
  return existing;
@@ -2423,20 +2524,72 @@ async function resolveWriteKey(index) {
2423
2524
  "-o",
2424
2525
  "json"
2425
2526
  ]);
2426
- const { key, value } = createdKeySchema.parse(JSON.parse(created));
2427
- const writeKey = key ?? value;
2527
+ const writeKey = parseCliJson(createdKeySchema, created, "apikeys create");
2428
2528
  if (!writeKey) throw new Error("apikeys create returned no key value");
2429
2529
  return writeKey;
2430
2530
  }
2431
- async function resolveSearchOnlyKey(index) {
2432
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2433
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
2434
- if (existing) {
2435
- logger.info({ index }, "reusing existing search-only API key");
2436
- return existing;
2531
+ async function createSearchOnlyKey(index) {
2532
+ logger.info({ index }, "creating a search-only API key");
2533
+ const stdout = await runAlgoliaCli([
2534
+ "apikeys",
2535
+ "create",
2536
+ "--acl",
2537
+ "search",
2538
+ "--indices",
2539
+ index,
2540
+ "--description",
2541
+ `Algolia Wizard search-only key for ${index}`,
2542
+ "-o",
2543
+ "json"
2544
+ ]);
2545
+ let payload;
2546
+ try {
2547
+ payload = JSON.parse(stdout);
2548
+ } catch {
2549
+ throw new Error("apikeys create returned output that is not valid JSON");
2550
+ }
2551
+ const created = createdKeySchema.parse(payload);
2552
+ if (!created) throw new Error("apikeys create returned no key value");
2553
+ return created;
2554
+ }
2555
+ async function apiKeyExists(key) {
2556
+ try {
2557
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
2558
+ return true;
2559
+ } catch (err) {
2560
+ return !/does not exist|not found|404/i.test(err.message);
2561
+ }
2562
+ }
2563
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2564
+ if (envKey) {
2565
+ await recordSearchKey(index, appId, envKey);
2566
+ return { key: envKey, source: "env" };
2567
+ }
2568
+ const stored = await getStoredSearchKey(index, appId);
2569
+ if (stored) {
2570
+ if (await apiKeyExists(stored)) {
2571
+ logger.info({ index, appId }, "reusing the stored search-only API key");
2572
+ return { key: stored, source: "config" };
2573
+ }
2574
+ logger.warn(
2575
+ { index, appId },
2576
+ "the stored search-only API key no longer exists; creating a replacement"
2577
+ );
2578
+ await forgetSearchKey(index);
2579
+ }
2580
+ const key = await createSearchOnlyKey(index);
2581
+ await recordSearchKey(index, appId, key);
2582
+ return { key, source: "created" };
2583
+ }
2584
+ async function recordSearchKey(index, appId, key) {
2585
+ try {
2586
+ await storeSearchKey(index, appId, key);
2587
+ } catch (err) {
2588
+ logger.warn(
2589
+ { err: err.message, index },
2590
+ "could not record the search-only API key; a later run may create another"
2591
+ );
2437
2592
  }
2438
- logger.info({ index }, "no reusable search-only key found; creating one");
2439
- return createSearchKey(index);
2440
2593
  }
2441
2594
 
2442
2595
  // src/lib/tools/writeAlgoliaCredentials.ts
@@ -2511,11 +2664,19 @@ import z13 from "zod";
2511
2664
  import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
2512
2665
  import { join as join7 } from "node:path";
2513
2666
  var MAX_QUERY_LENGTH = 1e3;
2667
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2668
+ "node_modules",
2669
+ "dist",
2670
+ "build",
2671
+ "vendor",
2672
+ "venv",
2673
+ "__pycache__",
2674
+ "target"
2675
+ ]);
2514
2676
  async function walkFiles(dir) {
2515
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2516
2677
  const out = [];
2517
2678
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2518
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2679
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2519
2680
  const full = join7(dir, e.name);
2520
2681
  if (e.isDirectory()) out.push(...await walkFiles(full));
2521
2682
  else if (e.isFile()) out.push(full);
@@ -2569,92 +2730,194 @@ function searchFilesTool(ctx) {
2569
2730
  });
2570
2731
  }
2571
2732
 
2572
- // src/lib/tools/verifyImplementation.ts
2733
+ // src/lib/tools/runShell.ts
2573
2734
  import { tool as tool8 } from "ai";
2574
2735
  import z14 from "zod";
2736
+ import { relative as relative2 } from "node:path";
2575
2737
 
2576
- // src/lib/tools/utils/runCommand.ts
2738
+ // src/lib/tools/utils/runShell.ts
2577
2739
  import { spawn as spawn2 } from "node:child_process";
2578
- function runCommand(command, args, cwd) {
2740
+
2741
+ // src/lib/tools/context.ts
2742
+ var DEFAULT_TOOL_LIMITS = {
2743
+ list: 10,
2744
+ search: 10,
2745
+ read: 20,
2746
+ match: 100,
2747
+ shell: 30
2748
+ };
2749
+ var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
2750
+ async function refuseByDefault() {
2751
+ return "reject";
2752
+ }
2753
+ function createShellContext(overrides = {}) {
2754
+ return {
2755
+ env: async () => ({}),
2756
+ timeoutMs: DEFAULT_SHELL_TIMEOUT_MS,
2757
+ approved: /* @__PURE__ */ new Set(),
2758
+ executions: [],
2759
+ approve: refuseByDefault,
2760
+ ...overrides
2761
+ };
2762
+ }
2763
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), shell2 = createShellContext()) {
2764
+ return {
2765
+ root: cwd,
2766
+ cwd,
2767
+ limits: { ...limits },
2768
+ counts: { list: 0, search: 0, read: 0, shell: 0 },
2769
+ shell: shell2
2770
+ };
2771
+ }
2772
+
2773
+ // src/lib/tools/utils/runShell.ts
2774
+ var SIGKILL_DELAY_MS = 5e3;
2775
+ var HEAD_CHARS = 4e3;
2776
+ var TAIL_CHARS = 8e3;
2777
+ function truncateOutput(output) {
2778
+ if (output.length <= HEAD_CHARS + TAIL_CHARS) return output;
2779
+ const omitted = output.length - HEAD_CHARS - TAIL_CHARS;
2780
+ return [
2781
+ output.slice(0, HEAD_CHARS),
2782
+ `
2783
+ \u2026 [${omitted} characters omitted] \u2026
2784
+ `,
2785
+ output.slice(-TAIL_CHARS)
2786
+ ].join("");
2787
+ }
2788
+ function runShell(command, opts) {
2789
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2790
+ const startedAt = Date.now();
2579
2791
  return new Promise((resolve4) => {
2580
2792
  let output = "";
2581
- const child = spawn2(command, args, {
2582
- cwd,
2583
- stdio: ["ignore", "pipe", "pipe"]
2793
+ let timedOut = false;
2794
+ let settled = false;
2795
+ const child = spawn2(command, {
2796
+ shell: true,
2797
+ cwd: opts.cwd,
2798
+ stdio: ["ignore", "pipe", "pipe"],
2799
+ env: { ...process.env, ...opts.env }
2584
2800
  });
2801
+ const finish = (exitCode) => {
2802
+ if (settled) return;
2803
+ settled = true;
2804
+ clearTimeout(timer);
2805
+ clearTimeout(killTimer);
2806
+ resolve4({
2807
+ exitCode,
2808
+ output: truncateOutput(output.trim()),
2809
+ timedOut,
2810
+ durationMs: Date.now() - startedAt
2811
+ });
2812
+ };
2813
+ let killTimer;
2814
+ const timer = setTimeout(() => {
2815
+ timedOut = true;
2816
+ output += `
2817
+ [timed out after ${timeoutMs}ms]`;
2818
+ child.kill("SIGTERM");
2819
+ killTimer = setTimeout(() => child.kill("SIGKILL"), SIGKILL_DELAY_MS);
2820
+ }, timeoutMs);
2585
2821
  child.stdout?.on("data", (d) => output += d);
2586
2822
  child.stderr?.on("data", (d) => output += d);
2587
- child.on(
2588
- "error",
2589
- (err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
2590
- );
2591
- child.on("close", (code) => resolve4({ code: code ?? 1, output }));
2823
+ child.on("error", (err) => {
2824
+ output += `Failed to run ${command}: ${err.message}`;
2825
+ finish(1);
2826
+ });
2827
+ child.on("close", (code) => finish(code ?? 1));
2592
2828
  });
2593
2829
  }
2594
2830
 
2595
- // src/lib/tools/utils/packageManager.ts
2596
- import { readFile as readFile6 } from "node:fs/promises";
2597
- import { existsSync } from "node:fs";
2598
- import { join as join8 } from "node:path";
2599
- var LOCKFILES = [
2600
- ["pnpm-lock.yaml", "pnpm"],
2601
- ["yarn.lock", "yarn"],
2602
- ["bun.lockb", "bun"],
2603
- ["bun.lock", "bun"],
2604
- ["package-lock.json", "npm"]
2605
- ];
2606
- async function readPackageJson(cwd = process.cwd()) {
2607
- return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2608
- }
2609
- function packageManagerFrom(pkg) {
2610
- return pkg.packageManager?.split("@")[0] ?? "npm";
2611
- }
2612
- function packageManagerFromLockfile(cwd) {
2613
- return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2614
- }
2615
- async function detectPackageManager(cwd) {
2616
- try {
2617
- const pkg = await readPackageJson(cwd);
2618
- if (pkg.packageManager) return packageManagerFrom(pkg);
2619
- } catch {
2620
- }
2621
- return packageManagerFromLockfile(cwd) ?? "npm";
2622
- }
2623
-
2624
- // src/lib/tools/repoVerification.ts
2625
- var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
2626
- async function runRepoVerificationCheck() {
2627
- let pkg;
2628
- try {
2629
- pkg = await readPackageJson();
2630
- } catch (err) {
2631
- const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2632
- return { ok: false, checks: [], limitation };
2633
- }
2634
- const scripts = pkg.scripts ?? {};
2635
- const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
2636
- if (present.length === 0) {
2637
- const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2638
- return { ok: false, checks: [], limitation };
2639
- }
2640
- const pm = await detectPackageManager(process.cwd());
2641
- const checks = [];
2642
- for (const script of present) {
2643
- const command = `${pm} run ${script}`;
2644
- const { code, output } = await runCommand(pm, ["run", script]);
2645
- checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
2646
- }
2647
- return { ok: checks.every((c) => c.ok), checks };
2831
+ // src/lib/tools/runShell.ts
2832
+ function approvalKey(cwd, command) {
2833
+ return `${cwd}\0${command}`;
2834
+ }
2835
+ function storeApproval(root) {
2836
+ return async (req) => {
2837
+ const rel = relative2(root, req.cwd);
2838
+ const answer = await useWizard.getState().requestUserInput({
2839
+ prompt: "Run this command?",
2840
+ promptType: "commandApproval",
2841
+ options: [],
2842
+ command: {
2843
+ ...req,
2844
+ cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
2845
+ }
2846
+ });
2847
+ return answer === "approve" || answer === "always" ? answer : "reject";
2848
+ };
2648
2849
  }
2649
-
2650
- // src/lib/tools/verifyImplementation.ts
2651
- function verifyImplementationTool() {
2850
+ function runShellTool(ctx) {
2652
2851
  return tool8({
2653
- 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.",
2654
- inputSchema: z14.object(),
2655
- execute: async () => {
2656
- logger.info("called verifyImplementation tool");
2657
- return runRepoVerificationCheck();
2852
+ description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
2853
+ inputSchema: z14.object({
2854
+ command: z14.string().describe(
2855
+ "The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
2856
+ ),
2857
+ cwd: z14.string().optional().describe(
2858
+ "Directory to run in, relative to the project root. Defaults to the project root."
2859
+ ),
2860
+ explanation: z14.string().describe(
2861
+ "One short line telling the user what this command does and why, including any side effect (e.g. writes records to Algolia). This is what they approve against."
2862
+ )
2863
+ }),
2864
+ execute: async ({ command, cwd, explanation }) => {
2865
+ if (++ctx.counts.shell > ctx.limits.shell) {
2866
+ return `Refused: command limit (${ctx.limits.shell}) reached. Stop running commands and report what you have.`;
2867
+ }
2868
+ const resolved = resolveInRoot(ctx, cwd ?? ".");
2869
+ if (!resolved.ok) return resolved.error;
2870
+ logger.info({ command, cwd: resolved.target }, "called runShell tool");
2871
+ const key = approvalKey(resolved.target, command);
2872
+ const decision = ctx.shell.approved.has(key) ? "approve" : await ctx.shell.approve({
2873
+ command,
2874
+ cwd: resolved.target,
2875
+ explanation
2876
+ });
2877
+ if (decision === "reject") {
2878
+ ctx.shell.executions.push({
2879
+ command,
2880
+ cwd: resolved.target,
2881
+ approved: false
2882
+ });
2883
+ logger.info({ command }, "runShell: user rejected the command");
2884
+ return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2885
+ }
2886
+ if (decision === "always") ctx.shell.approved.add(key);
2887
+ useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
2888
+ const env = await ctx.shell.env().catch((err) => {
2889
+ logger.warn({ err, command }, "runShell: could not resolve command env");
2890
+ return {};
2891
+ });
2892
+ const run2 = await (ctx.shell.run ?? runShell)(command, {
2893
+ cwd: resolved.target,
2894
+ env,
2895
+ timeoutMs: ctx.shell.timeoutMs
2896
+ });
2897
+ logger.info(
2898
+ {
2899
+ command,
2900
+ exitCode: run2.exitCode,
2901
+ timedOut: run2.timedOut,
2902
+ durationMs: run2.durationMs
2903
+ },
2904
+ "runShell finished"
2905
+ );
2906
+ await markInteraction();
2907
+ ctx.shell.executions.push({
2908
+ command,
2909
+ cwd: resolved.target,
2910
+ approved: true,
2911
+ exitCode: run2.exitCode,
2912
+ output: run2.output,
2913
+ timedOut: run2.timedOut,
2914
+ durationMs: run2.durationMs
2915
+ });
2916
+ return {
2917
+ exitCode: run2.exitCode,
2918
+ timedOut: run2.timedOut,
2919
+ output: run2.output
2920
+ };
2658
2921
  }
2659
2922
  });
2660
2923
  }
@@ -2745,7 +3008,7 @@ function generateRecordTool(ctx) {
2745
3008
  return {
2746
3009
  filePath: relPath,
2747
3010
  count: records.length,
2748
- message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) \u2014 do not inline the records as literals.`
3011
+ message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime using your language's standard JSON support \u2014 do not inline the records as literals.`
2749
3012
  };
2750
3013
  } catch (err) {
2751
3014
  return `Error generating records: ${err.message}`;
@@ -2773,22 +3036,6 @@ function notifyUserTool() {
2773
3036
  });
2774
3037
  }
2775
3038
 
2776
- // src/lib/tools/context.ts
2777
- var DEFAULT_TOOL_LIMITS = {
2778
- list: 10,
2779
- search: 10,
2780
- read: 20,
2781
- match: 100
2782
- };
2783
- function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
2784
- return {
2785
- root: cwd,
2786
- cwd,
2787
- limits,
2788
- counts: { list: 0, search: 0, read: 0 }
2789
- };
2790
- }
2791
-
2792
3039
  // src/lib/tools/index.ts
2793
3040
  function withLogging(name, def) {
2794
3041
  const execute = def.execute;
@@ -2820,10 +3067,7 @@ function createTools(ctx, { output, tools }) {
2820
3067
  writeCredentialsTool(ctx)
2821
3068
  ),
2822
3069
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2823
- verifyImplementation: withLogging(
2824
- "verifyImplementation",
2825
- verifyImplementationTool()
2826
- ),
3070
+ runShell: withLogging("runShell", runShellTool(ctx)),
2827
3071
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2828
3072
  notifyUser: withLogging("notifyUser", notifyUserTool())
2829
3073
  };
@@ -2858,7 +3102,7 @@ async function runAgent(req) {
2858
3102
  baseURL: PROXY_BASE_URL,
2859
3103
  fetch: proxyFetch
2860
3104
  });
2861
- const toolContext = createToolContext();
3105
+ const toolContext = req.toolContext ?? createToolContext();
2862
3106
  const readTools = ["readFile", "searchFiles", "listFiles"];
2863
3107
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2864
3108
  const instructions = [
@@ -2923,7 +3167,11 @@ async function runAgent(req) {
2923
3167
  "runAgent finished"
2924
3168
  );
2925
3169
  logger.info(
2926
- { counts: toolContext.counts, limits: toolContext.limits },
3170
+ {
3171
+ counts: toolContext.counts,
3172
+ limits: toolContext.limits,
3173
+ commandsRun: toolContext.shell.executions.length
3174
+ },
2927
3175
  "tool usage"
2928
3176
  );
2929
3177
  const toolResults = await stream.toolResults;
@@ -2949,8 +3197,8 @@ var detectLanguageSchema = z19.object({
2949
3197
  var detectLanguage = () => runAgent({
2950
3198
  instructions: [
2951
3199
  "Analyze the codebase and determine the programming languages and frameworks used",
2952
- "If a superset language is found, exclude the subset language. TS-over-JS.",
2953
- "If a meta-framework is used, exclude the framework. Next-over-React.",
3200
+ "If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
3201
+ "If a meta-framework is used, exclude the framework it builds on (e.g. Next.js over React, Rails over Rack).",
2954
3202
  "Return the exact version",
2955
3203
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
2956
3204
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -3048,7 +3296,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3048
3296
  // package.json
3049
3297
  var package_default = {
3050
3298
  name: "@algolia/wizard",
3051
- version: "0.9.0-rc.85.78",
3299
+ version: "0.9.0-rc.87.83",
3052
3300
  description: "Magically implement Algolia functionality in your codebase",
3053
3301
  type: "module",
3054
3302
  engines: {
@@ -3196,9 +3444,11 @@ var CURATED_FRAMEWORKS = [
3196
3444
  "Next.js",
3197
3445
  "React",
3198
3446
  "Vue",
3199
- "Angular",
3200
- "Svelte",
3201
- "Vanilla JS"
3447
+ "Vanilla JS",
3448
+ "Django",
3449
+ "Laravel",
3450
+ "Rails",
3451
+ "Symfony"
3202
3452
  ];
3203
3453
  var OTHER_OPTION = "Other";
3204
3454
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -3211,12 +3461,15 @@ var FRAMEWORK_ALIASES = {
3211
3461
  vuejs: "vue",
3212
3462
  angular: "angular",
3213
3463
  angularjs: "angular",
3214
- svelte: "svelte",
3215
- sveltekit: "svelte",
3216
3464
  vanillajs: "vanillajs",
3217
3465
  vanilla: "vanillajs",
3218
3466
  javascript: "vanillajs",
3219
- js: "vanillajs"
3467
+ js: "vanillajs",
3468
+ django: "django",
3469
+ laravel: "laravel",
3470
+ rails: "rails",
3471
+ rubyonrails: "rails",
3472
+ symfony: "symfony"
3220
3473
  };
3221
3474
  var isSameFramework = (a, b) => {
3222
3475
  const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
@@ -3450,16 +3703,9 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3450
3703
  import z26 from "zod";
3451
3704
 
3452
3705
  // src/lib/worktree.ts
3453
- import { execFile, spawn as spawn3 } from "node:child_process";
3454
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3455
- import {
3456
- basename as basename2,
3457
- dirname as dirname7,
3458
- isAbsolute as isAbsolute2,
3459
- join as join9,
3460
- relative as relative2,
3461
- resolve as resolve3
3462
- } from "node:path";
3706
+ import { execFile } from "node:child_process";
3707
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile6, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3708
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
3463
3709
  var MAX_BUFFER = 32 * 1024 * 1024;
3464
3710
  var MAX_WIZARD_WORKTREES = 3;
3465
3711
  var WIZARD_BRANCH_PREFIX = "wizard/implement-";
@@ -3490,7 +3736,7 @@ async function isWorkingTreeDirty(repoRoot) {
3490
3736
  return out.trim().length > 0;
3491
3737
  }
3492
3738
  async function pruneOldWorktrees(repoRoot) {
3493
- const dir = join9(stateDir(repoRoot), "worktrees");
3739
+ const dir = join8(stateDir(repoRoot), "worktrees");
3494
3740
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3495
3741
  for (const slug of stale) {
3496
3742
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3501,7 +3747,7 @@ async function pruneOldWorktrees(repoRoot) {
3501
3747
  "worktree",
3502
3748
  "remove",
3503
3749
  "--force",
3504
- join9(dir, slug)
3750
+ join8(dir, slug)
3505
3751
  ]);
3506
3752
  await git(["-C", repoRoot, "branch", "-D", branch]);
3507
3753
  } catch (err) {
@@ -3515,113 +3761,13 @@ async function pruneOldWorktrees(repoRoot) {
3515
3761
  async function createWorktree(repoRoot) {
3516
3762
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3517
3763
  const dirSlug = branch.replace(/\//g, "-");
3518
- const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
3764
+ const path = join8(stateDir(repoRoot), "worktrees", dirSlug);
3519
3765
  await git(["-C", repoRoot, "worktree", "prune"]);
3520
3766
  await pruneOldWorktrees(repoRoot);
3521
3767
  await mkdir6(dirname7(path), { recursive: true });
3522
3768
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
3523
3769
  return { path, branch };
3524
3770
  }
3525
- async function installWorktreeDeps(worktreePath) {
3526
- try {
3527
- await readPackageJson(worktreePath);
3528
- } catch {
3529
- return { ok: true, output: "no package.json; skipped install" };
3530
- }
3531
- const pm = await detectPackageManager(worktreePath);
3532
- return new Promise((resolve4) => {
3533
- let output = "";
3534
- const child = spawn3(pm, ["install"], {
3535
- cwd: worktreePath,
3536
- stdio: ["ignore", "pipe", "pipe"]
3537
- });
3538
- child.stdout?.on("data", (d) => output += d);
3539
- child.stderr?.on("data", (d) => output += d);
3540
- child.on(
3541
- "error",
3542
- (err) => resolve4({
3543
- ok: false,
3544
- output: `Failed to run ${pm} install: ${err.message}`
3545
- })
3546
- );
3547
- child.on(
3548
- "close",
3549
- (code) => resolve4({ ok: code === 0, output: output.trim() })
3550
- );
3551
- });
3552
- }
3553
- var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3554
- function validateIngestEntrypoint(worktreePath, entrypoint) {
3555
- if (!entrypoint || entrypoint.startsWith("-")) {
3556
- return {
3557
- ok: false,
3558
- reason: `entrypoint "${entrypoint}" is not a plain file path`
3559
- };
3560
- }
3561
- const target = resolve3(worktreePath, entrypoint);
3562
- const rel = relative2(worktreePath, target);
3563
- if (rel.startsWith("..") || isAbsolute2(rel)) {
3564
- return {
3565
- ok: false,
3566
- reason: `entrypoint "${entrypoint}" resolves outside the worktree`
3567
- };
3568
- }
3569
- return { ok: true, target };
3570
- }
3571
- async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3572
- if (!INGEST_RUNTIMES.includes(runtime)) {
3573
- return {
3574
- ran: false,
3575
- ok: false,
3576
- output: "",
3577
- reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
3578
- };
3579
- }
3580
- const validated = validateIngestEntrypoint(worktreePath, entrypoint);
3581
- if (!validated.ok) {
3582
- return { ran: false, ok: false, output: "", reason: validated.reason };
3583
- }
3584
- try {
3585
- if (!(await stat2(validated.target)).isFile()) {
3586
- return {
3587
- ran: false,
3588
- ok: false,
3589
- output: "",
3590
- reason: `entrypoint "${entrypoint}" is not a file`
3591
- };
3592
- }
3593
- } catch {
3594
- return {
3595
- ran: false,
3596
- ok: false,
3597
- output: "",
3598
- reason: `entrypoint "${entrypoint}" does not exist`
3599
- };
3600
- }
3601
- return new Promise((resolveRun) => {
3602
- let output = "";
3603
- const child = spawn3(runtime, [entrypoint], {
3604
- cwd: worktreePath,
3605
- shell: false,
3606
- stdio: ["ignore", "pipe", "pipe"],
3607
- env: { ...process.env, ...env }
3608
- });
3609
- child.stdout?.on("data", (d) => output += d);
3610
- child.stderr?.on("data", (d) => output += d);
3611
- child.on(
3612
- "error",
3613
- (err) => resolveRun({
3614
- ran: true,
3615
- ok: false,
3616
- output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
3617
- })
3618
- );
3619
- child.on(
3620
- "close",
3621
- (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
3622
- );
3623
- });
3624
- }
3625
3771
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
3626
3772
  const trimmed = sourcePath.trim();
3627
3773
  if (!trimmed) {
@@ -3635,8 +3781,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3635
3781
  } catch {
3636
3782
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3637
3783
  }
3638
- const relPath = join9(ingestDir, basename2(source));
3639
- const dest = join9(worktreePath, relPath);
3784
+ const relPath = join8(ingestDir, basename2(source));
3785
+ const dest = join8(worktreePath, relPath);
3640
3786
  try {
3641
3787
  await mkdir6(dirname7(dest), { recursive: true });
3642
3788
  await copyFile(source, dest);
@@ -3651,11 +3797,28 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3651
3797
  function hasEnvVar(content, name) {
3652
3798
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3653
3799
  }
3800
+ async function readEnvVar(worktreePath, name) {
3801
+ let content;
3802
+ try {
3803
+ content = await readFile6(join8(worktreePath, ".env"), "utf8");
3804
+ } catch (err) {
3805
+ if (err.code !== "ENOENT") throw err;
3806
+ return void 0;
3807
+ }
3808
+ const match = new RegExp(
3809
+ `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3810
+ "m"
3811
+ ).exec(content);
3812
+ if (!match) return void 0;
3813
+ const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3814
+ if (!value || value.startsWith("<")) return void 0;
3815
+ return value;
3816
+ }
3654
3817
  async function writeSearchEnvValues(worktreePath, vars) {
3655
- const target = join9(worktreePath, ".env");
3818
+ const target = join8(worktreePath, ".env");
3656
3819
  let existing = "";
3657
3820
  try {
3658
- existing = await readFile7(target, "utf8");
3821
+ existing = await readFile6(target, "utf8");
3659
3822
  } catch (err) {
3660
3823
  if (err.code !== "ENOENT") throw err;
3661
3824
  }
@@ -3724,15 +3887,15 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3724
3887
  }
3725
3888
 
3726
3889
  // src/lib/algoliaDocs.ts
3727
- import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3728
- import { dirname as dirname8, join as join10 } from "node:path";
3890
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
3891
+ import { dirname as dirname8, join as join9 } from "node:path";
3729
3892
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3730
- var DOCS_SUBPATH = join10("docs", "algolia-sdk");
3893
+ var DOCS_SUBPATH = join9("docs", "algolia-sdk");
3731
3894
  function findDocsDir() {
3732
3895
  let dir = dirname8(fileURLToPath2(import.meta.url));
3733
3896
  for (; ; ) {
3734
- const candidate = join10(dir, DOCS_SUBPATH);
3735
- if (existsSync2(candidate)) return candidate;
3897
+ const candidate = join9(dir, DOCS_SUBPATH);
3898
+ if (existsSync(candidate)) return candidate;
3736
3899
  const parent = dirname8(dir);
3737
3900
  if (parent === dir) return void 0;
3738
3901
  dir = parent;
@@ -3754,7 +3917,7 @@ function loadAlgoliaDoc(language) {
3754
3917
  );
3755
3918
  return "";
3756
3919
  }
3757
- return readFileSync(join10(docsDir, files[0]), "utf8").trim();
3920
+ return readFileSync(join9(docsDir, files[0]), "utf8").trim();
3758
3921
  }
3759
3922
  function getNamedDoc(name, language) {
3760
3923
  const docsDir = findDocsDir();
@@ -3762,14 +3925,15 @@ function getNamedDoc(name, language) {
3762
3925
  logger.warn("docs/algolia-sdk not found");
3763
3926
  return "";
3764
3927
  }
3765
- const file = join10(docsDir, `${name}-${language}.md`);
3766
- if (!existsSync2(file)) {
3928
+ const file = join9(docsDir, `${name}-${language}.md`);
3929
+ if (!existsSync(file)) {
3767
3930
  logger.warn({ name, language }, "named SDK reference not found");
3768
3931
  return "";
3769
3932
  }
3770
3933
  return readFileSync(file, "utf8").trim();
3771
3934
  }
3772
3935
  function getFrameworkSpecificDoc(frameworks) {
3936
+ if (frameworks.length === 0) return "";
3773
3937
  const fw = frameworks.map((f) => f.toLowerCase());
3774
3938
  if (fw.includes("vue") || fw.includes("nuxt")) {
3775
3939
  return loadAlgoliaDoc("vue");
@@ -3777,9 +3941,6 @@ function getFrameworkSpecificDoc(frameworks) {
3777
3941
  if (fw.includes("react") || fw.includes("next.js")) {
3778
3942
  return loadAlgoliaDoc("react");
3779
3943
  }
3780
- if (fw.includes("angular")) {
3781
- return loadAlgoliaDoc("angular");
3782
- }
3783
3944
  return loadAlgoliaDoc("js");
3784
3945
  }
3785
3946
 
@@ -3807,11 +3968,7 @@ var implementSchema = z26.object({
3807
3968
  });
3808
3969
  var implementationOutputSchema = z26.object({
3809
3970
  summary: z26.string(),
3810
- // Ingestion only: a structured pair the wizard turns into an argv, never a
3811
- // free-form command string. `runtime` is allowlisted and `entrypoint` is
3812
- // validated worktree-relative, so the agent cannot inject extra commands.
3813
- runtime: z26.enum(INGEST_RUNTIMES).optional(),
3814
- entrypoint: z26.string().optional()
3971
+ ingestCommand: z26.string().optional()
3815
3972
  });
3816
3973
  var verificationOutputSchema = z26.object({
3817
3974
  summary: z26.string(),
@@ -3821,30 +3978,35 @@ var verificationOutputSchema = z26.object({
3821
3978
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3822
3979
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
3823
3980
  var INGEST_DIR = ".algolia-wizard";
3824
- function detectUiFramework(language) {
3825
- const names = language.frameworks.map((f) => f.name.toLowerCase());
3826
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3827
- if (names.some((n) => n.includes("react") || n.includes("next")))
3828
- return "React";
3829
- if (names.some((n) => n.includes("angular"))) return "Angular";
3830
- return "JavaScript";
3831
- }
3832
- function frameworksForDoc(framework) {
3833
- switch (framework) {
3834
- case "React":
3835
- return ["react"];
3836
- case "Vue":
3837
- return ["vue"];
3838
- case "Angular":
3839
- return ["angular"];
3840
- case "JavaScript":
3841
- return [];
3842
- }
3981
+ var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
3982
+ function lower(entries) {
3983
+ return entries.map((entry) => entry.name.toLowerCase());
3843
3984
  }
3844
- function publicEnvPrefix(language) {
3845
- const frameworkNames = language.frameworks.map(
3846
- (framework) => framework.name.toLowerCase()
3985
+ function isJsProject(language) {
3986
+ return lower(language.languages).some(
3987
+ (name) => JS_LANGUAGES.some((js) => name.includes(js))
3847
3988
  );
3989
+ }
3990
+ var UI_FRAMEWORKS = [
3991
+ { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
3992
+ { match: ["react", "next"], target: "React", doc: "react" },
3993
+ { match: ["angular"], target: "Angular" }
3994
+ ];
3995
+ function matchUiFramework(language) {
3996
+ const names = lower(language.frameworks);
3997
+ return UI_FRAMEWORKS.find(
3998
+ (ui) => ui.match.some((needle) => names.some((name) => name.includes(needle)))
3999
+ );
4000
+ }
4001
+ function searchUiTarget(language) {
4002
+ return matchUiFramework(language)?.target ?? language.frameworks[0]?.name ?? (isJsProject(language) ? "JavaScript" : "this project");
4003
+ }
4004
+ function frameworksForDoc(language) {
4005
+ if (!isJsProject(language)) return [];
4006
+ return [matchUiFramework(language)?.doc ?? "js"];
4007
+ }
4008
+ function publicEnvPrefix(language) {
4009
+ const frameworkNames = lower(language.frameworks);
3848
4010
  if (frameworkNames.some((name) => name.includes("next"))) {
3849
4011
  return "NEXT_PUBLIC_";
3850
4012
  }
@@ -3857,17 +4019,24 @@ function publicEnvPrefix(language) {
3857
4019
  if (frameworkNames.some((name) => name.includes("vite"))) {
3858
4020
  return "VITE_";
3859
4021
  }
3860
- return "PUBLIC_";
4022
+ return isJsProject(language) ? "PUBLIC_" : "";
4023
+ }
4024
+ var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4025
+ var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4026
+ function appIdVar(language) {
4027
+ return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
4028
+ }
4029
+ function searchKeyVar(language) {
4030
+ return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3861
4031
  }
3862
4032
  function searchEnvVars(language, appId, searchKey) {
3863
- const prefix = publicEnvPrefix(language);
3864
4033
  return [
3865
4034
  {
3866
- name: `${prefix}ALGOLIA_APP_ID`,
4035
+ name: appIdVar(language),
3867
4036
  value: appId ?? "<your-algolia-app-id>"
3868
4037
  },
3869
4038
  {
3870
- name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
4039
+ name: searchKeyVar(language),
3871
4040
  value: searchKey ?? "<your-algolia-search-only-api-key>"
3872
4041
  }
3873
4042
  ];
@@ -3876,7 +4045,10 @@ function baseInstructions(input) {
3876
4045
  return [
3877
4046
  `Target Algolia index: ${input.targetIndex}`,
3878
4047
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3879
- "Make minimal, idiomatic changes; do not touch unrelated code."
4048
+ "Make minimal, idiomatic changes; do not touch unrelated code.",
4049
+ `Commands run through a shell on ${process.platform}. Write commands that work there.`,
4050
+ "Use the project's own tooling for every command \u2014 its package manager, task runner, and test/lint commands. Do not assume a JavaScript toolchain.",
4051
+ "runShell needs the developer to approve each command, so give every call a clear `explanation` naming what it does and any side effect. If a command is rejected, do not retry it \u2014 take a different approach or report the limitation."
3880
4052
  ];
3881
4053
  }
3882
4054
  function sourceSpecificInstructions(input) {
@@ -3896,43 +4068,60 @@ function sourceSpecificInstructions(input) {
3896
4068
  generated: [
3897
4069
  "No real data source exists; use sample records for each confirmed entity.",
3898
4070
  "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.",
3899
- "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.",
4071
+ "In the script, read and parse each returned file path at runtime using your language's standard JSON support, instead of inlining the records as literals.",
3900
4072
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
3901
4073
  ]
3902
4074
  };
3903
4075
  return byLine[input.ingestionSource];
3904
4076
  }
4077
+ function algoliaClientDoc(input) {
4078
+ const doc = getNamedDoc("save-records", "js");
4079
+ if (!doc) return [];
4080
+ if (isJsProject(input.language)) return [doc];
4081
+ return [
4082
+ "The reference below is written in JavaScript. Use it for the method names, arguments, and record shape, then translate to this project's language and its official Algolia client:",
4083
+ doc
4084
+ ];
4085
+ }
3905
4086
  function ingestionInstructions(input) {
3906
4087
  return [
3907
4088
  ...input.confirmed && input.confirmed.length ? [
3908
4089
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3909
4090
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3910
4091
  `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.`,
3911
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4092
+ "Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
3912
4093
  "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.",
3913
- getNamedDoc("save-records", "js"),
3914
- 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4094
+ ...algoliaClientDoc(input),
4095
+ "Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
4096
+ 'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
3915
4097
  "The summary should be extremely concise.",
3916
- `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.`,
3917
4098
  ...sourceSpecificInstructions(input)
3918
4099
  ] : []
3919
4100
  ];
3920
4101
  }
3921
4102
  function searchInstructions(input) {
3922
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4103
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.language));
3923
4104
  return [
3924
4105
  "Implement an in-app Algolia search experience.",
3925
- `Build the search UI for ${input.uiFramework}.`,
3926
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3927
- doc,
3928
- `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.`,
3929
- "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.",
3930
- `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
4106
+ `Build the search UI for ${input.searchUiTarget}.`,
4107
+ ...doc ? [
4108
+ "Follow the Algolia SDK reference below for client setup and search UI wiring; prefer it over prior knowledge:",
4109
+ doc
4110
+ ] : [
4111
+ "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
4112
+ ],
4113
+ `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 search input and results list against the "${input.targetIndex}" index.`,
4114
+ "Read the App ID and a search-only API key from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4115
+ // The key is provisioned only after verification passes, so the agent never
4116
+ // sees one. It must also leave .env alone: the wizard reads that file to
4117
+ // decide whether a key already exists, and an agent-invented value there
4118
+ // would be reused as if it were real.
4119
+ `Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
3931
4120
  // Not the agent's to rename: the wizard writes these exact names into
3932
4121
  // ".env" right after this step, so a renamed prefix would leave the code
3933
4122
  // reading a var the wizard never wrote.
3934
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3935
- '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.',
4123
+ `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4124
+ "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
3936
4125
  "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."
3937
4126
  ];
3938
4127
  }
@@ -3940,11 +4129,12 @@ function verificationInstructions(input) {
3940
4129
  return [
3941
4130
  "Verify the Algolia implementation changes in the current worktree.",
3942
4131
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
3943
- "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
3944
- "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
3945
- "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.",
4132
+ "Run the project's own checks (lint, type check, tests) via runShell, using the commands the project actually defines \u2014 its task runner, manifest scripts, or Makefile. Run every check that applies, not just the first.",
4133
+ "This worktree starts with no installed dependencies. If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
4134
+ "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4135
+ "Do not make speculative fixes when no checks exist, a check cannot run, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
3946
4136
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
3947
- `Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
4137
+ `Do not modify "${input.ingestDir}/" unless a check reports an actionable issue in its files.`,
3948
4138
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
3949
4139
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
3950
4140
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -3965,14 +4155,15 @@ var IMPLEMENT_CONFIG = {
3965
4155
  }
3966
4156
  };
3967
4157
  var useCaseToolMap = {
3968
- ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials", "notifyUser"],
3969
- search: [...FS_READ_TOOLS, "writeFile", "notifyUser"],
3970
- verification: [
4158
+ ingestion: [
3971
4159
  ...FS_READ_TOOLS,
3972
4160
  "writeFile",
3973
- "verifyImplementation",
4161
+ "writeCredentials",
4162
+ "runShell",
3974
4163
  "notifyUser"
3975
- ]
4164
+ ],
4165
+ search: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"],
4166
+ verification: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"]
3976
4167
  };
3977
4168
  function toolsForUseCase(useCase, ingestionSource) {
3978
4169
  const tools = useCaseToolMap[useCase];
@@ -3995,15 +4186,31 @@ function formatSummary(useCase, summary) {
3995
4186
  const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3996
4187
  return `${label}: ${summary}`;
3997
4188
  }
3998
- function buildIngestCommand(worktree, runtime, entrypoint) {
3999
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4000
- }
4001
4189
  function parseIngestRecordCount(output) {
4002
4190
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
4003
4191
  if (!match) return void 0;
4004
4192
  const count = Number(match[1]);
4005
4193
  return Number.isFinite(count) ? count : void 0;
4006
4194
  }
4195
+ function ingestOutcome(executions, ingestCommand) {
4196
+ const newestFirst = [...executions].reverse();
4197
+ const withCount = newestFirst.filter(
4198
+ (e) => parseIngestRecordCount(e.output ?? "") != null
4199
+ );
4200
+ const succeeded = newestFirst.filter((e) => e.approved && e.exitCode === 0);
4201
+ return {
4202
+ run: succeeded.find((e) => e.command === ingestCommand) ?? succeeded.find((e) => withCount.includes(e)),
4203
+ recordCount: parseIngestRecordCount(withCount[0]?.output ?? "")
4204
+ };
4205
+ }
4206
+ var PLACEHOLDER_WRITE_KEY = "ALGOLIA_WRITE_KEY_PLACEHOLDER";
4207
+ function makeToolContext(worktree, env = async () => ({})) {
4208
+ return createToolContext(
4209
+ DEFAULT_TOOL_LIMITS,
4210
+ worktree,
4211
+ createShellContext({ env, approve: storeApproval(worktree) })
4212
+ );
4213
+ }
4007
4214
  function verificationRetryInstructions(verification) {
4008
4215
  return [
4009
4216
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
@@ -4082,17 +4289,12 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4082
4289
  const confirmed2 = normalized.confirmedEntities;
4083
4290
  const searchLocation = normalized.searchImplementationAnalysis;
4084
4291
  let appId;
4085
- let searchKey;
4292
+ let ingestAppId;
4086
4293
  if (useCases.includes("search")) {
4087
4294
  appId = (await requireApplication()).id;
4088
- try {
4089
- searchKey = await resolveSearchOnlyKey(targetIndex);
4090
- } catch (err) {
4091
- logger.warn(
4092
- { err: err.message },
4093
- "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
4094
- );
4095
- }
4295
+ }
4296
+ if (useCases.includes("ingestion")) {
4297
+ ingestAppId = appId ?? (await requireApplication()).id;
4096
4298
  }
4097
4299
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
4098
4300
  try {
@@ -4124,51 +4326,58 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4124
4326
  targetIndex,
4125
4327
  language,
4126
4328
  appId,
4127
- searchKey,
4128
- searchEnvVars: searchEnvVars(language, appId, searchKey),
4329
+ // Names only: the search-only key is provisioned after verification, so
4330
+ // every value here is still a placeholder when the agent reads them.
4331
+ searchEnvVars: searchEnvVars(language, appId),
4129
4332
  ingestDir: INGEST_DIR,
4130
4333
  ingestionSource,
4131
4334
  uploadFilePath,
4132
- uiFramework: detectUiFramework(language)
4335
+ searchUiTarget: searchUiTarget(language)
4133
4336
  };
4134
4337
  const summaries = [];
4135
4338
  if (uploadWarning) summaries.push(uploadWarning);
4339
+ let envSearchKey;
4340
+ let envAppIdMismatch = false;
4341
+ if (useCases.includes("search") && appId) {
4342
+ const envAppId = await readEnvVar(worktree, appIdVar(language));
4343
+ if (envAppId === appId) {
4344
+ envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
4345
+ } else if (envAppId) {
4346
+ envAppIdMismatch = true;
4347
+ summaries.push(
4348
+ `\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
4349
+ );
4350
+ logger.warn(
4351
+ { envAppId, appId },
4352
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4353
+ );
4354
+ }
4355
+ }
4356
+ let finalSearchEnvVars = input.searchEnvVars;
4136
4357
  let agentRuns = 0;
4137
- let ingestRuntime;
4138
- let ingestEntrypoint;
4358
+ let ingestCommand;
4139
4359
  let ingestScriptRan = false;
4140
4360
  let ingestRecordCount;
4141
4361
  let ingestDurationMs;
4142
- let installFailed = false;
4143
4362
  let ingestOutcomeMessage;
4363
+ const ingestionTools = ingestAppId ? makeToolContext(worktree, async () => ({
4364
+ [APP_ID_VAR]: ingestAppId,
4365
+ [API_KEY_VAR]: PLACEHOLDER_WRITE_KEY
4366
+ })) : void 0;
4367
+ const searchTools = makeToolContext(worktree);
4144
4368
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4145
4369
  if (agentRuns > 0) ctx.recordStepExecution();
4146
4370
  agentRuns += 1;
4147
- const result = await runAgent({
4371
+ return runAgent({
4148
4372
  instructions: buildAgentInstructions(
4149
4373
  currentUseCase,
4150
4374
  input,
4151
4375
  extraInstructions
4152
4376
  ),
4153
4377
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4154
- outputSchema: implementationOutputSchema
4378
+ outputSchema: implementationOutputSchema,
4379
+ toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4155
4380
  });
4156
- ctx.notify({
4157
- messages: [`Installing dependencies for ${currentUseCase}\u2026`]
4158
- });
4159
- const installLogId = ctx.logStart("installWorktreeDeps", {
4160
- useCase: currentUseCase
4161
- });
4162
- const install = await installWorktreeDeps(worktree);
4163
- ctx.logEnd(installLogId, install.ok ? "success" : "error");
4164
- if (!install.ok) {
4165
- installFailed = true;
4166
- logger.warn(
4167
- { useCase: currentUseCase, output: install.output },
4168
- "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
4169
- );
4170
- }
4171
- return result;
4172
4381
  }
4173
4382
  async function runVerificationUseCase() {
4174
4383
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -4176,110 +4385,55 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4176
4385
  return runAgent({
4177
4386
  instructions: buildAgentInstructions("verification", input),
4178
4387
  tools: toolsForUseCase("verification"),
4179
- outputSchema: verificationOutputSchema
4388
+ outputSchema: verificationOutputSchema,
4389
+ toolContext: searchTools
4180
4390
  });
4181
4391
  }
4182
4392
  if (useCases.includes("ingestion")) {
4183
- const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4184
- summaries.push(formatSummary("ingestion", summary));
4185
- ingestRuntime = runtime;
4186
- ingestEntrypoint = entrypoint;
4187
- if (ingestRuntime && ingestEntrypoint && !installFailed) {
4188
- ctx.clearNotices();
4189
- const runNow = await ctx.requestUserInput({
4190
- prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
4191
- promptType: "acceptReject",
4192
- options: ["Yes", "No"],
4193
- messages: []
4194
- }) === true;
4195
- if (runNow) {
4196
- const ingestApp = await requireApplication();
4197
- const writeKey = await resolveWriteKey(targetIndex);
4198
- ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
4199
- const scriptLogId = ctx.logStart("runIngestScript", {
4200
- runtime: ingestRuntime,
4201
- entrypoint: ingestEntrypoint
4393
+ const result = await runImplementationUseCase("ingestion");
4394
+ summaries.push(formatSummary("ingestion", result.summary));
4395
+ ingestCommand = result.ingestCommand;
4396
+ const executions = (ingestionTools ?? searchTools).shell.executions;
4397
+ const { run: ingestRun, recordCount } = ingestOutcome(
4398
+ executions,
4399
+ ingestCommand
4400
+ );
4401
+ ingestScriptRan = ingestRun != null;
4402
+ ingestRecordCount = recordCount;
4403
+ ingestDurationMs = ingestRun?.durationMs;
4404
+ if (ingestScriptRan) {
4405
+ ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4406
+ if (ingestRecordCount != null) {
4407
+ track("AI Wizard Ingest Successful", {
4408
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4409
+ record_count: ingestRecordCount,
4410
+ duration_ms: ingestDurationMs ?? 0
4202
4411
  });
4203
- const startedAt = Date.now();
4204
- const run2 = await runIngestScript(
4205
- worktree,
4206
- ingestRuntime,
4207
- ingestEntrypoint,
4208
- {
4209
- [APP_ID_VAR]: ingestApp.id,
4210
- [API_KEY_VAR]: writeKey
4211
- }
4212
- );
4213
- ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
4214
- ingestScriptRan = run2.ran && run2.ok;
4215
- if (ingestScriptRan) {
4216
- ingestDurationMs = Date.now() - startedAt;
4217
- ingestRecordCount = parseIngestRecordCount(run2.output);
4218
- if (ingestRecordCount != null) {
4219
- track("AI Wizard Ingest Successful", {
4220
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4221
- record_count: ingestRecordCount,
4222
- duration_ms: ingestDurationMs
4223
- });
4224
- }
4225
- }
4226
- let summaryLine;
4227
- let outcomeMessage;
4228
- if (!run2.ran) {
4229
- summaryLine = `\u26A0\uFE0F Skipped running the ingestion script: ${run2.reason}`;
4230
- outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
4231
- logger.warn(
4232
- {
4233
- runtime: ingestRuntime,
4234
- entrypoint: ingestEntrypoint,
4235
- reason: run2.reason
4236
- },
4237
- "implement: refused to auto-run ingestion script"
4238
- );
4239
- track("Error", {
4240
- step: "Push Data",
4241
- error: `ingestion script skipped: ${run2.reason}`,
4242
- product_area: "AI Wizard"
4243
- });
4244
- } else if (run2.ok) {
4245
- const status = "Ingestion run: succeeded.";
4246
- summaryLine = run2.output ? `${status}
4247
- ${run2.output}` : status;
4248
- outcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4249
- } else {
4250
- const status = "\u26A0\uFE0F Ingestion run failed:";
4251
- summaryLine = run2.output ? `${status}
4252
- ${run2.output}` : status;
4253
- outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
4254
- logger.warn(
4255
- {
4256
- runtime: ingestRuntime,
4257
- entrypoint: ingestEntrypoint,
4258
- output: run2.output
4259
- },
4260
- "implement: ingestion script run failed"
4261
- );
4262
- track("Error", {
4263
- step: "Push Data",
4264
- error: run2.output || "ingestion script exited non-zero",
4265
- product_area: "AI Wizard"
4266
- });
4267
- }
4268
- summaries.push(summaryLine);
4269
- ingestOutcomeMessage = outcomeMessage;
4270
4412
  }
4413
+ } else {
4414
+ const rejected = executions.some((e) => !e.approved);
4415
+ const reason = rejected ? "you declined to run it" : "no successful run was recorded";
4416
+ ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4417
+ summaries.push(`\u26A0\uFE0F The ingestion script did not run: ${reason}.`);
4418
+ logger.warn(
4419
+ { ingestCommand, rejected, commandsRun: executions.length },
4420
+ "implement: ingestion script did not complete successfully"
4421
+ );
4422
+ track("Error", {
4423
+ step: "Push Data",
4424
+ error: `ingestion did not run: ${reason}`,
4425
+ product_area: "AI Wizard"
4426
+ });
4271
4427
  }
4272
4428
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4273
- if (ingestRuntime && ingestEntrypoint) {
4274
- commandMessages.push(
4275
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4276
- );
4429
+ if (ingestCommand) {
4430
+ commandMessages.push(`Ingestion command: ${ingestCommand}`);
4277
4431
  }
4278
4432
  await ctx.requestUserInput({
4279
4433
  prompt: "",
4280
4434
  promptType: "enterToContinue",
4281
4435
  options: [],
4282
- messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4436
+ messages: [ingestOutcomeMessage, ...commandMessages]
4283
4437
  });
4284
4438
  }
4285
4439
  if (useCases.includes("search")) {
@@ -4324,7 +4478,29 @@ ${run2.output}` : status;
4324
4478
  }
4325
4479
  extraInstructions = verificationRetryInstructions(verification);
4326
4480
  }
4327
- const resolvedSearchEnvVars = input.searchEnvVars.filter(
4481
+ let searchKey;
4482
+ let searchKeyError;
4483
+ if (appId) {
4484
+ try {
4485
+ const resolved = await resolveSearchOnlyKey(
4486
+ targetIndex,
4487
+ appId,
4488
+ envSearchKey
4489
+ );
4490
+ searchKey = resolved.key;
4491
+ summaries.push(
4492
+ resolved.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
4493
+ );
4494
+ } catch (err) {
4495
+ searchKeyError = err.message;
4496
+ logger.warn(
4497
+ { err: searchKeyError },
4498
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4499
+ );
4500
+ }
4501
+ }
4502
+ finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4503
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4328
4504
  (v) => !v.value.startsWith("<")
4329
4505
  );
4330
4506
  if (resolvedSearchEnvVars.length > 0) {
@@ -4335,13 +4511,29 @@ ${run2.output}` : status;
4335
4511
  if (written.length > 0) {
4336
4512
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4337
4513
  }
4514
+ const stale = [];
4515
+ for (const v of resolvedSearchEnvVars) {
4516
+ if (written.includes(v.name)) continue;
4517
+ const current = await readEnvVar(worktree, v.name);
4518
+ if (current && current !== v.value) stale.push(v);
4519
+ }
4520
+ if (stale.length > 0 && !envAppIdMismatch) {
4521
+ summaries.push(
4522
+ `\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
4523
+ );
4524
+ logger.warn(
4525
+ { vars: stale.map((v) => v.name) },
4526
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4527
+ );
4528
+ }
4338
4529
  }
4339
- const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4530
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4340
4531
  (v) => v.value.startsWith("<")
4341
4532
  );
4342
4533
  if (unresolvedSearchEnvVars.length > 0) {
4343
4534
  summaries.push(
4344
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4535
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
4536
+ (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4345
4537
  );
4346
4538
  }
4347
4539
  } else {
@@ -4353,27 +4545,18 @@ ${run2.output}` : status;
4353
4545
  "implement: agent reported success but no files changed in the worktree"
4354
4546
  );
4355
4547
  }
4356
- if (installFailed) {
4357
- summaries.push(
4358
- '\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".'
4359
- );
4360
- }
4361
4548
  return {
4362
4549
  ingestionSource,
4363
4550
  filesChanged,
4364
4551
  summary: summaries.join("\n\n"),
4365
4552
  worktreePath: worktree,
4366
- ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
4367
- ingestCommand: buildIngestCommand(
4368
- worktree,
4369
- ingestRuntime,
4370
- ingestEntrypoint
4371
- ),
4553
+ ...useCases.includes("ingestion") && ingestCommand ? {
4554
+ ingestCommand,
4372
4555
  ingestScriptRan,
4373
4556
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4374
4557
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4375
4558
  } : {},
4376
- ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4559
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4377
4560
  };
4378
4561
  } finally {
4379
4562
  process.chdir(repoRoot);
@@ -4696,7 +4879,7 @@ function parseCliArgs(argv) {
4696
4879
 
4697
4880
  // src/lib/resetState.ts
4698
4881
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4699
- import { join as join11 } from "node:path";
4882
+ import { join as join10 } from "node:path";
4700
4883
  var KEEP = ["wizard.log"];
4701
4884
  async function resetProjectState() {
4702
4885
  const dir = stateDir();
@@ -4708,13 +4891,13 @@ async function resetProjectState() {
4708
4891
  }
4709
4892
  const targets = entries.filter((name) => !KEEP.includes(name));
4710
4893
  await Promise.all(
4711
- targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
4894
+ targets.map((name) => rm2(join10(dir, name), { recursive: true, force: true }))
4712
4895
  );
4713
4896
  return { dir, removed: targets };
4714
4897
  }
4715
4898
 
4716
4899
  // src/main.tsx
4717
- import { jsx as jsx14 } from "react/jsx-runtime";
4900
+ import { jsx as jsx15 } from "react/jsx-runtime";
4718
4901
  async function startup() {
4719
4902
  let args;
4720
4903
  try {
@@ -4763,7 +4946,7 @@ ${formatStepList(workflow)}`);
4763
4946
  }
4764
4947
  async function run(workflow) {
4765
4948
  const store = useWizard.getState();
4766
- const instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4949
+ const instance = render(/* @__PURE__ */ jsx15(App, {}), { incrementalRendering: true });
4767
4950
  await store.waitForStart();
4768
4951
  let user = await getUser();
4769
4952
  if (!user) {