@algolia/wizard 0.24.0-rc.111.205 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/main.js +438 -319
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,22 @@ 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
|
|
15
18
|
```
|
|
16
19
|
|
|
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
|
+
|
|
17
31
|
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`.
|
|
18
32
|
|
|
19
33
|
You'll also be asked once to consent to AI-authored changes to the repository.
|
package/dist/main.js
CHANGED
|
@@ -251,7 +251,6 @@ var useWizard = create((set, get) => ({
|
|
|
251
251
|
_noticeTimer: null,
|
|
252
252
|
cliOutput: [],
|
|
253
253
|
targetIndex: null,
|
|
254
|
-
writtenFiles: [],
|
|
255
254
|
logs: [],
|
|
256
255
|
error: null,
|
|
257
256
|
inputReq: null,
|
|
@@ -345,8 +344,6 @@ var useWizard = create((set, get) => ({
|
|
|
345
344
|
})),
|
|
346
345
|
clearCliOutput: () => set({ cliOutput: [] }),
|
|
347
346
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
348
|
-
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
349
|
-
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
350
347
|
logStart: (kind, name, input) => {
|
|
351
348
|
const id = nanoid();
|
|
352
349
|
set((s) => ({
|
|
@@ -394,7 +391,6 @@ var useWizard = create((set, get) => ({
|
|
|
394
391
|
notices: [],
|
|
395
392
|
cliOutput: [],
|
|
396
393
|
targetIndex: null,
|
|
397
|
-
writtenFiles: [],
|
|
398
394
|
logs: [],
|
|
399
395
|
error: null,
|
|
400
396
|
inputReq: null,
|
|
@@ -1263,7 +1259,7 @@ var accessItems = [
|
|
|
1263
1259
|
{
|
|
1264
1260
|
tag: "WRITE",
|
|
1265
1261
|
title: "Code changes",
|
|
1266
|
-
description: "creates & edits files (search UI, config)
|
|
1262
|
+
description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
|
|
1267
1263
|
},
|
|
1268
1264
|
{
|
|
1269
1265
|
tag: "EXEC",
|
|
@@ -1278,7 +1274,7 @@ var accessItems = [
|
|
|
1278
1274
|
{
|
|
1279
1275
|
tag: "KEY",
|
|
1280
1276
|
title: "Credentials",
|
|
1281
|
-
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in
|
|
1277
|
+
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
|
|
1282
1278
|
}
|
|
1283
1279
|
];
|
|
1284
1280
|
var neverItems = [
|
|
@@ -2558,7 +2554,6 @@ function writeFileTool(ctx) {
|
|
|
2558
2554
|
}
|
|
2559
2555
|
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2560
2556
|
await writeFile3(resolved2.target, content, "utf8");
|
|
2561
|
-
useWizard.getState().recordWrittenFile(resolved2.target);
|
|
2562
2557
|
return `Wrote to ${filePath}`;
|
|
2563
2558
|
} catch (err) {
|
|
2564
2559
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -3183,6 +3178,10 @@ function storeApproval(root) {
|
|
|
3183
3178
|
return answer === "approve" ? "approve" : "reject";
|
|
3184
3179
|
};
|
|
3185
3180
|
}
|
|
3181
|
+
var EXPLORATORY_COMMANDS = /* @__PURE__ */ new Set(["ls", "find", "tree", "dir"]);
|
|
3182
|
+
function isExploratoryCommand(command) {
|
|
3183
|
+
return command.split(/&&|;|\|/).map((segment) => segment.trim().split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
|
|
3184
|
+
}
|
|
3186
3185
|
async function approveAndRun(ctx, command, cwd, explanation) {
|
|
3187
3186
|
const decision = await ctx.shell.approve({ command, cwd, explanation });
|
|
3188
3187
|
if (decision === "reject") {
|
|
@@ -3227,7 +3226,7 @@ async function approveAndRun(ctx, command, cwd, explanation) {
|
|
|
3227
3226
|
}
|
|
3228
3227
|
function runShellTool(ctx) {
|
|
3229
3228
|
return tool8({
|
|
3230
|
-
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
|
|
3229
|
+
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. To inspect the project, use listFiles or searchFiles instead of ls/find \u2014 this tool refuses those. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
|
|
3231
3230
|
inputSchema: z15.object({
|
|
3232
3231
|
command: z15.string().describe(
|
|
3233
3232
|
"The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
|
|
@@ -3245,6 +3244,9 @@ function runShellTool(ctx) {
|
|
|
3245
3244
|
}
|
|
3246
3245
|
const resolved2 = resolveInRoot(ctx, cwd ?? ".");
|
|
3247
3246
|
if (!resolved2.ok) return resolved2.error;
|
|
3247
|
+
if (isExploratoryCommand(command)) {
|
|
3248
|
+
return "Refused: use listFiles or searchFiles to inspect the project instead of ls/find/tree.";
|
|
3249
|
+
}
|
|
3248
3250
|
logger.info({ command, cwd: resolved2.target }, "called runShell tool");
|
|
3249
3251
|
return serializePrompt(
|
|
3250
3252
|
() => approveAndRun(ctx, command, resolved2.target, explanation)
|
|
@@ -3339,7 +3341,7 @@ function defaultCreateModel() {
|
|
|
3339
3341
|
}
|
|
3340
3342
|
function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
3341
3343
|
return tool10({
|
|
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.",
|
|
3344
|
+
description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
|
|
3343
3345
|
inputSchema: z17.object({
|
|
3344
3346
|
entityName: z17.string().describe("Name of the entity to generate records for."),
|
|
3345
3347
|
attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
|
|
@@ -3705,7 +3707,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3705
3707
|
// package.json
|
|
3706
3708
|
var package_default = {
|
|
3707
3709
|
name: "@algolia/wizard",
|
|
3708
|
-
version: "0.24.0
|
|
3710
|
+
version: "0.24.0",
|
|
3709
3711
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3710
3712
|
type: "module",
|
|
3711
3713
|
engines: {
|
|
@@ -4072,10 +4074,11 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
4072
4074
|
function formatReviewSummary(result) {
|
|
4073
4075
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
4074
4076
|
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
4077
|
+
const isWorktreeCommand = step.includes("/worktrees/");
|
|
4075
4078
|
return {
|
|
4076
4079
|
text: `\u2192 ${step}`,
|
|
4077
|
-
color: isIngestCommand ? COLORS.brand : void 0,
|
|
4078
|
-
bold: isIngestCommand
|
|
4080
|
+
color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
|
|
4081
|
+
bold: isIngestCommand || isWorktreeCommand
|
|
4079
4082
|
};
|
|
4080
4083
|
});
|
|
4081
4084
|
return [
|
|
@@ -4110,13 +4113,15 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4110
4113
|
|
|
4111
4114
|
// src/actions/implement.ts
|
|
4112
4115
|
import z29 from "zod";
|
|
4113
|
-
import { join as join12
|
|
4116
|
+
import { join as join12 } from "node:path";
|
|
4114
4117
|
|
|
4115
|
-
// src/lib/
|
|
4118
|
+
// src/lib/worktree.ts
|
|
4116
4119
|
import { execFile as execFile2 } from "node:child_process";
|
|
4117
|
-
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4120
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4118
4121
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4119
4122
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4123
|
+
var MAX_WIZARD_WORKTREES = 3;
|
|
4124
|
+
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4120
4125
|
function git(args) {
|
|
4121
4126
|
return new Promise((resolve4, reject) => {
|
|
4122
4127
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4139,7 +4144,44 @@ async function assertGitRepoWithHead(repoRoot) {
|
|
|
4139
4144
|
);
|
|
4140
4145
|
}
|
|
4141
4146
|
}
|
|
4142
|
-
async function
|
|
4147
|
+
async function isWorkingTreeDirty(repoRoot) {
|
|
4148
|
+
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4149
|
+
return out.trim().length > 0;
|
|
4150
|
+
}
|
|
4151
|
+
async function pruneOldWorktrees(repoRoot) {
|
|
4152
|
+
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
4153
|
+
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4154
|
+
for (const slug of stale) {
|
|
4155
|
+
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4156
|
+
try {
|
|
4157
|
+
await git([
|
|
4158
|
+
"-C",
|
|
4159
|
+
repoRoot,
|
|
4160
|
+
"worktree",
|
|
4161
|
+
"remove",
|
|
4162
|
+
"--force",
|
|
4163
|
+
join10(dir, slug)
|
|
4164
|
+
]);
|
|
4165
|
+
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4166
|
+
} catch (err) {
|
|
4167
|
+
logger.warn(
|
|
4168
|
+
{ branch, err: err.message },
|
|
4169
|
+
"createWorktree: failed to prune a stale wizard worktree; continuing"
|
|
4170
|
+
);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
async function createWorktree(repoRoot) {
|
|
4175
|
+
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4176
|
+
const dirSlug = branch.replace(/\//g, "-");
|
|
4177
|
+
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4178
|
+
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4179
|
+
await pruneOldWorktrees(repoRoot);
|
|
4180
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
4181
|
+
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4182
|
+
return { path, branch };
|
|
4183
|
+
}
|
|
4184
|
+
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4143
4185
|
const trimmed = sourcePath.trim();
|
|
4144
4186
|
if (!trimmed) {
|
|
4145
4187
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4153,10 +4195,7 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4153
4195
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4154
4196
|
}
|
|
4155
4197
|
const relPath = join10(ingestDir, basename2(source));
|
|
4156
|
-
const dest = join10(
|
|
4157
|
-
if (resolve3(source) === resolve3(dest)) {
|
|
4158
|
-
return { ok: true, relPath };
|
|
4159
|
-
}
|
|
4198
|
+
const dest = join10(worktreePath, relPath);
|
|
4160
4199
|
try {
|
|
4161
4200
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4162
4201
|
await copyFile(source, dest);
|
|
@@ -4171,10 +4210,10 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4171
4210
|
function hasEnvVar(content, name) {
|
|
4172
4211
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4173
4212
|
}
|
|
4174
|
-
async function readEnvVar(
|
|
4213
|
+
async function readEnvVar(worktreePath, name) {
|
|
4175
4214
|
let content;
|
|
4176
4215
|
try {
|
|
4177
|
-
content = await readFile8(join10(
|
|
4216
|
+
content = await readFile8(join10(worktreePath, ".env"), "utf8");
|
|
4178
4217
|
} catch (err) {
|
|
4179
4218
|
if (err.code !== "ENOENT") throw err;
|
|
4180
4219
|
return void 0;
|
|
@@ -4188,8 +4227,8 @@ async function readEnvVar(repoRoot, name) {
|
|
|
4188
4227
|
if (!value || value.startsWith("<")) return void 0;
|
|
4189
4228
|
return value;
|
|
4190
4229
|
}
|
|
4191
|
-
async function writeSearchEnvValues(
|
|
4192
|
-
const target = join10(
|
|
4230
|
+
async function writeSearchEnvValues(worktreePath, vars) {
|
|
4231
|
+
const target = join10(worktreePath, ".env");
|
|
4193
4232
|
let existing = "";
|
|
4194
4233
|
try {
|
|
4195
4234
|
existing = await readFile8(target, "utf8");
|
|
@@ -4204,27 +4243,61 @@ async function writeSearchEnvValues(repoRoot, vars) {
|
|
|
4204
4243
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4205
4244
|
return missing.map((v) => v.name);
|
|
4206
4245
|
}
|
|
4246
|
+
async function listChangedFiles(worktreePath) {
|
|
4247
|
+
const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
|
|
4248
|
+
const entries = raw.split("\0");
|
|
4249
|
+
const files = [];
|
|
4250
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
4251
|
+
const entry = entries[i];
|
|
4252
|
+
if (!entry) continue;
|
|
4253
|
+
files.push(entry.slice(3));
|
|
4254
|
+
if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
|
|
4255
|
+
}
|
|
4256
|
+
return files;
|
|
4257
|
+
}
|
|
4207
4258
|
function normalizeFindingPaths(findings) {
|
|
4208
4259
|
return {
|
|
4209
4260
|
...findings,
|
|
4210
4261
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4211
4262
|
...e,
|
|
4212
|
-
paths: e.paths.map(
|
|
4263
|
+
paths: e.paths.map(toWorktreeRelative)
|
|
4213
4264
|
})),
|
|
4214
4265
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4215
4266
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4216
4267
|
...e,
|
|
4217
|
-
paths: e.paths.map(
|
|
4268
|
+
paths: e.paths.map(toWorktreeRelative)
|
|
4218
4269
|
}))
|
|
4219
4270
|
};
|
|
4220
4271
|
}
|
|
4221
4272
|
function normalizeSearchLocation(path) {
|
|
4222
|
-
const normalized = path ?
|
|
4273
|
+
const normalized = path ? toWorktreeRelative(path).trim() : "";
|
|
4223
4274
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4224
4275
|
}
|
|
4225
|
-
function
|
|
4276
|
+
function toWorktreeRelative(p) {
|
|
4226
4277
|
return p.replace(/^\/+/, "");
|
|
4227
4278
|
}
|
|
4279
|
+
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
4280
|
+
const MAX_LISTED_DIRTY_FILES = 10;
|
|
4281
|
+
const dirty = await listChangedFiles(repoRoot);
|
|
4282
|
+
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4283
|
+
const overflow = dirty.length - shown.length;
|
|
4284
|
+
const answer = await ctx.requestUserInput({
|
|
4285
|
+
prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
|
|
4286
|
+
promptType: "acceptReject",
|
|
4287
|
+
options: [],
|
|
4288
|
+
messages: [
|
|
4289
|
+
`${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
|
|
4290
|
+
...shown.map((file) => ` \u2022 ${file}`),
|
|
4291
|
+
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4292
|
+
"Commit or stash them first to include them in the implementation."
|
|
4293
|
+
]
|
|
4294
|
+
});
|
|
4295
|
+
if (answer !== true) {
|
|
4296
|
+
throw new Error(
|
|
4297
|
+
"implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4228
4301
|
|
|
4229
4302
|
// src/lib/algoliaDocs.ts
|
|
4230
4303
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
@@ -4284,6 +4357,11 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4284
4357
|
return loadAlgoliaDoc("js");
|
|
4285
4358
|
}
|
|
4286
4359
|
|
|
4360
|
+
// src/lib/shell.ts
|
|
4361
|
+
function shellQuote(value) {
|
|
4362
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4287
4365
|
// src/actions/resolveEnvVarPrefix.ts
|
|
4288
4366
|
import z28 from "zod";
|
|
4289
4367
|
var resolveEnvVarPrefixSchema = z28.object({
|
|
@@ -4303,7 +4381,9 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
|
4303
4381
|
|
|
4304
4382
|
// src/actions/implement.ts
|
|
4305
4383
|
var implementSchema = z29.object({
|
|
4384
|
+
filesChanged: z29.array(z29.string()),
|
|
4306
4385
|
summary: z29.string(),
|
|
4386
|
+
worktreePath: z29.string().optional(),
|
|
4307
4387
|
ingestCommand: z29.string().optional(),
|
|
4308
4388
|
ingestScriptRan: z29.boolean().optional(),
|
|
4309
4389
|
ingestRecordCount: z29.number().optional(),
|
|
@@ -4356,6 +4436,8 @@ function frameworksForDoc(language) {
|
|
|
4356
4436
|
}
|
|
4357
4437
|
function baseInstructions(input) {
|
|
4358
4438
|
return [
|
|
4439
|
+
// Agents have renamed this (e.g. appending the project name), which the
|
|
4440
|
+
// index-scoped keys then reject with a 403.
|
|
4359
4441
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4360
4442
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4361
4443
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4373,14 +4455,14 @@ function sourceSpecificInstructions(input) {
|
|
|
4373
4455
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4374
4456
|
],
|
|
4375
4457
|
fileUpload: [
|
|
4376
|
-
`Records come from the developer's file, already copied into the
|
|
4458
|
+
`Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4377
4459
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4378
4460
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4379
4461
|
"Never fabricate, hardcode, or substitute a different file."
|
|
4380
4462
|
],
|
|
4381
4463
|
generated: [
|
|
4382
4464
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
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.",
|
|
4465
|
+
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
|
|
4384
4466
|
"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.",
|
|
4385
4467
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
4386
4468
|
]
|
|
@@ -4429,11 +4511,14 @@ function searchInstructions(input) {
|
|
|
4429
4511
|
"If a search box already exists, replace it with yours.",
|
|
4430
4512
|
`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.`,
|
|
4431
4513
|
"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.",
|
|
4432
|
-
// The key is provisioned only after verification passes,
|
|
4433
|
-
//
|
|
4434
|
-
//
|
|
4514
|
+
// The key is provisioned only after verification passes, so the agent never
|
|
4515
|
+
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4516
|
+
// decide whether a key already exists, and an agent-invented value there
|
|
4517
|
+
// would be reused as if it were real.
|
|
4435
4518
|
`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.`,
|
|
4436
|
-
//
|
|
4519
|
+
// Not the agent's to rename: the wizard writes these exact names into
|
|
4520
|
+
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4521
|
+
// reading a var the wizard never wrote.
|
|
4437
4522
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4438
4523
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4439
4524
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4442,10 +4527,10 @@ function searchInstructions(input) {
|
|
|
4442
4527
|
}
|
|
4443
4528
|
function verificationInstructions(input) {
|
|
4444
4529
|
return [
|
|
4445
|
-
"Verify the Algolia implementation changes.",
|
|
4530
|
+
"Verify the Algolia implementation changes in the current worktree.",
|
|
4446
4531
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4447
4532
|
"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.",
|
|
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.",
|
|
4533
|
+
"This worktree starts with no installed dependencies. If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4449
4534
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4450
4535
|
"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.",
|
|
4451
4536
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4540,11 +4625,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4540
4625
|
}
|
|
4541
4626
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4542
4627
|
}
|
|
4543
|
-
function makeToolContext(
|
|
4628
|
+
function makeToolContext(worktree, env = async () => ({})) {
|
|
4544
4629
|
return createToolContext(
|
|
4545
4630
|
DEFAULT_TOOL_LIMITS,
|
|
4546
|
-
|
|
4547
|
-
createShellContext({ env, approve: storeApproval(
|
|
4631
|
+
worktree,
|
|
4632
|
+
createShellContext({ env, approve: storeApproval(worktree) })
|
|
4548
4633
|
);
|
|
4549
4634
|
}
|
|
4550
4635
|
function verificationRetryInstructions(verification) {
|
|
@@ -4552,7 +4637,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4552
4637
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4553
4638
|
];
|
|
4554
4639
|
}
|
|
4555
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4640
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
|
|
4556
4641
|
const repoRoot = process.cwd();
|
|
4557
4642
|
const scan = ctx.getStepOutput("project-scan");
|
|
4558
4643
|
const entities = ctx.getStepOutput(
|
|
@@ -4633,6 +4718,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4633
4718
|
const targetIndex = selected?.selection;
|
|
4634
4719
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4635
4720
|
await assertGitRepoWithHead(repoRoot);
|
|
4721
|
+
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4722
|
+
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4723
|
+
}
|
|
4636
4724
|
const normalized = normalizeFindingPaths(findings);
|
|
4637
4725
|
const confirmed2 = normalized.confirmedEntities;
|
|
4638
4726
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4644,303 +4732,328 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4644
4732
|
if (useCases.includes("ingestion")) {
|
|
4645
4733
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4646
4734
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
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
|
+
const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
|
|
4736
|
+
try {
|
|
4737
|
+
process.chdir(worktree);
|
|
4738
|
+
let uploadFilePath;
|
|
4739
|
+
let uploadWarning;
|
|
4740
|
+
if (ingestionSource === "fileUpload") {
|
|
4741
|
+
const copied = await copyUploadIntoWorktree(
|
|
4692
4742
|
repoRoot,
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
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"
|
|
4743
|
+
worktree,
|
|
4744
|
+
INGEST_DIR,
|
|
4745
|
+
uploadSourcePath ?? ""
|
|
4705
4746
|
);
|
|
4747
|
+
if (copied.ok) {
|
|
4748
|
+
uploadFilePath = copied.relPath;
|
|
4749
|
+
} else {
|
|
4750
|
+
ingestionSource = "generated";
|
|
4751
|
+
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4752
|
+
logger.warn(
|
|
4753
|
+
{ reason: copied.reason },
|
|
4754
|
+
"implement: file upload unavailable; falling back to generated sample records"
|
|
4755
|
+
);
|
|
4756
|
+
}
|
|
4706
4757
|
}
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
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
|
|
4758
|
+
const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
|
|
4759
|
+
const input = {
|
|
4760
|
+
findings: normalized,
|
|
4761
|
+
confirmed: confirmed2,
|
|
4762
|
+
searchLocation,
|
|
4763
|
+
targetIndex,
|
|
4764
|
+
language,
|
|
4765
|
+
publicEnvVarPrefix,
|
|
4766
|
+
appId,
|
|
4767
|
+
searchEnvVars: publicSearchEnvVars(
|
|
4768
|
+
publicEnvVarPrefix,
|
|
4769
|
+
targetIndex,
|
|
4770
|
+
appId
|
|
4730
4771
|
),
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
if (
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
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"
|
|
4772
|
+
ingestDir: INGEST_DIR,
|
|
4773
|
+
ingestionSource,
|
|
4774
|
+
uploadFilePath,
|
|
4775
|
+
searchUiTarget: searchUiTarget(language)
|
|
4776
|
+
};
|
|
4777
|
+
const summaries = [];
|
|
4778
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4779
|
+
let envSearchKey;
|
|
4780
|
+
let envAppIdMismatch = false;
|
|
4781
|
+
if (useCases.includes("search") && appId) {
|
|
4782
|
+
const envAppId = await readEnvVar(
|
|
4783
|
+
worktree,
|
|
4784
|
+
publicAppIdVar(publicEnvVarPrefix)
|
|
4768
4785
|
);
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4786
|
+
if (envAppId === appId) {
|
|
4787
|
+
envSearchKey = await readEnvVar(
|
|
4788
|
+
worktree,
|
|
4789
|
+
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4790
|
+
);
|
|
4791
|
+
} else if (envAppId) {
|
|
4792
|
+
envAppIdMismatch = true;
|
|
4793
|
+
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4794
|
+
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4795
|
+
summaries.push(
|
|
4796
|
+
`\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.`
|
|
4797
|
+
);
|
|
4798
|
+
logger.warn(
|
|
4799
|
+
{ envAppId, appId },
|
|
4800
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4801
|
+
);
|
|
4778
4802
|
}
|
|
4779
|
-
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4803
|
+
}
|
|
4804
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4805
|
+
let agentRuns = 0;
|
|
4806
|
+
let ingestCommand;
|
|
4807
|
+
let ingestScriptRan = false;
|
|
4808
|
+
let ingestRecordCount;
|
|
4809
|
+
let ingestDurationMs;
|
|
4810
|
+
let ingestOutcomeMessage;
|
|
4811
|
+
const ingestKeyAppId = ingestAppId;
|
|
4812
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4813
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4814
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4815
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4816
|
+
})) : void 0;
|
|
4817
|
+
const searchTools = makeToolContext(worktree);
|
|
4818
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4819
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4820
|
+
agentRuns += 1;
|
|
4821
|
+
return runAgent({
|
|
4822
|
+
instructions: buildAgentInstructions(
|
|
4823
|
+
currentUseCase,
|
|
4824
|
+
input,
|
|
4825
|
+
extraInstructions
|
|
4826
|
+
),
|
|
4827
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4828
|
+
outputSchema: implementationOutputSchema,
|
|
4829
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4802
4830
|
});
|
|
4803
4831
|
}
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4832
|
+
async function runVerificationUseCase() {
|
|
4833
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4834
|
+
agentRuns += 1;
|
|
4835
|
+
return runAgent({
|
|
4836
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4837
|
+
tools: toolsForUseCase("verification"),
|
|
4838
|
+
outputSchema: verificationOutputSchema,
|
|
4839
|
+
toolContext: searchTools
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
if (useCases.includes("ingestion")) {
|
|
4843
|
+
let ingestFailureDetail;
|
|
4844
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4845
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4846
|
+
ingestCommand = result.ingestCommand;
|
|
4847
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4848
|
+
const executions = ingestionContext.shell.executions;
|
|
4849
|
+
const {
|
|
4850
|
+
run: ingestRun,
|
|
4851
|
+
attempt: ingestAttempt,
|
|
4852
|
+
recordCount
|
|
4853
|
+
} = ingestOutcome(executions, ingestCommand);
|
|
4854
|
+
ingestScriptRan = ingestRun != null;
|
|
4855
|
+
ingestRecordCount = recordCount;
|
|
4856
|
+
ingestDurationMs = ingestRun?.durationMs;
|
|
4857
|
+
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4858
|
+
summaries.push(
|
|
4859
|
+
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
|
|
4830
4860
|
);
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
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"
|
|
4849
|
-
});
|
|
4850
|
-
break;
|
|
4851
|
-
}
|
|
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}`
|
|
4861
|
+
logger.warn(
|
|
4862
|
+
{ ingestCommand },
|
|
4863
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4856
4864
|
);
|
|
4857
4865
|
}
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
);
|
|
4869
|
-
|
|
4866
|
+
if (ingestScriptRan) {
|
|
4867
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4868
|
+
if (ingestRecordCount != null) {
|
|
4869
|
+
track("AI Wizard Ingest Successful", {
|
|
4870
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4871
|
+
record_count: ingestRecordCount,
|
|
4872
|
+
duration_ms: ingestDurationMs ?? 0
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
} else {
|
|
4876
|
+
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4877
|
+
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4878
|
+
ingestFailureDetail = detail;
|
|
4870
4879
|
summaries.push(
|
|
4871
|
-
|
|
4880
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4881
|
+
${detail}` : ""}`
|
|
4872
4882
|
);
|
|
4873
|
-
} catch (err) {
|
|
4874
|
-
searchKeyError = err.message;
|
|
4875
4883
|
logger.warn(
|
|
4876
|
-
{
|
|
4877
|
-
|
|
4884
|
+
{
|
|
4885
|
+
ingestCommand,
|
|
4886
|
+
reason,
|
|
4887
|
+
approved: ingestAttempt?.approved,
|
|
4888
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4889
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4890
|
+
commandsRun: executions.length
|
|
4891
|
+
},
|
|
4892
|
+
"implement: ingestion script did not complete successfully"
|
|
4878
4893
|
);
|
|
4894
|
+
track("Error", {
|
|
4895
|
+
step: "Push Data",
|
|
4896
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4897
|
+
product_area: "AI Wizard"
|
|
4898
|
+
});
|
|
4879
4899
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
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
|
|
4894
|
-
);
|
|
4895
|
-
if (written.length > 0) {
|
|
4896
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4900
|
+
const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
|
|
4901
|
+
if (ingestCommand) {
|
|
4902
|
+
commandMessages.push(`Ingestion command: ${ingestCommand}`);
|
|
4897
4903
|
}
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
+
await ctx.requestUserInput({
|
|
4905
|
+
prompt: "",
|
|
4906
|
+
promptType: "enterToContinue",
|
|
4907
|
+
options: [],
|
|
4908
|
+
// The wizard never streams command output, so a failed run's tail is the
|
|
4909
|
+
// only place the developer sees why it failed. One message per line:
|
|
4910
|
+
// the panel's height accounting counts a message as one wrapped line
|
|
4911
|
+
// (see Notices.tsx), so an embedded newline overflows it.
|
|
4912
|
+
messages: [
|
|
4913
|
+
ingestOutcomeMessage,
|
|
4914
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4915
|
+
...commandMessages
|
|
4916
|
+
]
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
if (useCases.includes("search")) {
|
|
4920
|
+
let extraInstructions = [];
|
|
4921
|
+
const preSearchFiles = new Set(await listChangedFiles(worktree));
|
|
4922
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4923
|
+
if (attempt > 1) {
|
|
4924
|
+
logger.info(
|
|
4925
|
+
{
|
|
4926
|
+
attempt,
|
|
4927
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4928
|
+
extraInstructions
|
|
4929
|
+
},
|
|
4930
|
+
"implement: retrying search implementation after failed verification"
|
|
4931
|
+
);
|
|
4932
|
+
}
|
|
4933
|
+
const { summary } = await runImplementationUseCase(
|
|
4934
|
+
"search",
|
|
4935
|
+
extraInstructions
|
|
4904
4936
|
);
|
|
4937
|
+
summaries.push(formatSummary("search", summary));
|
|
4938
|
+
const verification = await runVerificationUseCase();
|
|
4939
|
+
summaries.push(formatSummary("verification", verification.summary));
|
|
4940
|
+
if (verification.sufficient) {
|
|
4941
|
+
ctx.setUserInput("implementation", "success");
|
|
4942
|
+
const searchFilesChanged = (await listChangedFiles(worktree)).filter(
|
|
4943
|
+
(file) => !preSearchFiles.has(file)
|
|
4944
|
+
);
|
|
4945
|
+
track("AI Wizard Frontend Component Generated", {
|
|
4946
|
+
filePaths: searchFilesChanged
|
|
4947
|
+
});
|
|
4948
|
+
track("AI Wizard Wired to UI", {
|
|
4949
|
+
location_heuristic: searchLocation ?? "unknown"
|
|
4950
|
+
});
|
|
4951
|
+
break;
|
|
4952
|
+
}
|
|
4953
|
+
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4954
|
+
ctx.setUserInput("implementation", "fail");
|
|
4955
|
+
throw new Error(
|
|
4956
|
+
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4957
|
+
);
|
|
4958
|
+
}
|
|
4959
|
+
extraInstructions = verificationRetryInstructions(verification);
|
|
4905
4960
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4961
|
+
let searchKey;
|
|
4962
|
+
let searchKeyError;
|
|
4963
|
+
if (appId) {
|
|
4964
|
+
try {
|
|
4965
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4966
|
+
targetIndex,
|
|
4967
|
+
appId,
|
|
4968
|
+
envSearchKey
|
|
4969
|
+
);
|
|
4970
|
+
searchKey = resolved2.key;
|
|
4971
|
+
summaries.push(
|
|
4972
|
+
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}.`
|
|
4973
|
+
);
|
|
4974
|
+
} catch (err) {
|
|
4975
|
+
searchKeyError = err.message;
|
|
4976
|
+
logger.warn(
|
|
4977
|
+
{ err: searchKeyError },
|
|
4978
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4911
4981
|
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4982
|
+
finalSearchEnvVars = publicSearchEnvVars(
|
|
4983
|
+
publicEnvVarPrefix,
|
|
4984
|
+
targetIndex,
|
|
4985
|
+
appId,
|
|
4986
|
+
searchKey
|
|
4987
|
+
);
|
|
4988
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4989
|
+
(v) => !v.value.startsWith("<")
|
|
4990
|
+
);
|
|
4991
|
+
if (resolvedSearchEnvVars.length > 0) {
|
|
4992
|
+
const written = await writeSearchEnvValues(
|
|
4993
|
+
worktree,
|
|
4994
|
+
resolvedSearchEnvVars
|
|
4915
4995
|
);
|
|
4916
|
-
|
|
4917
|
-
{
|
|
4918
|
-
|
|
4996
|
+
if (written.length > 0) {
|
|
4997
|
+
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4998
|
+
}
|
|
4999
|
+
const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
|
|
5000
|
+
if (ignored === "added") {
|
|
5001
|
+
summaries.push("Added .env to .gitignore.");
|
|
5002
|
+
} else if (ignored === "tracked") {
|
|
5003
|
+
summaries.push(
|
|
5004
|
+
'\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.'
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
5007
|
+
const stale = [];
|
|
5008
|
+
for (const v of resolvedSearchEnvVars) {
|
|
5009
|
+
if (written.includes(v.name)) continue;
|
|
5010
|
+
const current = await readEnvVar(worktree, v.name);
|
|
5011
|
+
if (current && current !== v.value) stale.push(v);
|
|
5012
|
+
}
|
|
5013
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5014
|
+
summaries.push(
|
|
5015
|
+
`\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.`
|
|
5016
|
+
);
|
|
5017
|
+
logger.warn(
|
|
5018
|
+
{ vars: stale.map((v) => v.name) },
|
|
5019
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
5024
|
+
(v) => v.value.startsWith("<")
|
|
5025
|
+
);
|
|
5026
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
5027
|
+
summaries.push(
|
|
5028
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
|
|
5029
|
+
(searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4919
5030
|
);
|
|
4920
5031
|
}
|
|
5032
|
+
} else {
|
|
5033
|
+
ctx.setUserInput("implementation", "success");
|
|
4921
5034
|
}
|
|
4922
|
-
const
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
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}` : "")
|
|
5035
|
+
const filesChanged = await listChangedFiles(worktree);
|
|
5036
|
+
if (filesChanged.length === 0) {
|
|
5037
|
+
logger.warn(
|
|
5038
|
+
"implement: agent reported success but no files changed in the worktree"
|
|
4928
5039
|
);
|
|
4929
5040
|
}
|
|
4930
|
-
|
|
4931
|
-
|
|
5041
|
+
return {
|
|
5042
|
+
ingestionSource,
|
|
5043
|
+
filesChanged,
|
|
5044
|
+
summary: summaries.join("\n\n"),
|
|
5045
|
+
worktreePath: worktree,
|
|
5046
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
5047
|
+
ingestCommand,
|
|
5048
|
+
ingestScriptRan,
|
|
5049
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
5050
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5051
|
+
} : {},
|
|
5052
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
5053
|
+
};
|
|
5054
|
+
} finally {
|
|
5055
|
+
process.chdir(repoRoot);
|
|
4932
5056
|
}
|
|
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
|
-
};
|
|
4944
5057
|
}
|
|
4945
5058
|
|
|
4946
5059
|
// src/workflows/default.ts
|
|
@@ -5012,7 +5125,10 @@ var defaultWorkflow = {
|
|
|
5012
5125
|
ctx.notify({
|
|
5013
5126
|
messages: ["Building your Algolia search experience\u2026"]
|
|
5014
5127
|
});
|
|
5015
|
-
|
|
5128
|
+
const ingestion2 = ctx.getStepOutput(
|
|
5129
|
+
"ingestion"
|
|
5130
|
+
);
|
|
5131
|
+
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
5016
5132
|
}
|
|
5017
5133
|
}),
|
|
5018
5134
|
defineStep({
|
|
@@ -5027,8 +5143,9 @@ var defaultWorkflow = {
|
|
|
5027
5143
|
"ingestion"
|
|
5028
5144
|
);
|
|
5029
5145
|
return reviewStep(ctx, {
|
|
5030
|
-
//
|
|
5031
|
-
//
|
|
5146
|
+
// The ingestion step already showed the user the exact `ingestCommand`
|
|
5147
|
+
// and worktree path as a notice, so nextSteps must not restate it —
|
|
5148
|
+
// an LLM-paraphrased command risks being wrong.
|
|
5032
5149
|
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."
|
|
5033
5150
|
});
|
|
5034
5151
|
}
|
|
@@ -5075,6 +5192,7 @@ var selectIndex = {
|
|
|
5075
5192
|
selection: "wizard_seed_products"
|
|
5076
5193
|
};
|
|
5077
5194
|
var ingestion = {
|
|
5195
|
+
filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
|
|
5078
5196
|
summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
|
|
5079
5197
|
ingestCommand: "node algolia/ingest.mjs",
|
|
5080
5198
|
ingestScriptRan: true,
|
|
@@ -5086,6 +5204,7 @@ var confirmFramework2 = {
|
|
|
5086
5204
|
frameworks: projectScan2.frameworks
|
|
5087
5205
|
};
|
|
5088
5206
|
var search = {
|
|
5207
|
+
filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
|
|
5089
5208
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
5090
5209
|
ingestionSource: "generated",
|
|
5091
5210
|
searchEnvVars: [
|
|
@@ -5098,7 +5217,7 @@ var review = {
|
|
|
5098
5217
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5099
5218
|
"Added an InstantSearch search experience to the shared header."
|
|
5100
5219
|
],
|
|
5101
|
-
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5220
|
+
reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
|
|
5102
5221
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5103
5222
|
};
|
|
5104
5223
|
var SEEDS = {
|
|
@@ -5215,9 +5334,9 @@ Options:
|
|
|
5215
5334
|
steps pre-filled with test data. Pass with no value to print
|
|
5216
5335
|
the step ids. See CONTRIBUTING.md.
|
|
5217
5336
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5218
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5219
|
-
before starting, so the run behaves like a
|
|
5220
|
-
run. Also drops every API key the wizard has
|
|
5337
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent,
|
|
5338
|
+
worktrees) before starting, so the run behaves like a
|
|
5339
|
+
first-ever run. Also drops every API key the wizard has
|
|
5221
5340
|
stored in your keychain (or, where the platform has none,
|
|
5222
5341
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5223
5342
|
for this project and any other, so later runs create new
|
|
@@ -5257,7 +5376,7 @@ function parseCliArgs(argv) {
|
|
|
5257
5376
|
}
|
|
5258
5377
|
|
|
5259
5378
|
// src/lib/resetState.ts
|
|
5260
|
-
import { readdir as
|
|
5379
|
+
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
5261
5380
|
import { join as join13 } from "node:path";
|
|
5262
5381
|
var KEEP = ["wizard.log"];
|
|
5263
5382
|
async function resetProjectState() {
|
|
@@ -5265,7 +5384,7 @@ async function resetProjectState() {
|
|
|
5265
5384
|
await forgetResolvedKeys();
|
|
5266
5385
|
let entries;
|
|
5267
5386
|
try {
|
|
5268
|
-
entries = await
|
|
5387
|
+
entries = await readdir4(dir);
|
|
5269
5388
|
} catch {
|
|
5270
5389
|
return { dir, removed: [] };
|
|
5271
5390
|
}
|