@algolia/wizard 0.3.0-rc.44.2 → 0.3.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 (2) hide show
  1. package/dist/main.js +941 -568
  2. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -4,27 +4,191 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/ui/App.tsx
7
- import { useEffect as useEffect2, useState as useState4 } from "react";
8
- import { Box as Box10, Text as Text10, useApp, useInput as useInput3, useWindowSize as useWindowSize2 } from "ink";
7
+ import { Box as Box12, Text as Text12, useApp, useInput as useInput5, useWindowSize as useWindowSize4 } from "ink";
9
8
 
10
9
  // src/core/store.ts
11
10
  import { create } from "zustand";
11
+
12
+ // src/lib/algoliaCli.ts
13
+ import { spawn } from "node:child_process";
14
+ import { createRequire } from "node:module";
15
+ var require2 = createRequire(import.meta.url);
16
+ function algoliaCliEntry() {
17
+ return require2.resolve("@algolia/cli/bin/run.js");
18
+ }
19
+ function runAlgoliaCli(args) {
20
+ return new Promise((resolve4, reject) => {
21
+ const child = spawn(process.execPath, [algoliaCliEntry(), ...args]);
22
+ let stdout = "";
23
+ let stderr = "";
24
+ child.stdout.on("data", (chunk) => stdout += chunk);
25
+ child.stderr.on("data", (chunk) => stderr += chunk);
26
+ child.on("error", reject);
27
+ child.on("close", (code) => {
28
+ if (code === 0) {
29
+ resolve4(stdout);
30
+ } else {
31
+ const detail = stderr.trim() || stdout.trim();
32
+ reject(
33
+ new Error(
34
+ `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail ? `: ${detail}` : ""}`
35
+ )
36
+ );
37
+ }
38
+ });
39
+ });
40
+ }
41
+ async function getUser() {
42
+ let raw;
43
+ try {
44
+ raw = await runAlgoliaCli(["auth", "get", "--with-access-token"]);
45
+ } catch {
46
+ return null;
47
+ }
48
+ try {
49
+ return toUserInfo(JSON.parse(raw));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function runAuthLogin() {
55
+ return new Promise((resolve4, reject) => {
56
+ const child = spawn(
57
+ process.execPath,
58
+ [algoliaCliEntry(), "auth", "login", "--default"],
59
+ { stdio: "inherit" }
60
+ );
61
+ child.on("error", reject);
62
+ child.on("close", (code) => {
63
+ if (code === 0) resolve4();
64
+ else reject(new Error(`Algolia authentication failed (exit ${code}).`));
65
+ });
66
+ });
67
+ }
68
+
69
+ // src/lib/auth.ts
70
+ function getAuthToken() {
71
+ return useWizard.getState().user?.token || null;
72
+ }
73
+ var inFlightRefresh = null;
74
+ function refreshAuthToken() {
75
+ inFlightRefresh ??= (async () => {
76
+ try {
77
+ const raw = await runAlgoliaCli(["auth", "get", "--with-access-token"]);
78
+ const user2 = toUserInfo(JSON.parse(raw));
79
+ useWizard.getState().setUser(user2);
80
+ return user2.token;
81
+ } catch {
82
+ return null;
83
+ } finally {
84
+ inFlightRefresh = null;
85
+ }
86
+ })();
87
+ return inFlightRefresh;
88
+ }
89
+
90
+ // src/lib/logger.ts
91
+ import pino from "pino";
92
+ import { join as join2, dirname } from "node:path";
93
+ import { devNull } from "node:os";
94
+ import { mkdirSync, openSync, closeSync } from "node:fs";
95
+
96
+ // src/core/constants.ts
97
+ import { homedir } from "node:os";
98
+ import { join, resolve } from "node:path";
99
+ function rootDir() {
100
+ return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
101
+ }
102
+ function projectSlug(cwd = process.cwd()) {
103
+ return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
104
+ }
105
+ function stateDir(cwd = process.cwd()) {
106
+ return join(rootDir(), projectSlug(cwd));
107
+ }
108
+
109
+ // src/lib/logger.ts
110
+ var STDERR_FD = 2;
111
+ function resolveDest() {
112
+ const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
113
+ try {
114
+ mkdirSync(dirname(target), { recursive: true });
115
+ closeSync(openSync(target, "a"));
116
+ return target;
117
+ } catch {
118
+ return STDERR_FD;
119
+ }
120
+ }
121
+ function logDestination() {
122
+ return pino.destination({ dest: resolveDest(), sync: false });
123
+ }
124
+ var logger = pino(
125
+ { level: process.env.LOG_LEVEL ?? "info" },
126
+ logDestination()
127
+ );
128
+
129
+ // src/lib/proxyFetch.ts
130
+ var PROXY_BASE_URL = process.env.PROXY_BASE_URL ?? "https://proxy-624203421261.us-east4.run.app";
131
+ var PROXY_AUTH_REJECTED_HEADER = "x-wizard-proxy-auth";
132
+ var proxyFetch = async (input, init) => {
133
+ const req = new Request(input, init);
134
+ const retry = req.clone();
135
+ const res = await fetch(req);
136
+ if (res.status !== 401) return res;
137
+ if (res.headers.get(PROXY_AUTH_REJECTED_HEADER) !== "rejected") return res;
138
+ const fresh = await refreshAuthToken();
139
+ if (!fresh) return res;
140
+ const headers = new Headers(retry.headers);
141
+ if (headers.has("authorization")) {
142
+ headers.set("authorization", `Bearer ${fresh}`);
143
+ } else {
144
+ headers.set("x-api-key", fresh);
145
+ }
146
+ return fetch(new Request(retry, { headers }));
147
+ };
148
+
149
+ // src/lib/interaction.ts
150
+ async function markInteraction() {
151
+ const token = getAuthToken();
152
+ if (!token) return;
153
+ try {
154
+ const res = await proxyFetch(`${PROXY_BASE_URL}/interaction`, {
155
+ method: "POST",
156
+ headers: { authorization: `Bearer ${token}` }
157
+ });
158
+ if (!res.ok) throw new Error(`proxy interaction ${res.status}`);
159
+ } catch (err) {
160
+ logger.warn({ err }, "failed to mark wizard interaction");
161
+ }
162
+ }
163
+
164
+ // src/core/store.ts
12
165
  function toUserInfo({ user_id, ...rest }) {
13
166
  return { userId: user_id, ...rest };
14
167
  }
168
+ var NOTICE_INTERVAL_MS = 2e3;
15
169
  var useWizard = create((set, get) => ({
16
170
  phase: "idle",
171
+ homeScreen: "home",
17
172
  user: null,
18
173
  workflow: null,
19
174
  steps: [],
20
175
  currentStepIndex: 0,
21
176
  output: "",
177
+ notices: [],
178
+ _noticeQueue: [],
179
+ _noticeTimer: null,
22
180
  error: null,
23
181
  inputReq: null,
24
182
  _resolve: null,
25
183
  // Advances past the welcome screen. Only meaningful from 'idle' — once the
26
184
  // workflow is running there's nothing left to confirm.
27
- confirmStart: () => set((s) => s.phase === "idle" ? { phase: "preflight" } : {}),
185
+ // Reset `homeScreen` so preflight shows Welcome, not the Learn more sub-view.
186
+ confirmStart: () => set(
187
+ (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
188
+ ),
189
+ // Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
190
+ openLearnMore: () => set({ homeScreen: "learnMore" }),
191
+ backToHome: () => set({ homeScreen: "home" }),
28
192
  // Resolves once the phase leaves 'idle', whether that happens before or
29
193
  // after this is called (the welcome screen's spacebar handler is what
30
194
  // drives the transition via `confirmStart`).
@@ -48,10 +212,49 @@ var useWizard = create((set, get) => ({
48
212
  error: null
49
213
  }),
50
214
  syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
51
- setActiveStep: (index) => set({ phase: "running", currentStepIndex: index, output: "" }),
215
+ setActiveStep: (index) => {
216
+ get()._clearNoticeQueue();
217
+ set({ phase: "running", currentStepIndex: index, output: "", notices: [] });
218
+ },
52
219
  setUser: (user2) => set({ user: user2 }),
53
220
  appendToken: (text) => set((s) => ({ output: s.output + text })),
54
221
  clearOutput: () => set({ output: "" }),
222
+ // Renders the first notice of a burst immediately, then holds later
223
+ // arrivals in `_noticeQueue` and drains one per `NOTICE_INTERVAL_MS` —
224
+ // the timer stays armed through an empty drain so the cooldown always
225
+ // covers the time since the last render, even across bursts.
226
+ pushNotice: (notice) => {
227
+ const { notices, _noticeQueue, _noticeTimer } = get();
228
+ if (_noticeTimer === null) {
229
+ set({
230
+ notices: [...notices, notice],
231
+ _noticeTimer: setTimeout(() => get()._drainNoticeQueue(), NOTICE_INTERVAL_MS)
232
+ });
233
+ } else {
234
+ set({ _noticeQueue: [..._noticeQueue, notice] });
235
+ }
236
+ },
237
+ _drainNoticeQueue: () => {
238
+ const [next, ...rest] = get()._noticeQueue;
239
+ if (next) {
240
+ set((s) => ({
241
+ notices: [...s.notices, next],
242
+ _noticeQueue: rest,
243
+ _noticeTimer: setTimeout(() => get()._drainNoticeQueue(), NOTICE_INTERVAL_MS)
244
+ }));
245
+ } else {
246
+ set({ _noticeTimer: null });
247
+ }
248
+ },
249
+ _clearNoticeQueue: () => {
250
+ const timer = get()._noticeTimer;
251
+ if (timer) clearTimeout(timer);
252
+ set({ _noticeQueue: [], _noticeTimer: null });
253
+ },
254
+ clearNotices: () => {
255
+ get()._clearNoticeQueue();
256
+ set({ notices: [] });
257
+ },
55
258
  requestUserInput: (req) => new Promise((resolve4) => {
56
259
  set({
57
260
  phase: "awaitingInput",
@@ -59,43 +262,33 @@ var useWizard = create((set, get) => ({
59
262
  _resolve: resolve4
60
263
  });
61
264
  }),
62
- submitInput: (value) => {
265
+ submitInput: async (value) => {
266
+ await markInteraction();
63
267
  get()._resolve?.(value);
64
268
  set({ inputReq: null, _resolve: null, phase: "running" });
65
269
  },
66
270
  setDone: () => set({ phase: "done" }),
67
271
  setError: (message) => set({ phase: "error", error: message }),
68
- reset: () => set({
69
- phase: "idle",
70
- workflow: null,
71
- steps: [],
72
- currentStepIndex: 0,
73
- output: "",
74
- error: null,
75
- inputReq: null,
76
- _resolve: null
77
- })
272
+ reset: () => {
273
+ get()._clearNoticeQueue();
274
+ set({
275
+ phase: "idle",
276
+ homeScreen: "home",
277
+ workflow: null,
278
+ steps: [],
279
+ currentStepIndex: 0,
280
+ output: "",
281
+ notices: [],
282
+ error: null,
283
+ inputReq: null,
284
+ _resolve: null
285
+ });
286
+ }
78
287
  }));
79
288
 
80
- // src/lib/clipboard.ts
81
- function copyToClipboard(text, stream = process.stdout) {
82
- const encoded = Buffer.from(text, "utf8").toString("base64");
83
- stream.write(`\x1B]52;c;${encoded}\x07`);
84
- }
85
-
86
- // src/lib/shell.ts
87
- function shellQuote(value) {
88
- return "'" + value.replace(/'/g, "'\\''") + "'";
89
- }
90
-
91
- // src/ui/PromptInput.tsx
92
- import { Box as Box3, Text as Text3 } from "ink";
93
- import TextInput from "ink-text-input";
94
- import { useState as useState3 } from "react";
95
-
96
- // src/ui/SelectPrompt.tsx
97
- import { Box as Box2, Text as Text2, useInput } from "ink";
98
- import { useState as useState2 } from "react";
289
+ // src/ui/Notices.tsx
290
+ import { Box as Box2, Text as Text2, useWindowSize as useWindowSize2 } from "ink";
291
+ import { useEffect as useEffect2, useState as useState2 } from "react";
99
292
 
100
293
  // src/ui/Table.tsx
101
294
  import { Box, Text, measureElement, useWindowSize } from "ink";
@@ -159,6 +352,7 @@ var MARKER = {
159
352
  };
160
353
  var BRAND = "#003DFF";
161
354
  var SECONDARY = "#5468FF";
355
+ var DANGER = "#F86E7E";
162
356
  var COLORS = {
163
357
  brand: BRAND,
164
358
  primary: "#E6EDF3",
@@ -168,23 +362,145 @@ var COLORS = {
168
362
  dim: "#484F58",
169
363
  highlight: { bg: "#12331C", fg: "#4ADE80" },
170
364
  badge: "#E3B341",
171
- warning: "#F86E7E",
365
+ danger: DANGER,
172
366
  success: "#4ADE80",
173
367
  bg: {
174
368
  main: "#0B0E14",
175
369
  sidebar: "#14171E"
176
370
  },
371
+ border: "#30363D",
177
372
  accent: "#76A0FF",
178
373
  status: {
179
374
  pending: "gray",
180
375
  running: "#76A0FF",
181
376
  done: "#4ADE80",
182
- error: "#F86E7E"
377
+ error: DANGER
183
378
  }
184
379
  };
185
380
 
186
- // src/ui/SelectPrompt.tsx
381
+ // src/ui/Notices.tsx
187
382
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
383
+ var AGENT_MARKER = "\u2726";
384
+ var RESERVED_ROWS = 14;
385
+ var PANEL_TEXT_WIDTH = 45;
386
+ function messageLineCount(text) {
387
+ return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH));
388
+ }
389
+ function noticeLineCount(notice) {
390
+ const messageLines = (notice.messages ?? []).reduce((sum, m) => {
391
+ const text = typeof m === "string" ? m : m.text;
392
+ return sum + messageLineCount(text);
393
+ }, 0);
394
+ const tableLines = notice.table ? notice.table.rows.length + 4 : 0;
395
+ return messageLines + tableLines;
396
+ }
397
+ function fitVisibleNotices(notices, windowRows) {
398
+ const budget = Math.max(windowRows - RESERVED_ROWS, 3);
399
+ let used = 0;
400
+ let count = 0;
401
+ for (let i = notices.length - 1; i >= 0; i--) {
402
+ const height = noticeLineCount(notices[i]) + (count > 0 ? 1 : 0);
403
+ if (count > 0 && used + height > budget) break;
404
+ used += height;
405
+ count++;
406
+ }
407
+ return notices.slice(notices.length - count);
408
+ }
409
+ var PULSE_STEPS = 12;
410
+ var PULSE_STEP_MS = 150;
411
+ var PULSE_COLORS = Array.from(
412
+ { length: PULSE_STEPS },
413
+ (_, i) => mixHex(COLORS.accent, COLORS.strong, i / (PULSE_STEPS - 1))
414
+ );
415
+ function mixHex(from, to, t) {
416
+ const a = parseHex(from);
417
+ const b = parseHex(to);
418
+ const channel = (k) => Math.round(a[k] + (b[k] - a[k]) * t).toString(16).padStart(2, "0");
419
+ return `#${channel("r")}${channel("g")}${channel("b")}`;
420
+ }
421
+ function parseHex(hex) {
422
+ const n = hex.replace("#", "");
423
+ return {
424
+ r: parseInt(n.slice(0, 2), 16),
425
+ g: parseInt(n.slice(2, 4), 16),
426
+ b: parseInt(n.slice(4, 6), 16)
427
+ };
428
+ }
429
+ function Notices() {
430
+ const notices = useWizard((s) => s.notices);
431
+ const { rows: windowRows } = useWindowSize2();
432
+ const visible = fitVisibleNotices(notices, windowRows);
433
+ const [pulseStep, setPulseStep] = useState2(0);
434
+ useEffect2(() => {
435
+ let direction = 1;
436
+ const id = setInterval(() => {
437
+ setPulseStep((step) => {
438
+ const next = step + direction;
439
+ if (next >= PULSE_COLORS.length - 1) direction = -1;
440
+ else if (next <= 0) direction = 1;
441
+ return Math.min(Math.max(next, 0), PULSE_COLORS.length - 1);
442
+ });
443
+ }, PULSE_STEP_MS);
444
+ return () => clearInterval(id);
445
+ }, []);
446
+ if (!visible.length) return null;
447
+ const pulseColor = PULSE_COLORS[pulseStep];
448
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
449
+ const isLatest = i === visible.length - 1;
450
+ return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
451
+ notice.messages?.map((m, j) => {
452
+ const line = typeof m === "string" ? { text: m } : m;
453
+ const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
454
+ return /* @__PURE__ */ jsxs(
455
+ Text2,
456
+ {
457
+ color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
458
+ bold: line.bold,
459
+ children: [
460
+ prefix,
461
+ line.text
462
+ ]
463
+ },
464
+ `notice-${i}-${j}`
465
+ );
466
+ }),
467
+ notice.table && /* @__PURE__ */ jsx2(Table, { columns: notice.table.columns, rows: notice.table.rows })
468
+ ] }, `notice-${i}`);
469
+ }) });
470
+ }
471
+
472
+ // src/ui/PromptInput.tsx
473
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
474
+ import TextInput from "ink-text-input";
475
+ import { useState as useState4 } from "react";
476
+
477
+ // src/ui/NextAction.tsx
478
+ import { Box as Box3, Text as Text3 } from "ink";
479
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
480
+ function NextAction({
481
+ action,
482
+ keyHint,
483
+ hierarchy = "primary"
484
+ }) {
485
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "row", gap: 1, children: [
486
+ hierarchy === "primary" && /* @__PURE__ */ jsx3(Text3, { color: COLORS.success, bold: true, children: `> ${action}` }),
487
+ hierarchy === "secondary" && /* @__PURE__ */ jsxs2(Fragment, { children: [
488
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.success, bold: true, children: `>` }),
489
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.primary, bold: true, children: action })
490
+ ] }),
491
+ /* @__PURE__ */ jsxs2(Box3, { children: [
492
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: "press " }),
493
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: `[` }),
494
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.primary, children: keyHint }),
495
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: `]` })
496
+ ] })
497
+ ] });
498
+ }
499
+
500
+ // src/ui/SelectPrompt.tsx
501
+ import { Box as Box4, Text as Text4, useInput } from "ink";
502
+ import { useState as useState3 } from "react";
503
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
188
504
  var CANCEL = "cancel";
189
505
  function SelectPrompt({
190
506
  options,
@@ -199,10 +515,10 @@ function SelectPrompt({
199
515
  secondary,
200
516
  defaultSelectedIndex = 0
201
517
  }) {
202
- const [index, setIndex] = useState2(
518
+ const [index, setIndex] = useState3(
203
519
  () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
204
520
  );
205
- const [checked, setChecked] = useState2(() => /* @__PURE__ */ new Set());
521
+ const [checked, setChecked] = useState3(() => /* @__PURE__ */ new Set());
206
522
  const hasCancel = Boolean(multi || cancelable);
207
523
  const rows = hasCancel ? [...options, "Cancel"] : options;
208
524
  const cancelIndex = hasCancel ? options.length : -1;
@@ -239,46 +555,46 @@ function SelectPrompt({
239
555
  }
240
556
  }
241
557
  });
242
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", gap: 1, children: [
243
- error && /* @__PURE__ */ jsx2(Text2, { color: COLORS.warning, children: error }),
244
- messages?.map((m, i) => /* @__PURE__ */ jsx2(Text2, { color: COLORS.muted, children: m }, `msg-${i}`)),
245
- table && /* @__PURE__ */ jsx2(Table, { columns: table.columns, rows: table.rows }),
246
- /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
247
- question && /* @__PURE__ */ jsx2(Text2, { color: COLORS.muted, children: question }),
248
- helpText && /* @__PURE__ */ jsx2(Text2, { color: COLORS.dim, children: helpText })
558
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 1, children: [
559
+ error && /* @__PURE__ */ jsx4(Text4, { color: COLORS.danger, children: error }),
560
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: m }, `msg-${i}`)),
561
+ table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
562
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
563
+ question && /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: question }),
564
+ helpText && /* @__PURE__ */ jsx4(Text4, { color: COLORS.dim, children: helpText })
249
565
  ] }),
250
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: rows.map((option, i) => {
566
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: rows.map((option, i) => {
251
567
  const highlighted = i === index;
252
568
  const isCancel = i === cancelIndex;
253
569
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
254
570
  const sec = isCancel ? void 0 : secondary?.[i];
255
571
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
256
- const label = /* @__PURE__ */ jsxs(Text2, { color: labelColor, children: [
572
+ const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, children: [
257
573
  highlighted ? "\u276F " : " ",
258
574
  bullet,
259
575
  option
260
576
  ] });
261
- return /* @__PURE__ */ jsxs(
262
- Box2,
577
+ return /* @__PURE__ */ jsxs3(
578
+ Box4,
263
579
  {
264
580
  width: rowWidth,
265
581
  paddingX: 1,
266
582
  paddingY: 1,
267
583
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
268
584
  children: [
269
- sec?.kind === "text" ? /* @__PURE__ */ jsx2(Box2, { width: labelColWidth, flexShrink: 0, children: label }) : label,
270
- sec?.kind === "text" && /* @__PURE__ */ jsx2(Text2, { color: highlighted ? COLORS.primary : COLORS.muted, children: sec.value }),
271
- /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
272
- sec?.kind === "badge" && /* @__PURE__ */ jsx2(Text2, { color: COLORS.badge, children: sec.value })
585
+ sec?.kind === "text" ? /* @__PURE__ */ jsx4(Box4, { width: labelColWidth, flexShrink: 0, children: label }) : label,
586
+ sec?.kind === "text" && /* @__PURE__ */ jsx4(Text4, { color: highlighted ? COLORS.primary : COLORS.muted, children: sec.value }),
587
+ /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
588
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, children: sec.value })
273
589
  ]
274
590
  },
275
591
  `row-${i}`
276
592
  );
277
593
  }) }),
278
- /* @__PURE__ */ jsx2(Box2, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(Text2, { children: [
594
+ /* @__PURE__ */ jsx4(Box4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
279
595
  i > 0 ? " " : "",
280
- /* @__PURE__ */ jsx2(Text2, { color: COLORS.primary, children: key }),
281
- /* @__PURE__ */ jsxs(Text2, { color: COLORS.dim, children: [
596
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: key }),
597
+ /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
282
598
  " ",
283
599
  label
284
600
  ] })
@@ -287,17 +603,35 @@ function SelectPrompt({
287
603
  }
288
604
 
289
605
  // src/ui/PromptInput.tsx
290
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
606
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
291
607
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
608
+ function SpaceToContinuePrompt({
609
+ question,
610
+ messages,
611
+ onDecide
612
+ }) {
613
+ useInput2((input, key) => {
614
+ if (input === " ") onDecide(true);
615
+ else if (key.escape) onDecide(false);
616
+ });
617
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
618
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
619
+ question && /* @__PURE__ */ jsx5(Text5, { color: COLORS.primary, children: question }),
620
+ /* @__PURE__ */ jsxs4(Box5, { gap: 1, flexDirection: "column", children: [
621
+ /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "space" }),
622
+ /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
623
+ ] })
624
+ ] });
625
+ }
292
626
  function PromptInput() {
293
627
  const { phase, inputReq, submitInput } = useWizard();
294
- const [draft, setDraft] = useState3("");
628
+ const [draft, setDraft] = useState4("");
295
629
  if (phase === "done" || phase === "error") {
296
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
630
+ return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
297
631
  }
298
632
  if (phase !== "awaitingInput" || !inputReq) return null;
299
633
  if (inputReq.promptType === "multipleChoice") {
300
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
634
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
301
635
  SelectPrompt,
302
636
  {
303
637
  question: inputReq.prompt,
@@ -314,7 +648,7 @@ function PromptInput() {
314
648
  ) });
315
649
  }
316
650
  if (inputReq.promptType === "multiSelect") {
317
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
651
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
318
652
  SelectPrompt,
319
653
  {
320
654
  multi: true,
@@ -329,7 +663,7 @@ function PromptInput() {
329
663
  ) });
330
664
  }
331
665
  if (inputReq.promptType === "notice") {
332
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
666
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
333
667
  SelectPrompt,
334
668
  {
335
669
  question: inputReq.prompt,
@@ -339,9 +673,19 @@ function PromptInput() {
339
673
  }
340
674
  ) });
341
675
  }
676
+ if (inputReq.promptType === "spaceToContinue") {
677
+ return /* @__PURE__ */ jsx5(
678
+ SpaceToContinuePrompt,
679
+ {
680
+ question: inputReq.prompt,
681
+ messages: inputReq.messages,
682
+ onDecide: submitInput
683
+ }
684
+ );
685
+ }
342
686
  if (inputReq.promptType === "acceptReject") {
343
687
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
344
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
688
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
345
689
  SelectPrompt,
346
690
  {
347
691
  question: inputReq.prompt,
@@ -352,15 +696,15 @@ function PromptInput() {
352
696
  }
353
697
  ) });
354
698
  }
355
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
356
- inputReq.error && /* @__PURE__ */ jsx3(Text3, { color: COLORS.warning, children: inputReq.error }),
357
- inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: m }, `msg-${i}`)),
358
- /* @__PURE__ */ jsxs2(Box3, { children: [
359
- /* @__PURE__ */ jsxs2(Text3, { color: COLORS.brand, children: [
699
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
700
+ inputReq.error && /* @__PURE__ */ jsx5(Text5, { color: COLORS.danger, children: inputReq.error }),
701
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
702
+ /* @__PURE__ */ jsxs4(Box5, { children: [
703
+ /* @__PURE__ */ jsxs4(Text5, { color: COLORS.primary, children: [
360
704
  inputReq.prompt,
361
705
  " "
362
706
  ] }),
363
- /* @__PURE__ */ jsx3(
707
+ /* @__PURE__ */ jsx5(
364
708
  TextInput,
365
709
  {
366
710
  value: draft,
@@ -376,32 +720,9 @@ function PromptInput() {
376
720
  }
377
721
 
378
722
  // src/ui/Welcome.tsx
379
- import { dirname, join } from "node:path";
723
+ import { dirname as dirname2, join as join3 } from "node:path";
380
724
  import { fileURLToPath } from "node:url";
381
- import { Box as Box5, Spacer, Text as Text5, useInput as useInput2 } from "ink";
382
-
383
- // src/ui/NextAction.tsx
384
- import { Box as Box4, Text as Text4 } from "ink";
385
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
386
- function NextAction({
387
- action,
388
- keyHint,
389
- hierarchy = "primary"
390
- }) {
391
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
392
- hierarchy === "primary" && /* @__PURE__ */ jsx4(Text4, { color: COLORS.success, bold: true, children: `> ${action}` }),
393
- hierarchy === "secondary" && /* @__PURE__ */ jsxs3(Fragment, { children: [
394
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.success, bold: true, children: `>` }),
395
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, bold: true, children: action })
396
- ] }),
397
- /* @__PURE__ */ jsxs3(Box4, { children: [
398
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: "press " }),
399
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: `[` }),
400
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: keyHint }),
401
- /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: `]` })
402
- ] })
403
- ] });
404
- }
725
+ import { Box as Box6, Spacer, Text as Text6, useInput as useInput3 } from "ink";
405
726
 
406
727
  // src/ui/copy/welcome.ts
407
728
  var sidebarItems = [
@@ -429,31 +750,33 @@ var sidebarItems = [
429
750
 
430
751
  // src/ui/Welcome.tsx
431
752
  import Image, { InkPictureProvider } from "ink-picture";
432
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
433
- var IMAGE_PATH = join(dirname(fileURLToPath(import.meta.url)), "algolia.png");
753
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
754
+ var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
434
755
  function SidebarItem({
435
756
  title,
436
757
  description
437
758
  }) {
438
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
439
- /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
440
- /* @__PURE__ */ jsx5(Text5, { color: COLORS.success, children: "\u2192" }),
441
- /* @__PURE__ */ jsx5(Text5, { color: COLORS.strong, bold: true, children: title })
759
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
760
+ /* @__PURE__ */ jsxs5(Box6, { gap: 1, children: [
761
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.success, children: "\u2192" }),
762
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.strong, bold: true, children: title })
442
763
  ] }),
443
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "row", gap: 2, children: [
444
- /* @__PURE__ */ jsx5(Spacer, {}),
445
- /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: description })
764
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", gap: 2, children: [
765
+ /* @__PURE__ */ jsx6(Spacer, {}),
766
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: description })
446
767
  ] })
447
768
  ] });
448
769
  }
449
770
  function Welcome() {
450
771
  const confirmStart = useWizard((s) => s.confirmStart);
451
- useInput2((input) => {
772
+ const openLearnMore = useWizard((s) => s.openLearnMore);
773
+ useInput3((input) => {
452
774
  if (input === " ") confirmStart();
775
+ else if (input === "i") openLearnMore();
453
776
  });
454
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
455
- /* @__PURE__ */ jsx5(Box5, { padding: 8, flexDirection: "column", justifyContent: "center", children: /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 4, children: [
456
- /* @__PURE__ */ jsx5(InkPictureProvider, { children: /* @__PURE__ */ jsx5(
777
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
778
+ /* @__PURE__ */ jsx6(Box6, { padding: 8, flexDirection: "column", justifyContent: "center", children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 4, children: [
779
+ /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
457
780
  Image,
458
781
  {
459
782
  src: IMAGE_PATH,
@@ -464,14 +787,14 @@ function Welcome() {
464
787
  protocol: "halfBlock"
465
788
  }
466
789
  ) }),
467
- /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
468
- /* @__PURE__ */ jsxs4(Box5, { gap: 1, flexDirection: "column", children: [
469
- /* @__PURE__ */ jsx5(NextAction, { action: "start wizard", keyHint: "space" }),
470
- /* @__PURE__ */ jsx5(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
790
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
791
+ /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
792
+ /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "space" }),
793
+ /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
471
794
  ] })
472
795
  ] }) }),
473
- /* @__PURE__ */ jsxs4(
474
- Box5,
796
+ /* @__PURE__ */ jsxs5(
797
+ Box6,
475
798
  {
476
799
  backgroundColor: COLORS.bg.sidebar,
477
800
  width: 40,
@@ -480,41 +803,168 @@ function Welcome() {
480
803
  flexDirection: "column",
481
804
  justifyContent: "center",
482
805
  children: [
483
- /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
484
- sidebarItems.map((i, idx) => /* @__PURE__ */ jsx5(SidebarItem, { title: i.title, description: i.description }, idx))
806
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
807
+ sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
485
808
  ]
486
809
  }
487
810
  )
488
811
  ] });
489
812
  }
490
813
 
491
- // src/ui/Sidebar.tsx
492
- import { Box as Box8, Text as Text8 } from "ink";
814
+ // src/ui/LearnMore.tsx
815
+ import { Fragment as Fragment2 } from "react";
816
+ import { Box as Box7, Text as Text7, useInput as useInput4, useWindowSize as useWindowSize3 } from "ink";
493
817
 
494
- // src/ui/Steps.tsx
495
- import { Box as Box6, Text as Text6 } from "ink";
496
- import Spinner from "ink-spinner";
497
-
498
- // src/core/persistence.ts
499
- import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
500
- import { join as join3 } from "node:path";
818
+ // src/ui/copy/learn-more.ts
819
+ var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
820
+ var accessItems = [
821
+ {
822
+ tag: "READ",
823
+ title: "Project files",
824
+ description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
825
+ },
826
+ {
827
+ tag: "WRITE",
828
+ title: "Code changes",
829
+ description: "creates & edits files (search UI, config). Shown as a diff first \u2014 nothing lands without your approval."
830
+ },
831
+ {
832
+ tag: "NET",
833
+ title: "Algolia API",
834
+ description: "sends index settings & the records you pick to your Algolia app over HTTPS."
835
+ },
836
+ {
837
+ tag: "KEY",
838
+ title: "Credentials",
839
+ description: "saves your Admin API key to .env and adds it to .gitignore."
840
+ }
841
+ ];
842
+ var neverItems = [
843
+ "Send your source code to a model or third party",
844
+ "Commit or push to git",
845
+ "Touch files outside your project directory"
846
+ ];
847
+ var policyLinks = [
848
+ { label: "Terms", url: "https://www.algolia.com/policies/terms" },
849
+ { label: "Privacy Policy", url: "https://www.algolia.com/policies/privacy" }
850
+ ];
501
851
 
502
- // src/core/constants.ts
503
- import { homedir } from "node:os";
504
- import { join as join2, resolve } from "node:path";
505
- function rootDir() {
506
- return process.env.WIZARD_HOME ?? join2(homedir(), ".algolia");
507
- }
508
- function projectSlug(cwd = process.cwd()) {
509
- return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
852
+ // src/ui/LearnMore.tsx
853
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
854
+ var TAG_COLORS = {
855
+ READ: COLORS.success,
856
+ WRITE: COLORS.badge,
857
+ NET: COLORS.accent,
858
+ KEY: COLORS.muted
859
+ };
860
+ var TAG_COLUMN_WIDTH = 10;
861
+ var PADDING_X = 6;
862
+ var NEVER_BOX_PAD_X = 2;
863
+ function NeverLine({
864
+ width,
865
+ segments = []
866
+ }) {
867
+ const used = segments.reduce((n, s) => n + s.text.length, 0);
868
+ const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
869
+ return /* @__PURE__ */ jsxs6(Text7, { children: [
870
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" }),
871
+ " ".repeat(NEVER_BOX_PAD_X),
872
+ segments.map((s, i) => /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: s.bold, children: s.text }, i)),
873
+ " ".repeat(rightPad),
874
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" })
875
+ ] });
510
876
  }
511
- function stateDir(cwd = process.cwd()) {
512
- return join2(rootDir(), projectSlug(cwd));
877
+ function LearnMore() {
878
+ const confirmStart = useWizard((s) => s.confirmStart);
879
+ const backToHome = useWizard((s) => s.backToHome);
880
+ const { columns } = useWindowSize3();
881
+ const dividerWidth = Math.max(0, columns - PADDING_X * 2);
882
+ useInput4((input, key) => {
883
+ if (key.escape) backToHome();
884
+ else if (input === " ") confirmStart();
885
+ });
886
+ return /* @__PURE__ */ jsxs6(
887
+ Box7,
888
+ {
889
+ flexDirection: "column",
890
+ paddingX: PADDING_X,
891
+ paddingY: 2,
892
+ width: "100%",
893
+ gap: 1,
894
+ children: [
895
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
896
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: accessIntro }),
897
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
898
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
899
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, marginTop: 1, children: [
900
+ /* @__PURE__ */ jsx7(Box7, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text7, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
901
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs6(Text7, { children: [
902
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: item.title }),
903
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
904
+ ] }) })
905
+ ] })
906
+ ] }, item.tag)) }),
907
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
908
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
909
+ /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
910
+ /* @__PURE__ */ jsx7(
911
+ NeverLine,
912
+ {
913
+ width: dividerWidth,
914
+ segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
915
+ }
916
+ ),
917
+ neverItems.map((item) => /* @__PURE__ */ jsxs6(Fragment2, { children: [
918
+ /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
919
+ /* @__PURE__ */ jsx7(
920
+ NeverLine,
921
+ {
922
+ width: dividerWidth,
923
+ segments: [
924
+ { text: "\u2715", color: COLORS.danger },
925
+ { text: " " },
926
+ { text: item, color: COLORS.primary }
927
+ ]
928
+ }
929
+ )
930
+ ] }, item)),
931
+ /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
932
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
933
+ ] }),
934
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
935
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
936
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.accent, children: link.url })
937
+ ] }, link.label)) }),
938
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "row", gap: 3, children: [
939
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
940
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
941
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "esc" }),
942
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "] back" })
943
+ ] }),
944
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
945
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
946
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "space" }),
947
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "]" }),
948
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.success, bold: true, children: "start wizard" })
949
+ ] })
950
+ ] })
951
+ ]
952
+ }
953
+ );
513
954
  }
514
955
 
956
+ // src/ui/Sidebar.tsx
957
+ import { Box as Box10, Text as Text10 } from "ink";
958
+
959
+ // src/ui/Steps.tsx
960
+ import { Box as Box8, Text as Text8 } from "ink";
961
+ import Spinner from "ink-spinner";
962
+
515
963
  // src/core/persistence.ts
964
+ import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
965
+ import { join as join4 } from "node:path";
516
966
  var isStepVisible = (s) => s.visible !== false;
517
- var stateFile = (workflowId) => join3(stateDir(), `state-${workflowId}.json`);
967
+ var stateFile = (workflowId) => join4(stateDir(), `state-${workflowId}.json`);
518
968
  async function loadWorkflowState(workflowId) {
519
969
  try {
520
970
  const raw = await readFile(stateFile(workflowId), "utf8");
@@ -536,154 +986,55 @@ async function clearWorkflowState(workflowId) {
536
986
  }
537
987
 
538
988
  // src/ui/Steps.tsx
539
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
540
- function introspectDetail(output) {
541
- if (!output || typeof output !== "object") return null;
542
- const o = output;
543
- if (!o.recordShape || typeof o.recordShape !== "object" || !Array.isArray(o.facets)) {
544
- return null;
545
- }
546
- const fields = Object.entries(o.recordShape).map(
547
- ([name, type]) => ({ name, type: String(type) })
548
- );
549
- return { fields, facets: o.facets.map(String) };
550
- }
551
- function reviewDetail(output) {
552
- if (!output || typeof output !== "object") return null;
553
- const o = output;
554
- if (!Array.isArray(o.stepSummaries) || typeof o.reviewPrompt !== "string" || !Array.isArray(o.nextSteps)) {
555
- return null;
556
- }
557
- const stepSummaries = o.stepSummaries.filter((entry) => {
558
- if (!entry || typeof entry !== "object") return false;
559
- const s = entry;
560
- return typeof s.stepId === "string" && typeof s.stepTitle === "string" && typeof s.summary === "string";
561
- }).map((s) => ({
562
- stepId: s.stepId,
563
- stepTitle: s.stepTitle,
564
- summary: s.summary
565
- }));
566
- return {
567
- stepSummaries,
568
- reviewPrompt: o.reviewPrompt,
569
- nextSteps: o.nextSteps.map(String)
570
- };
571
- }
572
- function plural(count, noun) {
573
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
574
- }
575
- function IntrospectSummary({ detail }) {
576
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
577
- /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
578
- " ",
579
- "Inferred ",
580
- plural(detail.fields.length, "field"),
581
- ",",
582
- " ",
583
- plural(detail.facets.length, "facet")
584
- ] }),
585
- detail.fields.map((f) => /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
586
- " ",
587
- f.name,
588
- ": ",
589
- f.type
590
- ] }, f.name)),
591
- /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
592
- " ",
593
- "Facets:",
594
- " ",
595
- detail.facets.length ? detail.facets.join(", ") : "none"
596
- ] })
597
- ] });
598
- }
599
- function ReviewSummary({ detail }) {
600
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
601
- detail.stepSummaries.map((s) => /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
602
- " ",
603
- s.stepTitle,
604
- ": ",
605
- s.summary
606
- ] }, s.stepId)),
607
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.brand, children: [
608
- " ",
609
- detail.reviewPrompt
610
- ] }),
611
- detail.nextSteps.map((step, i) => {
612
- const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
613
- const isWorktreeCommand = step.includes("/worktrees/");
614
- const color = isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : "gray";
615
- return /* @__PURE__ */ jsxs5(
616
- Text6,
617
- {
618
- color,
619
- bold: isIngestCommand || isWorktreeCommand,
620
- children: [
621
- " ",
622
- "\u2192 ",
623
- step
624
- ]
625
- },
626
- `next-${i}`
627
- );
628
- })
629
- ] });
630
- }
989
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
631
990
  function Steps() {
632
991
  const { steps } = useWizard();
633
992
  const visibleSteps = steps.filter(isStepVisible);
634
- return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => {
635
- const introspect = s.status === "done" ? introspectDetail(s.output) : null;
636
- const review = s.status === "done" ? reviewDetail(s.output) : null;
637
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
638
- /* @__PURE__ */ jsxs5(Text6, { color: COLORS.status[s.status], children: [
639
- s.status === "running" ? /* @__PURE__ */ jsx6(Spinner, { type: "dots" }) : MARKER[s.status],
640
- " ",
641
- s.title
642
- ] }),
643
- introspect && /* @__PURE__ */ jsx6(IntrospectSummary, { detail: introspect }),
644
- review && /* @__PURE__ */ jsx6(ReviewSummary, { detail: review })
645
- ] }, s.id);
646
- }) });
993
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status[s.status], children: [
994
+ s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
995
+ " ",
996
+ s.title
997
+ ] }) }, s.id)) });
647
998
  }
648
999
  function CurrentStep() {
649
1000
  const { steps } = useWizard();
650
1001
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
651
1002
  if (!currentStep) return null;
652
- return /* @__PURE__ */ jsxs5(Text6, { color: COLORS.status.running, children: [
653
- /* @__PURE__ */ jsx6(Spinner, { type: "dots" }),
1003
+ return /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status.running, children: [
1004
+ /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
654
1005
  " ",
655
1006
  ` ${currentStep.title}`
656
1007
  ] });
657
1008
  }
658
1009
 
659
1010
  // src/ui/Progress.tsx
660
- import { Box as Box7, Text as Text7 } from "ink";
661
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1011
+ import { Box as Box9, Text as Text9 } from "ink";
1012
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
662
1013
  function Progress() {
663
1014
  const { steps, currentStepIndex } = useWizard();
664
1015
  const visibleSteps = steps.filter(isStepVisible);
665
1016
  if (visibleSteps.length === 0) return null;
666
1017
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
667
1018
  const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
668
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
669
- /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "STEP" }),
670
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: activeStepNumber }),
671
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "/" }),
672
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: visibleSteps.length })
1019
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1020
+ /* @__PURE__ */ jsx9(Text9, { color: COLORS.muted, children: "STEP" }),
1021
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: activeStepNumber }),
1022
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "/" }),
1023
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: visibleSteps.length })
673
1024
  ] });
674
1025
  }
675
1026
 
676
1027
  // src/ui/copy/sidebar-commands.ts
677
1028
  var sidebarCommands = [
678
1029
  { keyHint: "tab", description: "toggle logs" },
679
- { keyHint: "esc", description: "close" }
1030
+ { keyHint: "esc", description: "exit wizard" }
680
1031
  ];
681
1032
 
682
1033
  // src/ui/Sidebar.tsx
683
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1034
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
684
1035
  function Sidebar() {
685
- return /* @__PURE__ */ jsxs7(
686
- Box8,
1036
+ return /* @__PURE__ */ jsxs9(
1037
+ Box10,
687
1038
  {
688
1039
  backgroundColor: "#14171E",
689
1040
  width: 30,
@@ -692,16 +1043,16 @@ function Sidebar() {
692
1043
  flexDirection: "column",
693
1044
  justifyContent: "space-between",
694
1045
  children: [
695
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
696
- /* @__PURE__ */ jsx8(Text8, { color: COLORS.muted, children: "PROGRESS" }),
697
- /* @__PURE__ */ jsx8(Steps, {})
1046
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1047
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "PROGRESS" }),
1048
+ /* @__PURE__ */ jsx10(Steps, {})
698
1049
  ] }),
699
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
700
- /* @__PURE__ */ jsx8(Progress, {}),
701
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: sidebarCommands.map((c) => {
702
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 1, children: [
703
- /* @__PURE__ */ jsx8(Text8, { color: COLORS.primary, children: `[${c.keyHint}]` }),
704
- /* @__PURE__ */ jsx8(Text8, { color: COLORS.muted, children: c.description })
1050
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1051
+ /* @__PURE__ */ jsx10(Progress, {}),
1052
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: sidebarCommands.map((c) => {
1053
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1054
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1055
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: c.description })
705
1056
  ] });
706
1057
  }) })
707
1058
  ] })
@@ -711,12 +1062,12 @@ function Sidebar() {
711
1062
  }
712
1063
 
713
1064
  // src/ui/Ribbon.tsx
714
- import { Box as Box9, Text as Text9 } from "ink";
715
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1065
+ import { Box as Box11, Text as Text11 } from "ink";
1066
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
716
1067
  function Ribbon() {
717
1068
  const firstCommand = sidebarCommands[0];
718
- return /* @__PURE__ */ jsxs8(
719
- Box9,
1069
+ return /* @__PURE__ */ jsxs10(
1070
+ Box11,
720
1071
  {
721
1072
  backgroundColor: "#14171E",
722
1073
  flexDirection: "row",
@@ -724,11 +1075,11 @@ function Ribbon() {
724
1075
  paddingX: 2,
725
1076
  paddingY: 1,
726
1077
  children: [
727
- /* @__PURE__ */ jsx9(Progress, {}),
728
- /* @__PURE__ */ jsx9(CurrentStep, {}),
729
- /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
730
- /* @__PURE__ */ jsx9(Text9, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
731
- /* @__PURE__ */ jsx9(Text9, { color: COLORS.muted, children: firstCommand.description })
1078
+ /* @__PURE__ */ jsx11(Progress, {}),
1079
+ /* @__PURE__ */ jsx11(CurrentStep, {}),
1080
+ /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1081
+ /* @__PURE__ */ jsx11(Text11, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1082
+ /* @__PURE__ */ jsx11(Text11, { color: COLORS.muted, children: firstCommand.description })
732
1083
  ] })
733
1084
  ]
734
1085
  }
@@ -736,36 +1087,16 @@ function Ribbon() {
736
1087
  }
737
1088
 
738
1089
  // src/ui/App.tsx
739
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1090
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
740
1091
  function App() {
741
- const { phase, error } = useWizard();
742
- const implementOutput = useWizard((s) => {
743
- return s.steps.find((st) => st.id === "ingestion")?.output;
744
- });
745
- const ingestCommand = implementOutput?.ingestCommand;
746
- const openWorktreeCommand = implementOutput?.worktreePath ? `cd ${shellQuote(implementOutput.worktreePath)}` : void 0;
1092
+ const { phase, error, homeScreen } = useWizard();
747
1093
  const { exit } = useApp();
748
- const { columns, rows } = useWindowSize2();
749
- const [copied, setCopied] = useState4(null);
1094
+ const { columns, rows } = useWindowSize4();
750
1095
  const finished = phase === "done" || phase === "error";
751
- useEffect2(() => {
752
- if (!copied) return;
753
- const timer = setTimeout(() => setCopied(null), 5e3);
754
- return () => clearTimeout(timer);
755
- }, [copied]);
756
- useInput3(
757
- (input, key) => {
1096
+ useInput5(
1097
+ (_input, key) => {
758
1098
  if (key.return || key.escape) {
759
1099
  exit();
760
- return;
761
- }
762
- if (ingestCommand && (input === "c" || input === "C")) {
763
- copyToClipboard(ingestCommand);
764
- setCopied("ingest");
765
- }
766
- if (openWorktreeCommand && (input === "d" || input === "D")) {
767
- copyToClipboard(openWorktreeCommand);
768
- setCopied("worktree");
769
1100
  }
770
1101
  },
771
1102
  { isActive: finished }
@@ -773,38 +1104,36 @@ function App() {
773
1104
  const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
774
1105
  const flexDirection = columns > 90 ? "row" : "column";
775
1106
  const showSidebar = flexDirection === "row";
776
- return /* @__PURE__ */ jsxs9(
777
- Box10,
1107
+ return /* @__PURE__ */ jsxs11(
1108
+ Box12,
778
1109
  {
779
1110
  backgroundColor: COLORS.bg.main,
780
1111
  flexDirection: "row",
781
1112
  width: columns,
782
1113
  minHeight: rows,
783
1114
  children: [
784
- mainWindowVisible && /* @__PURE__ */ jsxs9(
785
- Box10,
1115
+ mainWindowVisible && /* @__PURE__ */ jsxs11(
1116
+ Box12,
786
1117
  {
787
1118
  flexDirection,
788
1119
  width: "100%",
789
1120
  justifyContent: "space-between",
790
1121
  children: [
791
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 4, paddingY: 2, children: [
792
- /* @__PURE__ */ jsx10(PromptInput, {}),
793
- phase === "error" && error && /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.error, children: [
1122
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 4, paddingY: 2, width: 70, children: [
1123
+ /* @__PURE__ */ jsx12(Notices, {}),
1124
+ /* @__PURE__ */ jsx12(PromptInput, {}),
1125
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(CurrentStep, {}) }),
1126
+ phase === "error" && error && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: COLORS.status.error, children: [
794
1127
  "\u2716 ",
795
1128
  error
796
- ] }) }),
797
- finished && (ingestCommand || openWorktreeCommand) && /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", marginTop: 1, children: [
798
- ingestCommand && /* @__PURE__ */ jsx10(Text10, { color: COLORS.brand, children: copied === "ingest" ? "\u2713 Ingestion command copied to clipboard" : 'Press "c" to copy the ingestion command' }),
799
- openWorktreeCommand && /* @__PURE__ */ jsx10(Text10, { color: COLORS.secondary, children: copied === "worktree" ? "\u2713 Worktree command copied to clipboard" : 'Press "d" to copy the command to open the worktree' })
800
- ] })
1129
+ ] }) })
801
1130
  ] }),
802
- /* @__PURE__ */ jsx10(Text10, { color: "white", backgroundColor: "#14171E" }),
803
- showSidebar ? /* @__PURE__ */ jsx10(Sidebar, {}) : /* @__PURE__ */ jsx10(Ribbon, {})
1131
+ /* @__PURE__ */ jsx12(Text12, { color: "white", backgroundColor: "#14171E" }),
1132
+ showSidebar ? /* @__PURE__ */ jsx12(Sidebar, {}) : /* @__PURE__ */ jsx12(Ribbon, {})
804
1133
  ]
805
1134
  }
806
1135
  ),
807
- (phase === "idle" || phase === "preflight") && /* @__PURE__ */ jsx10(Welcome, {})
1136
+ (phase === "idle" || phase === "preflight") && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx12(LearnMore, {}) : /* @__PURE__ */ jsx12(Welcome, {}))
808
1137
  ]
809
1138
  }
810
1139
  );
@@ -815,8 +1144,8 @@ import "zod";
815
1144
 
816
1145
  // src/core/config.ts
817
1146
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
818
- import { join as join4 } from "node:path";
819
- var configFile = () => join4(stateDir(), "config.json");
1147
+ import { join as join5 } from "node:path";
1148
+ var configFile = () => join5(stateDir(), "config.json");
820
1149
  var DEFAULT_CONFIG = {
821
1150
  version: 1,
822
1151
  aiConsent: false,
@@ -840,128 +1169,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
840
1169
  await saveConfig(config);
841
1170
  }
842
1171
 
843
- // src/lib/algoliaCli.ts
844
- import { spawn } from "node:child_process";
845
- import { createRequire } from "node:module";
846
- var require2 = createRequire(import.meta.url);
847
- function algoliaCliEntry() {
848
- return require2.resolve("@algolia/cli/bin/run.js");
849
- }
850
- function runAlgoliaCli(args) {
851
- return new Promise((resolve4, reject) => {
852
- const child = spawn(process.execPath, [algoliaCliEntry(), ...args]);
853
- let stdout = "";
854
- let stderr = "";
855
- child.stdout.on("data", (chunk) => stdout += chunk);
856
- child.stderr.on("data", (chunk) => stderr += chunk);
857
- child.on("error", reject);
858
- child.on("close", (code) => {
859
- if (code === 0) {
860
- resolve4(stdout);
861
- } else {
862
- const detail = stderr.trim() || stdout.trim();
863
- reject(
864
- new Error(
865
- `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail ? `: ${detail}` : ""}`
866
- )
867
- );
868
- }
869
- });
870
- });
871
- }
872
- async function getUser() {
873
- let raw;
874
- try {
875
- raw = await runAlgoliaCli(["auth", "get", "--with-access-token"]);
876
- } catch {
877
- return null;
878
- }
879
- try {
880
- return toUserInfo(JSON.parse(raw));
881
- } catch {
882
- return null;
883
- }
884
- }
885
- function runAuthLogin() {
886
- return new Promise((resolve4, reject) => {
887
- const child = spawn(
888
- process.execPath,
889
- [algoliaCliEntry(), "auth", "login", "--default"],
890
- { stdio: "inherit" }
891
- );
892
- child.on("error", reject);
893
- child.on("close", (code) => {
894
- if (code === 0) resolve4();
895
- else reject(new Error(`Algolia authentication failed (exit ${code}).`));
896
- });
897
- });
898
- }
899
-
900
- // src/lib/auth.ts
901
- function getAuthToken() {
902
- return useWizard.getState().user?.token || null;
903
- }
904
- var inFlightRefresh = null;
905
- function refreshAuthToken() {
906
- inFlightRefresh ??= (async () => {
907
- try {
908
- const raw = await runAlgoliaCli(["auth", "get", "--with-access-token"]);
909
- const user2 = toUserInfo(JSON.parse(raw));
910
- useWizard.getState().setUser(user2);
911
- return user2.token;
912
- } catch {
913
- return null;
914
- } finally {
915
- inFlightRefresh = null;
916
- }
917
- })();
918
- return inFlightRefresh;
919
- }
920
-
921
- // src/lib/logger.ts
922
- import pino from "pino";
923
- import { join as join5, dirname as dirname2 } from "node:path";
924
- import { devNull } from "node:os";
925
- import { mkdirSync, openSync, closeSync } from "node:fs";
926
- var STDERR_FD = 2;
927
- function resolveDest() {
928
- const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join5(stateDir(), "wizard.log");
929
- try {
930
- mkdirSync(dirname2(target), { recursive: true });
931
- closeSync(openSync(target, "a"));
932
- return target;
933
- } catch {
934
- return STDERR_FD;
935
- }
936
- }
937
- function logDestination() {
938
- return pino.destination({ dest: resolveDest(), sync: false });
939
- }
940
- var logger = pino(
941
- { level: process.env.LOG_LEVEL ?? "info" },
942
- logDestination()
943
- );
944
-
945
- // src/lib/proxyFetch.ts
946
- var PROXY_BASE_URL = process.env.PROXY_BASE_URL ?? "https://proxy-624203421261.us-east4.run.app";
947
- var PROXY_AUTH_REJECTED_HEADER = "x-wizard-proxy-auth";
948
- var proxyFetch = async (input, init) => {
949
- const req = new Request(input, init);
950
- const retry = req.clone();
951
- const res = await fetch(req);
952
- if (res.status !== 401) return res;
953
- if (res.headers.get(PROXY_AUTH_REJECTED_HEADER) !== "rejected") return res;
954
- const fresh = await refreshAuthToken();
955
- if (!fresh) return res;
956
- const headers = new Headers(retry.headers);
957
- if (headers.has("authorization")) {
958
- headers.set("authorization", `Bearer ${fresh}`);
959
- } else {
960
- headers.set("x-api-key", fresh);
961
- }
962
- return fetch(new Request(retry, { headers }));
963
- };
964
-
965
1172
  // src/lib/telemetry.ts
966
1173
  function isTelemetryEnabled() {
967
1174
  return Boolean(getAuthToken()) && !process.env.VITEST && process.env.WIZARD_TELEMETRY !== "false";
@@ -1186,12 +1393,14 @@ async function ensureConsent() {
1186
1393
  if (config.aiConsent) return;
1187
1394
  const store2 = useWizard.getState();
1188
1395
  const answer = await store2.requestUserInput({
1189
- prompt: 'Wizard will make AI-authored changes to this repository. Type "yes" to consent:',
1190
- promptType: "textInput",
1396
+ prompt: "Wizard will make AI-authored changes to this repository.",
1397
+ promptType: "spaceToContinue",
1191
1398
  options: []
1192
1399
  });
1193
- if (typeof answer !== "string" || answer.trim().toLowerCase() !== "yes") {
1194
- throw new Error("AI consent declined \u2014 cannot proceed.");
1400
+ if (answer !== true) {
1401
+ throw new Error(
1402
+ "AI consent declined \u2014 cannot proceed. If you change your mind, just run the Wizard again!"
1403
+ );
1195
1404
  }
1196
1405
  config.aiConsent = true;
1197
1406
  await saveConfig(config);
@@ -1212,6 +1421,8 @@ async function makeContext(state) {
1212
1421
  completedSteps,
1213
1422
  getStepOutput: (stepId) => outputs[stepId],
1214
1423
  requestUserInput: (prompt) => useWizard.getState().requestUserInput(prompt),
1424
+ notify: (notice) => useWizard.getState().pushNotice(notice),
1425
+ clearNotices: () => useWizard.getState().clearNotices(),
1215
1426
  updateAlgoliaState: (key, value) => {
1216
1427
  state.algoliaState[key] = value;
1217
1428
  },
@@ -1243,6 +1454,7 @@ async function runStep(state, index, step, appId) {
1243
1454
  store2.setActiveStep(index);
1244
1455
  store2.syncSteps([...state.steps], index);
1245
1456
  await saveWorkflowState(state);
1457
+ await markInteraction();
1246
1458
  const ctx = await makeContext(state);
1247
1459
  const raw = await step.run(ctx);
1248
1460
  const output = step.outputSchema.parse(raw);
@@ -1263,7 +1475,6 @@ async function runStep(state, index, step, appId) {
1263
1475
  async function runWorkflow(workflow2, appId) {
1264
1476
  const store2 = useWizard.getState();
1265
1477
  try {
1266
- await ensureConsent();
1267
1478
  const persisted = await loadWorkflowState(workflow2.id);
1268
1479
  const state = (persisted && reconcileWorkflowState(persisted, workflow2)) ?? initWorkflowState(workflow2, nowIso());
1269
1480
  ensureExecutedStepCount(state);
@@ -1275,6 +1486,7 @@ async function runWorkflow(workflow2, appId) {
1275
1486
  },
1276
1487
  [...state.steps]
1277
1488
  );
1489
+ await ensureConsent();
1278
1490
  trackWorkflowStart({ workflowId: workflow2.id, appId });
1279
1491
  for (let i = state.currentStepIndex; i < workflow2.steps.length; i++) {
1280
1492
  await runStep(state, i, workflow2.steps[i], appId);
@@ -1384,7 +1596,7 @@ async function loadActiveProfile() {
1384
1596
  }
1385
1597
 
1386
1598
  // src/workflows/default.ts
1387
- import { z as z24 } from "zod";
1599
+ import { z as z25 } from "zod";
1388
1600
 
1389
1601
  // src/actions/listIndices.ts
1390
1602
  import { z as z3 } from "zod";
@@ -1864,7 +2076,10 @@ function verifyImplementationTool() {
1864
2076
  import { tool as tool9, generateText, Output, NoObjectGeneratedError } from "ai";
1865
2077
  import { createAnthropic } from "@ai-sdk/anthropic";
1866
2078
  import { nanoid } from "nanoid";
2079
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2080
+ import { dirname as dirname6 } from "node:path";
1867
2081
  import z12 from "zod";
2082
+ var DATA_DIR = ".algolia-wizard/data";
1868
2083
  var RECORD_MODEL = "claude-haiku-4-5";
1869
2084
  var MAX_RECORDS = 100;
1870
2085
  var BATCH_SIZE = 10;
@@ -1872,9 +2087,9 @@ var MAX_BATCH_ATTEMPTS = 3;
1872
2087
  var anthropic = createAnthropic({
1873
2088
  apiKey: process.env.PROVIDER_API_KEY ?? ""
1874
2089
  });
1875
- function generateRecordTool() {
2090
+ function generateRecordTool(ctx) {
1876
2091
  return tool9({
1877
- description: "Generate realistic sample records for an entity. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values and returns fully-formed records, each with a unique objectID. Do not invent the record values or objectIDs yourself \u2014 call this tool.",
2092
+ 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.",
1878
2093
  inputSchema: z12.object({
1879
2094
  entityName: z12.string().describe("Name of the entity to generate records for."),
1880
2095
  attributes: z12.array(z12.string()).describe("Attribute names each record must contain."),
@@ -1930,8 +2145,21 @@ function generateRecordTool() {
1930
2145
  ...record,
1931
2146
  objectID: nanoid()
1932
2147
  }));
1933
- logger.info(records);
1934
- return { records };
2148
+ const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2149
+ const relPath = `${DATA_DIR}/${slug}.json`;
2150
+ const resolved = resolveInRoot(ctx, relPath);
2151
+ if (resolved.ok === false) return resolved.error;
2152
+ if (await hasSymlinkParent(ctx, resolved.target)) {
2153
+ return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2154
+ }
2155
+ await mkdir5(dirname6(resolved.target), { recursive: true });
2156
+ await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2157
+ logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
2158
+ return {
2159
+ filePath: relPath,
2160
+ count: records.length,
2161
+ message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) \u2014 do not inline the records as literals.`
2162
+ };
1935
2163
  } catch (err) {
1936
2164
  return `Error generating records: ${err.message}`;
1937
2165
  }
@@ -1939,6 +2167,25 @@ function generateRecordTool() {
1939
2167
  });
1940
2168
  }
1941
2169
 
2170
+ // src/lib/tools/notifyUser.ts
2171
+ import { tool as tool10 } from "ai";
2172
+ import z13 from "zod";
2173
+ function notifyUserTool() {
2174
+ return tool10({
2175
+ 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.`,
2176
+ inputSchema: z13.object({
2177
+ message: z13.string().describe(
2178
+ "Short, plain-language description of what you are doing now."
2179
+ )
2180
+ }),
2181
+ execute: async ({ message }) => {
2182
+ logger.info({ message }, "called notifyUser tool");
2183
+ useWizard.getState().pushNotice({ messages: [message] });
2184
+ return "ok";
2185
+ }
2186
+ });
2187
+ }
2188
+
1942
2189
  // src/lib/tools/context.ts
1943
2190
  var DEFAULT_TOOL_LIMITS = {
1944
2191
  list: 10,
@@ -1966,10 +2213,11 @@ function createTools(ctx, { output, tools }) {
1966
2213
  writeCredentials: writeCredentialsTool(ctx),
1967
2214
  searchFiles: searchFilesTool(ctx),
1968
2215
  verifyImplementation: verifyImplementationTool(),
1969
- generateRecord: generateRecordTool()
2216
+ generateRecord: generateRecordTool(ctx),
2217
+ notifyUser: notifyUserTool()
1970
2218
  };
1971
2219
  if (!tools) return all;
1972
- const selection = /* @__PURE__ */ new Set([...tools, "reportStatus"]);
2220
+ const selection = /* @__PURE__ */ new Set([...tools, "reportStatus", "notifyUser"]);
1973
2221
  return Object.fromEntries(
1974
2222
  Object.entries(all).filter(([name]) => selection.has(name))
1975
2223
  );
@@ -2002,16 +2250,19 @@ async function runAgent(req) {
2002
2250
  const toolContext = createToolContext();
2003
2251
  const readTools = ["readFile", "searchFiles", "listFiles"];
2004
2252
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2005
- const instructions = hasReadTools ? [
2253
+ const instructions = [
2006
2254
  ...req.instructions ?? [],
2007
- "When you need to read or search multiple files, issue those tool calls together in one step rather than one at a time."
2008
- ] : req.instructions;
2255
+ ...hasReadTools ? [
2256
+ "When you need to read or search multiple files, issue those tool calls together in one step rather than one at a time."
2257
+ ] : [],
2258
+ "Call notifyUser whenever you start a new phase of work or your focus shifts (e.g. moving from reading code to writing files) \u2014 a short, plain-language, high-level update. Do not call it for every tool use."
2259
+ ];
2009
2260
  const agent = new ToolLoopAgent({
2010
2261
  model: anthropic2(MODEL_BY_SIZE[req.modelSize ?? "medium"]),
2011
2262
  // Cache tools + system on the last system block. Tools render before
2012
2263
  // system, so one breakpoint here caches both, reused on every loop turn
2013
2264
  // after the first.
2014
- instructions: instructions?.map((i, idx, arr) => {
2265
+ instructions: instructions.map((i, idx, arr) => {
2015
2266
  return {
2016
2267
  role: "system",
2017
2268
  content: i,
@@ -2079,10 +2330,10 @@ async function runAgent(req) {
2079
2330
  }
2080
2331
 
2081
2332
  // src/actions/detectLanguage.ts
2082
- import z15 from "zod";
2083
- var detectLanguageSchema = z15.object({
2084
- languages: z15.array(z15.object({ name: z15.string(), version: z15.string() })),
2085
- frameworks: z15.array(z15.object({ name: z15.string(), version: z15.string() }))
2333
+ import z16 from "zod";
2334
+ var detectLanguageSchema = z16.object({
2335
+ languages: z16.array(z16.object({ name: z16.string(), version: z16.string() })),
2336
+ frameworks: z16.array(z16.object({ name: z16.string(), version: z16.string() }))
2086
2337
  });
2087
2338
  var detectLanguage = () => runAgent({
2088
2339
  instructions: [
@@ -2100,31 +2351,31 @@ var detectLanguage = () => runAgent({
2100
2351
  });
2101
2352
 
2102
2353
  // src/actions/analyzeCodebase.ts
2103
- import z16 from "zod";
2354
+ import z17 from "zod";
2104
2355
  var READONLY_TOOLS = [
2105
2356
  "listFiles",
2106
2357
  "changeDirectory",
2107
2358
  "readFile",
2108
2359
  "searchFiles"
2109
2360
  ];
2110
- var ingestionAnalysisSchema = z16.object({
2111
- ingestionAnalysis: z16.array(
2112
- z16.object({
2113
- name: z16.string(),
2114
- paths: z16.array(z16.string()),
2361
+ var ingestionAnalysisSchema = z17.object({
2362
+ ingestionAnalysis: z17.array(
2363
+ z17.object({
2364
+ name: z17.string(),
2365
+ paths: z17.array(z17.string()),
2115
2366
  // indexable fields the agent found for this entity
2116
- attributes: z16.array(z16.string())
2367
+ attributes: z17.array(z17.string())
2117
2368
  })
2118
2369
  )
2119
2370
  });
2120
- var searchImplementationAnalysisSchema = z16.object({
2121
- searchImplementationAnalysis: z16.string()
2371
+ var searchImplementationAnalysisSchema = z17.object({
2372
+ searchImplementationAnalysis: z17.string()
2122
2373
  });
2123
- var verificationSchema = z16.object({
2124
- verification: z16.array(z16.string())
2374
+ var verificationSchema = z17.object({
2375
+ verification: z17.array(z17.string())
2125
2376
  });
2126
2377
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
2127
- var analyzeCodebaseSchema = z16.object({
2378
+ var analyzeCodebaseSchema = z17.object({
2128
2379
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2129
2380
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
2130
2381
  verification: verificationSchema.shape.verification.optional(),
@@ -2186,7 +2437,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2186
2437
  // package.json
2187
2438
  var package_default = {
2188
2439
  name: "@algolia/wizard",
2189
- version: "0.3.0-rc.44.2",
2440
+ version: "0.3.0",
2190
2441
  description: "Magically implement Algolia functionality in your codebase",
2191
2442
  type: "module",
2192
2443
  engines: {
@@ -2202,7 +2453,7 @@ var package_default = {
2202
2453
  scripts: {
2203
2454
  "build:proxy": "node scripts/build.mjs proxy",
2204
2455
  build: "node scripts/build.mjs",
2205
- "dev:proxy": "NODE_OPTIONS=--use-system-ca tsx watch src/proxy/index.ts",
2456
+ "dev:proxy": "touch .env && NODE_OPTIONS=--use-system-ca tsx watch --env-file=.env src/proxy/index.ts",
2206
2457
  dev: "touch .env && tsx --env-file=.env ./src/main.tsx",
2207
2458
  "env:load": "pnpm exec -- varlock load",
2208
2459
  prepare: "husky",
@@ -2307,8 +2558,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
2307
2558
  }
2308
2559
 
2309
2560
  // src/actions/confirmLanguage.ts
2310
- import z18 from "zod";
2311
- var confirmLanguageSchema = z18.object({
2561
+ import z19 from "zod";
2562
+ var confirmLanguageSchema = z19.object({
2312
2563
  languages: detectLanguageSchema.shape.languages
2313
2564
  });
2314
2565
  async function confirmLanguage(ctx) {
@@ -2329,8 +2580,8 @@ async function confirmLanguage(ctx) {
2329
2580
  }
2330
2581
 
2331
2582
  // src/actions/confirmFramework.ts
2332
- import z19 from "zod";
2333
- var confirmFrameworkSchema = z19.object({
2583
+ import z20 from "zod";
2584
+ var confirmFrameworkSchema = z20.object({
2334
2585
  frameworks: detectLanguageSchema.shape.frameworks
2335
2586
  });
2336
2587
  var CURATED_FRAMEWORKS = [
@@ -2458,8 +2709,8 @@ async function promptUser(ctx, params) {
2458
2709
  }
2459
2710
 
2460
2711
  // src/actions/confirmEntities.ts
2461
- import z20 from "zod";
2462
- var confirmEntitiesSchema = z20.object({
2712
+ import z21 from "zod";
2713
+ var confirmEntitiesSchema = z21.object({
2463
2714
  // Final detection — the focused re-run may supersede project-scan's.
2464
2715
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2465
2716
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -2529,17 +2780,15 @@ async function confirmEntities(ctx) {
2529
2780
  }
2530
2781
 
2531
2782
  // src/actions/review.ts
2532
- import { z as z21 } from "zod";
2533
- var reviewSchema = z21.object({
2534
- stepSummaries: z21.array(
2535
- z21.object({
2536
- stepId: z21.string(),
2537
- stepTitle: z21.string(),
2538
- summary: z21.string()
2539
- })
2540
- ),
2541
- reviewPrompt: z21.string(),
2542
- nextSteps: z21.array(z21.string())
2783
+ import { z as z22 } from "zod";
2784
+ var reviewSchema = z22.object({
2785
+ // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
2786
+ // not one entry per workflow step — a step's raw output can be a long,
2787
+ // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
2788
+ // that 1:1 is what made the old per-step summary an unreadable wall of text.
2789
+ summaryPoints: z22.array(z22.string()),
2790
+ reviewPrompt: z22.string(),
2791
+ nextSteps: z22.array(z22.string())
2543
2792
  });
2544
2793
  function formatCompletedSteps(steps) {
2545
2794
  if (!steps.length) return "(no prior steps completed)";
@@ -2549,32 +2798,55 @@ Output:
2549
2798
  ${JSON.stringify(s.output, null, 2)}`
2550
2799
  ).join("\n\n");
2551
2800
  }
2552
- var reviewStep = (ctx, options) => runAgent({
2553
- instructions: [
2554
- "Summarize what was accomplished in the workflow, leaving out verbose details.",
2555
- "Base your summary only on the step outputs provided \u2014 do not read the repository.",
2556
- "Write one entry in stepSummaries per step \u2014 use the exact stepId and stepTitle provided.",
2557
- 'Set reviewPrompt to a short message that asks the user to review the changes \u2014 it must include wording like "Please review the generated code".',
2558
- "Set nextSteps to concrete actions the user should take to use what this workflow set up \u2014 do not suggest open-ended or generic tasks (e.g. deploying, writing tests) unless they are directly required.",
2559
- options.nextStepsGuidance,
2560
- `Completed steps:
2801
+ function formatReviewSummary(result) {
2802
+ const nextStepLines = result.nextSteps.map((step) => {
2803
+ const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
2804
+ const isWorktreeCommand = step.includes("/worktrees/");
2805
+ return {
2806
+ text: `\u2192 ${step}`,
2807
+ color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
2808
+ bold: isIngestCommand || isWorktreeCommand
2809
+ };
2810
+ });
2811
+ return [
2812
+ // Plain lines, same as nextSteps' un-highlighted entries — the summary is
2813
+ // an overview, not a call to action, so it gets no arrow/color/bold.
2814
+ ...result.summaryPoints,
2815
+ { text: result.reviewPrompt, color: COLORS.brand },
2816
+ ...nextStepLines
2817
+ ];
2818
+ }
2819
+ var reviewStep = async (ctx, options) => {
2820
+ const result = await runAgent({
2821
+ instructions: [
2822
+ "Summarize what was accomplished in the workflow, leaving out verbose details.",
2823
+ "Base your summary only on the step outputs provided \u2014 do not read the repository.",
2824
+ "Group summaryPoints into a few broad, high-level takeaways (e.g. what was ingested, what search UI was built) rather than one entry per step \u2014 do not mirror the step outputs 1:1 or restate every detail.",
2825
+ "Each summaryPoint should be a short, standalone statement.",
2826
+ 'Set reviewPrompt to a short message that asks the user to review the changes \u2014 it must include wording like "Please review the generated code".',
2827
+ "Set nextSteps to concrete actions the user should take to use what this workflow set up \u2014 do not suggest open-ended or generic tasks (e.g. deploying, writing tests) unless they are directly required.",
2828
+ options.nextStepsGuidance,
2829
+ `Completed steps:
2561
2830
  ${formatCompletedSteps(ctx.completedSteps)}`,
2562
- "When done, call reportStatus"
2563
- ],
2564
- tools: [],
2565
- outputSchema: reviewSchema,
2566
- modelSize: "small"
2567
- });
2831
+ "When done, call reportStatus"
2832
+ ],
2833
+ tools: [],
2834
+ outputSchema: reviewSchema,
2835
+ modelSize: "small"
2836
+ });
2837
+ ctx.notify({ messages: formatReviewSummary(result) });
2838
+ return result;
2839
+ };
2568
2840
 
2569
2841
  // src/actions/implement.ts
2570
- import z23 from "zod";
2842
+ import z24 from "zod";
2571
2843
 
2572
2844
  // src/lib/worktree.ts
2573
2845
  import { execFile, spawn as spawn3 } from "node:child_process";
2574
- import { copyFile, mkdir as mkdir5, readdir as readdir3, stat as stat2 } from "node:fs/promises";
2846
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
2575
2847
  import {
2576
2848
  basename as basename2,
2577
- dirname as dirname6,
2849
+ dirname as dirname7,
2578
2850
  isAbsolute as isAbsolute2,
2579
2851
  join as join10,
2580
2852
  relative as relative2,
@@ -2638,7 +2910,7 @@ async function createWorktree(repoRoot) {
2638
2910
  const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
2639
2911
  await git(["-C", repoRoot, "worktree", "prune"]);
2640
2912
  await pruneOldWorktrees(repoRoot);
2641
- await mkdir5(dirname6(path), { recursive: true });
2913
+ await mkdir6(dirname7(path), { recursive: true });
2642
2914
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
2643
2915
  return { path, branch };
2644
2916
  }
@@ -2758,7 +3030,7 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
2758
3030
  const relPath = join10(ingestDir, basename2(source));
2759
3031
  const dest = join10(worktreePath, relPath);
2760
3032
  try {
2761
- await mkdir5(dirname6(dest), { recursive: true });
3033
+ await mkdir6(dirname7(dest), { recursive: true });
2762
3034
  await copyFile(source, dest);
2763
3035
  } catch (err) {
2764
3036
  return {
@@ -2768,6 +3040,25 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
2768
3040
  }
2769
3041
  return { ok: true, relPath };
2770
3042
  }
3043
+ function hasEnvVar(content, name) {
3044
+ return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3045
+ }
3046
+ async function writeSearchEnvValues(worktreePath, vars) {
3047
+ const target = join10(worktreePath, ".env");
3048
+ let existing = "";
3049
+ try {
3050
+ existing = await readFile8(target, "utf8");
3051
+ } catch (err) {
3052
+ if (err.code !== "ENOENT") throw err;
3053
+ }
3054
+ const missing = vars.filter((v) => !hasEnvVar(existing, v.name));
3055
+ if (missing.length === 0) return [];
3056
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3057
+ const lines = missing.map(({ name, value }) => `${name}=${value}
3058
+ `).join("");
3059
+ await writeFile6(target, existing + prefix + lines, "utf8");
3060
+ return missing.map((v) => v.name);
3061
+ }
2771
3062
  async function listChangedFiles(worktreePath) {
2772
3063
  const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
2773
3064
  const entries = raw.split("\0");
@@ -2825,20 +3116,20 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
2825
3116
  }
2826
3117
 
2827
3118
  // src/lib/algoliaApiKey.ts
2828
- import { z as z22 } from "zod";
3119
+ import { z as z23 } from "zod";
2829
3120
  var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
2830
- var apiKeySchema = z22.object({
2831
- value: z22.string().min(1),
2832
- acl: z22.array(z22.string()).default([]),
2833
- indexes: z22.array(z22.string()).default([])
3121
+ var apiKeySchema = z23.object({
3122
+ value: z23.string().min(1),
3123
+ acl: z23.array(z23.string()).default([]),
3124
+ indexes: z23.array(z23.string()).default([])
2834
3125
  });
2835
- var apiKeyListSchema = z22.object({
2836
- items: z22.array(apiKeySchema).optional(),
2837
- keys: z22.array(apiKeySchema).optional()
3126
+ var apiKeyListSchema = z23.object({
3127
+ items: z23.array(apiKeySchema).optional(),
3128
+ keys: z23.array(apiKeySchema).optional()
2838
3129
  }).transform((o) => o.items ?? o.keys ?? []);
2839
- var createdKeySchema = z22.object({
2840
- key: z22.string().min(1).optional(),
2841
- value: z22.string().min(1).optional()
3130
+ var createdKeySchema = z23.object({
3131
+ key: z23.string().min(1).optional(),
3132
+ value: z23.string().min(1).optional()
2842
3133
  });
2843
3134
  function canReuse(key, index) {
2844
3135
  return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
@@ -2874,15 +3165,15 @@ async function resolveSearchOnlyKey(index) {
2874
3165
 
2875
3166
  // src/lib/algoliaDocs.ts
2876
3167
  import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
2877
- import { dirname as dirname7, join as join11 } from "node:path";
3168
+ import { dirname as dirname8, join as join11 } from "node:path";
2878
3169
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2879
3170
  var DOCS_SUBPATH = join11("docs", "algolia-sdk");
2880
3171
  function findDocsDir() {
2881
- let dir = dirname7(fileURLToPath2(import.meta.url));
3172
+ let dir = dirname8(fileURLToPath2(import.meta.url));
2882
3173
  for (; ; ) {
2883
3174
  const candidate = join11(dir, DOCS_SUBPATH);
2884
3175
  if (existsSync2(candidate)) return candidate;
2885
- const parent = dirname7(dir);
3176
+ const parent = dirname8(dir);
2886
3177
  if (parent === dir) return void 0;
2887
3178
  dir = parent;
2888
3179
  }
@@ -2932,51 +3223,56 @@ function getFrameworkSpecificDoc(frameworks) {
2932
3223
  return loadAlgoliaDoc("js");
2933
3224
  }
2934
3225
 
3226
+ // src/lib/shell.ts
3227
+ function shellQuote(value) {
3228
+ return "'" + value.replace(/'/g, "'\\''") + "'";
3229
+ }
3230
+
2935
3231
  // src/actions/implement.ts
2936
- var implementSchema = z23.object({
2937
- filesChanged: z23.array(z23.string()),
2938
- summary: z23.string(),
3232
+ var implementSchema = z24.object({
3233
+ filesChanged: z24.array(z24.string()),
3234
+ summary: z24.string(),
2939
3235
  // Absolute path to the throwaway worktree holding the generated changes, so
2940
3236
  // the user can open it (`cd <worktreePath>`) or inspect the diff
2941
3237
  // (`git -C <worktreePath> status/diff`).
2942
- worktreePath: z23.string().optional(),
2943
- ingestCommand: z23.string().optional(),
3238
+ worktreePath: z24.string().optional(),
3239
+ ingestCommand: z24.string().optional(),
2944
3240
  // True when the user accepted the run-now prompt and the wizard executed the
2945
3241
  // ingestion script; downstream steps use this to avoid telling the user to run
2946
3242
  // a script that already ran.
2947
- ingestScriptRan: z23.boolean().optional(),
3243
+ ingestScriptRan: z24.boolean().optional(),
2948
3244
  // Records ingested by the run-now execution, parsed from the script's
2949
3245
  // machine-readable count line; absent when the script didn't run or emitted
2950
3246
  // no parseable count.
2951
- ingestRecordCount: z23.number().optional(),
3247
+ ingestRecordCount: z24.number().optional(),
2952
3248
  // Wall-clock duration of the run-now ingestion execution, in ms.
2953
- ingestDurationMs: z23.number().optional(),
2954
- ingestionSource: z23.enum(["local", "fileUpload", "generated"]),
3249
+ ingestDurationMs: z24.number().optional(),
3250
+ ingestionSource: z24.enum(["local", "fileUpload", "generated"]),
2955
3251
  // Suggested names/values, built from framework detection. The search agent is
2956
3252
  // instructed to rename the prefix if it doesn't match the project's build
2957
3253
  // tool, so the names it actually wrote can differ — treat these as hints, not
2958
3254
  // ground truth (the agent's summary carries the final names).
2959
- searchEnvVars: z23.array(
2960
- z23.object({
2961
- name: z23.string(),
2962
- value: z23.string()
3255
+ searchEnvVars: z24.array(
3256
+ z24.object({
3257
+ name: z24.string(),
3258
+ value: z24.string()
2963
3259
  })
2964
3260
  ).optional()
2965
3261
  });
2966
- var implementationOutputSchema = z23.object({
2967
- summary: z23.string(),
3262
+ var implementationOutputSchema = z24.object({
3263
+ summary: z24.string(),
2968
3264
  // Ingestion only: how to run the generated script, as a structured pair the
2969
3265
  // wizard turns into an argv (`<runtime> <entrypoint>`) — never a free-form
2970
3266
  // command string. `runtime` is constrained to an allowlisted interpreter and
2971
3267
  // `entrypoint` is validated to a worktree-relative path before execution, so
2972
3268
  // the agent cannot inject extra commands or swap the interpreter.
2973
- runtime: z23.enum(INGEST_RUNTIMES).optional(),
2974
- entrypoint: z23.string().optional()
3269
+ runtime: z24.enum(INGEST_RUNTIMES).optional(),
3270
+ entrypoint: z24.string().optional()
2975
3271
  });
2976
- var verificationOutputSchema = z23.object({
2977
- summary: z23.string(),
2978
- sufficient: z23.boolean(),
2979
- additionalInstructions: z23.string().optional()
3272
+ var verificationOutputSchema = z24.object({
3273
+ summary: z24.string(),
3274
+ sufficient: z24.boolean(),
3275
+ additionalInstructions: z24.string().optional()
2980
3276
  });
2981
3277
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
2982
3278
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -3058,9 +3354,9 @@ function sourceSpecificInstructions(input) {
3058
3354
  ],
3059
3355
  generated: [
3060
3356
  "No real data source exists; use sample records for each confirmed entity.",
3061
- "Call the generateRecord tool once per entity (entityName, attributes, count 20-50) to get the records; it invents the values and unique objectIDs. Do not write records or objectIDs yourself.",
3062
- "Bake the returned records inline into the script, clearly commented as generated sample data.",
3063
- "Add a prominent TODO where the developer swaps the generated records for their real record source."
3357
+ "Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
3358
+ "In the script, read and parse each returned file path at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) instead of inlining the records as literals.",
3359
+ "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
3064
3360
  ]
3065
3361
  };
3066
3362
  return byLine[input.ingestionSource];
@@ -3091,11 +3387,12 @@ function searchInstructions(input) {
3091
3387
  `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.`,
3092
3388
  "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.",
3093
3389
  `Values: App ID ${input.appId ? `"${input.appId}"` : "(placeholder for the developer to fill in)"}, search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
3094
- `Suggested public env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3095
- `The prefix on those names was derived from framework detection and may not match the project. If the project's build tool exposes client-side env vars under a different convention (e.g. NEXT_PUBLIC_ for Next.js, NUXT_PUBLIC_ for Nuxt, VITE_ for Vite, PUBLIC_ for Astro), rename the prefix to match, keeping the ALGOLIA_APP_ID and ALGOLIA_SEARCH_API_KEY suffixes. Use the final names consistently in the code, ".env.example", and your summary.`,
3096
- '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.',
3097
- 'Add these env var placeholders to ".env.example", preserving existing entries; create the file if it does not exist.',
3098
- "In your summary, tell the developer to fill in the concrete env values after reviewing the code."
3390
+ // Names are fixed, not the agent's to rename: the wizard writes the
3391
+ // resolved app id / search-only key into ".env" under these exact names
3392
+ // right after this step, so a renamed prefix here would leave the code
3393
+ // reading a var the wizard never wrote.
3394
+ `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3395
+ '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.'
3099
3396
  ];
3100
3397
  }
3101
3398
  function verificationInstructions(input) {
@@ -3127,9 +3424,14 @@ var IMPLEMENT_CONFIG = {
3127
3424
  }
3128
3425
  };
3129
3426
  var useCaseToolMap = {
3130
- ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials"],
3131
- search: [...FS_READ_TOOLS, "writeFile"],
3132
- verification: [...FS_READ_TOOLS, "verifyImplementation"]
3427
+ ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials", "notifyUser"],
3428
+ search: [...FS_READ_TOOLS, "writeFile", "notifyUser"],
3429
+ verification: [
3430
+ ...FS_READ_TOOLS,
3431
+ "writeFile",
3432
+ "verifyImplementation",
3433
+ "notifyUser"
3434
+ ]
3133
3435
  };
3134
3436
  function toolsForUseCase(useCase, ingestionSource) {
3135
3437
  const tools = useCaseToolMap[useCase];
@@ -3152,6 +3454,9 @@ function formatSummary(useCase, summary) {
3152
3454
  const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3153
3455
  return `${label}: ${summary}`;
3154
3456
  }
3457
+ function buildIngestCommand(worktree, runtime, entrypoint) {
3458
+ return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
3459
+ }
3155
3460
  function parseIngestRecordCount(output) {
3156
3461
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
3157
3462
  if (!match) return void 0;
@@ -3295,6 +3600,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3295
3600
  let ingestRecordCount;
3296
3601
  let ingestDurationMs;
3297
3602
  let installFailed = false;
3603
+ let ingestOutcomeMessage;
3298
3604
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
3299
3605
  if (agentRuns > 0) ctx.recordStepExecution();
3300
3606
  agentRuns += 1;
@@ -3332,6 +3638,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3332
3638
  ingestRuntime = runtime;
3333
3639
  ingestEntrypoint = entrypoint;
3334
3640
  if (ingestRuntime && ingestEntrypoint && !installFailed) {
3641
+ ctx.clearNotices();
3335
3642
  const runNow = await ctx.requestUserInput({
3336
3643
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
3337
3644
  promptType: "acceptReject",
@@ -3405,19 +3712,38 @@ ${run.output}` : status;
3405
3712
  });
3406
3713
  }
3407
3714
  summaries.push(summaryLine);
3408
- await ctx.requestUserInput({
3409
- prompt: "Continue",
3410
- promptType: "notice",
3411
- options: [],
3412
- messages: [outcomeMessage]
3413
- });
3715
+ ingestOutcomeMessage = outcomeMessage;
3414
3716
  }
3415
3717
  }
3718
+ const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
3719
+ if (ingestRuntime && ingestEntrypoint) {
3720
+ commandMessages.push(
3721
+ `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
3722
+ );
3723
+ }
3724
+ await ctx.requestUserInput({
3725
+ // No question being asked here, just an acknowledgement — the
3726
+ // continue/decline hints below already say "continue".
3727
+ prompt: "",
3728
+ promptType: "spaceToContinue",
3729
+ options: [],
3730
+ messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
3731
+ });
3416
3732
  }
3417
3733
  if (useCases.includes("search")) {
3418
3734
  let extraInstructions = [];
3419
3735
  const preSearchFiles = new Set(await listChangedFiles(worktree));
3420
3736
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
3737
+ if (attempt > 1) {
3738
+ logger.info(
3739
+ {
3740
+ attempt,
3741
+ maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
3742
+ extraInstructions
3743
+ },
3744
+ "implement: retrying search implementation after failed verification"
3745
+ );
3746
+ }
3421
3747
  const { summary } = await runImplementationUseCase(
3422
3748
  "search",
3423
3749
  extraInstructions
@@ -3446,6 +3772,26 @@ ${run.output}` : status;
3446
3772
  }
3447
3773
  extraInstructions = verificationRetryInstructions(verification);
3448
3774
  }
3775
+ const resolvedSearchEnvVars = input.searchEnvVars.filter(
3776
+ (v) => !v.value.startsWith("<")
3777
+ );
3778
+ if (resolvedSearchEnvVars.length > 0) {
3779
+ const written = await writeSearchEnvValues(
3780
+ worktree,
3781
+ resolvedSearchEnvVars
3782
+ );
3783
+ if (written.length > 0) {
3784
+ summaries.push(`Wrote ${written.join(", ")} to .env.`);
3785
+ }
3786
+ }
3787
+ const unresolvedSearchEnvVars = input.searchEnvVars.filter(
3788
+ (v) => v.value.startsWith("<")
3789
+ );
3790
+ if (unresolvedSearchEnvVars.length > 0) {
3791
+ summaries.push(
3792
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
3793
+ );
3794
+ }
3449
3795
  } else {
3450
3796
  ctx.setUserInput("implementation", "success");
3451
3797
  }
@@ -3466,7 +3812,11 @@ ${run.output}` : status;
3466
3812
  summary: summaries.join("\n\n"),
3467
3813
  worktreePath: worktree,
3468
3814
  ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
3469
- ingestCommand: `cd ${shellQuote(worktree)} && ${ingestRuntime} ${shellQuote(ingestEntrypoint)}`,
3815
+ ingestCommand: buildIngestCommand(
3816
+ worktree,
3817
+ ingestRuntime,
3818
+ ingestEntrypoint
3819
+ ),
3470
3820
  ingestScriptRan,
3471
3821
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
3472
3822
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
@@ -3488,7 +3838,14 @@ var defaultWorkflow = {
3488
3838
  id: "project-scan",
3489
3839
  title: "project scan",
3490
3840
  outputSchema: projectScanSchema,
3491
- run: (ctx) => projectScan(ctx)
3841
+ run: (ctx) => {
3842
+ ctx.notify({
3843
+ messages: [
3844
+ "Scanning your project for languages, frameworks, and Algolia integration points\u2026"
3845
+ ]
3846
+ });
3847
+ return projectScan(ctx);
3848
+ }
3492
3849
  }),
3493
3850
  defineStep({
3494
3851
  id: "confirm-language",
@@ -3507,8 +3864,8 @@ var defaultWorkflow = {
3507
3864
  defineStep({
3508
3865
  id: "select-index",
3509
3866
  title: "Set up index",
3510
- outputSchema: z24.object({
3511
- selection: z24.string()
3867
+ outputSchema: z25.object({
3868
+ selection: z25.string()
3512
3869
  }),
3513
3870
  run: (ctx) => selectIndexStep(ctx)
3514
3871
  }),
@@ -3516,7 +3873,14 @@ var defaultWorkflow = {
3516
3873
  id: "ingestion",
3517
3874
  title: "ingest records",
3518
3875
  outputSchema: implementSchema,
3519
- run: (ctx) => implement(ctx, ["ingestion"])
3876
+ run: (ctx) => {
3877
+ ctx.notify({
3878
+ messages: [
3879
+ "Setting up an Algolia ingestion pipeline in your project\u2026"
3880
+ ]
3881
+ });
3882
+ return implement(ctx, ["ingestion"]);
3883
+ }
3520
3884
  }),
3521
3885
  defineStep({
3522
3886
  id: "confirm-framework",
@@ -3530,6 +3894,9 @@ var defaultWorkflow = {
3530
3894
  title: "create search ui",
3531
3895
  outputSchema: implementSchema,
3532
3896
  run: (ctx) => {
3897
+ ctx.notify({
3898
+ messages: ["Building your Algolia search experience\u2026"]
3899
+ });
3533
3900
  const ingestion = ctx.getStepOutput(
3534
3901
  "ingestion"
3535
3902
  );
@@ -3541,11 +3908,17 @@ var defaultWorkflow = {
3541
3908
  title: "done",
3542
3909
  outputSchema: reviewSchema,
3543
3910
  run: (ctx) => {
3911
+ ctx.notify({
3912
+ messages: ["Summarizing what we did\u2026"]
3913
+ });
3544
3914
  const ingestion = ctx.getStepOutput(
3545
3915
  "ingestion"
3546
3916
  );
3547
3917
  return reviewStep(ctx, {
3548
- nextStepsGuidance: ingestion?.ingestScriptRan ? "The wizard already ran the ingestion script and records are in the index. Do NOT tell the user to run it again; instead point them at the target index to confirm the records, and note they can re-run the `ingestCommand` from the implement step output if they change the data." : "Tell the user to run the ingestion script. Use prior step outputs to make the instructions specific, and include the exact `ingestCommand` from the implement step output."
3918
+ // The ingestion step already showed the user the exact `ingestCommand`
3919
+ // and worktree path as a notice, so nextSteps must not restate it —
3920
+ // an LLM-paraphrased command risks being wrong.
3921
+ nextStepsGuidance: ingestion?.ingestScriptRan ? "The wizard already ran the ingestion script and records are in the index. Do NOT tell the user to run it again; instead point them at the target index to confirm the records. Do not restate the ingestion command \u2014 the wizard already showed it to them." : "Tell the user to run the ingestion script; do not restate the exact command \u2014 the wizard already showed it to them above."
3549
3922
  });
3550
3923
  }
3551
3924
  })
@@ -3561,7 +3934,7 @@ function getWorkflow(id) {
3561
3934
  }
3562
3935
 
3563
3936
  // src/main.tsx
3564
- import { jsx as jsx11 } from "react/jsx-runtime";
3937
+ import { jsx as jsx13 } from "react/jsx-runtime";
3565
3938
  var requestedId = process.argv[2] ?? defaultWorkflow.id;
3566
3939
  var workflow = getWorkflow(requestedId);
3567
3940
  if (!workflow) {
@@ -3570,7 +3943,7 @@ if (!workflow) {
3570
3943
  process.exit(1);
3571
3944
  }
3572
3945
  var store = useWizard.getState();
3573
- var instance = render(/* @__PURE__ */ jsx11(App, {}));
3946
+ var instance = render(/* @__PURE__ */ jsx13(App, {}), { incrementalRendering: true });
3574
3947
  var user = await getUser();
3575
3948
  if (!user) {
3576
3949
  await instance.waitUntilRenderFlush();
@@ -3581,7 +3954,7 @@ if (!user) {
3581
3954
  console.error(err instanceof Error ? err.message : String(err));
3582
3955
  process.exit(1);
3583
3956
  }
3584
- instance = render(/* @__PURE__ */ jsx11(App, {}));
3957
+ instance = render(/* @__PURE__ */ jsx13(App, {}), { incrementalRendering: true });
3585
3958
  user = await getUser();
3586
3959
  if (!user) {
3587
3960
  store.setError(