@algolia/wizard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js ADDED
@@ -0,0 +1,3597 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/main.tsx
4
+ import { render } from "ink";
5
+
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";
9
+
10
+ // src/core/store.ts
11
+ import { create } from "zustand";
12
+ function toUserInfo({ user_id, ...rest }) {
13
+ return { userId: user_id, ...rest };
14
+ }
15
+ var useWizard = create((set, get) => ({
16
+ phase: "idle",
17
+ user: null,
18
+ workflow: null,
19
+ steps: [],
20
+ currentStepIndex: 0,
21
+ output: "",
22
+ error: null,
23
+ inputReq: null,
24
+ _resolve: null,
25
+ // Advances past the welcome screen. Only meaningful from 'idle' — once the
26
+ // workflow is running there's nothing left to confirm.
27
+ confirmStart: () => set((s) => s.phase === "idle" ? { phase: "preflight" } : {}),
28
+ // Resolves once the phase leaves 'idle', whether that happens before or
29
+ // after this is called (the welcome screen's spacebar handler is what
30
+ // drives the transition via `confirmStart`).
31
+ waitForStart: () => new Promise((resolve4) => {
32
+ if (get().phase !== "idle") {
33
+ resolve4();
34
+ return;
35
+ }
36
+ const unsubscribe = useWizard.subscribe((s) => {
37
+ if (s.phase !== "idle") {
38
+ unsubscribe();
39
+ resolve4();
40
+ }
41
+ });
42
+ }),
43
+ startWorkflow: (workflow2, steps) => set({
44
+ phase: "running",
45
+ workflow: workflow2,
46
+ steps,
47
+ currentStepIndex: 0,
48
+ error: null
49
+ }),
50
+ syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
51
+ setActiveStep: (index) => set({ phase: "running", currentStepIndex: index, output: "" }),
52
+ setUser: (user2) => set({ user: user2 }),
53
+ appendToken: (text) => set((s) => ({ output: s.output + text })),
54
+ clearOutput: () => set({ output: "" }),
55
+ requestUserInput: (req) => new Promise((resolve4) => {
56
+ set({
57
+ phase: "awaitingInput",
58
+ inputReq: req,
59
+ _resolve: resolve4
60
+ });
61
+ }),
62
+ submitInput: (value) => {
63
+ get()._resolve?.(value);
64
+ set({ inputReq: null, _resolve: null, phase: "running" });
65
+ },
66
+ setDone: () => set({ phase: "done" }),
67
+ 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
+ })
78
+ }));
79
+
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";
99
+
100
+ // src/ui/Table.tsx
101
+ import { Box, Text, measureElement, useWindowSize } from "ink";
102
+ import { useEffect, useRef, useState } from "react";
103
+ import { jsx } from "react/jsx-runtime";
104
+ function Table({ columns, rows }) {
105
+ const { columns: termCols } = useWindowSize();
106
+ const ref = useRef(null);
107
+ const [width, setWidth] = useState(0);
108
+ useEffect(() => {
109
+ if (ref.current) setWidth(measureElement(ref.current).width);
110
+ }, [termCols, columns, rows]);
111
+ if (rows.length === 0) return null;
112
+ const lines = formatTable(columns, rows, width || void 0);
113
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: line }, `tbl-${i}`)) });
114
+ }
115
+ function formatTable(columns, rows, width) {
116
+ const natural = columns.map(
117
+ (c, i) => Math.max(c.length, ...rows.map((r) => (r[i] ?? "").length))
118
+ );
119
+ const overhead = 3 * columns.length + 1;
120
+ const widths = width !== void 0 ? resize(natural, Math.max(width - overhead, columns.length)) : natural;
121
+ const border = (left, mid, right) => left + widths.map((w) => "\u2500".repeat(w + 2)).join(mid) + right;
122
+ const rowLine = (cells) => "\u2502" + widths.map((w, i) => ` ${truncate(cells[i] ?? "", w).padEnd(w)} `).join("\u2502") + "\u2502";
123
+ return [
124
+ border("\u250C", "\u252C", "\u2510"),
125
+ rowLine(columns),
126
+ border("\u251C", "\u253C", "\u2524"),
127
+ ...rows.map(rowLine),
128
+ border("\u2514", "\u2534", "\u2518")
129
+ ];
130
+ }
131
+ function resize(widths, budget) {
132
+ const out = [...widths];
133
+ const widest = () => {
134
+ let w = 0;
135
+ for (let i = 1; i < out.length; i++) if (out[i] > out[w]) w = i;
136
+ return w;
137
+ };
138
+ let total = out.reduce((a, b) => a + b, 0);
139
+ while (total > budget) {
140
+ const i = widest();
141
+ if (out[i] <= 1) break;
142
+ out[i]--;
143
+ total--;
144
+ }
145
+ while (total < budget) {
146
+ out[widest()]++;
147
+ total++;
148
+ }
149
+ return out;
150
+ }
151
+ var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, width) : `${s.slice(0, width - 1)}\u2026`;
152
+
153
+ // src/ui/theme.ts
154
+ var MARKER = {
155
+ pending: "\u25CB",
156
+ running: "\u25D0",
157
+ done: "\u2713",
158
+ error: "\u2716"
159
+ };
160
+ var BRAND = "#003DFF";
161
+ var SECONDARY = "#5468FF";
162
+ var COLORS = {
163
+ brand: BRAND,
164
+ primary: "#E6EDF3",
165
+ secondary: SECONDARY,
166
+ strong: "#FFFFFF",
167
+ muted: "#8B949E",
168
+ dim: "#484F58",
169
+ highlight: { bg: "#12331C", fg: "#4ADE80" },
170
+ badge: "#E3B341",
171
+ warning: "#F86E7E",
172
+ success: "#4ADE80",
173
+ bg: {
174
+ main: "#0B0E14",
175
+ sidebar: "#14171E"
176
+ },
177
+ accent: "#76A0FF",
178
+ status: {
179
+ pending: "gray",
180
+ running: "#76A0FF",
181
+ done: "#4ADE80",
182
+ error: "#F86E7E"
183
+ }
184
+ };
185
+
186
+ // src/ui/SelectPrompt.tsx
187
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
188
+ var CANCEL = "cancel";
189
+ function SelectPrompt({
190
+ options,
191
+ onSelect,
192
+ question,
193
+ helpText,
194
+ messages,
195
+ table,
196
+ error,
197
+ multi,
198
+ cancelable,
199
+ secondary,
200
+ defaultSelectedIndex = 0
201
+ }) {
202
+ const [index, setIndex] = useState2(
203
+ () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
204
+ );
205
+ const [checked, setChecked] = useState2(() => /* @__PURE__ */ new Set());
206
+ const hasCancel = Boolean(multi || cancelable);
207
+ const rows = hasCancel ? [...options, "Cancel"] : options;
208
+ const cancelIndex = hasCancel ? options.length : -1;
209
+ const hints = [];
210
+ if (rows.length > 1) hints.push({ key: "[\u2191] [\u2193]", label: "move" });
211
+ if (multi) hints.push({ key: "[space]", label: "select" });
212
+ hints.push({ key: "[enter]", label: "confirm" });
213
+ const labelColWidth = 6 + Math.max(0, ...rows.map((opt) => opt.length));
214
+ const secondaryWidth = Math.max(
215
+ 0,
216
+ ...rows.map((_, i) => secondary?.[i]?.value.length ?? 0)
217
+ );
218
+ const rowWidth = labelColWidth + (secondaryWidth ? secondaryWidth + 2 : 0) + 6;
219
+ useInput((input, key) => {
220
+ if (rows.length === 0) return;
221
+ if (key.upArrow || input === "k") {
222
+ setIndex((i) => (i - 1 + rows.length) % rows.length);
223
+ } else if (key.downArrow || input === "j") {
224
+ setIndex((i) => (i + 1) % rows.length);
225
+ } else if (multi && input === " " && index !== cancelIndex) {
226
+ setChecked((prev) => {
227
+ const next = new Set(prev);
228
+ if (next.has(index)) next.delete(index);
229
+ else next.add(index);
230
+ return next;
231
+ });
232
+ } else if (key.return) {
233
+ if (index === cancelIndex) {
234
+ onSelect(CANCEL);
235
+ } else if (!multi) {
236
+ onSelect(options[index]);
237
+ } else if (checked.size > 0) {
238
+ onSelect(options.filter((_, i) => checked.has(i)));
239
+ }
240
+ }
241
+ });
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 })
249
+ ] }),
250
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: rows.map((option, i) => {
251
+ const highlighted = i === index;
252
+ const isCancel = i === cancelIndex;
253
+ const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
254
+ const sec = isCancel ? void 0 : secondary?.[i];
255
+ const labelColor = highlighted ? COLORS.highlight.fg : void 0;
256
+ const label = /* @__PURE__ */ jsxs(Text2, { color: labelColor, children: [
257
+ highlighted ? "\u276F " : " ",
258
+ bullet,
259
+ option
260
+ ] });
261
+ return /* @__PURE__ */ jsxs(
262
+ Box2,
263
+ {
264
+ width: rowWidth,
265
+ paddingX: 1,
266
+ paddingY: 1,
267
+ backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
268
+ 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 })
273
+ ]
274
+ },
275
+ `row-${i}`
276
+ );
277
+ }) }),
278
+ /* @__PURE__ */ jsx2(Box2, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(Text2, { children: [
279
+ i > 0 ? " " : "",
280
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.primary, children: key }),
281
+ /* @__PURE__ */ jsxs(Text2, { color: COLORS.dim, children: [
282
+ " ",
283
+ label
284
+ ] })
285
+ ] }, label)) })
286
+ ] });
287
+ }
288
+
289
+ // src/ui/PromptInput.tsx
290
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
291
+ var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
292
+ function PromptInput() {
293
+ const { phase, inputReq, submitInput } = useWizard();
294
+ const [draft, setDraft] = useState3("");
295
+ 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" }) });
297
+ }
298
+ if (phase !== "awaitingInput" || !inputReq) return null;
299
+ if (inputReq.promptType === "multipleChoice") {
300
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
301
+ SelectPrompt,
302
+ {
303
+ question: inputReq.prompt,
304
+ helpText: inputReq.helpText,
305
+ messages: inputReq.messages,
306
+ table: inputReq.table,
307
+ options: inputReq.options,
308
+ secondary: inputReq.secondary,
309
+ defaultSelectedIndex: inputReq.defaultSelectedIndex,
310
+ cancelable: inputReq.cancelable,
311
+ error: inputReq.error,
312
+ onSelect: submitInput
313
+ }
314
+ ) });
315
+ }
316
+ if (inputReq.promptType === "multiSelect") {
317
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
318
+ SelectPrompt,
319
+ {
320
+ multi: true,
321
+ question: inputReq.prompt,
322
+ helpText: inputReq.helpText,
323
+ messages: inputReq.messages,
324
+ table: inputReq.table,
325
+ options: inputReq.options,
326
+ error: inputReq.error,
327
+ onSelect: submitInput
328
+ }
329
+ ) });
330
+ }
331
+ if (inputReq.promptType === "notice") {
332
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
333
+ SelectPrompt,
334
+ {
335
+ question: inputReq.prompt,
336
+ messages: inputReq.messages,
337
+ options: ["Continue"],
338
+ onSelect: submitInput
339
+ }
340
+ ) });
341
+ }
342
+ if (inputReq.promptType === "acceptReject") {
343
+ const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
344
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(
345
+ SelectPrompt,
346
+ {
347
+ question: inputReq.prompt,
348
+ messages: inputReq.messages,
349
+ options: labels,
350
+ secondary: inputReq.secondary,
351
+ onSelect: (value) => submitInput(value === labels[0])
352
+ }
353
+ ) });
354
+ }
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: [
360
+ inputReq.prompt,
361
+ " "
362
+ ] }),
363
+ /* @__PURE__ */ jsx3(
364
+ TextInput,
365
+ {
366
+ value: draft,
367
+ onChange: setDraft,
368
+ onSubmit: (v) => {
369
+ submitInput(v);
370
+ setDraft("");
371
+ }
372
+ }
373
+ )
374
+ ] })
375
+ ] });
376
+ }
377
+
378
+ // src/ui/Welcome.tsx
379
+ import { dirname, join } from "node:path";
380
+ 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
+ }
405
+
406
+ // src/ui/copy/welcome.ts
407
+ var sidebarItems = [
408
+ {
409
+ title: "scan your project",
410
+ description: "detect models, schemas, and data worth indexing"
411
+ },
412
+ {
413
+ title: "select and index",
414
+ description: "push 100 records to Algolia in seconds"
415
+ },
416
+ {
417
+ title: "detect your framework",
418
+ description: "React, Vue, Angular, Vanilla JS"
419
+ },
420
+ {
421
+ title: "scaffold a search UI",
422
+ description: "a styled InstantSearch component, wired into your app"
423
+ },
424
+ {
425
+ title: "ship it",
426
+ description: "build check passes, search is live with your real data"
427
+ }
428
+ ];
429
+
430
+ // src/ui/Welcome.tsx
431
+ 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");
434
+ function SidebarItem({
435
+ title,
436
+ description
437
+ }) {
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 })
442
+ ] }),
443
+ /* @__PURE__ */ jsxs4(Box5, { flexDirection: "row", gap: 2, children: [
444
+ /* @__PURE__ */ jsx5(Spacer, {}),
445
+ /* @__PURE__ */ jsx5(Text5, { color: COLORS.muted, children: description })
446
+ ] })
447
+ ] });
448
+ }
449
+ function Welcome() {
450
+ const confirmStart = useWizard((s) => s.confirmStart);
451
+ useInput2((input) => {
452
+ if (input === " ") confirmStart();
453
+ });
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(
457
+ Image,
458
+ {
459
+ src: IMAGE_PATH,
460
+ objectFit: "contain",
461
+ alt: "Algolia",
462
+ width: 30,
463
+ height: 15,
464
+ protocol: "halfBlock"
465
+ }
466
+ ) }),
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" })
471
+ ] })
472
+ ] }) }),
473
+ /* @__PURE__ */ jsxs4(
474
+ Box5,
475
+ {
476
+ backgroundColor: COLORS.bg.sidebar,
477
+ width: 40,
478
+ padding: 4,
479
+ gap: 2,
480
+ flexDirection: "column",
481
+ justifyContent: "center",
482
+ 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))
485
+ ]
486
+ }
487
+ )
488
+ ] });
489
+ }
490
+
491
+ // src/ui/Sidebar.tsx
492
+ import { Box as Box8, Text as Text8 } from "ink";
493
+
494
+ // src/ui/Steps.tsx
495
+ import { Box as Box6, Text as Text6 } from "ink";
496
+ import Spinner from "ink-spinner";
497
+
498
+ // src/core/persistence.ts
499
+ import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
500
+ import { join as join3 } from "node:path";
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
516
+ var isStepVisible = (s) => s.visible !== false;
517
+ var stateFile = (workflowId) => join3(stateDir(), `state-${workflowId}.json`);
518
+ async function loadWorkflowState(workflowId) {
519
+ try {
520
+ const raw = await readFile(stateFile(workflowId), "utf8");
521
+ return JSON.parse(raw);
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
526
+ async function saveWorkflowState(state) {
527
+ await mkdir(stateDir(), { recursive: true });
528
+ await writeFile(
529
+ stateFile(state.workflowId),
530
+ JSON.stringify(state, null, 2),
531
+ "utf8"
532
+ );
533
+ }
534
+ async function clearWorkflowState(workflowId) {
535
+ await rm(stateFile(workflowId), { force: true });
536
+ }
537
+
538
+ // 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
+ }
631
+ function Steps() {
632
+ const { steps } = useWizard();
633
+ 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
+ }) });
647
+ }
648
+ function CurrentStep() {
649
+ const { steps } = useWizard();
650
+ const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
651
+ if (!currentStep) return null;
652
+ return /* @__PURE__ */ jsxs5(Text6, { color: COLORS.status.running, children: [
653
+ /* @__PURE__ */ jsx6(Spinner, { type: "dots" }),
654
+ " ",
655
+ ` ${currentStep.title}`
656
+ ] });
657
+ }
658
+
659
+ // 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";
662
+ function Progress() {
663
+ const { steps, currentStepIndex } = useWizard();
664
+ const visibleSteps = steps.filter(isStepVisible);
665
+ if (visibleSteps.length === 0) return null;
666
+ const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
667
+ 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 })
673
+ ] });
674
+ }
675
+
676
+ // src/ui/copy/sidebar-commands.ts
677
+ var sidebarCommands = [
678
+ { keyHint: "tab", description: "toggle logs" },
679
+ { keyHint: "esc", description: "close" }
680
+ ];
681
+
682
+ // src/ui/Sidebar.tsx
683
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
684
+ function Sidebar() {
685
+ return /* @__PURE__ */ jsxs7(
686
+ Box8,
687
+ {
688
+ backgroundColor: "#14171E",
689
+ width: 30,
690
+ paddingX: 4,
691
+ paddingY: 2,
692
+ flexDirection: "column",
693
+ justifyContent: "space-between",
694
+ children: [
695
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 1, children: [
696
+ /* @__PURE__ */ jsx8(Text8, { color: COLORS.muted, children: "PROGRESS" }),
697
+ /* @__PURE__ */ jsx8(Steps, {})
698
+ ] }),
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 })
705
+ ] });
706
+ }) })
707
+ ] })
708
+ ]
709
+ }
710
+ );
711
+ }
712
+
713
+ // 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";
716
+ function Ribbon() {
717
+ const firstCommand = sidebarCommands[0];
718
+ return /* @__PURE__ */ jsxs8(
719
+ Box9,
720
+ {
721
+ backgroundColor: "#14171E",
722
+ flexDirection: "row",
723
+ justifyContent: "space-between",
724
+ paddingX: 2,
725
+ paddingY: 1,
726
+ 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 })
732
+ ] })
733
+ ]
734
+ }
735
+ );
736
+ }
737
+
738
+ // src/ui/App.tsx
739
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
740
+ 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;
747
+ const { exit } = useApp();
748
+ const { columns, rows } = useWindowSize2();
749
+ const [copied, setCopied] = useState4(null);
750
+ 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) {
759
+ 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
+ }
770
+ },
771
+ { isActive: finished }
772
+ );
773
+ const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
774
+ const flexDirection = columns > 90 ? "row" : "column";
775
+ const showSidebar = flexDirection === "row";
776
+ return /* @__PURE__ */ jsxs9(
777
+ Box10,
778
+ {
779
+ backgroundColor: COLORS.bg.main,
780
+ flexDirection: "row",
781
+ width: columns,
782
+ minHeight: rows,
783
+ children: [
784
+ mainWindowVisible && /* @__PURE__ */ jsxs9(
785
+ Box10,
786
+ {
787
+ flexDirection,
788
+ width: "100%",
789
+ justifyContent: "space-between",
790
+ 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: [
794
+ "\u2716 ",
795
+ 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
+ ] })
801
+ ] }),
802
+ /* @__PURE__ */ jsx10(Text10, { color: "white", backgroundColor: "#14171E" }),
803
+ showSidebar ? /* @__PURE__ */ jsx10(Sidebar, {}) : /* @__PURE__ */ jsx10(Ribbon, {})
804
+ ]
805
+ }
806
+ ),
807
+ (phase === "idle" || phase === "preflight") && /* @__PURE__ */ jsx10(Welcome, {})
808
+ ]
809
+ }
810
+ );
811
+ }
812
+
813
+ // src/core/orchestrator.ts
814
+ import "zod";
815
+
816
+ // src/core/config.ts
817
+ 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");
820
+ var DEFAULT_CONFIG = {
821
+ version: 1,
822
+ aiConsent: false,
823
+ workflowsRun: []
824
+ };
825
+ async function loadConfig() {
826
+ try {
827
+ const raw = await readFile2(configFile(), "utf8");
828
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
829
+ } catch {
830
+ return { ...DEFAULT_CONFIG };
831
+ }
832
+ }
833
+ async function saveConfig(config) {
834
+ await mkdir2(stateDir(), { recursive: true });
835
+ await writeFile2(configFile(), JSON.stringify(config, null, 2), "utf8");
836
+ }
837
+ async function recordWorkflowRun(workflowId, completedAt) {
838
+ const config = await loadConfig();
839
+ config.workflowsRun.push({ workflowId, completedAt });
840
+ await saveConfig(config);
841
+ }
842
+
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
+ // src/lib/telemetry.ts
966
+ function isTelemetryEnabled() {
967
+ return Boolean(getAuthToken()) && !process.env.VITEST && process.env.WIZARD_TELEMETRY !== "false";
968
+ }
969
+ function fireAndForget(promise) {
970
+ void promise.catch(() => {
971
+ });
972
+ }
973
+ function sendTelemetry(payload) {
974
+ if (!isTelemetryEnabled()) return;
975
+ const token = getAuthToken();
976
+ if (!token) return;
977
+ const body = {
978
+ env: process.env.DD_ENV ?? "development",
979
+ ...payload
980
+ };
981
+ fireAndForget(
982
+ proxyFetch(`${PROXY_BASE_URL}/telemetry`, {
983
+ method: "POST",
984
+ headers: {
985
+ "Content-Type": "application/json",
986
+ authorization: `Bearer ${token}`
987
+ },
988
+ body: JSON.stringify(body)
989
+ }).then((res) => {
990
+ if (!res.ok) {
991
+ throw new Error(`Proxy telemetry ${res.status}`);
992
+ }
993
+ })
994
+ );
995
+ }
996
+ function sendLog(status, message, attributes = {}, tags = []) {
997
+ sendTelemetry({
998
+ logs: [{ status, message, attributes, tags }]
999
+ });
1000
+ }
1001
+ function sendMetric(name, value, type, tags = []) {
1002
+ sendTelemetry({
1003
+ metrics: [{ name, value, type, tags }]
1004
+ });
1005
+ }
1006
+ function metricTags(workflowId, actionId) {
1007
+ return actionId ? [`workflow:${workflowId}`, `action:${actionId}`] : [`workflow:${workflowId}`];
1008
+ }
1009
+ function logTags(workflowId, actionId, appId) {
1010
+ return [
1011
+ ...metricTags(workflowId, actionId),
1012
+ ...appId ? [`app_id:${appId}`] : []
1013
+ ];
1014
+ }
1015
+ function emitTelemetryLog(level, message, attributes, tags) {
1016
+ if (level === "info") {
1017
+ logger.info(attributes, message);
1018
+ } else {
1019
+ logger.error(attributes, message);
1020
+ }
1021
+ sendLog(level, message, attributes, tags);
1022
+ }
1023
+ function trackWorkflowStart(ctx) {
1024
+ const attributes = {
1025
+ event: "wizard.workflow.start",
1026
+ workflow_id: ctx.workflowId,
1027
+ app_id: ctx.appId
1028
+ };
1029
+ emitTelemetryLog(
1030
+ "info",
1031
+ "wizard workflow started",
1032
+ attributes,
1033
+ logTags(ctx.workflowId, void 0, ctx.appId)
1034
+ );
1035
+ sendMetric("wizard.workflow.invocation", 1, 1, metricTags(ctx.workflowId));
1036
+ }
1037
+ function trackActionStart(ctx) {
1038
+ const attributes = {
1039
+ event: "wizard.action.start",
1040
+ workflow_id: ctx.workflowId,
1041
+ action_id: ctx.actionId,
1042
+ action_title: ctx.actionTitle,
1043
+ app_id: ctx.appId
1044
+ };
1045
+ emitTelemetryLog(
1046
+ "info",
1047
+ "wizard action started",
1048
+ attributes,
1049
+ logTags(ctx.workflowId, ctx.actionId, ctx.appId)
1050
+ );
1051
+ }
1052
+ function trackActionEnd(ctx) {
1053
+ const attributes = {
1054
+ event: "wizard.action.end",
1055
+ workflow_id: ctx.workflowId,
1056
+ action_id: ctx.actionId,
1057
+ action_title: ctx.actionTitle,
1058
+ duration_ms: ctx.durationMs,
1059
+ app_id: ctx.appId
1060
+ };
1061
+ emitTelemetryLog(
1062
+ "info",
1063
+ "wizard action completed",
1064
+ attributes,
1065
+ logTags(ctx.workflowId, ctx.actionId, ctx.appId)
1066
+ );
1067
+ sendMetric(
1068
+ "wizard.action.invocation",
1069
+ 1,
1070
+ 1,
1071
+ metricTags(ctx.workflowId, ctx.actionId)
1072
+ );
1073
+ }
1074
+ function trackActionError(ctx) {
1075
+ const attributes = {
1076
+ event: "wizard.action.error",
1077
+ workflow_id: ctx.workflowId,
1078
+ action_id: ctx.actionId,
1079
+ action_title: ctx.actionTitle,
1080
+ error: ctx.error,
1081
+ app_id: ctx.appId
1082
+ };
1083
+ emitTelemetryLog(
1084
+ "error",
1085
+ "wizard action failed",
1086
+ attributes,
1087
+ logTags(ctx.workflowId, ctx.actionId, ctx.appId)
1088
+ );
1089
+ sendMetric(
1090
+ "wizard.action.error",
1091
+ 1,
1092
+ 1,
1093
+ metricTags(ctx.workflowId, ctx.actionId)
1094
+ );
1095
+ }
1096
+ function trackWizardComplete(event) {
1097
+ const attributes = {
1098
+ event: "wizard.workflow.complete",
1099
+ workflow_id: event.workflowId,
1100
+ app_id: event.appId,
1101
+ total_duration: event.total_duration,
1102
+ total_steps: event.total_steps
1103
+ };
1104
+ emitTelemetryLog(
1105
+ "info",
1106
+ "wizard workflow completed",
1107
+ attributes,
1108
+ logTags(event.workflowId, void 0, event.appId)
1109
+ );
1110
+ }
1111
+ function trackWorkflowError(ctx) {
1112
+ const attributes = {
1113
+ event: "wizard.workflow.error",
1114
+ workflow_id: ctx.workflowId,
1115
+ action_id: ctx.actionId,
1116
+ error: ctx.error,
1117
+ app_id: ctx.appId
1118
+ };
1119
+ emitTelemetryLog(
1120
+ "error",
1121
+ "wizard workflow failed",
1122
+ attributes,
1123
+ logTags(ctx.workflowId, ctx.actionId, ctx.appId)
1124
+ );
1125
+ }
1126
+
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
+ // src/core/orchestrator.ts
1147
+ function defineStep(step) {
1148
+ return { visible: true, ...step };
1149
+ }
1150
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1151
+ function ensureExecutedStepCount(state) {
1152
+ if (state.executedStepCount == null) {
1153
+ state.executedStepCount = state.steps.filter(
1154
+ (s) => s.status === "done" && isStepVisible(s)
1155
+ ).length;
1156
+ }
1157
+ }
1158
+ function reconcileWorkflowState(state, workflow2) {
1159
+ const persistedIds = state.steps.map((s) => s.id).join("\n");
1160
+ const definedIds = workflow2.steps.map((s) => s.id).join("\n");
1161
+ if (persistedIds !== definedIds) return null;
1162
+ for (let i = 0; i < state.steps.length; i++) {
1163
+ state.steps[i].title = workflow2.steps[i].title;
1164
+ state.steps[i].visible = workflow2.steps[i].visible;
1165
+ }
1166
+ return state;
1167
+ }
1168
+ function initWorkflowState(workflow2, now) {
1169
+ return {
1170
+ workflowId: workflow2.id,
1171
+ startedAt: now,
1172
+ updatedAt: now,
1173
+ currentStepIndex: 0,
1174
+ steps: workflow2.steps.map((s) => ({
1175
+ id: s.id,
1176
+ title: s.title,
1177
+ visible: s.visible,
1178
+ status: "pending"
1179
+ })),
1180
+ algoliaState: {},
1181
+ userInputs: {}
1182
+ };
1183
+ }
1184
+ async function ensureConsent() {
1185
+ const config = await loadConfig();
1186
+ if (config.aiConsent) return;
1187
+ const store2 = useWizard.getState();
1188
+ const answer = await store2.requestUserInput({
1189
+ prompt: 'Wizard will make AI-authored changes to this repository. Type "yes" to consent:',
1190
+ promptType: "textInput",
1191
+ options: []
1192
+ });
1193
+ if (typeof answer !== "string" || answer.trim().toLowerCase() !== "yes") {
1194
+ throw new Error("AI consent declined \u2014 cannot proceed.");
1195
+ }
1196
+ config.aiConsent = true;
1197
+ await saveConfig(config);
1198
+ }
1199
+ async function makeContext(state) {
1200
+ const outputs = {};
1201
+ const completedSteps = [];
1202
+ for (const s of state.steps) {
1203
+ if (s.status === "done") {
1204
+ outputs[s.id] = s.output;
1205
+ if (s.visible) {
1206
+ completedSteps.push({ id: s.id, title: s.title, output: s.output });
1207
+ }
1208
+ }
1209
+ }
1210
+ return {
1211
+ outputs,
1212
+ completedSteps,
1213
+ getStepOutput: (stepId) => outputs[stepId],
1214
+ requestUserInput: (prompt) => useWizard.getState().requestUserInput(prompt),
1215
+ updateAlgoliaState: (key, value) => {
1216
+ state.algoliaState[key] = value;
1217
+ },
1218
+ setUserInput: (key, value) => {
1219
+ state.userInputs[key] = value;
1220
+ },
1221
+ recordStepExecution: (count = 1) => {
1222
+ state.executedStepCount = (state.executedStepCount ?? 0) + count;
1223
+ },
1224
+ config: await loadConfig()
1225
+ };
1226
+ }
1227
+ async function runStep(state, index, step, appId) {
1228
+ const store2 = useWizard.getState();
1229
+ const record = state.steps[index];
1230
+ const startedAt = Date.now();
1231
+ trackActionStart({
1232
+ workflowId: state.workflowId,
1233
+ appId,
1234
+ actionId: step.id,
1235
+ actionTitle: step.title
1236
+ });
1237
+ record.status = "running";
1238
+ state.currentStepIndex = index;
1239
+ if (step.visible) {
1240
+ state.executedStepCount = (state.executedStepCount ?? 0) + 1;
1241
+ }
1242
+ state.updatedAt = nowIso();
1243
+ store2.setActiveStep(index);
1244
+ store2.syncSteps([...state.steps], index);
1245
+ await saveWorkflowState(state);
1246
+ const ctx = await makeContext(state);
1247
+ const raw = await step.run(ctx);
1248
+ const output = step.outputSchema.parse(raw);
1249
+ await step.onStepComplete?.(output, ctx);
1250
+ record.status = "done";
1251
+ record.output = output;
1252
+ state.updatedAt = nowIso();
1253
+ store2.syncSteps([...state.steps], index);
1254
+ await saveWorkflowState(state);
1255
+ trackActionEnd({
1256
+ workflowId: state.workflowId,
1257
+ appId,
1258
+ actionId: step.id,
1259
+ actionTitle: step.title,
1260
+ durationMs: Date.now() - startedAt
1261
+ });
1262
+ }
1263
+ async function runWorkflow(workflow2, appId) {
1264
+ const store2 = useWizard.getState();
1265
+ try {
1266
+ await ensureConsent();
1267
+ const persisted = await loadWorkflowState(workflow2.id);
1268
+ const state = (persisted && reconcileWorkflowState(persisted, workflow2)) ?? initWorkflowState(workflow2, nowIso());
1269
+ ensureExecutedStepCount(state);
1270
+ store2.startWorkflow(
1271
+ {
1272
+ id: workflow2.id,
1273
+ title: workflow2.title,
1274
+ description: workflow2.description
1275
+ },
1276
+ [...state.steps]
1277
+ );
1278
+ trackWorkflowStart({ workflowId: workflow2.id, appId });
1279
+ for (let i = state.currentStepIndex; i < workflow2.steps.length; i++) {
1280
+ await runStep(state, i, workflow2.steps[i], appId);
1281
+ }
1282
+ const totalDurationMs = Date.now() - Date.parse(state.startedAt);
1283
+ const stepCount = state.executedStepCount ?? state.steps.filter((s) => s.status === "done" && isStepVisible(s)).length;
1284
+ trackWizardComplete({
1285
+ workflowId: workflow2.id,
1286
+ appId,
1287
+ total_duration: Math.round(totalDurationMs / 1e3),
1288
+ total_steps: stepCount
1289
+ });
1290
+ track("AI Wizard Complete", {
1291
+ total_duration_ms: totalDurationMs,
1292
+ step_count: stepCount
1293
+ });
1294
+ await recordWorkflowRun(workflow2.id, nowIso());
1295
+ await clearWorkflowState(workflow2.id);
1296
+ store2.setDone();
1297
+ } catch (err) {
1298
+ const message = err instanceof Error ? err.message : String(err);
1299
+ const state = await loadWorkflowState(workflow2.id);
1300
+ let step = "unknown";
1301
+ let failedActionId;
1302
+ let failedActionTitle;
1303
+ if (state) {
1304
+ const record = state.steps[state.currentStepIndex];
1305
+ if (record) {
1306
+ step = record.title;
1307
+ if (record.status === "running") {
1308
+ record.status = "error";
1309
+ record.error = message;
1310
+ await saveWorkflowState(state);
1311
+ store2.syncSteps([...state.steps], state.currentStepIndex);
1312
+ }
1313
+ failedActionId = record.id;
1314
+ failedActionTitle = record.title;
1315
+ }
1316
+ }
1317
+ if (failedActionId && failedActionTitle) {
1318
+ trackActionError({
1319
+ workflowId: workflow2.id,
1320
+ appId,
1321
+ actionId: failedActionId,
1322
+ actionTitle: failedActionTitle,
1323
+ error: message
1324
+ });
1325
+ }
1326
+ trackWorkflowError({
1327
+ workflowId: workflow2.id,
1328
+ appId,
1329
+ error: message,
1330
+ actionId: failedActionId
1331
+ });
1332
+ track("Error", {
1333
+ step,
1334
+ error: message,
1335
+ product_area: "AI Wizard"
1336
+ });
1337
+ store2.setError(message);
1338
+ }
1339
+ }
1340
+
1341
+ // src/lib/algoliaProfile.ts
1342
+ import { readFile as readFile3 } from "node:fs/promises";
1343
+ import { createRequire as createRequire2 } from "node:module";
1344
+ import { homedir as homedir2 } from "node:os";
1345
+ import { join as join6 } from "node:path";
1346
+ import { parse as parseToml } from "toml";
1347
+ var require3 = createRequire2(import.meta.url);
1348
+ function configPath() {
1349
+ const base = process.env.XDG_CONFIG_HOME || join6(homedir2(), ".config");
1350
+ return join6(base, "algolia", "config.toml");
1351
+ }
1352
+ function profilesFromConfig(tomlText) {
1353
+ let parsed;
1354
+ try {
1355
+ parsed = parseToml(tomlText);
1356
+ } catch {
1357
+ return [];
1358
+ }
1359
+ const profiles = Object.entries(parsed).filter(
1360
+ ([, t]) => typeof t.application_id === "string" && typeof t.api_key === "string"
1361
+ ).map(([name, t]) => ({
1362
+ name,
1363
+ appId: t.application_id,
1364
+ apiKey: t.api_key,
1365
+ isDefault: t.default === true
1366
+ }));
1367
+ profiles.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
1368
+ return profiles.map(({ name, appId, apiKey }) => ({ name, appId, apiKey }));
1369
+ }
1370
+ async function loadActiveProfile() {
1371
+ let profiles;
1372
+ try {
1373
+ profiles = profilesFromConfig(await readFile3(configPath(), "utf8"));
1374
+ } catch {
1375
+ profiles = [];
1376
+ }
1377
+ const profile2 = profiles[0];
1378
+ if (!profile2) {
1379
+ throw new Error(
1380
+ "No Algolia profile is configured. Run `npx @algolia/cli auth login` to authenticate."
1381
+ );
1382
+ }
1383
+ return profile2;
1384
+ }
1385
+
1386
+ // src/workflows/default.ts
1387
+ import { z as z24 } from "zod";
1388
+
1389
+ // src/actions/listIndices.ts
1390
+ import { z as z3 } from "zod";
1391
+ var indicesListSchema = z3.object({
1392
+ items: z3.array(
1393
+ z3.object({
1394
+ name: z3.string(),
1395
+ entries: z3.number().default(0)
1396
+ })
1397
+ )
1398
+ });
1399
+ async function listIndices() {
1400
+ const stdout = await runAlgoliaCli(["indices", "list", "-o", "json"]);
1401
+ const { items } = indicesListSchema.parse(JSON.parse(stdout));
1402
+ return items.map((i) => ({ name: i.name, entries: i.entries })).sort((a, b) => a.name.localeCompare(b.name));
1403
+ }
1404
+
1405
+ // src/actions/selectIndex.ts
1406
+ var CREATE_NEW_INDEX = "Create a new index\u2026";
1407
+ var selectIndexStep = async (ctx) => {
1408
+ const indices = await listIndices();
1409
+ const names = indices.map((i) => i.name);
1410
+ const hasIndices = names.length > 0;
1411
+ let error;
1412
+ for (; ; ) {
1413
+ const selection = hasIndices ? await ctx.requestUserInput({
1414
+ prompt: "Which index do you want to ingest into?",
1415
+ promptType: "multipleChoice",
1416
+ options: [...names, CREATE_NEW_INDEX],
1417
+ messages: ["We found these indices:"],
1418
+ error
1419
+ }) : await ctx.requestUserInput({
1420
+ prompt: "Name the index to create and ingest into:",
1421
+ promptType: "textInput",
1422
+ options: [],
1423
+ messages: [
1424
+ "This app has no existing indices \u2014 enter a name to create one."
1425
+ ],
1426
+ error
1427
+ });
1428
+ if (typeof selection !== "string") {
1429
+ throw new Error("selectIndex received an unexpected non-text result");
1430
+ }
1431
+ let chosen;
1432
+ if (!hasIndices) {
1433
+ chosen = selection.trim();
1434
+ } else if (selection === CREATE_NEW_INDEX) {
1435
+ const name = await ctx.requestUserInput({
1436
+ prompt: "Name the new index:",
1437
+ promptType: "textInput",
1438
+ options: []
1439
+ });
1440
+ if (typeof name !== "string") {
1441
+ throw new Error("selectIndex received an unexpected non-text result");
1442
+ }
1443
+ chosen = name.trim();
1444
+ } else {
1445
+ chosen = selection;
1446
+ }
1447
+ if (!chosen) {
1448
+ error = "Index name cannot be empty.";
1449
+ continue;
1450
+ }
1451
+ ctx.setUserInput("index", chosen);
1452
+ return { selection: chosen };
1453
+ }
1454
+ };
1455
+
1456
+ // src/lib/agent.ts
1457
+ import { ToolLoopAgent, hasToolCall, Output as Output2 } from "ai";
1458
+ import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1459
+ import "zod";
1460
+
1461
+ // src/lib/tools/index.ts
1462
+ import "zod";
1463
+
1464
+ // src/lib/tools/listFiles.ts
1465
+ import { tool } from "ai";
1466
+ import z4 from "zod";
1467
+ import { readdir } from "node:fs/promises";
1468
+
1469
+ // src/lib/tools/path.ts
1470
+ import { lstat } from "node:fs/promises";
1471
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
1472
+ function resolveInRoot(ctx, path) {
1473
+ const target = resolve2(ctx.cwd, path);
1474
+ const rel = relative(ctx.root, target);
1475
+ if (rel.startsWith("..") || isAbsolute(rel)) {
1476
+ return {
1477
+ ok: false,
1478
+ error: `Refused: ${target} is outside the repo root (${ctx.root}).`
1479
+ };
1480
+ }
1481
+ return { ok: true, target };
1482
+ }
1483
+ async function hasSymlinkParent(ctx, target) {
1484
+ let current = ctx.root;
1485
+ const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
1486
+ for (const part of parts) {
1487
+ current = join7(current, part);
1488
+ try {
1489
+ if ((await lstat(current)).isSymbolicLink()) return true;
1490
+ } catch (err) {
1491
+ if (err.code === "ENOENT") return false;
1492
+ throw err;
1493
+ }
1494
+ }
1495
+ return false;
1496
+ }
1497
+
1498
+ // src/lib/tools/listFiles.ts
1499
+ function listFilesTool(ctx) {
1500
+ return tool({
1501
+ description: "List files in the current working directory",
1502
+ inputSchema: z4.object(),
1503
+ execute: async () => {
1504
+ logger.info("called listFiles tool");
1505
+ if (++ctx.counts.list > ctx.limits.list) {
1506
+ return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
1507
+ }
1508
+ const resolved = resolveInRoot(ctx, ".");
1509
+ if (!resolved.ok) return resolved.error;
1510
+ const entries = await readdir(resolved.target, { withFileTypes: true });
1511
+ return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
1512
+ }
1513
+ });
1514
+ }
1515
+
1516
+ // src/lib/tools/changeDirectory.ts
1517
+ import { tool as tool2 } from "ai";
1518
+ import z5 from "zod";
1519
+ import { stat } from "node:fs/promises";
1520
+ function changeDirectoryTool(ctx) {
1521
+ return tool2({
1522
+ description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
1523
+ inputSchema: z5.object({
1524
+ path: z5.string().describe("Directory to change into")
1525
+ }),
1526
+ execute: async ({ path }) => {
1527
+ logger.info({ path }, "called changeDirectory tool");
1528
+ const resolved = resolveInRoot(ctx, path);
1529
+ if (!resolved.ok) return resolved.error;
1530
+ try {
1531
+ const info = await stat(resolved.target);
1532
+ if (!info.isDirectory()) {
1533
+ return `Error changing directory to ${path}: not a directory`;
1534
+ }
1535
+ ctx.cwd = resolved.target;
1536
+ return `Changed working directory to ${ctx.cwd}`;
1537
+ } catch (err) {
1538
+ return `Error changing directory to ${path}: ${err.message}`;
1539
+ }
1540
+ }
1541
+ });
1542
+ }
1543
+
1544
+ // src/lib/tools/reportStatus.ts
1545
+ import { tool as tool3 } from "ai";
1546
+ import z6 from "zod";
1547
+ function reportStatusTool(output) {
1548
+ return tool3({
1549
+ description: "Report the status of your execution. Return a reason in case of failure.",
1550
+ inputSchema: z6.object({
1551
+ status: z6.enum(["success", "fail"]),
1552
+ reason: z6.string().optional(),
1553
+ output
1554
+ }),
1555
+ execute: async ({ status, reason, output: output2 }) => {
1556
+ logger.info({ status, reason }, "called reportStatus tool");
1557
+ return { status, reason, output: output2 };
1558
+ }
1559
+ });
1560
+ }
1561
+
1562
+ // src/lib/tools/readFile.ts
1563
+ import { tool as tool4 } from "ai";
1564
+ import z7 from "zod";
1565
+ import { readFile as readFile4 } from "node:fs/promises";
1566
+
1567
+ // src/lib/tools/env.ts
1568
+ import { basename } from "node:path";
1569
+ function isEnvFile(filePath) {
1570
+ const name = basename(filePath);
1571
+ return name === ".env" || name.startsWith(".env.");
1572
+ }
1573
+ function isSecretEnvFile(filePath) {
1574
+ if (!isEnvFile(filePath)) return false;
1575
+ const name = basename(filePath);
1576
+ return !/\.(example|sample|template)$/.test(name);
1577
+ }
1578
+
1579
+ // src/lib/tools/readFile.ts
1580
+ function redactEnvValues(content) {
1581
+ return content.split("\n").map((line) => {
1582
+ const match = line.match(/^(\s*(?:export\s+)?[\w.-]+\s*=)(.*)$/);
1583
+ if (!match) return line;
1584
+ const value = match[2].trim();
1585
+ if (value === "") return line;
1586
+ return `${match[1]}[REDACTED]`;
1587
+ }).join("\n");
1588
+ }
1589
+ function readFileTool(ctx) {
1590
+ return tool4({
1591
+ description: "Read the contents of a file at the given path",
1592
+ inputSchema: z7.object({
1593
+ filePath: z7.string().describe("Path to the file to read")
1594
+ }),
1595
+ execute: async ({ filePath }) => {
1596
+ if (++ctx.counts.read > ctx.limits.read) {
1597
+ return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
1598
+ }
1599
+ logger.info({ filePath }, "called readFile tool");
1600
+ const resolved = resolveInRoot(ctx, filePath);
1601
+ if (!resolved.ok) return resolved.error;
1602
+ try {
1603
+ const content = await readFile4(resolved.target, "utf8");
1604
+ return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
1605
+ } catch (err) {
1606
+ return `Error reading ${filePath}: ${err.message}`;
1607
+ }
1608
+ }
1609
+ });
1610
+ }
1611
+
1612
+ // src/lib/tools/writeFile.ts
1613
+ import { tool as tool5 } from "ai";
1614
+ import z8 from "zod";
1615
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
1616
+ import { dirname as dirname4 } from "node:path";
1617
+ function writeFileTool(ctx) {
1618
+ return tool5({
1619
+ description: "Write content to a file at the given path, overwriting it. To set Algolia credentials in an env file, use writeCredentials instead of this tool.",
1620
+ inputSchema: z8.object({
1621
+ filePath: z8.string().describe("Path to the file to write"),
1622
+ content: z8.string().describe("Content to write to the file")
1623
+ }),
1624
+ execute: async ({ filePath, content }) => {
1625
+ logger.info({ filePath }, "called writeFile tool");
1626
+ const resolved = resolveInRoot(ctx, filePath);
1627
+ if (resolved.ok === false) return resolved.error;
1628
+ if (isSecretEnvFile(resolved.target)) {
1629
+ return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
1630
+ }
1631
+ try {
1632
+ if (await hasSymlinkParent(ctx, resolved.target)) {
1633
+ return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
1634
+ }
1635
+ await mkdir3(dirname4(resolved.target), { recursive: true });
1636
+ await writeFile3(resolved.target, content, "utf8");
1637
+ return `Wrote to ${filePath}`;
1638
+ } catch (err) {
1639
+ return `Error writing ${filePath}: ${err.message}`;
1640
+ }
1641
+ }
1642
+ });
1643
+ }
1644
+
1645
+ // src/lib/tools/writeAlgoliaCredentials.ts
1646
+ import { tool as tool6 } from "ai";
1647
+ import z9 from "zod";
1648
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
1649
+ import { dirname as dirname5 } from "node:path";
1650
+ var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
1651
+ var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
1652
+ function appendEnv(content, entries) {
1653
+ const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
1654
+ const lines = entries.map(([name, value]) => `${name}=${value}
1655
+ `).join("");
1656
+ return content + prefix + lines;
1657
+ }
1658
+ function hasEnv(content, name) {
1659
+ return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
1660
+ }
1661
+ function writeCredentialsTool(ctx) {
1662
+ return tool6({
1663
+ description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials are read from the local Algolia CLI profile; you only pass the path to the env file (e.g. ".env"). If the file already defines ${APP_ID_VAR} or ${API_KEY_VAR}, the write is skipped and existing values are left untouched.`,
1664
+ inputSchema: z9.object({
1665
+ filePath: z9.string().describe(
1666
+ 'Path to the env file to write credentials into (e.g. ".env")'
1667
+ )
1668
+ }),
1669
+ execute: async ({ filePath }) => {
1670
+ logger.info({ filePath }, "called writeCredentials tool");
1671
+ const resolved = resolveInRoot(ctx, filePath);
1672
+ if (resolved.ok === false) return resolved.error;
1673
+ let profile2;
1674
+ try {
1675
+ profile2 = await loadActiveProfile();
1676
+ } catch {
1677
+ return "Error: no Algolia profile is configured, so credentials cannot be written. Ask the user to authenticate with the Algolia CLI first.";
1678
+ }
1679
+ try {
1680
+ if (await hasSymlinkParent(ctx, resolved.target)) {
1681
+ return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
1682
+ }
1683
+ let existing = "";
1684
+ try {
1685
+ existing = await readFile5(resolved.target, "utf8");
1686
+ } catch (err) {
1687
+ if (err.code !== "ENOENT") throw err;
1688
+ }
1689
+ const present = [APP_ID_VAR, API_KEY_VAR].filter(
1690
+ (name) => hasEnv(existing, name)
1691
+ );
1692
+ if (present.length > 0) {
1693
+ return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
1694
+ }
1695
+ const envWithCredentials = appendEnv(existing, [
1696
+ [APP_ID_VAR, profile2.appId],
1697
+ [API_KEY_VAR, profile2.apiKey]
1698
+ ]);
1699
+ await mkdir4(dirname5(resolved.target), { recursive: true });
1700
+ await writeFile4(resolved.target, envWithCredentials, "utf8");
1701
+ return `Wrote Algolia credentials to ${filePath}`;
1702
+ } catch (err) {
1703
+ return `Error writing credentials to ${filePath}: ${err.message}`;
1704
+ }
1705
+ }
1706
+ });
1707
+ }
1708
+
1709
+ // src/lib/tools/searchFiles.ts
1710
+ import { tool as tool7 } from "ai";
1711
+ import z10 from "zod";
1712
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
1713
+ import { join as join8 } from "node:path";
1714
+ var MAX_QUERY_LENGTH = 1e3;
1715
+ async function walkFiles(dir) {
1716
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1717
+ const out = [];
1718
+ for (const e of await readdir2(dir, { withFileTypes: true })) {
1719
+ if (e.name.startsWith(".") || skip.has(e.name)) continue;
1720
+ const full = join8(dir, e.name);
1721
+ if (e.isDirectory()) out.push(...await walkFiles(full));
1722
+ else if (e.isFile()) out.push(full);
1723
+ }
1724
+ return out;
1725
+ }
1726
+ function searchFilesTool(ctx) {
1727
+ return tool7({
1728
+ description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
1729
+ inputSchema: z10.object({
1730
+ query: z10.string().describe("JavaScript RegExp pattern to search for"),
1731
+ path: z10.string().optional().describe("Directory to search in (default: cwd)")
1732
+ }),
1733
+ execute: async ({ query, path = "." }) => {
1734
+ logger.info({ query, path }, "called searchFiles tool");
1735
+ if (++ctx.counts.search > ctx.limits.search) {
1736
+ return `Refused: search limit (${ctx.limits.search}) reached. Stop searching and proceed with the information you already have.`;
1737
+ }
1738
+ if (query.length > MAX_QUERY_LENGTH) {
1739
+ return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
1740
+ }
1741
+ const resolved = resolveInRoot(ctx, path);
1742
+ if (!resolved.ok) return resolved.error;
1743
+ let re;
1744
+ try {
1745
+ re = new RegExp(query);
1746
+ } catch (err) {
1747
+ return `Invalid regex: ${err.message}`;
1748
+ }
1749
+ const matches = [];
1750
+ for (const file of await walkFiles(resolved.target)) {
1751
+ let content;
1752
+ try {
1753
+ content = await readFile6(file, "utf8");
1754
+ } catch {
1755
+ continue;
1756
+ }
1757
+ const lines = content.split("\n");
1758
+ for (let i = 0; i < lines.length; i++) {
1759
+ if (re.test(lines[i])) {
1760
+ matches.push(`${file}:${i + 1}:${lines[i]}`);
1761
+ if (matches.length >= ctx.limits.match) {
1762
+ matches.push(`... (truncated at ${ctx.limits.match} matches)`);
1763
+ return matches.join("\n");
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ return matches.length ? matches.join("\n") : "No matches";
1769
+ }
1770
+ });
1771
+ }
1772
+
1773
+ // src/lib/tools/verifyImplementation.ts
1774
+ import { tool as tool8 } from "ai";
1775
+ import z11 from "zod";
1776
+
1777
+ // src/lib/tools/utils/runCommand.ts
1778
+ import { spawn as spawn2 } from "node:child_process";
1779
+ function runCommand(command, args, cwd) {
1780
+ return new Promise((resolve4) => {
1781
+ let output = "";
1782
+ const child = spawn2(command, args, {
1783
+ cwd,
1784
+ stdio: ["ignore", "pipe", "pipe"]
1785
+ });
1786
+ child.stdout?.on("data", (d) => output += d);
1787
+ child.stderr?.on("data", (d) => output += d);
1788
+ child.on(
1789
+ "error",
1790
+ (err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
1791
+ );
1792
+ child.on("close", (code) => resolve4({ code: code ?? 1, output }));
1793
+ });
1794
+ }
1795
+
1796
+ // src/lib/tools/utils/packageManager.ts
1797
+ import { readFile as readFile7 } from "node:fs/promises";
1798
+ import { existsSync } from "node:fs";
1799
+ import { join as join9 } from "node:path";
1800
+ var LOCKFILES = [
1801
+ ["pnpm-lock.yaml", "pnpm"],
1802
+ ["yarn.lock", "yarn"],
1803
+ ["bun.lockb", "bun"],
1804
+ ["bun.lock", "bun"],
1805
+ ["package-lock.json", "npm"]
1806
+ ];
1807
+ async function readPackageJson(cwd = process.cwd()) {
1808
+ return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
1809
+ }
1810
+ function packageManagerFrom(pkg) {
1811
+ return pkg.packageManager?.split("@")[0] ?? "npm";
1812
+ }
1813
+ function packageManagerFromLockfile(cwd) {
1814
+ return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
1815
+ }
1816
+ async function detectPackageManager(cwd) {
1817
+ try {
1818
+ const pkg = await readPackageJson(cwd);
1819
+ if (pkg.packageManager) return packageManagerFrom(pkg);
1820
+ } catch {
1821
+ }
1822
+ return packageManagerFromLockfile(cwd) ?? "npm";
1823
+ }
1824
+
1825
+ // src/lib/tools/repoVerification.ts
1826
+ var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
1827
+ async function runRepoVerificationCheck() {
1828
+ let pkg;
1829
+ try {
1830
+ pkg = await readPackageJson();
1831
+ } catch (err) {
1832
+ const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
1833
+ return { ok: false, checks: [], limitation };
1834
+ }
1835
+ const scripts = pkg.scripts ?? {};
1836
+ const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
1837
+ if (present.length === 0) {
1838
+ const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
1839
+ return { ok: false, checks: [], limitation };
1840
+ }
1841
+ const pm = await detectPackageManager(process.cwd());
1842
+ const checks = [];
1843
+ for (const script of present) {
1844
+ const command = `${pm} run ${script}`;
1845
+ const { code, output } = await runCommand(pm, ["run", script]);
1846
+ checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
1847
+ }
1848
+ return { ok: checks.every((c) => c.ok), checks };
1849
+ }
1850
+
1851
+ // src/lib/tools/verifyImplementation.ts
1852
+ function verifyImplementationTool() {
1853
+ return tool8({
1854
+ description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
1855
+ inputSchema: z11.object(),
1856
+ execute: async () => {
1857
+ logger.info("called verifyImplementation tool");
1858
+ return runRepoVerificationCheck();
1859
+ }
1860
+ });
1861
+ }
1862
+
1863
+ // src/lib/tools/generateRecord.ts
1864
+ import { tool as tool9, generateText, Output, NoObjectGeneratedError } from "ai";
1865
+ import { createAnthropic } from "@ai-sdk/anthropic";
1866
+ import { nanoid } from "nanoid";
1867
+ import z12 from "zod";
1868
+ var RECORD_MODEL = "claude-haiku-4-5";
1869
+ var MAX_RECORDS = 100;
1870
+ var BATCH_SIZE = 10;
1871
+ var MAX_BATCH_ATTEMPTS = 3;
1872
+ var anthropic = createAnthropic({
1873
+ apiKey: process.env.PROVIDER_API_KEY ?? ""
1874
+ });
1875
+ function generateRecordTool() {
1876
+ 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.",
1878
+ inputSchema: z12.object({
1879
+ entityName: z12.string().describe("Name of the entity to generate records for."),
1880
+ attributes: z12.array(z12.string()).describe("Attribute names each record must contain."),
1881
+ count: z12.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
1882
+ hint: z12.string().optional().describe("Optional context to steer realistic values.")
1883
+ }),
1884
+ execute: async ({ entityName, attributes, count, hint }) => {
1885
+ logger.info({ entityName, count }, "called generateRecord tool");
1886
+ try {
1887
+ const value = z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]);
1888
+ const recordSchema = z12.object(
1889
+ Object.fromEntries(attributes.map((attr) => [attr, value]))
1890
+ );
1891
+ const generateBatch = async (batchCount) => {
1892
+ let lastError;
1893
+ for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt++) {
1894
+ try {
1895
+ const { output } = await generateText({
1896
+ model: anthropic(RECORD_MODEL),
1897
+ output: Output.object({
1898
+ schema: z12.object({
1899
+ records: z12.array(recordSchema).length(batchCount)
1900
+ })
1901
+ }),
1902
+ prompt: [
1903
+ `Generate ${batchCount} realistic, varied sample records for the "${entityName}" entity.`,
1904
+ hint ? `Context: ${hint}` : "",
1905
+ // Batches run concurrently with identical prompts, so each call
1906
+ // would otherwise drift to the same high-probability values and
1907
+ // collide across batches. This per-batch seed pushes each call
1908
+ // into a different region of the output space.
1909
+ `Variety seed: ${nanoid()}. Use it to diversify values.`
1910
+ ].filter(Boolean).join("\n")
1911
+ });
1912
+ return output.records;
1913
+ } catch (err) {
1914
+ if (!NoObjectGeneratedError.isInstance(err)) throw err;
1915
+ lastError = err;
1916
+ logger.warn(
1917
+ { entityName, batchCount, attempt, err },
1918
+ "generateRecord batch failed schema validation, retrying"
1919
+ );
1920
+ }
1921
+ }
1922
+ throw lastError;
1923
+ };
1924
+ const batchSizes = [];
1925
+ for (let remaining = count; remaining > 0; remaining -= BATCH_SIZE) {
1926
+ batchSizes.push(Math.min(BATCH_SIZE, remaining));
1927
+ }
1928
+ const batches = await Promise.all(batchSizes.map(generateBatch));
1929
+ const records = batches.flat().map((record) => ({
1930
+ ...record,
1931
+ objectID: nanoid()
1932
+ }));
1933
+ logger.info(records);
1934
+ return { records };
1935
+ } catch (err) {
1936
+ return `Error generating records: ${err.message}`;
1937
+ }
1938
+ }
1939
+ });
1940
+ }
1941
+
1942
+ // src/lib/tools/context.ts
1943
+ var DEFAULT_TOOL_LIMITS = {
1944
+ list: 10,
1945
+ search: 10,
1946
+ read: 20,
1947
+ match: 100
1948
+ };
1949
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
1950
+ return {
1951
+ root: cwd,
1952
+ cwd,
1953
+ limits,
1954
+ counts: { list: 0, search: 0, read: 0 }
1955
+ };
1956
+ }
1957
+
1958
+ // src/lib/tools/index.ts
1959
+ function createTools(ctx, { output, tools }) {
1960
+ 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()
1970
+ };
1971
+ if (!tools) return all;
1972
+ const selection = /* @__PURE__ */ new Set([...tools, "reportStatus"]);
1973
+ return Object.fromEntries(
1974
+ Object.entries(all).filter(([name]) => selection.has(name))
1975
+ );
1976
+ }
1977
+ var FS_READ_TOOLS = [
1978
+ "listFiles",
1979
+ "changeDirectory",
1980
+ "readFile",
1981
+ "searchFiles"
1982
+ ];
1983
+
1984
+ // src/lib/agent.ts
1985
+ var MODEL_BY_SIZE = {
1986
+ small: "claude-haiku-4-5",
1987
+ medium: "claude-sonnet-4-6",
1988
+ large: "claude-opus-4-8"
1989
+ };
1990
+ async function runAgent(req) {
1991
+ const start = Date.now();
1992
+ logger.info({ startedAt: new Date(start).toISOString() }, "runAgent started");
1993
+ const token = getAuthToken();
1994
+ if (!token) {
1995
+ throw new Error("Not authenticated: no user token available");
1996
+ }
1997
+ const anthropic2 = createAnthropic2({
1998
+ apiKey: token,
1999
+ baseURL: PROXY_BASE_URL,
2000
+ fetch: proxyFetch
2001
+ });
2002
+ const toolContext = createToolContext();
2003
+ const readTools = ["readFile", "searchFiles", "listFiles"];
2004
+ const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2005
+ const instructions = hasReadTools ? [
2006
+ ...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;
2009
+ const agent = new ToolLoopAgent({
2010
+ model: anthropic2(MODEL_BY_SIZE[req.modelSize ?? "medium"]),
2011
+ // Cache tools + system on the last system block. Tools render before
2012
+ // system, so one breakpoint here caches both, reused on every loop turn
2013
+ // after the first.
2014
+ instructions: instructions?.map((i, idx, arr) => {
2015
+ return {
2016
+ role: "system",
2017
+ content: i,
2018
+ ...idx === arr.length - 1 && {
2019
+ providerOptions: {
2020
+ anthropic: { cacheControl: { type: "ephemeral" } }
2021
+ }
2022
+ }
2023
+ };
2024
+ }),
2025
+ output: Output2.object({ schema: req.outputSchema }),
2026
+ tools: createTools(toolContext, {
2027
+ output: req.outputSchema,
2028
+ tools: req.tools
2029
+ }),
2030
+ toolChoice: "required",
2031
+ stopWhen: [hasToolCall("reportStatus")]
2032
+ });
2033
+ const stream = await agent.stream({
2034
+ prompt: "Follow system instructions"
2035
+ });
2036
+ let chunks = [];
2037
+ const chunkLimit = 5;
2038
+ for await (const chunk of stream.textStream) {
2039
+ if (chunks.length < chunkLimit) {
2040
+ chunks.push(chunk);
2041
+ continue;
2042
+ }
2043
+ chunks.push(chunk);
2044
+ logger.debug(chunks.join(""));
2045
+ chunks = [];
2046
+ }
2047
+ if (chunks.length) {
2048
+ logger.debug(chunks.join(""));
2049
+ }
2050
+ const end = Date.now();
2051
+ const usage = await stream.totalUsage;
2052
+ logger.info(
2053
+ {
2054
+ finishedAt: new Date(end).toISOString(),
2055
+ durationMs: end - start,
2056
+ // cachedInputTokens > 0 confirms prompt caching engaged. If it stays 0
2057
+ // across turns, the tools+system prefix is under the model's min
2058
+ // cacheable size (2048 tokens for sonnet-4-6) and caching is a no-op.
2059
+ usage
2060
+ },
2061
+ "runAgent finished"
2062
+ );
2063
+ logger.info(
2064
+ { counts: toolContext.counts, limits: toolContext.limits },
2065
+ "tool usage"
2066
+ );
2067
+ const toolResults = await stream.toolResults;
2068
+ const report = [...toolResults].reverse().find((r) => r.toolName === "reportStatus");
2069
+ if (!report) {
2070
+ throw new Error("Agent finished without calling reportStatus");
2071
+ }
2072
+ const result = report.output;
2073
+ if (result.status !== "success") {
2074
+ throw new Error(
2075
+ `Agent reported failure: ${result.reason ?? "no reason given"}`
2076
+ );
2077
+ }
2078
+ return result.output;
2079
+ }
2080
+
2081
+ // 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() }))
2086
+ });
2087
+ var detectLanguage = () => runAgent({
2088
+ instructions: [
2089
+ "Analyze the codebase and determine the programming languages and frameworks used",
2090
+ "If a superset language is found, exclude the subset language. TS-over-JS.",
2091
+ "If a meta-framework is used, exclude the framework. Next-over-React.",
2092
+ "Return the exact version",
2093
+ "Exclude things like CSS frameworks, build tools, or testing frameworks",
2094
+ 'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
2095
+ "When done, call reportStatus"
2096
+ ],
2097
+ tools: ["listFiles", "changeDirectory", "readFile", "searchFiles"],
2098
+ outputSchema: detectLanguageSchema,
2099
+ modelSize: "small"
2100
+ });
2101
+
2102
+ // src/actions/analyzeCodebase.ts
2103
+ import z16 from "zod";
2104
+ var READONLY_TOOLS = [
2105
+ "listFiles",
2106
+ "changeDirectory",
2107
+ "readFile",
2108
+ "searchFiles"
2109
+ ];
2110
+ var ingestionAnalysisSchema = z16.object({
2111
+ ingestionAnalysis: z16.array(
2112
+ z16.object({
2113
+ name: z16.string(),
2114
+ paths: z16.array(z16.string()),
2115
+ // indexable fields the agent found for this entity
2116
+ attributes: z16.array(z16.string())
2117
+ })
2118
+ )
2119
+ });
2120
+ var searchImplementationAnalysisSchema = z16.object({
2121
+ searchImplementationAnalysis: z16.string()
2122
+ });
2123
+ var verificationSchema = z16.object({
2124
+ verification: z16.array(z16.string())
2125
+ });
2126
+ var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
2127
+ var analyzeCodebaseSchema = z16.object({
2128
+ ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2129
+ searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
2130
+ verification: verificationSchema.shape.verification.optional(),
2131
+ confirmedEntities: confirmedEntitiesFieldSchema
2132
+ });
2133
+ var toEntitySummary = (entities) => entities.map((e) => ({ name: e.name, attributes: e.attributes }));
2134
+ var MODE_CONFIG = {
2135
+ ingestion: {
2136
+ instructions: [
2137
+ "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
2138
+ "For each entity, return its name, the file path(s) where it is defined, and its indexable attribute keys (the fields a user would search or filter on).",
2139
+ "Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
2140
+ "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
2141
+ "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
2142
+ "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2143
+ "When done, call reportStatus"
2144
+ ],
2145
+ outputSchema: ingestionAnalysisSchema
2146
+ },
2147
+ searchImplementation: {
2148
+ instructions: [
2149
+ "Analyze the codebase to determine the single best location to add search UI functionality.",
2150
+ "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
2151
+ "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
2152
+ 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
2153
+ "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2154
+ "When done, call reportStatus"
2155
+ ],
2156
+ outputSchema: searchImplementationAnalysisSchema
2157
+ },
2158
+ verification: {
2159
+ instructions: [
2160
+ "Analyze the codebase to determine which code-quality tools are available to validate changes.",
2161
+ "Look at package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
2162
+ 'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
2163
+ "Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
2164
+ "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2165
+ "When done, call reportStatus"
2166
+ ],
2167
+ outputSchema: verificationSchema
2168
+ }
2169
+ };
2170
+ function runMode(mode, extraInstructions = []) {
2171
+ const { instructions, outputSchema } = MODE_CONFIG[mode];
2172
+ return runAgent({
2173
+ instructions: [...instructions, ...extraInstructions],
2174
+ tools: READONLY_TOOLS,
2175
+ outputSchema
2176
+ });
2177
+ }
2178
+ async function runAnalysis(mode, extraInstructions = []) {
2179
+ const modes = mode ? [mode] : ["ingestion", "searchImplementation", "verification"];
2180
+ const results = await Promise.all(
2181
+ modes.map((m) => runMode(m, extraInstructions))
2182
+ );
2183
+ return Object.assign({}, ...results);
2184
+ }
2185
+
2186
+ // package.json
2187
+ var package_default = {
2188
+ name: "@algolia/wizard",
2189
+ version: "0.1.0",
2190
+ description: "Magically implement Algolia functionality in your codebase",
2191
+ type: "module",
2192
+ engines: {
2193
+ node: ">=24"
2194
+ },
2195
+ bin: {
2196
+ wizard: "dist/main.js"
2197
+ },
2198
+ files: [
2199
+ "dist",
2200
+ "docs"
2201
+ ],
2202
+ 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",
2206
+ dev: "touch .env && tsx --env-file=.env ./src/main.tsx",
2207
+ "env:load": "pnpm exec -- varlock load",
2208
+ prepare: "husky",
2209
+ prepublishOnly: "pnpm build",
2210
+ reset: "tsx ./scripts/reset-state.ts",
2211
+ "test:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
2212
+ "test:tools": "tsx ./tool-evals/toolEval.ts",
2213
+ test: "vitest",
2214
+ typecheck: "tsc --noEmit -p tsconfig.json"
2215
+ },
2216
+ keywords: [],
2217
+ author: "",
2218
+ license: "ISC",
2219
+ packageManager: "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
2220
+ devDependencies: {
2221
+ "@clack/prompts": "^1.6.0",
2222
+ "@types/ioredis-mock": "^8.2.7",
2223
+ "@types/node": "^25.9.1",
2224
+ "@types/react": "^19.2.16",
2225
+ esbuild: "^0.28.0",
2226
+ husky: "^9.1.7",
2227
+ "ink-testing-library": "^4.0.0",
2228
+ "ioredis-mock": "^8.13.1",
2229
+ "lint-staged": "^17.0.8",
2230
+ tsx: "^4.22.3",
2231
+ typescript: "^6.0.3",
2232
+ vitest: "^4.1.8"
2233
+ },
2234
+ dependencies: {
2235
+ "@ai-sdk/anthropic": "^3.0.81",
2236
+ "@ai-sdk/openai-compatible": "^2.0.47",
2237
+ "@algolia/cli": "^5.11.0",
2238
+ "@hono/node-server": "^2.0.10",
2239
+ "@mishieck/ink-titled-box": "^0.4.2",
2240
+ "@segment/analytics-node": "^3.1.0",
2241
+ ai: "^6.0.190",
2242
+ dotenv: "^17.4.2",
2243
+ hono: "^4.12.27",
2244
+ ink: "^7.0.5",
2245
+ "ink-picture": "^2.1.0",
2246
+ "ink-spinner": "^5.0.0",
2247
+ "ink-text-input": "^6.0.0",
2248
+ ioredis: "^5.11.1",
2249
+ nanoid: "^5.1.15",
2250
+ pino: "^10.3.1",
2251
+ react: "^19.2.7",
2252
+ toml: "^4.1.1",
2253
+ varlock: "^1.5.1",
2254
+ zod: "^4.4.3",
2255
+ zustand: "^5.0.14"
2256
+ }
2257
+ };
2258
+
2259
+ // src/actions/projectScan.ts
2260
+ import "zod";
2261
+ var projectScanSchema = detectLanguageSchema.extend({
2262
+ ingestionAnalysis: analyzeCodebaseSchema.shape.ingestionAnalysis,
2263
+ searchImplementationAnalysis: analyzeCodebaseSchema.shape.searchImplementationAnalysis,
2264
+ verification: analyzeCodebaseSchema.shape.verification
2265
+ });
2266
+ async function projectScan(ctx) {
2267
+ const [detected, analyses] = await Promise.all([
2268
+ detectLanguage(),
2269
+ runAnalysis()
2270
+ ]);
2271
+ track("AI Wizard Started", {
2272
+ wizard_version: package_default.version,
2273
+ os: process.platform
2274
+ });
2275
+ track("AI Wizard Scan Completed", {
2276
+ entities: toEntitySummary(analyses.ingestionAnalysis ?? [])
2277
+ });
2278
+ for (const [key, value] of Object.entries(analyses)) {
2279
+ ctx.setUserInput(key, value);
2280
+ }
2281
+ return { ...detected, ...analyses };
2282
+ }
2283
+
2284
+ // src/actions/confirmStack.ts
2285
+ var MAX_FIELD_LEN = 80;
2286
+ var MAX_ENTRIES = 20;
2287
+ var clean = (s) => s.replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_FIELD_LEN);
2288
+ function parseEntries(raw) {
2289
+ return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
2290
+ }
2291
+ var summarize = (entries) => entries.length ? entries.map((e) => e.name).join(", ") : "none";
2292
+ async function askList(ctx, prompt, { required = false } = {}) {
2293
+ for (; ; ) {
2294
+ const answer = await ctx.requestUserInput({
2295
+ prompt,
2296
+ promptType: "textInput",
2297
+ options: [],
2298
+ helpText: 'Comma-separated, e.g. "TypeScript, Node".'
2299
+ });
2300
+ if (typeof answer !== "string") {
2301
+ throw new Error("askList received an unexpected non-text result");
2302
+ }
2303
+ const entries = parseEntries(answer);
2304
+ if (entries.length || !required) return entries;
2305
+ prompt = "Please enter at least one entry:";
2306
+ }
2307
+ }
2308
+
2309
+ // src/actions/confirmLanguage.ts
2310
+ import z18 from "zod";
2311
+ var confirmLanguageSchema = z18.object({
2312
+ languages: detectLanguageSchema.shape.languages
2313
+ });
2314
+ async function confirmLanguage(ctx) {
2315
+ const detected = ctx.getStepOutput("project-scan");
2316
+ const answer = await ctx.requestUserInput({
2317
+ prompt: "Did we detect your language(s) correctly?",
2318
+ promptType: "acceptReject",
2319
+ options: ["Yes", "No"],
2320
+ messages: [`Languages: ${summarize(detected.languages)}`]
2321
+ });
2322
+ const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
2323
+ required: true
2324
+ });
2325
+ track("AI Wizard Language Confirmed", {
2326
+ languages
2327
+ });
2328
+ return { languages };
2329
+ }
2330
+
2331
+ // src/actions/confirmFramework.ts
2332
+ import z19 from "zod";
2333
+ var confirmFrameworkSchema = z19.object({
2334
+ frameworks: detectLanguageSchema.shape.frameworks
2335
+ });
2336
+ var CURATED_FRAMEWORKS = [
2337
+ "Next.js",
2338
+ "React",
2339
+ "Vue",
2340
+ "Angular",
2341
+ "Svelte",
2342
+ "Vanilla JS"
2343
+ ];
2344
+ var OTHER_OPTION = "Other";
2345
+ var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2346
+ var FRAMEWORK_ALIASES = {
2347
+ next: "nextjs",
2348
+ nextjs: "nextjs",
2349
+ react: "react",
2350
+ reactjs: "react",
2351
+ vue: "vue",
2352
+ vuejs: "vue",
2353
+ angular: "angular",
2354
+ angularjs: "angular",
2355
+ svelte: "svelte",
2356
+ sveltekit: "svelte",
2357
+ vanillajs: "vanillajs",
2358
+ vanilla: "vanillajs",
2359
+ javascript: "vanillajs",
2360
+ js: "vanillajs"
2361
+ };
2362
+ var isSameFramework = (a, b) => {
2363
+ const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
2364
+ const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
2365
+ return x !== "" && x === y;
2366
+ };
2367
+ function confirmed(name, version) {
2368
+ const frameworks = [{ name, version: version ?? "unknown" }];
2369
+ track("AI Wizard Frontend Framework Confirmed", { frameworks });
2370
+ return { frameworks };
2371
+ }
2372
+ async function askOtherFramework(ctx) {
2373
+ let prompt = "enter your framework";
2374
+ for (; ; ) {
2375
+ const answer = await ctx.requestUserInput({
2376
+ prompt,
2377
+ promptType: "textInput",
2378
+ options: []
2379
+ });
2380
+ if (typeof answer !== "string") {
2381
+ throw new Error("confirmFramework received an unexpected non-text result");
2382
+ }
2383
+ const name = parseEntries(answer)[0]?.name;
2384
+ if (name) return name;
2385
+ prompt = "please enter a framework name:";
2386
+ }
2387
+ }
2388
+ async function confirmFramework(ctx) {
2389
+ const detected = ctx.getStepOutput("project-scan");
2390
+ const detectedFrameworks = detected.frameworks ?? [];
2391
+ const options = [...CURATED_FRAMEWORKS];
2392
+ for (const fw of detectedFrameworks) {
2393
+ if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
2394
+ }
2395
+ options.push(OTHER_OPTION);
2396
+ const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
2397
+ const primary = detectedFrameworks[0];
2398
+ if (primary) {
2399
+ const accepted = await ctx.requestUserInput({
2400
+ prompt: `Use ${primary.name}?`,
2401
+ promptType: "acceptReject",
2402
+ options: [`Confirm ${primary.name}`, "Use a different framework"],
2403
+ secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
2404
+ });
2405
+ if (accepted === true) return confirmed(primary.name, primary.version);
2406
+ }
2407
+ const secondary = options.map(
2408
+ (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
2409
+ );
2410
+ const defaultSelectedIndex = Math.max(
2411
+ options.findIndex((o) => detectedFor(o)),
2412
+ 0
2413
+ );
2414
+ const selection = await ctx.requestUserInput({
2415
+ prompt: "select a framework",
2416
+ promptType: "multipleChoice",
2417
+ options,
2418
+ secondary,
2419
+ defaultSelectedIndex
2420
+ });
2421
+ if (typeof selection !== "string") {
2422
+ throw new Error("confirmFramework received an unexpected non-text result");
2423
+ }
2424
+ if (selection === OTHER_OPTION) {
2425
+ return confirmed(await askOtherFramework(ctx));
2426
+ }
2427
+ return confirmed(selection, detectedFor(selection)?.version);
2428
+ }
2429
+
2430
+ // src/actions/promptUser.ts
2431
+ async function promptUser(ctx, params) {
2432
+ const {
2433
+ workflowStateKey,
2434
+ promptType,
2435
+ promptContent,
2436
+ table,
2437
+ secondary,
2438
+ cancelable,
2439
+ onSubmit
2440
+ } = params;
2441
+ const question = promptContent.find((c) => c.type === "question")?.value ?? "";
2442
+ const helpText = promptContent.find((c) => c.type === "helpText")?.value ?? "";
2443
+ const messages = promptContent.filter((c) => c.type === "message").map((c) => c.value);
2444
+ const options = promptContent.filter((c) => c.type === "option").map((c) => c.value);
2445
+ const selection = await ctx.requestUserInput({
2446
+ prompt: question,
2447
+ promptType,
2448
+ options,
2449
+ messages,
2450
+ table,
2451
+ secondary,
2452
+ cancelable,
2453
+ helpText
2454
+ });
2455
+ ctx.setUserInput(workflowStateKey, selection);
2456
+ onSubmit();
2457
+ return selection;
2458
+ }
2459
+
2460
+ // src/actions/confirmEntities.ts
2461
+ import z20 from "zod";
2462
+ var confirmEntitiesSchema = z20.object({
2463
+ // Final detection — the focused re-run may supersede project-scan's.
2464
+ ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
2465
+ confirmedEntities: confirmedEntitiesFieldSchema
2466
+ });
2467
+ async function confirmEntities(ctx) {
2468
+ const scan = ctx.getStepOutput("project-scan");
2469
+ let entities = scan.ingestionAnalysis;
2470
+ if (!entities?.length) {
2471
+ const declared = await promptUser(ctx, {
2472
+ workflowStateKey: "confirmedEntities",
2473
+ promptType: "textInput",
2474
+ promptContent: [
2475
+ {
2476
+ type: "message",
2477
+ value: "We couldn't detect any entities to ingest in this codebase."
2478
+ },
2479
+ {
2480
+ type: "question",
2481
+ value: "Name an entity to search for (leave blank to cancel):"
2482
+ }
2483
+ ],
2484
+ onSubmit: () => {
2485
+ }
2486
+ });
2487
+ if (typeof declared !== "string" || declared.trim() === "") {
2488
+ throw new Error("User declined to specify an entity \u2014 analysis halted.");
2489
+ }
2490
+ const declaredName = declared.trim();
2491
+ ctx.setUserInput("confirmedEntities", [declaredName]);
2492
+ const rerun = await runAnalysis("ingestion", [
2493
+ `The user has confirmed the codebase contains an entity named "${declaredName}". Focus your search on locating this entity, the file path(s) where it is defined, and its indexable attribute keys.`
2494
+ ]);
2495
+ entities = rerun.ingestionAnalysis;
2496
+ ctx.setUserInput("ingestionAnalysis", entities);
2497
+ if (!entities?.length) {
2498
+ throw new Error(
2499
+ `Could not locate the "${declaredName}" entity in this codebase \u2014 analysis halted.`
2500
+ );
2501
+ }
2502
+ }
2503
+ const selection = await promptUser(ctx, {
2504
+ workflowStateKey: "confirmedEntities",
2505
+ promptType: "multipleChoice",
2506
+ // A Cancel row lets the user halt here (the single-select equivalent of the
2507
+ // old multi-select Cancel); its 'cancel' sentinel matches no entity below.
2508
+ cancelable: true,
2509
+ secondary: entities.map((e) => ({
2510
+ kind: "text",
2511
+ value: e.attributes.join(", ") || "\u2014"
2512
+ })),
2513
+ promptContent: [
2514
+ { type: "question", value: "Pick the entity to index" },
2515
+ ...entities.map((e) => ({ type: "option", value: e.name }))
2516
+ ],
2517
+ onSubmit: () => {
2518
+ }
2519
+ });
2520
+ const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
2521
+ if (confirmed2.length === 0) {
2522
+ throw new Error("User cancelled entity selection \u2014 analysis halted.");
2523
+ }
2524
+ ctx.setUserInput("confirmedEntities", confirmed2);
2525
+ track("AI Wizard Entities Confirmed", {
2526
+ entities: toEntitySummary(confirmed2)
2527
+ });
2528
+ return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
2529
+ }
2530
+
2531
+ // 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())
2543
+ });
2544
+ function formatCompletedSteps(steps) {
2545
+ if (!steps.length) return "(no prior steps completed)";
2546
+ return steps.map(
2547
+ (s) => `### ${s.title} (${s.id})
2548
+ Output:
2549
+ ${JSON.stringify(s.output, null, 2)}`
2550
+ ).join("\n\n");
2551
+ }
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:
2561
+ ${formatCompletedSteps(ctx.completedSteps)}`,
2562
+ "When done, call reportStatus"
2563
+ ],
2564
+ tools: [],
2565
+ outputSchema: reviewSchema,
2566
+ modelSize: "small"
2567
+ });
2568
+
2569
+ // src/actions/implement.ts
2570
+ import z23 from "zod";
2571
+
2572
+ // src/lib/worktree.ts
2573
+ 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";
2575
+ import {
2576
+ basename as basename2,
2577
+ dirname as dirname6,
2578
+ isAbsolute as isAbsolute2,
2579
+ join as join10,
2580
+ relative as relative2,
2581
+ resolve as resolve3
2582
+ } from "node:path";
2583
+ var MAX_BUFFER = 32 * 1024 * 1024;
2584
+ var MAX_WIZARD_WORKTREES = 3;
2585
+ var WIZARD_BRANCH_PREFIX = "wizard/implement-";
2586
+ function git(args) {
2587
+ return new Promise((resolve4, reject) => {
2588
+ execFile("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
2589
+ if (err)
2590
+ return reject(
2591
+ new Error(
2592
+ `git ${args.join(" ")} failed: ${stderr.toString().trim() || err.message}`
2593
+ )
2594
+ );
2595
+ resolve4(stdout.toString());
2596
+ });
2597
+ });
2598
+ }
2599
+ async function assertGitRepoWithHead(repoRoot) {
2600
+ try {
2601
+ await git(["-C", repoRoot, "rev-parse", "HEAD"]);
2602
+ } catch {
2603
+ throw new Error(
2604
+ "implement requires a git repository with at least one commit. Initialize git and commit before running the wizard."
2605
+ );
2606
+ }
2607
+ }
2608
+ async function isWorkingTreeDirty(repoRoot) {
2609
+ const out = await git(["-C", repoRoot, "status", "--porcelain"]);
2610
+ return out.trim().length > 0;
2611
+ }
2612
+ async function pruneOldWorktrees(repoRoot) {
2613
+ const dir = join10(stateDir(repoRoot), "worktrees");
2614
+ const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
2615
+ for (const slug of stale) {
2616
+ const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
2617
+ try {
2618
+ await git([
2619
+ "-C",
2620
+ repoRoot,
2621
+ "worktree",
2622
+ "remove",
2623
+ "--force",
2624
+ join10(dir, slug)
2625
+ ]);
2626
+ await git(["-C", repoRoot, "branch", "-D", branch]);
2627
+ } catch (err) {
2628
+ logger.warn(
2629
+ { branch, err: err.message },
2630
+ "createWorktree: failed to prune a stale wizard worktree; continuing"
2631
+ );
2632
+ }
2633
+ }
2634
+ }
2635
+ async function createWorktree(repoRoot) {
2636
+ const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
2637
+ const dirSlug = branch.replace(/\//g, "-");
2638
+ const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
2639
+ await git(["-C", repoRoot, "worktree", "prune"]);
2640
+ await pruneOldWorktrees(repoRoot);
2641
+ await mkdir5(dirname6(path), { recursive: true });
2642
+ await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
2643
+ return { path, branch };
2644
+ }
2645
+ async function installWorktreeDeps(worktreePath) {
2646
+ try {
2647
+ await readPackageJson(worktreePath);
2648
+ } catch {
2649
+ return { ok: true, output: "no package.json; skipped install" };
2650
+ }
2651
+ const pm = await detectPackageManager(worktreePath);
2652
+ return new Promise((resolve4) => {
2653
+ let output = "";
2654
+ const child = spawn3(pm, ["install"], {
2655
+ cwd: worktreePath,
2656
+ stdio: ["ignore", "pipe", "pipe"]
2657
+ });
2658
+ child.stdout?.on("data", (d) => output += d);
2659
+ child.stderr?.on("data", (d) => output += d);
2660
+ child.on(
2661
+ "error",
2662
+ (err) => resolve4({
2663
+ ok: false,
2664
+ output: `Failed to run ${pm} install: ${err.message}`
2665
+ })
2666
+ );
2667
+ child.on(
2668
+ "close",
2669
+ (code) => resolve4({ ok: code === 0, output: output.trim() })
2670
+ );
2671
+ });
2672
+ }
2673
+ var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
2674
+ function validateIngestEntrypoint(worktreePath, entrypoint) {
2675
+ if (!entrypoint || entrypoint.startsWith("-")) {
2676
+ return {
2677
+ ok: false,
2678
+ reason: `entrypoint "${entrypoint}" is not a plain file path`
2679
+ };
2680
+ }
2681
+ const target = resolve3(worktreePath, entrypoint);
2682
+ const rel = relative2(worktreePath, target);
2683
+ if (rel.startsWith("..") || isAbsolute2(rel)) {
2684
+ return {
2685
+ ok: false,
2686
+ reason: `entrypoint "${entrypoint}" resolves outside the worktree`
2687
+ };
2688
+ }
2689
+ return { ok: true, target };
2690
+ }
2691
+ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
2692
+ if (!INGEST_RUNTIMES.includes(runtime)) {
2693
+ return {
2694
+ ran: false,
2695
+ ok: false,
2696
+ output: "",
2697
+ reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
2698
+ };
2699
+ }
2700
+ const validated = validateIngestEntrypoint(worktreePath, entrypoint);
2701
+ if (!validated.ok) {
2702
+ return { ran: false, ok: false, output: "", reason: validated.reason };
2703
+ }
2704
+ try {
2705
+ if (!(await stat2(validated.target)).isFile()) {
2706
+ return {
2707
+ ran: false,
2708
+ ok: false,
2709
+ output: "",
2710
+ reason: `entrypoint "${entrypoint}" is not a file`
2711
+ };
2712
+ }
2713
+ } catch {
2714
+ return {
2715
+ ran: false,
2716
+ ok: false,
2717
+ output: "",
2718
+ reason: `entrypoint "${entrypoint}" does not exist`
2719
+ };
2720
+ }
2721
+ return new Promise((resolveRun) => {
2722
+ let output = "";
2723
+ const child = spawn3(runtime, [entrypoint], {
2724
+ cwd: worktreePath,
2725
+ shell: false,
2726
+ stdio: ["ignore", "pipe", "pipe"],
2727
+ env: { ...process.env, ...env }
2728
+ });
2729
+ child.stdout?.on("data", (d) => output += d);
2730
+ child.stderr?.on("data", (d) => output += d);
2731
+ child.on(
2732
+ "error",
2733
+ (err) => resolveRun({
2734
+ ran: true,
2735
+ ok: false,
2736
+ output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
2737
+ })
2738
+ );
2739
+ child.on(
2740
+ "close",
2741
+ (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
2742
+ );
2743
+ });
2744
+ }
2745
+ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
2746
+ const trimmed = sourcePath.trim();
2747
+ if (!trimmed) {
2748
+ return { ok: false, reason: "no file path was provided" };
2749
+ }
2750
+ const source = isAbsolute2(trimmed) ? trimmed : resolve3(repoRoot, trimmed);
2751
+ try {
2752
+ if (!(await stat2(source)).isFile()) {
2753
+ return { ok: false, reason: `"${sourcePath}" is not a file` };
2754
+ }
2755
+ } catch {
2756
+ return { ok: false, reason: `"${sourcePath}" does not exist` };
2757
+ }
2758
+ const relPath = join10(ingestDir, basename2(source));
2759
+ const dest = join10(worktreePath, relPath);
2760
+ try {
2761
+ await mkdir5(dirname6(dest), { recursive: true });
2762
+ await copyFile(source, dest);
2763
+ } catch (err) {
2764
+ return {
2765
+ ok: false,
2766
+ reason: `failed to copy "${sourcePath}": ${err.message}`
2767
+ };
2768
+ }
2769
+ return { ok: true, relPath };
2770
+ }
2771
+ async function listChangedFiles(worktreePath) {
2772
+ const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
2773
+ const entries = raw.split("\0");
2774
+ const files = [];
2775
+ for (let i = 0; i < entries.length; i += 1) {
2776
+ const entry = entries[i];
2777
+ if (!entry) continue;
2778
+ files.push(entry.slice(3));
2779
+ if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
2780
+ }
2781
+ return files;
2782
+ }
2783
+ function normalizeFindingPaths(findings) {
2784
+ return {
2785
+ ...findings,
2786
+ ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
2787
+ ...e,
2788
+ paths: e.paths.map(toWorktreeRelative)
2789
+ })),
2790
+ searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
2791
+ confirmedEntities: findings.confirmedEntities?.map((e) => ({
2792
+ ...e,
2793
+ paths: e.paths.map(toWorktreeRelative)
2794
+ }))
2795
+ };
2796
+ }
2797
+ function normalizeSearchLocation(path) {
2798
+ const normalized = path ? toWorktreeRelative(path).trim() : "";
2799
+ return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
2800
+ }
2801
+ function toWorktreeRelative(p) {
2802
+ return p.replace(/^\/+/, "");
2803
+ }
2804
+ async function confirmDirtyWorkingTree(ctx, repoRoot) {
2805
+ const MAX_LISTED_DIRTY_FILES = 10;
2806
+ const dirty = await listChangedFiles(repoRoot);
2807
+ const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
2808
+ const overflow = dirty.length - shown.length;
2809
+ const answer = await ctx.requestUserInput({
2810
+ prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
2811
+ promptType: "acceptReject",
2812
+ options: [],
2813
+ messages: [
2814
+ `${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
2815
+ ...shown.map((file) => ` \u2022 ${file}`),
2816
+ ...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
2817
+ "Commit or stash them first to include them in the implementation."
2818
+ ]
2819
+ });
2820
+ if (answer !== true) {
2821
+ throw new Error(
2822
+ "implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
2823
+ );
2824
+ }
2825
+ }
2826
+
2827
+ // src/lib/algoliaApiKey.ts
2828
+ import { z as z22 } from "zod";
2829
+ 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([])
2834
+ });
2835
+ var apiKeyListSchema = z22.object({
2836
+ items: z22.array(apiKeySchema).optional(),
2837
+ keys: z22.array(apiKeySchema).optional()
2838
+ }).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()
2842
+ });
2843
+ function canReuse(key, index) {
2844
+ return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
2845
+ }
2846
+ async function createSearchKey(index) {
2847
+ const stdout = await runAlgoliaCli([
2848
+ "apikeys",
2849
+ "create",
2850
+ "--indices",
2851
+ index,
2852
+ "--acl",
2853
+ "search,browse",
2854
+ "--description",
2855
+ `wizard search-only key for ${index}`,
2856
+ "-o",
2857
+ "json"
2858
+ ]);
2859
+ const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
2860
+ const created = key ?? value;
2861
+ if (!created) throw new Error("apikeys create returned no key value");
2862
+ return created;
2863
+ }
2864
+ async function resolveSearchOnlyKey(index) {
2865
+ const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2866
+ const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
2867
+ if (existing) {
2868
+ logger.info({ index }, "reusing existing search-only API key");
2869
+ return existing;
2870
+ }
2871
+ logger.info({ index }, "no reusable search-only key found; creating one");
2872
+ return createSearchKey(index);
2873
+ }
2874
+
2875
+ // src/lib/algoliaDocs.ts
2876
+ import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
2877
+ import { dirname as dirname7, join as join11 } from "node:path";
2878
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2879
+ var DOCS_SUBPATH = join11("docs", "algolia-sdk");
2880
+ function findDocsDir() {
2881
+ let dir = dirname7(fileURLToPath2(import.meta.url));
2882
+ for (; ; ) {
2883
+ const candidate = join11(dir, DOCS_SUBPATH);
2884
+ if (existsSync2(candidate)) return candidate;
2885
+ const parent = dirname7(dir);
2886
+ if (parent === dir) return void 0;
2887
+ dir = parent;
2888
+ }
2889
+ }
2890
+ function loadAlgoliaDoc(language) {
2891
+ const docsDir = findDocsDir();
2892
+ if (!docsDir) {
2893
+ logger.warn(
2894
+ "algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
2895
+ );
2896
+ return "";
2897
+ }
2898
+ const files = readdirSync(docsDir).filter((f) => f.includes(language));
2899
+ if (files.length === 0) {
2900
+ logger.warn(
2901
+ { language },
2902
+ "algoliaDocs: no SDK reference found for language; skipping"
2903
+ );
2904
+ return "";
2905
+ }
2906
+ return readFileSync(join11(docsDir, files[0]), "utf8").trim();
2907
+ }
2908
+ function getNamedDoc(name, language) {
2909
+ const docsDir = findDocsDir();
2910
+ if (!docsDir) {
2911
+ logger.warn("docs/algolia-sdk not found");
2912
+ return "";
2913
+ }
2914
+ const file = join11(docsDir, `${name}-${language}.md`);
2915
+ if (!existsSync2(file)) {
2916
+ logger.warn({ name, language }, "named SDK reference not found");
2917
+ return "";
2918
+ }
2919
+ return readFileSync(file, "utf8").trim();
2920
+ }
2921
+ function getFrameworkSpecificDoc(frameworks) {
2922
+ const fw = frameworks.map((f) => f.toLowerCase());
2923
+ if (fw.includes("vue") || fw.includes("nuxt")) {
2924
+ return loadAlgoliaDoc("vue");
2925
+ }
2926
+ if (fw.includes("react") || fw.includes("next.js")) {
2927
+ return loadAlgoliaDoc("react");
2928
+ }
2929
+ if (fw.includes("angular")) {
2930
+ return loadAlgoliaDoc("angular");
2931
+ }
2932
+ return loadAlgoliaDoc("js");
2933
+ }
2934
+
2935
+ // src/actions/implement.ts
2936
+ var implementSchema = z23.object({
2937
+ filesChanged: z23.array(z23.string()),
2938
+ summary: z23.string(),
2939
+ // Absolute path to the throwaway worktree holding the generated changes, so
2940
+ // the user can open it (`cd <worktreePath>`) or inspect the diff
2941
+ // (`git -C <worktreePath> status/diff`).
2942
+ worktreePath: z23.string().optional(),
2943
+ ingestCommand: z23.string().optional(),
2944
+ // True when the user accepted the run-now prompt and the wizard executed the
2945
+ // ingestion script; downstream steps use this to avoid telling the user to run
2946
+ // a script that already ran.
2947
+ ingestScriptRan: z23.boolean().optional(),
2948
+ // Records ingested by the run-now execution, parsed from the script's
2949
+ // machine-readable count line; absent when the script didn't run or emitted
2950
+ // no parseable count.
2951
+ ingestRecordCount: z23.number().optional(),
2952
+ // Wall-clock duration of the run-now ingestion execution, in ms.
2953
+ ingestDurationMs: z23.number().optional(),
2954
+ ingestionSource: z23.enum(["local", "fileUpload", "generated"]),
2955
+ // Suggested names/values, built from framework detection. The search agent is
2956
+ // instructed to rename the prefix if it doesn't match the project's build
2957
+ // tool, so the names it actually wrote can differ — treat these as hints, not
2958
+ // 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()
2963
+ })
2964
+ ).optional()
2965
+ });
2966
+ var implementationOutputSchema = z23.object({
2967
+ summary: z23.string(),
2968
+ // Ingestion only: how to run the generated script, as a structured pair the
2969
+ // wizard turns into an argv (`<runtime> <entrypoint>`) — never a free-form
2970
+ // command string. `runtime` is constrained to an allowlisted interpreter and
2971
+ // `entrypoint` is validated to a worktree-relative path before execution, so
2972
+ // the agent cannot inject extra commands or swap the interpreter.
2973
+ runtime: z23.enum(INGEST_RUNTIMES).optional(),
2974
+ entrypoint: z23.string().optional()
2975
+ });
2976
+ var verificationOutputSchema = z23.object({
2977
+ summary: z23.string(),
2978
+ sufficient: z23.boolean(),
2979
+ additionalInstructions: z23.string().optional()
2980
+ });
2981
+ var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
2982
+ var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
2983
+ var INGEST_DIR = ".algolia-wizard";
2984
+ function detectUiFramework(language) {
2985
+ const names = language.frameworks.map((f) => f.name.toLowerCase());
2986
+ if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
2987
+ if (names.some((n) => n.includes("react") || n.includes("next")))
2988
+ return "React";
2989
+ if (names.some((n) => n.includes("angular"))) return "Angular";
2990
+ return "JavaScript";
2991
+ }
2992
+ function frameworksForDoc(framework) {
2993
+ switch (framework) {
2994
+ case "React":
2995
+ return ["react"];
2996
+ case "Vue":
2997
+ return ["vue"];
2998
+ case "Angular":
2999
+ return ["angular"];
3000
+ case "JavaScript":
3001
+ return [];
3002
+ }
3003
+ }
3004
+ function publicEnvPrefix(language) {
3005
+ const frameworkNames = language.frameworks.map(
3006
+ (framework) => framework.name.toLowerCase()
3007
+ );
3008
+ if (frameworkNames.some((name) => name.includes("next"))) {
3009
+ return "NEXT_PUBLIC_";
3010
+ }
3011
+ if (frameworkNames.some((name) => name.includes("nuxt"))) {
3012
+ return "NUXT_PUBLIC_";
3013
+ }
3014
+ if (frameworkNames.some((name) => name.includes("astro"))) {
3015
+ return "PUBLIC_";
3016
+ }
3017
+ if (frameworkNames.some((name) => name.includes("vite"))) {
3018
+ return "VITE_";
3019
+ }
3020
+ return "PUBLIC_";
3021
+ }
3022
+ function searchEnvVars(language, appId, searchKey) {
3023
+ const prefix = publicEnvPrefix(language);
3024
+ return [
3025
+ {
3026
+ name: `${prefix}ALGOLIA_APP_ID`,
3027
+ value: appId ?? "<your-algolia-app-id>"
3028
+ },
3029
+ {
3030
+ name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
3031
+ value: searchKey ?? "<your-algolia-search-only-api-key>"
3032
+ }
3033
+ ];
3034
+ }
3035
+ function baseInstructions(input) {
3036
+ return [
3037
+ `Target Algolia index: ${input.targetIndex}`,
3038
+ `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3039
+ "Make minimal, idiomatic changes; do not touch unrelated code."
3040
+ ];
3041
+ }
3042
+ function sourceSpecificInstructions(input) {
3043
+ const byLine = {
3044
+ local: [
3045
+ "Records live locally in this project (files, local DB, or local API).",
3046
+ "Read each record from its real local source at the confirmed entity paths.",
3047
+ "Never fabricate, hardcode, or use placeholder records.",
3048
+ "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
3049
+ ],
3050
+ fileUpload: [
3051
+ // The wizard already copied the developer's file into the worktree at this
3052
+ // exact path, so the agent must read it directly — never search for or
3053
+ // substitute another file.
3054
+ `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
3055
+ "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
3056
+ "Map parsed columns/fields to the confirmed entity attributes.",
3057
+ "Never fabricate, hardcode, or substitute a different file."
3058
+ ],
3059
+ generated: [
3060
+ "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."
3064
+ ]
3065
+ };
3066
+ return byLine[input.ingestionSource];
3067
+ }
3068
+ function ingestionInstructions(input) {
3069
+ return [
3070
+ ...input.confirmed && input.confirmed.length ? [
3071
+ `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3072
+ `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3073
+ `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
3074
+ "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
3075
+ "After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
3076
+ getNamedDoc("save-records", "js"),
3077
+ 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
3078
+ "The summary should be extremely concise.",
3079
+ `Return how to run the script as two fields, not a command string: "runtime" (one of ${INGEST_RUNTIMES.join(", ")}) and "entrypoint" (the script path relative to the worktree root, e.g. "${input.ingestDir}/ingest.mjs"). The wizard runs \`<runtime> <entrypoint>\` directly, so the entrypoint must be a plain path with no flags or arguments. Write a script one of those interpreters can run as-is.`,
3080
+ ...sourceSpecificInstructions(input)
3081
+ ] : []
3082
+ ];
3083
+ }
3084
+ function searchInstructions(input) {
3085
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
3086
+ return [
3087
+ "Implement an in-app Algolia search experience.",
3088
+ `Build the search UI for ${input.uiFramework}.`,
3089
+ "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3090
+ doc,
3091
+ `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
+ "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.`,
3096
+ 'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
3097
+ 'Add these env var placeholders to ".env.example", preserving existing entries; create the file if it does not exist.',
3098
+ "In your summary, tell the developer to fill in the concrete env values after reviewing the code."
3099
+ ];
3100
+ }
3101
+ function verificationInstructions(input) {
3102
+ return [
3103
+ "Verify the Algolia implementation changes in the current worktree.",
3104
+ `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
3105
+ "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
3106
+ "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
3107
+ "Do not make speculative fixes when verifyImplementation cannot run, no checks exist, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
3108
+ "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
3109
+ `Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
3110
+ "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
3111
+ "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
3112
+ "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
3113
+ ];
3114
+ }
3115
+ var IMPLEMENT_CONFIG = {
3116
+ ingestion: {
3117
+ title: "Algolia ingestion",
3118
+ buildInstructions: ingestionInstructions
3119
+ },
3120
+ search: {
3121
+ title: "Algolia search",
3122
+ buildInstructions: searchInstructions
3123
+ },
3124
+ verification: {
3125
+ title: "Algolia verification",
3126
+ buildInstructions: verificationInstructions
3127
+ }
3128
+ };
3129
+ var useCaseToolMap = {
3130
+ ingestion: [...FS_READ_TOOLS, "writeFile", "writeCredentials"],
3131
+ search: [...FS_READ_TOOLS, "writeFile"],
3132
+ verification: [...FS_READ_TOOLS, "verifyImplementation"]
3133
+ };
3134
+ function toolsForUseCase(useCase, ingestionSource) {
3135
+ const tools = useCaseToolMap[useCase];
3136
+ if (useCase === "ingestion" && ingestionSource === "generated") {
3137
+ return [...tools, "generateRecord"];
3138
+ }
3139
+ return tools;
3140
+ }
3141
+ function buildAgentInstructions(useCase, input, extraInstructions = []) {
3142
+ const config = IMPLEMENT_CONFIG[useCase];
3143
+ return [
3144
+ `You are implementing ${config.title}.`,
3145
+ ...baseInstructions(input),
3146
+ ...config.buildInstructions(input),
3147
+ ...extraInstructions,
3148
+ "When done, call reportStatus"
3149
+ ];
3150
+ }
3151
+ function formatSummary(useCase, summary) {
3152
+ const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3153
+ return `${label}: ${summary}`;
3154
+ }
3155
+ function parseIngestRecordCount(output) {
3156
+ const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
3157
+ if (!match) return void 0;
3158
+ const count = Number(match[1]);
3159
+ return Number.isFinite(count) ? count : void 0;
3160
+ }
3161
+ function verificationRetryInstructions(verification) {
3162
+ return [
3163
+ `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
3164
+ ];
3165
+ }
3166
+ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
3167
+ const repoRoot = process.cwd();
3168
+ const scan = ctx.getStepOutput("project-scan");
3169
+ const entities = ctx.getStepOutput(
3170
+ "confirm-entities"
3171
+ );
3172
+ const findings = {
3173
+ ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
3174
+ searchImplementationAnalysis: scan.searchImplementationAnalysis,
3175
+ verification: scan.verification,
3176
+ confirmedEntities: entities?.confirmedEntities
3177
+ };
3178
+ const language = {
3179
+ languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
3180
+ frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
3181
+ };
3182
+ const selected = ctx.getStepOutput(
3183
+ "select-index"
3184
+ );
3185
+ if (!selected?.selection) {
3186
+ throw new Error(`No index was selected`);
3187
+ }
3188
+ let ingestionSource = "generated";
3189
+ const ingestionSourceQuestions = [
3190
+ {
3191
+ question: "Do you have local access to the data you want to ingest in Algolia?",
3192
+ source: "local"
3193
+ },
3194
+ {
3195
+ question: "Do you have a JSON, CSV or TSV you would like to upload?",
3196
+ source: "fileUpload"
3197
+ },
3198
+ {
3199
+ question: "Do you want me to generate sample records based on your entity schema?",
3200
+ source: "generated"
3201
+ }
3202
+ ];
3203
+ let uploadSourcePath;
3204
+ if (useCases.includes("ingestion")) {
3205
+ for (const sq of ingestionSourceQuestions) {
3206
+ const result = Boolean(
3207
+ await ctx.requestUserInput({
3208
+ prompt: sq.question,
3209
+ promptType: "acceptReject",
3210
+ options: ["Yes", "No"],
3211
+ messages: []
3212
+ })
3213
+ );
3214
+ if (result) {
3215
+ ingestionSource = sq.source;
3216
+ break;
3217
+ }
3218
+ }
3219
+ if (ingestionSource === "fileUpload") {
3220
+ const answer = await ctx.requestUserInput({
3221
+ prompt: "Enter the path to your JSON, CSV, or TSV file (relative to the project root, or absolute):",
3222
+ promptType: "textInput",
3223
+ options: [],
3224
+ messages: []
3225
+ });
3226
+ uploadSourcePath = typeof answer === "string" ? answer : "";
3227
+ }
3228
+ }
3229
+ const targetIndex = selected?.selection;
3230
+ await assertGitRepoWithHead(repoRoot);
3231
+ if (await isWorkingTreeDirty(repoRoot)) {
3232
+ await confirmDirtyWorkingTree(ctx, repoRoot);
3233
+ }
3234
+ const normalized = normalizeFindingPaths(findings);
3235
+ const confirmed2 = normalized.confirmedEntities;
3236
+ const searchLocation = normalized.searchImplementationAnalysis;
3237
+ let appId;
3238
+ let searchKey;
3239
+ if (useCases.includes("search")) {
3240
+ appId = (await loadActiveProfile()).appId;
3241
+ try {
3242
+ searchKey = await resolveSearchOnlyKey(targetIndex);
3243
+ } catch (err) {
3244
+ logger.warn(
3245
+ { err: err.message },
3246
+ "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
3247
+ );
3248
+ }
3249
+ }
3250
+ const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
3251
+ try {
3252
+ process.chdir(worktree);
3253
+ let uploadFilePath;
3254
+ let uploadWarning;
3255
+ if (ingestionSource === "fileUpload") {
3256
+ const copied = await copyUploadIntoWorktree(
3257
+ repoRoot,
3258
+ worktree,
3259
+ INGEST_DIR,
3260
+ uploadSourcePath ?? ""
3261
+ );
3262
+ if (copied.ok) {
3263
+ uploadFilePath = copied.relPath;
3264
+ } else {
3265
+ ingestionSource = "generated";
3266
+ uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
3267
+ logger.warn(
3268
+ { reason: copied.reason },
3269
+ "implement: file upload unavailable; falling back to generated sample records"
3270
+ );
3271
+ }
3272
+ }
3273
+ const input = {
3274
+ findings: normalized,
3275
+ confirmed: confirmed2,
3276
+ searchLocation,
3277
+ targetIndex,
3278
+ language,
3279
+ appId,
3280
+ searchKey,
3281
+ searchEnvVars: searchEnvVars(language, appId, searchKey),
3282
+ ingestDir: INGEST_DIR,
3283
+ ingestionSource,
3284
+ uploadFilePath,
3285
+ // language.frameworks already prefers the confirm-framework step output,
3286
+ // so the user's confirmed stack (not just raw detection) picks the flavor.
3287
+ uiFramework: detectUiFramework(language)
3288
+ };
3289
+ const summaries = [];
3290
+ if (uploadWarning) summaries.push(uploadWarning);
3291
+ let agentRuns = 0;
3292
+ let ingestRuntime;
3293
+ let ingestEntrypoint;
3294
+ let ingestScriptRan = false;
3295
+ let ingestRecordCount;
3296
+ let ingestDurationMs;
3297
+ let installFailed = false;
3298
+ async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
3299
+ if (agentRuns > 0) ctx.recordStepExecution();
3300
+ agentRuns += 1;
3301
+ const result = await runAgent({
3302
+ instructions: buildAgentInstructions(
3303
+ currentUseCase,
3304
+ input,
3305
+ extraInstructions
3306
+ ),
3307
+ tools: toolsForUseCase(currentUseCase, input.ingestionSource),
3308
+ outputSchema: implementationOutputSchema
3309
+ });
3310
+ const install = await installWorktreeDeps(worktree);
3311
+ if (!install.ok) {
3312
+ installFailed = true;
3313
+ logger.warn(
3314
+ { useCase: currentUseCase, output: install.output },
3315
+ "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
3316
+ );
3317
+ }
3318
+ return result;
3319
+ }
3320
+ async function runVerificationUseCase() {
3321
+ if (agentRuns > 0) ctx.recordStepExecution();
3322
+ agentRuns += 1;
3323
+ return runAgent({
3324
+ instructions: buildAgentInstructions("verification", input),
3325
+ tools: toolsForUseCase("verification"),
3326
+ outputSchema: verificationOutputSchema
3327
+ });
3328
+ }
3329
+ if (useCases.includes("ingestion")) {
3330
+ const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
3331
+ summaries.push(formatSummary("ingestion", summary));
3332
+ ingestRuntime = runtime;
3333
+ ingestEntrypoint = entrypoint;
3334
+ if (ingestRuntime && ingestEntrypoint && !installFailed) {
3335
+ const runNow = await ctx.requestUserInput({
3336
+ prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
3337
+ promptType: "acceptReject",
3338
+ options: ["Yes", "No"],
3339
+ messages: []
3340
+ }) === true;
3341
+ if (runNow) {
3342
+ const profile2 = await loadActiveProfile();
3343
+ const startedAt = Date.now();
3344
+ const run = await runIngestScript(
3345
+ worktree,
3346
+ ingestRuntime,
3347
+ ingestEntrypoint,
3348
+ {
3349
+ [APP_ID_VAR]: profile2.appId,
3350
+ [API_KEY_VAR]: profile2.apiKey
3351
+ }
3352
+ );
3353
+ ingestScriptRan = run.ran && run.ok;
3354
+ if (ingestScriptRan) {
3355
+ ingestDurationMs = Date.now() - startedAt;
3356
+ ingestRecordCount = parseIngestRecordCount(run.output);
3357
+ if (ingestRecordCount != null) {
3358
+ track("AI Wizard Ingest Successful", {
3359
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
3360
+ record_count: ingestRecordCount,
3361
+ duration_ms: ingestDurationMs
3362
+ });
3363
+ }
3364
+ }
3365
+ let summaryLine;
3366
+ let outcomeMessage;
3367
+ if (!run.ran) {
3368
+ summaryLine = `\u26A0\uFE0F Skipped running the ingestion script: ${run.reason}`;
3369
+ outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run.reason}`;
3370
+ logger.warn(
3371
+ {
3372
+ runtime: ingestRuntime,
3373
+ entrypoint: ingestEntrypoint,
3374
+ reason: run.reason
3375
+ },
3376
+ "implement: refused to auto-run ingestion script"
3377
+ );
3378
+ track("Error", {
3379
+ step: "Push Data",
3380
+ error: `ingestion script skipped: ${run.reason}`,
3381
+ product_area: "AI Wizard"
3382
+ });
3383
+ } else if (run.ok) {
3384
+ const status = "Ingestion run: succeeded.";
3385
+ summaryLine = run.output ? `${status}
3386
+ ${run.output}` : status;
3387
+ outcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
3388
+ } else {
3389
+ const status = "\u26A0\uFE0F Ingestion run failed:";
3390
+ summaryLine = run.output ? `${status}
3391
+ ${run.output}` : status;
3392
+ outcomeMessage = `\u274C Ingestion failed.${run.output ? ` ${run.output}` : ""}`;
3393
+ logger.warn(
3394
+ {
3395
+ runtime: ingestRuntime,
3396
+ entrypoint: ingestEntrypoint,
3397
+ output: run.output
3398
+ },
3399
+ "implement: ingestion script run failed"
3400
+ );
3401
+ track("Error", {
3402
+ step: "Push Data",
3403
+ error: run.output || "ingestion script exited non-zero",
3404
+ product_area: "AI Wizard"
3405
+ });
3406
+ }
3407
+ summaries.push(summaryLine);
3408
+ await ctx.requestUserInput({
3409
+ prompt: "Continue",
3410
+ promptType: "notice",
3411
+ options: [],
3412
+ messages: [outcomeMessage]
3413
+ });
3414
+ }
3415
+ }
3416
+ }
3417
+ if (useCases.includes("search")) {
3418
+ let extraInstructions = [];
3419
+ const preSearchFiles = new Set(await listChangedFiles(worktree));
3420
+ for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
3421
+ const { summary } = await runImplementationUseCase(
3422
+ "search",
3423
+ extraInstructions
3424
+ );
3425
+ summaries.push(formatSummary("search", summary));
3426
+ const verification = await runVerificationUseCase();
3427
+ summaries.push(formatSummary("verification", verification.summary));
3428
+ if (verification.sufficient) {
3429
+ ctx.setUserInput("implementation", "success");
3430
+ const searchFilesChanged = (await listChangedFiles(worktree)).filter(
3431
+ (file) => !preSearchFiles.has(file)
3432
+ );
3433
+ track("AI Wizard Frontend Component Generated", {
3434
+ filePaths: searchFilesChanged
3435
+ });
3436
+ track("AI Wizard Wired to UI", {
3437
+ location_heuristic: searchLocation ?? "unknown"
3438
+ });
3439
+ break;
3440
+ }
3441
+ if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
3442
+ ctx.setUserInput("implementation", "fail");
3443
+ throw new Error(
3444
+ `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
3445
+ );
3446
+ }
3447
+ extraInstructions = verificationRetryInstructions(verification);
3448
+ }
3449
+ } else {
3450
+ ctx.setUserInput("implementation", "success");
3451
+ }
3452
+ const filesChanged = await listChangedFiles(worktree);
3453
+ if (filesChanged.length === 0) {
3454
+ logger.warn(
3455
+ "implement: agent reported success but no files changed in the worktree"
3456
+ );
3457
+ }
3458
+ if (installFailed) {
3459
+ summaries.push(
3460
+ '\u26A0\uFE0F Dependency install in the worktree failed. Run your package manager install in the worktree before the command below, or it will fail with "Cannot find module".'
3461
+ );
3462
+ }
3463
+ return {
3464
+ ingestionSource,
3465
+ filesChanged,
3466
+ summary: summaries.join("\n\n"),
3467
+ worktreePath: worktree,
3468
+ ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
3469
+ ingestCommand: `cd ${shellQuote(worktree)} && ${ingestRuntime} ${shellQuote(ingestEntrypoint)}`,
3470
+ ingestScriptRan,
3471
+ ...ingestRecordCount != null ? { ingestRecordCount } : {},
3472
+ ...ingestDurationMs != null ? { ingestDurationMs } : {}
3473
+ } : {},
3474
+ ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
3475
+ };
3476
+ } finally {
3477
+ process.chdir(repoRoot);
3478
+ }
3479
+ }
3480
+
3481
+ // src/workflows/default.ts
3482
+ var defaultWorkflow = {
3483
+ id: "default",
3484
+ title: "Default Workflow",
3485
+ description: "This workflow explores your repo and implements Algolia on your behalf",
3486
+ steps: [
3487
+ defineStep({
3488
+ id: "project-scan",
3489
+ title: "project scan",
3490
+ outputSchema: projectScanSchema,
3491
+ run: (ctx) => projectScan(ctx)
3492
+ }),
3493
+ defineStep({
3494
+ id: "confirm-language",
3495
+ title: "confirm language",
3496
+ outputSchema: confirmLanguageSchema,
3497
+ visible: false,
3498
+ run: (ctx) => confirmLanguage(ctx)
3499
+ }),
3500
+ defineStep({
3501
+ id: "confirm-entities",
3502
+ title: "confirm entities",
3503
+ outputSchema: confirmEntitiesSchema,
3504
+ visible: false,
3505
+ run: (ctx) => confirmEntities(ctx)
3506
+ }),
3507
+ defineStep({
3508
+ id: "select-index",
3509
+ title: "Set up index",
3510
+ outputSchema: z24.object({
3511
+ selection: z24.string()
3512
+ }),
3513
+ run: (ctx) => selectIndexStep(ctx)
3514
+ }),
3515
+ defineStep({
3516
+ id: "ingestion",
3517
+ title: "ingest records",
3518
+ outputSchema: implementSchema,
3519
+ run: (ctx) => implement(ctx, ["ingestion"])
3520
+ }),
3521
+ defineStep({
3522
+ id: "confirm-framework",
3523
+ title: "Confirm framework",
3524
+ outputSchema: confirmFrameworkSchema,
3525
+ visible: false,
3526
+ run: (ctx) => confirmFramework(ctx)
3527
+ }),
3528
+ defineStep({
3529
+ id: "search",
3530
+ title: "create search ui",
3531
+ outputSchema: implementSchema,
3532
+ run: (ctx) => {
3533
+ const ingestion = ctx.getStepOutput(
3534
+ "ingestion"
3535
+ );
3536
+ return implement(ctx, ["search"], ingestion?.worktreePath);
3537
+ }
3538
+ }),
3539
+ defineStep({
3540
+ id: "review",
3541
+ title: "done",
3542
+ outputSchema: reviewSchema,
3543
+ run: (ctx) => {
3544
+ const ingestion = ctx.getStepOutput(
3545
+ "ingestion"
3546
+ );
3547
+ 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."
3549
+ });
3550
+ }
3551
+ })
3552
+ ]
3553
+ };
3554
+
3555
+ // src/workflows/index.ts
3556
+ var workflows = {
3557
+ [defaultWorkflow.id]: defaultWorkflow
3558
+ };
3559
+ function getWorkflow(id) {
3560
+ return workflows[id];
3561
+ }
3562
+
3563
+ // src/main.tsx
3564
+ import { jsx as jsx11 } from "react/jsx-runtime";
3565
+ var requestedId = process.argv[2] ?? defaultWorkflow.id;
3566
+ var workflow = getWorkflow(requestedId);
3567
+ if (!workflow) {
3568
+ const available = Object.keys(workflows).join(", ");
3569
+ console.error(`Unknown workflow "${requestedId}". Available: ${available}`);
3570
+ process.exit(1);
3571
+ }
3572
+ var store = useWizard.getState();
3573
+ var instance = render(/* @__PURE__ */ jsx11(App, {}));
3574
+ var user = await getUser();
3575
+ if (!user) {
3576
+ await instance.waitUntilRenderFlush();
3577
+ instance.cleanup();
3578
+ try {
3579
+ await runAuthLogin();
3580
+ } catch (err) {
3581
+ console.error(err instanceof Error ? err.message : String(err));
3582
+ process.exit(1);
3583
+ }
3584
+ instance = render(/* @__PURE__ */ jsx11(App, {}));
3585
+ user = await getUser();
3586
+ if (!user) {
3587
+ store.setError(
3588
+ "Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli auth login` directly."
3589
+ );
3590
+ await instance.waitUntilExit();
3591
+ process.exit(1);
3592
+ }
3593
+ }
3594
+ store.setUser(user);
3595
+ var profile = await loadActiveProfile();
3596
+ await store.waitForStart();
3597
+ runWorkflow(workflow, profile?.appId);