@algolia/wizard 0.23.0 → 0.24.0-rc.111.205
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 +318 -430
- 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
|
@@ -251,6 +251,7 @@ var useWizard = create((set, get) => ({
|
|
|
251
251
|
_noticeTimer: null,
|
|
252
252
|
cliOutput: [],
|
|
253
253
|
targetIndex: null,
|
|
254
|
+
writtenFiles: [],
|
|
254
255
|
logs: [],
|
|
255
256
|
error: null,
|
|
256
257
|
inputReq: null,
|
|
@@ -344,6 +345,8 @@ var useWizard = create((set, get) => ({
|
|
|
344
345
|
})),
|
|
345
346
|
clearCliOutput: () => set({ cliOutput: [] }),
|
|
346
347
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
348
|
+
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
349
|
+
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
347
350
|
logStart: (kind, name, input) => {
|
|
348
351
|
const id = nanoid();
|
|
349
352
|
set((s) => ({
|
|
@@ -391,6 +394,7 @@ var useWizard = create((set, get) => ({
|
|
|
391
394
|
notices: [],
|
|
392
395
|
cliOutput: [],
|
|
393
396
|
targetIndex: null,
|
|
397
|
+
writtenFiles: [],
|
|
394
398
|
logs: [],
|
|
395
399
|
error: null,
|
|
396
400
|
inputReq: null,
|
|
@@ -1259,7 +1263,7 @@ var accessItems = [
|
|
|
1259
1263
|
{
|
|
1260
1264
|
tag: "WRITE",
|
|
1261
1265
|
title: "Code changes",
|
|
1262
|
-
description: "creates & edits files (search UI, config) in
|
|
1266
|
+
description: "creates & edits files (search UI, config) directly in your branch."
|
|
1263
1267
|
},
|
|
1264
1268
|
{
|
|
1265
1269
|
tag: "EXEC",
|
|
@@ -1274,7 +1278,7 @@ var accessItems = [
|
|
|
1274
1278
|
{
|
|
1275
1279
|
tag: "KEY",
|
|
1276
1280
|
title: "Credentials",
|
|
1277
|
-
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in
|
|
1281
|
+
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
|
|
1278
1282
|
}
|
|
1279
1283
|
];
|
|
1280
1284
|
var neverItems = [
|
|
@@ -2554,6 +2558,7 @@ function writeFileTool(ctx) {
|
|
|
2554
2558
|
}
|
|
2555
2559
|
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2556
2560
|
await writeFile3(resolved2.target, content, "utf8");
|
|
2561
|
+
useWizard.getState().recordWrittenFile(resolved2.target);
|
|
2557
2562
|
return `Wrote to ${filePath}`;
|
|
2558
2563
|
} catch (err) {
|
|
2559
2564
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -3334,7 +3339,7 @@ function defaultCreateModel() {
|
|
|
3334
3339
|
}
|
|
3335
3340
|
function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
3336
3341
|
return tool10({
|
|
3337
|
-
description: "Generate realistic sample records for an entity and write them to a JSON file
|
|
3342
|
+
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.",
|
|
3338
3343
|
inputSchema: z17.object({
|
|
3339
3344
|
entityName: z17.string().describe("Name of the entity to generate records for."),
|
|
3340
3345
|
attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
|
|
@@ -3700,7 +3705,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3700
3705
|
// package.json
|
|
3701
3706
|
var package_default = {
|
|
3702
3707
|
name: "@algolia/wizard",
|
|
3703
|
-
version: "0.
|
|
3708
|
+
version: "0.24.0-rc.111.205",
|
|
3704
3709
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3705
3710
|
type: "module",
|
|
3706
3711
|
engines: {
|
|
@@ -4067,11 +4072,10 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
4067
4072
|
function formatReviewSummary(result) {
|
|
4068
4073
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
4069
4074
|
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
4070
|
-
const isWorktreeCommand = step.includes("/worktrees/");
|
|
4071
4075
|
return {
|
|
4072
4076
|
text: `\u2192 ${step}`,
|
|
4073
|
-
color: isIngestCommand ? COLORS.brand :
|
|
4074
|
-
bold: isIngestCommand
|
|
4077
|
+
color: isIngestCommand ? COLORS.brand : void 0,
|
|
4078
|
+
bold: isIngestCommand
|
|
4075
4079
|
};
|
|
4076
4080
|
});
|
|
4077
4081
|
return [
|
|
@@ -4106,15 +4110,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4106
4110
|
|
|
4107
4111
|
// src/actions/implement.ts
|
|
4108
4112
|
import z29 from "zod";
|
|
4109
|
-
import { join as join12 } from "node:path";
|
|
4113
|
+
import { join as join12, relative as relative6 } from "node:path";
|
|
4110
4114
|
|
|
4111
|
-
// src/lib/
|
|
4115
|
+
// src/lib/git.ts
|
|
4112
4116
|
import { execFile as execFile2 } from "node:child_process";
|
|
4113
|
-
import { copyFile, mkdir as mkdir6,
|
|
4117
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4114
4118
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4115
4119
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4116
|
-
var MAX_WIZARD_WORKTREES = 3;
|
|
4117
|
-
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4118
4120
|
function git(args) {
|
|
4119
4121
|
return new Promise((resolve4, reject) => {
|
|
4120
4122
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4137,44 +4139,7 @@ async function assertGitRepoWithHead(repoRoot) {
|
|
|
4137
4139
|
);
|
|
4138
4140
|
}
|
|
4139
4141
|
}
|
|
4140
|
-
async function
|
|
4141
|
-
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4142
|
-
return out.trim().length > 0;
|
|
4143
|
-
}
|
|
4144
|
-
async function pruneOldWorktrees(repoRoot) {
|
|
4145
|
-
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
4146
|
-
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4147
|
-
for (const slug of stale) {
|
|
4148
|
-
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4149
|
-
try {
|
|
4150
|
-
await git([
|
|
4151
|
-
"-C",
|
|
4152
|
-
repoRoot,
|
|
4153
|
-
"worktree",
|
|
4154
|
-
"remove",
|
|
4155
|
-
"--force",
|
|
4156
|
-
join10(dir, slug)
|
|
4157
|
-
]);
|
|
4158
|
-
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4159
|
-
} catch (err) {
|
|
4160
|
-
logger.warn(
|
|
4161
|
-
{ branch, err: err.message },
|
|
4162
|
-
"createWorktree: failed to prune a stale wizard worktree; continuing"
|
|
4163
|
-
);
|
|
4164
|
-
}
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
|
-
async function createWorktree(repoRoot) {
|
|
4168
|
-
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4169
|
-
const dirSlug = branch.replace(/\//g, "-");
|
|
4170
|
-
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4171
|
-
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4172
|
-
await pruneOldWorktrees(repoRoot);
|
|
4173
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
4174
|
-
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4175
|
-
return { path, branch };
|
|
4176
|
-
}
|
|
4177
|
-
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4142
|
+
async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
4178
4143
|
const trimmed = sourcePath.trim();
|
|
4179
4144
|
if (!trimmed) {
|
|
4180
4145
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4188,7 +4153,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4188
4153
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4189
4154
|
}
|
|
4190
4155
|
const relPath = join10(ingestDir, basename2(source));
|
|
4191
|
-
const dest = join10(
|
|
4156
|
+
const dest = join10(repoRoot, relPath);
|
|
4157
|
+
if (resolve3(source) === resolve3(dest)) {
|
|
4158
|
+
return { ok: true, relPath };
|
|
4159
|
+
}
|
|
4192
4160
|
try {
|
|
4193
4161
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4194
4162
|
await copyFile(source, dest);
|
|
@@ -4203,10 +4171,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4203
4171
|
function hasEnvVar(content, name) {
|
|
4204
4172
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4205
4173
|
}
|
|
4206
|
-
async function readEnvVar(
|
|
4174
|
+
async function readEnvVar(repoRoot, name) {
|
|
4207
4175
|
let content;
|
|
4208
4176
|
try {
|
|
4209
|
-
content = await readFile8(join10(
|
|
4177
|
+
content = await readFile8(join10(repoRoot, ".env"), "utf8");
|
|
4210
4178
|
} catch (err) {
|
|
4211
4179
|
if (err.code !== "ENOENT") throw err;
|
|
4212
4180
|
return void 0;
|
|
@@ -4220,8 +4188,8 @@ async function readEnvVar(worktreePath, name) {
|
|
|
4220
4188
|
if (!value || value.startsWith("<")) return void 0;
|
|
4221
4189
|
return value;
|
|
4222
4190
|
}
|
|
4223
|
-
async function writeSearchEnvValues(
|
|
4224
|
-
const target = join10(
|
|
4191
|
+
async function writeSearchEnvValues(repoRoot, vars) {
|
|
4192
|
+
const target = join10(repoRoot, ".env");
|
|
4225
4193
|
let existing = "";
|
|
4226
4194
|
try {
|
|
4227
4195
|
existing = await readFile8(target, "utf8");
|
|
@@ -4236,61 +4204,27 @@ async function writeSearchEnvValues(worktreePath, vars) {
|
|
|
4236
4204
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4237
4205
|
return missing.map((v) => v.name);
|
|
4238
4206
|
}
|
|
4239
|
-
async function listChangedFiles(worktreePath) {
|
|
4240
|
-
const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
|
|
4241
|
-
const entries = raw.split("\0");
|
|
4242
|
-
const files = [];
|
|
4243
|
-
for (let i = 0; i < entries.length; i += 1) {
|
|
4244
|
-
const entry = entries[i];
|
|
4245
|
-
if (!entry) continue;
|
|
4246
|
-
files.push(entry.slice(3));
|
|
4247
|
-
if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
|
|
4248
|
-
}
|
|
4249
|
-
return files;
|
|
4250
|
-
}
|
|
4251
4207
|
function normalizeFindingPaths(findings) {
|
|
4252
4208
|
return {
|
|
4253
4209
|
...findings,
|
|
4254
4210
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4255
4211
|
...e,
|
|
4256
|
-
paths: e.paths.map(
|
|
4212
|
+
paths: e.paths.map(toRootRelative)
|
|
4257
4213
|
})),
|
|
4258
4214
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4259
4215
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4260
4216
|
...e,
|
|
4261
|
-
paths: e.paths.map(
|
|
4217
|
+
paths: e.paths.map(toRootRelative)
|
|
4262
4218
|
}))
|
|
4263
4219
|
};
|
|
4264
4220
|
}
|
|
4265
4221
|
function normalizeSearchLocation(path) {
|
|
4266
|
-
const normalized = path ?
|
|
4222
|
+
const normalized = path ? toRootRelative(path).trim() : "";
|
|
4267
4223
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4268
4224
|
}
|
|
4269
|
-
function
|
|
4225
|
+
function toRootRelative(p) {
|
|
4270
4226
|
return p.replace(/^\/+/, "");
|
|
4271
4227
|
}
|
|
4272
|
-
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
4273
|
-
const MAX_LISTED_DIRTY_FILES = 10;
|
|
4274
|
-
const dirty = await listChangedFiles(repoRoot);
|
|
4275
|
-
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4276
|
-
const overflow = dirty.length - shown.length;
|
|
4277
|
-
const answer = await ctx.requestUserInput({
|
|
4278
|
-
prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
|
|
4279
|
-
promptType: "acceptReject",
|
|
4280
|
-
options: [],
|
|
4281
|
-
messages: [
|
|
4282
|
-
`${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
|
|
4283
|
-
...shown.map((file) => ` \u2022 ${file}`),
|
|
4284
|
-
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4285
|
-
"Commit or stash them first to include them in the implementation."
|
|
4286
|
-
]
|
|
4287
|
-
});
|
|
4288
|
-
if (answer !== true) {
|
|
4289
|
-
throw new Error(
|
|
4290
|
-
"implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
|
|
4291
|
-
);
|
|
4292
|
-
}
|
|
4293
|
-
}
|
|
4294
4228
|
|
|
4295
4229
|
// src/lib/algoliaDocs.ts
|
|
4296
4230
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
@@ -4350,11 +4284,6 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4350
4284
|
return loadAlgoliaDoc("js");
|
|
4351
4285
|
}
|
|
4352
4286
|
|
|
4353
|
-
// src/lib/shell.ts
|
|
4354
|
-
function shellQuote(value) {
|
|
4355
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4356
|
-
}
|
|
4357
|
-
|
|
4358
4287
|
// src/actions/resolveEnvVarPrefix.ts
|
|
4359
4288
|
import z28 from "zod";
|
|
4360
4289
|
var resolveEnvVarPrefixSchema = z28.object({
|
|
@@ -4374,9 +4303,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
|
4374
4303
|
|
|
4375
4304
|
// src/actions/implement.ts
|
|
4376
4305
|
var implementSchema = z29.object({
|
|
4377
|
-
filesChanged: z29.array(z29.string()),
|
|
4378
4306
|
summary: z29.string(),
|
|
4379
|
-
worktreePath: z29.string().optional(),
|
|
4380
4307
|
ingestCommand: z29.string().optional(),
|
|
4381
4308
|
ingestScriptRan: z29.boolean().optional(),
|
|
4382
4309
|
ingestRecordCount: z29.number().optional(),
|
|
@@ -4429,8 +4356,6 @@ function frameworksForDoc(language) {
|
|
|
4429
4356
|
}
|
|
4430
4357
|
function baseInstructions(input) {
|
|
4431
4358
|
return [
|
|
4432
|
-
// Agents have renamed this (e.g. appending the project name), which the
|
|
4433
|
-
// index-scoped keys then reject with a 403.
|
|
4434
4359
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4435
4360
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4436
4361
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4448,14 +4373,14 @@ function sourceSpecificInstructions(input) {
|
|
|
4448
4373
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4449
4374
|
],
|
|
4450
4375
|
fileUpload: [
|
|
4451
|
-
`Records come from the developer's file, already copied into the
|
|
4376
|
+
`Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4452
4377
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4453
4378
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4454
4379
|
"Never fabricate, hardcode, or substitute a different file."
|
|
4455
4380
|
],
|
|
4456
4381
|
generated: [
|
|
4457
4382
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
4458
|
-
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file
|
|
4383
|
+
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file, returning the file path. Do not write records or objectIDs yourself.",
|
|
4459
4384
|
"In the script, read and parse each returned file path at runtime using your language's standard JSON support, instead of inlining the records as literals.",
|
|
4460
4385
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
4461
4386
|
]
|
|
@@ -4504,14 +4429,11 @@ function searchInstructions(input) {
|
|
|
4504
4429
|
"If a search box already exists, replace it with yours.",
|
|
4505
4430
|
`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.`,
|
|
4506
4431
|
"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.",
|
|
4507
|
-
// The key is provisioned only after verification passes,
|
|
4508
|
-
//
|
|
4509
|
-
//
|
|
4510
|
-
// would be reused as if it were real.
|
|
4432
|
+
// The key is provisioned only after verification passes, and the wizard
|
|
4433
|
+
// reads .env to decide whether a key already exists — an agent-invented
|
|
4434
|
+
// value there would be reused as if it were real.
|
|
4511
4435
|
`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.`,
|
|
4512
|
-
//
|
|
4513
|
-
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4514
|
-
// reading a var the wizard never wrote.
|
|
4436
|
+
// The wizard writes these exact names into .env right after this step.
|
|
4515
4437
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4516
4438
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4517
4439
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4520,10 +4442,10 @@ function searchInstructions(input) {
|
|
|
4520
4442
|
}
|
|
4521
4443
|
function verificationInstructions(input) {
|
|
4522
4444
|
return [
|
|
4523
|
-
"Verify the Algolia implementation changes
|
|
4445
|
+
"Verify the Algolia implementation changes.",
|
|
4524
4446
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4525
4447
|
"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.",
|
|
4526
|
-
"
|
|
4448
|
+
"If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4527
4449
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4528
4450
|
"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.",
|
|
4529
4451
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4618,11 +4540,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4618
4540
|
}
|
|
4619
4541
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4620
4542
|
}
|
|
4621
|
-
function makeToolContext(
|
|
4543
|
+
function makeToolContext(root, env = async () => ({})) {
|
|
4622
4544
|
return createToolContext(
|
|
4623
4545
|
DEFAULT_TOOL_LIMITS,
|
|
4624
|
-
|
|
4625
|
-
createShellContext({ env, approve: storeApproval(
|
|
4546
|
+
root,
|
|
4547
|
+
createShellContext({ env, approve: storeApproval(root) })
|
|
4626
4548
|
);
|
|
4627
4549
|
}
|
|
4628
4550
|
function verificationRetryInstructions(verification) {
|
|
@@ -4630,7 +4552,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4630
4552
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4631
4553
|
];
|
|
4632
4554
|
}
|
|
4633
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES
|
|
4555
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4634
4556
|
const repoRoot = process.cwd();
|
|
4635
4557
|
const scan = ctx.getStepOutput("project-scan");
|
|
4636
4558
|
const entities = ctx.getStepOutput(
|
|
@@ -4711,9 +4633,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4711
4633
|
const targetIndex = selected?.selection;
|
|
4712
4634
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4713
4635
|
await assertGitRepoWithHead(repoRoot);
|
|
4714
|
-
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4715
|
-
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4716
|
-
}
|
|
4717
4636
|
const normalized = normalizeFindingPaths(findings);
|
|
4718
4637
|
const confirmed2 = normalized.confirmedEntities;
|
|
4719
4638
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4725,328 +4644,303 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4725
4644
|
if (useCases.includes("ingestion")) {
|
|
4726
4645
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4727
4646
|
}
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4647
|
+
let uploadFilePath;
|
|
4648
|
+
let uploadWarning;
|
|
4649
|
+
if (ingestionSource === "fileUpload") {
|
|
4650
|
+
const copied = await copyUploadIntoProject(
|
|
4651
|
+
repoRoot,
|
|
4652
|
+
INGEST_DIR,
|
|
4653
|
+
uploadSourcePath ?? ""
|
|
4654
|
+
);
|
|
4655
|
+
if (copied.ok) {
|
|
4656
|
+
uploadFilePath = copied.relPath;
|
|
4657
|
+
} else {
|
|
4658
|
+
ingestionSource = "generated";
|
|
4659
|
+
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4660
|
+
logger.warn(
|
|
4661
|
+
{ reason: copied.reason },
|
|
4662
|
+
"implement: file upload unavailable; falling back to generated sample records"
|
|
4663
|
+
);
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
|
|
4667
|
+
const input = {
|
|
4668
|
+
findings: normalized,
|
|
4669
|
+
confirmed: confirmed2,
|
|
4670
|
+
searchLocation,
|
|
4671
|
+
targetIndex,
|
|
4672
|
+
language,
|
|
4673
|
+
publicEnvVarPrefix,
|
|
4674
|
+
appId,
|
|
4675
|
+
searchEnvVars: publicSearchEnvVars(publicEnvVarPrefix, targetIndex, appId),
|
|
4676
|
+
ingestDir: INGEST_DIR,
|
|
4677
|
+
ingestionSource,
|
|
4678
|
+
uploadFilePath,
|
|
4679
|
+
searchUiTarget: searchUiTarget(language)
|
|
4680
|
+
};
|
|
4681
|
+
const summaries = [];
|
|
4682
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4683
|
+
let envSearchKey;
|
|
4684
|
+
let envAppIdMismatch = false;
|
|
4685
|
+
if (useCases.includes("search") && appId) {
|
|
4686
|
+
const envAppId = await readEnvVar(
|
|
4687
|
+
repoRoot,
|
|
4688
|
+
publicAppIdVar(publicEnvVarPrefix)
|
|
4689
|
+
);
|
|
4690
|
+
if (envAppId === appId) {
|
|
4691
|
+
envSearchKey = await readEnvVar(
|
|
4735
4692
|
repoRoot,
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4693
|
+
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4694
|
+
);
|
|
4695
|
+
} else if (envAppId) {
|
|
4696
|
+
envAppIdMismatch = true;
|
|
4697
|
+
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4698
|
+
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4699
|
+
summaries.push(
|
|
4700
|
+
`\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.`
|
|
4701
|
+
);
|
|
4702
|
+
logger.warn(
|
|
4703
|
+
{ envAppId, appId },
|
|
4704
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4739
4705
|
);
|
|
4740
|
-
if (copied.ok) {
|
|
4741
|
-
uploadFilePath = copied.relPath;
|
|
4742
|
-
} else {
|
|
4743
|
-
ingestionSource = "generated";
|
|
4744
|
-
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4745
|
-
logger.warn(
|
|
4746
|
-
{ reason: copied.reason },
|
|
4747
|
-
"implement: file upload unavailable; falling back to generated sample records"
|
|
4748
|
-
);
|
|
4749
|
-
}
|
|
4750
4706
|
}
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4707
|
+
}
|
|
4708
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4709
|
+
let agentRuns = 0;
|
|
4710
|
+
let ingestCommand;
|
|
4711
|
+
let ingestScriptRan = false;
|
|
4712
|
+
let ingestRecordCount;
|
|
4713
|
+
let ingestDurationMs;
|
|
4714
|
+
let ingestOutcomeMessage;
|
|
4715
|
+
const ingestKeyAppId = ingestAppId;
|
|
4716
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
|
|
4717
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4718
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4719
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4720
|
+
})) : void 0;
|
|
4721
|
+
const searchTools = makeToolContext(repoRoot);
|
|
4722
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4723
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4724
|
+
agentRuns += 1;
|
|
4725
|
+
return runAgent({
|
|
4726
|
+
instructions: buildAgentInstructions(
|
|
4727
|
+
currentUseCase,
|
|
4728
|
+
input,
|
|
4729
|
+
extraInstructions
|
|
4764
4730
|
),
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
if (
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4731
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4732
|
+
outputSchema: implementationOutputSchema,
|
|
4733
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4734
|
+
});
|
|
4735
|
+
}
|
|
4736
|
+
async function runVerificationUseCase() {
|
|
4737
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4738
|
+
agentRuns += 1;
|
|
4739
|
+
return runAgent({
|
|
4740
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4741
|
+
tools: toolsForUseCase("verification"),
|
|
4742
|
+
outputSchema: verificationOutputSchema,
|
|
4743
|
+
toolContext: searchTools
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
if (useCases.includes("ingestion")) {
|
|
4747
|
+
let ingestFailureDetail;
|
|
4748
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4749
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4750
|
+
ingestCommand = result.ingestCommand;
|
|
4751
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4752
|
+
const executions = ingestionContext.shell.executions;
|
|
4753
|
+
const {
|
|
4754
|
+
run: ingestRun,
|
|
4755
|
+
attempt: ingestAttempt,
|
|
4756
|
+
recordCount
|
|
4757
|
+
} = ingestOutcome(executions, ingestCommand);
|
|
4758
|
+
ingestScriptRan = ingestRun != null;
|
|
4759
|
+
ingestRecordCount = recordCount;
|
|
4760
|
+
ingestDurationMs = ingestRun?.durationMs;
|
|
4761
|
+
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4762
|
+
summaries.push(
|
|
4763
|
+
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
|
|
4764
|
+
);
|
|
4765
|
+
logger.warn(
|
|
4766
|
+
{ ingestCommand },
|
|
4767
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4778
4768
|
);
|
|
4779
|
-
if (envAppId === appId) {
|
|
4780
|
-
envSearchKey = await readEnvVar(
|
|
4781
|
-
worktree,
|
|
4782
|
-
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4783
|
-
);
|
|
4784
|
-
} else if (envAppId) {
|
|
4785
|
-
envAppIdMismatch = true;
|
|
4786
|
-
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4787
|
-
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4788
|
-
summaries.push(
|
|
4789
|
-
`\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.`
|
|
4790
|
-
);
|
|
4791
|
-
logger.warn(
|
|
4792
|
-
{ envAppId, appId },
|
|
4793
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4794
|
-
);
|
|
4795
|
-
}
|
|
4796
|
-
}
|
|
4797
|
-
let finalSearchEnvVars = input.searchEnvVars;
|
|
4798
|
-
let agentRuns = 0;
|
|
4799
|
-
let ingestCommand;
|
|
4800
|
-
let ingestScriptRan = false;
|
|
4801
|
-
let ingestRecordCount;
|
|
4802
|
-
let ingestDurationMs;
|
|
4803
|
-
let ingestOutcomeMessage;
|
|
4804
|
-
const ingestKeyAppId = ingestAppId;
|
|
4805
|
-
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4806
|
-
[APP_ID_VAR]: ingestKeyAppId,
|
|
4807
|
-
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4808
|
-
[INDEX_NAME_VAR]: targetIndex
|
|
4809
|
-
})) : void 0;
|
|
4810
|
-
const searchTools = makeToolContext(worktree);
|
|
4811
|
-
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4812
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4813
|
-
agentRuns += 1;
|
|
4814
|
-
return runAgent({
|
|
4815
|
-
instructions: buildAgentInstructions(
|
|
4816
|
-
currentUseCase,
|
|
4817
|
-
input,
|
|
4818
|
-
extraInstructions
|
|
4819
|
-
),
|
|
4820
|
-
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4821
|
-
outputSchema: implementationOutputSchema,
|
|
4822
|
-
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4823
|
-
});
|
|
4824
|
-
}
|
|
4825
|
-
async function runVerificationUseCase() {
|
|
4826
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4827
|
-
agentRuns += 1;
|
|
4828
|
-
return runAgent({
|
|
4829
|
-
instructions: buildAgentInstructions("verification", input),
|
|
4830
|
-
tools: toolsForUseCase("verification"),
|
|
4831
|
-
outputSchema: verificationOutputSchema,
|
|
4832
|
-
toolContext: searchTools
|
|
4833
|
-
});
|
|
4834
4769
|
}
|
|
4835
|
-
if (
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
run: ingestRun,
|
|
4844
|
-
attempt: ingestAttempt,
|
|
4845
|
-
recordCount
|
|
4846
|
-
} = ingestOutcome(executions, ingestCommand);
|
|
4847
|
-
ingestScriptRan = ingestRun != null;
|
|
4848
|
-
ingestRecordCount = recordCount;
|
|
4849
|
-
ingestDurationMs = ingestRun?.durationMs;
|
|
4850
|
-
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4851
|
-
summaries.push(
|
|
4852
|
-
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
|
|
4853
|
-
);
|
|
4854
|
-
logger.warn(
|
|
4855
|
-
{ ingestCommand },
|
|
4856
|
-
"implement: ingestion ran without a reviewScript call"
|
|
4857
|
-
);
|
|
4770
|
+
if (ingestScriptRan) {
|
|
4771
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4772
|
+
if (ingestRecordCount != null) {
|
|
4773
|
+
track("AI Wizard Ingest Successful", {
|
|
4774
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4775
|
+
record_count: ingestRecordCount,
|
|
4776
|
+
duration_ms: ingestDurationMs ?? 0
|
|
4777
|
+
});
|
|
4858
4778
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
duration_ms: ingestDurationMs ?? 0
|
|
4866
|
-
});
|
|
4867
|
-
}
|
|
4868
|
-
} else {
|
|
4869
|
-
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4870
|
-
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4871
|
-
ingestFailureDetail = detail;
|
|
4872
|
-
summaries.push(
|
|
4873
|
-
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4779
|
+
} else {
|
|
4780
|
+
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4781
|
+
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4782
|
+
ingestFailureDetail = detail;
|
|
4783
|
+
summaries.push(
|
|
4784
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4874
4785
|
${detail}` : ""}`
|
|
4875
|
-
|
|
4876
|
-
|
|
4786
|
+
);
|
|
4787
|
+
logger.warn(
|
|
4788
|
+
{
|
|
4789
|
+
ingestCommand,
|
|
4790
|
+
reason,
|
|
4791
|
+
approved: ingestAttempt?.approved,
|
|
4792
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4793
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4794
|
+
commandsRun: executions.length
|
|
4795
|
+
},
|
|
4796
|
+
"implement: ingestion script did not complete successfully"
|
|
4797
|
+
);
|
|
4798
|
+
track("Error", {
|
|
4799
|
+
step: "Push Data",
|
|
4800
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4801
|
+
product_area: "AI Wizard"
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
|
|
4805
|
+
await ctx.requestUserInput({
|
|
4806
|
+
prompt: "",
|
|
4807
|
+
promptType: "enterToContinue",
|
|
4808
|
+
options: [],
|
|
4809
|
+
// One message per line: Notices.tsx counts a message as one wrapped
|
|
4810
|
+
// line, so an embedded newline overflows the panel's height accounting.
|
|
4811
|
+
messages: [
|
|
4812
|
+
ingestOutcomeMessage,
|
|
4813
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4814
|
+
...commandMessages
|
|
4815
|
+
]
|
|
4816
|
+
});
|
|
4817
|
+
}
|
|
4818
|
+
if (useCases.includes("search")) {
|
|
4819
|
+
let extraInstructions = [];
|
|
4820
|
+
useWizard.getState().clearWrittenFiles();
|
|
4821
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4822
|
+
if (attempt > 1) {
|
|
4823
|
+
logger.info(
|
|
4877
4824
|
{
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
exitCode: ingestAttempt?.exitCode,
|
|
4882
|
-
timedOut: ingestAttempt?.timedOut,
|
|
4883
|
-
commandsRun: executions.length
|
|
4825
|
+
attempt,
|
|
4826
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4827
|
+
extraInstructions
|
|
4884
4828
|
},
|
|
4885
|
-
"implement:
|
|
4829
|
+
"implement: retrying search implementation after failed verification"
|
|
4886
4830
|
);
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4831
|
+
}
|
|
4832
|
+
const { summary } = await runImplementationUseCase(
|
|
4833
|
+
"search",
|
|
4834
|
+
extraInstructions
|
|
4835
|
+
);
|
|
4836
|
+
summaries.push(formatSummary("search", summary));
|
|
4837
|
+
const verification = await runVerificationUseCase();
|
|
4838
|
+
summaries.push(formatSummary("verification", verification.summary));
|
|
4839
|
+
if (verification.sufficient) {
|
|
4840
|
+
ctx.setUserInput("implementation", "success");
|
|
4841
|
+
const searchFilesChanged = [
|
|
4842
|
+
...new Set(useWizard.getState().writtenFiles)
|
|
4843
|
+
].map((file) => relative6(repoRoot, file));
|
|
4844
|
+
track("AI Wizard Frontend Component Generated", {
|
|
4845
|
+
filePaths: searchFilesChanged
|
|
4846
|
+
});
|
|
4847
|
+
track("AI Wizard Wired to UI", {
|
|
4848
|
+
location_heuristic: searchLocation ?? "unknown"
|
|
4891
4849
|
});
|
|
4850
|
+
break;
|
|
4892
4851
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4852
|
+
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4853
|
+
ctx.setUserInput("implementation", "fail");
|
|
4854
|
+
throw new Error(
|
|
4855
|
+
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4856
|
+
);
|
|
4896
4857
|
}
|
|
4897
|
-
|
|
4898
|
-
prompt: "",
|
|
4899
|
-
promptType: "enterToContinue",
|
|
4900
|
-
options: [],
|
|
4901
|
-
// The wizard never streams command output, so a failed run's tail is the
|
|
4902
|
-
// only place the developer sees why it failed. One message per line:
|
|
4903
|
-
// the panel's height accounting counts a message as one wrapped line
|
|
4904
|
-
// (see Notices.tsx), so an embedded newline overflows it.
|
|
4905
|
-
messages: [
|
|
4906
|
-
ingestOutcomeMessage,
|
|
4907
|
-
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4908
|
-
...commandMessages
|
|
4909
|
-
]
|
|
4910
|
-
});
|
|
4858
|
+
extraInstructions = verificationRetryInstructions(verification);
|
|
4911
4859
|
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4860
|
+
let searchKey;
|
|
4861
|
+
let searchKeyError;
|
|
4862
|
+
if (appId) {
|
|
4863
|
+
try {
|
|
4864
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4865
|
+
targetIndex,
|
|
4866
|
+
appId,
|
|
4867
|
+
envSearchKey
|
|
4868
|
+
);
|
|
4869
|
+
searchKey = resolved2.key;
|
|
4870
|
+
summaries.push(
|
|
4871
|
+
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}.`
|
|
4872
|
+
);
|
|
4873
|
+
} catch (err) {
|
|
4874
|
+
searchKeyError = err.message;
|
|
4875
|
+
logger.warn(
|
|
4876
|
+
{ err: searchKeyError },
|
|
4877
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4929
4878
|
);
|
|
4930
|
-
summaries.push(formatSummary("search", summary));
|
|
4931
|
-
const verification = await runVerificationUseCase();
|
|
4932
|
-
summaries.push(formatSummary("verification", verification.summary));
|
|
4933
|
-
if (verification.sufficient) {
|
|
4934
|
-
ctx.setUserInput("implementation", "success");
|
|
4935
|
-
const searchFilesChanged = (await listChangedFiles(worktree)).filter(
|
|
4936
|
-
(file) => !preSearchFiles.has(file)
|
|
4937
|
-
);
|
|
4938
|
-
track("AI Wizard Frontend Component Generated", {
|
|
4939
|
-
filePaths: searchFilesChanged
|
|
4940
|
-
});
|
|
4941
|
-
track("AI Wizard Wired to UI", {
|
|
4942
|
-
location_heuristic: searchLocation ?? "unknown"
|
|
4943
|
-
});
|
|
4944
|
-
break;
|
|
4945
|
-
}
|
|
4946
|
-
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4947
|
-
ctx.setUserInput("implementation", "fail");
|
|
4948
|
-
throw new Error(
|
|
4949
|
-
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4950
|
-
);
|
|
4951
|
-
}
|
|
4952
|
-
extraInstructions = verificationRetryInstructions(verification);
|
|
4953
|
-
}
|
|
4954
|
-
let searchKey;
|
|
4955
|
-
let searchKeyError;
|
|
4956
|
-
if (appId) {
|
|
4957
|
-
try {
|
|
4958
|
-
const resolved2 = await resolveSearchOnlyKey(
|
|
4959
|
-
targetIndex,
|
|
4960
|
-
appId,
|
|
4961
|
-
envSearchKey
|
|
4962
|
-
);
|
|
4963
|
-
searchKey = resolved2.key;
|
|
4964
|
-
summaries.push(
|
|
4965
|
-
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}.`
|
|
4966
|
-
);
|
|
4967
|
-
} catch (err) {
|
|
4968
|
-
searchKeyError = err.message;
|
|
4969
|
-
logger.warn(
|
|
4970
|
-
{ err: searchKeyError },
|
|
4971
|
-
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4972
|
-
);
|
|
4973
|
-
}
|
|
4974
4879
|
}
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4880
|
+
}
|
|
4881
|
+
finalSearchEnvVars = publicSearchEnvVars(
|
|
4882
|
+
publicEnvVarPrefix,
|
|
4883
|
+
targetIndex,
|
|
4884
|
+
appId,
|
|
4885
|
+
searchKey
|
|
4886
|
+
);
|
|
4887
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4888
|
+
(v) => !v.value.startsWith("<")
|
|
4889
|
+
);
|
|
4890
|
+
if (resolvedSearchEnvVars.length > 0) {
|
|
4891
|
+
const written = await writeSearchEnvValues(
|
|
4892
|
+
repoRoot,
|
|
4893
|
+
resolvedSearchEnvVars
|
|
4983
4894
|
);
|
|
4984
|
-
if (
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4895
|
+
if (written.length > 0) {
|
|
4896
|
+
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4897
|
+
}
|
|
4898
|
+
const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
|
|
4899
|
+
if (ignored === "added") {
|
|
4900
|
+
summaries.push("Added .env to .gitignore.");
|
|
4901
|
+
} else if (ignored === "tracked") {
|
|
4902
|
+
summaries.push(
|
|
4903
|
+
'\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.'
|
|
4988
4904
|
);
|
|
4989
|
-
if (written.length > 0) {
|
|
4990
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4991
|
-
}
|
|
4992
|
-
const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
|
|
4993
|
-
if (ignored === "added") {
|
|
4994
|
-
summaries.push("Added .env to .gitignore.");
|
|
4995
|
-
} else if (ignored === "tracked") {
|
|
4996
|
-
summaries.push(
|
|
4997
|
-
'\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.'
|
|
4998
|
-
);
|
|
4999
|
-
}
|
|
5000
|
-
const stale = [];
|
|
5001
|
-
for (const v of resolvedSearchEnvVars) {
|
|
5002
|
-
if (written.includes(v.name)) continue;
|
|
5003
|
-
const current = await readEnvVar(worktree, v.name);
|
|
5004
|
-
if (current && current !== v.value) stale.push(v);
|
|
5005
|
-
}
|
|
5006
|
-
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5007
|
-
summaries.push(
|
|
5008
|
-
`\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.`
|
|
5009
|
-
);
|
|
5010
|
-
logger.warn(
|
|
5011
|
-
{ vars: stale.map((v) => v.name) },
|
|
5012
|
-
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5013
|
-
);
|
|
5014
|
-
}
|
|
5015
4905
|
}
|
|
5016
|
-
const
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
4906
|
+
const stale = [];
|
|
4907
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4908
|
+
if (written.includes(v.name)) continue;
|
|
4909
|
+
const current = await readEnvVar(repoRoot, v.name);
|
|
4910
|
+
if (current && current !== v.value) stale.push(v);
|
|
4911
|
+
}
|
|
4912
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5020
4913
|
summaries.push(
|
|
5021
|
-
|
|
5022
|
-
|
|
4914
|
+
`\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.`
|
|
4915
|
+
);
|
|
4916
|
+
logger.warn(
|
|
4917
|
+
{ vars: stale.map((v) => v.name) },
|
|
4918
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5023
4919
|
);
|
|
5024
4920
|
}
|
|
5025
|
-
} else {
|
|
5026
|
-
ctx.setUserInput("implementation", "success");
|
|
5027
4921
|
}
|
|
5028
|
-
const
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
4922
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4923
|
+
(v) => v.value.startsWith("<")
|
|
4924
|
+
);
|
|
4925
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
4926
|
+
summaries.push(
|
|
4927
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5032
4928
|
);
|
|
5033
4929
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
filesChanged,
|
|
5037
|
-
summary: summaries.join("\n\n"),
|
|
5038
|
-
worktreePath: worktree,
|
|
5039
|
-
...useCases.includes("ingestion") && ingestCommand ? {
|
|
5040
|
-
ingestCommand,
|
|
5041
|
-
ingestScriptRan,
|
|
5042
|
-
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
5043
|
-
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5044
|
-
} : {},
|
|
5045
|
-
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
5046
|
-
};
|
|
5047
|
-
} finally {
|
|
5048
|
-
process.chdir(repoRoot);
|
|
4930
|
+
} else {
|
|
4931
|
+
ctx.setUserInput("implementation", "success");
|
|
5049
4932
|
}
|
|
4933
|
+
return {
|
|
4934
|
+
ingestionSource,
|
|
4935
|
+
summary: summaries.join("\n\n"),
|
|
4936
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4937
|
+
ingestCommand,
|
|
4938
|
+
ingestScriptRan,
|
|
4939
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4940
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4941
|
+
} : {},
|
|
4942
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4943
|
+
};
|
|
5050
4944
|
}
|
|
5051
4945
|
|
|
5052
4946
|
// src/workflows/default.ts
|
|
@@ -5118,10 +5012,7 @@ var defaultWorkflow = {
|
|
|
5118
5012
|
ctx.notify({
|
|
5119
5013
|
messages: ["Building your Algolia search experience\u2026"]
|
|
5120
5014
|
});
|
|
5121
|
-
|
|
5122
|
-
"ingestion"
|
|
5123
|
-
);
|
|
5124
|
-
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
5015
|
+
return implement(ctx, ["search"]);
|
|
5125
5016
|
}
|
|
5126
5017
|
}),
|
|
5127
5018
|
defineStep({
|
|
@@ -5136,9 +5027,8 @@ var defaultWorkflow = {
|
|
|
5136
5027
|
"ingestion"
|
|
5137
5028
|
);
|
|
5138
5029
|
return reviewStep(ctx, {
|
|
5139
|
-
//
|
|
5140
|
-
//
|
|
5141
|
-
// an LLM-paraphrased command risks being wrong.
|
|
5030
|
+
// ingestCommand was already shown verbatim as a notice; an
|
|
5031
|
+
// LLM-paraphrased restatement in nextSteps risks being wrong.
|
|
5142
5032
|
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."
|
|
5143
5033
|
});
|
|
5144
5034
|
}
|
|
@@ -5185,7 +5075,6 @@ var selectIndex = {
|
|
|
5185
5075
|
selection: "wizard_seed_products"
|
|
5186
5076
|
};
|
|
5187
5077
|
var ingestion = {
|
|
5188
|
-
filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
|
|
5189
5078
|
summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
|
|
5190
5079
|
ingestCommand: "node algolia/ingest.mjs",
|
|
5191
5080
|
ingestScriptRan: true,
|
|
@@ -5197,7 +5086,6 @@ var confirmFramework2 = {
|
|
|
5197
5086
|
frameworks: projectScan2.frameworks
|
|
5198
5087
|
};
|
|
5199
5088
|
var search = {
|
|
5200
|
-
filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
|
|
5201
5089
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
5202
5090
|
ingestionSource: "generated",
|
|
5203
5091
|
searchEnvVars: [
|
|
@@ -5210,7 +5098,7 @@ var review = {
|
|
|
5210
5098
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5211
5099
|
"Added an InstantSearch search experience to the shared header."
|
|
5212
5100
|
],
|
|
5213
|
-
reviewPrompt: "Review the Algolia ingestion and search changes
|
|
5101
|
+
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5214
5102
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5215
5103
|
};
|
|
5216
5104
|
var SEEDS = {
|
|
@@ -5327,9 +5215,9 @@ Options:
|
|
|
5327
5215
|
steps pre-filled with test data. Pass with no value to print
|
|
5328
5216
|
the step ids. See CONTRIBUTING.md.
|
|
5329
5217
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5330
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5331
|
-
|
|
5332
|
-
|
|
5218
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent)
|
|
5219
|
+
before starting, so the run behaves like a first-ever
|
|
5220
|
+
run. Also drops every API key the wizard has
|
|
5333
5221
|
stored in your keychain (or, where the platform has none,
|
|
5334
5222
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5335
5223
|
for this project and any other, so later runs create new
|
|
@@ -5369,7 +5257,7 @@ function parseCliArgs(argv) {
|
|
|
5369
5257
|
}
|
|
5370
5258
|
|
|
5371
5259
|
// src/lib/resetState.ts
|
|
5372
|
-
import { readdir as
|
|
5260
|
+
import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
5373
5261
|
import { join as join13 } from "node:path";
|
|
5374
5262
|
var KEEP = ["wizard.log"];
|
|
5375
5263
|
async function resetProjectState() {
|
|
@@ -5377,7 +5265,7 @@ async function resetProjectState() {
|
|
|
5377
5265
|
await forgetResolvedKeys();
|
|
5378
5266
|
let entries;
|
|
5379
5267
|
try {
|
|
5380
|
-
entries = await
|
|
5268
|
+
entries = await readdir3(dir);
|
|
5381
5269
|
} catch {
|
|
5382
5270
|
return { dir, removed: [] };
|
|
5383
5271
|
}
|