@algolia/wizard 0.27.0 → 0.29.0-rc.118.223
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 +332 -452
- 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
|
@@ -252,6 +252,7 @@ var useWizard = create((set, get) => ({
|
|
|
252
252
|
_noticeTimer: null,
|
|
253
253
|
cliOutput: [],
|
|
254
254
|
targetIndex: null,
|
|
255
|
+
writtenFiles: [],
|
|
255
256
|
logs: [],
|
|
256
257
|
error: null,
|
|
257
258
|
inputReq: null,
|
|
@@ -345,6 +346,8 @@ var useWizard = create((set, get) => ({
|
|
|
345
346
|
})),
|
|
346
347
|
clearCliOutput: () => set({ cliOutput: [] }),
|
|
347
348
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
349
|
+
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
350
|
+
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
348
351
|
logStart: (kind, name, input) => {
|
|
349
352
|
const id = nanoid();
|
|
350
353
|
set((s) => ({
|
|
@@ -392,6 +395,7 @@ var useWizard = create((set, get) => ({
|
|
|
392
395
|
notices: [],
|
|
393
396
|
cliOutput: [],
|
|
394
397
|
targetIndex: null,
|
|
398
|
+
writtenFiles: [],
|
|
395
399
|
logs: [],
|
|
396
400
|
error: null,
|
|
397
401
|
inputReq: null,
|
|
@@ -1260,7 +1264,7 @@ var accessItems = [
|
|
|
1260
1264
|
{
|
|
1261
1265
|
tag: "WRITE",
|
|
1262
1266
|
title: "Code changes",
|
|
1263
|
-
description: "creates & edits files (search UI, config) in
|
|
1267
|
+
description: "creates & edits files (search UI, config) directly in your branch."
|
|
1264
1268
|
},
|
|
1265
1269
|
{
|
|
1266
1270
|
tag: "EXEC",
|
|
@@ -1275,7 +1279,7 @@ var accessItems = [
|
|
|
1275
1279
|
{
|
|
1276
1280
|
tag: "KEY",
|
|
1277
1281
|
title: "Credentials",
|
|
1278
|
-
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."
|
|
1279
1283
|
}
|
|
1280
1284
|
];
|
|
1281
1285
|
var neverItems = [
|
|
@@ -1934,8 +1938,20 @@ function reconcileWorkflowState(state, workflow) {
|
|
|
1934
1938
|
const definedIds = workflow.steps.map((s) => s.id).join("\n");
|
|
1935
1939
|
if (persistedIds !== definedIds) return null;
|
|
1936
1940
|
for (let i = 0; i < state.steps.length; i++) {
|
|
1937
|
-
|
|
1938
|
-
|
|
1941
|
+
const record = state.steps[i];
|
|
1942
|
+
const step = workflow.steps[i];
|
|
1943
|
+
if (record.status === "done") {
|
|
1944
|
+
const parsed = step.outputSchema.safeParse(record.output);
|
|
1945
|
+
if (!parsed.success) {
|
|
1946
|
+
logger.warn(
|
|
1947
|
+
{ stepId: step.id, issues: parsed.error.issues },
|
|
1948
|
+
"reconcileWorkflowState: persisted output for a completed step no longer matches its schema; restarting the run from scratch"
|
|
1949
|
+
);
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
record.title = step.title;
|
|
1954
|
+
record.visible = step.visible;
|
|
1939
1955
|
}
|
|
1940
1956
|
return state;
|
|
1941
1957
|
}
|
|
@@ -2566,6 +2582,7 @@ function writeFileTool(ctx) {
|
|
|
2566
2582
|
}
|
|
2567
2583
|
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2568
2584
|
await writeFile3(resolved2.target, content, "utf8");
|
|
2585
|
+
useWizard.getState().recordWrittenFile(resolved2.target);
|
|
2569
2586
|
return `Wrote to ${filePath}`;
|
|
2570
2587
|
} catch (err) {
|
|
2571
2588
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -3272,28 +3289,9 @@ import { tool as tool9 } from "ai";
|
|
|
3272
3289
|
import z16 from "zod";
|
|
3273
3290
|
import { stat as stat2 } from "node:fs/promises";
|
|
3274
3291
|
import { relative as relative5 } from "node:path";
|
|
3275
|
-
|
|
3276
|
-
// src/lib/editor.ts
|
|
3277
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
3278
|
-
function openInEditor(filePath) {
|
|
3279
|
-
const { command, args } = process.platform === "darwin" ? (
|
|
3280
|
-
// -t forces the default *text* editor; plain `open` on an extension
|
|
3281
|
-
// with no handler (.mjs, .py) pops a "choose an application" dialog.
|
|
3282
|
-
{ command: "open", args: ["-t", filePath] }
|
|
3283
|
-
) : process.platform === "win32" ? { command: "cmd", args: ["/c", "start", "", filePath] } : { command: "xdg-open", args: [filePath] };
|
|
3284
|
-
try {
|
|
3285
|
-
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
3286
|
-
child.on("error", () => {
|
|
3287
|
-
});
|
|
3288
|
-
child.unref();
|
|
3289
|
-
} catch {
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
// src/lib/tools/reviewScript.ts
|
|
3294
3292
|
function reviewScriptTool(ctx) {
|
|
3295
3293
|
return tool9({
|
|
3296
|
-
description: "
|
|
3294
|
+
description: "Call this as soon as the ingestion script is finished and before you run it, so the user can review it.It shows the file path and returns once they have confirmed reviewing it.",
|
|
3297
3295
|
inputSchema: z16.object({
|
|
3298
3296
|
filePath: z16.string().describe(
|
|
3299
3297
|
"Path to the script to review, relative to the project root."
|
|
@@ -3311,7 +3309,6 @@ function reviewScriptTool(ctx) {
|
|
|
3311
3309
|
return `Refused: ${filePath} is not a file. Write the script first, then review the path you wrote.`;
|
|
3312
3310
|
}
|
|
3313
3311
|
return serializePrompt(async () => {
|
|
3314
|
-
openInEditor(resolved2.target);
|
|
3315
3312
|
await useWizard.getState().requestUserInput({
|
|
3316
3313
|
prompt: "",
|
|
3317
3314
|
promptType: "enterToContinue",
|
|
@@ -3353,7 +3350,7 @@ function defaultCreateModel() {
|
|
|
3353
3350
|
}
|
|
3354
3351
|
function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
3355
3352
|
return tool10({
|
|
3356
|
-
description: "Generate realistic sample records for an entity and write them to a JSON file
|
|
3353
|
+
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.",
|
|
3357
3354
|
inputSchema: z17.object({
|
|
3358
3355
|
entityName: z17.string().describe("Name of the entity to generate records for."),
|
|
3359
3356
|
attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
|
|
@@ -3759,7 +3756,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3759
3756
|
// package.json
|
|
3760
3757
|
var package_default = {
|
|
3761
3758
|
name: "@algolia/wizard",
|
|
3762
|
-
version: "0.
|
|
3759
|
+
version: "0.29.0-rc.118.223",
|
|
3763
3760
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3764
3761
|
type: "module",
|
|
3765
3762
|
engines: {
|
|
@@ -4126,11 +4123,10 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
4126
4123
|
function formatReviewSummary(result) {
|
|
4127
4124
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
4128
4125
|
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
4129
|
-
const isWorktreeCommand = step.includes("/worktrees/");
|
|
4130
4126
|
return {
|
|
4131
4127
|
text: `\u2192 ${step}`,
|
|
4132
|
-
color: isIngestCommand ? COLORS.brand :
|
|
4133
|
-
bold: isIngestCommand
|
|
4128
|
+
color: isIngestCommand ? COLORS.brand : void 0,
|
|
4129
|
+
bold: isIngestCommand
|
|
4134
4130
|
};
|
|
4135
4131
|
});
|
|
4136
4132
|
return [
|
|
@@ -4165,15 +4161,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4165
4161
|
|
|
4166
4162
|
// src/actions/implement.ts
|
|
4167
4163
|
import z29 from "zod";
|
|
4168
|
-
import { join as join12 } from "node:path";
|
|
4164
|
+
import { join as join12, relative as relative6 } from "node:path";
|
|
4169
4165
|
|
|
4170
|
-
// src/lib/
|
|
4166
|
+
// src/lib/git.ts
|
|
4171
4167
|
import { execFile as execFile2 } from "node:child_process";
|
|
4172
|
-
import { copyFile, mkdir as mkdir6,
|
|
4168
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4173
4169
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4174
4170
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4175
|
-
var MAX_WIZARD_WORKTREES = 3;
|
|
4176
|
-
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4177
4171
|
function git(args) {
|
|
4178
4172
|
return new Promise((resolve4, reject) => {
|
|
4179
4173
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4196,44 +4190,7 @@ async function assertGitRepoWithHead(repoRoot) {
|
|
|
4196
4190
|
);
|
|
4197
4191
|
}
|
|
4198
4192
|
}
|
|
4199
|
-
async function
|
|
4200
|
-
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4201
|
-
return out.trim().length > 0;
|
|
4202
|
-
}
|
|
4203
|
-
async function pruneOldWorktrees(repoRoot) {
|
|
4204
|
-
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
4205
|
-
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4206
|
-
for (const slug of stale) {
|
|
4207
|
-
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4208
|
-
try {
|
|
4209
|
-
await git([
|
|
4210
|
-
"-C",
|
|
4211
|
-
repoRoot,
|
|
4212
|
-
"worktree",
|
|
4213
|
-
"remove",
|
|
4214
|
-
"--force",
|
|
4215
|
-
join10(dir, slug)
|
|
4216
|
-
]);
|
|
4217
|
-
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4218
|
-
} catch (err) {
|
|
4219
|
-
logger.warn(
|
|
4220
|
-
{ branch, err: err.message },
|
|
4221
|
-
"createWorktree: failed to prune a stale wizard worktree; continuing"
|
|
4222
|
-
);
|
|
4223
|
-
}
|
|
4224
|
-
}
|
|
4225
|
-
}
|
|
4226
|
-
async function createWorktree(repoRoot) {
|
|
4227
|
-
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4228
|
-
const dirSlug = branch.replace(/\//g, "-");
|
|
4229
|
-
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4230
|
-
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4231
|
-
await pruneOldWorktrees(repoRoot);
|
|
4232
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
4233
|
-
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4234
|
-
return { path, branch };
|
|
4235
|
-
}
|
|
4236
|
-
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4193
|
+
async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
4237
4194
|
const trimmed = sourcePath.trim();
|
|
4238
4195
|
if (!trimmed) {
|
|
4239
4196
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4247,7 +4204,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4247
4204
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4248
4205
|
}
|
|
4249
4206
|
const relPath = join10(ingestDir, basename2(source));
|
|
4250
|
-
const dest = join10(
|
|
4207
|
+
const dest = join10(repoRoot, relPath);
|
|
4208
|
+
if (resolve3(source) === resolve3(dest)) {
|
|
4209
|
+
return { ok: true, relPath };
|
|
4210
|
+
}
|
|
4251
4211
|
try {
|
|
4252
4212
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4253
4213
|
await copyFile(source, dest);
|
|
@@ -4262,10 +4222,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4262
4222
|
function hasEnvVar(content, name) {
|
|
4263
4223
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4264
4224
|
}
|
|
4265
|
-
async function readEnvVar(
|
|
4225
|
+
async function readEnvVar(repoRoot, name) {
|
|
4266
4226
|
let content;
|
|
4267
4227
|
try {
|
|
4268
|
-
content = await readFile8(join10(
|
|
4228
|
+
content = await readFile8(join10(repoRoot, ".env"), "utf8");
|
|
4269
4229
|
} catch (err) {
|
|
4270
4230
|
if (err.code !== "ENOENT") throw err;
|
|
4271
4231
|
return void 0;
|
|
@@ -4279,8 +4239,8 @@ async function readEnvVar(worktreePath, name) {
|
|
|
4279
4239
|
if (!value || value.startsWith("<")) return void 0;
|
|
4280
4240
|
return value;
|
|
4281
4241
|
}
|
|
4282
|
-
async function writeSearchEnvValues(
|
|
4283
|
-
const target = join10(
|
|
4242
|
+
async function writeSearchEnvValues(repoRoot, vars) {
|
|
4243
|
+
const target = join10(repoRoot, ".env");
|
|
4284
4244
|
let existing = "";
|
|
4285
4245
|
try {
|
|
4286
4246
|
existing = await readFile8(target, "utf8");
|
|
@@ -4295,61 +4255,27 @@ async function writeSearchEnvValues(worktreePath, vars) {
|
|
|
4295
4255
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4296
4256
|
return missing.map((v) => v.name);
|
|
4297
4257
|
}
|
|
4298
|
-
async function listChangedFiles(worktreePath) {
|
|
4299
|
-
const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
|
|
4300
|
-
const entries = raw.split("\0");
|
|
4301
|
-
const files = [];
|
|
4302
|
-
for (let i = 0; i < entries.length; i += 1) {
|
|
4303
|
-
const entry = entries[i];
|
|
4304
|
-
if (!entry) continue;
|
|
4305
|
-
files.push(entry.slice(3));
|
|
4306
|
-
if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
|
|
4307
|
-
}
|
|
4308
|
-
return files;
|
|
4309
|
-
}
|
|
4310
4258
|
function normalizeFindingPaths(findings) {
|
|
4311
4259
|
return {
|
|
4312
4260
|
...findings,
|
|
4313
4261
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4314
4262
|
...e,
|
|
4315
|
-
paths: e.paths.map(
|
|
4263
|
+
paths: e.paths.map(toRootRelative)
|
|
4316
4264
|
})),
|
|
4317
4265
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4318
4266
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4319
4267
|
...e,
|
|
4320
|
-
paths: e.paths.map(
|
|
4268
|
+
paths: e.paths.map(toRootRelative)
|
|
4321
4269
|
}))
|
|
4322
4270
|
};
|
|
4323
4271
|
}
|
|
4324
4272
|
function normalizeSearchLocation(path) {
|
|
4325
|
-
const normalized = path ?
|
|
4273
|
+
const normalized = path ? toRootRelative(path).trim() : "";
|
|
4326
4274
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4327
4275
|
}
|
|
4328
|
-
function
|
|
4276
|
+
function toRootRelative(p) {
|
|
4329
4277
|
return p.replace(/^\/+/, "");
|
|
4330
4278
|
}
|
|
4331
|
-
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
4332
|
-
const MAX_LISTED_DIRTY_FILES = 10;
|
|
4333
|
-
const dirty = await listChangedFiles(repoRoot);
|
|
4334
|
-
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4335
|
-
const overflow = dirty.length - shown.length;
|
|
4336
|
-
const answer = await ctx.requestUserInput({
|
|
4337
|
-
prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
|
|
4338
|
-
promptType: "acceptReject",
|
|
4339
|
-
options: [],
|
|
4340
|
-
messages: [
|
|
4341
|
-
`${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
|
|
4342
|
-
...shown.map((file) => ` \u2022 ${file}`),
|
|
4343
|
-
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4344
|
-
"Commit or stash them first to include them in the implementation."
|
|
4345
|
-
]
|
|
4346
|
-
});
|
|
4347
|
-
if (answer !== true) {
|
|
4348
|
-
throw new Error(
|
|
4349
|
-
"implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
|
|
4350
|
-
);
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
4279
|
|
|
4354
4280
|
// src/lib/algoliaDocs.ts
|
|
4355
4281
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
@@ -4409,11 +4335,6 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4409
4335
|
return loadAlgoliaDoc("js");
|
|
4410
4336
|
}
|
|
4411
4337
|
|
|
4412
|
-
// src/lib/shell.ts
|
|
4413
|
-
function shellQuote(value) {
|
|
4414
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4415
|
-
}
|
|
4416
|
-
|
|
4417
4338
|
// src/actions/resolveEnvVarPrefix.ts
|
|
4418
4339
|
import z28 from "zod";
|
|
4419
4340
|
var resolveEnvVarPrefixSchema = z28.object({
|
|
@@ -4433,9 +4354,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
|
4433
4354
|
|
|
4434
4355
|
// src/actions/implement.ts
|
|
4435
4356
|
var implementSchema = z29.object({
|
|
4436
|
-
filesChanged: z29.array(z29.string()),
|
|
4437
4357
|
summary: z29.string(),
|
|
4438
|
-
worktreePath: z29.string().optional(),
|
|
4439
4358
|
ingestCommand: z29.string().optional(),
|
|
4440
4359
|
ingestScriptRan: z29.boolean().optional(),
|
|
4441
4360
|
ingestRecordCount: z29.number().optional(),
|
|
@@ -4488,8 +4407,6 @@ function frameworksForDoc(language) {
|
|
|
4488
4407
|
}
|
|
4489
4408
|
function baseInstructions(input) {
|
|
4490
4409
|
return [
|
|
4491
|
-
// Agents have renamed this (e.g. appending the project name), which the
|
|
4492
|
-
// index-scoped keys then reject with a 403.
|
|
4493
4410
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4494
4411
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4495
4412
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4507,7 +4424,7 @@ function sourceSpecificInstructions(input) {
|
|
|
4507
4424
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4508
4425
|
],
|
|
4509
4426
|
fileUpload: [
|
|
4510
|
-
`Records come from the developer's file, already copied into the
|
|
4427
|
+
`Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4511
4428
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4512
4429
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4513
4430
|
"Never fabricate, hardcode, or substitute a different file."
|
|
@@ -4563,14 +4480,11 @@ function searchInstructions(input) {
|
|
|
4563
4480
|
"If a search box already exists, replace it with yours.",
|
|
4564
4481
|
`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.`,
|
|
4565
4482
|
"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.",
|
|
4566
|
-
// The key is provisioned only after verification passes,
|
|
4567
|
-
//
|
|
4568
|
-
//
|
|
4569
|
-
// would be reused as if it were real.
|
|
4483
|
+
// The key is provisioned only after verification passes, and the wizard
|
|
4484
|
+
// reads .env to decide whether a key already exists — an agent-invented
|
|
4485
|
+
// value there would be reused as if it were real.
|
|
4570
4486
|
`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.`,
|
|
4571
|
-
//
|
|
4572
|
-
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4573
|
-
// reading a var the wizard never wrote.
|
|
4487
|
+
// The wizard writes these exact names into .env right after this step.
|
|
4574
4488
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4575
4489
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4576
4490
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4579,10 +4493,10 @@ function searchInstructions(input) {
|
|
|
4579
4493
|
}
|
|
4580
4494
|
function verificationInstructions(input) {
|
|
4581
4495
|
return [
|
|
4582
|
-
"Verify the Algolia implementation changes
|
|
4496
|
+
"Verify the Algolia implementation changes.",
|
|
4583
4497
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4584
4498
|
"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.",
|
|
4585
|
-
"
|
|
4499
|
+
"If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4586
4500
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4587
4501
|
"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.",
|
|
4588
4502
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4677,11 +4591,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4677
4591
|
}
|
|
4678
4592
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4679
4593
|
}
|
|
4680
|
-
function makeToolContext(
|
|
4594
|
+
function makeToolContext(root, env = async () => ({})) {
|
|
4681
4595
|
return createToolContext(
|
|
4682
4596
|
DEFAULT_TOOL_LIMITS,
|
|
4683
|
-
|
|
4684
|
-
createShellContext({ env, approve: storeApproval(
|
|
4597
|
+
root,
|
|
4598
|
+
createShellContext({ env, approve: storeApproval(root) })
|
|
4685
4599
|
);
|
|
4686
4600
|
}
|
|
4687
4601
|
function verificationRetryInstructions(verification) {
|
|
@@ -4689,7 +4603,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4689
4603
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4690
4604
|
];
|
|
4691
4605
|
}
|
|
4692
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES
|
|
4606
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4693
4607
|
const repoRoot = process.cwd();
|
|
4694
4608
|
const scan = ctx.getStepOutput("project-scan");
|
|
4695
4609
|
const entities = ctx.getStepOutput(
|
|
@@ -4770,9 +4684,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4770
4684
|
const targetIndex = selected?.selection;
|
|
4771
4685
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4772
4686
|
await assertGitRepoWithHead(repoRoot);
|
|
4773
|
-
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4774
|
-
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4775
|
-
}
|
|
4776
4687
|
const normalized = normalizeFindingPaths(findings);
|
|
4777
4688
|
const confirmed2 = normalized.confirmedEntities;
|
|
4778
4689
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4784,328 +4695,303 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4784
4695
|
if (useCases.includes("ingestion")) {
|
|
4785
4696
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4786
4697
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4698
|
+
let uploadFilePath;
|
|
4699
|
+
let uploadWarning;
|
|
4700
|
+
if (ingestionSource === "fileUpload") {
|
|
4701
|
+
const copied = await copyUploadIntoProject(
|
|
4702
|
+
repoRoot,
|
|
4703
|
+
INGEST_DIR,
|
|
4704
|
+
uploadSourcePath ?? ""
|
|
4705
|
+
);
|
|
4706
|
+
if (copied.ok) {
|
|
4707
|
+
uploadFilePath = copied.relPath;
|
|
4708
|
+
} else {
|
|
4709
|
+
ingestionSource = "generated";
|
|
4710
|
+
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4711
|
+
logger.warn(
|
|
4712
|
+
{ reason: copied.reason },
|
|
4713
|
+
"implement: file upload unavailable; falling back to generated sample records"
|
|
4714
|
+
);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
|
|
4718
|
+
const input = {
|
|
4719
|
+
findings: normalized,
|
|
4720
|
+
confirmed: confirmed2,
|
|
4721
|
+
searchLocation,
|
|
4722
|
+
targetIndex,
|
|
4723
|
+
language,
|
|
4724
|
+
publicEnvVarPrefix,
|
|
4725
|
+
appId,
|
|
4726
|
+
searchEnvVars: publicSearchEnvVars(publicEnvVarPrefix, targetIndex, appId),
|
|
4727
|
+
ingestDir: INGEST_DIR,
|
|
4728
|
+
ingestionSource,
|
|
4729
|
+
uploadFilePath,
|
|
4730
|
+
searchUiTarget: searchUiTarget(language)
|
|
4731
|
+
};
|
|
4732
|
+
const summaries = [];
|
|
4733
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4734
|
+
let envSearchKey;
|
|
4735
|
+
let envAppIdMismatch = false;
|
|
4736
|
+
if (useCases.includes("search") && appId) {
|
|
4737
|
+
const envAppId = await readEnvVar(
|
|
4738
|
+
repoRoot,
|
|
4739
|
+
publicAppIdVar(publicEnvVarPrefix)
|
|
4740
|
+
);
|
|
4741
|
+
if (envAppId === appId) {
|
|
4742
|
+
envSearchKey = await readEnvVar(
|
|
4794
4743
|
repoRoot,
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4744
|
+
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4745
|
+
);
|
|
4746
|
+
} else if (envAppId) {
|
|
4747
|
+
envAppIdMismatch = true;
|
|
4748
|
+
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4749
|
+
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4750
|
+
summaries.push(
|
|
4751
|
+
`\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.`
|
|
4752
|
+
);
|
|
4753
|
+
logger.warn(
|
|
4754
|
+
{ envAppId, appId },
|
|
4755
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4798
4756
|
);
|
|
4799
|
-
if (copied.ok) {
|
|
4800
|
-
uploadFilePath = copied.relPath;
|
|
4801
|
-
} else {
|
|
4802
|
-
ingestionSource = "generated";
|
|
4803
|
-
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4804
|
-
logger.warn(
|
|
4805
|
-
{ reason: copied.reason },
|
|
4806
|
-
"implement: file upload unavailable; falling back to generated sample records"
|
|
4807
|
-
);
|
|
4808
|
-
}
|
|
4809
4757
|
}
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4758
|
+
}
|
|
4759
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4760
|
+
let agentRuns = 0;
|
|
4761
|
+
let ingestCommand;
|
|
4762
|
+
let ingestScriptRan = false;
|
|
4763
|
+
let ingestRecordCount;
|
|
4764
|
+
let ingestDurationMs;
|
|
4765
|
+
let ingestOutcomeMessage;
|
|
4766
|
+
const ingestKeyAppId = ingestAppId;
|
|
4767
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
|
|
4768
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4769
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4770
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4771
|
+
})) : void 0;
|
|
4772
|
+
const searchTools = makeToolContext(repoRoot);
|
|
4773
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4774
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4775
|
+
agentRuns += 1;
|
|
4776
|
+
return runAgent({
|
|
4777
|
+
instructions: buildAgentInstructions(
|
|
4778
|
+
currentUseCase,
|
|
4779
|
+
input,
|
|
4780
|
+
extraInstructions
|
|
4823
4781
|
),
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
if (
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4782
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4783
|
+
outputSchema: implementationOutputSchema,
|
|
4784
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4785
|
+
});
|
|
4786
|
+
}
|
|
4787
|
+
async function runVerificationUseCase() {
|
|
4788
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4789
|
+
agentRuns += 1;
|
|
4790
|
+
return runAgent({
|
|
4791
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4792
|
+
tools: toolsForUseCase("verification"),
|
|
4793
|
+
outputSchema: verificationOutputSchema,
|
|
4794
|
+
toolContext: searchTools
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
if (useCases.includes("ingestion")) {
|
|
4798
|
+
let ingestFailureDetail;
|
|
4799
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4800
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4801
|
+
ingestCommand = result.ingestCommand;
|
|
4802
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4803
|
+
const executions = ingestionContext.shell.executions;
|
|
4804
|
+
const {
|
|
4805
|
+
run: ingestRun,
|
|
4806
|
+
attempt: ingestAttempt,
|
|
4807
|
+
recordCount
|
|
4808
|
+
} = ingestOutcome(executions, ingestCommand);
|
|
4809
|
+
ingestScriptRan = ingestRun != null;
|
|
4810
|
+
ingestRecordCount = recordCount;
|
|
4811
|
+
ingestDurationMs = ingestRun?.durationMs;
|
|
4812
|
+
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4813
|
+
summaries.push(
|
|
4814
|
+
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
|
|
4815
|
+
);
|
|
4816
|
+
logger.warn(
|
|
4817
|
+
{ ingestCommand },
|
|
4818
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4837
4819
|
);
|
|
4838
|
-
if (envAppId === appId) {
|
|
4839
|
-
envSearchKey = await readEnvVar(
|
|
4840
|
-
worktree,
|
|
4841
|
-
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4842
|
-
);
|
|
4843
|
-
} else if (envAppId) {
|
|
4844
|
-
envAppIdMismatch = true;
|
|
4845
|
-
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4846
|
-
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4847
|
-
summaries.push(
|
|
4848
|
-
`\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.`
|
|
4849
|
-
);
|
|
4850
|
-
logger.warn(
|
|
4851
|
-
{ envAppId, appId },
|
|
4852
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4853
|
-
);
|
|
4854
|
-
}
|
|
4855
|
-
}
|
|
4856
|
-
let finalSearchEnvVars = input.searchEnvVars;
|
|
4857
|
-
let agentRuns = 0;
|
|
4858
|
-
let ingestCommand;
|
|
4859
|
-
let ingestScriptRan = false;
|
|
4860
|
-
let ingestRecordCount;
|
|
4861
|
-
let ingestDurationMs;
|
|
4862
|
-
let ingestOutcomeMessage;
|
|
4863
|
-
const ingestKeyAppId = ingestAppId;
|
|
4864
|
-
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4865
|
-
[APP_ID_VAR]: ingestKeyAppId,
|
|
4866
|
-
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4867
|
-
[INDEX_NAME_VAR]: targetIndex
|
|
4868
|
-
})) : void 0;
|
|
4869
|
-
const searchTools = makeToolContext(worktree);
|
|
4870
|
-
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4871
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4872
|
-
agentRuns += 1;
|
|
4873
|
-
return runAgent({
|
|
4874
|
-
instructions: buildAgentInstructions(
|
|
4875
|
-
currentUseCase,
|
|
4876
|
-
input,
|
|
4877
|
-
extraInstructions
|
|
4878
|
-
),
|
|
4879
|
-
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4880
|
-
outputSchema: implementationOutputSchema,
|
|
4881
|
-
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4882
|
-
});
|
|
4883
|
-
}
|
|
4884
|
-
async function runVerificationUseCase() {
|
|
4885
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4886
|
-
agentRuns += 1;
|
|
4887
|
-
return runAgent({
|
|
4888
|
-
instructions: buildAgentInstructions("verification", input),
|
|
4889
|
-
tools: toolsForUseCase("verification"),
|
|
4890
|
-
outputSchema: verificationOutputSchema,
|
|
4891
|
-
toolContext: searchTools
|
|
4892
|
-
});
|
|
4893
4820
|
}
|
|
4894
|
-
if (
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
run: ingestRun,
|
|
4903
|
-
attempt: ingestAttempt,
|
|
4904
|
-
recordCount
|
|
4905
|
-
} = ingestOutcome(executions, ingestCommand);
|
|
4906
|
-
ingestScriptRan = ingestRun != null;
|
|
4907
|
-
ingestRecordCount = recordCount;
|
|
4908
|
-
ingestDurationMs = ingestRun?.durationMs;
|
|
4909
|
-
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4910
|
-
summaries.push(
|
|
4911
|
-
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
|
|
4912
|
-
);
|
|
4913
|
-
logger.warn(
|
|
4914
|
-
{ ingestCommand },
|
|
4915
|
-
"implement: ingestion ran without a reviewScript call"
|
|
4916
|
-
);
|
|
4821
|
+
if (ingestScriptRan) {
|
|
4822
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4823
|
+
if (ingestRecordCount != null) {
|
|
4824
|
+
track("AI Wizard Ingest Successful", {
|
|
4825
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4826
|
+
record_count: ingestRecordCount,
|
|
4827
|
+
duration_ms: ingestDurationMs ?? 0
|
|
4828
|
+
});
|
|
4917
4829
|
}
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
duration_ms: ingestDurationMs ?? 0
|
|
4925
|
-
});
|
|
4926
|
-
}
|
|
4927
|
-
} else {
|
|
4928
|
-
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4929
|
-
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4930
|
-
ingestFailureDetail = detail;
|
|
4931
|
-
summaries.push(
|
|
4932
|
-
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4830
|
+
} else {
|
|
4831
|
+
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4832
|
+
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4833
|
+
ingestFailureDetail = detail;
|
|
4834
|
+
summaries.push(
|
|
4835
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4933
4836
|
${detail}` : ""}`
|
|
4934
|
-
|
|
4935
|
-
|
|
4837
|
+
);
|
|
4838
|
+
logger.warn(
|
|
4839
|
+
{
|
|
4840
|
+
ingestCommand,
|
|
4841
|
+
reason,
|
|
4842
|
+
approved: ingestAttempt?.approved,
|
|
4843
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4844
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4845
|
+
commandsRun: executions.length
|
|
4846
|
+
},
|
|
4847
|
+
"implement: ingestion script did not complete successfully"
|
|
4848
|
+
);
|
|
4849
|
+
track("Error", {
|
|
4850
|
+
step: "Push Data",
|
|
4851
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4852
|
+
product_area: "AI Wizard"
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
|
|
4856
|
+
await ctx.requestUserInput({
|
|
4857
|
+
prompt: "",
|
|
4858
|
+
promptType: "enterToContinue",
|
|
4859
|
+
options: [],
|
|
4860
|
+
// One message per line: Notices.tsx counts a message as one wrapped
|
|
4861
|
+
// line, so an embedded newline overflows the panel's height accounting.
|
|
4862
|
+
messages: [
|
|
4863
|
+
ingestOutcomeMessage,
|
|
4864
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4865
|
+
...commandMessages
|
|
4866
|
+
]
|
|
4867
|
+
});
|
|
4868
|
+
}
|
|
4869
|
+
if (useCases.includes("search")) {
|
|
4870
|
+
let extraInstructions = [];
|
|
4871
|
+
useWizard.getState().clearWrittenFiles();
|
|
4872
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4873
|
+
if (attempt > 1) {
|
|
4874
|
+
logger.info(
|
|
4936
4875
|
{
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
exitCode: ingestAttempt?.exitCode,
|
|
4941
|
-
timedOut: ingestAttempt?.timedOut,
|
|
4942
|
-
commandsRun: executions.length
|
|
4876
|
+
attempt,
|
|
4877
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4878
|
+
extraInstructions
|
|
4943
4879
|
},
|
|
4944
|
-
"implement:
|
|
4880
|
+
"implement: retrying search implementation after failed verification"
|
|
4945
4881
|
);
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4882
|
+
}
|
|
4883
|
+
const { summary } = await runImplementationUseCase(
|
|
4884
|
+
"search",
|
|
4885
|
+
extraInstructions
|
|
4886
|
+
);
|
|
4887
|
+
summaries.push(formatSummary("search", summary));
|
|
4888
|
+
const verification = await runVerificationUseCase();
|
|
4889
|
+
summaries.push(formatSummary("verification", verification.summary));
|
|
4890
|
+
if (verification.sufficient) {
|
|
4891
|
+
ctx.setUserInput("implementation", "success");
|
|
4892
|
+
const searchFilesChanged = [
|
|
4893
|
+
...new Set(useWizard.getState().writtenFiles)
|
|
4894
|
+
].map((file) => relative6(repoRoot, file));
|
|
4895
|
+
track("AI Wizard Frontend Component Generated", {
|
|
4896
|
+
filePaths: searchFilesChanged
|
|
4897
|
+
});
|
|
4898
|
+
track("AI Wizard Wired to UI", {
|
|
4899
|
+
location_heuristic: searchLocation ?? "unknown"
|
|
4950
4900
|
});
|
|
4901
|
+
break;
|
|
4951
4902
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4903
|
+
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4904
|
+
ctx.setUserInput("implementation", "fail");
|
|
4905
|
+
throw new Error(
|
|
4906
|
+
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4907
|
+
);
|
|
4955
4908
|
}
|
|
4956
|
-
|
|
4957
|
-
prompt: "",
|
|
4958
|
-
promptType: "enterToContinue",
|
|
4959
|
-
options: [],
|
|
4960
|
-
// The wizard never streams command output, so a failed run's tail is the
|
|
4961
|
-
// only place the developer sees why it failed. One message per line:
|
|
4962
|
-
// the panel's height accounting counts a message as one wrapped line
|
|
4963
|
-
// (see Notices.tsx), so an embedded newline overflows it.
|
|
4964
|
-
messages: [
|
|
4965
|
-
ingestOutcomeMessage,
|
|
4966
|
-
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4967
|
-
...commandMessages
|
|
4968
|
-
]
|
|
4969
|
-
});
|
|
4909
|
+
extraInstructions = verificationRetryInstructions(verification);
|
|
4970
4910
|
}
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4911
|
+
let searchKey;
|
|
4912
|
+
let searchKeyError;
|
|
4913
|
+
if (appId) {
|
|
4914
|
+
try {
|
|
4915
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4916
|
+
targetIndex,
|
|
4917
|
+
appId,
|
|
4918
|
+
envSearchKey
|
|
4919
|
+
);
|
|
4920
|
+
searchKey = resolved2.key;
|
|
4921
|
+
summaries.push(
|
|
4922
|
+
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}.`
|
|
4923
|
+
);
|
|
4924
|
+
} catch (err) {
|
|
4925
|
+
searchKeyError = err.message;
|
|
4926
|
+
logger.warn(
|
|
4927
|
+
{ err: searchKeyError },
|
|
4928
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4988
4929
|
);
|
|
4989
|
-
summaries.push(formatSummary("search", summary));
|
|
4990
|
-
const verification = await runVerificationUseCase();
|
|
4991
|
-
summaries.push(formatSummary("verification", verification.summary));
|
|
4992
|
-
if (verification.sufficient) {
|
|
4993
|
-
ctx.setUserInput("implementation", "success");
|
|
4994
|
-
const searchFilesChanged = (await listChangedFiles(worktree)).filter(
|
|
4995
|
-
(file) => !preSearchFiles.has(file)
|
|
4996
|
-
);
|
|
4997
|
-
track("AI Wizard Frontend Component Generated", {
|
|
4998
|
-
filePaths: searchFilesChanged
|
|
4999
|
-
});
|
|
5000
|
-
track("AI Wizard Wired to UI", {
|
|
5001
|
-
location_heuristic: searchLocation ?? "unknown"
|
|
5002
|
-
});
|
|
5003
|
-
break;
|
|
5004
|
-
}
|
|
5005
|
-
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
5006
|
-
ctx.setUserInput("implementation", "fail");
|
|
5007
|
-
throw new Error(
|
|
5008
|
-
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
5009
|
-
);
|
|
5010
|
-
}
|
|
5011
|
-
extraInstructions = verificationRetryInstructions(verification);
|
|
5012
|
-
}
|
|
5013
|
-
let searchKey;
|
|
5014
|
-
let searchKeyError;
|
|
5015
|
-
if (appId) {
|
|
5016
|
-
try {
|
|
5017
|
-
const resolved2 = await resolveSearchOnlyKey(
|
|
5018
|
-
targetIndex,
|
|
5019
|
-
appId,
|
|
5020
|
-
envSearchKey
|
|
5021
|
-
);
|
|
5022
|
-
searchKey = resolved2.key;
|
|
5023
|
-
summaries.push(
|
|
5024
|
-
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}.`
|
|
5025
|
-
);
|
|
5026
|
-
} catch (err) {
|
|
5027
|
-
searchKeyError = err.message;
|
|
5028
|
-
logger.warn(
|
|
5029
|
-
{ err: searchKeyError },
|
|
5030
|
-
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
5031
|
-
);
|
|
5032
|
-
}
|
|
5033
4930
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
4931
|
+
}
|
|
4932
|
+
finalSearchEnvVars = publicSearchEnvVars(
|
|
4933
|
+
publicEnvVarPrefix,
|
|
4934
|
+
targetIndex,
|
|
4935
|
+
appId,
|
|
4936
|
+
searchKey
|
|
4937
|
+
);
|
|
4938
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4939
|
+
(v) => !v.value.startsWith("<")
|
|
4940
|
+
);
|
|
4941
|
+
if (resolvedSearchEnvVars.length > 0) {
|
|
4942
|
+
const written = await writeSearchEnvValues(
|
|
4943
|
+
repoRoot,
|
|
4944
|
+
resolvedSearchEnvVars
|
|
5042
4945
|
);
|
|
5043
|
-
if (
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
4946
|
+
if (written.length > 0) {
|
|
4947
|
+
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4948
|
+
}
|
|
4949
|
+
const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
|
|
4950
|
+
if (ignored === "added") {
|
|
4951
|
+
summaries.push("Added .env to .gitignore.");
|
|
4952
|
+
} else if (ignored === "tracked") {
|
|
4953
|
+
summaries.push(
|
|
4954
|
+
'\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.'
|
|
5047
4955
|
);
|
|
5048
|
-
if (written.length > 0) {
|
|
5049
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
5050
|
-
}
|
|
5051
|
-
const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
|
|
5052
|
-
if (ignored === "added") {
|
|
5053
|
-
summaries.push("Added .env to .gitignore.");
|
|
5054
|
-
} else if (ignored === "tracked") {
|
|
5055
|
-
summaries.push(
|
|
5056
|
-
'\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.'
|
|
5057
|
-
);
|
|
5058
|
-
}
|
|
5059
|
-
const stale = [];
|
|
5060
|
-
for (const v of resolvedSearchEnvVars) {
|
|
5061
|
-
if (written.includes(v.name)) continue;
|
|
5062
|
-
const current = await readEnvVar(worktree, v.name);
|
|
5063
|
-
if (current && current !== v.value) stale.push(v);
|
|
5064
|
-
}
|
|
5065
|
-
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5066
|
-
summaries.push(
|
|
5067
|
-
`\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.`
|
|
5068
|
-
);
|
|
5069
|
-
logger.warn(
|
|
5070
|
-
{ vars: stale.map((v) => v.name) },
|
|
5071
|
-
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5072
|
-
);
|
|
5073
|
-
}
|
|
5074
4956
|
}
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
4957
|
+
const stale = [];
|
|
4958
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4959
|
+
if (written.includes(v.name)) continue;
|
|
4960
|
+
const current = await readEnvVar(repoRoot, v.name);
|
|
4961
|
+
if (current && current !== v.value) stale.push(v);
|
|
4962
|
+
}
|
|
4963
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5079
4964
|
summaries.push(
|
|
5080
|
-
|
|
5081
|
-
|
|
4965
|
+
`\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.`
|
|
4966
|
+
);
|
|
4967
|
+
logger.warn(
|
|
4968
|
+
{ vars: stale.map((v) => v.name) },
|
|
4969
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5082
4970
|
);
|
|
5083
4971
|
}
|
|
5084
|
-
} else {
|
|
5085
|
-
ctx.setUserInput("implementation", "success");
|
|
5086
4972
|
}
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
4973
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4974
|
+
(v) => v.value.startsWith("<")
|
|
4975
|
+
);
|
|
4976
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
4977
|
+
summaries.push(
|
|
4978
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5091
4979
|
);
|
|
5092
4980
|
}
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
filesChanged,
|
|
5096
|
-
summary: summaries.join("\n\n"),
|
|
5097
|
-
worktreePath: worktree,
|
|
5098
|
-
...useCases.includes("ingestion") && ingestCommand ? {
|
|
5099
|
-
ingestCommand,
|
|
5100
|
-
ingestScriptRan,
|
|
5101
|
-
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
5102
|
-
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5103
|
-
} : {},
|
|
5104
|
-
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
5105
|
-
};
|
|
5106
|
-
} finally {
|
|
5107
|
-
process.chdir(repoRoot);
|
|
4981
|
+
} else {
|
|
4982
|
+
ctx.setUserInput("implementation", "success");
|
|
5108
4983
|
}
|
|
4984
|
+
return {
|
|
4985
|
+
ingestionSource,
|
|
4986
|
+
summary: summaries.join("\n\n"),
|
|
4987
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4988
|
+
ingestCommand,
|
|
4989
|
+
ingestScriptRan,
|
|
4990
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4991
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4992
|
+
} : {},
|
|
4993
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4994
|
+
};
|
|
5109
4995
|
}
|
|
5110
4996
|
|
|
5111
4997
|
// src/workflows/default.ts
|
|
@@ -5177,10 +5063,7 @@ var defaultWorkflow = {
|
|
|
5177
5063
|
ctx.notify({
|
|
5178
5064
|
messages: ["Building your Algolia search experience\u2026"]
|
|
5179
5065
|
});
|
|
5180
|
-
|
|
5181
|
-
"ingestion"
|
|
5182
|
-
);
|
|
5183
|
-
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
5066
|
+
return implement(ctx, ["search"]);
|
|
5184
5067
|
}
|
|
5185
5068
|
}),
|
|
5186
5069
|
defineStep({
|
|
@@ -5195,9 +5078,8 @@ var defaultWorkflow = {
|
|
|
5195
5078
|
"ingestion"
|
|
5196
5079
|
);
|
|
5197
5080
|
return reviewStep(ctx, {
|
|
5198
|
-
//
|
|
5199
|
-
//
|
|
5200
|
-
// an LLM-paraphrased command risks being wrong.
|
|
5081
|
+
// ingestCommand was already shown verbatim as a notice; an
|
|
5082
|
+
// LLM-paraphrased restatement in nextSteps risks being wrong.
|
|
5201
5083
|
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."
|
|
5202
5084
|
});
|
|
5203
5085
|
}
|
|
@@ -5244,7 +5126,6 @@ var selectIndex = {
|
|
|
5244
5126
|
selection: "wizard_seed_products"
|
|
5245
5127
|
};
|
|
5246
5128
|
var ingestion = {
|
|
5247
|
-
filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
|
|
5248
5129
|
summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
|
|
5249
5130
|
ingestCommand: "node algolia/ingest.mjs",
|
|
5250
5131
|
ingestScriptRan: true,
|
|
@@ -5256,7 +5137,6 @@ var confirmFramework2 = {
|
|
|
5256
5137
|
frameworks: projectScan2.frameworks
|
|
5257
5138
|
};
|
|
5258
5139
|
var search = {
|
|
5259
|
-
filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
|
|
5260
5140
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
5261
5141
|
ingestionSource: "generated",
|
|
5262
5142
|
searchEnvVars: [
|
|
@@ -5269,7 +5149,7 @@ var review = {
|
|
|
5269
5149
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5270
5150
|
"Added an InstantSearch search experience to the shared header."
|
|
5271
5151
|
],
|
|
5272
|
-
reviewPrompt: "Review the Algolia ingestion and search changes
|
|
5152
|
+
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5273
5153
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5274
5154
|
};
|
|
5275
5155
|
var SEEDS = {
|
|
@@ -5386,9 +5266,9 @@ Options:
|
|
|
5386
5266
|
steps pre-filled with test data. Pass with no value to print
|
|
5387
5267
|
the step ids. See CONTRIBUTING.md.
|
|
5388
5268
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5389
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5390
|
-
|
|
5391
|
-
|
|
5269
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent)
|
|
5270
|
+
before starting, so the run behaves like a first-ever
|
|
5271
|
+
run. Also drops every API key the wizard has
|
|
5392
5272
|
stored in your keychain (or, where the platform has none,
|
|
5393
5273
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5394
5274
|
for this project and any other, so later runs create new
|
|
@@ -5428,7 +5308,7 @@ function parseCliArgs(argv) {
|
|
|
5428
5308
|
}
|
|
5429
5309
|
|
|
5430
5310
|
// src/lib/resetState.ts
|
|
5431
|
-
import { readdir as
|
|
5311
|
+
import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
5432
5312
|
import { join as join13 } from "node:path";
|
|
5433
5313
|
var KEEP = ["wizard.log"];
|
|
5434
5314
|
async function resetProjectState() {
|
|
@@ -5436,7 +5316,7 @@ async function resetProjectState() {
|
|
|
5436
5316
|
await forgetResolvedKeys();
|
|
5437
5317
|
let entries;
|
|
5438
5318
|
try {
|
|
5439
|
-
entries = await
|
|
5319
|
+
entries = await readdir3(dir);
|
|
5440
5320
|
} catch {
|
|
5441
5321
|
return { dir, removed: [] };
|
|
5442
5322
|
}
|