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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/ui/App.tsx
7
- import { Box as Box14, Text as Text14, useApp, useInput as useInput6, useWindowSize as useWindowSize7 } 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";
@@ -12,32 +12,128 @@ import { nanoid } from "nanoid";
12
12
 
13
13
  // src/lib/algoliaCli.ts
14
14
  import { spawn } from "node:child_process";
15
- import { createRequire } from "node:module";
16
- var require2 = createRequire(import.meta.url);
17
- function algoliaCliEntry() {
18
- return require2.resolve("@algolia/cli/bin/run.js");
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");
19
28
  }
20
- function runAlgoliaCli(args) {
29
+ function projectSlug(cwd = process.cwd()) {
30
+ return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
31
+ }
32
+ function stateDir(cwd = process.cwd()) {
33
+ return join(rootDir(), projectSlug(cwd));
34
+ }
35
+
36
+ // src/lib/logger.ts
37
+ var STDERR_FD = 2;
38
+ function resolveDest() {
39
+ const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
40
+ try {
41
+ mkdirSync(dirname(target), { recursive: true });
42
+ closeSync(openSync(target, "a"));
43
+ return target;
44
+ } catch {
45
+ return STDERR_FD;
46
+ }
47
+ }
48
+ function logDestination() {
49
+ return pino.destination({ dest: resolveDest(), sync: false });
50
+ }
51
+ var logger = pino(
52
+ { level: process.env.LOG_LEVEL ?? "info" },
53
+ logDestination()
54
+ );
55
+
56
+ // src/lib/algoliaCli.ts
57
+ function npxArgs(args) {
58
+ return ["--yes", "@algolia/cli@latest", ...args];
59
+ }
60
+ var shell = process.platform === "win32";
61
+ function lineSplitter(emit) {
62
+ let buffer = "";
63
+ return {
64
+ push(chunk) {
65
+ buffer += chunk;
66
+ const lines = buffer.split("\n");
67
+ buffer = lines.pop() ?? "";
68
+ for (const line of lines) emit(line.replace(/\r$/, ""));
69
+ },
70
+ flush() {
71
+ if (buffer) emit(buffer.replace(/\r$/, ""));
72
+ buffer = "";
73
+ }
74
+ };
75
+ }
76
+ var wizardSink = (stream, line) => {
77
+ if (!line.trim()) return;
78
+ useWizard.getState().pushCliOutput(stream, line);
79
+ };
80
+ var stderrSink = (stream, line) => {
81
+ if (stream === "stdout") return;
82
+ wizardSink(stream, line);
83
+ };
84
+ function runAlgoliaCli(args, { onOutput } = {}) {
85
+ const store = useWizard.getState();
86
+ const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
21
87
  return new Promise((resolve4, reject) => {
22
- const child = spawn(process.execPath, [algoliaCliEntry(), ...args]);
88
+ const child = spawn("npx", npxArgs(args), { shell });
23
89
  let stdout = "";
24
90
  let stderr = "";
25
- child.stdout.on("data", (chunk) => stdout += chunk);
26
- child.stderr.on("data", (chunk) => stderr += chunk);
91
+ const splitters = {
92
+ stdout: lineSplitter((line) => onOutput?.("stdout", line)),
93
+ stderr: lineSplitter((line) => onOutput?.("stderr", line))
94
+ };
95
+ child.stdout.on("data", (chunk) => {
96
+ const text = String(chunk);
97
+ stdout += text;
98
+ splitters.stdout.push(text);
99
+ });
100
+ child.stderr.on("data", (chunk) => {
101
+ const text = String(chunk);
102
+ stderr += text;
103
+ splitters.stderr.push(text);
104
+ });
27
105
  child.on("error", reject);
28
106
  child.on("close", (code) => {
107
+ splitters.stdout.flush();
108
+ splitters.stderr.flush();
29
109
  if (code === 0) {
30
110
  resolve4(stdout);
31
111
  } else {
32
- const detail = stderr.trim() || stdout.trim();
112
+ const failed = stderr.trim();
113
+ let detail = "";
114
+ if (failed) {
115
+ detail = `: ${failed}`;
116
+ } else if (stdout.trim()) {
117
+ detail = " (no stderr; stdout withheld \u2014 it may contain credentials)";
118
+ }
33
119
  reject(
34
120
  new Error(
35
- `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail ? `: ${detail}` : ""}`
121
+ `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail}`
36
122
  )
37
123
  );
38
124
  }
39
125
  });
40
- });
126
+ }).then(
127
+ (out) => {
128
+ useWizard.getState().logEnd(logId, "success");
129
+ return out;
130
+ },
131
+ (err) => {
132
+ useWizard.getState().logEnd(logId, "error");
133
+ logger.warn({ err, args }, "Algolia CLI command failed");
134
+ throw err;
135
+ }
136
+ );
41
137
  }
42
138
  async function getUser() {
43
139
  let raw;
@@ -52,19 +148,23 @@ async function getUser() {
52
148
  return null;
53
149
  }
54
150
  }
55
- function runAuthLogin() {
56
- return new Promise((resolve4, reject) => {
57
- const child = spawn(
58
- process.execPath,
59
- [algoliaCliEntry(), "auth", "login", "--default"],
60
- { stdio: "inherit" }
61
- );
62
- child.on("error", reject);
63
- child.on("close", (code) => {
64
- if (code === 0) resolve4();
65
- else reject(new Error(`Algolia authentication failed (exit ${code}).`));
66
- });
151
+ var loginResultSchema = z.object({
152
+ success: z.boolean(),
153
+ email: z.string().optional()
154
+ });
155
+ async function runAuthLogin() {
156
+ const raw = await runAlgoliaCli(["auth", "login", "--non-interactive"], {
157
+ onOutput: stderrSink
67
158
  });
159
+ let parsed;
160
+ try {
161
+ parsed = loginResultSchema.safeParse(JSON.parse(raw));
162
+ } catch {
163
+ parsed = void 0;
164
+ }
165
+ if (parsed?.success && !parsed.data.success) {
166
+ throw new Error("Algolia sign-in did not report success.");
167
+ }
68
168
  }
69
169
 
70
170
  // src/lib/auth.ts
@@ -88,45 +188,6 @@ function refreshAuthToken() {
88
188
  return inFlightRefresh;
89
189
  }
90
190
 
91
- // src/lib/logger.ts
92
- import pino from "pino";
93
- import { join as join2, dirname } from "node:path";
94
- import { devNull } from "node:os";
95
- import { mkdirSync, openSync, closeSync } from "node:fs";
96
-
97
- // src/core/constants.ts
98
- import { homedir } from "node:os";
99
- import { join, resolve } from "node:path";
100
- function rootDir() {
101
- return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
102
- }
103
- function projectSlug(cwd = process.cwd()) {
104
- return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
105
- }
106
- function stateDir(cwd = process.cwd()) {
107
- return join(rootDir(), projectSlug(cwd));
108
- }
109
-
110
- // src/lib/logger.ts
111
- var STDERR_FD = 2;
112
- function resolveDest() {
113
- const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
114
- try {
115
- mkdirSync(dirname(target), { recursive: true });
116
- closeSync(openSync(target, "a"));
117
- return target;
118
- } catch {
119
- return STDERR_FD;
120
- }
121
- }
122
- function logDestination() {
123
- return pino.destination({ dest: resolveDest(), sync: false });
124
- }
125
- var logger = pino(
126
- { level: process.env.LOG_LEVEL ?? "info" },
127
- logDestination()
128
- );
129
-
130
191
  // src/lib/proxyFetch.ts
131
192
  var PROXY_BASE_URL = process.env.PROXY_BASE_URL ?? "https://proxy-624203421261.us-east4.run.app";
132
193
  var PROXY_AUTH_REJECTED_HEADER = "x-wizard-proxy-auth";
@@ -171,6 +232,7 @@ function describeInputValue(value) {
171
232
  return Array.isArray(value) ? value.join(", ") : value;
172
233
  }
173
234
  var NOTICE_INTERVAL_MS = 2e3;
235
+ var CLI_OUTPUT_LIMIT = 200;
174
236
  var useWizard = create((set, get) => ({
175
237
  phase: "idle",
176
238
  homeScreen: "home",
@@ -182,22 +244,21 @@ var useWizard = create((set, get) => ({
182
244
  notices: [],
183
245
  _noticeQueue: [],
184
246
  _noticeTimer: null,
247
+ cliOutput: [],
248
+ targetIndex: null,
185
249
  logs: [],
186
250
  error: null,
187
251
  inputReq: null,
188
252
  _resolve: null,
189
- // Advances past the welcome screen. Only meaningful from 'idle' once the
190
- // workflow is running there's nothing left to confirm.
191
- // Reset `homeScreen` so preflight shows Welcome, not the Learn more sub-view.
253
+ // `endAuth` lands on 'preflight', not 'idle': sign-in happens after the
254
+ // welcome screen, so going back would gate the run a second time.
255
+ beginAuth: () => set({ phase: "authenticating", cliOutput: [] }),
256
+ endAuth: () => set((s) => s.phase === "authenticating" ? { phase: "preflight" } : {}),
192
257
  confirmStart: () => set(
193
258
  (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
194
259
  ),
195
- // Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
196
260
  openLearnMore: () => set({ homeScreen: "learnMore" }),
197
261
  backToHome: () => set({ homeScreen: "home" }),
198
- // Resolves once the phase leaves 'idle', whether that happens before or
199
- // after this is called (the welcome screen's enter handler is what
200
- // drives the transition via `confirmStart`).
201
262
  waitForStart: () => new Promise((resolve4) => {
202
263
  if (get().phase !== "idle") {
203
264
  resolve4();
@@ -220,15 +281,19 @@ var useWizard = create((set, get) => ({
220
281
  syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
221
282
  setActiveStep: (index) => {
222
283
  get()._clearNoticeQueue();
223
- set({ phase: "running", currentStepIndex: index, output: "", notices: [] });
284
+ set({
285
+ phase: "running",
286
+ currentStepIndex: index,
287
+ output: "",
288
+ notices: [],
289
+ cliOutput: []
290
+ });
224
291
  },
225
292
  setUser: (user) => set({ user }),
226
293
  appendToken: (text) => set((s) => ({ output: s.output + text })),
227
294
  clearOutput: () => set({ output: "" }),
228
- // Renders the first notice of a burst immediately, then holds later
229
- // arrivals in `_noticeQueue` and drains one per `NOTICE_INTERVAL_MS` —
230
- // the timer stays armed through an empty drain so the cooldown always
231
- // covers the time since the last render, even across bursts.
295
+ // The timer stays armed through an empty drain, so the spacing covers the
296
+ // time since the last render even across bursts.
232
297
  pushNotice: (notice) => {
233
298
  const { notices, _noticeQueue, _noticeTimer } = get();
234
299
  if (_noticeTimer === null) {
@@ -261,6 +326,13 @@ var useWizard = create((set, get) => ({
261
326
  get()._clearNoticeQueue();
262
327
  set({ notices: [] });
263
328
  },
329
+ pushCliOutput: (stream, text) => set((s) => ({
330
+ cliOutput: [...s.cliOutput, { id: nanoid(), stream, text }].slice(
331
+ -CLI_OUTPUT_LIMIT
332
+ )
333
+ })),
334
+ clearCliOutput: () => set({ cliOutput: [] }),
335
+ setTargetIndex: (index) => set({ targetIndex: index }),
264
336
  logStart: (kind, name, input) => {
265
337
  const id = nanoid();
266
338
  set((s) => ({
@@ -283,9 +355,6 @@ var useWizard = create((set, get) => ({
283
355
  _resolve: resolve4
284
356
  });
285
357
  }),
286
- // Logs what the user picked — not the prompt text that was shown, which
287
- // may repeat or duplicate on-screen content and isn't the useful signal
288
- // here.
289
358
  submitInput: async (value) => {
290
359
  await markInteraction();
291
360
  get()._resolve?.(value);
@@ -305,6 +374,8 @@ var useWizard = create((set, get) => ({
305
374
  currentStepIndex: 0,
306
375
  output: "",
307
376
  notices: [],
377
+ cliOutput: [],
378
+ targetIndex: null,
308
379
  logs: [],
309
380
  error: null,
310
381
  inputReq: null,
@@ -313,16 +384,100 @@ var useWizard = create((set, get) => ({
313
384
  }
314
385
  }));
315
386
 
387
+ // src/ui/CliOutput.tsx
388
+ import { Box, Text, useWindowSize } from "ink";
389
+
390
+ // src/ui/theme.ts
391
+ var MARKER = {
392
+ pending: "\u25CB",
393
+ running: "\u25D0",
394
+ done: "\u2713",
395
+ error: "\u2716"
396
+ };
397
+ var BRAND = "#003DFF";
398
+ var SECONDARY = "#5468FF";
399
+ var DANGER = "#F86E7E";
400
+ var COLORS = {
401
+ brand: BRAND,
402
+ primary: "#E6EDF3",
403
+ secondary: SECONDARY,
404
+ strong: "#FFFFFF",
405
+ muted: "#8B949E",
406
+ dim: "#484F58",
407
+ highlight: { bg: "#12331C", fg: "#4ADE80" },
408
+ badge: "#E3B341",
409
+ danger: DANGER,
410
+ success: "#4ADE80",
411
+ bg: {
412
+ main: "#0B0E14",
413
+ sidebar: "#14171E"
414
+ },
415
+ border: "#30363D",
416
+ accent: "#76A0FF",
417
+ status: {
418
+ pending: "gray",
419
+ running: "#76A0FF",
420
+ done: "#4ADE80",
421
+ error: DANGER
422
+ }
423
+ };
424
+
425
+ // src/ui/CliOutput.tsx
426
+ import { jsxs } from "react/jsx-runtime";
427
+ var CLI_MARKER = "\u203A";
428
+ var RESERVED_ROWS = 16;
429
+ var MAX_ROWS = 12;
430
+ var PANEL_TEXT_WIDTH = 45;
431
+ var URL_PATTERN = /https?:\/\//;
432
+ function rowCost(text) {
433
+ return URL_PATTERN.test(text) ? Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH)) : 1;
434
+ }
435
+ function CliOutput() {
436
+ const cliOutput = useWizard((s) => s.cliOutput);
437
+ const { rows } = useWindowSize();
438
+ if (!cliOutput.length) return null;
439
+ const rowBudget = Math.min(Math.max(rows - RESERVED_ROWS, 3), MAX_ROWS);
440
+ const visible = [];
441
+ let usedRows = 0;
442
+ for (let i = cliOutput.length - 1; i >= 0; i--) {
443
+ const cost = rowCost(cliOutput[i].text);
444
+ if (usedRows + cost > rowBudget && visible.length > 0) break;
445
+ visible.unshift(cliOutput[i]);
446
+ usedRows += cost;
447
+ }
448
+ const hidden = cliOutput.length - visible.length;
449
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
450
+ hidden > 0 && /* @__PURE__ */ jsxs(Text, { color: COLORS.dim, children: [
451
+ "\u2191 ",
452
+ hidden,
453
+ " earlier line(s)"
454
+ ] }),
455
+ visible.map((line) => /* @__PURE__ */ jsxs(
456
+ Text,
457
+ {
458
+ color: line.stream === "stderr" ? COLORS.muted : COLORS.dim,
459
+ wrap: URL_PATTERN.test(line.text) ? "wrap" : "truncate",
460
+ children: [
461
+ CLI_MARKER,
462
+ " ",
463
+ line.text
464
+ ]
465
+ },
466
+ line.id
467
+ ))
468
+ ] });
469
+ }
470
+
316
471
  // src/ui/Notices.tsx
317
- import { Box as Box2, Text as Text2, useWindowSize as useWindowSize2 } from "ink";
472
+ import { Box as Box3, Text as Text3, useWindowSize as useWindowSize3 } from "ink";
318
473
  import { useEffect as useEffect2, useState as useState2 } from "react";
319
474
 
320
475
  // src/ui/Table.tsx
321
- import { Box, Text, measureElement, useWindowSize } from "ink";
476
+ import { Box as Box2, Text as Text2, measureElement, useWindowSize as useWindowSize2 } from "ink";
322
477
  import { useEffect, useRef, useState } from "react";
323
478
  import { jsx } from "react/jsx-runtime";
324
479
  function Table({ columns, rows }) {
325
- const { columns: termCols } = useWindowSize();
480
+ const { columns: termCols } = useWindowSize2();
326
481
  const ref = useRef(null);
327
482
  const [width, setWidth] = useState(0);
328
483
  useEffect(() => {
@@ -330,7 +485,7 @@ function Table({ columns, rows }) {
330
485
  }, [termCols, columns, rows]);
331
486
  if (rows.length === 0) return null;
332
487
  const lines = formatTable(columns, rows, width || void 0);
333
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: line }, `tbl-${i}`)) });
488
+ return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text2, { wrap: "truncate", children: line }, `tbl-${i}`)) });
334
489
  }
335
490
  function formatTable(columns, rows, width) {
336
491
  const natural = columns.map(
@@ -370,48 +525,13 @@ function resize(widths, budget) {
370
525
  }
371
526
  var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, width) : `${s.slice(0, width - 1)}\u2026`;
372
527
 
373
- // src/ui/theme.ts
374
- var MARKER = {
375
- pending: "\u25CB",
376
- running: "\u25D0",
377
- done: "\u2713",
378
- error: "\u2716"
379
- };
380
- var BRAND = "#003DFF";
381
- var SECONDARY = "#5468FF";
382
- var DANGER = "#F86E7E";
383
- var COLORS = {
384
- brand: BRAND,
385
- primary: "#E6EDF3",
386
- secondary: SECONDARY,
387
- strong: "#FFFFFF",
388
- muted: "#8B949E",
389
- dim: "#484F58",
390
- highlight: { bg: "#12331C", fg: "#4ADE80" },
391
- badge: "#E3B341",
392
- danger: DANGER,
393
- success: "#4ADE80",
394
- bg: {
395
- main: "#0B0E14",
396
- sidebar: "#14171E"
397
- },
398
- border: "#30363D",
399
- accent: "#76A0FF",
400
- status: {
401
- pending: "gray",
402
- running: "#76A0FF",
403
- done: "#4ADE80",
404
- error: DANGER
405
- }
406
- };
407
-
408
528
  // src/ui/Notices.tsx
409
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
529
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
410
530
  var AGENT_MARKER = "\u2726";
411
- var RESERVED_ROWS = 14;
412
- var PANEL_TEXT_WIDTH = 45;
531
+ var RESERVED_ROWS2 = 14;
532
+ var PANEL_TEXT_WIDTH2 = 45;
413
533
  function messageLineCount(text) {
414
- return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH));
534
+ return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH2));
415
535
  }
416
536
  function noticeLineCount(notice) {
417
537
  const messageLines = (notice.messages ?? []).reduce((sum, m) => {
@@ -422,7 +542,7 @@ function noticeLineCount(notice) {
422
542
  return messageLines + tableLines;
423
543
  }
424
544
  function fitVisibleNotices(notices, windowRows) {
425
- const budget = Math.max(windowRows - RESERVED_ROWS, 3);
545
+ const budget = Math.max(windowRows - RESERVED_ROWS2, 3);
426
546
  let used = 0;
427
547
  let count = 0;
428
548
  for (let i = notices.length - 1; i >= 0; i--) {
@@ -455,7 +575,7 @@ function parseHex(hex) {
455
575
  }
456
576
  function Notices() {
457
577
  const notices = useWizard((s) => s.notices);
458
- const { rows: windowRows } = useWindowSize2();
578
+ const { rows: windowRows } = useWindowSize3();
459
579
  const visible = fitVisibleNotices(notices, windowRows);
460
580
  const [pulseStep, setPulseStep] = useState2(0);
461
581
  useEffect2(() => {
@@ -472,14 +592,14 @@ function Notices() {
472
592
  }, []);
473
593
  if (!visible.length) return null;
474
594
  const pulseColor = PULSE_COLORS[pulseStep];
475
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
595
+ return /* @__PURE__ */ jsx2(Box3, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
476
596
  const isLatest = i === visible.length - 1;
477
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
597
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
478
598
  notice.messages?.map((m, j) => {
479
599
  const line = typeof m === "string" ? { text: m } : m;
480
600
  const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
481
- return /* @__PURE__ */ jsxs(
482
- Text2,
601
+ return /* @__PURE__ */ jsxs2(
602
+ Text3,
483
603
  {
484
604
  color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
485
605
  bold: line.bold,
@@ -497,41 +617,99 @@ function Notices() {
497
617
  }
498
618
 
499
619
  // src/ui/PromptInput.tsx
500
- import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
620
+ import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
501
621
  import TextInput from "ink-text-input";
502
622
  import { useState as useState5 } from "react";
503
623
 
624
+ // src/ui/CommandApproval.tsx
625
+ import { Box as Box5, Text as Text5, useInput } from "ink";
626
+
504
627
  // src/ui/NextAction.tsx
505
- import { Box as Box3, Text as Text3 } from "ink";
506
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
628
+ import { Box as Box4, Text as Text4 } from "ink";
629
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
507
630
  function NextAction({
508
631
  action,
509
632
  keyHint,
510
633
  hierarchy = "primary"
511
634
  }) {
512
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "row", gap: 1, children: [
513
- hierarchy === "primary" && /* @__PURE__ */ jsx3(Text3, { color: COLORS.success, bold: true, children: `> ${action}` }),
514
- hierarchy === "secondary" && /* @__PURE__ */ jsxs2(Fragment, { children: [
515
- /* @__PURE__ */ jsx3(Text3, { color: COLORS.success, bold: true, children: `>` }),
516
- /* @__PURE__ */ jsx3(Text3, { color: COLORS.primary, bold: true, children: action })
635
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
636
+ hierarchy === "primary" && /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `> ${action}` }),
637
+ hierarchy === "secondary" && /* @__PURE__ */ jsxs3(Fragment, { children: [
638
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `>` }),
639
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, bold: true, children: action })
517
640
  ] }),
518
- /* @__PURE__ */ jsxs2(Box3, { children: [
519
- /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: "press " }),
520
- /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: `[` }),
521
- /* @__PURE__ */ jsx3(Text3, { color: COLORS.primary, children: keyHint }),
522
- /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: `]` })
641
+ /* @__PURE__ */ jsxs3(Box4, { children: [
642
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: "press " }),
643
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `[` }),
644
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, children: keyHint }),
645
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `]` })
646
+ ] })
647
+ ] });
648
+ }
649
+
650
+ // src/ui/CommandApproval.tsx
651
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
652
+ function CommandApproval({
653
+ command,
654
+ onDecide
655
+ }) {
656
+ useInput((input, key) => {
657
+ if (key.return) onDecide("approve");
658
+ else if (key.escape) onDecide("reject");
659
+ else if (input.toLowerCase() === "a") onDecide("always");
660
+ });
661
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
662
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, bold: true, children: "Run this command?" }),
663
+ /* @__PURE__ */ jsxs4(
664
+ Box5,
665
+ {
666
+ flexDirection: "column",
667
+ paddingLeft: 2,
668
+ borderStyle: "single",
669
+ borderColor: COLORS.success,
670
+ borderTop: false,
671
+ borderBottom: false,
672
+ borderRight: false,
673
+ gap: 1,
674
+ children: [
675
+ /* @__PURE__ */ jsxs4(Box5, { children: [
676
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "$ " }),
677
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.strong, wrap: "wrap", children: command.command })
678
+ ] }),
679
+ /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
680
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "in:" }),
681
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, wrap: "wrap", children: command.cwd })
682
+ ] }),
683
+ command.explanation && /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
684
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: "why:" }),
685
+ /* @__PURE__ */ jsx4(Text5, { color: COLORS.accent, wrap: "wrap", children: command.explanation })
686
+ ] })
687
+ ]
688
+ }
689
+ ),
690
+ /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
691
+ /* @__PURE__ */ jsx4(NextAction, { action: "approve", keyHint: "enter" }),
692
+ /* @__PURE__ */ jsx4(NextAction, { action: "reject", keyHint: "esc", hierarchy: "secondary" }),
693
+ /* @__PURE__ */ jsx4(
694
+ NextAction,
695
+ {
696
+ action: "approve, and don't ask again for this command",
697
+ keyHint: "a",
698
+ hierarchy: "secondary"
699
+ }
700
+ )
523
701
  ] })
524
702
  ] });
525
703
  }
526
704
 
527
705
  // src/ui/SelectPrompt.tsx
528
- import { Box as Box5, Text as Text5, useInput, useWindowSize as useWindowSize4 } from "ink";
706
+ import { Box as Box7, Text as Text7, useInput as useInput2, useWindowSize as useWindowSize5 } from "ink";
529
707
  import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
530
708
 
531
709
  // src/ui/ScrollView.tsx
532
- import { Box as Box4, Text as Text4, measureElement as measureElement2, useWindowSize as useWindowSize3 } from "ink";
710
+ import { Box as Box6, Text as Text6, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
533
711
  import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
534
- import { jsxs as jsxs3 } from "react/jsx-runtime";
712
+ import { jsxs as jsxs5 } from "react/jsx-runtime";
535
713
  var INDICATOR_ROWS = 2;
536
714
  function fittedWidth(node, columns) {
537
715
  let left = 0;
@@ -546,7 +724,7 @@ function useScrollWindow({
546
724
  followBottom = false
547
725
  }) {
548
726
  const viewportRef = useRef2(null);
549
- const { columns } = useWindowSize3();
727
+ const { columns } = useWindowSize4();
550
728
  const [size, setSize] = useState3(
551
729
  null
552
730
  );
@@ -601,14 +779,14 @@ function useScrollWindow({
601
779
  };
602
780
  }
603
781
  function ScrollView({ scroll, children }) {
604
- return /* @__PURE__ */ jsxs3(Box4, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
605
- scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
782
+ return /* @__PURE__ */ jsxs5(Box6, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
783
+ scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
606
784
  "\u2191 ",
607
785
  scroll.hiddenAbove,
608
786
  " more"
609
787
  ] }),
610
788
  children,
611
- scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
789
+ scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
612
790
  "\u2193 ",
613
791
  scroll.hiddenBelow,
614
792
  " more"
@@ -617,7 +795,7 @@ function ScrollView({ scroll, children }) {
617
795
  }
618
796
 
619
797
  // src/ui/SelectPrompt.tsx
620
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
798
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
621
799
  var CANCEL = "cancel";
622
800
  var ARROW_WIDTH = 4;
623
801
  var COLUMN_GAP = 2;
@@ -648,7 +826,7 @@ function SelectPrompt({
648
826
  if (multi) hints.push({ key: "[space]", label: "select" });
649
827
  hints.push({ key: "[enter]", label: "confirm" });
650
828
  const containerRef = useRef3(null);
651
- const { columns } = useWindowSize4();
829
+ const { columns } = useWindowSize5();
652
830
  const [width, setWidth] = useState4(columns);
653
831
  useLayoutEffect2(() => {
654
832
  if (!containerRef.current) return;
@@ -679,7 +857,7 @@ function SelectPrompt({
679
857
  revealIndex(index);
680
858
  }, [index, revealIndex]);
681
859
  const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
682
- useInput((input, key) => {
860
+ useInput2((input, key) => {
683
861
  if (rows.length === 0) return;
684
862
  if (key.upArrow || input === "k") {
685
863
  setIndex((i) => (i - 1 + rows.length) % rows.length);
@@ -702,56 +880,56 @@ function SelectPrompt({
702
880
  }
703
881
  }
704
882
  });
705
- return /* @__PURE__ */ jsx4(Box5, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, width, children: [
706
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
707
- error && /* @__PURE__ */ jsx4(Text5, { color: COLORS.danger, children: error }),
708
- messages?.map((m, i) => /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
709
- table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
710
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
711
- question && /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: question }),
712
- helpText && /* @__PURE__ */ jsx4(Text5, { color: COLORS.dim, children: helpText })
883
+ return /* @__PURE__ */ jsx5(Box7, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, width, children: [
884
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
885
+ error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: error }),
886
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
887
+ table && /* @__PURE__ */ jsx5(Table, { columns: table.columns, rows: table.rows }),
888
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
889
+ question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: question }),
890
+ helpText && /* @__PURE__ */ jsx5(Text7, { color: COLORS.dim, children: helpText })
713
891
  ] })
714
892
  ] }),
715
- /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
893
+ /* @__PURE__ */ jsx5(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
716
894
  const i = scroll.offset + visibleIndex;
717
895
  const highlighted = i === index;
718
896
  const isCancel = i === cancelIndex;
719
897
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
720
898
  const sec = isCancel ? void 0 : secondary?.[i];
721
899
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
722
- const label = /* @__PURE__ */ jsxs4(Text5, { color: labelColor, wrap: "truncate", children: [
900
+ const label = /* @__PURE__ */ jsxs6(Text7, { color: labelColor, wrap: "truncate", children: [
723
901
  highlighted ? "\u276F " : " ",
724
902
  bullet,
725
903
  option
726
904
  ] });
727
905
  const isText = sec?.kind === "text";
728
- return /* @__PURE__ */ jsxs4(
729
- Box5,
906
+ return /* @__PURE__ */ jsxs6(
907
+ Box7,
730
908
  {
731
909
  width: isText ? "100%" : barWidth,
732
910
  paddingX: 1,
733
911
  paddingY: 1,
734
912
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
735
913
  children: [
736
- /* @__PURE__ */ jsx4(Box5, { width: isText ? labelWidth : barLabelWidth, children: label }),
737
- isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box5, { width: textWidth, children: /* @__PURE__ */ jsx4(
738
- Text5,
914
+ /* @__PURE__ */ jsx5(Box7, { width: isText ? labelWidth : barLabelWidth, children: label }),
915
+ isText && textWidth > 0 && /* @__PURE__ */ jsx5(Box7, { width: textWidth, children: /* @__PURE__ */ jsx5(
916
+ Text7,
739
917
  {
740
918
  wrap: "truncate",
741
919
  color: highlighted ? COLORS.primary : COLORS.muted,
742
920
  children: sec.value
743
921
  }
744
922
  ) }),
745
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box5, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text5, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
923
+ sec?.kind === "badge" && /* @__PURE__ */ jsx5(Box7, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text7, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
746
924
  ]
747
925
  },
748
926
  `row-${i}`
749
927
  );
750
928
  }) }),
751
- /* @__PURE__ */ jsx4(Box5, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text5, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs4(Text5, { children: [
929
+ /* @__PURE__ */ jsx5(Box7, { flexShrink: 0, children: /* @__PURE__ */ jsx5(Text7, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs6(Text7, { children: [
752
930
  i > 0 ? " " : "",
753
- /* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, children: key }),
754
- /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
931
+ /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: key }),
932
+ /* @__PURE__ */ jsxs6(Text7, { color: COLORS.dim, children: [
755
933
  " ",
756
934
  label
757
935
  ] })
@@ -760,23 +938,23 @@ function SelectPrompt({
760
938
  }
761
939
 
762
940
  // src/ui/PromptInput.tsx
763
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
941
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
764
942
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
765
943
  function EnterToContinuePrompt({
766
944
  question,
767
945
  messages,
768
946
  onDecide
769
947
  }) {
770
- useInput2((_input, key) => {
948
+ useInput3((_input, key) => {
771
949
  if (key.return) onDecide(true);
772
950
  else if (key.escape) onDecide(false);
773
951
  });
774
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, children: [
775
- messages?.map((m, i) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
776
- question && /* @__PURE__ */ jsx5(Text6, { color: COLORS.primary, children: question }),
777
- /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
778
- /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
779
- /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
952
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
953
+ messages?.map((m, i) => /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: m }, `msg-${i}`)),
954
+ question && /* @__PURE__ */ jsx6(Text8, { color: COLORS.primary, children: question }),
955
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
956
+ /* @__PURE__ */ jsx6(NextAction, { action: "continue", keyHint: "enter" }),
957
+ /* @__PURE__ */ jsx6(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
780
958
  ] })
781
959
  ] });
782
960
  }
@@ -784,11 +962,11 @@ function PromptInput() {
784
962
  const { phase, inputReq, submitInput } = useWizard();
785
963
  const [draft, setDraft] = useState5("");
786
964
  if (phase === "done" || phase === "error") {
787
- return /* @__PURE__ */ jsx5(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text6, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
965
+ return /* @__PURE__ */ jsx6(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text8, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
788
966
  }
789
967
  if (phase !== "awaitingInput" || !inputReq) return null;
790
968
  if (inputReq.promptType === "multipleChoice") {
791
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
969
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
792
970
  SelectPrompt,
793
971
  {
794
972
  question: inputReq.prompt,
@@ -805,7 +983,7 @@ function PromptInput() {
805
983
  ) });
806
984
  }
807
985
  if (inputReq.promptType === "multiSelect") {
808
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
986
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
809
987
  SelectPrompt,
810
988
  {
811
989
  multi: true,
@@ -820,7 +998,7 @@ function PromptInput() {
820
998
  ) });
821
999
  }
822
1000
  if (inputReq.promptType === "notice") {
823
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
1001
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
824
1002
  SelectPrompt,
825
1003
  {
826
1004
  question: inputReq.prompt,
@@ -831,7 +1009,7 @@ function PromptInput() {
831
1009
  ) });
832
1010
  }
833
1011
  if (inputReq.promptType === "enterToContinue") {
834
- return /* @__PURE__ */ jsx5(
1012
+ return /* @__PURE__ */ jsx6(
835
1013
  EnterToContinuePrompt,
836
1014
  {
837
1015
  question: inputReq.prompt,
@@ -840,9 +1018,12 @@ function PromptInput() {
840
1018
  }
841
1019
  );
842
1020
  }
1021
+ if (inputReq.promptType === "commandApproval" && inputReq.command) {
1022
+ return /* @__PURE__ */ jsx6(CommandApproval, { command: inputReq.command, onDecide: submitInput });
1023
+ }
843
1024
  if (inputReq.promptType === "acceptReject") {
844
1025
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
845
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
1026
+ return /* @__PURE__ */ jsx6(Box8, { flexGrow: 1, children: /* @__PURE__ */ jsx6(
846
1027
  SelectPrompt,
847
1028
  {
848
1029
  question: inputReq.prompt,
@@ -853,15 +1034,15 @@ function PromptInput() {
853
1034
  }
854
1035
  ) });
855
1036
  }
856
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
857
- inputReq.error && /* @__PURE__ */ jsx5(Text6, { color: COLORS.danger, children: inputReq.error }),
858
- inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
859
- /* @__PURE__ */ jsxs5(Box6, { children: [
860
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.primary, children: [
1037
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1038
+ inputReq.error && /* @__PURE__ */ jsx6(Text8, { color: COLORS.danger, children: inputReq.error }),
1039
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: m }, `msg-${i}`)),
1040
+ /* @__PURE__ */ jsxs7(Box8, { children: [
1041
+ /* @__PURE__ */ jsxs7(Text8, { color: COLORS.primary, children: [
861
1042
  inputReq.prompt,
862
1043
  " "
863
1044
  ] }),
864
- /* @__PURE__ */ jsx5(
1045
+ /* @__PURE__ */ jsx6(
865
1046
  TextInput,
866
1047
  {
867
1048
  value: draft,
@@ -879,7 +1060,7 @@ function PromptInput() {
879
1060
  // src/ui/Welcome.tsx
880
1061
  import { dirname as dirname2, join as join3 } from "node:path";
881
1062
  import { fileURLToPath } from "node:url";
882
- import { Box as Box7, Spacer, Text as Text7, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
1063
+ import { Box as Box9, Spacer, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
883
1064
 
884
1065
  // src/ui/copy/welcome.ts
885
1066
  var sidebarItems = [
@@ -892,12 +1073,12 @@ var sidebarItems = [
892
1073
  description: "push 100 records to Algolia in seconds"
893
1074
  },
894
1075
  {
895
- title: "detect your framework",
896
- description: "React, Vue, Angular, Vanilla JS"
1076
+ title: "detect your stack",
1077
+ description: "whatever language and framework you already use"
897
1078
  },
898
1079
  {
899
1080
  title: "scaffold a search UI",
900
- description: "a styled InstantSearch component, wired into your app"
1081
+ description: "a search box and results, wired into your app"
901
1082
  },
902
1083
  {
903
1084
  title: "ship it",
@@ -907,28 +1088,28 @@ var sidebarItems = [
907
1088
 
908
1089
  // src/ui/Welcome.tsx
909
1090
  import Image, { InkPictureProvider } from "ink-picture";
910
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1091
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
911
1092
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
912
1093
  function SidebarItem({
913
1094
  title,
914
1095
  description
915
1096
  }) {
916
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
917
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, children: [
918
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.success, children: "\u2192" }),
919
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.strong, bold: true, children: title })
1097
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
1098
+ /* @__PURE__ */ jsxs8(Box9, { gap: 1, children: [
1099
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, children: "\u2192" }),
1100
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: title })
920
1101
  ] }),
921
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 2, children: [
922
- /* @__PURE__ */ jsx6(Spacer, {}),
923
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: description })
1102
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 2, children: [
1103
+ /* @__PURE__ */ jsx7(Spacer, {}),
1104
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: description })
924
1105
  ] })
925
1106
  ] });
926
1107
  }
927
1108
  function Welcome() {
928
1109
  const confirmStart = useWizard((s) => s.confirmStart);
929
1110
  const openLearnMore = useWizard((s) => s.openLearnMore);
930
- const { rows } = useWindowSize5();
931
- useInput3((input, key) => {
1111
+ const { rows } = useWindowSize6();
1112
+ useInput4((input, key) => {
932
1113
  if (key.return) confirmStart();
933
1114
  else if (input === "i") openLearnMore();
934
1115
  });
@@ -946,16 +1127,16 @@ function Welcome() {
946
1127
  if (rows < 30) {
947
1128
  layout = scales["small"];
948
1129
  }
949
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
950
- /* @__PURE__ */ jsx6(
951
- Box7,
1130
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1131
+ /* @__PURE__ */ jsx7(
1132
+ Box9,
952
1133
  {
953
1134
  paddingY: layout.main.padding.y,
954
1135
  paddingX: layout.main.padding.x,
955
1136
  flexDirection: "column",
956
1137
  justifyContent: "center",
957
- children: /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 2, children: [
958
- /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
1138
+ children: /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", gap: 2, children: [
1139
+ /* @__PURE__ */ jsx7(InkPictureProvider, { children: /* @__PURE__ */ jsx7(
959
1140
  Image,
960
1141
  {
961
1142
  src: IMAGE_PATH,
@@ -966,16 +1147,16 @@ function Welcome() {
966
1147
  protocol: "halfBlock"
967
1148
  }
968
1149
  ) }),
969
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
970
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
971
- /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
972
- /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
1150
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1151
+ /* @__PURE__ */ jsxs8(Box9, { gap: 1, flexDirection: "column", children: [
1152
+ /* @__PURE__ */ jsx7(NextAction, { action: "start wizard", keyHint: "enter" }),
1153
+ /* @__PURE__ */ jsx7(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
973
1154
  ] })
974
1155
  ] })
975
1156
  }
976
1157
  ),
977
- /* @__PURE__ */ jsxs6(
978
- Box7,
1158
+ /* @__PURE__ */ jsxs8(
1159
+ Box9,
979
1160
  {
980
1161
  backgroundColor: COLORS.bg.sidebar,
981
1162
  width: 40,
@@ -985,8 +1166,8 @@ function Welcome() {
985
1166
  flexDirection: "column",
986
1167
  justifyContent: "center",
987
1168
  children: [
988
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
989
- sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
1169
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1170
+ sidebarItems.map((i, idx) => /* @__PURE__ */ jsx7(SidebarItem, { title: i.title, description: i.description }, idx))
990
1171
  ]
991
1172
  }
992
1173
  )
@@ -995,7 +1176,7 @@ function Welcome() {
995
1176
 
996
1177
  // src/ui/LearnMore.tsx
997
1178
  import { Fragment as Fragment2 } from "react";
998
- import { Box as Box8, Text as Text8, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
1179
+ import { Box as Box10, Text as Text10, useInput as useInput5, useWindowSize as useWindowSize7 } from "ink";
999
1180
 
1000
1181
  // src/ui/copy/learn-more.ts
1001
1182
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -1003,12 +1184,17 @@ var accessItems = [
1003
1184
  {
1004
1185
  tag: "READ",
1005
1186
  title: "Project files",
1006
- description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
1187
+ description: "reads manifests, configs & source to detect your stack. Read-only; nothing is uploaded."
1007
1188
  },
1008
1189
  {
1009
1190
  tag: "WRITE",
1010
1191
  title: "Code changes",
1011
- description: "creates & edits files (search UI, config). Shown as a diff first \u2014 nothing lands without your approval."
1192
+ description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1193
+ },
1194
+ {
1195
+ tag: "EXEC",
1196
+ title: "Setup commands",
1197
+ description: "runs dependency installs, the ingestion script & your own checks. Every command is shown in full and needs your OK; its output is shown as-is, so a command that prints a secret will display it."
1012
1198
  },
1013
1199
  {
1014
1200
  tag: "NET",
@@ -1018,13 +1204,13 @@ var accessItems = [
1018
1204
  {
1019
1205
  tag: "KEY",
1020
1206
  title: "Credentials",
1021
- description: "saves your Admin API key to .env and adds it to .gitignore."
1207
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1022
1208
  }
1023
1209
  ];
1024
1210
  var neverItems = [
1025
1211
  "Send your source code to a model or third party",
1026
1212
  "Commit or push to git",
1027
- "Touch files outside your project directory"
1213
+ "Run a command you haven't approved"
1028
1214
  ];
1029
1215
  var policyLinks = [
1030
1216
  { label: "Terms", url: "https://www.algolia.com/policies/terms" },
@@ -1032,10 +1218,11 @@ var policyLinks = [
1032
1218
  ];
1033
1219
 
1034
1220
  // src/ui/LearnMore.tsx
1035
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1221
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1036
1222
  var TAG_COLORS = {
1037
1223
  READ: COLORS.success,
1038
1224
  WRITE: COLORS.badge,
1225
+ EXEC: COLORS.danger,
1039
1226
  NET: COLORS.accent,
1040
1227
  KEY: COLORS.muted
1041
1228
  };
@@ -1048,25 +1235,25 @@ function NeverLine({
1048
1235
  }) {
1049
1236
  const used = segments.reduce((n, s) => n + s.text.length, 0);
1050
1237
  const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
1051
- return /* @__PURE__ */ jsxs7(Text8, { children: [
1052
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: "\u2502" }),
1238
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
1239
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: "\u2502" }),
1053
1240
  " ".repeat(NEVER_BOX_PAD_X),
1054
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text8, { color: s.color, bold: s.bold, children: s.text }, i)),
1241
+ segments.map((s, i) => /* @__PURE__ */ jsx8(Text10, { color: s.color, bold: s.bold, children: s.text }, i)),
1055
1242
  " ".repeat(rightPad),
1056
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: "\u2502" })
1243
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: "\u2502" })
1057
1244
  ] });
1058
1245
  }
1059
1246
  function LearnMore() {
1060
1247
  const confirmStart = useWizard((s) => s.confirmStart);
1061
1248
  const backToHome = useWizard((s) => s.backToHome);
1062
- const { columns } = useWindowSize6();
1249
+ const { columns } = useWindowSize7();
1063
1250
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1064
- useInput4((_input, key) => {
1251
+ useInput5((_input, key) => {
1065
1252
  if (key.escape) backToHome();
1066
1253
  else if (key.return) confirmStart();
1067
1254
  });
1068
- return /* @__PURE__ */ jsxs7(
1069
- Box8,
1255
+ return /* @__PURE__ */ jsxs9(
1256
+ Box10,
1070
1257
  {
1071
1258
  flexDirection: "column",
1072
1259
  paddingX: PADDING_X,
@@ -1074,31 +1261,31 @@ function LearnMore() {
1074
1261
  width: "100%",
1075
1262
  gap: 1,
1076
1263
  children: [
1077
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1078
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: accessIntro }),
1079
- /* @__PURE__ */ jsx7(Box8, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", marginTop: 1, children: [
1080
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1081
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1082
- /* @__PURE__ */ jsx7(Box8, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text8, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1083
- /* @__PURE__ */ jsx7(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text8, { children: [
1084
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: item.title }),
1085
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1264
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1265
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: accessIntro }),
1266
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginTop: 1, children: [
1267
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1268
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1269
+ /* @__PURE__ */ jsx8(Box10, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text10, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1270
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text10, { children: [
1271
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: item.title }),
1272
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1086
1273
  ] }) })
1087
1274
  ] })
1088
1275
  ] }, item.tag)) }),
1089
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
1090
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1091
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1092
- /* @__PURE__ */ jsx7(
1276
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
1277
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1278
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1279
+ /* @__PURE__ */ jsx8(
1093
1280
  NeverLine,
1094
1281
  {
1095
1282
  width: dividerWidth,
1096
1283
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1097
1284
  }
1098
1285
  ),
1099
- neverItems.map((item) => /* @__PURE__ */ jsxs7(Fragment2, { children: [
1100
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1101
- /* @__PURE__ */ jsx7(
1286
+ neverItems.map((item) => /* @__PURE__ */ jsxs9(Fragment2, { children: [
1287
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1288
+ /* @__PURE__ */ jsx8(
1102
1289
  NeverLine,
1103
1290
  {
1104
1291
  width: dividerWidth,
@@ -1110,24 +1297,24 @@ function LearnMore() {
1110
1297
  }
1111
1298
  )
1112
1299
  ] }, item)),
1113
- /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1114
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1300
+ /* @__PURE__ */ jsx8(NeverLine, { width: dividerWidth }),
1301
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1115
1302
  ] }),
1116
- /* @__PURE__ */ jsx7(Box8, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1117
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1118
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.accent, children: link.url })
1303
+ /* @__PURE__ */ jsx8(Box10, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1304
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1305
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.accent, children: link.url })
1119
1306
  ] }, link.label)) }),
1120
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1121
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1122
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "[" }),
1123
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.primary, children: "esc" }),
1124
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "] back" })
1307
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1308
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1309
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "[" }),
1310
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "esc" }),
1311
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "] back" })
1125
1312
  ] }),
1126
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1127
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "[" }),
1128
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.primary, children: "enter" }),
1129
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "]" }),
1130
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.success, bold: true, children: "start wizard" })
1313
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1314
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "[" }),
1315
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "enter" }),
1316
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "]" }),
1317
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.success, bold: true, children: "start wizard" })
1131
1318
  ] })
1132
1319
  ] })
1133
1320
  ]
@@ -1136,10 +1323,10 @@ function LearnMore() {
1136
1323
  }
1137
1324
 
1138
1325
  // src/ui/Sidebar.tsx
1139
- import { Box as Box11, Text as Text11 } from "ink";
1326
+ import { Box as Box13, Text as Text13 } from "ink";
1140
1327
 
1141
1328
  // src/ui/Steps.tsx
1142
- import { Box as Box9, Text as Text9 } from "ink";
1329
+ import { Box as Box11, Text as Text11 } from "ink";
1143
1330
  import Spinner from "ink-spinner";
1144
1331
 
1145
1332
  // src/core/persistence.ts
@@ -1168,12 +1355,12 @@ async function clearWorkflowState(workflowId) {
1168
1355
  }
1169
1356
 
1170
1357
  // src/ui/Steps.tsx
1171
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1358
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1172
1359
  function Steps() {
1173
1360
  const { steps } = useWizard();
1174
1361
  const visibleSteps = steps.filter(isStepVisible);
1175
- return /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { color: COLORS.status[s.status], children: [
1176
- s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1362
+ return /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs10(Text11, { color: COLORS.status[s.status], children: [
1363
+ s.status === "running" ? /* @__PURE__ */ jsx9(Spinner, { type: "dots" }) : MARKER[s.status],
1177
1364
  " ",
1178
1365
  s.title
1179
1366
  ] }) }, s.id)) });
@@ -1182,27 +1369,27 @@ function CurrentStep() {
1182
1369
  const { steps } = useWizard();
1183
1370
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1184
1371
  if (!currentStep) return null;
1185
- return /* @__PURE__ */ jsxs8(Text9, { color: COLORS.status.running, children: [
1186
- /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1372
+ return /* @__PURE__ */ jsxs10(Text11, { color: COLORS.status.running, children: [
1373
+ /* @__PURE__ */ jsx9(Spinner, { type: "dots" }),
1187
1374
  " ",
1188
1375
  ` ${currentStep.title}`
1189
1376
  ] });
1190
1377
  }
1191
1378
 
1192
1379
  // src/ui/Progress.tsx
1193
- import { Box as Box10, Text as Text10 } from "ink";
1194
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1380
+ import { Box as Box12, Text as Text12 } from "ink";
1381
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1195
1382
  function Progress() {
1196
1383
  const { steps, currentStepIndex } = useWizard();
1197
1384
  const visibleSteps = steps.filter(isStepVisible);
1198
1385
  if (visibleSteps.length === 0) return null;
1199
1386
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1200
1387
  const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
1201
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1202
- /* @__PURE__ */ jsx9(Text10, { color: COLORS.muted, children: "STEP" }),
1203
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: activeStepNumber }),
1204
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: "/" }),
1205
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: visibleSteps.length })
1388
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1389
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "STEP" }),
1390
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: activeStepNumber }),
1391
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: "/" }),
1392
+ /* @__PURE__ */ jsx10(Text12, { bold: true, children: visibleSteps.length })
1206
1393
  ] });
1207
1394
  }
1208
1395
 
@@ -1213,10 +1400,10 @@ var sidebarCommands = [
1213
1400
  ];
1214
1401
 
1215
1402
  // src/ui/Sidebar.tsx
1216
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1403
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1217
1404
  function Sidebar() {
1218
- return /* @__PURE__ */ jsxs10(
1219
- Box11,
1405
+ return /* @__PURE__ */ jsxs12(
1406
+ Box13,
1220
1407
  {
1221
1408
  backgroundColor: "#14171E",
1222
1409
  width: 30,
@@ -1225,16 +1412,16 @@ function Sidebar() {
1225
1412
  flexDirection: "column",
1226
1413
  justifyContent: "space-between",
1227
1414
  children: [
1228
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", gap: 1, children: [
1229
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.muted, children: "PROGRESS" }),
1230
- /* @__PURE__ */ jsx10(Steps, {})
1415
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
1416
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: "PROGRESS" }),
1417
+ /* @__PURE__ */ jsx11(Steps, {})
1231
1418
  ] }),
1232
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", gap: 1, children: [
1233
- /* @__PURE__ */ jsx10(Progress, {}),
1234
- /* @__PURE__ */ jsx10(Box11, { flexDirection: "column", children: sidebarCommands.map((c) => {
1235
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1236
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1237
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.muted, children: c.description })
1419
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", gap: 1, children: [
1420
+ /* @__PURE__ */ jsx11(Progress, {}),
1421
+ /* @__PURE__ */ jsx11(Box13, { flexDirection: "column", children: sidebarCommands.map((c) => {
1422
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1423
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1424
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: c.description })
1238
1425
  ] });
1239
1426
  }) })
1240
1427
  ] })
@@ -1244,12 +1431,12 @@ function Sidebar() {
1244
1431
  }
1245
1432
 
1246
1433
  // src/ui/Ribbon.tsx
1247
- import { Box as Box12, Text as Text12 } from "ink";
1248
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1434
+ import { Box as Box14, Text as Text14 } from "ink";
1435
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1249
1436
  function Ribbon() {
1250
1437
  const firstCommand = sidebarCommands[0];
1251
- return /* @__PURE__ */ jsxs11(
1252
- Box12,
1438
+ return /* @__PURE__ */ jsxs13(
1439
+ Box14,
1253
1440
  {
1254
1441
  backgroundColor: "#14171E",
1255
1442
  flexDirection: "row",
@@ -1257,11 +1444,11 @@ function Ribbon() {
1257
1444
  paddingX: 2,
1258
1445
  paddingY: 1,
1259
1446
  children: [
1260
- /* @__PURE__ */ jsx11(Progress, {}),
1261
- /* @__PURE__ */ jsx11(CurrentStep, {}),
1262
- /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1263
- /* @__PURE__ */ jsx11(Text12, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1264
- /* @__PURE__ */ jsx11(Text12, { color: COLORS.muted, children: firstCommand.description })
1447
+ /* @__PURE__ */ jsx12(Progress, {}),
1448
+ /* @__PURE__ */ jsx12(CurrentStep, {}),
1449
+ /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: 1, children: [
1450
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1451
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: firstCommand.description })
1265
1452
  ] })
1266
1453
  ]
1267
1454
  }
@@ -1272,8 +1459,8 @@ function Ribbon() {
1272
1459
  import { useState as useState6 } from "react";
1273
1460
 
1274
1461
  // src/ui/Logs.tsx
1275
- import { Box as Box13, Text as Text13, useInput as useInput5 } from "ink";
1276
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1462
+ import { Box as Box15, Text as Text15, useInput as useInput6 } from "ink";
1463
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1277
1464
  var KIND_COLOR = {
1278
1465
  tool: COLORS.primary,
1279
1466
  prompt: COLORS.badge
@@ -1304,14 +1491,14 @@ function formatTimestamp(ms) {
1304
1491
  function Logs() {
1305
1492
  const logs = useWizard((s) => s.logs);
1306
1493
  const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1307
- useInput5((_input, key) => {
1494
+ useInput6((_input, key) => {
1308
1495
  if (key.upArrow) scroll.scrollBy(-1);
1309
1496
  else if (key.downArrow) scroll.scrollBy(1);
1310
1497
  });
1311
1498
  const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1312
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1313
- logs.length === 0 && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: "No logs yet." }),
1314
- /* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
1499
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1500
+ logs.length === 0 && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: "No logs yet." }),
1501
+ /* @__PURE__ */ jsx13(ScrollView, { scroll, children: visible.map((entry) => {
1315
1502
  const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1316
1503
  const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1317
1504
  const rawPreview = rawInputText(entry.input);
@@ -1321,14 +1508,14 @@ function Logs() {
1321
1508
  const name = truncate2(entry.name, budget);
1322
1509
  budget -= name.length;
1323
1510
  const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1324
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: ROW_GAP, children: [
1325
- /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: timestamp }),
1326
- /* @__PURE__ */ jsx12(Text13, { color: logNameColor(entry), wrap: "truncate", children: name }),
1327
- preview && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, wrap: "truncate", children: preview }),
1328
- durationText && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: durationText })
1511
+ return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "row", gap: ROW_GAP, children: [
1512
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: timestamp }),
1513
+ /* @__PURE__ */ jsx13(Text15, { color: logNameColor(entry), wrap: "truncate", children: name }),
1514
+ preview && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, wrap: "truncate", children: preview }),
1515
+ durationText && /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: durationText })
1329
1516
  ] }, entry.id);
1330
1517
  }) }),
1331
- /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1518
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1332
1519
  ] });
1333
1520
  }
1334
1521
 
@@ -1520,15 +1707,15 @@ function track(event, payload) {
1520
1707
  }
1521
1708
 
1522
1709
  // src/ui/App.tsx
1523
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1710
+ import { jsx as jsx14, jsxs as jsxs15 } from "react/jsx-runtime";
1524
1711
  function App() {
1525
1712
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1526
1713
  const { exit } = useApp();
1527
- const { columns, rows } = useWindowSize7();
1714
+ const { columns, rows } = useWindowSize8();
1528
1715
  const [showLogs, setShowLogs] = useState6(false);
1529
1716
  const finished = phase === "done" || phase === "error";
1530
1717
  const currentStep = steps[currentStepIndex];
1531
- useInput6(
1718
+ useInput7(
1532
1719
  (_input, key) => {
1533
1720
  if (key.return) {
1534
1721
  exit();
@@ -1536,8 +1723,8 @@ function App() {
1536
1723
  },
1537
1724
  { isActive: finished }
1538
1725
  );
1539
- useInput6((_input, key) => {
1540
- if (phase === "idle" || phase === "preflight") return;
1726
+ useInput7((_input, key) => {
1727
+ if (phase === "idle" || phase === "authenticating") return;
1541
1728
  if (key.tab) {
1542
1729
  setShowLogs(!showLogs);
1543
1730
  track("AI Wizard Interaction", {
@@ -1547,49 +1734,45 @@ function App() {
1547
1734
  });
1548
1735
  }
1549
1736
  });
1550
- const escOwnedElsewhere = phase === "idle" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1551
- useInput6((_input, key) => {
1737
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
1738
+ useInput7((_input, key) => {
1552
1739
  if (escOwnedElsewhere) return;
1553
1740
  if (key.escape) {
1554
1741
  track("AI Wizard Interaction", {
1555
1742
  context: "global",
1556
1743
  key: "esc",
1557
- // No step is active until `startWorkflow` — report the phase instead.
1558
1744
  currentStep: currentStep?.id ?? phase
1559
1745
  });
1560
1746
  exit();
1561
1747
  }
1562
1748
  });
1563
- const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1749
+ const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1564
1750
  const flexDirection = columns > 90 ? "row" : "column";
1565
1751
  const showSidebar = flexDirection === "row";
1566
- return /* @__PURE__ */ jsxs13(
1567
- Box14,
1568
- {
1569
- backgroundColor: COLORS.bg.main,
1570
- flexDirection: "row",
1571
- width: columns,
1572
- minHeight: rows,
1573
- children: [
1574
- mainWindowVisible && // Ink sizes the root by width only, so without a cap the scrolling
1575
- // lists in here grow to their content instead of windowing (see
1576
- // `useScrollWindow`). The home screens below stay uncapped: they are
1577
- // long static copy that would be clipped rather than windowed.
1578
- /* @__PURE__ */ jsxs13(
1579
- Box14,
1580
- {
1581
- flexDirection,
1582
- width: "100%",
1583
- maxHeight: rows,
1584
- justifyContent: "space-between",
1585
- children: [
1586
- showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : (
1587
- /* Fill the space the sidebar/ribbon leaves — width beside the
1588
- sidebar, height above the ribbon. The height matters even
1589
- stacked: it is what the prompt's scrolling list measures itself
1590
- against (see SelectPrompt). */
1591
- /* @__PURE__ */ jsxs13(
1592
- Box14,
1752
+ const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
1753
+ return (
1754
+ /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1755
+ the whole screen, and the scrolling throws off its cursor arithmetic —
1756
+ flicker and leftover rows. */
1757
+ /* @__PURE__ */ jsxs15(
1758
+ Box16,
1759
+ {
1760
+ backgroundColor: COLORS.bg.main,
1761
+ flexDirection: "row",
1762
+ width: columns,
1763
+ height: scrollsPastViewport ? void 0 : rows,
1764
+ overflow: scrollsPastViewport ? "visible" : "hidden",
1765
+ children: [
1766
+ mainWindowVisible && /* @__PURE__ */ jsxs15(
1767
+ Box16,
1768
+ {
1769
+ flexDirection,
1770
+ width: "100%",
1771
+ maxHeight: rows,
1772
+ justifyContent: "space-between",
1773
+ children: [
1774
+ showLogs ? /* @__PURE__ */ jsx14(Logs, {}) : /* @__PURE__ */ jsxs15(
1775
+ Box16,
1593
1776
  {
1594
1777
  flexDirection: "column",
1595
1778
  paddingX: 4,
@@ -1597,24 +1780,29 @@ function App() {
1597
1780
  width: showSidebar ? 70 : "100%",
1598
1781
  flexGrow: 1,
1599
1782
  children: [
1600
- /* @__PURE__ */ jsx13(Notices, {}),
1601
- /* @__PURE__ */ jsx13(PromptInput, {}),
1602
- phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1603
- phase === "error" && error && /* @__PURE__ */ jsx13(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text14, { color: COLORS.status.error, children: [
1783
+ phase === "authenticating" && /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", marginBottom: 1, children: [
1784
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1785
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1786
+ ] }),
1787
+ /* @__PURE__ */ jsx14(CliOutput, {}),
1788
+ /* @__PURE__ */ jsx14(Notices, {}),
1789
+ /* @__PURE__ */ jsx14(PromptInput, {}),
1790
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx14(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx14(CurrentStep, {}) }),
1791
+ phase === "error" && error && /* @__PURE__ */ jsx14(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text16, { color: COLORS.status.error, children: [
1604
1792
  "\u2716 ",
1605
1793
  error
1606
1794
  ] }) })
1607
1795
  ]
1608
1796
  }
1609
- )
1610
- ),
1611
- showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1612
- ]
1613
- }
1614
- ),
1615
- (phase === "idle" || phase === "preflight") && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1616
- ]
1617
- }
1797
+ ),
1798
+ showSidebar ? /* @__PURE__ */ jsx14(Sidebar, {}) : /* @__PURE__ */ jsx14(Ribbon, {})
1799
+ ]
1800
+ }
1801
+ ),
1802
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx14(LearnMore, {}) : /* @__PURE__ */ jsx14(Welcome, {}))
1803
+ ]
1804
+ }
1805
+ )
1618
1806
  );
1619
1807
  }
1620
1808
 
@@ -1628,7 +1816,8 @@ var configFile = () => join5(stateDir(), "config.json");
1628
1816
  var DEFAULT_CONFIG = {
1629
1817
  version: 1,
1630
1818
  aiConsent: false,
1631
- workflowsRun: []
1819
+ workflowsRun: [],
1820
+ searchApiKeys: {}
1632
1821
  };
1633
1822
  async function loadConfig() {
1634
1823
  try {
@@ -1647,6 +1836,38 @@ async function recordWorkflowRun(workflowId, completedAt) {
1647
1836
  config.workflowsRun.push({ workflowId, completedAt });
1648
1837
  await saveConfig(config);
1649
1838
  }
1839
+ function isStoredSearchKey(value) {
1840
+ if (typeof value !== "object" || value === null) return false;
1841
+ const { appId, key } = value;
1842
+ return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
1843
+ }
1844
+ function storedSearchKeys(config) {
1845
+ const stored = config.searchApiKeys;
1846
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
1847
+ return {};
1848
+ }
1849
+ return stored;
1850
+ }
1851
+ async function getStoredSearchKey(index, appId) {
1852
+ const entry = storedSearchKeys(await loadConfig())[index];
1853
+ if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
1854
+ return entry.key;
1855
+ }
1856
+ async function storeSearchKey(index, appId, key) {
1857
+ const config = await loadConfig();
1858
+ config.searchApiKeys = {
1859
+ ...storedSearchKeys(config),
1860
+ [index]: { appId, key }
1861
+ };
1862
+ await saveConfig(config);
1863
+ }
1864
+ async function forgetSearchKey(index) {
1865
+ const config = await loadConfig();
1866
+ const remaining = { ...storedSearchKeys(config) };
1867
+ delete remaining[index];
1868
+ config.searchApiKeys = remaining;
1869
+ await saveConfig(config);
1870
+ }
1650
1871
 
1651
1872
  // src/core/orchestrator.ts
1652
1873
  function defineStep(step) {
@@ -1691,7 +1912,7 @@ async function ensureConsent() {
1691
1912
  if (config.aiConsent) return;
1692
1913
  const store = useWizard.getState();
1693
1914
  const answer = await store.requestUserInput({
1694
- prompt: "Wizard will make AI-authored changes to this repository.",
1915
+ prompt: "Wizard will make AI-authored changes to this repository, and will propose shell commands to set it up. You approve each command before it runs.",
1695
1916
  promptType: "enterToContinue",
1696
1917
  options: []
1697
1918
  });
@@ -1850,61 +2071,138 @@ async function runWorkflow(workflow, appId) {
1850
2071
  }
1851
2072
  }
1852
2073
 
1853
- // src/lib/algoliaProfile.ts
1854
- import { readFile as readFile3 } from "node:fs/promises";
1855
- import { createRequire as createRequire2 } from "node:module";
1856
- import { homedir as homedir2 } from "node:os";
1857
- import { join as join6 } from "node:path";
1858
- import { parse as parseToml } from "toml";
1859
- var require3 = createRequire2(import.meta.url);
1860
- function configPath() {
1861
- const base = process.env.XDG_CONFIG_HOME || join6(homedir2(), ".config");
1862
- return join6(base, "algolia", "config.toml");
1863
- }
1864
- function profilesFromConfig(tomlText) {
1865
- let parsed;
2074
+ // src/lib/algoliaApp.ts
2075
+ import { z as z4 } from "zod";
2076
+ var applicationSchema = z4.object({
2077
+ id: z4.string().min(1),
2078
+ name: z4.string().default(""),
2079
+ plan: z4.string().optional()
2080
+ });
2081
+ var listSchema = z4.array(
2082
+ z4.object({
2083
+ id: z4.string().min(1),
2084
+ name: z4.string().default(""),
2085
+ plan_label: z4.string().optional()
2086
+ }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
2087
+ );
2088
+ async function currentApplication() {
2089
+ let raw;
1866
2090
  try {
1867
- parsed = parseToml(tomlText);
2091
+ raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
1868
2092
  } catch {
1869
- return [];
2093
+ return null;
2094
+ }
2095
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2096
+ return parsed.success ? parsed.data : null;
2097
+ }
2098
+ async function requireApplication() {
2099
+ const app = await currentApplication();
2100
+ if (!app) {
2101
+ throw new Error(
2102
+ "No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
2103
+ );
1870
2104
  }
1871
- const profiles = Object.entries(parsed).filter(
1872
- ([, t]) => typeof t.application_id === "string" && typeof t.api_key === "string"
1873
- ).map(([name, t]) => ({
1874
- name,
1875
- appId: t.application_id,
1876
- apiKey: t.api_key,
1877
- isDefault: t.default === true
1878
- }));
1879
- profiles.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
1880
- return profiles.map(({ name, appId, apiKey }) => ({ name, appId, apiKey }));
1881
- }
1882
- async function loadActiveProfile() {
1883
- let profiles;
2105
+ return app;
2106
+ }
2107
+ async function listApplications() {
2108
+ const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
2109
+ const parsed = listSchema.safeParse(parseJson(raw));
2110
+ if (!parsed.success) {
2111
+ throw new Error("Could not read the list of Algolia applications.");
2112
+ }
2113
+ return parsed.data;
2114
+ }
2115
+ async function selectApplication(id) {
2116
+ const raw = await runAlgoliaCli(
2117
+ ["application", "select", "--non-interactive", "--app-id", id],
2118
+ { onOutput: stderrSink }
2119
+ );
2120
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2121
+ if (!parsed.success) {
2122
+ throw new Error(
2123
+ `Selected application ${id}, but the Algolia CLI returned an unreadable result.`
2124
+ );
2125
+ }
2126
+ return parsed.data;
2127
+ }
2128
+ function parseJson(text) {
1884
2129
  try {
1885
- profiles = profilesFromConfig(await readFile3(configPath(), "utf8"));
2130
+ return JSON.parse(text);
1886
2131
  } catch {
1887
- profiles = [];
2132
+ return void 0;
1888
2133
  }
1889
- const profile = profiles[0];
1890
- if (!profile) {
2134
+ }
2135
+
2136
+ // src/lib/algoliaAppPicker.ts
2137
+ function secondaryFor(app) {
2138
+ return app.plan ? { kind: "badge", value: app.plan } : void 0;
2139
+ }
2140
+ function labelFor(app) {
2141
+ return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
2142
+ }
2143
+ function selectAndReport(app) {
2144
+ useWizard.getState().pushCliOutput(
2145
+ "stdout",
2146
+ `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
2147
+ );
2148
+ return selectApplication(app.id);
2149
+ }
2150
+ async function promptForApplication() {
2151
+ const store = useWizard.getState();
2152
+ const apps = await listApplications();
2153
+ if (apps.length === 0) {
1891
2154
  throw new Error(
1892
- "No Algolia profile is configured. Run `npx @algolia/cli auth login` to authenticate."
2155
+ "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
1893
2156
  );
1894
2157
  }
1895
- return profile;
2158
+ if (apps.length === 1) {
2159
+ const only = apps[0];
2160
+ logger.info(
2161
+ { app: only.id },
2162
+ "single application on the account; selecting it"
2163
+ );
2164
+ return selectAndReport(only);
2165
+ }
2166
+ const messages = ["Which Algolia application should the wizard work in?"];
2167
+ for (; ; ) {
2168
+ const choice = await store.requestUserInput({
2169
+ prompt: "Select an application",
2170
+ promptType: "multipleChoice",
2171
+ options: apps.map(labelFor),
2172
+ secondary: apps.map(secondaryFor),
2173
+ messages
2174
+ });
2175
+ const chosen = apps.find((app) => labelFor(app) === choice);
2176
+ if (!chosen) {
2177
+ throw new Error("Application picker received an unexpected selection");
2178
+ }
2179
+ try {
2180
+ return await selectAndReport(chosen);
2181
+ } catch (err) {
2182
+ logger.warn(
2183
+ { app: chosen.id, err: err.message },
2184
+ "application select failed; re-prompting"
2185
+ );
2186
+ messages.push(
2187
+ `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
2188
+ );
2189
+ }
2190
+ }
2191
+ }
2192
+ async function ensureApplication() {
2193
+ return await currentApplication() ?? await promptForApplication();
1896
2194
  }
1897
2195
 
1898
2196
  // src/workflows/default.ts
1899
- import { z as z25 } from "zod";
2197
+ import { z as z27 } from "zod";
1900
2198
 
1901
2199
  // src/actions/listIndices.ts
1902
- import { z as z3 } from "zod";
1903
- var indicesListSchema = z3.object({
1904
- items: z3.array(
1905
- z3.object({
1906
- name: z3.string(),
1907
- entries: z3.number().default(0)
2200
+ import { z as z5 } from "zod";
2201
+ var indicesListSchema = z5.object({
2202
+ items: z5.array(
2203
+ z5.object({
2204
+ name: z5.string(),
2205
+ entries: z5.number().default(0)
1908
2206
  })
1909
2207
  )
1910
2208
  });
@@ -1975,12 +2273,12 @@ import "zod";
1975
2273
 
1976
2274
  // src/lib/tools/listFiles.ts
1977
2275
  import { tool } from "ai";
1978
- import z4 from "zod";
2276
+ import z6 from "zod";
1979
2277
  import { readdir } from "node:fs/promises";
1980
2278
 
1981
2279
  // src/lib/tools/path.ts
1982
2280
  import { lstat } from "node:fs/promises";
1983
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
2281
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join6, sep } from "node:path";
1984
2282
  function resolveInRoot(ctx, path) {
1985
2283
  const target = resolve2(ctx.cwd, path);
1986
2284
  const rel = relative(ctx.root, target);
@@ -1996,7 +2294,7 @@ async function hasSymlinkParent(ctx, target) {
1996
2294
  let current = ctx.root;
1997
2295
  const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
1998
2296
  for (const part of parts) {
1999
- current = join7(current, part);
2297
+ current = join6(current, part);
2000
2298
  try {
2001
2299
  if ((await lstat(current)).isSymbolicLink()) return true;
2002
2300
  } catch (err) {
@@ -2011,7 +2309,7 @@ async function hasSymlinkParent(ctx, target) {
2011
2309
  function listFilesTool(ctx) {
2012
2310
  return tool({
2013
2311
  description: "List files in the current working directory",
2014
- inputSchema: z4.object(),
2312
+ inputSchema: z6.object(),
2015
2313
  execute: async () => {
2016
2314
  logger.info("called listFiles tool");
2017
2315
  if (++ctx.counts.list > ctx.limits.list) {
@@ -2027,13 +2325,13 @@ function listFilesTool(ctx) {
2027
2325
 
2028
2326
  // src/lib/tools/changeDirectory.ts
2029
2327
  import { tool as tool2 } from "ai";
2030
- import z5 from "zod";
2328
+ import z7 from "zod";
2031
2329
  import { stat } from "node:fs/promises";
2032
2330
  function changeDirectoryTool(ctx) {
2033
2331
  return tool2({
2034
2332
  description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
2035
- inputSchema: z5.object({
2036
- path: z5.string().describe("Directory to change into")
2333
+ inputSchema: z7.object({
2334
+ path: z7.string().describe("Directory to change into")
2037
2335
  }),
2038
2336
  execute: async ({ path }) => {
2039
2337
  logger.info({ path }, "called changeDirectory tool");
@@ -2055,13 +2353,13 @@ function changeDirectoryTool(ctx) {
2055
2353
 
2056
2354
  // src/lib/tools/reportStatus.ts
2057
2355
  import { tool as tool3 } from "ai";
2058
- import z6 from "zod";
2356
+ import z8 from "zod";
2059
2357
  function reportStatusTool(output) {
2060
2358
  return tool3({
2061
2359
  description: "Report the status of your execution. Return a reason in case of failure.",
2062
- inputSchema: z6.object({
2063
- status: z6.enum(["success", "fail"]),
2064
- reason: z6.string().optional(),
2360
+ inputSchema: z8.object({
2361
+ status: z8.enum(["success", "fail"]),
2362
+ reason: z8.string().optional(),
2065
2363
  output
2066
2364
  }),
2067
2365
  execute: async ({ status, reason, output: output2 }) => {
@@ -2073,8 +2371,8 @@ function reportStatusTool(output) {
2073
2371
 
2074
2372
  // src/lib/tools/readFile.ts
2075
2373
  import { tool as tool4 } from "ai";
2076
- import z7 from "zod";
2077
- import { readFile as readFile4 } from "node:fs/promises";
2374
+ import z9 from "zod";
2375
+ import { readFile as readFile3 } from "node:fs/promises";
2078
2376
 
2079
2377
  // src/lib/tools/env.ts
2080
2378
  import { basename } from "node:path";
@@ -2101,8 +2399,8 @@ function redactEnvValues(content) {
2101
2399
  function readFileTool(ctx) {
2102
2400
  return tool4({
2103
2401
  description: "Read the contents of a file at the given path",
2104
- inputSchema: z7.object({
2105
- filePath: z7.string().describe("Path to the file to read")
2402
+ inputSchema: z9.object({
2403
+ filePath: z9.string().describe("Path to the file to read")
2106
2404
  }),
2107
2405
  execute: async ({ filePath }) => {
2108
2406
  if (++ctx.counts.read > ctx.limits.read) {
@@ -2112,7 +2410,7 @@ function readFileTool(ctx) {
2112
2410
  const resolved = resolveInRoot(ctx, filePath);
2113
2411
  if (!resolved.ok) return resolved.error;
2114
2412
  try {
2115
- const content = await readFile4(resolved.target, "utf8");
2413
+ const content = await readFile3(resolved.target, "utf8");
2116
2414
  return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2117
2415
  } catch (err) {
2118
2416
  return `Error reading ${filePath}: ${err.message}`;
@@ -2123,15 +2421,15 @@ function readFileTool(ctx) {
2123
2421
 
2124
2422
  // src/lib/tools/writeFile.ts
2125
2423
  import { tool as tool5 } from "ai";
2126
- import z8 from "zod";
2424
+ import z10 from "zod";
2127
2425
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2128
2426
  import { dirname as dirname4 } from "node:path";
2129
2427
  function writeFileTool(ctx) {
2130
2428
  return tool5({
2131
2429
  description: "Write content to a file at the given path, overwriting it. To set Algolia credentials in an env file, use writeCredentials instead of this tool.",
2132
- inputSchema: z8.object({
2133
- filePath: z8.string().describe("Path to the file to write"),
2134
- content: z8.string().describe("Content to write to the file")
2430
+ inputSchema: z10.object({
2431
+ filePath: z10.string().describe("Path to the file to write"),
2432
+ content: z10.string().describe("Content to write to the file")
2135
2433
  }),
2136
2434
  execute: async ({ filePath, content }) => {
2137
2435
  logger.info({ filePath }, "called writeFile tool");
@@ -2156,9 +2454,145 @@ function writeFileTool(ctx) {
2156
2454
 
2157
2455
  // src/lib/tools/writeAlgoliaCredentials.ts
2158
2456
  import { tool as tool6 } from "ai";
2159
- import z9 from "zod";
2160
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2457
+ import z12 from "zod";
2458
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
2161
2459
  import { dirname as dirname5 } from "node:path";
2460
+
2461
+ // src/lib/algoliaApiKey.ts
2462
+ import { z as z11 } from "zod";
2463
+ var WRITE_ACLS = [
2464
+ "addObject",
2465
+ "deleteObject",
2466
+ "settings",
2467
+ "editSettings",
2468
+ "listIndexes"
2469
+ ];
2470
+ var WRITE_ACL_SET = new Set(WRITE_ACLS);
2471
+ var apiKeySchema = z11.object({
2472
+ value: z11.string().min(1),
2473
+ acl: z11.array(z11.string()).default([]),
2474
+ indexes: z11.array(z11.string()).default([])
2475
+ });
2476
+ var apiKeyListSchema = z11.union([
2477
+ z11.array(apiKeySchema),
2478
+ z11.object({
2479
+ items: z11.array(apiKeySchema).optional(),
2480
+ keys: z11.array(apiKeySchema).optional()
2481
+ })
2482
+ ]).transform((o) => Array.isArray(o) ? o : o.items ?? o.keys ?? []);
2483
+ function parseCliJson(schema, stdout, command) {
2484
+ let raw;
2485
+ try {
2486
+ raw = JSON.parse(stdout);
2487
+ } catch {
2488
+ throw new Error(`Algolia CLI \`${command}\` returned unreadable JSON.`);
2489
+ }
2490
+ const parsed = schema.safeParse(raw);
2491
+ if (!parsed.success) {
2492
+ throw new Error(
2493
+ `Algolia CLI \`${command}\` returned JSON in an unexpected shape.`
2494
+ );
2495
+ }
2496
+ return parsed.data;
2497
+ }
2498
+ var createdKeySchema = z11.object({
2499
+ key: z11.string().min(1).optional(),
2500
+ value: z11.string().min(1).optional()
2501
+ }).transform((o) => o.key ?? o.value);
2502
+ function canReuseForWrites(key, index) {
2503
+ return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2504
+ }
2505
+ async function resolveWriteKey(index) {
2506
+ const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2507
+ const existing = parseCliJson(apiKeyListSchema, stdout, "apikeys list").find(
2508
+ (key) => canReuseForWrites(key, index)
2509
+ )?.value;
2510
+ if (existing) {
2511
+ logger.info({ index }, "reusing existing write API key");
2512
+ return existing;
2513
+ }
2514
+ logger.info({ index }, "no reusable write key found; creating one");
2515
+ const created = await runAlgoliaCli([
2516
+ "apikeys",
2517
+ "create",
2518
+ "--indices",
2519
+ index,
2520
+ "--acl",
2521
+ WRITE_ACLS.join(","),
2522
+ "--description",
2523
+ `wizard write key for ${index}`,
2524
+ "-o",
2525
+ "json"
2526
+ ]);
2527
+ const writeKey = parseCliJson(createdKeySchema, created, "apikeys create");
2528
+ if (!writeKey) throw new Error("apikeys create returned no key value");
2529
+ return writeKey;
2530
+ }
2531
+ async function createSearchOnlyKey(index) {
2532
+ logger.info({ index }, "creating a search-only API key");
2533
+ const stdout = await runAlgoliaCli([
2534
+ "apikeys",
2535
+ "create",
2536
+ "--acl",
2537
+ "search",
2538
+ "--indices",
2539
+ index,
2540
+ "--description",
2541
+ `Algolia Wizard search-only key for ${index}`,
2542
+ "-o",
2543
+ "json"
2544
+ ]);
2545
+ let payload;
2546
+ try {
2547
+ payload = JSON.parse(stdout);
2548
+ } catch {
2549
+ throw new Error("apikeys create returned output that is not valid JSON");
2550
+ }
2551
+ const created = createdKeySchema.parse(payload);
2552
+ if (!created) throw new Error("apikeys create returned no key value");
2553
+ return created;
2554
+ }
2555
+ async function apiKeyExists(key) {
2556
+ try {
2557
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
2558
+ return true;
2559
+ } catch (err) {
2560
+ return !/does not exist|not found|404/i.test(err.message);
2561
+ }
2562
+ }
2563
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2564
+ if (envKey) {
2565
+ await recordSearchKey(index, appId, envKey);
2566
+ return { key: envKey, source: "env" };
2567
+ }
2568
+ const stored = await getStoredSearchKey(index, appId);
2569
+ if (stored) {
2570
+ if (await apiKeyExists(stored)) {
2571
+ logger.info({ index, appId }, "reusing the stored search-only API key");
2572
+ return { key: stored, source: "config" };
2573
+ }
2574
+ logger.warn(
2575
+ { index, appId },
2576
+ "the stored search-only API key no longer exists; creating a replacement"
2577
+ );
2578
+ await forgetSearchKey(index);
2579
+ }
2580
+ const key = await createSearchOnlyKey(index);
2581
+ await recordSearchKey(index, appId, key);
2582
+ return { key, source: "created" };
2583
+ }
2584
+ async function recordSearchKey(index, appId, key) {
2585
+ try {
2586
+ await storeSearchKey(index, appId, key);
2587
+ } catch (err) {
2588
+ logger.warn(
2589
+ { err: err.message, index },
2590
+ "could not record the search-only API key; a later run may create another"
2591
+ );
2592
+ }
2593
+ }
2594
+
2595
+ // src/lib/tools/writeAlgoliaCredentials.ts
2162
2596
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2163
2597
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2164
2598
  function appendEnv(content, entries) {
@@ -2172,9 +2606,9 @@ function hasEnv(content, name) {
2172
2606
  }
2173
2607
  function writeCredentialsTool(ctx) {
2174
2608
  return tool6({
2175
- description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials are read from the local Algolia CLI profile; 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.`,
2176
- inputSchema: z9.object({
2177
- filePath: z9.string().describe(
2609
+ 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.`,
2610
+ inputSchema: z12.object({
2611
+ filePath: z12.string().describe(
2178
2612
  'Path to the env file to write credentials into (e.g. ".env")'
2179
2613
  )
2180
2614
  }),
@@ -2182,11 +2616,17 @@ function writeCredentialsTool(ctx) {
2182
2616
  logger.info({ filePath }, "called writeCredentials tool");
2183
2617
  const resolved = resolveInRoot(ctx, filePath);
2184
2618
  if (resolved.ok === false) return resolved.error;
2185
- let profile;
2619
+ const targetIndex = useWizard.getState().targetIndex;
2620
+ if (!targetIndex) {
2621
+ return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2622
+ }
2623
+ let appId;
2624
+ let writeKey;
2186
2625
  try {
2187
- profile = await loadActiveProfile();
2188
- } catch {
2189
- return "Error: no Algolia profile is configured, so credentials cannot be written. Ask the user to authenticate with the Algolia CLI first.";
2626
+ appId = (await requireApplication()).id;
2627
+ writeKey = await resolveWriteKey(targetIndex);
2628
+ } catch (err) {
2629
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2190
2630
  }
2191
2631
  try {
2192
2632
  if (await hasSymlinkParent(ctx, resolved.target)) {
@@ -2194,7 +2634,7 @@ function writeCredentialsTool(ctx) {
2194
2634
  }
2195
2635
  let existing = "";
2196
2636
  try {
2197
- existing = await readFile5(resolved.target, "utf8");
2637
+ existing = await readFile4(resolved.target, "utf8");
2198
2638
  } catch (err) {
2199
2639
  if (err.code !== "ENOENT") throw err;
2200
2640
  }
@@ -2205,8 +2645,8 @@ function writeCredentialsTool(ctx) {
2205
2645
  return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2206
2646
  }
2207
2647
  const envWithCredentials = appendEnv(existing, [
2208
- [APP_ID_VAR, profile.appId],
2209
- [API_KEY_VAR, profile.apiKey]
2648
+ [APP_ID_VAR, appId],
2649
+ [API_KEY_VAR, writeKey]
2210
2650
  ]);
2211
2651
  await mkdir4(dirname5(resolved.target), { recursive: true });
2212
2652
  await writeFile4(resolved.target, envWithCredentials, "utf8");
@@ -2220,16 +2660,24 @@ function writeCredentialsTool(ctx) {
2220
2660
 
2221
2661
  // src/lib/tools/searchFiles.ts
2222
2662
  import { tool as tool7 } from "ai";
2223
- import z10 from "zod";
2224
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2225
- import { join as join8 } from "node:path";
2663
+ import z13 from "zod";
2664
+ import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
2665
+ import { join as join7 } from "node:path";
2226
2666
  var MAX_QUERY_LENGTH = 1e3;
2667
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2668
+ "node_modules",
2669
+ "dist",
2670
+ "build",
2671
+ "vendor",
2672
+ "venv",
2673
+ "__pycache__",
2674
+ "target"
2675
+ ]);
2227
2676
  async function walkFiles(dir) {
2228
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2229
2677
  const out = [];
2230
2678
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2231
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2232
- const full = join8(dir, e.name);
2679
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2680
+ const full = join7(dir, e.name);
2233
2681
  if (e.isDirectory()) out.push(...await walkFiles(full));
2234
2682
  else if (e.isFile()) out.push(full);
2235
2683
  }
@@ -2238,9 +2686,9 @@ async function walkFiles(dir) {
2238
2686
  function searchFilesTool(ctx) {
2239
2687
  return tool7({
2240
2688
  description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
2241
- inputSchema: z10.object({
2242
- query: z10.string().describe("JavaScript RegExp pattern to search for"),
2243
- path: z10.string().optional().describe("Directory to search in (default: cwd)")
2689
+ inputSchema: z13.object({
2690
+ query: z13.string().describe("JavaScript RegExp pattern to search for"),
2691
+ path: z13.string().optional().describe("Directory to search in (default: cwd)")
2244
2692
  }),
2245
2693
  execute: async ({ query, path = "." }) => {
2246
2694
  logger.info({ query, path }, "called searchFiles tool");
@@ -2262,7 +2710,7 @@ function searchFilesTool(ctx) {
2262
2710
  for (const file of await walkFiles(resolved.target)) {
2263
2711
  let content;
2264
2712
  try {
2265
- content = await readFile6(file, "utf8");
2713
+ content = await readFile5(file, "utf8");
2266
2714
  } catch {
2267
2715
  continue;
2268
2716
  }
@@ -2282,92 +2730,194 @@ function searchFilesTool(ctx) {
2282
2730
  });
2283
2731
  }
2284
2732
 
2285
- // src/lib/tools/verifyImplementation.ts
2733
+ // src/lib/tools/runShell.ts
2286
2734
  import { tool as tool8 } from "ai";
2287
- import z11 from "zod";
2735
+ import z14 from "zod";
2736
+ import { relative as relative2 } from "node:path";
2288
2737
 
2289
- // src/lib/tools/utils/runCommand.ts
2738
+ // src/lib/tools/utils/runShell.ts
2290
2739
  import { spawn as spawn2 } from "node:child_process";
2291
- function runCommand(command, args, cwd) {
2740
+
2741
+ // src/lib/tools/context.ts
2742
+ var DEFAULT_TOOL_LIMITS = {
2743
+ list: 10,
2744
+ search: 10,
2745
+ read: 20,
2746
+ match: 100,
2747
+ shell: 30
2748
+ };
2749
+ var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
2750
+ async function refuseByDefault() {
2751
+ return "reject";
2752
+ }
2753
+ function createShellContext(overrides = {}) {
2754
+ return {
2755
+ env: async () => ({}),
2756
+ timeoutMs: DEFAULT_SHELL_TIMEOUT_MS,
2757
+ approved: /* @__PURE__ */ new Set(),
2758
+ executions: [],
2759
+ approve: refuseByDefault,
2760
+ ...overrides
2761
+ };
2762
+ }
2763
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), shell2 = createShellContext()) {
2764
+ return {
2765
+ root: cwd,
2766
+ cwd,
2767
+ limits: { ...limits },
2768
+ counts: { list: 0, search: 0, read: 0, shell: 0 },
2769
+ shell: shell2
2770
+ };
2771
+ }
2772
+
2773
+ // src/lib/tools/utils/runShell.ts
2774
+ var SIGKILL_DELAY_MS = 5e3;
2775
+ var HEAD_CHARS = 4e3;
2776
+ var TAIL_CHARS = 8e3;
2777
+ function truncateOutput(output) {
2778
+ if (output.length <= HEAD_CHARS + TAIL_CHARS) return output;
2779
+ const omitted = output.length - HEAD_CHARS - TAIL_CHARS;
2780
+ return [
2781
+ output.slice(0, HEAD_CHARS),
2782
+ `
2783
+ \u2026 [${omitted} characters omitted] \u2026
2784
+ `,
2785
+ output.slice(-TAIL_CHARS)
2786
+ ].join("");
2787
+ }
2788
+ function runShell(command, opts) {
2789
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2790
+ const startedAt = Date.now();
2292
2791
  return new Promise((resolve4) => {
2293
2792
  let output = "";
2294
- const child = spawn2(command, args, {
2295
- cwd,
2296
- stdio: ["ignore", "pipe", "pipe"]
2793
+ let timedOut = false;
2794
+ let settled = false;
2795
+ const child = spawn2(command, {
2796
+ shell: true,
2797
+ cwd: opts.cwd,
2798
+ stdio: ["ignore", "pipe", "pipe"],
2799
+ env: { ...process.env, ...opts.env }
2297
2800
  });
2801
+ const finish = (exitCode) => {
2802
+ if (settled) return;
2803
+ settled = true;
2804
+ clearTimeout(timer);
2805
+ clearTimeout(killTimer);
2806
+ resolve4({
2807
+ exitCode,
2808
+ output: truncateOutput(output.trim()),
2809
+ timedOut,
2810
+ durationMs: Date.now() - startedAt
2811
+ });
2812
+ };
2813
+ let killTimer;
2814
+ const timer = setTimeout(() => {
2815
+ timedOut = true;
2816
+ output += `
2817
+ [timed out after ${timeoutMs}ms]`;
2818
+ child.kill("SIGTERM");
2819
+ killTimer = setTimeout(() => child.kill("SIGKILL"), SIGKILL_DELAY_MS);
2820
+ }, timeoutMs);
2298
2821
  child.stdout?.on("data", (d) => output += d);
2299
2822
  child.stderr?.on("data", (d) => output += d);
2300
- child.on(
2301
- "error",
2302
- (err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
2303
- );
2304
- child.on("close", (code) => resolve4({ code: code ?? 1, output }));
2823
+ child.on("error", (err) => {
2824
+ output += `Failed to run ${command}: ${err.message}`;
2825
+ finish(1);
2826
+ });
2827
+ child.on("close", (code) => finish(code ?? 1));
2305
2828
  });
2306
2829
  }
2307
2830
 
2308
- // src/lib/tools/utils/packageManager.ts
2309
- import { readFile as readFile7 } from "node:fs/promises";
2310
- import { existsSync } from "node:fs";
2311
- import { join as join9 } from "node:path";
2312
- var LOCKFILES = [
2313
- ["pnpm-lock.yaml", "pnpm"],
2314
- ["yarn.lock", "yarn"],
2315
- ["bun.lockb", "bun"],
2316
- ["bun.lock", "bun"],
2317
- ["package-lock.json", "npm"]
2318
- ];
2319
- async function readPackageJson(cwd = process.cwd()) {
2320
- return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
2321
- }
2322
- function packageManagerFrom(pkg) {
2323
- return pkg.packageManager?.split("@")[0] ?? "npm";
2324
- }
2325
- function packageManagerFromLockfile(cwd) {
2326
- return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
2327
- }
2328
- async function detectPackageManager(cwd) {
2329
- try {
2330
- const pkg = await readPackageJson(cwd);
2331
- if (pkg.packageManager) return packageManagerFrom(pkg);
2332
- } catch {
2333
- }
2334
- return packageManagerFromLockfile(cwd) ?? "npm";
2335
- }
2336
-
2337
- // src/lib/tools/repoVerification.ts
2338
- var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
2339
- async function runRepoVerificationCheck() {
2340
- let pkg;
2341
- try {
2342
- pkg = await readPackageJson();
2343
- } catch (err) {
2344
- const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2345
- return { ok: false, checks: [], limitation };
2346
- }
2347
- const scripts = pkg.scripts ?? {};
2348
- const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
2349
- if (present.length === 0) {
2350
- const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2351
- return { ok: false, checks: [], limitation };
2352
- }
2353
- const pm = await detectPackageManager(process.cwd());
2354
- const checks = [];
2355
- for (const script of present) {
2356
- const command = `${pm} run ${script}`;
2357
- const { code, output } = await runCommand(pm, ["run", script]);
2358
- checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
2359
- }
2360
- return { ok: checks.every((c) => c.ok), checks };
2831
+ // src/lib/tools/runShell.ts
2832
+ function approvalKey(cwd, command) {
2833
+ return `${cwd}\0${command}`;
2834
+ }
2835
+ function storeApproval(root) {
2836
+ return async (req) => {
2837
+ const rel = relative2(root, req.cwd);
2838
+ const answer = await useWizard.getState().requestUserInput({
2839
+ prompt: "Run this command?",
2840
+ promptType: "commandApproval",
2841
+ options: [],
2842
+ command: {
2843
+ ...req,
2844
+ cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
2845
+ }
2846
+ });
2847
+ return answer === "approve" || answer === "always" ? answer : "reject";
2848
+ };
2361
2849
  }
2362
-
2363
- // src/lib/tools/verifyImplementation.ts
2364
- function verifyImplementationTool() {
2850
+ function runShellTool(ctx) {
2365
2851
  return tool8({
2366
- description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
2367
- inputSchema: z11.object(),
2368
- execute: async () => {
2369
- logger.info("called verifyImplementation tool");
2370
- return runRepoVerificationCheck();
2852
+ description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
2853
+ inputSchema: z14.object({
2854
+ command: z14.string().describe(
2855
+ "The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
2856
+ ),
2857
+ cwd: z14.string().optional().describe(
2858
+ "Directory to run in, relative to the project root. Defaults to the project root."
2859
+ ),
2860
+ explanation: z14.string().describe(
2861
+ "One short line telling the user what this command does and why, including any side effect (e.g. writes records to Algolia). This is what they approve against."
2862
+ )
2863
+ }),
2864
+ execute: async ({ command, cwd, explanation }) => {
2865
+ if (++ctx.counts.shell > ctx.limits.shell) {
2866
+ return `Refused: command limit (${ctx.limits.shell}) reached. Stop running commands and report what you have.`;
2867
+ }
2868
+ const resolved = resolveInRoot(ctx, cwd ?? ".");
2869
+ if (!resolved.ok) return resolved.error;
2870
+ logger.info({ command, cwd: resolved.target }, "called runShell tool");
2871
+ const key = approvalKey(resolved.target, command);
2872
+ const decision = ctx.shell.approved.has(key) ? "approve" : await ctx.shell.approve({
2873
+ command,
2874
+ cwd: resolved.target,
2875
+ explanation
2876
+ });
2877
+ if (decision === "reject") {
2878
+ ctx.shell.executions.push({
2879
+ command,
2880
+ cwd: resolved.target,
2881
+ approved: false
2882
+ });
2883
+ logger.info({ command }, "runShell: user rejected the command");
2884
+ return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2885
+ }
2886
+ if (decision === "always") ctx.shell.approved.add(key);
2887
+ useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
2888
+ const env = await ctx.shell.env().catch((err) => {
2889
+ logger.warn({ err, command }, "runShell: could not resolve command env");
2890
+ return {};
2891
+ });
2892
+ const run2 = await (ctx.shell.run ?? runShell)(command, {
2893
+ cwd: resolved.target,
2894
+ env,
2895
+ timeoutMs: ctx.shell.timeoutMs
2896
+ });
2897
+ logger.info(
2898
+ {
2899
+ command,
2900
+ exitCode: run2.exitCode,
2901
+ timedOut: run2.timedOut,
2902
+ durationMs: run2.durationMs
2903
+ },
2904
+ "runShell finished"
2905
+ );
2906
+ await markInteraction();
2907
+ ctx.shell.executions.push({
2908
+ command,
2909
+ cwd: resolved.target,
2910
+ approved: true,
2911
+ exitCode: run2.exitCode,
2912
+ output: run2.output,
2913
+ timedOut: run2.timedOut,
2914
+ durationMs: run2.durationMs
2915
+ });
2916
+ return {
2917
+ exitCode: run2.exitCode,
2918
+ timedOut: run2.timedOut,
2919
+ output: run2.output
2920
+ };
2371
2921
  }
2372
2922
  });
2373
2923
  }
@@ -2378,7 +2928,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
2378
2928
  import { nanoid as nanoid2 } from "nanoid";
2379
2929
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2380
2930
  import { dirname as dirname6 } from "node:path";
2381
- import z12 from "zod";
2931
+ import z15 from "zod";
2382
2932
  var DATA_DIR = ".algolia-wizard/data";
2383
2933
  var RECORD_MODEL = "claude-haiku-4-5";
2384
2934
  var MAX_RECORDS = 100;
@@ -2390,17 +2940,17 @@ var anthropic = createAnthropic({
2390
2940
  function generateRecordTool(ctx) {
2391
2941
  return tool9({
2392
2942
  description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
2393
- inputSchema: z12.object({
2394
- entityName: z12.string().describe("Name of the entity to generate records for."),
2395
- attributes: z12.array(z12.string()).describe("Attribute names each record must contain."),
2396
- count: z12.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2397
- hint: z12.string().optional().describe("Optional context to steer realistic values.")
2943
+ inputSchema: z15.object({
2944
+ entityName: z15.string().describe("Name of the entity to generate records for."),
2945
+ attributes: z15.array(z15.string()).describe("Attribute names each record must contain."),
2946
+ count: z15.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2947
+ hint: z15.string().optional().describe("Optional context to steer realistic values.")
2398
2948
  }),
2399
2949
  execute: async ({ entityName, attributes, count, hint }) => {
2400
2950
  logger.info({ entityName, count }, "called generateRecord tool");
2401
2951
  try {
2402
- const value = z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]);
2403
- const recordSchema = z12.object(
2952
+ const value = z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]);
2953
+ const recordSchema = z15.object(
2404
2954
  Object.fromEntries(attributes.map((attr) => [attr, value]))
2405
2955
  );
2406
2956
  const generateBatch = async (batchCount) => {
@@ -2410,8 +2960,8 @@ function generateRecordTool(ctx) {
2410
2960
  const { output } = await generateText({
2411
2961
  model: anthropic(RECORD_MODEL),
2412
2962
  output: Output.object({
2413
- schema: z12.object({
2414
- records: z12.array(recordSchema).length(batchCount)
2963
+ schema: z15.object({
2964
+ records: z15.array(recordSchema).length(batchCount)
2415
2965
  })
2416
2966
  }),
2417
2967
  prompt: [
@@ -2458,7 +3008,7 @@ function generateRecordTool(ctx) {
2458
3008
  return {
2459
3009
  filePath: relPath,
2460
3010
  count: records.length,
2461
- message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) \u2014 do not inline the records as literals.`
3011
+ message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime using your language's standard JSON support \u2014 do not inline the records as literals.`
2462
3012
  };
2463
3013
  } catch (err) {
2464
3014
  return `Error generating records: ${err.message}`;
@@ -2469,12 +3019,12 @@ function generateRecordTool(ctx) {
2469
3019
 
2470
3020
  // src/lib/tools/notifyUser.ts
2471
3021
  import { tool as tool10 } from "ai";
2472
- import z13 from "zod";
3022
+ import z16 from "zod";
2473
3023
  function notifyUserTool() {
2474
3024
  return tool10({
2475
3025
  description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work.`,
2476
- inputSchema: z13.object({
2477
- message: z13.string().describe(
3026
+ inputSchema: z16.object({
3027
+ message: z16.string().describe(
2478
3028
  "Short, plain-language description of what you are doing now."
2479
3029
  )
2480
3030
  }),
@@ -2486,22 +3036,6 @@ function notifyUserTool() {
2486
3036
  });
2487
3037
  }
2488
3038
 
2489
- // src/lib/tools/context.ts
2490
- var DEFAULT_TOOL_LIMITS = {
2491
- list: 10,
2492
- search: 10,
2493
- read: 20,
2494
- match: 100
2495
- };
2496
- function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
2497
- return {
2498
- root: cwd,
2499
- cwd,
2500
- limits,
2501
- counts: { list: 0, search: 0, read: 0 }
2502
- };
2503
- }
2504
-
2505
3039
  // src/lib/tools/index.ts
2506
3040
  function withLogging(name, def) {
2507
3041
  const execute = def.execute;
@@ -2533,10 +3067,7 @@ function createTools(ctx, { output, tools }) {
2533
3067
  writeCredentialsTool(ctx)
2534
3068
  ),
2535
3069
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2536
- verifyImplementation: withLogging(
2537
- "verifyImplementation",
2538
- verifyImplementationTool()
2539
- ),
3070
+ runShell: withLogging("runShell", runShellTool(ctx)),
2540
3071
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2541
3072
  notifyUser: withLogging("notifyUser", notifyUserTool())
2542
3073
  };
@@ -2571,7 +3102,7 @@ async function runAgent(req) {
2571
3102
  baseURL: PROXY_BASE_URL,
2572
3103
  fetch: proxyFetch
2573
3104
  });
2574
- const toolContext = createToolContext();
3105
+ const toolContext = req.toolContext ?? createToolContext();
2575
3106
  const readTools = ["readFile", "searchFiles", "listFiles"];
2576
3107
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2577
3108
  const instructions = [
@@ -2636,7 +3167,11 @@ async function runAgent(req) {
2636
3167
  "runAgent finished"
2637
3168
  );
2638
3169
  logger.info(
2639
- { counts: toolContext.counts, limits: toolContext.limits },
3170
+ {
3171
+ counts: toolContext.counts,
3172
+ limits: toolContext.limits,
3173
+ commandsRun: toolContext.shell.executions.length
3174
+ },
2640
3175
  "tool usage"
2641
3176
  );
2642
3177
  const toolResults = await stream.toolResults;
@@ -2654,16 +3189,16 @@ async function runAgent(req) {
2654
3189
  }
2655
3190
 
2656
3191
  // src/actions/detectLanguage.ts
2657
- import z16 from "zod";
2658
- var detectLanguageSchema = z16.object({
2659
- languages: z16.array(z16.object({ name: z16.string(), version: z16.string() })),
2660
- frameworks: z16.array(z16.object({ name: z16.string(), version: z16.string() }))
3192
+ import z19 from "zod";
3193
+ var detectLanguageSchema = z19.object({
3194
+ languages: z19.array(z19.object({ name: z19.string(), version: z19.string() })),
3195
+ frameworks: z19.array(z19.object({ name: z19.string(), version: z19.string() }))
2661
3196
  });
2662
3197
  var detectLanguage = () => runAgent({
2663
3198
  instructions: [
2664
3199
  "Analyze the codebase and determine the programming languages and frameworks used",
2665
- "If a superset language is found, exclude the subset language. TS-over-JS.",
2666
- "If a meta-framework is used, exclude the framework. Next-over-React.",
3200
+ "If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
3201
+ "If a meta-framework is used, exclude the framework it builds on (e.g. Next.js over React, Rails over Rack).",
2667
3202
  "Return the exact version",
2668
3203
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
2669
3204
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -2675,31 +3210,31 @@ var detectLanguage = () => runAgent({
2675
3210
  });
2676
3211
 
2677
3212
  // src/actions/analyzeCodebase.ts
2678
- import z17 from "zod";
3213
+ import z20 from "zod";
2679
3214
  var READONLY_TOOLS = [
2680
3215
  "listFiles",
2681
3216
  "changeDirectory",
2682
3217
  "readFile",
2683
3218
  "searchFiles"
2684
3219
  ];
2685
- var ingestionAnalysisSchema = z17.object({
2686
- ingestionAnalysis: z17.array(
2687
- z17.object({
2688
- name: z17.string(),
2689
- paths: z17.array(z17.string()),
3220
+ var ingestionAnalysisSchema = z20.object({
3221
+ ingestionAnalysis: z20.array(
3222
+ z20.object({
3223
+ name: z20.string(),
3224
+ paths: z20.array(z20.string()),
2690
3225
  // indexable fields the agent found for this entity
2691
- attributes: z17.array(z17.string())
3226
+ attributes: z20.array(z20.string())
2692
3227
  })
2693
3228
  )
2694
3229
  });
2695
- var searchImplementationAnalysisSchema = z17.object({
2696
- searchImplementationAnalysis: z17.string()
3230
+ var searchImplementationAnalysisSchema = z20.object({
3231
+ searchImplementationAnalysis: z20.string()
2697
3232
  });
2698
- var verificationSchema = z17.object({
2699
- verification: z17.array(z17.string())
3233
+ var verificationSchema = z20.object({
3234
+ verification: z20.array(z20.string())
2700
3235
  });
2701
3236
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
2702
- var analyzeCodebaseSchema = z17.object({
3237
+ var analyzeCodebaseSchema = z20.object({
2703
3238
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2704
3239
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
2705
3240
  verification: verificationSchema.shape.verification.optional(),
@@ -2761,7 +3296,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2761
3296
  // package.json
2762
3297
  var package_default = {
2763
3298
  name: "@algolia/wizard",
2764
- version: "0.9.0-rc.84.75",
3299
+ version: "0.9.0-rc.87.83",
2765
3300
  description: "Magically implement Algolia functionality in your codebase",
2766
3301
  type: "module",
2767
3302
  engines: {
@@ -2809,7 +3344,6 @@ var package_default = {
2809
3344
  dependencies: {
2810
3345
  "@ai-sdk/anthropic": "^3.0.81",
2811
3346
  "@ai-sdk/openai-compatible": "^2.0.47",
2812
- "@algolia/cli": "^5.11.0",
2813
3347
  "@hono/node-server": "^2.0.10",
2814
3348
  "@segment/analytics-node": "^3.1.0",
2815
3349
  ai: "^6.0.190",
@@ -2823,7 +3357,6 @@ var package_default = {
2823
3357
  nanoid: "^5.1.15",
2824
3358
  pino: "^10.3.1",
2825
3359
  react: "^19.2.7",
2826
- toml: "^4.1.1",
2827
3360
  varlock: "^1.5.1",
2828
3361
  zod: "^4.4.3",
2829
3362
  zustand: "^5.0.14"
@@ -2881,8 +3414,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
2881
3414
  }
2882
3415
 
2883
3416
  // src/actions/confirmLanguage.ts
2884
- import z19 from "zod";
2885
- var confirmLanguageSchema = z19.object({
3417
+ import z22 from "zod";
3418
+ var confirmLanguageSchema = z22.object({
2886
3419
  languages: detectLanguageSchema.shape.languages
2887
3420
  });
2888
3421
  async function confirmLanguage(ctx) {
@@ -2903,17 +3436,19 @@ async function confirmLanguage(ctx) {
2903
3436
  }
2904
3437
 
2905
3438
  // src/actions/confirmFramework.ts
2906
- import z20 from "zod";
2907
- var confirmFrameworkSchema = z20.object({
3439
+ import z23 from "zod";
3440
+ var confirmFrameworkSchema = z23.object({
2908
3441
  frameworks: detectLanguageSchema.shape.frameworks
2909
3442
  });
2910
3443
  var CURATED_FRAMEWORKS = [
2911
3444
  "Next.js",
2912
3445
  "React",
2913
3446
  "Vue",
2914
- "Angular",
2915
- "Svelte",
2916
- "Vanilla JS"
3447
+ "Vanilla JS",
3448
+ "Django",
3449
+ "Laravel",
3450
+ "Rails",
3451
+ "Symfony"
2917
3452
  ];
2918
3453
  var OTHER_OPTION = "Other";
2919
3454
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -2926,12 +3461,15 @@ var FRAMEWORK_ALIASES = {
2926
3461
  vuejs: "vue",
2927
3462
  angular: "angular",
2928
3463
  angularjs: "angular",
2929
- svelte: "svelte",
2930
- sveltekit: "svelte",
2931
3464
  vanillajs: "vanillajs",
2932
3465
  vanilla: "vanillajs",
2933
3466
  javascript: "vanillajs",
2934
- js: "vanillajs"
3467
+ js: "vanillajs",
3468
+ django: "django",
3469
+ laravel: "laravel",
3470
+ rails: "rails",
3471
+ rubyonrails: "rails",
3472
+ symfony: "symfony"
2935
3473
  };
2936
3474
  var isSameFramework = (a, b) => {
2937
3475
  const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
@@ -3032,8 +3570,8 @@ async function promptUser(ctx, params) {
3032
3570
  }
3033
3571
 
3034
3572
  // src/actions/confirmEntities.ts
3035
- import z21 from "zod";
3036
- var confirmEntitiesSchema = z21.object({
3573
+ import z24 from "zod";
3574
+ var confirmEntitiesSchema = z24.object({
3037
3575
  // Final detection — the focused re-run may supersede project-scan's.
3038
3576
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3039
3577
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3103,15 +3641,15 @@ async function confirmEntities(ctx) {
3103
3641
  }
3104
3642
 
3105
3643
  // src/actions/review.ts
3106
- import { z as z22 } from "zod";
3107
- var reviewSchema = z22.object({
3644
+ import { z as z25 } from "zod";
3645
+ var reviewSchema = z25.object({
3108
3646
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3109
3647
  // not one entry per workflow step — a step's raw output can be a long,
3110
3648
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3111
3649
  // that 1:1 is what made the old per-step summary an unreadable wall of text.
3112
- summaryPoints: z22.array(z22.string()),
3113
- reviewPrompt: z22.string(),
3114
- nextSteps: z22.array(z22.string())
3650
+ summaryPoints: z25.array(z25.string()),
3651
+ reviewPrompt: z25.string(),
3652
+ nextSteps: z25.array(z25.string())
3115
3653
  });
3116
3654
  function formatCompletedSteps(steps) {
3117
3655
  if (!steps.length) return "(no prior steps completed)";
@@ -3162,19 +3700,12 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3162
3700
  };
3163
3701
 
3164
3702
  // src/actions/implement.ts
3165
- import z24 from "zod";
3703
+ import z26 from "zod";
3166
3704
 
3167
3705
  // src/lib/worktree.ts
3168
- import { execFile, spawn as spawn3 } from "node:child_process";
3169
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3170
- import {
3171
- basename as basename2,
3172
- dirname as dirname7,
3173
- isAbsolute as isAbsolute2,
3174
- join as join10,
3175
- relative as relative2,
3176
- resolve as resolve3
3177
- } from "node:path";
3706
+ import { execFile } from "node:child_process";
3707
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile6, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3708
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
3178
3709
  var MAX_BUFFER = 32 * 1024 * 1024;
3179
3710
  var MAX_WIZARD_WORKTREES = 3;
3180
3711
  var WIZARD_BRANCH_PREFIX = "wizard/implement-";
@@ -3205,7 +3736,7 @@ async function isWorkingTreeDirty(repoRoot) {
3205
3736
  return out.trim().length > 0;
3206
3737
  }
3207
3738
  async function pruneOldWorktrees(repoRoot) {
3208
- const dir = join10(stateDir(repoRoot), "worktrees");
3739
+ const dir = join8(stateDir(repoRoot), "worktrees");
3209
3740
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3210
3741
  for (const slug of stale) {
3211
3742
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3216,7 +3747,7 @@ async function pruneOldWorktrees(repoRoot) {
3216
3747
  "worktree",
3217
3748
  "remove",
3218
3749
  "--force",
3219
- join10(dir, slug)
3750
+ join8(dir, slug)
3220
3751
  ]);
3221
3752
  await git(["-C", repoRoot, "branch", "-D", branch]);
3222
3753
  } catch (err) {
@@ -3230,113 +3761,13 @@ async function pruneOldWorktrees(repoRoot) {
3230
3761
  async function createWorktree(repoRoot) {
3231
3762
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3232
3763
  const dirSlug = branch.replace(/\//g, "-");
3233
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3764
+ const path = join8(stateDir(repoRoot), "worktrees", dirSlug);
3234
3765
  await git(["-C", repoRoot, "worktree", "prune"]);
3235
3766
  await pruneOldWorktrees(repoRoot);
3236
3767
  await mkdir6(dirname7(path), { recursive: true });
3237
3768
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
3238
3769
  return { path, branch };
3239
3770
  }
3240
- async function installWorktreeDeps(worktreePath) {
3241
- try {
3242
- await readPackageJson(worktreePath);
3243
- } catch {
3244
- return { ok: true, output: "no package.json; skipped install" };
3245
- }
3246
- const pm = await detectPackageManager(worktreePath);
3247
- return new Promise((resolve4) => {
3248
- let output = "";
3249
- const child = spawn3(pm, ["install"], {
3250
- cwd: worktreePath,
3251
- stdio: ["ignore", "pipe", "pipe"]
3252
- });
3253
- child.stdout?.on("data", (d) => output += d);
3254
- child.stderr?.on("data", (d) => output += d);
3255
- child.on(
3256
- "error",
3257
- (err) => resolve4({
3258
- ok: false,
3259
- output: `Failed to run ${pm} install: ${err.message}`
3260
- })
3261
- );
3262
- child.on(
3263
- "close",
3264
- (code) => resolve4({ ok: code === 0, output: output.trim() })
3265
- );
3266
- });
3267
- }
3268
- var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3269
- function validateIngestEntrypoint(worktreePath, entrypoint) {
3270
- if (!entrypoint || entrypoint.startsWith("-")) {
3271
- return {
3272
- ok: false,
3273
- reason: `entrypoint "${entrypoint}" is not a plain file path`
3274
- };
3275
- }
3276
- const target = resolve3(worktreePath, entrypoint);
3277
- const rel = relative2(worktreePath, target);
3278
- if (rel.startsWith("..") || isAbsolute2(rel)) {
3279
- return {
3280
- ok: false,
3281
- reason: `entrypoint "${entrypoint}" resolves outside the worktree`
3282
- };
3283
- }
3284
- return { ok: true, target };
3285
- }
3286
- async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3287
- if (!INGEST_RUNTIMES.includes(runtime)) {
3288
- return {
3289
- ran: false,
3290
- ok: false,
3291
- output: "",
3292
- reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
3293
- };
3294
- }
3295
- const validated = validateIngestEntrypoint(worktreePath, entrypoint);
3296
- if (!validated.ok) {
3297
- return { ran: false, ok: false, output: "", reason: validated.reason };
3298
- }
3299
- try {
3300
- if (!(await stat2(validated.target)).isFile()) {
3301
- return {
3302
- ran: false,
3303
- ok: false,
3304
- output: "",
3305
- reason: `entrypoint "${entrypoint}" is not a file`
3306
- };
3307
- }
3308
- } catch {
3309
- return {
3310
- ran: false,
3311
- ok: false,
3312
- output: "",
3313
- reason: `entrypoint "${entrypoint}" does not exist`
3314
- };
3315
- }
3316
- return new Promise((resolveRun) => {
3317
- let output = "";
3318
- const child = spawn3(runtime, [entrypoint], {
3319
- cwd: worktreePath,
3320
- shell: false,
3321
- stdio: ["ignore", "pipe", "pipe"],
3322
- env: { ...process.env, ...env }
3323
- });
3324
- child.stdout?.on("data", (d) => output += d);
3325
- child.stderr?.on("data", (d) => output += d);
3326
- child.on(
3327
- "error",
3328
- (err) => resolveRun({
3329
- ran: true,
3330
- ok: false,
3331
- output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
3332
- })
3333
- );
3334
- child.on(
3335
- "close",
3336
- (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
3337
- );
3338
- });
3339
- }
3340
3771
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
3341
3772
  const trimmed = sourcePath.trim();
3342
3773
  if (!trimmed) {
@@ -3350,8 +3781,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3350
3781
  } catch {
3351
3782
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3352
3783
  }
3353
- const relPath = join10(ingestDir, basename2(source));
3354
- const dest = join10(worktreePath, relPath);
3784
+ const relPath = join8(ingestDir, basename2(source));
3785
+ const dest = join8(worktreePath, relPath);
3355
3786
  try {
3356
3787
  await mkdir6(dirname7(dest), { recursive: true });
3357
3788
  await copyFile(source, dest);
@@ -3366,11 +3797,28 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3366
3797
  function hasEnvVar(content, name) {
3367
3798
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3368
3799
  }
3800
+ async function readEnvVar(worktreePath, name) {
3801
+ let content;
3802
+ try {
3803
+ content = await readFile6(join8(worktreePath, ".env"), "utf8");
3804
+ } catch (err) {
3805
+ if (err.code !== "ENOENT") throw err;
3806
+ return void 0;
3807
+ }
3808
+ const match = new RegExp(
3809
+ `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3810
+ "m"
3811
+ ).exec(content);
3812
+ if (!match) return void 0;
3813
+ const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3814
+ if (!value || value.startsWith("<")) return void 0;
3815
+ return value;
3816
+ }
3369
3817
  async function writeSearchEnvValues(worktreePath, vars) {
3370
- const target = join10(worktreePath, ".env");
3818
+ const target = join8(worktreePath, ".env");
3371
3819
  let existing = "";
3372
3820
  try {
3373
- existing = await readFile8(target, "utf8");
3821
+ existing = await readFile6(target, "utf8");
3374
3822
  } catch (err) {
3375
3823
  if (err.code !== "ENOENT") throw err;
3376
3824
  }
@@ -3438,64 +3886,16 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3438
3886
  }
3439
3887
  }
3440
3888
 
3441
- // src/lib/algoliaApiKey.ts
3442
- import { z as z23 } from "zod";
3443
- var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
3444
- var apiKeySchema = z23.object({
3445
- value: z23.string().min(1),
3446
- acl: z23.array(z23.string()).default([]),
3447
- indexes: z23.array(z23.string()).default([])
3448
- });
3449
- var apiKeyListSchema = z23.object({
3450
- items: z23.array(apiKeySchema).optional(),
3451
- keys: z23.array(apiKeySchema).optional()
3452
- }).transform((o) => o.items ?? o.keys ?? []);
3453
- var createdKeySchema = z23.object({
3454
- key: z23.string().min(1).optional(),
3455
- value: z23.string().min(1).optional()
3456
- });
3457
- function canReuse(key, index) {
3458
- return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
3459
- }
3460
- async function createSearchKey(index) {
3461
- const stdout = await runAlgoliaCli([
3462
- "apikeys",
3463
- "create",
3464
- "--indices",
3465
- index,
3466
- "--acl",
3467
- "search,browse",
3468
- "--description",
3469
- `wizard search-only key for ${index}`,
3470
- "-o",
3471
- "json"
3472
- ]);
3473
- const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
3474
- const created = key ?? value;
3475
- if (!created) throw new Error("apikeys create returned no key value");
3476
- return created;
3477
- }
3478
- async function resolveSearchOnlyKey(index) {
3479
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
3480
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
3481
- if (existing) {
3482
- logger.info({ index }, "reusing existing search-only API key");
3483
- return existing;
3484
- }
3485
- logger.info({ index }, "no reusable search-only key found; creating one");
3486
- return createSearchKey(index);
3487
- }
3488
-
3489
3889
  // src/lib/algoliaDocs.ts
3490
- import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3491
- import { dirname as dirname8, join as join11 } from "node:path";
3890
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
3891
+ import { dirname as dirname8, join as join9 } from "node:path";
3492
3892
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3493
- var DOCS_SUBPATH = join11("docs", "algolia-sdk");
3893
+ var DOCS_SUBPATH = join9("docs", "algolia-sdk");
3494
3894
  function findDocsDir() {
3495
3895
  let dir = dirname8(fileURLToPath2(import.meta.url));
3496
3896
  for (; ; ) {
3497
- const candidate = join11(dir, DOCS_SUBPATH);
3498
- if (existsSync2(candidate)) return candidate;
3897
+ const candidate = join9(dir, DOCS_SUBPATH);
3898
+ if (existsSync(candidate)) return candidate;
3499
3899
  const parent = dirname8(dir);
3500
3900
  if (parent === dir) return void 0;
3501
3901
  dir = parent;
@@ -3517,7 +3917,7 @@ function loadAlgoliaDoc(language) {
3517
3917
  );
3518
3918
  return "";
3519
3919
  }
3520
- return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3920
+ return readFileSync(join9(docsDir, files[0]), "utf8").trim();
3521
3921
  }
3522
3922
  function getNamedDoc(name, language) {
3523
3923
  const docsDir = findDocsDir();
@@ -3525,14 +3925,15 @@ function getNamedDoc(name, language) {
3525
3925
  logger.warn("docs/algolia-sdk not found");
3526
3926
  return "";
3527
3927
  }
3528
- const file = join11(docsDir, `${name}-${language}.md`);
3529
- if (!existsSync2(file)) {
3928
+ const file = join9(docsDir, `${name}-${language}.md`);
3929
+ if (!existsSync(file)) {
3530
3930
  logger.warn({ name, language }, "named SDK reference not found");
3531
3931
  return "";
3532
3932
  }
3533
3933
  return readFileSync(file, "utf8").trim();
3534
3934
  }
3535
3935
  function getFrameworkSpecificDoc(frameworks) {
3936
+ if (frameworks.length === 0) return "";
3536
3937
  const fw = frameworks.map((f) => f.toLowerCase());
3537
3938
  if (fw.includes("vue") || fw.includes("nuxt")) {
3538
3939
  return loadAlgoliaDoc("vue");
@@ -3540,9 +3941,6 @@ function getFrameworkSpecificDoc(frameworks) {
3540
3941
  if (fw.includes("react") || fw.includes("next.js")) {
3541
3942
  return loadAlgoliaDoc("react");
3542
3943
  }
3543
- if (fw.includes("angular")) {
3544
- return loadAlgoliaDoc("angular");
3545
- }
3546
3944
  return loadAlgoliaDoc("js");
3547
3945
  }
3548
3946
 
@@ -3552,78 +3950,63 @@ function shellQuote(value) {
3552
3950
  }
3553
3951
 
3554
3952
  // src/actions/implement.ts
3555
- var implementSchema = z24.object({
3556
- filesChanged: z24.array(z24.string()),
3557
- summary: z24.string(),
3558
- // Absolute path to the throwaway worktree holding the generated changes, so
3559
- // the user can open it (`cd <worktreePath>`) or inspect the diff
3560
- // (`git -C <worktreePath> status/diff`).
3561
- worktreePath: z24.string().optional(),
3562
- ingestCommand: z24.string().optional(),
3563
- // True when the user accepted the run-now prompt and the wizard executed the
3564
- // ingestion script; downstream steps use this to avoid telling the user to run
3565
- // a script that already ran.
3566
- ingestScriptRan: z24.boolean().optional(),
3567
- // Records ingested by the run-now execution, parsed from the script's
3568
- // machine-readable count line; absent when the script didn't run or emitted
3569
- // no parseable count.
3570
- ingestRecordCount: z24.number().optional(),
3571
- // Wall-clock duration of the run-now ingestion execution, in ms.
3572
- ingestDurationMs: z24.number().optional(),
3573
- ingestionSource: z24.enum(["local", "fileUpload", "generated"]),
3574
- // Suggested names/values, built from framework detection. The search agent is
3575
- // instructed to rename the prefix if it doesn't match the project's build
3576
- // tool, so the names it actually wrote can differ — treat these as hints, not
3577
- // ground truth (the agent's summary carries the final names).
3578
- searchEnvVars: z24.array(
3579
- z24.object({
3580
- name: z24.string(),
3581
- value: z24.string()
3953
+ var implementSchema = z26.object({
3954
+ filesChanged: z26.array(z26.string()),
3955
+ summary: z26.string(),
3956
+ worktreePath: z26.string().optional(),
3957
+ ingestCommand: z26.string().optional(),
3958
+ ingestScriptRan: z26.boolean().optional(),
3959
+ ingestRecordCount: z26.number().optional(),
3960
+ ingestDurationMs: z26.number().optional(),
3961
+ ingestionSource: z26.enum(["local", "fileUpload", "generated"]),
3962
+ searchEnvVars: z26.array(
3963
+ z26.object({
3964
+ name: z26.string(),
3965
+ value: z26.string()
3582
3966
  })
3583
3967
  ).optional()
3584
3968
  });
3585
- var implementationOutputSchema = z24.object({
3586
- summary: z24.string(),
3587
- // Ingestion only: how to run the generated script, as a structured pair the
3588
- // wizard turns into an argv (`<runtime> <entrypoint>`) — never a free-form
3589
- // command string. `runtime` is constrained to an allowlisted interpreter and
3590
- // `entrypoint` is validated to a worktree-relative path before execution, so
3591
- // the agent cannot inject extra commands or swap the interpreter.
3592
- runtime: z24.enum(INGEST_RUNTIMES).optional(),
3593
- entrypoint: z24.string().optional()
3969
+ var implementationOutputSchema = z26.object({
3970
+ summary: z26.string(),
3971
+ ingestCommand: z26.string().optional()
3594
3972
  });
3595
- var verificationOutputSchema = z24.object({
3596
- summary: z24.string(),
3597
- sufficient: z24.boolean(),
3598
- additionalInstructions: z24.string().optional()
3973
+ var verificationOutputSchema = z26.object({
3974
+ summary: z26.string(),
3975
+ sufficient: z26.boolean(),
3976
+ additionalInstructions: z26.string().optional()
3599
3977
  });
3600
3978
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3601
3979
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
3602
3980
  var INGEST_DIR = ".algolia-wizard";
3603
- function detectUiFramework(language) {
3604
- const names = language.frameworks.map((f) => f.name.toLowerCase());
3605
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3606
- if (names.some((n) => n.includes("react") || n.includes("next")))
3607
- return "React";
3608
- if (names.some((n) => n.includes("angular"))) return "Angular";
3609
- return "JavaScript";
3610
- }
3611
- function frameworksForDoc(framework) {
3612
- switch (framework) {
3613
- case "React":
3614
- return ["react"];
3615
- case "Vue":
3616
- return ["vue"];
3617
- case "Angular":
3618
- return ["angular"];
3619
- case "JavaScript":
3620
- return [];
3621
- }
3981
+ var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
3982
+ function lower(entries) {
3983
+ return entries.map((entry) => entry.name.toLowerCase());
3622
3984
  }
3623
- function publicEnvPrefix(language) {
3624
- const frameworkNames = language.frameworks.map(
3625
- (framework) => framework.name.toLowerCase()
3985
+ function isJsProject(language) {
3986
+ return lower(language.languages).some(
3987
+ (name) => JS_LANGUAGES.some((js) => name.includes(js))
3988
+ );
3989
+ }
3990
+ var UI_FRAMEWORKS = [
3991
+ { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
3992
+ { match: ["react", "next"], target: "React", doc: "react" },
3993
+ { match: ["angular"], target: "Angular" }
3994
+ ];
3995
+ function matchUiFramework(language) {
3996
+ const names = lower(language.frameworks);
3997
+ return UI_FRAMEWORKS.find(
3998
+ (ui) => ui.match.some((needle) => names.some((name) => name.includes(needle)))
3626
3999
  );
4000
+ }
4001
+ function searchUiTarget(language) {
4002
+ return matchUiFramework(language)?.target ?? language.frameworks[0]?.name ?? (isJsProject(language) ? "JavaScript" : "this project");
4003
+ }
4004
+ function frameworksForDoc(language) {
4005
+ if (!isJsProject(language)) return [];
4006
+ return [matchUiFramework(language)?.doc ?? "js"];
4007
+ }
4008
+ function publicEnvPrefix(language) {
4009
+ const frameworkNames = lower(language.frameworks);
3627
4010
  if (frameworkNames.some((name) => name.includes("next"))) {
3628
4011
  return "NEXT_PUBLIC_";
3629
4012
  }
@@ -3636,17 +4019,24 @@ function publicEnvPrefix(language) {
3636
4019
  if (frameworkNames.some((name) => name.includes("vite"))) {
3637
4020
  return "VITE_";
3638
4021
  }
3639
- return "PUBLIC_";
4022
+ return isJsProject(language) ? "PUBLIC_" : "";
4023
+ }
4024
+ var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4025
+ var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4026
+ function appIdVar(language) {
4027
+ return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
4028
+ }
4029
+ function searchKeyVar(language) {
4030
+ return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3640
4031
  }
3641
4032
  function searchEnvVars(language, appId, searchKey) {
3642
- const prefix = publicEnvPrefix(language);
3643
4033
  return [
3644
4034
  {
3645
- name: `${prefix}ALGOLIA_APP_ID`,
4035
+ name: appIdVar(language),
3646
4036
  value: appId ?? "<your-algolia-app-id>"
3647
4037
  },
3648
4038
  {
3649
- name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
4039
+ name: searchKeyVar(language),
3650
4040
  value: searchKey ?? "<your-algolia-search-only-api-key>"
3651
4041
  }
3652
4042
  ];
@@ -3655,7 +4045,10 @@ function baseInstructions(input) {
3655
4045
  return [
3656
4046
  `Target Algolia index: ${input.targetIndex}`,
3657
4047
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3658
- "Make minimal, idiomatic changes; do not touch unrelated code."
4048
+ "Make minimal, idiomatic changes; do not touch unrelated code.",
4049
+ `Commands run through a shell on ${process.platform}. Write commands that work there.`,
4050
+ "Use the project's own tooling for every command \u2014 its package manager, task runner, and test/lint commands. Do not assume a JavaScript toolchain.",
4051
+ "runShell needs the developer to approve each command, so give every call a clear `explanation` naming what it does and any side effect. If a command is rejected, do not retry it \u2014 take a different approach or report the limitation."
3659
4052
  ];
3660
4053
  }
3661
4054
  function sourceSpecificInstructions(input) {
@@ -3667,9 +4060,6 @@ function sourceSpecificInstructions(input) {
3667
4060
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
3668
4061
  ],
3669
4062
  fileUpload: [
3670
- // The wizard already copied the developer's file into the worktree at this
3671
- // exact path, so the agent must read it directly — never search for or
3672
- // substitute another file.
3673
4063
  `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
3674
4064
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
3675
4065
  "Map parsed columns/fields to the confirmed entity attributes.",
@@ -3678,46 +4068,60 @@ function sourceSpecificInstructions(input) {
3678
4068
  generated: [
3679
4069
  "No real data source exists; use sample records for each confirmed entity.",
3680
4070
  "Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
3681
- "In the script, read and parse each returned file path at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) instead of inlining the records as literals.",
4071
+ "In the script, read and parse each returned file path at runtime using your language's standard JSON support, instead of inlining the records as literals.",
3682
4072
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
3683
4073
  ]
3684
4074
  };
3685
4075
  return byLine[input.ingestionSource];
3686
4076
  }
4077
+ function algoliaClientDoc(input) {
4078
+ const doc = getNamedDoc("save-records", "js");
4079
+ if (!doc) return [];
4080
+ if (isJsProject(input.language)) return [doc];
4081
+ return [
4082
+ "The reference below is written in JavaScript. Use it for the method names, arguments, and record shape, then translate to this project's language and its official Algolia client:",
4083
+ doc
4084
+ ];
4085
+ }
3687
4086
  function ingestionInstructions(input) {
3688
4087
  return [
3689
4088
  ...input.confirmed && input.confirmed.length ? [
3690
4089
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3691
4090
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3692
4091
  `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
3693
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4092
+ "Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
3694
4093
  "After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
3695
- getNamedDoc("save-records", "js"),
3696
- 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4094
+ ...algoliaClientDoc(input),
4095
+ "Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
4096
+ 'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
3697
4097
  "The summary should be extremely concise.",
3698
- `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.`,
3699
4098
  ...sourceSpecificInstructions(input)
3700
4099
  ] : []
3701
4100
  ];
3702
4101
  }
3703
4102
  function searchInstructions(input) {
3704
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4103
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.language));
3705
4104
  return [
3706
4105
  "Implement an in-app Algolia search experience.",
3707
- `Build the search UI for ${input.uiFramework}.`,
3708
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3709
- doc,
3710
- `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.`,
3711
- "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.",
3712
- // appId always resolves (loadActiveProfile throws otherwise); only the
3713
- // search-only key is best-effort and can fall back to a placeholder.
3714
- `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
3715
- // Names are fixed, not the agent's to rename: the wizard writes the
3716
- // resolved app id / search-only key into ".env" under these exact names
3717
- // right after this step, so a renamed prefix here would leave the code
4106
+ `Build the search UI for ${input.searchUiTarget}.`,
4107
+ ...doc ? [
4108
+ "Follow the Algolia SDK reference below for client setup and search UI wiring; prefer it over prior knowledge:",
4109
+ doc
4110
+ ] : [
4111
+ "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
4112
+ ],
4113
+ `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the "${input.targetIndex}" index.`,
4114
+ "Read the App ID and a search-only API key from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4115
+ // The key is provisioned only after verification passes, so the agent never
4116
+ // sees one. It must also leave .env alone: the wizard reads that file to
4117
+ // decide whether a key already exists, and an agent-invented value there
4118
+ // would be reused as if it were real.
4119
+ `Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
4120
+ // Not the agent's to rename: the wizard writes these exact names into
4121
+ // ".env" right after this step, so a renamed prefix would leave the code
3718
4122
  // reading a var the wizard never wrote.
3719
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3720
- 'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
4123
+ `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4124
+ "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
3721
4125
  "The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
3722
4126
  ];
3723
4127
  }
@@ -3725,11 +4129,12 @@ function verificationInstructions(input) {
3725
4129
  return [
3726
4130
  "Verify the Algolia implementation changes in the current worktree.",
3727
4131
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
3728
- "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
3729
- "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
3730
- "Do not make speculative fixes when verifyImplementation cannot run, no checks exist, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
4132
+ "Run the project's own checks (lint, type check, tests) via runShell, using the commands the project actually defines \u2014 its task runner, manifest scripts, or Makefile. Run every check that applies, not just the first.",
4133
+ "This worktree starts with no installed dependencies. If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
4134
+ "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4135
+ "Do not make speculative fixes when no checks exist, a check cannot run, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
3731
4136
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
3732
- `Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
4137
+ `Do not modify "${input.ingestDir}/" unless a check reports an actionable issue in its files.`,
3733
4138
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
3734
4139
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
3735
4140
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -3750,14 +4155,15 @@ var IMPLEMENT_CONFIG = {
3750
4155
  }
3751
4156
  };
3752
4157
  var useCaseToolMap = {
3753
- ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials", "notifyUser"],
3754
- search: [...FS_READ_TOOLS, "writeFile", "notifyUser"],
3755
- verification: [
4158
+ ingestion: [
3756
4159
  ...FS_READ_TOOLS,
3757
4160
  "writeFile",
3758
- "verifyImplementation",
4161
+ "writeCredentials",
4162
+ "runShell",
3759
4163
  "notifyUser"
3760
- ]
4164
+ ],
4165
+ search: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"],
4166
+ verification: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"]
3761
4167
  };
3762
4168
  function toolsForUseCase(useCase, ingestionSource) {
3763
4169
  const tools = useCaseToolMap[useCase];
@@ -3780,15 +4186,31 @@ function formatSummary(useCase, summary) {
3780
4186
  const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3781
4187
  return `${label}: ${summary}`;
3782
4188
  }
3783
- function buildIngestCommand(worktree, runtime, entrypoint) {
3784
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
3785
- }
3786
4189
  function parseIngestRecordCount(output) {
3787
4190
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
3788
4191
  if (!match) return void 0;
3789
4192
  const count = Number(match[1]);
3790
4193
  return Number.isFinite(count) ? count : void 0;
3791
4194
  }
4195
+ function ingestOutcome(executions, ingestCommand) {
4196
+ const newestFirst = [...executions].reverse();
4197
+ const withCount = newestFirst.filter(
4198
+ (e) => parseIngestRecordCount(e.output ?? "") != null
4199
+ );
4200
+ const succeeded = newestFirst.filter((e) => e.approved && e.exitCode === 0);
4201
+ return {
4202
+ run: succeeded.find((e) => e.command === ingestCommand) ?? succeeded.find((e) => withCount.includes(e)),
4203
+ recordCount: parseIngestRecordCount(withCount[0]?.output ?? "")
4204
+ };
4205
+ }
4206
+ var PLACEHOLDER_WRITE_KEY = "ALGOLIA_WRITE_KEY_PLACEHOLDER";
4207
+ function makeToolContext(worktree, env = async () => ({})) {
4208
+ return createToolContext(
4209
+ DEFAULT_TOOL_LIMITS,
4210
+ worktree,
4211
+ createShellContext({ env, approve: storeApproval(worktree) })
4212
+ );
4213
+ }
3792
4214
  function verificationRetryInstructions(verification) {
3793
4215
  return [
3794
4216
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
@@ -3858,6 +4280,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3858
4280
  }
3859
4281
  }
3860
4282
  const targetIndex = selected?.selection;
4283
+ useWizard.getState().setTargetIndex(targetIndex ?? null);
3861
4284
  await assertGitRepoWithHead(repoRoot);
3862
4285
  if (await isWorkingTreeDirty(repoRoot)) {
3863
4286
  await confirmDirtyWorkingTree(ctx, repoRoot);
@@ -3866,17 +4289,12 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3866
4289
  const confirmed2 = normalized.confirmedEntities;
3867
4290
  const searchLocation = normalized.searchImplementationAnalysis;
3868
4291
  let appId;
3869
- let searchKey;
4292
+ let ingestAppId;
3870
4293
  if (useCases.includes("search")) {
3871
- appId = (await loadActiveProfile()).appId;
3872
- try {
3873
- searchKey = await resolveSearchOnlyKey(targetIndex);
3874
- } catch (err) {
3875
- logger.warn(
3876
- { err: err.message },
3877
- "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
3878
- );
3879
- }
4294
+ appId = (await requireApplication()).id;
4295
+ }
4296
+ if (useCases.includes("ingestion")) {
4297
+ ingestAppId = appId ?? (await requireApplication()).id;
3880
4298
  }
3881
4299
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
3882
4300
  try {
@@ -3908,53 +4326,58 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3908
4326
  targetIndex,
3909
4327
  language,
3910
4328
  appId,
3911
- searchKey,
3912
- searchEnvVars: searchEnvVars(language, appId, searchKey),
4329
+ // Names only: the search-only key is provisioned after verification, so
4330
+ // every value here is still a placeholder when the agent reads them.
4331
+ searchEnvVars: searchEnvVars(language, appId),
3913
4332
  ingestDir: INGEST_DIR,
3914
4333
  ingestionSource,
3915
4334
  uploadFilePath,
3916
- // language.frameworks already prefers the confirm-framework step output,
3917
- // so the user's confirmed stack (not just raw detection) picks the flavor.
3918
- uiFramework: detectUiFramework(language)
4335
+ searchUiTarget: searchUiTarget(language)
3919
4336
  };
3920
4337
  const summaries = [];
3921
4338
  if (uploadWarning) summaries.push(uploadWarning);
4339
+ let envSearchKey;
4340
+ let envAppIdMismatch = false;
4341
+ if (useCases.includes("search") && appId) {
4342
+ const envAppId = await readEnvVar(worktree, appIdVar(language));
4343
+ if (envAppId === appId) {
4344
+ envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
4345
+ } else if (envAppId) {
4346
+ envAppIdMismatch = true;
4347
+ summaries.push(
4348
+ `\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
4349
+ );
4350
+ logger.warn(
4351
+ { envAppId, appId },
4352
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4353
+ );
4354
+ }
4355
+ }
4356
+ let finalSearchEnvVars = input.searchEnvVars;
3922
4357
  let agentRuns = 0;
3923
- let ingestRuntime;
3924
- let ingestEntrypoint;
4358
+ let ingestCommand;
3925
4359
  let ingestScriptRan = false;
3926
4360
  let ingestRecordCount;
3927
4361
  let ingestDurationMs;
3928
- let installFailed = false;
3929
4362
  let ingestOutcomeMessage;
4363
+ const ingestionTools = ingestAppId ? makeToolContext(worktree, async () => ({
4364
+ [APP_ID_VAR]: ingestAppId,
4365
+ [API_KEY_VAR]: PLACEHOLDER_WRITE_KEY
4366
+ })) : void 0;
4367
+ const searchTools = makeToolContext(worktree);
3930
4368
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
3931
4369
  if (agentRuns > 0) ctx.recordStepExecution();
3932
4370
  agentRuns += 1;
3933
- const result = await runAgent({
4371
+ return runAgent({
3934
4372
  instructions: buildAgentInstructions(
3935
4373
  currentUseCase,
3936
4374
  input,
3937
4375
  extraInstructions
3938
4376
  ),
3939
4377
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
3940
- outputSchema: implementationOutputSchema
3941
- });
3942
- ctx.notify({
3943
- messages: [`Installing dependencies for ${currentUseCase}\u2026`]
4378
+ outputSchema: implementationOutputSchema,
4379
+ toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
3944
4380
  });
3945
- const installLogId = ctx.logStart("installWorktreeDeps", {
3946
- useCase: currentUseCase
3947
- });
3948
- const install = await installWorktreeDeps(worktree);
3949
- ctx.logEnd(installLogId, install.ok ? "success" : "error");
3950
- if (!install.ok) {
3951
- installFailed = true;
3952
- logger.warn(
3953
- { useCase: currentUseCase, output: install.output },
3954
- "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
3955
- );
3956
- }
3957
- return result;
3958
4381
  }
3959
4382
  async function runVerificationUseCase() {
3960
4383
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -3962,111 +4385,55 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3962
4385
  return runAgent({
3963
4386
  instructions: buildAgentInstructions("verification", input),
3964
4387
  tools: toolsForUseCase("verification"),
3965
- outputSchema: verificationOutputSchema
4388
+ outputSchema: verificationOutputSchema,
4389
+ toolContext: searchTools
3966
4390
  });
3967
4391
  }
3968
4392
  if (useCases.includes("ingestion")) {
3969
- const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
3970
- summaries.push(formatSummary("ingestion", summary));
3971
- ingestRuntime = runtime;
3972
- ingestEntrypoint = entrypoint;
3973
- if (ingestRuntime && ingestEntrypoint && !installFailed) {
3974
- ctx.clearNotices();
3975
- const runNow = await ctx.requestUserInput({
3976
- prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
3977
- promptType: "acceptReject",
3978
- options: ["Yes", "No"],
3979
- messages: []
3980
- }) === true;
3981
- if (runNow) {
3982
- const profile = await loadActiveProfile();
3983
- ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
3984
- const scriptLogId = ctx.logStart("runIngestScript", {
3985
- runtime: ingestRuntime,
3986
- entrypoint: ingestEntrypoint
4393
+ const result = await runImplementationUseCase("ingestion");
4394
+ summaries.push(formatSummary("ingestion", result.summary));
4395
+ ingestCommand = result.ingestCommand;
4396
+ const executions = (ingestionTools ?? searchTools).shell.executions;
4397
+ const { run: ingestRun, recordCount } = ingestOutcome(
4398
+ executions,
4399
+ ingestCommand
4400
+ );
4401
+ ingestScriptRan = ingestRun != null;
4402
+ ingestRecordCount = recordCount;
4403
+ ingestDurationMs = ingestRun?.durationMs;
4404
+ if (ingestScriptRan) {
4405
+ ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4406
+ if (ingestRecordCount != null) {
4407
+ track("AI Wizard Ingest Successful", {
4408
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4409
+ record_count: ingestRecordCount,
4410
+ duration_ms: ingestDurationMs ?? 0
3987
4411
  });
3988
- const startedAt = Date.now();
3989
- const run2 = await runIngestScript(
3990
- worktree,
3991
- ingestRuntime,
3992
- ingestEntrypoint,
3993
- {
3994
- [APP_ID_VAR]: profile.appId,
3995
- [API_KEY_VAR]: profile.apiKey
3996
- }
3997
- );
3998
- ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
3999
- ingestScriptRan = run2.ran && run2.ok;
4000
- if (ingestScriptRan) {
4001
- ingestDurationMs = Date.now() - startedAt;
4002
- ingestRecordCount = parseIngestRecordCount(run2.output);
4003
- if (ingestRecordCount != null) {
4004
- track("AI Wizard Ingest Successful", {
4005
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4006
- record_count: ingestRecordCount,
4007
- duration_ms: ingestDurationMs
4008
- });
4009
- }
4010
- }
4011
- let summaryLine;
4012
- let outcomeMessage;
4013
- if (!run2.ran) {
4014
- summaryLine = `\u26A0\uFE0F Skipped running the ingestion script: ${run2.reason}`;
4015
- outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
4016
- logger.warn(
4017
- {
4018
- runtime: ingestRuntime,
4019
- entrypoint: ingestEntrypoint,
4020
- reason: run2.reason
4021
- },
4022
- "implement: refused to auto-run ingestion script"
4023
- );
4024
- track("Error", {
4025
- step: "Push Data",
4026
- error: `ingestion script skipped: ${run2.reason}`,
4027
- product_area: "AI Wizard"
4028
- });
4029
- } else if (run2.ok) {
4030
- const status = "Ingestion run: succeeded.";
4031
- summaryLine = run2.output ? `${status}
4032
- ${run2.output}` : status;
4033
- outcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4034
- } else {
4035
- const status = "\u26A0\uFE0F Ingestion run failed:";
4036
- summaryLine = run2.output ? `${status}
4037
- ${run2.output}` : status;
4038
- outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
4039
- logger.warn(
4040
- {
4041
- runtime: ingestRuntime,
4042
- entrypoint: ingestEntrypoint,
4043
- output: run2.output
4044
- },
4045
- "implement: ingestion script run failed"
4046
- );
4047
- track("Error", {
4048
- step: "Push Data",
4049
- error: run2.output || "ingestion script exited non-zero",
4050
- product_area: "AI Wizard"
4051
- });
4052
- }
4053
- summaries.push(summaryLine);
4054
- ingestOutcomeMessage = outcomeMessage;
4055
4412
  }
4413
+ } else {
4414
+ const rejected = executions.some((e) => !e.approved);
4415
+ const reason = rejected ? "you declined to run it" : "no successful run was recorded";
4416
+ ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4417
+ summaries.push(`\u26A0\uFE0F The ingestion script did not run: ${reason}.`);
4418
+ logger.warn(
4419
+ { ingestCommand, rejected, commandsRun: executions.length },
4420
+ "implement: ingestion script did not complete successfully"
4421
+ );
4422
+ track("Error", {
4423
+ step: "Push Data",
4424
+ error: `ingestion did not run: ${reason}`,
4425
+ product_area: "AI Wizard"
4426
+ });
4056
4427
  }
4057
4428
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4058
- if (ingestRuntime && ingestEntrypoint) {
4059
- commandMessages.push(
4060
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4061
- );
4429
+ if (ingestCommand) {
4430
+ commandMessages.push(`Ingestion command: ${ingestCommand}`);
4062
4431
  }
4063
4432
  await ctx.requestUserInput({
4064
- // No question being asked here, just an acknowledgement — the
4065
- // continue/decline hints below already say "continue".
4066
4433
  prompt: "",
4067
4434
  promptType: "enterToContinue",
4068
4435
  options: [],
4069
- messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4436
+ messages: [ingestOutcomeMessage, ...commandMessages]
4070
4437
  });
4071
4438
  }
4072
4439
  if (useCases.includes("search")) {
@@ -4111,7 +4478,29 @@ ${run2.output}` : status;
4111
4478
  }
4112
4479
  extraInstructions = verificationRetryInstructions(verification);
4113
4480
  }
4114
- const resolvedSearchEnvVars = input.searchEnvVars.filter(
4481
+ let searchKey;
4482
+ let searchKeyError;
4483
+ if (appId) {
4484
+ try {
4485
+ const resolved = await resolveSearchOnlyKey(
4486
+ targetIndex,
4487
+ appId,
4488
+ envSearchKey
4489
+ );
4490
+ searchKey = resolved.key;
4491
+ summaries.push(
4492
+ resolved.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
4493
+ );
4494
+ } catch (err) {
4495
+ searchKeyError = err.message;
4496
+ logger.warn(
4497
+ { err: searchKeyError },
4498
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4499
+ );
4500
+ }
4501
+ }
4502
+ finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4503
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4115
4504
  (v) => !v.value.startsWith("<")
4116
4505
  );
4117
4506
  if (resolvedSearchEnvVars.length > 0) {
@@ -4122,13 +4511,29 @@ ${run2.output}` : status;
4122
4511
  if (written.length > 0) {
4123
4512
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4124
4513
  }
4514
+ const stale = [];
4515
+ for (const v of resolvedSearchEnvVars) {
4516
+ if (written.includes(v.name)) continue;
4517
+ const current = await readEnvVar(worktree, v.name);
4518
+ if (current && current !== v.value) stale.push(v);
4519
+ }
4520
+ if (stale.length > 0 && !envAppIdMismatch) {
4521
+ summaries.push(
4522
+ `\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
4523
+ );
4524
+ logger.warn(
4525
+ { vars: stale.map((v) => v.name) },
4526
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4527
+ );
4528
+ }
4125
4529
  }
4126
- const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4530
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4127
4531
  (v) => v.value.startsWith("<")
4128
4532
  );
4129
4533
  if (unresolvedSearchEnvVars.length > 0) {
4130
4534
  summaries.push(
4131
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4535
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
4536
+ (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4132
4537
  );
4133
4538
  }
4134
4539
  } else {
@@ -4140,27 +4545,18 @@ ${run2.output}` : status;
4140
4545
  "implement: agent reported success but no files changed in the worktree"
4141
4546
  );
4142
4547
  }
4143
- if (installFailed) {
4144
- summaries.push(
4145
- '\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".'
4146
- );
4147
- }
4148
4548
  return {
4149
4549
  ingestionSource,
4150
4550
  filesChanged,
4151
4551
  summary: summaries.join("\n\n"),
4152
4552
  worktreePath: worktree,
4153
- ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
4154
- ingestCommand: buildIngestCommand(
4155
- worktree,
4156
- ingestRuntime,
4157
- ingestEntrypoint
4158
- ),
4553
+ ...useCases.includes("ingestion") && ingestCommand ? {
4554
+ ingestCommand,
4159
4555
  ingestScriptRan,
4160
4556
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4161
4557
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4162
4558
  } : {},
4163
- ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4559
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4164
4560
  };
4165
4561
  } finally {
4166
4562
  process.chdir(repoRoot);
@@ -4203,8 +4599,8 @@ var defaultWorkflow = {
4203
4599
  defineStep({
4204
4600
  id: "select-index",
4205
4601
  title: "Set up index",
4206
- outputSchema: z25.object({
4207
- selection: z25.string()
4602
+ outputSchema: z27.object({
4603
+ selection: z27.string()
4208
4604
  }),
4209
4605
  run: (ctx) => selectIndexStep(ctx)
4210
4606
  }),
@@ -4483,7 +4879,7 @@ function parseCliArgs(argv) {
4483
4879
 
4484
4880
  // src/lib/resetState.ts
4485
4881
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4486
- import { join as join12 } from "node:path";
4882
+ import { join as join10 } from "node:path";
4487
4883
  var KEEP = ["wizard.log"];
4488
4884
  async function resetProjectState() {
4489
4885
  const dir = stateDir();
@@ -4495,13 +4891,13 @@ async function resetProjectState() {
4495
4891
  }
4496
4892
  const targets = entries.filter((name) => !KEEP.includes(name));
4497
4893
  await Promise.all(
4498
- targets.map((name) => rm2(join12(dir, name), { recursive: true, force: true }))
4894
+ targets.map((name) => rm2(join10(dir, name), { recursive: true, force: true }))
4499
4895
  );
4500
4896
  return { dir, removed: targets };
4501
4897
  }
4502
4898
 
4503
4899
  // src/main.tsx
4504
- import { jsx as jsx14 } from "react/jsx-runtime";
4900
+ import { jsx as jsx15 } from "react/jsx-runtime";
4505
4901
  async function startup() {
4506
4902
  let args;
4507
4903
  try {
@@ -4550,31 +4946,38 @@ ${formatStepList(workflow)}`);
4550
4946
  }
4551
4947
  async function run(workflow) {
4552
4948
  const store = useWizard.getState();
4553
- let instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4949
+ const instance = render(/* @__PURE__ */ jsx15(App, {}), { incrementalRendering: true });
4950
+ await store.waitForStart();
4554
4951
  let user = await getUser();
4555
4952
  if (!user) {
4556
- await instance.waitUntilRenderFlush();
4557
- instance.cleanup();
4953
+ store.beginAuth();
4558
4954
  try {
4559
4955
  await runAuthLogin();
4560
4956
  } catch (err) {
4561
- console.error(err instanceof Error ? err.message : String(err));
4957
+ store.setError(err instanceof Error ? err.message : String(err));
4958
+ await instance.waitUntilExit();
4562
4959
  process.exit(1);
4563
4960
  }
4564
- instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4961
+ store.endAuth();
4565
4962
  user = await getUser();
4566
4963
  if (!user) {
4567
4964
  store.setError(
4568
- "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli auth login` directly."
4965
+ "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli@latest auth login` directly."
4569
4966
  );
4570
4967
  await instance.waitUntilExit();
4571
4968
  process.exit(1);
4572
4969
  }
4573
4970
  }
4574
4971
  store.setUser(user);
4575
- const profile = await loadActiveProfile();
4576
- await store.waitForStart();
4577
- runWorkflow(workflow, profile?.appId);
4972
+ let app;
4973
+ try {
4974
+ app = await ensureApplication();
4975
+ } catch (err) {
4976
+ store.setError(err instanceof Error ? err.message : String(err));
4977
+ await instance.waitUntilExit();
4978
+ process.exit(1);
4979
+ }
4980
+ runWorkflow(workflow, app.id);
4578
4981
  }
4579
4982
  var started = await startup();
4580
4983
  if (typeof started === "number") {