@algolia/wizard 0.8.0-rc.67.55 → 0.9.0-rc.49.79
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/README.md +1 -1
- package/dist/main.js +1351 -1884
- package/docs/algolia-sdk/README.md +30 -53
- package/docs/algolia-sdk/search-single-index.md +42 -0
- package/package.json +2 -5
- package/docs/algolia-sdk/instantsearch-setup-templates.md +0 -92
- package/docs/algolia-sdk/save-records-csharp.md +0 -71
- package/docs/algolia-sdk/save-records-dart.md +0 -74
- package/docs/algolia-sdk/save-records-go.md +0 -62
- package/docs/algolia-sdk/save-records-java.md +0 -66
- package/docs/algolia-sdk/save-records-kotlin.md +0 -60
- package/docs/algolia-sdk/save-records-php.md +0 -50
- package/docs/algolia-sdk/save-records-python.md +0 -51
- package/docs/algolia-sdk/save-records-ruby.md +0 -48
- package/docs/algolia-sdk/save-records-scala.md +0 -68
- package/docs/algolia-sdk/save-records-swift.md +0 -88
package/dist/main.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { render } from "ink";
|
|
5
5
|
|
|
6
6
|
// src/ui/App.tsx
|
|
7
|
-
import { Box as
|
|
7
|
+
import { Box as Box15, Text as Text15, useApp, useInput as useInput6, useWindowSize as useWindowSize8 } from "ink";
|
|
8
8
|
|
|
9
9
|
// src/core/store.ts
|
|
10
10
|
import { create } from "zustand";
|
|
@@ -12,32 +12,86 @@ import { nanoid } from "nanoid";
|
|
|
12
12
|
|
|
13
13
|
// src/lib/algoliaCli.ts
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return require2.resolve("@algolia/cli/bin/run.js");
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
function npxArgs(args) {
|
|
17
|
+
return ["--yes", "@algolia/cli@latest", ...args];
|
|
19
18
|
}
|
|
20
|
-
|
|
19
|
+
var shell = process.platform === "win32";
|
|
20
|
+
function lineSplitter(emit) {
|
|
21
|
+
let buffer = "";
|
|
22
|
+
return {
|
|
23
|
+
push(chunk) {
|
|
24
|
+
buffer += chunk;
|
|
25
|
+
const lines = buffer.split("\n");
|
|
26
|
+
buffer = lines.pop() ?? "";
|
|
27
|
+
for (const line of lines) emit(line.replace(/\r$/, ""));
|
|
28
|
+
},
|
|
29
|
+
flush() {
|
|
30
|
+
if (buffer) emit(buffer.replace(/\r$/, ""));
|
|
31
|
+
buffer = "";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var wizardSink = (stream, line) => {
|
|
36
|
+
if (!line.trim()) return;
|
|
37
|
+
useWizard.getState().pushCliOutput(stream, line);
|
|
38
|
+
};
|
|
39
|
+
var stderrSink = (stream, line) => {
|
|
40
|
+
if (stream === "stdout") return;
|
|
41
|
+
wizardSink(stream, line);
|
|
42
|
+
};
|
|
43
|
+
function runAlgoliaCli(args, { onOutput } = {}) {
|
|
44
|
+
const store = useWizard.getState();
|
|
45
|
+
const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
|
|
21
46
|
return new Promise((resolve4, reject) => {
|
|
22
|
-
const child = spawn(
|
|
47
|
+
const child = spawn("npx", npxArgs(args), { shell });
|
|
23
48
|
let stdout = "";
|
|
24
49
|
let stderr = "";
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
const splitters = {
|
|
51
|
+
stdout: lineSplitter((line) => onOutput?.("stdout", line)),
|
|
52
|
+
stderr: lineSplitter((line) => onOutput?.("stderr", line))
|
|
53
|
+
};
|
|
54
|
+
child.stdout.on("data", (chunk) => {
|
|
55
|
+
const text = String(chunk);
|
|
56
|
+
stdout += text;
|
|
57
|
+
splitters.stdout.push(text);
|
|
58
|
+
});
|
|
59
|
+
child.stderr.on("data", (chunk) => {
|
|
60
|
+
const text = String(chunk);
|
|
61
|
+
stderr += text;
|
|
62
|
+
splitters.stderr.push(text);
|
|
63
|
+
});
|
|
27
64
|
child.on("error", reject);
|
|
28
65
|
child.on("close", (code) => {
|
|
66
|
+
splitters.stdout.flush();
|
|
67
|
+
splitters.stderr.flush();
|
|
29
68
|
if (code === 0) {
|
|
30
69
|
resolve4(stdout);
|
|
31
70
|
} else {
|
|
32
|
-
const
|
|
71
|
+
const failed = stderr.trim();
|
|
72
|
+
let detail = "";
|
|
73
|
+
if (failed) {
|
|
74
|
+
detail = `: ${failed}`;
|
|
75
|
+
} else if (stdout.trim()) {
|
|
76
|
+
detail = " (no stderr; stdout withheld \u2014 it may contain credentials)";
|
|
77
|
+
}
|
|
33
78
|
reject(
|
|
34
79
|
new Error(
|
|
35
|
-
`Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail
|
|
80
|
+
`Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail}`
|
|
36
81
|
)
|
|
37
82
|
);
|
|
38
83
|
}
|
|
39
84
|
});
|
|
40
|
-
})
|
|
85
|
+
}).then(
|
|
86
|
+
(out) => {
|
|
87
|
+
useWizard.getState().logEnd(logId, "success");
|
|
88
|
+
return out;
|
|
89
|
+
},
|
|
90
|
+
(err) => {
|
|
91
|
+
useWizard.getState().logEnd(logId, "error");
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
41
95
|
}
|
|
42
96
|
async function getUser() {
|
|
43
97
|
let raw;
|
|
@@ -52,19 +106,23 @@ async function getUser() {
|
|
|
52
106
|
return null;
|
|
53
107
|
}
|
|
54
108
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
child.on("error", reject);
|
|
63
|
-
child.on("close", (code) => {
|
|
64
|
-
if (code === 0) resolve4();
|
|
65
|
-
else reject(new Error(`Algolia authentication failed (exit ${code}).`));
|
|
66
|
-
});
|
|
109
|
+
var loginResultSchema = z.object({
|
|
110
|
+
success: z.boolean(),
|
|
111
|
+
email: z.string().optional()
|
|
112
|
+
});
|
|
113
|
+
async function runAuthLogin() {
|
|
114
|
+
const raw = await runAlgoliaCli(["auth", "login", "--non-interactive"], {
|
|
115
|
+
onOutput: stderrSink
|
|
67
116
|
});
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = loginResultSchema.safeParse(JSON.parse(raw));
|
|
120
|
+
} catch {
|
|
121
|
+
parsed = void 0;
|
|
122
|
+
}
|
|
123
|
+
if (parsed?.success && !parsed.data.success) {
|
|
124
|
+
throw new Error("Algolia sign-in did not report success.");
|
|
125
|
+
}
|
|
68
126
|
}
|
|
69
127
|
|
|
70
128
|
// src/lib/auth.ts
|
|
@@ -171,6 +229,7 @@ function describeInputValue(value) {
|
|
|
171
229
|
return Array.isArray(value) ? value.join(", ") : value;
|
|
172
230
|
}
|
|
173
231
|
var NOTICE_INTERVAL_MS = 2e3;
|
|
232
|
+
var CLI_OUTPUT_LIMIT = 200;
|
|
174
233
|
var useWizard = create((set, get) => ({
|
|
175
234
|
phase: "idle",
|
|
176
235
|
homeScreen: "home",
|
|
@@ -182,22 +241,21 @@ var useWizard = create((set, get) => ({
|
|
|
182
241
|
notices: [],
|
|
183
242
|
_noticeQueue: [],
|
|
184
243
|
_noticeTimer: null,
|
|
244
|
+
cliOutput: [],
|
|
245
|
+
targetIndex: null,
|
|
185
246
|
logs: [],
|
|
186
247
|
error: null,
|
|
187
248
|
inputReq: null,
|
|
188
249
|
_resolve: null,
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
250
|
+
// `endAuth` lands on 'preflight', not 'idle': sign-in happens after the
|
|
251
|
+
// welcome screen, so going back would gate the run a second time.
|
|
252
|
+
beginAuth: () => set({ phase: "authenticating", cliOutput: [] }),
|
|
253
|
+
endAuth: () => set((s) => s.phase === "authenticating" ? { phase: "preflight" } : {}),
|
|
192
254
|
confirmStart: () => set(
|
|
193
255
|
(s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
|
|
194
256
|
),
|
|
195
|
-
// Welcome sub-view navigation; leaves `phase` untouched so the workflow stays paused.
|
|
196
257
|
openLearnMore: () => set({ homeScreen: "learnMore" }),
|
|
197
258
|
backToHome: () => set({ homeScreen: "home" }),
|
|
198
|
-
// Resolves once the phase leaves 'idle', whether that happens before or
|
|
199
|
-
// after this is called (the welcome screen's enter handler is what
|
|
200
|
-
// drives the transition via `confirmStart`).
|
|
201
259
|
waitForStart: () => new Promise((resolve4) => {
|
|
202
260
|
if (get().phase !== "idle") {
|
|
203
261
|
resolve4();
|
|
@@ -220,15 +278,19 @@ var useWizard = create((set, get) => ({
|
|
|
220
278
|
syncSteps: (steps, currentStepIndex) => set({ steps, currentStepIndex }),
|
|
221
279
|
setActiveStep: (index) => {
|
|
222
280
|
get()._clearNoticeQueue();
|
|
223
|
-
set({
|
|
281
|
+
set({
|
|
282
|
+
phase: "running",
|
|
283
|
+
currentStepIndex: index,
|
|
284
|
+
output: "",
|
|
285
|
+
notices: [],
|
|
286
|
+
cliOutput: []
|
|
287
|
+
});
|
|
224
288
|
},
|
|
225
289
|
setUser: (user) => set({ user }),
|
|
226
290
|
appendToken: (text) => set((s) => ({ output: s.output + text })),
|
|
227
291
|
clearOutput: () => set({ output: "" }),
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
// the timer stays armed through an empty drain so the cooldown always
|
|
231
|
-
// covers the time since the last render, even across bursts.
|
|
292
|
+
// The timer stays armed through an empty drain, so the spacing covers the
|
|
293
|
+
// time since the last render even across bursts.
|
|
232
294
|
pushNotice: (notice) => {
|
|
233
295
|
const { notices, _noticeQueue, _noticeTimer } = get();
|
|
234
296
|
if (_noticeTimer === null) {
|
|
@@ -261,6 +323,13 @@ var useWizard = create((set, get) => ({
|
|
|
261
323
|
get()._clearNoticeQueue();
|
|
262
324
|
set({ notices: [] });
|
|
263
325
|
},
|
|
326
|
+
pushCliOutput: (stream, text) => set((s) => ({
|
|
327
|
+
cliOutput: [...s.cliOutput, { id: nanoid(), stream, text }].slice(
|
|
328
|
+
-CLI_OUTPUT_LIMIT
|
|
329
|
+
)
|
|
330
|
+
})),
|
|
331
|
+
clearCliOutput: () => set({ cliOutput: [] }),
|
|
332
|
+
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
264
333
|
logStart: (kind, name, input) => {
|
|
265
334
|
const id = nanoid();
|
|
266
335
|
set((s) => ({
|
|
@@ -283,9 +352,6 @@ var useWizard = create((set, get) => ({
|
|
|
283
352
|
_resolve: resolve4
|
|
284
353
|
});
|
|
285
354
|
}),
|
|
286
|
-
// Logs what the user picked — not the prompt text that was shown, which
|
|
287
|
-
// may repeat or duplicate on-screen content and isn't the useful signal
|
|
288
|
-
// here.
|
|
289
355
|
submitInput: async (value) => {
|
|
290
356
|
await markInteraction();
|
|
291
357
|
get()._resolve?.(value);
|
|
@@ -305,6 +371,8 @@ var useWizard = create((set, get) => ({
|
|
|
305
371
|
currentStepIndex: 0,
|
|
306
372
|
output: "",
|
|
307
373
|
notices: [],
|
|
374
|
+
cliOutput: [],
|
|
375
|
+
targetIndex: null,
|
|
308
376
|
logs: [],
|
|
309
377
|
error: null,
|
|
310
378
|
inputReq: null,
|
|
@@ -313,16 +381,100 @@ var useWizard = create((set, get) => ({
|
|
|
313
381
|
}
|
|
314
382
|
}));
|
|
315
383
|
|
|
384
|
+
// src/ui/CliOutput.tsx
|
|
385
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
386
|
+
|
|
387
|
+
// src/ui/theme.ts
|
|
388
|
+
var MARKER = {
|
|
389
|
+
pending: "\u25CB",
|
|
390
|
+
running: "\u25D0",
|
|
391
|
+
done: "\u2713",
|
|
392
|
+
error: "\u2716"
|
|
393
|
+
};
|
|
394
|
+
var BRAND = "#003DFF";
|
|
395
|
+
var SECONDARY = "#5468FF";
|
|
396
|
+
var DANGER = "#F86E7E";
|
|
397
|
+
var COLORS = {
|
|
398
|
+
brand: BRAND,
|
|
399
|
+
primary: "#E6EDF3",
|
|
400
|
+
secondary: SECONDARY,
|
|
401
|
+
strong: "#FFFFFF",
|
|
402
|
+
muted: "#8B949E",
|
|
403
|
+
dim: "#484F58",
|
|
404
|
+
highlight: { bg: "#12331C", fg: "#4ADE80" },
|
|
405
|
+
badge: "#E3B341",
|
|
406
|
+
danger: DANGER,
|
|
407
|
+
success: "#4ADE80",
|
|
408
|
+
bg: {
|
|
409
|
+
main: "#0B0E14",
|
|
410
|
+
sidebar: "#14171E"
|
|
411
|
+
},
|
|
412
|
+
border: "#30363D",
|
|
413
|
+
accent: "#76A0FF",
|
|
414
|
+
status: {
|
|
415
|
+
pending: "gray",
|
|
416
|
+
running: "#76A0FF",
|
|
417
|
+
done: "#4ADE80",
|
|
418
|
+
error: DANGER
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/ui/CliOutput.tsx
|
|
423
|
+
import { jsxs } from "react/jsx-runtime";
|
|
424
|
+
var CLI_MARKER = "\u203A";
|
|
425
|
+
var RESERVED_ROWS = 16;
|
|
426
|
+
var MAX_ROWS = 12;
|
|
427
|
+
var PANEL_TEXT_WIDTH = 45;
|
|
428
|
+
var URL_PATTERN = /https?:\/\//;
|
|
429
|
+
function rowCost(text) {
|
|
430
|
+
return URL_PATTERN.test(text) ? Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH)) : 1;
|
|
431
|
+
}
|
|
432
|
+
function CliOutput() {
|
|
433
|
+
const cliOutput = useWizard((s) => s.cliOutput);
|
|
434
|
+
const { rows } = useWindowSize();
|
|
435
|
+
if (!cliOutput.length) return null;
|
|
436
|
+
const rowBudget = Math.min(Math.max(rows - RESERVED_ROWS, 3), MAX_ROWS);
|
|
437
|
+
const visible = [];
|
|
438
|
+
let usedRows = 0;
|
|
439
|
+
for (let i = cliOutput.length - 1; i >= 0; i--) {
|
|
440
|
+
const cost = rowCost(cliOutput[i].text);
|
|
441
|
+
if (usedRows + cost > rowBudget && visible.length > 0) break;
|
|
442
|
+
visible.unshift(cliOutput[i]);
|
|
443
|
+
usedRows += cost;
|
|
444
|
+
}
|
|
445
|
+
const hidden = cliOutput.length - visible.length;
|
|
446
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
447
|
+
hidden > 0 && /* @__PURE__ */ jsxs(Text, { color: COLORS.dim, children: [
|
|
448
|
+
"\u2191 ",
|
|
449
|
+
hidden,
|
|
450
|
+
" earlier line(s)"
|
|
451
|
+
] }),
|
|
452
|
+
visible.map((line) => /* @__PURE__ */ jsxs(
|
|
453
|
+
Text,
|
|
454
|
+
{
|
|
455
|
+
color: line.stream === "stderr" ? COLORS.muted : COLORS.dim,
|
|
456
|
+
wrap: URL_PATTERN.test(line.text) ? "wrap" : "truncate",
|
|
457
|
+
children: [
|
|
458
|
+
CLI_MARKER,
|
|
459
|
+
" ",
|
|
460
|
+
line.text
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
line.id
|
|
464
|
+
))
|
|
465
|
+
] });
|
|
466
|
+
}
|
|
467
|
+
|
|
316
468
|
// src/ui/Notices.tsx
|
|
317
|
-
import { Box as
|
|
469
|
+
import { Box as Box3, Text as Text3, useWindowSize as useWindowSize3 } from "ink";
|
|
318
470
|
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
319
471
|
|
|
320
472
|
// src/ui/Table.tsx
|
|
321
|
-
import { Box, Text, measureElement, useWindowSize } from "ink";
|
|
473
|
+
import { Box as Box2, Text as Text2, measureElement, useWindowSize as useWindowSize2 } from "ink";
|
|
322
474
|
import { useEffect, useRef, useState } from "react";
|
|
323
475
|
import { jsx } from "react/jsx-runtime";
|
|
324
476
|
function Table({ columns, rows }) {
|
|
325
|
-
const { columns: termCols } =
|
|
477
|
+
const { columns: termCols } = useWindowSize2();
|
|
326
478
|
const ref = useRef(null);
|
|
327
479
|
const [width, setWidth] = useState(0);
|
|
328
480
|
useEffect(() => {
|
|
@@ -330,7 +482,7 @@ function Table({ columns, rows }) {
|
|
|
330
482
|
}, [termCols, columns, rows]);
|
|
331
483
|
if (rows.length === 0) return null;
|
|
332
484
|
const lines = formatTable(columns, rows, width || void 0);
|
|
333
|
-
return /* @__PURE__ */ jsx(
|
|
485
|
+
return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", ref, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text2, { wrap: "truncate", children: line }, `tbl-${i}`)) });
|
|
334
486
|
}
|
|
335
487
|
function formatTable(columns, rows, width) {
|
|
336
488
|
const natural = columns.map(
|
|
@@ -370,48 +522,13 @@ function resize(widths, budget) {
|
|
|
370
522
|
}
|
|
371
523
|
var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, width) : `${s.slice(0, width - 1)}\u2026`;
|
|
372
524
|
|
|
373
|
-
// src/ui/theme.ts
|
|
374
|
-
var MARKER = {
|
|
375
|
-
pending: "\u25CB",
|
|
376
|
-
running: "\u25D0",
|
|
377
|
-
done: "\u2713",
|
|
378
|
-
error: "\u2716"
|
|
379
|
-
};
|
|
380
|
-
var BRAND = "#003DFF";
|
|
381
|
-
var SECONDARY = "#5468FF";
|
|
382
|
-
var DANGER = "#F86E7E";
|
|
383
|
-
var COLORS = {
|
|
384
|
-
brand: BRAND,
|
|
385
|
-
primary: "#E6EDF3",
|
|
386
|
-
secondary: SECONDARY,
|
|
387
|
-
strong: "#FFFFFF",
|
|
388
|
-
muted: "#8B949E",
|
|
389
|
-
dim: "#484F58",
|
|
390
|
-
highlight: { bg: "#12331C", fg: "#4ADE80" },
|
|
391
|
-
badge: "#E3B341",
|
|
392
|
-
danger: DANGER,
|
|
393
|
-
success: "#4ADE80",
|
|
394
|
-
bg: {
|
|
395
|
-
main: "#0B0E14",
|
|
396
|
-
sidebar: "#14171E"
|
|
397
|
-
},
|
|
398
|
-
border: "#30363D",
|
|
399
|
-
accent: "#76A0FF",
|
|
400
|
-
status: {
|
|
401
|
-
pending: "gray",
|
|
402
|
-
running: "#76A0FF",
|
|
403
|
-
done: "#4ADE80",
|
|
404
|
-
error: DANGER
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
525
|
// src/ui/Notices.tsx
|
|
409
|
-
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
526
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
410
527
|
var AGENT_MARKER = "\u2726";
|
|
411
|
-
var
|
|
412
|
-
var
|
|
528
|
+
var RESERVED_ROWS2 = 14;
|
|
529
|
+
var PANEL_TEXT_WIDTH2 = 45;
|
|
413
530
|
function messageLineCount(text) {
|
|
414
|
-
return Math.max(1, Math.ceil(text.length /
|
|
531
|
+
return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH2));
|
|
415
532
|
}
|
|
416
533
|
function noticeLineCount(notice) {
|
|
417
534
|
const messageLines = (notice.messages ?? []).reduce((sum, m) => {
|
|
@@ -422,7 +539,7 @@ function noticeLineCount(notice) {
|
|
|
422
539
|
return messageLines + tableLines;
|
|
423
540
|
}
|
|
424
541
|
function fitVisibleNotices(notices, windowRows) {
|
|
425
|
-
const budget = Math.max(windowRows -
|
|
542
|
+
const budget = Math.max(windowRows - RESERVED_ROWS2, 3);
|
|
426
543
|
let used = 0;
|
|
427
544
|
let count = 0;
|
|
428
545
|
for (let i = notices.length - 1; i >= 0; i--) {
|
|
@@ -455,7 +572,7 @@ function parseHex(hex) {
|
|
|
455
572
|
}
|
|
456
573
|
function Notices() {
|
|
457
574
|
const notices = useWizard((s) => s.notices);
|
|
458
|
-
const { rows: windowRows } =
|
|
575
|
+
const { rows: windowRows } = useWindowSize3();
|
|
459
576
|
const visible = fitVisibleNotices(notices, windowRows);
|
|
460
577
|
const [pulseStep, setPulseStep] = useState2(0);
|
|
461
578
|
useEffect2(() => {
|
|
@@ -472,14 +589,14 @@ function Notices() {
|
|
|
472
589
|
}, []);
|
|
473
590
|
if (!visible.length) return null;
|
|
474
591
|
const pulseColor = PULSE_COLORS[pulseStep];
|
|
475
|
-
return /* @__PURE__ */ jsx2(
|
|
592
|
+
return /* @__PURE__ */ jsx2(Box3, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
|
|
476
593
|
const isLatest = i === visible.length - 1;
|
|
477
|
-
return /* @__PURE__ */
|
|
594
|
+
return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
|
|
478
595
|
notice.messages?.map((m, j) => {
|
|
479
596
|
const line = typeof m === "string" ? { text: m } : m;
|
|
480
597
|
const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
|
|
481
|
-
return /* @__PURE__ */
|
|
482
|
-
|
|
598
|
+
return /* @__PURE__ */ jsxs2(
|
|
599
|
+
Text3,
|
|
483
600
|
{
|
|
484
601
|
color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
|
|
485
602
|
bold: line.bold,
|
|
@@ -497,41 +614,42 @@ function Notices() {
|
|
|
497
614
|
}
|
|
498
615
|
|
|
499
616
|
// src/ui/PromptInput.tsx
|
|
500
|
-
import { Box as
|
|
617
|
+
import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
|
|
501
618
|
import TextInput from "ink-text-input";
|
|
502
|
-
import { useState as
|
|
619
|
+
import { useState as useState5 } from "react";
|
|
503
620
|
|
|
504
621
|
// src/ui/NextAction.tsx
|
|
505
|
-
import { Box as
|
|
506
|
-
import { Fragment, jsx as jsx3, jsxs as
|
|
622
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
623
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
507
624
|
function NextAction({
|
|
508
625
|
action,
|
|
509
626
|
keyHint,
|
|
510
627
|
hierarchy = "primary"
|
|
511
628
|
}) {
|
|
512
|
-
return /* @__PURE__ */
|
|
513
|
-
hierarchy === "primary" && /* @__PURE__ */ jsx3(
|
|
514
|
-
hierarchy === "secondary" && /* @__PURE__ */
|
|
515
|
-
/* @__PURE__ */ jsx3(
|
|
516
|
-
/* @__PURE__ */ jsx3(
|
|
629
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
|
|
630
|
+
hierarchy === "primary" && /* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `> ${action}` }),
|
|
631
|
+
hierarchy === "secondary" && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
632
|
+
/* @__PURE__ */ jsx3(Text4, { color: COLORS.success, bold: true, children: `>` }),
|
|
633
|
+
/* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, bold: true, children: action })
|
|
517
634
|
] }),
|
|
518
|
-
/* @__PURE__ */
|
|
519
|
-
/* @__PURE__ */ jsx3(
|
|
520
|
-
/* @__PURE__ */ jsx3(
|
|
521
|
-
/* @__PURE__ */ jsx3(
|
|
522
|
-
/* @__PURE__ */ jsx3(
|
|
635
|
+
/* @__PURE__ */ jsxs3(Box4, { children: [
|
|
636
|
+
/* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: "press " }),
|
|
637
|
+
/* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `[` }),
|
|
638
|
+
/* @__PURE__ */ jsx3(Text4, { color: COLORS.primary, children: keyHint }),
|
|
639
|
+
/* @__PURE__ */ jsx3(Text4, { color: COLORS.muted, children: `]` })
|
|
523
640
|
] })
|
|
524
641
|
] });
|
|
525
642
|
}
|
|
526
643
|
|
|
527
644
|
// src/ui/SelectPrompt.tsx
|
|
528
|
-
import { Box as
|
|
529
|
-
import { useLayoutEffect, useRef as
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
645
|
+
import { Box as Box6, Text as Text6, useInput, useWindowSize as useWindowSize5 } from "ink";
|
|
646
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
|
|
647
|
+
|
|
648
|
+
// src/ui/ScrollView.tsx
|
|
649
|
+
import { Box as Box5, Text as Text5, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
|
|
650
|
+
import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
|
|
651
|
+
import { jsxs as jsxs4 } from "react/jsx-runtime";
|
|
652
|
+
var INDICATOR_ROWS = 2;
|
|
535
653
|
function fittedWidth(node, columns) {
|
|
536
654
|
let left = 0;
|
|
537
655
|
for (let n = node; n; n = n.parentNode) {
|
|
@@ -539,6 +657,89 @@ function fittedWidth(node, columns) {
|
|
|
539
657
|
}
|
|
540
658
|
return Math.max(Math.min(measureElement2(node).width, columns - left), 0);
|
|
541
659
|
}
|
|
660
|
+
function useScrollWindow({
|
|
661
|
+
itemCount,
|
|
662
|
+
rowHeight = 1,
|
|
663
|
+
followBottom = false
|
|
664
|
+
}) {
|
|
665
|
+
const viewportRef = useRef2(null);
|
|
666
|
+
const { columns } = useWindowSize4();
|
|
667
|
+
const [size, setSize] = useState3(
|
|
668
|
+
null
|
|
669
|
+
);
|
|
670
|
+
useLayoutEffect(() => {
|
|
671
|
+
if (!viewportRef.current) return;
|
|
672
|
+
const width = fittedWidth(viewportRef.current, columns);
|
|
673
|
+
const { height } = measureElement2(viewportRef.current);
|
|
674
|
+
setSize(
|
|
675
|
+
(prev) => prev?.width === width && prev.height === height ? prev : { width, height }
|
|
676
|
+
);
|
|
677
|
+
});
|
|
678
|
+
const capacity = size === null || itemCount * rowHeight <= size.height ? itemCount : Math.max(Math.floor((size.height - INDICATOR_ROWS) / rowHeight), 1);
|
|
679
|
+
const maxOffset = Math.max(itemCount - capacity, 0);
|
|
680
|
+
const [offset, setOffset] = useState3(0);
|
|
681
|
+
const prevMaxOffsetRef = useRef2(0);
|
|
682
|
+
useLayoutEffect(() => {
|
|
683
|
+
const wasAtBottom = offset >= prevMaxOffsetRef.current;
|
|
684
|
+
prevMaxOffsetRef.current = maxOffset;
|
|
685
|
+
setOffset(
|
|
686
|
+
(o) => followBottom && wasAtBottom ? maxOffset : Math.min(o, maxOffset)
|
|
687
|
+
);
|
|
688
|
+
}, [maxOffset, followBottom]);
|
|
689
|
+
const scrollBy = useCallback(
|
|
690
|
+
(delta) => {
|
|
691
|
+
setOffset((o) => Math.min(Math.max(o + delta, 0), maxOffset));
|
|
692
|
+
},
|
|
693
|
+
[maxOffset]
|
|
694
|
+
);
|
|
695
|
+
const revealIndex = useCallback(
|
|
696
|
+
(index) => {
|
|
697
|
+
setOffset((o) => {
|
|
698
|
+
if (index < o) return index;
|
|
699
|
+
if (index >= o + capacity) {
|
|
700
|
+
return Math.min(index - capacity + 1, maxOffset);
|
|
701
|
+
}
|
|
702
|
+
return o;
|
|
703
|
+
});
|
|
704
|
+
},
|
|
705
|
+
[capacity, maxOffset]
|
|
706
|
+
);
|
|
707
|
+
const visibleCount = Math.min(capacity, Math.max(itemCount - offset, 0));
|
|
708
|
+
return {
|
|
709
|
+
viewportRef,
|
|
710
|
+
width: size?.width ?? columns,
|
|
711
|
+
offset,
|
|
712
|
+
capacity,
|
|
713
|
+
maxOffset,
|
|
714
|
+
hiddenAbove: Math.min(offset, itemCount),
|
|
715
|
+
hiddenBelow: Math.max(itemCount - offset - visibleCount, 0),
|
|
716
|
+
scrollBy,
|
|
717
|
+
revealIndex
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function ScrollView({ scroll, children }) {
|
|
721
|
+
return /* @__PURE__ */ jsxs4(Box5, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
|
|
722
|
+
scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
|
|
723
|
+
"\u2191 ",
|
|
724
|
+
scroll.hiddenAbove,
|
|
725
|
+
" more"
|
|
726
|
+
] }),
|
|
727
|
+
children,
|
|
728
|
+
scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text5, { color: COLORS.dim, children: [
|
|
729
|
+
"\u2193 ",
|
|
730
|
+
scroll.hiddenBelow,
|
|
731
|
+
" more"
|
|
732
|
+
] })
|
|
733
|
+
] });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/ui/SelectPrompt.tsx
|
|
737
|
+
import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
738
|
+
var CANCEL = "cancel";
|
|
739
|
+
var ARROW_WIDTH = 4;
|
|
740
|
+
var COLUMN_GAP = 2;
|
|
741
|
+
var BAR_PADDING = 2;
|
|
742
|
+
var ROW_HEIGHT = 3;
|
|
542
743
|
function SelectPrompt({
|
|
543
744
|
options,
|
|
544
745
|
onSelect,
|
|
@@ -552,10 +753,10 @@ function SelectPrompt({
|
|
|
552
753
|
secondary,
|
|
553
754
|
defaultSelectedIndex = 0
|
|
554
755
|
}) {
|
|
555
|
-
const [index, setIndex] =
|
|
756
|
+
const [index, setIndex] = useState4(
|
|
556
757
|
() => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
|
|
557
758
|
);
|
|
558
|
-
const [checked, setChecked] =
|
|
759
|
+
const [checked, setChecked] = useState4(() => /* @__PURE__ */ new Set());
|
|
559
760
|
const hasCancel = Boolean(multi || cancelable);
|
|
560
761
|
const rows = hasCancel ? [...options, "Cancel"] : options;
|
|
561
762
|
const cancelIndex = hasCancel ? options.length : -1;
|
|
@@ -563,14 +764,14 @@ function SelectPrompt({
|
|
|
563
764
|
if (rows.length > 1) hints.push({ key: "[\u2191] [\u2193]", label: "move" });
|
|
564
765
|
if (multi) hints.push({ key: "[space]", label: "select" });
|
|
565
766
|
hints.push({ key: "[enter]", label: "confirm" });
|
|
566
|
-
const containerRef =
|
|
567
|
-
const { columns } =
|
|
568
|
-
const [width, setWidth] =
|
|
569
|
-
|
|
570
|
-
if (containerRef.current)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
767
|
+
const containerRef = useRef3(null);
|
|
768
|
+
const { columns } = useWindowSize5();
|
|
769
|
+
const [width, setWidth] = useState4(columns);
|
|
770
|
+
useLayoutEffect2(() => {
|
|
771
|
+
if (!containerRef.current) return;
|
|
772
|
+
const measured = fittedWidth(containerRef.current, columns);
|
|
773
|
+
setWidth((prev) => prev === measured ? prev : measured);
|
|
774
|
+
});
|
|
574
775
|
const inner = Math.max(width - BAR_PADDING, 0);
|
|
575
776
|
const labelWidth = Math.min(
|
|
576
777
|
ARROW_WIDTH + (multi ? 2 : 0) + Math.max(0, ...rows.map((opt) => opt.length)) + COLUMN_GAP,
|
|
@@ -586,6 +787,15 @@ function SelectPrompt({
|
|
|
586
787
|
const barWidth = Math.min(labelWidth + badgeWidth + BAR_PADDING, width);
|
|
587
788
|
const barLabelWidth = Math.max(barWidth - BAR_PADDING - badgeWidth, 0);
|
|
588
789
|
const textWidth = inner - labelWidth;
|
|
790
|
+
const scroll = useScrollWindow({
|
|
791
|
+
itemCount: rows.length,
|
|
792
|
+
rowHeight: ROW_HEIGHT
|
|
793
|
+
});
|
|
794
|
+
const { revealIndex } = scroll;
|
|
795
|
+
useLayoutEffect2(() => {
|
|
796
|
+
revealIndex(index);
|
|
797
|
+
}, [index, revealIndex]);
|
|
798
|
+
const visible = rows.slice(scroll.offset, scroll.offset + scroll.capacity);
|
|
589
799
|
useInput((input, key) => {
|
|
590
800
|
if (rows.length === 0) return;
|
|
591
801
|
if (key.upArrow || input === "k") {
|
|
@@ -609,62 +819,65 @@ function SelectPrompt({
|
|
|
609
819
|
}
|
|
610
820
|
}
|
|
611
821
|
});
|
|
612
|
-
return /* @__PURE__ */ jsx4(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
822
|
+
return /* @__PURE__ */ jsx4(Box6, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, width, children: [
|
|
823
|
+
/* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
|
|
824
|
+
error && /* @__PURE__ */ jsx4(Text6, { color: COLORS.danger, children: error }),
|
|
825
|
+
messages?.map((m, i) => /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: m }, `msg-${i}`)),
|
|
826
|
+
table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
|
|
827
|
+
/* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
828
|
+
question && /* @__PURE__ */ jsx4(Text6, { color: COLORS.muted, children: question }),
|
|
829
|
+
helpText && /* @__PURE__ */ jsx4(Text6, { color: COLORS.dim, children: helpText })
|
|
830
|
+
] })
|
|
619
831
|
] }),
|
|
620
|
-
/* @__PURE__ */ jsx4(
|
|
832
|
+
/* @__PURE__ */ jsx4(ScrollView, { scroll, children: visible.map((option, visibleIndex) => {
|
|
833
|
+
const i = scroll.offset + visibleIndex;
|
|
621
834
|
const highlighted = i === index;
|
|
622
835
|
const isCancel = i === cancelIndex;
|
|
623
836
|
const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
|
|
624
837
|
const sec = isCancel ? void 0 : secondary?.[i];
|
|
625
838
|
const labelColor = highlighted ? COLORS.highlight.fg : void 0;
|
|
626
|
-
const label = /* @__PURE__ */
|
|
839
|
+
const label = /* @__PURE__ */ jsxs5(Text6, { color: labelColor, wrap: "truncate", children: [
|
|
627
840
|
highlighted ? "\u276F " : " ",
|
|
628
841
|
bullet,
|
|
629
842
|
option
|
|
630
843
|
] });
|
|
631
844
|
const isText = sec?.kind === "text";
|
|
632
|
-
return /* @__PURE__ */
|
|
633
|
-
|
|
845
|
+
return /* @__PURE__ */ jsxs5(
|
|
846
|
+
Box6,
|
|
634
847
|
{
|
|
635
848
|
width: isText ? "100%" : barWidth,
|
|
636
849
|
paddingX: 1,
|
|
637
850
|
paddingY: 1,
|
|
638
851
|
backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
|
|
639
852
|
children: [
|
|
640
|
-
/* @__PURE__ */ jsx4(
|
|
641
|
-
isText && textWidth > 0 && /* @__PURE__ */ jsx4(
|
|
642
|
-
|
|
853
|
+
/* @__PURE__ */ jsx4(Box6, { width: isText ? labelWidth : barLabelWidth, children: label }),
|
|
854
|
+
isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box6, { width: textWidth, children: /* @__PURE__ */ jsx4(
|
|
855
|
+
Text6,
|
|
643
856
|
{
|
|
644
857
|
wrap: "truncate",
|
|
645
858
|
color: highlighted ? COLORS.primary : COLORS.muted,
|
|
646
859
|
children: sec.value
|
|
647
860
|
}
|
|
648
861
|
) }),
|
|
649
|
-
sec?.kind === "badge" && /* @__PURE__ */ jsx4(
|
|
862
|
+
sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box6, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text6, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
|
|
650
863
|
]
|
|
651
864
|
},
|
|
652
865
|
`row-${i}`
|
|
653
866
|
);
|
|
654
867
|
}) }),
|
|
655
|
-
/* @__PURE__ */ jsx4(
|
|
868
|
+
/* @__PURE__ */ jsx4(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text6, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs5(Text6, { children: [
|
|
656
869
|
i > 0 ? " " : "",
|
|
657
|
-
/* @__PURE__ */ jsx4(
|
|
658
|
-
/* @__PURE__ */
|
|
870
|
+
/* @__PURE__ */ jsx4(Text6, { color: COLORS.primary, children: key }),
|
|
871
|
+
/* @__PURE__ */ jsxs5(Text6, { color: COLORS.dim, children: [
|
|
659
872
|
" ",
|
|
660
873
|
label
|
|
661
874
|
] })
|
|
662
|
-
] }, label)) })
|
|
875
|
+
] }, label)) }) })
|
|
663
876
|
] }) });
|
|
664
877
|
}
|
|
665
878
|
|
|
666
879
|
// src/ui/PromptInput.tsx
|
|
667
|
-
import { jsx as jsx5, jsxs as
|
|
880
|
+
import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
668
881
|
var ACCEPT_REJECT_OPTIONS = ["Accept", "Reject"];
|
|
669
882
|
function EnterToContinuePrompt({
|
|
670
883
|
question,
|
|
@@ -675,10 +888,10 @@ function EnterToContinuePrompt({
|
|
|
675
888
|
if (key.return) onDecide(true);
|
|
676
889
|
else if (key.escape) onDecide(false);
|
|
677
890
|
});
|
|
678
|
-
return /* @__PURE__ */
|
|
679
|
-
messages?.map((m, i) => /* @__PURE__ */ jsx5(
|
|
680
|
-
question && /* @__PURE__ */ jsx5(
|
|
681
|
-
/* @__PURE__ */
|
|
891
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", gap: 1, children: [
|
|
892
|
+
messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
|
|
893
|
+
question && /* @__PURE__ */ jsx5(Text7, { color: COLORS.primary, children: question }),
|
|
894
|
+
/* @__PURE__ */ jsxs6(Box7, { gap: 1, flexDirection: "column", children: [
|
|
682
895
|
/* @__PURE__ */ jsx5(NextAction, { action: "continue", keyHint: "enter" }),
|
|
683
896
|
/* @__PURE__ */ jsx5(NextAction, { action: "decline", keyHint: "esc", hierarchy: "secondary" })
|
|
684
897
|
] })
|
|
@@ -686,13 +899,13 @@ function EnterToContinuePrompt({
|
|
|
686
899
|
}
|
|
687
900
|
function PromptInput() {
|
|
688
901
|
const { phase, inputReq, submitInput } = useWizard();
|
|
689
|
-
const [draft, setDraft] =
|
|
902
|
+
const [draft, setDraft] = useState5("");
|
|
690
903
|
if (phase === "done" || phase === "error") {
|
|
691
|
-
return /* @__PURE__ */ jsx5(
|
|
904
|
+
return /* @__PURE__ */ jsx5(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text7, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
|
|
692
905
|
}
|
|
693
906
|
if (phase !== "awaitingInput" || !inputReq) return null;
|
|
694
907
|
if (inputReq.promptType === "multipleChoice") {
|
|
695
|
-
return /* @__PURE__ */ jsx5(
|
|
908
|
+
return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
|
|
696
909
|
SelectPrompt,
|
|
697
910
|
{
|
|
698
911
|
question: inputReq.prompt,
|
|
@@ -709,7 +922,7 @@ function PromptInput() {
|
|
|
709
922
|
) });
|
|
710
923
|
}
|
|
711
924
|
if (inputReq.promptType === "multiSelect") {
|
|
712
|
-
return /* @__PURE__ */ jsx5(
|
|
925
|
+
return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
|
|
713
926
|
SelectPrompt,
|
|
714
927
|
{
|
|
715
928
|
multi: true,
|
|
@@ -724,7 +937,7 @@ function PromptInput() {
|
|
|
724
937
|
) });
|
|
725
938
|
}
|
|
726
939
|
if (inputReq.promptType === "notice") {
|
|
727
|
-
return /* @__PURE__ */ jsx5(
|
|
940
|
+
return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
|
|
728
941
|
SelectPrompt,
|
|
729
942
|
{
|
|
730
943
|
question: inputReq.prompt,
|
|
@@ -746,7 +959,7 @@ function PromptInput() {
|
|
|
746
959
|
}
|
|
747
960
|
if (inputReq.promptType === "acceptReject") {
|
|
748
961
|
const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
|
|
749
|
-
return /* @__PURE__ */ jsx5(
|
|
962
|
+
return /* @__PURE__ */ jsx5(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
|
|
750
963
|
SelectPrompt,
|
|
751
964
|
{
|
|
752
965
|
question: inputReq.prompt,
|
|
@@ -757,11 +970,11 @@ function PromptInput() {
|
|
|
757
970
|
}
|
|
758
971
|
) });
|
|
759
972
|
}
|
|
760
|
-
return /* @__PURE__ */
|
|
761
|
-
inputReq.error && /* @__PURE__ */ jsx5(
|
|
762
|
-
inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(
|
|
763
|
-
/* @__PURE__ */
|
|
764
|
-
/* @__PURE__ */
|
|
973
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
|
|
974
|
+
inputReq.error && /* @__PURE__ */ jsx5(Text7, { color: COLORS.danger, children: inputReq.error }),
|
|
975
|
+
inputReq.messages?.map((m, i) => /* @__PURE__ */ jsx5(Text7, { color: COLORS.muted, children: m }, `msg-${i}`)),
|
|
976
|
+
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
977
|
+
/* @__PURE__ */ jsxs6(Text7, { color: COLORS.primary, children: [
|
|
765
978
|
inputReq.prompt,
|
|
766
979
|
" "
|
|
767
980
|
] }),
|
|
@@ -783,7 +996,7 @@ function PromptInput() {
|
|
|
783
996
|
// src/ui/Welcome.tsx
|
|
784
997
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
785
998
|
import { fileURLToPath } from "node:url";
|
|
786
|
-
import { Box as
|
|
999
|
+
import { Box as Box8, Spacer, Text as Text8, useInput as useInput3, useWindowSize as useWindowSize6 } from "ink";
|
|
787
1000
|
|
|
788
1001
|
// src/ui/copy/welcome.ts
|
|
789
1002
|
var sidebarItems = [
|
|
@@ -796,12 +1009,12 @@ var sidebarItems = [
|
|
|
796
1009
|
description: "push 100 records to Algolia in seconds"
|
|
797
1010
|
},
|
|
798
1011
|
{
|
|
799
|
-
title: "detect your
|
|
800
|
-
description: "React, Vue, Angular,
|
|
1012
|
+
title: "detect your framework",
|
|
1013
|
+
description: "React, Vue, Angular, Vanilla JS"
|
|
801
1014
|
},
|
|
802
1015
|
{
|
|
803
1016
|
title: "scaffold a search UI",
|
|
804
|
-
description: "a styled InstantSearch
|
|
1017
|
+
description: "a styled InstantSearch component, wired into your app"
|
|
805
1018
|
},
|
|
806
1019
|
{
|
|
807
1020
|
title: "ship it",
|
|
@@ -811,27 +1024,27 @@ var sidebarItems = [
|
|
|
811
1024
|
|
|
812
1025
|
// src/ui/Welcome.tsx
|
|
813
1026
|
import Image, { InkPictureProvider } from "ink-picture";
|
|
814
|
-
import { jsx as jsx6, jsxs as
|
|
1027
|
+
import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
815
1028
|
var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
|
|
816
1029
|
function SidebarItem({
|
|
817
1030
|
title,
|
|
818
1031
|
description
|
|
819
1032
|
}) {
|
|
820
|
-
return /* @__PURE__ */
|
|
821
|
-
/* @__PURE__ */
|
|
822
|
-
/* @__PURE__ */ jsx6(
|
|
823
|
-
/* @__PURE__ */ jsx6(
|
|
1033
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
|
|
1034
|
+
/* @__PURE__ */ jsxs7(Box8, { gap: 1, children: [
|
|
1035
|
+
/* @__PURE__ */ jsx6(Text8, { color: COLORS.success, children: "\u2192" }),
|
|
1036
|
+
/* @__PURE__ */ jsx6(Text8, { color: COLORS.strong, bold: true, children: title })
|
|
824
1037
|
] }),
|
|
825
|
-
/* @__PURE__ */
|
|
1038
|
+
/* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", gap: 2, children: [
|
|
826
1039
|
/* @__PURE__ */ jsx6(Spacer, {}),
|
|
827
|
-
/* @__PURE__ */ jsx6(
|
|
1040
|
+
/* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: description })
|
|
828
1041
|
] })
|
|
829
1042
|
] });
|
|
830
1043
|
}
|
|
831
1044
|
function Welcome() {
|
|
832
1045
|
const confirmStart = useWizard((s) => s.confirmStart);
|
|
833
1046
|
const openLearnMore = useWizard((s) => s.openLearnMore);
|
|
834
|
-
const { rows } =
|
|
1047
|
+
const { rows } = useWindowSize6();
|
|
835
1048
|
useInput3((input, key) => {
|
|
836
1049
|
if (key.return) confirmStart();
|
|
837
1050
|
else if (input === "i") openLearnMore();
|
|
@@ -850,15 +1063,15 @@ function Welcome() {
|
|
|
850
1063
|
if (rows < 30) {
|
|
851
1064
|
layout = scales["small"];
|
|
852
1065
|
}
|
|
853
|
-
return /* @__PURE__ */
|
|
1066
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
|
|
854
1067
|
/* @__PURE__ */ jsx6(
|
|
855
|
-
|
|
1068
|
+
Box8,
|
|
856
1069
|
{
|
|
857
1070
|
paddingY: layout.main.padding.y,
|
|
858
1071
|
paddingX: layout.main.padding.x,
|
|
859
1072
|
flexDirection: "column",
|
|
860
1073
|
justifyContent: "center",
|
|
861
|
-
children: /* @__PURE__ */
|
|
1074
|
+
children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
|
|
862
1075
|
/* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
|
|
863
1076
|
Image,
|
|
864
1077
|
{
|
|
@@ -870,16 +1083,16 @@ function Welcome() {
|
|
|
870
1083
|
protocol: "halfBlock"
|
|
871
1084
|
}
|
|
872
1085
|
) }),
|
|
873
|
-
/* @__PURE__ */ jsx6(
|
|
874
|
-
/* @__PURE__ */
|
|
1086
|
+
/* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
|
|
1087
|
+
/* @__PURE__ */ jsxs7(Box8, { gap: 1, flexDirection: "column", children: [
|
|
875
1088
|
/* @__PURE__ */ jsx6(NextAction, { action: "start wizard", keyHint: "enter" }),
|
|
876
1089
|
/* @__PURE__ */ jsx6(NextAction, { action: "learn more", keyHint: "i", hierarchy: "secondary" })
|
|
877
1090
|
] })
|
|
878
1091
|
] })
|
|
879
1092
|
}
|
|
880
1093
|
),
|
|
881
|
-
/* @__PURE__ */
|
|
882
|
-
|
|
1094
|
+
/* @__PURE__ */ jsxs7(
|
|
1095
|
+
Box8,
|
|
883
1096
|
{
|
|
884
1097
|
backgroundColor: COLORS.bg.sidebar,
|
|
885
1098
|
width: 40,
|
|
@@ -889,7 +1102,7 @@ function Welcome() {
|
|
|
889
1102
|
flexDirection: "column",
|
|
890
1103
|
justifyContent: "center",
|
|
891
1104
|
children: [
|
|
892
|
-
/* @__PURE__ */ jsx6(
|
|
1105
|
+
/* @__PURE__ */ jsx6(Text8, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
|
|
893
1106
|
sidebarItems.map((i, idx) => /* @__PURE__ */ jsx6(SidebarItem, { title: i.title, description: i.description }, idx))
|
|
894
1107
|
]
|
|
895
1108
|
}
|
|
@@ -899,7 +1112,7 @@ function Welcome() {
|
|
|
899
1112
|
|
|
900
1113
|
// src/ui/LearnMore.tsx
|
|
901
1114
|
import { Fragment as Fragment2 } from "react";
|
|
902
|
-
import { Box as
|
|
1115
|
+
import { Box as Box9, Text as Text9, useInput as useInput4, useWindowSize as useWindowSize7 } from "ink";
|
|
903
1116
|
|
|
904
1117
|
// src/ui/copy/learn-more.ts
|
|
905
1118
|
var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
|
|
@@ -907,7 +1120,7 @@ var accessItems = [
|
|
|
907
1120
|
{
|
|
908
1121
|
tag: "READ",
|
|
909
1122
|
title: "Project files",
|
|
910
|
-
description: "reads
|
|
1123
|
+
description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
|
|
911
1124
|
},
|
|
912
1125
|
{
|
|
913
1126
|
tag: "WRITE",
|
|
@@ -936,7 +1149,7 @@ var policyLinks = [
|
|
|
936
1149
|
];
|
|
937
1150
|
|
|
938
1151
|
// src/ui/LearnMore.tsx
|
|
939
|
-
import { jsx as jsx7, jsxs as
|
|
1152
|
+
import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
940
1153
|
var TAG_COLORS = {
|
|
941
1154
|
READ: COLORS.success,
|
|
942
1155
|
WRITE: COLORS.badge,
|
|
@@ -952,25 +1165,25 @@ function NeverLine({
|
|
|
952
1165
|
}) {
|
|
953
1166
|
const used = segments.reduce((n, s) => n + s.text.length, 0);
|
|
954
1167
|
const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
|
|
955
|
-
return /* @__PURE__ */
|
|
956
|
-
/* @__PURE__ */ jsx7(
|
|
1168
|
+
return /* @__PURE__ */ jsxs8(Text9, { children: [
|
|
1169
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" }),
|
|
957
1170
|
" ".repeat(NEVER_BOX_PAD_X),
|
|
958
|
-
segments.map((s, i) => /* @__PURE__ */ jsx7(
|
|
1171
|
+
segments.map((s, i) => /* @__PURE__ */ jsx7(Text9, { color: s.color, bold: s.bold, children: s.text }, i)),
|
|
959
1172
|
" ".repeat(rightPad),
|
|
960
|
-
/* @__PURE__ */ jsx7(
|
|
1173
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: "\u2502" })
|
|
961
1174
|
] });
|
|
962
1175
|
}
|
|
963
1176
|
function LearnMore() {
|
|
964
1177
|
const confirmStart = useWizard((s) => s.confirmStart);
|
|
965
1178
|
const backToHome = useWizard((s) => s.backToHome);
|
|
966
|
-
const { columns } =
|
|
1179
|
+
const { columns } = useWindowSize7();
|
|
967
1180
|
const dividerWidth = Math.max(0, columns - PADDING_X * 2);
|
|
968
1181
|
useInput4((_input, key) => {
|
|
969
1182
|
if (key.escape) backToHome();
|
|
970
1183
|
else if (key.return) confirmStart();
|
|
971
1184
|
});
|
|
972
|
-
return /* @__PURE__ */
|
|
973
|
-
|
|
1185
|
+
return /* @__PURE__ */ jsxs8(
|
|
1186
|
+
Box9,
|
|
974
1187
|
{
|
|
975
1188
|
flexDirection: "column",
|
|
976
1189
|
paddingX: PADDING_X,
|
|
@@ -978,20 +1191,20 @@ function LearnMore() {
|
|
|
978
1191
|
width: "100%",
|
|
979
1192
|
gap: 1,
|
|
980
1193
|
children: [
|
|
981
|
-
/* @__PURE__ */ jsx7(
|
|
982
|
-
/* @__PURE__ */ jsx7(
|
|
983
|
-
/* @__PURE__ */ jsx7(
|
|
984
|
-
/* @__PURE__ */ jsx7(
|
|
985
|
-
/* @__PURE__ */
|
|
986
|
-
/* @__PURE__ */ jsx7(
|
|
987
|
-
/* @__PURE__ */ jsx7(
|
|
988
|
-
/* @__PURE__ */ jsx7(
|
|
989
|
-
/* @__PURE__ */ jsx7(
|
|
1194
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
|
|
1195
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: accessIntro }),
|
|
1196
|
+
/* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", marginTop: 1, children: [
|
|
1197
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
|
|
1198
|
+
/* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, marginTop: 1, children: [
|
|
1199
|
+
/* @__PURE__ */ jsx7(Box9, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx7(Text9, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
|
|
1200
|
+
/* @__PURE__ */ jsx7(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text9, { children: [
|
|
1201
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: item.title }),
|
|
1202
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
|
|
990
1203
|
] }) })
|
|
991
1204
|
] })
|
|
992
1205
|
] }, item.tag)) }),
|
|
993
|
-
/* @__PURE__ */
|
|
994
|
-
/* @__PURE__ */ jsx7(
|
|
1206
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
1207
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
|
|
995
1208
|
/* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
|
|
996
1209
|
/* @__PURE__ */ jsx7(
|
|
997
1210
|
NeverLine,
|
|
@@ -1000,7 +1213,7 @@ function LearnMore() {
|
|
|
1000
1213
|
segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
|
|
1001
1214
|
}
|
|
1002
1215
|
),
|
|
1003
|
-
neverItems.map((item) => /* @__PURE__ */
|
|
1216
|
+
neverItems.map((item) => /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
1004
1217
|
/* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
|
|
1005
1218
|
/* @__PURE__ */ jsx7(
|
|
1006
1219
|
NeverLine,
|
|
@@ -1015,23 +1228,23 @@ function LearnMore() {
|
|
|
1015
1228
|
)
|
|
1016
1229
|
] }, item)),
|
|
1017
1230
|
/* @__PURE__ */ jsx7(NeverLine, { width: dividerWidth }),
|
|
1018
|
-
/* @__PURE__ */ jsx7(
|
|
1231
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
|
|
1019
1232
|
] }),
|
|
1020
|
-
/* @__PURE__ */ jsx7(
|
|
1021
|
-
/* @__PURE__ */ jsx7(
|
|
1022
|
-
/* @__PURE__ */ jsx7(
|
|
1233
|
+
/* @__PURE__ */ jsx7(Box9, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
|
|
1234
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
|
|
1235
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.accent, children: link.url })
|
|
1023
1236
|
] }, link.label)) }),
|
|
1024
|
-
/* @__PURE__ */
|
|
1025
|
-
/* @__PURE__ */
|
|
1026
|
-
/* @__PURE__ */ jsx7(
|
|
1027
|
-
/* @__PURE__ */ jsx7(
|
|
1028
|
-
/* @__PURE__ */ jsx7(
|
|
1237
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "row", gap: 3, children: [
|
|
1238
|
+
/* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
|
|
1239
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
|
|
1240
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "esc" }),
|
|
1241
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "] back" })
|
|
1029
1242
|
] }),
|
|
1030
|
-
/* @__PURE__ */
|
|
1031
|
-
/* @__PURE__ */ jsx7(
|
|
1032
|
-
/* @__PURE__ */ jsx7(
|
|
1033
|
-
/* @__PURE__ */ jsx7(
|
|
1034
|
-
/* @__PURE__ */ jsx7(
|
|
1243
|
+
/* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", gap: 1, children: [
|
|
1244
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "[" }),
|
|
1245
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: "enter" }),
|
|
1246
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: "]" }),
|
|
1247
|
+
/* @__PURE__ */ jsx7(Text9, { color: COLORS.success, bold: true, children: "start wizard" })
|
|
1035
1248
|
] })
|
|
1036
1249
|
] })
|
|
1037
1250
|
]
|
|
@@ -1040,10 +1253,10 @@ function LearnMore() {
|
|
|
1040
1253
|
}
|
|
1041
1254
|
|
|
1042
1255
|
// src/ui/Sidebar.tsx
|
|
1043
|
-
import { Box as
|
|
1256
|
+
import { Box as Box12, Text as Text12 } from "ink";
|
|
1044
1257
|
|
|
1045
1258
|
// src/ui/Steps.tsx
|
|
1046
|
-
import { Box as
|
|
1259
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1047
1260
|
import Spinner from "ink-spinner";
|
|
1048
1261
|
|
|
1049
1262
|
// src/core/persistence.ts
|
|
@@ -1072,11 +1285,11 @@ async function clearWorkflowState(workflowId) {
|
|
|
1072
1285
|
}
|
|
1073
1286
|
|
|
1074
1287
|
// src/ui/Steps.tsx
|
|
1075
|
-
import { jsx as jsx8, jsxs as
|
|
1288
|
+
import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1076
1289
|
function Steps() {
|
|
1077
1290
|
const { steps } = useWizard();
|
|
1078
1291
|
const visibleSteps = steps.filter(isStepVisible);
|
|
1079
|
-
return /* @__PURE__ */ jsx8(
|
|
1292
|
+
return /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status[s.status], children: [
|
|
1080
1293
|
s.status === "running" ? /* @__PURE__ */ jsx8(Spinner, { type: "dots" }) : MARKER[s.status],
|
|
1081
1294
|
" ",
|
|
1082
1295
|
s.title
|
|
@@ -1086,7 +1299,7 @@ function CurrentStep() {
|
|
|
1086
1299
|
const { steps } = useWizard();
|
|
1087
1300
|
const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
|
|
1088
1301
|
if (!currentStep) return null;
|
|
1089
|
-
return /* @__PURE__ */
|
|
1302
|
+
return /* @__PURE__ */ jsxs9(Text10, { color: COLORS.status.running, children: [
|
|
1090
1303
|
/* @__PURE__ */ jsx8(Spinner, { type: "dots" }),
|
|
1091
1304
|
" ",
|
|
1092
1305
|
` ${currentStep.title}`
|
|
@@ -1094,19 +1307,19 @@ function CurrentStep() {
|
|
|
1094
1307
|
}
|
|
1095
1308
|
|
|
1096
1309
|
// src/ui/Progress.tsx
|
|
1097
|
-
import { Box as
|
|
1098
|
-
import { jsx as jsx9, jsxs as
|
|
1310
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
1311
|
+
import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1099
1312
|
function Progress() {
|
|
1100
1313
|
const { steps, currentStepIndex } = useWizard();
|
|
1101
1314
|
const visibleSteps = steps.filter(isStepVisible);
|
|
1102
1315
|
if (visibleSteps.length === 0) return null;
|
|
1103
1316
|
const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
|
|
1104
1317
|
const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
|
|
1105
|
-
return /* @__PURE__ */
|
|
1106
|
-
/* @__PURE__ */ jsx9(
|
|
1107
|
-
/* @__PURE__ */ jsx9(
|
|
1108
|
-
/* @__PURE__ */ jsx9(
|
|
1109
|
-
/* @__PURE__ */ jsx9(
|
|
1318
|
+
return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
|
|
1319
|
+
/* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "STEP" }),
|
|
1320
|
+
/* @__PURE__ */ jsx9(Text11, { bold: true, children: activeStepNumber }),
|
|
1321
|
+
/* @__PURE__ */ jsx9(Text11, { bold: true, children: "/" }),
|
|
1322
|
+
/* @__PURE__ */ jsx9(Text11, { bold: true, children: visibleSteps.length })
|
|
1110
1323
|
] });
|
|
1111
1324
|
}
|
|
1112
1325
|
|
|
@@ -1117,10 +1330,10 @@ var sidebarCommands = [
|
|
|
1117
1330
|
];
|
|
1118
1331
|
|
|
1119
1332
|
// src/ui/Sidebar.tsx
|
|
1120
|
-
import { jsx as jsx10, jsxs as
|
|
1333
|
+
import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1121
1334
|
function Sidebar() {
|
|
1122
|
-
return /* @__PURE__ */
|
|
1123
|
-
|
|
1335
|
+
return /* @__PURE__ */ jsxs11(
|
|
1336
|
+
Box12,
|
|
1124
1337
|
{
|
|
1125
1338
|
backgroundColor: "#14171E",
|
|
1126
1339
|
width: 30,
|
|
@@ -1129,16 +1342,16 @@ function Sidebar() {
|
|
|
1129
1342
|
flexDirection: "column",
|
|
1130
1343
|
justifyContent: "space-between",
|
|
1131
1344
|
children: [
|
|
1132
|
-
/* @__PURE__ */
|
|
1133
|
-
/* @__PURE__ */ jsx10(
|
|
1345
|
+
/* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
|
|
1346
|
+
/* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: "PROGRESS" }),
|
|
1134
1347
|
/* @__PURE__ */ jsx10(Steps, {})
|
|
1135
1348
|
] }),
|
|
1136
|
-
/* @__PURE__ */
|
|
1349
|
+
/* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", gap: 1, children: [
|
|
1137
1350
|
/* @__PURE__ */ jsx10(Progress, {}),
|
|
1138
|
-
/* @__PURE__ */ jsx10(
|
|
1139
|
-
return /* @__PURE__ */
|
|
1140
|
-
/* @__PURE__ */ jsx10(
|
|
1141
|
-
/* @__PURE__ */ jsx10(
|
|
1351
|
+
/* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: sidebarCommands.map((c) => {
|
|
1352
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: 1, children: [
|
|
1353
|
+
/* @__PURE__ */ jsx10(Text12, { color: COLORS.primary, children: `[${c.keyHint}]` }),
|
|
1354
|
+
/* @__PURE__ */ jsx10(Text12, { color: COLORS.muted, children: c.description })
|
|
1142
1355
|
] });
|
|
1143
1356
|
}) })
|
|
1144
1357
|
] })
|
|
@@ -1148,12 +1361,12 @@ function Sidebar() {
|
|
|
1148
1361
|
}
|
|
1149
1362
|
|
|
1150
1363
|
// src/ui/Ribbon.tsx
|
|
1151
|
-
import { Box as
|
|
1152
|
-
import { jsx as jsx11, jsxs as
|
|
1364
|
+
import { Box as Box13, Text as Text13 } from "ink";
|
|
1365
|
+
import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1153
1366
|
function Ribbon() {
|
|
1154
1367
|
const firstCommand = sidebarCommands[0];
|
|
1155
|
-
return /* @__PURE__ */
|
|
1156
|
-
|
|
1368
|
+
return /* @__PURE__ */ jsxs12(
|
|
1369
|
+
Box13,
|
|
1157
1370
|
{
|
|
1158
1371
|
backgroundColor: "#14171E",
|
|
1159
1372
|
flexDirection: "row",
|
|
@@ -1163,9 +1376,9 @@ function Ribbon() {
|
|
|
1163
1376
|
children: [
|
|
1164
1377
|
/* @__PURE__ */ jsx11(Progress, {}),
|
|
1165
1378
|
/* @__PURE__ */ jsx11(CurrentStep, {}),
|
|
1166
|
-
/* @__PURE__ */
|
|
1167
|
-
/* @__PURE__ */ jsx11(
|
|
1168
|
-
/* @__PURE__ */ jsx11(
|
|
1379
|
+
/* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
|
|
1380
|
+
/* @__PURE__ */ jsx11(Text13, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
|
|
1381
|
+
/* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: firstCommand.description })
|
|
1169
1382
|
] })
|
|
1170
1383
|
]
|
|
1171
1384
|
}
|
|
@@ -1176,9 +1389,8 @@ function Ribbon() {
|
|
|
1176
1389
|
import { useState as useState6 } from "react";
|
|
1177
1390
|
|
|
1178
1391
|
// src/ui/Logs.tsx
|
|
1179
|
-
import {
|
|
1180
|
-
import {
|
|
1181
|
-
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1392
|
+
import { Box as Box14, Text as Text14, useInput as useInput5 } from "ink";
|
|
1393
|
+
import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1182
1394
|
var KIND_COLOR = {
|
|
1183
1395
|
tool: COLORS.primary,
|
|
1184
1396
|
prompt: COLORS.badge
|
|
@@ -1208,75 +1420,32 @@ function formatTimestamp(ms) {
|
|
|
1208
1420
|
}
|
|
1209
1421
|
function Logs() {
|
|
1210
1422
|
const logs = useWizard((s) => s.logs);
|
|
1211
|
-
const {
|
|
1212
|
-
const viewportRef = useRef3(null);
|
|
1213
|
-
const [viewportHeight, setViewportHeight] = useState5(0);
|
|
1214
|
-
const [viewportWidth, setViewportWidth] = useState5(0);
|
|
1215
|
-
const [scrollOffset, setScrollOffset] = useState5(0);
|
|
1216
|
-
const prevMaxOffsetRef = useRef3(0);
|
|
1217
|
-
useLayoutEffect2(() => {
|
|
1218
|
-
if (!viewportRef.current) return;
|
|
1219
|
-
const { width, height } = measureElement3(viewportRef.current);
|
|
1220
|
-
setViewportHeight(height);
|
|
1221
|
-
setViewportWidth(width);
|
|
1222
|
-
}, [rows, columns, logs.length === 0]);
|
|
1223
|
-
let capacity = viewportHeight;
|
|
1224
|
-
for (let i = 0; i < 2; i++) {
|
|
1225
|
-
const hasAbove = scrollOffset > 0;
|
|
1226
|
-
const hasBelow = scrollOffset + capacity < logs.length;
|
|
1227
|
-
capacity = Math.max(
|
|
1228
|
-
viewportHeight - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0),
|
|
1229
|
-
0
|
|
1230
|
-
);
|
|
1231
|
-
}
|
|
1232
|
-
const capacityAtBottom = logs.length > viewportHeight ? Math.max(viewportHeight - 1, 0) : viewportHeight;
|
|
1233
|
-
const maxOffset = Math.max(logs.length - capacityAtBottom, 0);
|
|
1234
|
-
useLayoutEffect2(() => {
|
|
1235
|
-
const wasAtBottom = scrollOffset >= prevMaxOffsetRef.current;
|
|
1236
|
-
prevMaxOffsetRef.current = maxOffset;
|
|
1237
|
-
setScrollOffset((o) => wasAtBottom ? maxOffset : Math.min(o, maxOffset));
|
|
1238
|
-
}, [maxOffset]);
|
|
1423
|
+
const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
|
|
1239
1424
|
useInput5((_input, key) => {
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
1242
|
-
(o) => key.upArrow ? Math.max(o - 1, 0) : Math.min(o + 1, maxOffset)
|
|
1243
|
-
);
|
|
1425
|
+
if (key.upArrow) scroll.scrollBy(-1);
|
|
1426
|
+
else if (key.downArrow) scroll.scrollBy(1);
|
|
1244
1427
|
});
|
|
1245
|
-
const visible = logs.slice(
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", gap: ROW_GAP, children: [
|
|
1267
|
-
/* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: timestamp }),
|
|
1268
|
-
/* @__PURE__ */ jsx12(Text12, { color: logNameColor(entry), wrap: "truncate", children: name }),
|
|
1269
|
-
preview && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, wrap: "truncate", children: preview }),
|
|
1270
|
-
durationText && /* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: durationText })
|
|
1271
|
-
] }, entry.id);
|
|
1272
|
-
}),
|
|
1273
|
-
hiddenBelow > 0 && /* @__PURE__ */ jsxs11(Text12, { color: COLORS.dim, children: [
|
|
1274
|
-
"\u2193 ",
|
|
1275
|
-
hiddenBelow,
|
|
1276
|
-
" more"
|
|
1277
|
-
] })
|
|
1278
|
-
] }),
|
|
1279
|
-
/* @__PURE__ */ jsx12(Text12, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
|
|
1428
|
+
const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
|
|
1429
|
+
return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
|
|
1430
|
+
logs.length === 0 && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "No logs yet." }),
|
|
1431
|
+
/* @__PURE__ */ jsx12(ScrollView, { scroll, children: visible.map((entry) => {
|
|
1432
|
+
const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
|
|
1433
|
+
const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
|
|
1434
|
+
const rawPreview = rawInputText(entry.input);
|
|
1435
|
+
const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
|
|
1436
|
+
const gaps = (partCount - 1) * ROW_GAP;
|
|
1437
|
+
let budget = scroll.width - timestamp.length - durationText.length - gaps;
|
|
1438
|
+
const name = truncate2(entry.name, budget);
|
|
1439
|
+
budget -= name.length;
|
|
1440
|
+
const preview = rawPreview ? truncate2(rawPreview, budget) : "";
|
|
1441
|
+
return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: ROW_GAP, children: [
|
|
1442
|
+
/* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: timestamp }),
|
|
1443
|
+
/* @__PURE__ */ jsx12(Text14, { color: logNameColor(entry), wrap: "truncate", children: name }),
|
|
1444
|
+
preview && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, wrap: "truncate", children: preview }),
|
|
1445
|
+
durationText && /* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: durationText })
|
|
1446
|
+
] }, entry.id);
|
|
1447
|
+
}) }),
|
|
1448
|
+
/* @__PURE__ */ jsx12(Text14, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
|
|
1280
1449
|
] });
|
|
1281
1450
|
}
|
|
1282
1451
|
|
|
@@ -1468,11 +1637,11 @@ function track(event, payload) {
|
|
|
1468
1637
|
}
|
|
1469
1638
|
|
|
1470
1639
|
// src/ui/App.tsx
|
|
1471
|
-
import { jsx as jsx13, jsxs as
|
|
1640
|
+
import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1472
1641
|
function App() {
|
|
1473
1642
|
const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
|
|
1474
1643
|
const { exit } = useApp();
|
|
1475
|
-
const { columns, rows } =
|
|
1644
|
+
const { columns, rows } = useWindowSize8();
|
|
1476
1645
|
const [showLogs, setShowLogs] = useState6(false);
|
|
1477
1646
|
const finished = phase === "done" || phase === "error";
|
|
1478
1647
|
const currentStep = steps[currentStepIndex];
|
|
@@ -1485,7 +1654,7 @@ function App() {
|
|
|
1485
1654
|
{ isActive: finished }
|
|
1486
1655
|
);
|
|
1487
1656
|
useInput6((_input, key) => {
|
|
1488
|
-
if (phase === "idle" || phase === "
|
|
1657
|
+
if (phase === "idle" || phase === "authenticating") return;
|
|
1489
1658
|
if (key.tab) {
|
|
1490
1659
|
setShowLogs(!showLogs);
|
|
1491
1660
|
track("AI Wizard Interaction", {
|
|
@@ -1495,66 +1664,75 @@ function App() {
|
|
|
1495
1664
|
});
|
|
1496
1665
|
}
|
|
1497
1666
|
});
|
|
1498
|
-
const escOwnedElsewhere = phase === "idle" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
|
|
1667
|
+
const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && inputReq?.promptType === "enterToContinue";
|
|
1499
1668
|
useInput6((_input, key) => {
|
|
1500
1669
|
if (escOwnedElsewhere) return;
|
|
1501
1670
|
if (key.escape) {
|
|
1502
1671
|
track("AI Wizard Interaction", {
|
|
1503
1672
|
context: "global",
|
|
1504
1673
|
key: "esc",
|
|
1505
|
-
// No step is active until `startWorkflow` — report the phase instead.
|
|
1506
1674
|
currentStep: currentStep?.id ?? phase
|
|
1507
1675
|
});
|
|
1508
1676
|
exit();
|
|
1509
1677
|
}
|
|
1510
1678
|
});
|
|
1511
|
-
const mainWindowVisible = phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
|
|
1679
|
+
const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
|
|
1512
1680
|
const flexDirection = columns > 90 ? "row" : "column";
|
|
1513
1681
|
const showSidebar = flexDirection === "row";
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1682
|
+
const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
|
|
1683
|
+
return (
|
|
1684
|
+
/* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
|
|
1685
|
+
the whole screen, and the scrolling throws off its cursor arithmetic —
|
|
1686
|
+
flicker and leftover rows. */
|
|
1687
|
+
/* @__PURE__ */ jsxs14(
|
|
1688
|
+
Box15,
|
|
1689
|
+
{
|
|
1690
|
+
backgroundColor: COLORS.bg.main,
|
|
1691
|
+
flexDirection: "row",
|
|
1692
|
+
width: columns,
|
|
1693
|
+
height: scrollsPastViewport ? void 0 : rows,
|
|
1694
|
+
overflow: scrollsPastViewport ? "visible" : "hidden",
|
|
1695
|
+
children: [
|
|
1696
|
+
mainWindowVisible && /* @__PURE__ */ jsxs14(
|
|
1697
|
+
Box15,
|
|
1698
|
+
{
|
|
1699
|
+
flexDirection,
|
|
1700
|
+
width: "100%",
|
|
1701
|
+
maxHeight: rows,
|
|
1702
|
+
justifyContent: "space-between",
|
|
1703
|
+
children: [
|
|
1704
|
+
showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : /* @__PURE__ */ jsxs14(
|
|
1705
|
+
Box15,
|
|
1533
1706
|
{
|
|
1534
1707
|
flexDirection: "column",
|
|
1535
1708
|
paddingX: 4,
|
|
1536
1709
|
paddingY: 2,
|
|
1537
1710
|
width: showSidebar ? 70 : "100%",
|
|
1538
|
-
flexGrow:
|
|
1711
|
+
flexGrow: 1,
|
|
1539
1712
|
children: [
|
|
1713
|
+
phase === "authenticating" && /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginBottom: 1, children: [
|
|
1714
|
+
/* @__PURE__ */ jsx13(Text15, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
|
|
1715
|
+
/* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
|
|
1716
|
+
] }),
|
|
1717
|
+
/* @__PURE__ */ jsx13(CliOutput, {}),
|
|
1540
1718
|
/* @__PURE__ */ jsx13(Notices, {}),
|
|
1541
1719
|
/* @__PURE__ */ jsx13(PromptInput, {}),
|
|
1542
|
-
phase === "running" && showSidebar && /* @__PURE__ */ jsx13(
|
|
1543
|
-
phase === "error" && error && /* @__PURE__ */ jsx13(
|
|
1720
|
+
phase === "running" && showSidebar && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx13(CurrentStep, {}) }),
|
|
1721
|
+
phase === "error" && error && /* @__PURE__ */ jsx13(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text15, { color: COLORS.status.error, children: [
|
|
1544
1722
|
"\u2716 ",
|
|
1545
1723
|
error
|
|
1546
1724
|
] }) })
|
|
1547
1725
|
]
|
|
1548
1726
|
}
|
|
1549
|
-
)
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1727
|
+
),
|
|
1728
|
+
showSidebar ? /* @__PURE__ */ jsx13(Sidebar, {}) : /* @__PURE__ */ jsx13(Ribbon, {})
|
|
1729
|
+
]
|
|
1730
|
+
}
|
|
1731
|
+
),
|
|
1732
|
+
phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx13(LearnMore, {}) : /* @__PURE__ */ jsx13(Welcome, {}))
|
|
1733
|
+
]
|
|
1734
|
+
}
|
|
1735
|
+
)
|
|
1558
1736
|
);
|
|
1559
1737
|
}
|
|
1560
1738
|
|
|
@@ -1568,7 +1746,8 @@ var configFile = () => join5(stateDir(), "config.json");
|
|
|
1568
1746
|
var DEFAULT_CONFIG = {
|
|
1569
1747
|
version: 1,
|
|
1570
1748
|
aiConsent: false,
|
|
1571
|
-
workflowsRun: []
|
|
1749
|
+
workflowsRun: [],
|
|
1750
|
+
searchApiKeys: {}
|
|
1572
1751
|
};
|
|
1573
1752
|
async function loadConfig() {
|
|
1574
1753
|
try {
|
|
@@ -1587,6 +1766,38 @@ async function recordWorkflowRun(workflowId, completedAt) {
|
|
|
1587
1766
|
config.workflowsRun.push({ workflowId, completedAt });
|
|
1588
1767
|
await saveConfig(config);
|
|
1589
1768
|
}
|
|
1769
|
+
function isStoredSearchKey(value) {
|
|
1770
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1771
|
+
const { appId, key } = value;
|
|
1772
|
+
return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
|
|
1773
|
+
}
|
|
1774
|
+
function storedSearchKeys(config) {
|
|
1775
|
+
const stored = config.searchApiKeys;
|
|
1776
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
1777
|
+
return {};
|
|
1778
|
+
}
|
|
1779
|
+
return stored;
|
|
1780
|
+
}
|
|
1781
|
+
async function getStoredSearchKey(index, appId) {
|
|
1782
|
+
const entry = storedSearchKeys(await loadConfig())[index];
|
|
1783
|
+
if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
|
|
1784
|
+
return entry.key;
|
|
1785
|
+
}
|
|
1786
|
+
async function storeSearchKey(index, appId, key) {
|
|
1787
|
+
const config = await loadConfig();
|
|
1788
|
+
config.searchApiKeys = {
|
|
1789
|
+
...storedSearchKeys(config),
|
|
1790
|
+
[index]: { appId, key }
|
|
1791
|
+
};
|
|
1792
|
+
await saveConfig(config);
|
|
1793
|
+
}
|
|
1794
|
+
async function forgetSearchKey(index) {
|
|
1795
|
+
const config = await loadConfig();
|
|
1796
|
+
const remaining = { ...storedSearchKeys(config) };
|
|
1797
|
+
delete remaining[index];
|
|
1798
|
+
config.searchApiKeys = remaining;
|
|
1799
|
+
await saveConfig(config);
|
|
1800
|
+
}
|
|
1590
1801
|
|
|
1591
1802
|
// src/core/orchestrator.ts
|
|
1592
1803
|
function defineStep(step) {
|
|
@@ -1790,61 +2001,138 @@ async function runWorkflow(workflow, appId) {
|
|
|
1790
2001
|
}
|
|
1791
2002
|
}
|
|
1792
2003
|
|
|
1793
|
-
// src/lib/
|
|
1794
|
-
import {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2004
|
+
// src/lib/algoliaApp.ts
|
|
2005
|
+
import { z as z4 } from "zod";
|
|
2006
|
+
var applicationSchema = z4.object({
|
|
2007
|
+
id: z4.string().min(1),
|
|
2008
|
+
name: z4.string().default(""),
|
|
2009
|
+
plan: z4.string().optional()
|
|
2010
|
+
});
|
|
2011
|
+
var listSchema = z4.array(
|
|
2012
|
+
z4.object({
|
|
2013
|
+
id: z4.string().min(1),
|
|
2014
|
+
name: z4.string().default(""),
|
|
2015
|
+
plan_label: z4.string().optional()
|
|
2016
|
+
}).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
|
|
2017
|
+
);
|
|
2018
|
+
async function currentApplication() {
|
|
2019
|
+
let raw;
|
|
1806
2020
|
try {
|
|
1807
|
-
|
|
2021
|
+
raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
|
|
1808
2022
|
} catch {
|
|
1809
|
-
return
|
|
2023
|
+
return null;
|
|
2024
|
+
}
|
|
2025
|
+
const parsed = applicationSchema.safeParse(parseJson(raw));
|
|
2026
|
+
return parsed.success ? parsed.data : null;
|
|
2027
|
+
}
|
|
2028
|
+
async function requireApplication() {
|
|
2029
|
+
const app = await currentApplication();
|
|
2030
|
+
if (!app) {
|
|
2031
|
+
throw new Error(
|
|
2032
|
+
"No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
return app;
|
|
2036
|
+
}
|
|
2037
|
+
async function listApplications() {
|
|
2038
|
+
const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
|
|
2039
|
+
const parsed = listSchema.safeParse(parseJson(raw));
|
|
2040
|
+
if (!parsed.success) {
|
|
2041
|
+
throw new Error("Could not read the list of Algolia applications.");
|
|
2042
|
+
}
|
|
2043
|
+
return parsed.data;
|
|
2044
|
+
}
|
|
2045
|
+
async function selectApplication(id) {
|
|
2046
|
+
const raw = await runAlgoliaCli(
|
|
2047
|
+
["application", "select", "--non-interactive", "--app-id", id],
|
|
2048
|
+
{ onOutput: stderrSink }
|
|
2049
|
+
);
|
|
2050
|
+
const parsed = applicationSchema.safeParse(parseJson(raw));
|
|
2051
|
+
if (!parsed.success) {
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
`Selected application ${id}, but the Algolia CLI returned an unreadable result.`
|
|
2054
|
+
);
|
|
1810
2055
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
name,
|
|
1815
|
-
appId: t.application_id,
|
|
1816
|
-
apiKey: t.api_key,
|
|
1817
|
-
isDefault: t.default === true
|
|
1818
|
-
}));
|
|
1819
|
-
profiles.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
|
|
1820
|
-
return profiles.map(({ name, appId, apiKey }) => ({ name, appId, apiKey }));
|
|
1821
|
-
}
|
|
1822
|
-
async function loadActiveProfile() {
|
|
1823
|
-
let profiles;
|
|
2056
|
+
return parsed.data;
|
|
2057
|
+
}
|
|
2058
|
+
function parseJson(text) {
|
|
1824
2059
|
try {
|
|
1825
|
-
|
|
2060
|
+
return JSON.parse(text);
|
|
1826
2061
|
} catch {
|
|
1827
|
-
|
|
2062
|
+
return void 0;
|
|
1828
2063
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/lib/algoliaAppPicker.ts
|
|
2067
|
+
function secondaryFor(app) {
|
|
2068
|
+
return app.plan ? { kind: "badge", value: app.plan } : void 0;
|
|
2069
|
+
}
|
|
2070
|
+
function labelFor(app) {
|
|
2071
|
+
return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
|
|
2072
|
+
}
|
|
2073
|
+
function selectAndReport(app) {
|
|
2074
|
+
useWizard.getState().pushCliOutput(
|
|
2075
|
+
"stdout",
|
|
2076
|
+
`Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
|
|
2077
|
+
);
|
|
2078
|
+
return selectApplication(app.id);
|
|
2079
|
+
}
|
|
2080
|
+
async function promptForApplication() {
|
|
2081
|
+
const store = useWizard.getState();
|
|
2082
|
+
const apps = await listApplications();
|
|
2083
|
+
if (apps.length === 0) {
|
|
1831
2084
|
throw new Error(
|
|
1832
|
-
"
|
|
2085
|
+
"This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
|
|
1833
2086
|
);
|
|
1834
2087
|
}
|
|
1835
|
-
|
|
2088
|
+
if (apps.length === 1) {
|
|
2089
|
+
const only = apps[0];
|
|
2090
|
+
logger.info(
|
|
2091
|
+
{ app: only.id },
|
|
2092
|
+
"single application on the account; selecting it"
|
|
2093
|
+
);
|
|
2094
|
+
return selectAndReport(only);
|
|
2095
|
+
}
|
|
2096
|
+
const messages = ["Which Algolia application should the wizard work in?"];
|
|
2097
|
+
for (; ; ) {
|
|
2098
|
+
const choice = await store.requestUserInput({
|
|
2099
|
+
prompt: "Select an application",
|
|
2100
|
+
promptType: "multipleChoice",
|
|
2101
|
+
options: apps.map(labelFor),
|
|
2102
|
+
secondary: apps.map(secondaryFor),
|
|
2103
|
+
messages
|
|
2104
|
+
});
|
|
2105
|
+
const chosen = apps.find((app) => labelFor(app) === choice);
|
|
2106
|
+
if (!chosen) {
|
|
2107
|
+
throw new Error("Application picker received an unexpected selection");
|
|
2108
|
+
}
|
|
2109
|
+
try {
|
|
2110
|
+
return await selectAndReport(chosen);
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
logger.warn(
|
|
2113
|
+
{ app: chosen.id, err: err.message },
|
|
2114
|
+
"application select failed; re-prompting"
|
|
2115
|
+
);
|
|
2116
|
+
messages.push(
|
|
2117
|
+
`Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
async function ensureApplication() {
|
|
2123
|
+
return await currentApplication() ?? await promptForApplication();
|
|
1836
2124
|
}
|
|
1837
2125
|
|
|
1838
2126
|
// src/workflows/default.ts
|
|
1839
|
-
import { z as
|
|
2127
|
+
import { z as z27 } from "zod";
|
|
1840
2128
|
|
|
1841
2129
|
// src/actions/listIndices.ts
|
|
1842
|
-
import { z as
|
|
1843
|
-
var indicesListSchema =
|
|
1844
|
-
items:
|
|
1845
|
-
|
|
1846
|
-
name:
|
|
1847
|
-
entries:
|
|
2130
|
+
import { z as z5 } from "zod";
|
|
2131
|
+
var indicesListSchema = z5.object({
|
|
2132
|
+
items: z5.array(
|
|
2133
|
+
z5.object({
|
|
2134
|
+
name: z5.string(),
|
|
2135
|
+
entries: z5.number().default(0)
|
|
1848
2136
|
})
|
|
1849
2137
|
)
|
|
1850
2138
|
});
|
|
@@ -1915,12 +2203,12 @@ import "zod";
|
|
|
1915
2203
|
|
|
1916
2204
|
// src/lib/tools/listFiles.ts
|
|
1917
2205
|
import { tool } from "ai";
|
|
1918
|
-
import
|
|
2206
|
+
import z6 from "zod";
|
|
1919
2207
|
import { readdir } from "node:fs/promises";
|
|
1920
2208
|
|
|
1921
2209
|
// src/lib/tools/path.ts
|
|
1922
2210
|
import { lstat } from "node:fs/promises";
|
|
1923
|
-
import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as
|
|
2211
|
+
import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join6, sep } from "node:path";
|
|
1924
2212
|
function resolveInRoot(ctx, path) {
|
|
1925
2213
|
const target = resolve2(ctx.cwd, path);
|
|
1926
2214
|
const rel = relative(ctx.root, target);
|
|
@@ -1936,7 +2224,7 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
1936
2224
|
let current = ctx.root;
|
|
1937
2225
|
const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
|
|
1938
2226
|
for (const part of parts) {
|
|
1939
|
-
current =
|
|
2227
|
+
current = join6(current, part);
|
|
1940
2228
|
try {
|
|
1941
2229
|
if ((await lstat(current)).isSymbolicLink()) return true;
|
|
1942
2230
|
} catch (err) {
|
|
@@ -1951,7 +2239,7 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
1951
2239
|
function listFilesTool(ctx) {
|
|
1952
2240
|
return tool({
|
|
1953
2241
|
description: "List files in the current working directory",
|
|
1954
|
-
inputSchema:
|
|
2242
|
+
inputSchema: z6.object(),
|
|
1955
2243
|
execute: async () => {
|
|
1956
2244
|
logger.info("called listFiles tool");
|
|
1957
2245
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
@@ -1967,13 +2255,13 @@ function listFilesTool(ctx) {
|
|
|
1967
2255
|
|
|
1968
2256
|
// src/lib/tools/changeDirectory.ts
|
|
1969
2257
|
import { tool as tool2 } from "ai";
|
|
1970
|
-
import
|
|
2258
|
+
import z7 from "zod";
|
|
1971
2259
|
import { stat } from "node:fs/promises";
|
|
1972
2260
|
function changeDirectoryTool(ctx) {
|
|
1973
2261
|
return tool2({
|
|
1974
2262
|
description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
|
|
1975
|
-
inputSchema:
|
|
1976
|
-
path:
|
|
2263
|
+
inputSchema: z7.object({
|
|
2264
|
+
path: z7.string().describe("Directory to change into")
|
|
1977
2265
|
}),
|
|
1978
2266
|
execute: async ({ path }) => {
|
|
1979
2267
|
logger.info({ path }, "called changeDirectory tool");
|
|
@@ -1995,13 +2283,13 @@ function changeDirectoryTool(ctx) {
|
|
|
1995
2283
|
|
|
1996
2284
|
// src/lib/tools/reportStatus.ts
|
|
1997
2285
|
import { tool as tool3 } from "ai";
|
|
1998
|
-
import
|
|
2286
|
+
import z8 from "zod";
|
|
1999
2287
|
function reportStatusTool(output) {
|
|
2000
2288
|
return tool3({
|
|
2001
2289
|
description: "Report the status of your execution. Return a reason in case of failure.",
|
|
2002
|
-
inputSchema:
|
|
2003
|
-
status:
|
|
2004
|
-
reason:
|
|
2290
|
+
inputSchema: z8.object({
|
|
2291
|
+
status: z8.enum(["success", "fail"]),
|
|
2292
|
+
reason: z8.string().optional(),
|
|
2005
2293
|
output
|
|
2006
2294
|
}),
|
|
2007
2295
|
execute: async ({ status, reason, output: output2 }) => {
|
|
@@ -2013,8 +2301,8 @@ function reportStatusTool(output) {
|
|
|
2013
2301
|
|
|
2014
2302
|
// src/lib/tools/readFile.ts
|
|
2015
2303
|
import { tool as tool4 } from "ai";
|
|
2016
|
-
import
|
|
2017
|
-
import { readFile as
|
|
2304
|
+
import z9 from "zod";
|
|
2305
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2018
2306
|
|
|
2019
2307
|
// src/lib/tools/env.ts
|
|
2020
2308
|
import { basename } from "node:path";
|
|
@@ -2041,8 +2329,8 @@ function redactEnvValues(content) {
|
|
|
2041
2329
|
function readFileTool(ctx) {
|
|
2042
2330
|
return tool4({
|
|
2043
2331
|
description: "Read the contents of a file at the given path",
|
|
2044
|
-
inputSchema:
|
|
2045
|
-
filePath:
|
|
2332
|
+
inputSchema: z9.object({
|
|
2333
|
+
filePath: z9.string().describe("Path to the file to read")
|
|
2046
2334
|
}),
|
|
2047
2335
|
execute: async ({ filePath }) => {
|
|
2048
2336
|
if (++ctx.counts.read > ctx.limits.read) {
|
|
@@ -2052,7 +2340,7 @@ function readFileTool(ctx) {
|
|
|
2052
2340
|
const resolved = resolveInRoot(ctx, filePath);
|
|
2053
2341
|
if (!resolved.ok) return resolved.error;
|
|
2054
2342
|
try {
|
|
2055
|
-
const content = await
|
|
2343
|
+
const content = await readFile3(resolved.target, "utf8");
|
|
2056
2344
|
return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
|
|
2057
2345
|
} catch (err) {
|
|
2058
2346
|
return `Error reading ${filePath}: ${err.message}`;
|
|
@@ -2063,15 +2351,15 @@ function readFileTool(ctx) {
|
|
|
2063
2351
|
|
|
2064
2352
|
// src/lib/tools/writeFile.ts
|
|
2065
2353
|
import { tool as tool5 } from "ai";
|
|
2066
|
-
import
|
|
2354
|
+
import z10 from "zod";
|
|
2067
2355
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2068
2356
|
import { dirname as dirname4 } from "node:path";
|
|
2069
2357
|
function writeFileTool(ctx) {
|
|
2070
2358
|
return tool5({
|
|
2071
2359
|
description: "Write content to a file at the given path, overwriting it. To set Algolia credentials in an env file, use writeCredentials instead of this tool.",
|
|
2072
|
-
inputSchema:
|
|
2073
|
-
filePath:
|
|
2074
|
-
content:
|
|
2360
|
+
inputSchema: z10.object({
|
|
2361
|
+
filePath: z10.string().describe("Path to the file to write"),
|
|
2362
|
+
content: z10.string().describe("Content to write to the file")
|
|
2075
2363
|
}),
|
|
2076
2364
|
execute: async ({ filePath, content }) => {
|
|
2077
2365
|
logger.info({ filePath }, "called writeFile tool");
|
|
@@ -2096,9 +2384,125 @@ function writeFileTool(ctx) {
|
|
|
2096
2384
|
|
|
2097
2385
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2098
2386
|
import { tool as tool6 } from "ai";
|
|
2099
|
-
import
|
|
2100
|
-
import { mkdir as mkdir4, readFile as
|
|
2387
|
+
import z12 from "zod";
|
|
2388
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
2101
2389
|
import { dirname as dirname5 } from "node:path";
|
|
2390
|
+
|
|
2391
|
+
// src/lib/algoliaApiKey.ts
|
|
2392
|
+
import { z as z11 } from "zod";
|
|
2393
|
+
var WRITE_ACLS = [
|
|
2394
|
+
"addObject",
|
|
2395
|
+
"deleteObject",
|
|
2396
|
+
"settings",
|
|
2397
|
+
"editSettings",
|
|
2398
|
+
"listIndexes"
|
|
2399
|
+
];
|
|
2400
|
+
var WRITE_ACL_SET = new Set(WRITE_ACLS);
|
|
2401
|
+
var apiKeySchema = z11.object({
|
|
2402
|
+
value: z11.string().min(1),
|
|
2403
|
+
acl: z11.array(z11.string()).default([]),
|
|
2404
|
+
indexes: z11.array(z11.string()).default([])
|
|
2405
|
+
});
|
|
2406
|
+
var apiKeyListSchema = z11.object({
|
|
2407
|
+
items: z11.array(apiKeySchema).optional(),
|
|
2408
|
+
keys: z11.array(apiKeySchema).optional()
|
|
2409
|
+
}).transform((o) => o.items ?? o.keys ?? []);
|
|
2410
|
+
var createdKeySchema = z11.object({
|
|
2411
|
+
key: z11.string().min(1).optional(),
|
|
2412
|
+
value: z11.string().min(1).optional()
|
|
2413
|
+
}).transform((o) => o.key ?? o.value);
|
|
2414
|
+
function canReuseForWrites(key, index) {
|
|
2415
|
+
return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
|
|
2416
|
+
}
|
|
2417
|
+
async function resolveWriteKey(index) {
|
|
2418
|
+
const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
|
|
2419
|
+
const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuseForWrites(key, index))?.value;
|
|
2420
|
+
if (existing) {
|
|
2421
|
+
logger.info({ index }, "reusing existing write API key");
|
|
2422
|
+
return existing;
|
|
2423
|
+
}
|
|
2424
|
+
logger.info({ index }, "no reusable write key found; creating one");
|
|
2425
|
+
const created = await runAlgoliaCli([
|
|
2426
|
+
"apikeys",
|
|
2427
|
+
"create",
|
|
2428
|
+
"--indices",
|
|
2429
|
+
index,
|
|
2430
|
+
"--acl",
|
|
2431
|
+
WRITE_ACLS.join(","),
|
|
2432
|
+
"--description",
|
|
2433
|
+
`wizard write key for ${index}`,
|
|
2434
|
+
"-o",
|
|
2435
|
+
"json"
|
|
2436
|
+
]);
|
|
2437
|
+
const writeKey = createdKeySchema.parse(JSON.parse(created));
|
|
2438
|
+
if (!writeKey) throw new Error("apikeys create returned no key value");
|
|
2439
|
+
return writeKey;
|
|
2440
|
+
}
|
|
2441
|
+
async function createSearchOnlyKey(index) {
|
|
2442
|
+
logger.info({ index }, "creating a search-only API key");
|
|
2443
|
+
const stdout = await runAlgoliaCli([
|
|
2444
|
+
"apikeys",
|
|
2445
|
+
"create",
|
|
2446
|
+
"--acl",
|
|
2447
|
+
"search",
|
|
2448
|
+
"--indices",
|
|
2449
|
+
index,
|
|
2450
|
+
"--description",
|
|
2451
|
+
`Algolia Wizard search-only key for ${index}`,
|
|
2452
|
+
"-o",
|
|
2453
|
+
"json"
|
|
2454
|
+
]);
|
|
2455
|
+
let payload;
|
|
2456
|
+
try {
|
|
2457
|
+
payload = JSON.parse(stdout);
|
|
2458
|
+
} catch {
|
|
2459
|
+
throw new Error("apikeys create returned output that is not valid JSON");
|
|
2460
|
+
}
|
|
2461
|
+
const created = createdKeySchema.parse(payload);
|
|
2462
|
+
if (!created) throw new Error("apikeys create returned no key value");
|
|
2463
|
+
return created;
|
|
2464
|
+
}
|
|
2465
|
+
async function apiKeyExists(key) {
|
|
2466
|
+
try {
|
|
2467
|
+
await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
|
|
2468
|
+
return true;
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
return !/does not exist|not found|404/i.test(err.message);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
async function resolveSearchOnlyKey(index, appId, envKey) {
|
|
2474
|
+
if (envKey) {
|
|
2475
|
+
await recordSearchKey(index, appId, envKey);
|
|
2476
|
+
return { key: envKey, source: "env" };
|
|
2477
|
+
}
|
|
2478
|
+
const stored = await getStoredSearchKey(index, appId);
|
|
2479
|
+
if (stored) {
|
|
2480
|
+
if (await apiKeyExists(stored)) {
|
|
2481
|
+
logger.info({ index, appId }, "reusing the stored search-only API key");
|
|
2482
|
+
return { key: stored, source: "config" };
|
|
2483
|
+
}
|
|
2484
|
+
logger.warn(
|
|
2485
|
+
{ index, appId },
|
|
2486
|
+
"the stored search-only API key no longer exists; creating a replacement"
|
|
2487
|
+
);
|
|
2488
|
+
await forgetSearchKey(index);
|
|
2489
|
+
}
|
|
2490
|
+
const key = await createSearchOnlyKey(index);
|
|
2491
|
+
await recordSearchKey(index, appId, key);
|
|
2492
|
+
return { key, source: "created" };
|
|
2493
|
+
}
|
|
2494
|
+
async function recordSearchKey(index, appId, key) {
|
|
2495
|
+
try {
|
|
2496
|
+
await storeSearchKey(index, appId, key);
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
logger.warn(
|
|
2499
|
+
{ err: err.message, index },
|
|
2500
|
+
"could not record the search-only API key; a later run may create another"
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2102
2506
|
var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
|
|
2103
2507
|
var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
|
|
2104
2508
|
function appendEnv(content, entries) {
|
|
@@ -2112,9 +2516,9 @@ function hasEnv(content, name) {
|
|
|
2112
2516
|
}
|
|
2113
2517
|
function writeCredentialsTool(ctx) {
|
|
2114
2518
|
return tool6({
|
|
2115
|
-
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials
|
|
2116
|
-
inputSchema:
|
|
2117
|
-
filePath:
|
|
2519
|
+
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file (e.g. ".env"). If the file already defines ${APP_ID_VAR} or ${API_KEY_VAR}, the write is skipped and existing values are left untouched.`,
|
|
2520
|
+
inputSchema: z12.object({
|
|
2521
|
+
filePath: z12.string().describe(
|
|
2118
2522
|
'Path to the env file to write credentials into (e.g. ".env")'
|
|
2119
2523
|
)
|
|
2120
2524
|
}),
|
|
@@ -2122,11 +2526,17 @@ function writeCredentialsTool(ctx) {
|
|
|
2122
2526
|
logger.info({ filePath }, "called writeCredentials tool");
|
|
2123
2527
|
const resolved = resolveInRoot(ctx, filePath);
|
|
2124
2528
|
if (resolved.ok === false) return resolved.error;
|
|
2125
|
-
|
|
2529
|
+
const targetIndex = useWizard.getState().targetIndex;
|
|
2530
|
+
if (!targetIndex) {
|
|
2531
|
+
return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
|
|
2532
|
+
}
|
|
2533
|
+
let appId;
|
|
2534
|
+
let writeKey;
|
|
2126
2535
|
try {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2536
|
+
appId = (await requireApplication()).id;
|
|
2537
|
+
writeKey = await resolveWriteKey(targetIndex);
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
|
|
2130
2540
|
}
|
|
2131
2541
|
try {
|
|
2132
2542
|
if (await hasSymlinkParent(ctx, resolved.target)) {
|
|
@@ -2134,7 +2544,7 @@ function writeCredentialsTool(ctx) {
|
|
|
2134
2544
|
}
|
|
2135
2545
|
let existing = "";
|
|
2136
2546
|
try {
|
|
2137
|
-
existing = await
|
|
2547
|
+
existing = await readFile4(resolved.target, "utf8");
|
|
2138
2548
|
} catch (err) {
|
|
2139
2549
|
if (err.code !== "ENOENT") throw err;
|
|
2140
2550
|
}
|
|
@@ -2145,8 +2555,8 @@ function writeCredentialsTool(ctx) {
|
|
|
2145
2555
|
return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
|
|
2146
2556
|
}
|
|
2147
2557
|
const envWithCredentials = appendEnv(existing, [
|
|
2148
|
-
[APP_ID_VAR,
|
|
2149
|
-
[API_KEY_VAR,
|
|
2558
|
+
[APP_ID_VAR, appId],
|
|
2559
|
+
[API_KEY_VAR, writeKey]
|
|
2150
2560
|
]);
|
|
2151
2561
|
await mkdir4(dirname5(resolved.target), { recursive: true });
|
|
2152
2562
|
await writeFile4(resolved.target, envWithCredentials, "utf8");
|
|
@@ -2160,746 +2570,16 @@ function writeCredentialsTool(ctx) {
|
|
|
2160
2570
|
|
|
2161
2571
|
// src/lib/tools/searchFiles.ts
|
|
2162
2572
|
import { tool as tool7 } from "ai";
|
|
2163
|
-
import
|
|
2164
|
-
import { readdir as
|
|
2165
|
-
import { join as
|
|
2166
|
-
|
|
2167
|
-
// src/lib/languages.ts
|
|
2168
|
-
import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
|
|
2169
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
2170
|
-
import { join as join9 } from "node:path";
|
|
2171
|
-
|
|
2172
|
-
// src/lib/tools/utils/packageManager.ts
|
|
2173
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
2174
|
-
import { existsSync } from "node:fs";
|
|
2175
|
-
import { join as join8 } from "node:path";
|
|
2176
|
-
var LOCKFILES = [
|
|
2177
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
2178
|
-
["yarn.lock", "yarn"],
|
|
2179
|
-
["bun.lockb", "bun"],
|
|
2180
|
-
["bun.lock", "bun"],
|
|
2181
|
-
["package-lock.json", "npm"]
|
|
2182
|
-
];
|
|
2183
|
-
async function readPackageJson(cwd = process.cwd()) {
|
|
2184
|
-
return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
|
|
2185
|
-
}
|
|
2186
|
-
function packageManagerFrom(pkg) {
|
|
2187
|
-
return pkg.packageManager?.split("@")[0] ?? "npm";
|
|
2188
|
-
}
|
|
2189
|
-
function packageManagerFromLockfile(cwd) {
|
|
2190
|
-
return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
|
|
2191
|
-
}
|
|
2192
|
-
async function detectPackageManager(cwd) {
|
|
2193
|
-
try {
|
|
2194
|
-
const pkg = await readPackageJson(cwd);
|
|
2195
|
-
if (pkg.packageManager) return packageManagerFrom(pkg);
|
|
2196
|
-
} catch {
|
|
2197
|
-
}
|
|
2198
|
-
return packageManagerFromLockfile(cwd) ?? "npm";
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
// src/lib/shell.ts
|
|
2202
|
-
function shellQuote(value) {
|
|
2203
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
// src/lib/languages.ts
|
|
2207
|
-
var ENTRYPOINT_TOKEN = "{entrypoint}";
|
|
2208
|
-
var INGEST_DIR = ".algolia-wizard";
|
|
2209
|
-
var SWIFT_PACKAGE_DIR = `${INGEST_DIR}/Ingest`;
|
|
2210
|
-
var CSHARP_PROJECT = `${INGEST_DIR}/ingest/ingest.csproj`;
|
|
2211
|
-
var VISIBLE_INGEST_DIR = "algolia-wizard";
|
|
2212
|
-
var PY_VENV = `${INGEST_DIR}/.venv`;
|
|
2213
|
-
var PY_VENV_PYTHON = `${PY_VENV}/bin/python`;
|
|
2214
|
-
var PY_REQUIREMENTS = `${INGEST_DIR}/requirements.txt`;
|
|
2215
|
-
var LANGUAGE_PROFILES = {
|
|
2216
|
-
javascript: {
|
|
2217
|
-
id: "javascript",
|
|
2218
|
-
displayName: "JavaScript/TypeScript",
|
|
2219
|
-
aliases: [
|
|
2220
|
-
"javascript",
|
|
2221
|
-
"js",
|
|
2222
|
-
"typescript",
|
|
2223
|
-
"ts",
|
|
2224
|
-
"node",
|
|
2225
|
-
"nodejs",
|
|
2226
|
-
"node.js",
|
|
2227
|
-
"bun",
|
|
2228
|
-
"deno",
|
|
2229
|
-
"ecmascript",
|
|
2230
|
-
"jsx",
|
|
2231
|
-
"tsx"
|
|
2232
|
-
],
|
|
2233
|
-
manifests: ["package.json"],
|
|
2234
|
-
// The concrete npm-family manager is resolved by detectPackageManager (it
|
|
2235
|
-
// honours the package.json `packageManager` field, which lockfiles can't
|
|
2236
|
-
// express), so one spec covers all four and `resolveToolchain` rewrites the
|
|
2237
|
-
// binary below.
|
|
2238
|
-
packageManagers: [
|
|
2239
|
-
{
|
|
2240
|
-
id: "npm",
|
|
2241
|
-
dependency: { mode: "agent-declares", file: "package.json" },
|
|
2242
|
-
installSteps: [{ argv: ["npm", "install"] }],
|
|
2243
|
-
ingest: {
|
|
2244
|
-
kind: "auto",
|
|
2245
|
-
argv: ["node", ENTRYPOINT_TOKEN],
|
|
2246
|
-
entrypointExtensions: [".mjs", ".cjs", ".js"]
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
],
|
|
2250
|
-
sdk: { packageName: "algoliasearch", versionPin: "^5", docKey: "js" },
|
|
2251
|
-
ingestEntrypointExample: `${INGEST_DIR}/ingest.mjs`,
|
|
2252
|
-
// package.json scripts are repo-defined, so they're resolved at run time by
|
|
2253
|
-
// repoVerification rather than listed here.
|
|
2254
|
-
verification: [],
|
|
2255
|
-
envReadInstruction: "Read them from `process.env`.",
|
|
2256
|
-
skipDirs: ["node_modules", "dist", "build", "coverage", ".next", "out"]
|
|
2257
|
-
},
|
|
2258
|
-
python: {
|
|
2259
|
-
id: "python",
|
|
2260
|
-
displayName: "Python",
|
|
2261
|
-
aliases: ["python", "python3", "py", "cpython"],
|
|
2262
|
-
manifests: [
|
|
2263
|
-
"pyproject.toml",
|
|
2264
|
-
"requirements.txt",
|
|
2265
|
-
"setup.py",
|
|
2266
|
-
"setup.cfg",
|
|
2267
|
-
"Pipfile"
|
|
2268
|
-
],
|
|
2269
|
-
// Deliberately one path for every Python repo: a wizard-owned venv under
|
|
2270
|
-
// .algolia-wizard. Reusing the project's uv/poetry environment would mean
|
|
2271
|
-
// mutating the developer's real dependency manifest and lockfile, and the
|
|
2272
|
-
// declare-here/install-there split is the main way ingestion silently ends
|
|
2273
|
-
// up without the SDK installed. The tradeoff: the script can import the
|
|
2274
|
-
// Algolia client and anything it declares itself, but not the project's own
|
|
2275
|
-
// packages (see the optional root-requirements step below).
|
|
2276
|
-
packageManagers: [
|
|
2277
|
-
{
|
|
2278
|
-
id: "pip-venv",
|
|
2279
|
-
dependency: { mode: "agent-declares", file: PY_REQUIREMENTS },
|
|
2280
|
-
installSteps: [
|
|
2281
|
-
{ argv: ["python3", "-m", "venv", PY_VENV] },
|
|
2282
|
-
{
|
|
2283
|
-
argv: [PY_VENV_PYTHON, "-m", "pip", "install", "-r", PY_REQUIREMENTS]
|
|
2284
|
-
},
|
|
2285
|
-
// Best-effort access to the project's own dependencies (DB drivers,
|
|
2286
|
-
// ORMs) when the repo pins them the classic way.
|
|
2287
|
-
{
|
|
2288
|
-
argv: [
|
|
2289
|
-
PY_VENV_PYTHON,
|
|
2290
|
-
"-m",
|
|
2291
|
-
"pip",
|
|
2292
|
-
"install",
|
|
2293
|
-
"-r",
|
|
2294
|
-
"requirements.txt"
|
|
2295
|
-
],
|
|
2296
|
-
requiresFile: "requirements.txt",
|
|
2297
|
-
optional: true
|
|
2298
|
-
}
|
|
2299
|
-
],
|
|
2300
|
-
ingest: {
|
|
2301
|
-
kind: "auto",
|
|
2302
|
-
argv: [PY_VENV_PYTHON, ENTRYPOINT_TOKEN],
|
|
2303
|
-
entrypointExtensions: [".py"]
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
],
|
|
2307
|
-
sdk: {
|
|
2308
|
-
packageName: "algoliasearch",
|
|
2309
|
-
versionPin: ">=4,<5",
|
|
2310
|
-
docKey: "python"
|
|
2311
|
-
},
|
|
2312
|
-
ingestEntrypointExample: `${INGEST_DIR}/ingest.py`,
|
|
2313
|
-
localSourceCaveat: {
|
|
2314
|
-
unless: "requirements.txt",
|
|
2315
|
-
message: "The ingestion script runs in its own environment under .algolia-wizard/, so it can install the Algolia client but not this project's packages (no requirements.txt to install from). If the script needs your database driver or ORM, add those packages to .algolia-wizard/requirements.txt and re-run the install."
|
|
2316
|
-
},
|
|
2317
|
-
verification: [
|
|
2318
|
-
{
|
|
2319
|
-
// -x skips the venv this same directory holds; without it the check
|
|
2320
|
-
// compiles every installed package instead of the generated script.
|
|
2321
|
-
label: "python compileall",
|
|
2322
|
-
argv: ["python3", "-m", "compileall", "-q", "-x", "[.]venv", INGEST_DIR],
|
|
2323
|
-
requiresFile: INGEST_DIR
|
|
2324
|
-
}
|
|
2325
|
-
],
|
|
2326
|
-
envReadInstruction: "Read them from `os.environ`.",
|
|
2327
|
-
skipDirs: [
|
|
2328
|
-
"venv",
|
|
2329
|
-
"__pycache__",
|
|
2330
|
-
"site-packages",
|
|
2331
|
-
"dist",
|
|
2332
|
-
"build",
|
|
2333
|
-
"htmlcov"
|
|
2334
|
-
]
|
|
2335
|
-
},
|
|
2336
|
-
ruby: {
|
|
2337
|
-
id: "ruby",
|
|
2338
|
-
displayName: "Ruby",
|
|
2339
|
-
aliases: ["ruby", "rb", "rails", "ruby on rails", "rubyonrails"],
|
|
2340
|
-
manifests: ["Gemfile", "*.gemspec"],
|
|
2341
|
-
packageManagers: [
|
|
2342
|
-
{
|
|
2343
|
-
id: "bundler",
|
|
2344
|
-
dependency: { mode: "agent-declares", file: "Gemfile" },
|
|
2345
|
-
installSteps: [{ argv: ["bundle", "install"] }],
|
|
2346
|
-
ingest: {
|
|
2347
|
-
kind: "auto",
|
|
2348
|
-
argv: ["bundle", "exec", "ruby", ENTRYPOINT_TOKEN],
|
|
2349
|
-
entrypointExtensions: [".rb"]
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
],
|
|
2353
|
-
sdk: { packageName: "algolia", versionPin: "~> 3.0", docKey: "ruby" },
|
|
2354
|
-
ingestEntrypointExample: `${INGEST_DIR}/ingest.rb`,
|
|
2355
|
-
// Ruby has no directory-level syntax check (`ruby -c` is one file at a
|
|
2356
|
-
// time), so verification relies on the agent's own review here.
|
|
2357
|
-
verification: [],
|
|
2358
|
-
envReadInstruction: "Read them from `ENV.fetch('NAME')`.",
|
|
2359
|
-
skipDirs: ["vendor", "tmp", "log", "coverage"]
|
|
2360
|
-
},
|
|
2361
|
-
php: {
|
|
2362
|
-
id: "php",
|
|
2363
|
-
displayName: "PHP",
|
|
2364
|
-
aliases: ["php", "laravel", "symfony"],
|
|
2365
|
-
manifests: ["composer.json"],
|
|
2366
|
-
packageManagers: [
|
|
2367
|
-
{
|
|
2368
|
-
id: "composer",
|
|
2369
|
-
// `composer require` both declares and installs, and unlike editing
|
|
2370
|
-
// composer.json by hand it can't leave composer.lock out of date (which
|
|
2371
|
-
// makes a later `composer install` refuse to run).
|
|
2372
|
-
dependency: { mode: "wizard-installs" },
|
|
2373
|
-
installSteps: [
|
|
2374
|
-
{
|
|
2375
|
-
argv: [
|
|
2376
|
-
"composer",
|
|
2377
|
-
"require",
|
|
2378
|
-
"algolia/algoliasearch-client-php:^4",
|
|
2379
|
-
"--no-interaction",
|
|
2380
|
-
// Repo post-install scripts are the project's code, not ours to
|
|
2381
|
-
// trigger; Laravel's package:discover also fails in a bare tree.
|
|
2382
|
-
"--no-scripts"
|
|
2383
|
-
]
|
|
2384
|
-
}
|
|
2385
|
-
],
|
|
2386
|
-
ingest: {
|
|
2387
|
-
kind: "auto",
|
|
2388
|
-
argv: ["php", ENTRYPOINT_TOKEN],
|
|
2389
|
-
entrypointExtensions: [".php"]
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
],
|
|
2393
|
-
sdk: {
|
|
2394
|
-
packageName: "algolia/algoliasearch-client-php",
|
|
2395
|
-
versionPin: "^4",
|
|
2396
|
-
docKey: "php"
|
|
2397
|
-
},
|
|
2398
|
-
ingestEntrypointExample: `${INGEST_DIR}/ingest.php`,
|
|
2399
|
-
verification: [],
|
|
2400
|
-
envReadInstruction: "Read them from `getenv('NAME')`.",
|
|
2401
|
-
skipDirs: ["vendor", "node_modules"]
|
|
2402
|
-
},
|
|
2403
|
-
go: {
|
|
2404
|
-
id: "go",
|
|
2405
|
-
displayName: "Go",
|
|
2406
|
-
aliases: ["go", "golang"],
|
|
2407
|
-
manifests: ["go.mod"],
|
|
2408
|
-
packageManagers: [
|
|
2409
|
-
{
|
|
2410
|
-
id: "gomod",
|
|
2411
|
-
// Imports in the generated file are the declaration; `go mod tidy`
|
|
2412
|
-
// resolves and fetches them — which only works because the script lives
|
|
2413
|
-
// outside INGEST_DIR (see VISIBLE_INGEST_DIR).
|
|
2414
|
-
dependency: { mode: "code-imports" },
|
|
2415
|
-
installSteps: [{ argv: ["go", "mod", "tidy"] }],
|
|
2416
|
-
ingest: {
|
|
2417
|
-
kind: "auto",
|
|
2418
|
-
argv: ["go", "run", ENTRYPOINT_TOKEN],
|
|
2419
|
-
entrypointExtensions: [".go"]
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
],
|
|
2423
|
-
sdk: {
|
|
2424
|
-
packageName: "github.com/algolia/algoliasearch-client-go/v4",
|
|
2425
|
-
versionPin: "v4",
|
|
2426
|
-
docKey: "go"
|
|
2427
|
-
},
|
|
2428
|
-
ingestEntrypointExample: `${VISIBLE_INGEST_DIR}/ingest.go`,
|
|
2429
|
-
verification: [
|
|
2430
|
-
{ label: "go vet", argv: ["go", "vet", "./..."], requiresFile: "go.mod" }
|
|
2431
|
-
],
|
|
2432
|
-
envReadInstruction: "Read them from `os.Getenv`.",
|
|
2433
|
-
skipDirs: ["vendor", "bin"]
|
|
2434
|
-
},
|
|
2435
|
-
java: {
|
|
2436
|
-
id: "java",
|
|
2437
|
-
displayName: "Java",
|
|
2438
|
-
aliases: ["java"],
|
|
2439
|
-
manifests: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
2440
|
-
packageManagers: [
|
|
2441
|
-
{
|
|
2442
|
-
id: "maven",
|
|
2443
|
-
detectFiles: ["pom.xml"],
|
|
2444
|
-
sdkVersionPin: "[4,5)",
|
|
2445
|
-
dependency: { mode: "agent-declares", file: "pom.xml" },
|
|
2446
|
-
installSteps: [{ argv: ["mvn", "-q", "-DskipTests", "compile"] }],
|
|
2447
|
-
// The main class is a wizard constant the instructions require the agent
|
|
2448
|
-
// to use, so execution can't be redirected by agent output. Runnable only
|
|
2449
|
-
// because the install step above compiles src/main/java first — which is
|
|
2450
|
-
// why the entrypoint lives there rather than under .algolia-wizard/.
|
|
2451
|
-
ingest: {
|
|
2452
|
-
kind: "auto",
|
|
2453
|
-
argv: [
|
|
2454
|
-
"mvn",
|
|
2455
|
-
"-q",
|
|
2456
|
-
"org.codehaus.mojo:exec-maven-plugin:3.5.0:java",
|
|
2457
|
-
"-Dexec.mainClass=AlgoliaWizardIngest"
|
|
2458
|
-
],
|
|
2459
|
-
entrypointExtensions: [".java"]
|
|
2460
|
-
}
|
|
2461
|
-
},
|
|
2462
|
-
{
|
|
2463
|
-
id: "gradle",
|
|
2464
|
-
detectFiles: ["build.gradle", "build.gradle.kts"],
|
|
2465
|
-
dependency: {
|
|
2466
|
-
mode: "agent-declares",
|
|
2467
|
-
file: "build.gradle",
|
|
2468
|
-
alternatives: ["build.gradle.kts"]
|
|
2469
|
-
},
|
|
2470
|
-
installSteps: [],
|
|
2471
|
-
// Auto-running means executing the repo's own ./gradlew wrapper; out of
|
|
2472
|
-
// scope for now, so the wizard writes the code and prints the command.
|
|
2473
|
-
ingest: {
|
|
2474
|
-
kind: "manual",
|
|
2475
|
-
entrypointExtensions: [".java"],
|
|
2476
|
-
runCommand: "./gradlew runAlgoliaIngest",
|
|
2477
|
-
requiresBuildTask: "runAlgoliaIngest"
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
],
|
|
2481
|
-
sdk: {
|
|
2482
|
-
packageName: "com.algolia:algoliasearch",
|
|
2483
|
-
versionPin: "4.+",
|
|
2484
|
-
docKey: "java",
|
|
2485
|
-
alsoRequires: "The class must be named AlgoliaWizardIngest, in the default package (no `package` statement), with a `public static void main`."
|
|
2486
|
-
},
|
|
2487
|
-
// Not under .algolia-wizard/: Maven and Gradle only compile src/main/<lang>,
|
|
2488
|
-
// so a class outside it never makes it onto the classpath and the run command
|
|
2489
|
-
// fails with "class not found".
|
|
2490
|
-
ingestEntrypointExample: "src/main/java/AlgoliaWizardIngest.java",
|
|
2491
|
-
verification: [
|
|
2492
|
-
{
|
|
2493
|
-
label: "mvn compile",
|
|
2494
|
-
argv: ["mvn", "-q", "-DskipTests", "compile"],
|
|
2495
|
-
requiresFile: "pom.xml"
|
|
2496
|
-
}
|
|
2497
|
-
],
|
|
2498
|
-
envReadInstruction: "Read them from `System.getenv`.",
|
|
2499
|
-
skipDirs: ["target", "build", "out"]
|
|
2500
|
-
},
|
|
2501
|
-
kotlin: {
|
|
2502
|
-
id: "kotlin",
|
|
2503
|
-
displayName: "Kotlin",
|
|
2504
|
-
aliases: ["kotlin", "kt", "ktor"],
|
|
2505
|
-
manifests: ["build.gradle.kts", "build.gradle", "pom.xml"],
|
|
2506
|
-
packageManagers: [
|
|
2507
|
-
{
|
|
2508
|
-
id: "gradle",
|
|
2509
|
-
detectFiles: ["build.gradle.kts", "build.gradle"],
|
|
2510
|
-
dependency: {
|
|
2511
|
-
mode: "agent-declares",
|
|
2512
|
-
file: "build.gradle.kts",
|
|
2513
|
-
alternatives: ["build.gradle"]
|
|
2514
|
-
},
|
|
2515
|
-
installSteps: [],
|
|
2516
|
-
ingest: {
|
|
2517
|
-
kind: "manual",
|
|
2518
|
-
entrypointExtensions: [".kt"],
|
|
2519
|
-
runCommand: "./gradlew runAlgoliaIngest",
|
|
2520
|
-
requiresBuildTask: "runAlgoliaIngest"
|
|
2521
|
-
}
|
|
2522
|
-
},
|
|
2523
|
-
// Kotlin/Maven is rare but real, and pom.xml is a Kotlin manifest — without
|
|
2524
|
-
// this spec such a repo falls through to Gradle and is told to run a
|
|
2525
|
-
// ./gradlew task that doesn't exist. Compiling needs the repo's own
|
|
2526
|
-
// kotlin-maven-plugin, so the run stays the developer's step.
|
|
2527
|
-
{
|
|
2528
|
-
id: "maven",
|
|
2529
|
-
detectFiles: ["pom.xml"],
|
|
2530
|
-
sdkVersionPin: "[3,4)",
|
|
2531
|
-
dependency: { mode: "agent-declares", file: "pom.xml" },
|
|
2532
|
-
installSteps: [{ argv: ["mvn", "-q", "-DskipTests", "compile"] }],
|
|
2533
|
-
ingest: {
|
|
2534
|
-
kind: "manual",
|
|
2535
|
-
entrypointExtensions: [".kt"],
|
|
2536
|
-
runCommand: "mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=AlgoliaWizardIngest"
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
],
|
|
2540
|
-
sdk: {
|
|
2541
|
-
packageName: "com.algolia:algoliasearch-client-kotlin",
|
|
2542
|
-
versionPin: "3.+",
|
|
2543
|
-
docKey: "kotlin",
|
|
2544
|
-
// The published client's commonMain ships only ktor-client-core; without an
|
|
2545
|
-
// engine the script compiles and then fails at its first request.
|
|
2546
|
-
alsoRequires: "The Kotlin client bundles no HTTP engine, so also declare one (e.g. io.ktor:ktor-client-okhttp). Name the object AlgoliaWizardIngest in the default package, with a @JvmStatic main."
|
|
2547
|
-
},
|
|
2548
|
-
ingestEntrypointExample: "src/main/kotlin/AlgoliaWizardIngest.kt",
|
|
2549
|
-
verification: [],
|
|
2550
|
-
envReadInstruction: "Read them from `System.getenv`.",
|
|
2551
|
-
skipDirs: ["build", "out"]
|
|
2552
|
-
},
|
|
2553
|
-
scala: {
|
|
2554
|
-
id: "scala",
|
|
2555
|
-
displayName: "Scala",
|
|
2556
|
-
aliases: ["scala", "sbt"],
|
|
2557
|
-
manifests: ["build.sbt", "build.sc"],
|
|
2558
|
-
packageManagers: [
|
|
2559
|
-
{
|
|
2560
|
-
id: "sbt",
|
|
2561
|
-
dependency: { mode: "agent-declares", file: "build.sbt" },
|
|
2562
|
-
installSteps: [],
|
|
2563
|
-
ingest: {
|
|
2564
|
-
kind: "manual",
|
|
2565
|
-
entrypointExtensions: [".scala"],
|
|
2566
|
-
runCommand: 'sbt "runMain AlgoliaWizardIngest"'
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
],
|
|
2570
|
-
sdk: {
|
|
2571
|
-
packageName: "com.algolia:algoliasearch-scala_2.13",
|
|
2572
|
-
versionPin: "2.+",
|
|
2573
|
-
docKey: "scala",
|
|
2574
|
-
alsoRequires: "Name the object AlgoliaWizardIngest in the default package (no `package` statement) so `runMain AlgoliaWizardIngest` resolves it."
|
|
2575
|
-
},
|
|
2576
|
-
ingestEntrypointExample: "src/main/scala/AlgoliaWizardIngest.scala",
|
|
2577
|
-
verification: [],
|
|
2578
|
-
envReadInstruction: "Read them from `sys.env`.",
|
|
2579
|
-
// `project/` holds sbt's build definition, but the name is generic enough
|
|
2580
|
-
// that some repos use it for source; scanning it is cheap, missing source
|
|
2581
|
-
// is not.
|
|
2582
|
-
skipDirs: ["target"]
|
|
2583
|
-
},
|
|
2584
|
-
csharp: {
|
|
2585
|
-
id: "csharp",
|
|
2586
|
-
displayName: "C#",
|
|
2587
|
-
aliases: ["c#", "csharp", "cs", ".net", "dotnet", "net", "asp.net"],
|
|
2588
|
-
manifests: ["*.csproj", "*.sln", "global.json"],
|
|
2589
|
-
packageManagers: [
|
|
2590
|
-
{
|
|
2591
|
-
id: "dotnet",
|
|
2592
|
-
// A self-contained project under .algolia-wizard keeps the ingest script
|
|
2593
|
-
// out of the repo's own build graph.
|
|
2594
|
-
dependency: { mode: "agent-declares", file: CSHARP_PROJECT },
|
|
2595
|
-
installSteps: [{ argv: ["dotnet", "restore", CSHARP_PROJECT] }],
|
|
2596
|
-
ingest: {
|
|
2597
|
-
kind: "auto",
|
|
2598
|
-
argv: ["dotnet", "run", "--project", ENTRYPOINT_TOKEN],
|
|
2599
|
-
entrypointExtensions: [".csproj"]
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
],
|
|
2603
|
-
sdk: {
|
|
2604
|
-
packageName: "Algolia.Search",
|
|
2605
|
-
versionPin: "7.*",
|
|
2606
|
-
docKey: "csharp"
|
|
2607
|
-
},
|
|
2608
|
-
ingestEntrypointExample: CSHARP_PROJECT,
|
|
2609
|
-
verification: [
|
|
2610
|
-
{
|
|
2611
|
-
label: "dotnet build",
|
|
2612
|
-
argv: ["dotnet", "build", CSHARP_PROJECT, "--nologo"],
|
|
2613
|
-
requiresFile: CSHARP_PROJECT
|
|
2614
|
-
}
|
|
2615
|
-
],
|
|
2616
|
-
envReadInstruction: 'Read them from `Environment.GetEnvironmentVariable("NAME")`.',
|
|
2617
|
-
// Deliberately not `packages`: modern .NET uses PackageReference, and
|
|
2618
|
-
// `packages/` is where pnpm/Lerna/Turborepo monorepos keep all their source —
|
|
2619
|
-
// skipping it would hide the entities the scan is looking for.
|
|
2620
|
-
skipDirs: ["bin", "obj"]
|
|
2621
|
-
},
|
|
2622
|
-
swift: {
|
|
2623
|
-
id: "swift",
|
|
2624
|
-
displayName: "Swift",
|
|
2625
|
-
aliases: ["swift", "swiftui", "ios", "vapor"],
|
|
2626
|
-
manifests: ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
|
|
2627
|
-
packageManagers: [
|
|
2628
|
-
{
|
|
2629
|
-
id: "swiftpm",
|
|
2630
|
-
dependency: {
|
|
2631
|
-
mode: "agent-declares",
|
|
2632
|
-
file: `${SWIFT_PACKAGE_DIR}/Package.swift`
|
|
2633
|
-
},
|
|
2634
|
-
// `swift build` resolves and fetches; a cold build of the client is slow
|
|
2635
|
-
// (minutes), which is why the caller degrades to the manual command when
|
|
2636
|
-
// this fails.
|
|
2637
|
-
installSteps: [
|
|
2638
|
-
{
|
|
2639
|
-
argv: ["swift", "build", "--package-path", SWIFT_PACKAGE_DIR],
|
|
2640
|
-
requiresFile: `${SWIFT_PACKAGE_DIR}/Package.swift`
|
|
2641
|
-
}
|
|
2642
|
-
],
|
|
2643
|
-
ingest: {
|
|
2644
|
-
kind: "auto",
|
|
2645
|
-
argv: ["swift", "run", "--package-path", SWIFT_PACKAGE_DIR],
|
|
2646
|
-
entrypointExtensions: [".swift"]
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
],
|
|
2650
|
-
sdk: {
|
|
2651
|
-
packageName: "algoliasearch-client-swift",
|
|
2652
|
-
// SwiftPM range syntax, not an exact version — a bare "9.0.0" in a
|
|
2653
|
-
// Package.swift dependency pins the patch.
|
|
2654
|
-
versionPin: 'from: "9.0.0"',
|
|
2655
|
-
docKey: "swift"
|
|
2656
|
-
},
|
|
2657
|
-
ingestEntrypointExample: `${SWIFT_PACKAGE_DIR}/Sources/Ingest/main.swift`,
|
|
2658
|
-
verification: [
|
|
2659
|
-
{
|
|
2660
|
-
label: "swift build",
|
|
2661
|
-
argv: ["swift", "build", "--package-path", SWIFT_PACKAGE_DIR],
|
|
2662
|
-
requiresFile: `${SWIFT_PACKAGE_DIR}/Package.swift`
|
|
2663
|
-
}
|
|
2664
|
-
],
|
|
2665
|
-
envReadInstruction: "Read them from `ProcessInfo.processInfo.environment`.",
|
|
2666
|
-
skipDirs: ["Pods", "DerivedData", "Carthage", ".build"]
|
|
2667
|
-
},
|
|
2668
|
-
dart: {
|
|
2669
|
-
id: "dart",
|
|
2670
|
-
displayName: "Dart",
|
|
2671
|
-
aliases: ["dart", "flutter"],
|
|
2672
|
-
manifests: ["pubspec.yaml"],
|
|
2673
|
-
packageManagers: [
|
|
2674
|
-
{
|
|
2675
|
-
id: "flutter-pub",
|
|
2676
|
-
detectFiles: [".metadata"],
|
|
2677
|
-
dependency: { mode: "agent-declares", file: "pubspec.yaml" },
|
|
2678
|
-
installSteps: [{ argv: ["flutter", "pub", "get"] }],
|
|
2679
|
-
ingest: {
|
|
2680
|
-
kind: "auto",
|
|
2681
|
-
argv: ["dart", "run", ENTRYPOINT_TOKEN],
|
|
2682
|
-
entrypointExtensions: [".dart"]
|
|
2683
|
-
}
|
|
2684
|
-
},
|
|
2685
|
-
{
|
|
2686
|
-
id: "pub",
|
|
2687
|
-
dependency: { mode: "agent-declares", file: "pubspec.yaml" },
|
|
2688
|
-
installSteps: [{ argv: ["dart", "pub", "get"] }],
|
|
2689
|
-
ingest: {
|
|
2690
|
-
kind: "auto",
|
|
2691
|
-
argv: ["dart", "run", ENTRYPOINT_TOKEN],
|
|
2692
|
-
entrypointExtensions: [".dart"]
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
],
|
|
2696
|
-
sdk: {
|
|
2697
|
-
packageName: "algolia_client_search",
|
|
2698
|
-
versionPin: "^1.0.0",
|
|
2699
|
-
docKey: "dart"
|
|
2700
|
-
},
|
|
2701
|
-
ingestEntrypointExample: `${INGEST_DIR}/ingest.dart`,
|
|
2702
|
-
verification: [
|
|
2703
|
-
{
|
|
2704
|
-
// Gated on the directory it analyzes, not just pubspec.yaml: a run that
|
|
2705
|
-
// only built a search UI never created it, and `dart analyze` on a
|
|
2706
|
-
// missing path fails the whole verification pass.
|
|
2707
|
-
label: "dart analyze",
|
|
2708
|
-
argv: ["dart", "analyze", INGEST_DIR],
|
|
2709
|
-
requiresFile: INGEST_DIR
|
|
2710
|
-
}
|
|
2711
|
-
],
|
|
2712
|
-
envReadInstruction: "Read them from `Platform.environment`.",
|
|
2713
|
-
skipDirs: ["build"]
|
|
2714
|
-
}
|
|
2715
|
-
};
|
|
2716
|
-
var DEFAULT_LANGUAGE_ID = "javascript";
|
|
2717
|
-
var JAVASCRIPT = "javascript";
|
|
2718
|
-
var CURATED_LANGUAGES = Object.values(
|
|
2719
|
-
LANGUAGE_PROFILES
|
|
2720
|
-
).map((profile) => profile.displayName);
|
|
2721
|
-
function isBackendLanguage(profile) {
|
|
2722
|
-
return profile.id !== JAVASCRIPT;
|
|
2723
|
-
}
|
|
2724
|
-
function normalizeLanguageName(name) {
|
|
2725
|
-
return name.toLowerCase().trim().replace(/[\s_-]+/g, "");
|
|
2726
|
-
}
|
|
2727
|
-
var ALIAS_TO_ID = /* @__PURE__ */ new Map();
|
|
2728
|
-
for (const profile of Object.values(LANGUAGE_PROFILES)) {
|
|
2729
|
-
for (const alias of [profile.id, profile.displayName, ...profile.aliases]) {
|
|
2730
|
-
ALIAS_TO_ID.set(normalizeLanguageName(alias), profile.id);
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
function resolveLanguageProfile(name) {
|
|
2734
|
-
const id = ALIAS_TO_ID.get(normalizeLanguageName(name));
|
|
2735
|
-
return id ? LANGUAGE_PROFILES[id] : void 0;
|
|
2736
|
-
}
|
|
2737
|
-
function isSameLanguage(a, b) {
|
|
2738
|
-
const x = resolveLanguageProfile(a);
|
|
2739
|
-
const y = resolveLanguageProfile(b);
|
|
2740
|
-
if (x && y) return x.id === y.id;
|
|
2741
|
-
if (x || y) return false;
|
|
2742
|
-
const folded = normalizeLanguageName(a);
|
|
2743
|
-
return folded !== "" && folded === normalizeLanguageName(b);
|
|
2744
|
-
}
|
|
2745
|
-
var BASE_SKIP_DIRS = ["node_modules", ".git", "dist"];
|
|
2746
|
-
var ALL_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2747
|
-
...BASE_SKIP_DIRS,
|
|
2748
|
-
...Object.values(LANGUAGE_PROFILES).flatMap((p) => p.skipDirs)
|
|
2749
|
-
]);
|
|
2750
|
-
var ALLOWED_BINARIES = new Set(
|
|
2751
|
-
Object.values(LANGUAGE_PROFILES).flatMap((profile) => [
|
|
2752
|
-
...profile.packageManagers.flatMap((pm) => [
|
|
2753
|
-
...pm.installSteps.map((s) => s.argv[0]),
|
|
2754
|
-
...pm.ingest.kind === "auto" ? [pm.ingest.argv[0]] : []
|
|
2755
|
-
]),
|
|
2756
|
-
...profile.verification.map((v) => v.argv[0])
|
|
2757
|
-
])
|
|
2758
|
-
);
|
|
2759
|
-
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
2760
|
-
function isWorktreeRelativeCommand(command) {
|
|
2761
|
-
return command.includes("/");
|
|
2762
|
-
}
|
|
2763
|
-
function withCommand(argv, command) {
|
|
2764
|
-
return [command, ...argv.slice(1)];
|
|
2765
|
-
}
|
|
2766
|
-
function resolveDeclaredManifest(root, packageManager) {
|
|
2767
|
-
const { dependency } = packageManager;
|
|
2768
|
-
if (dependency.mode !== "agent-declares" || !dependency.alternatives?.length) {
|
|
2769
|
-
return packageManager;
|
|
2770
|
-
}
|
|
2771
|
-
const present = [dependency.file, ...dependency.alternatives].find(
|
|
2772
|
-
(file) => existsSync2(join9(root, file))
|
|
2773
|
-
);
|
|
2774
|
-
if (!present || present === dependency.file) return packageManager;
|
|
2775
|
-
return { ...packageManager, dependency: { ...dependency, file: present } };
|
|
2776
|
-
}
|
|
2777
|
-
async function manifestPresent(root, manifest, listing) {
|
|
2778
|
-
if (!manifest.startsWith("*.")) return existsSync2(join9(root, manifest));
|
|
2779
|
-
if (!listing.entries) {
|
|
2780
|
-
const entries = await readdir2(root).catch(() => []);
|
|
2781
|
-
listing.entries = Array.isArray(entries) ? entries : [];
|
|
2782
|
-
}
|
|
2783
|
-
const suffix = manifest.slice(1);
|
|
2784
|
-
return listing.entries.some((e) => e.endsWith(suffix));
|
|
2785
|
-
}
|
|
2786
|
-
async function profileManifestPresent(root, profile, listing) {
|
|
2787
|
-
for (const manifest of profile.manifests) {
|
|
2788
|
-
if (await manifestPresent(root, manifest, listing)) return true;
|
|
2789
|
-
}
|
|
2790
|
-
return false;
|
|
2791
|
-
}
|
|
2792
|
-
async function detectProfilesFromManifests(root) {
|
|
2793
|
-
const listing = {};
|
|
2794
|
-
const found = [];
|
|
2795
|
-
for (const profile of Object.values(LANGUAGE_PROFILES)) {
|
|
2796
|
-
if (await profileManifestPresent(root, profile, listing)) found.push(profile);
|
|
2797
|
-
}
|
|
2798
|
-
return found;
|
|
2799
|
-
}
|
|
2800
|
-
async function hasProfileManifest(root, profile) {
|
|
2801
|
-
return profileManifestPresent(root, profile, {});
|
|
2802
|
-
}
|
|
2803
|
-
async function pickIngestionCandidates(root, confirmedNames) {
|
|
2804
|
-
const confirmed3 = confirmedNames.map((name) => resolveLanguageProfile(name)).filter((p) => p !== void 0);
|
|
2805
|
-
const onDisk = await detectProfilesFromManifests(root);
|
|
2806
|
-
const onDiskIds = new Set(onDisk.map((p) => p.id));
|
|
2807
|
-
const candidates = [
|
|
2808
|
-
...new Map(
|
|
2809
|
-
confirmed3.filter((p) => onDiskIds.has(p.id)).map((p) => [p.id, p])
|
|
2810
|
-
).values()
|
|
2811
|
-
];
|
|
2812
|
-
return { candidates, confirmed: confirmed3, onDisk };
|
|
2813
|
-
}
|
|
2814
|
-
async function resolveToolchain(root, profile) {
|
|
2815
|
-
const signals = (pm) => [
|
|
2816
|
-
...pm.lockfiles ?? [],
|
|
2817
|
-
...pm.detectFiles ?? []
|
|
2818
|
-
];
|
|
2819
|
-
const matched = profile.packageManagers.find(
|
|
2820
|
-
(pm) => signals(pm).some((f) => existsSync2(join9(root, f)))
|
|
2821
|
-
);
|
|
2822
|
-
const fallback = profile.packageManagers.find((pm) => signals(pm).length === 0) ?? profile.packageManagers[0];
|
|
2823
|
-
const packageManager = resolveDeclaredManifest(root, matched ?? fallback);
|
|
2824
|
-
let { installSteps, ingest } = packageManager;
|
|
2825
|
-
installSteps = installSteps.map(
|
|
2826
|
-
(step) => isWorktreeRelativeCommand(step.argv[0]) ? { ...step, argv: withCommand(step.argv, join9(root, step.argv[0])) } : step
|
|
2827
|
-
);
|
|
2828
|
-
if (ingest.kind === "auto" && isWorktreeRelativeCommand(ingest.argv[0])) {
|
|
2829
|
-
ingest = {
|
|
2830
|
-
...ingest,
|
|
2831
|
-
argv: withCommand(ingest.argv, join9(root, ingest.argv[0]))
|
|
2832
|
-
};
|
|
2833
|
-
}
|
|
2834
|
-
if (profile.id === "javascript") {
|
|
2835
|
-
const pm = await detectPackageManager(root);
|
|
2836
|
-
if (JS_PACKAGE_MANAGERS.has(pm)) {
|
|
2837
|
-
installSteps = installSteps.map((step) => ({
|
|
2838
|
-
...step,
|
|
2839
|
-
argv: withCommand(step.argv, pm)
|
|
2840
|
-
}));
|
|
2841
|
-
if (pm === "bun" && ingest.kind === "auto") {
|
|
2842
|
-
ingest = { ...ingest, argv: withCommand(ingest.argv, "bun") };
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
return { profile, packageManager, installSteps, ingest };
|
|
2847
|
-
}
|
|
2848
|
-
function resolveIngestArgv(ingest, entrypoint) {
|
|
2849
|
-
if (ingest.kind !== "auto") {
|
|
2850
|
-
throw new Error("resolveIngestArgv called for a manual-run toolchain");
|
|
2851
|
-
}
|
|
2852
|
-
return ingest.argv.map(
|
|
2853
|
-
(part) => part === ENTRYPOINT_TOKEN ? entrypoint : part
|
|
2854
|
-
);
|
|
2855
|
-
}
|
|
2856
|
-
function describeIngestCommand(ingest, entrypoint) {
|
|
2857
|
-
if (ingest.kind !== "auto") return ingest.runCommand;
|
|
2858
|
-
return ingest.argv.map((part) => part === ENTRYPOINT_TOKEN ? shellQuote(entrypoint) : part).join(" ");
|
|
2859
|
-
}
|
|
2860
|
-
function ingestScriptDir(profile) {
|
|
2861
|
-
const parts = profile.ingestEntrypointExample.split("/");
|
|
2862
|
-
return parts.slice(0, -1).join("/") || ".";
|
|
2863
|
-
}
|
|
2864
|
-
function localSourceLimitation(root, profile) {
|
|
2865
|
-
const caveat = profile.localSourceCaveat;
|
|
2866
|
-
if (!caveat) return void 0;
|
|
2867
|
-
return existsSync2(join9(root, caveat.unless)) ? void 0 : caveat.message;
|
|
2868
|
-
}
|
|
2869
|
-
async function missingBuildTask(root, toolchain) {
|
|
2870
|
-
const { ingest, packageManager } = toolchain;
|
|
2871
|
-
if (ingest.kind !== "manual" || !ingest.requiresBuildTask) return void 0;
|
|
2872
|
-
if (packageManager.dependency.mode !== "agent-declares") return void 0;
|
|
2873
|
-
const buildFile = join9(root, packageManager.dependency.file);
|
|
2874
|
-
const contents = await readFile7(buildFile, "utf8").catch(() => void 0);
|
|
2875
|
-
if (contents === void 0) return void 0;
|
|
2876
|
-
return contents.includes(ingest.requiresBuildTask) ? void 0 : ingest.requiresBuildTask;
|
|
2877
|
-
}
|
|
2878
|
-
function sdkVersionPin(profile, packageManager) {
|
|
2879
|
-
return packageManager.sdkVersionPin ?? profile.sdk.versionPin;
|
|
2880
|
-
}
|
|
2881
|
-
function dependencyInstruction(toolchain) {
|
|
2882
|
-
const { profile, packageManager } = toolchain;
|
|
2883
|
-
const { packageName } = profile.sdk;
|
|
2884
|
-
const versionPin = sdkVersionPin(profile, packageManager);
|
|
2885
|
-
const also = profile.sdk.alsoRequires ? ` ${profile.sdk.alsoRequires}` : "";
|
|
2886
|
-
switch (packageManager.dependency.mode) {
|
|
2887
|
-
case "wizard-installs":
|
|
2888
|
-
return `The wizard installs ${packageName} ${versionPin} in the worktree after you finish \u2014 import it directly and do not edit dependency manifests for it.${also}`;
|
|
2889
|
-
case "code-imports":
|
|
2890
|
-
return `Import ${packageName} in the script; the wizard resolves and fetches it in the worktree after you finish. Do not edit dependency manifests by hand.${also}`;
|
|
2891
|
-
case "agent-declares":
|
|
2892
|
-
return `Declare ${packageName} ${versionPin} in "${packageManager.dependency.file}" (create the file if needed), plus any other dependency your script imports; the wizard installs them in the worktree after you finish.${also}`;
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
// src/lib/tools/searchFiles.ts
|
|
2573
|
+
import z13 from "zod";
|
|
2574
|
+
import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
|
|
2575
|
+
import { join as join7 } from "node:path";
|
|
2897
2576
|
var MAX_QUERY_LENGTH = 1e3;
|
|
2898
2577
|
async function walkFiles(dir) {
|
|
2578
|
+
const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
2899
2579
|
const out = [];
|
|
2900
|
-
for (const e of await
|
|
2901
|
-
if (e.name.startsWith(".") ||
|
|
2902
|
-
const full =
|
|
2580
|
+
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2581
|
+
if (e.name.startsWith(".") || skip.has(e.name)) continue;
|
|
2582
|
+
const full = join7(dir, e.name);
|
|
2903
2583
|
if (e.isDirectory()) out.push(...await walkFiles(full));
|
|
2904
2584
|
else if (e.isFile()) out.push(full);
|
|
2905
2585
|
}
|
|
@@ -2908,9 +2588,9 @@ async function walkFiles(dir) {
|
|
|
2908
2588
|
function searchFilesTool(ctx) {
|
|
2909
2589
|
return tool7({
|
|
2910
2590
|
description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
|
|
2911
|
-
inputSchema:
|
|
2912
|
-
query:
|
|
2913
|
-
path:
|
|
2591
|
+
inputSchema: z13.object({
|
|
2592
|
+
query: z13.string().describe("JavaScript RegExp pattern to search for"),
|
|
2593
|
+
path: z13.string().optional().describe("Directory to search in (default: cwd)")
|
|
2914
2594
|
}),
|
|
2915
2595
|
execute: async ({ query, path = "." }) => {
|
|
2916
2596
|
logger.info({ query, path }, "called searchFiles tool");
|
|
@@ -2932,7 +2612,7 @@ function searchFilesTool(ctx) {
|
|
|
2932
2612
|
for (const file of await walkFiles(resolved.target)) {
|
|
2933
2613
|
let content;
|
|
2934
2614
|
try {
|
|
2935
|
-
content = await
|
|
2615
|
+
content = await readFile5(file, "utf8");
|
|
2936
2616
|
} catch {
|
|
2937
2617
|
continue;
|
|
2938
2618
|
}
|
|
@@ -2954,146 +2634,90 @@ function searchFilesTool(ctx) {
|
|
|
2954
2634
|
|
|
2955
2635
|
// src/lib/tools/verifyImplementation.ts
|
|
2956
2636
|
import { tool as tool8 } from "ai";
|
|
2957
|
-
import
|
|
2958
|
-
|
|
2959
|
-
// src/lib/tools/repoVerification.ts
|
|
2960
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
2961
|
-
import { join as join11 } from "node:path";
|
|
2637
|
+
import z14 from "zod";
|
|
2962
2638
|
|
|
2963
2639
|
// src/lib/tools/utils/runCommand.ts
|
|
2964
2640
|
import { spawn as spawn2 } from "node:child_process";
|
|
2965
|
-
|
|
2966
|
-
var INGEST_TIMEOUT_MS = 15 * 6e4;
|
|
2967
|
-
var VERIFY_TIMEOUT_MS = 10 * 6e4;
|
|
2968
|
-
var KILL_GRACE_MS = 5e3;
|
|
2969
|
-
function runCommand(command, args, options = {}) {
|
|
2970
|
-
const { cwd, env, timeoutMs = VERIFY_TIMEOUT_MS } = options;
|
|
2641
|
+
function runCommand(command, args, cwd) {
|
|
2971
2642
|
return new Promise((resolve4) => {
|
|
2972
2643
|
let output = "";
|
|
2973
|
-
let settled = false;
|
|
2974
2644
|
const child = spawn2(command, args, {
|
|
2975
2645
|
cwd,
|
|
2976
|
-
|
|
2977
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2978
|
-
...env ? { env: { ...process.env, ...env } } : {}
|
|
2646
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2979
2647
|
});
|
|
2980
|
-
const settle = (result) => {
|
|
2981
|
-
if (settled) return;
|
|
2982
|
-
settled = true;
|
|
2983
|
-
clearTimeout(timer);
|
|
2984
|
-
resolve4(result);
|
|
2985
|
-
};
|
|
2986
|
-
const timer = setTimeout(() => {
|
|
2987
|
-
child.kill("SIGTERM");
|
|
2988
|
-
setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
|
|
2989
|
-
const seconds = Math.round(timeoutMs / 1e3);
|
|
2990
|
-
settle({
|
|
2991
|
-
code: 1,
|
|
2992
|
-
output: `${output}
|
|
2993
|
-
Timed out after ${seconds}s: ${command} ${args.join(" ")}`.trim(),
|
|
2994
|
-
timedOut: true
|
|
2995
|
-
});
|
|
2996
|
-
}, timeoutMs);
|
|
2997
2648
|
child.stdout?.on("data", (d) => output += d);
|
|
2998
2649
|
child.stderr?.on("data", (d) => output += d);
|
|
2999
2650
|
child.on(
|
|
3000
2651
|
"error",
|
|
3001
|
-
(err) =>
|
|
3002
|
-
code: 1,
|
|
3003
|
-
output: `Failed to run ${command}: ${err.message}`,
|
|
3004
|
-
timedOut: false
|
|
3005
|
-
})
|
|
3006
|
-
);
|
|
3007
|
-
child.on(
|
|
3008
|
-
"close",
|
|
3009
|
-
(code) => settle({ code: code ?? 1, output, timedOut: false })
|
|
2652
|
+
(err) => resolve4({ code: 1, output: `Failed to run ${command}: ${err.message}` })
|
|
3010
2653
|
);
|
|
2654
|
+
child.on("close", (code) => resolve4({ code: code ?? 1, output }));
|
|
3011
2655
|
});
|
|
3012
2656
|
}
|
|
3013
2657
|
|
|
2658
|
+
// src/lib/tools/utils/packageManager.ts
|
|
2659
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2660
|
+
import { existsSync } from "node:fs";
|
|
2661
|
+
import { join as join8 } from "node:path";
|
|
2662
|
+
var LOCKFILES = [
|
|
2663
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
2664
|
+
["yarn.lock", "yarn"],
|
|
2665
|
+
["bun.lockb", "bun"],
|
|
2666
|
+
["bun.lock", "bun"],
|
|
2667
|
+
["package-lock.json", "npm"]
|
|
2668
|
+
];
|
|
2669
|
+
async function readPackageJson(cwd = process.cwd()) {
|
|
2670
|
+
return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
|
|
2671
|
+
}
|
|
2672
|
+
function packageManagerFrom(pkg) {
|
|
2673
|
+
return pkg.packageManager?.split("@")[0] ?? "npm";
|
|
2674
|
+
}
|
|
2675
|
+
function packageManagerFromLockfile(cwd) {
|
|
2676
|
+
return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
|
|
2677
|
+
}
|
|
2678
|
+
async function detectPackageManager(cwd) {
|
|
2679
|
+
try {
|
|
2680
|
+
const pkg = await readPackageJson(cwd);
|
|
2681
|
+
if (pkg.packageManager) return packageManagerFrom(pkg);
|
|
2682
|
+
} catch {
|
|
2683
|
+
}
|
|
2684
|
+
return packageManagerFromLockfile(cwd) ?? "npm";
|
|
2685
|
+
}
|
|
2686
|
+
|
|
3014
2687
|
// src/lib/tools/repoVerification.ts
|
|
3015
2688
|
var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
|
|
3016
|
-
async function
|
|
3017
|
-
const { code, output } = await runCommand(binary, args, {
|
|
3018
|
-
timeoutMs: VERIFY_TIMEOUT_MS
|
|
3019
|
-
});
|
|
3020
|
-
return { command, exitCode: code, ok: code === 0, output: output.trim() };
|
|
3021
|
-
}
|
|
3022
|
-
async function javascriptChecks() {
|
|
2689
|
+
async function runRepoVerificationCheck() {
|
|
3023
2690
|
let pkg;
|
|
3024
2691
|
try {
|
|
3025
2692
|
pkg = await readPackageJson();
|
|
3026
2693
|
} catch (err) {
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
};
|
|
2694
|
+
const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
|
|
2695
|
+
return { ok: false, checks: [], limitation };
|
|
3030
2696
|
}
|
|
3031
2697
|
const scripts = pkg.scripts ?? {};
|
|
3032
2698
|
const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
|
|
3033
2699
|
if (present.length === 0) {
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
};
|
|
2700
|
+
const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
|
|
2701
|
+
return { ok: false, checks: [], limitation };
|
|
3037
2702
|
}
|
|
3038
2703
|
const pm = await detectPackageManager(process.cwd());
|
|
3039
|
-
const checks = [];
|
|
3040
|
-
for (const script of present) {
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
);
|
|
3044
|
-
}
|
|
3045
|
-
return { checks };
|
|
3046
|
-
}
|
|
3047
|
-
async function registryChecks(id) {
|
|
3048
|
-
const profile = LANGUAGE_PROFILES[id];
|
|
3049
|
-
const runnable = profile.verification.filter(
|
|
3050
|
-
(spec) => !spec.requiresFile || existsSync3(join11(process.cwd(), spec.requiresFile))
|
|
3051
|
-
);
|
|
3052
|
-
if (runnable.length === 0) {
|
|
3053
|
-
return {
|
|
3054
|
-
limitation: `No mechanical verification available for ${profile.displayName} in this repo.`
|
|
3055
|
-
};
|
|
3056
|
-
}
|
|
3057
|
-
const checks = [];
|
|
3058
|
-
for (const spec of runnable) {
|
|
3059
|
-
checks.push(
|
|
3060
|
-
await runCheck(spec.argv.join(" "), spec.argv[0], [...spec.argv.slice(1)])
|
|
3061
|
-
);
|
|
3062
|
-
}
|
|
3063
|
-
return { checks };
|
|
3064
|
-
}
|
|
3065
|
-
async function runRepoVerificationCheck(languages = [DEFAULT_LANGUAGE_ID]) {
|
|
3066
|
-
const ids = [...new Set(languages)];
|
|
3067
|
-
if (ids.length === 0) ids.push(DEFAULT_LANGUAGE_ID);
|
|
3068
|
-
const checks = [];
|
|
3069
|
-
const limitations = [];
|
|
3070
|
-
for (const id of ids) {
|
|
3071
|
-
const result = id === JAVASCRIPT ? await javascriptChecks() : await registryChecks(id);
|
|
3072
|
-
if ("checks" in result) checks.push(...result.checks);
|
|
3073
|
-
else limitations.push(result.limitation);
|
|
3074
|
-
}
|
|
3075
|
-
if (checks.length === 0) {
|
|
3076
|
-
return {
|
|
3077
|
-
ok: false,
|
|
3078
|
-
checks: [],
|
|
3079
|
-
limitation: limitations.join(" ") || "No verification checks available."
|
|
3080
|
-
};
|
|
2704
|
+
const checks = [];
|
|
2705
|
+
for (const script of present) {
|
|
2706
|
+
const command = `${pm} run ${script}`;
|
|
2707
|
+
const { code, output } = await runCommand(pm, ["run", script]);
|
|
2708
|
+
checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
|
|
3081
2709
|
}
|
|
3082
|
-
return {
|
|
3083
|
-
ok: checks.every((c) => c.ok),
|
|
3084
|
-
checks,
|
|
3085
|
-
...limitations.length ? { limitation: limitations.join(" ") } : {}
|
|
3086
|
-
};
|
|
2710
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
3087
2711
|
}
|
|
3088
2712
|
|
|
3089
2713
|
// src/lib/tools/verifyImplementation.ts
|
|
3090
|
-
function verifyImplementationTool(
|
|
2714
|
+
function verifyImplementationTool() {
|
|
3091
2715
|
return tool8({
|
|
3092
|
-
description: "Run the repo's mechanical verification
|
|
3093
|
-
inputSchema:
|
|
2716
|
+
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.",
|
|
2717
|
+
inputSchema: z14.object(),
|
|
3094
2718
|
execute: async () => {
|
|
3095
|
-
logger.info(
|
|
3096
|
-
return runRepoVerificationCheck(
|
|
2719
|
+
logger.info("called verifyImplementation tool");
|
|
2720
|
+
return runRepoVerificationCheck();
|
|
3097
2721
|
}
|
|
3098
2722
|
});
|
|
3099
2723
|
}
|
|
@@ -3104,7 +2728,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
3104
2728
|
import { nanoid as nanoid2 } from "nanoid";
|
|
3105
2729
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
3106
2730
|
import { dirname as dirname6 } from "node:path";
|
|
3107
|
-
import
|
|
2731
|
+
import z15 from "zod";
|
|
3108
2732
|
var DATA_DIR = ".algolia-wizard/data";
|
|
3109
2733
|
var RECORD_MODEL = "claude-haiku-4-5";
|
|
3110
2734
|
var MAX_RECORDS = 100;
|
|
@@ -3116,17 +2740,17 @@ var anthropic = createAnthropic({
|
|
|
3116
2740
|
function generateRecordTool(ctx) {
|
|
3117
2741
|
return tool9({
|
|
3118
2742
|
description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
|
|
3119
|
-
inputSchema:
|
|
3120
|
-
entityName:
|
|
3121
|
-
attributes:
|
|
3122
|
-
count:
|
|
3123
|
-
hint:
|
|
2743
|
+
inputSchema: z15.object({
|
|
2744
|
+
entityName: z15.string().describe("Name of the entity to generate records for."),
|
|
2745
|
+
attributes: z15.array(z15.string()).describe("Attribute names each record must contain."),
|
|
2746
|
+
count: z15.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
|
|
2747
|
+
hint: z15.string().optional().describe("Optional context to steer realistic values.")
|
|
3124
2748
|
}),
|
|
3125
2749
|
execute: async ({ entityName, attributes, count, hint }) => {
|
|
3126
2750
|
logger.info({ entityName, count }, "called generateRecord tool");
|
|
3127
2751
|
try {
|
|
3128
|
-
const value =
|
|
3129
|
-
const recordSchema =
|
|
2752
|
+
const value = z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]);
|
|
2753
|
+
const recordSchema = z15.object(
|
|
3130
2754
|
Object.fromEntries(attributes.map((attr) => [attr, value]))
|
|
3131
2755
|
);
|
|
3132
2756
|
const generateBatch = async (batchCount) => {
|
|
@@ -3136,8 +2760,8 @@ function generateRecordTool(ctx) {
|
|
|
3136
2760
|
const { output } = await generateText({
|
|
3137
2761
|
model: anthropic(RECORD_MODEL),
|
|
3138
2762
|
output: Output.object({
|
|
3139
|
-
schema:
|
|
3140
|
-
records:
|
|
2763
|
+
schema: z15.object({
|
|
2764
|
+
records: z15.array(recordSchema).length(batchCount)
|
|
3141
2765
|
})
|
|
3142
2766
|
}),
|
|
3143
2767
|
prompt: [
|
|
@@ -3195,12 +2819,12 @@ function generateRecordTool(ctx) {
|
|
|
3195
2819
|
|
|
3196
2820
|
// src/lib/tools/notifyUser.ts
|
|
3197
2821
|
import { tool as tool10 } from "ai";
|
|
3198
|
-
import
|
|
2822
|
+
import z16 from "zod";
|
|
3199
2823
|
function notifyUserTool() {
|
|
3200
2824
|
return tool10({
|
|
3201
2825
|
description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work.`,
|
|
3202
|
-
inputSchema:
|
|
3203
|
-
message:
|
|
2826
|
+
inputSchema: z16.object({
|
|
2827
|
+
message: z16.string().describe(
|
|
3204
2828
|
"Short, plain-language description of what you are doing now."
|
|
3205
2829
|
)
|
|
3206
2830
|
}),
|
|
@@ -3219,17 +2843,12 @@ var DEFAULT_TOOL_LIMITS = {
|
|
|
3219
2843
|
read: 20,
|
|
3220
2844
|
match: 100
|
|
3221
2845
|
};
|
|
3222
|
-
function createToolContext({
|
|
3223
|
-
limits = DEFAULT_TOOL_LIMITS,
|
|
3224
|
-
cwd = process.cwd(),
|
|
3225
|
-
languages = [DEFAULT_LANGUAGE_ID]
|
|
3226
|
-
} = {}) {
|
|
2846
|
+
function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
|
|
3227
2847
|
return {
|
|
3228
2848
|
root: cwd,
|
|
3229
2849
|
cwd,
|
|
3230
2850
|
limits,
|
|
3231
|
-
counts: { list: 0, search: 0, read: 0 }
|
|
3232
|
-
languages: languages.length ? languages : [DEFAULT_LANGUAGE_ID]
|
|
2851
|
+
counts: { list: 0, search: 0, read: 0 }
|
|
3233
2852
|
};
|
|
3234
2853
|
}
|
|
3235
2854
|
|
|
@@ -3266,7 +2885,7 @@ function createTools(ctx, { output, tools }) {
|
|
|
3266
2885
|
searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
|
|
3267
2886
|
verifyImplementation: withLogging(
|
|
3268
2887
|
"verifyImplementation",
|
|
3269
|
-
verifyImplementationTool(
|
|
2888
|
+
verifyImplementationTool()
|
|
3270
2889
|
),
|
|
3271
2890
|
generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
|
|
3272
2891
|
notifyUser: withLogging("notifyUser", notifyUserTool())
|
|
@@ -3302,7 +2921,7 @@ async function runAgent(req) {
|
|
|
3302
2921
|
baseURL: PROXY_BASE_URL,
|
|
3303
2922
|
fetch: proxyFetch
|
|
3304
2923
|
});
|
|
3305
|
-
const toolContext = createToolContext(
|
|
2924
|
+
const toolContext = createToolContext();
|
|
3306
2925
|
const readTools = ["readFile", "searchFiles", "listFiles"];
|
|
3307
2926
|
const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
|
|
3308
2927
|
const instructions = [
|
|
@@ -3385,19 +3004,16 @@ async function runAgent(req) {
|
|
|
3385
3004
|
}
|
|
3386
3005
|
|
|
3387
3006
|
// src/actions/detectLanguage.ts
|
|
3388
|
-
import
|
|
3389
|
-
var detectLanguageSchema =
|
|
3390
|
-
languages:
|
|
3391
|
-
frameworks:
|
|
3007
|
+
import z19 from "zod";
|
|
3008
|
+
var detectLanguageSchema = z19.object({
|
|
3009
|
+
languages: z19.array(z19.object({ name: z19.string(), version: z19.string() })),
|
|
3010
|
+
frameworks: z19.array(z19.object({ name: z19.string(), version: z19.string() }))
|
|
3392
3011
|
});
|
|
3393
3012
|
var detectLanguage = () => runAgent({
|
|
3394
3013
|
instructions: [
|
|
3395
3014
|
"Analyze the codebase and determine the programming languages and frameworks used",
|
|
3396
|
-
"
|
|
3397
|
-
"List the language that owns the backend/data code first \u2014 that is the one an ingestion script will be written in.",
|
|
3398
|
-
"If a superset language is found, exclude the subset language. TS-over-JS. Kotlin-over-Java when Kotlin is primary.",
|
|
3015
|
+
"If a superset language is found, exclude the subset language. TS-over-JS.",
|
|
3399
3016
|
"If a meta-framework is used, exclude the framework. Next-over-React.",
|
|
3400
|
-
"Frameworks include backend and server-rendering frameworks (e.g. Rails, Django, Laravel, Symfony, Spring Boot, ASP.NET Core, Flask, Gin, Ktor) as well as frontend ones (React, Vue, Angular, Svelte) and mobile ones (Flutter, SwiftUI).",
|
|
3401
3017
|
"Return the exact version",
|
|
3402
3018
|
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
3403
3019
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
@@ -3409,31 +3025,31 @@ var detectLanguage = () => runAgent({
|
|
|
3409
3025
|
});
|
|
3410
3026
|
|
|
3411
3027
|
// src/actions/analyzeCodebase.ts
|
|
3412
|
-
import
|
|
3028
|
+
import z20 from "zod";
|
|
3413
3029
|
var READONLY_TOOLS = [
|
|
3414
3030
|
"listFiles",
|
|
3415
3031
|
"changeDirectory",
|
|
3416
3032
|
"readFile",
|
|
3417
3033
|
"searchFiles"
|
|
3418
3034
|
];
|
|
3419
|
-
var ingestionAnalysisSchema =
|
|
3420
|
-
ingestionAnalysis:
|
|
3421
|
-
|
|
3422
|
-
name:
|
|
3423
|
-
paths:
|
|
3035
|
+
var ingestionAnalysisSchema = z20.object({
|
|
3036
|
+
ingestionAnalysis: z20.array(
|
|
3037
|
+
z20.object({
|
|
3038
|
+
name: z20.string(),
|
|
3039
|
+
paths: z20.array(z20.string()),
|
|
3424
3040
|
// indexable fields the agent found for this entity
|
|
3425
|
-
attributes:
|
|
3041
|
+
attributes: z20.array(z20.string())
|
|
3426
3042
|
})
|
|
3427
3043
|
)
|
|
3428
3044
|
});
|
|
3429
|
-
var searchImplementationAnalysisSchema =
|
|
3430
|
-
searchImplementationAnalysis:
|
|
3045
|
+
var searchImplementationAnalysisSchema = z20.object({
|
|
3046
|
+
searchImplementationAnalysis: z20.string()
|
|
3431
3047
|
});
|
|
3432
|
-
var verificationSchema =
|
|
3433
|
-
verification:
|
|
3048
|
+
var verificationSchema = z20.object({
|
|
3049
|
+
verification: z20.array(z20.string())
|
|
3434
3050
|
});
|
|
3435
3051
|
var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
|
|
3436
|
-
var analyzeCodebaseSchema =
|
|
3052
|
+
var analyzeCodebaseSchema = z20.object({
|
|
3437
3053
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
3438
3054
|
searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
|
|
3439
3055
|
verification: verificationSchema.shape.verification.optional(),
|
|
@@ -3446,7 +3062,6 @@ var MODE_CONFIG = {
|
|
|
3446
3062
|
"Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
|
|
3447
3063
|
"For each entity, return its name, the file path(s) where it is defined, and its indexable attribute keys (the fields a user would search or filter on).",
|
|
3448
3064
|
"Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
|
|
3449
|
-
"Entities live wherever the stack keeps them: TypeScript interfaces or a Prisma schema, Django models.py, Rails app/models, Laravel Eloquent models, JPA @Entity classes, Go structs, C# entity classes, Pydantic models.",
|
|
3450
3065
|
"Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
|
|
3451
3066
|
"Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
|
|
3452
3067
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
@@ -3458,9 +3073,8 @@ var MODE_CONFIG = {
|
|
|
3458
3073
|
instructions: [
|
|
3459
3074
|
"Analyze the codebase to determine the single best location to add search UI functionality.",
|
|
3460
3075
|
"Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
|
|
3461
|
-
"
|
|
3462
|
-
|
|
3463
|
-
'Use as few tools as possible, but do not guess. If the project renders no UI at all (an API-only service), say "unknown".',
|
|
3076
|
+
"Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
|
|
3077
|
+
'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
|
|
3464
3078
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
3465
3079
|
"When done, call reportStatus"
|
|
3466
3080
|
],
|
|
@@ -3469,8 +3083,8 @@ var MODE_CONFIG = {
|
|
|
3469
3083
|
verification: {
|
|
3470
3084
|
instructions: [
|
|
3471
3085
|
"Analyze the codebase to determine which code-quality tools are available to validate changes.",
|
|
3472
|
-
"Look at
|
|
3473
|
-
'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"]
|
|
3086
|
+
"Look at package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
|
|
3087
|
+
'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
|
|
3474
3088
|
"Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
|
|
3475
3089
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
3476
3090
|
"When done, call reportStatus"
|
|
@@ -3497,7 +3111,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3497
3111
|
// package.json
|
|
3498
3112
|
var package_default = {
|
|
3499
3113
|
name: "@algolia/wizard",
|
|
3500
|
-
version: "0.
|
|
3114
|
+
version: "0.9.0-rc.49.79",
|
|
3501
3115
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3502
3116
|
type: "module",
|
|
3503
3117
|
engines: {
|
|
@@ -3519,7 +3133,7 @@ var package_default = {
|
|
|
3519
3133
|
prepare: "husky",
|
|
3520
3134
|
prepublishOnly: "pnpm build",
|
|
3521
3135
|
reset: "tsx ./scripts/reset-state.ts",
|
|
3522
|
-
"test:
|
|
3136
|
+
"test:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
|
|
3523
3137
|
"test:tools": "tsx ./tool-evals/toolEval.ts",
|
|
3524
3138
|
test: "vitest",
|
|
3525
3139
|
typecheck: "tsc --noEmit -p tsconfig.json"
|
|
@@ -3545,9 +3159,7 @@ var package_default = {
|
|
|
3545
3159
|
dependencies: {
|
|
3546
3160
|
"@ai-sdk/anthropic": "^3.0.81",
|
|
3547
3161
|
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
3548
|
-
"@algolia/cli": "^5.11.0",
|
|
3549
3162
|
"@hono/node-server": "^2.0.10",
|
|
3550
|
-
"@mishieck/ink-titled-box": "^0.4.2",
|
|
3551
3163
|
"@segment/analytics-node": "^3.1.0",
|
|
3552
3164
|
ai: "^6.0.190",
|
|
3553
3165
|
dotenv: "^17.4.2",
|
|
@@ -3560,7 +3172,6 @@ var package_default = {
|
|
|
3560
3172
|
nanoid: "^5.1.15",
|
|
3561
3173
|
pino: "^10.3.1",
|
|
3562
3174
|
react: "^19.2.7",
|
|
3563
|
-
toml: "^4.1.1",
|
|
3564
3175
|
varlock: "^1.5.1",
|
|
3565
3176
|
zod: "^4.4.3",
|
|
3566
3177
|
zustand: "^5.0.14"
|
|
@@ -3600,185 +3211,82 @@ function parseEntries(raw) {
|
|
|
3600
3211
|
return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
|
|
3601
3212
|
}
|
|
3602
3213
|
var summarize = (entries) => entries.length ? entries.map((e) => e.name).join(", ") : "none";
|
|
3603
|
-
|
|
3604
|
-
// src/actions/confirmLanguage.ts
|
|
3605
|
-
import z19 from "zod";
|
|
3606
|
-
var confirmLanguageSchema = z19.object({
|
|
3607
|
-
languages: detectLanguageSchema.shape.languages
|
|
3608
|
-
});
|
|
3609
|
-
var OTHER_OPTION = "Other";
|
|
3610
|
-
function confirmed(languages) {
|
|
3611
|
-
track("AI Wizard Language Confirmed", { languages });
|
|
3612
|
-
return { languages };
|
|
3613
|
-
}
|
|
3614
|
-
async function askOtherLanguage(ctx) {
|
|
3615
|
-
let prompt = "enter the language for your ingestion script";
|
|
3214
|
+
async function askList(ctx, prompt, { required = false } = {}) {
|
|
3616
3215
|
for (; ; ) {
|
|
3617
3216
|
const answer = await ctx.requestUserInput({
|
|
3618
3217
|
prompt,
|
|
3619
3218
|
promptType: "textInput",
|
|
3620
|
-
options: []
|
|
3219
|
+
options: [],
|
|
3220
|
+
helpText: 'Comma-separated, e.g. "TypeScript, Node".'
|
|
3621
3221
|
});
|
|
3622
3222
|
if (typeof answer !== "string") {
|
|
3623
|
-
throw new Error("
|
|
3223
|
+
throw new Error("askList received an unexpected non-text result");
|
|
3624
3224
|
}
|
|
3625
|
-
const
|
|
3626
|
-
if (
|
|
3627
|
-
prompt = "
|
|
3225
|
+
const entries = parseEntries(answer);
|
|
3226
|
+
if (entries.length || !required) return entries;
|
|
3227
|
+
prompt = "Please enter at least one entry:";
|
|
3628
3228
|
}
|
|
3629
3229
|
}
|
|
3230
|
+
|
|
3231
|
+
// src/actions/confirmLanguage.ts
|
|
3232
|
+
import z22 from "zod";
|
|
3233
|
+
var confirmLanguageSchema = z22.object({
|
|
3234
|
+
languages: detectLanguageSchema.shape.languages
|
|
3235
|
+
});
|
|
3630
3236
|
async function confirmLanguage(ctx) {
|
|
3631
3237
|
const detected = ctx.getStepOutput("project-scan");
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
prompt: `Write the ingestion script in ${primary.name}?`,
|
|
3638
|
-
promptType: "acceptReject",
|
|
3639
|
-
options: [`Confirm ${primary.name}`, "Use a different language"],
|
|
3640
|
-
secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0],
|
|
3641
|
-
messages: detectedLanguages.length > 1 ? [`Detected: ${summarize(detectedLanguages)}`] : []
|
|
3642
|
-
});
|
|
3643
|
-
if (accepted === true) return confirmed(detectedLanguages);
|
|
3644
|
-
}
|
|
3645
|
-
const options = [...CURATED_LANGUAGES];
|
|
3646
|
-
for (const language of detectedLanguages) {
|
|
3647
|
-
if (!options.some((o) => isSameLanguage(o, language.name))) {
|
|
3648
|
-
options.push(language.name);
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
options.push(OTHER_OPTION);
|
|
3652
|
-
const detectedFor = (option) => detectedLanguages.find((l) => isSameLanguage(option, l.name));
|
|
3653
|
-
const secondary = options.map(
|
|
3654
|
-
(o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
|
|
3655
|
-
);
|
|
3656
|
-
const defaultSelectedIndex = Math.max(
|
|
3657
|
-
options.findIndex((o) => detectedFor(o)),
|
|
3658
|
-
0
|
|
3659
|
-
);
|
|
3660
|
-
const selection = await ctx.requestUserInput({
|
|
3661
|
-
prompt: "select the language for your ingestion script",
|
|
3662
|
-
promptType: "multipleChoice",
|
|
3663
|
-
options,
|
|
3664
|
-
secondary,
|
|
3665
|
-
defaultSelectedIndex
|
|
3238
|
+
const answer = await ctx.requestUserInput({
|
|
3239
|
+
prompt: "Did we detect your language(s) correctly?",
|
|
3240
|
+
promptType: "acceptReject",
|
|
3241
|
+
options: ["Yes", "No"],
|
|
3242
|
+
messages: [`Languages: ${summarize(detected.languages)}`]
|
|
3666
3243
|
});
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3244
|
+
const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
|
|
3245
|
+
required: true
|
|
3246
|
+
});
|
|
3247
|
+
track("AI Wizard Language Confirmed", {
|
|
3248
|
+
languages
|
|
3249
|
+
});
|
|
3250
|
+
return { languages };
|
|
3673
3251
|
}
|
|
3674
3252
|
|
|
3675
3253
|
// src/actions/confirmFramework.ts
|
|
3676
|
-
import
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
var
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
{ name: "Svelte", strategy: "js", aliases: ["sveltekit"] },
|
|
3688
|
-
{
|
|
3689
|
-
name: "Vanilla JS",
|
|
3690
|
-
strategy: "js",
|
|
3691
|
-
aliases: ["vanilla", "javascript", "js", "astro", "vite"]
|
|
3692
|
-
},
|
|
3693
|
-
// Backend — Algolia's official framework integrations. Server-rendered
|
|
3694
|
-
// templates get InstantSearch.js from a CDN.
|
|
3695
|
-
{
|
|
3696
|
-
name: "Rails",
|
|
3697
|
-
strategy: "cdn-template",
|
|
3698
|
-
aliases: ["rubyonrails", "ruby on rails", "erb"]
|
|
3699
|
-
},
|
|
3700
|
-
{ name: "Django", strategy: "cdn-template", aliases: ["jinja", "jinja2"] },
|
|
3701
|
-
{ name: "Laravel", strategy: "cdn-template", aliases: ["blade"] },
|
|
3702
|
-
{ name: "Symfony", strategy: "cdn-template", aliases: ["twig"] },
|
|
3703
|
-
// Mobile — Algolia ships InstantSearch iOS/Android and Dart clients, but the
|
|
3704
|
-
// wizard can't scaffold a native UI, so it points at the docs instead.
|
|
3705
|
-
{ name: "Flutter", strategy: "none", aliases: [] },
|
|
3706
|
-
{ name: "iOS", strategy: "none", aliases: ["swiftui", "uikit"] },
|
|
3707
|
-
{ name: "Android", strategy: "none", aliases: ["jetpack compose", "compose"] },
|
|
3708
|
-
{ name: BACKEND_ONLY_FRAMEWORK, strategy: "cdn-template", aliases: [] }
|
|
3254
|
+
import z23 from "zod";
|
|
3255
|
+
var confirmFrameworkSchema = z23.object({
|
|
3256
|
+
frameworks: detectLanguageSchema.shape.frameworks
|
|
3257
|
+
});
|
|
3258
|
+
var CURATED_FRAMEWORKS = [
|
|
3259
|
+
"Next.js",
|
|
3260
|
+
"React",
|
|
3261
|
+
"Vue",
|
|
3262
|
+
"Angular",
|
|
3263
|
+
"Svelte",
|
|
3264
|
+
"Vanilla JS"
|
|
3709
3265
|
];
|
|
3710
|
-
var
|
|
3711
|
-
(f) => f.name
|
|
3712
|
-
);
|
|
3266
|
+
var OTHER_OPTION = "Other";
|
|
3713
3267
|
var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3714
|
-
var
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3268
|
+
var FRAMEWORK_ALIASES = {
|
|
3269
|
+
next: "nextjs",
|
|
3270
|
+
nextjs: "nextjs",
|
|
3271
|
+
react: "react",
|
|
3272
|
+
reactjs: "react",
|
|
3273
|
+
vue: "vue",
|
|
3274
|
+
vuejs: "vue",
|
|
3275
|
+
angular: "angular",
|
|
3276
|
+
angularjs: "angular",
|
|
3277
|
+
svelte: "svelte",
|
|
3278
|
+
sveltekit: "svelte",
|
|
3279
|
+
vanillajs: "vanillajs",
|
|
3280
|
+
vanilla: "vanillajs",
|
|
3281
|
+
javascript: "vanillajs",
|
|
3282
|
+
js: "vanillajs"
|
|
3283
|
+
};
|
|
3284
|
+
var isSameFramework = (a, b) => {
|
|
3285
|
+
const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
|
|
3286
|
+
const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
|
|
3727
3287
|
return x !== "" && x === y;
|
|
3728
|
-
}
|
|
3729
|
-
function
|
|
3730
|
-
const canonical = frameworkName ? canonicalFrameworkName(frameworkName) : void 0;
|
|
3731
|
-
const strategy = canonical ? STRATEGY_BY_NAME.get(canonical) : void 0;
|
|
3732
|
-
if (strategy) return strategy;
|
|
3733
|
-
return hasJavaScriptInStack ? "js" : "cdn-template";
|
|
3734
|
-
}
|
|
3735
|
-
function searchDocKey(strategy) {
|
|
3736
|
-
return strategy === "cdn-template" ? "templates" : strategy;
|
|
3737
|
-
}
|
|
3738
|
-
function bundlesJavaScript(strategy) {
|
|
3739
|
-
return strategy !== "cdn-template" && strategy !== "none";
|
|
3740
|
-
}
|
|
3741
|
-
function canScaffoldSearchUI(strategy) {
|
|
3742
|
-
return strategy !== "none";
|
|
3743
|
-
}
|
|
3744
|
-
var ENV_PREFIXES = [
|
|
3745
|
-
{ aliases: ["next", "nextjs"], prefix: "NEXT_PUBLIC_" },
|
|
3746
|
-
{ aliases: ["nuxt", "nuxtjs"], prefix: "NUXT_PUBLIC_" },
|
|
3747
|
-
{ aliases: ["astro"], prefix: "PUBLIC_" },
|
|
3748
|
-
{ aliases: ["vite"], prefix: "VITE_" }
|
|
3749
|
-
];
|
|
3750
|
-
var DEFAULT_ENV_PREFIX = "PUBLIC_";
|
|
3751
|
-
function publicEnvPrefix(frameworkNames, strategy) {
|
|
3752
|
-
if (!bundlesJavaScript(strategy)) return "";
|
|
3753
|
-
const present = new Set(frameworkNames.map(normalize));
|
|
3754
|
-
for (const { aliases, prefix } of ENV_PREFIXES) {
|
|
3755
|
-
if (aliases.some((alias) => present.has(alias))) return prefix;
|
|
3756
|
-
}
|
|
3757
|
-
return DEFAULT_ENV_PREFIX;
|
|
3758
|
-
}
|
|
3759
|
-
function describeSearchTarget(strategy, frameworkName) {
|
|
3760
|
-
switch (strategy) {
|
|
3761
|
-
case "react":
|
|
3762
|
-
return "React (react-instantsearch)";
|
|
3763
|
-
case "vue":
|
|
3764
|
-
return "Vue (vue-instantsearch)";
|
|
3765
|
-
case "angular":
|
|
3766
|
-
return "Angular (angular-instantsearch)";
|
|
3767
|
-
case "js":
|
|
3768
|
-
return "plain JavaScript (InstantSearch.js)";
|
|
3769
|
-
case "cdn-template":
|
|
3770
|
-
return `${frameworkName ?? "server-rendered"} templates (InstantSearch.js via CDN)`;
|
|
3771
|
-
case "none":
|
|
3772
|
-
return frameworkName ?? "a native mobile app";
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
|
|
3776
|
-
// src/actions/confirmFramework.ts
|
|
3777
|
-
var confirmFrameworkSchema = z20.object({
|
|
3778
|
-
frameworks: detectLanguageSchema.shape.frameworks
|
|
3779
|
-
});
|
|
3780
|
-
var OTHER_OPTION2 = "Other";
|
|
3781
|
-
function confirmed2(name, version) {
|
|
3288
|
+
};
|
|
3289
|
+
function confirmed(name, version) {
|
|
3782
3290
|
const frameworks = [{ name, version: version ?? "unknown" }];
|
|
3783
3291
|
track("AI Wizard Frontend Framework Confirmed", { frameworks });
|
|
3784
3292
|
return { frameworks };
|
|
@@ -3806,7 +3314,7 @@ async function confirmFramework(ctx) {
|
|
|
3806
3314
|
for (const fw of detectedFrameworks) {
|
|
3807
3315
|
if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
|
|
3808
3316
|
}
|
|
3809
|
-
options.push(
|
|
3317
|
+
options.push(OTHER_OPTION);
|
|
3810
3318
|
const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
|
|
3811
3319
|
const primary = detectedFrameworks[0];
|
|
3812
3320
|
if (primary) {
|
|
@@ -3816,7 +3324,7 @@ async function confirmFramework(ctx) {
|
|
|
3816
3324
|
options: [`Confirm ${primary.name}`, "Use a different framework"],
|
|
3817
3325
|
secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
|
|
3818
3326
|
});
|
|
3819
|
-
if (accepted === true) return
|
|
3327
|
+
if (accepted === true) return confirmed(primary.name, primary.version);
|
|
3820
3328
|
}
|
|
3821
3329
|
const secondary = options.map(
|
|
3822
3330
|
(o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
|
|
@@ -3826,7 +3334,7 @@ async function confirmFramework(ctx) {
|
|
|
3826
3334
|
0
|
|
3827
3335
|
);
|
|
3828
3336
|
const selection = await ctx.requestUserInput({
|
|
3829
|
-
prompt: "select
|
|
3337
|
+
prompt: "select a framework",
|
|
3830
3338
|
promptType: "multipleChoice",
|
|
3831
3339
|
options,
|
|
3832
3340
|
secondary,
|
|
@@ -3835,10 +3343,10 @@ async function confirmFramework(ctx) {
|
|
|
3835
3343
|
if (typeof selection !== "string") {
|
|
3836
3344
|
throw new Error("confirmFramework received an unexpected non-text result");
|
|
3837
3345
|
}
|
|
3838
|
-
if (selection ===
|
|
3839
|
-
return
|
|
3346
|
+
if (selection === OTHER_OPTION) {
|
|
3347
|
+
return confirmed(await askOtherFramework(ctx));
|
|
3840
3348
|
}
|
|
3841
|
-
return
|
|
3349
|
+
return confirmed(selection, detectedFor(selection)?.version);
|
|
3842
3350
|
}
|
|
3843
3351
|
|
|
3844
3352
|
// src/actions/promptUser.ts
|
|
@@ -3872,8 +3380,8 @@ async function promptUser(ctx, params) {
|
|
|
3872
3380
|
}
|
|
3873
3381
|
|
|
3874
3382
|
// src/actions/confirmEntities.ts
|
|
3875
|
-
import
|
|
3876
|
-
var confirmEntitiesSchema =
|
|
3383
|
+
import z24 from "zod";
|
|
3384
|
+
var confirmEntitiesSchema = z24.object({
|
|
3877
3385
|
// Final detection — the focused re-run may supersede project-scan's.
|
|
3878
3386
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
3879
3387
|
confirmedEntities: confirmedEntitiesFieldSchema
|
|
@@ -3931,27 +3439,27 @@ async function confirmEntities(ctx) {
|
|
|
3931
3439
|
onSubmit: () => {
|
|
3932
3440
|
}
|
|
3933
3441
|
});
|
|
3934
|
-
const
|
|
3935
|
-
if (
|
|
3442
|
+
const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
|
|
3443
|
+
if (confirmed2.length === 0) {
|
|
3936
3444
|
throw new Error("User cancelled entity selection \u2014 analysis halted.");
|
|
3937
3445
|
}
|
|
3938
|
-
ctx.setUserInput("confirmedEntities",
|
|
3446
|
+
ctx.setUserInput("confirmedEntities", confirmed2);
|
|
3939
3447
|
track("AI Wizard Entities Confirmed", {
|
|
3940
|
-
entities: toEntitySummary(
|
|
3448
|
+
entities: toEntitySummary(confirmed2)
|
|
3941
3449
|
});
|
|
3942
|
-
return { ingestionAnalysis: entities, confirmedEntities:
|
|
3450
|
+
return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
|
|
3943
3451
|
}
|
|
3944
3452
|
|
|
3945
3453
|
// src/actions/review.ts
|
|
3946
|
-
import { z as
|
|
3947
|
-
var reviewSchema =
|
|
3454
|
+
import { z as z25 } from "zod";
|
|
3455
|
+
var reviewSchema = z25.object({
|
|
3948
3456
|
// Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
|
|
3949
3457
|
// not one entry per workflow step — a step's raw output can be a long,
|
|
3950
3458
|
// multi-paragraph blob (see implement.ts's summaries.join), and mirroring
|
|
3951
3459
|
// that 1:1 is what made the old per-step summary an unreadable wall of text.
|
|
3952
|
-
summaryPoints:
|
|
3953
|
-
reviewPrompt:
|
|
3954
|
-
nextSteps:
|
|
3460
|
+
summaryPoints: z25.array(z25.string()),
|
|
3461
|
+
reviewPrompt: z25.string(),
|
|
3462
|
+
nextSteps: z25.array(z25.string())
|
|
3955
3463
|
});
|
|
3956
3464
|
function formatCompletedSteps(steps) {
|
|
3957
3465
|
if (!steps.length) return "(no prior steps completed)";
|
|
@@ -3963,7 +3471,7 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
3963
3471
|
}
|
|
3964
3472
|
function formatReviewSummary(result) {
|
|
3965
3473
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
3966
|
-
const isIngestCommand = step.includes("algolia-wizard/
|
|
3474
|
+
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
3967
3475
|
const isWorktreeCommand = step.includes("/worktrees/");
|
|
3968
3476
|
return {
|
|
3969
3477
|
text: `\u2192 ${step}`,
|
|
@@ -4002,17 +3510,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4002
3510
|
};
|
|
4003
3511
|
|
|
4004
3512
|
// src/actions/implement.ts
|
|
4005
|
-
import
|
|
3513
|
+
import z26 from "zod";
|
|
4006
3514
|
|
|
4007
3515
|
// src/lib/worktree.ts
|
|
4008
|
-
import { execFile } from "node:child_process";
|
|
4009
|
-
import {
|
|
4010
|
-
import { copyFile, mkdir as mkdir6, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
3516
|
+
import { execFile, spawn as spawn3 } from "node:child_process";
|
|
3517
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
4011
3518
|
import {
|
|
4012
3519
|
basename as basename2,
|
|
4013
3520
|
dirname as dirname7,
|
|
4014
3521
|
isAbsolute as isAbsolute2,
|
|
4015
|
-
join as
|
|
3522
|
+
join as join9,
|
|
4016
3523
|
relative as relative2,
|
|
4017
3524
|
resolve as resolve3
|
|
4018
3525
|
} from "node:path";
|
|
@@ -4046,8 +3553,8 @@ async function isWorkingTreeDirty(repoRoot) {
|
|
|
4046
3553
|
return out.trim().length > 0;
|
|
4047
3554
|
}
|
|
4048
3555
|
async function pruneOldWorktrees(repoRoot) {
|
|
4049
|
-
const dir =
|
|
4050
|
-
const stale = (await
|
|
3556
|
+
const dir = join9(stateDir(repoRoot), "worktrees");
|
|
3557
|
+
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4051
3558
|
for (const slug of stale) {
|
|
4052
3559
|
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4053
3560
|
try {
|
|
@@ -4057,7 +3564,7 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
4057
3564
|
"worktree",
|
|
4058
3565
|
"remove",
|
|
4059
3566
|
"--force",
|
|
4060
|
-
|
|
3567
|
+
join9(dir, slug)
|
|
4061
3568
|
]);
|
|
4062
3569
|
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4063
3570
|
} catch (err) {
|
|
@@ -4071,55 +3578,43 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
4071
3578
|
async function createWorktree(repoRoot) {
|
|
4072
3579
|
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4073
3580
|
const dirSlug = branch.replace(/\//g, "-");
|
|
4074
|
-
const path =
|
|
3581
|
+
const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4075
3582
|
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4076
3583
|
await pruneOldWorktrees(repoRoot);
|
|
4077
3584
|
await mkdir6(dirname7(path), { recursive: true });
|
|
4078
3585
|
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4079
3586
|
return { path, branch };
|
|
4080
3587
|
}
|
|
4081
|
-
async function
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
return { ok: code === 0, output: output.trim() };
|
|
4087
|
-
}
|
|
4088
|
-
async function installWorktreeDeps(worktreePath, toolchain) {
|
|
4089
|
-
const { profile, installSteps, packageManager } = toolchain;
|
|
4090
|
-
const declared = packageManager.dependency.mode === "agent-declares" ? packageManager.dependency.file : void 0;
|
|
4091
|
-
const haveSomethingToInstall = await hasProfileManifest(worktreePath, profile) || declared !== void 0 && existsSync4(join12(worktreePath, declared));
|
|
4092
|
-
if (!haveSomethingToInstall) {
|
|
4093
|
-
return {
|
|
4094
|
-
ok: true,
|
|
4095
|
-
output: `no ${profile.displayName} manifest; skipped install`
|
|
4096
|
-
};
|
|
4097
|
-
}
|
|
4098
|
-
if (installSteps.length === 0) {
|
|
4099
|
-
return {
|
|
4100
|
-
ok: true,
|
|
4101
|
-
output: `${profile.displayName} (${toolchain.packageManager.id}) has no wizard-run install step`
|
|
4102
|
-
};
|
|
4103
|
-
}
|
|
4104
|
-
const outputs = [];
|
|
4105
|
-
for (const step of installSteps) {
|
|
4106
|
-
if (step.requiresFile && !existsSync4(join12(worktreePath, step.requiresFile)))
|
|
4107
|
-
continue;
|
|
4108
|
-
const result = await spawnStep(worktreePath, step.argv);
|
|
4109
|
-
if (result.output) outputs.push(result.output);
|
|
4110
|
-
if (result.ok) continue;
|
|
4111
|
-
if (step.optional) {
|
|
4112
|
-
logger.warn(
|
|
4113
|
-
{ step: step.argv.join(" "), output: result.output },
|
|
4114
|
-
"installWorktreeDeps: optional install step failed; continuing"
|
|
4115
|
-
);
|
|
4116
|
-
continue;
|
|
4117
|
-
}
|
|
4118
|
-
return { ok: false, output: outputs.join("\n").trim() };
|
|
3588
|
+
async function installWorktreeDeps(worktreePath) {
|
|
3589
|
+
try {
|
|
3590
|
+
await readPackageJson(worktreePath);
|
|
3591
|
+
} catch {
|
|
3592
|
+
return { ok: true, output: "no package.json; skipped install" };
|
|
4119
3593
|
}
|
|
4120
|
-
|
|
3594
|
+
const pm = await detectPackageManager(worktreePath);
|
|
3595
|
+
return new Promise((resolve4) => {
|
|
3596
|
+
let output = "";
|
|
3597
|
+
const child = spawn3(pm, ["install"], {
|
|
3598
|
+
cwd: worktreePath,
|
|
3599
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3600
|
+
});
|
|
3601
|
+
child.stdout?.on("data", (d) => output += d);
|
|
3602
|
+
child.stderr?.on("data", (d) => output += d);
|
|
3603
|
+
child.on(
|
|
3604
|
+
"error",
|
|
3605
|
+
(err) => resolve4({
|
|
3606
|
+
ok: false,
|
|
3607
|
+
output: `Failed to run ${pm} install: ${err.message}`
|
|
3608
|
+
})
|
|
3609
|
+
);
|
|
3610
|
+
child.on(
|
|
3611
|
+
"close",
|
|
3612
|
+
(code) => resolve4({ ok: code === 0, output: output.trim() })
|
|
3613
|
+
);
|
|
3614
|
+
});
|
|
4121
3615
|
}
|
|
4122
|
-
|
|
3616
|
+
var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
|
|
3617
|
+
function validateIngestEntrypoint(worktreePath, entrypoint) {
|
|
4123
3618
|
if (!entrypoint || entrypoint.startsWith("-")) {
|
|
4124
3619
|
return {
|
|
4125
3620
|
ok: false,
|
|
@@ -4134,29 +3629,18 @@ function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
|
|
|
4134
3629
|
reason: `entrypoint "${entrypoint}" resolves outside the worktree`
|
|
4135
3630
|
};
|
|
4136
3631
|
}
|
|
4137
|
-
if (allowedExtensions?.length && !allowedExtensions.some((ext) => entrypoint.endsWith(ext))) {
|
|
4138
|
-
return {
|
|
4139
|
-
ok: false,
|
|
4140
|
-
reason: `entrypoint "${entrypoint}" is not one of ${allowedExtensions.join(", ")}`
|
|
4141
|
-
};
|
|
4142
|
-
}
|
|
4143
3632
|
return { ok: true, target };
|
|
4144
3633
|
}
|
|
4145
|
-
async function runIngestScript(worktreePath,
|
|
4146
|
-
|
|
4147
|
-
if (ingest.kind !== "auto") {
|
|
3634
|
+
async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
|
|
3635
|
+
if (!INGEST_RUNTIMES.includes(runtime)) {
|
|
4148
3636
|
return {
|
|
4149
3637
|
ran: false,
|
|
4150
3638
|
ok: false,
|
|
4151
3639
|
output: "",
|
|
4152
|
-
reason:
|
|
3640
|
+
reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
|
|
4153
3641
|
};
|
|
4154
3642
|
}
|
|
4155
|
-
const validated = validateIngestEntrypoint(
|
|
4156
|
-
worktreePath,
|
|
4157
|
-
entrypoint,
|
|
4158
|
-
ingest.entrypointExtensions
|
|
4159
|
-
);
|
|
3643
|
+
const validated = validateIngestEntrypoint(worktreePath, entrypoint);
|
|
4160
3644
|
if (!validated.ok) {
|
|
4161
3645
|
return { ran: false, ok: false, output: "", reason: validated.reason };
|
|
4162
3646
|
}
|
|
@@ -4177,13 +3661,29 @@ async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
|
|
|
4177
3661
|
reason: `entrypoint "${entrypoint}" does not exist`
|
|
4178
3662
|
};
|
|
4179
3663
|
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
3664
|
+
return new Promise((resolveRun) => {
|
|
3665
|
+
let output = "";
|
|
3666
|
+
const child = spawn3(runtime, [entrypoint], {
|
|
3667
|
+
cwd: worktreePath,
|
|
3668
|
+
shell: false,
|
|
3669
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3670
|
+
env: { ...process.env, ...env }
|
|
3671
|
+
});
|
|
3672
|
+
child.stdout?.on("data", (d) => output += d);
|
|
3673
|
+
child.stderr?.on("data", (d) => output += d);
|
|
3674
|
+
child.on(
|
|
3675
|
+
"error",
|
|
3676
|
+
(err) => resolveRun({
|
|
3677
|
+
ran: true,
|
|
3678
|
+
ok: false,
|
|
3679
|
+
output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
|
|
3680
|
+
})
|
|
3681
|
+
);
|
|
3682
|
+
child.on(
|
|
3683
|
+
"close",
|
|
3684
|
+
(code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
|
|
3685
|
+
);
|
|
4185
3686
|
});
|
|
4186
|
-
return { ran: true, ok: code === 0, output: output.trim() };
|
|
4187
3687
|
}
|
|
4188
3688
|
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4189
3689
|
const trimmed = sourcePath.trim();
|
|
@@ -4198,8 +3698,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4198
3698
|
} catch {
|
|
4199
3699
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4200
3700
|
}
|
|
4201
|
-
const relPath =
|
|
4202
|
-
const dest =
|
|
3701
|
+
const relPath = join9(ingestDir, basename2(source));
|
|
3702
|
+
const dest = join9(worktreePath, relPath);
|
|
4203
3703
|
try {
|
|
4204
3704
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4205
3705
|
await copyFile(source, dest);
|
|
@@ -4214,11 +3714,28 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4214
3714
|
function hasEnvVar(content, name) {
|
|
4215
3715
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4216
3716
|
}
|
|
3717
|
+
async function readEnvVar(worktreePath, name) {
|
|
3718
|
+
let content;
|
|
3719
|
+
try {
|
|
3720
|
+
content = await readFile7(join9(worktreePath, ".env"), "utf8");
|
|
3721
|
+
} catch (err) {
|
|
3722
|
+
if (err.code !== "ENOENT") throw err;
|
|
3723
|
+
return void 0;
|
|
3724
|
+
}
|
|
3725
|
+
const match = new RegExp(
|
|
3726
|
+
`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
|
|
3727
|
+
"m"
|
|
3728
|
+
).exec(content);
|
|
3729
|
+
if (!match) return void 0;
|
|
3730
|
+
const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
3731
|
+
if (!value || value.startsWith("<")) return void 0;
|
|
3732
|
+
return value;
|
|
3733
|
+
}
|
|
4217
3734
|
async function writeSearchEnvValues(worktreePath, vars) {
|
|
4218
|
-
const target =
|
|
3735
|
+
const target = join9(worktreePath, ".env");
|
|
4219
3736
|
let existing = "";
|
|
4220
3737
|
try {
|
|
4221
|
-
existing = await
|
|
3738
|
+
existing = await readFile7(target, "utf8");
|
|
4222
3739
|
} catch (err) {
|
|
4223
3740
|
if (err.code !== "ENOENT") throw err;
|
|
4224
3741
|
}
|
|
@@ -4286,178 +3803,162 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
|
4286
3803
|
}
|
|
4287
3804
|
}
|
|
4288
3805
|
|
|
4289
|
-
// src/lib/algoliaApiKey.ts
|
|
4290
|
-
import { z as z23 } from "zod";
|
|
4291
|
-
var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
|
|
4292
|
-
var apiKeySchema = z23.object({
|
|
4293
|
-
value: z23.string().min(1),
|
|
4294
|
-
acl: z23.array(z23.string()).default([]),
|
|
4295
|
-
indexes: z23.array(z23.string()).default([])
|
|
4296
|
-
});
|
|
4297
|
-
var apiKeyListSchema = z23.object({
|
|
4298
|
-
items: z23.array(apiKeySchema).optional(),
|
|
4299
|
-
keys: z23.array(apiKeySchema).optional()
|
|
4300
|
-
}).transform((o) => o.items ?? o.keys ?? []);
|
|
4301
|
-
var createdKeySchema = z23.object({
|
|
4302
|
-
key: z23.string().min(1).optional(),
|
|
4303
|
-
value: z23.string().min(1).optional()
|
|
4304
|
-
});
|
|
4305
|
-
function canReuse(key, index) {
|
|
4306
|
-
return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
|
|
4307
|
-
}
|
|
4308
|
-
async function createSearchKey(index) {
|
|
4309
|
-
const stdout = await runAlgoliaCli([
|
|
4310
|
-
"apikeys",
|
|
4311
|
-
"create",
|
|
4312
|
-
"--indices",
|
|
4313
|
-
index,
|
|
4314
|
-
"--acl",
|
|
4315
|
-
"search,browse",
|
|
4316
|
-
"--description",
|
|
4317
|
-
`wizard search-only key for ${index}`,
|
|
4318
|
-
"-o",
|
|
4319
|
-
"json"
|
|
4320
|
-
]);
|
|
4321
|
-
const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
|
|
4322
|
-
const created = key ?? value;
|
|
4323
|
-
if (!created) throw new Error("apikeys create returned no key value");
|
|
4324
|
-
return created;
|
|
4325
|
-
}
|
|
4326
|
-
async function resolveSearchOnlyKey(index) {
|
|
4327
|
-
const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
|
|
4328
|
-
const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
|
|
4329
|
-
if (existing) {
|
|
4330
|
-
logger.info({ index }, "reusing existing search-only API key");
|
|
4331
|
-
return existing;
|
|
4332
|
-
}
|
|
4333
|
-
logger.info({ index }, "no reusable search-only key found; creating one");
|
|
4334
|
-
return createSearchKey(index);
|
|
4335
|
-
}
|
|
4336
|
-
|
|
4337
3806
|
// src/lib/algoliaDocs.ts
|
|
4338
|
-
import { readFileSync, existsSync as
|
|
4339
|
-
import { dirname as dirname8, join as
|
|
3807
|
+
import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
|
|
3808
|
+
import { dirname as dirname8, join as join10 } from "node:path";
|
|
4340
3809
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4341
|
-
var DOCS_SUBPATH =
|
|
3810
|
+
var DOCS_SUBPATH = join10("docs", "algolia-sdk");
|
|
4342
3811
|
function findDocsDir() {
|
|
4343
3812
|
let dir = dirname8(fileURLToPath2(import.meta.url));
|
|
4344
3813
|
for (; ; ) {
|
|
4345
|
-
const candidate =
|
|
4346
|
-
if (
|
|
3814
|
+
const candidate = join10(dir, DOCS_SUBPATH);
|
|
3815
|
+
if (existsSync2(candidate)) return candidate;
|
|
4347
3816
|
const parent = dirname8(dir);
|
|
4348
3817
|
if (parent === dir) return void 0;
|
|
4349
3818
|
dir = parent;
|
|
4350
3819
|
}
|
|
4351
3820
|
}
|
|
4352
|
-
function
|
|
3821
|
+
function loadAlgoliaDoc(language) {
|
|
3822
|
+
const docsDir = findDocsDir();
|
|
3823
|
+
if (!docsDir) {
|
|
3824
|
+
logger.warn(
|
|
3825
|
+
"algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
|
|
3826
|
+
);
|
|
3827
|
+
return "";
|
|
3828
|
+
}
|
|
3829
|
+
const files = readdirSync(docsDir).filter((f) => f.includes(language));
|
|
3830
|
+
if (files.length === 0) {
|
|
3831
|
+
logger.warn(
|
|
3832
|
+
{ language },
|
|
3833
|
+
"algoliaDocs: no SDK reference found for language; skipping"
|
|
3834
|
+
);
|
|
3835
|
+
return "";
|
|
3836
|
+
}
|
|
3837
|
+
return readFileSync(join10(docsDir, files[0]), "utf8").trim();
|
|
3838
|
+
}
|
|
3839
|
+
function getNamedDoc(name, language) {
|
|
4353
3840
|
const docsDir = findDocsDir();
|
|
4354
3841
|
if (!docsDir) {
|
|
4355
3842
|
logger.warn("docs/algolia-sdk not found");
|
|
4356
3843
|
return "";
|
|
4357
3844
|
}
|
|
4358
|
-
const file =
|
|
4359
|
-
if (!
|
|
4360
|
-
logger.warn({ name,
|
|
3845
|
+
const file = join10(docsDir, `${name}-${language}.md`);
|
|
3846
|
+
if (!existsSync2(file)) {
|
|
3847
|
+
logger.warn({ name, language }, "named SDK reference not found");
|
|
4361
3848
|
return "";
|
|
4362
3849
|
}
|
|
4363
3850
|
return readFileSync(file, "utf8").trim();
|
|
4364
3851
|
}
|
|
3852
|
+
function getFrameworkSpecificDoc(frameworks) {
|
|
3853
|
+
const fw = frameworks.map((f) => f.toLowerCase());
|
|
3854
|
+
if (fw.includes("vue") || fw.includes("nuxt")) {
|
|
3855
|
+
return loadAlgoliaDoc("vue");
|
|
3856
|
+
}
|
|
3857
|
+
if (fw.includes("react") || fw.includes("next.js")) {
|
|
3858
|
+
return loadAlgoliaDoc("react");
|
|
3859
|
+
}
|
|
3860
|
+
if (fw.includes("angular")) {
|
|
3861
|
+
return loadAlgoliaDoc("angular");
|
|
3862
|
+
}
|
|
3863
|
+
return loadAlgoliaDoc("js");
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
// src/lib/shell.ts
|
|
3867
|
+
function shellQuote(value) {
|
|
3868
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
3869
|
+
}
|
|
4365
3870
|
|
|
4366
3871
|
// src/actions/implement.ts
|
|
4367
|
-
var implementSchema =
|
|
4368
|
-
filesChanged:
|
|
4369
|
-
summary:
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
// machine-readable count line; absent when the script didn't run or emitted
|
|
4381
|
-
// no parseable count.
|
|
4382
|
-
ingestRecordCount: z24.number().optional(),
|
|
4383
|
-
// Wall-clock duration of the run-now ingestion execution, in ms.
|
|
4384
|
-
ingestDurationMs: z24.number().optional(),
|
|
4385
|
-
ingestionSource: z24.enum(["local", "fileUpload", "generated"]),
|
|
4386
|
-
// Suggested names/values, built from framework detection. The search agent is
|
|
4387
|
-
// instructed to rename the prefix if it doesn't match the project's build
|
|
4388
|
-
// tool, so the names it actually wrote can differ — treat these as hints, not
|
|
4389
|
-
// ground truth (the agent's summary carries the final names).
|
|
4390
|
-
searchEnvVars: z24.array(
|
|
4391
|
-
z24.object({
|
|
4392
|
-
name: z24.string(),
|
|
4393
|
-
value: z24.string()
|
|
3872
|
+
var implementSchema = z26.object({
|
|
3873
|
+
filesChanged: z26.array(z26.string()),
|
|
3874
|
+
summary: z26.string(),
|
|
3875
|
+
worktreePath: z26.string().optional(),
|
|
3876
|
+
ingestCommand: z26.string().optional(),
|
|
3877
|
+
ingestScriptRan: z26.boolean().optional(),
|
|
3878
|
+
ingestRecordCount: z26.number().optional(),
|
|
3879
|
+
ingestDurationMs: z26.number().optional(),
|
|
3880
|
+
ingestionSource: z26.enum(["local", "fileUpload", "generated"]),
|
|
3881
|
+
searchEnvVars: z26.array(
|
|
3882
|
+
z26.object({
|
|
3883
|
+
name: z26.string(),
|
|
3884
|
+
value: z26.string()
|
|
4394
3885
|
})
|
|
4395
3886
|
).optional()
|
|
4396
3887
|
});
|
|
4397
|
-
var implementationOutputSchema =
|
|
4398
|
-
summary:
|
|
4399
|
-
// Ingestion only:
|
|
4400
|
-
// command string
|
|
4401
|
-
//
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
entrypoint: z24.string().optional()
|
|
3888
|
+
var implementationOutputSchema = z26.object({
|
|
3889
|
+
summary: z26.string(),
|
|
3890
|
+
// Ingestion only: a structured pair the wizard turns into an argv, never a
|
|
3891
|
+
// free-form command string. `runtime` is allowlisted and `entrypoint` is
|
|
3892
|
+
// validated worktree-relative, so the agent cannot inject extra commands.
|
|
3893
|
+
runtime: z26.enum(INGEST_RUNTIMES).optional(),
|
|
3894
|
+
entrypoint: z26.string().optional()
|
|
4405
3895
|
});
|
|
4406
|
-
var verificationOutputSchema =
|
|
4407
|
-
summary:
|
|
4408
|
-
sufficient:
|
|
4409
|
-
additionalInstructions:
|
|
3896
|
+
var verificationOutputSchema = z26.object({
|
|
3897
|
+
summary: z26.string(),
|
|
3898
|
+
sufficient: z26.boolean(),
|
|
3899
|
+
additionalInstructions: z26.string().optional()
|
|
4410
3900
|
});
|
|
4411
3901
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
4412
3902
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
3903
|
+
var INGEST_DIR = ".algolia-wizard";
|
|
3904
|
+
function detectUiFramework(language) {
|
|
3905
|
+
const names = language.frameworks.map((f) => f.name.toLowerCase());
|
|
3906
|
+
if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
|
|
3907
|
+
if (names.some((n) => n.includes("react") || n.includes("next")))
|
|
3908
|
+
return "React";
|
|
3909
|
+
if (names.some((n) => n.includes("angular"))) return "Angular";
|
|
3910
|
+
return "JavaScript";
|
|
3911
|
+
}
|
|
3912
|
+
function frameworksForDoc(framework) {
|
|
3913
|
+
switch (framework) {
|
|
3914
|
+
case "React":
|
|
3915
|
+
return ["react"];
|
|
3916
|
+
case "Vue":
|
|
3917
|
+
return ["vue"];
|
|
3918
|
+
case "Angular":
|
|
3919
|
+
return ["angular"];
|
|
3920
|
+
case "JavaScript":
|
|
3921
|
+
return [];
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
function publicEnvPrefix(language) {
|
|
3925
|
+
const frameworkNames = language.frameworks.map(
|
|
3926
|
+
(framework) => framework.name.toLowerCase()
|
|
4417
3927
|
);
|
|
3928
|
+
if (frameworkNames.some((name) => name.includes("next"))) {
|
|
3929
|
+
return "NEXT_PUBLIC_";
|
|
3930
|
+
}
|
|
3931
|
+
if (frameworkNames.some((name) => name.includes("nuxt"))) {
|
|
3932
|
+
return "NUXT_PUBLIC_";
|
|
3933
|
+
}
|
|
3934
|
+
if (frameworkNames.some((name) => name.includes("astro"))) {
|
|
3935
|
+
return "PUBLIC_";
|
|
3936
|
+
}
|
|
3937
|
+
if (frameworkNames.some((name) => name.includes("vite"))) {
|
|
3938
|
+
return "VITE_";
|
|
3939
|
+
}
|
|
3940
|
+
return "PUBLIC_";
|
|
3941
|
+
}
|
|
3942
|
+
var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
|
|
3943
|
+
var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
|
|
3944
|
+
function appIdVar(language) {
|
|
3945
|
+
return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
|
|
3946
|
+
}
|
|
3947
|
+
function searchKeyVar(language) {
|
|
3948
|
+
return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
|
|
3949
|
+
}
|
|
3950
|
+
function searchEnvVars(language, appId, searchKey) {
|
|
4418
3951
|
return [
|
|
4419
3952
|
{
|
|
4420
|
-
name:
|
|
3953
|
+
name: appIdVar(language),
|
|
4421
3954
|
value: appId ?? "<your-algolia-app-id>"
|
|
4422
3955
|
},
|
|
4423
3956
|
{
|
|
4424
|
-
name:
|
|
3957
|
+
name: searchKeyVar(language),
|
|
4425
3958
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4426
3959
|
}
|
|
4427
3960
|
];
|
|
4428
3961
|
}
|
|
4429
|
-
async function resolveIngestionProfile(ctx, language, repoRoot) {
|
|
4430
|
-
const { candidates, confirmed: confirmed3, onDisk } = await pickIngestionCandidates(
|
|
4431
|
-
repoRoot,
|
|
4432
|
-
language.languages.map((l) => l.name)
|
|
4433
|
-
);
|
|
4434
|
-
if (candidates.length === 0) {
|
|
4435
|
-
const chosen = confirmed3[0] ?? onDisk[0] ?? LANGUAGE_PROFILES[DEFAULT_LANGUAGE_ID];
|
|
4436
|
-
logger.warn(
|
|
4437
|
-
{
|
|
4438
|
-
confirmed: language.languages.map((l) => l.name),
|
|
4439
|
-
onDisk: onDisk.map((p) => p.id),
|
|
4440
|
-
chosen: chosen.id
|
|
4441
|
-
},
|
|
4442
|
-
"implement: no confirmed language matched a manifest on disk; falling back"
|
|
4443
|
-
);
|
|
4444
|
-
return chosen;
|
|
4445
|
-
}
|
|
4446
|
-
if (candidates.length === 1) return candidates[0];
|
|
4447
|
-
const backends = candidates.filter(isBackendLanguage);
|
|
4448
|
-
if (backends.length === 1) return backends[0];
|
|
4449
|
-
if (backends.length === 0) return candidates[0];
|
|
4450
|
-
if (isBackendLanguage(candidates[0])) return candidates[0];
|
|
4451
|
-
const options = backends.map((p) => p.displayName);
|
|
4452
|
-
const selection = await ctx.requestUserInput({
|
|
4453
|
-
prompt: "Which language should the ingestion script use?",
|
|
4454
|
-
promptType: "multipleChoice",
|
|
4455
|
-
options,
|
|
4456
|
-
defaultSelectedIndex: 0
|
|
4457
|
-
});
|
|
4458
|
-
const picked = typeof selection === "string" ? backends.find((p) => p.displayName === selection) : void 0;
|
|
4459
|
-
return picked ?? backends[0];
|
|
4460
|
-
}
|
|
4461
3962
|
function baseInstructions(input) {
|
|
4462
3963
|
return [
|
|
4463
3964
|
`Target Algolia index: ${input.targetIndex}`,
|
|
@@ -4474,9 +3975,6 @@ function sourceSpecificInstructions(input) {
|
|
|
4474
3975
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4475
3976
|
],
|
|
4476
3977
|
fileUpload: [
|
|
4477
|
-
// The wizard already copied the developer's file into the worktree at this
|
|
4478
|
-
// exact path, so the agent must read it directly — never search for or
|
|
4479
|
-
// substitute another file.
|
|
4480
3978
|
`Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4481
3979
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4482
3980
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
@@ -4485,71 +3983,59 @@ function sourceSpecificInstructions(input) {
|
|
|
4485
3983
|
generated: [
|
|
4486
3984
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
4487
3985
|
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
|
|
4488
|
-
"In the script, read and parse each returned
|
|
3986
|
+
"In the script, read and parse each returned file path at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) instead of inlining the records as literals.",
|
|
4489
3987
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
4490
3988
|
]
|
|
4491
3989
|
};
|
|
4492
3990
|
return byLine[input.ingestionSource];
|
|
4493
3991
|
}
|
|
4494
3992
|
function ingestionInstructions(input) {
|
|
4495
|
-
const { ingestionProfile: profile, toolchain } = input;
|
|
4496
|
-
const { ingest } = toolchain;
|
|
4497
|
-
const extensions = ingest.entrypointExtensions.join(", ");
|
|
4498
|
-
const runInstruction = ingest.kind === "auto" ? `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard runs it with \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so it must be a plain path with no flags or arguments and must run as-is under that command.` : `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard does NOT run ${profile.displayName} ${toolchain.packageManager.id} projects itself \u2014 it tells the developer to run \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so also add whatever build configuration that command needs${ingest.kind === "manual" && ingest.requiresBuildTask ? `, including a "${ingest.requiresBuildTask}" task in "${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"}" that runs the script` : ""}.`;
|
|
4499
3993
|
return [
|
|
4500
3994
|
...input.confirmed && input.confirmed.length ? [
|
|
4501
|
-
`
|
|
3995
|
+
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
4502
3996
|
`Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
4503
|
-
`Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them.
|
|
4504
|
-
|
|
3997
|
+
`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.`,
|
|
3998
|
+
"Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
|
|
4505
3999
|
"After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
|
|
4506
|
-
getNamedDoc("save-records",
|
|
4507
|
-
|
|
4000
|
+
getNamedDoc("save-records", "js"),
|
|
4001
|
+
'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
|
|
4508
4002
|
"The summary should be extremely concise.",
|
|
4509
|
-
|
|
4003
|
+
`Return how to run the script as two fields, not a command string: "runtime" (one of ${INGEST_RUNTIMES.join(", ")}) and "entrypoint" (the script path relative to the worktree root, e.g. "${input.ingestDir}/ingest.mjs"). The wizard runs \`<runtime> <entrypoint>\` directly, so the entrypoint must be a plain path with no flags or arguments. Write a script one of those interpreters can run as-is.`,
|
|
4510
4004
|
...sourceSpecificInstructions(input)
|
|
4511
4005
|
] : []
|
|
4512
4006
|
];
|
|
4513
4007
|
}
|
|
4514
4008
|
function searchInstructions(input) {
|
|
4515
|
-
const doc =
|
|
4516
|
-
"instantsearch-setup",
|
|
4517
|
-
searchDocKey(input.searchStrategy)
|
|
4518
|
-
);
|
|
4519
|
-
const isTemplate = input.searchStrategy === "cdn-template";
|
|
4520
|
-
const placement = isTemplate ? input.searchLocation ? `Add the search UI to the server-rendered template at "${input.searchLocation}" \u2014 ideally a shared layout, so it is reachable across the app.` : `This project has no shared template to host the UI, so create a standalone page at "${input.ingestDir}/search-demo.html" the developer can open directly, and add a TODO explaining how to move the snippet into their own layout.` : `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app.`;
|
|
4009
|
+
const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
|
|
4521
4010
|
return [
|
|
4522
4011
|
"Implement an in-app Algolia search experience.",
|
|
4523
|
-
`Build the search UI for ${
|
|
4524
|
-
"Follow the Algolia reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
|
|
4012
|
+
`Build the search UI for ${input.uiFramework}.`,
|
|
4013
|
+
"Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
|
|
4525
4014
|
doc,
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
//
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
//
|
|
4534
|
-
// resolved app id / search-only key into ".env" under these exact names
|
|
4535
|
-
// right after this step, so a renamed prefix here would leave the code
|
|
4015
|
+
`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.`,
|
|
4016
|
+
// The key is provisioned only after verification passes, so the agent never
|
|
4017
|
+
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4018
|
+
// decide whether a key already exists, and an agent-invented value there
|
|
4019
|
+
// would be reused as if it were real.
|
|
4020
|
+
`Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
|
|
4021
|
+
// Not the agent's to rename: the wizard writes these exact names into
|
|
4022
|
+
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4536
4023
|
// reading a var the wizard never wrote.
|
|
4537
|
-
`Use exactly these env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4024
|
+
`Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4025
|
+
"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.",
|
|
4026
|
+
'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
|
|
4538
4027
|
"The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
|
|
4539
4028
|
];
|
|
4540
4029
|
}
|
|
4541
4030
|
function verificationInstructions(input) {
|
|
4542
|
-
const protectedDirs = [
|
|
4543
|
-
.../* @__PURE__ */ new Set([input.ingestDir, ingestScriptDir(input.ingestionProfile)])
|
|
4544
|
-
];
|
|
4545
4031
|
return [
|
|
4546
4032
|
"Verify the Algolia implementation changes in the current worktree.",
|
|
4547
4033
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4548
|
-
|
|
4034
|
+
"Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
|
|
4549
4035
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
|
|
4550
4036
|
"Do not make speculative fixes when verifyImplementation cannot run, no checks exist, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
|
|
4551
4037
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
4552
|
-
`Do not modify ${
|
|
4038
|
+
`Do not modify "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
|
|
4553
4039
|
"Always call reportStatus with status=success once verification has run, even when sufficient=false.",
|
|
4554
4040
|
"Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
|
|
4555
4041
|
"Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
|
|
@@ -4558,17 +4044,14 @@ function verificationInstructions(input) {
|
|
|
4558
4044
|
var IMPLEMENT_CONFIG = {
|
|
4559
4045
|
ingestion: {
|
|
4560
4046
|
title: "Algolia ingestion",
|
|
4561
|
-
label: "Ingestion",
|
|
4562
4047
|
buildInstructions: ingestionInstructions
|
|
4563
4048
|
},
|
|
4564
4049
|
search: {
|
|
4565
4050
|
title: "Algolia search",
|
|
4566
|
-
label: "Search",
|
|
4567
4051
|
buildInstructions: searchInstructions
|
|
4568
4052
|
},
|
|
4569
4053
|
verification: {
|
|
4570
4054
|
title: "Algolia verification",
|
|
4571
|
-
label: "Verification",
|
|
4572
4055
|
buildInstructions: verificationInstructions
|
|
4573
4056
|
}
|
|
4574
4057
|
};
|
|
@@ -4600,10 +4083,11 @@ function buildAgentInstructions(useCase, input, extraInstructions = []) {
|
|
|
4600
4083
|
];
|
|
4601
4084
|
}
|
|
4602
4085
|
function formatSummary(useCase, summary) {
|
|
4603
|
-
|
|
4086
|
+
const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
|
|
4087
|
+
return `${label}: ${summary}`;
|
|
4604
4088
|
}
|
|
4605
|
-
function buildIngestCommand(worktree,
|
|
4606
|
-
return `cd ${shellQuote(worktree)} && ${
|
|
4089
|
+
function buildIngestCommand(worktree, runtime, entrypoint) {
|
|
4090
|
+
return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
|
|
4607
4091
|
}
|
|
4608
4092
|
function parseIngestRecordCount(output) {
|
|
4609
4093
|
const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
|
|
@@ -4680,25 +4164,17 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4680
4164
|
}
|
|
4681
4165
|
}
|
|
4682
4166
|
const targetIndex = selected?.selection;
|
|
4167
|
+
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4683
4168
|
await assertGitRepoWithHead(repoRoot);
|
|
4684
4169
|
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4685
4170
|
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4686
4171
|
}
|
|
4687
4172
|
const normalized = normalizeFindingPaths(findings);
|
|
4688
|
-
const
|
|
4173
|
+
const confirmed2 = normalized.confirmedEntities;
|
|
4689
4174
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
4690
4175
|
let appId;
|
|
4691
|
-
let searchKey;
|
|
4692
4176
|
if (useCases.includes("search")) {
|
|
4693
|
-
appId = (await
|
|
4694
|
-
try {
|
|
4695
|
-
searchKey = await resolveSearchOnlyKey(targetIndex);
|
|
4696
|
-
} catch (err) {
|
|
4697
|
-
logger.warn(
|
|
4698
|
-
{ err: err.message },
|
|
4699
|
-
"implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
|
|
4700
|
-
);
|
|
4701
|
-
}
|
|
4177
|
+
appId = (await requireApplication()).id;
|
|
4702
4178
|
}
|
|
4703
4179
|
const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
|
|
4704
4180
|
try {
|
|
@@ -4723,66 +4199,48 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4723
4199
|
);
|
|
4724
4200
|
}
|
|
4725
4201
|
}
|
|
4726
|
-
const ingestionProfile = await resolveIngestionProfile(
|
|
4727
|
-
ctx,
|
|
4728
|
-
language,
|
|
4729
|
-
worktree
|
|
4730
|
-
);
|
|
4731
|
-
const toolchain = await resolveToolchain(worktree, ingestionProfile);
|
|
4732
|
-
const verificationLanguages = [
|
|
4733
|
-
.../* @__PURE__ */ new Set([
|
|
4734
|
-
ingestionProfile.id,
|
|
4735
|
-
...(await detectProfilesFromManifests(worktree)).map((p) => p.id)
|
|
4736
|
-
])
|
|
4737
|
-
];
|
|
4738
|
-
const frameworkName = language.frameworks[0]?.name;
|
|
4739
|
-
const searchStrategy = resolveSearchStrategy(
|
|
4740
|
-
frameworkName,
|
|
4741
|
-
verificationLanguages.includes(JAVASCRIPT)
|
|
4742
|
-
);
|
|
4743
|
-
logger.info(
|
|
4744
|
-
{
|
|
4745
|
-
language: ingestionProfile.id,
|
|
4746
|
-
packageManager: toolchain.packageManager.id,
|
|
4747
|
-
ingest: toolchain.ingest.kind,
|
|
4748
|
-
framework: frameworkName,
|
|
4749
|
-
searchStrategy
|
|
4750
|
-
},
|
|
4751
|
-
"implement: resolved ingestion toolchain and search strategy"
|
|
4752
|
-
);
|
|
4753
4202
|
const input = {
|
|
4754
4203
|
findings: normalized,
|
|
4755
|
-
confirmed:
|
|
4204
|
+
confirmed: confirmed2,
|
|
4756
4205
|
searchLocation,
|
|
4757
4206
|
targetIndex,
|
|
4758
4207
|
language,
|
|
4759
4208
|
appId,
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
searchStrategy,
|
|
4764
|
-
appId,
|
|
4765
|
-
searchKey
|
|
4766
|
-
),
|
|
4209
|
+
// Names only: the search-only key is provisioned after verification, so
|
|
4210
|
+
// every value here is still a placeholder when the agent reads them.
|
|
4211
|
+
searchEnvVars: searchEnvVars(language, appId),
|
|
4767
4212
|
ingestDir: INGEST_DIR,
|
|
4768
4213
|
ingestionSource,
|
|
4769
4214
|
uploadFilePath,
|
|
4770
|
-
|
|
4771
|
-
frameworkName,
|
|
4772
|
-
ingestionProfile,
|
|
4773
|
-
toolchain,
|
|
4774
|
-
verificationLanguages
|
|
4215
|
+
uiFramework: detectUiFramework(language)
|
|
4775
4216
|
};
|
|
4776
|
-
const searchToolchain = !bundlesJavaScript(searchStrategy) ? void 0 : ingestionProfile.id === JAVASCRIPT ? toolchain : await resolveToolchain(worktree, LANGUAGE_PROFILES[JAVASCRIPT]);
|
|
4777
|
-
const toolchainForUseCase = (useCase) => useCase === "search" ? searchToolchain : toolchain;
|
|
4778
4217
|
const summaries = [];
|
|
4779
4218
|
if (uploadWarning) summaries.push(uploadWarning);
|
|
4219
|
+
let envSearchKey;
|
|
4220
|
+
let envAppIdMismatch = false;
|
|
4221
|
+
if (useCases.includes("search") && appId) {
|
|
4222
|
+
const envAppId = await readEnvVar(worktree, appIdVar(language));
|
|
4223
|
+
if (envAppId === appId) {
|
|
4224
|
+
envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
|
|
4225
|
+
} else if (envAppId) {
|
|
4226
|
+
envAppIdMismatch = true;
|
|
4227
|
+
summaries.push(
|
|
4228
|
+
`\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
|
|
4229
|
+
);
|
|
4230
|
+
logger.warn(
|
|
4231
|
+
{ envAppId, appId },
|
|
4232
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4233
|
+
);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4780
4237
|
let agentRuns = 0;
|
|
4238
|
+
let ingestRuntime;
|
|
4781
4239
|
let ingestEntrypoint;
|
|
4782
4240
|
let ingestScriptRan = false;
|
|
4783
4241
|
let ingestRecordCount;
|
|
4784
4242
|
let ingestDurationMs;
|
|
4785
|
-
|
|
4243
|
+
let installFailed = false;
|
|
4786
4244
|
let ingestOutcomeMessage;
|
|
4787
4245
|
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4788
4246
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
@@ -4796,19 +4254,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4796
4254
|
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4797
4255
|
outputSchema: implementationOutputSchema
|
|
4798
4256
|
});
|
|
4799
|
-
const useCaseToolchain = toolchainForUseCase(currentUseCase);
|
|
4800
|
-
if (!useCaseToolchain) return result;
|
|
4801
4257
|
ctx.notify({
|
|
4802
4258
|
messages: [`Installing dependencies for ${currentUseCase}\u2026`]
|
|
4803
4259
|
});
|
|
4804
4260
|
const installLogId = ctx.logStart("installWorktreeDeps", {
|
|
4805
|
-
useCase: currentUseCase
|
|
4806
|
-
language: useCaseToolchain.profile.id
|
|
4261
|
+
useCase: currentUseCase
|
|
4807
4262
|
});
|
|
4808
|
-
const install = await installWorktreeDeps(worktree
|
|
4263
|
+
const install = await installWorktreeDeps(worktree);
|
|
4809
4264
|
ctx.logEnd(installLogId, install.ok ? "success" : "error");
|
|
4810
4265
|
if (!install.ok) {
|
|
4811
|
-
|
|
4266
|
+
installFailed = true;
|
|
4812
4267
|
logger.warn(
|
|
4813
4268
|
{ useCase: currentUseCase, output: install.output },
|
|
4814
4269
|
"implement: dependency install in worktree failed; generated commands may not run until deps are installed"
|
|
@@ -4822,16 +4277,15 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4822
4277
|
return runAgent({
|
|
4823
4278
|
instructions: buildAgentInstructions("verification", input),
|
|
4824
4279
|
tools: toolsForUseCase("verification"),
|
|
4825
|
-
outputSchema: verificationOutputSchema
|
|
4826
|
-
// So verifyImplementation runs this repo's checks, not just npm scripts.
|
|
4827
|
-
languages: input.verificationLanguages
|
|
4280
|
+
outputSchema: verificationOutputSchema
|
|
4828
4281
|
});
|
|
4829
4282
|
}
|
|
4830
4283
|
if (useCases.includes("ingestion")) {
|
|
4831
|
-
const { summary, entrypoint } = await runImplementationUseCase("ingestion");
|
|
4284
|
+
const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
|
|
4832
4285
|
summaries.push(formatSummary("ingestion", summary));
|
|
4286
|
+
ingestRuntime = runtime;
|
|
4833
4287
|
ingestEntrypoint = entrypoint;
|
|
4834
|
-
if (
|
|
4288
|
+
if (ingestRuntime && ingestEntrypoint && !installFailed) {
|
|
4835
4289
|
ctx.clearNotices();
|
|
4836
4290
|
const runNow = await ctx.requestUserInput({
|
|
4837
4291
|
prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
|
|
@@ -4840,20 +4294,21 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4840
4294
|
messages: []
|
|
4841
4295
|
}) === true;
|
|
4842
4296
|
if (runNow) {
|
|
4843
|
-
const
|
|
4297
|
+
const ingestApp = await requireApplication();
|
|
4298
|
+
const writeKey = await resolveWriteKey(targetIndex);
|
|
4844
4299
|
ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
|
|
4845
4300
|
const scriptLogId = ctx.logStart("runIngestScript", {
|
|
4846
|
-
|
|
4301
|
+
runtime: ingestRuntime,
|
|
4847
4302
|
entrypoint: ingestEntrypoint
|
|
4848
4303
|
});
|
|
4849
4304
|
const startedAt = Date.now();
|
|
4850
4305
|
const run2 = await runIngestScript(
|
|
4851
4306
|
worktree,
|
|
4852
|
-
|
|
4307
|
+
ingestRuntime,
|
|
4853
4308
|
ingestEntrypoint,
|
|
4854
4309
|
{
|
|
4855
|
-
[APP_ID_VAR]:
|
|
4856
|
-
[API_KEY_VAR]:
|
|
4310
|
+
[APP_ID_VAR]: ingestApp.id,
|
|
4311
|
+
[API_KEY_VAR]: writeKey
|
|
4857
4312
|
}
|
|
4858
4313
|
);
|
|
4859
4314
|
ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
|
|
@@ -4863,7 +4318,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4863
4318
|
ingestRecordCount = parseIngestRecordCount(run2.output);
|
|
4864
4319
|
if (ingestRecordCount != null) {
|
|
4865
4320
|
track("AI Wizard Ingest Successful", {
|
|
4866
|
-
entity_name:
|
|
4321
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4867
4322
|
record_count: ingestRecordCount,
|
|
4868
4323
|
duration_ms: ingestDurationMs
|
|
4869
4324
|
});
|
|
@@ -4876,7 +4331,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4876
4331
|
outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
|
|
4877
4332
|
logger.warn(
|
|
4878
4333
|
{
|
|
4879
|
-
|
|
4334
|
+
runtime: ingestRuntime,
|
|
4880
4335
|
entrypoint: ingestEntrypoint,
|
|
4881
4336
|
reason: run2.reason
|
|
4882
4337
|
},
|
|
@@ -4899,7 +4354,7 @@ ${run2.output}` : status;
|
|
|
4899
4354
|
outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
|
|
4900
4355
|
logger.warn(
|
|
4901
4356
|
{
|
|
4902
|
-
|
|
4357
|
+
runtime: ingestRuntime,
|
|
4903
4358
|
entrypoint: ingestEntrypoint,
|
|
4904
4359
|
output: run2.output
|
|
4905
4360
|
},
|
|
@@ -4916,52 +4371,19 @@ ${run2.output}` : status;
|
|
|
4916
4371
|
}
|
|
4917
4372
|
}
|
|
4918
4373
|
const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
|
|
4919
|
-
if (ingestEntrypoint) {
|
|
4374
|
+
if (ingestRuntime && ingestEntrypoint) {
|
|
4920
4375
|
commandMessages.push(
|
|
4921
|
-
`Ingestion command: ${buildIngestCommand(worktree,
|
|
4376
|
+
`Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
|
|
4922
4377
|
);
|
|
4923
|
-
if (toolchain.ingest.kind === "manual") {
|
|
4924
|
-
commandMessages.push(
|
|
4925
|
-
`The wizard does not run ${ingestionProfile.displayName} ${toolchain.packageManager.id} projects \u2014 run the command above yourself to ingest.`
|
|
4926
|
-
);
|
|
4927
|
-
const missingTask = await missingBuildTask(worktree, toolchain);
|
|
4928
|
-
if (missingTask) {
|
|
4929
|
-
const warning = `\u26A0\uFE0F The command above needs a "${missingTask}" task, which is not in ${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"} \u2014 add it before running, or run the script through your IDE instead.`;
|
|
4930
|
-
commandMessages.push(warning);
|
|
4931
|
-
summaries.push(warning);
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
}
|
|
4935
|
-
if (ingestionSource === "local") {
|
|
4936
|
-
const limitation = localSourceLimitation(worktree, ingestionProfile);
|
|
4937
|
-
if (limitation) {
|
|
4938
|
-
commandMessages.push(`\u26A0\uFE0F ${limitation}`);
|
|
4939
|
-
summaries.push(`\u26A0\uFE0F ${limitation}`);
|
|
4940
|
-
}
|
|
4941
4378
|
}
|
|
4942
4379
|
await ctx.requestUserInput({
|
|
4943
|
-
// No question being asked here, just an acknowledgement — the
|
|
4944
|
-
// continue/decline hints below already say "continue".
|
|
4945
4380
|
prompt: "",
|
|
4946
4381
|
promptType: "enterToContinue",
|
|
4947
4382
|
options: [],
|
|
4948
4383
|
messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
|
|
4949
4384
|
});
|
|
4950
4385
|
}
|
|
4951
|
-
|
|
4952
|
-
if (skipSearch) {
|
|
4953
|
-
const target = describeSearchTarget(
|
|
4954
|
-
input.searchStrategy,
|
|
4955
|
-
input.frameworkName
|
|
4956
|
-
);
|
|
4957
|
-
summaries.push(
|
|
4958
|
-
`Search UI skipped: the wizard can't scaffold a native search UI for ${target}. Your records are in the "${targetIndex}" index \u2014 build the UI with Algolia's mobile InstantSearch libraries (https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/ios/ for iOS, .../android for Android).`
|
|
4959
|
-
);
|
|
4960
|
-
track("AI Wizard Search UI Skipped", {
|
|
4961
|
-
framework: input.frameworkName ?? "unknown"
|
|
4962
|
-
});
|
|
4963
|
-
}
|
|
4964
|
-
if (useCases.includes("search") && !skipSearch) {
|
|
4386
|
+
if (useCases.includes("search")) {
|
|
4965
4387
|
let extraInstructions = [];
|
|
4966
4388
|
const preSearchFiles = new Set(await listChangedFiles(worktree));
|
|
4967
4389
|
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
@@ -5003,7 +4425,29 @@ ${run2.output}` : status;
|
|
|
5003
4425
|
}
|
|
5004
4426
|
extraInstructions = verificationRetryInstructions(verification);
|
|
5005
4427
|
}
|
|
5006
|
-
|
|
4428
|
+
let searchKey;
|
|
4429
|
+
let searchKeyError;
|
|
4430
|
+
if (appId) {
|
|
4431
|
+
try {
|
|
4432
|
+
const resolved = await resolveSearchOnlyKey(
|
|
4433
|
+
targetIndex,
|
|
4434
|
+
appId,
|
|
4435
|
+
envSearchKey
|
|
4436
|
+
);
|
|
4437
|
+
searchKey = resolved.key;
|
|
4438
|
+
summaries.push(
|
|
4439
|
+
resolved.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
|
|
4440
|
+
);
|
|
4441
|
+
} catch (err) {
|
|
4442
|
+
searchKeyError = err.message;
|
|
4443
|
+
logger.warn(
|
|
4444
|
+
{ err: searchKeyError },
|
|
4445
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4446
|
+
);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
|
|
4450
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
5007
4451
|
(v) => !v.value.startsWith("<")
|
|
5008
4452
|
);
|
|
5009
4453
|
if (resolvedSearchEnvVars.length > 0) {
|
|
@@ -5014,13 +4458,29 @@ ${run2.output}` : status;
|
|
|
5014
4458
|
if (written.length > 0) {
|
|
5015
4459
|
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
5016
4460
|
}
|
|
4461
|
+
const stale = [];
|
|
4462
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4463
|
+
if (written.includes(v.name)) continue;
|
|
4464
|
+
const current = await readEnvVar(worktree, v.name);
|
|
4465
|
+
if (current && current !== v.value) stale.push(v);
|
|
4466
|
+
}
|
|
4467
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
4468
|
+
summaries.push(
|
|
4469
|
+
`\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
|
|
4470
|
+
);
|
|
4471
|
+
logger.warn(
|
|
4472
|
+
{ vars: stale.map((v) => v.name) },
|
|
4473
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
4474
|
+
);
|
|
4475
|
+
}
|
|
5017
4476
|
}
|
|
5018
|
-
const unresolvedSearchEnvVars =
|
|
4477
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
5019
4478
|
(v) => v.value.startsWith("<")
|
|
5020
4479
|
);
|
|
5021
4480
|
if (unresolvedSearchEnvVars.length > 0) {
|
|
5022
4481
|
summaries.push(
|
|
5023
|
-
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
|
|
4482
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
|
|
4483
|
+
(searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5024
4484
|
);
|
|
5025
4485
|
}
|
|
5026
4486
|
} else {
|
|
@@ -5032,9 +4492,9 @@ ${run2.output}` : status;
|
|
|
5032
4492
|
"implement: agent reported success but no files changed in the worktree"
|
|
5033
4493
|
);
|
|
5034
4494
|
}
|
|
5035
|
-
if (
|
|
4495
|
+
if (installFailed) {
|
|
5036
4496
|
summaries.push(
|
|
5037
|
-
|
|
4497
|
+
'\u26A0\uFE0F Dependency install in the worktree failed. Run your package manager install in the worktree before the command below, or it will fail with "Cannot find module".'
|
|
5038
4498
|
);
|
|
5039
4499
|
}
|
|
5040
4500
|
return {
|
|
@@ -5042,17 +4502,17 @@ ${run2.output}` : status;
|
|
|
5042
4502
|
filesChanged,
|
|
5043
4503
|
summary: summaries.join("\n\n"),
|
|
5044
4504
|
worktreePath: worktree,
|
|
5045
|
-
...useCases.includes("ingestion") && ingestEntrypoint ? {
|
|
4505
|
+
...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
|
|
5046
4506
|
ingestCommand: buildIngestCommand(
|
|
5047
4507
|
worktree,
|
|
5048
|
-
|
|
4508
|
+
ingestRuntime,
|
|
5049
4509
|
ingestEntrypoint
|
|
5050
4510
|
),
|
|
5051
4511
|
ingestScriptRan,
|
|
5052
4512
|
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
5053
4513
|
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5054
4514
|
} : {},
|
|
5055
|
-
...useCases.includes("search") ? { searchEnvVars:
|
|
4515
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
5056
4516
|
};
|
|
5057
4517
|
} finally {
|
|
5058
4518
|
process.chdir(repoRoot);
|
|
@@ -5095,8 +4555,8 @@ var defaultWorkflow = {
|
|
|
5095
4555
|
defineStep({
|
|
5096
4556
|
id: "select-index",
|
|
5097
4557
|
title: "Set up index",
|
|
5098
|
-
outputSchema:
|
|
5099
|
-
selection:
|
|
4558
|
+
outputSchema: z27.object({
|
|
4559
|
+
selection: z27.string()
|
|
5100
4560
|
}),
|
|
5101
4561
|
run: (ctx) => selectIndexStep(ctx)
|
|
5102
4562
|
}),
|
|
@@ -5374,20 +4834,20 @@ function parseCliArgs(argv) {
|
|
|
5374
4834
|
}
|
|
5375
4835
|
|
|
5376
4836
|
// src/lib/resetState.ts
|
|
5377
|
-
import { readdir as
|
|
5378
|
-
import { join as
|
|
4837
|
+
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
4838
|
+
import { join as join11 } from "node:path";
|
|
5379
4839
|
var KEEP = ["wizard.log"];
|
|
5380
4840
|
async function resetProjectState() {
|
|
5381
4841
|
const dir = stateDir();
|
|
5382
4842
|
let entries;
|
|
5383
4843
|
try {
|
|
5384
|
-
entries = await
|
|
4844
|
+
entries = await readdir4(dir);
|
|
5385
4845
|
} catch {
|
|
5386
4846
|
return { dir, removed: [] };
|
|
5387
4847
|
}
|
|
5388
4848
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
5389
4849
|
await Promise.all(
|
|
5390
|
-
targets.map((name) => rm2(
|
|
4850
|
+
targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
|
|
5391
4851
|
);
|
|
5392
4852
|
return { dir, removed: targets };
|
|
5393
4853
|
}
|
|
@@ -5442,31 +4902,38 @@ ${formatStepList(workflow)}`);
|
|
|
5442
4902
|
}
|
|
5443
4903
|
async function run(workflow) {
|
|
5444
4904
|
const store = useWizard.getState();
|
|
5445
|
-
|
|
4905
|
+
const instance = render(/* @__PURE__ */ jsx14(App, {}), { incrementalRendering: true });
|
|
4906
|
+
await store.waitForStart();
|
|
5446
4907
|
let user = await getUser();
|
|
5447
4908
|
if (!user) {
|
|
5448
|
-
|
|
5449
|
-
instance.cleanup();
|
|
4909
|
+
store.beginAuth();
|
|
5450
4910
|
try {
|
|
5451
4911
|
await runAuthLogin();
|
|
5452
4912
|
} catch (err) {
|
|
5453
|
-
|
|
4913
|
+
store.setError(err instanceof Error ? err.message : String(err));
|
|
4914
|
+
await instance.waitUntilExit();
|
|
5454
4915
|
process.exit(1);
|
|
5455
4916
|
}
|
|
5456
|
-
|
|
4917
|
+
store.endAuth();
|
|
5457
4918
|
user = await getUser();
|
|
5458
4919
|
if (!user) {
|
|
5459
4920
|
store.setError(
|
|
5460
|
-
"Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli auth login` directly."
|
|
4921
|
+
"Authentication completed but no Algolia user was returned. Try running `npx @algolia/cli@latest auth login` directly."
|
|
5461
4922
|
);
|
|
5462
4923
|
await instance.waitUntilExit();
|
|
5463
4924
|
process.exit(1);
|
|
5464
4925
|
}
|
|
5465
4926
|
}
|
|
5466
4927
|
store.setUser(user);
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
4928
|
+
let app;
|
|
4929
|
+
try {
|
|
4930
|
+
app = await ensureApplication();
|
|
4931
|
+
} catch (err) {
|
|
4932
|
+
store.setError(err instanceof Error ? err.message : String(err));
|
|
4933
|
+
await instance.waitUntilExit();
|
|
4934
|
+
process.exit(1);
|
|
4935
|
+
}
|
|
4936
|
+
runWorkflow(workflow, app.id);
|
|
5470
4937
|
}
|
|
5471
4938
|
var started = await startup();
|
|
5472
4939
|
if (typeof started === "number") {
|