@algolia/wizard 0.27.0 → 0.28.0

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