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

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,10 +13,60 @@ 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
+ var pinnedRoot;
30
+ function setProjectRoot(cwd) {
31
+ pinnedRoot = resolve(cwd);
32
+ }
33
+ function projectSlug(cwd = pinnedRoot ?? process.cwd()) {
34
+ return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
35
+ }
36
+ function stateDir(cwd = pinnedRoot ?? process.cwd()) {
37
+ return join(rootDir(), projectSlug(cwd));
38
+ }
39
+
40
+ // src/lib/logger.ts
41
+ var STDERR_FD = 2;
42
+ function resolveDest() {
43
+ const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
44
+ try {
45
+ mkdirSync(dirname(target), { recursive: true });
46
+ closeSync(openSync(target, "a"));
47
+ return target;
48
+ } catch {
49
+ return STDERR_FD;
50
+ }
51
+ }
52
+ function logDestination() {
53
+ return pino.destination({ dest: resolveDest(), sync: false });
54
+ }
55
+ var logger = pino(
56
+ { level: process.env.LOG_LEVEL ?? "info" },
57
+ logDestination()
58
+ );
59
+
60
+ // src/lib/algoliaCli.ts
16
61
  function npxArgs(args) {
17
62
  return ["--yes", "@algolia/cli@latest", ...args];
18
63
  }
19
64
  var shell = process.platform === "win32";
65
+ function childEnv(withoutAdminKey) {
66
+ if (!withoutAdminKey) return void 0;
67
+ const { ALGOLIA_API_KEY: _adminKey, ...rest } = process.env;
68
+ return rest;
69
+ }
20
70
  function lineSplitter(emit) {
21
71
  let buffer = "";
22
72
  return {
@@ -40,11 +90,14 @@ var stderrSink = (stream, line) => {
40
90
  if (stream === "stdout") return;
41
91
  wizardSink(stream, line);
42
92
  };
43
- function runAlgoliaCli(args, { onOutput } = {}) {
93
+ function runAlgoliaCli(args, { onOutput, withoutAdminKey } = {}) {
44
94
  const store = useWizard.getState();
45
95
  const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
46
96
  return new Promise((resolve4, reject) => {
47
- const child = spawn("npx", npxArgs(args), { shell });
97
+ const child = spawn("npx", npxArgs(args), {
98
+ shell,
99
+ env: childEnv(withoutAdminKey)
100
+ });
48
101
  let stdout = "";
49
102
  let stderr = "";
50
103
  const splitters = {
@@ -89,6 +142,7 @@ function runAlgoliaCli(args, { onOutput } = {}) {
89
142
  },
90
143
  (err) => {
91
144
  useWizard.getState().logEnd(logId, "error");
145
+ logger.warn({ err, args }, "Algolia CLI command failed");
92
146
  throw err;
93
147
  }
94
148
  );
@@ -146,45 +200,6 @@ function refreshAuthToken() {
146
200
  return inFlightRefresh;
147
201
  }
148
202
 
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
203
  // src/lib/proxyFetch.ts
189
204
  var PROXY_BASE_URL = process.env.PROXY_BASE_URL ?? "https://proxy-624203421261.us-east4.run.app";
190
205
  var PROXY_AUTH_REJECTED_HEADER = "x-wizard-proxy-auth";
@@ -614,10 +629,13 @@ function Notices() {
614
629
  }
615
630
 
616
631
  // src/ui/PromptInput.tsx
617
- import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
632
+ import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
618
633
  import TextInput from "ink-text-input";
619
634
  import { useState as useState5 } from "react";
620
635
 
636
+ // src/ui/CommandApproval.tsx
637
+ import { Box as Box5, Text as Text5, useInput } from "ink";
638
+
621
639
  // src/ui/NextAction.tsx
622
640
  import { Box as Box4, Text as Text4 } from "ink";
623
641
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -641,14 +659,69 @@ function NextAction({
641
659
  ] });
642
660
  }
643
661
 
662
+ // src/ui/CommandApproval.tsx
663
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
664
+ function CommandApproval({
665
+ command,
666
+ onDecide
667
+ }) {
668
+ useInput((input, key) => {
669
+ if (key.return) onDecide("approve");
670
+ else if (key.escape) onDecide("reject");
671
+ else if (input.toLowerCase() === "a") onDecide("always");
672
+ });
673
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
674
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, bold: true, children: "Run this command?" }),
675
+ /* @__PURE__ */ jsxs4(
676
+ Box5,
677
+ {
678
+ flexDirection: "column",
679
+ paddingLeft: 2,
680
+ borderStyle: "single",
681
+ borderColor: COLORS.success,
682
+ borderTop: false,
683
+ borderBottom: false,
684
+ borderRight: false,
685
+ gap: 1,
686
+ children: [
687
+ /* @__PURE__ */ jsxs4(Box5, { children: [
688
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "$ " }),
689
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.strong, wrap: "wrap", children: command.command })
690
+ ] }),
691
+ /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
692
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "in:" }),
693
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, wrap: "wrap", children: command.cwd })
694
+ ] }),
695
+ command.explanation && /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
696
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "why:" }),
697
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.accent, wrap: "wrap", children: command.explanation })
698
+ ] })
699
+ ]
700
+ }
701
+ ),
702
+ /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
703
+ /* @__PURE__ */ jsx4(NextAction, { action: "approve", keyHint: "enter" }),
704
+ /* @__PURE__ */ jsx4(NextAction, { action: "reject", keyHint: "esc", hierarchy: "secondary" }),
705
+ /* @__PURE__ */ jsx4(
706
+ NextAction,
707
+ {
708
+ action: "approve, and don't ask again for this command",
709
+ keyHint: "a",
710
+ hierarchy: "secondary"
711
+ }
712
+ )
713
+ ] })
714
+ ] });
715
+ }
716
+
644
717
  // src/ui/SelectPrompt.tsx
645
- import { Box as Box6, Text as Text6, useInput, useWindowSize as useWindowSize5 } from "ink";
718
+ import { Box as Box7, Text as Text7, useInput as useInput2, useWindowSize as useWindowSize5 } from "ink";
646
719
  import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
647
720
 
648
721
  // src/ui/ScrollView.tsx
649
- import { Box as Box5, Text as Text5, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
722
+ import { Box as Box6, Text as Text6, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
650
723
  import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
651
- import { jsxs as jsxs4 } from "react/jsx-runtime";
724
+ import { jsxs as jsxs5 } from "react/jsx-runtime";
652
725
  var INDICATOR_ROWS = 2;
653
726
  function fittedWidth(node, columns) {
654
727
  let left = 0;
@@ -718,14 +791,14 @@ function useScrollWindow({
718
791
  };
719
792
  }
720
793
  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: [
794
+ return /* @__PURE__ */ jsxs5(Box6, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
795
+ scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
723
796
  "\u2191 ",
724
797
  scroll.hiddenAbove,
725
798
  " more"
726
799
  ] }),
727
800
  children,
728
- scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
801
+ scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
729
802
  "\u2193 ",
730
803
  scroll.hiddenBelow,
731
804
  " more"
@@ -734,7 +807,7 @@ function ScrollView({ scroll, children }) {
734
807
  }
735
808
 
736
809
  // src/ui/SelectPrompt.tsx
737
- import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
810
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
738
811
  var CANCEL = "cancel";
739
812
  var ARROW_WIDTH = 4;
740
813
  var COLUMN_GAP = 2;
@@ -796,7 +869,7 @@ function SelectPrompt({
796
869
  revealIndex(index);
797
870
  }, [index, revealIndex]);
798
871
  const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
799
- useInput((input, key) => {
872
+ useInput2((input, key) => {
800
873
  if (rows.length === 0) return;
801
874
  if (key.upArrow || input === "k") {
802
875
  setIndex((i) => (i - 1 + rows.length) % rows.length);
@@ -819,56 +892,56 @@ function SelectPrompt({
819
892
  }
820
893
  }
821
894
  });
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 })
895
+ return /* @__PURE__ */ jsx5(Box7, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, width, children: [
896
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
897
+ error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: error }),
898
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
899
+ table && /* @__PURE__ */ jsx5(Table, { columns: table.columns, rows: table.rows }),
900
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
901
+ question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: question }),
902
+ helpText && /* @__PURE__ */ jsx5(Text7, { color: COLORS.dim, children: helpText })
830
903
  ] })
831
904
  ] }),
832
- /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
905
+ /* @__PURE__ */ jsx5(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
833
906
  const i = scroll.offset + visibleIndex;
834
907
  const highlighted = i === index;
835
908
  const isCancel = i === cancelIndex;
836
909
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
837
910
  const sec = isCancel ? void 0 : secondary?.[i];
838
911
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
839
- const label = /* @__PURE__ */ jsxs5(Text6, { color: labelColor, wrap: "truncate", children: [
912
+ const label = /* @__PURE__ */ jsxs6(Text7, { color: labelColor, wrap: "truncate", children: [
840
913
  highlighted ? "\u276F " : " ",
841
914
  bullet,
842
915
  option
843
916
  ] });
844
917
  const isText = sec?.kind === "text";
845
- return /* @__PURE__ */ jsxs5(
846
- Box6,
918
+ return /* @__PURE__ */ jsxs6(
919
+ Box7,
847
920
  {
848
921
  width: isText ? "100%" : barWidth,
849
922
  paddingX: 1,
850
923
  paddingY: 1,
851
924
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
852
925
  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,
926
+ /* @__PURE__ */ jsx5(Box7, { width: isText ? labelWidth : barLabelWidth, children: label }),
927
+ isText && textWidth > 0 && /* @__PURE__ */ jsx5(Box7, { width: textWidth, children: /* @__PURE__ */ jsx5(
928
+ Text7,
856
929
  {
857
930
  wrap: "truncate",
858
931
  color: highlighted ? COLORS.primary : COLORS.muted,
859
932
  children: sec.value
860
933
  }
861
934
  ) }),
862
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box6, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text6, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
935
+ sec?.kind === "badge" && /* @__PURE__ */ jsx5(Box7, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text7, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
863
936
  ]
864
937
  },
865
938
  `row-${i}`
866
939
  );
867
940
  }) }),
868
- /* @__PURE__ */ jsx4(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text6, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs5(Text6, { children: [
941
+ /* @__PURE__ */ jsx5(Box7, { flexShrink: 0, children: /* @__PURE__ */ jsx5(Text7, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs6(Text7, { children: [
869
942
  i > 0 ? " " : "",
870
- /* @__PURE__ */ jsx4(Text6, { color: COLORS.primary, children: key }),
871
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
943
+ /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: key }),
944
+ /* @__PURE__ */ jsxs6(Text7, { color: COLORS.dim, children: [
872
945
  " ",
873
946
  label
874
947
  ] })
@@ -877,23 +950,23 @@ function SelectPrompt({
877
950
  }
878
951
 
879
952
  // src/ui/PromptInput.tsx
880
- import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
953
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
881
954
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
882
955
  function EnterToContinuePrompt({
883
956
  question,
884
957
  messages,
885
958
  onDecide
886
959
  }) {
887
- useInput2((_input, key) => {
960
+ useInput3((_input, key) => {
888
961
  if (key.return) onDecide(true);
889
962
  else if (key.escape) onDecide(false);
890
963
  });
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" })
964
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
965
+ messages?.map((m, i) => /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: m }, `msg-${i}`)),
966
+ question && /* @__PURE__ */ jsx6(Text8, { color: COLORS.primary, children: question }),
967
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
968
+ /* @__PURE__ */ jsx6(NextAction, { action: "continue", keyHint: "enter" }),
969
+ /* @__PURE__ */ jsx6(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
897
970
  ] })
898
971
  ] });
899
972
  }
@@ -901,11 +974,11 @@ function PromptInput() {
901
974
  const { phase, inputReq, submitInput } = useWizard();
902
975
  const [draft, setDraft] = useState5("");
903
976
  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" }) });
977
+ return /* @__PURE__ */ jsx6(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text8, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
905
978
  }
906
979
  if (phase !== "awaitingInput" || !inputReq) return null;
907
980
  if (inputReq.promptType === "multipleChoice") {
908
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
981
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
909
982
  SelectPrompt,
910
983
  {
911
984
  question: inputReq.prompt,
@@ -922,7 +995,7 @@ function PromptInput() {
922
995
  ) });
923
996
  }
924
997
  if (inputReq.promptType === "multiSelect") {
925
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
998
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
926
999
  SelectPrompt,
927
1000
  {
928
1001
  multi: true,
@@ -937,7 +1010,7 @@ function PromptInput() {
937
1010
  ) });
938
1011
  }
939
1012
  if (inputReq.promptType === "notice") {
940
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
1013
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
941
1014
  SelectPrompt,
942
1015
  {
943
1016
  question: inputReq.prompt,
@@ -948,7 +1021,7 @@ function PromptInput() {
948
1021
  ) });
949
1022
  }
950
1023
  if (inputReq.promptType === "enterToContinue") {
951
- return /* @__PURE__ */ jsx5(
1024
+ return /* @__PURE__ */ jsx6(
952
1025
  EnterToContinuePrompt,
953
1026
  {
954
1027
  question: inputReq.prompt,
@@ -957,9 +1030,12 @@ function PromptInput() {
957
1030
  }
958
1031
  );
959
1032
  }
1033
+ if (inputReq.promptType === "commandApproval" && inputReq.command) {
1034
+ return /* @__PURE__ */ jsx6(CommandApproval, { command: inputReq.command, onDecide: submitInput });
1035
+ }
960
1036
  if (inputReq.promptType === "acceptReject") {
961
1037
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
962
- return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
1038
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
963
1039
  SelectPrompt,
964
1040
  {
965
1041
  question: inputReq.prompt,
@@ -970,15 +1046,15 @@ function PromptInput() {
970
1046
  }
971
1047
  ) });
972
1048
  }
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: [
1049
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1050
+ inputReq.error && /* @__PURE__ */ jsx6(Text8, { color: COLORS.danger, children: inputReq.error }),
1051
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: m }, `msg-${i}`)),
1052
+ /* @__PURE__ */ jsxs7(Box8, { children: [
1053
+ /* @__PURE__ */ jsxs7(Text8, { color: COLORS.primary, children: [
978
1054
  inputReq.prompt,
979
1055
  " "
980
1056
  ] }),
981
- /* @__PURE__ */ jsx5(
1057
+ /* @__PURE__ */ jsx6(
982
1058
  TextInput,
983
1059
  {
984
1060
  value: draft,
@@ -996,7 +1072,7 @@ function PromptInput() {
996
1072
  // src/ui/Welcome.tsx
997
1073
  import { dirname as dirname2, join as join3 } from "node:path";
998
1074
  import { fileURLToPath } from "node:url";
999
- import { Box as Box8, Spacer, Text as Text8, useInput as useInput3, useWindowSize as useWindowSize6 } from "ink";
1075
+ import { Box as Box9, Spacer, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
1000
1076
 
1001
1077
  // src/ui/copy/welcome.ts
1002
1078
  var sidebarItems = [
@@ -1009,12 +1085,12 @@ var sidebarItems = [
1009
1085
  description: "push 100 records to Algolia in seconds"
1010
1086
  },
1011
1087
  {
1012
- title: "detect your framework",
1013
- description: "React, Vue, Angular, Vanilla JS"
1088
+ title: "detect your stack",
1089
+ description: "whatever language and framework you already use"
1014
1090
  },
1015
1091
  {
1016
1092
  title: "scaffold a search UI",
1017
- description: "a styled InstantSearch component, wired into your app"
1093
+ description: "a search box and results, wired into your app"
1018
1094
  },
1019
1095
  {
1020
1096
  title: "ship it",
@@ -1024,20 +1100,20 @@ var sidebarItems = [
1024
1100
 
1025
1101
  // src/ui/Welcome.tsx
1026
1102
  import Image, { InkPictureProvider } from "ink-picture";
1027
- import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
1103
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
1028
1104
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
1029
1105
  function SidebarItem({
1030
1106
  title,
1031
1107
  description
1032
1108
  }) {
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 })
1109
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
1110
+ /* @__PURE__ */ jsxs8(Box9, { gap: 1, children: [
1111
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, children: "\u2192" }),
1112
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: title })
1037
1113
  ] }),
1038
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 2, children: [
1039
- /* @__PURE__ */ jsx6(Spacer, {}),
1040
- /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: description })
1114
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 2, children: [
1115
+ /* @__PURE__ */ jsx7(Spacer, {}),
1116
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: description })
1041
1117
  ] })
1042
1118
  ] });
1043
1119
  }
@@ -1045,7 +1121,7 @@ function Welcome() {
1045
1121
  const confirmStart = useWizard((s) => s.confirmStart);
1046
1122
  const openLearnMore = useWizard((s) => s.openLearnMore);
1047
1123
  const { rows } = useWindowSize6();
1048
- useInput3((input, key) => {
1124
+ useInput4((input, key) => {
1049
1125
  if (key.return) confirmStart();
1050
1126
  else if (input === "i") openLearnMore();
1051
1127
  });
@@ -1063,16 +1139,16 @@ function Welcome() {
1063
1139
  if (rows < 30) {
1064
1140
  layout = scales["small"];
1065
1141
  }
1066
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1067
- /* @__PURE__ */ jsx6(
1068
- Box8,
1142
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1143
+ /* @__PURE__ */ jsx7(
1144
+ Box9,
1069
1145
  {
1070
1146
  paddingY: layout.main.padding.y,
1071
1147
  paddingX: layout.main.padding.x,
1072
1148
  flexDirection: "column",
1073
1149
  justifyContent: "center",
1074
- children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
1075
- /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
1150
+ children: /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", gap: 2, children: [
1151
+ /* @__PURE__ */ jsx7(InkPictureProvider, { children: /* @__PURE__ */ jsx7(
1076
1152
  Image,
1077
1153
  {
1078
1154
  src: IMAGE_PATH,
@@ -1083,16 +1159,16 @@ function Welcome() {
1083
1159
  protocol: "halfBlock"
1084
1160
  }
1085
1161
  ) }),
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" })
1162
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1163
+ /* @__PURE__ */ jsxs8(Box9, { gap: 1, flexDirection: "column", children: [
1164
+ /* @__PURE__ */ jsx7(NextAction, { action: "start wizard", keyHint: "enter" }),
1165
+ /* @__PURE__ */ jsx7(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
1090
1166
  ] })
1091
1167
  ] })
1092
1168
  }
1093
1169
  ),
1094
- /* @__PURE__ */ jsxs7(
1095
- Box8,
1170
+ /* @__PURE__ */ jsxs8(
1171
+ Box9,
1096
1172
  {
1097
1173
  backgroundColor: COLORS.bg.sidebar,
1098
1174
  width: 40,
@@ -1102,8 +1178,8 @@ function Welcome() {
1102
1178
  flexDirection: "column",
1103
1179
  justifyContent: "center",
1104
1180
  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))
1181
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1182
+ sidebarItems.map((i, idx) => /* @__PURE__ */ jsx7(SidebarItem, { title: i.title, description: i.description }, idx))
1107
1183
  ]
1108
1184
  }
1109
1185
  )
@@ -1112,7 +1188,7 @@ function Welcome() {
1112
1188
 
1113
1189
  // src/ui/LearnMore.tsx
1114
1190
  import { Fragment as Fragment2 } from "react";
1115
- import { Box as Box9, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize7 } from "ink";
1191
+ import { Box as Box10, Text as Text10, useInput as useInput5, useWindowSize as useWindowSize7 } from "ink";
1116
1192
 
1117
1193
  // src/ui/copy/learn-more.ts
1118
1194
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -1120,12 +1196,17 @@ var accessItems = [
1120
1196
  {
1121
1197
  tag: "READ",
1122
1198
  title: "Project files",
1123
- description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
1199
+ description: "reads manifests, configs & source to detect your stack. Read-only; nothing is uploaded."
1124
1200
  },
1125
1201
  {
1126
1202
  tag: "WRITE",
1127
1203
  title: "Code changes",
1128
- description: "creates & edits files (search UI, config). Shown as a diff first \u2014 nothing lands without your approval."
1204
+ description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1205
+ },
1206
+ {
1207
+ tag: "EXEC",
1208
+ title: "Setup commands",
1209
+ 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
1210
  },
1130
1211
  {
1131
1212
  tag: "NET",
@@ -1135,13 +1216,13 @@ var accessItems = [
1135
1216
  {
1136
1217
  tag: "KEY",
1137
1218
  title: "Credentials",
1138
- description: "saves your Admin API key to .env and adds it to .gitignore."
1219
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1139
1220
  }
1140
1221
  ];
1141
1222
  var neverItems = [
1142
1223
  "Send your source code to a model or third party",
1143
1224
  "Commit or push to git",
1144
- "Touch files outside your project directory"
1225
+ "Run a command you haven't approved"
1145
1226
  ];
1146
1227
  var policyLinks = [
1147
1228
  { label: "Terms", url: "https://www.algolia.com/policies/terms" },
@@ -1149,10 +1230,11 @@ var policyLinks = [
1149
1230
  ];
1150
1231
 
1151
1232
  // src/ui/LearnMore.tsx
1152
- import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
1233
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1153
1234
  var TAG_COLORS = {
1154
1235
  READ: COLORS.success,
1155
1236
  WRITE: COLORS.badge,
1237
+ EXEC: COLORS.danger,
1156
1238
  NET: COLORS.accent,
1157
1239
  KEY: COLORS.muted
1158
1240
  };
@@ -1165,12 +1247,12 @@ function NeverLine({
1165
1247
  }) {
1166
1248
  const used = segments.reduce((n, s) => n + s.text.length, 0);
1167
1249
  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" }),
1250
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
1251
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: "\u2502" }),
1170
1252
  " ".repeat(NEVER_BOX_PAD_X),
1171
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text9, { color: s.color, bold: s.bold, children: s.text }, i)),
1253
+ segments.map((s, i) => /* @__PURE__ */ jsx8(Text10, { color: s.color, bold: s.bold, children: s.text }, i)),
1172
1254
  " ".repeat(rightPad),
1173
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" })
1255
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: "\u2502" })
1174
1256
  ] });
1175
1257
  }
1176
1258
  function LearnMore() {
@@ -1178,12 +1260,12 @@ function LearnMore() {
1178
1260
  const backToHome = useWizard((s) => s.backToHome);
1179
1261
  const { columns } = useWindowSize7();
1180
1262
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1181
- useInput4((_input, key) => {
1263
+ useInput5((_input, key) => {
1182
1264
  if (key.escape) backToHome();
1183
1265
  else if (key.return) confirmStart();
1184
1266
  });
1185
- return /* @__PURE__ */ jsxs8(
1186
- Box9,
1267
+ return /* @__PURE__ */ jsxs9(
1268
+ Box10,
1187
1269
  {
1188
1270
  flexDirection: "column",
1189
1271
  paddingX: PADDING_X,
@@ -1191,31 +1273,31 @@ function LearnMore() {
1191
1273
  width: "100%",
1192
1274
  gap: 1,
1193
1275
  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}` })
1276
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1277
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: accessIntro }),
1278
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginTop: 1, children: [
1279
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1280
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1281
+ /* @__PURE__ */ jsx8(Box10, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text10, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1282
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text10, { children: [
1283
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: item.title }),
1284
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1203
1285
  ] }) })
1204
1286
  ] })
1205
1287
  ] }, 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(
1288
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
1289
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1290
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1291
+ /* @__PURE__ */ jsx8(
1210
1292
  NeverLine,
1211
1293
  {
1212
1294
  width: dividerWidth,
1213
1295
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1214
1296
  }
1215
1297
  ),
1216
- neverItems.map((item) => /* @__PURE__ */ jsxs8(Fragment2, { children: [
1217
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1218
- /* @__PURE__ */ jsx7(
1298
+ neverItems.map((item) => /* @__PURE__ */ jsxs9(Fragment2, { children: [
1299
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1300
+ /* @__PURE__ */ jsx8(
1219
1301
  NeverLine,
1220
1302
  {
1221
1303
  width: dividerWidth,
@@ -1227,24 +1309,24 @@ function LearnMore() {
1227
1309
  }
1228
1310
  )
1229
1311
  ] }, item)),
1230
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1231
- /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1312
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1313
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1232
1314
  ] }),
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 })
1315
+ /* @__PURE__ */ jsx8(Box10, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1316
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1317
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.accent, children: link.url })
1236
1318
  ] }, 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" })
1319
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1320
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1321
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "[" }),
1322
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "esc" }),
1323
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "] back" })
1242
1324
  ] }),
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" })
1325
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1326
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "[" }),
1327
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "enter" }),
1328
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "]" }),
1329
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.success, bold: true, children: "start wizard" })
1248
1330
  ] })
1249
1331
  ] })
1250
1332
  ]
@@ -1253,10 +1335,10 @@ function LearnMore() {
1253
1335
  }
1254
1336
 
1255
1337
  // src/ui/Sidebar.tsx
1256
- import { Box as Box12, Text as Text12 } from "ink";
1338
+ import { Box as Box13, Text as Text13 } from "ink";
1257
1339
 
1258
1340
  // src/ui/Steps.tsx
1259
- import { Box as Box10, Text as Text10 } from "ink";
1341
+ import { Box as Box11, Text as Text11 } from "ink";
1260
1342
  import Spinner from "ink-spinner";
1261
1343
 
1262
1344
  // src/core/persistence.ts
@@ -1285,12 +1367,12 @@ async function clearWorkflowState(workflowId) {
1285
1367
  }
1286
1368
 
1287
1369
  // src/ui/Steps.tsx
1288
- import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1370
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1289
1371
  function Steps() {
1290
1372
  const { steps } = useWizard();
1291
1373
  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],
1374
+ 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: [
1375
+ s.status === "running" ? /* @__PURE__ */ jsx9(Spinner, { type: "dots" }) : MARKER[s.status],
1294
1376
  " ",
1295
1377
  s.title
1296
1378
  ] }) }, s.id)) });
@@ -1299,27 +1381,27 @@ function CurrentStep() {
1299
1381
  const { steps } = useWizard();
1300
1382
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1301
1383
  if (!currentStep) return null;
1302
- return /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.running, children: [
1303
- /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1384
+ return /* @__PURE__ */ jsxs10(Text11, { color: COLORS.status.running, children: [
1385
+ /* @__PURE__ */ jsx9(Spinner, { type: "dots" }),
1304
1386
  " ",
1305
1387
  ` ${currentStep.title}`
1306
1388
  ] });
1307
1389
  }
1308
1390
 
1309
1391
  // 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";
1392
+ import { Box as Box12, Text as Text12 } from "ink";
1393
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1312
1394
  function Progress() {
1313
1395
  const { steps, currentStepIndex } = useWizard();
1314
1396
  const visibleSteps = steps.filter(isStepVisible);
1315
1397
  if (visibleSteps.length === 0) return null;
1316
1398
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1317
1399
  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 })
1400
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1401
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "STEP" }),
1402
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: activeStepNumber }),
1403
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: "/" }),
1404
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: visibleSteps.length })
1323
1405
  ] });
1324
1406
  }
1325
1407
 
@@ -1330,10 +1412,10 @@ var sidebarCommands = [
1330
1412
  ];
1331
1413
 
1332
1414
  // src/ui/Sidebar.tsx
1333
- import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1415
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1334
1416
  function Sidebar() {
1335
- return /* @__PURE__ */ jsxs11(
1336
- Box12,
1417
+ return /* @__PURE__ */ jsxs12(
1418
+ Box13,
1337
1419
  {
1338
1420
  backgroundColor: "#14171E",
1339
1421
  width: 30,
@@ -1342,16 +1424,16 @@ function Sidebar() {
1342
1424
  flexDirection: "column",
1343
1425
  justifyContent: "space-between",
1344
1426
  children: [
1345
- /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1346
- /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "PROGRESS" }),
1347
- /* @__PURE__ */ jsx10(Steps, {})
1427
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
1428
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: "PROGRESS" }),
1429
+ /* @__PURE__ */ jsx11(Steps, {})
1348
1430
  ] }),
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 })
1431
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
1432
+ /* @__PURE__ */ jsx11(Progress, {}),
1433
+ /* @__PURE__ */ jsx11(Box13, { flexDirection: "column", children: sidebarCommands.map((c) => {
1434
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1435
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1436
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: c.description })
1355
1437
  ] });
1356
1438
  }) })
1357
1439
  ] })
@@ -1361,12 +1443,12 @@ function Sidebar() {
1361
1443
  }
1362
1444
 
1363
1445
  // 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";
1446
+ import { Box as Box14, Text as Text14 } from "ink";
1447
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1366
1448
  function Ribbon() {
1367
1449
  const firstCommand = sidebarCommands[0];
1368
- return /* @__PURE__ */ jsxs12(
1369
- Box13,
1450
+ return /* @__PURE__ */ jsxs13(
1451
+ Box14,
1370
1452
  {
1371
1453
  backgroundColor: "#14171E",
1372
1454
  flexDirection: "row",
@@ -1374,11 +1456,11 @@ function Ribbon() {
1374
1456
  paddingX: 2,
1375
1457
  paddingY: 1,
1376
1458
  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 })
1459
+ /* @__PURE__ */ jsx12(Progress, {}),
1460
+ /* @__PURE__ */ jsx12(CurrentStep, {}),
1461
+ /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: 1, children: [
1462
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1463
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: firstCommand.description })
1382
1464
  ] })
1383
1465
  ]
1384
1466
  }
@@ -1389,8 +1471,8 @@ function Ribbon() {
1389
1471
  import { useState as useState6 } from "react";
1390
1472
 
1391
1473
  // 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";
1474
+ import { Box as Box15, Text as Text15, useInput as useInput6 } from "ink";
1475
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1394
1476
  var KIND_COLOR = {
1395
1477
  tool: COLORS.primary,
1396
1478
  prompt: COLORS.badge
@@ -1421,14 +1503,14 @@ function formatTimestamp(ms) {
1421
1503
  function Logs() {
1422
1504
  const logs = useWizard((s) => s.logs);
1423
1505
  const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1424
- useInput5((_input, key) => {
1506
+ useInput6((_input, key) => {
1425
1507
  if (key.upArrow) scroll.scrollBy(-1);
1426
1508
  else if (key.downArrow) scroll.scrollBy(1);
1427
1509
  });
1428
1510
  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) => {
1511
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1512
+ logs.length === 0 && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: "No logs yet." }),
1513
+ /* @__PURE__ */ jsx13(ScrollView, { scroll, children: visible.map((entry) => {
1432
1514
  const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1433
1515
  const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1434
1516
  const rawPreview = rawInputText(entry.input);
@@ -1438,14 +1520,14 @@ function Logs() {
1438
1520
  const name = truncate2(entry.name, budget);
1439
1521
  budget -= name.length;
1440
1522
  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 })
1523
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "row", gap: ROW_GAP, children: [
1524
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: timestamp }),
1525
+ /* @__PURE__ */ jsx13(Text15, { color: logNameColor(entry), wrap: "truncate", children: name }),
1526
+ preview && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, wrap: "truncate", children: preview }),
1527
+ durationText && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: durationText })
1446
1528
  ] }, entry.id);
1447
1529
  }) }),
1448
- /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1530
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1449
1531
  ] });
1450
1532
  }
1451
1533
 
@@ -1637,7 +1719,7 @@ function track(event, payload) {
1637
1719
  }
1638
1720
 
1639
1721
  // src/ui/App.tsx
1640
- import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1722
+ import { jsx as jsx14, jsxs as jsxs15 } from "react/jsx-runtime";
1641
1723
  function App() {
1642
1724
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1643
1725
  const { exit } = useApp();
@@ -1645,7 +1727,7 @@ function App() {
1645
1727
  const [showLogs, setShowLogs] = useState6(false);
1646
1728
  const finished = phase === "done" || phase === "error";
1647
1729
  const currentStep = steps[currentStepIndex];
1648
- useInput6(
1730
+ useInput7(
1649
1731
  (_input, key) => {
1650
1732
  if (key.return) {
1651
1733
  exit();
@@ -1653,7 +1735,7 @@ function App() {
1653
1735
  },
1654
1736
  { isActive: finished }
1655
1737
  );
1656
- useInput6((_input, key) => {
1738
+ useInput7((_input, key) => {
1657
1739
  if (phase === "idle" || phase === "authenticating") return;
1658
1740
  if (key.tab) {
1659
1741
  setShowLogs(!showLogs);
@@ -1664,8 +1746,8 @@ function App() {
1664
1746
  });
1665
1747
  }
1666
1748
  });
1667
- const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1668
- useInput6((_input, key) => {
1749
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
1750
+ useInput7((_input, key) => {
1669
1751
  if (escOwnedElsewhere) return;
1670
1752
  if (key.escape) {
1671
1753
  track("AI Wizard Interaction", {
@@ -1684,8 +1766,8 @@ function App() {
1684
1766
  /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1685
1767
  the whole screen, and the scrolling throws off its cursor arithmetic —
1686
1768
  flicker and leftover rows. */
1687
- /* @__PURE__ */ jsxs14(
1688
- Box15,
1769
+ /* @__PURE__ */ jsxs15(
1770
+ Box16,
1689
1771
  {
1690
1772
  backgroundColor: COLORS.bg.main,
1691
1773
  flexDirection: "row",
@@ -1693,16 +1775,16 @@ function App() {
1693
1775
  height: scrollsPastViewport ? void 0 : rows,
1694
1776
  overflow: scrollsPastViewport ? "visible" : "hidden",
1695
1777
  children: [
1696
- mainWindowVisible && /* @__PURE__ */ jsxs14(
1697
- Box15,
1778
+ mainWindowVisible && /* @__PURE__ */ jsxs15(
1779
+ Box16,
1698
1780
  {
1699
1781
  flexDirection,
1700
1782
  width: "100%",
1701
1783
  maxHeight: rows,
1702
1784
  justifyContent: "space-between",
1703
1785
  children: [
1704
- showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : /* @__PURE__ */ jsxs14(
1705
- Box15,
1786
+ showLogs ? /* @__PURE__ */ jsx14(Logs, {}) : /* @__PURE__ */ jsxs15(
1787
+ Box16,
1706
1788
  {
1707
1789
  flexDirection: "column",
1708
1790
  paddingX: 4,
@@ -1710,26 +1792,26 @@ function App() {
1710
1792
  width: showSidebar ? 70 : "100%",
1711
1793
  flexGrow: 1,
1712
1794
  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." })
1795
+ phase === "authenticating" && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginBottom: 1, children: [
1796
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1797
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1716
1798
  ] }),
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: [
1799
+ /* @__PURE__ */ jsx14(CliOutput, {}),
1800
+ /* @__PURE__ */ jsx14(Notices, {}),
1801
+ /* @__PURE__ */ jsx14(PromptInput, {}),
1802
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx14(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx14(CurrentStep, {}) }),
1803
+ phase === "error" && error && /* @__PURE__ */ jsx14(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text16, { color: COLORS.status.error, children: [
1722
1804
  "\u2716 ",
1723
1805
  error
1724
1806
  ] }) })
1725
1807
  ]
1726
1808
  }
1727
1809
  ),
1728
- showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1810
+ showSidebar ? /* @__PURE__ */ jsx14(Sidebar, {}) : /* @__PURE__ */ jsx14(Ribbon, {})
1729
1811
  ]
1730
1812
  }
1731
1813
  ),
1732
- phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1814
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx14(LearnMore, {}) : /* @__PURE__ */ jsx14(Welcome, {}))
1733
1815
  ]
1734
1816
  }
1735
1817
  )
@@ -1743,17 +1825,17 @@ import "zod";
1743
1825
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1744
1826
  import { join as join5 } from "node:path";
1745
1827
  var configFile = () => join5(stateDir(), "config.json");
1746
- var DEFAULT_CONFIG = {
1828
+ var defaultConfig = () => ({
1747
1829
  version: 1,
1748
1830
  aiConsent: false,
1749
1831
  workflowsRun: []
1750
- };
1832
+ });
1751
1833
  async function loadConfig() {
1752
1834
  try {
1753
1835
  const raw = await readFile2(configFile(), "utf8");
1754
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1836
+ return { ...defaultConfig(), ...JSON.parse(raw) };
1755
1837
  } catch {
1756
- return { ...DEFAULT_CONFIG };
1838
+ return defaultConfig();
1757
1839
  }
1758
1840
  }
1759
1841
  async function saveConfig(config) {
@@ -1809,7 +1891,7 @@ async function ensureConsent() {
1809
1891
  if (config.aiConsent) return;
1810
1892
  const store = useWizard.getState();
1811
1893
  const answer = await store.requestUserInput({
1812
- prompt: "Wizard will make AI-authored changes to this repository.",
1894
+ 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
1895
  promptType: "enterToContinue",
1814
1896
  options: []
1815
1897
  });
@@ -2212,9 +2294,9 @@ function listFilesTool(ctx) {
2212
2294
  if (++ctx.counts.list > ctx.limits.list) {
2213
2295
  return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
2214
2296
  }
2215
- const resolved = resolveInRoot(ctx, ".");
2216
- if (!resolved.ok) return resolved.error;
2217
- const entries = await readdir(resolved.target, { withFileTypes: true });
2297
+ const resolved2 = resolveInRoot(ctx, ".");
2298
+ if (!resolved2.ok) return resolved2.error;
2299
+ const entries = await readdir(resolved2.target, { withFileTypes: true });
2218
2300
  return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2219
2301
  }
2220
2302
  });
@@ -2232,14 +2314,14 @@ function changeDirectoryTool(ctx) {
2232
2314
  }),
2233
2315
  execute: async ({ path }) => {
2234
2316
  logger.info({ path }, "called changeDirectory tool");
2235
- const resolved = resolveInRoot(ctx, path);
2236
- if (!resolved.ok) return resolved.error;
2317
+ const resolved2 = resolveInRoot(ctx, path);
2318
+ if (!resolved2.ok) return resolved2.error;
2237
2319
  try {
2238
- const info = await stat(resolved.target);
2320
+ const info = await stat(resolved2.target);
2239
2321
  if (!info.isDirectory()) {
2240
2322
  return `Error changing directory to ${path}: not a directory`;
2241
2323
  }
2242
- ctx.cwd = resolved.target;
2324
+ ctx.cwd = resolved2.target;
2243
2325
  return `Changed working directory to ${ctx.cwd}`;
2244
2326
  } catch (err) {
2245
2327
  return `Error changing directory to ${path}: ${err.message}`;
@@ -2304,11 +2386,11 @@ function readFileTool(ctx) {
2304
2386
  return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
2305
2387
  }
2306
2388
  logger.info({ filePath }, "called readFile tool");
2307
- const resolved = resolveInRoot(ctx, filePath);
2308
- if (!resolved.ok) return resolved.error;
2389
+ const resolved2 = resolveInRoot(ctx, filePath);
2390
+ if (!resolved2.ok) return resolved2.error;
2309
2391
  try {
2310
- const content = await readFile3(resolved.target, "utf8");
2311
- return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2392
+ const content = await readFile3(resolved2.target, "utf8");
2393
+ return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2312
2394
  } catch (err) {
2313
2395
  return `Error reading ${filePath}: ${err.message}`;
2314
2396
  }
@@ -2330,17 +2412,17 @@ function writeFileTool(ctx) {
2330
2412
  }),
2331
2413
  execute: async ({ filePath, content }) => {
2332
2414
  logger.info({ filePath }, "called writeFile tool");
2333
- const resolved = resolveInRoot(ctx, filePath);
2334
- if (resolved.ok === false) return resolved.error;
2335
- if (isSecretEnvFile(resolved.target)) {
2415
+ const resolved2 = resolveInRoot(ctx, filePath);
2416
+ if (resolved2.ok === false) return resolved2.error;
2417
+ if (isSecretEnvFile(resolved2.target)) {
2336
2418
  return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
2337
2419
  }
2338
2420
  try {
2339
- if (await hasSymlinkParent(ctx, resolved.target)) {
2340
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2421
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2422
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2341
2423
  }
2342
- await mkdir3(dirname4(resolved.target), { recursive: true });
2343
- await writeFile3(resolved.target, content, "utf8");
2424
+ await mkdir3(dirname4(resolved2.target), { recursive: true });
2425
+ await writeFile3(resolved2.target, content, "utf8");
2344
2426
  return `Wrote to ${filePath}`;
2345
2427
  } catch (err) {
2346
2428
  return `Error writing ${filePath}: ${err.message}`;
@@ -2357,7 +2439,40 @@ import { dirname as dirname5 } from "node:path";
2357
2439
 
2358
2440
  // src/lib/algoliaApiKey.ts
2359
2441
  import { z as z11 } from "zod";
2360
- var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
2442
+
2443
+ // src/lib/keychain.ts
2444
+ import { getPassword, setPassword } from "cross-keychain";
2445
+ var SERVICE = "algolia-wizard";
2446
+ function account(kind, index, appId) {
2447
+ return `${kind}:${appId}:${index}`;
2448
+ }
2449
+ async function readStoredKey(kind, index, appId) {
2450
+ try {
2451
+ return await getPassword(SERVICE, account(kind, index, appId));
2452
+ } catch (err) {
2453
+ logger.warn(
2454
+ { err: err.message, kind, index, appId },
2455
+ "could not read the API key from the keychain"
2456
+ );
2457
+ return null;
2458
+ }
2459
+ }
2460
+ async function storeKey(kind, index, appId, value) {
2461
+ const name = account(kind, index, appId);
2462
+ try {
2463
+ await setPassword(SERVICE, name, value);
2464
+ if (await getPassword(SERVICE, name) !== value) {
2465
+ throw new Error("the keychain did not store the value");
2466
+ }
2467
+ } catch (err) {
2468
+ logger.warn(
2469
+ { err: err.message, kind, index, appId },
2470
+ "could not store the API key in the keychain; the next run will create another"
2471
+ );
2472
+ }
2473
+ }
2474
+
2475
+ // src/lib/algoliaApiKey.ts
2361
2476
  var WRITE_ACLS = [
2362
2477
  "addObject",
2363
2478
  "deleteObject",
@@ -2365,83 +2480,88 @@ var WRITE_ACLS = [
2365
2480
  "editSettings",
2366
2481
  "listIndexes"
2367
2482
  ];
2368
- var WRITE_ACL_SET = new Set(WRITE_ACLS);
2369
- var apiKeySchema = z11.object({
2370
- value: z11.string().min(1),
2371
- acl: z11.array(z11.string()).default([]),
2372
- indexes: z11.array(z11.string()).default([])
2373
- });
2374
- var apiKeyListSchema = z11.object({
2375
- items: z11.array(apiKeySchema).optional(),
2376
- keys: z11.array(apiKeySchema).optional()
2377
- }).transform((o) => o.items ?? o.keys ?? []);
2378
2483
  var createdKeySchema = z11.object({
2379
2484
  key: z11.string().min(1).optional(),
2380
2485
  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;
2486
+ }).transform((o) => o.key ?? o.value);
2487
+ async function createKey(index, acls, description) {
2488
+ logger.info({ index, acls }, "creating an API key");
2489
+ const stdout = await runAlgoliaCli(
2490
+ [
2491
+ "apikeys",
2492
+ "create",
2493
+ "--acl",
2494
+ acls.join(","),
2495
+ "--indices",
2496
+ index,
2497
+ "--description",
2498
+ description,
2499
+ "-o",
2500
+ "json"
2501
+ ],
2502
+ { withoutAdminKey: true }
2503
+ );
2504
+ let payload;
2505
+ try {
2506
+ payload = JSON.parse(stdout);
2507
+ } catch {
2508
+ throw new Error("apikeys create returned output that is not valid JSON");
2509
+ }
2510
+ const created = createdKeySchema.parse(payload);
2400
2511
  if (!created) throw new Error("apikeys create returned no key value");
2401
2512
  return created;
2402
2513
  }
2403
- function canReuseForWrites(key, index) {
2404
- return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2514
+ var resolved = /* @__PURE__ */ new Map();
2515
+ function forgetResolvedKeys() {
2516
+ resolved.clear();
2405
2517
  }
2406
- async function resolveWriteKey(index) {
2407
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2408
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key2) => canReuseForWrites(key2, index))?.value;
2409
- if (existing) {
2410
- logger.info({ index }, "reusing existing write API key");
2411
- return existing;
2518
+ function resolveKey(kind, index, appId, acls, description) {
2519
+ const cacheKey = `${kind}:${appId}:${index}`;
2520
+ const cached = resolved.get(cacheKey);
2521
+ if (cached) return cached;
2522
+ const pending = provisionKey(kind, index, appId, acls, description).catch(
2523
+ (err) => {
2524
+ resolved.delete(cacheKey);
2525
+ throw err;
2526
+ }
2527
+ );
2528
+ resolved.set(cacheKey, pending);
2529
+ return pending;
2530
+ }
2531
+ async function provisionKey(kind, index, appId, acls, description) {
2532
+ const stored = await readStoredKey(kind, index, appId);
2533
+ if (stored) {
2534
+ logger.info({ kind, index, appId }, "reusing the stored API key");
2535
+ return { key: stored, source: "keychain" };
2412
2536
  }
2413
- logger.info({ index }, "no reusable write key found; creating one");
2414
- const created = await runAlgoliaCli([
2415
- "apikeys",
2416
- "create",
2417
- "--indices",
2537
+ const key = await createKey(index, acls, description);
2538
+ await storeKey(kind, index, appId, key);
2539
+ return { key, source: "created" };
2540
+ }
2541
+ function resolveWriteKey(index, appId) {
2542
+ return resolveKey(
2543
+ "write",
2418
2544
  index,
2419
- "--acl",
2420
- WRITE_ACLS.join(","),
2421
- "--description",
2422
- `wizard write key for ${index}`,
2423
- "-o",
2424
- "json"
2425
- ]);
2426
- const { key, value } = createdKeySchema.parse(JSON.parse(created));
2427
- const writeKey = key ?? value;
2428
- if (!writeKey) throw new Error("apikeys create returned no key value");
2429
- return writeKey;
2430
- }
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;
2437
- }
2438
- logger.info({ index }, "no reusable search-only key found; creating one");
2439
- return createSearchKey(index);
2545
+ appId,
2546
+ WRITE_ACLS,
2547
+ `Algolia Wizard write key for ${index} index`
2548
+ );
2549
+ }
2550
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2551
+ if (envKey) return { key: envKey, source: "env" };
2552
+ return resolveKey(
2553
+ "search",
2554
+ index,
2555
+ appId,
2556
+ ["search"],
2557
+ `Algolia Wizard search-only key for ${index} index`
2558
+ );
2440
2559
  }
2441
2560
 
2442
2561
  // src/lib/tools/writeAlgoliaCredentials.ts
2443
2562
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2444
2563
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2564
+ var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
2445
2565
  function appendEnv(content, entries) {
2446
2566
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2447
2567
  const lines = entries.map(([name, value]) => `${name}=${value}
@@ -2451,9 +2571,16 @@ function appendEnv(content, entries) {
2451
2571
  function hasEnv(content, name) {
2452
2572
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
2453
2573
  }
2574
+ function upsertEnv(content, name, value) {
2575
+ if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
2576
+ return content.replace(
2577
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "m"),
2578
+ `${name}=${value}`
2579
+ );
2580
+ }
2454
2581
  function writeCredentialsTool(ctx) {
2455
2582
  return tool6({
2456
- description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file (e.g. ".env"). If the file already defines ${APP_ID_VAR} or ${API_KEY_VAR}, the write is skipped and existing values are left untouched.`,
2583
+ description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file (e.g. ".env"). Any name the file already defines is left untouched.`,
2457
2584
  inputSchema: z12.object({
2458
2585
  filePath: z12.string().describe(
2459
2586
  'Path to the env file to write credentials into (e.g. ".env")'
@@ -2461,43 +2588,51 @@ function writeCredentialsTool(ctx) {
2461
2588
  }),
2462
2589
  execute: async ({ filePath }) => {
2463
2590
  logger.info({ filePath }, "called writeCredentials tool");
2464
- const resolved = resolveInRoot(ctx, filePath);
2465
- if (resolved.ok === false) return resolved.error;
2591
+ const resolved2 = resolveInRoot(ctx, filePath);
2592
+ if (resolved2.ok === false) return resolved2.error;
2466
2593
  const targetIndex = useWizard.getState().targetIndex;
2467
2594
  if (!targetIndex) {
2468
2595
  return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2469
2596
  }
2470
- let appId;
2471
- let writeKey;
2597
+ let existing = "";
2598
+ let present;
2472
2599
  try {
2473
- appId = (await requireApplication()).id;
2474
- writeKey = await resolveWriteKey(targetIndex);
2475
- } catch (err) {
2476
- return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2477
- }
2478
- try {
2479
- if (await hasSymlinkParent(ctx, resolved.target)) {
2480
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2600
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2601
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2481
2602
  }
2482
- let existing = "";
2483
2603
  try {
2484
- existing = await readFile4(resolved.target, "utf8");
2604
+ existing = await readFile4(resolved2.target, "utf8");
2485
2605
  } catch (err) {
2486
2606
  if (err.code !== "ENOENT") throw err;
2487
2607
  }
2488
- const present = [APP_ID_VAR, API_KEY_VAR].filter(
2608
+ present = [APP_ID_VAR, API_KEY_VAR].filter(
2489
2609
  (name) => hasEnv(existing, name)
2490
2610
  );
2491
- if (present.length > 0) {
2492
- return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2611
+ } catch (err) {
2612
+ return `Error writing credentials to ${filePath}: ${err.message}`;
2613
+ }
2614
+ let credentials = [];
2615
+ if (present.length === 0) {
2616
+ try {
2617
+ const appId = (await requireApplication()).id;
2618
+ credentials = [
2619
+ [APP_ID_VAR, appId],
2620
+ [API_KEY_VAR, (await resolveWriteKey(targetIndex, appId)).key]
2621
+ ];
2622
+ } catch (err) {
2623
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2493
2624
  }
2494
- const envWithCredentials = appendEnv(existing, [
2495
- [APP_ID_VAR, appId],
2496
- [API_KEY_VAR, writeKey]
2497
- ]);
2498
- await mkdir4(dirname5(resolved.target), { recursive: true });
2499
- await writeFile4(resolved.target, envWithCredentials, "utf8");
2500
- return `Wrote Algolia credentials to ${filePath}`;
2625
+ }
2626
+ try {
2627
+ const updated = upsertEnv(
2628
+ appendEnv(existing, credentials),
2629
+ INDEX_NAME_VAR,
2630
+ targetIndex
2631
+ );
2632
+ await mkdir4(dirname5(resolved2.target), { recursive: true });
2633
+ await writeFile4(resolved2.target, updated, "utf8");
2634
+ const wrote = `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}`;
2635
+ return present.length > 0 ? `${wrote}. Skipped ${present.join(" and ")}: already defined there.` : wrote;
2501
2636
  } catch (err) {
2502
2637
  return `Error writing credentials to ${filePath}: ${err.message}`;
2503
2638
  }
@@ -2511,11 +2646,19 @@ import z13 from "zod";
2511
2646
  import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
2512
2647
  import { join as join7 } from "node:path";
2513
2648
  var MAX_QUERY_LENGTH = 1e3;
2649
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2650
+ "node_modules",
2651
+ "dist",
2652
+ "build",
2653
+ "vendor",
2654
+ "venv",
2655
+ "__pycache__",
2656
+ "target"
2657
+ ]);
2514
2658
  async function walkFiles(dir) {
2515
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2516
2659
  const out = [];
2517
2660
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2518
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2661
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2519
2662
  const full = join7(dir, e.name);
2520
2663
  if (e.isDirectory()) out.push(...await walkFiles(full));
2521
2664
  else if (e.isFile()) out.push(full);
@@ -2537,8 +2680,8 @@ function searchFilesTool(ctx) {
2537
2680
  if (query.length > MAX_QUERY_LENGTH) {
2538
2681
  return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
2539
2682
  }
2540
- const resolved = resolveInRoot(ctx, path);
2541
- if (!resolved.ok) return resolved.error;
2683
+ const resolved2 = resolveInRoot(ctx, path);
2684
+ if (!resolved2.ok) return resolved2.error;
2542
2685
  let re;
2543
2686
  try {
2544
2687
  re = new RegExp(query);
@@ -2546,7 +2689,7 @@ function searchFilesTool(ctx) {
2546
2689
  return `Invalid regex: ${err.message}`;
2547
2690
  }
2548
2691
  const matches = [];
2549
- for (const file of await walkFiles(resolved.target)) {
2692
+ for (const file of await walkFiles(resolved2.target)) {
2550
2693
  let content;
2551
2694
  try {
2552
2695
  content = await readFile5(file, "utf8");
@@ -2569,92 +2712,194 @@ function searchFilesTool(ctx) {
2569
2712
  });
2570
2713
  }
2571
2714
 
2572
- // src/lib/tools/verifyImplementation.ts
2715
+ // src/lib/tools/runShell.ts
2573
2716
  import { tool as tool8 } from "ai";
2574
2717
  import z14 from "zod";
2718
+ import { relative as relative2 } from "node:path";
2575
2719
 
2576
- // src/lib/tools/utils/runCommand.ts
2720
+ // src/lib/tools/utils/runShell.ts
2577
2721
  import { spawn as spawn2 } from "node:child_process";
2578
- function runCommand(command, args, cwd) {
2722
+
2723
+ // src/lib/tools/context.ts
2724
+ var DEFAULT_TOOL_LIMITS = {
2725
+ list: 10,
2726
+ search: 10,
2727
+ read: 20,
2728
+ match: 100,
2729
+ shell: 30
2730
+ };
2731
+ var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
2732
+ async function refuseByDefault() {
2733
+ return "reject";
2734
+ }
2735
+ function createShellContext(overrides = {}) {
2736
+ return {
2737
+ env: async () => ({}),
2738
+ timeoutMs: DEFAULT_SHELL_TIMEOUT_MS,
2739
+ approved: /* @__PURE__ */ new Set(),
2740
+ executions: [],
2741
+ approve: refuseByDefault,
2742
+ ...overrides
2743
+ };
2744
+ }
2745
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), shell2 = createShellContext()) {
2746
+ return {
2747
+ root: cwd,
2748
+ cwd,
2749
+ limits: { ...limits },
2750
+ counts: { list: 0, search: 0, read: 0, shell: 0 },
2751
+ shell: shell2
2752
+ };
2753
+ }
2754
+
2755
+ // src/lib/tools/utils/runShell.ts
2756
+ var SIGKILL_DELAY_MS = 5e3;
2757
+ var HEAD_CHARS = 4e3;
2758
+ var TAIL_CHARS = 8e3;
2759
+ function truncateOutput(output) {
2760
+ if (output.length <= HEAD_CHARS + TAIL_CHARS) return output;
2761
+ const omitted = output.length - HEAD_CHARS - TAIL_CHARS;
2762
+ return [
2763
+ output.slice(0, HEAD_CHARS),
2764
+ `
2765
+ \u2026 [${omitted} characters omitted] \u2026
2766
+ `,
2767
+ output.slice(-TAIL_CHARS)
2768
+ ].join("");
2769
+ }
2770
+ function runShell(command, opts) {
2771
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2772
+ const startedAt = Date.now();
2579
2773
  return new Promise((resolve4) => {
2580
2774
  let output = "";
2581
- const child = spawn2(command, args, {
2582
- cwd,
2583
- stdio: ["ignore", "pipe", "pipe"]
2775
+ let timedOut = false;
2776
+ let settled = false;
2777
+ const child = spawn2(command, {
2778
+ shell: true,
2779
+ cwd: opts.cwd,
2780
+ stdio: ["ignore", "pipe", "pipe"],
2781
+ env: { ...process.env, ...opts.env }
2584
2782
  });
2783
+ const finish = (exitCode) => {
2784
+ if (settled) return;
2785
+ settled = true;
2786
+ clearTimeout(timer);
2787
+ clearTimeout(killTimer);
2788
+ resolve4({
2789
+ exitCode,
2790
+ output: truncateOutput(output.trim()),
2791
+ timedOut,
2792
+ durationMs: Date.now() - startedAt
2793
+ });
2794
+ };
2795
+ let killTimer;
2796
+ const timer = setTimeout(() => {
2797
+ timedOut = true;
2798
+ output += `
2799
+ [timed out after ${timeoutMs}ms]`;
2800
+ child.kill("SIGTERM");
2801
+ killTimer = setTimeout(() => child.kill("SIGKILL"), SIGKILL_DELAY_MS);
2802
+ }, timeoutMs);
2585
2803
  child.stdout?.on("data", (d) => output += d);
2586
2804
  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 }));
2805
+ child.on("error", (err) => {
2806
+ output += `Failed to run ${command}: ${err.message}`;
2807
+ finish(1);
2808
+ });
2809
+ child.on("close", (code) => finish(code ?? 1));
2592
2810
  });
2593
2811
  }
2594
2812
 
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 };
2813
+ // src/lib/tools/runShell.ts
2814
+ function approvalKey(cwd, command) {
2815
+ return `${cwd}\0${command}`;
2816
+ }
2817
+ function storeApproval(root) {
2818
+ return async (req) => {
2819
+ const rel = relative2(root, req.cwd);
2820
+ const answer = await useWizard.getState().requestUserInput({
2821
+ prompt: "Run this command?",
2822
+ promptType: "commandApproval",
2823
+ options: [],
2824
+ command: {
2825
+ ...req,
2826
+ cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
2827
+ }
2828
+ });
2829
+ return answer === "approve" || answer === "always" ? answer : "reject";
2830
+ };
2648
2831
  }
2649
-
2650
- // src/lib/tools/verifyImplementation.ts
2651
- function verifyImplementationTool() {
2832
+ function runShellTool(ctx) {
2652
2833
  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();
2834
+ 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.",
2835
+ inputSchema: z14.object({
2836
+ command: z14.string().describe(
2837
+ "The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
2838
+ ),
2839
+ cwd: z14.string().optional().describe(
2840
+ "Directory to run in, relative to the project root. Defaults to the project root."
2841
+ ),
2842
+ explanation: z14.string().describe(
2843
+ "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."
2844
+ )
2845
+ }),
2846
+ execute: async ({ command, cwd, explanation }) => {
2847
+ if (++ctx.counts.shell > ctx.limits.shell) {
2848
+ return `Refused: command limit (${ctx.limits.shell}) reached. Stop running commands and report what you have.`;
2849
+ }
2850
+ const resolved2 = resolveInRoot(ctx, cwd ?? ".");
2851
+ if (!resolved2.ok) return resolved2.error;
2852
+ logger.info({ command, cwd: resolved2.target }, "called runShell tool");
2853
+ const key = approvalKey(resolved2.target, command);
2854
+ const decision = ctx.shell.approved.has(key) ? "approve" : await ctx.shell.approve({
2855
+ command,
2856
+ cwd: resolved2.target,
2857
+ explanation
2858
+ });
2859
+ if (decision === "reject") {
2860
+ ctx.shell.executions.push({
2861
+ command,
2862
+ cwd: resolved2.target,
2863
+ approved: false
2864
+ });
2865
+ logger.info({ command }, "runShell: user rejected the command");
2866
+ return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2867
+ }
2868
+ if (decision === "always") ctx.shell.approved.add(key);
2869
+ useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
2870
+ const env = await ctx.shell.env().catch((err) => {
2871
+ logger.warn({ err, command }, "runShell: could not resolve command env");
2872
+ return {};
2873
+ });
2874
+ const run2 = await (ctx.shell.run ?? runShell)(command, {
2875
+ cwd: resolved2.target,
2876
+ env,
2877
+ timeoutMs: ctx.shell.timeoutMs
2878
+ });
2879
+ logger.info(
2880
+ {
2881
+ command,
2882
+ exitCode: run2.exitCode,
2883
+ timedOut: run2.timedOut,
2884
+ durationMs: run2.durationMs
2885
+ },
2886
+ "runShell finished"
2887
+ );
2888
+ await markInteraction();
2889
+ ctx.shell.executions.push({
2890
+ command,
2891
+ cwd: resolved2.target,
2892
+ approved: true,
2893
+ exitCode: run2.exitCode,
2894
+ output: run2.output,
2895
+ timedOut: run2.timedOut,
2896
+ durationMs: run2.durationMs
2897
+ });
2898
+ return {
2899
+ exitCode: run2.exitCode,
2900
+ timedOut: run2.timedOut,
2901
+ output: run2.output
2902
+ };
2658
2903
  }
2659
2904
  });
2660
2905
  }
@@ -2734,18 +2979,18 @@ function generateRecordTool(ctx) {
2734
2979
  }));
2735
2980
  const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2736
2981
  const relPath = `${DATA_DIR}/${slug}.json`;
2737
- const resolved = resolveInRoot(ctx, relPath);
2738
- if (resolved.ok === false) return resolved.error;
2739
- if (await hasSymlinkParent(ctx, resolved.target)) {
2740
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2982
+ const resolved2 = resolveInRoot(ctx, relPath);
2983
+ if (resolved2.ok === false) return resolved2.error;
2984
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2985
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2741
2986
  }
2742
- await mkdir5(dirname6(resolved.target), { recursive: true });
2743
- await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2987
+ await mkdir5(dirname6(resolved2.target), { recursive: true });
2988
+ await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
2744
2989
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
2745
2990
  return {
2746
2991
  filePath: relPath,
2747
2992
  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.`
2993
+ 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
2994
  };
2750
2995
  } catch (err) {
2751
2996
  return `Error generating records: ${err.message}`;
@@ -2773,22 +3018,6 @@ function notifyUserTool() {
2773
3018
  });
2774
3019
  }
2775
3020
 
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
3021
  // src/lib/tools/index.ts
2793
3022
  function withLogging(name, def) {
2794
3023
  const execute = def.execute;
@@ -2820,10 +3049,7 @@ function createTools(ctx, { output, tools }) {
2820
3049
  writeCredentialsTool(ctx)
2821
3050
  ),
2822
3051
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2823
- verifyImplementation: withLogging(
2824
- "verifyImplementation",
2825
- verifyImplementationTool()
2826
- ),
3052
+ runShell: withLogging("runShell", runShellTool(ctx)),
2827
3053
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2828
3054
  notifyUser: withLogging("notifyUser", notifyUserTool())
2829
3055
  };
@@ -2858,7 +3084,7 @@ async function runAgent(req) {
2858
3084
  baseURL: PROXY_BASE_URL,
2859
3085
  fetch: proxyFetch
2860
3086
  });
2861
- const toolContext = createToolContext();
3087
+ const toolContext = req.toolContext ?? createToolContext();
2862
3088
  const readTools = ["readFile", "searchFiles", "listFiles"];
2863
3089
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2864
3090
  const instructions = [
@@ -2923,7 +3149,11 @@ async function runAgent(req) {
2923
3149
  "runAgent finished"
2924
3150
  );
2925
3151
  logger.info(
2926
- { counts: toolContext.counts, limits: toolContext.limits },
3152
+ {
3153
+ counts: toolContext.counts,
3154
+ limits: toolContext.limits,
3155
+ commandsRun: toolContext.shell.executions.length
3156
+ },
2927
3157
  "tool usage"
2928
3158
  );
2929
3159
  const toolResults = await stream.toolResults;
@@ -2949,8 +3179,8 @@ var detectLanguageSchema = z19.object({
2949
3179
  var detectLanguage = () => runAgent({
2950
3180
  instructions: [
2951
3181
  "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.",
3182
+ "If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
3183
+ "If a meta-framework is used, exclude the framework it builds on (e.g. Next.js over React, Rails over Rack).",
2954
3184
  "Return the exact version",
2955
3185
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
2956
3186
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -3048,7 +3278,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3048
3278
  // package.json
3049
3279
  var package_default = {
3050
3280
  name: "@algolia/wizard",
3051
- version: "0.9.0-rc.85.78",
3281
+ version: "0.9.0-rc.87.86",
3052
3282
  description: "Magically implement Algolia functionality in your codebase",
3053
3283
  type: "module",
3054
3284
  engines: {
@@ -3099,6 +3329,7 @@ var package_default = {
3099
3329
  "@hono/node-server": "^2.0.10",
3100
3330
  "@segment/analytics-node": "^3.1.0",
3101
3331
  ai: "^6.0.190",
3332
+ "cross-keychain": "^1.1.0",
3102
3333
  dotenv: "^17.4.2",
3103
3334
  hono: "^4.12.27",
3104
3335
  ink: "^7.0.5",
@@ -3196,9 +3427,11 @@ var CURATED_FRAMEWORKS = [
3196
3427
  "Next.js",
3197
3428
  "React",
3198
3429
  "Vue",
3199
- "Angular",
3200
- "Svelte",
3201
- "Vanilla JS"
3430
+ "Vanilla JS",
3431
+ "Django",
3432
+ "Laravel",
3433
+ "Rails",
3434
+ "Symfony"
3202
3435
  ];
3203
3436
  var OTHER_OPTION = "Other";
3204
3437
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -3211,12 +3444,15 @@ var FRAMEWORK_ALIASES = {
3211
3444
  vuejs: "vue",
3212
3445
  angular: "angular",
3213
3446
  angularjs: "angular",
3214
- svelte: "svelte",
3215
- sveltekit: "svelte",
3216
3447
  vanillajs: "vanillajs",
3217
3448
  vanilla: "vanillajs",
3218
3449
  javascript: "vanillajs",
3219
- js: "vanillajs"
3450
+ js: "vanillajs",
3451
+ django: "django",
3452
+ laravel: "laravel",
3453
+ rails: "rails",
3454
+ rubyonrails: "rails",
3455
+ symfony: "symfony"
3220
3456
  };
3221
3457
  var isSameFramework = (a, b) => {
3222
3458
  const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
@@ -3450,16 +3686,9 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3450
3686
  import z26 from "zod";
3451
3687
 
3452
3688
  // 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";
3689
+ import { execFile } from "node:child_process";
3690
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile6, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3691
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
3463
3692
  var MAX_BUFFER = 32 * 1024 * 1024;
3464
3693
  var MAX_WIZARD_WORKTREES = 3;
3465
3694
  var WIZARD_BRANCH_PREFIX = "wizard/implement-";
@@ -3490,7 +3719,7 @@ async function isWorkingTreeDirty(repoRoot) {
3490
3719
  return out.trim().length > 0;
3491
3720
  }
3492
3721
  async function pruneOldWorktrees(repoRoot) {
3493
- const dir = join9(stateDir(repoRoot), "worktrees");
3722
+ const dir = join8(stateDir(repoRoot), "worktrees");
3494
3723
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3495
3724
  for (const slug of stale) {
3496
3725
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3501,7 +3730,7 @@ async function pruneOldWorktrees(repoRoot) {
3501
3730
  "worktree",
3502
3731
  "remove",
3503
3732
  "--force",
3504
- join9(dir, slug)
3733
+ join8(dir, slug)
3505
3734
  ]);
3506
3735
  await git(["-C", repoRoot, "branch", "-D", branch]);
3507
3736
  } catch (err) {
@@ -3515,113 +3744,13 @@ async function pruneOldWorktrees(repoRoot) {
3515
3744
  async function createWorktree(repoRoot) {
3516
3745
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3517
3746
  const dirSlug = branch.replace(/\//g, "-");
3518
- const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
3747
+ const path = join8(stateDir(repoRoot), "worktrees", dirSlug);
3519
3748
  await git(["-C", repoRoot, "worktree", "prune"]);
3520
3749
  await pruneOldWorktrees(repoRoot);
3521
3750
  await mkdir6(dirname7(path), { recursive: true });
3522
3751
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
3523
3752
  return { path, branch };
3524
3753
  }
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
3754
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
3626
3755
  const trimmed = sourcePath.trim();
3627
3756
  if (!trimmed) {
@@ -3635,8 +3764,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3635
3764
  } catch {
3636
3765
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3637
3766
  }
3638
- const relPath = join9(ingestDir, basename2(source));
3639
- const dest = join9(worktreePath, relPath);
3767
+ const relPath = join8(ingestDir, basename2(source));
3768
+ const dest = join8(worktreePath, relPath);
3640
3769
  try {
3641
3770
  await mkdir6(dirname7(dest), { recursive: true });
3642
3771
  await copyFile(source, dest);
@@ -3651,11 +3780,28 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3651
3780
  function hasEnvVar(content, name) {
3652
3781
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3653
3782
  }
3783
+ async function readEnvVar(worktreePath, name) {
3784
+ let content;
3785
+ try {
3786
+ content = await readFile6(join8(worktreePath, ".env"), "utf8");
3787
+ } catch (err) {
3788
+ if (err.code !== "ENOENT") throw err;
3789
+ return void 0;
3790
+ }
3791
+ const match = new RegExp(
3792
+ `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3793
+ "m"
3794
+ ).exec(content);
3795
+ if (!match) return void 0;
3796
+ const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3797
+ if (!value || value.startsWith("<")) return void 0;
3798
+ return value;
3799
+ }
3654
3800
  async function writeSearchEnvValues(worktreePath, vars) {
3655
- const target = join9(worktreePath, ".env");
3801
+ const target = join8(worktreePath, ".env");
3656
3802
  let existing = "";
3657
3803
  try {
3658
- existing = await readFile7(target, "utf8");
3804
+ existing = await readFile6(target, "utf8");
3659
3805
  } catch (err) {
3660
3806
  if (err.code !== "ENOENT") throw err;
3661
3807
  }
@@ -3724,15 +3870,15 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3724
3870
  }
3725
3871
 
3726
3872
  // 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";
3873
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
3874
+ import { dirname as dirname8, join as join9 } from "node:path";
3729
3875
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3730
- var DOCS_SUBPATH = join10("docs", "algolia-sdk");
3876
+ var DOCS_SUBPATH = join9("docs", "algolia-sdk");
3731
3877
  function findDocsDir() {
3732
3878
  let dir = dirname8(fileURLToPath2(import.meta.url));
3733
3879
  for (; ; ) {
3734
- const candidate = join10(dir, DOCS_SUBPATH);
3735
- if (existsSync2(candidate)) return candidate;
3880
+ const candidate = join9(dir, DOCS_SUBPATH);
3881
+ if (existsSync(candidate)) return candidate;
3736
3882
  const parent = dirname8(dir);
3737
3883
  if (parent === dir) return void 0;
3738
3884
  dir = parent;
@@ -3754,7 +3900,7 @@ function loadAlgoliaDoc(language) {
3754
3900
  );
3755
3901
  return "";
3756
3902
  }
3757
- return readFileSync(join10(docsDir, files[0]), "utf8").trim();
3903
+ return readFileSync(join9(docsDir, files[0]), "utf8").trim();
3758
3904
  }
3759
3905
  function getNamedDoc(name, language) {
3760
3906
  const docsDir = findDocsDir();
@@ -3762,14 +3908,15 @@ function getNamedDoc(name, language) {
3762
3908
  logger.warn("docs/algolia-sdk not found");
3763
3909
  return "";
3764
3910
  }
3765
- const file = join10(docsDir, `${name}-${language}.md`);
3766
- if (!existsSync2(file)) {
3911
+ const file = join9(docsDir, `${name}-${language}.md`);
3912
+ if (!existsSync(file)) {
3767
3913
  logger.warn({ name, language }, "named SDK reference not found");
3768
3914
  return "";
3769
3915
  }
3770
3916
  return readFileSync(file, "utf8").trim();
3771
3917
  }
3772
3918
  function getFrameworkSpecificDoc(frameworks) {
3919
+ if (frameworks.length === 0) return "";
3773
3920
  const fw = frameworks.map((f) => f.toLowerCase());
3774
3921
  if (fw.includes("vue") || fw.includes("nuxt")) {
3775
3922
  return loadAlgoliaDoc("vue");
@@ -3777,9 +3924,6 @@ function getFrameworkSpecificDoc(frameworks) {
3777
3924
  if (fw.includes("react") || fw.includes("next.js")) {
3778
3925
  return loadAlgoliaDoc("react");
3779
3926
  }
3780
- if (fw.includes("angular")) {
3781
- return loadAlgoliaDoc("angular");
3782
- }
3783
3927
  return loadAlgoliaDoc("js");
3784
3928
  }
3785
3929
 
@@ -3807,11 +3951,7 @@ var implementSchema = z26.object({
3807
3951
  });
3808
3952
  var implementationOutputSchema = z26.object({
3809
3953
  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()
3954
+ ingestCommand: z26.string().optional()
3815
3955
  });
3816
3956
  var verificationOutputSchema = z26.object({
3817
3957
  summary: z26.string(),
@@ -3821,30 +3961,35 @@ var verificationOutputSchema = z26.object({
3821
3961
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3822
3962
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
3823
3963
  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
- }
3964
+ var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
3965
+ function lower(entries) {
3966
+ return entries.map((entry) => entry.name.toLowerCase());
3843
3967
  }
3844
- function publicEnvPrefix(language) {
3845
- const frameworkNames = language.frameworks.map(
3846
- (framework) => framework.name.toLowerCase()
3968
+ function isJsProject(language) {
3969
+ return lower(language.languages).some(
3970
+ (name) => JS_LANGUAGES.some((js) => name.includes(js))
3971
+ );
3972
+ }
3973
+ var UI_FRAMEWORKS = [
3974
+ { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
3975
+ { match: ["react", "next"], target: "React", doc: "react" },
3976
+ { match: ["angular"], target: "Angular" }
3977
+ ];
3978
+ function matchUiFramework(language) {
3979
+ const names = lower(language.frameworks);
3980
+ return UI_FRAMEWORKS.find(
3981
+ (ui) => ui.match.some((needle) => names.some((name) => name.includes(needle)))
3847
3982
  );
3983
+ }
3984
+ function searchUiTarget(language) {
3985
+ return matchUiFramework(language)?.target ?? language.frameworks[0]?.name ?? (isJsProject(language) ? "JavaScript" : "this project");
3986
+ }
3987
+ function frameworksForDoc(language) {
3988
+ if (!isJsProject(language)) return [];
3989
+ return [matchUiFramework(language)?.doc ?? "js"];
3990
+ }
3991
+ function publicEnvPrefix(language) {
3992
+ const frameworkNames = lower(language.frameworks);
3848
3993
  if (frameworkNames.some((name) => name.includes("next"))) {
3849
3994
  return "NEXT_PUBLIC_";
3850
3995
  }
@@ -3857,26 +4002,49 @@ function publicEnvPrefix(language) {
3857
4002
  if (frameworkNames.some((name) => name.includes("vite"))) {
3858
4003
  return "VITE_";
3859
4004
  }
3860
- return "PUBLIC_";
4005
+ return isJsProject(language) ? "PUBLIC_" : "";
4006
+ }
4007
+ var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4008
+ var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4009
+ var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
4010
+ function appIdVar(language) {
4011
+ return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
4012
+ }
4013
+ function searchKeyVar(language) {
4014
+ return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3861
4015
  }
3862
- function searchEnvVars(language, appId, searchKey) {
3863
- const prefix = publicEnvPrefix(language);
4016
+ function searchIndexVar(language) {
4017
+ return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
4018
+ }
4019
+ function searchEnvVars(language, index, appId, searchKey) {
3864
4020
  return [
3865
4021
  {
3866
- name: `${prefix}ALGOLIA_APP_ID`,
4022
+ name: appIdVar(language),
3867
4023
  value: appId ?? "<your-algolia-app-id>"
3868
4024
  },
3869
4025
  {
3870
- name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
4026
+ name: searchKeyVar(language),
3871
4027
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4028
+ },
4029
+ // Wizard-supplied rather than written into the generated code, because an
4030
+ // agent that retypes the name (appending the project name, re-casing it)
4031
+ // leaves the UI querying an index that does not exist.
4032
+ {
4033
+ name: searchIndexVar(language),
4034
+ value: index
3872
4035
  }
3873
4036
  ];
3874
4037
  }
3875
4038
  function baseInstructions(input) {
3876
4039
  return [
3877
- `Target Algolia index: ${input.targetIndex}`,
4040
+ // Agents have renamed this (e.g. appending the project name), which the
4041
+ // index-scoped keys then reject with a 403.
4042
+ `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
3878
4043
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3879
- "Make minimal, idiomatic changes; do not touch unrelated code."
4044
+ "Make minimal, idiomatic changes; do not touch unrelated code.",
4045
+ `Commands run through a shell on ${process.platform}. Write commands that work there.`,
4046
+ "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.",
4047
+ "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
4048
  ];
3881
4049
  }
3882
4050
  function sourceSpecificInstructions(input) {
@@ -3896,43 +4064,62 @@ function sourceSpecificInstructions(input) {
3896
4064
  generated: [
3897
4065
  "No real data source exists; use sample records for each confirmed entity.",
3898
4066
  "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.",
4067
+ "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
4068
  "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
4069
  ]
3902
4070
  };
3903
4071
  return byLine[input.ingestionSource];
3904
4072
  }
4073
+ function algoliaClientDoc(input) {
4074
+ const doc = getNamedDoc("save-records", "js");
4075
+ if (!doc) return [];
4076
+ if (isJsProject(input.language)) return [doc];
4077
+ return [
4078
+ "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:",
4079
+ doc
4080
+ ];
4081
+ }
3905
4082
  function ingestionInstructions(input) {
3906
4083
  return [
3907
4084
  ...input.confirmed && input.confirmed.length ? [
3908
4085
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3909
4086
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3910
4087
  `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.",
4088
+ `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
4089
+ "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
4090
  "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.',
4091
+ ...algoliaClientDoc(input),
4092
+ "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.",
4093
+ '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
4094
  "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
4095
  ...sourceSpecificInstructions(input)
3918
4096
  ] : []
3919
4097
  ];
3920
4098
  }
3921
4099
  function searchInstructions(input) {
3922
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4100
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.language));
3923
4101
  return [
3924
4102
  "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)"}.`,
4103
+ `Build the search UI for ${input.searchUiTarget}.`,
4104
+ ...doc ? [
4105
+ "Follow the Algolia SDK reference below for client setup and search UI wiring; prefer it over prior knowledge:",
4106
+ doc
4107
+ ] : [
4108
+ "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."
4109
+ ],
4110
+ `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 target index.`,
4111
+ `Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4112
+ "Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4113
+ // The key is provisioned only after verification passes, so the agent never
4114
+ // sees one. It must also leave .env alone: the wizard reads that file to
4115
+ // decide whether a key already exists, and an agent-invented value there
4116
+ // would be reused as if it were real.
4117
+ `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
4118
  // Not the agent's to rename: the wizard writes these exact names into
3932
4119
  // ".env" right after this step, so a renamed prefix would leave the code
3933
4120
  // 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.',
4121
+ `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4122
+ "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
4123
  "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
4124
  ];
3938
4125
  }
@@ -3940,11 +4127,12 @@ function verificationInstructions(input) {
3940
4127
  return [
3941
4128
  "Verify the Algolia implementation changes in the current worktree.",
3942
4129
  `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.",
4130
+ "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.",
4131
+ "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.",
4132
+ "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4133
+ "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
4134
  "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.`,
4135
+ `Do not modify "${input.ingestDir}/" unless a check reports an actionable issue in its files.`,
3948
4136
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
3949
4137
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
3950
4138
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -3965,14 +4153,15 @@ var IMPLEMENT_CONFIG = {
3965
4153
  }
3966
4154
  };
3967
4155
  var useCaseToolMap = {
3968
- ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials", "notifyUser"],
3969
- search: [...FS_READ_TOOLS, "writeFile", "notifyUser"],
3970
- verification: [
4156
+ ingestion: [
3971
4157
  ...FS_READ_TOOLS,
3972
4158
  "writeFile",
3973
- "verifyImplementation",
4159
+ "writeCredentials",
4160
+ "runShell",
3974
4161
  "notifyUser"
3975
- ]
4162
+ ],
4163
+ search: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"],
4164
+ verification: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"]
3976
4165
  };
3977
4166
  function toolsForUseCase(useCase, ingestionSource) {
3978
4167
  const tools = useCaseToolMap[useCase];
@@ -3995,15 +4184,30 @@ function formatSummary(useCase, summary) {
3995
4184
  const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3996
4185
  return `${label}: ${summary}`;
3997
4186
  }
3998
- function buildIngestCommand(worktree, runtime, entrypoint) {
3999
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4000
- }
4001
4187
  function parseIngestRecordCount(output) {
4002
4188
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
4003
4189
  if (!match) return void 0;
4004
4190
  const count = Number(match[1]);
4005
4191
  return Number.isFinite(count) ? count : void 0;
4006
4192
  }
4193
+ function ingestOutcome(executions, ingestCommand) {
4194
+ const newestFirst = [...executions].reverse();
4195
+ const withCount = newestFirst.filter(
4196
+ (e) => parseIngestRecordCount(e.output ?? "") != null
4197
+ );
4198
+ const succeeded = newestFirst.filter((e) => e.approved && e.exitCode === 0);
4199
+ return {
4200
+ run: succeeded.find((e) => e.command === ingestCommand) ?? succeeded.find((e) => withCount.includes(e)),
4201
+ recordCount: parseIngestRecordCount(withCount[0]?.output ?? "")
4202
+ };
4203
+ }
4204
+ function makeToolContext(worktree, env = async () => ({})) {
4205
+ return createToolContext(
4206
+ DEFAULT_TOOL_LIMITS,
4207
+ worktree,
4208
+ createShellContext({ env, approve: storeApproval(worktree) })
4209
+ );
4210
+ }
4007
4211
  function verificationRetryInstructions(verification) {
4008
4212
  return [
4009
4213
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
@@ -4082,17 +4286,12 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4082
4286
  const confirmed2 = normalized.confirmedEntities;
4083
4287
  const searchLocation = normalized.searchImplementationAnalysis;
4084
4288
  let appId;
4085
- let searchKey;
4289
+ let ingestAppId;
4086
4290
  if (useCases.includes("search")) {
4087
4291
  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
- }
4292
+ }
4293
+ if (useCases.includes("ingestion")) {
4294
+ ingestAppId = appId ?? (await requireApplication()).id;
4096
4295
  }
4097
4296
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
4098
4297
  try {
@@ -4124,51 +4323,60 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4124
4323
  targetIndex,
4125
4324
  language,
4126
4325
  appId,
4127
- searchKey,
4128
- searchEnvVars: searchEnvVars(language, appId, searchKey),
4326
+ // Names only: the search-only key is provisioned after verification, so
4327
+ // every value here is still a placeholder when the agent reads them.
4328
+ searchEnvVars: searchEnvVars(language, targetIndex, appId),
4129
4329
  ingestDir: INGEST_DIR,
4130
4330
  ingestionSource,
4131
4331
  uploadFilePath,
4132
- uiFramework: detectUiFramework(language)
4332
+ searchUiTarget: searchUiTarget(language)
4133
4333
  };
4134
4334
  const summaries = [];
4135
4335
  if (uploadWarning) summaries.push(uploadWarning);
4336
+ let envSearchKey;
4337
+ let envAppIdMismatch = false;
4338
+ if (useCases.includes("search") && appId) {
4339
+ const envAppId = await readEnvVar(worktree, appIdVar(language));
4340
+ if (envAppId === appId) {
4341
+ envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
4342
+ } else if (envAppId) {
4343
+ envAppIdMismatch = true;
4344
+ summaries.push(
4345
+ `\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.`
4346
+ );
4347
+ logger.warn(
4348
+ { envAppId, appId },
4349
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4350
+ );
4351
+ }
4352
+ }
4353
+ let finalSearchEnvVars = input.searchEnvVars;
4136
4354
  let agentRuns = 0;
4137
- let ingestRuntime;
4138
- let ingestEntrypoint;
4355
+ let ingestCommand;
4139
4356
  let ingestScriptRan = false;
4140
4357
  let ingestRecordCount;
4141
4358
  let ingestDurationMs;
4142
- let installFailed = false;
4143
4359
  let ingestOutcomeMessage;
4360
+ const ingestKeyAppId = ingestAppId;
4361
+ const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
4362
+ [APP_ID_VAR]: ingestKeyAppId,
4363
+ [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4364
+ [INDEX_NAME_VAR]: targetIndex
4365
+ })) : void 0;
4366
+ const searchTools = makeToolContext(worktree);
4144
4367
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4145
4368
  if (agentRuns > 0) ctx.recordStepExecution();
4146
4369
  agentRuns += 1;
4147
- const result = await runAgent({
4370
+ return runAgent({
4148
4371
  instructions: buildAgentInstructions(
4149
4372
  currentUseCase,
4150
4373
  input,
4151
4374
  extraInstructions
4152
4375
  ),
4153
4376
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4154
- outputSchema: implementationOutputSchema
4155
- });
4156
- ctx.notify({
4157
- messages: [`Installing dependencies for ${currentUseCase}\u2026`]
4377
+ outputSchema: implementationOutputSchema,
4378
+ toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4158
4379
  });
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
4380
  }
4173
4381
  async function runVerificationUseCase() {
4174
4382
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -4176,110 +4384,55 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4176
4384
  return runAgent({
4177
4385
  instructions: buildAgentInstructions("verification", input),
4178
4386
  tools: toolsForUseCase("verification"),
4179
- outputSchema: verificationOutputSchema
4387
+ outputSchema: verificationOutputSchema,
4388
+ toolContext: searchTools
4180
4389
  });
4181
4390
  }
4182
4391
  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
4392
+ const result = await runImplementationUseCase("ingestion");
4393
+ summaries.push(formatSummary("ingestion", result.summary));
4394
+ ingestCommand = result.ingestCommand;
4395
+ const executions = (ingestionTools ?? searchTools).shell.executions;
4396
+ const { run: ingestRun, recordCount } = ingestOutcome(
4397
+ executions,
4398
+ ingestCommand
4399
+ );
4400
+ ingestScriptRan = ingestRun != null;
4401
+ ingestRecordCount = recordCount;
4402
+ ingestDurationMs = ingestRun?.durationMs;
4403
+ if (ingestScriptRan) {
4404
+ ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4405
+ if (ingestRecordCount != null) {
4406
+ track("AI Wizard Ingest Successful", {
4407
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4408
+ record_count: ingestRecordCount,
4409
+ duration_ms: ingestDurationMs ?? 0
4202
4410
  });
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
4411
  }
4412
+ } else {
4413
+ const rejected = executions.some((e) => !e.approved);
4414
+ const reason = rejected ? "you declined to run it" : "no successful run was recorded";
4415
+ ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4416
+ summaries.push(`\u26A0\uFE0F The ingestion script did not run: ${reason}.`);
4417
+ logger.warn(
4418
+ { ingestCommand, rejected, commandsRun: executions.length },
4419
+ "implement: ingestion script did not complete successfully"
4420
+ );
4421
+ track("Error", {
4422
+ step: "Push Data",
4423
+ error: `ingestion did not run: ${reason}`,
4424
+ product_area: "AI Wizard"
4425
+ });
4271
4426
  }
4272
4427
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4273
- if (ingestRuntime && ingestEntrypoint) {
4274
- commandMessages.push(
4275
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4276
- );
4428
+ if (ingestCommand) {
4429
+ commandMessages.push(`Ingestion command: ${ingestCommand}`);
4277
4430
  }
4278
4431
  await ctx.requestUserInput({
4279
4432
  prompt: "",
4280
4433
  promptType: "enterToContinue",
4281
4434
  options: [],
4282
- messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4435
+ messages: [ingestOutcomeMessage, ...commandMessages]
4283
4436
  });
4284
4437
  }
4285
4438
  if (useCases.includes("search")) {
@@ -4324,7 +4477,34 @@ ${run2.output}` : status;
4324
4477
  }
4325
4478
  extraInstructions = verificationRetryInstructions(verification);
4326
4479
  }
4327
- const resolvedSearchEnvVars = input.searchEnvVars.filter(
4480
+ let searchKey;
4481
+ let searchKeyError;
4482
+ if (appId) {
4483
+ try {
4484
+ const resolved2 = await resolveSearchOnlyKey(
4485
+ targetIndex,
4486
+ appId,
4487
+ envSearchKey
4488
+ );
4489
+ searchKey = resolved2.key;
4490
+ summaries.push(
4491
+ resolved2.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}.`
4492
+ );
4493
+ } catch (err) {
4494
+ searchKeyError = err.message;
4495
+ logger.warn(
4496
+ { err: searchKeyError },
4497
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4498
+ );
4499
+ }
4500
+ }
4501
+ finalSearchEnvVars = searchEnvVars(
4502
+ language,
4503
+ targetIndex,
4504
+ appId,
4505
+ searchKey
4506
+ );
4507
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4328
4508
  (v) => !v.value.startsWith("<")
4329
4509
  );
4330
4510
  if (resolvedSearchEnvVars.length > 0) {
@@ -4335,13 +4515,29 @@ ${run2.output}` : status;
4335
4515
  if (written.length > 0) {
4336
4516
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4337
4517
  }
4518
+ const stale = [];
4519
+ for (const v of resolvedSearchEnvVars) {
4520
+ if (written.includes(v.name)) continue;
4521
+ const current = await readEnvVar(worktree, v.name);
4522
+ if (current && current !== v.value) stale.push(v);
4523
+ }
4524
+ if (stale.length > 0 && !envAppIdMismatch) {
4525
+ summaries.push(
4526
+ `\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.`
4527
+ );
4528
+ logger.warn(
4529
+ { vars: stale.map((v) => v.name) },
4530
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4531
+ );
4532
+ }
4338
4533
  }
4339
- const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4534
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4340
4535
  (v) => v.value.startsWith("<")
4341
4536
  );
4342
4537
  if (unresolvedSearchEnvVars.length > 0) {
4343
4538
  summaries.push(
4344
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4539
+ `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.
4540
+ (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4345
4541
  );
4346
4542
  }
4347
4543
  } else {
@@ -4353,27 +4549,18 @@ ${run2.output}` : status;
4353
4549
  "implement: agent reported success but no files changed in the worktree"
4354
4550
  );
4355
4551
  }
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
4552
  return {
4362
4553
  ingestionSource,
4363
4554
  filesChanged,
4364
4555
  summary: summaries.join("\n\n"),
4365
4556
  worktreePath: worktree,
4366
- ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
4367
- ingestCommand: buildIngestCommand(
4368
- worktree,
4369
- ingestRuntime,
4370
- ingestEntrypoint
4371
- ),
4557
+ ...useCases.includes("ingestion") && ingestCommand ? {
4558
+ ingestCommand,
4372
4559
  ingestScriptRan,
4373
4560
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4374
4561
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4375
4562
  } : {},
4376
- ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4563
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4377
4564
  };
4378
4565
  } finally {
4379
4566
  process.chdir(repoRoot);
@@ -4696,10 +4883,11 @@ function parseCliArgs(argv) {
4696
4883
 
4697
4884
  // src/lib/resetState.ts
4698
4885
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4699
- import { join as join11 } from "node:path";
4886
+ import { join as join10 } from "node:path";
4700
4887
  var KEEP = ["wizard.log"];
4701
4888
  async function resetProjectState() {
4702
4889
  const dir = stateDir();
4890
+ forgetResolvedKeys();
4703
4891
  let entries;
4704
4892
  try {
4705
4893
  entries = await readdir4(dir);
@@ -4708,14 +4896,17 @@ async function resetProjectState() {
4708
4896
  }
4709
4897
  const targets = entries.filter((name) => !KEEP.includes(name));
4710
4898
  await Promise.all(
4711
- targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
4899
+ targets.map(
4900
+ (name) => rm2(join10(dir, name), { recursive: true, force: true })
4901
+ )
4712
4902
  );
4713
4903
  return { dir, removed: targets };
4714
4904
  }
4715
4905
 
4716
4906
  // src/main.tsx
4717
- import { jsx as jsx14 } from "react/jsx-runtime";
4907
+ import { jsx as jsx15 } from "react/jsx-runtime";
4718
4908
  async function startup() {
4909
+ setProjectRoot(process.cwd());
4719
4910
  let args;
4720
4911
  try {
4721
4912
  args = parseCliArgs(process.argv.slice(2));
@@ -4763,7 +4954,7 @@ ${formatStepList(workflow)}`);
4763
4954
  }
4764
4955
  async function run(workflow) {
4765
4956
  const store = useWizard.getState();
4766
- const instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4957
+ const instance = render(/* @__PURE__ */ jsx15(App, {}), { incrementalRendering: true });
4767
4958
  await store.waitForStart();
4768
4959
  let user = await getUser();
4769
4960
  if (!user) {