@algolia/wizard 0.8.0-rc.67.55 → 0.9.0-rc.49.79

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 Box13, Text as Text13, useApp, useInput as useInput6, useWindowSize as useWindowSize7 } from "ink";
7
+ import { Box as Box15, Text as Text15, useApp, useInput as useInput6, useWindowSize as useWindowSize8 } from "ink";
8
8
 
9
9
  // src/core/store.ts
10
10
  import { create } from "zustand";
@@ -12,32 +12,86 @@ 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
+ function npxArgs(args) {
17
+ return ["--yes", "@algolia/cli@latest", ...args];
19
18
  }
20
- function runAlgoliaCli(args) {
19
+ var shell = process.platform === "win32";
20
+ function lineSplitter(emit) {
21
+ let buffer = "";
22
+ return {
23
+ push(chunk) {
24
+ buffer += chunk;
25
+ const lines = buffer.split("\n");
26
+ buffer = lines.pop() ?? "";
27
+ for (const line of lines) emit(line.replace(/\r$/, ""));
28
+ },
29
+ flush() {
30
+ if (buffer) emit(buffer.replace(/\r$/, ""));
31
+ buffer = "";
32
+ }
33
+ };
34
+ }
35
+ var wizardSink = (stream, line) => {
36
+ if (!line.trim()) return;
37
+ useWizard.getState().pushCliOutput(stream, line);
38
+ };
39
+ var stderrSink = (stream, line) => {
40
+ if (stream === "stdout") return;
41
+ wizardSink(stream, line);
42
+ };
43
+ function runAlgoliaCli(args, { onOutput } = {}) {
44
+ const store = useWizard.getState();
45
+ const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
21
46
  return new Promise((resolve4, reject) => {
22
- const child = spawn(process.execPath, [algoliaCliEntry(), ...args]);
47
+ const child = spawn("npx", npxArgs(args), { shell });
23
48
  let stdout = "";
24
49
  let stderr = "";
25
- child.stdout.on("data", (chunk) => stdout += chunk);
26
- child.stderr.on("data", (chunk) => stderr += chunk);
50
+ const splitters = {
51
+ stdout: lineSplitter((line) => onOutput?.("stdout", line)),
52
+ stderr: lineSplitter((line) => onOutput?.("stderr", line))
53
+ };
54
+ child.stdout.on("data", (chunk) => {
55
+ const text = String(chunk);
56
+ stdout += text;
57
+ splitters.stdout.push(text);
58
+ });
59
+ child.stderr.on("data", (chunk) => {
60
+ const text = String(chunk);
61
+ stderr += text;
62
+ splitters.stderr.push(text);
63
+ });
27
64
  child.on("error", reject);
28
65
  child.on("close", (code) => {
66
+ splitters.stdout.flush();
67
+ splitters.stderr.flush();
29
68
  if (code === 0) {
30
69
  resolve4(stdout);
31
70
  } else {
32
- const detail = stderr.trim() || stdout.trim();
71
+ const failed = stderr.trim();
72
+ let detail = "";
73
+ if (failed) {
74
+ detail = `: ${failed}`;
75
+ } else if (stdout.trim()) {
76
+ detail = " (no stderr; stdout withheld \u2014 it may contain credentials)";
77
+ }
33
78
  reject(
34
79
  new Error(
35
- `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail ? `: ${detail}` : ""}`
80
+ `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail}`
36
81
  )
37
82
  );
38
83
  }
39
84
  });
40
- });
85
+ }).then(
86
+ (out) => {
87
+ useWizard.getState().logEnd(logId, "success");
88
+ return out;
89
+ },
90
+ (err) => {
91
+ useWizard.getState().logEnd(logId, "error");
92
+ throw err;
93
+ }
94
+ );
41
95
  }
42
96
  async function getUser() {
43
97
  let raw;
@@ -52,19 +106,23 @@ async function getUser() {
52
106
  return null;
53
107
  }
54
108
  }
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
- });
109
+ var loginResultSchema = z.object({
110
+ success: z.boolean(),
111
+ email: z.string().optional()
112
+ });
113
+ async function runAuthLogin() {
114
+ const raw = await runAlgoliaCli(["auth", "login", "--non-interactive"], {
115
+ onOutput: stderrSink
67
116
  });
117
+ let parsed;
118
+ try {
119
+ parsed = loginResultSchema.safeParse(JSON.parse(raw));
120
+ } catch {
121
+ parsed = void 0;
122
+ }
123
+ if (parsed?.success && !parsed.data.success) {
124
+ throw new Error("Algolia sign-in did not report success.");
125
+ }
68
126
  }
69
127
 
70
128
  // src/lib/auth.ts
@@ -171,6 +229,7 @@ function describeInputValue(value) {
171
229
  return Array.isArray(value) ? value.join(", ") : value;
172
230
  }
173
231
  var NOTICE_INTERVAL_MS = 2e3;
232
+ var CLI_OUTPUT_LIMIT = 200;
174
233
  var useWizard = create((set, get) => ({
175
234
  phase: "idle",
176
235
  homeScreen: "home",
@@ -182,22 +241,21 @@ var useWizard = create((set, get) => ({
182
241
  notices: [],
183
242
  _noticeQueue: [],
184
243
  _noticeTimer: null,
244
+ cliOutput: [],
245
+ targetIndex: null,
185
246
  logs: [],
186
247
  error: null,
187
248
  inputReq: null,
188
249
  _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.
250
+ // `endAuth` lands on 'preflight', not 'idle': sign-in happens after the
251
+ // welcome screen, so going back would gate the run a second time.
252
+ beginAuth: () => set({ phase: "authenticating", cliOutput: [] }),
253
+ endAuth: () => set((s) => s.phase === "authenticating" ? { phase: "preflight" } : {}),
192
254
  confirmStart: () => set(
193
255
  (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
194
256
  ),
195
- // Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
196
257
  openLearnMore: () => set({ homeScreen: "learnMore" }),
197
258
  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
259
  waitForStart: () => new Promise((resolve4) => {
202
260
  if (get().phase !== "idle") {
203
261
  resolve4();
@@ -220,15 +278,19 @@ var useWizard = create((set, get) => ({
220
278
  syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
221
279
  setActiveStep: (index) => {
222
280
  get()._clearNoticeQueue();
223
- set({ phase: "running", currentStepIndex: index, output: "", notices: [] });
281
+ set({
282
+ phase: "running",
283
+ currentStepIndex: index,
284
+ output: "",
285
+ notices: [],
286
+ cliOutput: []
287
+ });
224
288
  },
225
289
  setUser: (user) => set({ user }),
226
290
  appendToken: (text) => set((s) => ({ output: s.output + text })),
227
291
  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.
292
+ // The timer stays armed through an empty drain, so the spacing covers the
293
+ // time since the last render even across bursts.
232
294
  pushNotice: (notice) => {
233
295
  const { notices, _noticeQueue, _noticeTimer } = get();
234
296
  if (_noticeTimer === null) {
@@ -261,6 +323,13 @@ var useWizard = create((set, get) => ({
261
323
  get()._clearNoticeQueue();
262
324
  set({ notices: [] });
263
325
  },
326
+ pushCliOutput: (stream, text) => set((s) => ({
327
+ cliOutput: [...s.cliOutput, { id: nanoid(), stream, text }].slice(
328
+ -CLI_OUTPUT_LIMIT
329
+ )
330
+ })),
331
+ clearCliOutput: () => set({ cliOutput: [] }),
332
+ setTargetIndex: (index) => set({ targetIndex: index }),
264
333
  logStart: (kind, name, input) => {
265
334
  const id = nanoid();
266
335
  set((s) => ({
@@ -283,9 +352,6 @@ var useWizard = create((set, get) => ({
283
352
  _resolve: resolve4
284
353
  });
285
354
  }),
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
355
  submitInput: async (value) => {
290
356
  await markInteraction();
291
357
  get()._resolve?.(value);
@@ -305,6 +371,8 @@ var useWizard = create((set, get) => ({
305
371
  currentStepIndex: 0,
306
372
  output: "",
307
373
  notices: [],
374
+ cliOutput: [],
375
+ targetIndex: null,
308
376
  logs: [],
309
377
  error: null,
310
378
  inputReq: null,
@@ -313,16 +381,100 @@ var useWizard = create((set, get) => ({
313
381
  }
314
382
  }));
315
383
 
384
+ // src/ui/CliOutput.tsx
385
+ import { Box, Text, useWindowSize } from "ink";
386
+
387
+ // src/ui/theme.ts
388
+ var MARKER = {
389
+ pending: "\u25CB",
390
+ running: "\u25D0",
391
+ done: "\u2713",
392
+ error: "\u2716"
393
+ };
394
+ var BRAND = "#003DFF";
395
+ var SECONDARY = "#5468FF";
396
+ var DANGER = "#F86E7E";
397
+ var COLORS = {
398
+ brand: BRAND,
399
+ primary: "#E6EDF3",
400
+ secondary: SECONDARY,
401
+ strong: "#FFFFFF",
402
+ muted: "#8B949E",
403
+ dim: "#484F58",
404
+ highlight: { bg: "#12331C", fg: "#4ADE80" },
405
+ badge: "#E3B341",
406
+ danger: DANGER,
407
+ success: "#4ADE80",
408
+ bg: {
409
+ main: "#0B0E14",
410
+ sidebar: "#14171E"
411
+ },
412
+ border: "#30363D",
413
+ accent: "#76A0FF",
414
+ status: {
415
+ pending: "gray",
416
+ running: "#76A0FF",
417
+ done: "#4ADE80",
418
+ error: DANGER
419
+ }
420
+ };
421
+
422
+ // src/ui/CliOutput.tsx
423
+ import { jsxs } from "react/jsx-runtime";
424
+ var CLI_MARKER = "\u203A";
425
+ var RESERVED_ROWS = 16;
426
+ var MAX_ROWS = 12;
427
+ var PANEL_TEXT_WIDTH = 45;
428
+ var URL_PATTERN = /https?:\/\//;
429
+ function rowCost(text) {
430
+ return URL_PATTERN.test(text) ? Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH)) : 1;
431
+ }
432
+ function CliOutput() {
433
+ const cliOutput = useWizard((s) => s.cliOutput);
434
+ const { rows } = useWindowSize();
435
+ if (!cliOutput.length) return null;
436
+ const rowBudget = Math.min(Math.max(rows - RESERVED_ROWS, 3), MAX_ROWS);
437
+ const visible = [];
438
+ let usedRows = 0;
439
+ for (let i = cliOutput.length - 1; i >= 0; i--) {
440
+ const cost = rowCost(cliOutput[i].text);
441
+ if (usedRows + cost > rowBudget && visible.length > 0) break;
442
+ visible.unshift(cliOutput[i]);
443
+ usedRows += cost;
444
+ }
445
+ const hidden = cliOutput.length - visible.length;
446
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
447
+ hidden > 0 && /* @__PURE__ */ jsxs(Text, { color: COLORS.dim, children: [
448
+ "\u2191 ",
449
+ hidden,
450
+ " earlier line(s)"
451
+ ] }),
452
+ visible.map((line) => /* @__PURE__ */ jsxs(
453
+ Text,
454
+ {
455
+ color: line.stream === "stderr" ? COLORS.muted : COLORS.dim,
456
+ wrap: URL_PATTERN.test(line.text) ? "wrap" : "truncate",
457
+ children: [
458
+ CLI_MARKER,
459
+ " ",
460
+ line.text
461
+ ]
462
+ },
463
+ line.id
464
+ ))
465
+ ] });
466
+ }
467
+
316
468
  // src/ui/Notices.tsx
317
- import { Box as Box2, Text as Text2, useWindowSize as useWindowSize2 } from "ink";
469
+ import { Box as Box3, Text as Text3, useWindowSize as useWindowSize3 } from "ink";
318
470
  import { useEffect as useEffect2, useState as useState2 } from "react";
319
471
 
320
472
  // src/ui/Table.tsx
321
- import { Box, Text, measureElement, useWindowSize } from "ink";
473
+ import { Box as Box2, Text as Text2, measureElement, useWindowSize as useWindowSize2 } from "ink";
322
474
  import { useEffect, useRef, useState } from "react";
323
475
  import { jsx } from "react/jsx-runtime";
324
476
  function Table({ columns, rows }) {
325
- const { columns: termCols } = useWindowSize();
477
+ const { columns: termCols } = useWindowSize2();
326
478
  const ref = useRef(null);
327
479
  const [width, setWidth] = useState(0);
328
480
  useEffect(() => {
@@ -330,7 +482,7 @@ function Table({ columns, rows }) {
330
482
  }, [termCols, columns, rows]);
331
483
  if (rows.length === 0) return null;
332
484
  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}`)) });
485
+ return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text2, { wrap: "truncate", children: line }, `tbl-${i}`)) });
334
486
  }
335
487
  function formatTable(columns, rows, width) {
336
488
  const natural = columns.map(
@@ -370,48 +522,13 @@ function resize(widths, budget) {
370
522
  }
371
523
  var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, width) : `${s.slice(0, width - 1)}\u2026`;
372
524
 
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
525
  // src/ui/Notices.tsx
409
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
526
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
410
527
  var AGENT_MARKER = "\u2726";
411
- var RESERVED_ROWS = 14;
412
- var PANEL_TEXT_WIDTH = 45;
528
+ var RESERVED_ROWS2 = 14;
529
+ var PANEL_TEXT_WIDTH2 = 45;
413
530
  function messageLineCount(text) {
414
- return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH));
531
+ return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH2));
415
532
  }
416
533
  function noticeLineCount(notice) {
417
534
  const messageLines = (notice.messages ?? []).reduce((sum, m) => {
@@ -422,7 +539,7 @@ function noticeLineCount(notice) {
422
539
  return messageLines + tableLines;
423
540
  }
424
541
  function fitVisibleNotices(notices, windowRows) {
425
- const budget = Math.max(windowRows - RESERVED_ROWS, 3);
542
+ const budget = Math.max(windowRows - RESERVED_ROWS2, 3);
426
543
  let used = 0;
427
544
  let count = 0;
428
545
  for (let i = notices.length - 1; i >= 0; i--) {
@@ -455,7 +572,7 @@ function parseHex(hex) {
455
572
  }
456
573
  function Notices() {
457
574
  const notices = useWizard((s) => s.notices);
458
- const { rows: windowRows } = useWindowSize2();
575
+ const { rows: windowRows } = useWindowSize3();
459
576
  const visible = fitVisibleNotices(notices, windowRows);
460
577
  const [pulseStep, setPulseStep] = useState2(0);
461
578
  useEffect2(() => {
@@ -472,14 +589,14 @@ function Notices() {
472
589
  }, []);
473
590
  if (!visible.length) return null;
474
591
  const pulseColor = PULSE_COLORS[pulseStep];
475
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
592
+ return /* @__PURE__ */ jsx2(Box3, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
476
593
  const isLatest = i === visible.length - 1;
477
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
594
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
478
595
  notice.messages?.map((m, j) => {
479
596
  const line = typeof m === "string" ? { text: m } : m;
480
597
  const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
481
- return /* @__PURE__ */ jsxs(
482
- Text2,
598
+ return /* @__PURE__ */ jsxs2(
599
+ Text3,
483
600
  {
484
601
  color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
485
602
  bold: line.bold,
@@ -497,41 +614,42 @@ function Notices() {
497
614
  }
498
615
 
499
616
  // src/ui/PromptInput.tsx
500
- import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
617
+ import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
501
618
  import TextInput from "ink-text-input";
502
- import { useState as useState4 } from "react";
619
+ import { useState as useState5 } from "react";
503
620
 
504
621
  // 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";
622
+ import { Box as Box4, Text as Text4 } from "ink";
623
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
507
624
  function NextAction({
508
625
  action,
509
626
  keyHint,
510
627
  hierarchy = "primary"
511
628
  }) {
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 })
629
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
630
+ hierarchy === "primary" && /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `> ${action}` }),
631
+ hierarchy === "secondary" && /* @__PURE__ */ jsxs3(Fragment, { children: [
632
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `>` }),
633
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, bold: true, children: action })
517
634
  ] }),
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: `]` })
635
+ /* @__PURE__ */ jsxs3(Box4, { children: [
636
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: "press " }),
637
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `[` }),
638
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, children: keyHint }),
639
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `]` })
523
640
  ] })
524
641
  ] });
525
642
  }
526
643
 
527
644
  // src/ui/SelectPrompt.tsx
528
- import { Box as Box4, Text as Text4, measureElement as measureElement2, useInput, useWindowSize as useWindowSize3 } from "ink";
529
- import { useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
530
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
531
- var CANCEL = "cancel";
532
- var ARROW_WIDTH = 4;
533
- var COLUMN_GAP = 2;
534
- var BAR_PADDING = 2;
645
+ import { Box as Box6, Text as Text6, useInput, useWindowSize as useWindowSize5 } from "ink";
646
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
647
+
648
+ // src/ui/ScrollView.tsx
649
+ import { Box as Box5, Text as Text5, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
650
+ import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
651
+ import { jsxs as jsxs4 } from "react/jsx-runtime";
652
+ var INDICATOR_ROWS = 2;
535
653
  function fittedWidth(node, columns) {
536
654
  let left = 0;
537
655
  for (let n = node; n; n = n.parentNode) {
@@ -539,6 +657,89 @@ function fittedWidth(node, columns) {
539
657
  }
540
658
  return Math.max(Math.min(measureElement2(node).width, columns - left), 0);
541
659
  }
660
+ function useScrollWindow({
661
+ itemCount,
662
+ rowHeight = 1,
663
+ followBottom = false
664
+ }) {
665
+ const viewportRef = useRef2(null);
666
+ const { columns } = useWindowSize4();
667
+ const [size, setSize] = useState3(
668
+ null
669
+ );
670
+ useLayoutEffect(() => {
671
+ if (!viewportRef.current) return;
672
+ const width = fittedWidth(viewportRef.current, columns);
673
+ const { height } = measureElement2(viewportRef.current);
674
+ setSize(
675
+ (prev) => prev?.width === width && prev.height === height ? prev : { width, height }
676
+ );
677
+ });
678
+ const capacity = size === null || itemCount * rowHeight <= size.height ? itemCount : Math.max(Math.floor((size.height - INDICATOR_ROWS) / rowHeight), 1);
679
+ const maxOffset = Math.max(itemCount - capacity, 0);
680
+ const [offset, setOffset] = useState3(0);
681
+ const prevMaxOffsetRef = useRef2(0);
682
+ useLayoutEffect(() => {
683
+ const wasAtBottom = offset >= prevMaxOffsetRef.current;
684
+ prevMaxOffsetRef.current = maxOffset;
685
+ setOffset(
686
+ (o) => followBottom && wasAtBottom ? maxOffset : Math.min(o, maxOffset)
687
+ );
688
+ }, [maxOffset, followBottom]);
689
+ const scrollBy = useCallback(
690
+ (delta) => {
691
+ setOffset((o) => Math.min(Math.max(o + delta, 0), maxOffset));
692
+ },
693
+ [maxOffset]
694
+ );
695
+ const revealIndex = useCallback(
696
+ (index) => {
697
+ setOffset((o) => {
698
+ if (index < o) return index;
699
+ if (index >= o + capacity) {
700
+ return Math.min(index - capacity + 1, maxOffset);
701
+ }
702
+ return o;
703
+ });
704
+ },
705
+ [capacity, maxOffset]
706
+ );
707
+ const visibleCount = Math.min(capacity, Math.max(itemCount - offset, 0));
708
+ return {
709
+ viewportRef,
710
+ width: size?.width ?? columns,
711
+ offset,
712
+ capacity,
713
+ maxOffset,
714
+ hiddenAbove: Math.min(offset, itemCount),
715
+ hiddenBelow: Math.max(itemCount - offset - visibleCount, 0),
716
+ scrollBy,
717
+ revealIndex
718
+ };
719
+ }
720
+ function ScrollView({ scroll, children }) {
721
+ return /* @__PURE__ */ jsxs4(Box5, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
722
+ scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
723
+ "\u2191 ",
724
+ scroll.hiddenAbove,
725
+ " more"
726
+ ] }),
727
+ children,
728
+ scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
729
+ "\u2193 ",
730
+ scroll.hiddenBelow,
731
+ " more"
732
+ ] })
733
+ ] });
734
+ }
735
+
736
+ // src/ui/SelectPrompt.tsx
737
+ import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
738
+ var CANCEL = "cancel";
739
+ var ARROW_WIDTH = 4;
740
+ var COLUMN_GAP = 2;
741
+ var BAR_PADDING = 2;
742
+ var ROW_HEIGHT = 3;
542
743
  function SelectPrompt({
543
744
  options,
544
745
  onSelect,
@@ -552,10 +753,10 @@ function SelectPrompt({
552
753
  secondary,
553
754
  defaultSelectedIndex = 0
554
755
  }) {
555
- const [index, setIndex] = useState3(
756
+ const [index, setIndex] = useState4(
556
757
  () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
557
758
  );
558
- const [checked, setChecked] = useState3(() => /* @__PURE__ */ new Set());
759
+ const [checked, setChecked] = useState4(() => /* @__PURE__ */ new Set());
559
760
  const hasCancel = Boolean(multi || cancelable);
560
761
  const rows = hasCancel ? [...options, "Cancel"] : options;
561
762
  const cancelIndex = hasCancel ? options.length : -1;
@@ -563,14 +764,14 @@ function SelectPrompt({
563
764
  if (rows.length > 1) hints.push({ key: "[\u2191] [\u2193]", label: "move" });
564
765
  if (multi) hints.push({ key: "[space]", label: "select" });
565
766
  hints.push({ key: "[enter]", label: "confirm" });
566
- const containerRef = useRef2(null);
567
- const { columns } = useWindowSize3();
568
- const [width, setWidth] = useState3(columns);
569
- useLayoutEffect(() => {
570
- if (containerRef.current) {
571
- setWidth(fittedWidth(containerRef.current, columns));
572
- }
573
- }, [columns]);
767
+ const containerRef = useRef3(null);
768
+ const { columns } = useWindowSize5();
769
+ const [width, setWidth] = useState4(columns);
770
+ useLayoutEffect2(() => {
771
+ if (!containerRef.current) return;
772
+ const measured = fittedWidth(containerRef.current, columns);
773
+ setWidth((prev) => prev === measured ? prev : measured);
774
+ });
574
775
  const inner = Math.max(width - BAR_PADDING, 0);
575
776
  const labelWidth = Math.min(
576
777
  ARROW_WIDTH + (multi ? 2 : 0) + Math.max(0, ...rows.map((opt) => opt.length)) + COLUMN_GAP,
@@ -586,6 +787,15 @@ function SelectPrompt({
586
787
  const barWidth = Math.min(labelWidth + badgeWidth + BAR_PADDING, width);
587
788
  const barLabelWidth = Math.max(barWidth - BAR_PADDING - badgeWidth, 0);
588
789
  const textWidth = inner - labelWidth;
790
+ const scroll = useScrollWindow({
791
+ itemCount: rows.length,
792
+ rowHeight: ROW_HEIGHT
793
+ });
794
+ const { revealIndex } = scroll;
795
+ useLayoutEffect2(() => {
796
+ revealIndex(index);
797
+ }, [index, revealIndex]);
798
+ const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
589
799
  useInput((input, key) => {
590
800
  if (rows.length === 0) return;
591
801
  if (key.upArrow || input === "k") {
@@ -609,62 +819,65 @@ function SelectPrompt({
609
819
  }
610
820
  }
611
821
  });
612
- return /* @__PURE__ */ jsx4(Box4, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 1, width, children: [
613
- error && /* @__PURE__ */ jsx4(Text4, { color: COLORS.danger, children: error }),
614
- messages?.map((m, i) => /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: m }, `msg-${i}`)),
615
- table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
616
- /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
617
- question && /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: question }),
618
- helpText && /* @__PURE__ */ jsx4(Text4, { color: COLORS.dim, children: helpText })
822
+ return /* @__PURE__ */ jsx4(Box6, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, width, children: [
823
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
824
+ error && /* @__PURE__ */ jsx4(Text6, { color: COLORS.danger, children: error }),
825
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
826
+ table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
827
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
828
+ question && /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: question }),
829
+ helpText && /* @__PURE__ */ jsx4(Text6, { color: COLORS.dim, children: helpText })
830
+ ] })
619
831
  ] }),
620
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: rows.map((option, i) => {
832
+ /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
833
+ const i = scroll.offset + visibleIndex;
621
834
  const highlighted = i === index;
622
835
  const isCancel = i === cancelIndex;
623
836
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
624
837
  const sec = isCancel ? void 0 : secondary?.[i];
625
838
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
626
- const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, wrap: "truncate", children: [
839
+ const label = /* @__PURE__ */ jsxs5(Text6, { color: labelColor, wrap: "truncate", children: [
627
840
  highlighted ? "\u276F " : " ",
628
841
  bullet,
629
842
  option
630
843
  ] });
631
844
  const isText = sec?.kind === "text";
632
- return /* @__PURE__ */ jsxs3(
633
- Box4,
845
+ return /* @__PURE__ */ jsxs5(
846
+ Box6,
634
847
  {
635
848
  width: isText ? "100%" : barWidth,
636
849
  paddingX: 1,
637
850
  paddingY: 1,
638
851
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
639
852
  children: [
640
- /* @__PURE__ */ jsx4(Box4, { width: isText ? labelWidth : barLabelWidth, children: label }),
641
- isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box4, { width: textWidth, children: /* @__PURE__ */ jsx4(
642
- Text4,
853
+ /* @__PURE__ */ jsx4(Box6, { width: isText ? labelWidth : barLabelWidth, children: label }),
854
+ isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box6, { width: textWidth, children: /* @__PURE__ */ jsx4(
855
+ Text6,
643
856
  {
644
857
  wrap: "truncate",
645
858
  color: highlighted ? COLORS.primary : COLORS.muted,
646
859
  children: sec.value
647
860
  }
648
861
  ) }),
649
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box4, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
862
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box6, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text6, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
650
863
  ]
651
864
  },
652
865
  `row-${i}`
653
866
  );
654
867
  }) }),
655
- /* @__PURE__ */ jsx4(Text4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
868
+ /* @__PURE__ */ jsx4(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text6, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs5(Text6, { children: [
656
869
  i > 0 ? " " : "",
657
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: key }),
658
- /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
870
+ /* @__PURE__ */ jsx4(Text6, { color: COLORS.primary, children: key }),
871
+ /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
659
872
  " ",
660
873
  label
661
874
  ] })
662
- ] }, label)) })
875
+ ] }, label)) }) })
663
876
  ] }) });
664
877
  }
665
878
 
666
879
  // src/ui/PromptInput.tsx
667
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
880
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
668
881
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
669
882
  function EnterToContinuePrompt({
670
883
  question,
@@ -675,10 +888,10 @@ function EnterToContinuePrompt({
675
888
  if (key.return) onDecide(true);
676
889
  else if (key.escape) onDecide(false);
677
890
  });
678
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
679
- messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
680
- question && /* @__PURE__ */ jsx5(Text5, { color: COLORS.primary, children: question }),
681
- /* @__PURE__ */ jsxs4(Box5, { gap: 1, flexDirection: "column", children: [
891
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, children: [
892
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
893
+ question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: question }),
894
+ /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
682
895
  /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
683
896
  /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
684
897
  ] })
@@ -686,13 +899,13 @@ function EnterToContinuePrompt({
686
899
  }
687
900
  function PromptInput() {
688
901
  const { phase, inputReq, submitInput } = useWizard();
689
- const [draft, setDraft] = useState4("");
902
+ const [draft, setDraft] = useState5("");
690
903
  if (phase === "done" || phase === "error") {
691
- return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
904
+ return /* @__PURE__ */ jsx5(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text7, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
692
905
  }
693
906
  if (phase !== "awaitingInput" || !inputReq) return null;
694
907
  if (inputReq.promptType === "multipleChoice") {
695
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
908
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
696
909
  SelectPrompt,
697
910
  {
698
911
  question: inputReq.prompt,
@@ -709,7 +922,7 @@ function PromptInput() {
709
922
  ) });
710
923
  }
711
924
  if (inputReq.promptType === "multiSelect") {
712
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
925
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
713
926
  SelectPrompt,
714
927
  {
715
928
  multi: true,
@@ -724,7 +937,7 @@ function PromptInput() {
724
937
  ) });
725
938
  }
726
939
  if (inputReq.promptType === "notice") {
727
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
940
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
728
941
  SelectPrompt,
729
942
  {
730
943
  question: inputReq.prompt,
@@ -746,7 +959,7 @@ function PromptInput() {
746
959
  }
747
960
  if (inputReq.promptType === "acceptReject") {
748
961
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
749
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
962
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
750
963
  SelectPrompt,
751
964
  {
752
965
  question: inputReq.prompt,
@@ -757,11 +970,11 @@ function PromptInput() {
757
970
  }
758
971
  ) });
759
972
  }
760
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
761
- inputReq.error && /* @__PURE__ */ jsx5(Text5, { color: COLORS.danger, children: inputReq.error }),
762
- inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
763
- /* @__PURE__ */ jsxs4(Box5, { children: [
764
- /* @__PURE__ */ jsxs4(Text5, { color: COLORS.primary, children: [
973
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
974
+ inputReq.error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: inputReq.error }),
975
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
976
+ /* @__PURE__ */ jsxs6(Box7, { children: [
977
+ /* @__PURE__ */ jsxs6(Text7, { color: COLORS.primary, children: [
765
978
  inputReq.prompt,
766
979
  " "
767
980
  ] }),
@@ -783,7 +996,7 @@ function PromptInput() {
783
996
  // src/ui/Welcome.tsx
784
997
  import { dirname as dirname2, join as join3 } from "node:path";
785
998
  import { fileURLToPath } from "node:url";
786
- import { Box as Box6, Spacer, Text as Text6, useInput as useInput3, useWindowSize as useWindowSize4 } from "ink";
999
+ import { Box as Box8, Spacer, Text as Text8, useInput as useInput3, useWindowSize as useWindowSize6 } from "ink";
787
1000
 
788
1001
  // src/ui/copy/welcome.ts
789
1002
  var sidebarItems = [
@@ -796,12 +1009,12 @@ var sidebarItems = [
796
1009
  description: "push 100 records to Algolia in seconds"
797
1010
  },
798
1011
  {
799
- title: "detect your stack",
800
- description: "React, Vue, Angular, Rails, Django, Laravel & more"
1012
+ title: "detect your framework",
1013
+ description: "React, Vue, Angular, Vanilla JS"
801
1014
  },
802
1015
  {
803
1016
  title: "scaffold a search UI",
804
- description: "a styled InstantSearch UI, wired into your app or templates"
1017
+ description: "a styled InstantSearch component, wired into your app"
805
1018
  },
806
1019
  {
807
1020
  title: "ship it",
@@ -811,27 +1024,27 @@ var sidebarItems = [
811
1024
 
812
1025
  // src/ui/Welcome.tsx
813
1026
  import Image, { InkPictureProvider } from "ink-picture";
814
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1027
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
815
1028
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
816
1029
  function SidebarItem({
817
1030
  title,
818
1031
  description
819
1032
  }) {
820
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
821
- /* @__PURE__ */ jsxs5(Box6, { gap: 1, children: [
822
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.success, children: "\u2192" }),
823
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.strong, bold: true, children: title })
1033
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1034
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, children: [
1035
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.success, children: "\u2192" }),
1036
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.strong, bold: true, children: title })
824
1037
  ] }),
825
- /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", gap: 2, children: [
1038
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 2, children: [
826
1039
  /* @__PURE__ */ jsx6(Spacer, {}),
827
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: description })
1040
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: description })
828
1041
  ] })
829
1042
  ] });
830
1043
  }
831
1044
  function Welcome() {
832
1045
  const confirmStart = useWizard((s) => s.confirmStart);
833
1046
  const openLearnMore = useWizard((s) => s.openLearnMore);
834
- const { rows } = useWindowSize4();
1047
+ const { rows } = useWindowSize6();
835
1048
  useInput3((input, key) => {
836
1049
  if (key.return) confirmStart();
837
1050
  else if (input === "i") openLearnMore();
@@ -850,15 +1063,15 @@ function Welcome() {
850
1063
  if (rows < 30) {
851
1064
  layout = scales["small"];
852
1065
  }
853
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1066
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
854
1067
  /* @__PURE__ */ jsx6(
855
- Box6,
1068
+ Box8,
856
1069
  {
857
1070
  paddingY: layout.main.padding.y,
858
1071
  paddingX: layout.main.padding.x,
859
1072
  flexDirection: "column",
860
1073
  justifyContent: "center",
861
- children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 2, children: [
1074
+ children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
862
1075
  /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
863
1076
  Image,
864
1077
  {
@@ -870,16 +1083,16 @@ function Welcome() {
870
1083
  protocol: "halfBlock"
871
1084
  }
872
1085
  ) }),
873
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
874
- /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
1086
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1087
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
875
1088
  /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
876
1089
  /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
877
1090
  ] })
878
1091
  ] })
879
1092
  }
880
1093
  ),
881
- /* @__PURE__ */ jsxs5(
882
- Box6,
1094
+ /* @__PURE__ */ jsxs7(
1095
+ Box8,
883
1096
  {
884
1097
  backgroundColor: COLORS.bg.sidebar,
885
1098
  width: 40,
@@ -889,7 +1102,7 @@ function Welcome() {
889
1102
  flexDirection: "column",
890
1103
  justifyContent: "center",
891
1104
  children: [
892
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1105
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
893
1106
  sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
894
1107
  ]
895
1108
  }
@@ -899,7 +1112,7 @@ function Welcome() {
899
1112
 
900
1113
  // src/ui/LearnMore.tsx
901
1114
  import { Fragment as Fragment2 } from "react";
902
- import { Box as Box7, Text as Text7, useInput as useInput4, useWindowSize as useWindowSize5 } from "ink";
1115
+ import { Box as Box9, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize7 } from "ink";
903
1116
 
904
1117
  // src/ui/copy/learn-more.ts
905
1118
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -907,7 +1120,7 @@ var accessItems = [
907
1120
  {
908
1121
  tag: "READ",
909
1122
  title: "Project files",
910
- description: "reads your dependency manifests (package.json, pyproject.toml, Gemfile, go.mod, pom.xml\u2026), configs & source to detect your stack. Read-only; nothing is uploaded."
1123
+ description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
911
1124
  },
912
1125
  {
913
1126
  tag: "WRITE",
@@ -936,7 +1149,7 @@ var policyLinks = [
936
1149
  ];
937
1150
 
938
1151
  // src/ui/LearnMore.tsx
939
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1152
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
940
1153
  var TAG_COLORS = {
941
1154
  READ: COLORS.success,
942
1155
  WRITE: COLORS.badge,
@@ -952,25 +1165,25 @@ function NeverLine({
952
1165
  }) {
953
1166
  const used = segments.reduce((n, s) => n + s.text.length, 0);
954
1167
  const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
955
- return /* @__PURE__ */ jsxs6(Text7, { children: [
956
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" }),
1168
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
1169
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" }),
957
1170
  " ".repeat(NEVER_BOX_PAD_X),
958
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: s.bold, children: s.text }, i)),
1171
+ segments.map((s, i) => /* @__PURE__ */ jsx7(Text9, { color: s.color, bold: s.bold, children: s.text }, i)),
959
1172
  " ".repeat(rightPad),
960
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" })
1173
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" })
961
1174
  ] });
962
1175
  }
963
1176
  function LearnMore() {
964
1177
  const confirmStart = useWizard((s) => s.confirmStart);
965
1178
  const backToHome = useWizard((s) => s.backToHome);
966
- const { columns } = useWindowSize5();
1179
+ const { columns } = useWindowSize7();
967
1180
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
968
1181
  useInput4((_input, key) => {
969
1182
  if (key.escape) backToHome();
970
1183
  else if (key.return) confirmStart();
971
1184
  });
972
- return /* @__PURE__ */ jsxs6(
973
- Box7,
1185
+ return /* @__PURE__ */ jsxs8(
1186
+ Box9,
974
1187
  {
975
1188
  flexDirection: "column",
976
1189
  paddingX: PADDING_X,
@@ -978,20 +1191,20 @@ function LearnMore() {
978
1191
  width: "100%",
979
1192
  gap: 1,
980
1193
  children: [
981
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
982
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: accessIntro }),
983
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
984
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
985
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, marginTop: 1, children: [
986
- /* @__PURE__ */ jsx7(Box7, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text7, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
987
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs6(Text7, { children: [
988
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: item.title }),
989
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1194
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1195
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: accessIntro }),
1196
+ /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginTop: 1, children: [
1197
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1198
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1199
+ /* @__PURE__ */ jsx7(Box9, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text9, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1200
+ /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { children: [
1201
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: item.title }),
1202
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
990
1203
  ] }) })
991
1204
  ] })
992
1205
  ] }, item.tag)) }),
993
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
994
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1206
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
1207
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
995
1208
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
996
1209
  /* @__PURE__ */ jsx7(
997
1210
  NeverLine,
@@ -1000,7 +1213,7 @@ function LearnMore() {
1000
1213
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1001
1214
  }
1002
1215
  ),
1003
- neverItems.map((item) => /* @__PURE__ */ jsxs6(Fragment2, { children: [
1216
+ neverItems.map((item) => /* @__PURE__ */ jsxs8(Fragment2, { children: [
1004
1217
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1005
1218
  /* @__PURE__ */ jsx7(
1006
1219
  NeverLine,
@@ -1015,23 +1228,23 @@ function LearnMore() {
1015
1228
  )
1016
1229
  ] }, item)),
1017
1230
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1018
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1231
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1019
1232
  ] }),
1020
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
1021
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1022
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.accent, children: link.url })
1233
+ /* @__PURE__ */ jsx7(Box9, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1234
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1235
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.accent, children: link.url })
1023
1236
  ] }, link.label)) }),
1024
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1025
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
1026
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
1027
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "esc" }),
1028
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "] back" })
1237
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1238
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1239
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1240
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "esc" }),
1241
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "] back" })
1029
1242
  ] }),
1030
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
1031
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
1032
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "enter" }),
1033
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "]" }),
1034
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.success, bold: true, children: "start wizard" })
1243
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1244
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1245
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "enter" }),
1246
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "]" }),
1247
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, bold: true, children: "start wizard" })
1035
1248
  ] })
1036
1249
  ] })
1037
1250
  ]
@@ -1040,10 +1253,10 @@ function LearnMore() {
1040
1253
  }
1041
1254
 
1042
1255
  // src/ui/Sidebar.tsx
1043
- import { Box as Box10, Text as Text10 } from "ink";
1256
+ import { Box as Box12, Text as Text12 } from "ink";
1044
1257
 
1045
1258
  // src/ui/Steps.tsx
1046
- import { Box as Box8, Text as Text8 } from "ink";
1259
+ import { Box as Box10, Text as Text10 } from "ink";
1047
1260
  import Spinner from "ink-spinner";
1048
1261
 
1049
1262
  // src/core/persistence.ts
@@ -1072,11 +1285,11 @@ async function clearWorkflowState(workflowId) {
1072
1285
  }
1073
1286
 
1074
1287
  // src/ui/Steps.tsx
1075
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1288
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1076
1289
  function Steps() {
1077
1290
  const { steps } = useWizard();
1078
1291
  const visibleSteps = steps.filter(isStepVisible);
1079
- return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status[s.status], children: [
1292
+ return /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status[s.status], children: [
1080
1293
  s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1081
1294
  " ",
1082
1295
  s.title
@@ -1086,7 +1299,7 @@ function CurrentStep() {
1086
1299
  const { steps } = useWizard();
1087
1300
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1088
1301
  if (!currentStep) return null;
1089
- return /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status.running, children: [
1302
+ return /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.running, children: [
1090
1303
  /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1091
1304
  " ",
1092
1305
  ` ${currentStep.title}`
@@ -1094,19 +1307,19 @@ function CurrentStep() {
1094
1307
  }
1095
1308
 
1096
1309
  // src/ui/Progress.tsx
1097
- import { Box as Box9, Text as Text9 } from "ink";
1098
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1310
+ import { Box as Box11, Text as Text11 } from "ink";
1311
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1099
1312
  function Progress() {
1100
1313
  const { steps, currentStepIndex } = useWizard();
1101
1314
  const visibleSteps = steps.filter(isStepVisible);
1102
1315
  if (visibleSteps.length === 0) return null;
1103
1316
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1104
1317
  const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
1105
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1106
- /* @__PURE__ */ jsx9(Text9, { color: COLORS.muted, children: "STEP" }),
1107
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: activeStepNumber }),
1108
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "/" }),
1109
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: visibleSteps.length })
1318
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1319
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "STEP" }),
1320
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: activeStepNumber }),
1321
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: "/" }),
1322
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: visibleSteps.length })
1110
1323
  ] });
1111
1324
  }
1112
1325
 
@@ -1117,10 +1330,10 @@ var sidebarCommands = [
1117
1330
  ];
1118
1331
 
1119
1332
  // src/ui/Sidebar.tsx
1120
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1333
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1121
1334
  function Sidebar() {
1122
- return /* @__PURE__ */ jsxs9(
1123
- Box10,
1335
+ return /* @__PURE__ */ jsxs11(
1336
+ Box12,
1124
1337
  {
1125
1338
  backgroundColor: "#14171E",
1126
1339
  width: 30,
@@ -1129,16 +1342,16 @@ function Sidebar() {
1129
1342
  flexDirection: "column",
1130
1343
  justifyContent: "space-between",
1131
1344
  children: [
1132
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1133
- /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "PROGRESS" }),
1345
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1346
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "PROGRESS" }),
1134
1347
  /* @__PURE__ */ jsx10(Steps, {})
1135
1348
  ] }),
1136
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1349
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1137
1350
  /* @__PURE__ */ jsx10(Progress, {}),
1138
- /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: sidebarCommands.map((c) => {
1139
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1140
- /* @__PURE__ */ jsx10(Text10, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1141
- /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: c.description })
1351
+ /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: sidebarCommands.map((c) => {
1352
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1353
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1354
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: c.description })
1142
1355
  ] });
1143
1356
  }) })
1144
1357
  ] })
@@ -1148,12 +1361,12 @@ function Sidebar() {
1148
1361
  }
1149
1362
 
1150
1363
  // src/ui/Ribbon.tsx
1151
- import { Box as Box11, Text as Text11 } from "ink";
1152
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1364
+ import { Box as Box13, Text as Text13 } from "ink";
1365
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1153
1366
  function Ribbon() {
1154
1367
  const firstCommand = sidebarCommands[0];
1155
- return /* @__PURE__ */ jsxs10(
1156
- Box11,
1368
+ return /* @__PURE__ */ jsxs12(
1369
+ Box13,
1157
1370
  {
1158
1371
  backgroundColor: "#14171E",
1159
1372
  flexDirection: "row",
@@ -1163,9 +1376,9 @@ function Ribbon() {
1163
1376
  children: [
1164
1377
  /* @__PURE__ */ jsx11(Progress, {}),
1165
1378
  /* @__PURE__ */ jsx11(CurrentStep, {}),
1166
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1167
- /* @__PURE__ */ jsx11(Text11, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1168
- /* @__PURE__ */ jsx11(Text11, { color: COLORS.muted, children: firstCommand.description })
1379
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1380
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1381
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: firstCommand.description })
1169
1382
  ] })
1170
1383
  ]
1171
1384
  }
@@ -1176,9 +1389,8 @@ function Ribbon() {
1176
1389
  import { useState as useState6 } from "react";
1177
1390
 
1178
1391
  // src/ui/Logs.tsx
1179
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
1180
- import { Box as Box12, Text as Text12, measureElement as measureElement3, useInput as useInput5, useWindowSize as useWindowSize6 } from "ink";
1181
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1392
+ import { Box as Box14, Text as Text14, useInput as useInput5 } from "ink";
1393
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1182
1394
  var KIND_COLOR = {
1183
1395
  tool: COLORS.primary,
1184
1396
  prompt: COLORS.badge
@@ -1208,75 +1420,32 @@ function formatTimestamp(ms) {
1208
1420
  }
1209
1421
  function Logs() {
1210
1422
  const logs = useWizard((s) => s.logs);
1211
- const { rows, columns } = useWindowSize6();
1212
- const viewportRef = useRef3(null);
1213
- const [viewportHeight, setViewportHeight] = useState5(0);
1214
- const [viewportWidth, setViewportWidth] = useState5(0);
1215
- const [scrollOffset, setScrollOffset] = useState5(0);
1216
- const prevMaxOffsetRef = useRef3(0);
1217
- useLayoutEffect2(() => {
1218
- if (!viewportRef.current) return;
1219
- const { width, height } = measureElement3(viewportRef.current);
1220
- setViewportHeight(height);
1221
- setViewportWidth(width);
1222
- }, [rows, columns, logs.length === 0]);
1223
- let capacity = viewportHeight;
1224
- for (let i = 0; i < 2; i++) {
1225
- const hasAbove = scrollOffset > 0;
1226
- const hasBelow = scrollOffset + capacity < logs.length;
1227
- capacity = Math.max(
1228
- viewportHeight - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0),
1229
- 0
1230
- );
1231
- }
1232
- const capacityAtBottom = logs.length > viewportHeight ? Math.max(viewportHeight - 1, 0) : viewportHeight;
1233
- const maxOffset = Math.max(logs.length - capacityAtBottom, 0);
1234
- useLayoutEffect2(() => {
1235
- const wasAtBottom = scrollOffset >= prevMaxOffsetRef.current;
1236
- prevMaxOffsetRef.current = maxOffset;
1237
- setScrollOffset((o) => wasAtBottom ? maxOffset : Math.min(o, maxOffset));
1238
- }, [maxOffset]);
1423
+ const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1239
1424
  useInput5((_input, key) => {
1240
- if (!key.upArrow && !key.downArrow) return;
1241
- setScrollOffset(
1242
- (o) => key.upArrow ? Math.max(o - 1, 0) : Math.min(o + 1, maxOffset)
1243
- );
1425
+ if (key.upArrow) scroll.scrollBy(-1);
1426
+ else if (key.downArrow) scroll.scrollBy(1);
1244
1427
  });
1245
- const visible = logs.slice(scrollOffset, scrollOffset + capacity);
1246
- const hiddenAbove = scrollOffset;
1247
- const hiddenBelow = logs.length - scrollOffset - visible.length;
1248
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1249
- logs.length === 0 && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "No logs yet." }),
1250
- /* @__PURE__ */ jsxs11(Box12, { ref: viewportRef, flexDirection: "column", flexGrow: 1, children: [
1251
- hiddenAbove > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
1252
- "\u2191 ",
1253
- hiddenAbove,
1254
- " more"
1255
- ] }),
1256
- visible.map((entry) => {
1257
- const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1258
- const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1259
- const rawPreview = rawInputText(entry.input);
1260
- const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1261
- const gaps = (partCount - 1) * ROW_GAP;
1262
- let budget = viewportWidth - timestamp.length - durationText.length - gaps;
1263
- const name = truncate2(entry.name, budget);
1264
- budget -= name.length;
1265
- const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1266
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: ROW_GAP, children: [
1267
- /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: timestamp }),
1268
- /* @__PURE__ */ jsx12(Text12, { color: logNameColor(entry), wrap: "truncate", children: name }),
1269
- preview && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, wrap: "truncate", children: preview }),
1270
- durationText && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: durationText })
1271
- ] }, entry.id);
1272
- }),
1273
- hiddenBelow > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
1274
- "\u2193 ",
1275
- hiddenBelow,
1276
- " more"
1277
- ] })
1278
- ] }),
1279
- /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1428
+ const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1429
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1430
+ logs.length === 0 && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "No logs yet." }),
1431
+ /* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
1432
+ const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1433
+ const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1434
+ const rawPreview = rawInputText(entry.input);
1435
+ const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1436
+ const gaps = (partCount - 1) * ROW_GAP;
1437
+ let budget = scroll.width - timestamp.length - durationText.length - gaps;
1438
+ const name = truncate2(entry.name, budget);
1439
+ budget -= name.length;
1440
+ const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1441
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: ROW_GAP, children: [
1442
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: timestamp }),
1443
+ /* @__PURE__ */ jsx12(Text14, { color: logNameColor(entry), wrap: "truncate", children: name }),
1444
+ preview && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, wrap: "truncate", children: preview }),
1445
+ durationText && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: durationText })
1446
+ ] }, entry.id);
1447
+ }) }),
1448
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1280
1449
  ] });
1281
1450
  }
1282
1451
 
@@ -1468,11 +1637,11 @@ function track(event, payload) {
1468
1637
  }
1469
1638
 
1470
1639
  // src/ui/App.tsx
1471
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1640
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1472
1641
  function App() {
1473
1642
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1474
1643
  const { exit } = useApp();
1475
- const { columns, rows } = useWindowSize7();
1644
+ const { columns, rows } = useWindowSize8();
1476
1645
  const [showLogs, setShowLogs] = useState6(false);
1477
1646
  const finished = phase === "done" || phase === "error";
1478
1647
  const currentStep = steps[currentStepIndex];
@@ -1485,7 +1654,7 @@ function App() {
1485
1654
  { isActive: finished }
1486
1655
  );
1487
1656
  useInput6((_input, key) => {
1488
- if (phase === "idle" || phase === "preflight") return;
1657
+ if (phase === "idle" || phase === "authenticating") return;
1489
1658
  if (key.tab) {
1490
1659
  setShowLogs(!showLogs);
1491
1660
  track("AI Wizard Interaction", {
@@ -1495,66 +1664,75 @@ function App() {
1495
1664
  });
1496
1665
  }
1497
1666
  });
1498
- const escOwnedElsewhere = phase === "idle" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1667
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1499
1668
  useInput6((_input, key) => {
1500
1669
  if (escOwnedElsewhere) return;
1501
1670
  if (key.escape) {
1502
1671
  track("AI Wizard Interaction", {
1503
1672
  context: "global",
1504
1673
  key: "esc",
1505
- // No step is active until `startWorkflow` — report the phase instead.
1506
1674
  currentStep: currentStep?.id ?? phase
1507
1675
  });
1508
1676
  exit();
1509
1677
  }
1510
1678
  });
1511
- const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1679
+ const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1512
1680
  const flexDirection = columns > 90 ? "row" : "column";
1513
1681
  const showSidebar = flexDirection === "row";
1514
- return /* @__PURE__ */ jsxs12(
1515
- Box13,
1516
- {
1517
- backgroundColor: COLORS.bg.main,
1518
- flexDirection: "row",
1519
- width: columns,
1520
- minHeight: rows,
1521
- children: [
1522
- mainWindowVisible && /* @__PURE__ */ jsxs12(
1523
- Box13,
1524
- {
1525
- flexDirection,
1526
- width: "100%",
1527
- justifyContent: "space-between",
1528
- children: [
1529
- showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : (
1530
- /* Fill the width beside the sidebar; row layout only (would grow vertically when stacked). */
1531
- /* @__PURE__ */ jsxs12(
1532
- Box13,
1682
+ const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
1683
+ return (
1684
+ /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1685
+ the whole screen, and the scrolling throws off its cursor arithmetic —
1686
+ flicker and leftover rows. */
1687
+ /* @__PURE__ */ jsxs14(
1688
+ Box15,
1689
+ {
1690
+ backgroundColor: COLORS.bg.main,
1691
+ flexDirection: "row",
1692
+ width: columns,
1693
+ height: scrollsPastViewport ? void 0 : rows,
1694
+ overflow: scrollsPastViewport ? "visible" : "hidden",
1695
+ children: [
1696
+ mainWindowVisible && /* @__PURE__ */ jsxs14(
1697
+ Box15,
1698
+ {
1699
+ flexDirection,
1700
+ width: "100%",
1701
+ maxHeight: rows,
1702
+ justifyContent: "space-between",
1703
+ children: [
1704
+ showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : /* @__PURE__ */ jsxs14(
1705
+ Box15,
1533
1706
  {
1534
1707
  flexDirection: "column",
1535
1708
  paddingX: 4,
1536
1709
  paddingY: 2,
1537
1710
  width: showSidebar ? 70 : "100%",
1538
- flexGrow: showSidebar ? 1 : 0,
1711
+ flexGrow: 1,
1539
1712
  children: [
1713
+ phase === "authenticating" && /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginBottom: 1, children: [
1714
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1715
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1716
+ ] }),
1717
+ /* @__PURE__ */ jsx13(CliOutput, {}),
1540
1718
  /* @__PURE__ */ jsx13(Notices, {}),
1541
1719
  /* @__PURE__ */ jsx13(PromptInput, {}),
1542
- phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1543
- phase === "error" && error && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text13, { color: COLORS.status.error, children: [
1720
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1721
+ phase === "error" && error && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text15, { color: COLORS.status.error, children: [
1544
1722
  "\u2716 ",
1545
1723
  error
1546
1724
  ] }) })
1547
1725
  ]
1548
1726
  }
1549
- )
1550
- ),
1551
- showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1552
- ]
1553
- }
1554
- ),
1555
- (phase === "idle" || phase === "preflight") && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1556
- ]
1557
- }
1727
+ ),
1728
+ showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1729
+ ]
1730
+ }
1731
+ ),
1732
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1733
+ ]
1734
+ }
1735
+ )
1558
1736
  );
1559
1737
  }
1560
1738
 
@@ -1568,7 +1746,8 @@ var configFile = () => join5(stateDir(), "config.json");
1568
1746
  var DEFAULT_CONFIG = {
1569
1747
  version: 1,
1570
1748
  aiConsent: false,
1571
- workflowsRun: []
1749
+ workflowsRun: [],
1750
+ searchApiKeys: {}
1572
1751
  };
1573
1752
  async function loadConfig() {
1574
1753
  try {
@@ -1587,6 +1766,38 @@ async function recordWorkflowRun(workflowId, completedAt) {
1587
1766
  config.workflowsRun.push({ workflowId, completedAt });
1588
1767
  await saveConfig(config);
1589
1768
  }
1769
+ function isStoredSearchKey(value) {
1770
+ if (typeof value !== "object" || value === null) return false;
1771
+ const { appId, key } = value;
1772
+ return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
1773
+ }
1774
+ function storedSearchKeys(config) {
1775
+ const stored = config.searchApiKeys;
1776
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
1777
+ return {};
1778
+ }
1779
+ return stored;
1780
+ }
1781
+ async function getStoredSearchKey(index, appId) {
1782
+ const entry = storedSearchKeys(await loadConfig())[index];
1783
+ if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
1784
+ return entry.key;
1785
+ }
1786
+ async function storeSearchKey(index, appId, key) {
1787
+ const config = await loadConfig();
1788
+ config.searchApiKeys = {
1789
+ ...storedSearchKeys(config),
1790
+ [index]: { appId, key }
1791
+ };
1792
+ await saveConfig(config);
1793
+ }
1794
+ async function forgetSearchKey(index) {
1795
+ const config = await loadConfig();
1796
+ const remaining = { ...storedSearchKeys(config) };
1797
+ delete remaining[index];
1798
+ config.searchApiKeys = remaining;
1799
+ await saveConfig(config);
1800
+ }
1590
1801
 
1591
1802
  // src/core/orchestrator.ts
1592
1803
  function defineStep(step) {
@@ -1790,61 +2001,138 @@ async function runWorkflow(workflow, appId) {
1790
2001
  }
1791
2002
  }
1792
2003
 
1793
- // src/lib/algoliaProfile.ts
1794
- import { readFile as readFile3 } from "node:fs/promises";
1795
- import { createRequire as createRequire2 } from "node:module";
1796
- import { homedir as homedir2 } from "node:os";
1797
- import { join as join6 } from "node:path";
1798
- import { parse as parseToml } from "toml";
1799
- var require3 = createRequire2(import.meta.url);
1800
- function configPath() {
1801
- const base = process.env.XDG_CONFIG_HOME || join6(homedir2(), ".config");
1802
- return join6(base, "algolia", "config.toml");
1803
- }
1804
- function profilesFromConfig(tomlText) {
1805
- let parsed;
2004
+ // src/lib/algoliaApp.ts
2005
+ import { z as z4 } from "zod";
2006
+ var applicationSchema = z4.object({
2007
+ id: z4.string().min(1),
2008
+ name: z4.string().default(""),
2009
+ plan: z4.string().optional()
2010
+ });
2011
+ var listSchema = z4.array(
2012
+ z4.object({
2013
+ id: z4.string().min(1),
2014
+ name: z4.string().default(""),
2015
+ plan_label: z4.string().optional()
2016
+ }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
2017
+ );
2018
+ async function currentApplication() {
2019
+ let raw;
1806
2020
  try {
1807
- parsed = parseToml(tomlText);
2021
+ raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
1808
2022
  } catch {
1809
- return [];
2023
+ return null;
2024
+ }
2025
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2026
+ return parsed.success ? parsed.data : null;
2027
+ }
2028
+ async function requireApplication() {
2029
+ const app = await currentApplication();
2030
+ if (!app) {
2031
+ throw new Error(
2032
+ "No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
2033
+ );
2034
+ }
2035
+ return app;
2036
+ }
2037
+ async function listApplications() {
2038
+ const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
2039
+ const parsed = listSchema.safeParse(parseJson(raw));
2040
+ if (!parsed.success) {
2041
+ throw new Error("Could not read the list of Algolia applications.");
2042
+ }
2043
+ return parsed.data;
2044
+ }
2045
+ async function selectApplication(id) {
2046
+ const raw = await runAlgoliaCli(
2047
+ ["application", "select", "--non-interactive", "--app-id", id],
2048
+ { onOutput: stderrSink }
2049
+ );
2050
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2051
+ if (!parsed.success) {
2052
+ throw new Error(
2053
+ `Selected application ${id}, but the Algolia CLI returned an unreadable result.`
2054
+ );
1810
2055
  }
1811
- const profiles = Object.entries(parsed).filter(
1812
- ([, t]) => typeof t.application_id === "string" && typeof t.api_key === "string"
1813
- ).map(([name, t]) => ({
1814
- name,
1815
- appId: t.application_id,
1816
- apiKey: t.api_key,
1817
- isDefault: t.default === true
1818
- }));
1819
- profiles.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
1820
- return profiles.map(({ name, appId, apiKey }) => ({ name, appId, apiKey }));
1821
- }
1822
- async function loadActiveProfile() {
1823
- let profiles;
2056
+ return parsed.data;
2057
+ }
2058
+ function parseJson(text) {
1824
2059
  try {
1825
- profiles = profilesFromConfig(await readFile3(configPath(), "utf8"));
2060
+ return JSON.parse(text);
1826
2061
  } catch {
1827
- profiles = [];
2062
+ return void 0;
1828
2063
  }
1829
- const profile = profiles[0];
1830
- if (!profile) {
2064
+ }
2065
+
2066
+ // src/lib/algoliaAppPicker.ts
2067
+ function secondaryFor(app) {
2068
+ return app.plan ? { kind: "badge", value: app.plan } : void 0;
2069
+ }
2070
+ function labelFor(app) {
2071
+ return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
2072
+ }
2073
+ function selectAndReport(app) {
2074
+ useWizard.getState().pushCliOutput(
2075
+ "stdout",
2076
+ `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
2077
+ );
2078
+ return selectApplication(app.id);
2079
+ }
2080
+ async function promptForApplication() {
2081
+ const store = useWizard.getState();
2082
+ const apps = await listApplications();
2083
+ if (apps.length === 0) {
1831
2084
  throw new Error(
1832
- "No Algolia profile is configured. Run `npx @algolia/cli auth login` to authenticate."
2085
+ "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
1833
2086
  );
1834
2087
  }
1835
- return profile;
2088
+ if (apps.length === 1) {
2089
+ const only = apps[0];
2090
+ logger.info(
2091
+ { app: only.id },
2092
+ "single application on the account; selecting it"
2093
+ );
2094
+ return selectAndReport(only);
2095
+ }
2096
+ const messages = ["Which Algolia application should the wizard work in?"];
2097
+ for (; ; ) {
2098
+ const choice = await store.requestUserInput({
2099
+ prompt: "Select an application",
2100
+ promptType: "multipleChoice",
2101
+ options: apps.map(labelFor),
2102
+ secondary: apps.map(secondaryFor),
2103
+ messages
2104
+ });
2105
+ const chosen = apps.find((app) => labelFor(app) === choice);
2106
+ if (!chosen) {
2107
+ throw new Error("Application picker received an unexpected selection");
2108
+ }
2109
+ try {
2110
+ return await selectAndReport(chosen);
2111
+ } catch (err) {
2112
+ logger.warn(
2113
+ { app: chosen.id, err: err.message },
2114
+ "application select failed; re-prompting"
2115
+ );
2116
+ messages.push(
2117
+ `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
2118
+ );
2119
+ }
2120
+ }
2121
+ }
2122
+ async function ensureApplication() {
2123
+ return await currentApplication() ?? await promptForApplication();
1836
2124
  }
1837
2125
 
1838
2126
  // src/workflows/default.ts
1839
- import { z as z25 } from "zod";
2127
+ import { z as z27 } from "zod";
1840
2128
 
1841
2129
  // src/actions/listIndices.ts
1842
- import { z as z3 } from "zod";
1843
- var indicesListSchema = z3.object({
1844
- items: z3.array(
1845
- z3.object({
1846
- name: z3.string(),
1847
- entries: z3.number().default(0)
2130
+ import { z as z5 } from "zod";
2131
+ var indicesListSchema = z5.object({
2132
+ items: z5.array(
2133
+ z5.object({
2134
+ name: z5.string(),
2135
+ entries: z5.number().default(0)
1848
2136
  })
1849
2137
  )
1850
2138
  });
@@ -1915,12 +2203,12 @@ import "zod";
1915
2203
 
1916
2204
  // src/lib/tools/listFiles.ts
1917
2205
  import { tool } from "ai";
1918
- import z4 from "zod";
2206
+ import z6 from "zod";
1919
2207
  import { readdir } from "node:fs/promises";
1920
2208
 
1921
2209
  // src/lib/tools/path.ts
1922
2210
  import { lstat } from "node:fs/promises";
1923
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
2211
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join6, sep } from "node:path";
1924
2212
  function resolveInRoot(ctx, path) {
1925
2213
  const target = resolve2(ctx.cwd, path);
1926
2214
  const rel = relative(ctx.root, target);
@@ -1936,7 +2224,7 @@ async function hasSymlinkParent(ctx, target) {
1936
2224
  let current = ctx.root;
1937
2225
  const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
1938
2226
  for (const part of parts) {
1939
- current = join7(current, part);
2227
+ current = join6(current, part);
1940
2228
  try {
1941
2229
  if ((await lstat(current)).isSymbolicLink()) return true;
1942
2230
  } catch (err) {
@@ -1951,7 +2239,7 @@ async function hasSymlinkParent(ctx, target) {
1951
2239
  function listFilesTool(ctx) {
1952
2240
  return tool({
1953
2241
  description: "List files in the current working directory",
1954
- inputSchema: z4.object(),
2242
+ inputSchema: z6.object(),
1955
2243
  execute: async () => {
1956
2244
  logger.info("called listFiles tool");
1957
2245
  if (++ctx.counts.list > ctx.limits.list) {
@@ -1967,13 +2255,13 @@ function listFilesTool(ctx) {
1967
2255
 
1968
2256
  // src/lib/tools/changeDirectory.ts
1969
2257
  import { tool as tool2 } from "ai";
1970
- import z5 from "zod";
2258
+ import z7 from "zod";
1971
2259
  import { stat } from "node:fs/promises";
1972
2260
  function changeDirectoryTool(ctx) {
1973
2261
  return tool2({
1974
2262
  description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
1975
- inputSchema: z5.object({
1976
- path: z5.string().describe("Directory to change into")
2263
+ inputSchema: z7.object({
2264
+ path: z7.string().describe("Directory to change into")
1977
2265
  }),
1978
2266
  execute: async ({ path }) => {
1979
2267
  logger.info({ path }, "called changeDirectory tool");
@@ -1995,13 +2283,13 @@ function changeDirectoryTool(ctx) {
1995
2283
 
1996
2284
  // src/lib/tools/reportStatus.ts
1997
2285
  import { tool as tool3 } from "ai";
1998
- import z6 from "zod";
2286
+ import z8 from "zod";
1999
2287
  function reportStatusTool(output) {
2000
2288
  return tool3({
2001
2289
  description: "Report the status of your execution. Return a reason in case of failure.",
2002
- inputSchema: z6.object({
2003
- status: z6.enum(["success", "fail"]),
2004
- reason: z6.string().optional(),
2290
+ inputSchema: z8.object({
2291
+ status: z8.enum(["success", "fail"]),
2292
+ reason: z8.string().optional(),
2005
2293
  output
2006
2294
  }),
2007
2295
  execute: async ({ status, reason, output: output2 }) => {
@@ -2013,8 +2301,8 @@ function reportStatusTool(output) {
2013
2301
 
2014
2302
  // src/lib/tools/readFile.ts
2015
2303
  import { tool as tool4 } from "ai";
2016
- import z7 from "zod";
2017
- import { readFile as readFile4 } from "node:fs/promises";
2304
+ import z9 from "zod";
2305
+ import { readFile as readFile3 } from "node:fs/promises";
2018
2306
 
2019
2307
  // src/lib/tools/env.ts
2020
2308
  import { basename } from "node:path";
@@ -2041,8 +2329,8 @@ function redactEnvValues(content) {
2041
2329
  function readFileTool(ctx) {
2042
2330
  return tool4({
2043
2331
  description: "Read the contents of a file at the given path",
2044
- inputSchema: z7.object({
2045
- filePath: z7.string().describe("Path to the file to read")
2332
+ inputSchema: z9.object({
2333
+ filePath: z9.string().describe("Path to the file to read")
2046
2334
  }),
2047
2335
  execute: async ({ filePath }) => {
2048
2336
  if (++ctx.counts.read > ctx.limits.read) {
@@ -2052,7 +2340,7 @@ function readFileTool(ctx) {
2052
2340
  const resolved = resolveInRoot(ctx, filePath);
2053
2341
  if (!resolved.ok) return resolved.error;
2054
2342
  try {
2055
- const content = await readFile4(resolved.target, "utf8");
2343
+ const content = await readFile3(resolved.target, "utf8");
2056
2344
  return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2057
2345
  } catch (err) {
2058
2346
  return `Error reading ${filePath}: ${err.message}`;
@@ -2063,15 +2351,15 @@ function readFileTool(ctx) {
2063
2351
 
2064
2352
  // src/lib/tools/writeFile.ts
2065
2353
  import { tool as tool5 } from "ai";
2066
- import z8 from "zod";
2354
+ import z10 from "zod";
2067
2355
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2068
2356
  import { dirname as dirname4 } from "node:path";
2069
2357
  function writeFileTool(ctx) {
2070
2358
  return tool5({
2071
2359
  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.",
2072
- inputSchema: z8.object({
2073
- filePath: z8.string().describe("Path to the file to write"),
2074
- content: z8.string().describe("Content to write to the file")
2360
+ inputSchema: z10.object({
2361
+ filePath: z10.string().describe("Path to the file to write"),
2362
+ content: z10.string().describe("Content to write to the file")
2075
2363
  }),
2076
2364
  execute: async ({ filePath, content }) => {
2077
2365
  logger.info({ filePath }, "called writeFile tool");
@@ -2096,9 +2384,125 @@ function writeFileTool(ctx) {
2096
2384
 
2097
2385
  // src/lib/tools/writeAlgoliaCredentials.ts
2098
2386
  import { tool as tool6 } from "ai";
2099
- import z9 from "zod";
2100
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2387
+ import z12 from "zod";
2388
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
2101
2389
  import { dirname as dirname5 } from "node:path";
2390
+
2391
+ // src/lib/algoliaApiKey.ts
2392
+ import { z as z11 } from "zod";
2393
+ var WRITE_ACLS = [
2394
+ "addObject",
2395
+ "deleteObject",
2396
+ "settings",
2397
+ "editSettings",
2398
+ "listIndexes"
2399
+ ];
2400
+ var WRITE_ACL_SET = new Set(WRITE_ACLS);
2401
+ var apiKeySchema = z11.object({
2402
+ value: z11.string().min(1),
2403
+ acl: z11.array(z11.string()).default([]),
2404
+ indexes: z11.array(z11.string()).default([])
2405
+ });
2406
+ var apiKeyListSchema = z11.object({
2407
+ items: z11.array(apiKeySchema).optional(),
2408
+ keys: z11.array(apiKeySchema).optional()
2409
+ }).transform((o) => o.items ?? o.keys ?? []);
2410
+ var createdKeySchema = z11.object({
2411
+ key: z11.string().min(1).optional(),
2412
+ value: z11.string().min(1).optional()
2413
+ }).transform((o) => o.key ?? o.value);
2414
+ function canReuseForWrites(key, index) {
2415
+ return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2416
+ }
2417
+ async function resolveWriteKey(index) {
2418
+ const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2419
+ const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuseForWrites(key, index))?.value;
2420
+ if (existing) {
2421
+ logger.info({ index }, "reusing existing write API key");
2422
+ return existing;
2423
+ }
2424
+ logger.info({ index }, "no reusable write key found; creating one");
2425
+ const created = await runAlgoliaCli([
2426
+ "apikeys",
2427
+ "create",
2428
+ "--indices",
2429
+ index,
2430
+ "--acl",
2431
+ WRITE_ACLS.join(","),
2432
+ "--description",
2433
+ `wizard write key for ${index}`,
2434
+ "-o",
2435
+ "json"
2436
+ ]);
2437
+ const writeKey = createdKeySchema.parse(JSON.parse(created));
2438
+ if (!writeKey) throw new Error("apikeys create returned no key value");
2439
+ return writeKey;
2440
+ }
2441
+ async function createSearchOnlyKey(index) {
2442
+ logger.info({ index }, "creating a search-only API key");
2443
+ const stdout = await runAlgoliaCli([
2444
+ "apikeys",
2445
+ "create",
2446
+ "--acl",
2447
+ "search",
2448
+ "--indices",
2449
+ index,
2450
+ "--description",
2451
+ `Algolia Wizard search-only key for ${index}`,
2452
+ "-o",
2453
+ "json"
2454
+ ]);
2455
+ let payload;
2456
+ try {
2457
+ payload = JSON.parse(stdout);
2458
+ } catch {
2459
+ throw new Error("apikeys create returned output that is not valid JSON");
2460
+ }
2461
+ const created = createdKeySchema.parse(payload);
2462
+ if (!created) throw new Error("apikeys create returned no key value");
2463
+ return created;
2464
+ }
2465
+ async function apiKeyExists(key) {
2466
+ try {
2467
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
2468
+ return true;
2469
+ } catch (err) {
2470
+ return !/does not exist|not found|404/i.test(err.message);
2471
+ }
2472
+ }
2473
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2474
+ if (envKey) {
2475
+ await recordSearchKey(index, appId, envKey);
2476
+ return { key: envKey, source: "env" };
2477
+ }
2478
+ const stored = await getStoredSearchKey(index, appId);
2479
+ if (stored) {
2480
+ if (await apiKeyExists(stored)) {
2481
+ logger.info({ index, appId }, "reusing the stored search-only API key");
2482
+ return { key: stored, source: "config" };
2483
+ }
2484
+ logger.warn(
2485
+ { index, appId },
2486
+ "the stored search-only API key no longer exists; creating a replacement"
2487
+ );
2488
+ await forgetSearchKey(index);
2489
+ }
2490
+ const key = await createSearchOnlyKey(index);
2491
+ await recordSearchKey(index, appId, key);
2492
+ return { key, source: "created" };
2493
+ }
2494
+ async function recordSearchKey(index, appId, key) {
2495
+ try {
2496
+ await storeSearchKey(index, appId, key);
2497
+ } catch (err) {
2498
+ logger.warn(
2499
+ { err: err.message, index },
2500
+ "could not record the search-only API key; a later run may create another"
2501
+ );
2502
+ }
2503
+ }
2504
+
2505
+ // src/lib/tools/writeAlgoliaCredentials.ts
2102
2506
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2103
2507
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2104
2508
  function appendEnv(content, entries) {
@@ -2112,9 +2516,9 @@ function hasEnv(content, name) {
2112
2516
  }
2113
2517
  function writeCredentialsTool(ctx) {
2114
2518
  return tool6({
2115
- 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.`,
2116
- inputSchema: z9.object({
2117
- filePath: z9.string().describe(
2519
+ 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.`,
2520
+ inputSchema: z12.object({
2521
+ filePath: z12.string().describe(
2118
2522
  'Path to the env file to write credentials into (e.g. ".env")'
2119
2523
  )
2120
2524
  }),
@@ -2122,11 +2526,17 @@ function writeCredentialsTool(ctx) {
2122
2526
  logger.info({ filePath }, "called writeCredentials tool");
2123
2527
  const resolved = resolveInRoot(ctx, filePath);
2124
2528
  if (resolved.ok === false) return resolved.error;
2125
- let profile;
2529
+ const targetIndex = useWizard.getState().targetIndex;
2530
+ if (!targetIndex) {
2531
+ return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2532
+ }
2533
+ let appId;
2534
+ let writeKey;
2126
2535
  try {
2127
- profile = await loadActiveProfile();
2128
- } catch {
2129
- return "Error: no Algolia profile is configured, so credentials cannot be written. Ask the user to authenticate with the Algolia CLI first.";
2536
+ appId = (await requireApplication()).id;
2537
+ writeKey = await resolveWriteKey(targetIndex);
2538
+ } catch (err) {
2539
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2130
2540
  }
2131
2541
  try {
2132
2542
  if (await hasSymlinkParent(ctx, resolved.target)) {
@@ -2134,7 +2544,7 @@ function writeCredentialsTool(ctx) {
2134
2544
  }
2135
2545
  let existing = "";
2136
2546
  try {
2137
- existing = await readFile5(resolved.target, "utf8");
2547
+ existing = await readFile4(resolved.target, "utf8");
2138
2548
  } catch (err) {
2139
2549
  if (err.code !== "ENOENT") throw err;
2140
2550
  }
@@ -2145,8 +2555,8 @@ function writeCredentialsTool(ctx) {
2145
2555
  return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2146
2556
  }
2147
2557
  const envWithCredentials = appendEnv(existing, [
2148
- [APP_ID_VAR, profile.appId],
2149
- [API_KEY_VAR, profile.apiKey]
2558
+ [APP_ID_VAR, appId],
2559
+ [API_KEY_VAR, writeKey]
2150
2560
  ]);
2151
2561
  await mkdir4(dirname5(resolved.target), { recursive: true });
2152
2562
  await writeFile4(resolved.target, envWithCredentials, "utf8");
@@ -2160,746 +2570,16 @@ function writeCredentialsTool(ctx) {
2160
2570
 
2161
2571
  // src/lib/tools/searchFiles.ts
2162
2572
  import { tool as tool7 } from "ai";
2163
- import z10 from "zod";
2164
- import { readdir as readdir3, readFile as readFile8 } from "node:fs/promises";
2165
- import { join as join10 } from "node:path";
2166
-
2167
- // src/lib/languages.ts
2168
- import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
2169
- import { existsSync as existsSync2 } from "node:fs";
2170
- import { join as join9 } from "node:path";
2171
-
2172
- // src/lib/tools/utils/packageManager.ts
2173
- import { readFile as readFile6 } from "node:fs/promises";
2174
- import { existsSync } from "node:fs";
2175
- import { join as join8 } from "node:path";
2176
- var LOCKFILES = [
2177
- ["pnpm-lock.yaml", "pnpm"],
2178
- ["yarn.lock", "yarn"],
2179
- ["bun.lockb", "bun"],
2180
- ["bun.lock", "bun"],
2181
- ["package-lock.json", "npm"]
2182
- ];
2183
- async function readPackageJson(cwd = process.cwd()) {
2184
- return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2185
- }
2186
- function packageManagerFrom(pkg) {
2187
- return pkg.packageManager?.split("@")[0] ?? "npm";
2188
- }
2189
- function packageManagerFromLockfile(cwd) {
2190
- return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2191
- }
2192
- async function detectPackageManager(cwd) {
2193
- try {
2194
- const pkg = await readPackageJson(cwd);
2195
- if (pkg.packageManager) return packageManagerFrom(pkg);
2196
- } catch {
2197
- }
2198
- return packageManagerFromLockfile(cwd) ?? "npm";
2199
- }
2200
-
2201
- // src/lib/shell.ts
2202
- function shellQuote(value) {
2203
- return "'" + value.replace(/'/g, "'\\''") + "'";
2204
- }
2205
-
2206
- // src/lib/languages.ts
2207
- var ENTRYPOINT_TOKEN = "{entrypoint}";
2208
- var INGEST_DIR = ".algolia-wizard";
2209
- var SWIFT_PACKAGE_DIR = `${INGEST_DIR}/Ingest`;
2210
- var CSHARP_PROJECT = `${INGEST_DIR}/ingest/ingest.csproj`;
2211
- var VISIBLE_INGEST_DIR = "algolia-wizard";
2212
- var PY_VENV = `${INGEST_DIR}/.venv`;
2213
- var PY_VENV_PYTHON = `${PY_VENV}/bin/python`;
2214
- var PY_REQUIREMENTS = `${INGEST_DIR}/requirements.txt`;
2215
- var LANGUAGE_PROFILES = {
2216
- javascript: {
2217
- id: "javascript",
2218
- displayName: "JavaScript/TypeScript",
2219
- aliases: [
2220
- "javascript",
2221
- "js",
2222
- "typescript",
2223
- "ts",
2224
- "node",
2225
- "nodejs",
2226
- "node.js",
2227
- "bun",
2228
- "deno",
2229
- "ecmascript",
2230
- "jsx",
2231
- "tsx"
2232
- ],
2233
- manifests: ["package.json"],
2234
- // The concrete npm-family manager is resolved by detectPackageManager (it
2235
- // honours the package.json `packageManager` field, which lockfiles can't
2236
- // express), so one spec covers all four and `resolveToolchain` rewrites the
2237
- // binary below.
2238
- packageManagers: [
2239
- {
2240
- id: "npm",
2241
- dependency: { mode: "agent-declares", file: "package.json" },
2242
- installSteps: [{ argv: ["npm", "install"] }],
2243
- ingest: {
2244
- kind: "auto",
2245
- argv: ["node", ENTRYPOINT_TOKEN],
2246
- entrypointExtensions: [".mjs", ".cjs", ".js"]
2247
- }
2248
- }
2249
- ],
2250
- sdk: { packageName: "algoliasearch", versionPin: "^5", docKey: "js" },
2251
- ingestEntrypointExample: `${INGEST_DIR}/ingest.mjs`,
2252
- // package.json scripts are repo-defined, so they're resolved at run time by
2253
- // repoVerification rather than listed here.
2254
- verification: [],
2255
- envReadInstruction: "Read them from `process.env`.",
2256
- skipDirs: ["node_modules", "dist", "build", "coverage", ".next", "out"]
2257
- },
2258
- python: {
2259
- id: "python",
2260
- displayName: "Python",
2261
- aliases: ["python", "python3", "py", "cpython"],
2262
- manifests: [
2263
- "pyproject.toml",
2264
- "requirements.txt",
2265
- "setup.py",
2266
- "setup.cfg",
2267
- "Pipfile"
2268
- ],
2269
- // Deliberately one path for every Python repo: a wizard-owned venv under
2270
- // .algolia-wizard. Reusing the project's uv/poetry environment would mean
2271
- // mutating the developer's real dependency manifest and lockfile, and the
2272
- // declare-here/install-there split is the main way ingestion silently ends
2273
- // up without the SDK installed. The tradeoff: the script can import the
2274
- // Algolia client and anything it declares itself, but not the project's own
2275
- // packages (see the optional root-requirements step below).
2276
- packageManagers: [
2277
- {
2278
- id: "pip-venv",
2279
- dependency: { mode: "agent-declares", file: PY_REQUIREMENTS },
2280
- installSteps: [
2281
- { argv: ["python3", "-m", "venv", PY_VENV] },
2282
- {
2283
- argv: [PY_VENV_PYTHON, "-m", "pip", "install", "-r", PY_REQUIREMENTS]
2284
- },
2285
- // Best-effort access to the project's own dependencies (DB drivers,
2286
- // ORMs) when the repo pins them the classic way.
2287
- {
2288
- argv: [
2289
- PY_VENV_PYTHON,
2290
- "-m",
2291
- "pip",
2292
- "install",
2293
- "-r",
2294
- "requirements.txt"
2295
- ],
2296
- requiresFile: "requirements.txt",
2297
- optional: true
2298
- }
2299
- ],
2300
- ingest: {
2301
- kind: "auto",
2302
- argv: [PY_VENV_PYTHON, ENTRYPOINT_TOKEN],
2303
- entrypointExtensions: [".py"]
2304
- }
2305
- }
2306
- ],
2307
- sdk: {
2308
- packageName: "algoliasearch",
2309
- versionPin: ">=4,<5",
2310
- docKey: "python"
2311
- },
2312
- ingestEntrypointExample: `${INGEST_DIR}/ingest.py`,
2313
- localSourceCaveat: {
2314
- unless: "requirements.txt",
2315
- message: "The ingestion script runs in its own environment under .algolia-wizard/, so it can install the Algolia client but not this project's packages (no requirements.txt to install from). If the script needs your database driver or ORM, add those packages to .algolia-wizard/requirements.txt and re-run the install."
2316
- },
2317
- verification: [
2318
- {
2319
- // -x skips the venv this same directory holds; without it the check
2320
- // compiles every installed package instead of the generated script.
2321
- label: "python compileall",
2322
- argv: ["python3", "-m", "compileall", "-q", "-x", "[.]venv", INGEST_DIR],
2323
- requiresFile: INGEST_DIR
2324
- }
2325
- ],
2326
- envReadInstruction: "Read them from `os.environ`.",
2327
- skipDirs: [
2328
- "venv",
2329
- "__pycache__",
2330
- "site-packages",
2331
- "dist",
2332
- "build",
2333
- "htmlcov"
2334
- ]
2335
- },
2336
- ruby: {
2337
- id: "ruby",
2338
- displayName: "Ruby",
2339
- aliases: ["ruby", "rb", "rails", "ruby on rails", "rubyonrails"],
2340
- manifests: ["Gemfile", "*.gemspec"],
2341
- packageManagers: [
2342
- {
2343
- id: "bundler",
2344
- dependency: { mode: "agent-declares", file: "Gemfile" },
2345
- installSteps: [{ argv: ["bundle", "install"] }],
2346
- ingest: {
2347
- kind: "auto",
2348
- argv: ["bundle", "exec", "ruby", ENTRYPOINT_TOKEN],
2349
- entrypointExtensions: [".rb"]
2350
- }
2351
- }
2352
- ],
2353
- sdk: { packageName: "algolia", versionPin: "~> 3.0", docKey: "ruby" },
2354
- ingestEntrypointExample: `${INGEST_DIR}/ingest.rb`,
2355
- // Ruby has no directory-level syntax check (`ruby -c` is one file at a
2356
- // time), so verification relies on the agent's own review here.
2357
- verification: [],
2358
- envReadInstruction: "Read them from `ENV.fetch('NAME')`.",
2359
- skipDirs: ["vendor", "tmp", "log", "coverage"]
2360
- },
2361
- php: {
2362
- id: "php",
2363
- displayName: "PHP",
2364
- aliases: ["php", "laravel", "symfony"],
2365
- manifests: ["composer.json"],
2366
- packageManagers: [
2367
- {
2368
- id: "composer",
2369
- // `composer require` both declares and installs, and unlike editing
2370
- // composer.json by hand it can't leave composer.lock out of date (which
2371
- // makes a later `composer install` refuse to run).
2372
- dependency: { mode: "wizard-installs" },
2373
- installSteps: [
2374
- {
2375
- argv: [
2376
- "composer",
2377
- "require",
2378
- "algolia/algoliasearch-client-php:^4",
2379
- "--no-interaction",
2380
- // Repo post-install scripts are the project's code, not ours to
2381
- // trigger; Laravel's package:discover also fails in a bare tree.
2382
- "--no-scripts"
2383
- ]
2384
- }
2385
- ],
2386
- ingest: {
2387
- kind: "auto",
2388
- argv: ["php", ENTRYPOINT_TOKEN],
2389
- entrypointExtensions: [".php"]
2390
- }
2391
- }
2392
- ],
2393
- sdk: {
2394
- packageName: "algolia/algoliasearch-client-php",
2395
- versionPin: "^4",
2396
- docKey: "php"
2397
- },
2398
- ingestEntrypointExample: `${INGEST_DIR}/ingest.php`,
2399
- verification: [],
2400
- envReadInstruction: "Read them from `getenv('NAME')`.",
2401
- skipDirs: ["vendor", "node_modules"]
2402
- },
2403
- go: {
2404
- id: "go",
2405
- displayName: "Go",
2406
- aliases: ["go", "golang"],
2407
- manifests: ["go.mod"],
2408
- packageManagers: [
2409
- {
2410
- id: "gomod",
2411
- // Imports in the generated file are the declaration; `go mod tidy`
2412
- // resolves and fetches them — which only works because the script lives
2413
- // outside INGEST_DIR (see VISIBLE_INGEST_DIR).
2414
- dependency: { mode: "code-imports" },
2415
- installSteps: [{ argv: ["go", "mod", "tidy"] }],
2416
- ingest: {
2417
- kind: "auto",
2418
- argv: ["go", "run", ENTRYPOINT_TOKEN],
2419
- entrypointExtensions: [".go"]
2420
- }
2421
- }
2422
- ],
2423
- sdk: {
2424
- packageName: "github.com/algolia/algoliasearch-client-go/v4",
2425
- versionPin: "v4",
2426
- docKey: "go"
2427
- },
2428
- ingestEntrypointExample: `${VISIBLE_INGEST_DIR}/ingest.go`,
2429
- verification: [
2430
- { label: "go vet", argv: ["go", "vet", "./..."], requiresFile: "go.mod" }
2431
- ],
2432
- envReadInstruction: "Read them from `os.Getenv`.",
2433
- skipDirs: ["vendor", "bin"]
2434
- },
2435
- java: {
2436
- id: "java",
2437
- displayName: "Java",
2438
- aliases: ["java"],
2439
- manifests: ["pom.xml", "build.gradle", "build.gradle.kts"],
2440
- packageManagers: [
2441
- {
2442
- id: "maven",
2443
- detectFiles: ["pom.xml"],
2444
- sdkVersionPin: "[4,5)",
2445
- dependency: { mode: "agent-declares", file: "pom.xml" },
2446
- installSteps: [{ argv: ["mvn", "-q", "-DskipTests", "compile"] }],
2447
- // The main class is a wizard constant the instructions require the agent
2448
- // to use, so execution can't be redirected by agent output. Runnable only
2449
- // because the install step above compiles src/main/java first — which is
2450
- // why the entrypoint lives there rather than under .algolia-wizard/.
2451
- ingest: {
2452
- kind: "auto",
2453
- argv: [
2454
- "mvn",
2455
- "-q",
2456
- "org.codehaus.mojo:exec-maven-plugin:3.5.0:java",
2457
- "-Dexec.mainClass=AlgoliaWizardIngest"
2458
- ],
2459
- entrypointExtensions: [".java"]
2460
- }
2461
- },
2462
- {
2463
- id: "gradle",
2464
- detectFiles: ["build.gradle", "build.gradle.kts"],
2465
- dependency: {
2466
- mode: "agent-declares",
2467
- file: "build.gradle",
2468
- alternatives: ["build.gradle.kts"]
2469
- },
2470
- installSteps: [],
2471
- // Auto-running means executing the repo's own ./gradlew wrapper; out of
2472
- // scope for now, so the wizard writes the code and prints the command.
2473
- ingest: {
2474
- kind: "manual",
2475
- entrypointExtensions: [".java"],
2476
- runCommand: "./gradlew runAlgoliaIngest",
2477
- requiresBuildTask: "runAlgoliaIngest"
2478
- }
2479
- }
2480
- ],
2481
- sdk: {
2482
- packageName: "com.algolia:algoliasearch",
2483
- versionPin: "4.+",
2484
- docKey: "java",
2485
- alsoRequires: "The class must be named AlgoliaWizardIngest, in the default package (no `package` statement), with a `public static void main`."
2486
- },
2487
- // Not under .algolia-wizard/: Maven and Gradle only compile src/main/<lang>,
2488
- // so a class outside it never makes it onto the classpath and the run command
2489
- // fails with "class not found".
2490
- ingestEntrypointExample: "src/main/java/AlgoliaWizardIngest.java",
2491
- verification: [
2492
- {
2493
- label: "mvn compile",
2494
- argv: ["mvn", "-q", "-DskipTests", "compile"],
2495
- requiresFile: "pom.xml"
2496
- }
2497
- ],
2498
- envReadInstruction: "Read them from `System.getenv`.",
2499
- skipDirs: ["target", "build", "out"]
2500
- },
2501
- kotlin: {
2502
- id: "kotlin",
2503
- displayName: "Kotlin",
2504
- aliases: ["kotlin", "kt", "ktor"],
2505
- manifests: ["build.gradle.kts", "build.gradle", "pom.xml"],
2506
- packageManagers: [
2507
- {
2508
- id: "gradle",
2509
- detectFiles: ["build.gradle.kts", "build.gradle"],
2510
- dependency: {
2511
- mode: "agent-declares",
2512
- file: "build.gradle.kts",
2513
- alternatives: ["build.gradle"]
2514
- },
2515
- installSteps: [],
2516
- ingest: {
2517
- kind: "manual",
2518
- entrypointExtensions: [".kt"],
2519
- runCommand: "./gradlew runAlgoliaIngest",
2520
- requiresBuildTask: "runAlgoliaIngest"
2521
- }
2522
- },
2523
- // Kotlin/Maven is rare but real, and pom.xml is a Kotlin manifest — without
2524
- // this spec such a repo falls through to Gradle and is told to run a
2525
- // ./gradlew task that doesn't exist. Compiling needs the repo's own
2526
- // kotlin-maven-plugin, so the run stays the developer's step.
2527
- {
2528
- id: "maven",
2529
- detectFiles: ["pom.xml"],
2530
- sdkVersionPin: "[3,4)",
2531
- dependency: { mode: "agent-declares", file: "pom.xml" },
2532
- installSteps: [{ argv: ["mvn", "-q", "-DskipTests", "compile"] }],
2533
- ingest: {
2534
- kind: "manual",
2535
- entrypointExtensions: [".kt"],
2536
- runCommand: "mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=AlgoliaWizardIngest"
2537
- }
2538
- }
2539
- ],
2540
- sdk: {
2541
- packageName: "com.algolia:algoliasearch-client-kotlin",
2542
- versionPin: "3.+",
2543
- docKey: "kotlin",
2544
- // The published client's commonMain ships only ktor-client-core; without an
2545
- // engine the script compiles and then fails at its first request.
2546
- alsoRequires: "The Kotlin client bundles no HTTP engine, so also declare one (e.g. io.ktor:ktor-client-okhttp). Name the object AlgoliaWizardIngest in the default package, with a @JvmStatic main."
2547
- },
2548
- ingestEntrypointExample: "src/main/kotlin/AlgoliaWizardIngest.kt",
2549
- verification: [],
2550
- envReadInstruction: "Read them from `System.getenv`.",
2551
- skipDirs: ["build", "out"]
2552
- },
2553
- scala: {
2554
- id: "scala",
2555
- displayName: "Scala",
2556
- aliases: ["scala", "sbt"],
2557
- manifests: ["build.sbt", "build.sc"],
2558
- packageManagers: [
2559
- {
2560
- id: "sbt",
2561
- dependency: { mode: "agent-declares", file: "build.sbt" },
2562
- installSteps: [],
2563
- ingest: {
2564
- kind: "manual",
2565
- entrypointExtensions: [".scala"],
2566
- runCommand: 'sbt "runMain AlgoliaWizardIngest"'
2567
- }
2568
- }
2569
- ],
2570
- sdk: {
2571
- packageName: "com.algolia:algoliasearch-scala_2.13",
2572
- versionPin: "2.+",
2573
- docKey: "scala",
2574
- alsoRequires: "Name the object AlgoliaWizardIngest in the default package (no `package` statement) so `runMain AlgoliaWizardIngest` resolves it."
2575
- },
2576
- ingestEntrypointExample: "src/main/scala/AlgoliaWizardIngest.scala",
2577
- verification: [],
2578
- envReadInstruction: "Read them from `sys.env`.",
2579
- // `project/` holds sbt's build definition, but the name is generic enough
2580
- // that some repos use it for source; scanning it is cheap, missing source
2581
- // is not.
2582
- skipDirs: ["target"]
2583
- },
2584
- csharp: {
2585
- id: "csharp",
2586
- displayName: "C#",
2587
- aliases: ["c#", "csharp", "cs", ".net", "dotnet", "net", "asp.net"],
2588
- manifests: ["*.csproj", "*.sln", "global.json"],
2589
- packageManagers: [
2590
- {
2591
- id: "dotnet",
2592
- // A self-contained project under .algolia-wizard keeps the ingest script
2593
- // out of the repo's own build graph.
2594
- dependency: { mode: "agent-declares", file: CSHARP_PROJECT },
2595
- installSteps: [{ argv: ["dotnet", "restore", CSHARP_PROJECT] }],
2596
- ingest: {
2597
- kind: "auto",
2598
- argv: ["dotnet", "run", "--project", ENTRYPOINT_TOKEN],
2599
- entrypointExtensions: [".csproj"]
2600
- }
2601
- }
2602
- ],
2603
- sdk: {
2604
- packageName: "Algolia.Search",
2605
- versionPin: "7.*",
2606
- docKey: "csharp"
2607
- },
2608
- ingestEntrypointExample: CSHARP_PROJECT,
2609
- verification: [
2610
- {
2611
- label: "dotnet build",
2612
- argv: ["dotnet", "build", CSHARP_PROJECT, "--nologo"],
2613
- requiresFile: CSHARP_PROJECT
2614
- }
2615
- ],
2616
- envReadInstruction: 'Read them from `Environment.GetEnvironmentVariable("NAME")`.',
2617
- // Deliberately not `packages`: modern .NET uses PackageReference, and
2618
- // `packages/` is where pnpm/Lerna/Turborepo monorepos keep all their source —
2619
- // skipping it would hide the entities the scan is looking for.
2620
- skipDirs: ["bin", "obj"]
2621
- },
2622
- swift: {
2623
- id: "swift",
2624
- displayName: "Swift",
2625
- aliases: ["swift", "swiftui", "ios", "vapor"],
2626
- manifests: ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
2627
- packageManagers: [
2628
- {
2629
- id: "swiftpm",
2630
- dependency: {
2631
- mode: "agent-declares",
2632
- file: `${SWIFT_PACKAGE_DIR}/Package.swift`
2633
- },
2634
- // `swift build` resolves and fetches; a cold build of the client is slow
2635
- // (minutes), which is why the caller degrades to the manual command when
2636
- // this fails.
2637
- installSteps: [
2638
- {
2639
- argv: ["swift", "build", "--package-path", SWIFT_PACKAGE_DIR],
2640
- requiresFile: `${SWIFT_PACKAGE_DIR}/Package.swift`
2641
- }
2642
- ],
2643
- ingest: {
2644
- kind: "auto",
2645
- argv: ["swift", "run", "--package-path", SWIFT_PACKAGE_DIR],
2646
- entrypointExtensions: [".swift"]
2647
- }
2648
- }
2649
- ],
2650
- sdk: {
2651
- packageName: "algoliasearch-client-swift",
2652
- // SwiftPM range syntax, not an exact version — a bare "9.0.0" in a
2653
- // Package.swift dependency pins the patch.
2654
- versionPin: 'from: "9.0.0"',
2655
- docKey: "swift"
2656
- },
2657
- ingestEntrypointExample: `${SWIFT_PACKAGE_DIR}/Sources/Ingest/main.swift`,
2658
- verification: [
2659
- {
2660
- label: "swift build",
2661
- argv: ["swift", "build", "--package-path", SWIFT_PACKAGE_DIR],
2662
- requiresFile: `${SWIFT_PACKAGE_DIR}/Package.swift`
2663
- }
2664
- ],
2665
- envReadInstruction: "Read them from `ProcessInfo.processInfo.environment`.",
2666
- skipDirs: ["Pods", "DerivedData", "Carthage", ".build"]
2667
- },
2668
- dart: {
2669
- id: "dart",
2670
- displayName: "Dart",
2671
- aliases: ["dart", "flutter"],
2672
- manifests: ["pubspec.yaml"],
2673
- packageManagers: [
2674
- {
2675
- id: "flutter-pub",
2676
- detectFiles: [".metadata"],
2677
- dependency: { mode: "agent-declares", file: "pubspec.yaml" },
2678
- installSteps: [{ argv: ["flutter", "pub", "get"] }],
2679
- ingest: {
2680
- kind: "auto",
2681
- argv: ["dart", "run", ENTRYPOINT_TOKEN],
2682
- entrypointExtensions: [".dart"]
2683
- }
2684
- },
2685
- {
2686
- id: "pub",
2687
- dependency: { mode: "agent-declares", file: "pubspec.yaml" },
2688
- installSteps: [{ argv: ["dart", "pub", "get"] }],
2689
- ingest: {
2690
- kind: "auto",
2691
- argv: ["dart", "run", ENTRYPOINT_TOKEN],
2692
- entrypointExtensions: [".dart"]
2693
- }
2694
- }
2695
- ],
2696
- sdk: {
2697
- packageName: "algolia_client_search",
2698
- versionPin: "^1.0.0",
2699
- docKey: "dart"
2700
- },
2701
- ingestEntrypointExample: `${INGEST_DIR}/ingest.dart`,
2702
- verification: [
2703
- {
2704
- // Gated on the directory it analyzes, not just pubspec.yaml: a run that
2705
- // only built a search UI never created it, and `dart analyze` on a
2706
- // missing path fails the whole verification pass.
2707
- label: "dart analyze",
2708
- argv: ["dart", "analyze", INGEST_DIR],
2709
- requiresFile: INGEST_DIR
2710
- }
2711
- ],
2712
- envReadInstruction: "Read them from `Platform.environment`.",
2713
- skipDirs: ["build"]
2714
- }
2715
- };
2716
- var DEFAULT_LANGUAGE_ID = "javascript";
2717
- var JAVASCRIPT = "javascript";
2718
- var CURATED_LANGUAGES = Object.values(
2719
- LANGUAGE_PROFILES
2720
- ).map((profile) => profile.displayName);
2721
- function isBackendLanguage(profile) {
2722
- return profile.id !== JAVASCRIPT;
2723
- }
2724
- function normalizeLanguageName(name) {
2725
- return name.toLowerCase().trim().replace(/[\s_-]+/g, "");
2726
- }
2727
- var ALIAS_TO_ID = /* @__PURE__ */ new Map();
2728
- for (const profile of Object.values(LANGUAGE_PROFILES)) {
2729
- for (const alias of [profile.id, profile.displayName, ...profile.aliases]) {
2730
- ALIAS_TO_ID.set(normalizeLanguageName(alias), profile.id);
2731
- }
2732
- }
2733
- function resolveLanguageProfile(name) {
2734
- const id = ALIAS_TO_ID.get(normalizeLanguageName(name));
2735
- return id ? LANGUAGE_PROFILES[id] : void 0;
2736
- }
2737
- function isSameLanguage(a, b) {
2738
- const x = resolveLanguageProfile(a);
2739
- const y = resolveLanguageProfile(b);
2740
- if (x && y) return x.id === y.id;
2741
- if (x || y) return false;
2742
- const folded = normalizeLanguageName(a);
2743
- return folded !== "" && folded === normalizeLanguageName(b);
2744
- }
2745
- var BASE_SKIP_DIRS = ["node_modules", ".git", "dist"];
2746
- var ALL_SKIP_DIRS = /* @__PURE__ */ new Set([
2747
- ...BASE_SKIP_DIRS,
2748
- ...Object.values(LANGUAGE_PROFILES).flatMap((p) => p.skipDirs)
2749
- ]);
2750
- var ALLOWED_BINARIES = new Set(
2751
- Object.values(LANGUAGE_PROFILES).flatMap((profile) => [
2752
- ...profile.packageManagers.flatMap((pm) => [
2753
- ...pm.installSteps.map((s) => s.argv[0]),
2754
- ...pm.ingest.kind === "auto" ? [pm.ingest.argv[0]] : []
2755
- ]),
2756
- ...profile.verification.map((v) => v.argv[0])
2757
- ])
2758
- );
2759
- var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
2760
- function isWorktreeRelativeCommand(command) {
2761
- return command.includes("/");
2762
- }
2763
- function withCommand(argv, command) {
2764
- return [command, ...argv.slice(1)];
2765
- }
2766
- function resolveDeclaredManifest(root, packageManager) {
2767
- const { dependency } = packageManager;
2768
- if (dependency.mode !== "agent-declares" || !dependency.alternatives?.length) {
2769
- return packageManager;
2770
- }
2771
- const present = [dependency.file, ...dependency.alternatives].find(
2772
- (file) => existsSync2(join9(root, file))
2773
- );
2774
- if (!present || present === dependency.file) return packageManager;
2775
- return { ...packageManager, dependency: { ...dependency, file: present } };
2776
- }
2777
- async function manifestPresent(root, manifest, listing) {
2778
- if (!manifest.startsWith("*.")) return existsSync2(join9(root, manifest));
2779
- if (!listing.entries) {
2780
- const entries = await readdir2(root).catch(() => []);
2781
- listing.entries = Array.isArray(entries) ? entries : [];
2782
- }
2783
- const suffix = manifest.slice(1);
2784
- return listing.entries.some((e) => e.endsWith(suffix));
2785
- }
2786
- async function profileManifestPresent(root, profile, listing) {
2787
- for (const manifest of profile.manifests) {
2788
- if (await manifestPresent(root, manifest, listing)) return true;
2789
- }
2790
- return false;
2791
- }
2792
- async function detectProfilesFromManifests(root) {
2793
- const listing = {};
2794
- const found = [];
2795
- for (const profile of Object.values(LANGUAGE_PROFILES)) {
2796
- if (await profileManifestPresent(root, profile, listing)) found.push(profile);
2797
- }
2798
- return found;
2799
- }
2800
- async function hasProfileManifest(root, profile) {
2801
- return profileManifestPresent(root, profile, {});
2802
- }
2803
- async function pickIngestionCandidates(root, confirmedNames) {
2804
- const confirmed3 = confirmedNames.map((name) => resolveLanguageProfile(name)).filter((p) => p !== void 0);
2805
- const onDisk = await detectProfilesFromManifests(root);
2806
- const onDiskIds = new Set(onDisk.map((p) => p.id));
2807
- const candidates = [
2808
- ...new Map(
2809
- confirmed3.filter((p) => onDiskIds.has(p.id)).map((p) => [p.id, p])
2810
- ).values()
2811
- ];
2812
- return { candidates, confirmed: confirmed3, onDisk };
2813
- }
2814
- async function resolveToolchain(root, profile) {
2815
- const signals = (pm) => [
2816
- ...pm.lockfiles ?? [],
2817
- ...pm.detectFiles ?? []
2818
- ];
2819
- const matched = profile.packageManagers.find(
2820
- (pm) => signals(pm).some((f) => existsSync2(join9(root, f)))
2821
- );
2822
- const fallback = profile.packageManagers.find((pm) => signals(pm).length === 0) ?? profile.packageManagers[0];
2823
- const packageManager = resolveDeclaredManifest(root, matched ?? fallback);
2824
- let { installSteps, ingest } = packageManager;
2825
- installSteps = installSteps.map(
2826
- (step) => isWorktreeRelativeCommand(step.argv[0]) ? { ...step, argv: withCommand(step.argv, join9(root, step.argv[0])) } : step
2827
- );
2828
- if (ingest.kind === "auto" && isWorktreeRelativeCommand(ingest.argv[0])) {
2829
- ingest = {
2830
- ...ingest,
2831
- argv: withCommand(ingest.argv, join9(root, ingest.argv[0]))
2832
- };
2833
- }
2834
- if (profile.id === "javascript") {
2835
- const pm = await detectPackageManager(root);
2836
- if (JS_PACKAGE_MANAGERS.has(pm)) {
2837
- installSteps = installSteps.map((step) => ({
2838
- ...step,
2839
- argv: withCommand(step.argv, pm)
2840
- }));
2841
- if (pm === "bun" && ingest.kind === "auto") {
2842
- ingest = { ...ingest, argv: withCommand(ingest.argv, "bun") };
2843
- }
2844
- }
2845
- }
2846
- return { profile, packageManager, installSteps, ingest };
2847
- }
2848
- function resolveIngestArgv(ingest, entrypoint) {
2849
- if (ingest.kind !== "auto") {
2850
- throw new Error("resolveIngestArgv called for a manual-run toolchain");
2851
- }
2852
- return ingest.argv.map(
2853
- (part) => part === ENTRYPOINT_TOKEN ? entrypoint : part
2854
- );
2855
- }
2856
- function describeIngestCommand(ingest, entrypoint) {
2857
- if (ingest.kind !== "auto") return ingest.runCommand;
2858
- return ingest.argv.map((part) => part === ENTRYPOINT_TOKEN ? shellQuote(entrypoint) : part).join(" ");
2859
- }
2860
- function ingestScriptDir(profile) {
2861
- const parts = profile.ingestEntrypointExample.split("/");
2862
- return parts.slice(0, -1).join("/") || ".";
2863
- }
2864
- function localSourceLimitation(root, profile) {
2865
- const caveat = profile.localSourceCaveat;
2866
- if (!caveat) return void 0;
2867
- return existsSync2(join9(root, caveat.unless)) ? void 0 : caveat.message;
2868
- }
2869
- async function missingBuildTask(root, toolchain) {
2870
- const { ingest, packageManager } = toolchain;
2871
- if (ingest.kind !== "manual" || !ingest.requiresBuildTask) return void 0;
2872
- if (packageManager.dependency.mode !== "agent-declares") return void 0;
2873
- const buildFile = join9(root, packageManager.dependency.file);
2874
- const contents = await readFile7(buildFile, "utf8").catch(() => void 0);
2875
- if (contents === void 0) return void 0;
2876
- return contents.includes(ingest.requiresBuildTask) ? void 0 : ingest.requiresBuildTask;
2877
- }
2878
- function sdkVersionPin(profile, packageManager) {
2879
- return packageManager.sdkVersionPin ?? profile.sdk.versionPin;
2880
- }
2881
- function dependencyInstruction(toolchain) {
2882
- const { profile, packageManager } = toolchain;
2883
- const { packageName } = profile.sdk;
2884
- const versionPin = sdkVersionPin(profile, packageManager);
2885
- const also = profile.sdk.alsoRequires ? ` ${profile.sdk.alsoRequires}` : "";
2886
- switch (packageManager.dependency.mode) {
2887
- case "wizard-installs":
2888
- return `The wizard installs ${packageName} ${versionPin} in the worktree after you finish \u2014 import it directly and do not edit dependency manifests for it.${also}`;
2889
- case "code-imports":
2890
- return `Import ${packageName} in the script; the wizard resolves and fetches it in the worktree after you finish. Do not edit dependency manifests by hand.${also}`;
2891
- case "agent-declares":
2892
- return `Declare ${packageName} ${versionPin} in "${packageManager.dependency.file}" (create the file if needed), plus any other dependency your script imports; the wizard installs them in the worktree after you finish.${also}`;
2893
- }
2894
- }
2895
-
2896
- // src/lib/tools/searchFiles.ts
2573
+ import z13 from "zod";
2574
+ import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
2575
+ import { join as join7 } from "node:path";
2897
2576
  var MAX_QUERY_LENGTH = 1e3;
2898
2577
  async function walkFiles(dir) {
2578
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2899
2579
  const out = [];
2900
- for (const e of await readdir3(dir, { withFileTypes: true })) {
2901
- if (e.name.startsWith(".") || ALL_SKIP_DIRS.has(e.name)) continue;
2902
- const full = join10(dir, e.name);
2580
+ for (const e of await readdir2(dir, { withFileTypes: true })) {
2581
+ if (e.name.startsWith(".") || skip.has(e.name)) continue;
2582
+ const full = join7(dir, e.name);
2903
2583
  if (e.isDirectory()) out.push(...await walkFiles(full));
2904
2584
  else if (e.isFile()) out.push(full);
2905
2585
  }
@@ -2908,9 +2588,9 @@ async function walkFiles(dir) {
2908
2588
  function searchFilesTool(ctx) {
2909
2589
  return tool7({
2910
2590
  description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
2911
- inputSchema: z10.object({
2912
- query: z10.string().describe("JavaScript RegExp pattern to search for"),
2913
- path: z10.string().optional().describe("Directory to search in (default: cwd)")
2591
+ inputSchema: z13.object({
2592
+ query: z13.string().describe("JavaScript RegExp pattern to search for"),
2593
+ path: z13.string().optional().describe("Directory to search in (default: cwd)")
2914
2594
  }),
2915
2595
  execute: async ({ query, path = "." }) => {
2916
2596
  logger.info({ query, path }, "called searchFiles tool");
@@ -2932,7 +2612,7 @@ function searchFilesTool(ctx) {
2932
2612
  for (const file of await walkFiles(resolved.target)) {
2933
2613
  let content;
2934
2614
  try {
2935
- content = await readFile8(file, "utf8");
2615
+ content = await readFile5(file, "utf8");
2936
2616
  } catch {
2937
2617
  continue;
2938
2618
  }
@@ -2954,146 +2634,90 @@ function searchFilesTool(ctx) {
2954
2634
 
2955
2635
  // src/lib/tools/verifyImplementation.ts
2956
2636
  import { tool as tool8 } from "ai";
2957
- import z11 from "zod";
2958
-
2959
- // src/lib/tools/repoVerification.ts
2960
- import { existsSync as existsSync3 } from "node:fs";
2961
- import { join as join11 } from "node:path";
2637
+ import z14 from "zod";
2962
2638
 
2963
2639
  // src/lib/tools/utils/runCommand.ts
2964
2640
  import { spawn as spawn2 } from "node:child_process";
2965
- var INSTALL_TIMEOUT_MS = 15 * 6e4;
2966
- var INGEST_TIMEOUT_MS = 15 * 6e4;
2967
- var VERIFY_TIMEOUT_MS = 10 * 6e4;
2968
- var KILL_GRACE_MS = 5e3;
2969
- function runCommand(command, args, options = {}) {
2970
- const { cwd, env, timeoutMs = VERIFY_TIMEOUT_MS } = options;
2641
+ function runCommand(command, args, cwd) {
2971
2642
  return new Promise((resolve4) => {
2972
2643
  let output = "";
2973
- let settled = false;
2974
2644
  const child = spawn2(command, args, {
2975
2645
  cwd,
2976
- shell: false,
2977
- stdio: ["ignore", "pipe", "pipe"],
2978
- ...env ? { env: { ...process.env, ...env } } : {}
2646
+ stdio: ["ignore", "pipe", "pipe"]
2979
2647
  });
2980
- const settle = (result) => {
2981
- if (settled) return;
2982
- settled = true;
2983
- clearTimeout(timer);
2984
- resolve4(result);
2985
- };
2986
- const timer = setTimeout(() => {
2987
- child.kill("SIGTERM");
2988
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
2989
- const seconds = Math.round(timeoutMs / 1e3);
2990
- settle({
2991
- code: 1,
2992
- output: `${output}
2993
- Timed out after ${seconds}s: ${command} ${args.join(" ")}`.trim(),
2994
- timedOut: true
2995
- });
2996
- }, timeoutMs);
2997
2648
  child.stdout?.on("data", (d) => output += d);
2998
2649
  child.stderr?.on("data", (d) => output += d);
2999
2650
  child.on(
3000
2651
  "error",
3001
- (err) => settle({
3002
- code: 1,
3003
- output: `Failed to run ${command}: ${err.message}`,
3004
- timedOut: false
3005
- })
3006
- );
3007
- child.on(
3008
- "close",
3009
- (code) => settle({ code: code ?? 1, output, timedOut: false })
2652
+ (err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
3010
2653
  );
2654
+ child.on("close", (code) => resolve4({ code: code ?? 1, output }));
3011
2655
  });
3012
2656
  }
3013
2657
 
2658
+ // src/lib/tools/utils/packageManager.ts
2659
+ import { readFile as readFile6 } from "node:fs/promises";
2660
+ import { existsSync } from "node:fs";
2661
+ import { join as join8 } from "node:path";
2662
+ var LOCKFILES = [
2663
+ ["pnpm-lock.yaml", "pnpm"],
2664
+ ["yarn.lock", "yarn"],
2665
+ ["bun.lockb", "bun"],
2666
+ ["bun.lock", "bun"],
2667
+ ["package-lock.json", "npm"]
2668
+ ];
2669
+ async function readPackageJson(cwd = process.cwd()) {
2670
+ return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2671
+ }
2672
+ function packageManagerFrom(pkg) {
2673
+ return pkg.packageManager?.split("@")[0] ?? "npm";
2674
+ }
2675
+ function packageManagerFromLockfile(cwd) {
2676
+ return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2677
+ }
2678
+ async function detectPackageManager(cwd) {
2679
+ try {
2680
+ const pkg = await readPackageJson(cwd);
2681
+ if (pkg.packageManager) return packageManagerFrom(pkg);
2682
+ } catch {
2683
+ }
2684
+ return packageManagerFromLockfile(cwd) ?? "npm";
2685
+ }
2686
+
3014
2687
  // src/lib/tools/repoVerification.ts
3015
2688
  var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
3016
- async function runCheck(command, binary, args) {
3017
- const { code, output } = await runCommand(binary, args, {
3018
- timeoutMs: VERIFY_TIMEOUT_MS
3019
- });
3020
- return { command, exitCode: code, ok: code === 0, output: output.trim() };
3021
- }
3022
- async function javascriptChecks() {
2689
+ async function runRepoVerificationCheck() {
3023
2690
  let pkg;
3024
2691
  try {
3025
2692
  pkg = await readPackageJson();
3026
2693
  } catch (err) {
3027
- return {
3028
- limitation: `Could not read package.json to detect verification conventions: ${err.message}`
3029
- };
2694
+ const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2695
+ return { ok: false, checks: [], limitation };
3030
2696
  }
3031
2697
  const scripts = pkg.scripts ?? {};
3032
2698
  const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
3033
2699
  if (present.length === 0) {
3034
- return {
3035
- limitation: `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`
3036
- };
2700
+ const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2701
+ return { ok: false, checks: [], limitation };
3037
2702
  }
3038
2703
  const pm = await detectPackageManager(process.cwd());
3039
- const checks = [];
3040
- for (const script of present) {
3041
- checks.push(
3042
- await runCheck(`${pm} run ${script}`, pm, ["run", script])
3043
- );
3044
- }
3045
- return { checks };
3046
- }
3047
- async function registryChecks(id) {
3048
- const profile = LANGUAGE_PROFILES[id];
3049
- const runnable = profile.verification.filter(
3050
- (spec) => !spec.requiresFile || existsSync3(join11(process.cwd(), spec.requiresFile))
3051
- );
3052
- if (runnable.length === 0) {
3053
- return {
3054
- limitation: `No mechanical verification available for ${profile.displayName} in this repo.`
3055
- };
3056
- }
3057
- const checks = [];
3058
- for (const spec of runnable) {
3059
- checks.push(
3060
- await runCheck(spec.argv.join(" "), spec.argv[0], [...spec.argv.slice(1)])
3061
- );
3062
- }
3063
- return { checks };
3064
- }
3065
- async function runRepoVerificationCheck(languages = [DEFAULT_LANGUAGE_ID]) {
3066
- const ids = [...new Set(languages)];
3067
- if (ids.length === 0) ids.push(DEFAULT_LANGUAGE_ID);
3068
- const checks = [];
3069
- const limitations = [];
3070
- for (const id of ids) {
3071
- const result = id === JAVASCRIPT ? await javascriptChecks() : await registryChecks(id);
3072
- if ("checks" in result) checks.push(...result.checks);
3073
- else limitations.push(result.limitation);
3074
- }
3075
- if (checks.length === 0) {
3076
- return {
3077
- ok: false,
3078
- checks: [],
3079
- limitation: limitations.join(" ") || "No verification checks available."
3080
- };
2704
+ const checks = [];
2705
+ for (const script of present) {
2706
+ const command = `${pm} run ${script}`;
2707
+ const { code, output } = await runCommand(pm, ["run", script]);
2708
+ checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
3081
2709
  }
3082
- return {
3083
- ok: checks.every((c) => c.ok),
3084
- checks,
3085
- ...limitations.length ? { limitation: limitations.join(" ") } : {}
3086
- };
2710
+ return { ok: checks.every((c) => c.ok), checks };
3087
2711
  }
3088
2712
 
3089
2713
  // src/lib/tools/verifyImplementation.ts
3090
- function verifyImplementationTool(ctx) {
2714
+ function verifyImplementationTool() {
3091
2715
  return tool8({
3092
- description: "Run the repo's mechanical verification checks for generated implementation changes. Uses the conventions of the repo's languages (package.json lint/typecheck/check scripts for JavaScript, the equivalent compile/analyze command elsewhere) and returns structured pass/fail evidence for the verifier to interpret.",
3093
- inputSchema: z11.object(),
2716
+ 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.",
2717
+ inputSchema: z14.object(),
3094
2718
  execute: async () => {
3095
- logger.info({ languages: ctx.languages }, "called verifyImplementation tool");
3096
- return runRepoVerificationCheck(ctx.languages);
2719
+ logger.info("called verifyImplementation tool");
2720
+ return runRepoVerificationCheck();
3097
2721
  }
3098
2722
  });
3099
2723
  }
@@ -3104,7 +2728,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
3104
2728
  import { nanoid as nanoid2 } from "nanoid";
3105
2729
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
3106
2730
  import { dirname as dirname6 } from "node:path";
3107
- import z12 from "zod";
2731
+ import z15 from "zod";
3108
2732
  var DATA_DIR = ".algolia-wizard/data";
3109
2733
  var RECORD_MODEL = "claude-haiku-4-5";
3110
2734
  var MAX_RECORDS = 100;
@@ -3116,17 +2740,17 @@ var anthropic = createAnthropic({
3116
2740
  function generateRecordTool(ctx) {
3117
2741
  return tool9({
3118
2742
  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.",
3119
- inputSchema: z12.object({
3120
- entityName: z12.string().describe("Name of the entity to generate records for."),
3121
- attributes: z12.array(z12.string()).describe("Attribute names each record must contain."),
3122
- count: z12.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
3123
- hint: z12.string().optional().describe("Optional context to steer realistic values.")
2743
+ inputSchema: z15.object({
2744
+ entityName: z15.string().describe("Name of the entity to generate records for."),
2745
+ attributes: z15.array(z15.string()).describe("Attribute names each record must contain."),
2746
+ count: z15.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2747
+ hint: z15.string().optional().describe("Optional context to steer realistic values.")
3124
2748
  }),
3125
2749
  execute: async ({ entityName, attributes, count, hint }) => {
3126
2750
  logger.info({ entityName, count }, "called generateRecord tool");
3127
2751
  try {
3128
- const value = z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]);
3129
- const recordSchema = z12.object(
2752
+ const value = z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]);
2753
+ const recordSchema = z15.object(
3130
2754
  Object.fromEntries(attributes.map((attr) => [attr, value]))
3131
2755
  );
3132
2756
  const generateBatch = async (batchCount) => {
@@ -3136,8 +2760,8 @@ function generateRecordTool(ctx) {
3136
2760
  const { output } = await generateText({
3137
2761
  model: anthropic(RECORD_MODEL),
3138
2762
  output: Output.object({
3139
- schema: z12.object({
3140
- records: z12.array(recordSchema).length(batchCount)
2763
+ schema: z15.object({
2764
+ records: z15.array(recordSchema).length(batchCount)
3141
2765
  })
3142
2766
  }),
3143
2767
  prompt: [
@@ -3195,12 +2819,12 @@ function generateRecordTool(ctx) {
3195
2819
 
3196
2820
  // src/lib/tools/notifyUser.ts
3197
2821
  import { tool as tool10 } from "ai";
3198
- import z13 from "zod";
2822
+ import z16 from "zod";
3199
2823
  function notifyUserTool() {
3200
2824
  return tool10({
3201
2825
  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.`,
3202
- inputSchema: z13.object({
3203
- message: z13.string().describe(
2826
+ inputSchema: z16.object({
2827
+ message: z16.string().describe(
3204
2828
  "Short, plain-language description of what you are doing now."
3205
2829
  )
3206
2830
  }),
@@ -3219,17 +2843,12 @@ var DEFAULT_TOOL_LIMITS = {
3219
2843
  read: 20,
3220
2844
  match: 100
3221
2845
  };
3222
- function createToolContext({
3223
- limits = DEFAULT_TOOL_LIMITS,
3224
- cwd = process.cwd(),
3225
- languages = [DEFAULT_LANGUAGE_ID]
3226
- } = {}) {
2846
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
3227
2847
  return {
3228
2848
  root: cwd,
3229
2849
  cwd,
3230
2850
  limits,
3231
- counts: { list: 0, search: 0, read: 0 },
3232
- languages: languages.length ? languages : [DEFAULT_LANGUAGE_ID]
2851
+ counts: { list: 0, search: 0, read: 0 }
3233
2852
  };
3234
2853
  }
3235
2854
 
@@ -3266,7 +2885,7 @@ function createTools(ctx, { output, tools }) {
3266
2885
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
3267
2886
  verifyImplementation: withLogging(
3268
2887
  "verifyImplementation",
3269
- verifyImplementationTool(ctx)
2888
+ verifyImplementationTool()
3270
2889
  ),
3271
2890
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
3272
2891
  notifyUser: withLogging("notifyUser", notifyUserTool())
@@ -3302,7 +2921,7 @@ async function runAgent(req) {
3302
2921
  baseURL: PROXY_BASE_URL,
3303
2922
  fetch: proxyFetch
3304
2923
  });
3305
- const toolContext = createToolContext({ languages: req.languages });
2924
+ const toolContext = createToolContext();
3306
2925
  const readTools = ["readFile", "searchFiles", "listFiles"];
3307
2926
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
3308
2927
  const instructions = [
@@ -3385,19 +3004,16 @@ async function runAgent(req) {
3385
3004
  }
3386
3005
 
3387
3006
  // src/actions/detectLanguage.ts
3388
- import z16 from "zod";
3389
- var detectLanguageSchema = z16.object({
3390
- languages: z16.array(z16.object({ name: z16.string(), version: z16.string() })),
3391
- frameworks: z16.array(z16.object({ name: z16.string(), version: z16.string() }))
3007
+ import z19 from "zod";
3008
+ var detectLanguageSchema = z19.object({
3009
+ languages: z19.array(z19.object({ name: z19.string(), version: z19.string() })),
3010
+ frameworks: z19.array(z19.object({ name: z19.string(), version: z19.string() }))
3392
3011
  });
3393
3012
  var detectLanguage = () => runAgent({
3394
3013
  instructions: [
3395
3014
  "Analyze the codebase and determine the programming languages and frameworks used",
3396
- "Start from the dependency manifests: package.json, pyproject.toml, requirements.txt, Gemfile, composer.json, go.mod, pom.xml, build.gradle(.kts), *.csproj, build.sbt, Package.swift, pubspec.yaml.",
3397
- "List the language that owns the backend/data code first \u2014 that is the one an ingestion script will be written in.",
3398
- "If a superset language is found, exclude the subset language. TS-over-JS. Kotlin-over-Java when Kotlin is primary.",
3015
+ "If a superset language is found, exclude the subset language. TS-over-JS.",
3399
3016
  "If a meta-framework is used, exclude the framework. Next-over-React.",
3400
- "Frameworks include backend and server-rendering frameworks (e.g. Rails, Django, Laravel, Symfony, Spring Boot, ASP.NET Core, Flask, Gin, Ktor) as well as frontend ones (React, Vue, Angular, Svelte) and mobile ones (Flutter, SwiftUI).",
3401
3017
  "Return the exact version",
3402
3018
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
3403
3019
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -3409,31 +3025,31 @@ var detectLanguage = () => runAgent({
3409
3025
  });
3410
3026
 
3411
3027
  // src/actions/analyzeCodebase.ts
3412
- import z17 from "zod";
3028
+ import z20 from "zod";
3413
3029
  var READONLY_TOOLS = [
3414
3030
  "listFiles",
3415
3031
  "changeDirectory",
3416
3032
  "readFile",
3417
3033
  "searchFiles"
3418
3034
  ];
3419
- var ingestionAnalysisSchema = z17.object({
3420
- ingestionAnalysis: z17.array(
3421
- z17.object({
3422
- name: z17.string(),
3423
- paths: z17.array(z17.string()),
3035
+ var ingestionAnalysisSchema = z20.object({
3036
+ ingestionAnalysis: z20.array(
3037
+ z20.object({
3038
+ name: z20.string(),
3039
+ paths: z20.array(z20.string()),
3424
3040
  // indexable fields the agent found for this entity
3425
- attributes: z17.array(z17.string())
3041
+ attributes: z20.array(z20.string())
3426
3042
  })
3427
3043
  )
3428
3044
  });
3429
- var searchImplementationAnalysisSchema = z17.object({
3430
- searchImplementationAnalysis: z17.string()
3045
+ var searchImplementationAnalysisSchema = z20.object({
3046
+ searchImplementationAnalysis: z20.string()
3431
3047
  });
3432
- var verificationSchema = z17.object({
3433
- verification: z17.array(z17.string())
3048
+ var verificationSchema = z20.object({
3049
+ verification: z20.array(z20.string())
3434
3050
  });
3435
3051
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
3436
- var analyzeCodebaseSchema = z17.object({
3052
+ var analyzeCodebaseSchema = z20.object({
3437
3053
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3438
3054
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
3439
3055
  verification: verificationSchema.shape.verification.optional(),
@@ -3446,7 +3062,6 @@ var MODE_CONFIG = {
3446
3062
  "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
3447
3063
  "For each entity, return its name, the file path(s) where it is defined, and its indexable attribute keys (the fields a user would search or filter on).",
3448
3064
  "Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
3449
- "Entities live wherever the stack keeps them: TypeScript interfaces or a Prisma schema, Django models.py, Rails app/models, Laravel Eloquent models, JPA @Entity classes, Go structs, C# entity classes, Pydantic models.",
3450
3065
  "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
3451
3066
  "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
3452
3067
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -3458,9 +3073,8 @@ var MODE_CONFIG = {
3458
3073
  instructions: [
3459
3074
  "Analyze the codebase to determine the single best location to add search UI functionality.",
3460
3075
  "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
3461
- "It may be a client-side component or a server-rendered template \u2014 return whichever the project actually renders its UI from (e.g. /layouts/header.tsx, app/views/layouts/application.html.erb, templates/base.html, resources/views/layouts/app.blade.php, templates/base.html.twig).",
3462
- "Return one file path as searchImplementationAnalysis.",
3463
- 'Use as few tools as possible, but do not guess. If the project renders no UI at all (an API-only service), say "unknown".',
3076
+ "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
3077
+ 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
3464
3078
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
3465
3079
  "When done, call reportStatus"
3466
3080
  ],
@@ -3469,8 +3083,8 @@ var MODE_CONFIG = {
3469
3083
  verification: {
3470
3084
  instructions: [
3471
3085
  "Analyze the codebase to determine which code-quality tools are available to validate changes.",
3472
- "Look at the dependency manifest and config files for the project's languages: package.json scripts with tsconfig/eslint/prettier, pyproject.toml or setup.cfg (ruff, mypy, black), Gemfile with .rubocop.yml, composer.json scripts (phpstan, pint), go.mod with a golangci-lint config, Maven/Gradle verification tasks, .NET analyzers, analysis_options.yaml.",
3473
- 'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"] or ["ruff", "mypy"].',
3086
+ "Look at package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
3087
+ 'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
3474
3088
  "Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
3475
3089
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
3476
3090
  "When done, call reportStatus"
@@ -3497,7 +3111,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3497
3111
  // package.json
3498
3112
  var package_default = {
3499
3113
  name: "@algolia/wizard",
3500
- version: "0.8.0-rc.67.55",
3114
+ version: "0.9.0-rc.49.79",
3501
3115
  description: "Magically implement Algolia functionality in your codebase",
3502
3116
  type: "module",
3503
3117
  engines: {
@@ -3519,7 +3133,7 @@ var package_default = {
3519
3133
  prepare: "husky",
3520
3134
  prepublishOnly: "pnpm build",
3521
3135
  reset: "tsx ./scripts/reset-state.ts",
3522
- "test:toolchains": "tsx ./scripts/verify-toolchains.ts",
3136
+ "test:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
3523
3137
  "test:tools": "tsx ./tool-evals/toolEval.ts",
3524
3138
  test: "vitest",
3525
3139
  typecheck: "tsc --noEmit -p tsconfig.json"
@@ -3545,9 +3159,7 @@ var package_default = {
3545
3159
  dependencies: {
3546
3160
  "@ai-sdk/anthropic": "^3.0.81",
3547
3161
  "@ai-sdk/openai-compatible": "^2.0.47",
3548
- "@algolia/cli": "^5.11.0",
3549
3162
  "@hono/node-server": "^2.0.10",
3550
- "@mishieck/ink-titled-box": "^0.4.2",
3551
3163
  "@segment/analytics-node": "^3.1.0",
3552
3164
  ai: "^6.0.190",
3553
3165
  dotenv: "^17.4.2",
@@ -3560,7 +3172,6 @@ var package_default = {
3560
3172
  nanoid: "^5.1.15",
3561
3173
  pino: "^10.3.1",
3562
3174
  react: "^19.2.7",
3563
- toml: "^4.1.1",
3564
3175
  varlock: "^1.5.1",
3565
3176
  zod: "^4.4.3",
3566
3177
  zustand: "^5.0.14"
@@ -3600,185 +3211,82 @@ function parseEntries(raw) {
3600
3211
  return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
3601
3212
  }
3602
3213
  var summarize = (entries) => entries.length ? entries.map((e) => e.name).join(", ") : "none";
3603
-
3604
- // src/actions/confirmLanguage.ts
3605
- import z19 from "zod";
3606
- var confirmLanguageSchema = z19.object({
3607
- languages: detectLanguageSchema.shape.languages
3608
- });
3609
- var OTHER_OPTION = "Other";
3610
- function confirmed(languages) {
3611
- track("AI Wizard Language Confirmed", { languages });
3612
- return { languages };
3613
- }
3614
- async function askOtherLanguage(ctx) {
3615
- let prompt = "enter the language for your ingestion script";
3214
+ async function askList(ctx, prompt, { required = false } = {}) {
3616
3215
  for (; ; ) {
3617
3216
  const answer = await ctx.requestUserInput({
3618
3217
  prompt,
3619
3218
  promptType: "textInput",
3620
- options: []
3219
+ options: [],
3220
+ helpText: 'Comma-separated, e.g. "TypeScript, Node".'
3621
3221
  });
3622
3222
  if (typeof answer !== "string") {
3623
- throw new Error("confirmLanguage received an unexpected non-text result");
3223
+ throw new Error("askList received an unexpected non-text result");
3624
3224
  }
3625
- const name = parseEntries(answer)[0]?.name;
3626
- if (name) return name;
3627
- prompt = "please enter a language name:";
3225
+ const entries = parseEntries(answer);
3226
+ if (entries.length || !required) return entries;
3227
+ prompt = "Please enter at least one entry:";
3628
3228
  }
3629
3229
  }
3230
+
3231
+ // src/actions/confirmLanguage.ts
3232
+ import z22 from "zod";
3233
+ var confirmLanguageSchema = z22.object({
3234
+ languages: detectLanguageSchema.shape.languages
3235
+ });
3630
3236
  async function confirmLanguage(ctx) {
3631
3237
  const detected = ctx.getStepOutput("project-scan");
3632
- const detectedLanguages = detected.languages ?? [];
3633
- const others = (chosen) => detectedLanguages.filter((l) => !isSameLanguage(l.name, chosen));
3634
- const primary = detectedLanguages[0];
3635
- if (primary) {
3636
- const accepted = await ctx.requestUserInput({
3637
- prompt: `Write the ingestion script in ${primary.name}?`,
3638
- promptType: "acceptReject",
3639
- options: [`Confirm ${primary.name}`, "Use a different language"],
3640
- secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0],
3641
- messages: detectedLanguages.length > 1 ? [`Detected: ${summarize(detectedLanguages)}`] : []
3642
- });
3643
- if (accepted === true) return confirmed(detectedLanguages);
3644
- }
3645
- const options = [...CURATED_LANGUAGES];
3646
- for (const language of detectedLanguages) {
3647
- if (!options.some((o) => isSameLanguage(o, language.name))) {
3648
- options.push(language.name);
3649
- }
3650
- }
3651
- options.push(OTHER_OPTION);
3652
- const detectedFor = (option) => detectedLanguages.find((l) => isSameLanguage(option, l.name));
3653
- const secondary = options.map(
3654
- (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
3655
- );
3656
- const defaultSelectedIndex = Math.max(
3657
- options.findIndex((o) => detectedFor(o)),
3658
- 0
3659
- );
3660
- const selection = await ctx.requestUserInput({
3661
- prompt: "select the language for your ingestion script",
3662
- promptType: "multipleChoice",
3663
- options,
3664
- secondary,
3665
- defaultSelectedIndex
3238
+ const answer = await ctx.requestUserInput({
3239
+ prompt: "Did we detect your language(s) correctly?",
3240
+ promptType: "acceptReject",
3241
+ options: ["Yes", "No"],
3242
+ messages: [`Languages: ${summarize(detected.languages)}`]
3666
3243
  });
3667
- if (typeof selection !== "string") {
3668
- throw new Error("confirmLanguage received an unexpected non-text result");
3669
- }
3670
- const name = selection === OTHER_OPTION ? await askOtherLanguage(ctx) : selection;
3671
- const version = detectedFor(name)?.version ?? "unknown";
3672
- return confirmed([{ name, version }, ...others(name)]);
3244
+ const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
3245
+ required: true
3246
+ });
3247
+ track("AI Wizard Language Confirmed", {
3248
+ languages
3249
+ });
3250
+ return { languages };
3673
3251
  }
3674
3252
 
3675
3253
  // src/actions/confirmFramework.ts
3676
- import z20 from "zod";
3677
-
3678
- // src/lib/frameworks.ts
3679
- var BACKEND_ONLY_FRAMEWORK = "Backend only / API";
3680
- var FRAMEWORKS = [
3681
- // Frontend — InstantSearch component flavors.
3682
- { name: "Next.js", strategy: "react", aliases: ["next", "nextjs"] },
3683
- { name: "React", strategy: "react", aliases: ["reactjs"] },
3684
- { name: "Vue", strategy: "vue", aliases: ["vuejs", "nuxt", "nuxtjs"] },
3685
- { name: "Angular", strategy: "angular", aliases: ["angularjs"] },
3686
- // No Svelte InstantSearch flavor exists, so it uses InstantSearch.js.
3687
- { name: "Svelte", strategy: "js", aliases: ["sveltekit"] },
3688
- {
3689
- name: "Vanilla JS",
3690
- strategy: "js",
3691
- aliases: ["vanilla", "javascript", "js", "astro", "vite"]
3692
- },
3693
- // Backend — Algolia's official framework integrations. Server-rendered
3694
- // templates get InstantSearch.js from a CDN.
3695
- {
3696
- name: "Rails",
3697
- strategy: "cdn-template",
3698
- aliases: ["rubyonrails", "ruby on rails", "erb"]
3699
- },
3700
- { name: "Django", strategy: "cdn-template", aliases: ["jinja", "jinja2"] },
3701
- { name: "Laravel", strategy: "cdn-template", aliases: ["blade"] },
3702
- { name: "Symfony", strategy: "cdn-template", aliases: ["twig"] },
3703
- // Mobile — Algolia ships InstantSearch iOS/Android and Dart clients, but the
3704
- // wizard can't scaffold a native UI, so it points at the docs instead.
3705
- { name: "Flutter", strategy: "none", aliases: [] },
3706
- { name: "iOS", strategy: "none", aliases: ["swiftui", "uikit"] },
3707
- { name: "Android", strategy: "none", aliases: ["jetpack compose", "compose"] },
3708
- { name: BACKEND_ONLY_FRAMEWORK, strategy: "cdn-template", aliases: [] }
3254
+ import z23 from "zod";
3255
+ var confirmFrameworkSchema = z23.object({
3256
+ frameworks: detectLanguageSchema.shape.frameworks
3257
+ });
3258
+ var CURATED_FRAMEWORKS = [
3259
+ "Next.js",
3260
+ "React",
3261
+ "Vue",
3262
+ "Angular",
3263
+ "Svelte",
3264
+ "Vanilla JS"
3709
3265
  ];
3710
- var CURATED_FRAMEWORKS = FRAMEWORKS.map(
3711
- (f) => f.name
3712
- );
3266
+ var OTHER_OPTION = "Other";
3713
3267
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
3714
- var ALIAS_TO_NAME = /* @__PURE__ */ new Map();
3715
- for (const framework of FRAMEWORKS) {
3716
- for (const alias of [framework.name, ...framework.aliases]) {
3717
- ALIAS_TO_NAME.set(normalize(alias), framework.name);
3718
- }
3719
- }
3720
- var STRATEGY_BY_NAME = new Map(FRAMEWORKS.map((f) => [f.name, f.strategy]));
3721
- function canonicalFrameworkName(name) {
3722
- return ALIAS_TO_NAME.get(normalize(name));
3723
- }
3724
- function isSameFramework(a, b) {
3725
- const x = canonicalFrameworkName(a) ?? normalize(a);
3726
- const y = canonicalFrameworkName(b) ?? normalize(b);
3268
+ var FRAMEWORK_ALIASES = {
3269
+ next: "nextjs",
3270
+ nextjs: "nextjs",
3271
+ react: "react",
3272
+ reactjs: "react",
3273
+ vue: "vue",
3274
+ vuejs: "vue",
3275
+ angular: "angular",
3276
+ angularjs: "angular",
3277
+ svelte: "svelte",
3278
+ sveltekit: "svelte",
3279
+ vanillajs: "vanillajs",
3280
+ vanilla: "vanillajs",
3281
+ javascript: "vanillajs",
3282
+ js: "vanillajs"
3283
+ };
3284
+ var isSameFramework = (a, b) => {
3285
+ const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
3286
+ const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
3727
3287
  return x !== "" && x === y;
3728
- }
3729
- function resolveSearchStrategy(frameworkName, hasJavaScriptInStack) {
3730
- const canonical = frameworkName ? canonicalFrameworkName(frameworkName) : void 0;
3731
- const strategy = canonical ? STRATEGY_BY_NAME.get(canonical) : void 0;
3732
- if (strategy) return strategy;
3733
- return hasJavaScriptInStack ? "js" : "cdn-template";
3734
- }
3735
- function searchDocKey(strategy) {
3736
- return strategy === "cdn-template" ? "templates" : strategy;
3737
- }
3738
- function bundlesJavaScript(strategy) {
3739
- return strategy !== "cdn-template" && strategy !== "none";
3740
- }
3741
- function canScaffoldSearchUI(strategy) {
3742
- return strategy !== "none";
3743
- }
3744
- var ENV_PREFIXES = [
3745
- { aliases: ["next", "nextjs"], prefix: "NEXT_PUBLIC_" },
3746
- { aliases: ["nuxt", "nuxtjs"], prefix: "NUXT_PUBLIC_" },
3747
- { aliases: ["astro"], prefix: "PUBLIC_" },
3748
- { aliases: ["vite"], prefix: "VITE_" }
3749
- ];
3750
- var DEFAULT_ENV_PREFIX = "PUBLIC_";
3751
- function publicEnvPrefix(frameworkNames, strategy) {
3752
- if (!bundlesJavaScript(strategy)) return "";
3753
- const present = new Set(frameworkNames.map(normalize));
3754
- for (const { aliases, prefix } of ENV_PREFIXES) {
3755
- if (aliases.some((alias) => present.has(alias))) return prefix;
3756
- }
3757
- return DEFAULT_ENV_PREFIX;
3758
- }
3759
- function describeSearchTarget(strategy, frameworkName) {
3760
- switch (strategy) {
3761
- case "react":
3762
- return "React (react-instantsearch)";
3763
- case "vue":
3764
- return "Vue (vue-instantsearch)";
3765
- case "angular":
3766
- return "Angular (angular-instantsearch)";
3767
- case "js":
3768
- return "plain JavaScript (InstantSearch.js)";
3769
- case "cdn-template":
3770
- return `${frameworkName ?? "server-rendered"} templates (InstantSearch.js via CDN)`;
3771
- case "none":
3772
- return frameworkName ?? "a native mobile app";
3773
- }
3774
- }
3775
-
3776
- // src/actions/confirmFramework.ts
3777
- var confirmFrameworkSchema = z20.object({
3778
- frameworks: detectLanguageSchema.shape.frameworks
3779
- });
3780
- var OTHER_OPTION2 = "Other";
3781
- function confirmed2(name, version) {
3288
+ };
3289
+ function confirmed(name, version) {
3782
3290
  const frameworks = [{ name, version: version ?? "unknown" }];
3783
3291
  track("AI Wizard Frontend Framework Confirmed", { frameworks });
3784
3292
  return { frameworks };
@@ -3806,7 +3314,7 @@ async function confirmFramework(ctx) {
3806
3314
  for (const fw of detectedFrameworks) {
3807
3315
  if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
3808
3316
  }
3809
- options.push(OTHER_OPTION2);
3317
+ options.push(OTHER_OPTION);
3810
3318
  const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
3811
3319
  const primary = detectedFrameworks[0];
3812
3320
  if (primary) {
@@ -3816,7 +3324,7 @@ async function confirmFramework(ctx) {
3816
3324
  options: [`Confirm ${primary.name}`, "Use a different framework"],
3817
3325
  secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
3818
3326
  });
3819
- if (accepted === true) return confirmed2(primary.name, primary.version);
3327
+ if (accepted === true) return confirmed(primary.name, primary.version);
3820
3328
  }
3821
3329
  const secondary = options.map(
3822
3330
  (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
@@ -3826,7 +3334,7 @@ async function confirmFramework(ctx) {
3826
3334
  0
3827
3335
  );
3828
3336
  const selection = await ctx.requestUserInput({
3829
- prompt: "select the framework that renders your UI",
3337
+ prompt: "select a framework",
3830
3338
  promptType: "multipleChoice",
3831
3339
  options,
3832
3340
  secondary,
@@ -3835,10 +3343,10 @@ async function confirmFramework(ctx) {
3835
3343
  if (typeof selection !== "string") {
3836
3344
  throw new Error("confirmFramework received an unexpected non-text result");
3837
3345
  }
3838
- if (selection === OTHER_OPTION2) {
3839
- return confirmed2(await askOtherFramework(ctx));
3346
+ if (selection === OTHER_OPTION) {
3347
+ return confirmed(await askOtherFramework(ctx));
3840
3348
  }
3841
- return confirmed2(selection, detectedFor(selection)?.version);
3349
+ return confirmed(selection, detectedFor(selection)?.version);
3842
3350
  }
3843
3351
 
3844
3352
  // src/actions/promptUser.ts
@@ -3872,8 +3380,8 @@ async function promptUser(ctx, params) {
3872
3380
  }
3873
3381
 
3874
3382
  // src/actions/confirmEntities.ts
3875
- import z21 from "zod";
3876
- var confirmEntitiesSchema = z21.object({
3383
+ import z24 from "zod";
3384
+ var confirmEntitiesSchema = z24.object({
3877
3385
  // Final detection — the focused re-run may supersede project-scan's.
3878
3386
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3879
3387
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3931,27 +3439,27 @@ async function confirmEntities(ctx) {
3931
3439
  onSubmit: () => {
3932
3440
  }
3933
3441
  });
3934
- const confirmed3 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3935
- if (confirmed3.length === 0) {
3442
+ const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3443
+ if (confirmed2.length === 0) {
3936
3444
  throw new Error("User cancelled entity selection \u2014 analysis halted.");
3937
3445
  }
3938
- ctx.setUserInput("confirmedEntities", confirmed3);
3446
+ ctx.setUserInput("confirmedEntities", confirmed2);
3939
3447
  track("AI Wizard Entities Confirmed", {
3940
- entities: toEntitySummary(confirmed3)
3448
+ entities: toEntitySummary(confirmed2)
3941
3449
  });
3942
- return { ingestionAnalysis: entities, confirmedEntities: confirmed3 };
3450
+ return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
3943
3451
  }
3944
3452
 
3945
3453
  // src/actions/review.ts
3946
- import { z as z22 } from "zod";
3947
- var reviewSchema = z22.object({
3454
+ import { z as z25 } from "zod";
3455
+ var reviewSchema = z25.object({
3948
3456
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3949
3457
  // not one entry per workflow step — a step's raw output can be a long,
3950
3458
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3951
3459
  // that 1:1 is what made the old per-step summary an unreadable wall of text.
3952
- summaryPoints: z22.array(z22.string()),
3953
- reviewPrompt: z22.string(),
3954
- nextSteps: z22.array(z22.string())
3460
+ summaryPoints: z25.array(z25.string()),
3461
+ reviewPrompt: z25.string(),
3462
+ nextSteps: z25.array(z25.string())
3955
3463
  });
3956
3464
  function formatCompletedSteps(steps) {
3957
3465
  if (!steps.length) return "(no prior steps completed)";
@@ -3963,7 +3471,7 @@ ${JSON.stringify(s.output, null, 2)}`
3963
3471
  }
3964
3472
  function formatReviewSummary(result) {
3965
3473
  const nextStepLines = result.nextSteps.map((step) => {
3966
- const isIngestCommand = step.includes("algolia-wizard/") || step.includes("AlgoliaWizardIngest");
3474
+ const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3967
3475
  const isWorktreeCommand = step.includes("/worktrees/");
3968
3476
  return {
3969
3477
  text: `\u2192 ${step}`,
@@ -4002,17 +3510,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4002
3510
  };
4003
3511
 
4004
3512
  // src/actions/implement.ts
4005
- import z24 from "zod";
3513
+ import z26 from "zod";
4006
3514
 
4007
3515
  // src/lib/worktree.ts
4008
- import { execFile } from "node:child_process";
4009
- import { existsSync as existsSync4 } from "node:fs";
4010
- import { copyFile, mkdir as mkdir6, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3516
+ import { execFile, spawn as spawn3 } from "node:child_process";
3517
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
4011
3518
  import {
4012
3519
  basename as basename2,
4013
3520
  dirname as dirname7,
4014
3521
  isAbsolute as isAbsolute2,
4015
- join as join12,
3522
+ join as join9,
4016
3523
  relative as relative2,
4017
3524
  resolve as resolve3
4018
3525
  } from "node:path";
@@ -4046,8 +3553,8 @@ async function isWorkingTreeDirty(repoRoot) {
4046
3553
  return out.trim().length > 0;
4047
3554
  }
4048
3555
  async function pruneOldWorktrees(repoRoot) {
4049
- const dir = join12(stateDir(repoRoot), "worktrees");
4050
- const stale = (await readdir4(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3556
+ const dir = join9(stateDir(repoRoot), "worktrees");
3557
+ const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
4051
3558
  for (const slug of stale) {
4052
3559
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
4053
3560
  try {
@@ -4057,7 +3564,7 @@ async function pruneOldWorktrees(repoRoot) {
4057
3564
  "worktree",
4058
3565
  "remove",
4059
3566
  "--force",
4060
- join12(dir, slug)
3567
+ join9(dir, slug)
4061
3568
  ]);
4062
3569
  await git(["-C", repoRoot, "branch", "-D", branch]);
4063
3570
  } catch (err) {
@@ -4071,55 +3578,43 @@ async function pruneOldWorktrees(repoRoot) {
4071
3578
  async function createWorktree(repoRoot) {
4072
3579
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
4073
3580
  const dirSlug = branch.replace(/\//g, "-");
4074
- const path = join12(stateDir(repoRoot), "worktrees", dirSlug);
3581
+ const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
4075
3582
  await git(["-C", repoRoot, "worktree", "prune"]);
4076
3583
  await pruneOldWorktrees(repoRoot);
4077
3584
  await mkdir6(dirname7(path), { recursive: true });
4078
3585
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
4079
3586
  return { path, branch };
4080
3587
  }
4081
- async function spawnStep(worktreePath, argv) {
4082
- const { code, output } = await runCommand(argv[0], [...argv.slice(1)], {
4083
- cwd: worktreePath,
4084
- timeoutMs: INSTALL_TIMEOUT_MS
4085
- });
4086
- return { ok: code === 0, output: output.trim() };
4087
- }
4088
- async function installWorktreeDeps(worktreePath, toolchain) {
4089
- const { profile, installSteps, packageManager } = toolchain;
4090
- const declared = packageManager.dependency.mode === "agent-declares" ? packageManager.dependency.file : void 0;
4091
- const haveSomethingToInstall = await hasProfileManifest(worktreePath, profile) || declared !== void 0 && existsSync4(join12(worktreePath, declared));
4092
- if (!haveSomethingToInstall) {
4093
- return {
4094
- ok: true,
4095
- output: `no ${profile.displayName} manifest; skipped install`
4096
- };
4097
- }
4098
- if (installSteps.length === 0) {
4099
- return {
4100
- ok: true,
4101
- output: `${profile.displayName} (${toolchain.packageManager.id}) has no wizard-run install step`
4102
- };
4103
- }
4104
- const outputs = [];
4105
- for (const step of installSteps) {
4106
- if (step.requiresFile && !existsSync4(join12(worktreePath, step.requiresFile)))
4107
- continue;
4108
- const result = await spawnStep(worktreePath, step.argv);
4109
- if (result.output) outputs.push(result.output);
4110
- if (result.ok) continue;
4111
- if (step.optional) {
4112
- logger.warn(
4113
- { step: step.argv.join(" "), output: result.output },
4114
- "installWorktreeDeps: optional install step failed; continuing"
4115
- );
4116
- continue;
4117
- }
4118
- return { ok: false, output: outputs.join("\n").trim() };
3588
+ async function installWorktreeDeps(worktreePath) {
3589
+ try {
3590
+ await readPackageJson(worktreePath);
3591
+ } catch {
3592
+ return { ok: true, output: "no package.json; skipped install" };
4119
3593
  }
4120
- return { ok: true, output: outputs.join("\n").trim() };
3594
+ const pm = await detectPackageManager(worktreePath);
3595
+ return new Promise((resolve4) => {
3596
+ let output = "";
3597
+ const child = spawn3(pm, ["install"], {
3598
+ cwd: worktreePath,
3599
+ stdio: ["ignore", "pipe", "pipe"]
3600
+ });
3601
+ child.stdout?.on("data", (d) => output += d);
3602
+ child.stderr?.on("data", (d) => output += d);
3603
+ child.on(
3604
+ "error",
3605
+ (err) => resolve4({
3606
+ ok: false,
3607
+ output: `Failed to run ${pm} install: ${err.message}`
3608
+ })
3609
+ );
3610
+ child.on(
3611
+ "close",
3612
+ (code) => resolve4({ ok: code === 0, output: output.trim() })
3613
+ );
3614
+ });
4121
3615
  }
4122
- function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
3616
+ var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3617
+ function validateIngestEntrypoint(worktreePath, entrypoint) {
4123
3618
  if (!entrypoint || entrypoint.startsWith("-")) {
4124
3619
  return {
4125
3620
  ok: false,
@@ -4134,29 +3629,18 @@ function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
4134
3629
  reason: `entrypoint "${entrypoint}" resolves outside the worktree`
4135
3630
  };
4136
3631
  }
4137
- if (allowedExtensions?.length && !allowedExtensions.some((ext) => entrypoint.endsWith(ext))) {
4138
- return {
4139
- ok: false,
4140
- reason: `entrypoint "${entrypoint}" is not one of ${allowedExtensions.join(", ")}`
4141
- };
4142
- }
4143
3632
  return { ok: true, target };
4144
3633
  }
4145
- async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
4146
- const { ingest, profile, packageManager } = toolchain;
4147
- if (ingest.kind !== "auto") {
3634
+ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3635
+ if (!INGEST_RUNTIMES.includes(runtime)) {
4148
3636
  return {
4149
3637
  ran: false,
4150
3638
  ok: false,
4151
3639
  output: "",
4152
- reason: `${profile.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
3640
+ reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
4153
3641
  };
4154
3642
  }
4155
- const validated = validateIngestEntrypoint(
4156
- worktreePath,
4157
- entrypoint,
4158
- ingest.entrypointExtensions
4159
- );
3643
+ const validated = validateIngestEntrypoint(worktreePath, entrypoint);
4160
3644
  if (!validated.ok) {
4161
3645
  return { ran: false, ok: false, output: "", reason: validated.reason };
4162
3646
  }
@@ -4177,13 +3661,29 @@ async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
4177
3661
  reason: `entrypoint "${entrypoint}" does not exist`
4178
3662
  };
4179
3663
  }
4180
- const argv = resolveIngestArgv(ingest, entrypoint);
4181
- const { code, output } = await runCommand(argv[0], argv.slice(1), {
4182
- cwd: worktreePath,
4183
- env,
4184
- timeoutMs: INGEST_TIMEOUT_MS
3664
+ return new Promise((resolveRun) => {
3665
+ let output = "";
3666
+ const child = spawn3(runtime, [entrypoint], {
3667
+ cwd: worktreePath,
3668
+ shell: false,
3669
+ stdio: ["ignore", "pipe", "pipe"],
3670
+ env: { ...process.env, ...env }
3671
+ });
3672
+ child.stdout?.on("data", (d) => output += d);
3673
+ child.stderr?.on("data", (d) => output += d);
3674
+ child.on(
3675
+ "error",
3676
+ (err) => resolveRun({
3677
+ ran: true,
3678
+ ok: false,
3679
+ output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
3680
+ })
3681
+ );
3682
+ child.on(
3683
+ "close",
3684
+ (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
3685
+ );
4185
3686
  });
4186
- return { ran: true, ok: code === 0, output: output.trim() };
4187
3687
  }
4188
3688
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
4189
3689
  const trimmed = sourcePath.trim();
@@ -4198,8 +3698,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4198
3698
  } catch {
4199
3699
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4200
3700
  }
4201
- const relPath = join12(ingestDir, basename2(source));
4202
- const dest = join12(worktreePath, relPath);
3701
+ const relPath = join9(ingestDir, basename2(source));
3702
+ const dest = join9(worktreePath, relPath);
4203
3703
  try {
4204
3704
  await mkdir6(dirname7(dest), { recursive: true });
4205
3705
  await copyFile(source, dest);
@@ -4214,11 +3714,28 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4214
3714
  function hasEnvVar(content, name) {
4215
3715
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
4216
3716
  }
3717
+ async function readEnvVar(worktreePath, name) {
3718
+ let content;
3719
+ try {
3720
+ content = await readFile7(join9(worktreePath, ".env"), "utf8");
3721
+ } catch (err) {
3722
+ if (err.code !== "ENOENT") throw err;
3723
+ return void 0;
3724
+ }
3725
+ const match = new RegExp(
3726
+ `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3727
+ "m"
3728
+ ).exec(content);
3729
+ if (!match) return void 0;
3730
+ const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3731
+ if (!value || value.startsWith("<")) return void 0;
3732
+ return value;
3733
+ }
4217
3734
  async function writeSearchEnvValues(worktreePath, vars) {
4218
- const target = join12(worktreePath, ".env");
3735
+ const target = join9(worktreePath, ".env");
4219
3736
  let existing = "";
4220
3737
  try {
4221
- existing = await readFile9(target, "utf8");
3738
+ existing = await readFile7(target, "utf8");
4222
3739
  } catch (err) {
4223
3740
  if (err.code !== "ENOENT") throw err;
4224
3741
  }
@@ -4286,178 +3803,162 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
4286
3803
  }
4287
3804
  }
4288
3805
 
4289
- // src/lib/algoliaApiKey.ts
4290
- import { z as z23 } from "zod";
4291
- var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
4292
- var apiKeySchema = z23.object({
4293
- value: z23.string().min(1),
4294
- acl: z23.array(z23.string()).default([]),
4295
- indexes: z23.array(z23.string()).default([])
4296
- });
4297
- var apiKeyListSchema = z23.object({
4298
- items: z23.array(apiKeySchema).optional(),
4299
- keys: z23.array(apiKeySchema).optional()
4300
- }).transform((o) => o.items ?? o.keys ?? []);
4301
- var createdKeySchema = z23.object({
4302
- key: z23.string().min(1).optional(),
4303
- value: z23.string().min(1).optional()
4304
- });
4305
- function canReuse(key, index) {
4306
- return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
4307
- }
4308
- async function createSearchKey(index) {
4309
- const stdout = await runAlgoliaCli([
4310
- "apikeys",
4311
- "create",
4312
- "--indices",
4313
- index,
4314
- "--acl",
4315
- "search,browse",
4316
- "--description",
4317
- `wizard search-only key for ${index}`,
4318
- "-o",
4319
- "json"
4320
- ]);
4321
- const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
4322
- const created = key ?? value;
4323
- if (!created) throw new Error("apikeys create returned no key value");
4324
- return created;
4325
- }
4326
- async function resolveSearchOnlyKey(index) {
4327
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
4328
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
4329
- if (existing) {
4330
- logger.info({ index }, "reusing existing search-only API key");
4331
- return existing;
4332
- }
4333
- logger.info({ index }, "no reusable search-only key found; creating one");
4334
- return createSearchKey(index);
4335
- }
4336
-
4337
3806
  // src/lib/algoliaDocs.ts
4338
- import { readFileSync, existsSync as existsSync5 } from "node:fs";
4339
- import { dirname as dirname8, join as join13 } from "node:path";
3807
+ import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3808
+ import { dirname as dirname8, join as join10 } from "node:path";
4340
3809
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4341
- var DOCS_SUBPATH = join13("docs", "algolia-sdk");
3810
+ var DOCS_SUBPATH = join10("docs", "algolia-sdk");
4342
3811
  function findDocsDir() {
4343
3812
  let dir = dirname8(fileURLToPath2(import.meta.url));
4344
3813
  for (; ; ) {
4345
- const candidate = join13(dir, DOCS_SUBPATH);
4346
- if (existsSync5(candidate)) return candidate;
3814
+ const candidate = join10(dir, DOCS_SUBPATH);
3815
+ if (existsSync2(candidate)) return candidate;
4347
3816
  const parent = dirname8(dir);
4348
3817
  if (parent === dir) return void 0;
4349
3818
  dir = parent;
4350
3819
  }
4351
3820
  }
4352
- function getNamedDoc(name, key) {
3821
+ function loadAlgoliaDoc(language) {
3822
+ const docsDir = findDocsDir();
3823
+ if (!docsDir) {
3824
+ logger.warn(
3825
+ "algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
3826
+ );
3827
+ return "";
3828
+ }
3829
+ const files = readdirSync(docsDir).filter((f) => f.includes(language));
3830
+ if (files.length === 0) {
3831
+ logger.warn(
3832
+ { language },
3833
+ "algoliaDocs: no SDK reference found for language; skipping"
3834
+ );
3835
+ return "";
3836
+ }
3837
+ return readFileSync(join10(docsDir, files[0]), "utf8").trim();
3838
+ }
3839
+ function getNamedDoc(name, language) {
4353
3840
  const docsDir = findDocsDir();
4354
3841
  if (!docsDir) {
4355
3842
  logger.warn("docs/algolia-sdk not found");
4356
3843
  return "";
4357
3844
  }
4358
- const file = join13(docsDir, `${name}-${key}.md`);
4359
- if (!existsSync5(file)) {
4360
- logger.warn({ name, key }, "named SDK reference not found");
3845
+ const file = join10(docsDir, `${name}-${language}.md`);
3846
+ if (!existsSync2(file)) {
3847
+ logger.warn({ name, language }, "named SDK reference not found");
4361
3848
  return "";
4362
3849
  }
4363
3850
  return readFileSync(file, "utf8").trim();
4364
3851
  }
3852
+ function getFrameworkSpecificDoc(frameworks) {
3853
+ const fw = frameworks.map((f) => f.toLowerCase());
3854
+ if (fw.includes("vue") || fw.includes("nuxt")) {
3855
+ return loadAlgoliaDoc("vue");
3856
+ }
3857
+ if (fw.includes("react") || fw.includes("next.js")) {
3858
+ return loadAlgoliaDoc("react");
3859
+ }
3860
+ if (fw.includes("angular")) {
3861
+ return loadAlgoliaDoc("angular");
3862
+ }
3863
+ return loadAlgoliaDoc("js");
3864
+ }
3865
+
3866
+ // src/lib/shell.ts
3867
+ function shellQuote(value) {
3868
+ return "'" + value.replace(/'/g, "'\\''") + "'";
3869
+ }
4365
3870
 
4366
3871
  // src/actions/implement.ts
4367
- var implementSchema = z24.object({
4368
- filesChanged: z24.array(z24.string()),
4369
- summary: z24.string(),
4370
- // Absolute path to the throwaway worktree holding the generated changes, so
4371
- // the user can open it (`cd <worktreePath>`) or inspect the diff
4372
- // (`git -C <worktreePath> status/diff`).
4373
- worktreePath: z24.string().optional(),
4374
- ingestCommand: z24.string().optional(),
4375
- // True when the user accepted the run-now prompt and the wizard executed the
4376
- // ingestion script; downstream steps use this to avoid telling the user to run
4377
- // a script that already ran.
4378
- ingestScriptRan: z24.boolean().optional(),
4379
- // Records ingested by the run-now execution, parsed from the script's
4380
- // machine-readable count line; absent when the script didn't run or emitted
4381
- // no parseable count.
4382
- ingestRecordCount: z24.number().optional(),
4383
- // Wall-clock duration of the run-now ingestion execution, in ms.
4384
- ingestDurationMs: z24.number().optional(),
4385
- ingestionSource: z24.enum(["local", "fileUpload", "generated"]),
4386
- // Suggested names/values, built from framework detection. The search agent is
4387
- // instructed to rename the prefix if it doesn't match the project's build
4388
- // tool, so the names it actually wrote can differ — treat these as hints, not
4389
- // ground truth (the agent's summary carries the final names).
4390
- searchEnvVars: z24.array(
4391
- z24.object({
4392
- name: z24.string(),
4393
- value: z24.string()
3872
+ var implementSchema = z26.object({
3873
+ filesChanged: z26.array(z26.string()),
3874
+ summary: z26.string(),
3875
+ worktreePath: z26.string().optional(),
3876
+ ingestCommand: z26.string().optional(),
3877
+ ingestScriptRan: z26.boolean().optional(),
3878
+ ingestRecordCount: z26.number().optional(),
3879
+ ingestDurationMs: z26.number().optional(),
3880
+ ingestionSource: z26.enum(["local", "fileUpload", "generated"]),
3881
+ searchEnvVars: z26.array(
3882
+ z26.object({
3883
+ name: z26.string(),
3884
+ value: z26.string()
4394
3885
  })
4395
3886
  ).optional()
4396
3887
  });
4397
- var implementationOutputSchema = z24.object({
4398
- summary: z24.string(),
4399
- // Ingestion only: the script the wizard should run, as a bare path — never a
4400
- // command string, and never the interpreter. The command comes from the
4401
- // resolved language toolchain (a registry constant); this path is validated to
4402
- // a worktree-relative file with a runnable extension and substituted into it.
4403
- // So the agent contributes no part of the command that gets executed.
4404
- entrypoint: z24.string().optional()
3888
+ var implementationOutputSchema = z26.object({
3889
+ summary: z26.string(),
3890
+ // Ingestion only: a structured pair the wizard turns into an argv, never a
3891
+ // free-form command string. `runtime` is allowlisted and `entrypoint` is
3892
+ // validated worktree-relative, so the agent cannot inject extra commands.
3893
+ runtime: z26.enum(INGEST_RUNTIMES).optional(),
3894
+ entrypoint: z26.string().optional()
4405
3895
  });
4406
- var verificationOutputSchema = z24.object({
4407
- summary: z24.string(),
4408
- sufficient: z24.boolean(),
4409
- additionalInstructions: z24.string().optional()
3896
+ var verificationOutputSchema = z26.object({
3897
+ summary: z26.string(),
3898
+ sufficient: z26.boolean(),
3899
+ additionalInstructions: z26.string().optional()
4410
3900
  });
4411
3901
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
4412
3902
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
4413
- function buildSearchEnvVars(language, strategy, appId, searchKey) {
4414
- const prefix = publicEnvPrefix(
4415
- language.frameworks.map((framework) => framework.name),
4416
- strategy
3903
+ var INGEST_DIR = ".algolia-wizard";
3904
+ function detectUiFramework(language) {
3905
+ const names = language.frameworks.map((f) => f.name.toLowerCase());
3906
+ if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3907
+ if (names.some((n) => n.includes("react") || n.includes("next")))
3908
+ return "React";
3909
+ if (names.some((n) => n.includes("angular"))) return "Angular";
3910
+ return "JavaScript";
3911
+ }
3912
+ function frameworksForDoc(framework) {
3913
+ switch (framework) {
3914
+ case "React":
3915
+ return ["react"];
3916
+ case "Vue":
3917
+ return ["vue"];
3918
+ case "Angular":
3919
+ return ["angular"];
3920
+ case "JavaScript":
3921
+ return [];
3922
+ }
3923
+ }
3924
+ function publicEnvPrefix(language) {
3925
+ const frameworkNames = language.frameworks.map(
3926
+ (framework) => framework.name.toLowerCase()
4417
3927
  );
3928
+ if (frameworkNames.some((name) => name.includes("next"))) {
3929
+ return "NEXT_PUBLIC_";
3930
+ }
3931
+ if (frameworkNames.some((name) => name.includes("nuxt"))) {
3932
+ return "NUXT_PUBLIC_";
3933
+ }
3934
+ if (frameworkNames.some((name) => name.includes("astro"))) {
3935
+ return "PUBLIC_";
3936
+ }
3937
+ if (frameworkNames.some((name) => name.includes("vite"))) {
3938
+ return "VITE_";
3939
+ }
3940
+ return "PUBLIC_";
3941
+ }
3942
+ var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
3943
+ var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
3944
+ function appIdVar(language) {
3945
+ return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
3946
+ }
3947
+ function searchKeyVar(language) {
3948
+ return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3949
+ }
3950
+ function searchEnvVars(language, appId, searchKey) {
4418
3951
  return [
4419
3952
  {
4420
- name: `${prefix}ALGOLIA_APP_ID`,
3953
+ name: appIdVar(language),
4421
3954
  value: appId ?? "<your-algolia-app-id>"
4422
3955
  },
4423
3956
  {
4424
- name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
3957
+ name: searchKeyVar(language),
4425
3958
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4426
3959
  }
4427
3960
  ];
4428
3961
  }
4429
- async function resolveIngestionProfile(ctx, language, repoRoot) {
4430
- const { candidates, confirmed: confirmed3, onDisk } = await pickIngestionCandidates(
4431
- repoRoot,
4432
- language.languages.map((l) => l.name)
4433
- );
4434
- if (candidates.length === 0) {
4435
- const chosen = confirmed3[0] ?? onDisk[0] ?? LANGUAGE_PROFILES[DEFAULT_LANGUAGE_ID];
4436
- logger.warn(
4437
- {
4438
- confirmed: language.languages.map((l) => l.name),
4439
- onDisk: onDisk.map((p) => p.id),
4440
- chosen: chosen.id
4441
- },
4442
- "implement: no confirmed language matched a manifest on disk; falling back"
4443
- );
4444
- return chosen;
4445
- }
4446
- if (candidates.length === 1) return candidates[0];
4447
- const backends = candidates.filter(isBackendLanguage);
4448
- if (backends.length === 1) return backends[0];
4449
- if (backends.length === 0) return candidates[0];
4450
- if (isBackendLanguage(candidates[0])) return candidates[0];
4451
- const options = backends.map((p) => p.displayName);
4452
- const selection = await ctx.requestUserInput({
4453
- prompt: "Which language should the ingestion script use?",
4454
- promptType: "multipleChoice",
4455
- options,
4456
- defaultSelectedIndex: 0
4457
- });
4458
- const picked = typeof selection === "string" ? backends.find((p) => p.displayName === selection) : void 0;
4459
- return picked ?? backends[0];
4460
- }
4461
3962
  function baseInstructions(input) {
4462
3963
  return [
4463
3964
  `Target Algolia index: ${input.targetIndex}`,
@@ -4474,9 +3975,6 @@ function sourceSpecificInstructions(input) {
4474
3975
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
4475
3976
  ],
4476
3977
  fileUpload: [
4477
- // The wizard already copied the developer's file into the worktree at this
4478
- // exact path, so the agent must read it directly — never search for or
4479
- // substitute another file.
4480
3978
  `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
4481
3979
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
4482
3980
  "Map parsed columns/fields to the confirmed entity attributes.",
@@ -4485,71 +3983,59 @@ function sourceSpecificInstructions(input) {
4485
3983
  generated: [
4486
3984
  "No real data source exists; use sample records for each confirmed entity.",
4487
3985
  "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.",
4488
- "In the script, read and parse each returned JSON file path at runtime using the idiomatic file read for the language you are writing in (the SDK reference above shows one) instead of inlining the records as literals.",
3986
+ "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.",
4489
3987
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4490
3988
  ]
4491
3989
  };
4492
3990
  return byLine[input.ingestionSource];
4493
3991
  }
4494
3992
  function ingestionInstructions(input) {
4495
- const { ingestionProfile: profile, toolchain } = input;
4496
- const { ingest } = toolchain;
4497
- const extensions = ingest.entrypointExtensions.join(", ");
4498
- const runInstruction = ingest.kind === "auto" ? `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard runs it with \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so it must be a plain path with no flags or arguments and must run as-is under that command.` : `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard does NOT run ${profile.displayName} ${toolchain.packageManager.id} projects itself \u2014 it tells the developer to run \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so also add whatever build configuration that command needs${ingest.kind === "manual" && ingest.requiresBuildTask ? `, including a "${ingest.requiresBuildTask}" task in "${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"}" that runs the script` : ""}.`;
4499
3993
  return [
4500
3994
  ...input.confirmed && input.confirmed.length ? [
4501
- `Write the ingestion script in ${profile.displayName}, at "${ingestScriptDir(profile)}/" in the repo.`,
3995
+ `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
4502
3996
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
4503
- `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. ${profile.envReadInstruction} The wizard sets these when it runs the script.`,
4504
- `Use the official Algolia ${profile.displayName} client (${profile.sdk.packageName}). Do not use the raw HTTP API, and do not use a client for another language.`,
3997
+ `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.`,
3998
+ "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4505
3999
  "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.",
4506
- getNamedDoc("save-records", profile.sdk.docKey),
4507
- dependencyInstruction(toolchain),
4000
+ getNamedDoc("save-records", "js"),
4001
+ 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4508
4002
  "The summary should be extremely concise.",
4509
- runInstruction,
4003
+ `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.`,
4510
4004
  ...sourceSpecificInstructions(input)
4511
4005
  ] : []
4512
4006
  ];
4513
4007
  }
4514
4008
  function searchInstructions(input) {
4515
- const doc = getNamedDoc(
4516
- "instantsearch-setup",
4517
- searchDocKey(input.searchStrategy)
4518
- );
4519
- const isTemplate = input.searchStrategy === "cdn-template";
4520
- const placement = isTemplate ? input.searchLocation ? `Add the search UI to the server-rendered template at "${input.searchLocation}" \u2014 ideally a shared layout, so it is reachable across the app.` : `This project has no shared template to host the UI, so create a standalone page at "${input.ingestDir}/search-demo.html" the developer can open directly, and add a TODO explaining how to move the snippet into their own layout.` : `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app.`;
4009
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4521
4010
  return [
4522
4011
  "Implement an in-app Algolia search experience.",
4523
- `Build the search UI for ${describeSearchTarget(input.searchStrategy, input.frameworkName)}.`,
4524
- "Follow the Algolia reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4012
+ `Build the search UI for ${input.uiFramework}.`,
4013
+ "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4525
4014
  doc,
4526
- placement,
4527
- `It needs at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
4528
- isTemplate ? "Load InstantSearch from a CDN with script tags as shown in the reference. Do not add JavaScript package dependencies, a bundler, or a build step." : 'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
4529
- isTemplate ? "Read the App ID and search-only API key from server-side configuration/environment and render them into the page (e.g. as data- attributes the script reads); never hardcode them, and never put a write/admin key in HTML." : "Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
4530
- // appId always resolves (loadActiveProfile throws otherwise); only the
4531
- // search-only key is best-effort and can fall back to a placeholder.
4532
- `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
4533
- // Names are fixed, not the agent's to rename: the wizard writes the
4534
- // resolved app id / search-only key into ".env" under these exact names
4535
- // right after this step, so a renamed prefix here would leave the code
4015
+ `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.`,
4016
+ // The key is provisioned only after verification passes, so the agent never
4017
+ // sees one. It must also leave .env alone: the wizard reads that file to
4018
+ // decide whether a key already exists, and an agent-invented value there
4019
+ // would be reused as if it were real.
4020
+ `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.`,
4021
+ // Not the agent's to rename: the wizard writes these exact names into
4022
+ // ".env" right after this step, so a renamed prefix would leave the code
4536
4023
  // reading a var the wizard never wrote.
4537
- `Use exactly these env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4024
+ `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4025
+ "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.",
4026
+ '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.',
4538
4027
  "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."
4539
4028
  ];
4540
4029
  }
4541
4030
  function verificationInstructions(input) {
4542
- const protectedDirs = [
4543
- .../* @__PURE__ */ new Set([input.ingestDir, ingestScriptDir(input.ingestionProfile)])
4544
- ];
4545
4031
  return [
4546
4032
  "Verify the Algolia implementation changes in the current worktree.",
4547
4033
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
4548
- `Call verifyImplementation at least once; it runs the mechanical checks available for this repo's languages (${input.verificationLanguages.join(", ")}) and returns per-check results plus an aggregate ok.`,
4034
+ "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
4549
4035
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
4550
4036
  "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.",
4551
4037
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
4552
- `Do not modify ${protectedDirs.map((dir) => `"${dir}/"`).join(" or ")} unless verifyImplementation reports an actionable issue in its files.`,
4038
+ `Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
4553
4039
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
4554
4040
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
4555
4041
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -4558,17 +4044,14 @@ function verificationInstructions(input) {
4558
4044
  var IMPLEMENT_CONFIG = {
4559
4045
  ingestion: {
4560
4046
  title: "Algolia ingestion",
4561
- label: "Ingestion",
4562
4047
  buildInstructions: ingestionInstructions
4563
4048
  },
4564
4049
  search: {
4565
4050
  title: "Algolia search",
4566
- label: "Search",
4567
4051
  buildInstructions: searchInstructions
4568
4052
  },
4569
4053
  verification: {
4570
4054
  title: "Algolia verification",
4571
- label: "Verification",
4572
4055
  buildInstructions: verificationInstructions
4573
4056
  }
4574
4057
  };
@@ -4600,10 +4083,11 @@ function buildAgentInstructions(useCase, input, extraInstructions = []) {
4600
4083
  ];
4601
4084
  }
4602
4085
  function formatSummary(useCase, summary) {
4603
- return `${IMPLEMENT_CONFIG[useCase].label}: ${summary}`;
4086
+ const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
4087
+ return `${label}: ${summary}`;
4604
4088
  }
4605
- function buildIngestCommand(worktree, toolchain, entrypoint) {
4606
- return `cd ${shellQuote(worktree)} && ${describeIngestCommand(toolchain.ingest, entrypoint)}`;
4089
+ function buildIngestCommand(worktree, runtime, entrypoint) {
4090
+ return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4607
4091
  }
4608
4092
  function parseIngestRecordCount(output) {
4609
4093
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
@@ -4680,25 +4164,17 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4680
4164
  }
4681
4165
  }
4682
4166
  const targetIndex = selected?.selection;
4167
+ useWizard.getState().setTargetIndex(targetIndex ?? null);
4683
4168
  await assertGitRepoWithHead(repoRoot);
4684
4169
  if (await isWorkingTreeDirty(repoRoot)) {
4685
4170
  await confirmDirtyWorkingTree(ctx, repoRoot);
4686
4171
  }
4687
4172
  const normalized = normalizeFindingPaths(findings);
4688
- const confirmed3 = normalized.confirmedEntities;
4173
+ const confirmed2 = normalized.confirmedEntities;
4689
4174
  const searchLocation = normalized.searchImplementationAnalysis;
4690
4175
  let appId;
4691
- let searchKey;
4692
4176
  if (useCases.includes("search")) {
4693
- appId = (await loadActiveProfile()).appId;
4694
- try {
4695
- searchKey = await resolveSearchOnlyKey(targetIndex);
4696
- } catch (err) {
4697
- logger.warn(
4698
- { err: err.message },
4699
- "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
4700
- );
4701
- }
4177
+ appId = (await requireApplication()).id;
4702
4178
  }
4703
4179
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
4704
4180
  try {
@@ -4723,66 +4199,48 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4723
4199
  );
4724
4200
  }
4725
4201
  }
4726
- const ingestionProfile = await resolveIngestionProfile(
4727
- ctx,
4728
- language,
4729
- worktree
4730
- );
4731
- const toolchain = await resolveToolchain(worktree, ingestionProfile);
4732
- const verificationLanguages = [
4733
- .../* @__PURE__ */ new Set([
4734
- ingestionProfile.id,
4735
- ...(await detectProfilesFromManifests(worktree)).map((p) => p.id)
4736
- ])
4737
- ];
4738
- const frameworkName = language.frameworks[0]?.name;
4739
- const searchStrategy = resolveSearchStrategy(
4740
- frameworkName,
4741
- verificationLanguages.includes(JAVASCRIPT)
4742
- );
4743
- logger.info(
4744
- {
4745
- language: ingestionProfile.id,
4746
- packageManager: toolchain.packageManager.id,
4747
- ingest: toolchain.ingest.kind,
4748
- framework: frameworkName,
4749
- searchStrategy
4750
- },
4751
- "implement: resolved ingestion toolchain and search strategy"
4752
- );
4753
4202
  const input = {
4754
4203
  findings: normalized,
4755
- confirmed: confirmed3,
4204
+ confirmed: confirmed2,
4756
4205
  searchLocation,
4757
4206
  targetIndex,
4758
4207
  language,
4759
4208
  appId,
4760
- searchKey,
4761
- searchEnvVars: buildSearchEnvVars(
4762
- language,
4763
- searchStrategy,
4764
- appId,
4765
- searchKey
4766
- ),
4209
+ // Names only: the search-only key is provisioned after verification, so
4210
+ // every value here is still a placeholder when the agent reads them.
4211
+ searchEnvVars: searchEnvVars(language, appId),
4767
4212
  ingestDir: INGEST_DIR,
4768
4213
  ingestionSource,
4769
4214
  uploadFilePath,
4770
- searchStrategy,
4771
- frameworkName,
4772
- ingestionProfile,
4773
- toolchain,
4774
- verificationLanguages
4215
+ uiFramework: detectUiFramework(language)
4775
4216
  };
4776
- const searchToolchain = !bundlesJavaScript(searchStrategy) ? void 0 : ingestionProfile.id === JAVASCRIPT ? toolchain : await resolveToolchain(worktree, LANGUAGE_PROFILES[JAVASCRIPT]);
4777
- const toolchainForUseCase = (useCase) => useCase === "search" ? searchToolchain : toolchain;
4778
4217
  const summaries = [];
4779
4218
  if (uploadWarning) summaries.push(uploadWarning);
4219
+ let envSearchKey;
4220
+ let envAppIdMismatch = false;
4221
+ if (useCases.includes("search") && appId) {
4222
+ const envAppId = await readEnvVar(worktree, appIdVar(language));
4223
+ if (envAppId === appId) {
4224
+ envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
4225
+ } else if (envAppId) {
4226
+ envAppIdMismatch = true;
4227
+ summaries.push(
4228
+ `\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.`
4229
+ );
4230
+ logger.warn(
4231
+ { envAppId, appId },
4232
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4233
+ );
4234
+ }
4235
+ }
4236
+ let finalSearchEnvVars = input.searchEnvVars;
4780
4237
  let agentRuns = 0;
4238
+ let ingestRuntime;
4781
4239
  let ingestEntrypoint;
4782
4240
  let ingestScriptRan = false;
4783
4241
  let ingestRecordCount;
4784
4242
  let ingestDurationMs;
4785
- const failedInstalls = /* @__PURE__ */ new Set();
4243
+ let installFailed = false;
4786
4244
  let ingestOutcomeMessage;
4787
4245
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4788
4246
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -4796,19 +4254,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4796
4254
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4797
4255
  outputSchema: implementationOutputSchema
4798
4256
  });
4799
- const useCaseToolchain = toolchainForUseCase(currentUseCase);
4800
- if (!useCaseToolchain) return result;
4801
4257
  ctx.notify({
4802
4258
  messages: [`Installing dependencies for ${currentUseCase}\u2026`]
4803
4259
  });
4804
4260
  const installLogId = ctx.logStart("installWorktreeDeps", {
4805
- useCase: currentUseCase,
4806
- language: useCaseToolchain.profile.id
4261
+ useCase: currentUseCase
4807
4262
  });
4808
- const install = await installWorktreeDeps(worktree, useCaseToolchain);
4263
+ const install = await installWorktreeDeps(worktree);
4809
4264
  ctx.logEnd(installLogId, install.ok ? "success" : "error");
4810
4265
  if (!install.ok) {
4811
- failedInstalls.add(useCaseToolchain.profile.displayName);
4266
+ installFailed = true;
4812
4267
  logger.warn(
4813
4268
  { useCase: currentUseCase, output: install.output },
4814
4269
  "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
@@ -4822,16 +4277,15 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4822
4277
  return runAgent({
4823
4278
  instructions: buildAgentInstructions("verification", input),
4824
4279
  tools: toolsForUseCase("verification"),
4825
- outputSchema: verificationOutputSchema,
4826
- // So verifyImplementation runs this repo's checks, not just npm scripts.
4827
- languages: input.verificationLanguages
4280
+ outputSchema: verificationOutputSchema
4828
4281
  });
4829
4282
  }
4830
4283
  if (useCases.includes("ingestion")) {
4831
- const { summary, entrypoint } = await runImplementationUseCase("ingestion");
4284
+ const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4832
4285
  summaries.push(formatSummary("ingestion", summary));
4286
+ ingestRuntime = runtime;
4833
4287
  ingestEntrypoint = entrypoint;
4834
- if (ingestEntrypoint && toolchain.ingest.kind === "auto" && failedInstalls.size === 0) {
4288
+ if (ingestRuntime && ingestEntrypoint && !installFailed) {
4835
4289
  ctx.clearNotices();
4836
4290
  const runNow = await ctx.requestUserInput({
4837
4291
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
@@ -4840,20 +4294,21 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4840
4294
  messages: []
4841
4295
  }) === true;
4842
4296
  if (runNow) {
4843
- const profile = await loadActiveProfile();
4297
+ const ingestApp = await requireApplication();
4298
+ const writeKey = await resolveWriteKey(targetIndex);
4844
4299
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
4845
4300
  const scriptLogId = ctx.logStart("runIngestScript", {
4846
- language: ingestionProfile.id,
4301
+ runtime: ingestRuntime,
4847
4302
  entrypoint: ingestEntrypoint
4848
4303
  });
4849
4304
  const startedAt = Date.now();
4850
4305
  const run2 = await runIngestScript(
4851
4306
  worktree,
4852
- toolchain,
4307
+ ingestRuntime,
4853
4308
  ingestEntrypoint,
4854
4309
  {
4855
- [APP_ID_VAR]: profile.appId,
4856
- [API_KEY_VAR]: profile.apiKey
4310
+ [APP_ID_VAR]: ingestApp.id,
4311
+ [API_KEY_VAR]: writeKey
4857
4312
  }
4858
4313
  );
4859
4314
  ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
@@ -4863,7 +4318,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4863
4318
  ingestRecordCount = parseIngestRecordCount(run2.output);
4864
4319
  if (ingestRecordCount != null) {
4865
4320
  track("AI Wizard Ingest Successful", {
4866
- entity_name: confirmed3?.map((e) => e.name).join(", ") || "unknown",
4321
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4867
4322
  record_count: ingestRecordCount,
4868
4323
  duration_ms: ingestDurationMs
4869
4324
  });
@@ -4876,7 +4331,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4876
4331
  outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
4877
4332
  logger.warn(
4878
4333
  {
4879
- language: ingestionProfile.id,
4334
+ runtime: ingestRuntime,
4880
4335
  entrypoint: ingestEntrypoint,
4881
4336
  reason: run2.reason
4882
4337
  },
@@ -4899,7 +4354,7 @@ ${run2.output}` : status;
4899
4354
  outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
4900
4355
  logger.warn(
4901
4356
  {
4902
- language: ingestionProfile.id,
4357
+ runtime: ingestRuntime,
4903
4358
  entrypoint: ingestEntrypoint,
4904
4359
  output: run2.output
4905
4360
  },
@@ -4916,52 +4371,19 @@ ${run2.output}` : status;
4916
4371
  }
4917
4372
  }
4918
4373
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4919
- if (ingestEntrypoint) {
4374
+ if (ingestRuntime && ingestEntrypoint) {
4920
4375
  commandMessages.push(
4921
- `Ingestion command: ${buildIngestCommand(worktree, toolchain, ingestEntrypoint)}`
4376
+ `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4922
4377
  );
4923
- if (toolchain.ingest.kind === "manual") {
4924
- commandMessages.push(
4925
- `The wizard does not run ${ingestionProfile.displayName} ${toolchain.packageManager.id} projects \u2014 run the command above yourself to ingest.`
4926
- );
4927
- const missingTask = await missingBuildTask(worktree, toolchain);
4928
- if (missingTask) {
4929
- const warning = `\u26A0\uFE0F The command above needs a "${missingTask}" task, which is not in ${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"} \u2014 add it before running, or run the script through your IDE instead.`;
4930
- commandMessages.push(warning);
4931
- summaries.push(warning);
4932
- }
4933
- }
4934
- }
4935
- if (ingestionSource === "local") {
4936
- const limitation = localSourceLimitation(worktree, ingestionProfile);
4937
- if (limitation) {
4938
- commandMessages.push(`\u26A0\uFE0F ${limitation}`);
4939
- summaries.push(`\u26A0\uFE0F ${limitation}`);
4940
- }
4941
4378
  }
4942
4379
  await ctx.requestUserInput({
4943
- // No question being asked here, just an acknowledgement — the
4944
- // continue/decline hints below already say "continue".
4945
4380
  prompt: "",
4946
4381
  promptType: "enterToContinue",
4947
4382
  options: [],
4948
4383
  messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4949
4384
  });
4950
4385
  }
4951
- const skipSearch = useCases.includes("search") && !canScaffoldSearchUI(input.searchStrategy);
4952
- if (skipSearch) {
4953
- const target = describeSearchTarget(
4954
- input.searchStrategy,
4955
- input.frameworkName
4956
- );
4957
- summaries.push(
4958
- `Search UI skipped: the wizard can't scaffold a native search UI for ${target}. Your records are in the "${targetIndex}" index \u2014 build the UI with Algolia's mobile InstantSearch libraries (https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/ios/ for iOS, .../android for Android).`
4959
- );
4960
- track("AI Wizard Search UI Skipped", {
4961
- framework: input.frameworkName ?? "unknown"
4962
- });
4963
- }
4964
- if (useCases.includes("search") && !skipSearch) {
4386
+ if (useCases.includes("search")) {
4965
4387
  let extraInstructions = [];
4966
4388
  const preSearchFiles = new Set(await listChangedFiles(worktree));
4967
4389
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
@@ -5003,7 +4425,29 @@ ${run2.output}` : status;
5003
4425
  }
5004
4426
  extraInstructions = verificationRetryInstructions(verification);
5005
4427
  }
5006
- const resolvedSearchEnvVars = input.searchEnvVars.filter(
4428
+ let searchKey;
4429
+ let searchKeyError;
4430
+ if (appId) {
4431
+ try {
4432
+ const resolved = await resolveSearchOnlyKey(
4433
+ targetIndex,
4434
+ appId,
4435
+ envSearchKey
4436
+ );
4437
+ searchKey = resolved.key;
4438
+ summaries.push(
4439
+ 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}.`
4440
+ );
4441
+ } catch (err) {
4442
+ searchKeyError = err.message;
4443
+ logger.warn(
4444
+ { err: searchKeyError },
4445
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4446
+ );
4447
+ }
4448
+ }
4449
+ finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4450
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
5007
4451
  (v) => !v.value.startsWith("<")
5008
4452
  );
5009
4453
  if (resolvedSearchEnvVars.length > 0) {
@@ -5014,13 +4458,29 @@ ${run2.output}` : status;
5014
4458
  if (written.length > 0) {
5015
4459
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
5016
4460
  }
4461
+ const stale = [];
4462
+ for (const v of resolvedSearchEnvVars) {
4463
+ if (written.includes(v.name)) continue;
4464
+ const current = await readEnvVar(worktree, v.name);
4465
+ if (current && current !== v.value) stale.push(v);
4466
+ }
4467
+ if (stale.length > 0 && !envAppIdMismatch) {
4468
+ summaries.push(
4469
+ `\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.`
4470
+ );
4471
+ logger.warn(
4472
+ { vars: stale.map((v) => v.name) },
4473
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4474
+ );
4475
+ }
5017
4476
  }
5018
- const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4477
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
5019
4478
  (v) => v.value.startsWith("<")
5020
4479
  );
5021
4480
  if (unresolvedSearchEnvVars.length > 0) {
5022
4481
  summaries.push(
5023
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4482
+ `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.
4483
+ (searchKeyError ? ` Reason: ${searchKeyError}` : "")
5024
4484
  );
5025
4485
  }
5026
4486
  } else {
@@ -5032,9 +4492,9 @@ ${run2.output}` : status;
5032
4492
  "implement: agent reported success but no files changed in the worktree"
5033
4493
  );
5034
4494
  }
5035
- if (failedInstalls.size > 0) {
4495
+ if (installFailed) {
5036
4496
  summaries.push(
5037
- `\u26A0\uFE0F Dependency install in the worktree failed. Install the ${[...failedInstalls].join(" and ")} dependencies in the worktree before the command below, or it will fail on a missing package.`
4497
+ '\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".'
5038
4498
  );
5039
4499
  }
5040
4500
  return {
@@ -5042,17 +4502,17 @@ ${run2.output}` : status;
5042
4502
  filesChanged,
5043
4503
  summary: summaries.join("\n\n"),
5044
4504
  worktreePath: worktree,
5045
- ...useCases.includes("ingestion") && ingestEntrypoint ? {
4505
+ ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
5046
4506
  ingestCommand: buildIngestCommand(
5047
4507
  worktree,
5048
- toolchain,
4508
+ ingestRuntime,
5049
4509
  ingestEntrypoint
5050
4510
  ),
5051
4511
  ingestScriptRan,
5052
4512
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
5053
4513
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
5054
4514
  } : {},
5055
- ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4515
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
5056
4516
  };
5057
4517
  } finally {
5058
4518
  process.chdir(repoRoot);
@@ -5095,8 +4555,8 @@ var defaultWorkflow = {
5095
4555
  defineStep({
5096
4556
  id: "select-index",
5097
4557
  title: "Set up index",
5098
- outputSchema: z25.object({
5099
- selection: z25.string()
4558
+ outputSchema: z27.object({
4559
+ selection: z27.string()
5100
4560
  }),
5101
4561
  run: (ctx) => selectIndexStep(ctx)
5102
4562
  }),
@@ -5374,20 +4834,20 @@ function parseCliArgs(argv) {
5374
4834
  }
5375
4835
 
5376
4836
  // src/lib/resetState.ts
5377
- import { readdir as readdir5, rm as rm2 } from "node:fs/promises";
5378
- import { join as join14 } from "node:path";
4837
+ import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4838
+ import { join as join11 } from "node:path";
5379
4839
  var KEEP = ["wizard.log"];
5380
4840
  async function resetProjectState() {
5381
4841
  const dir = stateDir();
5382
4842
  let entries;
5383
4843
  try {
5384
- entries = await readdir5(dir);
4844
+ entries = await readdir4(dir);
5385
4845
  } catch {
5386
4846
  return { dir, removed: [] };
5387
4847
  }
5388
4848
  const targets = entries.filter((name) => !KEEP.includes(name));
5389
4849
  await Promise.all(
5390
- targets.map((name) => rm2(join14(dir, name), { recursive: true, force: true }))
4850
+ targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
5391
4851
  );
5392
4852
  return { dir, removed: targets };
5393
4853
  }
@@ -5442,31 +4902,38 @@ ${formatStepList(workflow)}`);
5442
4902
  }
5443
4903
  async function run(workflow) {
5444
4904
  const store = useWizard.getState();
5445
- let instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4905
+ const instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4906
+ await store.waitForStart();
5446
4907
  let user = await getUser();
5447
4908
  if (!user) {
5448
- await instance.waitUntilRenderFlush();
5449
- instance.cleanup();
4909
+ store.beginAuth();
5450
4910
  try {
5451
4911
  await runAuthLogin();
5452
4912
  } catch (err) {
5453
- console.error(err instanceof Error ? err.message : String(err));
4913
+ store.setError(err instanceof Error ? err.message : String(err));
4914
+ await instance.waitUntilExit();
5454
4915
  process.exit(1);
5455
4916
  }
5456
- instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4917
+ store.endAuth();
5457
4918
  user = await getUser();
5458
4919
  if (!user) {
5459
4920
  store.setError(
5460
- "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli auth login` directly."
4921
+ "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli@latest auth login` directly."
5461
4922
  );
5462
4923
  await instance.waitUntilExit();
5463
4924
  process.exit(1);
5464
4925
  }
5465
4926
  }
5466
4927
  store.setUser(user);
5467
- const profile = await loadActiveProfile();
5468
- await store.waitForStart();
5469
- runWorkflow(workflow, profile?.appId);
4928
+ let app;
4929
+ try {
4930
+ app = await ensureApplication();
4931
+ } catch (err) {
4932
+ store.setError(err instanceof Error ? err.message : String(err));
4933
+ await instance.waitUntilExit();
4934
+ process.exit(1);
4935
+ }
4936
+ runWorkflow(workflow, app.id);
5470
4937
  }
5471
4938
  var started = await startup();
5472
4939
  if (typeof started === "number") {