@algolia/wizard 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -14
- package/dist/main.js +340 -431
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,22 +12,8 @@ npx @algolia/wizard
|
|
|
12
12
|
|
|
13
13
|
# run a specific workflow by id
|
|
14
14
|
npx @algolia/wizard <workflow-id>
|
|
15
|
-
|
|
16
|
-
# see all options
|
|
17
|
-
npx @algolia/wizard --help
|
|
18
15
|
```
|
|
19
16
|
|
|
20
|
-
### Options
|
|
21
|
-
|
|
22
|
-
| Flag | Effect |
|
|
23
|
-
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
-
| `--seed <step-id>` | Start the workflow at the step with this id (e.g. `--seed ingestion`), with the earlier steps pre-filled with test data. Pass with no value to print the step ids. |
|
|
25
|
-
| `--no-telemetry` | Send no telemetry or analytics for this run. |
|
|
26
|
-
| `--reset-on-run` | Wipe this project's wizard state (run state, AI-changes consent, worktrees) before starting, so the run behaves like a first-ever run. Credentials are untouched. |
|
|
27
|
-
| `-h`, `--help` | Print usage. |
|
|
28
|
-
|
|
29
|
-
`--seed` pre-fills the earlier steps with fabricated data so a single step can be exercised without running the whole workflow — useful for testing a step, not for a real implementation. It replaces any in-progress run for that workflow and pre-grants the AI-changes consent. See [Starting mid-workflow](CONTRIBUTING.md#starting-mid-workflow---seed).
|
|
30
|
-
|
|
31
17
|
On first run, Wizard checks whether you're signed in to the Algolia CLI. If you aren't, it runs `algolia auth login --non-interactive` — the browser opens for sign-in, and no prompts land in the wizard's terminal. It then asks which Algolia application to work in (skipping the question when the account has only one) and makes it current with `algolia application select`.
|
|
32
18
|
|
|
33
19
|
You'll also be asked once to consent to AI-authored changes to the repository.
|
package/dist/main.js
CHANGED
|
@@ -5,6 +5,7 @@ import { render } from "ink";
|
|
|
5
5
|
|
|
6
6
|
// src/ui/App.tsx
|
|
7
7
|
import { Box as Box17, Text as Text17, useApp, useInput as useInput7, useWindowSize as useWindowSize8 } from "ink";
|
|
8
|
+
import Spinner2 from "ink-spinner";
|
|
8
9
|
|
|
9
10
|
// src/core/store.ts
|
|
10
11
|
import { create } from "zustand";
|
|
@@ -251,6 +252,7 @@ var useWizard = create((set, get) => ({
|
|
|
251
252
|
_noticeTimer: null,
|
|
252
253
|
cliOutput: [],
|
|
253
254
|
targetIndex: null,
|
|
255
|
+
writtenFiles: [],
|
|
254
256
|
logs: [],
|
|
255
257
|
error: null,
|
|
256
258
|
inputReq: null,
|
|
@@ -344,6 +346,8 @@ var useWizard = create((set, get) => ({
|
|
|
344
346
|
})),
|
|
345
347
|
clearCliOutput: () => set({ cliOutput: [] }),
|
|
346
348
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
349
|
+
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
350
|
+
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
347
351
|
logStart: (kind, name, input) => {
|
|
348
352
|
const id = nanoid();
|
|
349
353
|
set((s) => ({
|
|
@@ -391,6 +395,7 @@ var useWizard = create((set, get) => ({
|
|
|
391
395
|
notices: [],
|
|
392
396
|
cliOutput: [],
|
|
393
397
|
targetIndex: null,
|
|
398
|
+
writtenFiles: [],
|
|
394
399
|
logs: [],
|
|
395
400
|
error: null,
|
|
396
401
|
inputReq: null,
|
|
@@ -1259,7 +1264,7 @@ var accessItems = [
|
|
|
1259
1264
|
{
|
|
1260
1265
|
tag: "WRITE",
|
|
1261
1266
|
title: "Code changes",
|
|
1262
|
-
description: "creates & edits files (search UI, config) in
|
|
1267
|
+
description: "creates & edits files (search UI, config) directly in your branch."
|
|
1263
1268
|
},
|
|
1264
1269
|
{
|
|
1265
1270
|
tag: "EXEC",
|
|
@@ -1274,7 +1279,7 @@ var accessItems = [
|
|
|
1274
1279
|
{
|
|
1275
1280
|
tag: "KEY",
|
|
1276
1281
|
title: "Credentials",
|
|
1277
|
-
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in
|
|
1282
|
+
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
|
|
1278
1283
|
}
|
|
1279
1284
|
];
|
|
1280
1285
|
var neverItems = [
|
|
@@ -1779,7 +1784,7 @@ function track(event, payload) {
|
|
|
1779
1784
|
// src/ui/App.tsx
|
|
1780
1785
|
import { jsx as jsx15, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1781
1786
|
function App() {
|
|
1782
|
-
const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
|
|
1787
|
+
const { phase, error, homeScreen, currentStepIndex, steps, inputReq, user } = useWizard();
|
|
1783
1788
|
const { exit } = useApp();
|
|
1784
1789
|
const { columns, rows } = useWindowSize8();
|
|
1785
1790
|
const [showLogs, setShowLogs] = useState7(false);
|
|
@@ -1850,8 +1855,19 @@ function App() {
|
|
|
1850
1855
|
width: showSidebar ? 70 : "100%",
|
|
1851
1856
|
flexGrow: 1,
|
|
1852
1857
|
children: [
|
|
1858
|
+
phase === "preflight" && !user && /* @__PURE__ */ jsx15(Box17, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: COLORS.strong, bold: true, children: [
|
|
1859
|
+
/* @__PURE__ */ jsx15(Spinner2, { type: "dots" }),
|
|
1860
|
+
" Signing in to Algolia"
|
|
1861
|
+
] }) }),
|
|
1862
|
+
phase === "preflight" && user && /* @__PURE__ */ jsx15(Box17, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: COLORS.strong, bold: true, children: [
|
|
1863
|
+
/* @__PURE__ */ jsx15(Spinner2, { type: "dots" }),
|
|
1864
|
+
" Getting things ready"
|
|
1865
|
+
] }) }),
|
|
1853
1866
|
phase === "authenticating" && /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", marginBottom: 1, children: [
|
|
1854
|
-
/* @__PURE__ */
|
|
1867
|
+
/* @__PURE__ */ jsxs16(Text17, { color: COLORS.strong, bold: true, children: [
|
|
1868
|
+
/* @__PURE__ */ jsx15(Spinner2, { type: "dots" }),
|
|
1869
|
+
" Signing in to Algolia"
|
|
1870
|
+
] }),
|
|
1855
1871
|
/* @__PURE__ */ jsx15(Text17, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
|
|
1856
1872
|
] }),
|
|
1857
1873
|
/* @__PURE__ */ jsx15(CliOutput, {}),
|
|
@@ -2554,6 +2570,7 @@ function writeFileTool(ctx) {
|
|
|
2554
2570
|
}
|
|
2555
2571
|
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2556
2572
|
await writeFile3(resolved2.target, content, "utf8");
|
|
2573
|
+
useWizard.getState().recordWrittenFile(resolved2.target);
|
|
2557
2574
|
return `Wrote to ${filePath}`;
|
|
2558
2575
|
} catch (err) {
|
|
2559
2576
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -3341,7 +3358,7 @@ function defaultCreateModel() {
|
|
|
3341
3358
|
}
|
|
3342
3359
|
function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
3343
3360
|
return tool10({
|
|
3344
|
-
description: "Generate realistic sample records for an entity and write them to a JSON file
|
|
3361
|
+
description: "Generate realistic sample records for an entity and write them to a JSON file. 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.",
|
|
3345
3362
|
inputSchema: z17.object({
|
|
3346
3363
|
entityName: z17.string().describe("Name of the entity to generate records for."),
|
|
3347
3364
|
attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
|
|
@@ -3747,7 +3764,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3747
3764
|
// package.json
|
|
3748
3765
|
var package_default = {
|
|
3749
3766
|
name: "@algolia/wizard",
|
|
3750
|
-
version: "0.
|
|
3767
|
+
version: "0.28.0",
|
|
3751
3768
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3752
3769
|
type: "module",
|
|
3753
3770
|
engines: {
|
|
@@ -4114,11 +4131,10 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
4114
4131
|
function formatReviewSummary(result) {
|
|
4115
4132
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
4116
4133
|
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
4117
|
-
const isWorktreeCommand = step.includes("/worktrees/");
|
|
4118
4134
|
return {
|
|
4119
4135
|
text: `\u2192 ${step}`,
|
|
4120
|
-
color: isIngestCommand ? COLORS.brand :
|
|
4121
|
-
bold: isIngestCommand
|
|
4136
|
+
color: isIngestCommand ? COLORS.brand : void 0,
|
|
4137
|
+
bold: isIngestCommand
|
|
4122
4138
|
};
|
|
4123
4139
|
});
|
|
4124
4140
|
return [
|
|
@@ -4153,15 +4169,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4153
4169
|
|
|
4154
4170
|
// src/actions/implement.ts
|
|
4155
4171
|
import z29 from "zod";
|
|
4156
|
-
import { join as join12 } from "node:path";
|
|
4172
|
+
import { join as join12, relative as relative6 } from "node:path";
|
|
4157
4173
|
|
|
4158
|
-
// src/lib/
|
|
4174
|
+
// src/lib/git.ts
|
|
4159
4175
|
import { execFile as execFile2 } from "node:child_process";
|
|
4160
|
-
import { copyFile, mkdir as mkdir6,
|
|
4176
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4161
4177
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4162
4178
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4163
|
-
var MAX_WIZARD_WORKTREES = 3;
|
|
4164
|
-
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4165
4179
|
function git(args) {
|
|
4166
4180
|
return new Promise((resolve4, reject) => {
|
|
4167
4181
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4184,44 +4198,7 @@ async function assertGitRepoWithHead(repoRoot) {
|
|
|
4184
4198
|
);
|
|
4185
4199
|
}
|
|
4186
4200
|
}
|
|
4187
|
-
async function
|
|
4188
|
-
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4189
|
-
return out.trim().length > 0;
|
|
4190
|
-
}
|
|
4191
|
-
async function pruneOldWorktrees(repoRoot) {
|
|
4192
|
-
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
4193
|
-
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4194
|
-
for (const slug of stale) {
|
|
4195
|
-
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4196
|
-
try {
|
|
4197
|
-
await git([
|
|
4198
|
-
"-C",
|
|
4199
|
-
repoRoot,
|
|
4200
|
-
"worktree",
|
|
4201
|
-
"remove",
|
|
4202
|
-
"--force",
|
|
4203
|
-
join10(dir, slug)
|
|
4204
|
-
]);
|
|
4205
|
-
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4206
|
-
} catch (err) {
|
|
4207
|
-
logger.warn(
|
|
4208
|
-
{ branch, err: err.message },
|
|
4209
|
-
"createWorktree: failed to prune a stale wizard worktree; continuing"
|
|
4210
|
-
);
|
|
4211
|
-
}
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
async function createWorktree(repoRoot) {
|
|
4215
|
-
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4216
|
-
const dirSlug = branch.replace(/\//g, "-");
|
|
4217
|
-
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4218
|
-
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4219
|
-
await pruneOldWorktrees(repoRoot);
|
|
4220
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
4221
|
-
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4222
|
-
return { path, branch };
|
|
4223
|
-
}
|
|
4224
|
-
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4201
|
+
async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
4225
4202
|
const trimmed = sourcePath.trim();
|
|
4226
4203
|
if (!trimmed) {
|
|
4227
4204
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4235,7 +4212,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4235
4212
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4236
4213
|
}
|
|
4237
4214
|
const relPath = join10(ingestDir, basename2(source));
|
|
4238
|
-
const dest = join10(
|
|
4215
|
+
const dest = join10(repoRoot, relPath);
|
|
4216
|
+
if (resolve3(source) === resolve3(dest)) {
|
|
4217
|
+
return { ok: true, relPath };
|
|
4218
|
+
}
|
|
4239
4219
|
try {
|
|
4240
4220
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4241
4221
|
await copyFile(source, dest);
|
|
@@ -4250,10 +4230,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4250
4230
|
function hasEnvVar(content, name) {
|
|
4251
4231
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4252
4232
|
}
|
|
4253
|
-
async function readEnvVar(
|
|
4233
|
+
async function readEnvVar(repoRoot, name) {
|
|
4254
4234
|
let content;
|
|
4255
4235
|
try {
|
|
4256
|
-
content = await readFile8(join10(
|
|
4236
|
+
content = await readFile8(join10(repoRoot, ".env"), "utf8");
|
|
4257
4237
|
} catch (err) {
|
|
4258
4238
|
if (err.code !== "ENOENT") throw err;
|
|
4259
4239
|
return void 0;
|
|
@@ -4267,8 +4247,8 @@ async function readEnvVar(worktreePath, name) {
|
|
|
4267
4247
|
if (!value || value.startsWith("<")) return void 0;
|
|
4268
4248
|
return value;
|
|
4269
4249
|
}
|
|
4270
|
-
async function writeSearchEnvValues(
|
|
4271
|
-
const target = join10(
|
|
4250
|
+
async function writeSearchEnvValues(repoRoot, vars) {
|
|
4251
|
+
const target = join10(repoRoot, ".env");
|
|
4272
4252
|
let existing = "";
|
|
4273
4253
|
try {
|
|
4274
4254
|
existing = await readFile8(target, "utf8");
|
|
@@ -4283,61 +4263,27 @@ async function writeSearchEnvValues(worktreePath, vars) {
|
|
|
4283
4263
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4284
4264
|
return missing.map((v) => v.name);
|
|
4285
4265
|
}
|
|
4286
|
-
async function listChangedFiles(worktreePath) {
|
|
4287
|
-
const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
|
|
4288
|
-
const entries = raw.split("\0");
|
|
4289
|
-
const files = [];
|
|
4290
|
-
for (let i = 0; i < entries.length; i += 1) {
|
|
4291
|
-
const entry = entries[i];
|
|
4292
|
-
if (!entry) continue;
|
|
4293
|
-
files.push(entry.slice(3));
|
|
4294
|
-
if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
|
|
4295
|
-
}
|
|
4296
|
-
return files;
|
|
4297
|
-
}
|
|
4298
4266
|
function normalizeFindingPaths(findings) {
|
|
4299
4267
|
return {
|
|
4300
4268
|
...findings,
|
|
4301
4269
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4302
4270
|
...e,
|
|
4303
|
-
paths: e.paths.map(
|
|
4271
|
+
paths: e.paths.map(toRootRelative)
|
|
4304
4272
|
})),
|
|
4305
4273
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4306
4274
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4307
4275
|
...e,
|
|
4308
|
-
paths: e.paths.map(
|
|
4276
|
+
paths: e.paths.map(toRootRelative)
|
|
4309
4277
|
}))
|
|
4310
4278
|
};
|
|
4311
4279
|
}
|
|
4312
4280
|
function normalizeSearchLocation(path) {
|
|
4313
|
-
const normalized = path ?
|
|
4281
|
+
const normalized = path ? toRootRelative(path).trim() : "";
|
|
4314
4282
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4315
4283
|
}
|
|
4316
|
-
function
|
|
4284
|
+
function toRootRelative(p) {
|
|
4317
4285
|
return p.replace(/^\/+/, "");
|
|
4318
4286
|
}
|
|
4319
|
-
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
4320
|
-
const MAX_LISTED_DIRTY_FILES = 10;
|
|
4321
|
-
const dirty = await listChangedFiles(repoRoot);
|
|
4322
|
-
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4323
|
-
const overflow = dirty.length - shown.length;
|
|
4324
|
-
const answer = await ctx.requestUserInput({
|
|
4325
|
-
prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
|
|
4326
|
-
promptType: "acceptReject",
|
|
4327
|
-
options: [],
|
|
4328
|
-
messages: [
|
|
4329
|
-
`${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
|
|
4330
|
-
...shown.map((file) => ` \u2022 ${file}`),
|
|
4331
|
-
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4332
|
-
"Commit or stash them first to include them in the implementation."
|
|
4333
|
-
]
|
|
4334
|
-
});
|
|
4335
|
-
if (answer !== true) {
|
|
4336
|
-
throw new Error(
|
|
4337
|
-
"implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
|
|
4338
|
-
);
|
|
4339
|
-
}
|
|
4340
|
-
}
|
|
4341
4287
|
|
|
4342
4288
|
// src/lib/algoliaDocs.ts
|
|
4343
4289
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
@@ -4397,11 +4343,6 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4397
4343
|
return loadAlgoliaDoc("js");
|
|
4398
4344
|
}
|
|
4399
4345
|
|
|
4400
|
-
// src/lib/shell.ts
|
|
4401
|
-
function shellQuote(value) {
|
|
4402
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
4346
|
// src/actions/resolveEnvVarPrefix.ts
|
|
4406
4347
|
import z28 from "zod";
|
|
4407
4348
|
var resolveEnvVarPrefixSchema = z28.object({
|
|
@@ -4421,9 +4362,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
|
4421
4362
|
|
|
4422
4363
|
// src/actions/implement.ts
|
|
4423
4364
|
var implementSchema = z29.object({
|
|
4424
|
-
filesChanged: z29.array(z29.string()),
|
|
4425
4365
|
summary: z29.string(),
|
|
4426
|
-
worktreePath: z29.string().optional(),
|
|
4427
4366
|
ingestCommand: z29.string().optional(),
|
|
4428
4367
|
ingestScriptRan: z29.boolean().optional(),
|
|
4429
4368
|
ingestRecordCount: z29.number().optional(),
|
|
@@ -4476,8 +4415,6 @@ function frameworksForDoc(language) {
|
|
|
4476
4415
|
}
|
|
4477
4416
|
function baseInstructions(input) {
|
|
4478
4417
|
return [
|
|
4479
|
-
// Agents have renamed this (e.g. appending the project name), which the
|
|
4480
|
-
// index-scoped keys then reject with a 403.
|
|
4481
4418
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4482
4419
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4483
4420
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4495,7 +4432,7 @@ function sourceSpecificInstructions(input) {
|
|
|
4495
4432
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4496
4433
|
],
|
|
4497
4434
|
fileUpload: [
|
|
4498
|
-
`Records come from the developer's file, already copied into the
|
|
4435
|
+
`Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4499
4436
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4500
4437
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4501
4438
|
"Never fabricate, hardcode, or substitute a different file."
|
|
@@ -4551,14 +4488,11 @@ function searchInstructions(input) {
|
|
|
4551
4488
|
"If a search box already exists, replace it with yours.",
|
|
4552
4489
|
`Read the index name from the ${publicIndexNameVar(input.publicEnvVarPrefix)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
|
|
4553
4490
|
"Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
|
|
4554
|
-
// The key is provisioned only after verification passes,
|
|
4555
|
-
//
|
|
4556
|
-
//
|
|
4557
|
-
// would be reused as if it were real.
|
|
4491
|
+
// The key is provisioned only after verification passes, and the wizard
|
|
4492
|
+
// reads .env to decide whether a key already exists — an agent-invented
|
|
4493
|
+
// value there would be reused as if it were real.
|
|
4558
4494
|
`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.`,
|
|
4559
|
-
//
|
|
4560
|
-
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4561
|
-
// reading a var the wizard never wrote.
|
|
4495
|
+
// The wizard writes these exact names into .env right after this step.
|
|
4562
4496
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4563
4497
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4564
4498
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4567,10 +4501,10 @@ function searchInstructions(input) {
|
|
|
4567
4501
|
}
|
|
4568
4502
|
function verificationInstructions(input) {
|
|
4569
4503
|
return [
|
|
4570
|
-
"Verify the Algolia implementation changes
|
|
4504
|
+
"Verify the Algolia implementation changes.",
|
|
4571
4505
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4572
4506
|
"Run the project's own checks (lint, type check, tests) via runShell, using the commands the project actually defines \u2014 its task runner, manifest scripts, or Makefile. Run every check that applies, not just the first.",
|
|
4573
|
-
"
|
|
4507
|
+
"If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4574
4508
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4575
4509
|
"Do not make speculative fixes when no checks exist, a check cannot run, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
|
|
4576
4510
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4665,11 +4599,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4665
4599
|
}
|
|
4666
4600
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4667
4601
|
}
|
|
4668
|
-
function makeToolContext(
|
|
4602
|
+
function makeToolContext(root, env = async () => ({})) {
|
|
4669
4603
|
return createToolContext(
|
|
4670
4604
|
DEFAULT_TOOL_LIMITS,
|
|
4671
|
-
|
|
4672
|
-
createShellContext({ env, approve: storeApproval(
|
|
4605
|
+
root,
|
|
4606
|
+
createShellContext({ env, approve: storeApproval(root) })
|
|
4673
4607
|
);
|
|
4674
4608
|
}
|
|
4675
4609
|
function verificationRetryInstructions(verification) {
|
|
@@ -4677,7 +4611,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4677
4611
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4678
4612
|
];
|
|
4679
4613
|
}
|
|
4680
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES
|
|
4614
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4681
4615
|
const repoRoot = process.cwd();
|
|
4682
4616
|
const scan = ctx.getStepOutput("project-scan");
|
|
4683
4617
|
const entities = ctx.getStepOutput(
|
|
@@ -4758,9 +4692,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4758
4692
|
const targetIndex = selected?.selection;
|
|
4759
4693
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4760
4694
|
await assertGitRepoWithHead(repoRoot);
|
|
4761
|
-
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4762
|
-
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4763
|
-
}
|
|
4764
4695
|
const normalized = normalizeFindingPaths(findings);
|
|
4765
4696
|
const confirmed2 = normalized.confirmedEntities;
|
|
4766
4697
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4772,328 +4703,303 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4772
4703
|
if (useCases.includes("ingestion")) {
|
|
4773
4704
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4774
4705
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4706
|
+
let uploadFilePath;
|
|
4707
|
+
let uploadWarning;
|
|
4708
|
+
if (ingestionSource === "fileUpload") {
|
|
4709
|
+
const copied = await copyUploadIntoProject(
|
|
4710
|
+
repoRoot,
|
|
4711
|
+
INGEST_DIR,
|
|
4712
|
+
uploadSourcePath ?? ""
|
|
4713
|
+
);
|
|
4714
|
+
if (copied.ok) {
|
|
4715
|
+
uploadFilePath = copied.relPath;
|
|
4716
|
+
} else {
|
|
4717
|
+
ingestionSource = "generated";
|
|
4718
|
+
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4719
|
+
logger.warn(
|
|
4720
|
+
{ reason: copied.reason },
|
|
4721
|
+
"implement: file upload unavailable; falling back to generated sample records"
|
|
4722
|
+
);
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
|
|
4726
|
+
const input = {
|
|
4727
|
+
findings: normalized,
|
|
4728
|
+
confirmed: confirmed2,
|
|
4729
|
+
searchLocation,
|
|
4730
|
+
targetIndex,
|
|
4731
|
+
language,
|
|
4732
|
+
publicEnvVarPrefix,
|
|
4733
|
+
appId,
|
|
4734
|
+
searchEnvVars: publicSearchEnvVars(publicEnvVarPrefix, targetIndex, appId),
|
|
4735
|
+
ingestDir: INGEST_DIR,
|
|
4736
|
+
ingestionSource,
|
|
4737
|
+
uploadFilePath,
|
|
4738
|
+
searchUiTarget: searchUiTarget(language)
|
|
4739
|
+
};
|
|
4740
|
+
const summaries = [];
|
|
4741
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4742
|
+
let envSearchKey;
|
|
4743
|
+
let envAppIdMismatch = false;
|
|
4744
|
+
if (useCases.includes("search") && appId) {
|
|
4745
|
+
const envAppId = await readEnvVar(
|
|
4746
|
+
repoRoot,
|
|
4747
|
+
publicAppIdVar(publicEnvVarPrefix)
|
|
4748
|
+
);
|
|
4749
|
+
if (envAppId === appId) {
|
|
4750
|
+
envSearchKey = await readEnvVar(
|
|
4782
4751
|
repoRoot,
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4752
|
+
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4753
|
+
);
|
|
4754
|
+
} else if (envAppId) {
|
|
4755
|
+
envAppIdMismatch = true;
|
|
4756
|
+
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4757
|
+
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4758
|
+
summaries.push(
|
|
4759
|
+
`\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
|
|
4760
|
+
);
|
|
4761
|
+
logger.warn(
|
|
4762
|
+
{ envAppId, appId },
|
|
4763
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4786
4764
|
);
|
|
4787
|
-
if (copied.ok) {
|
|
4788
|
-
uploadFilePath = copied.relPath;
|
|
4789
|
-
} else {
|
|
4790
|
-
ingestionSource = "generated";
|
|
4791
|
-
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4792
|
-
logger.warn(
|
|
4793
|
-
{ reason: copied.reason },
|
|
4794
|
-
"implement: file upload unavailable; falling back to generated sample records"
|
|
4795
|
-
);
|
|
4796
|
-
}
|
|
4797
4765
|
}
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4766
|
+
}
|
|
4767
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4768
|
+
let agentRuns = 0;
|
|
4769
|
+
let ingestCommand;
|
|
4770
|
+
let ingestScriptRan = false;
|
|
4771
|
+
let ingestRecordCount;
|
|
4772
|
+
let ingestDurationMs;
|
|
4773
|
+
let ingestOutcomeMessage;
|
|
4774
|
+
const ingestKeyAppId = ingestAppId;
|
|
4775
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
|
|
4776
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4777
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4778
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4779
|
+
})) : void 0;
|
|
4780
|
+
const searchTools = makeToolContext(repoRoot);
|
|
4781
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4782
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4783
|
+
agentRuns += 1;
|
|
4784
|
+
return runAgent({
|
|
4785
|
+
instructions: buildAgentInstructions(
|
|
4786
|
+
currentUseCase,
|
|
4787
|
+
input,
|
|
4788
|
+
extraInstructions
|
|
4811
4789
|
),
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
if (
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4790
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4791
|
+
outputSchema: implementationOutputSchema,
|
|
4792
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
async function runVerificationUseCase() {
|
|
4796
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4797
|
+
agentRuns += 1;
|
|
4798
|
+
return runAgent({
|
|
4799
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4800
|
+
tools: toolsForUseCase("verification"),
|
|
4801
|
+
outputSchema: verificationOutputSchema,
|
|
4802
|
+
toolContext: searchTools
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
if (useCases.includes("ingestion")) {
|
|
4806
|
+
let ingestFailureDetail;
|
|
4807
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4808
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4809
|
+
ingestCommand = result.ingestCommand;
|
|
4810
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4811
|
+
const executions = ingestionContext.shell.executions;
|
|
4812
|
+
const {
|
|
4813
|
+
run: ingestRun,
|
|
4814
|
+
attempt: ingestAttempt,
|
|
4815
|
+
recordCount
|
|
4816
|
+
} = ingestOutcome(executions, ingestCommand);
|
|
4817
|
+
ingestScriptRan = ingestRun != null;
|
|
4818
|
+
ingestRecordCount = recordCount;
|
|
4819
|
+
ingestDurationMs = ingestRun?.durationMs;
|
|
4820
|
+
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4821
|
+
summaries.push(
|
|
4822
|
+
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
|
|
4823
|
+
);
|
|
4824
|
+
logger.warn(
|
|
4825
|
+
{ ingestCommand },
|
|
4826
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4825
4827
|
);
|
|
4826
|
-
if (envAppId === appId) {
|
|
4827
|
-
envSearchKey = await readEnvVar(
|
|
4828
|
-
worktree,
|
|
4829
|
-
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4830
|
-
);
|
|
4831
|
-
} else if (envAppId) {
|
|
4832
|
-
envAppIdMismatch = true;
|
|
4833
|
-
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4834
|
-
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4835
|
-
summaries.push(
|
|
4836
|
-
`\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
|
|
4837
|
-
);
|
|
4838
|
-
logger.warn(
|
|
4839
|
-
{ envAppId, appId },
|
|
4840
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4841
|
-
);
|
|
4842
|
-
}
|
|
4843
|
-
}
|
|
4844
|
-
let finalSearchEnvVars = input.searchEnvVars;
|
|
4845
|
-
let agentRuns = 0;
|
|
4846
|
-
let ingestCommand;
|
|
4847
|
-
let ingestScriptRan = false;
|
|
4848
|
-
let ingestRecordCount;
|
|
4849
|
-
let ingestDurationMs;
|
|
4850
|
-
let ingestOutcomeMessage;
|
|
4851
|
-
const ingestKeyAppId = ingestAppId;
|
|
4852
|
-
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4853
|
-
[APP_ID_VAR]: ingestKeyAppId,
|
|
4854
|
-
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4855
|
-
[INDEX_NAME_VAR]: targetIndex
|
|
4856
|
-
})) : void 0;
|
|
4857
|
-
const searchTools = makeToolContext(worktree);
|
|
4858
|
-
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4859
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4860
|
-
agentRuns += 1;
|
|
4861
|
-
return runAgent({
|
|
4862
|
-
instructions: buildAgentInstructions(
|
|
4863
|
-
currentUseCase,
|
|
4864
|
-
input,
|
|
4865
|
-
extraInstructions
|
|
4866
|
-
),
|
|
4867
|
-
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4868
|
-
outputSchema: implementationOutputSchema,
|
|
4869
|
-
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4870
|
-
});
|
|
4871
|
-
}
|
|
4872
|
-
async function runVerificationUseCase() {
|
|
4873
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4874
|
-
agentRuns += 1;
|
|
4875
|
-
return runAgent({
|
|
4876
|
-
instructions: buildAgentInstructions("verification", input),
|
|
4877
|
-
tools: toolsForUseCase("verification"),
|
|
4878
|
-
outputSchema: verificationOutputSchema,
|
|
4879
|
-
toolContext: searchTools
|
|
4880
|
-
});
|
|
4881
4828
|
}
|
|
4882
|
-
if (
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
run: ingestRun,
|
|
4891
|
-
attempt: ingestAttempt,
|
|
4892
|
-
recordCount
|
|
4893
|
-
} = ingestOutcome(executions, ingestCommand);
|
|
4894
|
-
ingestScriptRan = ingestRun != null;
|
|
4895
|
-
ingestRecordCount = recordCount;
|
|
4896
|
-
ingestDurationMs = ingestRun?.durationMs;
|
|
4897
|
-
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4898
|
-
summaries.push(
|
|
4899
|
-
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
|
|
4900
|
-
);
|
|
4901
|
-
logger.warn(
|
|
4902
|
-
{ ingestCommand },
|
|
4903
|
-
"implement: ingestion ran without a reviewScript call"
|
|
4904
|
-
);
|
|
4829
|
+
if (ingestScriptRan) {
|
|
4830
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4831
|
+
if (ingestRecordCount != null) {
|
|
4832
|
+
track("AI Wizard Ingest Successful", {
|
|
4833
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4834
|
+
record_count: ingestRecordCount,
|
|
4835
|
+
duration_ms: ingestDurationMs ?? 0
|
|
4836
|
+
});
|
|
4905
4837
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
duration_ms: ingestDurationMs ?? 0
|
|
4913
|
-
});
|
|
4914
|
-
}
|
|
4915
|
-
} else {
|
|
4916
|
-
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4917
|
-
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4918
|
-
ingestFailureDetail = detail;
|
|
4919
|
-
summaries.push(
|
|
4920
|
-
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4838
|
+
} else {
|
|
4839
|
+
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4840
|
+
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4841
|
+
ingestFailureDetail = detail;
|
|
4842
|
+
summaries.push(
|
|
4843
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4921
4844
|
${detail}` : ""}`
|
|
4922
|
-
|
|
4923
|
-
|
|
4845
|
+
);
|
|
4846
|
+
logger.warn(
|
|
4847
|
+
{
|
|
4848
|
+
ingestCommand,
|
|
4849
|
+
reason,
|
|
4850
|
+
approved: ingestAttempt?.approved,
|
|
4851
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4852
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4853
|
+
commandsRun: executions.length
|
|
4854
|
+
},
|
|
4855
|
+
"implement: ingestion script did not complete successfully"
|
|
4856
|
+
);
|
|
4857
|
+
track("Error", {
|
|
4858
|
+
step: "Push Data",
|
|
4859
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4860
|
+
product_area: "AI Wizard"
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
|
|
4864
|
+
await ctx.requestUserInput({
|
|
4865
|
+
prompt: "",
|
|
4866
|
+
promptType: "enterToContinue",
|
|
4867
|
+
options: [],
|
|
4868
|
+
// One message per line: Notices.tsx counts a message as one wrapped
|
|
4869
|
+
// line, so an embedded newline overflows the panel's height accounting.
|
|
4870
|
+
messages: [
|
|
4871
|
+
ingestOutcomeMessage,
|
|
4872
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4873
|
+
...commandMessages
|
|
4874
|
+
]
|
|
4875
|
+
});
|
|
4876
|
+
}
|
|
4877
|
+
if (useCases.includes("search")) {
|
|
4878
|
+
let extraInstructions = [];
|
|
4879
|
+
useWizard.getState().clearWrittenFiles();
|
|
4880
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4881
|
+
if (attempt > 1) {
|
|
4882
|
+
logger.info(
|
|
4924
4883
|
{
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
exitCode: ingestAttempt?.exitCode,
|
|
4929
|
-
timedOut: ingestAttempt?.timedOut,
|
|
4930
|
-
commandsRun: executions.length
|
|
4884
|
+
attempt,
|
|
4885
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4886
|
+
extraInstructions
|
|
4931
4887
|
},
|
|
4932
|
-
"implement:
|
|
4888
|
+
"implement: retrying search implementation after failed verification"
|
|
4933
4889
|
);
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4890
|
+
}
|
|
4891
|
+
const { summary } = await runImplementationUseCase(
|
|
4892
|
+
"search",
|
|
4893
|
+
extraInstructions
|
|
4894
|
+
);
|
|
4895
|
+
summaries.push(formatSummary("search", summary));
|
|
4896
|
+
const verification = await runVerificationUseCase();
|
|
4897
|
+
summaries.push(formatSummary("verification", verification.summary));
|
|
4898
|
+
if (verification.sufficient) {
|
|
4899
|
+
ctx.setUserInput("implementation", "success");
|
|
4900
|
+
const searchFilesChanged = [
|
|
4901
|
+
...new Set(useWizard.getState().writtenFiles)
|
|
4902
|
+
].map((file) => relative6(repoRoot, file));
|
|
4903
|
+
track("AI Wizard Frontend Component Generated", {
|
|
4904
|
+
filePaths: searchFilesChanged
|
|
4905
|
+
});
|
|
4906
|
+
track("AI Wizard Wired to UI", {
|
|
4907
|
+
location_heuristic: searchLocation ?? "unknown"
|
|
4938
4908
|
});
|
|
4909
|
+
break;
|
|
4939
4910
|
}
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4911
|
+
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4912
|
+
ctx.setUserInput("implementation", "fail");
|
|
4913
|
+
throw new Error(
|
|
4914
|
+
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4915
|
+
);
|
|
4943
4916
|
}
|
|
4944
|
-
|
|
4945
|
-
prompt: "",
|
|
4946
|
-
promptType: "enterToContinue",
|
|
4947
|
-
options: [],
|
|
4948
|
-
// The wizard never streams command output, so a failed run's tail is the
|
|
4949
|
-
// only place the developer sees why it failed. One message per line:
|
|
4950
|
-
// the panel's height accounting counts a message as one wrapped line
|
|
4951
|
-
// (see Notices.tsx), so an embedded newline overflows it.
|
|
4952
|
-
messages: [
|
|
4953
|
-
ingestOutcomeMessage,
|
|
4954
|
-
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4955
|
-
...commandMessages
|
|
4956
|
-
]
|
|
4957
|
-
});
|
|
4917
|
+
extraInstructions = verificationRetryInstructions(verification);
|
|
4958
4918
|
}
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4919
|
+
let searchKey;
|
|
4920
|
+
let searchKeyError;
|
|
4921
|
+
if (appId) {
|
|
4922
|
+
try {
|
|
4923
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4924
|
+
targetIndex,
|
|
4925
|
+
appId,
|
|
4926
|
+
envSearchKey
|
|
4927
|
+
);
|
|
4928
|
+
searchKey = resolved2.key;
|
|
4929
|
+
summaries.push(
|
|
4930
|
+
resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
|
|
4931
|
+
);
|
|
4932
|
+
} catch (err) {
|
|
4933
|
+
searchKeyError = err.message;
|
|
4934
|
+
logger.warn(
|
|
4935
|
+
{ err: searchKeyError },
|
|
4936
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4976
4937
|
);
|
|
4977
|
-
summaries.push(formatSummary("search", summary));
|
|
4978
|
-
const verification = await runVerificationUseCase();
|
|
4979
|
-
summaries.push(formatSummary("verification", verification.summary));
|
|
4980
|
-
if (verification.sufficient) {
|
|
4981
|
-
ctx.setUserInput("implementation", "success");
|
|
4982
|
-
const searchFilesChanged = (await listChangedFiles(worktree)).filter(
|
|
4983
|
-
(file) => !preSearchFiles.has(file)
|
|
4984
|
-
);
|
|
4985
|
-
track("AI Wizard Frontend Component Generated", {
|
|
4986
|
-
filePaths: searchFilesChanged
|
|
4987
|
-
});
|
|
4988
|
-
track("AI Wizard Wired to UI", {
|
|
4989
|
-
location_heuristic: searchLocation ?? "unknown"
|
|
4990
|
-
});
|
|
4991
|
-
break;
|
|
4992
|
-
}
|
|
4993
|
-
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4994
|
-
ctx.setUserInput("implementation", "fail");
|
|
4995
|
-
throw new Error(
|
|
4996
|
-
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4997
|
-
);
|
|
4998
|
-
}
|
|
4999
|
-
extraInstructions = verificationRetryInstructions(verification);
|
|
5000
|
-
}
|
|
5001
|
-
let searchKey;
|
|
5002
|
-
let searchKeyError;
|
|
5003
|
-
if (appId) {
|
|
5004
|
-
try {
|
|
5005
|
-
const resolved2 = await resolveSearchOnlyKey(
|
|
5006
|
-
targetIndex,
|
|
5007
|
-
appId,
|
|
5008
|
-
envSearchKey
|
|
5009
|
-
);
|
|
5010
|
-
searchKey = resolved2.key;
|
|
5011
|
-
summaries.push(
|
|
5012
|
-
resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
|
|
5013
|
-
);
|
|
5014
|
-
} catch (err) {
|
|
5015
|
-
searchKeyError = err.message;
|
|
5016
|
-
logger.warn(
|
|
5017
|
-
{ err: searchKeyError },
|
|
5018
|
-
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
5019
|
-
);
|
|
5020
|
-
}
|
|
5021
4938
|
}
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
4939
|
+
}
|
|
4940
|
+
finalSearchEnvVars = publicSearchEnvVars(
|
|
4941
|
+
publicEnvVarPrefix,
|
|
4942
|
+
targetIndex,
|
|
4943
|
+
appId,
|
|
4944
|
+
searchKey
|
|
4945
|
+
);
|
|
4946
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4947
|
+
(v) => !v.value.startsWith("<")
|
|
4948
|
+
);
|
|
4949
|
+
if (resolvedSearchEnvVars.length > 0) {
|
|
4950
|
+
const written = await writeSearchEnvValues(
|
|
4951
|
+
repoRoot,
|
|
4952
|
+
resolvedSearchEnvVars
|
|
5030
4953
|
);
|
|
5031
|
-
if (
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
4954
|
+
if (written.length > 0) {
|
|
4955
|
+
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4956
|
+
}
|
|
4957
|
+
const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
|
|
4958
|
+
if (ignored === "added") {
|
|
4959
|
+
summaries.push("Added .env to .gitignore.");
|
|
4960
|
+
} else if (ignored === "tracked") {
|
|
4961
|
+
summaries.push(
|
|
4962
|
+
'\u26A0\uFE0F .env is tracked by git, so a .gitignore rule cannot un-stage it. Run "git rm --cached .env" before committing, or the credentials go into history.'
|
|
5035
4963
|
);
|
|
5036
|
-
if (written.length > 0) {
|
|
5037
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
5038
|
-
}
|
|
5039
|
-
const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
|
|
5040
|
-
if (ignored === "added") {
|
|
5041
|
-
summaries.push("Added .env to .gitignore.");
|
|
5042
|
-
} else if (ignored === "tracked") {
|
|
5043
|
-
summaries.push(
|
|
5044
|
-
'\u26A0\uFE0F .env is tracked by git, so a .gitignore rule cannot un-stage it. Run "git rm --cached .env" before committing, or the credentials go into history.'
|
|
5045
|
-
);
|
|
5046
|
-
}
|
|
5047
|
-
const stale = [];
|
|
5048
|
-
for (const v of resolvedSearchEnvVars) {
|
|
5049
|
-
if (written.includes(v.name)) continue;
|
|
5050
|
-
const current = await readEnvVar(worktree, v.name);
|
|
5051
|
-
if (current && current !== v.value) stale.push(v);
|
|
5052
|
-
}
|
|
5053
|
-
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5054
|
-
summaries.push(
|
|
5055
|
-
`\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.`
|
|
5056
|
-
);
|
|
5057
|
-
logger.warn(
|
|
5058
|
-
{ vars: stale.map((v) => v.name) },
|
|
5059
|
-
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5060
|
-
);
|
|
5061
|
-
}
|
|
5062
4964
|
}
|
|
5063
|
-
const
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
4965
|
+
const stale = [];
|
|
4966
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4967
|
+
if (written.includes(v.name)) continue;
|
|
4968
|
+
const current = await readEnvVar(repoRoot, v.name);
|
|
4969
|
+
if (current && current !== v.value) stale.push(v);
|
|
4970
|
+
}
|
|
4971
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5067
4972
|
summaries.push(
|
|
5068
|
-
|
|
5069
|
-
|
|
4973
|
+
`\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.`
|
|
4974
|
+
);
|
|
4975
|
+
logger.warn(
|
|
4976
|
+
{ vars: stale.map((v) => v.name) },
|
|
4977
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5070
4978
|
);
|
|
5071
4979
|
}
|
|
5072
|
-
} else {
|
|
5073
|
-
ctx.setUserInput("implementation", "success");
|
|
5074
4980
|
}
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
4981
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4982
|
+
(v) => v.value.startsWith("<")
|
|
4983
|
+
);
|
|
4984
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
4985
|
+
summaries.push(
|
|
4986
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5079
4987
|
);
|
|
5080
4988
|
}
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
filesChanged,
|
|
5084
|
-
summary: summaries.join("\n\n"),
|
|
5085
|
-
worktreePath: worktree,
|
|
5086
|
-
...useCases.includes("ingestion") && ingestCommand ? {
|
|
5087
|
-
ingestCommand,
|
|
5088
|
-
ingestScriptRan,
|
|
5089
|
-
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
5090
|
-
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5091
|
-
} : {},
|
|
5092
|
-
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
5093
|
-
};
|
|
5094
|
-
} finally {
|
|
5095
|
-
process.chdir(repoRoot);
|
|
4989
|
+
} else {
|
|
4990
|
+
ctx.setUserInput("implementation", "success");
|
|
5096
4991
|
}
|
|
4992
|
+
return {
|
|
4993
|
+
ingestionSource,
|
|
4994
|
+
summary: summaries.join("\n\n"),
|
|
4995
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4996
|
+
ingestCommand,
|
|
4997
|
+
ingestScriptRan,
|
|
4998
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4999
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5000
|
+
} : {},
|
|
5001
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
5002
|
+
};
|
|
5097
5003
|
}
|
|
5098
5004
|
|
|
5099
5005
|
// src/workflows/default.ts
|
|
@@ -5165,10 +5071,7 @@ var defaultWorkflow = {
|
|
|
5165
5071
|
ctx.notify({
|
|
5166
5072
|
messages: ["Building your Algolia search experience\u2026"]
|
|
5167
5073
|
});
|
|
5168
|
-
|
|
5169
|
-
"ingestion"
|
|
5170
|
-
);
|
|
5171
|
-
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
5074
|
+
return implement(ctx, ["search"]);
|
|
5172
5075
|
}
|
|
5173
5076
|
}),
|
|
5174
5077
|
defineStep({
|
|
@@ -5183,9 +5086,8 @@ var defaultWorkflow = {
|
|
|
5183
5086
|
"ingestion"
|
|
5184
5087
|
);
|
|
5185
5088
|
return reviewStep(ctx, {
|
|
5186
|
-
//
|
|
5187
|
-
//
|
|
5188
|
-
// an LLM-paraphrased command risks being wrong.
|
|
5089
|
+
// ingestCommand was already shown verbatim as a notice; an
|
|
5090
|
+
// LLM-paraphrased restatement in nextSteps risks being wrong.
|
|
5189
5091
|
nextStepsGuidance: ingestion2?.ingestScriptRan ? "The wizard already ran the ingestion script and records are in the index. Do NOT tell the user to run it again; instead point them at the target index to confirm the records. Do not restate the ingestion command \u2014 the wizard already showed it to them." : "Tell the user to run the ingestion script; do not restate the exact command \u2014 the wizard already showed it to them above."
|
|
5190
5092
|
});
|
|
5191
5093
|
}
|
|
@@ -5232,7 +5134,6 @@ var selectIndex = {
|
|
|
5232
5134
|
selection: "wizard_seed_products"
|
|
5233
5135
|
};
|
|
5234
5136
|
var ingestion = {
|
|
5235
|
-
filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
|
|
5236
5137
|
summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
|
|
5237
5138
|
ingestCommand: "node algolia/ingest.mjs",
|
|
5238
5139
|
ingestScriptRan: true,
|
|
@@ -5244,7 +5145,6 @@ var confirmFramework2 = {
|
|
|
5244
5145
|
frameworks: projectScan2.frameworks
|
|
5245
5146
|
};
|
|
5246
5147
|
var search = {
|
|
5247
|
-
filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
|
|
5248
5148
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
5249
5149
|
ingestionSource: "generated",
|
|
5250
5150
|
searchEnvVars: [
|
|
@@ -5257,7 +5157,7 @@ var review = {
|
|
|
5257
5157
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5258
5158
|
"Added an InstantSearch search experience to the shared header."
|
|
5259
5159
|
],
|
|
5260
|
-
reviewPrompt: "Review the Algolia ingestion and search changes
|
|
5160
|
+
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5261
5161
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5262
5162
|
};
|
|
5263
5163
|
var SEEDS = {
|
|
@@ -5374,9 +5274,9 @@ Options:
|
|
|
5374
5274
|
steps pre-filled with test data. Pass with no value to print
|
|
5375
5275
|
the step ids. See CONTRIBUTING.md.
|
|
5376
5276
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5377
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5378
|
-
|
|
5379
|
-
|
|
5277
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent)
|
|
5278
|
+
before starting, so the run behaves like a first-ever
|
|
5279
|
+
run. Also drops every API key the wizard has
|
|
5380
5280
|
stored in your keychain (or, where the platform has none,
|
|
5381
5281
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5382
5282
|
for this project and any other, so later runs create new
|
|
@@ -5416,7 +5316,7 @@ function parseCliArgs(argv) {
|
|
|
5416
5316
|
}
|
|
5417
5317
|
|
|
5418
5318
|
// src/lib/resetState.ts
|
|
5419
|
-
import { readdir as
|
|
5319
|
+
import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
5420
5320
|
import { join as join13 } from "node:path";
|
|
5421
5321
|
var KEEP = ["wizard.log"];
|
|
5422
5322
|
async function resetProjectState() {
|
|
@@ -5424,7 +5324,7 @@ async function resetProjectState() {
|
|
|
5424
5324
|
await forgetResolvedKeys();
|
|
5425
5325
|
let entries;
|
|
5426
5326
|
try {
|
|
5427
|
-
entries = await
|
|
5327
|
+
entries = await readdir3(dir);
|
|
5428
5328
|
} catch {
|
|
5429
5329
|
return { dir, removed: [] };
|
|
5430
5330
|
}
|
|
@@ -5529,6 +5429,15 @@ async function run(workflow) {
|
|
|
5529
5429
|
const store = useWizard.getState();
|
|
5530
5430
|
const instance = render(/* @__PURE__ */ jsx16(App, {}), { incrementalRendering: true });
|
|
5531
5431
|
await store.waitForStart();
|
|
5432
|
+
store.syncSteps(
|
|
5433
|
+
workflow.steps.map((s) => ({
|
|
5434
|
+
id: s.id,
|
|
5435
|
+
title: s.title,
|
|
5436
|
+
visible: s.visible,
|
|
5437
|
+
status: "pending"
|
|
5438
|
+
})),
|
|
5439
|
+
0
|
|
5440
|
+
);
|
|
5532
5441
|
let user = await getUser();
|
|
5533
5442
|
if (!user) {
|
|
5534
5443
|
store.beginAuth();
|