@algolia/wizard 0.2.0 → 0.3.0-rc.43.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -49
  2. package/dist/main.js +1177 -600
  3. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -4,27 +4,197 @@
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 Box13, Text as Text13, useApp, useInput as useInput6, useWindowSize as useWindowSize5 } from "ink";
9
8
 
10
9
  // src/core/store.ts
11
10
  import { create } from "zustand";
11
+ import { nanoid } from "nanoid";
12
+
13
+ // src/lib/algoliaCli.ts
14
+ import { spawn } from "node:child_process";
15
+ import { createRequire } from "node:module";
16
+ var require2 = createRequire(import.meta.url);
17
+ function algoliaCliEntry() {
18
+ return require2.resolve("@algolia/cli/bin/run.js");
19
+ }
20
+ function runAlgoliaCli(args) {
21
+ return new Promise((resolve4, reject) => {
22
+ const child = spawn(process.execPath, [algoliaCliEntry(), ...args]);
23
+ let stdout = "";
24
+ let stderr = "";
25
+ child.stdout.on("data", (chunk) => stdout += chunk);
26
+ child.stderr.on("data", (chunk) => stderr += chunk);
27
+ child.on("error", reject);
28
+ child.on("close", (code) => {
29
+ if (code === 0) {
30
+ resolve4(stdout);
31
+ } else {
32
+ const detail = stderr.trim() || stdout.trim();
33
+ reject(
34
+ new Error(
35
+ `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail ? `: ${detail}` : ""}`
36
+ )
37
+ );
38
+ }
39
+ });
40
+ });
41
+ }
42
+ async function getUser() {
43
+ let raw;
44
+ try {
45
+ raw = await runAlgoliaCli(["auth", "get", "--with-access-token"]);
46
+ } catch {
47
+ return null;
48
+ }
49
+ try {
50
+ return toUserInfo(JSON.parse(raw));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function runAuthLogin() {
56
+ return new Promise((resolve4, reject) => {
57
+ const child = spawn(
58
+ process.execPath,
59
+ [algoliaCliEntry(), "auth", "login", "--default"],
60
+ { stdio: "inherit" }
61
+ );
62
+ child.on("error", reject);
63
+ child.on("close", (code) => {
64
+ if (code === 0) resolve4();
65
+ else reject(new Error(`Algolia authentication failed (exit ${code}).`));
66
+ });
67
+ });
68
+ }
69
+
70
+ // src/lib/auth.ts
71
+ function getAuthToken() {
72
+ return useWizard.getState().user?.token || null;
73
+ }
74
+ var inFlightRefresh = null;
75
+ function refreshAuthToken() {
76
+ inFlightRefresh ??= (async () => {
77
+ try {
78
+ const raw = await runAlgoliaCli(["auth", "get", "--with-access-token"]);
79
+ const user2 = toUserInfo(JSON.parse(raw));
80
+ useWizard.getState().setUser(user2);
81
+ return user2.token;
82
+ } catch {
83
+ return null;
84
+ } finally {
85
+ inFlightRefresh = null;
86
+ }
87
+ })();
88
+ return inFlightRefresh;
89
+ }
90
+
91
+ // src/lib/logger.ts
92
+ import pino from "pino";
93
+ import { join as join2, dirname } from "node:path";
94
+ import { devNull } from "node:os";
95
+ import { mkdirSync, openSync, closeSync } from "node:fs";
96
+
97
+ // src/core/constants.ts
98
+ import { homedir } from "node:os";
99
+ import { join, resolve } from "node:path";
100
+ function rootDir() {
101
+ return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
102
+ }
103
+ function projectSlug(cwd = process.cwd()) {
104
+ return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
105
+ }
106
+ function stateDir(cwd = process.cwd()) {
107
+ return join(rootDir(), projectSlug(cwd));
108
+ }
109
+
110
+ // src/lib/logger.ts
111
+ var STDERR_FD = 2;
112
+ function resolveDest() {
113
+ const target = process.env.VITEST ? devNull : process.env.WIZARD_LOG ?? join2(stateDir(), "wizard.log");
114
+ try {
115
+ mkdirSync(dirname(target), { recursive: true });
116
+ closeSync(openSync(target, "a"));
117
+ return target;
118
+ } catch {
119
+ return STDERR_FD;
120
+ }
121
+ }
122
+ function logDestination() {
123
+ return pino.destination({ dest: resolveDest(), sync: false });
124
+ }
125
+ var logger = pino(
126
+ { level: process.env.LOG_LEVEL ?? "info" },
127
+ logDestination()
128
+ );
129
+
130
+ // src/lib/proxyFetch.ts
131
+ var PROXY_BASE_URL = process.env.PROXY_BASE_URL ?? "https://proxy-624203421261.us-east4.run.app";
132
+ var PROXY_AUTH_REJECTED_HEADER = "x-wizard-proxy-auth";
133
+ var proxyFetch = async (input, init) => {
134
+ const req = new Request(input, init);
135
+ const retry = req.clone();
136
+ const res = await fetch(req);
137
+ if (res.status !== 401) return res;
138
+ if (res.headers.get(PROXY_AUTH_REJECTED_HEADER) !== "rejected") return res;
139
+ const fresh = await refreshAuthToken();
140
+ if (!fresh) return res;
141
+ const headers = new Headers(retry.headers);
142
+ if (headers.has("authorization")) {
143
+ headers.set("authorization", `Bearer ${fresh}`);
144
+ } else {
145
+ headers.set("x-api-key", fresh);
146
+ }
147
+ return fetch(new Request(retry, { headers }));
148
+ };
149
+
150
+ // src/lib/interaction.ts
151
+ async function markInteraction() {
152
+ const token = getAuthToken();
153
+ if (!token) return;
154
+ try {
155
+ const res = await proxyFetch(`${PROXY_BASE_URL}/interaction`, {
156
+ method: "POST",
157
+ headers: { authorization: `Bearer ${token}` }
158
+ });
159
+ if (!res.ok) throw new Error(`proxy interaction ${res.status}`);
160
+ } catch (err) {
161
+ logger.warn({ err }, "failed to mark wizard interaction");
162
+ }
163
+ }
164
+
165
+ // src/core/store.ts
12
166
  function toUserInfo({ user_id, ...rest }) {
13
167
  return { userId: user_id, ...rest };
14
168
  }
169
+ function describeInputValue(value) {
170
+ if (typeof value === "boolean") return value ? "Yes" : "No";
171
+ return Array.isArray(value) ? value.join(", ") : value;
172
+ }
173
+ var NOTICE_INTERVAL_MS = 2e3;
15
174
  var useWizard = create((set, get) => ({
16
175
  phase: "idle",
176
+ homeScreen: "home",
17
177
  user: null,
18
178
  workflow: null,
19
179
  steps: [],
20
180
  currentStepIndex: 0,
21
181
  output: "",
182
+ notices: [],
183
+ _noticeQueue: [],
184
+ _noticeTimer: null,
185
+ logs: [],
22
186
  error: null,
23
187
  inputReq: null,
24
188
  _resolve: null,
25
189
  // Advances past the welcome screen. Only meaningful from 'idle' — once the
26
190
  // workflow is running there's nothing left to confirm.
27
- confirmStart: () => set((s) => s.phase === "idle" ? { phase: "preflight" } : {}),
191
+ // Reset `homeScreen` so preflight shows Welcome, not the Learn more sub-view.
192
+ confirmStart: () => set(
193
+ (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
194
+ ),
195
+ // Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
196
+ openLearnMore: () => set({ homeScreen: "learnMore" }),
197
+ backToHome: () => set({ homeScreen: "home" }),
28
198
  // Resolves once the phase leaves 'idle', whether that happens before or
29
199
  // after this is called (the welcome screen's spacebar handler is what
30
200
  // drives the transition via `confirmStart`).
@@ -48,10 +218,64 @@ var useWizard = create((set, get) => ({
48
218
  error: null
49
219
  }),
50
220
  syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
51
- setActiveStep: (index) => set({ phase: "running", currentStepIndex: index, output: "" }),
221
+ setActiveStep: (index) => {
222
+ get()._clearNoticeQueue();
223
+ set({ phase: "running", currentStepIndex: index, output: "", notices: [] });
224
+ },
52
225
  setUser: (user2) => set({ user: user2 }),
53
226
  appendToken: (text) => set((s) => ({ output: s.output + text })),
54
227
  clearOutput: () => set({ output: "" }),
228
+ // Renders the first notice of a burst immediately, then holds later
229
+ // arrivals in `_noticeQueue` and drains one per `NOTICE_INTERVAL_MS` —
230
+ // the timer stays armed through an empty drain so the cooldown always
231
+ // covers the time since the last render, even across bursts.
232
+ pushNotice: (notice) => {
233
+ const { notices, _noticeQueue, _noticeTimer } = get();
234
+ if (_noticeTimer === null) {
235
+ set({
236
+ notices: [...notices, notice],
237
+ _noticeTimer: setTimeout(() => get()._drainNoticeQueue(), NOTICE_INTERVAL_MS)
238
+ });
239
+ } else {
240
+ set({ _noticeQueue: [..._noticeQueue, notice] });
241
+ }
242
+ },
243
+ _drainNoticeQueue: () => {
244
+ const [next, ...rest] = get()._noticeQueue;
245
+ if (next) {
246
+ set((s) => ({
247
+ notices: [...s.notices, next],
248
+ _noticeQueue: rest,
249
+ _noticeTimer: setTimeout(() => get()._drainNoticeQueue(), NOTICE_INTERVAL_MS)
250
+ }));
251
+ } else {
252
+ set({ _noticeTimer: null });
253
+ }
254
+ },
255
+ _clearNoticeQueue: () => {
256
+ const timer = get()._noticeTimer;
257
+ if (timer) clearTimeout(timer);
258
+ set({ _noticeQueue: [], _noticeTimer: null });
259
+ },
260
+ clearNotices: () => {
261
+ get()._clearNoticeQueue();
262
+ set({ notices: [] });
263
+ },
264
+ logStart: (kind, name, input) => {
265
+ const id = nanoid();
266
+ set((s) => ({
267
+ logs: [
268
+ ...s.logs,
269
+ { id, kind, name, input, status: "running", startedAt: Date.now() }
270
+ ]
271
+ }));
272
+ return id;
273
+ },
274
+ logEnd: (id, { status, output }) => set((s) => ({
275
+ logs: s.logs.map(
276
+ (t) => t.id === id ? { ...t, status, output, durationMs: Date.now() - t.startedAt } : t
277
+ )
278
+ })),
55
279
  requestUserInput: (req) => new Promise((resolve4) => {
56
280
  set({
57
281
  phase: "awaitingInput",
@@ -59,43 +283,39 @@ var useWizard = create((set, get) => ({
59
283
  _resolve: resolve4
60
284
  });
61
285
  }),
62
- submitInput: (value) => {
286
+ // Logs what the user picked — not the prompt text that was shown, which
287
+ // may repeat or duplicate on-screen content and isn't the useful signal
288
+ // here.
289
+ submitInput: async (value) => {
290
+ await markInteraction();
63
291
  get()._resolve?.(value);
64
292
  set({ inputReq: null, _resolve: null, phase: "running" });
293
+ const id = get().logStart("prompt", `User input: ${describeInputValue(value)}`);
294
+ get().logEnd(id, { status: "success" });
65
295
  },
66
296
  setDone: () => set({ phase: "done" }),
67
297
  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
- })
298
+ reset: () => {
299
+ get()._clearNoticeQueue();
300
+ set({
301
+ phase: "idle",
302
+ homeScreen: "home",
303
+ workflow: null,
304
+ steps: [],
305
+ currentStepIndex: 0,
306
+ output: "",
307
+ notices: [],
308
+ logs: [],
309
+ error: null,
310
+ inputReq: null,
311
+ _resolve: null
312
+ });
313
+ }
78
314
  }));
79
315
 
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";
316
+ // src/ui/Notices.tsx
317
+ import { Box as Box2, Text as Text2, useWindowSize as useWindowSize2 } from "ink";
318
+ import { useEffect as useEffect2, useState as useState2 } from "react";
99
319
 
100
320
  // src/ui/Table.tsx
101
321
  import { Box, Text, measureElement, useWindowSize } from "ink";
@@ -159,6 +379,7 @@ var MARKER = {
159
379
  };
160
380
  var BRAND = "#003DFF";
161
381
  var SECONDARY = "#5468FF";
382
+ var DANGER = "#F86E7E";
162
383
  var COLORS = {
163
384
  brand: BRAND,
164
385
  primary: "#E6EDF3",
@@ -168,23 +389,145 @@ var COLORS = {
168
389
  dim: "#484F58",
169
390
  highlight: { bg: "#12331C", fg: "#4ADE80" },
170
391
  badge: "#E3B341",
171
- warning: "#F86E7E",
392
+ danger: DANGER,
172
393
  success: "#4ADE80",
173
394
  bg: {
174
395
  main: "#0B0E14",
175
396
  sidebar: "#14171E"
176
397
  },
398
+ border: "#30363D",
177
399
  accent: "#76A0FF",
178
400
  status: {
179
401
  pending: "gray",
180
402
  running: "#76A0FF",
181
403
  done: "#4ADE80",
182
- error: "#F86E7E"
404
+ error: DANGER
183
405
  }
184
406
  };
185
407
 
186
- // src/ui/SelectPrompt.tsx
408
+ // src/ui/Notices.tsx
187
409
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
410
+ var AGENT_MARKER = "\u2726";
411
+ var RESERVED_ROWS = 14;
412
+ var PANEL_TEXT_WIDTH = 45;
413
+ function messageLineCount(text) {
414
+ return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH));
415
+ }
416
+ function noticeLineCount(notice) {
417
+ const messageLines = (notice.messages ?? []).reduce((sum, m) => {
418
+ const text = typeof m === "string" ? m : m.text;
419
+ return sum + messageLineCount(text);
420
+ }, 0);
421
+ const tableLines = notice.table ? notice.table.rows.length + 4 : 0;
422
+ return messageLines + tableLines;
423
+ }
424
+ function fitVisibleNotices(notices, windowRows) {
425
+ const budget = Math.max(windowRows - RESERVED_ROWS, 3);
426
+ let used = 0;
427
+ let count = 0;
428
+ for (let i = notices.length - 1; i >= 0; i--) {
429
+ const height = noticeLineCount(notices[i]) + (count > 0 ? 1 : 0);
430
+ if (count > 0 && used + height > budget) break;
431
+ used += height;
432
+ count++;
433
+ }
434
+ return notices.slice(notices.length - count);
435
+ }
436
+ var PULSE_STEPS = 12;
437
+ var PULSE_STEP_MS = 150;
438
+ var PULSE_COLORS = Array.from(
439
+ { length: PULSE_STEPS },
440
+ (_, i) => mixHex(COLORS.accent, COLORS.strong, i / (PULSE_STEPS - 1))
441
+ );
442
+ function mixHex(from, to, t) {
443
+ const a = parseHex(from);
444
+ const b = parseHex(to);
445
+ const channel = (k) => Math.round(a[k] + (b[k] - a[k]) * t).toString(16).padStart(2, "0");
446
+ return `#${channel("r")}${channel("g")}${channel("b")}`;
447
+ }
448
+ function parseHex(hex) {
449
+ const n = hex.replace("#", "");
450
+ return {
451
+ r: parseInt(n.slice(0, 2), 16),
452
+ g: parseInt(n.slice(2, 4), 16),
453
+ b: parseInt(n.slice(4, 6), 16)
454
+ };
455
+ }
456
+ function Notices() {
457
+ const notices = useWizard((s) => s.notices);
458
+ const { rows: windowRows } = useWindowSize2();
459
+ const visible = fitVisibleNotices(notices, windowRows);
460
+ const [pulseStep, setPulseStep] = useState2(0);
461
+ useEffect2(() => {
462
+ let direction = 1;
463
+ const id = setInterval(() => {
464
+ setPulseStep((step) => {
465
+ const next = step + direction;
466
+ if (next >= PULSE_COLORS.length - 1) direction = -1;
467
+ else if (next <= 0) direction = 1;
468
+ return Math.min(Math.max(next, 0), PULSE_COLORS.length - 1);
469
+ });
470
+ }, PULSE_STEP_MS);
471
+ return () => clearInterval(id);
472
+ }, []);
473
+ if (!visible.length) return null;
474
+ const pulseColor = PULSE_COLORS[pulseStep];
475
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
476
+ const isLatest = i === visible.length - 1;
477
+ return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
478
+ notice.messages?.map((m, j) => {
479
+ const line = typeof m === "string" ? { text: m } : m;
480
+ const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
481
+ return /* @__PURE__ */ jsxs(
482
+ Text2,
483
+ {
484
+ color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
485
+ bold: line.bold,
486
+ children: [
487
+ prefix,
488
+ line.text
489
+ ]
490
+ },
491
+ `notice-${i}-${j}`
492
+ );
493
+ }),
494
+ notice.table && /* @__PURE__ */ jsx2(Table, { columns: notice.table.columns, rows: notice.table.rows })
495
+ ] }, `notice-${i}`);
496
+ }) });
497
+ }
498
+
499
+ // src/ui/PromptInput.tsx
500
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
501
+ import TextInput from "ink-text-input";
502
+ import { useState as useState4 } from "react";
503
+
504
+ // src/ui/NextAction.tsx
505
+ import { Box as Box3, Text as Text3 } from "ink";
506
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
507
+ function NextAction({
508
+ action,
509
+ keyHint,
510
+ hierarchy = "primary"
511
+ }) {
512
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "row", gap: 1, children: [
513
+ hierarchy === "primary" && /* @__PURE__ */ jsx3(Text3, { color: COLORS.success, bold: true, children: `> ${action}` }),
514
+ hierarchy === "secondary" && /* @__PURE__ */ jsxs2(Fragment, { children: [
515
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.success, bold: true, children: `>` }),
516
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.primary, bold: true, children: action })
517
+ ] }),
518
+ /* @__PURE__ */ jsxs2(Box3, { children: [
519
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: "press " }),
520
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: `[` }),
521
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.primary, children: keyHint }),
522
+ /* @__PURE__ */ jsx3(Text3, { color: COLORS.muted, children: `]` })
523
+ ] })
524
+ ] });
525
+ }
526
+
527
+ // src/ui/SelectPrompt.tsx
528
+ import { Box as Box4, Text as Text4, useInput } from "ink";
529
+ import { useState as useState3 } from "react";
530
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
188
531
  var CANCEL = "cancel";
189
532
  function SelectPrompt({
190
533
  options,
@@ -199,10 +542,10 @@ function SelectPrompt({
199
542
  secondary,
200
543
  defaultSelectedIndex = 0
201
544
  }) {
202
- const [index, setIndex] = useState2(
545
+ const [index, setIndex] = useState3(
203
546
  () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
204
547
  );
205
- const [checked, setChecked] = useState2(() => /* @__PURE__ */ new Set());
548
+ const [checked, setChecked] = useState3(() => /* @__PURE__ */ new Set());
206
549
  const hasCancel = Boolean(multi || cancelable);
207
550
  const rows = hasCancel ? [...options, "Cancel"] : options;
208
551
  const cancelIndex = hasCancel ? options.length : -1;
@@ -239,46 +582,46 @@ function SelectPrompt({
239
582
  }
240
583
  }
241
584
  });
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 })
585
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 1, children: [
586
+ error && /* @__PURE__ */ jsx4(Text4, { color: COLORS.danger, children: error }),
587
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: m }, `msg-${i}`)),
588
+ table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
589
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
590
+ question && /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: question }),
591
+ helpText && /* @__PURE__ */ jsx4(Text4, { color: COLORS.dim, children: helpText })
249
592
  ] }),
250
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: rows.map((option, i) => {
593
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: rows.map((option, i) => {
251
594
  const highlighted = i === index;
252
595
  const isCancel = i === cancelIndex;
253
596
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
254
597
  const sec = isCancel ? void 0 : secondary?.[i];
255
598
  const labelColor = highlighted ? COLORS.highlight.fg : void 0;
256
- const label = /* @__PURE__ */ jsxs(Text2, { color: labelColor, children: [
599
+ const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, children: [
257
600
  highlighted ? "\u276F " : " ",
258
601
  bullet,
259
602
  option
260
603
  ] });
261
- return /* @__PURE__ */ jsxs(
262
- Box2,
604
+ return /* @__PURE__ */ jsxs3(
605
+ Box4,
263
606
  {
264
607
  width: rowWidth,
265
608
  paddingX: 1,
266
609
  paddingY: 1,
267
610
  backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
268
611
  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 })
612
+ sec?.kind === "text" ? /* @__PURE__ */ jsx4(Box4, { width: labelColWidth, flexShrink: 0, children: label }) : label,
613
+ sec?.kind === "text" && /* @__PURE__ */ jsx4(Text4, { color: highlighted ? COLORS.primary : COLORS.muted, children: sec.value }),
614
+ /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
615
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, children: sec.value })
273
616
  ]
274
617
  },
275
618
  `row-${i}`
276
619
  );
277
620
  }) }),
278
- /* @__PURE__ */ jsx2(Box2, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(Text2, { children: [
621
+ /* @__PURE__ */ jsx4(Box4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
279
622
  i > 0 ? " " : "",
280
- /* @__PURE__ */ jsx2(Text2, { color: COLORS.primary, children: key }),
281
- /* @__PURE__ */ jsxs(Text2, { color: COLORS.dim, children: [
623
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: key }),
624
+ /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
282
625
  " ",
283
626
  label
284
627
  ] })
@@ -287,17 +630,35 @@ function SelectPrompt({
287
630
  }
288
631
 
289
632
  // src/ui/PromptInput.tsx
290
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
633
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
291
634
  var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
635
+ function SpaceToContinuePrompt({
636
+ question,
637
+ messages,
638
+ onDecide
639
+ }) {
640
+ useInput2((input, key) => {
641
+ if (input === " ") onDecide(true);
642
+ else if (key.escape) onDecide(false);
643
+ });
644
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
645
+ messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
646
+ question && /* @__PURE__ */ jsx5(Text5, { color: COLORS.primary, children: question }),
647
+ /* @__PURE__ */ jsxs4(Box5, { gap: 1, flexDirection: "column", children: [
648
+ /* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "space" }),
649
+ /* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
650
+ ] })
651
+ ] });
652
+ }
292
653
  function PromptInput() {
293
654
  const { phase, inputReq, submitInput } = useWizard();
294
- const [draft, setDraft] = useState3("");
655
+ const [draft, setDraft] = useState4("");
295
656
  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" }) });
657
+ return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
297
658
  }
298
659
  if (phase !== "awaitingInput" || !inputReq) return null;
299
660
  if (inputReq.promptType === "multipleChoice") {
300
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
661
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
301
662
  SelectPrompt,
302
663
  {
303
664
  question: inputReq.prompt,
@@ -314,7 +675,7 @@ function PromptInput() {
314
675
  ) });
315
676
  }
316
677
  if (inputReq.promptType === "multiSelect") {
317
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
678
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
318
679
  SelectPrompt,
319
680
  {
320
681
  multi: true,
@@ -329,7 +690,7 @@ function PromptInput() {
329
690
  ) });
330
691
  }
331
692
  if (inputReq.promptType === "notice") {
332
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
693
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
333
694
  SelectPrompt,
334
695
  {
335
696
  question: inputReq.prompt,
@@ -339,9 +700,19 @@ function PromptInput() {
339
700
  }
340
701
  ) });
341
702
  }
703
+ if (inputReq.promptType === "spaceToContinue") {
704
+ return /* @__PURE__ */ jsx5(
705
+ SpaceToContinuePrompt,
706
+ {
707
+ question: inputReq.prompt,
708
+ messages: inputReq.messages,
709
+ onDecide: submitInput
710
+ }
711
+ );
712
+ }
342
713
  if (inputReq.promptType === "acceptReject") {
343
714
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
344
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
715
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
345
716
  SelectPrompt,
346
717
  {
347
718
  question: inputReq.prompt,
@@ -352,15 +723,15 @@ function PromptInput() {
352
723
  }
353
724
  ) });
354
725
  }
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: [
726
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
727
+ inputReq.error && /* @__PURE__ */ jsx5(Text5, { color: COLORS.danger, children: inputReq.error }),
728
+ inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: m }, `msg-${i}`)),
729
+ /* @__PURE__ */ jsxs4(Box5, { children: [
730
+ /* @__PURE__ */ jsxs4(Text5, { color: COLORS.primary, children: [
360
731
  inputReq.prompt,
361
732
  " "
362
733
  ] }),
363
- /* @__PURE__ */ jsx3(
734
+ /* @__PURE__ */ jsx5(
364
735
  TextInput,
365
736
  {
366
737
  value: draft,
@@ -376,32 +747,9 @@ function PromptInput() {
376
747
  }
377
748
 
378
749
  // src/ui/Welcome.tsx
379
- import { dirname, join } from "node:path";
750
+ import { dirname as dirname2, join as join3 } from "node:path";
380
751
  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
- }
752
+ import { Box as Box6, Spacer, Text as Text6, useInput as useInput3 } from "ink";
405
753
 
406
754
  // src/ui/copy/welcome.ts
407
755
  var sidebarItems = [
@@ -429,31 +777,33 @@ var sidebarItems = [
429
777
 
430
778
  // src/ui/Welcome.tsx
431
779
  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");
780
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
781
+ var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
434
782
  function SidebarItem({
435
783
  title,
436
784
  description
437
785
  }) {
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 })
786
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
787
+ /* @__PURE__ */ jsxs5(Box6, { gap: 1, children: [
788
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.success, children: "\u2192" }),
789
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.strong, bold: true, children: title })
442
790
  ] }),
443
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "row", gap: 2, children: [
444
- /* @__PURE__ */ jsx5(Spacer, {}),
445
- /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: description })
791
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", gap: 2, children: [
792
+ /* @__PURE__ */ jsx6(Spacer, {}),
793
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: description })
446
794
  ] })
447
795
  ] });
448
796
  }
449
797
  function Welcome() {
450
798
  const confirmStart = useWizard((s) => s.confirmStart);
451
- useInput2((input) => {
799
+ const openLearnMore = useWizard((s) => s.openLearnMore);
800
+ useInput3((input) => {
452
801
  if (input === " ") confirmStart();
802
+ else if (input === "i") openLearnMore();
453
803
  });
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(
804
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
805
+ /* @__PURE__ */ jsx6(Box6, { padding: 8, flexDirection: "column", justifyContent: "center", children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 4, children: [
806
+ /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
457
807
  Image,
458
808
  {
459
809
  src: IMAGE_PATH,
@@ -464,14 +814,14 @@ function Welcome() {
464
814
  protocol: "halfBlock"
465
815
  }
466
816
  ) }),
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" })
817
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
818
+ /* @__PURE__ */ jsxs5(Box6, { gap: 1, flexDirection: "column", children: [
819
+ /* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "space" }),
820
+ /* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
471
821
  ] })
472
822
  ] }) }),
473
- /* @__PURE__ */ jsxs4(
474
- Box5,
823
+ /* @__PURE__ */ jsxs5(
824
+ Box6,
475
825
  {
476
826
  backgroundColor: COLORS.bg.sidebar,
477
827
  width: 40,
@@ -480,41 +830,168 @@ function Welcome() {
480
830
  flexDirection: "column",
481
831
  justifyContent: "center",
482
832
  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))
833
+ /* @__PURE__ */ jsx6(Text6, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
834
+ sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
485
835
  ]
486
836
  }
487
837
  )
488
838
  ] });
489
839
  }
490
840
 
841
+ // src/ui/LearnMore.tsx
842
+ import { Fragment as Fragment2 } from "react";
843
+ import { Box as Box7, Text as Text7, useInput as useInput4, useWindowSize as useWindowSize3 } from "ink";
844
+
845
+ // src/ui/copy/learn-more.ts
846
+ var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
847
+ var accessItems = [
848
+ {
849
+ tag: "READ",
850
+ title: "Project files",
851
+ description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
852
+ },
853
+ {
854
+ tag: "WRITE",
855
+ title: "Code changes",
856
+ description: "creates & edits files (search UI, config). Shown as a diff first \u2014 nothing lands without your approval."
857
+ },
858
+ {
859
+ tag: "NET",
860
+ title: "Algolia API",
861
+ description: "sends index settings & the records you pick to your Algolia app over HTTPS."
862
+ },
863
+ {
864
+ tag: "KEY",
865
+ title: "Credentials",
866
+ description: "saves your Admin API key to .env and adds it to .gitignore."
867
+ }
868
+ ];
869
+ var neverItems = [
870
+ "Send your source code to a model or third party",
871
+ "Commit or push to git",
872
+ "Touch files outside your project directory"
873
+ ];
874
+ var policyLinks = [
875
+ { label: "Terms", url: "https://www.algolia.com/policies/terms" },
876
+ { label: "Privacy Policy", url: "https://www.algolia.com/policies/privacy" }
877
+ ];
878
+
879
+ // src/ui/LearnMore.tsx
880
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
881
+ var TAG_COLORS = {
882
+ READ: COLORS.success,
883
+ WRITE: COLORS.badge,
884
+ NET: COLORS.accent,
885
+ KEY: COLORS.muted
886
+ };
887
+ var TAG_COLUMN_WIDTH = 10;
888
+ var PADDING_X = 6;
889
+ var NEVER_BOX_PAD_X = 2;
890
+ function NeverLine({
891
+ width,
892
+ segments = []
893
+ }) {
894
+ const used = segments.reduce((n, s) => n + s.text.length, 0);
895
+ const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
896
+ return /* @__PURE__ */ jsxs6(Text7, { children: [
897
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" }),
898
+ " ".repeat(NEVER_BOX_PAD_X),
899
+ segments.map((s, i) => /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: s.bold, children: s.text }, i)),
900
+ " ".repeat(rightPad),
901
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: "\u2502" })
902
+ ] });
903
+ }
904
+ function LearnMore() {
905
+ const confirmStart = useWizard((s) => s.confirmStart);
906
+ const backToHome = useWizard((s) => s.backToHome);
907
+ const { columns } = useWindowSize3();
908
+ const dividerWidth = Math.max(0, columns - PADDING_X * 2);
909
+ useInput4((input, key) => {
910
+ if (key.escape) backToHome();
911
+ else if (input === " ") confirmStart();
912
+ });
913
+ return /* @__PURE__ */ jsxs6(
914
+ Box7,
915
+ {
916
+ flexDirection: "column",
917
+ paddingX: PADDING_X,
918
+ paddingY: 2,
919
+ width: "100%",
920
+ gap: 1,
921
+ children: [
922
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
923
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: accessIntro }),
924
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
925
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
926
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, marginTop: 1, children: [
927
+ /* @__PURE__ */ jsx7(Box7, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text7, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
928
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs6(Text7, { children: [
929
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: item.title }),
930
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
931
+ ] }) })
932
+ ] })
933
+ ] }, item.tag)) }),
934
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
935
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
936
+ /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
937
+ /* @__PURE__ */ jsx7(
938
+ NeverLine,
939
+ {
940
+ width: dividerWidth,
941
+ segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
942
+ }
943
+ ),
944
+ neverItems.map((item) => /* @__PURE__ */ jsxs6(Fragment2, { children: [
945
+ /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
946
+ /* @__PURE__ */ jsx7(
947
+ NeverLine,
948
+ {
949
+ width: dividerWidth,
950
+ segments: [
951
+ { text: "\u2715", color: COLORS.danger },
952
+ { text: " " },
953
+ { text: item, color: COLORS.primary }
954
+ ]
955
+ }
956
+ )
957
+ ] }, item)),
958
+ /* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
959
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
960
+ ] }),
961
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
962
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
963
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.accent, children: link.url })
964
+ ] }, link.label)) }),
965
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "row", gap: 3, children: [
966
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
967
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
968
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "esc" }),
969
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "] back" })
970
+ ] }),
971
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", gap: 1, children: [
972
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "[" }),
973
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.primary, children: "space" }),
974
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.muted, children: "]" }),
975
+ /* @__PURE__ */ jsx7(Text7, { color: COLORS.success, bold: true, children: "start wizard" })
976
+ ] })
977
+ ] })
978
+ ]
979
+ }
980
+ );
981
+ }
982
+
491
983
  // src/ui/Sidebar.tsx
492
- import { Box as Box8, Text as Text8 } from "ink";
984
+ import { Box as Box10, Text as Text10 } from "ink";
493
985
 
494
986
  // src/ui/Steps.tsx
495
- import { Box as Box6, Text as Text6 } from "ink";
987
+ import { Box as Box8, Text as Text8 } from "ink";
496
988
  import Spinner from "ink-spinner";
497
989
 
498
990
  // src/core/persistence.ts
499
991
  import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
500
- import { join as join3 } from "node:path";
501
-
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";
510
- }
511
- function stateDir(cwd = process.cwd()) {
512
- return join2(rootDir(), projectSlug(cwd));
513
- }
514
-
515
- // src/core/persistence.ts
992
+ import { join as join4 } from "node:path";
516
993
  var isStepVisible = (s) => s.visible !== false;
517
- var stateFile = (workflowId) => join3(stateDir(), `state-${workflowId}.json`);
994
+ var stateFile = (workflowId) => join4(stateDir(), `state-${workflowId}.json`);
518
995
  async function loadWorkflowState(workflowId) {
519
996
  try {
520
997
  const raw = await readFile(stateFile(workflowId), "utf8");
@@ -536,154 +1013,55 @@ async function clearWorkflowState(workflowId) {
536
1013
  }
537
1014
 
538
1015
  // 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
- }
1016
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
631
1017
  function Steps() {
632
1018
  const { steps } = useWizard();
633
1019
  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
- }) });
1020
+ 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: [
1021
+ s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
1022
+ " ",
1023
+ s.title
1024
+ ] }) }, s.id)) });
647
1025
  }
648
1026
  function CurrentStep() {
649
1027
  const { steps } = useWizard();
650
1028
  const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
651
1029
  if (!currentStep) return null;
652
- return /* @__PURE__ */ jsxs5(Text6, { color: COLORS.status.running, children: [
653
- /* @__PURE__ */ jsx6(Spinner, { type: "dots" }),
1030
+ return /* @__PURE__ */ jsxs7(Text8, { color: COLORS.status.running, children: [
1031
+ /* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
654
1032
  " ",
655
1033
  ` ${currentStep.title}`
656
1034
  ] });
657
1035
  }
658
1036
 
659
1037
  // 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";
1038
+ import { Box as Box9, Text as Text9 } from "ink";
1039
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
662
1040
  function Progress() {
663
1041
  const { steps, currentStepIndex } = useWizard();
664
1042
  const visibleSteps = steps.filter(isStepVisible);
665
1043
  if (visibleSteps.length === 0) return null;
666
1044
  const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
667
1045
  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 })
1046
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
1047
+ /* @__PURE__ */ jsx9(Text9, { color: COLORS.muted, children: "STEP" }),
1048
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: activeStepNumber }),
1049
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "/" }),
1050
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: visibleSteps.length })
673
1051
  ] });
674
1052
  }
675
1053
 
676
1054
  // src/ui/copy/sidebar-commands.ts
677
1055
  var sidebarCommands = [
678
1056
  { keyHint: "tab", description: "toggle logs" },
679
- { keyHint: "esc", description: "close" }
1057
+ { keyHint: "esc", description: "exit wizard" }
680
1058
  ];
681
1059
 
682
1060
  // src/ui/Sidebar.tsx
683
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1061
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
684
1062
  function Sidebar() {
685
- return /* @__PURE__ */ jsxs7(
686
- Box8,
1063
+ return /* @__PURE__ */ jsxs9(
1064
+ Box10,
687
1065
  {
688
1066
  backgroundColor: "#14171E",
689
1067
  width: 30,
@@ -692,16 +1070,16 @@ function Sidebar() {
692
1070
  flexDirection: "column",
693
1071
  justifyContent: "space-between",
694
1072
  children: [
695
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
696
- /* @__PURE__ */ jsx8(Text8, { color: COLORS.muted, children: "PROGRESS" }),
697
- /* @__PURE__ */ jsx8(Steps, {})
1073
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1074
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "PROGRESS" }),
1075
+ /* @__PURE__ */ jsx10(Steps, {})
698
1076
  ] }),
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 })
1077
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 1, children: [
1078
+ /* @__PURE__ */ jsx10(Progress, {}),
1079
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: sidebarCommands.map((c) => {
1080
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 1, children: [
1081
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1082
+ /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: c.description })
705
1083
  ] });
706
1084
  }) })
707
1085
  ] })
@@ -711,12 +1089,12 @@ function Sidebar() {
711
1089
  }
712
1090
 
713
1091
  // 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";
1092
+ import { Box as Box11, Text as Text11 } from "ink";
1093
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
716
1094
  function Ribbon() {
717
1095
  const firstCommand = sidebarCommands[0];
718
- return /* @__PURE__ */ jsxs8(
719
- Box9,
1096
+ return /* @__PURE__ */ jsxs10(
1097
+ Box11,
720
1098
  {
721
1099
  backgroundColor: "#14171E",
722
1100
  flexDirection: "row",
@@ -724,11 +1102,11 @@ function Ribbon() {
724
1102
  paddingX: 2,
725
1103
  paddingY: 1,
726
1104
  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 })
1105
+ /* @__PURE__ */ jsx11(Progress, {}),
1106
+ /* @__PURE__ */ jsx11(CurrentStep, {}),
1107
+ /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1108
+ /* @__PURE__ */ jsx11(Text11, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1109
+ /* @__PURE__ */ jsx11(Text11, { color: COLORS.muted, children: firstCommand.description })
732
1110
  ] })
733
1111
  ]
734
1112
  }
@@ -736,75 +1114,198 @@ function Ribbon() {
736
1114
  }
737
1115
 
738
1116
  // src/ui/App.tsx
739
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1117
+ import { useState as useState6 } from "react";
1118
+
1119
+ // src/ui/Logs.tsx
1120
+ import { useLayoutEffect, useRef as useRef2, useState as useState5 } from "react";
1121
+ import { Box as Box12, Text as Text12, measureElement as measureElement2, useInput as useInput5, useWindowSize as useWindowSize4 } from "ink";
1122
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1123
+ var KIND_COLOR = {
1124
+ tool: COLORS.primary,
1125
+ prompt: COLORS.badge
1126
+ };
1127
+ var ROW_GAP = 1;
1128
+ function truncate2(str, maxWidth) {
1129
+ if (maxWidth <= 0) return "";
1130
+ return str.length > maxWidth ? `${str.slice(0, maxWidth - 1)}\u2026` : str;
1131
+ }
1132
+ function rawInputText(input) {
1133
+ if (input === void 0) return "";
1134
+ const str = typeof input === "string" ? input : JSON.stringify(input);
1135
+ if (!str || str === "{}") return "";
1136
+ return str.replace(/\s+/g, " ").trim();
1137
+ }
1138
+ function formatTimestamp(ms) {
1139
+ const d = new Date(ms);
1140
+ const pad = (n) => String(n).padStart(2, "0");
1141
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1142
+ }
1143
+ function Logs() {
1144
+ const logs = useWizard((s) => s.logs);
1145
+ const { rows, columns } = useWindowSize4();
1146
+ const viewportRef = useRef2(null);
1147
+ const [viewportHeight, setViewportHeight] = useState5(0);
1148
+ const [viewportWidth, setViewportWidth] = useState5(0);
1149
+ const [scrollOffset, setScrollOffset] = useState5(0);
1150
+ const prevMaxOffsetRef = useRef2(0);
1151
+ useLayoutEffect(() => {
1152
+ if (!viewportRef.current) return;
1153
+ const { width, height } = measureElement2(viewportRef.current);
1154
+ setViewportHeight(height);
1155
+ setViewportWidth(width);
1156
+ }, [rows, columns, logs.length === 0]);
1157
+ let capacity = viewportHeight;
1158
+ for (let i = 0; i < 2; i++) {
1159
+ const hasAbove = scrollOffset > 0;
1160
+ const hasBelow = scrollOffset + capacity < logs.length;
1161
+ capacity = Math.max(
1162
+ viewportHeight - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0),
1163
+ 0
1164
+ );
1165
+ }
1166
+ const capacityAtBottom = logs.length > viewportHeight ? Math.max(viewportHeight - 1, 0) : viewportHeight;
1167
+ const maxOffset = Math.max(logs.length - capacityAtBottom, 0);
1168
+ useLayoutEffect(() => {
1169
+ const wasAtBottom = scrollOffset >= prevMaxOffsetRef.current;
1170
+ prevMaxOffsetRef.current = maxOffset;
1171
+ setScrollOffset((o) => wasAtBottom ? maxOffset : Math.min(o, maxOffset));
1172
+ }, [maxOffset]);
1173
+ useInput5((_input, key) => {
1174
+ if (!key.upArrow && !key.downArrow) return;
1175
+ setScrollOffset(
1176
+ (o) => key.upArrow ? Math.max(o - 1, 0) : Math.min(o + 1, maxOffset)
1177
+ );
1178
+ });
1179
+ const visible = logs.slice(scrollOffset, scrollOffset + capacity);
1180
+ const hiddenAbove = scrollOffset;
1181
+ const hiddenBelow = logs.length - scrollOffset - visible.length;
1182
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1183
+ logs.length === 0 && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "No logs yet." }),
1184
+ /* @__PURE__ */ jsxs11(Box12, { ref: viewportRef, flexDirection: "column", flexGrow: 1, children: [
1185
+ hiddenAbove > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
1186
+ "\u2191 ",
1187
+ hiddenAbove,
1188
+ " more"
1189
+ ] }),
1190
+ visible.map((entry) => {
1191
+ const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1192
+ const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1193
+ const rawPreview = rawInputText(entry.input);
1194
+ const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1195
+ const gaps = (partCount - 1) * ROW_GAP;
1196
+ let budget = viewportWidth - timestamp.length - durationText.length - gaps;
1197
+ const name = truncate2(entry.name, budget);
1198
+ budget -= name.length;
1199
+ const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1200
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: ROW_GAP, children: [
1201
+ /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: timestamp }),
1202
+ /* @__PURE__ */ jsx12(Text12, { color: KIND_COLOR[entry.kind], wrap: "truncate", children: name }),
1203
+ preview && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, wrap: "truncate", children: preview }),
1204
+ durationText && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: durationText })
1205
+ ] }, entry.id);
1206
+ }),
1207
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
1208
+ "\u2193 ",
1209
+ hiddenBelow,
1210
+ " more"
1211
+ ] })
1212
+ ] }),
1213
+ /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1214
+ ] });
1215
+ }
1216
+
1217
+ // src/lib/events.ts
1218
+ import "zod";
1219
+ function track(event, payload) {
1220
+ const token = getAuthToken();
1221
+ if (!token) return;
1222
+ const userId = useWizard.getState().user?.userId;
1223
+ if (!userId) return;
1224
+ void proxyFetch(`${PROXY_BASE_URL}/events`, {
1225
+ method: "POST",
1226
+ headers: {
1227
+ "content-type": "application/json",
1228
+ authorization: `Bearer ${token}`
1229
+ },
1230
+ body: JSON.stringify({ userId, event, properties: payload })
1231
+ }).catch((err) => {
1232
+ logger.warn({ err, event }, "failed to send analytics event");
1233
+ });
1234
+ }
1235
+
1236
+ // src/ui/App.tsx
1237
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
740
1238
  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;
1239
+ const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
747
1240
  const { exit } = useApp();
748
- const { columns, rows } = useWindowSize2();
749
- const [copied, setCopied] = useState4(null);
1241
+ const { columns, rows } = useWindowSize5();
1242
+ const [showLogs, setShowLogs] = useState6(false);
750
1243
  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) => {
758
- if (key.return || key.escape) {
1244
+ const currentStep = steps[currentStepIndex];
1245
+ useInput6(
1246
+ (_input, key) => {
1247
+ if (key.return) {
759
1248
  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
1249
  }
770
1250
  },
771
1251
  { isActive: finished }
772
1252
  );
1253
+ useInput6((_input, key) => {
1254
+ if (phase === "idle" || phase === "preflight") return;
1255
+ if (key.tab) {
1256
+ setShowLogs(!showLogs);
1257
+ track("AI Wizard Interaction", {
1258
+ context: "global",
1259
+ key: "tab",
1260
+ currentStep: currentStep?.id
1261
+ });
1262
+ }
1263
+ });
1264
+ const escOwnedElsewhere = phase === "idle" || phase === "awaitingInput" && inputReq?.promptType === "spaceToContinue";
1265
+ useInput6((_input, key) => {
1266
+ if (escOwnedElsewhere) return;
1267
+ if (key.escape) {
1268
+ track("AI Wizard Interaction", {
1269
+ context: "global",
1270
+ key: "esc",
1271
+ // No step is active until `startWorkflow` — report the phase instead.
1272
+ currentStep: currentStep?.id ?? phase
1273
+ });
1274
+ exit();
1275
+ }
1276
+ });
773
1277
  const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
774
1278
  const flexDirection = columns > 90 ? "row" : "column";
775
1279
  const showSidebar = flexDirection === "row";
776
- return /* @__PURE__ */ jsxs9(
777
- Box10,
1280
+ return /* @__PURE__ */ jsxs12(
1281
+ Box13,
778
1282
  {
779
1283
  backgroundColor: COLORS.bg.main,
780
1284
  flexDirection: "row",
781
1285
  width: columns,
782
1286
  minHeight: rows,
783
1287
  children: [
784
- mainWindowVisible && /* @__PURE__ */ jsxs9(
785
- Box10,
1288
+ mainWindowVisible && /* @__PURE__ */ jsxs12(
1289
+ Box13,
786
1290
  {
787
1291
  flexDirection,
788
1292
  width: "100%",
789
1293
  justifyContent: "space-between",
790
1294
  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: [
1295
+ showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 4, paddingY: 2, width: 70, children: [
1296
+ /* @__PURE__ */ jsx13(Notices, {}),
1297
+ /* @__PURE__ */ jsx13(PromptInput, {}),
1298
+ phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
1299
+ phase === "error" && error && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text13, { color: COLORS.status.error, children: [
794
1300
  "\u2716 ",
795
1301
  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
- ] })
1302
+ ] }) })
801
1303
  ] }),
802
- /* @__PURE__ */ jsx10(Text10, { color: "white", backgroundColor: "#14171E" }),
803
- showSidebar ? /* @__PURE__ */ jsx10(Sidebar, {}) : /* @__PURE__ */ jsx10(Ribbon, {})
1304
+ showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
804
1305
  ]
805
1306
  }
806
1307
  ),
807
- (phase === "idle" || phase === "preflight") && /* @__PURE__ */ jsx10(Welcome, {})
1308
+ (phase === "idle" || phase === "preflight") && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
808
1309
  ]
809
1310
  }
810
1311
  );
@@ -815,8 +1316,8 @@ import "zod";
815
1316
 
816
1317
  // src/core/config.ts
817
1318
  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");
1319
+ import { join as join5 } from "node:path";
1320
+ var configFile = () => join5(stateDir(), "config.json");
820
1321
  var DEFAULT_CONFIG = {
821
1322
  version: 1,
822
1323
  aiConsent: false,
@@ -840,128 +1341,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
840
1341
  await saveConfig(config);
841
1342
  }
842
1343
 
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
1344
  // src/lib/telemetry.ts
966
1345
  function isTelemetryEnabled() {
967
1346
  return Boolean(getAuthToken()) && !process.env.VITEST && process.env.WIZARD_TELEMETRY !== "false";
@@ -1124,25 +1503,6 @@ function trackWorkflowError(ctx) {
1124
1503
  );
1125
1504
  }
1126
1505
 
1127
- // src/lib/events.ts
1128
- import "zod";
1129
- function track(event, payload) {
1130
- const token = getAuthToken();
1131
- if (!token) return;
1132
- const userId = useWizard.getState().user?.userId;
1133
- if (!userId) return;
1134
- void proxyFetch(`${PROXY_BASE_URL}/events`, {
1135
- method: "POST",
1136
- headers: {
1137
- "content-type": "application/json",
1138
- authorization: `Bearer ${token}`
1139
- },
1140
- body: JSON.stringify({ userId, event, properties: payload })
1141
- }).catch((err) => {
1142
- logger.warn({ err, event }, "failed to send analytics event");
1143
- });
1144
- }
1145
-
1146
1506
  // src/core/orchestrator.ts
1147
1507
  function defineStep(step) {
1148
1508
  return { visible: true, ...step };
@@ -1186,12 +1546,14 @@ async function ensureConsent() {
1186
1546
  if (config.aiConsent) return;
1187
1547
  const store2 = useWizard.getState();
1188
1548
  const answer = await store2.requestUserInput({
1189
- prompt: 'Wizard will make AI-authored changes to this repository. Type "yes" to consent:',
1190
- promptType: "textInput",
1549
+ prompt: "Wizard will make AI-authored changes to this repository.",
1550
+ promptType: "spaceToContinue",
1191
1551
  options: []
1192
1552
  });
1193
- if (typeof answer !== "string" || answer.trim().toLowerCase() !== "yes") {
1194
- throw new Error("AI consent declined \u2014 cannot proceed.");
1553
+ if (answer !== true) {
1554
+ throw new Error(
1555
+ "AI consent declined \u2014 cannot proceed. If you change your mind, just run the Wizard again!"
1556
+ );
1195
1557
  }
1196
1558
  config.aiConsent = true;
1197
1559
  await saveConfig(config);
@@ -1212,6 +1574,10 @@ async function makeContext(state) {
1212
1574
  completedSteps,
1213
1575
  getStepOutput: (stepId) => outputs[stepId],
1214
1576
  requestUserInput: (prompt) => useWizard.getState().requestUserInput(prompt),
1577
+ notify: (notice) => useWizard.getState().pushNotice(notice),
1578
+ clearNotices: () => useWizard.getState().clearNotices(),
1579
+ logStart: (name, input) => useWizard.getState().logStart("tool", name, input),
1580
+ logEnd: (id, result) => useWizard.getState().logEnd(id, result),
1215
1581
  updateAlgoliaState: (key, value) => {
1216
1582
  state.algoliaState[key] = value;
1217
1583
  },
@@ -1243,6 +1609,7 @@ async function runStep(state, index, step, appId) {
1243
1609
  store2.setActiveStep(index);
1244
1610
  store2.syncSteps([...state.steps], index);
1245
1611
  await saveWorkflowState(state);
1612
+ await markInteraction();
1246
1613
  const ctx = await makeContext(state);
1247
1614
  const raw = await step.run(ctx);
1248
1615
  const output = step.outputSchema.parse(raw);
@@ -1263,7 +1630,6 @@ async function runStep(state, index, step, appId) {
1263
1630
  async function runWorkflow(workflow2, appId) {
1264
1631
  const store2 = useWizard.getState();
1265
1632
  try {
1266
- await ensureConsent();
1267
1633
  const persisted = await loadWorkflowState(workflow2.id);
1268
1634
  const state = (persisted && reconcileWorkflowState(persisted, workflow2)) ?? initWorkflowState(workflow2, nowIso());
1269
1635
  ensureExecutedStepCount(state);
@@ -1275,6 +1641,7 @@ async function runWorkflow(workflow2, appId) {
1275
1641
  },
1276
1642
  [...state.steps]
1277
1643
  );
1644
+ await ensureConsent();
1278
1645
  trackWorkflowStart({ workflowId: workflow2.id, appId });
1279
1646
  for (let i = state.currentStepIndex; i < workflow2.steps.length; i++) {
1280
1647
  await runStep(state, i, workflow2.steps[i], appId);
@@ -1384,7 +1751,7 @@ async function loadActiveProfile() {
1384
1751
  }
1385
1752
 
1386
1753
  // src/workflows/default.ts
1387
- import { z as z24 } from "zod";
1754
+ import { z as z25 } from "zod";
1388
1755
 
1389
1756
  // src/actions/listIndices.ts
1390
1757
  import { z as z3 } from "zod";
@@ -1863,8 +2230,11 @@ function verifyImplementationTool() {
1863
2230
  // src/lib/tools/generateRecord.ts
1864
2231
  import { tool as tool9, generateText, Output, NoObjectGeneratedError } from "ai";
1865
2232
  import { createAnthropic } from "@ai-sdk/anthropic";
1866
- import { nanoid } from "nanoid";
2233
+ import { nanoid as nanoid2 } from "nanoid";
2234
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2235
+ import { dirname as dirname6 } from "node:path";
1867
2236
  import z12 from "zod";
2237
+ var DATA_DIR = ".algolia-wizard/data";
1868
2238
  var RECORD_MODEL = "claude-haiku-4-5";
1869
2239
  var MAX_RECORDS = 100;
1870
2240
  var BATCH_SIZE = 10;
@@ -1872,9 +2242,9 @@ var MAX_BATCH_ATTEMPTS = 3;
1872
2242
  var anthropic = createAnthropic({
1873
2243
  apiKey: process.env.PROVIDER_API_KEY ?? ""
1874
2244
  });
1875
- function generateRecordTool() {
2245
+ function generateRecordTool(ctx) {
1876
2246
  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.",
2247
+ 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
2248
  inputSchema: z12.object({
1879
2249
  entityName: z12.string().describe("Name of the entity to generate records for."),
1880
2250
  attributes: z12.array(z12.string()).describe("Attribute names each record must contain."),
@@ -1906,7 +2276,7 @@ function generateRecordTool() {
1906
2276
  // would otherwise drift to the same high-probability values and
1907
2277
  // collide across batches. This per-batch seed pushes each call
1908
2278
  // into a different region of the output space.
1909
- `Variety seed: ${nanoid()}. Use it to diversify values.`
2279
+ `Variety seed: ${nanoid2()}. Use it to diversify values.`
1910
2280
  ].filter(Boolean).join("\n")
1911
2281
  });
1912
2282
  return output.records;
@@ -1928,10 +2298,23 @@ function generateRecordTool() {
1928
2298
  const batches = await Promise.all(batchSizes.map(generateBatch));
1929
2299
  const records = batches.flat().map((record) => ({
1930
2300
  ...record,
1931
- objectID: nanoid()
2301
+ objectID: nanoid2()
1932
2302
  }));
1933
- logger.info(records);
1934
- return { records };
2303
+ const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2304
+ const relPath = `${DATA_DIR}/${slug}.json`;
2305
+ const resolved = resolveInRoot(ctx, relPath);
2306
+ if (resolved.ok === false) return resolved.error;
2307
+ if (await hasSymlinkParent(ctx, resolved.target)) {
2308
+ return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2309
+ }
2310
+ await mkdir5(dirname6(resolved.target), { recursive: true });
2311
+ await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2312
+ logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
2313
+ return {
2314
+ filePath: relPath,
2315
+ count: records.length,
2316
+ 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.`
2317
+ };
1935
2318
  } catch (err) {
1936
2319
  return `Error generating records: ${err.message}`;
1937
2320
  }
@@ -1939,6 +2322,25 @@ function generateRecordTool() {
1939
2322
  });
1940
2323
  }
1941
2324
 
2325
+ // src/lib/tools/notifyUser.ts
2326
+ import { tool as tool10 } from "ai";
2327
+ import z13 from "zod";
2328
+ function notifyUserTool() {
2329
+ return tool10({
2330
+ 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.`,
2331
+ inputSchema: z13.object({
2332
+ message: z13.string().describe(
2333
+ "Short, plain-language description of what you are doing now."
2334
+ )
2335
+ }),
2336
+ execute: async ({ message }) => {
2337
+ logger.info({ message }, "called notifyUser tool");
2338
+ useWizard.getState().pushNotice({ messages: [message] });
2339
+ return "ok";
2340
+ }
2341
+ });
2342
+ }
2343
+
1942
2344
  // src/lib/tools/context.ts
1943
2345
  var DEFAULT_TOOL_LIMITS = {
1944
2346
  list: 10,
@@ -1956,20 +2358,48 @@ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
1956
2358
  }
1957
2359
 
1958
2360
  // src/lib/tools/index.ts
2361
+ function withLogging(name, def) {
2362
+ const execute = def.execute;
2363
+ if (!execute) return def;
2364
+ return {
2365
+ ...def,
2366
+ execute: async (input, options) => {
2367
+ const id = useWizard.getState().logStart("tool", name, input);
2368
+ try {
2369
+ const output = await execute(input, options);
2370
+ useWizard.getState().logEnd(id, { status: "success", output });
2371
+ return output;
2372
+ } catch (err) {
2373
+ useWizard.getState().logEnd(id, {
2374
+ status: "error",
2375
+ output: err instanceof Error ? err.message : String(err)
2376
+ });
2377
+ throw err;
2378
+ }
2379
+ }
2380
+ };
2381
+ }
1959
2382
  function createTools(ctx, { output, tools }) {
1960
2383
  const all = {
1961
- listFiles: listFilesTool(ctx),
1962
- changeDirectory: changeDirectoryTool(ctx),
1963
- reportStatus: reportStatusTool(output),
1964
- readFile: readFileTool(ctx),
1965
- writeFile: writeFileTool(ctx),
1966
- writeCredentials: writeCredentialsTool(ctx),
1967
- searchFiles: searchFilesTool(ctx),
1968
- verifyImplementation: verifyImplementationTool(),
1969
- generateRecord: generateRecordTool()
2384
+ listFiles: withLogging("listFiles", listFilesTool(ctx)),
2385
+ changeDirectory: withLogging("changeDirectory", changeDirectoryTool(ctx)),
2386
+ reportStatus: withLogging("reportStatus", reportStatusTool(output)),
2387
+ readFile: withLogging("readFile", readFileTool(ctx)),
2388
+ writeFile: withLogging("writeFile", writeFileTool(ctx)),
2389
+ writeCredentials: withLogging(
2390
+ "writeCredentials",
2391
+ writeCredentialsTool(ctx)
2392
+ ),
2393
+ searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2394
+ verifyImplementation: withLogging(
2395
+ "verifyImplementation",
2396
+ verifyImplementationTool()
2397
+ ),
2398
+ generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2399
+ notifyUser: withLogging("notifyUser", notifyUserTool())
1970
2400
  };
1971
2401
  if (!tools) return all;
1972
- const selection = /* @__PURE__ */ new Set([...tools, "reportStatus"]);
2402
+ const selection = /* @__PURE__ */ new Set([...tools, "reportStatus", "notifyUser"]);
1973
2403
  return Object.fromEntries(
1974
2404
  Object.entries(all).filter(([name]) => selection.has(name))
1975
2405
  );
@@ -2002,16 +2432,19 @@ async function runAgent(req) {
2002
2432
  const toolContext = createToolContext();
2003
2433
  const readTools = ["readFile", "searchFiles", "listFiles"];
2004
2434
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2005
- const instructions = hasReadTools ? [
2435
+ const instructions = [
2006
2436
  ...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;
2437
+ ...hasReadTools ? [
2438
+ "When you need to read or search multiple files, issue those tool calls together in one step rather than one at a time."
2439
+ ] : [],
2440
+ "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."
2441
+ ];
2009
2442
  const agent = new ToolLoopAgent({
2010
2443
  model: anthropic2(MODEL_BY_SIZE[req.modelSize ?? "medium"]),
2011
2444
  // Cache tools + system on the last system block. Tools render before
2012
2445
  // system, so one breakpoint here caches both, reused on every loop turn
2013
2446
  // after the first.
2014
- instructions: instructions?.map((i, idx, arr) => {
2447
+ instructions: instructions.map((i, idx, arr) => {
2015
2448
  return {
2016
2449
  role: "system",
2017
2450
  content: i,
@@ -2079,10 +2512,10 @@ async function runAgent(req) {
2079
2512
  }
2080
2513
 
2081
2514
  // 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() }))
2515
+ import z16 from "zod";
2516
+ var detectLanguageSchema = z16.object({
2517
+ languages: z16.array(z16.object({ name: z16.string(), version: z16.string() })),
2518
+ frameworks: z16.array(z16.object({ name: z16.string(), version: z16.string() }))
2086
2519
  });
2087
2520
  var detectLanguage = () => runAgent({
2088
2521
  instructions: [
@@ -2100,31 +2533,31 @@ var detectLanguage = () => runAgent({
2100
2533
  });
2101
2534
 
2102
2535
  // src/actions/analyzeCodebase.ts
2103
- import z16 from "zod";
2536
+ import z17 from "zod";
2104
2537
  var READONLY_TOOLS = [
2105
2538
  "listFiles",
2106
2539
  "changeDirectory",
2107
2540
  "readFile",
2108
2541
  "searchFiles"
2109
2542
  ];
2110
- var ingestionAnalysisSchema = z16.object({
2111
- ingestionAnalysis: z16.array(
2112
- z16.object({
2113
- name: z16.string(),
2114
- paths: z16.array(z16.string()),
2543
+ var ingestionAnalysisSchema = z17.object({
2544
+ ingestionAnalysis: z17.array(
2545
+ z17.object({
2546
+ name: z17.string(),
2547
+ paths: z17.array(z17.string()),
2115
2548
  // indexable fields the agent found for this entity
2116
- attributes: z16.array(z16.string())
2549
+ attributes: z17.array(z17.string())
2117
2550
  })
2118
2551
  )
2119
2552
  });
2120
- var searchImplementationAnalysisSchema = z16.object({
2121
- searchImplementationAnalysis: z16.string()
2553
+ var searchImplementationAnalysisSchema = z17.object({
2554
+ searchImplementationAnalysis: z17.string()
2122
2555
  });
2123
- var verificationSchema = z16.object({
2124
- verification: z16.array(z16.string())
2556
+ var verificationSchema = z17.object({
2557
+ verification: z17.array(z17.string())
2125
2558
  });
2126
2559
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
2127
- var analyzeCodebaseSchema = z16.object({
2560
+ var analyzeCodebaseSchema = z17.object({
2128
2561
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2129
2562
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
2130
2563
  verification: verificationSchema.shape.verification.optional(),
@@ -2186,7 +2619,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2186
2619
  // package.json
2187
2620
  var package_default = {
2188
2621
  name: "@algolia/wizard",
2189
- version: "0.2.0",
2622
+ version: "0.3.0-rc.43.5",
2190
2623
  description: "Magically implement Algolia functionality in your codebase",
2191
2624
  type: "module",
2192
2625
  engines: {
@@ -2200,9 +2633,9 @@ var package_default = {
2200
2633
  "docs"
2201
2634
  ],
2202
2635
  scripts: {
2203
- "build:proxy": `esbuild src/proxy/index.ts --bundle --platform=node --format=esm --target=node24 --banner:js='import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);' --outfile=dist/proxy.mjs`,
2204
- build: "esbuild src/main.tsx --bundle --platform=node --format=esm --target=node18 --packages=external --jsx=automatic --banner:js='#!/usr/bin/env node' --outfile=dist/main.js && cp src/ui/algolia.png dist/algolia.png",
2205
- "dev:proxy": "NODE_OPTIONS=--use-system-ca tsx watch src/proxy/index.ts",
2636
+ "build:proxy": "node scripts/build.mjs proxy",
2637
+ build: "node scripts/build.mjs",
2638
+ "dev:proxy": "touch .env && NODE_OPTIONS=--use-system-ca tsx watch --env-file=.env src/proxy/index.ts",
2206
2639
  dev: "touch .env && tsx --env-file=.env ./src/main.tsx",
2207
2640
  "env:load": "pnpm exec -- varlock load",
2208
2641
  prepare: "husky",
@@ -2307,8 +2740,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
2307
2740
  }
2308
2741
 
2309
2742
  // src/actions/confirmLanguage.ts
2310
- import z18 from "zod";
2311
- var confirmLanguageSchema = z18.object({
2743
+ import z19 from "zod";
2744
+ var confirmLanguageSchema = z19.object({
2312
2745
  languages: detectLanguageSchema.shape.languages
2313
2746
  });
2314
2747
  async function confirmLanguage(ctx) {
@@ -2329,8 +2762,8 @@ async function confirmLanguage(ctx) {
2329
2762
  }
2330
2763
 
2331
2764
  // src/actions/confirmFramework.ts
2332
- import z19 from "zod";
2333
- var confirmFrameworkSchema = z19.object({
2765
+ import z20 from "zod";
2766
+ var confirmFrameworkSchema = z20.object({
2334
2767
  frameworks: detectLanguageSchema.shape.frameworks
2335
2768
  });
2336
2769
  var CURATED_FRAMEWORKS = [
@@ -2458,8 +2891,8 @@ async function promptUser(ctx, params) {
2458
2891
  }
2459
2892
 
2460
2893
  // src/actions/confirmEntities.ts
2461
- import z20 from "zod";
2462
- var confirmEntitiesSchema = z20.object({
2894
+ import z21 from "zod";
2895
+ var confirmEntitiesSchema = z21.object({
2463
2896
  // Final detection — the focused re-run may supersede project-scan's.
2464
2897
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2465
2898
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -2529,17 +2962,15 @@ async function confirmEntities(ctx) {
2529
2962
  }
2530
2963
 
2531
2964
  // 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())
2965
+ import { z as z22 } from "zod";
2966
+ var reviewSchema = z22.object({
2967
+ // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
2968
+ // not one entry per workflow step — a step's raw output can be a long,
2969
+ // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
2970
+ // that 1:1 is what made the old per-step summary an unreadable wall of text.
2971
+ summaryPoints: z22.array(z22.string()),
2972
+ reviewPrompt: z22.string(),
2973
+ nextSteps: z22.array(z22.string())
2543
2974
  });
2544
2975
  function formatCompletedSteps(steps) {
2545
2976
  if (!steps.length) return "(no prior steps completed)";
@@ -2549,32 +2980,55 @@ Output:
2549
2980
  ${JSON.stringify(s.output, null, 2)}`
2550
2981
  ).join("\n\n");
2551
2982
  }
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:
2983
+ function formatReviewSummary(result) {
2984
+ const nextStepLines = result.nextSteps.map((step) => {
2985
+ const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
2986
+ const isWorktreeCommand = step.includes("/worktrees/");
2987
+ return {
2988
+ text: `\u2192 ${step}`,
2989
+ color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
2990
+ bold: isIngestCommand || isWorktreeCommand
2991
+ };
2992
+ });
2993
+ return [
2994
+ // Plain lines, same as nextSteps' un-highlighted entries — the summary is
2995
+ // an overview, not a call to action, so it gets no arrow/color/bold.
2996
+ ...result.summaryPoints,
2997
+ { text: result.reviewPrompt, color: COLORS.brand },
2998
+ ...nextStepLines
2999
+ ];
3000
+ }
3001
+ var reviewStep = async (ctx, options) => {
3002
+ const result = await runAgent({
3003
+ instructions: [
3004
+ "Summarize what was accomplished in the workflow, leaving out verbose details.",
3005
+ "Base your summary only on the step outputs provided \u2014 do not read the repository.",
3006
+ "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.",
3007
+ "Each summaryPoint should be a short, standalone statement.",
3008
+ '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".',
3009
+ "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.",
3010
+ options.nextStepsGuidance,
3011
+ `Completed steps:
2561
3012
  ${formatCompletedSteps(ctx.completedSteps)}`,
2562
- "When done, call reportStatus"
2563
- ],
2564
- tools: [],
2565
- outputSchema: reviewSchema,
2566
- modelSize: "small"
2567
- });
3013
+ "When done, call reportStatus"
3014
+ ],
3015
+ tools: [],
3016
+ outputSchema: reviewSchema,
3017
+ modelSize: "small"
3018
+ });
3019
+ ctx.notify({ messages: formatReviewSummary(result) });
3020
+ return result;
3021
+ };
2568
3022
 
2569
3023
  // src/actions/implement.ts
2570
- import z23 from "zod";
3024
+ import z24 from "zod";
2571
3025
 
2572
3026
  // src/lib/worktree.ts
2573
3027
  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";
3028
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
2575
3029
  import {
2576
3030
  basename as basename2,
2577
- dirname as dirname6,
3031
+ dirname as dirname7,
2578
3032
  isAbsolute as isAbsolute2,
2579
3033
  join as join10,
2580
3034
  relative as relative2,
@@ -2638,7 +3092,7 @@ async function createWorktree(repoRoot) {
2638
3092
  const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
2639
3093
  await git(["-C", repoRoot, "worktree", "prune"]);
2640
3094
  await pruneOldWorktrees(repoRoot);
2641
- await mkdir5(dirname6(path), { recursive: true });
3095
+ await mkdir6(dirname7(path), { recursive: true });
2642
3096
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
2643
3097
  return { path, branch };
2644
3098
  }
@@ -2758,7 +3212,7 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
2758
3212
  const relPath = join10(ingestDir, basename2(source));
2759
3213
  const dest = join10(worktreePath, relPath);
2760
3214
  try {
2761
- await mkdir5(dirname6(dest), { recursive: true });
3215
+ await mkdir6(dirname7(dest), { recursive: true });
2762
3216
  await copyFile(source, dest);
2763
3217
  } catch (err) {
2764
3218
  return {
@@ -2768,6 +3222,25 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
2768
3222
  }
2769
3223
  return { ok: true, relPath };
2770
3224
  }
3225
+ function hasEnvVar(content, name) {
3226
+ return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3227
+ }
3228
+ async function writeSearchEnvValues(worktreePath, vars) {
3229
+ const target = join10(worktreePath, ".env");
3230
+ let existing = "";
3231
+ try {
3232
+ existing = await readFile8(target, "utf8");
3233
+ } catch (err) {
3234
+ if (err.code !== "ENOENT") throw err;
3235
+ }
3236
+ const missing = vars.filter((v) => !hasEnvVar(existing, v.name));
3237
+ if (missing.length === 0) return [];
3238
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3239
+ const lines = missing.map(({ name, value }) => `${name}=${value}
3240
+ `).join("");
3241
+ await writeFile6(target, existing + prefix + lines, "utf8");
3242
+ return missing.map((v) => v.name);
3243
+ }
2771
3244
  async function listChangedFiles(worktreePath) {
2772
3245
  const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
2773
3246
  const entries = raw.split("\0");
@@ -2825,20 +3298,20 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
2825
3298
  }
2826
3299
 
2827
3300
  // src/lib/algoliaApiKey.ts
2828
- import { z as z22 } from "zod";
3301
+ import { z as z23 } from "zod";
2829
3302
  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([])
3303
+ var apiKeySchema = z23.object({
3304
+ value: z23.string().min(1),
3305
+ acl: z23.array(z23.string()).default([]),
3306
+ indexes: z23.array(z23.string()).default([])
2834
3307
  });
2835
- var apiKeyListSchema = z22.object({
2836
- items: z22.array(apiKeySchema).optional(),
2837
- keys: z22.array(apiKeySchema).optional()
3308
+ var apiKeyListSchema = z23.object({
3309
+ items: z23.array(apiKeySchema).optional(),
3310
+ keys: z23.array(apiKeySchema).optional()
2838
3311
  }).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()
3312
+ var createdKeySchema = z23.object({
3313
+ key: z23.string().min(1).optional(),
3314
+ value: z23.string().min(1).optional()
2842
3315
  });
2843
3316
  function canReuse(key, index) {
2844
3317
  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 +3347,15 @@ async function resolveSearchOnlyKey(index) {
2874
3347
 
2875
3348
  // src/lib/algoliaDocs.ts
2876
3349
  import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
2877
- import { dirname as dirname7, join as join11 } from "node:path";
3350
+ import { dirname as dirname8, join as join11 } from "node:path";
2878
3351
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2879
3352
  var DOCS_SUBPATH = join11("docs", "algolia-sdk");
2880
3353
  function findDocsDir() {
2881
- let dir = dirname7(fileURLToPath2(import.meta.url));
3354
+ let dir = dirname8(fileURLToPath2(import.meta.url));
2882
3355
  for (; ; ) {
2883
3356
  const candidate = join11(dir, DOCS_SUBPATH);
2884
3357
  if (existsSync2(candidate)) return candidate;
2885
- const parent = dirname7(dir);
3358
+ const parent = dirname8(dir);
2886
3359
  if (parent === dir) return void 0;
2887
3360
  dir = parent;
2888
3361
  }
@@ -2932,51 +3405,56 @@ function getFrameworkSpecificDoc(frameworks) {
2932
3405
  return loadAlgoliaDoc("js");
2933
3406
  }
2934
3407
 
3408
+ // src/lib/shell.ts
3409
+ function shellQuote(value) {
3410
+ return "'" + value.replace(/'/g, "'\\''") + "'";
3411
+ }
3412
+
2935
3413
  // src/actions/implement.ts
2936
- var implementSchema = z23.object({
2937
- filesChanged: z23.array(z23.string()),
2938
- summary: z23.string(),
3414
+ var implementSchema = z24.object({
3415
+ filesChanged: z24.array(z24.string()),
3416
+ summary: z24.string(),
2939
3417
  // Absolute path to the throwaway worktree holding the generated changes, so
2940
3418
  // the user can open it (`cd <worktreePath>`) or inspect the diff
2941
3419
  // (`git -C <worktreePath> status/diff`).
2942
- worktreePath: z23.string().optional(),
2943
- ingestCommand: z23.string().optional(),
3420
+ worktreePath: z24.string().optional(),
3421
+ ingestCommand: z24.string().optional(),
2944
3422
  // True when the user accepted the run-now prompt and the wizard executed the
2945
3423
  // ingestion script; downstream steps use this to avoid telling the user to run
2946
3424
  // a script that already ran.
2947
- ingestScriptRan: z23.boolean().optional(),
3425
+ ingestScriptRan: z24.boolean().optional(),
2948
3426
  // Records ingested by the run-now execution, parsed from the script's
2949
3427
  // machine-readable count line; absent when the script didn't run or emitted
2950
3428
  // no parseable count.
2951
- ingestRecordCount: z23.number().optional(),
3429
+ ingestRecordCount: z24.number().optional(),
2952
3430
  // Wall-clock duration of the run-now ingestion execution, in ms.
2953
- ingestDurationMs: z23.number().optional(),
2954
- ingestionSource: z23.enum(["local", "fileUpload", "generated"]),
3431
+ ingestDurationMs: z24.number().optional(),
3432
+ ingestionSource: z24.enum(["local", "fileUpload", "generated"]),
2955
3433
  // Suggested names/values, built from framework detection. The search agent is
2956
3434
  // instructed to rename the prefix if it doesn't match the project's build
2957
3435
  // tool, so the names it actually wrote can differ — treat these as hints, not
2958
3436
  // 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()
3437
+ searchEnvVars: z24.array(
3438
+ z24.object({
3439
+ name: z24.string(),
3440
+ value: z24.string()
2963
3441
  })
2964
3442
  ).optional()
2965
3443
  });
2966
- var implementationOutputSchema = z23.object({
2967
- summary: z23.string(),
3444
+ var implementationOutputSchema = z24.object({
3445
+ summary: z24.string(),
2968
3446
  // Ingestion only: how to run the generated script, as a structured pair the
2969
3447
  // wizard turns into an argv (`<runtime> <entrypoint>`) — never a free-form
2970
3448
  // command string. `runtime` is constrained to an allowlisted interpreter and
2971
3449
  // `entrypoint` is validated to a worktree-relative path before execution, so
2972
3450
  // the agent cannot inject extra commands or swap the interpreter.
2973
- runtime: z23.enum(INGEST_RUNTIMES).optional(),
2974
- entrypoint: z23.string().optional()
3451
+ runtime: z24.enum(INGEST_RUNTIMES).optional(),
3452
+ entrypoint: z24.string().optional()
2975
3453
  });
2976
- var verificationOutputSchema = z23.object({
2977
- summary: z23.string(),
2978
- sufficient: z23.boolean(),
2979
- additionalInstructions: z23.string().optional()
3454
+ var verificationOutputSchema = z24.object({
3455
+ summary: z24.string(),
3456
+ sufficient: z24.boolean(),
3457
+ additionalInstructions: z24.string().optional()
2980
3458
  });
2981
3459
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
2982
3460
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -3058,9 +3536,9 @@ function sourceSpecificInstructions(input) {
3058
3536
  ],
3059
3537
  generated: [
3060
3538
  "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."
3539
+ "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.",
3540
+ "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.",
3541
+ "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
3542
  ]
3065
3543
  };
3066
3544
  return byLine[input.ingestionSource];
@@ -3090,12 +3568,16 @@ function searchInstructions(input) {
3090
3568
  doc,
3091
3569
  `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
3570
  "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
- `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.`,
3571
+ // appId always resolves (loadActiveProfile throws otherwise); only the
3572
+ // search-only key is best-effort and can fall back to a placeholder.
3573
+ `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
3574
+ // Names are fixed, not the agent's to rename: the wizard writes the
3575
+ // resolved app id / search-only key into ".env" under these exact names
3576
+ // right after this step, so a renamed prefix here would leave the code
3577
+ // reading a var the wizard never wrote.
3578
+ `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3096
3579
  '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."
3580
+ "The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
3099
3581
  ];
3100
3582
  }
3101
3583
  function verificationInstructions(input) {
@@ -3127,9 +3609,14 @@ var IMPLEMENT_CONFIG = {
3127
3609
  }
3128
3610
  };
3129
3611
  var useCaseToolMap = {
3130
- ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials"],
3131
- search: [...FS_READ_TOOLS, "writeFile"],
3132
- verification: [...FS_READ_TOOLS, "verifyImplementation"]
3612
+ ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials", "notifyUser"],
3613
+ search: [...FS_READ_TOOLS, "writeFile", "notifyUser"],
3614
+ verification: [
3615
+ ...FS_READ_TOOLS,
3616
+ "writeFile",
3617
+ "verifyImplementation",
3618
+ "notifyUser"
3619
+ ]
3133
3620
  };
3134
3621
  function toolsForUseCase(useCase, ingestionSource) {
3135
3622
  const tools = useCaseToolMap[useCase];
@@ -3152,6 +3639,9 @@ function formatSummary(useCase, summary) {
3152
3639
  const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3153
3640
  return `${label}: ${summary}`;
3154
3641
  }
3642
+ function buildIngestCommand(worktree, runtime, entrypoint) {
3643
+ return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
3644
+ }
3155
3645
  function parseIngestRecordCount(output) {
3156
3646
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
3157
3647
  if (!match) return void 0;
@@ -3295,6 +3785,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3295
3785
  let ingestRecordCount;
3296
3786
  let ingestDurationMs;
3297
3787
  let installFailed = false;
3788
+ let ingestOutcomeMessage;
3298
3789
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
3299
3790
  if (agentRuns > 0) ctx.recordStepExecution();
3300
3791
  agentRuns += 1;
@@ -3307,7 +3798,17 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3307
3798
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
3308
3799
  outputSchema: implementationOutputSchema
3309
3800
  });
3801
+ ctx.notify({
3802
+ messages: [`Installing dependencies for ${currentUseCase}\u2026`]
3803
+ });
3804
+ const installLogId = ctx.logStart("installWorktreeDeps", {
3805
+ useCase: currentUseCase
3806
+ });
3310
3807
  const install = await installWorktreeDeps(worktree);
3808
+ ctx.logEnd(installLogId, {
3809
+ status: install.ok ? "success" : "error",
3810
+ output: install.output
3811
+ });
3311
3812
  if (!install.ok) {
3312
3813
  installFailed = true;
3313
3814
  logger.warn(
@@ -3332,6 +3833,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3332
3833
  ingestRuntime = runtime;
3333
3834
  ingestEntrypoint = entrypoint;
3334
3835
  if (ingestRuntime && ingestEntrypoint && !installFailed) {
3836
+ ctx.clearNotices();
3335
3837
  const runNow = await ctx.requestUserInput({
3336
3838
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
3337
3839
  promptType: "acceptReject",
@@ -3340,6 +3842,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3340
3842
  }) === true;
3341
3843
  if (runNow) {
3342
3844
  const profile2 = await loadActiveProfile();
3845
+ ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
3846
+ const scriptLogId = ctx.logStart("runIngestScript", {
3847
+ runtime: ingestRuntime,
3848
+ entrypoint: ingestEntrypoint
3849
+ });
3343
3850
  const startedAt = Date.now();
3344
3851
  const run = await runIngestScript(
3345
3852
  worktree,
@@ -3350,6 +3857,10 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3350
3857
  [API_KEY_VAR]: profile2.apiKey
3351
3858
  }
3352
3859
  );
3860
+ ctx.logEnd(scriptLogId, {
3861
+ status: run.ok ? "success" : "error",
3862
+ output: run.output
3863
+ });
3353
3864
  ingestScriptRan = run.ran && run.ok;
3354
3865
  if (ingestScriptRan) {
3355
3866
  ingestDurationMs = Date.now() - startedAt;
@@ -3405,19 +3916,38 @@ ${run.output}` : status;
3405
3916
  });
3406
3917
  }
3407
3918
  summaries.push(summaryLine);
3408
- await ctx.requestUserInput({
3409
- prompt: "Continue",
3410
- promptType: "notice",
3411
- options: [],
3412
- messages: [outcomeMessage]
3413
- });
3919
+ ingestOutcomeMessage = outcomeMessage;
3414
3920
  }
3415
3921
  }
3922
+ const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
3923
+ if (ingestRuntime && ingestEntrypoint) {
3924
+ commandMessages.push(
3925
+ `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
3926
+ );
3927
+ }
3928
+ await ctx.requestUserInput({
3929
+ // No question being asked here, just an acknowledgement — the
3930
+ // continue/decline hints below already say "continue".
3931
+ prompt: "",
3932
+ promptType: "spaceToContinue",
3933
+ options: [],
3934
+ messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
3935
+ });
3416
3936
  }
3417
3937
  if (useCases.includes("search")) {
3418
3938
  let extraInstructions = [];
3419
3939
  const preSearchFiles = new Set(await listChangedFiles(worktree));
3420
3940
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
3941
+ if (attempt > 1) {
3942
+ logger.info(
3943
+ {
3944
+ attempt,
3945
+ maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
3946
+ extraInstructions
3947
+ },
3948
+ "implement: retrying search implementation after failed verification"
3949
+ );
3950
+ }
3421
3951
  const { summary } = await runImplementationUseCase(
3422
3952
  "search",
3423
3953
  extraInstructions
@@ -3446,6 +3976,26 @@ ${run.output}` : status;
3446
3976
  }
3447
3977
  extraInstructions = verificationRetryInstructions(verification);
3448
3978
  }
3979
+ const resolvedSearchEnvVars = input.searchEnvVars.filter(
3980
+ (v) => !v.value.startsWith("<")
3981
+ );
3982
+ if (resolvedSearchEnvVars.length > 0) {
3983
+ const written = await writeSearchEnvValues(
3984
+ worktree,
3985
+ resolvedSearchEnvVars
3986
+ );
3987
+ if (written.length > 0) {
3988
+ summaries.push(`Wrote ${written.join(", ")} to .env.`);
3989
+ }
3990
+ }
3991
+ const unresolvedSearchEnvVars = input.searchEnvVars.filter(
3992
+ (v) => v.value.startsWith("<")
3993
+ );
3994
+ if (unresolvedSearchEnvVars.length > 0) {
3995
+ summaries.push(
3996
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
3997
+ );
3998
+ }
3449
3999
  } else {
3450
4000
  ctx.setUserInput("implementation", "success");
3451
4001
  }
@@ -3466,7 +4016,11 @@ ${run.output}` : status;
3466
4016
  summary: summaries.join("\n\n"),
3467
4017
  worktreePath: worktree,
3468
4018
  ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
3469
- ingestCommand: `cd ${shellQuote(worktree)} && ${ingestRuntime} ${shellQuote(ingestEntrypoint)}`,
4019
+ ingestCommand: buildIngestCommand(
4020
+ worktree,
4021
+ ingestRuntime,
4022
+ ingestEntrypoint
4023
+ ),
3470
4024
  ingestScriptRan,
3471
4025
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
3472
4026
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
@@ -3488,7 +4042,14 @@ var defaultWorkflow = {
3488
4042
  id: "project-scan",
3489
4043
  title: "project scan",
3490
4044
  outputSchema: projectScanSchema,
3491
- run: (ctx) => projectScan(ctx)
4045
+ run: (ctx) => {
4046
+ ctx.notify({
4047
+ messages: [
4048
+ "Scanning your project for languages, frameworks, and Algolia integration points\u2026"
4049
+ ]
4050
+ });
4051
+ return projectScan(ctx);
4052
+ }
3492
4053
  }),
3493
4054
  defineStep({
3494
4055
  id: "confirm-language",
@@ -3507,8 +4068,8 @@ var defaultWorkflow = {
3507
4068
  defineStep({
3508
4069
  id: "select-index",
3509
4070
  title: "Set up index",
3510
- outputSchema: z24.object({
3511
- selection: z24.string()
4071
+ outputSchema: z25.object({
4072
+ selection: z25.string()
3512
4073
  }),
3513
4074
  run: (ctx) => selectIndexStep(ctx)
3514
4075
  }),
@@ -3516,7 +4077,14 @@ var defaultWorkflow = {
3516
4077
  id: "ingestion",
3517
4078
  title: "ingest records",
3518
4079
  outputSchema: implementSchema,
3519
- run: (ctx) => implement(ctx, ["ingestion"])
4080
+ run: (ctx) => {
4081
+ ctx.notify({
4082
+ messages: [
4083
+ "Setting up an Algolia ingestion pipeline in your project\u2026"
4084
+ ]
4085
+ });
4086
+ return implement(ctx, ["ingestion"]);
4087
+ }
3520
4088
  }),
3521
4089
  defineStep({
3522
4090
  id: "confirm-framework",
@@ -3530,6 +4098,9 @@ var defaultWorkflow = {
3530
4098
  title: "create search ui",
3531
4099
  outputSchema: implementSchema,
3532
4100
  run: (ctx) => {
4101
+ ctx.notify({
4102
+ messages: ["Building your Algolia search experience\u2026"]
4103
+ });
3533
4104
  const ingestion = ctx.getStepOutput(
3534
4105
  "ingestion"
3535
4106
  );
@@ -3541,11 +4112,17 @@ var defaultWorkflow = {
3541
4112
  title: "done",
3542
4113
  outputSchema: reviewSchema,
3543
4114
  run: (ctx) => {
4115
+ ctx.notify({
4116
+ messages: ["Summarizing what we did\u2026"]
4117
+ });
3544
4118
  const ingestion = ctx.getStepOutput(
3545
4119
  "ingestion"
3546
4120
  );
3547
4121
  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."
4122
+ // The ingestion step already showed the user the exact `ingestCommand`
4123
+ // and worktree path as a notice, so nextSteps must not restate it —
4124
+ // an LLM-paraphrased command risks being wrong.
4125
+ 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
4126
  });
3550
4127
  }
3551
4128
  })
@@ -3561,7 +4138,7 @@ function getWorkflow(id) {
3561
4138
  }
3562
4139
 
3563
4140
  // src/main.tsx
3564
- import { jsx as jsx11 } from "react/jsx-runtime";
4141
+ import { jsx as jsx14 } from "react/jsx-runtime";
3565
4142
  var requestedId = process.argv[2] ?? defaultWorkflow.id;
3566
4143
  var workflow = getWorkflow(requestedId);
3567
4144
  if (!workflow) {
@@ -3570,7 +4147,7 @@ if (!workflow) {
3570
4147
  process.exit(1);
3571
4148
  }
3572
4149
  var store = useWizard.getState();
3573
- var instance = render(/* @__PURE__ */ jsx11(App, {}));
4150
+ var instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
3574
4151
  var user = await getUser();
3575
4152
  if (!user) {
3576
4153
  await instance.waitUntilRenderFlush();
@@ -3581,7 +4158,7 @@ if (!user) {
3581
4158
  console.error(err instanceof Error ? err.message : String(err));
3582
4159
  process.exit(1);
3583
4160
  }
3584
- instance = render(/* @__PURE__ */ jsx11(App, {}));
4161
+ instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
3585
4162
  user = await getUser();
3586
4163
  if (!user) {
3587
4164
  store.setError(