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

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,26 @@ 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
+ // Sign-in happens after the welcome screen's enter, so `endAuth` lands on
251
+ // 'preflight': returning to 'idle' would put the welcome screen back up and
252
+ // ask the user to confirm the run a second time.
253
+ beginAuth: () => set({ phase: "authenticating", cliOutput: [] }),
254
+ endAuth: () => set((s) => s.phase === "authenticating" ? { phase: "preflight" } : {}),
255
+ // Only meaningful from 'idle' — once the workflow is running there's nothing
256
+ // left to confirm. `homeScreen` resets so preflight shows Welcome rather than
257
+ // the Learn more sub-view.
192
258
  confirmStart: () => set(
193
259
  (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
194
260
  ),
195
- // Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
196
261
  openLearnMore: () => set({ homeScreen: "learnMore" }),
197
262
  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`).
263
+ // Resolves whether the phase left 'idle' before or after this is called.
201
264
  waitForStart: () => new Promise((resolve4) => {
202
265
  if (get().phase !== "idle") {
203
266
  resolve4();
@@ -220,15 +283,20 @@ var useWizard = create((set, get) => ({
220
283
  syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
221
284
  setActiveStep: (index) => {
222
285
  get()._clearNoticeQueue();
223
- set({ phase: "running", currentStepIndex: index, output: "", notices: [] });
286
+ set({
287
+ phase: "running",
288
+ currentStepIndex: index,
289
+ output: "",
290
+ notices: [],
291
+ cliOutput: []
292
+ });
224
293
  },
225
294
  setUser: (user) => set({ user }),
226
295
  appendToken: (text) => set((s) => ({ output: s.output + text })),
227
296
  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.
297
+ // Renders the first notice of a burst immediately, then drains later arrivals
298
+ // one per `NOTICE_INTERVAL_MS`. The timer stays armed through an empty drain
299
+ // so the cooldown covers the time since the last render, across bursts.
232
300
  pushNotice: (notice) => {
233
301
  const { notices, _noticeQueue, _noticeTimer } = get();
234
302
  if (_noticeTimer === null) {
@@ -261,6 +329,15 @@ var useWizard = create((set, get) => ({
261
329
  get()._clearNoticeQueue();
262
330
  set({ notices: [] });
263
331
  },
332
+ // Unthrottled, unlike `pushNotice`: holding these back would land output
333
+ // after the command it belongs to has already exited.
334
+ pushCliOutput: (stream, text) => set((s) => ({
335
+ cliOutput: [...s.cliOutput, { id: nanoid(), stream, text }].slice(
336
+ -CLI_OUTPUT_LIMIT
337
+ )
338
+ })),
339
+ clearCliOutput: () => set({ cliOutput: [] }),
340
+ setTargetIndex: (index) => set({ targetIndex: index }),
264
341
  logStart: (kind, name, input) => {
265
342
  const id = nanoid();
266
343
  set((s) => ({
@@ -283,9 +360,8 @@ var useWizard = create((set, get) => ({
283
360
  _resolve: resolve4
284
361
  });
285
362
  }),
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.
363
+ // Logs what the user picked, not the prompt text — that just duplicates
364
+ // on-screen content.
289
365
  submitInput: async (value) => {
290
366
  await markInteraction();
291
367
  get()._resolve?.(value);
@@ -305,6 +381,8 @@ var useWizard = create((set, get) => ({
305
381
  currentStepIndex: 0,
306
382
  output: "",
307
383
  notices: [],
384
+ cliOutput: [],
385
+ targetIndex: null,
308
386
  logs: [],
309
387
  error: null,
310
388
  inputReq: null,
@@ -313,16 +391,100 @@ var useWizard = create((set, get) => ({
313
391
  }
314
392
  }));
315
393
 
394
+ // src/ui/CliOutput.tsx
395
+ import { Box, Text, useWindowSize } from "ink";
396
+
397
+ // src/ui/theme.ts
398
+ var MARKER = {
399
+ pending: "\u25CB",
400
+ running: "\u25D0",
401
+ done: "\u2713",
402
+ error: "\u2716"
403
+ };
404
+ var BRAND = "#003DFF";
405
+ var SECONDARY = "#5468FF";
406
+ var DANGER = "#F86E7E";
407
+ var COLORS = {
408
+ brand: BRAND,
409
+ primary: "#E6EDF3",
410
+ secondary: SECONDARY,
411
+ strong: "#FFFFFF",
412
+ muted: "#8B949E",
413
+ dim: "#484F58",
414
+ highlight: { bg: "#12331C", fg: "#4ADE80" },
415
+ badge: "#E3B341",
416
+ danger: DANGER,
417
+ success: "#4ADE80",
418
+ bg: {
419
+ main: "#0B0E14",
420
+ sidebar: "#14171E"
421
+ },
422
+ border: "#30363D",
423
+ accent: "#76A0FF",
424
+ status: {
425
+ pending: "gray",
426
+ running: "#76A0FF",
427
+ done: "#4ADE80",
428
+ error: DANGER
429
+ }
430
+ };
431
+
432
+ // src/ui/CliOutput.tsx
433
+ import { jsxs } from "react/jsx-runtime";
434
+ var CLI_MARKER = "\u203A";
435
+ var RESERVED_ROWS = 16;
436
+ var MAX_ROWS = 12;
437
+ var PANEL_TEXT_WIDTH = 45;
438
+ var URL_PATTERN = /https?:\/\//;
439
+ function rowCost(text) {
440
+ return URL_PATTERN.test(text) ? Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH)) : 1;
441
+ }
442
+ function CliOutput() {
443
+ const cliOutput = useWizard((s) => s.cliOutput);
444
+ const { rows } = useWindowSize();
445
+ if (!cliOutput.length) return null;
446
+ const rowBudget = Math.min(Math.max(rows - RESERVED_ROWS, 3), MAX_ROWS);
447
+ const visible = [];
448
+ let usedRows = 0;
449
+ for (let i = cliOutput.length - 1; i >= 0; i--) {
450
+ const cost = rowCost(cliOutput[i].text);
451
+ if (usedRows + cost > rowBudget && visible.length > 0) break;
452
+ visible.unshift(cliOutput[i]);
453
+ usedRows += cost;
454
+ }
455
+ const hidden = cliOutput.length - visible.length;
456
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
457
+ hidden > 0 && /* @__PURE__ */ jsxs(Text, { color: COLORS.dim, children: [
458
+ "\u2191 ",
459
+ hidden,
460
+ " earlier line(s)"
461
+ ] }),
462
+ visible.map((line) => /* @__PURE__ */ jsxs(
463
+ Text,
464
+ {
465
+ color: line.stream === "stderr" ? COLORS.muted : COLORS.dim,
466
+ wrap: URL_PATTERN.test(line.text) ? "wrap" : "truncate",
467
+ children: [
468
+ CLI_MARKER,
469
+ " ",
470
+ line.text
471
+ ]
472
+ },
473
+ line.id
474
+ ))
475
+ ] });
476
+ }
477
+
316
478
  // src/ui/Notices.tsx
317
- import { Box as Box2, Text as Text2, useWindowSize as useWindowSize2 } from "ink";
479
+ import { Box as Box3, Text as Text3, useWindowSize as useWindowSize3 } from "ink";
318
480
  import { useEffect as useEffect2, useState as useState2 } from "react";
319
481
 
320
482
  // src/ui/Table.tsx
321
- import { Box, Text, measureElement, useWindowSize } from "ink";
483
+ import { Box as Box2, Text as Text2, measureElement, useWindowSize as useWindowSize2 } from "ink";
322
484
  import { useEffect, useRef, useState } from "react";
323
485
  import { jsx } from "react/jsx-runtime";
324
486
  function Table({ columns, rows }) {
325
- const { columns: termCols } = useWindowSize();
487
+ const { columns: termCols } = useWindowSize2();
326
488
  const ref = useRef(null);
327
489
  const [width, setWidth] = useState(0);
328
490
  useEffect(() => {
@@ -330,7 +492,7 @@ function Table({ columns, rows }) {
330
492
  }, [termCols, columns, rows]);
331
493
  if (rows.length === 0) return null;
332
494
  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}`)) });
495
+ return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text2, { wrap: "truncate", children: line }, `tbl-${i}`)) });
334
496
  }
335
497
  function formatTable(columns, rows, width) {
336
498
  const natural = columns.map(
@@ -370,48 +532,13 @@ function resize(widths, budget) {
370
532
  }
371
533
  var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, width) : `${s.slice(0, width - 1)}\u2026`;
372
534
 
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
535
  // src/ui/Notices.tsx
409
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
536
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
410
537
  var AGENT_MARKER = "\u2726";
411
- var RESERVED_ROWS = 14;
412
- var PANEL_TEXT_WIDTH = 45;
538
+ var RESERVED_ROWS2 = 14;
539
+ var PANEL_TEXT_WIDTH2 = 45;
413
540
  function messageLineCount(text) {
414
- return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH));
541
+ return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH2));
415
542
  }
416
543
  function noticeLineCount(notice) {
417
544
  const messageLines = (notice.messages ?? []).reduce((sum, m) => {
@@ -422,7 +549,7 @@ function noticeLineCount(notice) {
422
549
  return messageLines + tableLines;
423
550
  }
424
551
  function fitVisibleNotices(notices, windowRows) {
425
- const budget = Math.max(windowRows - RESERVED_ROWS, 3);
552
+ const budget = Math.max(windowRows - RESERVED_ROWS2, 3);
426
553
  let used = 0;
427
554
  let count = 0;
428
555
  for (let i = notices.length - 1; i >= 0; i--) {
@@ -455,7 +582,7 @@ function parseHex(hex) {
455
582
  }
456
583
  function Notices() {
457
584
  const notices = useWizard((s) => s.notices);
458
- const { rows: windowRows } = useWindowSize2();
585
+ const { rows: windowRows } = useWindowSize3();
459
586
  const visible = fitVisibleNotices(notices, windowRows);
460
587
  const [pulseStep, setPulseStep] = useState2(0);
461
588
  useEffect2(() => {
@@ -472,14 +599,14 @@ function Notices() {
472
599
  }, []);
473
600
  if (!visible.length) return null;
474
601
  const pulseColor = PULSE_COLORS[pulseStep];
475
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
602
+ return /* @__PURE__ */ jsx2(Box3, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
476
603
  const isLatest = i === visible.length - 1;
477
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
604
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
478
605
  notice.messages?.map((m, j) => {
479
606
  const line = typeof m === "string" ? { text: m } : m;
480
607
  const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
481
- return /* @__PURE__ */ jsxs(
482
- Text2,
608
+ return /* @__PURE__ */ jsxs2(
609
+ Text3,
483
610
  {
484
611
  color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
485
612
  bold: line.bold,
@@ -497,41 +624,42 @@ function Notices() {
497
624
  }
498
625
 
499
626
  // src/ui/PromptInput.tsx
500
- import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
627
+ import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
501
628
  import TextInput from "ink-text-input";
502
- import { useState as useState4 } from "react";
629
+ import { useState as useState5 } from "react";
503
630
 
504
631
  // 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";
632
+ import { Box as Box4, Text as Text4 } from "ink";
633
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
507
634
  function NextAction({
508
635
  action,
509
636
  keyHint,
510
637
  hierarchy = "primary"
511
638
  }) {
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 })
639
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
640
+ hierarchy === "primary" && /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `> ${action}` }),
641
+ hierarchy === "secondary" && /* @__PURE__ */ jsxs3(Fragment, { children: [
642
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `>` }),
643
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, bold: true, children: action })
517
644
  ] }),
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: `]` })
645
+ /* @__PURE__ */ jsxs3(Box4, { children: [
646
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: "press " }),
647
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `[` }),
648
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, children: keyHint }),
649
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `]` })
523
650
  ] })
524
651
  ] });
525
652
  }
526
653
 
527
654
  // 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;
655
+ import { Box as Box6, Text as Text6, useInput, useWindowSize as useWindowSize5 } from "ink";
656
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
657
+
658
+ // src/ui/ScrollView.tsx
659
+ import { Box as Box5, Text as Text5, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
660
+ import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
661
+ import { jsxs as jsxs4 } from "react/jsx-runtime";
662
+ var INDICATOR_ROWS = 2;
535
663
  function fittedWidth(node, columns) {
536
664
  let left = 0;
537
665
  for (let n = node; n; n = n.parentNode) {
@@ -539,6 +667,89 @@ function fittedWidth(node, columns) {
539
667
  }
540
668
  return Math.max(Math.min(measureElement2(node).width, columns - left), 0);
541
669
  }
670
+ function useScrollWindow({
671
+ itemCount,
672
+ rowHeight = 1,
673
+ followBottom = false
674
+ }) {
675
+ const viewportRef = useRef2(null);
676
+ const { columns } = useWindowSize4();
677
+ const [size, setSize] = useState3(
678
+ null
679
+ );
680
+ useLayoutEffect(() => {
681
+ if (!viewportRef.current) return;
682
+ const width = fittedWidth(viewportRef.current, columns);
683
+ const { height } = measureElement2(viewportRef.current);
684
+ setSize(
685
+ (prev) => prev?.width === width && prev.height === height ? prev : { width, height }
686
+ );
687
+ });
688
+ const capacity = size === null || itemCount * rowHeight <= size.height ? itemCount : Math.max(Math.floor((size.height - INDICATOR_ROWS) / rowHeight), 1);
689
+ const maxOffset = Math.max(itemCount - capacity, 0);
690
+ const [offset, setOffset] = useState3(0);
691
+ const prevMaxOffsetRef = useRef2(0);
692
+ useLayoutEffect(() => {
693
+ const wasAtBottom = offset >= prevMaxOffsetRef.current;
694
+ prevMaxOffsetRef.current = maxOffset;
695
+ setOffset(
696
+ (o) => followBottom && wasAtBottom ? maxOffset : Math.min(o, maxOffset)
697
+ );
698
+ }, [maxOffset, followBottom]);
699
+ const scrollBy = useCallback(
700
+ (delta) => {
701
+ setOffset((o) => Math.min(Math.max(o + delta, 0), maxOffset));
702
+ },
703
+ [maxOffset]
704
+ );
705
+ const revealIndex = useCallback(
706
+ (index) => {
707
+ setOffset((o) => {
708
+ if (index < o) return index;
709
+ if (index >= o + capacity) {
710
+ return Math.min(index - capacity + 1, maxOffset);
711
+ }
712
+ return o;
713
+ });
714
+ },
715
+ [capacity, maxOffset]
716
+ );
717
+ const visibleCount = Math.min(capacity, Math.max(itemCount - offset, 0));
718
+ return {
719
+ viewportRef,
720
+ width: size?.width ?? columns,
721
+ offset,
722
+ capacity,
723
+ maxOffset,
724
+ hiddenAbove: Math.min(offset, itemCount),
725
+ hiddenBelow: Math.max(itemCount - offset - visibleCount, 0),
726
+ scrollBy,
727
+ revealIndex
728
+ };
729
+ }
730
+ function ScrollView({ scroll, children }) {
731
+ return /* @__PURE__ */ jsxs4(Box5, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
732
+ scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
733
+ "\u2191 ",
734
+ scroll.hiddenAbove,
735
+ " more"
736
+ ] }),
737
+ children,
738
+ scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
739
+ "\u2193 ",
740
+ scroll.hiddenBelow,
741
+ " more"
742
+ ] })
743
+ ] });
744
+ }
745
+
746
+ // src/ui/SelectPrompt.tsx
747
+ import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
748
+ var CANCEL = "cancel";
749
+ var ARROW_WIDTH = 4;
750
+ var COLUMN_GAP = 2;
751
+ var BAR_PADDING = 2;
752
+ var ROW_HEIGHT = 3;
542
753
  function SelectPrompt({
543
754
  options,
544
755
  onSelect,
@@ -552,10 +763,10 @@ function SelectPrompt({
552
763
  secondary,
553
764
  defaultSelectedIndex = 0
554
765
  }) {
555
- const [index, setIndex] = useState3(
766
+ const [index, setIndex] = useState4(
556
767
  () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
557
768
  );
558
- const [checked, setChecked] = useState3(() => /* @__PURE__ */ new Set());
769
+ const [checked, setChecked] = useState4(() => /* @__PURE__ */ new Set());
559
770
  const hasCancel = Boolean(multi || cancelable);
560
771
  const rows = hasCancel ? [...options, "Cancel"] : options;
561
772
  const cancelIndex = hasCancel ? options.length : -1;
@@ -563,14 +774,14 @@ function SelectPrompt({
563
774
  if (rows.length > 1) hints.push({ key: "[\u2191] [\u2193]", label: "move" });
564
775
  if (multi) hints.push({ key: "[space]", label: "select" });
565
776
  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]);
777
+ const containerRef = useRef3(null);
778
+ const { columns } = useWindowSize5();
779
+ const [width, setWidth] = useState4(columns);
780
+ useLayoutEffect2(() => {
781
+ if (!containerRef.current) return;
782
+ const measured = fittedWidth(containerRef.current, columns);
783
+ setWidth((prev) => prev === measured ? prev : measured);
784
+ });
574
785
  const inner = Math.max(width - BAR_PADDING, 0);
575
786
  const labelWidth = Math.min(
576
787
  ARROW_WIDTH + (multi ? 2 : 0) + Math.max(0, ...rows.map((opt) => opt.length)) + COLUMN_GAP,
@@ -586,6 +797,15 @@ function SelectPrompt({
586
797
  const barWidth = Math.min(labelWidth + badgeWidth + BAR_PADDING, width);
587
798
  const barLabelWidth = Math.max(barWidth - BAR_PADDING - badgeWidth, 0);
588
799
  const textWidth = inner - labelWidth;
800
+ const scroll = useScrollWindow({
801
+ itemCount: rows.length,
802
+ rowHeight: ROW_HEIGHT
803
+ });
804
+ const { revealIndex } = scroll;
805
+ useLayoutEffect2(() => {
806
+ revealIndex(index);
807
+ }, [index, revealIndex]);
808
+ const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
589
809
  useInput((input, key) => {
590
810
  if (rows.length === 0) return;
591
811
  if (key.upArrow || input === "k") {
@@ -609,62 +829,65 @@ function SelectPrompt({
609
829
  }
610
830
  }
611
831
  });
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 })
832
+ return /* @__PURE__ */ jsx4(Box6, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, width, children: [
833
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
834
+ error && /* @__PURE__ */ jsx4(Text6, { color: COLORS.danger, children: error }),
835
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
836
+ table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
837
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
838
+ question && /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: question }),
839
+ helpText && /* @__PURE__ */ jsx4(Text6, { color: COLORS.dim, children: helpText })
840
+ ] })
619
841
  ] }),
620
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: rows.map((option, i) => {
842
+ /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
843
+ const i = scroll.offset + visibleIndex;
621
844
  const highlighted = i === index;
622
845
  const isCancel = i === cancelIndex;
623
846
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
624
847
  const sec = isCancel ? void 0 : secondary?.[i];
625
848
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
626
- const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, wrap: "truncate", children: [
849
+ const label = /* @__PURE__ */ jsxs5(Text6, { color: labelColor, wrap: "truncate", children: [
627
850
  highlighted ? "\u276F " : " ",
628
851
  bullet,
629
852
  option
630
853
  ] });
631
854
  const isText = sec?.kind === "text";
632
- return /* @__PURE__ */ jsxs3(
633
- Box4,
855
+ return /* @__PURE__ */ jsxs5(
856
+ Box6,
634
857
  {
635
858
  width: isText ? "100%" : barWidth,
636
859
  paddingX: 1,
637
860
  paddingY: 1,
638
861
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
639
862
  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,
863
+ /* @__PURE__ */ jsx4(Box6, { width: isText ? labelWidth : barLabelWidth, children: label }),
864
+ isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box6, { width: textWidth, children: /* @__PURE__ */ jsx4(
865
+ Text6,
643
866
  {
644
867
  wrap: "truncate",
645
868
  color: highlighted ? COLORS.primary : COLORS.muted,
646
869
  children: sec.value
647
870
  }
648
871
  ) }),
649
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box4, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
872
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box6, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text6, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
650
873
  ]
651
874
  },
652
875
  `row-${i}`
653
876
  );
654
877
  }) }),
655
- /* @__PURE__ */ jsx4(Text4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
878
+ /* @__PURE__ */ jsx4(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text6, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs5(Text6, { children: [
656
879
  i > 0 ? " " : "",
657
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: key }),
658
- /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
880
+ /* @__PURE__ */ jsx4(Text6, { color: COLORS.primary, children: key }),
881
+ /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
659
882
  " ",
660
883
  label
661
884
  ] })
662
- ] }, label)) })
885
+ ] }, label)) }) })
663
886
  ] }) });
664
887
  }
665
888
 
666
889
  // src/ui/PromptInput.tsx
667
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
890
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
668
891
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
669
892
  function EnterToContinuePrompt({
670
893
  question,
@@ -675,10 +898,10 @@ function EnterToContinuePrompt({
675
898
  if (key.return) onDecide(true);
676
899
  else if (key.escape) onDecide(false);
677
900
  });
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: [
901
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, children: [
902
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
903
+ question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: question }),
904
+ /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
682
905
  /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
683
906
  /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
684
907
  ] })
@@ -686,13 +909,13 @@ function EnterToContinuePrompt({
686
909
  }
687
910
  function PromptInput() {
688
911
  const { phase, inputReq, submitInput } = useWizard();
689
- const [draft, setDraft] = useState4("");
912
+ const [draft, setDraft] = useState5("");
690
913
  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" }) });
914
+ return /* @__PURE__ */ jsx5(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text7, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
692
915
  }
693
916
  if (phase !== "awaitingInput" || !inputReq) return null;
694
917
  if (inputReq.promptType === "multipleChoice") {
695
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
918
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
696
919
  SelectPrompt,
697
920
  {
698
921
  question: inputReq.prompt,
@@ -709,7 +932,7 @@ function PromptInput() {
709
932
  ) });
710
933
  }
711
934
  if (inputReq.promptType === "multiSelect") {
712
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
935
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
713
936
  SelectPrompt,
714
937
  {
715
938
  multi: true,
@@ -724,7 +947,7 @@ function PromptInput() {
724
947
  ) });
725
948
  }
726
949
  if (inputReq.promptType === "notice") {
727
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
950
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
728
951
  SelectPrompt,
729
952
  {
730
953
  question: inputReq.prompt,
@@ -746,7 +969,7 @@ function PromptInput() {
746
969
  }
747
970
  if (inputReq.promptType === "acceptReject") {
748
971
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
749
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
972
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
750
973
  SelectPrompt,
751
974
  {
752
975
  question: inputReq.prompt,
@@ -757,11 +980,11 @@ function PromptInput() {
757
980
  }
758
981
  ) });
759
982
  }
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: [
983
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
984
+ inputReq.error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: inputReq.error }),
985
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
986
+ /* @__PURE__ */ jsxs6(Box7, { children: [
987
+ /* @__PURE__ */ jsxs6(Text7, { color: COLORS.primary, children: [
765
988
  inputReq.prompt,
766
989
  " "
767
990
  ] }),
@@ -783,7 +1006,7 @@ function PromptInput() {
783
1006
  // src/ui/Welcome.tsx
784
1007
  import { dirname as dirname2, join as join3 } from "node:path";
785
1008
  import { fileURLToPath } from "node:url";
786
- import { Box as Box6, Spacer, Text as Text6, useInput as useInput3, useWindowSize as useWindowSize4 } from "ink";
1009
+ import { Box as Box8, Spacer, Text as Text8, useInput as useInput3, useWindowSize as useWindowSize6 } from "ink";
787
1010
 
788
1011
  // src/ui/copy/welcome.ts
789
1012
  var sidebarItems = [
@@ -796,12 +1019,12 @@ var sidebarItems = [
796
1019
  description: "push 100 records to Algolia in seconds"
797
1020
  },
798
1021
  {
799
- title: "detect your stack",
800
- description: "React, Vue, Angular, Rails, Django, Laravel & more"
1022
+ title: "detect your framework",
1023
+ description: "React, Vue, Angular, Vanilla JS"
801
1024
  },
802
1025
  {
803
1026
  title: "scaffold a search UI",
804
- description: "a styled InstantSearch UI, wired into your app or templates"
1027
+ description: "a styled InstantSearch component, wired into your app"
805
1028
  },
806
1029
  {
807
1030
  title: "ship it",
@@ -811,27 +1034,27 @@ var sidebarItems = [
811
1034
 
812
1035
  // src/ui/Welcome.tsx
813
1036
  import Image, { InkPictureProvider } from "ink-picture";
814
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1037
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
815
1038
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
816
1039
  function SidebarItem({
817
1040
  title,
818
1041
  description
819
1042
  }) {
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 })
1043
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1044
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, children: [
1045
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.success, children: "\u2192" }),
1046
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.strong, bold: true, children: title })
824
1047
  ] }),
825
- /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", gap: 2, children: [
1048
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 2, children: [
826
1049
  /* @__PURE__ */ jsx6(Spacer, {}),
827
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: description })
1050
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: description })
828
1051
  ] })
829
1052
  ] });
830
1053
  }
831
1054
  function Welcome() {
832
1055
  const confirmStart = useWizard((s) => s.confirmStart);
833
1056
  const openLearnMore = useWizard((s) => s.openLearnMore);
834
- const { rows } = useWindowSize4();
1057
+ const { rows } = useWindowSize6();
835
1058
  useInput3((input, key) => {
836
1059
  if (key.return) confirmStart();
837
1060
  else if (input === "i") openLearnMore();
@@ -850,15 +1073,15 @@ function Welcome() {
850
1073
  if (rows < 30) {
851
1074
  layout = scales["small"];
852
1075
  }
853
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1076
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
854
1077
  /* @__PURE__ */ jsx6(
855
- Box6,
1078
+ Box8,
856
1079
  {
857
1080
  paddingY: layout.main.padding.y,
858
1081
  paddingX: layout.main.padding.x,
859
1082
  flexDirection: "column",
860
1083
  justifyContent: "center",
861
- children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 2, children: [
1084
+ children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
862
1085
  /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
863
1086
  Image,
864
1087
  {
@@ -870,16 +1093,16 @@ function Welcome() {
870
1093
  protocol: "halfBlock"
871
1094
  }
872
1095
  ) }),
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: [
1096
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1097
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
875
1098
  /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
876
1099
  /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
877
1100
  ] })
878
1101
  ] })
879
1102
  }
880
1103
  ),
881
- /* @__PURE__ */ jsxs5(
882
- Box6,
1104
+ /* @__PURE__ */ jsxs7(
1105
+ Box8,
883
1106
  {
884
1107
  backgroundColor: COLORS.bg.sidebar,
885
1108
  width: 40,
@@ -889,7 +1112,7 @@ function Welcome() {
889
1112
  flexDirection: "column",
890
1113
  justifyContent: "center",
891
1114
  children: [
892
- /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1115
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
893
1116
  sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
894
1117
  ]
895
1118
  }
@@ -899,7 +1122,7 @@ function Welcome() {
899
1122
 
900
1123
  // src/ui/LearnMore.tsx
901
1124
  import { Fragment as Fragment2 } from "react";
902
- import { Box as Box7, Text as Text7, useInput as useInput4, useWindowSize as useWindowSize5 } from "ink";
1125
+ import { Box as Box9, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize7 } from "ink";
903
1126
 
904
1127
  // src/ui/copy/learn-more.ts
905
1128
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -907,7 +1130,7 @@ var accessItems = [
907
1130
  {
908
1131
  tag: "READ",
909
1132
  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."
1133
+ description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
911
1134
  },
912
1135
  {
913
1136
  tag: "WRITE",
@@ -936,7 +1159,7 @@ var policyLinks = [
936
1159
  ];
937
1160
 
938
1161
  // src/ui/LearnMore.tsx
939
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1162
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
940
1163
  var TAG_COLORS = {
941
1164
  READ: COLORS.success,
942
1165
  WRITE: COLORS.badge,
@@ -952,25 +1175,25 @@ function NeverLine({
952
1175
  }) {
953
1176
  const used = segments.reduce((n, s) => n + s.text.length, 0);
954
1177
  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" }),
1178
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
1179
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" }),
957
1180
  " ".repeat(NEVER_BOX_PAD_X),
958
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: s.bold, children: s.text }, i)),
1181
+ segments.map((s, i) => /* @__PURE__ */ jsx7(Text9, { color: s.color, bold: s.bold, children: s.text }, i)),
959
1182
  " ".repeat(rightPad),
960
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" })
1183
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" })
961
1184
  ] });
962
1185
  }
963
1186
  function LearnMore() {
964
1187
  const confirmStart = useWizard((s) => s.confirmStart);
965
1188
  const backToHome = useWizard((s) => s.backToHome);
966
- const { columns } = useWindowSize5();
1189
+ const { columns } = useWindowSize7();
967
1190
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
968
1191
  useInput4((_input, key) => {
969
1192
  if (key.escape) backToHome();
970
1193
  else if (key.return) confirmStart();
971
1194
  });
972
- return /* @__PURE__ */ jsxs6(
973
- Box7,
1195
+ return /* @__PURE__ */ jsxs8(
1196
+ Box9,
974
1197
  {
975
1198
  flexDirection: "column",
976
1199
  paddingX: PADDING_X,
@@ -978,20 +1201,20 @@ function LearnMore() {
978
1201
  width: "100%",
979
1202
  gap: 1,
980
1203
  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}` })
1204
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1205
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: accessIntro }),
1206
+ /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginTop: 1, children: [
1207
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1208
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1209
+ /* @__PURE__ */ jsx7(Box9, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text9, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1210
+ /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { children: [
1211
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: item.title }),
1212
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
990
1213
  ] }) })
991
1214
  ] })
992
1215
  ] }, 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` }),
1216
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
1217
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
995
1218
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
996
1219
  /* @__PURE__ */ jsx7(
997
1220
  NeverLine,
@@ -1000,7 +1223,7 @@ function LearnMore() {
1000
1223
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1001
1224
  }
1002
1225
  ),
1003
- neverItems.map((item) => /* @__PURE__ */ jsxs6(Fragment2, { children: [
1226
+ neverItems.map((item) => /* @__PURE__ */ jsxs8(Fragment2, { children: [
1004
1227
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1005
1228
  /* @__PURE__ */ jsx7(
1006
1229
  NeverLine,
@@ -1015,23 +1238,23 @@ function LearnMore() {
1015
1238
  )
1016
1239
  ] }, item)),
1017
1240
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1018
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1241
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1019
1242
  ] }),
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 })
1243
+ /* @__PURE__ */ jsx7(Box9, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1244
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1245
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.accent, children: link.url })
1023
1246
  ] }, 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" })
1247
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1248
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1249
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1250
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "esc" }),
1251
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "] back" })
1029
1252
  ] }),
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" })
1253
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1254
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1255
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "enter" }),
1256
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "]" }),
1257
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, bold: true, children: "start wizard" })
1035
1258
  ] })
1036
1259
  ] })
1037
1260
  ]
@@ -1040,10 +1263,10 @@ function LearnMore() {
1040
1263
  }
1041
1264
 
1042
1265
  // src/ui/Sidebar.tsx
1043
- import { Box as Box10, Text as Text10 } from "ink";
1266
+ import { Box as Box12, Text as Text12 } from "ink";
1044
1267
 
1045
1268
  // src/ui/Steps.tsx
1046
- import { Box as Box8, Text as Text8 } from "ink";
1269
+ import { Box as Box10, Text as Text10 } from "ink";
1047
1270
  import Spinner from "ink-spinner";
1048
1271
 
1049
1272
  // src/core/persistence.ts
@@ -1072,11 +1295,11 @@ async function clearWorkflowState(workflowId) {
1072
1295
  }
1073
1296
 
1074
1297
  // src/ui/Steps.tsx
1075
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1298
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1076
1299
  function Steps() {
1077
1300
  const { steps } = useWizard();
1078
1301
  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: [
1302
+ 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
1303
  s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1081
1304
  " ",
1082
1305
  s.title
@@ -1086,7 +1309,7 @@ function CurrentStep() {
1086
1309
  const { steps } = useWizard();
1087
1310
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1088
1311
  if (!currentStep) return null;
1089
- return /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status.running, children: [
1312
+ return /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.running, children: [
1090
1313
  /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1091
1314
  " ",
1092
1315
  ` ${currentStep.title}`
@@ -1094,19 +1317,19 @@ function CurrentStep() {
1094
1317
  }
1095
1318
 
1096
1319
  // 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";
1320
+ import { Box as Box11, Text as Text11 } from "ink";
1321
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1099
1322
  function Progress() {
1100
1323
  const { steps, currentStepIndex } = useWizard();
1101
1324
  const visibleSteps = steps.filter(isStepVisible);
1102
1325
  if (visibleSteps.length === 0) return null;
1103
1326
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1104
1327
  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 })
1328
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1329
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "STEP" }),
1330
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: activeStepNumber }),
1331
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: "/" }),
1332
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: visibleSteps.length })
1110
1333
  ] });
1111
1334
  }
1112
1335
 
@@ -1117,10 +1340,10 @@ var sidebarCommands = [
1117
1340
  ];
1118
1341
 
1119
1342
  // src/ui/Sidebar.tsx
1120
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1343
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1121
1344
  function Sidebar() {
1122
- return /* @__PURE__ */ jsxs9(
1123
- Box10,
1345
+ return /* @__PURE__ */ jsxs11(
1346
+ Box12,
1124
1347
  {
1125
1348
  backgroundColor: "#14171E",
1126
1349
  width: 30,
@@ -1129,16 +1352,16 @@ function Sidebar() {
1129
1352
  flexDirection: "column",
1130
1353
  justifyContent: "space-between",
1131
1354
  children: [
1132
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1133
- /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "PROGRESS" }),
1355
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1356
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "PROGRESS" }),
1134
1357
  /* @__PURE__ */ jsx10(Steps, {})
1135
1358
  ] }),
1136
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1359
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1137
1360
  /* @__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 })
1361
+ /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: sidebarCommands.map((c) => {
1362
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1363
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1364
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: c.description })
1142
1365
  ] });
1143
1366
  }) })
1144
1367
  ] })
@@ -1148,12 +1371,12 @@ function Sidebar() {
1148
1371
  }
1149
1372
 
1150
1373
  // 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";
1374
+ import { Box as Box13, Text as Text13 } from "ink";
1375
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1153
1376
  function Ribbon() {
1154
1377
  const firstCommand = sidebarCommands[0];
1155
- return /* @__PURE__ */ jsxs10(
1156
- Box11,
1378
+ return /* @__PURE__ */ jsxs12(
1379
+ Box13,
1157
1380
  {
1158
1381
  backgroundColor: "#14171E",
1159
1382
  flexDirection: "row",
@@ -1163,9 +1386,9 @@ function Ribbon() {
1163
1386
  children: [
1164
1387
  /* @__PURE__ */ jsx11(Progress, {}),
1165
1388
  /* @__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 })
1389
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1390
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1391
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: firstCommand.description })
1169
1392
  ] })
1170
1393
  ]
1171
1394
  }
@@ -1176,9 +1399,8 @@ function Ribbon() {
1176
1399
  import { useState as useState6 } from "react";
1177
1400
 
1178
1401
  // 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";
1402
+ import { Box as Box14, Text as Text14, useInput as useInput5 } from "ink";
1403
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1182
1404
  var KIND_COLOR = {
1183
1405
  tool: COLORS.primary,
1184
1406
  prompt: COLORS.badge
@@ -1208,75 +1430,32 @@ function formatTimestamp(ms) {
1208
1430
  }
1209
1431
  function Logs() {
1210
1432
  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]);
1433
+ const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1239
1434
  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
- );
1435
+ if (key.upArrow) scroll.scrollBy(-1);
1436
+ else if (key.downArrow) scroll.scrollBy(1);
1244
1437
  });
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" })
1438
+ const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1439
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1440
+ logs.length === 0 && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "No logs yet." }),
1441
+ /* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
1442
+ const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1443
+ const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1444
+ const rawPreview = rawInputText(entry.input);
1445
+ const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1446
+ const gaps = (partCount - 1) * ROW_GAP;
1447
+ let budget = scroll.width - timestamp.length - durationText.length - gaps;
1448
+ const name = truncate2(entry.name, budget);
1449
+ budget -= name.length;
1450
+ const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1451
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: ROW_GAP, children: [
1452
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: timestamp }),
1453
+ /* @__PURE__ */ jsx12(Text14, { color: logNameColor(entry), wrap: "truncate", children: name }),
1454
+ preview && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, wrap: "truncate", children: preview }),
1455
+ durationText && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: durationText })
1456
+ ] }, entry.id);
1457
+ }) }),
1458
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1280
1459
  ] });
1281
1460
  }
1282
1461
 
@@ -1468,11 +1647,11 @@ function track(event, payload) {
1468
1647
  }
1469
1648
 
1470
1649
  // src/ui/App.tsx
1471
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1650
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1472
1651
  function App() {
1473
1652
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1474
1653
  const { exit } = useApp();
1475
- const { columns, rows } = useWindowSize7();
1654
+ const { columns, rows } = useWindowSize8();
1476
1655
  const [showLogs, setShowLogs] = useState6(false);
1477
1656
  const finished = phase === "done" || phase === "error";
1478
1657
  const currentStep = steps[currentStepIndex];
@@ -1485,7 +1664,7 @@ function App() {
1485
1664
  { isActive: finished }
1486
1665
  );
1487
1666
  useInput6((_input, key) => {
1488
- if (phase === "idle" || phase === "preflight") return;
1667
+ if (phase === "idle" || phase === "authenticating") return;
1489
1668
  if (key.tab) {
1490
1669
  setShowLogs(!showLogs);
1491
1670
  track("AI Wizard Interaction", {
@@ -1495,7 +1674,7 @@ function App() {
1495
1674
  });
1496
1675
  }
1497
1676
  });
1498
- const escOwnedElsewhere = phase === "idle" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1677
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1499
1678
  useInput6((_input, key) => {
1500
1679
  if (escOwnedElsewhere) return;
1501
1680
  if (key.escape) {
@@ -1508,53 +1687,71 @@ function App() {
1508
1687
  exit();
1509
1688
  }
1510
1689
  });
1511
- const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1690
+ const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1512
1691
  const flexDirection = columns > 90 ? "row" : "column";
1513
1692
  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,
1533
- {
1534
- flexDirection: "column",
1535
- paddingX: 4,
1536
- paddingY: 2,
1537
- width: showSidebar ? 70 : "100%",
1538
- flexGrow: showSidebar ? 1 : 0,
1539
- children: [
1540
- /* @__PURE__ */ jsx13(Notices, {}),
1541
- /* @__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: [
1544
- "\u2716 ",
1545
- error
1546
- ] }) })
1547
- ]
1548
- }
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
- }
1693
+ const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
1694
+ return (
1695
+ /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1696
+ the whole screen, and the scrolling throws off its cursor arithmetic —
1697
+ flicker and leftover rows. */
1698
+ /* @__PURE__ */ jsxs14(
1699
+ Box15,
1700
+ {
1701
+ backgroundColor: COLORS.bg.main,
1702
+ flexDirection: "row",
1703
+ width: columns,
1704
+ height: scrollsPastViewport ? void 0 : rows,
1705
+ overflow: scrollsPastViewport ? "visible" : "hidden",
1706
+ children: [
1707
+ mainWindowVisible && // Without a cap the scrolling lists in here grow to their content
1708
+ // instead of windowing (see `useScrollWindow`).
1709
+ /* @__PURE__ */ jsxs14(
1710
+ Box15,
1711
+ {
1712
+ flexDirection,
1713
+ width: "100%",
1714
+ maxHeight: rows,
1715
+ justifyContent: "space-between",
1716
+ children: [
1717
+ showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : (
1718
+ /* Fill the space the sidebar/ribbon leaves — width beside the
1719
+ sidebar, height above the ribbon. The height matters even
1720
+ stacked: it is what the prompt's scrolling list measures itself
1721
+ against (see SelectPrompt). */
1722
+ /* @__PURE__ */ jsxs14(
1723
+ Box15,
1724
+ {
1725
+ flexDirection: "column",
1726
+ paddingX: 4,
1727
+ paddingY: 2,
1728
+ width: showSidebar ? 70 : "100%",
1729
+ flexGrow: 1,
1730
+ children: [
1731
+ phase === "authenticating" && /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginBottom: 1, children: [
1732
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1733
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1734
+ ] }),
1735
+ /* @__PURE__ */ jsx13(CliOutput, {}),
1736
+ /* @__PURE__ */ jsx13(Notices, {}),
1737
+ /* @__PURE__ */ jsx13(PromptInput, {}),
1738
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1739
+ phase === "error" && error && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text15, { color: COLORS.status.error, children: [
1740
+ "\u2716 ",
1741
+ error
1742
+ ] }) })
1743
+ ]
1744
+ }
1745
+ )
1746
+ ),
1747
+ showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1748
+ ]
1749
+ }
1750
+ ),
1751
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1752
+ ]
1753
+ }
1754
+ )
1558
1755
  );
1559
1756
  }
1560
1757
 
@@ -1790,61 +1987,138 @@ async function runWorkflow(workflow, appId) {
1790
1987
  }
1791
1988
  }
1792
1989
 
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;
1990
+ // src/lib/algoliaApp.ts
1991
+ import { z as z4 } from "zod";
1992
+ var applicationSchema = z4.object({
1993
+ id: z4.string().min(1),
1994
+ name: z4.string().default(""),
1995
+ plan: z4.string().optional()
1996
+ });
1997
+ var listSchema = z4.array(
1998
+ z4.object({
1999
+ id: z4.string().min(1),
2000
+ name: z4.string().default(""),
2001
+ plan_label: z4.string().optional()
2002
+ }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
2003
+ );
2004
+ async function currentApplication() {
2005
+ let raw;
1806
2006
  try {
1807
- parsed = parseToml(tomlText);
2007
+ raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
1808
2008
  } catch {
1809
- return [];
2009
+ return null;
2010
+ }
2011
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2012
+ return parsed.success ? parsed.data : null;
2013
+ }
2014
+ async function requireApplication() {
2015
+ const app = await currentApplication();
2016
+ if (!app) {
2017
+ throw new Error(
2018
+ "No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
2019
+ );
2020
+ }
2021
+ return app;
2022
+ }
2023
+ async function listApplications() {
2024
+ const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
2025
+ const parsed = listSchema.safeParse(parseJson(raw));
2026
+ if (!parsed.success) {
2027
+ throw new Error("Could not read the list of Algolia applications.");
1810
2028
  }
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;
2029
+ return parsed.data;
2030
+ }
2031
+ async function selectApplication(id) {
2032
+ const raw = await runAlgoliaCli(
2033
+ ["application", "select", "--non-interactive", "--app-id", id],
2034
+ { onOutput: stderrSink }
2035
+ );
2036
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2037
+ if (!parsed.success) {
2038
+ throw new Error(
2039
+ `Selected application ${id}, but the Algolia CLI returned an unreadable result.`
2040
+ );
2041
+ }
2042
+ return parsed.data;
2043
+ }
2044
+ function parseJson(text) {
1824
2045
  try {
1825
- profiles = profilesFromConfig(await readFile3(configPath(), "utf8"));
2046
+ return JSON.parse(text);
1826
2047
  } catch {
1827
- profiles = [];
2048
+ return void 0;
1828
2049
  }
1829
- const profile = profiles[0];
1830
- if (!profile) {
2050
+ }
2051
+
2052
+ // src/lib/algoliaAppPicker.ts
2053
+ function secondaryFor(app) {
2054
+ return app.plan ? { kind: "badge", value: app.plan } : void 0;
2055
+ }
2056
+ function labelFor(app) {
2057
+ return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
2058
+ }
2059
+ function selectAndReport(app) {
2060
+ useWizard.getState().pushCliOutput(
2061
+ "stdout",
2062
+ `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
2063
+ );
2064
+ return selectApplication(app.id);
2065
+ }
2066
+ async function promptForApplication() {
2067
+ const store = useWizard.getState();
2068
+ const apps = await listApplications();
2069
+ if (apps.length === 0) {
1831
2070
  throw new Error(
1832
- "No Algolia profile is configured. Run `npx @algolia/cli auth login` to authenticate."
2071
+ "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
2072
+ );
2073
+ }
2074
+ if (apps.length === 1) {
2075
+ const only = apps[0];
2076
+ logger.info(
2077
+ { app: only.id },
2078
+ "single application on the account; selecting it"
1833
2079
  );
2080
+ return selectAndReport(only);
2081
+ }
2082
+ const messages = ["Which Algolia application should the wizard work in?"];
2083
+ for (; ; ) {
2084
+ const choice = await store.requestUserInput({
2085
+ prompt: "Select an application",
2086
+ promptType: "multipleChoice",
2087
+ options: apps.map(labelFor),
2088
+ secondary: apps.map(secondaryFor),
2089
+ messages
2090
+ });
2091
+ const chosen = apps.find((app) => labelFor(app) === choice);
2092
+ if (!chosen) {
2093
+ throw new Error("Application picker received an unexpected selection");
2094
+ }
2095
+ try {
2096
+ return await selectAndReport(chosen);
2097
+ } catch (err) {
2098
+ logger.warn(
2099
+ { app: chosen.id, err: err.message },
2100
+ "application select failed; re-prompting"
2101
+ );
2102
+ messages.push(
2103
+ `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
2104
+ );
2105
+ }
1834
2106
  }
1835
- return profile;
2107
+ }
2108
+ async function ensureApplication() {
2109
+ return await currentApplication() ?? await promptForApplication();
1836
2110
  }
1837
2111
 
1838
2112
  // src/workflows/default.ts
1839
- import { z as z25 } from "zod";
2113
+ import { z as z27 } from "zod";
1840
2114
 
1841
2115
  // 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)
2116
+ import { z as z5 } from "zod";
2117
+ var indicesListSchema = z5.object({
2118
+ items: z5.array(
2119
+ z5.object({
2120
+ name: z5.string(),
2121
+ entries: z5.number().default(0)
1848
2122
  })
1849
2123
  )
1850
2124
  });
@@ -1915,12 +2189,12 @@ import "zod";
1915
2189
 
1916
2190
  // src/lib/tools/listFiles.ts
1917
2191
  import { tool } from "ai";
1918
- import z4 from "zod";
2192
+ import z6 from "zod";
1919
2193
  import { readdir } from "node:fs/promises";
1920
2194
 
1921
2195
  // src/lib/tools/path.ts
1922
2196
  import { lstat } from "node:fs/promises";
1923
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
2197
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join6, sep } from "node:path";
1924
2198
  function resolveInRoot(ctx, path) {
1925
2199
  const target = resolve2(ctx.cwd, path);
1926
2200
  const rel = relative(ctx.root, target);
@@ -1936,7 +2210,7 @@ async function hasSymlinkParent(ctx, target) {
1936
2210
  let current = ctx.root;
1937
2211
  const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
1938
2212
  for (const part of parts) {
1939
- current = join7(current, part);
2213
+ current = join6(current, part);
1940
2214
  try {
1941
2215
  if ((await lstat(current)).isSymbolicLink()) return true;
1942
2216
  } catch (err) {
@@ -1951,7 +2225,7 @@ async function hasSymlinkParent(ctx, target) {
1951
2225
  function listFilesTool(ctx) {
1952
2226
  return tool({
1953
2227
  description: "List files in the current working directory",
1954
- inputSchema: z4.object(),
2228
+ inputSchema: z6.object(),
1955
2229
  execute: async () => {
1956
2230
  logger.info("called listFiles tool");
1957
2231
  if (++ctx.counts.list > ctx.limits.list) {
@@ -1967,13 +2241,13 @@ function listFilesTool(ctx) {
1967
2241
 
1968
2242
  // src/lib/tools/changeDirectory.ts
1969
2243
  import { tool as tool2 } from "ai";
1970
- import z5 from "zod";
2244
+ import z7 from "zod";
1971
2245
  import { stat } from "node:fs/promises";
1972
2246
  function changeDirectoryTool(ctx) {
1973
2247
  return tool2({
1974
2248
  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")
2249
+ inputSchema: z7.object({
2250
+ path: z7.string().describe("Directory to change into")
1977
2251
  }),
1978
2252
  execute: async ({ path }) => {
1979
2253
  logger.info({ path }, "called changeDirectory tool");
@@ -1995,13 +2269,13 @@ function changeDirectoryTool(ctx) {
1995
2269
 
1996
2270
  // src/lib/tools/reportStatus.ts
1997
2271
  import { tool as tool3 } from "ai";
1998
- import z6 from "zod";
2272
+ import z8 from "zod";
1999
2273
  function reportStatusTool(output) {
2000
2274
  return tool3({
2001
2275
  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(),
2276
+ inputSchema: z8.object({
2277
+ status: z8.enum(["success", "fail"]),
2278
+ reason: z8.string().optional(),
2005
2279
  output
2006
2280
  }),
2007
2281
  execute: async ({ status, reason, output: output2 }) => {
@@ -2013,8 +2287,8 @@ function reportStatusTool(output) {
2013
2287
 
2014
2288
  // src/lib/tools/readFile.ts
2015
2289
  import { tool as tool4 } from "ai";
2016
- import z7 from "zod";
2017
- import { readFile as readFile4 } from "node:fs/promises";
2290
+ import z9 from "zod";
2291
+ import { readFile as readFile3 } from "node:fs/promises";
2018
2292
 
2019
2293
  // src/lib/tools/env.ts
2020
2294
  import { basename } from "node:path";
@@ -2041,8 +2315,8 @@ function redactEnvValues(content) {
2041
2315
  function readFileTool(ctx) {
2042
2316
  return tool4({
2043
2317
  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")
2318
+ inputSchema: z9.object({
2319
+ filePath: z9.string().describe("Path to the file to read")
2046
2320
  }),
2047
2321
  execute: async ({ filePath }) => {
2048
2322
  if (++ctx.counts.read > ctx.limits.read) {
@@ -2052,7 +2326,7 @@ function readFileTool(ctx) {
2052
2326
  const resolved = resolveInRoot(ctx, filePath);
2053
2327
  if (!resolved.ok) return resolved.error;
2054
2328
  try {
2055
- const content = await readFile4(resolved.target, "utf8");
2329
+ const content = await readFile3(resolved.target, "utf8");
2056
2330
  return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2057
2331
  } catch (err) {
2058
2332
  return `Error reading ${filePath}: ${err.message}`;
@@ -2063,15 +2337,15 @@ function readFileTool(ctx) {
2063
2337
 
2064
2338
  // src/lib/tools/writeFile.ts
2065
2339
  import { tool as tool5 } from "ai";
2066
- import z8 from "zod";
2340
+ import z10 from "zod";
2067
2341
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2068
2342
  import { dirname as dirname4 } from "node:path";
2069
2343
  function writeFileTool(ctx) {
2070
2344
  return tool5({
2071
2345
  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")
2346
+ inputSchema: z10.object({
2347
+ filePath: z10.string().describe("Path to the file to write"),
2348
+ content: z10.string().describe("Content to write to the file")
2075
2349
  }),
2076
2350
  execute: async ({ filePath, content }) => {
2077
2351
  logger.info({ filePath }, "called writeFile tool");
@@ -2096,10 +2370,96 @@ function writeFileTool(ctx) {
2096
2370
 
2097
2371
  // src/lib/tools/writeAlgoliaCredentials.ts
2098
2372
  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";
2373
+ import z12 from "zod";
2374
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
2101
2375
  import { dirname as dirname5 } from "node:path";
2102
- var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2376
+
2377
+ // src/lib/algoliaApiKey.ts
2378
+ import { z as z11 } from "zod";
2379
+ var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
2380
+ var WRITE_ACLS = [
2381
+ "addObject",
2382
+ "deleteObject",
2383
+ "settings",
2384
+ "editSettings",
2385
+ "listIndexes"
2386
+ ];
2387
+ var WRITE_ACL_SET = new Set(WRITE_ACLS);
2388
+ var apiKeySchema = z11.object({
2389
+ value: z11.string().min(1),
2390
+ acl: z11.array(z11.string()).default([]),
2391
+ indexes: z11.array(z11.string()).default([])
2392
+ });
2393
+ var apiKeyListSchema = z11.object({
2394
+ items: z11.array(apiKeySchema).optional(),
2395
+ keys: z11.array(apiKeySchema).optional()
2396
+ }).transform((o) => o.items ?? o.keys ?? []);
2397
+ var createdKeySchema = z11.object({
2398
+ key: z11.string().min(1).optional(),
2399
+ value: z11.string().min(1).optional()
2400
+ });
2401
+ function canReuse(key, index) {
2402
+ return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
2403
+ }
2404
+ async function createSearchKey(index) {
2405
+ const stdout = await runAlgoliaCli([
2406
+ "apikeys",
2407
+ "create",
2408
+ "--indices",
2409
+ index,
2410
+ "--acl",
2411
+ "search,browse",
2412
+ "--description",
2413
+ `wizard search-only key for ${index}`,
2414
+ "-o",
2415
+ "json"
2416
+ ]);
2417
+ const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
2418
+ const created = key ?? value;
2419
+ if (!created) throw new Error("apikeys create returned no key value");
2420
+ return created;
2421
+ }
2422
+ function canReuseForWrites(key, index) {
2423
+ return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2424
+ }
2425
+ async function resolveWriteKey(index) {
2426
+ const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2427
+ const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key2) => canReuseForWrites(key2, index))?.value;
2428
+ if (existing) {
2429
+ logger.info({ index }, "reusing existing write API key");
2430
+ return existing;
2431
+ }
2432
+ logger.info({ index }, "no reusable write key found; creating one");
2433
+ const created = await runAlgoliaCli([
2434
+ "apikeys",
2435
+ "create",
2436
+ "--indices",
2437
+ index,
2438
+ "--acl",
2439
+ WRITE_ACLS.join(","),
2440
+ "--description",
2441
+ `wizard write key for ${index}`,
2442
+ "-o",
2443
+ "json"
2444
+ ]);
2445
+ const { key, value } = createdKeySchema.parse(JSON.parse(created));
2446
+ const writeKey = key ?? value;
2447
+ if (!writeKey) throw new Error("apikeys create returned no key value");
2448
+ return writeKey;
2449
+ }
2450
+ async function resolveSearchOnlyKey(index) {
2451
+ const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2452
+ const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
2453
+ if (existing) {
2454
+ logger.info({ index }, "reusing existing search-only API key");
2455
+ return existing;
2456
+ }
2457
+ logger.info({ index }, "no reusable search-only key found; creating one");
2458
+ return createSearchKey(index);
2459
+ }
2460
+
2461
+ // src/lib/tools/writeAlgoliaCredentials.ts
2462
+ var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2103
2463
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2104
2464
  function appendEnv(content, entries) {
2105
2465
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
@@ -2112,9 +2472,9 @@ function hasEnv(content, name) {
2112
2472
  }
2113
2473
  function writeCredentialsTool(ctx) {
2114
2474
  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(
2475
+ 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.`,
2476
+ inputSchema: z12.object({
2477
+ filePath: z12.string().describe(
2118
2478
  'Path to the env file to write credentials into (e.g. ".env")'
2119
2479
  )
2120
2480
  }),
@@ -2122,11 +2482,17 @@ function writeCredentialsTool(ctx) {
2122
2482
  logger.info({ filePath }, "called writeCredentials tool");
2123
2483
  const resolved = resolveInRoot(ctx, filePath);
2124
2484
  if (resolved.ok === false) return resolved.error;
2125
- let profile;
2485
+ const targetIndex = useWizard.getState().targetIndex;
2486
+ if (!targetIndex) {
2487
+ return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2488
+ }
2489
+ let appId;
2490
+ let writeKey;
2126
2491
  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.";
2492
+ appId = (await requireApplication()).id;
2493
+ writeKey = await resolveWriteKey(targetIndex);
2494
+ } catch (err) {
2495
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2130
2496
  }
2131
2497
  try {
2132
2498
  if (await hasSymlinkParent(ctx, resolved.target)) {
@@ -2134,7 +2500,7 @@ function writeCredentialsTool(ctx) {
2134
2500
  }
2135
2501
  let existing = "";
2136
2502
  try {
2137
- existing = await readFile5(resolved.target, "utf8");
2503
+ existing = await readFile4(resolved.target, "utf8");
2138
2504
  } catch (err) {
2139
2505
  if (err.code !== "ENOENT") throw err;
2140
2506
  }
@@ -2145,8 +2511,8 @@ function writeCredentialsTool(ctx) {
2145
2511
  return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2146
2512
  }
2147
2513
  const envWithCredentials = appendEnv(existing, [
2148
- [APP_ID_VAR, profile.appId],
2149
- [API_KEY_VAR, profile.apiKey]
2514
+ [APP_ID_VAR, appId],
2515
+ [API_KEY_VAR, writeKey]
2150
2516
  ]);
2151
2517
  await mkdir4(dirname5(resolved.target), { recursive: true });
2152
2518
  await writeFile4(resolved.target, envWithCredentials, "utf8");
@@ -2160,746 +2526,16 @@ function writeCredentialsTool(ctx) {
2160
2526
 
2161
2527
  // src/lib/tools/searchFiles.ts
2162
2528
  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
2529
+ import z13 from "zod";
2530
+ import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
2531
+ import { join as join7 } from "node:path";
2897
2532
  var MAX_QUERY_LENGTH = 1e3;
2898
2533
  async function walkFiles(dir) {
2534
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2899
2535
  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);
2536
+ for (const e of await readdir2(dir, { withFileTypes: true })) {
2537
+ if (e.name.startsWith(".") || skip.has(e.name)) continue;
2538
+ const full = join7(dir, e.name);
2903
2539
  if (e.isDirectory()) out.push(...await walkFiles(full));
2904
2540
  else if (e.isFile()) out.push(full);
2905
2541
  }
@@ -2908,9 +2544,9 @@ async function walkFiles(dir) {
2908
2544
  function searchFilesTool(ctx) {
2909
2545
  return tool7({
2910
2546
  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)")
2547
+ inputSchema: z13.object({
2548
+ query: z13.string().describe("JavaScript RegExp pattern to search for"),
2549
+ path: z13.string().optional().describe("Directory to search in (default: cwd)")
2914
2550
  }),
2915
2551
  execute: async ({ query, path = "." }) => {
2916
2552
  logger.info({ query, path }, "called searchFiles tool");
@@ -2932,7 +2568,7 @@ function searchFilesTool(ctx) {
2932
2568
  for (const file of await walkFiles(resolved.target)) {
2933
2569
  let content;
2934
2570
  try {
2935
- content = await readFile8(file, "utf8");
2571
+ content = await readFile5(file, "utf8");
2936
2572
  } catch {
2937
2573
  continue;
2938
2574
  }
@@ -2954,146 +2590,90 @@ function searchFilesTool(ctx) {
2954
2590
 
2955
2591
  // src/lib/tools/verifyImplementation.ts
2956
2592
  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";
2593
+ import z14 from "zod";
2962
2594
 
2963
2595
  // src/lib/tools/utils/runCommand.ts
2964
2596
  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;
2597
+ function runCommand(command, args, cwd) {
2971
2598
  return new Promise((resolve4) => {
2972
2599
  let output = "";
2973
- let settled = false;
2974
2600
  const child = spawn2(command, args, {
2975
2601
  cwd,
2976
- shell: false,
2977
- stdio: ["ignore", "pipe", "pipe"],
2978
- ...env ? { env: { ...process.env, ...env } } : {}
2602
+ stdio: ["ignore", "pipe", "pipe"]
2979
2603
  });
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
2604
  child.stdout?.on("data", (d) => output += d);
2998
2605
  child.stderr?.on("data", (d) => output += d);
2999
2606
  child.on(
3000
2607
  "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 })
2608
+ (err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
3010
2609
  );
2610
+ child.on("close", (code) => resolve4({ code: code ?? 1, output }));
3011
2611
  });
3012
2612
  }
3013
2613
 
2614
+ // src/lib/tools/utils/packageManager.ts
2615
+ import { readFile as readFile6 } from "node:fs/promises";
2616
+ import { existsSync } from "node:fs";
2617
+ import { join as join8 } from "node:path";
2618
+ var LOCKFILES = [
2619
+ ["pnpm-lock.yaml", "pnpm"],
2620
+ ["yarn.lock", "yarn"],
2621
+ ["bun.lockb", "bun"],
2622
+ ["bun.lock", "bun"],
2623
+ ["package-lock.json", "npm"]
2624
+ ];
2625
+ async function readPackageJson(cwd = process.cwd()) {
2626
+ return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2627
+ }
2628
+ function packageManagerFrom(pkg) {
2629
+ return pkg.packageManager?.split("@")[0] ?? "npm";
2630
+ }
2631
+ function packageManagerFromLockfile(cwd) {
2632
+ return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2633
+ }
2634
+ async function detectPackageManager(cwd) {
2635
+ try {
2636
+ const pkg = await readPackageJson(cwd);
2637
+ if (pkg.packageManager) return packageManagerFrom(pkg);
2638
+ } catch {
2639
+ }
2640
+ return packageManagerFromLockfile(cwd) ?? "npm";
2641
+ }
2642
+
3014
2643
  // src/lib/tools/repoVerification.ts
3015
2644
  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() {
2645
+ async function runRepoVerificationCheck() {
3023
2646
  let pkg;
3024
2647
  try {
3025
2648
  pkg = await readPackageJson();
3026
- } catch (err) {
3027
- return {
3028
- limitation: `Could not read package.json to detect verification conventions: ${err.message}`
3029
- };
2649
+ } catch (err) {
2650
+ const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2651
+ return { ok: false, checks: [], limitation };
3030
2652
  }
3031
2653
  const scripts = pkg.scripts ?? {};
3032
2654
  const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
3033
2655
  if (present.length === 0) {
3034
- return {
3035
- limitation: `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`
3036
- };
2656
+ const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2657
+ return { ok: false, checks: [], limitation };
3037
2658
  }
3038
2659
  const pm = await detectPackageManager(process.cwd());
3039
2660
  const checks = [];
3040
2661
  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
- };
2662
+ const command = `${pm} run ${script}`;
2663
+ const { code, output } = await runCommand(pm, ["run", script]);
2664
+ checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
3081
2665
  }
3082
- return {
3083
- ok: checks.every((c) => c.ok),
3084
- checks,
3085
- ...limitations.length ? { limitation: limitations.join(" ") } : {}
3086
- };
2666
+ return { ok: checks.every((c) => c.ok), checks };
3087
2667
  }
3088
2668
 
3089
2669
  // src/lib/tools/verifyImplementation.ts
3090
- function verifyImplementationTool(ctx) {
2670
+ function verifyImplementationTool() {
3091
2671
  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(),
2672
+ 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.",
2673
+ inputSchema: z14.object(),
3094
2674
  execute: async () => {
3095
- logger.info({ languages: ctx.languages }, "called verifyImplementation tool");
3096
- return runRepoVerificationCheck(ctx.languages);
2675
+ logger.info("called verifyImplementation tool");
2676
+ return runRepoVerificationCheck();
3097
2677
  }
3098
2678
  });
3099
2679
  }
@@ -3104,7 +2684,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
3104
2684
  import { nanoid as nanoid2 } from "nanoid";
3105
2685
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
3106
2686
  import { dirname as dirname6 } from "node:path";
3107
- import z12 from "zod";
2687
+ import z15 from "zod";
3108
2688
  var DATA_DIR = ".algolia-wizard/data";
3109
2689
  var RECORD_MODEL = "claude-haiku-4-5";
3110
2690
  var MAX_RECORDS = 100;
@@ -3116,17 +2696,17 @@ var anthropic = createAnthropic({
3116
2696
  function generateRecordTool(ctx) {
3117
2697
  return tool9({
3118
2698
  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.")
2699
+ inputSchema: z15.object({
2700
+ entityName: z15.string().describe("Name of the entity to generate records for."),
2701
+ attributes: z15.array(z15.string()).describe("Attribute names each record must contain."),
2702
+ count: z15.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2703
+ hint: z15.string().optional().describe("Optional context to steer realistic values.")
3124
2704
  }),
3125
2705
  execute: async ({ entityName, attributes, count, hint }) => {
3126
2706
  logger.info({ entityName, count }, "called generateRecord tool");
3127
2707
  try {
3128
- const value = z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]);
3129
- const recordSchema = z12.object(
2708
+ const value = z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]);
2709
+ const recordSchema = z15.object(
3130
2710
  Object.fromEntries(attributes.map((attr) => [attr, value]))
3131
2711
  );
3132
2712
  const generateBatch = async (batchCount) => {
@@ -3136,8 +2716,8 @@ function generateRecordTool(ctx) {
3136
2716
  const { output } = await generateText({
3137
2717
  model: anthropic(RECORD_MODEL),
3138
2718
  output: Output.object({
3139
- schema: z12.object({
3140
- records: z12.array(recordSchema).length(batchCount)
2719
+ schema: z15.object({
2720
+ records: z15.array(recordSchema).length(batchCount)
3141
2721
  })
3142
2722
  }),
3143
2723
  prompt: [
@@ -3195,12 +2775,12 @@ function generateRecordTool(ctx) {
3195
2775
 
3196
2776
  // src/lib/tools/notifyUser.ts
3197
2777
  import { tool as tool10 } from "ai";
3198
- import z13 from "zod";
2778
+ import z16 from "zod";
3199
2779
  function notifyUserTool() {
3200
2780
  return tool10({
3201
2781
  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(
2782
+ inputSchema: z16.object({
2783
+ message: z16.string().describe(
3204
2784
  "Short, plain-language description of what you are doing now."
3205
2785
  )
3206
2786
  }),
@@ -3219,17 +2799,12 @@ var DEFAULT_TOOL_LIMITS = {
3219
2799
  read: 20,
3220
2800
  match: 100
3221
2801
  };
3222
- function createToolContext({
3223
- limits = DEFAULT_TOOL_LIMITS,
3224
- cwd = process.cwd(),
3225
- languages = [DEFAULT_LANGUAGE_ID]
3226
- } = {}) {
2802
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
3227
2803
  return {
3228
2804
  root: cwd,
3229
2805
  cwd,
3230
2806
  limits,
3231
- counts: { list: 0, search: 0, read: 0 },
3232
- languages: languages.length ? languages : [DEFAULT_LANGUAGE_ID]
2807
+ counts: { list: 0, search: 0, read: 0 }
3233
2808
  };
3234
2809
  }
3235
2810
 
@@ -3266,7 +2841,7 @@ function createTools(ctx, { output, tools }) {
3266
2841
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
3267
2842
  verifyImplementation: withLogging(
3268
2843
  "verifyImplementation",
3269
- verifyImplementationTool(ctx)
2844
+ verifyImplementationTool()
3270
2845
  ),
3271
2846
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
3272
2847
  notifyUser: withLogging("notifyUser", notifyUserTool())
@@ -3302,7 +2877,7 @@ async function runAgent(req) {
3302
2877
  baseURL: PROXY_BASE_URL,
3303
2878
  fetch: proxyFetch
3304
2879
  });
3305
- const toolContext = createToolContext({ languages: req.languages });
2880
+ const toolContext = createToolContext();
3306
2881
  const readTools = ["readFile", "searchFiles", "listFiles"];
3307
2882
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
3308
2883
  const instructions = [
@@ -3385,19 +2960,16 @@ async function runAgent(req) {
3385
2960
  }
3386
2961
 
3387
2962
  // 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() }))
2963
+ import z19 from "zod";
2964
+ var detectLanguageSchema = z19.object({
2965
+ languages: z19.array(z19.object({ name: z19.string(), version: z19.string() })),
2966
+ frameworks: z19.array(z19.object({ name: z19.string(), version: z19.string() }))
3392
2967
  });
3393
2968
  var detectLanguage = () => runAgent({
3394
2969
  instructions: [
3395
2970
  "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.",
2971
+ "If a superset language is found, exclude the subset language. TS-over-JS.",
3399
2972
  "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
2973
  "Return the exact version",
3402
2974
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
3403
2975
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -3409,31 +2981,31 @@ var detectLanguage = () => runAgent({
3409
2981
  });
3410
2982
 
3411
2983
  // src/actions/analyzeCodebase.ts
3412
- import z17 from "zod";
2984
+ import z20 from "zod";
3413
2985
  var READONLY_TOOLS = [
3414
2986
  "listFiles",
3415
2987
  "changeDirectory",
3416
2988
  "readFile",
3417
2989
  "searchFiles"
3418
2990
  ];
3419
- var ingestionAnalysisSchema = z17.object({
3420
- ingestionAnalysis: z17.array(
3421
- z17.object({
3422
- name: z17.string(),
3423
- paths: z17.array(z17.string()),
2991
+ var ingestionAnalysisSchema = z20.object({
2992
+ ingestionAnalysis: z20.array(
2993
+ z20.object({
2994
+ name: z20.string(),
2995
+ paths: z20.array(z20.string()),
3424
2996
  // indexable fields the agent found for this entity
3425
- attributes: z17.array(z17.string())
2997
+ attributes: z20.array(z20.string())
3426
2998
  })
3427
2999
  )
3428
3000
  });
3429
- var searchImplementationAnalysisSchema = z17.object({
3430
- searchImplementationAnalysis: z17.string()
3001
+ var searchImplementationAnalysisSchema = z20.object({
3002
+ searchImplementationAnalysis: z20.string()
3431
3003
  });
3432
- var verificationSchema = z17.object({
3433
- verification: z17.array(z17.string())
3004
+ var verificationSchema = z20.object({
3005
+ verification: z20.array(z20.string())
3434
3006
  });
3435
3007
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
3436
- var analyzeCodebaseSchema = z17.object({
3008
+ var analyzeCodebaseSchema = z20.object({
3437
3009
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3438
3010
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
3439
3011
  verification: verificationSchema.shape.verification.optional(),
@@ -3446,7 +3018,6 @@ var MODE_CONFIG = {
3446
3018
  "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
3447
3019
  "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
3020
  "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
3021
  "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
3451
3022
  "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
3452
3023
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -3458,9 +3029,8 @@ var MODE_CONFIG = {
3458
3029
  instructions: [
3459
3030
  "Analyze the codebase to determine the single best location to add search UI functionality.",
3460
3031
  "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".',
3032
+ "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
3033
+ 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
3464
3034
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
3465
3035
  "When done, call reportStatus"
3466
3036
  ],
@@ -3469,8 +3039,8 @@ var MODE_CONFIG = {
3469
3039
  verification: {
3470
3040
  instructions: [
3471
3041
  "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"].',
3042
+ "Look at package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
3043
+ 'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
3474
3044
  "Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
3475
3045
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
3476
3046
  "When done, call reportStatus"
@@ -3497,7 +3067,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3497
3067
  // package.json
3498
3068
  var package_default = {
3499
3069
  name: "@algolia/wizard",
3500
- version: "0.8.0-rc.67.55",
3070
+ version: "0.9.0-rc.53.57",
3501
3071
  description: "Magically implement Algolia functionality in your codebase",
3502
3072
  type: "module",
3503
3073
  engines: {
@@ -3519,7 +3089,7 @@ var package_default = {
3519
3089
  prepare: "husky",
3520
3090
  prepublishOnly: "pnpm build",
3521
3091
  reset: "tsx ./scripts/reset-state.ts",
3522
- "test:toolchains": "tsx ./scripts/verify-toolchains.ts",
3092
+ "test:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
3523
3093
  "test:tools": "tsx ./tool-evals/toolEval.ts",
3524
3094
  test: "vitest",
3525
3095
  typecheck: "tsc --noEmit -p tsconfig.json"
@@ -3545,7 +3115,6 @@ var package_default = {
3545
3115
  dependencies: {
3546
3116
  "@ai-sdk/anthropic": "^3.0.81",
3547
3117
  "@ai-sdk/openai-compatible": "^2.0.47",
3548
- "@algolia/cli": "^5.11.0",
3549
3118
  "@hono/node-server": "^2.0.10",
3550
3119
  "@mishieck/ink-titled-box": "^0.4.2",
3551
3120
  "@segment/analytics-node": "^3.1.0",
@@ -3560,7 +3129,6 @@ var package_default = {
3560
3129
  nanoid: "^5.1.15",
3561
3130
  pino: "^10.3.1",
3562
3131
  react: "^19.2.7",
3563
- toml: "^4.1.1",
3564
3132
  varlock: "^1.5.1",
3565
3133
  zod: "^4.4.3",
3566
3134
  zustand: "^5.0.14"
@@ -3600,185 +3168,82 @@ function parseEntries(raw) {
3600
3168
  return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
3601
3169
  }
3602
3170
  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";
3171
+ async function askList(ctx, prompt, { required = false } = {}) {
3616
3172
  for (; ; ) {
3617
3173
  const answer = await ctx.requestUserInput({
3618
3174
  prompt,
3619
3175
  promptType: "textInput",
3620
- options: []
3176
+ options: [],
3177
+ helpText: 'Comma-separated, e.g. "TypeScript, Node".'
3621
3178
  });
3622
3179
  if (typeof answer !== "string") {
3623
- throw new Error("confirmLanguage received an unexpected non-text result");
3180
+ throw new Error("askList received an unexpected non-text result");
3624
3181
  }
3625
- const name = parseEntries(answer)[0]?.name;
3626
- if (name) return name;
3627
- prompt = "please enter a language name:";
3182
+ const entries = parseEntries(answer);
3183
+ if (entries.length || !required) return entries;
3184
+ prompt = "Please enter at least one entry:";
3628
3185
  }
3629
3186
  }
3187
+
3188
+ // src/actions/confirmLanguage.ts
3189
+ import z22 from "zod";
3190
+ var confirmLanguageSchema = z22.object({
3191
+ languages: detectLanguageSchema.shape.languages
3192
+ });
3630
3193
  async function confirmLanguage(ctx) {
3631
3194
  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
3195
+ const answer = await ctx.requestUserInput({
3196
+ prompt: "Did we detect your language(s) correctly?",
3197
+ promptType: "acceptReject",
3198
+ options: ["Yes", "No"],
3199
+ messages: [`Languages: ${summarize(detected.languages)}`]
3666
3200
  });
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)]);
3201
+ const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
3202
+ required: true
3203
+ });
3204
+ track("AI Wizard Language Confirmed", {
3205
+ languages
3206
+ });
3207
+ return { languages };
3673
3208
  }
3674
3209
 
3675
3210
  // 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: [] }
3211
+ import z23 from "zod";
3212
+ var confirmFrameworkSchema = z23.object({
3213
+ frameworks: detectLanguageSchema.shape.frameworks
3214
+ });
3215
+ var CURATED_FRAMEWORKS = [
3216
+ "Next.js",
3217
+ "React",
3218
+ "Vue",
3219
+ "Angular",
3220
+ "Svelte",
3221
+ "Vanilla JS"
3709
3222
  ];
3710
- var CURATED_FRAMEWORKS = FRAMEWORKS.map(
3711
- (f) => f.name
3712
- );
3223
+ var OTHER_OPTION = "Other";
3713
3224
  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);
3225
+ var FRAMEWORK_ALIASES = {
3226
+ next: "nextjs",
3227
+ nextjs: "nextjs",
3228
+ react: "react",
3229
+ reactjs: "react",
3230
+ vue: "vue",
3231
+ vuejs: "vue",
3232
+ angular: "angular",
3233
+ angularjs: "angular",
3234
+ svelte: "svelte",
3235
+ sveltekit: "svelte",
3236
+ vanillajs: "vanillajs",
3237
+ vanilla: "vanillajs",
3238
+ javascript: "vanillajs",
3239
+ js: "vanillajs"
3240
+ };
3241
+ var isSameFramework = (a, b) => {
3242
+ const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
3243
+ const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
3727
3244
  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) {
3245
+ };
3246
+ function confirmed(name, version) {
3782
3247
  const frameworks = [{ name, version: version ?? "unknown" }];
3783
3248
  track("AI Wizard Frontend Framework Confirmed", { frameworks });
3784
3249
  return { frameworks };
@@ -3806,7 +3271,7 @@ async function confirmFramework(ctx) {
3806
3271
  for (const fw of detectedFrameworks) {
3807
3272
  if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
3808
3273
  }
3809
- options.push(OTHER_OPTION2);
3274
+ options.push(OTHER_OPTION);
3810
3275
  const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
3811
3276
  const primary = detectedFrameworks[0];
3812
3277
  if (primary) {
@@ -3816,7 +3281,7 @@ async function confirmFramework(ctx) {
3816
3281
  options: [`Confirm ${primary.name}`, "Use a different framework"],
3817
3282
  secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
3818
3283
  });
3819
- if (accepted === true) return confirmed2(primary.name, primary.version);
3284
+ if (accepted === true) return confirmed(primary.name, primary.version);
3820
3285
  }
3821
3286
  const secondary = options.map(
3822
3287
  (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
@@ -3826,7 +3291,7 @@ async function confirmFramework(ctx) {
3826
3291
  0
3827
3292
  );
3828
3293
  const selection = await ctx.requestUserInput({
3829
- prompt: "select the framework that renders your UI",
3294
+ prompt: "select a framework",
3830
3295
  promptType: "multipleChoice",
3831
3296
  options,
3832
3297
  secondary,
@@ -3835,10 +3300,10 @@ async function confirmFramework(ctx) {
3835
3300
  if (typeof selection !== "string") {
3836
3301
  throw new Error("confirmFramework received an unexpected non-text result");
3837
3302
  }
3838
- if (selection === OTHER_OPTION2) {
3839
- return confirmed2(await askOtherFramework(ctx));
3303
+ if (selection === OTHER_OPTION) {
3304
+ return confirmed(await askOtherFramework(ctx));
3840
3305
  }
3841
- return confirmed2(selection, detectedFor(selection)?.version);
3306
+ return confirmed(selection, detectedFor(selection)?.version);
3842
3307
  }
3843
3308
 
3844
3309
  // src/actions/promptUser.ts
@@ -3872,8 +3337,8 @@ async function promptUser(ctx, params) {
3872
3337
  }
3873
3338
 
3874
3339
  // src/actions/confirmEntities.ts
3875
- import z21 from "zod";
3876
- var confirmEntitiesSchema = z21.object({
3340
+ import z24 from "zod";
3341
+ var confirmEntitiesSchema = z24.object({
3877
3342
  // Final detection — the focused re-run may supersede project-scan's.
3878
3343
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3879
3344
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3931,27 +3396,27 @@ async function confirmEntities(ctx) {
3931
3396
  onSubmit: () => {
3932
3397
  }
3933
3398
  });
3934
- const confirmed3 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3935
- if (confirmed3.length === 0) {
3399
+ const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3400
+ if (confirmed2.length === 0) {
3936
3401
  throw new Error("User cancelled entity selection \u2014 analysis halted.");
3937
3402
  }
3938
- ctx.setUserInput("confirmedEntities", confirmed3);
3403
+ ctx.setUserInput("confirmedEntities", confirmed2);
3939
3404
  track("AI Wizard Entities Confirmed", {
3940
- entities: toEntitySummary(confirmed3)
3405
+ entities: toEntitySummary(confirmed2)
3941
3406
  });
3942
- return { ingestionAnalysis: entities, confirmedEntities: confirmed3 };
3407
+ return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
3943
3408
  }
3944
3409
 
3945
3410
  // src/actions/review.ts
3946
- import { z as z22 } from "zod";
3947
- var reviewSchema = z22.object({
3411
+ import { z as z25 } from "zod";
3412
+ var reviewSchema = z25.object({
3948
3413
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3949
3414
  // not one entry per workflow step — a step's raw output can be a long,
3950
3415
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3951
3416
  // 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())
3417
+ summaryPoints: z25.array(z25.string()),
3418
+ reviewPrompt: z25.string(),
3419
+ nextSteps: z25.array(z25.string())
3955
3420
  });
3956
3421
  function formatCompletedSteps(steps) {
3957
3422
  if (!steps.length) return "(no prior steps completed)";
@@ -3963,7 +3428,7 @@ ${JSON.stringify(s.output, null, 2)}`
3963
3428
  }
3964
3429
  function formatReviewSummary(result) {
3965
3430
  const nextStepLines = result.nextSteps.map((step) => {
3966
- const isIngestCommand = step.includes("algolia-wizard/") || step.includes("AlgoliaWizardIngest");
3431
+ const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3967
3432
  const isWorktreeCommand = step.includes("/worktrees/");
3968
3433
  return {
3969
3434
  text: `\u2192 ${step}`,
@@ -4002,17 +3467,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4002
3467
  };
4003
3468
 
4004
3469
  // src/actions/implement.ts
4005
- import z24 from "zod";
3470
+ import z26 from "zod";
4006
3471
 
4007
3472
  // 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";
3473
+ import { execFile, spawn as spawn3 } from "node:child_process";
3474
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
4011
3475
  import {
4012
3476
  basename as basename2,
4013
3477
  dirname as dirname7,
4014
3478
  isAbsolute as isAbsolute2,
4015
- join as join12,
3479
+ join as join9,
4016
3480
  relative as relative2,
4017
3481
  resolve as resolve3
4018
3482
  } from "node:path";
@@ -4046,8 +3510,8 @@ async function isWorkingTreeDirty(repoRoot) {
4046
3510
  return out.trim().length > 0;
4047
3511
  }
4048
3512
  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);
3513
+ const dir = join9(stateDir(repoRoot), "worktrees");
3514
+ const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
4051
3515
  for (const slug of stale) {
4052
3516
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
4053
3517
  try {
@@ -4057,7 +3521,7 @@ async function pruneOldWorktrees(repoRoot) {
4057
3521
  "worktree",
4058
3522
  "remove",
4059
3523
  "--force",
4060
- join12(dir, slug)
3524
+ join9(dir, slug)
4061
3525
  ]);
4062
3526
  await git(["-C", repoRoot, "branch", "-D", branch]);
4063
3527
  } catch (err) {
@@ -4071,55 +3535,43 @@ async function pruneOldWorktrees(repoRoot) {
4071
3535
  async function createWorktree(repoRoot) {
4072
3536
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
4073
3537
  const dirSlug = branch.replace(/\//g, "-");
4074
- const path = join12(stateDir(repoRoot), "worktrees", dirSlug);
3538
+ const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
4075
3539
  await git(["-C", repoRoot, "worktree", "prune"]);
4076
3540
  await pruneOldWorktrees(repoRoot);
4077
3541
  await mkdir6(dirname7(path), { recursive: true });
4078
3542
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
4079
3543
  return { path, branch };
4080
3544
  }
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() };
3545
+ async function installWorktreeDeps(worktreePath) {
3546
+ try {
3547
+ await readPackageJson(worktreePath);
3548
+ } catch {
3549
+ return { ok: true, output: "no package.json; skipped install" };
4119
3550
  }
4120
- return { ok: true, output: outputs.join("\n").trim() };
3551
+ const pm = await detectPackageManager(worktreePath);
3552
+ return new Promise((resolve4) => {
3553
+ let output = "";
3554
+ const child = spawn3(pm, ["install"], {
3555
+ cwd: worktreePath,
3556
+ stdio: ["ignore", "pipe", "pipe"]
3557
+ });
3558
+ child.stdout?.on("data", (d) => output += d);
3559
+ child.stderr?.on("data", (d) => output += d);
3560
+ child.on(
3561
+ "error",
3562
+ (err) => resolve4({
3563
+ ok: false,
3564
+ output: `Failed to run ${pm} install: ${err.message}`
3565
+ })
3566
+ );
3567
+ child.on(
3568
+ "close",
3569
+ (code) => resolve4({ ok: code === 0, output: output.trim() })
3570
+ );
3571
+ });
4121
3572
  }
4122
- function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
3573
+ var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3574
+ function validateIngestEntrypoint(worktreePath, entrypoint) {
4123
3575
  if (!entrypoint || entrypoint.startsWith("-")) {
4124
3576
  return {
4125
3577
  ok: false,
@@ -4134,29 +3586,18 @@ function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
4134
3586
  reason: `entrypoint "${entrypoint}" resolves outside the worktree`
4135
3587
  };
4136
3588
  }
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
3589
  return { ok: true, target };
4144
3590
  }
4145
- async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
4146
- const { ingest, profile, packageManager } = toolchain;
4147
- if (ingest.kind !== "auto") {
3591
+ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3592
+ if (!INGEST_RUNTIMES.includes(runtime)) {
4148
3593
  return {
4149
3594
  ran: false,
4150
3595
  ok: false,
4151
3596
  output: "",
4152
- reason: `${profile.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
3597
+ reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
4153
3598
  };
4154
3599
  }
4155
- const validated = validateIngestEntrypoint(
4156
- worktreePath,
4157
- entrypoint,
4158
- ingest.entrypointExtensions
4159
- );
3600
+ const validated = validateIngestEntrypoint(worktreePath, entrypoint);
4160
3601
  if (!validated.ok) {
4161
3602
  return { ran: false, ok: false, output: "", reason: validated.reason };
4162
3603
  }
@@ -4177,13 +3618,29 @@ async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
4177
3618
  reason: `entrypoint "${entrypoint}" does not exist`
4178
3619
  };
4179
3620
  }
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
3621
+ return new Promise((resolveRun) => {
3622
+ let output = "";
3623
+ const child = spawn3(runtime, [entrypoint], {
3624
+ cwd: worktreePath,
3625
+ shell: false,
3626
+ stdio: ["ignore", "pipe", "pipe"],
3627
+ env: { ...process.env, ...env }
3628
+ });
3629
+ child.stdout?.on("data", (d) => output += d);
3630
+ child.stderr?.on("data", (d) => output += d);
3631
+ child.on(
3632
+ "error",
3633
+ (err) => resolveRun({
3634
+ ran: true,
3635
+ ok: false,
3636
+ output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
3637
+ })
3638
+ );
3639
+ child.on(
3640
+ "close",
3641
+ (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
3642
+ );
4185
3643
  });
4186
- return { ran: true, ok: code === 0, output: output.trim() };
4187
3644
  }
4188
3645
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
4189
3646
  const trimmed = sourcePath.trim();
@@ -4198,8 +3655,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4198
3655
  } catch {
4199
3656
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4200
3657
  }
4201
- const relPath = join12(ingestDir, basename2(source));
4202
- const dest = join12(worktreePath, relPath);
3658
+ const relPath = join9(ingestDir, basename2(source));
3659
+ const dest = join9(worktreePath, relPath);
4203
3660
  try {
4204
3661
  await mkdir6(dirname7(dest), { recursive: true });
4205
3662
  await copyFile(source, dest);
@@ -4215,10 +3672,10 @@ function hasEnvVar(content, name) {
4215
3672
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
4216
3673
  }
4217
3674
  async function writeSearchEnvValues(worktreePath, vars) {
4218
- const target = join12(worktreePath, ".env");
3675
+ const target = join9(worktreePath, ".env");
4219
3676
  let existing = "";
4220
3677
  try {
4221
- existing = await readFile9(target, "utf8");
3678
+ existing = await readFile7(target, "utf8");
4222
3679
  } catch (err) {
4223
3680
  if (err.code !== "ENOENT") throw err;
4224
3681
  }
@@ -4286,135 +3743,146 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
4286
3743
  }
4287
3744
  }
4288
3745
 
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
3746
  // src/lib/algoliaDocs.ts
4338
- import { readFileSync, existsSync as existsSync5 } from "node:fs";
4339
- import { dirname as dirname8, join as join13 } from "node:path";
3747
+ import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3748
+ import { dirname as dirname8, join as join10 } from "node:path";
4340
3749
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4341
- var DOCS_SUBPATH = join13("docs", "algolia-sdk");
3750
+ var DOCS_SUBPATH = join10("docs", "algolia-sdk");
4342
3751
  function findDocsDir() {
4343
3752
  let dir = dirname8(fileURLToPath2(import.meta.url));
4344
3753
  for (; ; ) {
4345
- const candidate = join13(dir, DOCS_SUBPATH);
4346
- if (existsSync5(candidate)) return candidate;
3754
+ const candidate = join10(dir, DOCS_SUBPATH);
3755
+ if (existsSync2(candidate)) return candidate;
4347
3756
  const parent = dirname8(dir);
4348
3757
  if (parent === dir) return void 0;
4349
3758
  dir = parent;
4350
3759
  }
4351
3760
  }
4352
- function getNamedDoc(name, key) {
3761
+ function loadAlgoliaDoc(language) {
3762
+ const docsDir = findDocsDir();
3763
+ if (!docsDir) {
3764
+ logger.warn(
3765
+ "algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
3766
+ );
3767
+ return "";
3768
+ }
3769
+ const files = readdirSync(docsDir).filter((f) => f.includes(language));
3770
+ if (files.length === 0) {
3771
+ logger.warn(
3772
+ { language },
3773
+ "algoliaDocs: no SDK reference found for language; skipping"
3774
+ );
3775
+ return "";
3776
+ }
3777
+ return readFileSync(join10(docsDir, files[0]), "utf8").trim();
3778
+ }
3779
+ function getNamedDoc(name, language) {
4353
3780
  const docsDir = findDocsDir();
4354
3781
  if (!docsDir) {
4355
3782
  logger.warn("docs/algolia-sdk not found");
4356
3783
  return "";
4357
3784
  }
4358
- const file = join13(docsDir, `${name}-${key}.md`);
4359
- if (!existsSync5(file)) {
4360
- logger.warn({ name, key }, "named SDK reference not found");
3785
+ const file = join10(docsDir, `${name}-${language}.md`);
3786
+ if (!existsSync2(file)) {
3787
+ logger.warn({ name, language }, "named SDK reference not found");
4361
3788
  return "";
4362
3789
  }
4363
3790
  return readFileSync(file, "utf8").trim();
4364
3791
  }
3792
+ function getFrameworkSpecificDoc(frameworks) {
3793
+ const fw = frameworks.map((f) => f.toLowerCase());
3794
+ if (fw.includes("vue") || fw.includes("nuxt")) {
3795
+ return loadAlgoliaDoc("vue");
3796
+ }
3797
+ if (fw.includes("react") || fw.includes("next.js")) {
3798
+ return loadAlgoliaDoc("react");
3799
+ }
3800
+ if (fw.includes("angular")) {
3801
+ return loadAlgoliaDoc("angular");
3802
+ }
3803
+ return loadAlgoliaDoc("js");
3804
+ }
3805
+
3806
+ // src/lib/shell.ts
3807
+ function shellQuote(value) {
3808
+ return "'" + value.replace(/'/g, "'\\''") + "'";
3809
+ }
4365
3810
 
4366
3811
  // 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()
3812
+ var implementSchema = z26.object({
3813
+ filesChanged: z26.array(z26.string()),
3814
+ summary: z26.string(),
3815
+ worktreePath: z26.string().optional(),
3816
+ ingestCommand: z26.string().optional(),
3817
+ ingestScriptRan: z26.boolean().optional(),
3818
+ ingestRecordCount: z26.number().optional(),
3819
+ ingestDurationMs: z26.number().optional(),
3820
+ ingestionSource: z26.enum(["local", "fileUpload", "generated"]),
3821
+ // Hints, not ground truth: the search agent may rename the prefix to match
3822
+ // the project's build tool, and its summary carries the final names.
3823
+ searchEnvVars: z26.array(
3824
+ z26.object({
3825
+ name: z26.string(),
3826
+ value: z26.string()
4394
3827
  })
4395
3828
  ).optional()
4396
3829
  });
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()
3830
+ var implementationOutputSchema = z26.object({
3831
+ summary: z26.string(),
3832
+ // Ingestion only: a structured pair the wizard turns into an argv, never a
3833
+ // free-form command string. `runtime` is allowlisted and `entrypoint` is
3834
+ // validated worktree-relative, so the agent cannot inject extra commands.
3835
+ runtime: z26.enum(INGEST_RUNTIMES).optional(),
3836
+ entrypoint: z26.string().optional()
4405
3837
  });
4406
- var verificationOutputSchema = z24.object({
4407
- summary: z24.string(),
4408
- sufficient: z24.boolean(),
4409
- additionalInstructions: z24.string().optional()
3838
+ var verificationOutputSchema = z26.object({
3839
+ summary: z26.string(),
3840
+ sufficient: z26.boolean(),
3841
+ additionalInstructions: z26.string().optional()
4410
3842
  });
4411
3843
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
4412
3844
  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
3845
+ var INGEST_DIR = ".algolia-wizard";
3846
+ function detectUiFramework(language) {
3847
+ const names = language.frameworks.map((f) => f.name.toLowerCase());
3848
+ if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3849
+ if (names.some((n) => n.includes("react") || n.includes("next")))
3850
+ return "React";
3851
+ if (names.some((n) => n.includes("angular"))) return "Angular";
3852
+ return "JavaScript";
3853
+ }
3854
+ function frameworksForDoc(framework) {
3855
+ switch (framework) {
3856
+ case "React":
3857
+ return ["react"];
3858
+ case "Vue":
3859
+ return ["vue"];
3860
+ case "Angular":
3861
+ return ["angular"];
3862
+ case "JavaScript":
3863
+ return [];
3864
+ }
3865
+ }
3866
+ function publicEnvPrefix(language) {
3867
+ const frameworkNames = language.frameworks.map(
3868
+ (framework) => framework.name.toLowerCase()
4417
3869
  );
3870
+ if (frameworkNames.some((name) => name.includes("next"))) {
3871
+ return "NEXT_PUBLIC_";
3872
+ }
3873
+ if (frameworkNames.some((name) => name.includes("nuxt"))) {
3874
+ return "NUXT_PUBLIC_";
3875
+ }
3876
+ if (frameworkNames.some((name) => name.includes("astro"))) {
3877
+ return "PUBLIC_";
3878
+ }
3879
+ if (frameworkNames.some((name) => name.includes("vite"))) {
3880
+ return "VITE_";
3881
+ }
3882
+ return "PUBLIC_";
3883
+ }
3884
+ function searchEnvVars(language, appId, searchKey) {
3885
+ const prefix = publicEnvPrefix(language);
4418
3886
  return [
4419
3887
  {
4420
3888
  name: `${prefix}ALGOLIA_APP_ID`,
@@ -4426,38 +3894,6 @@ function buildSearchEnvVars(language, strategy, appId, searchKey) {
4426
3894
  }
4427
3895
  ];
4428
3896
  }
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
3897
  function baseInstructions(input) {
4462
3898
  return [
4463
3899
  `Target Algolia index: ${input.targetIndex}`,
@@ -4474,9 +3910,6 @@ function sourceSpecificInstructions(input) {
4474
3910
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
4475
3911
  ],
4476
3912
  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
3913
  `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
4481
3914
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
4482
3915
  "Map parsed columns/fields to the confirmed entity attributes.",
@@ -4485,71 +3918,57 @@ function sourceSpecificInstructions(input) {
4485
3918
  generated: [
4486
3919
  "No real data source exists; use sample records for each confirmed entity.",
4487
3920
  "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.",
3921
+ "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
3922
  "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
3923
  ]
4491
3924
  };
4492
3925
  return byLine[input.ingestionSource];
4493
3926
  }
4494
3927
  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
3928
  return [
4500
3929
  ...input.confirmed && input.confirmed.length ? [
4501
- `Write the ingestion script in ${profile.displayName}, at "${ingestScriptDir(profile)}/" in the repo.`,
3930
+ `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
4502
3931
  `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.`,
3932
+ `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.`,
3933
+ "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4505
3934
  "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),
3935
+ getNamedDoc("save-records", "js"),
3936
+ 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4508
3937
  "The summary should be extremely concise.",
4509
- runInstruction,
3938
+ `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
3939
  ...sourceSpecificInstructions(input)
4511
3940
  ] : []
4512
3941
  ];
4513
3942
  }
4514
3943
  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.`;
3944
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4521
3945
  return [
4522
3946
  "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:",
3947
+ `Build the search UI for ${input.uiFramework}.`,
3948
+ "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4525
3949
  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.
3950
+ `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.`,
3951
+ "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.",
3952
+ // appId always resolves (requireApplication throws otherwise); only the key
3953
+ // can fall back to a placeholder.
4532
3954
  `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
3955
+ // Not the agent's to rename: the wizard writes these exact names into
3956
+ // ".env" right after this step, so a renamed prefix would leave the code
4536
3957
  // reading a var the wizard never wrote.
4537
- `Use exactly these env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3958
+ `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3959
+ '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
3960
  "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
3961
  ];
4540
3962
  }
4541
3963
  function verificationInstructions(input) {
4542
- const protectedDirs = [
4543
- .../* @__PURE__ */ new Set([input.ingestDir, ingestScriptDir(input.ingestionProfile)])
4544
- ];
4545
3964
  return [
4546
3965
  "Verify the Algolia implementation changes in the current worktree.",
4547
3966
  `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.`,
3967
+ "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
4549
3968
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
4550
3969
  "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
3970
  "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.`,
3971
+ `Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
4553
3972
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
4554
3973
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
4555
3974
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -4558,17 +3977,14 @@ function verificationInstructions(input) {
4558
3977
  var IMPLEMENT_CONFIG = {
4559
3978
  ingestion: {
4560
3979
  title: "Algolia ingestion",
4561
- label: "Ingestion",
4562
3980
  buildInstructions: ingestionInstructions
4563
3981
  },
4564
3982
  search: {
4565
3983
  title: "Algolia search",
4566
- label: "Search",
4567
3984
  buildInstructions: searchInstructions
4568
3985
  },
4569
3986
  verification: {
4570
3987
  title: "Algolia verification",
4571
- label: "Verification",
4572
3988
  buildInstructions: verificationInstructions
4573
3989
  }
4574
3990
  };
@@ -4600,10 +4016,11 @@ function buildAgentInstructions(useCase, input, extraInstructions = []) {
4600
4016
  ];
4601
4017
  }
4602
4018
  function formatSummary(useCase, summary) {
4603
- return `${IMPLEMENT_CONFIG[useCase].label}: ${summary}`;
4019
+ const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
4020
+ return `${label}: ${summary}`;
4604
4021
  }
4605
- function buildIngestCommand(worktree, toolchain, entrypoint) {
4606
- return `cd ${shellQuote(worktree)} && ${describeIngestCommand(toolchain.ingest, entrypoint)}`;
4022
+ function buildIngestCommand(worktree, runtime, entrypoint) {
4023
+ return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4607
4024
  }
4608
4025
  function parseIngestRecordCount(output) {
4609
4026
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
@@ -4680,17 +4097,18 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4680
4097
  }
4681
4098
  }
4682
4099
  const targetIndex = selected?.selection;
4100
+ useWizard.getState().setTargetIndex(targetIndex ?? null);
4683
4101
  await assertGitRepoWithHead(repoRoot);
4684
4102
  if (await isWorkingTreeDirty(repoRoot)) {
4685
4103
  await confirmDirtyWorkingTree(ctx, repoRoot);
4686
4104
  }
4687
4105
  const normalized = normalizeFindingPaths(findings);
4688
- const confirmed3 = normalized.confirmedEntities;
4106
+ const confirmed2 = normalized.confirmedEntities;
4689
4107
  const searchLocation = normalized.searchImplementationAnalysis;
4690
4108
  let appId;
4691
4109
  let searchKey;
4692
4110
  if (useCases.includes("search")) {
4693
- appId = (await loadActiveProfile()).appId;
4111
+ appId = (await requireApplication()).id;
4694
4112
  try {
4695
4113
  searchKey = await resolveSearchOnlyKey(targetIndex);
4696
4114
  } catch (err) {
@@ -4723,66 +4141,29 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4723
4141
  );
4724
4142
  }
4725
4143
  }
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
4144
  const input = {
4754
4145
  findings: normalized,
4755
- confirmed: confirmed3,
4146
+ confirmed: confirmed2,
4756
4147
  searchLocation,
4757
4148
  targetIndex,
4758
4149
  language,
4759
4150
  appId,
4760
4151
  searchKey,
4761
- searchEnvVars: buildSearchEnvVars(
4762
- language,
4763
- searchStrategy,
4764
- appId,
4765
- searchKey
4766
- ),
4152
+ searchEnvVars: searchEnvVars(language, appId, searchKey),
4767
4153
  ingestDir: INGEST_DIR,
4768
4154
  ingestionSource,
4769
4155
  uploadFilePath,
4770
- searchStrategy,
4771
- frameworkName,
4772
- ingestionProfile,
4773
- toolchain,
4774
- verificationLanguages
4156
+ uiFramework: detectUiFramework(language)
4775
4157
  };
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
4158
  const summaries = [];
4779
4159
  if (uploadWarning) summaries.push(uploadWarning);
4780
4160
  let agentRuns = 0;
4161
+ let ingestRuntime;
4781
4162
  let ingestEntrypoint;
4782
4163
  let ingestScriptRan = false;
4783
4164
  let ingestRecordCount;
4784
4165
  let ingestDurationMs;
4785
- const failedInstalls = /* @__PURE__ */ new Set();
4166
+ let installFailed = false;
4786
4167
  let ingestOutcomeMessage;
4787
4168
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4788
4169
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -4796,19 +4177,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4796
4177
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4797
4178
  outputSchema: implementationOutputSchema
4798
4179
  });
4799
- const useCaseToolchain = toolchainForUseCase(currentUseCase);
4800
- if (!useCaseToolchain) return result;
4801
4180
  ctx.notify({
4802
4181
  messages: [`Installing dependencies for ${currentUseCase}\u2026`]
4803
4182
  });
4804
4183
  const installLogId = ctx.logStart("installWorktreeDeps", {
4805
- useCase: currentUseCase,
4806
- language: useCaseToolchain.profile.id
4184
+ useCase: currentUseCase
4807
4185
  });
4808
- const install = await installWorktreeDeps(worktree, useCaseToolchain);
4186
+ const install = await installWorktreeDeps(worktree);
4809
4187
  ctx.logEnd(installLogId, install.ok ? "success" : "error");
4810
4188
  if (!install.ok) {
4811
- failedInstalls.add(useCaseToolchain.profile.displayName);
4189
+ installFailed = true;
4812
4190
  logger.warn(
4813
4191
  { useCase: currentUseCase, output: install.output },
4814
4192
  "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
@@ -4822,16 +4200,15 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4822
4200
  return runAgent({
4823
4201
  instructions: buildAgentInstructions("verification", input),
4824
4202
  tools: toolsForUseCase("verification"),
4825
- outputSchema: verificationOutputSchema,
4826
- // So verifyImplementation runs this repo's checks, not just npm scripts.
4827
- languages: input.verificationLanguages
4203
+ outputSchema: verificationOutputSchema
4828
4204
  });
4829
4205
  }
4830
4206
  if (useCases.includes("ingestion")) {
4831
- const { summary, entrypoint } = await runImplementationUseCase("ingestion");
4207
+ const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4832
4208
  summaries.push(formatSummary("ingestion", summary));
4209
+ ingestRuntime = runtime;
4833
4210
  ingestEntrypoint = entrypoint;
4834
- if (ingestEntrypoint && toolchain.ingest.kind === "auto" && failedInstalls.size === 0) {
4211
+ if (ingestRuntime && ingestEntrypoint && !installFailed) {
4835
4212
  ctx.clearNotices();
4836
4213
  const runNow = await ctx.requestUserInput({
4837
4214
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
@@ -4840,20 +4217,21 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4840
4217
  messages: []
4841
4218
  }) === true;
4842
4219
  if (runNow) {
4843
- const profile = await loadActiveProfile();
4220
+ const ingestApp = await requireApplication();
4221
+ const writeKey = await resolveWriteKey(targetIndex);
4844
4222
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
4845
4223
  const scriptLogId = ctx.logStart("runIngestScript", {
4846
- language: ingestionProfile.id,
4224
+ runtime: ingestRuntime,
4847
4225
  entrypoint: ingestEntrypoint
4848
4226
  });
4849
4227
  const startedAt = Date.now();
4850
4228
  const run2 = await runIngestScript(
4851
4229
  worktree,
4852
- toolchain,
4230
+ ingestRuntime,
4853
4231
  ingestEntrypoint,
4854
4232
  {
4855
- [APP_ID_VAR]: profile.appId,
4856
- [API_KEY_VAR]: profile.apiKey
4233
+ [APP_ID_VAR]: ingestApp.id,
4234
+ [API_KEY_VAR]: writeKey
4857
4235
  }
4858
4236
  );
4859
4237
  ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
@@ -4863,7 +4241,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4863
4241
  ingestRecordCount = parseIngestRecordCount(run2.output);
4864
4242
  if (ingestRecordCount != null) {
4865
4243
  track("AI Wizard Ingest Successful", {
4866
- entity_name: confirmed3?.map((e) => e.name).join(", ") || "unknown",
4244
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4867
4245
  record_count: ingestRecordCount,
4868
4246
  duration_ms: ingestDurationMs
4869
4247
  });
@@ -4876,7 +4254,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4876
4254
  outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
4877
4255
  logger.warn(
4878
4256
  {
4879
- language: ingestionProfile.id,
4257
+ runtime: ingestRuntime,
4880
4258
  entrypoint: ingestEntrypoint,
4881
4259
  reason: run2.reason
4882
4260
  },
@@ -4899,7 +4277,7 @@ ${run2.output}` : status;
4899
4277
  outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
4900
4278
  logger.warn(
4901
4279
  {
4902
- language: ingestionProfile.id,
4280
+ runtime: ingestRuntime,
4903
4281
  entrypoint: ingestEntrypoint,
4904
4282
  output: run2.output
4905
4283
  },
@@ -4916,52 +4294,20 @@ ${run2.output}` : status;
4916
4294
  }
4917
4295
  }
4918
4296
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4919
- if (ingestEntrypoint) {
4297
+ if (ingestRuntime && ingestEntrypoint) {
4920
4298
  commandMessages.push(
4921
- `Ingestion command: ${buildIngestCommand(worktree, toolchain, ingestEntrypoint)}`
4299
+ `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4922
4300
  );
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
4301
  }
4942
4302
  await ctx.requestUserInput({
4943
- // No question being asked here, just an acknowledgement — the
4944
- // continue/decline hints below already say "continue".
4303
+ // Nothing to ask — the continue/decline hints carry the whole prompt.
4945
4304
  prompt: "",
4946
4305
  promptType: "enterToContinue",
4947
4306
  options: [],
4948
4307
  messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4949
4308
  });
4950
4309
  }
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) {
4310
+ if (useCases.includes("search")) {
4965
4311
  let extraInstructions = [];
4966
4312
  const preSearchFiles = new Set(await listChangedFiles(worktree));
4967
4313
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
@@ -5032,9 +4378,9 @@ ${run2.output}` : status;
5032
4378
  "implement: agent reported success but no files changed in the worktree"
5033
4379
  );
5034
4380
  }
5035
- if (failedInstalls.size > 0) {
4381
+ if (installFailed) {
5036
4382
  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.`
4383
+ '\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
4384
  );
5039
4385
  }
5040
4386
  return {
@@ -5042,10 +4388,10 @@ ${run2.output}` : status;
5042
4388
  filesChanged,
5043
4389
  summary: summaries.join("\n\n"),
5044
4390
  worktreePath: worktree,
5045
- ...useCases.includes("ingestion") && ingestEntrypoint ? {
4391
+ ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
5046
4392
  ingestCommand: buildIngestCommand(
5047
4393
  worktree,
5048
- toolchain,
4394
+ ingestRuntime,
5049
4395
  ingestEntrypoint
5050
4396
  ),
5051
4397
  ingestScriptRan,
@@ -5095,8 +4441,8 @@ var defaultWorkflow = {
5095
4441
  defineStep({
5096
4442
  id: "select-index",
5097
4443
  title: "Set up index",
5098
- outputSchema: z25.object({
5099
- selection: z25.string()
4444
+ outputSchema: z27.object({
4445
+ selection: z27.string()
5100
4446
  }),
5101
4447
  run: (ctx) => selectIndexStep(ctx)
5102
4448
  }),
@@ -5374,20 +4720,20 @@ function parseCliArgs(argv) {
5374
4720
  }
5375
4721
 
5376
4722
  // src/lib/resetState.ts
5377
- import { readdir as readdir5, rm as rm2 } from "node:fs/promises";
5378
- import { join as join14 } from "node:path";
4723
+ import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4724
+ import { join as join11 } from "node:path";
5379
4725
  var KEEP = ["wizard.log"];
5380
4726
  async function resetProjectState() {
5381
4727
  const dir = stateDir();
5382
4728
  let entries;
5383
4729
  try {
5384
- entries = await readdir5(dir);
4730
+ entries = await readdir4(dir);
5385
4731
  } catch {
5386
4732
  return { dir, removed: [] };
5387
4733
  }
5388
4734
  const targets = entries.filter((name) => !KEEP.includes(name));
5389
4735
  await Promise.all(
5390
- targets.map((name) => rm2(join14(dir, name), { recursive: true, force: true }))
4736
+ targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
5391
4737
  );
5392
4738
  return { dir, removed: targets };
5393
4739
  }
@@ -5442,31 +4788,38 @@ ${formatStepList(workflow)}`);
5442
4788
  }
5443
4789
  async function run(workflow) {
5444
4790
  const store = useWizard.getState();
5445
- let instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4791
+ const instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4792
+ await store.waitForStart();
5446
4793
  let user = await getUser();
5447
4794
  if (!user) {
5448
- await instance.waitUntilRenderFlush();
5449
- instance.cleanup();
4795
+ store.beginAuth();
5450
4796
  try {
5451
4797
  await runAuthLogin();
5452
4798
  } catch (err) {
5453
- console.error(err instanceof Error ? err.message : String(err));
4799
+ store.setError(err instanceof Error ? err.message : String(err));
4800
+ await instance.waitUntilExit();
5454
4801
  process.exit(1);
5455
4802
  }
5456
- instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
4803
+ store.endAuth();
5457
4804
  user = await getUser();
5458
4805
  if (!user) {
5459
4806
  store.setError(
5460
- "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli auth login` directly."
4807
+ "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli@latest auth login` directly."
5461
4808
  );
5462
4809
  await instance.waitUntilExit();
5463
4810
  process.exit(1);
5464
4811
  }
5465
4812
  }
5466
4813
  store.setUser(user);
5467
- const profile = await loadActiveProfile();
5468
- await store.waitForStart();
5469
- runWorkflow(workflow, profile?.appId);
4814
+ let app;
4815
+ try {
4816
+ app = await ensureApplication();
4817
+ } catch (err) {
4818
+ store.setError(err instanceof Error ? err.message : String(err));
4819
+ await instance.waitUntilExit();
4820
+ process.exit(1);
4821
+ }
4822
+ runWorkflow(workflow, app.id);
5470
4823
  }
5471
4824
  var started = await startup();
5472
4825
  if (typeof started === "number") {