@algolia/wizard 0.9.0 → 0.11.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/main.js +1143 -582
  3. package/package.json +2 -3
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/ui/App.tsx
7
- import { Box as Box14, Text as Text14, useApp, useInput as useInput6, useWindowSize as useWindowSize7 } from "ink";
7
+ import { Box as 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,90 @@ 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 mask(text, secret) {
21
+ return secret ? text.replaceAll(secret, "***") : text;
22
+ }
23
+ function lineSplitter(emit) {
24
+ let buffer = "";
25
+ return {
26
+ push(chunk) {
27
+ buffer += chunk;
28
+ const lines = buffer.split("\n");
29
+ buffer = lines.pop() ?? "";
30
+ for (const line of lines) emit(line.replace(/\r$/, ""));
31
+ },
32
+ flush() {
33
+ if (buffer) emit(buffer.replace(/\r$/, ""));
34
+ buffer = "";
35
+ }
36
+ };
37
+ }
38
+ var wizardSink = (stream, line) => {
39
+ if (!line.trim()) return;
40
+ useWizard.getState().pushCliOutput(stream, line);
41
+ };
42
+ var stderrSink = (stream, line) => {
43
+ if (stream === "stdout") return;
44
+ wizardSink(stream, line);
45
+ };
46
+ function runAlgoliaCli(args, { onOutput, redact } = {}) {
47
+ const store = useWizard.getState();
48
+ const command = mask(args.join(" "), redact);
49
+ const logId = store.logStart("tool", `algolia ${command}`);
21
50
  return new Promise((resolve4, reject) => {
22
- const child = spawn(process.execPath, [algoliaCliEntry(), ...args]);
51
+ const child = spawn("npx", npxArgs(args), { shell });
23
52
  let stdout = "";
24
53
  let stderr = "";
25
- child.stdout.on("data", (chunk) => stdout += chunk);
26
- child.stderr.on("data", (chunk) => stderr += chunk);
54
+ const splitters = {
55
+ stdout: lineSplitter((line) => onOutput?.("stdout", line)),
56
+ stderr: lineSplitter((line) => onOutput?.("stderr", line))
57
+ };
58
+ child.stdout.on("data", (chunk) => {
59
+ const text = String(chunk);
60
+ stdout += text;
61
+ splitters.stdout.push(text);
62
+ });
63
+ child.stderr.on("data", (chunk) => {
64
+ const text = String(chunk);
65
+ stderr += text;
66
+ splitters.stderr.push(text);
67
+ });
27
68
  child.on("error", reject);
28
69
  child.on("close", (code) => {
70
+ splitters.stdout.flush();
71
+ splitters.stderr.flush();
29
72
  if (code === 0) {
30
73
  resolve4(stdout);
31
74
  } else {
32
- const detail = stderr.trim() || stdout.trim();
75
+ const failed = stderr.trim();
76
+ let detail = "";
77
+ if (failed) {
78
+ detail = `: ${mask(failed, redact)}`;
79
+ } else if (stdout.trim()) {
80
+ detail = " (no stderr; stdout withheld \u2014 it may contain credentials)";
81
+ }
33
82
  reject(
34
83
  new Error(
35
- `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail ? `: ${detail}` : ""}`
84
+ `Algolia CLI \`${command}\` failed (exit ${code})${detail}`
36
85
  )
37
86
  );
38
87
  }
39
88
  });
40
- });
89
+ }).then(
90
+ (out) => {
91
+ useWizard.getState().logEnd(logId, "success");
92
+ return out;
93
+ },
94
+ (err) => {
95
+ useWizard.getState().logEnd(logId, "error");
96
+ throw err;
97
+ }
98
+ );
41
99
  }
42
100
  async function getUser() {
43
101
  let raw;
@@ -52,19 +110,23 @@ async function getUser() {
52
110
  return null;
53
111
  }
54
112
  }
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
- });
113
+ var loginResultSchema = z.object({
114
+ success: z.boolean(),
115
+ email: z.string().optional()
116
+ });
117
+ async function runAuthLogin() {
118
+ const raw = await runAlgoliaCli(["auth", "login", "--non-interactive"], {
119
+ onOutput: stderrSink
67
120
  });
121
+ let parsed;
122
+ try {
123
+ parsed = loginResultSchema.safeParse(JSON.parse(raw));
124
+ } catch {
125
+ parsed = void 0;
126
+ }
127
+ if (parsed?.success && !parsed.data.success) {
128
+ throw new Error("Algolia sign-in did not report success.");
129
+ }
68
130
  }
69
131
 
70
132
  // src/lib/auth.ts
@@ -100,10 +162,14 @@ import { join, resolve } from "node:path";
100
162
  function rootDir() {
101
163
  return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
102
164
  }
103
- function projectSlug(cwd = process.cwd()) {
165
+ var pinnedRoot;
166
+ function setProjectRoot(cwd) {
167
+ pinnedRoot = resolve(cwd);
168
+ }
169
+ function projectSlug(cwd = pinnedRoot ?? process.cwd()) {
104
170
  return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
105
171
  }
106
- function stateDir(cwd = process.cwd()) {
172
+ function stateDir(cwd = pinnedRoot ?? process.cwd()) {
107
173
  return join(rootDir(), projectSlug(cwd));
108
174
  }
109
175
 
@@ -171,6 +237,7 @@ function describeInputValue(value) {
171
237
  return Array.isArray(value) ? value.join(", ") : value;
172
238
  }
173
239
  var NOTICE_INTERVAL_MS = 2e3;
240
+ var CLI_OUTPUT_LIMIT = 200;
174
241
  var useWizard = create((set, get) => ({
175
242
  phase: "idle",
176
243
  homeScreen: "home",
@@ -182,22 +249,21 @@ var useWizard = create((set, get) => ({
182
249
  notices: [],
183
250
  _noticeQueue: [],
184
251
  _noticeTimer: null,
252
+ cliOutput: [],
253
+ targetIndex: null,
185
254
  logs: [],
186
255
  error: null,
187
256
  inputReq: null,
188
257
  _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.
258
+ // `endAuth` lands on 'preflight', not 'idle': sign-in happens after the
259
+ // welcome screen, so going back would gate the run a second time.
260
+ beginAuth: () => set({ phase: "authenticating", cliOutput: [] }),
261
+ endAuth: () => set((s) => s.phase === "authenticating" ? { phase: "preflight" } : {}),
192
262
  confirmStart: () => set(
193
263
  (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
194
264
  ),
195
- // Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
196
265
  openLearnMore: () => set({ homeScreen: "learnMore" }),
197
266
  backToHome: () => set({ homeScreen: "home" }),
198
- // Resolves once the phase leaves 'idle', whether that happens before or
199
- // after this is called (the welcome screen's enter handler is what
200
- // drives the transition via `confirmStart`).
201
267
  waitForStart: () => new Promise((resolve4) => {
202
268
  if (get().phase !== "idle") {
203
269
  resolve4();
@@ -220,15 +286,19 @@ var useWizard = create((set, get) => ({
220
286
  syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
221
287
  setActiveStep: (index) => {
222
288
  get()._clearNoticeQueue();
223
- set({ phase: "running", currentStepIndex: index, output: "", notices: [] });
289
+ set({
290
+ phase: "running",
291
+ currentStepIndex: index,
292
+ output: "",
293
+ notices: [],
294
+ cliOutput: []
295
+ });
224
296
  },
225
297
  setUser: (user) => set({ user }),
226
298
  appendToken: (text) => set((s) => ({ output: s.output + text })),
227
299
  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.
300
+ // The timer stays armed through an empty drain, so the spacing covers the
301
+ // time since the last render even across bursts.
232
302
  pushNotice: (notice) => {
233
303
  const { notices, _noticeQueue, _noticeTimer } = get();
234
304
  if (_noticeTimer === null) {
@@ -261,6 +331,13 @@ var useWizard = create((set, get) => ({
261
331
  get()._clearNoticeQueue();
262
332
  set({ notices: [] });
263
333
  },
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,6 @@ 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.
289
363
  submitInput: async (value) => {
290
364
  await markInteraction();
291
365
  get()._resolve?.(value);
@@ -305,6 +379,8 @@ var useWizard = create((set, get) => ({
305
379
  currentStepIndex: 0,
306
380
  output: "",
307
381
  notices: [],
382
+ cliOutput: [],
383
+ targetIndex: null,
308
384
  logs: [],
309
385
  error: null,
310
386
  inputReq: null,
@@ -313,16 +389,100 @@ var useWizard = create((set, get) => ({
313
389
  }
314
390
  }));
315
391
 
392
+ // src/ui/CliOutput.tsx
393
+ import { Box, Text, useWindowSize } from "ink";
394
+
395
+ // src/ui/theme.ts
396
+ var MARKER = {
397
+ pending: "\u25CB",
398
+ running: "\u25D0",
399
+ done: "\u2713",
400
+ error: "\u2716"
401
+ };
402
+ var BRAND = "#003DFF";
403
+ var SECONDARY = "#5468FF";
404
+ var DANGER = "#F86E7E";
405
+ var COLORS = {
406
+ brand: BRAND,
407
+ primary: "#E6EDF3",
408
+ secondary: SECONDARY,
409
+ strong: "#FFFFFF",
410
+ muted: "#8B949E",
411
+ dim: "#484F58",
412
+ highlight: { bg: "#12331C", fg: "#4ADE80" },
413
+ badge: "#E3B341",
414
+ danger: DANGER,
415
+ success: "#4ADE80",
416
+ bg: {
417
+ main: "#0B0E14",
418
+ sidebar: "#14171E"
419
+ },
420
+ border: "#30363D",
421
+ accent: "#76A0FF",
422
+ status: {
423
+ pending: "gray",
424
+ running: "#76A0FF",
425
+ done: "#4ADE80",
426
+ error: DANGER
427
+ }
428
+ };
429
+
430
+ // src/ui/CliOutput.tsx
431
+ import { jsxs } from "react/jsx-runtime";
432
+ var CLI_MARKER = "\u203A";
433
+ var RESERVED_ROWS = 16;
434
+ var MAX_ROWS = 12;
435
+ var PANEL_TEXT_WIDTH = 45;
436
+ var URL_PATTERN = /https?:\/\//;
437
+ function rowCost(text) {
438
+ return URL_PATTERN.test(text) ? Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH)) : 1;
439
+ }
440
+ function CliOutput() {
441
+ const cliOutput = useWizard((s) => s.cliOutput);
442
+ const { rows } = useWindowSize();
443
+ if (!cliOutput.length) return null;
444
+ const rowBudget = Math.min(Math.max(rows - RESERVED_ROWS, 3), MAX_ROWS);
445
+ const visible = [];
446
+ let usedRows = 0;
447
+ for (let i = cliOutput.length - 1; i >= 0; i--) {
448
+ const cost = rowCost(cliOutput[i].text);
449
+ if (usedRows + cost > rowBudget && visible.length > 0) break;
450
+ visible.unshift(cliOutput[i]);
451
+ usedRows += cost;
452
+ }
453
+ const hidden = cliOutput.length - visible.length;
454
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
455
+ hidden > 0 && /* @__PURE__ */ jsxs(Text, { color: COLORS.dim, children: [
456
+ "\u2191 ",
457
+ hidden,
458
+ " earlier line(s)"
459
+ ] }),
460
+ visible.map((line) => /* @__PURE__ */ jsxs(
461
+ Text,
462
+ {
463
+ color: line.stream === "stderr" ? COLORS.muted : COLORS.dim,
464
+ wrap: URL_PATTERN.test(line.text) ? "wrap" : "truncate",
465
+ children: [
466
+ CLI_MARKER,
467
+ " ",
468
+ line.text
469
+ ]
470
+ },
471
+ line.id
472
+ ))
473
+ ] });
474
+ }
475
+
316
476
  // src/ui/Notices.tsx
317
- import { Box as Box2, Text as Text2, useWindowSize as useWindowSize2 } from "ink";
477
+ import { Box as Box3, Text as Text3, useWindowSize as useWindowSize3 } from "ink";
318
478
  import { useEffect as useEffect2, useState as useState2 } from "react";
319
479
 
320
480
  // src/ui/Table.tsx
321
- import { Box, Text, measureElement, useWindowSize } from "ink";
481
+ import { Box as Box2, Text as Text2, measureElement, useWindowSize as useWindowSize2 } from "ink";
322
482
  import { useEffect, useRef, useState } from "react";
323
483
  import { jsx } from "react/jsx-runtime";
324
484
  function Table({ columns, rows }) {
325
- const { columns: termCols } = useWindowSize();
485
+ const { columns: termCols } = useWindowSize2();
326
486
  const ref = useRef(null);
327
487
  const [width, setWidth] = useState(0);
328
488
  useEffect(() => {
@@ -330,7 +490,7 @@ function Table({ columns, rows }) {
330
490
  }, [termCols, columns, rows]);
331
491
  if (rows.length === 0) return null;
332
492
  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}`)) });
493
+ return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text2, { wrap: "truncate", children: line }, `tbl-${i}`)) });
334
494
  }
335
495
  function formatTable(columns, rows, width) {
336
496
  const natural = columns.map(
@@ -370,48 +530,13 @@ function resize(widths, budget) {
370
530
  }
371
531
  var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, width) : `${s.slice(0, width - 1)}\u2026`;
372
532
 
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
533
  // src/ui/Notices.tsx
409
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
534
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
410
535
  var AGENT_MARKER = "\u2726";
411
- var RESERVED_ROWS = 14;
412
- var PANEL_TEXT_WIDTH = 45;
536
+ var RESERVED_ROWS2 = 14;
537
+ var PANEL_TEXT_WIDTH2 = 45;
413
538
  function messageLineCount(text) {
414
- return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH));
539
+ return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH2));
415
540
  }
416
541
  function noticeLineCount(notice) {
417
542
  const messageLines = (notice.messages ?? []).reduce((sum, m) => {
@@ -422,7 +547,7 @@ function noticeLineCount(notice) {
422
547
  return messageLines + tableLines;
423
548
  }
424
549
  function fitVisibleNotices(notices, windowRows) {
425
- const budget = Math.max(windowRows - RESERVED_ROWS, 3);
550
+ const budget = Math.max(windowRows - RESERVED_ROWS2, 3);
426
551
  let used = 0;
427
552
  let count = 0;
428
553
  for (let i = notices.length - 1; i >= 0; i--) {
@@ -455,7 +580,7 @@ function parseHex(hex) {
455
580
  }
456
581
  function Notices() {
457
582
  const notices = useWizard((s) => s.notices);
458
- const { rows: windowRows } = useWindowSize2();
583
+ const { rows: windowRows } = useWindowSize3();
459
584
  const visible = fitVisibleNotices(notices, windowRows);
460
585
  const [pulseStep, setPulseStep] = useState2(0);
461
586
  useEffect2(() => {
@@ -472,14 +597,14 @@ function Notices() {
472
597
  }, []);
473
598
  if (!visible.length) return null;
474
599
  const pulseColor = PULSE_COLORS[pulseStep];
475
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
600
+ return /* @__PURE__ */ jsx2(Box3, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
476
601
  const isLatest = i === visible.length - 1;
477
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
602
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
478
603
  notice.messages?.map((m, j) => {
479
604
  const line = typeof m === "string" ? { text: m } : m;
480
605
  const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
481
- return /* @__PURE__ */ jsxs(
482
- Text2,
606
+ return /* @__PURE__ */ jsxs2(
607
+ Text3,
483
608
  {
484
609
  color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
485
610
  bold: line.bold,
@@ -497,41 +622,41 @@ function Notices() {
497
622
  }
498
623
 
499
624
  // src/ui/PromptInput.tsx
500
- import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
625
+ import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
501
626
  import TextInput from "ink-text-input";
502
627
  import { useState as useState5 } from "react";
503
628
 
504
629
  // 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";
630
+ import { Box as Box4, Text as Text4 } from "ink";
631
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
507
632
  function NextAction({
508
633
  action,
509
634
  keyHint,
510
635
  hierarchy = "primary"
511
636
  }) {
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 })
637
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
638
+ hierarchy === "primary" && /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `> ${action}` }),
639
+ hierarchy === "secondary" && /* @__PURE__ */ jsxs3(Fragment, { children: [
640
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `>` }),
641
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, bold: true, children: action })
517
642
  ] }),
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: `]` })
643
+ /* @__PURE__ */ jsxs3(Box4, { children: [
644
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: "press " }),
645
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `[` }),
646
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, children: keyHint }),
647
+ /* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `]` })
523
648
  ] })
524
649
  ] });
525
650
  }
526
651
 
527
652
  // src/ui/SelectPrompt.tsx
528
- import { Box as Box5, Text as Text5, useInput, useWindowSize as useWindowSize4 } from "ink";
653
+ import { Box as Box6, Text as Text6, useInput, useWindowSize as useWindowSize5 } from "ink";
529
654
  import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
530
655
 
531
656
  // src/ui/ScrollView.tsx
532
- import { Box as Box4, Text as Text4, measureElement as measureElement2, useWindowSize as useWindowSize3 } from "ink";
657
+ import { Box as Box5, Text as Text5, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
533
658
  import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
534
- import { jsxs as jsxs3 } from "react/jsx-runtime";
659
+ import { jsxs as jsxs4 } from "react/jsx-runtime";
535
660
  var INDICATOR_ROWS = 2;
536
661
  function fittedWidth(node, columns) {
537
662
  let left = 0;
@@ -546,7 +671,7 @@ function useScrollWindow({
546
671
  followBottom = false
547
672
  }) {
548
673
  const viewportRef = useRef2(null);
549
- const { columns } = useWindowSize3();
674
+ const { columns } = useWindowSize4();
550
675
  const [size, setSize] = useState3(
551
676
  null
552
677
  );
@@ -601,14 +726,14 @@ function useScrollWindow({
601
726
  };
602
727
  }
603
728
  function ScrollView({ scroll, children }) {
604
- return /* @__PURE__ */ jsxs3(Box4, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
605
- scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
729
+ return /* @__PURE__ */ jsxs4(Box5, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
730
+ scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
606
731
  "\u2191 ",
607
732
  scroll.hiddenAbove,
608
733
  " more"
609
734
  ] }),
610
735
  children,
611
- scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
736
+ scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
612
737
  "\u2193 ",
613
738
  scroll.hiddenBelow,
614
739
  " more"
@@ -617,7 +742,7 @@ function ScrollView({ scroll, children }) {
617
742
  }
618
743
 
619
744
  // src/ui/SelectPrompt.tsx
620
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
745
+ import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
621
746
  var CANCEL = "cancel";
622
747
  var ARROW_WIDTH = 4;
623
748
  var COLUMN_GAP = 2;
@@ -648,7 +773,7 @@ function SelectPrompt({
648
773
  if (multi) hints.push({ key: "[space]", label: "select" });
649
774
  hints.push({ key: "[enter]", label: "confirm" });
650
775
  const containerRef = useRef3(null);
651
- const { columns } = useWindowSize4();
776
+ const { columns } = useWindowSize5();
652
777
  const [width, setWidth] = useState4(columns);
653
778
  useLayoutEffect2(() => {
654
779
  if (!containerRef.current) return;
@@ -702,14 +827,14 @@ function SelectPrompt({
702
827
  }
703
828
  }
704
829
  });
705
- return /* @__PURE__ */ jsx4(Box5, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, width, children: [
706
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
707
- error && /* @__PURE__ */ jsx4(Text5, { color: COLORS.danger, children: error }),
708
- messages?.map((m, i) => /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
830
+ return /* @__PURE__ */ jsx4(Box6, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, width, children: [
831
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
832
+ error && /* @__PURE__ */ jsx4(Text6, { color: COLORS.danger, children: error }),
833
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
709
834
  table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
710
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
711
- question && /* @__PURE__ */ jsx4(Text5, { color: COLORS.muted, children: question }),
712
- helpText && /* @__PURE__ */ jsx4(Text5, { color: COLORS.dim, children: helpText })
835
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
836
+ question && /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: question }),
837
+ helpText && /* @__PURE__ */ jsx4(Text6, { color: COLORS.dim, children: helpText })
713
838
  ] })
714
839
  ] }),
715
840
  /* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
@@ -719,39 +844,39 @@ function SelectPrompt({
719
844
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
720
845
  const sec = isCancel ? void 0 : secondary?.[i];
721
846
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
722
- const label = /* @__PURE__ */ jsxs4(Text5, { color: labelColor, wrap: "truncate", children: [
847
+ const label = /* @__PURE__ */ jsxs5(Text6, { color: labelColor, wrap: "truncate", children: [
723
848
  highlighted ? "\u276F " : " ",
724
849
  bullet,
725
850
  option
726
851
  ] });
727
852
  const isText = sec?.kind === "text";
728
- return /* @__PURE__ */ jsxs4(
729
- Box5,
853
+ return /* @__PURE__ */ jsxs5(
854
+ Box6,
730
855
  {
731
856
  width: isText ? "100%" : barWidth,
732
857
  paddingX: 1,
733
858
  paddingY: 1,
734
859
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
735
860
  children: [
736
- /* @__PURE__ */ jsx4(Box5, { width: isText ? labelWidth : barLabelWidth, children: label }),
737
- isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box5, { width: textWidth, children: /* @__PURE__ */ jsx4(
738
- Text5,
861
+ /* @__PURE__ */ jsx4(Box6, { width: isText ? labelWidth : barLabelWidth, children: label }),
862
+ isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box6, { width: textWidth, children: /* @__PURE__ */ jsx4(
863
+ Text6,
739
864
  {
740
865
  wrap: "truncate",
741
866
  color: highlighted ? COLORS.primary : COLORS.muted,
742
867
  children: sec.value
743
868
  }
744
869
  ) }),
745
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box5, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text5, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
870
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box6, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text6, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
746
871
  ]
747
872
  },
748
873
  `row-${i}`
749
874
  );
750
875
  }) }),
751
- /* @__PURE__ */ jsx4(Box5, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text5, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs4(Text5, { children: [
876
+ /* @__PURE__ */ jsx4(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text6, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs5(Text6, { children: [
752
877
  i > 0 ? " " : "",
753
- /* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, children: key }),
754
- /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
878
+ /* @__PURE__ */ jsx4(Text6, { color: COLORS.primary, children: key }),
879
+ /* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
755
880
  " ",
756
881
  label
757
882
  ] })
@@ -760,7 +885,7 @@ function SelectPrompt({
760
885
  }
761
886
 
762
887
  // src/ui/PromptInput.tsx
763
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
888
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
764
889
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
765
890
  function EnterToContinuePrompt({
766
891
  question,
@@ -771,10 +896,10 @@ function EnterToContinuePrompt({
771
896
  if (key.return) onDecide(true);
772
897
  else if (key.escape) onDecide(false);
773
898
  });
774
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, children: [
775
- messages?.map((m, i) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
776
- question && /* @__PURE__ */ jsx5(Text6, { color: COLORS.primary, children: question }),
777
- /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
899
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, children: [
900
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
901
+ question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: question }),
902
+ /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
778
903
  /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
779
904
  /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
780
905
  ] })
@@ -784,11 +909,11 @@ function PromptInput() {
784
909
  const { phase, inputReq, submitInput } = useWizard();
785
910
  const [draft, setDraft] = useState5("");
786
911
  if (phase === "done" || phase === "error") {
787
- return /* @__PURE__ */ jsx5(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text6, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
912
+ return /* @__PURE__ */ jsx5(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text7, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
788
913
  }
789
914
  if (phase !== "awaitingInput" || !inputReq) return null;
790
915
  if (inputReq.promptType === "multipleChoice") {
791
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
916
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
792
917
  SelectPrompt,
793
918
  {
794
919
  question: inputReq.prompt,
@@ -805,7 +930,7 @@ function PromptInput() {
805
930
  ) });
806
931
  }
807
932
  if (inputReq.promptType === "multiSelect") {
808
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
933
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
809
934
  SelectPrompt,
810
935
  {
811
936
  multi: true,
@@ -820,7 +945,7 @@ function PromptInput() {
820
945
  ) });
821
946
  }
822
947
  if (inputReq.promptType === "notice") {
823
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
948
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
824
949
  SelectPrompt,
825
950
  {
826
951
  question: inputReq.prompt,
@@ -842,7 +967,7 @@ function PromptInput() {
842
967
  }
843
968
  if (inputReq.promptType === "acceptReject") {
844
969
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
845
- return /* @__PURE__ */ jsx5(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
970
+ return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
846
971
  SelectPrompt,
847
972
  {
848
973
  question: inputReq.prompt,
@@ -853,11 +978,11 @@ function PromptInput() {
853
978
  }
854
979
  ) });
855
980
  }
856
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
857
- inputReq.error && /* @__PURE__ */ jsx5(Text6, { color: COLORS.danger, children: inputReq.error }),
858
- inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
859
- /* @__PURE__ */ jsxs5(Box6, { children: [
860
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.primary, children: [
981
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
982
+ inputReq.error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: inputReq.error }),
983
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
984
+ /* @__PURE__ */ jsxs6(Box7, { children: [
985
+ /* @__PURE__ */ jsxs6(Text7, { color: COLORS.primary, children: [
861
986
  inputReq.prompt,
862
987
  " "
863
988
  ] }),
@@ -879,7 +1004,7 @@ function PromptInput() {
879
1004
  // src/ui/Welcome.tsx
880
1005
  import { dirname as dirname2, join as join3 } from "node:path";
881
1006
  import { fileURLToPath } from "node:url";
882
- import { Box as Box7, Spacer, Text as Text7, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
1007
+ import { Box as Box8, Spacer, Text as Text8, useInput as useInput3, useWindowSize as useWindowSize6 } from "ink";
883
1008
 
884
1009
  // src/ui/copy/welcome.ts
885
1010
  var sidebarItems = [
@@ -907,27 +1032,27 @@ var sidebarItems = [
907
1032
 
908
1033
  // src/ui/Welcome.tsx
909
1034
  import Image, { InkPictureProvider } from "ink-picture";
910
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1035
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
911
1036
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
912
1037
  function SidebarItem({
913
1038
  title,
914
1039
  description
915
1040
  }) {
916
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
917
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, children: [
918
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.success, children: "\u2192" }),
919
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.strong, bold: true, children: title })
1041
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1042
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, children: [
1043
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.success, children: "\u2192" }),
1044
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.strong, bold: true, children: title })
920
1045
  ] }),
921
- /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 2, children: [
1046
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 2, children: [
922
1047
  /* @__PURE__ */ jsx6(Spacer, {}),
923
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: description })
1048
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: description })
924
1049
  ] })
925
1050
  ] });
926
1051
  }
927
1052
  function Welcome() {
928
1053
  const confirmStart = useWizard((s) => s.confirmStart);
929
1054
  const openLearnMore = useWizard((s) => s.openLearnMore);
930
- const { rows } = useWindowSize5();
1055
+ const { rows } = useWindowSize6();
931
1056
  useInput3((input, key) => {
932
1057
  if (key.return) confirmStart();
933
1058
  else if (input === "i") openLearnMore();
@@ -946,15 +1071,15 @@ function Welcome() {
946
1071
  if (rows < 30) {
947
1072
  layout = scales["small"];
948
1073
  }
949
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1074
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
950
1075
  /* @__PURE__ */ jsx6(
951
- Box7,
1076
+ Box8,
952
1077
  {
953
1078
  paddingY: layout.main.padding.y,
954
1079
  paddingX: layout.main.padding.x,
955
1080
  flexDirection: "column",
956
1081
  justifyContent: "center",
957
- children: /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 2, children: [
1082
+ children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
958
1083
  /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
959
1084
  Image,
960
1085
  {
@@ -966,16 +1091,16 @@ function Welcome() {
966
1091
  protocol: "halfBlock"
967
1092
  }
968
1093
  ) }),
969
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
970
- /* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
1094
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1095
+ /* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
971
1096
  /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
972
1097
  /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
973
1098
  ] })
974
1099
  ] })
975
1100
  }
976
1101
  ),
977
- /* @__PURE__ */ jsxs6(
978
- Box7,
1102
+ /* @__PURE__ */ jsxs7(
1103
+ Box8,
979
1104
  {
980
1105
  backgroundColor: COLORS.bg.sidebar,
981
1106
  width: 40,
@@ -985,7 +1110,7 @@ function Welcome() {
985
1110
  flexDirection: "column",
986
1111
  justifyContent: "center",
987
1112
  children: [
988
- /* @__PURE__ */ jsx6(Text7, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1113
+ /* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
989
1114
  sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
990
1115
  ]
991
1116
  }
@@ -995,7 +1120,7 @@ function Welcome() {
995
1120
 
996
1121
  // src/ui/LearnMore.tsx
997
1122
  import { Fragment as Fragment2 } from "react";
998
- import { Box as Box8, Text as Text8, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
1123
+ import { Box as Box9, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize7 } from "ink";
999
1124
 
1000
1125
  // src/ui/copy/learn-more.ts
1001
1126
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -1032,7 +1157,7 @@ var policyLinks = [
1032
1157
  ];
1033
1158
 
1034
1159
  // src/ui/LearnMore.tsx
1035
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1160
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
1036
1161
  var TAG_COLORS = {
1037
1162
  READ: COLORS.success,
1038
1163
  WRITE: COLORS.badge,
@@ -1048,25 +1173,25 @@ function NeverLine({
1048
1173
  }) {
1049
1174
  const used = segments.reduce((n, s) => n + s.text.length, 0);
1050
1175
  const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
1051
- return /* @__PURE__ */ jsxs7(Text8, { children: [
1052
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: "\u2502" }),
1176
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
1177
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" }),
1053
1178
  " ".repeat(NEVER_BOX_PAD_X),
1054
- segments.map((s, i) => /* @__PURE__ */ jsx7(Text8, { color: s.color, bold: s.bold, children: s.text }, i)),
1179
+ segments.map((s, i) => /* @__PURE__ */ jsx7(Text9, { color: s.color, bold: s.bold, children: s.text }, i)),
1055
1180
  " ".repeat(rightPad),
1056
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: "\u2502" })
1181
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" })
1057
1182
  ] });
1058
1183
  }
1059
1184
  function LearnMore() {
1060
1185
  const confirmStart = useWizard((s) => s.confirmStart);
1061
1186
  const backToHome = useWizard((s) => s.backToHome);
1062
- const { columns } = useWindowSize6();
1187
+ const { columns } = useWindowSize7();
1063
1188
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1064
1189
  useInput4((_input, key) => {
1065
1190
  if (key.escape) backToHome();
1066
1191
  else if (key.return) confirmStart();
1067
1192
  });
1068
- return /* @__PURE__ */ jsxs7(
1069
- Box8,
1193
+ return /* @__PURE__ */ jsxs8(
1194
+ Box9,
1070
1195
  {
1071
1196
  flexDirection: "column",
1072
1197
  paddingX: PADDING_X,
@@ -1074,20 +1199,20 @@ function LearnMore() {
1074
1199
  width: "100%",
1075
1200
  gap: 1,
1076
1201
  children: [
1077
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1078
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: accessIntro }),
1079
- /* @__PURE__ */ jsx7(Box8, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", marginTop: 1, children: [
1080
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1081
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1082
- /* @__PURE__ */ jsx7(Box8, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text8, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1083
- /* @__PURE__ */ jsx7(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text8, { children: [
1084
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: item.title }),
1085
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1202
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1203
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: accessIntro }),
1204
+ /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginTop: 1, children: [
1205
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1206
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1207
+ /* @__PURE__ */ jsx7(Box9, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text9, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1208
+ /* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { children: [
1209
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: item.title }),
1210
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1086
1211
  ] }) })
1087
1212
  ] })
1088
1213
  ] }, item.tag)) }),
1089
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
1090
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1214
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
1215
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1091
1216
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1092
1217
  /* @__PURE__ */ jsx7(
1093
1218
  NeverLine,
@@ -1096,7 +1221,7 @@ function LearnMore() {
1096
1221
  segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1097
1222
  }
1098
1223
  ),
1099
- neverItems.map((item) => /* @__PURE__ */ jsxs7(Fragment2, { children: [
1224
+ neverItems.map((item) => /* @__PURE__ */ jsxs8(Fragment2, { children: [
1100
1225
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1101
1226
  /* @__PURE__ */ jsx7(
1102
1227
  NeverLine,
@@ -1111,23 +1236,23 @@ function LearnMore() {
1111
1236
  )
1112
1237
  ] }, item)),
1113
1238
  /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
1114
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1239
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1115
1240
  ] }),
1116
- /* @__PURE__ */ jsx7(Box8, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1117
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1118
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.accent, children: link.url })
1241
+ /* @__PURE__ */ jsx7(Box9, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1242
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1243
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.accent, children: link.url })
1119
1244
  ] }, link.label)) }),
1120
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1121
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1122
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "[" }),
1123
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.primary, children: "esc" }),
1124
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "] back" })
1245
+ /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1246
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1247
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1248
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "esc" }),
1249
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "] back" })
1125
1250
  ] }),
1126
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
1127
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "[" }),
1128
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.primary, children: "enter" }),
1129
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.muted, children: "]" }),
1130
- /* @__PURE__ */ jsx7(Text8, { color: COLORS.success, bold: true, children: "start wizard" })
1251
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1252
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
1253
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "enter" }),
1254
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "]" }),
1255
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.success, bold: true, children: "start wizard" })
1131
1256
  ] })
1132
1257
  ] })
1133
1258
  ]
@@ -1136,10 +1261,10 @@ function LearnMore() {
1136
1261
  }
1137
1262
 
1138
1263
  // src/ui/Sidebar.tsx
1139
- import { Box as Box11, Text as Text11 } from "ink";
1264
+ import { Box as Box12, Text as Text12 } from "ink";
1140
1265
 
1141
1266
  // src/ui/Steps.tsx
1142
- import { Box as Box9, Text as Text9 } from "ink";
1267
+ import { Box as Box10, Text as Text10 } from "ink";
1143
1268
  import Spinner from "ink-spinner";
1144
1269
 
1145
1270
  // src/core/persistence.ts
@@ -1168,11 +1293,11 @@ async function clearWorkflowState(workflowId) {
1168
1293
  }
1169
1294
 
1170
1295
  // src/ui/Steps.tsx
1171
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1296
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1172
1297
  function Steps() {
1173
1298
  const { steps } = useWizard();
1174
1299
  const visibleSteps = steps.filter(isStepVisible);
1175
- return /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { color: COLORS.status[s.status], children: [
1300
+ 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: [
1176
1301
  s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1177
1302
  " ",
1178
1303
  s.title
@@ -1182,7 +1307,7 @@ function CurrentStep() {
1182
1307
  const { steps } = useWizard();
1183
1308
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1184
1309
  if (!currentStep) return null;
1185
- return /* @__PURE__ */ jsxs8(Text9, { color: COLORS.status.running, children: [
1310
+ return /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.running, children: [
1186
1311
  /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
1187
1312
  " ",
1188
1313
  ` ${currentStep.title}`
@@ -1190,19 +1315,19 @@ function CurrentStep() {
1190
1315
  }
1191
1316
 
1192
1317
  // src/ui/Progress.tsx
1193
- import { Box as Box10, Text as Text10 } from "ink";
1194
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1318
+ import { Box as Box11, Text as Text11 } from "ink";
1319
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1195
1320
  function Progress() {
1196
1321
  const { steps, currentStepIndex } = useWizard();
1197
1322
  const visibleSteps = steps.filter(isStepVisible);
1198
1323
  if (visibleSteps.length === 0) return null;
1199
1324
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1200
1325
  const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
1201
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1202
- /* @__PURE__ */ jsx9(Text10, { color: COLORS.muted, children: "STEP" }),
1203
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: activeStepNumber }),
1204
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: "/" }),
1205
- /* @__PURE__ */ jsx9(Text10, { bold: true, children: visibleSteps.length })
1326
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1327
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "STEP" }),
1328
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: activeStepNumber }),
1329
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: "/" }),
1330
+ /* @__PURE__ */ jsx9(Text11, { bold: true, children: visibleSteps.length })
1206
1331
  ] });
1207
1332
  }
1208
1333
 
@@ -1213,10 +1338,10 @@ var sidebarCommands = [
1213
1338
  ];
1214
1339
 
1215
1340
  // src/ui/Sidebar.tsx
1216
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1341
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1217
1342
  function Sidebar() {
1218
- return /* @__PURE__ */ jsxs10(
1219
- Box11,
1343
+ return /* @__PURE__ */ jsxs11(
1344
+ Box12,
1220
1345
  {
1221
1346
  backgroundColor: "#14171E",
1222
1347
  width: 30,
@@ -1225,16 +1350,16 @@ function Sidebar() {
1225
1350
  flexDirection: "column",
1226
1351
  justifyContent: "space-between",
1227
1352
  children: [
1228
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", gap: 1, children: [
1229
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.muted, children: "PROGRESS" }),
1353
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1354
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "PROGRESS" }),
1230
1355
  /* @__PURE__ */ jsx10(Steps, {})
1231
1356
  ] }),
1232
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", gap: 1, children: [
1357
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
1233
1358
  /* @__PURE__ */ jsx10(Progress, {}),
1234
- /* @__PURE__ */ jsx10(Box11, { flexDirection: "column", children: sidebarCommands.map((c) => {
1235
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1236
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1237
- /* @__PURE__ */ jsx10(Text11, { color: COLORS.muted, children: c.description })
1359
+ /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: sidebarCommands.map((c) => {
1360
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1361
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1362
+ /* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: c.description })
1238
1363
  ] });
1239
1364
  }) })
1240
1365
  ] })
@@ -1244,12 +1369,12 @@ function Sidebar() {
1244
1369
  }
1245
1370
 
1246
1371
  // src/ui/Ribbon.tsx
1247
- import { Box as Box12, Text as Text12 } from "ink";
1248
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1372
+ import { Box as Box13, Text as Text13 } from "ink";
1373
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1249
1374
  function Ribbon() {
1250
1375
  const firstCommand = sidebarCommands[0];
1251
- return /* @__PURE__ */ jsxs11(
1252
- Box12,
1376
+ return /* @__PURE__ */ jsxs12(
1377
+ Box13,
1253
1378
  {
1254
1379
  backgroundColor: "#14171E",
1255
1380
  flexDirection: "row",
@@ -1259,9 +1384,9 @@ function Ribbon() {
1259
1384
  children: [
1260
1385
  /* @__PURE__ */ jsx11(Progress, {}),
1261
1386
  /* @__PURE__ */ jsx11(CurrentStep, {}),
1262
- /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
1263
- /* @__PURE__ */ jsx11(Text12, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1264
- /* @__PURE__ */ jsx11(Text12, { color: COLORS.muted, children: firstCommand.description })
1387
+ /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1388
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1389
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: firstCommand.description })
1265
1390
  ] })
1266
1391
  ]
1267
1392
  }
@@ -1272,8 +1397,8 @@ function Ribbon() {
1272
1397
  import { useState as useState6 } from "react";
1273
1398
 
1274
1399
  // src/ui/Logs.tsx
1275
- import { Box as Box13, Text as Text13, useInput as useInput5 } from "ink";
1276
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1400
+ import { Box as Box14, Text as Text14, useInput as useInput5 } from "ink";
1401
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1277
1402
  var KIND_COLOR = {
1278
1403
  tool: COLORS.primary,
1279
1404
  prompt: COLORS.badge
@@ -1309,8 +1434,8 @@ function Logs() {
1309
1434
  else if (key.downArrow) scroll.scrollBy(1);
1310
1435
  });
1311
1436
  const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1312
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1313
- logs.length === 0 && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: "No logs yet." }),
1437
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1438
+ logs.length === 0 && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "No logs yet." }),
1314
1439
  /* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
1315
1440
  const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1316
1441
  const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
@@ -1321,14 +1446,14 @@ function Logs() {
1321
1446
  const name = truncate2(entry.name, budget);
1322
1447
  budget -= name.length;
1323
1448
  const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1324
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: ROW_GAP, children: [
1325
- /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: timestamp }),
1326
- /* @__PURE__ */ jsx12(Text13, { color: logNameColor(entry), wrap: "truncate", children: name }),
1327
- preview && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, wrap: "truncate", children: preview }),
1328
- durationText && /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: durationText })
1449
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: ROW_GAP, children: [
1450
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: timestamp }),
1451
+ /* @__PURE__ */ jsx12(Text14, { color: logNameColor(entry), wrap: "truncate", children: name }),
1452
+ preview && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, wrap: "truncate", children: preview }),
1453
+ durationText && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: durationText })
1329
1454
  ] }, entry.id);
1330
1455
  }) }),
1331
- /* @__PURE__ */ jsx12(Text13, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1456
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1332
1457
  ] });
1333
1458
  }
1334
1459
 
@@ -1520,11 +1645,11 @@ function track(event, payload) {
1520
1645
  }
1521
1646
 
1522
1647
  // src/ui/App.tsx
1523
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1648
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1524
1649
  function App() {
1525
1650
  const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
1526
1651
  const { exit } = useApp();
1527
- const { columns, rows } = useWindowSize7();
1652
+ const { columns, rows } = useWindowSize8();
1528
1653
  const [showLogs, setShowLogs] = useState6(false);
1529
1654
  const finished = phase === "done" || phase === "error";
1530
1655
  const currentStep = steps[currentStepIndex];
@@ -1537,7 +1662,7 @@ function App() {
1537
1662
  { isActive: finished }
1538
1663
  );
1539
1664
  useInput6((_input, key) => {
1540
- if (phase === "idle" || phase === "preflight") return;
1665
+ if (phase === "idle" || phase === "authenticating") return;
1541
1666
  if (key.tab) {
1542
1667
  setShowLogs(!showLogs);
1543
1668
  track("AI Wizard Interaction", {
@@ -1547,49 +1672,45 @@ function App() {
1547
1672
  });
1548
1673
  }
1549
1674
  });
1550
- const escOwnedElsewhere = phase === "idle" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1675
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
1551
1676
  useInput6((_input, key) => {
1552
1677
  if (escOwnedElsewhere) return;
1553
1678
  if (key.escape) {
1554
1679
  track("AI Wizard Interaction", {
1555
1680
  context: "global",
1556
1681
  key: "esc",
1557
- // No step is active until `startWorkflow` — report the phase instead.
1558
1682
  currentStep: currentStep?.id ?? phase
1559
1683
  });
1560
1684
  exit();
1561
1685
  }
1562
1686
  });
1563
- const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1687
+ const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
1564
1688
  const flexDirection = columns > 90 ? "row" : "column";
1565
1689
  const showSidebar = flexDirection === "row";
1566
- return /* @__PURE__ */ jsxs13(
1567
- Box14,
1568
- {
1569
- backgroundColor: COLORS.bg.main,
1570
- flexDirection: "row",
1571
- width: columns,
1572
- minHeight: rows,
1573
- children: [
1574
- mainWindowVisible && // Ink sizes the root by width only, so without a cap the scrolling
1575
- // lists in here grow to their content instead of windowing (see
1576
- // `useScrollWindow`). The home screens below stay uncapped: they are
1577
- // long static copy that would be clipped rather than windowed.
1578
- /* @__PURE__ */ jsxs13(
1579
- Box14,
1580
- {
1581
- flexDirection,
1582
- width: "100%",
1583
- maxHeight: rows,
1584
- justifyContent: "space-between",
1585
- children: [
1586
- showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : (
1587
- /* Fill the space the sidebar/ribbon leaves — width beside the
1588
- sidebar, height above the ribbon. The height matters even
1589
- stacked: it is what the prompt's scrolling list measures itself
1590
- against (see SelectPrompt). */
1591
- /* @__PURE__ */ jsxs13(
1592
- Box14,
1690
+ const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
1691
+ return (
1692
+ /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1693
+ the whole screen, and the scrolling throws off its cursor arithmetic —
1694
+ flicker and leftover rows. */
1695
+ /* @__PURE__ */ jsxs14(
1696
+ Box15,
1697
+ {
1698
+ backgroundColor: COLORS.bg.main,
1699
+ flexDirection: "row",
1700
+ width: columns,
1701
+ height: scrollsPastViewport ? void 0 : rows,
1702
+ overflow: scrollsPastViewport ? "visible" : "hidden",
1703
+ children: [
1704
+ mainWindowVisible && /* @__PURE__ */ jsxs14(
1705
+ Box15,
1706
+ {
1707
+ flexDirection,
1708
+ width: "100%",
1709
+ maxHeight: rows,
1710
+ justifyContent: "space-between",
1711
+ children: [
1712
+ showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : /* @__PURE__ */ jsxs14(
1713
+ Box15,
1593
1714
  {
1594
1715
  flexDirection: "column",
1595
1716
  paddingX: 4,
@@ -1597,24 +1718,29 @@ function App() {
1597
1718
  width: showSidebar ? 70 : "100%",
1598
1719
  flexGrow: 1,
1599
1720
  children: [
1721
+ phase === "authenticating" && /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginBottom: 1, children: [
1722
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1723
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
1724
+ ] }),
1725
+ /* @__PURE__ */ jsx13(CliOutput, {}),
1600
1726
  /* @__PURE__ */ jsx13(Notices, {}),
1601
1727
  /* @__PURE__ */ jsx13(PromptInput, {}),
1602
- phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1603
- phase === "error" && error && /* @__PURE__ */ jsx13(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text14, { color: COLORS.status.error, children: [
1728
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1729
+ phase === "error" && error && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text15, { color: COLORS.status.error, children: [
1604
1730
  "\u2716 ",
1605
1731
  error
1606
1732
  ] }) })
1607
1733
  ]
1608
1734
  }
1609
- )
1610
- ),
1611
- showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1612
- ]
1613
- }
1614
- ),
1615
- (phase === "idle" || phase === "preflight") && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1616
- ]
1617
- }
1735
+ ),
1736
+ showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
1737
+ ]
1738
+ }
1739
+ ),
1740
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
1741
+ ]
1742
+ }
1743
+ )
1618
1744
  );
1619
1745
  }
1620
1746
 
@@ -1625,17 +1751,17 @@ import "zod";
1625
1751
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1626
1752
  import { join as join5 } from "node:path";
1627
1753
  var configFile = () => join5(stateDir(), "config.json");
1628
- var DEFAULT_CONFIG = {
1754
+ var defaultConfig = () => ({
1629
1755
  version: 1,
1630
1756
  aiConsent: false,
1631
1757
  workflowsRun: []
1632
- };
1758
+ });
1633
1759
  async function loadConfig() {
1634
1760
  try {
1635
1761
  const raw = await readFile2(configFile(), "utf8");
1636
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1762
+ return { ...defaultConfig(), ...JSON.parse(raw) };
1637
1763
  } catch {
1638
- return { ...DEFAULT_CONFIG };
1764
+ return defaultConfig();
1639
1765
  }
1640
1766
  }
1641
1767
  async function saveConfig(config) {
@@ -1850,61 +1976,212 @@ async function runWorkflow(workflow, appId) {
1850
1976
  }
1851
1977
  }
1852
1978
 
1853
- // src/lib/algoliaProfile.ts
1854
- import { readFile as readFile3 } from "node:fs/promises";
1855
- import { createRequire as createRequire2 } from "node:module";
1856
- import { homedir as homedir2 } from "node:os";
1857
- import { join as join6 } from "node:path";
1858
- import { parse as parseToml } from "toml";
1859
- var require3 = createRequire2(import.meta.url);
1860
- function configPath() {
1861
- const base = process.env.XDG_CONFIG_HOME || join6(homedir2(), ".config");
1862
- return join6(base, "algolia", "config.toml");
1863
- }
1864
- function profilesFromConfig(tomlText) {
1865
- let parsed;
1979
+ // src/lib/algoliaApp.ts
1980
+ import { z as z4 } from "zod";
1981
+ var applicationSchema = z4.object({
1982
+ id: z4.string().min(1),
1983
+ name: z4.string().default(""),
1984
+ plan: z4.string().optional()
1985
+ });
1986
+ var listSchema = z4.array(
1987
+ z4.object({
1988
+ id: z4.string().min(1),
1989
+ name: z4.string().default(""),
1990
+ plan_label: z4.string().optional()
1991
+ }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
1992
+ );
1993
+ async function currentApplication() {
1994
+ let raw;
1866
1995
  try {
1867
- parsed = parseToml(tomlText);
1996
+ raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
1868
1997
  } catch {
1869
- return [];
1998
+ return null;
1999
+ }
2000
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2001
+ return parsed.success ? parsed.data : null;
2002
+ }
2003
+ async function requireApplication() {
2004
+ const app = await currentApplication();
2005
+ if (!app) {
2006
+ throw new Error(
2007
+ "No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
2008
+ );
2009
+ }
2010
+ return app;
2011
+ }
2012
+ async function listApplications() {
2013
+ const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
2014
+ const parsed = listSchema.safeParse(parseJson(raw));
2015
+ if (!parsed.success) {
2016
+ throw new Error("Could not read the list of Algolia applications.");
2017
+ }
2018
+ return parsed.data;
2019
+ }
2020
+ async function selectApplication(id) {
2021
+ const raw = await runAlgoliaCli(
2022
+ ["application", "select", "--non-interactive", "--app-id", id],
2023
+ { onOutput: stderrSink }
2024
+ );
2025
+ const parsed = applicationSchema.safeParse(parseJson(raw));
2026
+ if (!parsed.success) {
2027
+ throw new Error(
2028
+ `Selected application ${id}, but the Algolia CLI returned an unreadable result.`
2029
+ );
1870
2030
  }
1871
- const profiles = Object.entries(parsed).filter(
1872
- ([, t]) => typeof t.application_id === "string" && typeof t.api_key === "string"
1873
- ).map(([name, t]) => ({
1874
- name,
1875
- appId: t.application_id,
1876
- apiKey: t.api_key,
1877
- isDefault: t.default === true
1878
- }));
1879
- profiles.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
1880
- return profiles.map(({ name, appId, apiKey }) => ({ name, appId, apiKey }));
1881
- }
1882
- async function loadActiveProfile() {
1883
- let profiles;
2031
+ return parsed.data;
2032
+ }
2033
+ function parseJson(text) {
1884
2034
  try {
1885
- profiles = profilesFromConfig(await readFile3(configPath(), "utf8"));
2035
+ return JSON.parse(text);
1886
2036
  } catch {
1887
- profiles = [];
2037
+ return void 0;
1888
2038
  }
1889
- const profile = profiles[0];
1890
- if (!profile) {
2039
+ }
2040
+
2041
+ // src/lib/envAppId.ts
2042
+ import { readFile as readFile3 } from "node:fs/promises";
2043
+ import { join as join6 } from "node:path";
2044
+ var ENV_FILES = [".env", ".env.local"];
2045
+ var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
2046
+ async function findEnvApplicationId(root = process.cwd()) {
2047
+ for (const file of ENV_FILES) {
2048
+ let content;
2049
+ try {
2050
+ content = await readFile3(join6(root, file), "utf8");
2051
+ } catch (err) {
2052
+ if (err.code !== "ENOENT") {
2053
+ logger.warn(
2054
+ { file, err },
2055
+ "could not read env file for an application id"
2056
+ );
2057
+ }
2058
+ continue;
2059
+ }
2060
+ for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
2061
+ const id = readValue(raw);
2062
+ if (id) {
2063
+ logger.info({ file, name, app: id }, "found an application id in env");
2064
+ return { id, name, file };
2065
+ }
2066
+ }
2067
+ }
2068
+ return null;
2069
+ }
2070
+ function readValue(raw) {
2071
+ const trimmed = raw.trim();
2072
+ const quoted = trimmed.match(/^(['"])(.*)\1/);
2073
+ const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
2074
+ return value.length > 0 && !value.startsWith("<") ? value : null;
2075
+ }
2076
+
2077
+ // src/lib/algoliaAppPicker.ts
2078
+ function secondaryFor(app) {
2079
+ return app.plan ? { kind: "badge", value: app.plan } : void 0;
2080
+ }
2081
+ function labelFor(app) {
2082
+ return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
2083
+ }
2084
+ function selectAndReport(app) {
2085
+ useWizard.getState().pushCliOutput(
2086
+ "stdout",
2087
+ `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
2088
+ );
2089
+ return selectApplication(app.id);
2090
+ }
2091
+ async function promptForApplication(leadIn = []) {
2092
+ const store = useWizard.getState();
2093
+ const apps = await listApplications();
2094
+ if (apps.length === 0) {
1891
2095
  throw new Error(
1892
- "No Algolia profile is configured. Run `npx @algolia/cli auth login` to authenticate."
2096
+ "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
2097
+ );
2098
+ }
2099
+ if (apps.length === 1) {
2100
+ const only = apps[0];
2101
+ logger.info(
2102
+ { app: only.id },
2103
+ "single application on the account; selecting it"
2104
+ );
2105
+ for (const line of leadIn) store.pushCliOutput("stdout", line);
2106
+ return selectAndReport(only);
2107
+ }
2108
+ const messages = [
2109
+ ...leadIn,
2110
+ "Which Algolia application should the wizard work in?"
2111
+ ];
2112
+ for (; ; ) {
2113
+ const choice = await store.requestUserInput({
2114
+ prompt: "Select an application",
2115
+ promptType: "multipleChoice",
2116
+ options: apps.map(labelFor),
2117
+ secondary: apps.map(secondaryFor),
2118
+ messages
2119
+ });
2120
+ const chosen = apps.find((app) => labelFor(app) === choice);
2121
+ if (!chosen) {
2122
+ throw new Error("Application picker received an unexpected selection");
2123
+ }
2124
+ try {
2125
+ return await selectAndReport(chosen);
2126
+ } catch (err) {
2127
+ logger.warn(
2128
+ { app: chosen.id, err: err.message },
2129
+ "application select failed; re-prompting"
2130
+ );
2131
+ messages.push(
2132
+ `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
2133
+ );
2134
+ }
2135
+ }
2136
+ }
2137
+ async function confirmEnvApplication(env, current) {
2138
+ const useEnv = `Use ${env.id} (from ${env.file})`;
2139
+ const choice = await useWizard.getState().requestUserInput({
2140
+ prompt: "Select an application",
2141
+ promptType: "multipleChoice",
2142
+ options: [
2143
+ useEnv,
2144
+ current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
2145
+ ],
2146
+ messages: [
2147
+ `${env.file} already sets ${env.name}=${env.id}.`,
2148
+ "Which Algolia application should the wizard work in?"
2149
+ ]
2150
+ });
2151
+ return choice === useEnv;
2152
+ }
2153
+ async function selectEnvApplication(env) {
2154
+ try {
2155
+ return await selectAndReport({ id: env.id, name: "" });
2156
+ } catch (err) {
2157
+ logger.warn(
2158
+ { app: env.id, err: err.message },
2159
+ "could not select the application named in env; falling back to the picker"
1893
2160
  );
2161
+ return promptForApplication([
2162
+ `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
2163
+ ]);
1894
2164
  }
1895
- return profile;
2165
+ }
2166
+ async function ensureApplication() {
2167
+ const current = await currentApplication();
2168
+ const env = await findEnvApplicationId();
2169
+ if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
2170
+ return selectEnvApplication(env);
2171
+ }
2172
+ return current ?? await promptForApplication();
1896
2173
  }
1897
2174
 
1898
2175
  // src/workflows/default.ts
1899
- import { z as z25 } from "zod";
2176
+ import { z as z28 } from "zod";
1900
2177
 
1901
2178
  // src/actions/listIndices.ts
1902
- import { z as z3 } from "zod";
1903
- var indicesListSchema = z3.object({
1904
- items: z3.array(
1905
- z3.object({
1906
- name: z3.string(),
1907
- entries: z3.number().default(0)
2179
+ import { z as z5 } from "zod";
2180
+ var indicesListSchema = z5.object({
2181
+ items: z5.array(
2182
+ z5.object({
2183
+ name: z5.string(),
2184
+ entries: z5.number().default(0)
1908
2185
  })
1909
2186
  )
1910
2187
  });
@@ -1975,7 +2252,7 @@ import "zod";
1975
2252
 
1976
2253
  // src/lib/tools/listFiles.ts
1977
2254
  import { tool } from "ai";
1978
- import z4 from "zod";
2255
+ import z6 from "zod";
1979
2256
  import { readdir } from "node:fs/promises";
1980
2257
 
1981
2258
  // src/lib/tools/path.ts
@@ -2011,15 +2288,15 @@ async function hasSymlinkParent(ctx, target) {
2011
2288
  function listFilesTool(ctx) {
2012
2289
  return tool({
2013
2290
  description: "List files in the current working directory",
2014
- inputSchema: z4.object(),
2291
+ inputSchema: z6.object(),
2015
2292
  execute: async () => {
2016
2293
  logger.info("called listFiles tool");
2017
2294
  if (++ctx.counts.list > ctx.limits.list) {
2018
2295
  return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
2019
2296
  }
2020
- const resolved = resolveInRoot(ctx, ".");
2021
- if (!resolved.ok) return resolved.error;
2022
- const entries = await readdir(resolved.target, { withFileTypes: true });
2297
+ const resolved2 = resolveInRoot(ctx, ".");
2298
+ if (!resolved2.ok) return resolved2.error;
2299
+ const entries = await readdir(resolved2.target, { withFileTypes: true });
2023
2300
  return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2024
2301
  }
2025
2302
  });
@@ -2027,24 +2304,24 @@ function listFilesTool(ctx) {
2027
2304
 
2028
2305
  // src/lib/tools/changeDirectory.ts
2029
2306
  import { tool as tool2 } from "ai";
2030
- import z5 from "zod";
2307
+ import z7 from "zod";
2031
2308
  import { stat } from "node:fs/promises";
2032
2309
  function changeDirectoryTool(ctx) {
2033
2310
  return tool2({
2034
2311
  description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
2035
- inputSchema: z5.object({
2036
- path: z5.string().describe("Directory to change into")
2312
+ inputSchema: z7.object({
2313
+ path: z7.string().describe("Directory to change into")
2037
2314
  }),
2038
2315
  execute: async ({ path }) => {
2039
2316
  logger.info({ path }, "called changeDirectory tool");
2040
- const resolved = resolveInRoot(ctx, path);
2041
- if (!resolved.ok) return resolved.error;
2317
+ const resolved2 = resolveInRoot(ctx, path);
2318
+ if (!resolved2.ok) return resolved2.error;
2042
2319
  try {
2043
- const info = await stat(resolved.target);
2320
+ const info = await stat(resolved2.target);
2044
2321
  if (!info.isDirectory()) {
2045
2322
  return `Error changing directory to ${path}: not a directory`;
2046
2323
  }
2047
- ctx.cwd = resolved.target;
2324
+ ctx.cwd = resolved2.target;
2048
2325
  return `Changed working directory to ${ctx.cwd}`;
2049
2326
  } catch (err) {
2050
2327
  return `Error changing directory to ${path}: ${err.message}`;
@@ -2055,13 +2332,13 @@ function changeDirectoryTool(ctx) {
2055
2332
 
2056
2333
  // src/lib/tools/reportStatus.ts
2057
2334
  import { tool as tool3 } from "ai";
2058
- import z6 from "zod";
2335
+ import z8 from "zod";
2059
2336
  function reportStatusTool(output) {
2060
2337
  return tool3({
2061
2338
  description: "Report the status of your execution. Return a reason in case of failure.",
2062
- inputSchema: z6.object({
2063
- status: z6.enum(["success", "fail"]),
2064
- reason: z6.string().optional(),
2339
+ inputSchema: z8.object({
2340
+ status: z8.enum(["success", "fail"]),
2341
+ reason: z8.string().optional(),
2065
2342
  output
2066
2343
  }),
2067
2344
  execute: async ({ status, reason, output: output2 }) => {
@@ -2073,7 +2350,7 @@ function reportStatusTool(output) {
2073
2350
 
2074
2351
  // src/lib/tools/readFile.ts
2075
2352
  import { tool as tool4 } from "ai";
2076
- import z7 from "zod";
2353
+ import z9 from "zod";
2077
2354
  import { readFile as readFile4 } from "node:fs/promises";
2078
2355
 
2079
2356
  // src/lib/tools/env.ts
@@ -2101,19 +2378,19 @@ function redactEnvValues(content) {
2101
2378
  function readFileTool(ctx) {
2102
2379
  return tool4({
2103
2380
  description: "Read the contents of a file at the given path",
2104
- inputSchema: z7.object({
2105
- filePath: z7.string().describe("Path to the file to read")
2381
+ inputSchema: z9.object({
2382
+ filePath: z9.string().describe("Path to the file to read")
2106
2383
  }),
2107
2384
  execute: async ({ filePath }) => {
2108
2385
  if (++ctx.counts.read > ctx.limits.read) {
2109
2386
  return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
2110
2387
  }
2111
2388
  logger.info({ filePath }, "called readFile tool");
2112
- const resolved = resolveInRoot(ctx, filePath);
2113
- if (!resolved.ok) return resolved.error;
2389
+ const resolved2 = resolveInRoot(ctx, filePath);
2390
+ if (!resolved2.ok) return resolved2.error;
2114
2391
  try {
2115
- const content = await readFile4(resolved.target, "utf8");
2116
- return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2392
+ const content = await readFile4(resolved2.target, "utf8");
2393
+ return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2117
2394
  } catch (err) {
2118
2395
  return `Error reading ${filePath}: ${err.message}`;
2119
2396
  }
@@ -2123,29 +2400,29 @@ function readFileTool(ctx) {
2123
2400
 
2124
2401
  // src/lib/tools/writeFile.ts
2125
2402
  import { tool as tool5 } from "ai";
2126
- import z8 from "zod";
2403
+ import z10 from "zod";
2127
2404
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2128
2405
  import { dirname as dirname4 } from "node:path";
2129
2406
  function writeFileTool(ctx) {
2130
2407
  return tool5({
2131
2408
  description: "Write content to a file at the given path, overwriting it. To set Algolia credentials in an env file, use writeCredentials instead of this tool.",
2132
- inputSchema: z8.object({
2133
- filePath: z8.string().describe("Path to the file to write"),
2134
- content: z8.string().describe("Content to write to the file")
2409
+ inputSchema: z10.object({
2410
+ filePath: z10.string().describe("Path to the file to write"),
2411
+ content: z10.string().describe("Content to write to the file")
2135
2412
  }),
2136
2413
  execute: async ({ filePath, content }) => {
2137
2414
  logger.info({ filePath }, "called writeFile tool");
2138
- const resolved = resolveInRoot(ctx, filePath);
2139
- if (resolved.ok === false) return resolved.error;
2140
- if (isSecretEnvFile(resolved.target)) {
2415
+ const resolved2 = resolveInRoot(ctx, filePath);
2416
+ if (resolved2.ok === false) return resolved2.error;
2417
+ if (isSecretEnvFile(resolved2.target)) {
2141
2418
  return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
2142
2419
  }
2143
2420
  try {
2144
- if (await hasSymlinkParent(ctx, resolved.target)) {
2145
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2421
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2422
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2146
2423
  }
2147
- await mkdir3(dirname4(resolved.target), { recursive: true });
2148
- await writeFile3(resolved.target, content, "utf8");
2424
+ await mkdir3(dirname4(resolved2.target), { recursive: true });
2425
+ await writeFile3(resolved2.target, content, "utf8");
2149
2426
  return `Wrote to ${filePath}`;
2150
2427
  } catch (err) {
2151
2428
  return `Error writing ${filePath}: ${err.message}`;
@@ -2156,11 +2433,194 @@ function writeFileTool(ctx) {
2156
2433
 
2157
2434
  // src/lib/tools/writeAlgoliaCredentials.ts
2158
2435
  import { tool as tool6 } from "ai";
2159
- import z9 from "zod";
2436
+ import z13 from "zod";
2160
2437
  import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2161
2438
  import { dirname as dirname5 } from "node:path";
2439
+
2440
+ // src/lib/algoliaApiKey.ts
2441
+ import { z as z12 } from "zod";
2442
+
2443
+ // src/lib/keychain.ts
2444
+ import { deletePassword, getPassword, setPassword } from "cross-keychain";
2445
+ import { z as z11 } from "zod";
2446
+ var SERVICE = "algolia-wizard";
2447
+ var ACCOUNT = "api-keys";
2448
+ var storedKeysSchema = z11.record(z11.string(), z11.string());
2449
+ function entryId(kind, index, appId) {
2450
+ return `${kind}:${appId}:${index}`;
2451
+ }
2452
+ async function loadKeys() {
2453
+ const raw = await getPassword(SERVICE, ACCOUNT);
2454
+ if (!raw) return {};
2455
+ let payload;
2456
+ try {
2457
+ payload = JSON.parse(raw);
2458
+ } catch {
2459
+ payload = null;
2460
+ }
2461
+ const keys = storedKeysSchema.safeParse(payload);
2462
+ if (!keys.success) {
2463
+ logger.warn("the stored API keys are unreadable; treating them as empty");
2464
+ return {};
2465
+ }
2466
+ return keys.data;
2467
+ }
2468
+ var queue = Promise.resolve();
2469
+ function serialized(op) {
2470
+ const next = queue.then(op);
2471
+ queue = next.catch(() => {
2472
+ });
2473
+ return next;
2474
+ }
2475
+ async function readStoredKey(kind, index, appId) {
2476
+ try {
2477
+ return (await loadKeys())[entryId(kind, index, appId)] ?? null;
2478
+ } catch (err) {
2479
+ logger.warn(
2480
+ { err: err.message, kind, index, appId },
2481
+ "could not read the API key from the keychain"
2482
+ );
2483
+ return null;
2484
+ }
2485
+ }
2486
+ function storeKey(kind, index, appId, value) {
2487
+ return serialized(async () => {
2488
+ const id = entryId(kind, index, appId);
2489
+ try {
2490
+ const keys = await loadKeys();
2491
+ await setPassword(
2492
+ SERVICE,
2493
+ ACCOUNT,
2494
+ JSON.stringify({ ...keys, [id]: value })
2495
+ );
2496
+ if ((await loadKeys())[id] !== value) {
2497
+ throw new Error("the keychain did not store the value");
2498
+ }
2499
+ } catch (err) {
2500
+ logger.warn(
2501
+ { err: err.message, kind, index, appId },
2502
+ "could not store the API key in the keychain; the next run will create another"
2503
+ );
2504
+ }
2505
+ });
2506
+ }
2507
+ function deleteStoredKeys() {
2508
+ return serialized(async () => {
2509
+ try {
2510
+ await deletePassword(SERVICE, ACCOUNT);
2511
+ } catch (err) {
2512
+ const message = err.message;
2513
+ if (/not found/i.test(message)) return;
2514
+ logger.warn(
2515
+ { err: message },
2516
+ "could not delete the API keys from the keychain"
2517
+ );
2518
+ }
2519
+ });
2520
+ }
2521
+
2522
+ // src/lib/algoliaApiKey.ts
2523
+ var WRITE_ACLS = [
2524
+ "addObject",
2525
+ "deleteObject",
2526
+ "settings",
2527
+ "editSettings",
2528
+ "listIndexes"
2529
+ ];
2530
+ var createdKeySchema = z12.object({
2531
+ key: z12.string().min(1).optional(),
2532
+ value: z12.string().min(1).optional()
2533
+ }).transform((o) => o.key ?? o.value);
2534
+ async function createKey(index, acls, description) {
2535
+ logger.info({ index, acls }, "creating an API key");
2536
+ const stdout = await runAlgoliaCli([
2537
+ "apikeys",
2538
+ "create",
2539
+ "--acl",
2540
+ acls.join(","),
2541
+ "--indices",
2542
+ index,
2543
+ "--description",
2544
+ description,
2545
+ "-o",
2546
+ "json"
2547
+ ]);
2548
+ let payload;
2549
+ try {
2550
+ payload = JSON.parse(stdout);
2551
+ } catch {
2552
+ throw new Error("apikeys create returned output that is not valid JSON");
2553
+ }
2554
+ const created = createdKeySchema.parse(payload);
2555
+ if (!created) throw new Error("apikeys create returned no key value");
2556
+ return created;
2557
+ }
2558
+ async function keyExists(key) {
2559
+ try {
2560
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"], { redact: key });
2561
+ return true;
2562
+ } catch (err) {
2563
+ return !/does not exist/i.test(err.message);
2564
+ }
2565
+ }
2566
+ var resolved = /* @__PURE__ */ new Map();
2567
+ async function forgetResolvedKeys() {
2568
+ resolved.clear();
2569
+ await deleteStoredKeys();
2570
+ }
2571
+ function resolveKey(kind, index, appId, acls, description) {
2572
+ const cacheKey = `${kind}:${appId}:${index}`;
2573
+ const cached = resolved.get(cacheKey);
2574
+ if (cached) return cached;
2575
+ const pending = provisionKey(kind, index, appId, acls, description).catch(
2576
+ (err) => {
2577
+ resolved.delete(cacheKey);
2578
+ throw err;
2579
+ }
2580
+ );
2581
+ resolved.set(cacheKey, pending);
2582
+ return pending;
2583
+ }
2584
+ async function provisionKey(kind, index, appId, acls, description) {
2585
+ const stored = await readStoredKey(kind, index, appId);
2586
+ if (stored) {
2587
+ if (await keyExists(stored)) {
2588
+ logger.info({ kind, index, appId }, "reusing the stored API key");
2589
+ return { key: stored, source: "keychain" };
2590
+ }
2591
+ logger.info(
2592
+ { kind, index, appId },
2593
+ "the stored API key no longer exists; creating another"
2594
+ );
2595
+ }
2596
+ const key = await createKey(index, acls, description);
2597
+ await storeKey(kind, index, appId, key);
2598
+ return { key, source: "created" };
2599
+ }
2600
+ function resolveWriteKey(index, appId) {
2601
+ return resolveKey(
2602
+ "write",
2603
+ index,
2604
+ appId,
2605
+ WRITE_ACLS,
2606
+ `Algolia Wizard write key for ${index} index`
2607
+ );
2608
+ }
2609
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2610
+ if (envKey) return { key: envKey, source: "env" };
2611
+ return resolveKey(
2612
+ "search",
2613
+ index,
2614
+ appId,
2615
+ ["search"],
2616
+ `Algolia Wizard search-only key for ${index} index`
2617
+ );
2618
+ }
2619
+
2620
+ // src/lib/tools/writeAlgoliaCredentials.ts
2162
2621
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2163
2622
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2623
+ var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
2164
2624
  function appendEnv(content, entries) {
2165
2625
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2166
2626
  const lines = entries.map(([name, value]) => `${name}=${value}
@@ -2170,47 +2630,111 @@ function appendEnv(content, entries) {
2170
2630
  function hasEnv(content, name) {
2171
2631
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
2172
2632
  }
2633
+ function readEnv(content, name) {
2634
+ const found = content.match(
2635
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*(.*)$`, "m")
2636
+ );
2637
+ if (!found) return null;
2638
+ const raw = found[1].trim();
2639
+ const quoted = raw.match(/^(['"])(.*)\1/);
2640
+ const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, "");
2641
+ return value.length > 0 ? value : null;
2642
+ }
2643
+ function upsertEnv(content, name, value) {
2644
+ if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
2645
+ return content.replace(
2646
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "gm"),
2647
+ () => `${name}=${value}`
2648
+ );
2649
+ }
2173
2650
  function writeCredentialsTool(ctx) {
2174
2651
  return tool6({
2175
- description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials are read from the local Algolia CLI profile; you only pass the path to the env file (e.g. ".env"). If the file already defines ${APP_ID_VAR} or ${API_KEY_VAR}, the write is skipped and existing values are left untouched.`,
2176
- inputSchema: z9.object({
2177
- filePath: z9.string().describe(
2652
+ description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application.`,
2653
+ inputSchema: z13.object({
2654
+ filePath: z13.string().describe(
2178
2655
  'Path to the env file to write credentials into (e.g. ".env")'
2179
2656
  )
2180
2657
  }),
2181
2658
  execute: async ({ filePath }) => {
2182
2659
  logger.info({ filePath }, "called writeCredentials tool");
2183
- const resolved = resolveInRoot(ctx, filePath);
2184
- if (resolved.ok === false) return resolved.error;
2185
- let profile;
2186
- try {
2187
- profile = await loadActiveProfile();
2188
- } catch {
2189
- return "Error: no Algolia profile is configured, so credentials cannot be written. Ask the user to authenticate with the Algolia CLI first.";
2660
+ const resolved2 = resolveInRoot(ctx, filePath);
2661
+ if (resolved2.ok === false) return resolved2.error;
2662
+ const targetIndex = useWizard.getState().targetIndex;
2663
+ if (!targetIndex) {
2664
+ return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2190
2665
  }
2666
+ let existing = "";
2667
+ let present;
2191
2668
  try {
2192
- if (await hasSymlinkParent(ctx, resolved.target)) {
2193
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2669
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2670
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2194
2671
  }
2195
- let existing = "";
2196
2672
  try {
2197
- existing = await readFile5(resolved.target, "utf8");
2673
+ existing = await readFile5(resolved2.target, "utf8");
2198
2674
  } catch (err) {
2199
2675
  if (err.code !== "ENOENT") throw err;
2200
2676
  }
2201
- const present = [APP_ID_VAR, API_KEY_VAR].filter(
2202
- (name) => hasEnv(existing, name)
2677
+ present = [APP_ID_VAR, API_KEY_VAR].filter(
2678
+ (name) => readEnv(existing, name) !== null
2679
+ );
2680
+ } catch (err) {
2681
+ return `Error writing credentials to ${filePath}: ${err.message}`;
2682
+ }
2683
+ const credentials = [];
2684
+ const notes = [];
2685
+ const fileAppId = readEnv(existing, APP_ID_VAR);
2686
+ const fileKey = readEnv(existing, API_KEY_VAR);
2687
+ const fileIndex = readEnv(existing, INDEX_NAME_VAR);
2688
+ if (fileAppId === null || fileKey === null) {
2689
+ try {
2690
+ const selected = (await requireApplication()).id;
2691
+ if (fileAppId === null) credentials.push([APP_ID_VAR, selected]);
2692
+ if (fileKey === null) {
2693
+ if (fileAppId !== null && fileAppId !== selected) {
2694
+ notes.push(
2695
+ `No ${API_KEY_VAR} was provisioned: ${filePath} names application ${fileAppId}, but ${selected} is selected. Tell the user to remove ${APP_ID_VAR} from ${filePath} and re-run so a matching pair can be written, or to fill in a ${API_KEY_VAR} for ${fileAppId} by hand.`
2696
+ );
2697
+ } else {
2698
+ credentials.push([
2699
+ API_KEY_VAR,
2700
+ (await resolveWriteKey(targetIndex, selected)).key
2701
+ ]);
2702
+ }
2703
+ } else {
2704
+ notes.push(
2705
+ `The existing ${API_KEY_VAR} must belong to application ${selected} or writes will fail.`
2706
+ );
2707
+ }
2708
+ } catch (err) {
2709
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2710
+ }
2711
+ }
2712
+ try {
2713
+ const updated = [
2714
+ ...credentials,
2715
+ // Unconditional, unlike the credentials: the write key is scoped to
2716
+ // the run's index, so a stale name left in the file earns a 403.
2717
+ [INDEX_NAME_VAR, targetIndex]
2718
+ ].reduce(
2719
+ (content, [name, value]) => upsertEnv(content, name, value),
2720
+ existing
2203
2721
  );
2722
+ await mkdir4(dirname5(resolved2.target), { recursive: true });
2723
+ await writeFile4(resolved2.target, updated, "utf8");
2724
+ const sentences = [
2725
+ `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
2726
+ ];
2727
+ if (fileIndex !== null && fileIndex !== targetIndex) {
2728
+ sentences.push(
2729
+ `That replaced the ${INDEX_NAME_VAR} already there ("${fileIndex}"), which this run does not target.`
2730
+ );
2731
+ }
2204
2732
  if (present.length > 0) {
2205
- return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2733
+ sentences.push(
2734
+ `Skipped ${present.join(" and ")}: already defined there.`
2735
+ );
2206
2736
  }
2207
- const envWithCredentials = appendEnv(existing, [
2208
- [APP_ID_VAR, profile.appId],
2209
- [API_KEY_VAR, profile.apiKey]
2210
- ]);
2211
- await mkdir4(dirname5(resolved.target), { recursive: true });
2212
- await writeFile4(resolved.target, envWithCredentials, "utf8");
2213
- return `Wrote Algolia credentials to ${filePath}`;
2737
+ return [...sentences, ...notes].join(" ");
2214
2738
  } catch (err) {
2215
2739
  return `Error writing credentials to ${filePath}: ${err.message}`;
2216
2740
  }
@@ -2220,7 +2744,7 @@ function writeCredentialsTool(ctx) {
2220
2744
 
2221
2745
  // src/lib/tools/searchFiles.ts
2222
2746
  import { tool as tool7 } from "ai";
2223
- import z10 from "zod";
2747
+ import z14 from "zod";
2224
2748
  import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2225
2749
  import { join as join8 } from "node:path";
2226
2750
  var MAX_QUERY_LENGTH = 1e3;
@@ -2238,9 +2762,9 @@ async function walkFiles(dir) {
2238
2762
  function searchFilesTool(ctx) {
2239
2763
  return tool7({
2240
2764
  description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
2241
- inputSchema: z10.object({
2242
- query: z10.string().describe("JavaScript RegExp pattern to search for"),
2243
- path: z10.string().optional().describe("Directory to search in (default: cwd)")
2765
+ inputSchema: z14.object({
2766
+ query: z14.string().describe("JavaScript RegExp pattern to search for"),
2767
+ path: z14.string().optional().describe("Directory to search in (default: cwd)")
2244
2768
  }),
2245
2769
  execute: async ({ query, path = "." }) => {
2246
2770
  logger.info({ query, path }, "called searchFiles tool");
@@ -2250,8 +2774,8 @@ function searchFilesTool(ctx) {
2250
2774
  if (query.length > MAX_QUERY_LENGTH) {
2251
2775
  return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
2252
2776
  }
2253
- const resolved = resolveInRoot(ctx, path);
2254
- if (!resolved.ok) return resolved.error;
2777
+ const resolved2 = resolveInRoot(ctx, path);
2778
+ if (!resolved2.ok) return resolved2.error;
2255
2779
  let re;
2256
2780
  try {
2257
2781
  re = new RegExp(query);
@@ -2259,7 +2783,7 @@ function searchFilesTool(ctx) {
2259
2783
  return `Invalid regex: ${err.message}`;
2260
2784
  }
2261
2785
  const matches = [];
2262
- for (const file of await walkFiles(resolved.target)) {
2786
+ for (const file of await walkFiles(resolved2.target)) {
2263
2787
  let content;
2264
2788
  try {
2265
2789
  content = await readFile6(file, "utf8");
@@ -2284,7 +2808,7 @@ function searchFilesTool(ctx) {
2284
2808
 
2285
2809
  // src/lib/tools/verifyImplementation.ts
2286
2810
  import { tool as tool8 } from "ai";
2287
- import z11 from "zod";
2811
+ import z15 from "zod";
2288
2812
 
2289
2813
  // src/lib/tools/utils/runCommand.ts
2290
2814
  import { spawn as spawn2 } from "node:child_process";
@@ -2364,7 +2888,7 @@ async function runRepoVerificationCheck() {
2364
2888
  function verifyImplementationTool() {
2365
2889
  return tool8({
2366
2890
  description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
2367
- inputSchema: z11.object(),
2891
+ inputSchema: z15.object(),
2368
2892
  execute: async () => {
2369
2893
  logger.info("called verifyImplementation tool");
2370
2894
  return runRepoVerificationCheck();
@@ -2378,7 +2902,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
2378
2902
  import { nanoid as nanoid2 } from "nanoid";
2379
2903
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2380
2904
  import { dirname as dirname6 } from "node:path";
2381
- import z12 from "zod";
2905
+ import z16 from "zod";
2382
2906
  var DATA_DIR = ".algolia-wizard/data";
2383
2907
  var RECORD_MODEL = "claude-haiku-4-5";
2384
2908
  var MAX_RECORDS = 100;
@@ -2390,17 +2914,17 @@ var anthropic = createAnthropic({
2390
2914
  function generateRecordTool(ctx) {
2391
2915
  return tool9({
2392
2916
  description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
2393
- inputSchema: z12.object({
2394
- entityName: z12.string().describe("Name of the entity to generate records for."),
2395
- attributes: z12.array(z12.string()).describe("Attribute names each record must contain."),
2396
- count: z12.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2397
- hint: z12.string().optional().describe("Optional context to steer realistic values.")
2917
+ inputSchema: z16.object({
2918
+ entityName: z16.string().describe("Name of the entity to generate records for."),
2919
+ attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
2920
+ count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2921
+ hint: z16.string().optional().describe("Optional context to steer realistic values.")
2398
2922
  }),
2399
2923
  execute: async ({ entityName, attributes, count, hint }) => {
2400
2924
  logger.info({ entityName, count }, "called generateRecord tool");
2401
2925
  try {
2402
- const value = z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]);
2403
- const recordSchema = z12.object(
2926
+ const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
2927
+ const recordSchema = z16.object(
2404
2928
  Object.fromEntries(attributes.map((attr) => [attr, value]))
2405
2929
  );
2406
2930
  const generateBatch = async (batchCount) => {
@@ -2410,8 +2934,8 @@ function generateRecordTool(ctx) {
2410
2934
  const { output } = await generateText({
2411
2935
  model: anthropic(RECORD_MODEL),
2412
2936
  output: Output.object({
2413
- schema: z12.object({
2414
- records: z12.array(recordSchema).length(batchCount)
2937
+ schema: z16.object({
2938
+ records: z16.array(recordSchema).length(batchCount)
2415
2939
  })
2416
2940
  }),
2417
2941
  prompt: [
@@ -2447,13 +2971,13 @@ function generateRecordTool(ctx) {
2447
2971
  }));
2448
2972
  const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2449
2973
  const relPath = `${DATA_DIR}/${slug}.json`;
2450
- const resolved = resolveInRoot(ctx, relPath);
2451
- if (resolved.ok === false) return resolved.error;
2452
- if (await hasSymlinkParent(ctx, resolved.target)) {
2453
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2974
+ const resolved2 = resolveInRoot(ctx, relPath);
2975
+ if (resolved2.ok === false) return resolved2.error;
2976
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2977
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2454
2978
  }
2455
- await mkdir5(dirname6(resolved.target), { recursive: true });
2456
- await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2979
+ await mkdir5(dirname6(resolved2.target), { recursive: true });
2980
+ await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
2457
2981
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
2458
2982
  return {
2459
2983
  filePath: relPath,
@@ -2469,12 +2993,12 @@ function generateRecordTool(ctx) {
2469
2993
 
2470
2994
  // src/lib/tools/notifyUser.ts
2471
2995
  import { tool as tool10 } from "ai";
2472
- import z13 from "zod";
2996
+ import z17 from "zod";
2473
2997
  function notifyUserTool() {
2474
2998
  return tool10({
2475
2999
  description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work.`,
2476
- inputSchema: z13.object({
2477
- message: z13.string().describe(
3000
+ inputSchema: z17.object({
3001
+ message: z17.string().describe(
2478
3002
  "Short, plain-language description of what you are doing now."
2479
3003
  )
2480
3004
  }),
@@ -2654,10 +3178,10 @@ async function runAgent(req) {
2654
3178
  }
2655
3179
 
2656
3180
  // src/actions/detectLanguage.ts
2657
- import z16 from "zod";
2658
- var detectLanguageSchema = z16.object({
2659
- languages: z16.array(z16.object({ name: z16.string(), version: z16.string() })),
2660
- frameworks: z16.array(z16.object({ name: z16.string(), version: z16.string() }))
3181
+ import z20 from "zod";
3182
+ var detectLanguageSchema = z20.object({
3183
+ languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
3184
+ frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
2661
3185
  });
2662
3186
  var detectLanguage = () => runAgent({
2663
3187
  instructions: [
@@ -2675,31 +3199,31 @@ var detectLanguage = () => runAgent({
2675
3199
  });
2676
3200
 
2677
3201
  // src/actions/analyzeCodebase.ts
2678
- import z17 from "zod";
3202
+ import z21 from "zod";
2679
3203
  var READONLY_TOOLS = [
2680
3204
  "listFiles",
2681
3205
  "changeDirectory",
2682
3206
  "readFile",
2683
3207
  "searchFiles"
2684
3208
  ];
2685
- var ingestionAnalysisSchema = z17.object({
2686
- ingestionAnalysis: z17.array(
2687
- z17.object({
2688
- name: z17.string(),
2689
- paths: z17.array(z17.string()),
3209
+ var ingestionAnalysisSchema = z21.object({
3210
+ ingestionAnalysis: z21.array(
3211
+ z21.object({
3212
+ name: z21.string(),
3213
+ paths: z21.array(z21.string()),
2690
3214
  // indexable fields the agent found for this entity
2691
- attributes: z17.array(z17.string())
3215
+ attributes: z21.array(z21.string())
2692
3216
  })
2693
3217
  )
2694
3218
  });
2695
- var searchImplementationAnalysisSchema = z17.object({
2696
- searchImplementationAnalysis: z17.string()
3219
+ var searchImplementationAnalysisSchema = z21.object({
3220
+ searchImplementationAnalysis: z21.string()
2697
3221
  });
2698
- var verificationSchema = z17.object({
2699
- verification: z17.array(z17.string())
3222
+ var verificationSchema = z21.object({
3223
+ verification: z21.array(z21.string())
2700
3224
  });
2701
3225
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
2702
- var analyzeCodebaseSchema = z17.object({
3226
+ var analyzeCodebaseSchema = z21.object({
2703
3227
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2704
3228
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
2705
3229
  verification: verificationSchema.shape.verification.optional(),
@@ -2761,7 +3285,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2761
3285
  // package.json
2762
3286
  var package_default = {
2763
3287
  name: "@algolia/wizard",
2764
- version: "0.9.0",
3288
+ version: "0.11.0",
2765
3289
  description: "Magically implement Algolia functionality in your codebase",
2766
3290
  type: "module",
2767
3291
  engines: {
@@ -2809,10 +3333,10 @@ var package_default = {
2809
3333
  dependencies: {
2810
3334
  "@ai-sdk/anthropic": "^3.0.81",
2811
3335
  "@ai-sdk/openai-compatible": "^2.0.47",
2812
- "@algolia/cli": "^5.11.0",
2813
3336
  "@hono/node-server": "^2.0.10",
2814
3337
  "@segment/analytics-node": "^3.1.0",
2815
3338
  ai: "^6.0.190",
3339
+ "cross-keychain": "^1.1.0",
2816
3340
  dotenv: "^17.4.2",
2817
3341
  hono: "^4.12.27",
2818
3342
  ink: "^7.0.5",
@@ -2823,7 +3347,6 @@ var package_default = {
2823
3347
  nanoid: "^5.1.15",
2824
3348
  pino: "^10.3.1",
2825
3349
  react: "^19.2.7",
2826
- toml: "^4.1.1",
2827
3350
  varlock: "^1.5.1",
2828
3351
  zod: "^4.4.3",
2829
3352
  zustand: "^5.0.14"
@@ -2881,8 +3404,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
2881
3404
  }
2882
3405
 
2883
3406
  // src/actions/confirmLanguage.ts
2884
- import z19 from "zod";
2885
- var confirmLanguageSchema = z19.object({
3407
+ import z23 from "zod";
3408
+ var confirmLanguageSchema = z23.object({
2886
3409
  languages: detectLanguageSchema.shape.languages
2887
3410
  });
2888
3411
  async function confirmLanguage(ctx) {
@@ -2903,8 +3426,8 @@ async function confirmLanguage(ctx) {
2903
3426
  }
2904
3427
 
2905
3428
  // src/actions/confirmFramework.ts
2906
- import z20 from "zod";
2907
- var confirmFrameworkSchema = z20.object({
3429
+ import z24 from "zod";
3430
+ var confirmFrameworkSchema = z24.object({
2908
3431
  frameworks: detectLanguageSchema.shape.frameworks
2909
3432
  });
2910
3433
  var CURATED_FRAMEWORKS = [
@@ -3032,8 +3555,8 @@ async function promptUser(ctx, params) {
3032
3555
  }
3033
3556
 
3034
3557
  // src/actions/confirmEntities.ts
3035
- import z21 from "zod";
3036
- var confirmEntitiesSchema = z21.object({
3558
+ import z25 from "zod";
3559
+ var confirmEntitiesSchema = z25.object({
3037
3560
  // Final detection — the focused re-run may supersede project-scan's.
3038
3561
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3039
3562
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3103,15 +3626,15 @@ async function confirmEntities(ctx) {
3103
3626
  }
3104
3627
 
3105
3628
  // src/actions/review.ts
3106
- import { z as z22 } from "zod";
3107
- var reviewSchema = z22.object({
3629
+ import { z as z26 } from "zod";
3630
+ var reviewSchema = z26.object({
3108
3631
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3109
3632
  // not one entry per workflow step — a step's raw output can be a long,
3110
3633
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3111
3634
  // that 1:1 is what made the old per-step summary an unreadable wall of text.
3112
- summaryPoints: z22.array(z22.string()),
3113
- reviewPrompt: z22.string(),
3114
- nextSteps: z22.array(z22.string())
3635
+ summaryPoints: z26.array(z26.string()),
3636
+ reviewPrompt: z26.string(),
3637
+ nextSteps: z26.array(z26.string())
3115
3638
  });
3116
3639
  function formatCompletedSteps(steps) {
3117
3640
  if (!steps.length) return "(no prior steps completed)";
@@ -3162,7 +3685,7 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3162
3685
  };
3163
3686
 
3164
3687
  // src/actions/implement.ts
3165
- import z24 from "zod";
3688
+ import z27 from "zod";
3166
3689
 
3167
3690
  // src/lib/worktree.ts
3168
3691
  import { execFile, spawn as spawn3 } from "node:child_process";
@@ -3366,6 +3889,23 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3366
3889
  function hasEnvVar(content, name) {
3367
3890
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3368
3891
  }
3892
+ async function readEnvVar(worktreePath, name) {
3893
+ let content;
3894
+ try {
3895
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
3896
+ } catch (err) {
3897
+ if (err.code !== "ENOENT") throw err;
3898
+ return void 0;
3899
+ }
3900
+ const match = new RegExp(
3901
+ `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3902
+ "m"
3903
+ ).exec(content);
3904
+ if (!match) return void 0;
3905
+ const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3906
+ if (!value || value.startsWith("<")) return void 0;
3907
+ return value;
3908
+ }
3369
3909
  async function writeSearchEnvValues(worktreePath, vars) {
3370
3910
  const target = join10(worktreePath, ".env");
3371
3911
  let existing = "";
@@ -3438,54 +3978,6 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3438
3978
  }
3439
3979
  }
3440
3980
 
3441
- // src/lib/algoliaApiKey.ts
3442
- import { z as z23 } from "zod";
3443
- var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
3444
- var apiKeySchema = z23.object({
3445
- value: z23.string().min(1),
3446
- acl: z23.array(z23.string()).default([]),
3447
- indexes: z23.array(z23.string()).default([])
3448
- });
3449
- var apiKeyListSchema = z23.object({
3450
- items: z23.array(apiKeySchema).optional(),
3451
- keys: z23.array(apiKeySchema).optional()
3452
- }).transform((o) => o.items ?? o.keys ?? []);
3453
- var createdKeySchema = z23.object({
3454
- key: z23.string().min(1).optional(),
3455
- value: z23.string().min(1).optional()
3456
- });
3457
- function canReuse(key, index) {
3458
- return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
3459
- }
3460
- async function createSearchKey(index) {
3461
- const stdout = await runAlgoliaCli([
3462
- "apikeys",
3463
- "create",
3464
- "--indices",
3465
- index,
3466
- "--acl",
3467
- "search,browse",
3468
- "--description",
3469
- `wizard search-only key for ${index}`,
3470
- "-o",
3471
- "json"
3472
- ]);
3473
- const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
3474
- const created = key ?? value;
3475
- if (!created) throw new Error("apikeys create returned no key value");
3476
- return created;
3477
- }
3478
- async function resolveSearchOnlyKey(index) {
3479
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
3480
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
3481
- if (existing) {
3482
- logger.info({ index }, "reusing existing search-only API key");
3483
- return existing;
3484
- }
3485
- logger.info({ index }, "no reusable search-only key found; creating one");
3486
- return createSearchKey(index);
3487
- }
3488
-
3489
3981
  // src/lib/algoliaDocs.ts
3490
3982
  import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3491
3983
  import { dirname as dirname8, join as join11 } from "node:path";
@@ -3552,50 +4044,34 @@ function shellQuote(value) {
3552
4044
  }
3553
4045
 
3554
4046
  // src/actions/implement.ts
3555
- var implementSchema = z24.object({
3556
- filesChanged: z24.array(z24.string()),
3557
- summary: z24.string(),
3558
- // Absolute path to the throwaway worktree holding the generated changes, so
3559
- // the user can open it (`cd <worktreePath>`) or inspect the diff
3560
- // (`git -C <worktreePath> status/diff`).
3561
- worktreePath: z24.string().optional(),
3562
- ingestCommand: z24.string().optional(),
3563
- // True when the user accepted the run-now prompt and the wizard executed the
3564
- // ingestion script; downstream steps use this to avoid telling the user to run
3565
- // a script that already ran.
3566
- ingestScriptRan: z24.boolean().optional(),
3567
- // Records ingested by the run-now execution, parsed from the script's
3568
- // machine-readable count line; absent when the script didn't run or emitted
3569
- // no parseable count.
3570
- ingestRecordCount: z24.number().optional(),
3571
- // Wall-clock duration of the run-now ingestion execution, in ms.
3572
- ingestDurationMs: z24.number().optional(),
3573
- ingestionSource: z24.enum(["local", "fileUpload", "generated"]),
3574
- // Suggested names/values, built from framework detection. The search agent is
3575
- // instructed to rename the prefix if it doesn't match the project's build
3576
- // tool, so the names it actually wrote can differ — treat these as hints, not
3577
- // ground truth (the agent's summary carries the final names).
3578
- searchEnvVars: z24.array(
3579
- z24.object({
3580
- name: z24.string(),
3581
- value: z24.string()
4047
+ var implementSchema = z27.object({
4048
+ filesChanged: z27.array(z27.string()),
4049
+ summary: z27.string(),
4050
+ worktreePath: z27.string().optional(),
4051
+ ingestCommand: z27.string().optional(),
4052
+ ingestScriptRan: z27.boolean().optional(),
4053
+ ingestRecordCount: z27.number().optional(),
4054
+ ingestDurationMs: z27.number().optional(),
4055
+ ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
4056
+ searchEnvVars: z27.array(
4057
+ z27.object({
4058
+ name: z27.string(),
4059
+ value: z27.string()
3582
4060
  })
3583
4061
  ).optional()
3584
4062
  });
3585
- var implementationOutputSchema = z24.object({
3586
- summary: z24.string(),
3587
- // Ingestion only: how to run the generated script, as a structured pair the
3588
- // wizard turns into an argv (`<runtime> <entrypoint>`) never a free-form
3589
- // command string. `runtime` is constrained to an allowlisted interpreter and
3590
- // `entrypoint` is validated to a worktree-relative path before execution, so
3591
- // the agent cannot inject extra commands or swap the interpreter.
3592
- runtime: z24.enum(INGEST_RUNTIMES).optional(),
3593
- entrypoint: z24.string().optional()
4063
+ var implementationOutputSchema = z27.object({
4064
+ summary: z27.string(),
4065
+ // Ingestion only: a structured pair the wizard turns into an argv, never a
4066
+ // free-form command string. `runtime` is allowlisted and `entrypoint` is
4067
+ // validated worktree-relative, so the agent cannot inject extra commands.
4068
+ runtime: z27.enum(INGEST_RUNTIMES).optional(),
4069
+ entrypoint: z27.string().optional()
3594
4070
  });
3595
- var verificationOutputSchema = z24.object({
3596
- summary: z24.string(),
3597
- sufficient: z24.boolean(),
3598
- additionalInstructions: z24.string().optional()
4071
+ var verificationOutputSchema = z27.object({
4072
+ summary: z27.string(),
4073
+ sufficient: z27.boolean(),
4074
+ additionalInstructions: z27.string().optional()
3599
4075
  });
3600
4076
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3601
4077
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -3638,22 +4114,42 @@ function publicEnvPrefix(language) {
3638
4114
  }
3639
4115
  return "PUBLIC_";
3640
4116
  }
3641
- function searchEnvVars(language, appId, searchKey) {
3642
- const prefix = publicEnvPrefix(language);
4117
+ var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4118
+ var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4119
+ var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
4120
+ function appIdVar(language) {
4121
+ return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
4122
+ }
4123
+ function searchKeyVar(language) {
4124
+ return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
4125
+ }
4126
+ function searchIndexVar(language) {
4127
+ return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
4128
+ }
4129
+ function searchEnvVars(language, index, appId, searchKey) {
3643
4130
  return [
3644
4131
  {
3645
- name: `${prefix}ALGOLIA_APP_ID`,
4132
+ name: appIdVar(language),
3646
4133
  value: appId ?? "<your-algolia-app-id>"
3647
4134
  },
3648
4135
  {
3649
- name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
4136
+ name: searchKeyVar(language),
3650
4137
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4138
+ },
4139
+ // Wizard-supplied rather than written into the generated code, because an
4140
+ // agent that retypes the name (appending the project name, re-casing it)
4141
+ // leaves the UI querying an index that does not exist.
4142
+ {
4143
+ name: searchIndexVar(language),
4144
+ value: index
3651
4145
  }
3652
4146
  ];
3653
4147
  }
3654
4148
  function baseInstructions(input) {
3655
4149
  return [
3656
- `Target Algolia index: ${input.targetIndex}`,
4150
+ // Agents have renamed this (e.g. appending the project name), which the
4151
+ // index-scoped keys then reject with a 403.
4152
+ `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
3657
4153
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3658
4154
  "Make minimal, idiomatic changes; do not touch unrelated code."
3659
4155
  ];
@@ -3667,9 +4163,6 @@ function sourceSpecificInstructions(input) {
3667
4163
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
3668
4164
  ],
3669
4165
  fileUpload: [
3670
- // The wizard already copied the developer's file into the worktree at this
3671
- // exact path, so the agent must read it directly — never search for or
3672
- // substitute another file.
3673
4166
  `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
3674
4167
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
3675
4168
  "Map parsed columns/fields to the confirmed entity attributes.",
@@ -3690,6 +4183,7 @@ function ingestionInstructions(input) {
3690
4183
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3691
4184
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3692
4185
  `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.`,
4186
+ `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
3693
4187
  "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
3694
4188
  "After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
3695
4189
  getNamedDoc("save-records", "js"),
@@ -3707,16 +4201,18 @@ function searchInstructions(input) {
3707
4201
  `Build the search UI for ${input.uiFramework}.`,
3708
4202
  "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3709
4203
  doc,
3710
- `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
3711
- "Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
3712
- // appId always resolves (loadActiveProfile throws otherwise); only the
3713
- // search-only key is best-effort and can fall back to a placeholder.
3714
- `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
3715
- // Names are fixed, not the agent's to rename: the wizard writes the
3716
- // resolved app id / search-only key into ".env" under these exact names
3717
- // right after this step, so a renamed prefix here would leave the code
4204
+ `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 target index.`,
4205
+ `Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4206
+ // The key is provisioned only after verification passes, so the agent never
4207
+ // sees one. It must also leave .env alone: the wizard reads that file to
4208
+ // decide whether a key already exists, and an agent-invented value there
4209
+ // would be reused as if it were real.
4210
+ `Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
4211
+ // Not the agent's to rename: the wizard writes these exact names into
4212
+ // ".env" right after this step, so a renamed prefix would leave the code
3718
4213
  // reading a var the wizard never wrote.
3719
4214
  `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4215
+ "Read the App ID, the search-only API key, and the index name from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
3720
4216
  '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.',
3721
4217
  "The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
3722
4218
  ];
@@ -3858,6 +4354,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3858
4354
  }
3859
4355
  }
3860
4356
  const targetIndex = selected?.selection;
4357
+ useWizard.getState().setTargetIndex(targetIndex ?? null);
3861
4358
  await assertGitRepoWithHead(repoRoot);
3862
4359
  if (await isWorkingTreeDirty(repoRoot)) {
3863
4360
  await confirmDirtyWorkingTree(ctx, repoRoot);
@@ -3866,17 +4363,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3866
4363
  const confirmed2 = normalized.confirmedEntities;
3867
4364
  const searchLocation = normalized.searchImplementationAnalysis;
3868
4365
  let appId;
3869
- let searchKey;
3870
4366
  if (useCases.includes("search")) {
3871
- appId = (await loadActiveProfile()).appId;
3872
- try {
3873
- searchKey = await resolveSearchOnlyKey(targetIndex);
3874
- } catch (err) {
3875
- logger.warn(
3876
- { err: err.message },
3877
- "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
3878
- );
3879
- }
4367
+ appId = (await requireApplication()).id;
3880
4368
  }
3881
4369
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
3882
4370
  try {
@@ -3908,17 +4396,32 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3908
4396
  targetIndex,
3909
4397
  language,
3910
4398
  appId,
3911
- searchKey,
3912
- searchEnvVars: searchEnvVars(language, appId, searchKey),
4399
+ searchEnvVars: searchEnvVars(language, targetIndex, appId),
3913
4400
  ingestDir: INGEST_DIR,
3914
4401
  ingestionSource,
3915
4402
  uploadFilePath,
3916
- // language.frameworks already prefers the confirm-framework step output,
3917
- // so the user's confirmed stack (not just raw detection) picks the flavor.
3918
4403
  uiFramework: detectUiFramework(language)
3919
4404
  };
3920
4405
  const summaries = [];
3921
4406
  if (uploadWarning) summaries.push(uploadWarning);
4407
+ let envSearchKey;
4408
+ let envAppIdMismatch = false;
4409
+ if (useCases.includes("search") && appId) {
4410
+ const envAppId = await readEnvVar(worktree, appIdVar(language));
4411
+ if (envAppId === appId) {
4412
+ envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
4413
+ } else if (envAppId) {
4414
+ envAppIdMismatch = true;
4415
+ summaries.push(
4416
+ `\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
4417
+ );
4418
+ logger.warn(
4419
+ { envAppId, appId },
4420
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4421
+ );
4422
+ }
4423
+ }
4424
+ let finalSearchEnvVars = input.searchEnvVars;
3922
4425
  let agentRuns = 0;
3923
4426
  let ingestRuntime;
3924
4427
  let ingestEntrypoint;
@@ -3979,7 +4482,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3979
4482
  messages: []
3980
4483
  }) === true;
3981
4484
  if (runNow) {
3982
- const profile = await loadActiveProfile();
4485
+ const ingestApp = await requireApplication();
4486
+ const writeKey = (await resolveWriteKey(targetIndex, ingestApp.id)).key;
3983
4487
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
3984
4488
  const scriptLogId = ctx.logStart("runIngestScript", {
3985
4489
  runtime: ingestRuntime,
@@ -3991,8 +4495,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3991
4495
  ingestRuntime,
3992
4496
  ingestEntrypoint,
3993
4497
  {
3994
- [APP_ID_VAR]: profile.appId,
3995
- [API_KEY_VAR]: profile.apiKey
4498
+ [APP_ID_VAR]: ingestApp.id,
4499
+ [API_KEY_VAR]: writeKey,
4500
+ [INDEX_NAME_VAR]: targetIndex
3996
4501
  }
3997
4502
  );
3998
4503
  ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
@@ -4061,8 +4566,6 @@ ${run2.output}` : status;
4061
4566
  );
4062
4567
  }
4063
4568
  await ctx.requestUserInput({
4064
- // No question being asked here, just an acknowledgement — the
4065
- // continue/decline hints below already say "continue".
4066
4569
  prompt: "",
4067
4570
  promptType: "enterToContinue",
4068
4571
  options: [],
@@ -4111,7 +4614,34 @@ ${run2.output}` : status;
4111
4614
  }
4112
4615
  extraInstructions = verificationRetryInstructions(verification);
4113
4616
  }
4114
- const resolvedSearchEnvVars = input.searchEnvVars.filter(
4617
+ let searchKey;
4618
+ let searchKeyError;
4619
+ if (appId) {
4620
+ try {
4621
+ const resolved2 = await resolveSearchOnlyKey(
4622
+ targetIndex,
4623
+ appId,
4624
+ envSearchKey
4625
+ );
4626
+ searchKey = resolved2.key;
4627
+ summaries.push(
4628
+ resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
4629
+ );
4630
+ } catch (err) {
4631
+ searchKeyError = err.message;
4632
+ logger.warn(
4633
+ { err: searchKeyError },
4634
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4635
+ );
4636
+ }
4637
+ }
4638
+ finalSearchEnvVars = searchEnvVars(
4639
+ language,
4640
+ targetIndex,
4641
+ appId,
4642
+ searchKey
4643
+ );
4644
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4115
4645
  (v) => !v.value.startsWith("<")
4116
4646
  );
4117
4647
  if (resolvedSearchEnvVars.length > 0) {
@@ -4122,13 +4652,29 @@ ${run2.output}` : status;
4122
4652
  if (written.length > 0) {
4123
4653
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4124
4654
  }
4655
+ const stale = [];
4656
+ for (const v of resolvedSearchEnvVars) {
4657
+ if (written.includes(v.name)) continue;
4658
+ const current = await readEnvVar(worktree, v.name);
4659
+ if (current && current !== v.value) stale.push(v);
4660
+ }
4661
+ if (stale.length > 0 && !envAppIdMismatch) {
4662
+ summaries.push(
4663
+ `\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
4664
+ );
4665
+ logger.warn(
4666
+ { vars: stale.map((v) => v.name) },
4667
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4668
+ );
4669
+ }
4125
4670
  }
4126
- const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4671
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4127
4672
  (v) => v.value.startsWith("<")
4128
4673
  );
4129
4674
  if (unresolvedSearchEnvVars.length > 0) {
4130
4675
  summaries.push(
4131
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4676
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
4677
+ (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4132
4678
  );
4133
4679
  }
4134
4680
  } else {
@@ -4160,7 +4706,7 @@ ${run2.output}` : status;
4160
4706
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4161
4707
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4162
4708
  } : {},
4163
- ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4709
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4164
4710
  };
4165
4711
  } finally {
4166
4712
  process.chdir(repoRoot);
@@ -4203,8 +4749,8 @@ var defaultWorkflow = {
4203
4749
  defineStep({
4204
4750
  id: "select-index",
4205
4751
  title: "Set up index",
4206
- outputSchema: z25.object({
4207
- selection: z25.string()
4752
+ outputSchema: z28.object({
4753
+ selection: z28.string()
4208
4754
  }),
4209
4755
  run: (ctx) => selectIndexStep(ctx)
4210
4756
  }),
@@ -4446,7 +4992,11 @@ Options:
4446
4992
  --no-telemetry Send no telemetry or analytics for this run.
4447
4993
  --reset-on-run Wipe this project's wizard state (run state, AI consent,
4448
4994
  worktrees) before starting, so the run behaves like a
4449
- first-ever run. Algolia credentials are not touched.
4995
+ first-ever run. Also drops every API key the wizard has
4996
+ stored in your keychain (or, where the platform has none,
4997
+ the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
4998
+ for this project and any other, so later runs create new
4999
+ ones. Your Algolia login is not touched.
4450
5000
  -h, --help Print this message.`;
4451
5001
  function parseCliArgs(argv) {
4452
5002
  const positionals = [];
@@ -4487,6 +5037,7 @@ import { join as join12 } from "node:path";
4487
5037
  var KEEP = ["wizard.log"];
4488
5038
  async function resetProjectState() {
4489
5039
  const dir = stateDir();
5040
+ await forgetResolvedKeys();
4490
5041
  let entries;
4491
5042
  try {
4492
5043
  entries = await readdir4(dir);
@@ -4495,7 +5046,9 @@ async function resetProjectState() {
4495
5046
  }
4496
5047
  const targets = entries.filter((name) => !KEEP.includes(name));
4497
5048
  await Promise.all(
4498
- targets.map((name) => rm2(join12(dir, name), { recursive: true, force: true }))
5049
+ targets.map(
5050
+ (name) => rm2(join12(dir, name), { recursive: true, force: true })
5051
+ )
4499
5052
  );
4500
5053
  return { dir, removed: targets };
4501
5054
  }
@@ -4503,6 +5056,7 @@ async function resetProjectState() {
4503
5056
  // src/main.tsx
4504
5057
  import { jsx as jsx14 } from "react/jsx-runtime";
4505
5058
  async function startup() {
5059
+ setProjectRoot(process.cwd());
4506
5060
  let args;
4507
5061
  try {
4508
5062
  args = parseCliArgs(process.argv.slice(2));
@@ -4550,31 +5104,38 @@ ${formatStepList(workflow)}`);
4550
5104
  }
4551
5105
  async function run(workflow) {
4552
5106
  const store = useWizard.getState();
4553
- let instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
5107
+ const instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
5108
+ await store.waitForStart();
4554
5109
  let user = await getUser();
4555
5110
  if (!user) {
4556
- await instance.waitUntilRenderFlush();
4557
- instance.cleanup();
5111
+ store.beginAuth();
4558
5112
  try {
4559
5113
  await runAuthLogin();
4560
5114
  } catch (err) {
4561
- console.error(err instanceof Error ? err.message : String(err));
5115
+ store.setError(err instanceof Error ? err.message : String(err));
5116
+ await instance.waitUntilExit();
4562
5117
  process.exit(1);
4563
5118
  }
4564
- instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
5119
+ store.endAuth();
4565
5120
  user = await getUser();
4566
5121
  if (!user) {
4567
5122
  store.setError(
4568
- "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli auth login` directly."
5123
+ "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli@latest auth login` directly."
4569
5124
  );
4570
5125
  await instance.waitUntilExit();
4571
5126
  process.exit(1);
4572
5127
  }
4573
5128
  }
4574
5129
  store.setUser(user);
4575
- const profile = await loadActiveProfile();
4576
- await store.waitForStart();
4577
- runWorkflow(workflow, profile?.appId);
5130
+ let app;
5131
+ try {
5132
+ app = await ensureApplication();
5133
+ } catch (err) {
5134
+ store.setError(err instanceof Error ? err.message : String(err));
5135
+ await instance.waitUntilExit();
5136
+ process.exit(1);
5137
+ }
5138
+ runWorkflow(workflow, app.id);
4578
5139
  }
4579
5140
  var started = await startup();
4580
5141
  if (typeof started === "number") {