@algolia/wizard 0.20.0 → 0.21.0-rc.111.195

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 +318 -430
  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
@@ -251,6 +251,7 @@ var useWizard = create((set, get) => ({
251
251
  _noticeTimer: null,
252
252
  cliOutput: [],
253
253
  targetIndex: null,
254
+ writtenFiles: [],
254
255
  logs: [],
255
256
  error: null,
256
257
  inputReq: null,
@@ -344,6 +345,8 @@ var useWizard = create((set, get) => ({
344
345
  })),
345
346
  clearCliOutput: () => set({ cliOutput: [] }),
346
347
  setTargetIndex: (index) => set({ targetIndex: index }),
348
+ recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
349
+ clearWrittenFiles: () => set({ writtenFiles: [] }),
347
350
  logStart: (kind, name, input) => {
348
351
  const id = nanoid();
349
352
  set((s) => ({
@@ -391,6 +394,7 @@ var useWizard = create((set, get) => ({
391
394
  notices: [],
392
395
  cliOutput: [],
393
396
  targetIndex: null,
397
+ writtenFiles: [],
394
398
  logs: [],
395
399
  error: null,
396
400
  inputReq: null,
@@ -1200,7 +1204,7 @@ var accessItems = [
1200
1204
  {
1201
1205
  tag: "WRITE",
1202
1206
  title: "Code changes",
1203
- description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1207
+ description: "creates & edits files (search UI, config) directly in your branch."
1204
1208
  },
1205
1209
  {
1206
1210
  tag: "EXEC",
@@ -1215,7 +1219,7 @@ var accessItems = [
1215
1219
  {
1216
1220
  tag: "KEY",
1217
1221
  title: "Credentials",
1218
- description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1222
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
1219
1223
  }
1220
1224
  ];
1221
1225
  var neverItems = [
@@ -2496,6 +2500,7 @@ function writeFileTool(ctx) {
2496
2500
  }
2497
2501
  await mkdir3(dirname4(resolved2.target), { recursive: true });
2498
2502
  await writeFile3(resolved2.target, content, "utf8");
2503
+ useWizard.getState().recordWrittenFile(resolved2.target);
2499
2504
  return `Wrote to ${filePath}`;
2500
2505
  } catch (err) {
2501
2506
  return `Error writing ${filePath}: ${err.message}`;
@@ -3268,7 +3273,7 @@ var anthropic = createAnthropic({
3268
3273
  });
3269
3274
  function generateRecordTool(ctx) {
3270
3275
  return tool10({
3271
- 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.",
3276
+ 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.",
3272
3277
  inputSchema: z17.object({
3273
3278
  entityName: z17.string().describe("Name of the entity to generate records for."),
3274
3279
  attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
@@ -3630,7 +3635,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3630
3635
  // package.json
3631
3636
  var package_default = {
3632
3637
  name: "@algolia/wizard",
3633
- version: "0.20.0",
3638
+ version: "0.21.0-rc.111.195",
3634
3639
  description: "Magically implement Algolia functionality in your codebase",
3635
3640
  type: "module",
3636
3641
  engines: {
@@ -3997,11 +4002,10 @@ ${JSON.stringify(s.output, null, 2)}`
3997
4002
  function formatReviewSummary(result) {
3998
4003
  const nextStepLines = result.nextSteps.map((step) => {
3999
4004
  const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
4000
- const isWorktreeCommand = step.includes("/worktrees/");
4001
4005
  return {
4002
4006
  text: `\u2192 ${step}`,
4003
- color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
4004
- bold: isIngestCommand || isWorktreeCommand
4007
+ color: isIngestCommand ? COLORS.brand : void 0,
4008
+ bold: isIngestCommand
4005
4009
  };
4006
4010
  });
4007
4011
  return [
@@ -4036,15 +4040,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4036
4040
 
4037
4041
  // src/actions/implement.ts
4038
4042
  import z29 from "zod";
4039
- import { join as join12 } from "node:path";
4043
+ import { join as join12, relative as relative6 } from "node:path";
4040
4044
 
4041
- // src/lib/worktree.ts
4045
+ // src/lib/git.ts
4042
4046
  import { execFile as execFile2 } from "node:child_process";
4043
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4047
+ import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4044
4048
  import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
4045
4049
  var MAX_BUFFER = 32 * 1024 * 1024;
4046
- var MAX_WIZARD_WORKTREES = 3;
4047
- var WIZARD_BRANCH_PREFIX = "wizard/implement-";
4048
4050
  function git(args) {
4049
4051
  return new Promise((resolve4, reject) => {
4050
4052
  execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
@@ -4067,44 +4069,7 @@ async function assertGitRepoWithHead(repoRoot) {
4067
4069
  );
4068
4070
  }
4069
4071
  }
4070
- async function isWorkingTreeDirty(repoRoot) {
4071
- const out = await git(["-C", repoRoot, "status", "--porcelain"]);
4072
- return out.trim().length > 0;
4073
- }
4074
- async function pruneOldWorktrees(repoRoot) {
4075
- const dir = join10(stateDir(repoRoot), "worktrees");
4076
- const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
4077
- for (const slug of stale) {
4078
- const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
4079
- try {
4080
- await git([
4081
- "-C",
4082
- repoRoot,
4083
- "worktree",
4084
- "remove",
4085
- "--force",
4086
- join10(dir, slug)
4087
- ]);
4088
- await git(["-C", repoRoot, "branch", "-D", branch]);
4089
- } catch (err) {
4090
- logger.warn(
4091
- { branch, err: err.message },
4092
- "createWorktree: failed to prune a stale wizard worktree; continuing"
4093
- );
4094
- }
4095
- }
4096
- }
4097
- async function createWorktree(repoRoot) {
4098
- const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
4099
- const dirSlug = branch.replace(/\//g, "-");
4100
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
4101
- await git(["-C", repoRoot, "worktree", "prune"]);
4102
- await pruneOldWorktrees(repoRoot);
4103
- await mkdir6(dirname7(path), { recursive: true });
4104
- await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
4105
- return { path, branch };
4106
- }
4107
- async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
4072
+ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4108
4073
  const trimmed = sourcePath.trim();
4109
4074
  if (!trimmed) {
4110
4075
  return { ok: false, reason: "no file path was provided" };
@@ -4118,7 +4083,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4118
4083
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4119
4084
  }
4120
4085
  const relPath = join10(ingestDir, basename2(source));
4121
- const dest = join10(worktreePath, relPath);
4086
+ const dest = join10(repoRoot, relPath);
4087
+ if (resolve3(source) === resolve3(dest)) {
4088
+ return { ok: true, relPath };
4089
+ }
4122
4090
  try {
4123
4091
  await mkdir6(dirname7(dest), { recursive: true });
4124
4092
  await copyFile(source, dest);
@@ -4133,10 +4101,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4133
4101
  function hasEnvVar(content, name) {
4134
4102
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
4135
4103
  }
4136
- async function readEnvVar(worktreePath, name) {
4104
+ async function readEnvVar(repoRoot, name) {
4137
4105
  let content;
4138
4106
  try {
4139
- content = await readFile8(join10(worktreePath, ".env"), "utf8");
4107
+ content = await readFile8(join10(repoRoot, ".env"), "utf8");
4140
4108
  } catch (err) {
4141
4109
  if (err.code !== "ENOENT") throw err;
4142
4110
  return void 0;
@@ -4150,8 +4118,8 @@ async function readEnvVar(worktreePath, name) {
4150
4118
  if (!value || value.startsWith("<")) return void 0;
4151
4119
  return value;
4152
4120
  }
4153
- async function writeSearchEnvValues(worktreePath, vars) {
4154
- const target = join10(worktreePath, ".env");
4121
+ async function writeSearchEnvValues(repoRoot, vars) {
4122
+ const target = join10(repoRoot, ".env");
4155
4123
  let existing = "";
4156
4124
  try {
4157
4125
  existing = await readFile8(target, "utf8");
@@ -4166,61 +4134,27 @@ async function writeSearchEnvValues(worktreePath, vars) {
4166
4134
  await writeFile7(target, existing + prefix + lines, "utf8");
4167
4135
  return missing.map((v) => v.name);
4168
4136
  }
4169
- async function listChangedFiles(worktreePath) {
4170
- const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
4171
- const entries = raw.split("\0");
4172
- const files = [];
4173
- for (let i = 0; i < entries.length; i += 1) {
4174
- const entry = entries[i];
4175
- if (!entry) continue;
4176
- files.push(entry.slice(3));
4177
- if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
4178
- }
4179
- return files;
4180
- }
4181
4137
  function normalizeFindingPaths(findings) {
4182
4138
  return {
4183
4139
  ...findings,
4184
4140
  ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
4185
4141
  ...e,
4186
- paths: e.paths.map(toWorktreeRelative)
4142
+ paths: e.paths.map(toRootRelative)
4187
4143
  })),
4188
4144
  searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
4189
4145
  confirmedEntities: findings.confirmedEntities?.map((e) => ({
4190
4146
  ...e,
4191
- paths: e.paths.map(toWorktreeRelative)
4147
+ paths: e.paths.map(toRootRelative)
4192
4148
  }))
4193
4149
  };
4194
4150
  }
4195
4151
  function normalizeSearchLocation(path) {
4196
- const normalized = path ? toWorktreeRelative(path).trim() : "";
4152
+ const normalized = path ? toRootRelative(path).trim() : "";
4197
4153
  return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
4198
4154
  }
4199
- function toWorktreeRelative(p) {
4155
+ function toRootRelative(p) {
4200
4156
  return p.replace(/^\/+/, "");
4201
4157
  }
4202
- async function confirmDirtyWorkingTree(ctx, repoRoot) {
4203
- const MAX_LISTED_DIRTY_FILES = 10;
4204
- const dirty = await listChangedFiles(repoRoot);
4205
- const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
4206
- const overflow = dirty.length - shown.length;
4207
- const answer = await ctx.requestUserInput({
4208
- prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
4209
- promptType: "acceptReject",
4210
- options: [],
4211
- messages: [
4212
- `${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
4213
- ...shown.map((file) => ` \u2022 ${file}`),
4214
- ...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
4215
- "Commit or stash them first to include them in the implementation."
4216
- ]
4217
- });
4218
- if (answer !== true) {
4219
- throw new Error(
4220
- "implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
4221
- );
4222
- }
4223
- }
4224
4158
 
4225
4159
  // src/lib/algoliaDocs.ts
4226
4160
  import { readFileSync, readdirSync, existsSync } from "node:fs";
@@ -4280,11 +4214,6 @@ function getFrameworkSpecificDoc(frameworks) {
4280
4214
  return loadAlgoliaDoc("js");
4281
4215
  }
4282
4216
 
4283
- // src/lib/shell.ts
4284
- function shellQuote(value) {
4285
- return "'" + value.replace(/'/g, "'\\''") + "'";
4286
- }
4287
-
4288
4217
  // src/actions/resolveEnvVarPrefix.ts
4289
4218
  import z28 from "zod";
4290
4219
  var resolveEnvVarPrefixSchema = z28.object({
@@ -4304,9 +4233,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
4304
4233
 
4305
4234
  // src/actions/implement.ts
4306
4235
  var implementSchema = z29.object({
4307
- filesChanged: z29.array(z29.string()),
4308
4236
  summary: z29.string(),
4309
- worktreePath: z29.string().optional(),
4310
4237
  ingestCommand: z29.string().optional(),
4311
4238
  ingestScriptRan: z29.boolean().optional(),
4312
4239
  ingestRecordCount: z29.number().optional(),
@@ -4359,8 +4286,6 @@ function frameworksForDoc(language) {
4359
4286
  }
4360
4287
  function baseInstructions(input) {
4361
4288
  return [
4362
- // Agents have renamed this (e.g. appending the project name), which the
4363
- // index-scoped keys then reject with a 403.
4364
4289
  `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
4365
4290
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4366
4291
  "Make minimal, idiomatic changes; do not touch unrelated code.",
@@ -4378,14 +4303,14 @@ function sourceSpecificInstructions(input) {
4378
4303
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
4379
4304
  ],
4380
4305
  fileUpload: [
4381
- `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
4306
+ `Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
4382
4307
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
4383
4308
  "Map parsed columns/fields to the confirmed entity attributes.",
4384
4309
  "Never fabricate, hardcode, or substitute a different file."
4385
4310
  ],
4386
4311
  generated: [
4387
4312
  "No real data source exists; use sample records for each confirmed entity.",
4388
- "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.",
4313
+ "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.",
4389
4314
  "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.",
4390
4315
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4391
4316
  ]
@@ -4434,14 +4359,11 @@ function searchInstructions(input) {
4434
4359
  "If a search box already exists, replace it with yours.",
4435
4360
  `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.`,
4436
4361
  "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.",
4437
- // The key is provisioned only after verification passes, so the agent never
4438
- // sees one. It must also leave .env alone: the wizard reads that file to
4439
- // decide whether a key already exists, and an agent-invented value there
4440
- // would be reused as if it were real.
4362
+ // The key is provisioned only after verification passes, and the wizard
4363
+ // reads .env to decide whether a key already exists an agent-invented
4364
+ // value there would be reused as if it were real.
4441
4365
  `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.`,
4442
- // Not the agent's to rename: the wizard writes these exact names into
4443
- // ".env" right after this step, so a renamed prefix would leave the code
4444
- // reading a var the wizard never wrote.
4366
+ // The wizard writes these exact names into .env right after this step.
4445
4367
  `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4446
4368
  "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4447
4369
  "Match the styles of the application as closely as possible.",
@@ -4450,10 +4372,10 @@ function searchInstructions(input) {
4450
4372
  }
4451
4373
  function verificationInstructions(input) {
4452
4374
  return [
4453
- "Verify the Algolia implementation changes in the current worktree.",
4375
+ "Verify the Algolia implementation changes.",
4454
4376
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
4455
4377
  "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.",
4456
- "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.",
4378
+ "If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
4457
4379
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4458
4380
  "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.",
4459
4381
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
@@ -4548,11 +4470,11 @@ function ingestFailure(attempt, executions) {
4548
4470
  }
4549
4471
  return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
4550
4472
  }
4551
- function makeToolContext(worktree, env = async () => ({})) {
4473
+ function makeToolContext(root, env = async () => ({})) {
4552
4474
  return createToolContext(
4553
4475
  DEFAULT_TOOL_LIMITS,
4554
- worktree,
4555
- createShellContext({ env, approve: storeApproval(worktree) })
4476
+ root,
4477
+ createShellContext({ env, approve: storeApproval(root) })
4556
4478
  );
4557
4479
  }
4558
4480
  function verificationRetryInstructions(verification) {
@@ -4560,7 +4482,7 @@ function verificationRetryInstructions(verification) {
4560
4482
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
4561
4483
  ];
4562
4484
  }
4563
- async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
4485
+ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4564
4486
  const repoRoot = process.cwd();
4565
4487
  const scan = ctx.getStepOutput("project-scan");
4566
4488
  const entities = ctx.getStepOutput(
@@ -4641,9 +4563,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4641
4563
  const targetIndex = selected?.selection;
4642
4564
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4643
4565
  await assertGitRepoWithHead(repoRoot);
4644
- if (await isWorkingTreeDirty(repoRoot)) {
4645
- await confirmDirtyWorkingTree(ctx, repoRoot);
4646
- }
4647
4566
  const normalized = normalizeFindingPaths(findings);
4648
4567
  const confirmed2 = normalized.confirmedEntities;
4649
4568
  const searchLocation = normalized.searchImplementationAnalysis;
@@ -4655,328 +4574,303 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4655
4574
  if (useCases.includes("ingestion")) {
4656
4575
  ingestAppId = appId ?? (await requireApplication()).id;
4657
4576
  }
4658
- const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
4659
- try {
4660
- process.chdir(worktree);
4661
- let uploadFilePath;
4662
- let uploadWarning;
4663
- if (ingestionSource === "fileUpload") {
4664
- const copied = await copyUploadIntoWorktree(
4577
+ let uploadFilePath;
4578
+ let uploadWarning;
4579
+ if (ingestionSource === "fileUpload") {
4580
+ const copied = await copyUploadIntoProject(
4581
+ repoRoot,
4582
+ INGEST_DIR,
4583
+ uploadSourcePath ?? ""
4584
+ );
4585
+ if (copied.ok) {
4586
+ uploadFilePath = copied.relPath;
4587
+ } else {
4588
+ ingestionSource = "generated";
4589
+ uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4590
+ logger.warn(
4591
+ { reason: copied.reason },
4592
+ "implement: file upload unavailable; falling back to generated sample records"
4593
+ );
4594
+ }
4595
+ }
4596
+ const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
4597
+ const input = {
4598
+ findings: normalized,
4599
+ confirmed: confirmed2,
4600
+ searchLocation,
4601
+ targetIndex,
4602
+ language,
4603
+ publicEnvVarPrefix,
4604
+ appId,
4605
+ searchEnvVars: publicSearchEnvVars(publicEnvVarPrefix, targetIndex, appId),
4606
+ ingestDir: INGEST_DIR,
4607
+ ingestionSource,
4608
+ uploadFilePath,
4609
+ searchUiTarget: searchUiTarget(language)
4610
+ };
4611
+ const summaries = [];
4612
+ if (uploadWarning) summaries.push(uploadWarning);
4613
+ let envSearchKey;
4614
+ let envAppIdMismatch = false;
4615
+ if (useCases.includes("search") && appId) {
4616
+ const envAppId = await readEnvVar(
4617
+ repoRoot,
4618
+ publicAppIdVar(publicEnvVarPrefix)
4619
+ );
4620
+ if (envAppId === appId) {
4621
+ envSearchKey = await readEnvVar(
4665
4622
  repoRoot,
4666
- worktree,
4667
- INGEST_DIR,
4668
- uploadSourcePath ?? ""
4623
+ publicSearchKeyVar(publicEnvVarPrefix)
4624
+ );
4625
+ } else if (envAppId) {
4626
+ envAppIdMismatch = true;
4627
+ const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
4628
+ const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
4629
+ summaries.push(
4630
+ `\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.`
4631
+ );
4632
+ logger.warn(
4633
+ { envAppId, appId },
4634
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4669
4635
  );
4670
- if (copied.ok) {
4671
- uploadFilePath = copied.relPath;
4672
- } else {
4673
- ingestionSource = "generated";
4674
- uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4675
- logger.warn(
4676
- { reason: copied.reason },
4677
- "implement: file upload unavailable; falling back to generated sample records"
4678
- );
4679
- }
4680
4636
  }
4681
- const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
4682
- const input = {
4683
- findings: normalized,
4684
- confirmed: confirmed2,
4685
- searchLocation,
4686
- targetIndex,
4687
- language,
4688
- publicEnvVarPrefix,
4689
- appId,
4690
- searchEnvVars: publicSearchEnvVars(
4691
- publicEnvVarPrefix,
4692
- targetIndex,
4693
- appId
4637
+ }
4638
+ let finalSearchEnvVars = input.searchEnvVars;
4639
+ let agentRuns = 0;
4640
+ let ingestCommand;
4641
+ let ingestScriptRan = false;
4642
+ let ingestRecordCount;
4643
+ let ingestDurationMs;
4644
+ let ingestOutcomeMessage;
4645
+ const ingestKeyAppId = ingestAppId;
4646
+ const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
4647
+ [APP_ID_VAR]: ingestKeyAppId,
4648
+ [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4649
+ [INDEX_NAME_VAR]: targetIndex
4650
+ })) : void 0;
4651
+ const searchTools = makeToolContext(repoRoot);
4652
+ async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4653
+ if (agentRuns > 0) ctx.recordStepExecution();
4654
+ agentRuns += 1;
4655
+ return runAgent({
4656
+ instructions: buildAgentInstructions(
4657
+ currentUseCase,
4658
+ input,
4659
+ extraInstructions
4694
4660
  ),
4695
- ingestDir: INGEST_DIR,
4696
- ingestionSource,
4697
- uploadFilePath,
4698
- searchUiTarget: searchUiTarget(language)
4699
- };
4700
- const summaries = [];
4701
- if (uploadWarning) summaries.push(uploadWarning);
4702
- let envSearchKey;
4703
- let envAppIdMismatch = false;
4704
- if (useCases.includes("search") && appId) {
4705
- const envAppId = await readEnvVar(
4706
- worktree,
4707
- publicAppIdVar(publicEnvVarPrefix)
4661
+ tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4662
+ outputSchema: implementationOutputSchema,
4663
+ toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4664
+ });
4665
+ }
4666
+ async function runVerificationUseCase() {
4667
+ if (agentRuns > 0) ctx.recordStepExecution();
4668
+ agentRuns += 1;
4669
+ return runAgent({
4670
+ instructions: buildAgentInstructions("verification", input),
4671
+ tools: toolsForUseCase("verification"),
4672
+ outputSchema: verificationOutputSchema,
4673
+ toolContext: searchTools
4674
+ });
4675
+ }
4676
+ if (useCases.includes("ingestion")) {
4677
+ let ingestFailureDetail;
4678
+ const result = await runImplementationUseCase("ingestion");
4679
+ summaries.push(formatSummary("ingestion", result.summary));
4680
+ ingestCommand = result.ingestCommand;
4681
+ const ingestionContext = ingestionTools ?? searchTools;
4682
+ const executions = ingestionContext.shell.executions;
4683
+ const {
4684
+ run: ingestRun,
4685
+ attempt: ingestAttempt,
4686
+ recordCount
4687
+ } = ingestOutcome(executions, ingestCommand);
4688
+ ingestScriptRan = ingestRun != null;
4689
+ ingestRecordCount = recordCount;
4690
+ ingestDurationMs = ingestRun?.durationMs;
4691
+ if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4692
+ summaries.push(
4693
+ "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
4694
+ );
4695
+ logger.warn(
4696
+ { ingestCommand },
4697
+ "implement: ingestion ran without a reviewScript call"
4708
4698
  );
4709
- if (envAppId === appId) {
4710
- envSearchKey = await readEnvVar(
4711
- worktree,
4712
- publicSearchKeyVar(publicEnvVarPrefix)
4713
- );
4714
- } else if (envAppId) {
4715
- envAppIdMismatch = true;
4716
- const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
4717
- const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
4718
- summaries.push(
4719
- `\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.`
4720
- );
4721
- logger.warn(
4722
- { envAppId, appId },
4723
- "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4724
- );
4725
- }
4726
- }
4727
- let finalSearchEnvVars = input.searchEnvVars;
4728
- let agentRuns = 0;
4729
- let ingestCommand;
4730
- let ingestScriptRan = false;
4731
- let ingestRecordCount;
4732
- let ingestDurationMs;
4733
- let ingestOutcomeMessage;
4734
- const ingestKeyAppId = ingestAppId;
4735
- const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
4736
- [APP_ID_VAR]: ingestKeyAppId,
4737
- [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4738
- [INDEX_NAME_VAR]: targetIndex
4739
- })) : void 0;
4740
- const searchTools = makeToolContext(worktree);
4741
- async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4742
- if (agentRuns > 0) ctx.recordStepExecution();
4743
- agentRuns += 1;
4744
- return runAgent({
4745
- instructions: buildAgentInstructions(
4746
- currentUseCase,
4747
- input,
4748
- extraInstructions
4749
- ),
4750
- tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4751
- outputSchema: implementationOutputSchema,
4752
- toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4753
- });
4754
- }
4755
- async function runVerificationUseCase() {
4756
- if (agentRuns > 0) ctx.recordStepExecution();
4757
- agentRuns += 1;
4758
- return runAgent({
4759
- instructions: buildAgentInstructions("verification", input),
4760
- tools: toolsForUseCase("verification"),
4761
- outputSchema: verificationOutputSchema,
4762
- toolContext: searchTools
4763
- });
4764
4699
  }
4765
- if (useCases.includes("ingestion")) {
4766
- let ingestFailureDetail;
4767
- const result = await runImplementationUseCase("ingestion");
4768
- summaries.push(formatSummary("ingestion", result.summary));
4769
- ingestCommand = result.ingestCommand;
4770
- const ingestionContext = ingestionTools ?? searchTools;
4771
- const executions = ingestionContext.shell.executions;
4772
- const {
4773
- run: ingestRun,
4774
- attempt: ingestAttempt,
4775
- recordCount
4776
- } = ingestOutcome(executions, ingestCommand);
4777
- ingestScriptRan = ingestRun != null;
4778
- ingestRecordCount = recordCount;
4779
- ingestDurationMs = ingestRun?.durationMs;
4780
- if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4781
- summaries.push(
4782
- "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
4783
- );
4784
- logger.warn(
4785
- { ingestCommand },
4786
- "implement: ingestion ran without a reviewScript call"
4787
- );
4700
+ if (ingestScriptRan) {
4701
+ ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4702
+ if (ingestRecordCount != null) {
4703
+ track("AI Wizard Ingest Successful", {
4704
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4705
+ record_count: ingestRecordCount,
4706
+ duration_ms: ingestDurationMs ?? 0
4707
+ });
4788
4708
  }
4789
- if (ingestScriptRan) {
4790
- ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4791
- if (ingestRecordCount != null) {
4792
- track("AI Wizard Ingest Successful", {
4793
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4794
- record_count: ingestRecordCount,
4795
- duration_ms: ingestDurationMs ?? 0
4796
- });
4797
- }
4798
- } else {
4799
- const { reason, detail } = ingestFailure(ingestAttempt, executions);
4800
- ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4801
- ingestFailureDetail = detail;
4802
- summaries.push(
4803
- `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4709
+ } else {
4710
+ const { reason, detail } = ingestFailure(ingestAttempt, executions);
4711
+ ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4712
+ ingestFailureDetail = detail;
4713
+ summaries.push(
4714
+ `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4804
4715
  ${detail}` : ""}`
4805
- );
4806
- logger.warn(
4716
+ );
4717
+ logger.warn(
4718
+ {
4719
+ ingestCommand,
4720
+ reason,
4721
+ approved: ingestAttempt?.approved,
4722
+ exitCode: ingestAttempt?.exitCode,
4723
+ timedOut: ingestAttempt?.timedOut,
4724
+ commandsRun: executions.length
4725
+ },
4726
+ "implement: ingestion script did not complete successfully"
4727
+ );
4728
+ track("Error", {
4729
+ step: "Push Data",
4730
+ error: `ingestion did not complete: ${reason}`,
4731
+ product_area: "AI Wizard"
4732
+ });
4733
+ }
4734
+ const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
4735
+ await ctx.requestUserInput({
4736
+ prompt: "",
4737
+ promptType: "enterToContinue",
4738
+ options: [],
4739
+ // One message per line: Notices.tsx counts a message as one wrapped
4740
+ // line, so an embedded newline overflows the panel's height accounting.
4741
+ messages: [
4742
+ ingestOutcomeMessage,
4743
+ ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
4744
+ ...commandMessages
4745
+ ]
4746
+ });
4747
+ }
4748
+ if (useCases.includes("search")) {
4749
+ let extraInstructions = [];
4750
+ useWizard.getState().clearWrittenFiles();
4751
+ for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
4752
+ if (attempt > 1) {
4753
+ logger.info(
4807
4754
  {
4808
- ingestCommand,
4809
- reason,
4810
- approved: ingestAttempt?.approved,
4811
- exitCode: ingestAttempt?.exitCode,
4812
- timedOut: ingestAttempt?.timedOut,
4813
- commandsRun: executions.length
4755
+ attempt,
4756
+ maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
4757
+ extraInstructions
4814
4758
  },
4815
- "implement: ingestion script did not complete successfully"
4759
+ "implement: retrying search implementation after failed verification"
4816
4760
  );
4817
- track("Error", {
4818
- step: "Push Data",
4819
- error: `ingestion did not complete: ${reason}`,
4820
- product_area: "AI Wizard"
4761
+ }
4762
+ const { summary } = await runImplementationUseCase(
4763
+ "search",
4764
+ extraInstructions
4765
+ );
4766
+ summaries.push(formatSummary("search", summary));
4767
+ const verification = await runVerificationUseCase();
4768
+ summaries.push(formatSummary("verification", verification.summary));
4769
+ if (verification.sufficient) {
4770
+ ctx.setUserInput("implementation", "success");
4771
+ const searchFilesChanged = [
4772
+ ...new Set(useWizard.getState().writtenFiles)
4773
+ ].map((file) => relative6(repoRoot, file));
4774
+ track("AI Wizard Frontend Component Generated", {
4775
+ filePaths: searchFilesChanged
4776
+ });
4777
+ track("AI Wizard Wired to UI", {
4778
+ location_heuristic: searchLocation ?? "unknown"
4821
4779
  });
4780
+ break;
4822
4781
  }
4823
- const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4824
- if (ingestCommand) {
4825
- commandMessages.push(`Ingestion command: ${ingestCommand}`);
4782
+ if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
4783
+ ctx.setUserInput("implementation", "fail");
4784
+ throw new Error(
4785
+ `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
4786
+ );
4826
4787
  }
4827
- await ctx.requestUserInput({
4828
- prompt: "",
4829
- promptType: "enterToContinue",
4830
- options: [],
4831
- // The wizard never streams command output, so a failed run's tail is the
4832
- // only place the developer sees why it failed. One message per line:
4833
- // the panel's height accounting counts a message as one wrapped line
4834
- // (see Notices.tsx), so an embedded newline overflows it.
4835
- messages: [
4836
- ingestOutcomeMessage,
4837
- ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
4838
- ...commandMessages
4839
- ]
4840
- });
4788
+ extraInstructions = verificationRetryInstructions(verification);
4841
4789
  }
4842
- if (useCases.includes("search")) {
4843
- let extraInstructions = [];
4844
- const preSearchFiles = new Set(await listChangedFiles(worktree));
4845
- for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
4846
- if (attempt > 1) {
4847
- logger.info(
4848
- {
4849
- attempt,
4850
- maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
4851
- extraInstructions
4852
- },
4853
- "implement: retrying search implementation after failed verification"
4854
- );
4855
- }
4856
- const { summary } = await runImplementationUseCase(
4857
- "search",
4858
- extraInstructions
4790
+ let searchKey;
4791
+ let searchKeyError;
4792
+ if (appId) {
4793
+ try {
4794
+ const resolved2 = await resolveSearchOnlyKey(
4795
+ targetIndex,
4796
+ appId,
4797
+ envSearchKey
4798
+ );
4799
+ searchKey = resolved2.key;
4800
+ summaries.push(
4801
+ 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}.`
4802
+ );
4803
+ } catch (err) {
4804
+ searchKeyError = err.message;
4805
+ logger.warn(
4806
+ { err: searchKeyError },
4807
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4859
4808
  );
4860
- summaries.push(formatSummary("search", summary));
4861
- const verification = await runVerificationUseCase();
4862
- summaries.push(formatSummary("verification", verification.summary));
4863
- if (verification.sufficient) {
4864
- ctx.setUserInput("implementation", "success");
4865
- const searchFilesChanged = (await listChangedFiles(worktree)).filter(
4866
- (file) => !preSearchFiles.has(file)
4867
- );
4868
- track("AI Wizard Frontend Component Generated", {
4869
- filePaths: searchFilesChanged
4870
- });
4871
- track("AI Wizard Wired to UI", {
4872
- location_heuristic: searchLocation ?? "unknown"
4873
- });
4874
- break;
4875
- }
4876
- if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
4877
- ctx.setUserInput("implementation", "fail");
4878
- throw new Error(
4879
- `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
4880
- );
4881
- }
4882
- extraInstructions = verificationRetryInstructions(verification);
4883
- }
4884
- let searchKey;
4885
- let searchKeyError;
4886
- if (appId) {
4887
- try {
4888
- const resolved2 = await resolveSearchOnlyKey(
4889
- targetIndex,
4890
- appId,
4891
- envSearchKey
4892
- );
4893
- searchKey = resolved2.key;
4894
- summaries.push(
4895
- 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}.`
4896
- );
4897
- } catch (err) {
4898
- searchKeyError = err.message;
4899
- logger.warn(
4900
- { err: searchKeyError },
4901
- "implement: could not provision a search-only API key; the .env value stays a placeholder"
4902
- );
4903
- }
4904
4809
  }
4905
- finalSearchEnvVars = publicSearchEnvVars(
4906
- publicEnvVarPrefix,
4907
- targetIndex,
4908
- appId,
4909
- searchKey
4910
- );
4911
- const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4912
- (v) => !v.value.startsWith("<")
4810
+ }
4811
+ finalSearchEnvVars = publicSearchEnvVars(
4812
+ publicEnvVarPrefix,
4813
+ targetIndex,
4814
+ appId,
4815
+ searchKey
4816
+ );
4817
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4818
+ (v) => !v.value.startsWith("<")
4819
+ );
4820
+ if (resolvedSearchEnvVars.length > 0) {
4821
+ const written = await writeSearchEnvValues(
4822
+ repoRoot,
4823
+ resolvedSearchEnvVars
4913
4824
  );
4914
- if (resolvedSearchEnvVars.length > 0) {
4915
- const written = await writeSearchEnvValues(
4916
- worktree,
4917
- resolvedSearchEnvVars
4825
+ if (written.length > 0) {
4826
+ summaries.push(`Wrote ${written.join(", ")} to .env.`);
4827
+ }
4828
+ const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
4829
+ if (ignored === "added") {
4830
+ summaries.push("Added .env to .gitignore.");
4831
+ } else if (ignored === "tracked") {
4832
+ summaries.push(
4833
+ '\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.'
4918
4834
  );
4919
- if (written.length > 0) {
4920
- summaries.push(`Wrote ${written.join(", ")} to .env.`);
4921
- }
4922
- const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
4923
- if (ignored === "added") {
4924
- summaries.push("Added .env to .gitignore.");
4925
- } else if (ignored === "tracked") {
4926
- summaries.push(
4927
- '\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.'
4928
- );
4929
- }
4930
- const stale = [];
4931
- for (const v of resolvedSearchEnvVars) {
4932
- if (written.includes(v.name)) continue;
4933
- const current = await readEnvVar(worktree, v.name);
4934
- if (current && current !== v.value) stale.push(v);
4935
- }
4936
- if (stale.length > 0 && !envAppIdMismatch) {
4937
- summaries.push(
4938
- `\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.`
4939
- );
4940
- logger.warn(
4941
- { vars: stale.map((v) => v.name) },
4942
- "implement: .env holds different values for the resolved search credentials; not overwriting them"
4943
- );
4944
- }
4945
4835
  }
4946
- const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4947
- (v) => v.value.startsWith("<")
4948
- );
4949
- if (unresolvedSearchEnvVars.length > 0) {
4836
+ const stale = [];
4837
+ for (const v of resolvedSearchEnvVars) {
4838
+ if (written.includes(v.name)) continue;
4839
+ const current = await readEnvVar(repoRoot, v.name);
4840
+ if (current && current !== v.value) stale.push(v);
4841
+ }
4842
+ if (stale.length > 0 && !envAppIdMismatch) {
4950
4843
  summaries.push(
4951
- `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.
4952
- (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4844
+ `\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.`
4845
+ );
4846
+ logger.warn(
4847
+ { vars: stale.map((v) => v.name) },
4848
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4953
4849
  );
4954
4850
  }
4955
- } else {
4956
- ctx.setUserInput("implementation", "success");
4957
4851
  }
4958
- const filesChanged = await listChangedFiles(worktree);
4959
- if (filesChanged.length === 0) {
4960
- logger.warn(
4961
- "implement: agent reported success but no files changed in the worktree"
4852
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4853
+ (v) => v.value.startsWith("<")
4854
+ );
4855
+ if (unresolvedSearchEnvVars.length > 0) {
4856
+ summaries.push(
4857
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4962
4858
  );
4963
4859
  }
4964
- return {
4965
- ingestionSource,
4966
- filesChanged,
4967
- summary: summaries.join("\n\n"),
4968
- worktreePath: worktree,
4969
- ...useCases.includes("ingestion") && ingestCommand ? {
4970
- ingestCommand,
4971
- ingestScriptRan,
4972
- ...ingestRecordCount != null ? { ingestRecordCount } : {},
4973
- ...ingestDurationMs != null ? { ingestDurationMs } : {}
4974
- } : {},
4975
- ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4976
- };
4977
- } finally {
4978
- process.chdir(repoRoot);
4860
+ } else {
4861
+ ctx.setUserInput("implementation", "success");
4979
4862
  }
4863
+ return {
4864
+ ingestionSource,
4865
+ summary: summaries.join("\n\n"),
4866
+ ...useCases.includes("ingestion") && ingestCommand ? {
4867
+ ingestCommand,
4868
+ ingestScriptRan,
4869
+ ...ingestRecordCount != null ? { ingestRecordCount } : {},
4870
+ ...ingestDurationMs != null ? { ingestDurationMs } : {}
4871
+ } : {},
4872
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4873
+ };
4980
4874
  }
4981
4875
 
4982
4876
  // src/workflows/default.ts
@@ -5048,10 +4942,7 @@ var defaultWorkflow = {
5048
4942
  ctx.notify({
5049
4943
  messages: ["Building your Algolia search experience\u2026"]
5050
4944
  });
5051
- const ingestion2 = ctx.getStepOutput(
5052
- "ingestion"
5053
- );
5054
- return implement(ctx, ["search"], ingestion2?.worktreePath);
4945
+ return implement(ctx, ["search"]);
5055
4946
  }
5056
4947
  }),
5057
4948
  defineStep({
@@ -5066,9 +4957,8 @@ var defaultWorkflow = {
5066
4957
  "ingestion"
5067
4958
  );
5068
4959
  return reviewStep(ctx, {
5069
- // The ingestion step already showed the user the exact `ingestCommand`
5070
- // and worktree path as a notice, so nextSteps must not restate it —
5071
- // an LLM-paraphrased command risks being wrong.
4960
+ // ingestCommand was already shown verbatim as a notice; an
4961
+ // LLM-paraphrased restatement in nextSteps risks being wrong.
5072
4962
  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."
5073
4963
  });
5074
4964
  }
@@ -5115,7 +5005,6 @@ var selectIndex = {
5115
5005
  selection: "wizard_seed_products"
5116
5006
  };
5117
5007
  var ingestion = {
5118
- filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
5119
5008
  summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
5120
5009
  ingestCommand: "node algolia/ingest.mjs",
5121
5010
  ingestScriptRan: true,
@@ -5127,7 +5016,6 @@ var confirmFramework2 = {
5127
5016
  frameworks: projectScan2.frameworks
5128
5017
  };
5129
5018
  var search = {
5130
- filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
5131
5019
  summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
5132
5020
  ingestionSource: "generated",
5133
5021
  searchEnvVars: [
@@ -5140,7 +5028,7 @@ var review = {
5140
5028
  "Ingested 25 generated Product records into wizard_seed_products.",
5141
5029
  "Added an InstantSearch search experience to the shared header."
5142
5030
  ],
5143
- reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
5031
+ reviewPrompt: "Review the Algolia ingestion and search changes.",
5144
5032
  nextSteps: ["Point the ingestion script at your real product data."]
5145
5033
  };
5146
5034
  var SEEDS = {
@@ -5257,9 +5145,9 @@ Options:
5257
5145
  steps pre-filled with test data. Pass with no value to print
5258
5146
  the step ids. See CONTRIBUTING.md.
5259
5147
  --no-telemetry Send no telemetry or analytics for this run.
5260
- --reset-on-run Wipe this project's wizard state (run state, AI consent,
5261
- worktrees) before starting, so the run behaves like a
5262
- first-ever run. Also drops every API key the wizard has
5148
+ --reset-on-run Wipe this project's wizard state (run state, AI consent)
5149
+ before starting, so the run behaves like a first-ever
5150
+ run. Also drops every API key the wizard has
5263
5151
  stored in your keychain (or, where the platform has none,
5264
5152
  the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
5265
5153
  for this project and any other, so later runs create new
@@ -5299,7 +5187,7 @@ function parseCliArgs(argv) {
5299
5187
  }
5300
5188
 
5301
5189
  // src/lib/resetState.ts
5302
- import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
5190
+ import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
5303
5191
  import { join as join13 } from "node:path";
5304
5192
  var KEEP = ["wizard.log"];
5305
5193
  async function resetProjectState() {
@@ -5307,7 +5195,7 @@ async function resetProjectState() {
5307
5195
  await forgetResolvedKeys();
5308
5196
  let entries;
5309
5197
  try {
5310
- entries = await readdir4(dir);
5198
+ entries = await readdir3(dir);
5311
5199
  } catch {
5312
5200
  return { dir, removed: [] };
5313
5201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.20.0",
3
+ "version": "0.21.0-rc.111.195",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {