@algolia/wizard 0.18.0 → 0.19.0-rc.111.186

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 +1 -1
  2. package/dist/main.js +314 -383
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,7 +23,7 @@ npx @algolia/wizard --help
23
23
  | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
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
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. |
26
+ | `--reset-on-run` | Wipe this project's wizard state (run state, AI-changes consent) before starting, so the run behaves like a first-ever run. Credentials are untouched. |
27
27
  | `-h`, `--help` | Print usage. |
28
28
 
29
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).
package/dist/main.js CHANGED
@@ -1200,7 +1200,7 @@ var accessItems = [
1200
1200
  {
1201
1201
  tag: "WRITE",
1202
1202
  title: "Code changes",
1203
- description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1203
+ description: "creates & edits files (search UI, config) directly in your branch. You are asked to confirm first if it has uncommitted changes."
1204
1204
  },
1205
1205
  {
1206
1206
  tag: "EXEC",
@@ -1215,7 +1215,7 @@ var accessItems = [
1215
1215
  {
1216
1216
  tag: "KEY",
1217
1217
  title: "Credentials",
1218
- description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1218
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
1219
1219
  }
1220
1220
  ];
1221
1221
  var neverItems = [
@@ -3246,7 +3246,7 @@ var anthropic = createAnthropic({
3246
3246
  });
3247
3247
  function generateRecordTool(ctx) {
3248
3248
  return tool10({
3249
- 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.",
3249
+ 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.",
3250
3250
  inputSchema: z17.object({
3251
3251
  entityName: z17.string().describe("Name of the entity to generate records for."),
3252
3252
  attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
@@ -3604,7 +3604,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3604
3604
  // package.json
3605
3605
  var package_default = {
3606
3606
  name: "@algolia/wizard",
3607
- version: "0.18.0",
3607
+ version: "0.19.0-rc.111.186",
3608
3608
  description: "Magically implement Algolia functionality in your codebase",
3609
3609
  type: "module",
3610
3610
  engines: {
@@ -3971,11 +3971,10 @@ ${JSON.stringify(s.output, null, 2)}`
3971
3971
  function formatReviewSummary(result) {
3972
3972
  const nextStepLines = result.nextSteps.map((step) => {
3973
3973
  const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3974
- const isWorktreeCommand = step.includes("/worktrees/");
3975
3974
  return {
3976
3975
  text: `\u2192 ${step}`,
3977
- color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
3978
- bold: isIngestCommand || isWorktreeCommand
3976
+ color: isIngestCommand ? COLORS.brand : void 0,
3977
+ bold: isIngestCommand
3979
3978
  };
3980
3979
  });
3981
3980
  return [
@@ -4012,13 +4011,11 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4012
4011
  import z28 from "zod";
4013
4012
  import { join as join12 } from "node:path";
4014
4013
 
4015
- // src/lib/worktree.ts
4014
+ // src/lib/git.ts
4016
4015
  import { execFile as execFile2 } from "node:child_process";
4017
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4016
+ import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4018
4017
  import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
4019
4018
  var MAX_BUFFER = 32 * 1024 * 1024;
4020
- var MAX_WIZARD_WORKTREES = 3;
4021
- var WIZARD_BRANCH_PREFIX = "wizard/implement-";
4022
4019
  function git(args) {
4023
4020
  return new Promise((resolve4, reject) => {
4024
4021
  execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
@@ -4045,40 +4042,7 @@ async function isWorkingTreeDirty(repoRoot) {
4045
4042
  const out = await git(["-C", repoRoot, "status", "--porcelain"]);
4046
4043
  return out.trim().length > 0;
4047
4044
  }
4048
- async function pruneOldWorktrees(repoRoot) {
4049
- const dir = join10(stateDir(repoRoot), "worktrees");
4050
- const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
4051
- for (const slug of stale) {
4052
- const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
4053
- try {
4054
- await git([
4055
- "-C",
4056
- repoRoot,
4057
- "worktree",
4058
- "remove",
4059
- "--force",
4060
- join10(dir, slug)
4061
- ]);
4062
- await git(["-C", repoRoot, "branch", "-D", branch]);
4063
- } catch (err) {
4064
- logger.warn(
4065
- { branch, err: err.message },
4066
- "createWorktree: failed to prune a stale wizard worktree; continuing"
4067
- );
4068
- }
4069
- }
4070
- }
4071
- async function createWorktree(repoRoot) {
4072
- const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
4073
- const dirSlug = branch.replace(/\//g, "-");
4074
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
4075
- await git(["-C", repoRoot, "worktree", "prune"]);
4076
- await pruneOldWorktrees(repoRoot);
4077
- await mkdir6(dirname7(path), { recursive: true });
4078
- await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
4079
- return { path, branch };
4080
- }
4081
- async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
4045
+ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4082
4046
  const trimmed = sourcePath.trim();
4083
4047
  if (!trimmed) {
4084
4048
  return { ok: false, reason: "no file path was provided" };
@@ -4092,7 +4056,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4092
4056
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4093
4057
  }
4094
4058
  const relPath = join10(ingestDir, basename2(source));
4095
- const dest = join10(worktreePath, relPath);
4059
+ const dest = join10(repoRoot, relPath);
4060
+ if (resolve3(source) === resolve3(dest)) {
4061
+ return { ok: true, relPath };
4062
+ }
4096
4063
  try {
4097
4064
  await mkdir6(dirname7(dest), { recursive: true });
4098
4065
  await copyFile(source, dest);
@@ -4107,10 +4074,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4107
4074
  function hasEnvVar(content, name) {
4108
4075
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
4109
4076
  }
4110
- async function readEnvVar(worktreePath, name) {
4077
+ async function readEnvVar(repoRoot, name) {
4111
4078
  let content;
4112
4079
  try {
4113
- content = await readFile8(join10(worktreePath, ".env"), "utf8");
4080
+ content = await readFile8(join10(repoRoot, ".env"), "utf8");
4114
4081
  } catch (err) {
4115
4082
  if (err.code !== "ENOENT") throw err;
4116
4083
  return void 0;
@@ -4124,8 +4091,8 @@ async function readEnvVar(worktreePath, name) {
4124
4091
  if (!value || value.startsWith("<")) return void 0;
4125
4092
  return value;
4126
4093
  }
4127
- async function writeSearchEnvValues(worktreePath, vars) {
4128
- const target = join10(worktreePath, ".env");
4094
+ async function writeSearchEnvValues(repoRoot, vars) {
4095
+ const target = join10(repoRoot, ".env");
4129
4096
  let existing = "";
4130
4097
  try {
4131
4098
  existing = await readFile8(target, "utf8");
@@ -4140,8 +4107,8 @@ async function writeSearchEnvValues(worktreePath, vars) {
4140
4107
  await writeFile7(target, existing + prefix + lines, "utf8");
4141
4108
  return missing.map((v) => v.name);
4142
4109
  }
4143
- async function listChangedFiles(worktreePath) {
4144
- const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
4110
+ async function listChangedFiles(repoRoot) {
4111
+ const raw = await git(["-C", repoRoot, "status", "--porcelain", "-z"]);
4145
4112
  const entries = raw.split("\0");
4146
4113
  const files = [];
4147
4114
  for (let i = 0; i < entries.length; i += 1) {
@@ -4157,20 +4124,20 @@ function normalizeFindingPaths(findings) {
4157
4124
  ...findings,
4158
4125
  ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
4159
4126
  ...e,
4160
- paths: e.paths.map(toWorktreeRelative)
4127
+ paths: e.paths.map(toRootRelative)
4161
4128
  })),
4162
4129
  searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
4163
4130
  confirmedEntities: findings.confirmedEntities?.map((e) => ({
4164
4131
  ...e,
4165
- paths: e.paths.map(toWorktreeRelative)
4132
+ paths: e.paths.map(toRootRelative)
4166
4133
  }))
4167
4134
  };
4168
4135
  }
4169
4136
  function normalizeSearchLocation(path) {
4170
- const normalized = path ? toWorktreeRelative(path).trim() : "";
4137
+ const normalized = path ? toRootRelative(path).trim() : "";
4171
4138
  return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
4172
4139
  }
4173
- function toWorktreeRelative(p) {
4140
+ function toRootRelative(p) {
4174
4141
  return p.replace(/^\/+/, "");
4175
4142
  }
4176
4143
  async function confirmDirtyWorkingTree(ctx, repoRoot) {
@@ -4179,19 +4146,19 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
4179
4146
  const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
4180
4147
  const overflow = dirty.length - shown.length;
4181
4148
  const answer = await ctx.requestUserInput({
4182
- prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
4149
+ prompt: "Proceed with uncommitted changes in your checkout? The wizard will edit files there directly, alongside them.",
4183
4150
  promptType: "acceptReject",
4184
4151
  options: [],
4185
4152
  messages: [
4186
- `${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
4153
+ `${dirty.length} uncommitted change(s) detected:`,
4187
4154
  ...shown.map((file) => ` \u2022 ${file}`),
4188
4155
  ...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
4189
- "Commit or stash them first to include them in the implementation."
4156
+ "Commit or stash them first if you want to review the wizard's changes on their own."
4190
4157
  ]
4191
4158
  });
4192
4159
  if (answer !== true) {
4193
4160
  throw new Error(
4194
- "implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
4161
+ "implement aborted: commit or stash your changes first, then re-run the wizard."
4195
4162
  );
4196
4163
  }
4197
4164
  }
@@ -4254,16 +4221,10 @@ function getFrameworkSpecificDoc(frameworks) {
4254
4221
  return loadAlgoliaDoc("js");
4255
4222
  }
4256
4223
 
4257
- // src/lib/shell.ts
4258
- function shellQuote(value) {
4259
- return "'" + value.replace(/'/g, "'\\''") + "'";
4260
- }
4261
-
4262
4224
  // src/actions/implement.ts
4263
4225
  var implementSchema = z28.object({
4264
4226
  filesChanged: z28.array(z28.string()),
4265
4227
  summary: z28.string(),
4266
- worktreePath: z28.string().optional(),
4267
4228
  ingestCommand: z28.string().optional(),
4268
4229
  ingestScriptRan: z28.boolean().optional(),
4269
4230
  ingestRecordCount: z28.number().optional(),
@@ -4352,9 +4313,8 @@ function searchEnvVars(language, index, appId, searchKey) {
4352
4313
  name: searchKeyVar(language),
4353
4314
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4354
4315
  },
4355
- // Wizard-supplied rather than written into the generated code, because an
4356
- // agent that retypes the name (appending the project name, re-casing it)
4357
- // leaves the UI querying an index that does not exist.
4316
+ // Wizard-supplied rather than written into the generated code: an agent
4317
+ // that retypes the name leaves the UI querying an index that doesn't exist.
4358
4318
  {
4359
4319
  name: searchIndexVar(language),
4360
4320
  value: index
@@ -4363,8 +4323,6 @@ function searchEnvVars(language, index, appId, searchKey) {
4363
4323
  }
4364
4324
  function baseInstructions(input) {
4365
4325
  return [
4366
- // Agents have renamed this (e.g. appending the project name), which the
4367
- // index-scoped keys then reject with a 403.
4368
4326
  `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
4369
4327
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4370
4328
  "Make minimal, idiomatic changes; do not touch unrelated code.",
@@ -4382,14 +4340,14 @@ function sourceSpecificInstructions(input) {
4382
4340
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
4383
4341
  ],
4384
4342
  fileUpload: [
4385
- `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
4343
+ `Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
4386
4344
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
4387
4345
  "Map parsed columns/fields to the confirmed entity attributes.",
4388
4346
  "Never fabricate, hardcode, or substitute a different file."
4389
4347
  ],
4390
4348
  generated: [
4391
4349
  "No real data source exists; use sample records for each confirmed entity.",
4392
- "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.",
4350
+ "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.",
4393
4351
  "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.",
4394
4352
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4395
4353
  ]
@@ -4438,14 +4396,11 @@ function searchInstructions(input) {
4438
4396
  "If a search box already exists, replace it with yours.",
4439
4397
  `Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4440
4398
  "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.",
4441
- // The key is provisioned only after verification passes, so the agent never
4442
- // sees one. It must also leave .env alone: the wizard reads that file to
4443
- // decide whether a key already exists, and an agent-invented value there
4444
- // would be reused as if it were real.
4399
+ // The key is provisioned only after verification passes, and the wizard
4400
+ // reads .env to decide whether a key already exists an agent-invented
4401
+ // value there would be reused as if it were real.
4445
4402
  `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.`,
4446
- // Not the agent's to rename: the wizard writes these exact names into
4447
- // ".env" right after this step, so a renamed prefix would leave the code
4448
- // reading a var the wizard never wrote.
4403
+ // The wizard writes these exact names into .env right after this step.
4449
4404
  `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4450
4405
  "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4451
4406
  "Match the styles of the application as closely as possible.",
@@ -4454,10 +4409,10 @@ function searchInstructions(input) {
4454
4409
  }
4455
4410
  function verificationInstructions(input) {
4456
4411
  return [
4457
- "Verify the Algolia implementation changes in the current worktree.",
4412
+ "Verify the Algolia implementation changes.",
4458
4413
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
4459
4414
  "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.",
4460
- "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.",
4415
+ "If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
4461
4416
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4462
4417
  "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.",
4463
4418
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
@@ -4552,11 +4507,11 @@ function ingestFailure(attempt, executions) {
4552
4507
  }
4553
4508
  return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
4554
4509
  }
4555
- function makeToolContext(worktree, env = async () => ({})) {
4510
+ function makeToolContext(root, env = async () => ({})) {
4556
4511
  return createToolContext(
4557
4512
  DEFAULT_TOOL_LIMITS,
4558
- worktree,
4559
- createShellContext({ env, approve: storeApproval(worktree) })
4513
+ root,
4514
+ createShellContext({ env, approve: storeApproval(root) })
4560
4515
  );
4561
4516
  }
4562
4517
  function verificationRetryInstructions(verification) {
@@ -4564,7 +4519,7 @@ function verificationRetryInstructions(verification) {
4564
4519
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
4565
4520
  ];
4566
4521
  }
4567
- async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
4522
+ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4568
4523
  const repoRoot = process.cwd();
4569
4524
  const scan = ctx.getStepOutput("project-scan");
4570
4525
  const entities = ctx.getStepOutput(
@@ -4630,9 +4585,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4630
4585
  const targetIndex = selected?.selection;
4631
4586
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4632
4587
  await assertGitRepoWithHead(repoRoot);
4633
- if (await isWorkingTreeDirty(repoRoot)) {
4588
+ const ingestionAlreadyRan = ctx.getStepOutput("ingestion") != null;
4589
+ if (!ingestionAlreadyRan && await isWorkingTreeDirty(repoRoot)) {
4634
4590
  await confirmDirtyWorkingTree(ctx, repoRoot);
4635
4591
  }
4592
+ const baselineFiles = new Set(await listChangedFiles(repoRoot));
4636
4593
  const normalized = normalizeFindingPaths(findings);
4637
4594
  const confirmed2 = normalized.confirmedEntities;
4638
4595
  const searchLocation = normalized.searchImplementationAnalysis;
@@ -4644,314 +4601,292 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4644
4601
  if (useCases.includes("ingestion")) {
4645
4602
  ingestAppId = appId ?? (await requireApplication()).id;
4646
4603
  }
4647
- const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
4648
- try {
4649
- process.chdir(worktree);
4650
- let uploadFilePath;
4651
- let uploadWarning;
4652
- if (ingestionSource === "fileUpload") {
4653
- const copied = await copyUploadIntoWorktree(
4654
- repoRoot,
4655
- worktree,
4656
- INGEST_DIR,
4657
- uploadSourcePath ?? ""
4604
+ let uploadFilePath;
4605
+ let uploadWarning;
4606
+ if (ingestionSource === "fileUpload") {
4607
+ const copied = await copyUploadIntoProject(
4608
+ repoRoot,
4609
+ INGEST_DIR,
4610
+ uploadSourcePath ?? ""
4611
+ );
4612
+ if (copied.ok) {
4613
+ uploadFilePath = copied.relPath;
4614
+ } else {
4615
+ ingestionSource = "generated";
4616
+ uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4617
+ logger.warn(
4618
+ { reason: copied.reason },
4619
+ "implement: file upload unavailable; falling back to generated sample records"
4658
4620
  );
4659
- if (copied.ok) {
4660
- uploadFilePath = copied.relPath;
4661
- } else {
4662
- ingestionSource = "generated";
4663
- uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4664
- logger.warn(
4665
- { reason: copied.reason },
4666
- "implement: file upload unavailable; falling back to generated sample records"
4667
- );
4668
- }
4669
- }
4670
- const input = {
4671
- findings: normalized,
4672
- confirmed: confirmed2,
4673
- searchLocation,
4674
- targetIndex,
4675
- language,
4676
- appId,
4677
- searchEnvVars: searchEnvVars(language, targetIndex, appId),
4678
- ingestDir: INGEST_DIR,
4679
- ingestionSource,
4680
- uploadFilePath,
4681
- searchUiTarget: searchUiTarget(language)
4682
- };
4683
- const summaries = [];
4684
- if (uploadWarning) summaries.push(uploadWarning);
4685
- let envSearchKey;
4686
- let envAppIdMismatch = false;
4687
- if (useCases.includes("search") && appId) {
4688
- const envAppId = await readEnvVar(worktree, appIdVar(language));
4689
- if (envAppId === appId) {
4690
- envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
4691
- } else if (envAppId) {
4692
- envAppIdMismatch = true;
4693
- summaries.push(
4694
- `\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
4695
- );
4696
- logger.warn(
4697
- { envAppId, appId },
4698
- "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4699
- );
4700
- }
4701
4621
  }
4702
- let finalSearchEnvVars = input.searchEnvVars;
4703
- let agentRuns = 0;
4704
- let ingestCommand;
4705
- let ingestScriptRan = false;
4706
- let ingestRecordCount;
4707
- let ingestDurationMs;
4708
- let ingestOutcomeMessage;
4709
- const ingestKeyAppId = ingestAppId;
4710
- const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
4711
- [APP_ID_VAR]: ingestKeyAppId,
4712
- [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4713
- [INDEX_NAME_VAR]: targetIndex
4714
- })) : void 0;
4715
- const searchTools = makeToolContext(worktree);
4716
- async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4717
- if (agentRuns > 0) ctx.recordStepExecution();
4718
- agentRuns += 1;
4719
- return runAgent({
4720
- instructions: buildAgentInstructions(
4721
- currentUseCase,
4722
- input,
4723
- extraInstructions
4724
- ),
4725
- tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4726
- outputSchema: implementationOutputSchema,
4727
- toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4728
- });
4622
+ }
4623
+ const input = {
4624
+ findings: normalized,
4625
+ confirmed: confirmed2,
4626
+ searchLocation,
4627
+ targetIndex,
4628
+ language,
4629
+ appId,
4630
+ searchEnvVars: searchEnvVars(language, targetIndex, appId),
4631
+ ingestDir: INGEST_DIR,
4632
+ ingestionSource,
4633
+ uploadFilePath,
4634
+ searchUiTarget: searchUiTarget(language)
4635
+ };
4636
+ const summaries = [];
4637
+ if (uploadWarning) summaries.push(uploadWarning);
4638
+ let envSearchKey;
4639
+ let envAppIdMismatch = false;
4640
+ if (useCases.includes("search") && appId) {
4641
+ const envAppId = await readEnvVar(repoRoot, appIdVar(language));
4642
+ if (envAppId === appId) {
4643
+ envSearchKey = await readEnvVar(repoRoot, searchKeyVar(language));
4644
+ } else if (envAppId) {
4645
+ envAppIdMismatch = true;
4646
+ summaries.push(
4647
+ `\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
4648
+ );
4649
+ logger.warn(
4650
+ { envAppId, appId },
4651
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4652
+ );
4729
4653
  }
4730
- async function runVerificationUseCase() {
4731
- if (agentRuns > 0) ctx.recordStepExecution();
4732
- agentRuns += 1;
4733
- return runAgent({
4734
- instructions: buildAgentInstructions("verification", input),
4735
- tools: toolsForUseCase("verification"),
4736
- outputSchema: verificationOutputSchema,
4737
- toolContext: searchTools
4738
- });
4654
+ }
4655
+ let finalSearchEnvVars = input.searchEnvVars;
4656
+ let agentRuns = 0;
4657
+ let ingestCommand;
4658
+ let ingestScriptRan = false;
4659
+ let ingestRecordCount;
4660
+ let ingestDurationMs;
4661
+ let ingestOutcomeMessage;
4662
+ const ingestKeyAppId = ingestAppId;
4663
+ const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
4664
+ [APP_ID_VAR]: ingestKeyAppId,
4665
+ [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4666
+ [INDEX_NAME_VAR]: targetIndex
4667
+ })) : void 0;
4668
+ const searchTools = makeToolContext(repoRoot);
4669
+ async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4670
+ if (agentRuns > 0) ctx.recordStepExecution();
4671
+ agentRuns += 1;
4672
+ return runAgent({
4673
+ instructions: buildAgentInstructions(
4674
+ currentUseCase,
4675
+ input,
4676
+ extraInstructions
4677
+ ),
4678
+ tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4679
+ outputSchema: implementationOutputSchema,
4680
+ toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4681
+ });
4682
+ }
4683
+ async function runVerificationUseCase() {
4684
+ if (agentRuns > 0) ctx.recordStepExecution();
4685
+ agentRuns += 1;
4686
+ return runAgent({
4687
+ instructions: buildAgentInstructions("verification", input),
4688
+ tools: toolsForUseCase("verification"),
4689
+ outputSchema: verificationOutputSchema,
4690
+ toolContext: searchTools
4691
+ });
4692
+ }
4693
+ if (useCases.includes("ingestion")) {
4694
+ let ingestFailureDetail;
4695
+ const result = await runImplementationUseCase("ingestion");
4696
+ summaries.push(formatSummary("ingestion", result.summary));
4697
+ ingestCommand = result.ingestCommand;
4698
+ const ingestionContext = ingestionTools ?? searchTools;
4699
+ const executions = ingestionContext.shell.executions;
4700
+ const {
4701
+ run: ingestRun,
4702
+ attempt: ingestAttempt,
4703
+ recordCount
4704
+ } = ingestOutcome(executions, ingestCommand);
4705
+ ingestScriptRan = ingestRun != null;
4706
+ ingestRecordCount = recordCount;
4707
+ ingestDurationMs = ingestRun?.durationMs;
4708
+ if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4709
+ summaries.push(
4710
+ "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
4711
+ );
4712
+ logger.warn(
4713
+ { ingestCommand },
4714
+ "implement: ingestion ran without a reviewScript call"
4715
+ );
4739
4716
  }
4740
- if (useCases.includes("ingestion")) {
4741
- let ingestFailureDetail;
4742
- const result = await runImplementationUseCase("ingestion");
4743
- summaries.push(formatSummary("ingestion", result.summary));
4744
- ingestCommand = result.ingestCommand;
4745
- const ingestionContext = ingestionTools ?? searchTools;
4746
- const executions = ingestionContext.shell.executions;
4747
- const {
4748
- run: ingestRun,
4749
- attempt: ingestAttempt,
4750
- recordCount
4751
- } = ingestOutcome(executions, ingestCommand);
4752
- ingestScriptRan = ingestRun != null;
4753
- ingestRecordCount = recordCount;
4754
- ingestDurationMs = ingestRun?.durationMs;
4755
- if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4756
- summaries.push(
4757
- "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
4758
- );
4759
- logger.warn(
4760
- { ingestCommand },
4761
- "implement: ingestion ran without a reviewScript call"
4762
- );
4717
+ if (ingestScriptRan) {
4718
+ ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4719
+ if (ingestRecordCount != null) {
4720
+ track("AI Wizard Ingest Successful", {
4721
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4722
+ record_count: ingestRecordCount,
4723
+ duration_ms: ingestDurationMs ?? 0
4724
+ });
4763
4725
  }
4764
- if (ingestScriptRan) {
4765
- ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4766
- if (ingestRecordCount != null) {
4767
- track("AI Wizard Ingest Successful", {
4768
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4769
- record_count: ingestRecordCount,
4770
- duration_ms: ingestDurationMs ?? 0
4771
- });
4772
- }
4773
- } else {
4774
- const { reason, detail } = ingestFailure(ingestAttempt, executions);
4775
- ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4776
- ingestFailureDetail = detail;
4777
- summaries.push(
4778
- `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4726
+ } else {
4727
+ const { reason, detail } = ingestFailure(ingestAttempt, executions);
4728
+ ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4729
+ ingestFailureDetail = detail;
4730
+ summaries.push(
4731
+ `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4779
4732
  ${detail}` : ""}`
4780
- );
4781
- logger.warn(
4733
+ );
4734
+ logger.warn(
4735
+ {
4736
+ ingestCommand,
4737
+ reason,
4738
+ approved: ingestAttempt?.approved,
4739
+ exitCode: ingestAttempt?.exitCode,
4740
+ timedOut: ingestAttempt?.timedOut,
4741
+ commandsRun: executions.length
4742
+ },
4743
+ "implement: ingestion script did not complete successfully"
4744
+ );
4745
+ track("Error", {
4746
+ step: "Push Data",
4747
+ error: `ingestion did not complete: ${reason}`,
4748
+ product_area: "AI Wizard"
4749
+ });
4750
+ }
4751
+ const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
4752
+ await ctx.requestUserInput({
4753
+ prompt: "",
4754
+ promptType: "enterToContinue",
4755
+ options: [],
4756
+ // One message per line: Notices.tsx counts a message as one wrapped
4757
+ // line, so an embedded newline overflows the panel's height accounting.
4758
+ messages: [
4759
+ ingestOutcomeMessage,
4760
+ ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
4761
+ ...commandMessages
4762
+ ]
4763
+ });
4764
+ }
4765
+ if (useCases.includes("search")) {
4766
+ let extraInstructions = [];
4767
+ const preSearchFiles = new Set(await listChangedFiles(repoRoot));
4768
+ for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
4769
+ if (attempt > 1) {
4770
+ logger.info(
4782
4771
  {
4783
- ingestCommand,
4784
- reason,
4785
- approved: ingestAttempt?.approved,
4786
- exitCode: ingestAttempt?.exitCode,
4787
- timedOut: ingestAttempt?.timedOut,
4788
- commandsRun: executions.length
4772
+ attempt,
4773
+ maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
4774
+ extraInstructions
4789
4775
  },
4790
- "implement: ingestion script did not complete successfully"
4776
+ "implement: retrying search implementation after failed verification"
4777
+ );
4778
+ }
4779
+ const { summary } = await runImplementationUseCase(
4780
+ "search",
4781
+ extraInstructions
4782
+ );
4783
+ summaries.push(formatSummary("search", summary));
4784
+ const verification = await runVerificationUseCase();
4785
+ summaries.push(formatSummary("verification", verification.summary));
4786
+ if (verification.sufficient) {
4787
+ ctx.setUserInput("implementation", "success");
4788
+ const searchFilesChanged = (await listChangedFiles(repoRoot)).filter(
4789
+ (file) => !preSearchFiles.has(file)
4791
4790
  );
4792
- track("Error", {
4793
- step: "Push Data",
4794
- error: `ingestion did not complete: ${reason}`,
4795
- product_area: "AI Wizard"
4791
+ track("AI Wizard Frontend Component Generated", {
4792
+ filePaths: searchFilesChanged
4793
+ });
4794
+ track("AI Wizard Wired to UI", {
4795
+ location_heuristic: searchLocation ?? "unknown"
4796
4796
  });
4797
+ break;
4797
4798
  }
4798
- const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4799
- if (ingestCommand) {
4800
- commandMessages.push(`Ingestion command: ${ingestCommand}`);
4799
+ if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
4800
+ ctx.setUserInput("implementation", "fail");
4801
+ throw new Error(
4802
+ `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
4803
+ );
4801
4804
  }
4802
- await ctx.requestUserInput({
4803
- prompt: "",
4804
- promptType: "enterToContinue",
4805
- options: [],
4806
- // The wizard never streams command output, so a failed run's tail is the
4807
- // only place the developer sees why it failed. One message per line:
4808
- // the panel's height accounting counts a message as one wrapped line
4809
- // (see Notices.tsx), so an embedded newline overflows it.
4810
- messages: [
4811
- ingestOutcomeMessage,
4812
- ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
4813
- ...commandMessages
4814
- ]
4815
- });
4805
+ extraInstructions = verificationRetryInstructions(verification);
4816
4806
  }
4817
- if (useCases.includes("search")) {
4818
- let extraInstructions = [];
4819
- const preSearchFiles = new Set(await listChangedFiles(worktree));
4820
- for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
4821
- if (attempt > 1) {
4822
- logger.info(
4823
- {
4824
- attempt,
4825
- maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
4826
- extraInstructions
4827
- },
4828
- "implement: retrying search implementation after failed verification"
4829
- );
4830
- }
4831
- const { summary } = await runImplementationUseCase(
4832
- "search",
4833
- extraInstructions
4807
+ let searchKey;
4808
+ let searchKeyError;
4809
+ if (appId) {
4810
+ try {
4811
+ const resolved2 = await resolveSearchOnlyKey(
4812
+ targetIndex,
4813
+ appId,
4814
+ envSearchKey
4815
+ );
4816
+ searchKey = resolved2.key;
4817
+ summaries.push(
4818
+ 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}.`
4819
+ );
4820
+ } catch (err) {
4821
+ searchKeyError = err.message;
4822
+ logger.warn(
4823
+ { err: searchKeyError },
4824
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4834
4825
  );
4835
- summaries.push(formatSummary("search", summary));
4836
- const verification = await runVerificationUseCase();
4837
- summaries.push(formatSummary("verification", verification.summary));
4838
- if (verification.sufficient) {
4839
- ctx.setUserInput("implementation", "success");
4840
- const searchFilesChanged = (await listChangedFiles(worktree)).filter(
4841
- (file) => !preSearchFiles.has(file)
4842
- );
4843
- track("AI Wizard Frontend Component Generated", {
4844
- filePaths: searchFilesChanged
4845
- });
4846
- track("AI Wizard Wired to UI", {
4847
- location_heuristic: searchLocation ?? "unknown"
4848
- });
4849
- break;
4850
- }
4851
- if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
4852
- ctx.setUserInput("implementation", "fail");
4853
- throw new Error(
4854
- `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
4855
- );
4856
- }
4857
- extraInstructions = verificationRetryInstructions(verification);
4858
4826
  }
4859
- let searchKey;
4860
- let searchKeyError;
4861
- if (appId) {
4862
- try {
4863
- const resolved2 = await resolveSearchOnlyKey(
4864
- targetIndex,
4865
- appId,
4866
- envSearchKey
4867
- );
4868
- searchKey = resolved2.key;
4869
- summaries.push(
4870
- 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}.`
4871
- );
4872
- } catch (err) {
4873
- searchKeyError = err.message;
4874
- logger.warn(
4875
- { err: searchKeyError },
4876
- "implement: could not provision a search-only API key; the .env value stays a placeholder"
4877
- );
4878
- }
4827
+ }
4828
+ finalSearchEnvVars = searchEnvVars(language, targetIndex, appId, searchKey);
4829
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4830
+ (v) => !v.value.startsWith("<")
4831
+ );
4832
+ if (resolvedSearchEnvVars.length > 0) {
4833
+ const written = await writeSearchEnvValues(repoRoot, resolvedSearchEnvVars);
4834
+ if (written.length > 0) {
4835
+ summaries.push(`Wrote ${written.join(", ")} to .env.`);
4879
4836
  }
4880
- finalSearchEnvVars = searchEnvVars(
4881
- language,
4882
- targetIndex,
4883
- appId,
4884
- searchKey
4885
- );
4886
- const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4887
- (v) => !v.value.startsWith("<")
4888
- );
4889
- if (resolvedSearchEnvVars.length > 0) {
4890
- const written = await writeSearchEnvValues(
4891
- worktree,
4892
- resolvedSearchEnvVars
4837
+ const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
4838
+ if (ignored === "added") {
4839
+ summaries.push("Added .env to .gitignore.");
4840
+ } else if (ignored === "tracked") {
4841
+ summaries.push(
4842
+ '\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.'
4893
4843
  );
4894
- if (written.length > 0) {
4895
- summaries.push(`Wrote ${written.join(", ")} to .env.`);
4896
- }
4897
- const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
4898
- if (ignored === "added") {
4899
- summaries.push("Added .env to .gitignore.");
4900
- } else if (ignored === "tracked") {
4901
- summaries.push(
4902
- '\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.'
4903
- );
4904
- }
4905
- const stale = [];
4906
- for (const v of resolvedSearchEnvVars) {
4907
- if (written.includes(v.name)) continue;
4908
- const current = await readEnvVar(worktree, v.name);
4909
- if (current && current !== v.value) stale.push(v);
4910
- }
4911
- if (stale.length > 0 && !envAppIdMismatch) {
4912
- summaries.push(
4913
- `\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.`
4914
- );
4915
- logger.warn(
4916
- { vars: stale.map((v) => v.name) },
4917
- "implement: .env holds different values for the resolved search credentials; not overwriting them"
4918
- );
4919
- }
4920
4844
  }
4921
- const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4922
- (v) => v.value.startsWith("<")
4923
- );
4924
- if (unresolvedSearchEnvVars.length > 0) {
4845
+ const stale = [];
4846
+ for (const v of resolvedSearchEnvVars) {
4847
+ if (written.includes(v.name)) continue;
4848
+ const current = await readEnvVar(repoRoot, v.name);
4849
+ if (current && current !== v.value) stale.push(v);
4850
+ }
4851
+ if (stale.length > 0 && !envAppIdMismatch) {
4925
4852
  summaries.push(
4926
- `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.
4927
- (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4853
+ `\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.`
4854
+ );
4855
+ logger.warn(
4856
+ { vars: stale.map((v) => v.name) },
4857
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4928
4858
  );
4929
4859
  }
4930
- } else {
4931
- ctx.setUserInput("implementation", "success");
4932
4860
  }
4933
- const filesChanged = await listChangedFiles(worktree);
4934
- if (filesChanged.length === 0) {
4935
- logger.warn(
4936
- "implement: agent reported success but no files changed in the worktree"
4861
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4862
+ (v) => v.value.startsWith("<")
4863
+ );
4864
+ if (unresolvedSearchEnvVars.length > 0) {
4865
+ summaries.push(
4866
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4937
4867
  );
4938
4868
  }
4939
- return {
4940
- ingestionSource,
4941
- filesChanged,
4942
- summary: summaries.join("\n\n"),
4943
- worktreePath: worktree,
4944
- ...useCases.includes("ingestion") && ingestCommand ? {
4945
- ingestCommand,
4946
- ingestScriptRan,
4947
- ...ingestRecordCount != null ? { ingestRecordCount } : {},
4948
- ...ingestDurationMs != null ? { ingestDurationMs } : {}
4949
- } : {},
4950
- ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4951
- };
4952
- } finally {
4953
- process.chdir(repoRoot);
4869
+ } else {
4870
+ ctx.setUserInput("implementation", "success");
4871
+ }
4872
+ const filesChanged = (await listChangedFiles(repoRoot)).filter(
4873
+ (file) => !baselineFiles.has(file)
4874
+ );
4875
+ if (filesChanged.length === 0) {
4876
+ logger.warn("implement: agent reported success but no files changed");
4954
4877
  }
4878
+ return {
4879
+ ingestionSource,
4880
+ filesChanged,
4881
+ summary: summaries.join("\n\n"),
4882
+ ...useCases.includes("ingestion") && ingestCommand ? {
4883
+ ingestCommand,
4884
+ ingestScriptRan,
4885
+ ...ingestRecordCount != null ? { ingestRecordCount } : {},
4886
+ ...ingestDurationMs != null ? { ingestDurationMs } : {}
4887
+ } : {},
4888
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4889
+ };
4955
4890
  }
4956
4891
 
4957
4892
  // src/workflows/default.ts
@@ -5023,10 +4958,7 @@ var defaultWorkflow = {
5023
4958
  ctx.notify({
5024
4959
  messages: ["Building your Algolia search experience\u2026"]
5025
4960
  });
5026
- const ingestion2 = ctx.getStepOutput(
5027
- "ingestion"
5028
- );
5029
- return implement(ctx, ["search"], ingestion2?.worktreePath);
4961
+ return implement(ctx, ["search"]);
5030
4962
  }
5031
4963
  }),
5032
4964
  defineStep({
@@ -5041,9 +4973,8 @@ var defaultWorkflow = {
5041
4973
  "ingestion"
5042
4974
  );
5043
4975
  return reviewStep(ctx, {
5044
- // The ingestion step already showed the user the exact `ingestCommand`
5045
- // and worktree path as a notice, so nextSteps must not restate it —
5046
- // an LLM-paraphrased command risks being wrong.
4976
+ // ingestCommand was already shown verbatim as a notice; an
4977
+ // LLM-paraphrased restatement in nextSteps risks being wrong.
5047
4978
  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."
5048
4979
  });
5049
4980
  }
@@ -5114,7 +5045,7 @@ var review = {
5114
5045
  "Ingested 25 generated Product records into wizard_seed_products.",
5115
5046
  "Added an InstantSearch search experience to the shared header."
5116
5047
  ],
5117
- reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
5048
+ reviewPrompt: "Review the Algolia ingestion and search changes.",
5118
5049
  nextSteps: ["Point the ingestion script at your real product data."]
5119
5050
  };
5120
5051
  var SEEDS = {
@@ -5231,9 +5162,9 @@ Options:
5231
5162
  steps pre-filled with test data. Pass with no value to print
5232
5163
  the step ids. See CONTRIBUTING.md.
5233
5164
  --no-telemetry Send no telemetry or analytics for this run.
5234
- --reset-on-run Wipe this project's wizard state (run state, AI consent,
5235
- worktrees) before starting, so the run behaves like a
5236
- first-ever run. Also drops every API key the wizard has
5165
+ --reset-on-run Wipe this project's wizard state (run state, AI consent)
5166
+ before starting, so the run behaves like a first-ever
5167
+ run. Also drops every API key the wizard has
5237
5168
  stored in your keychain (or, where the platform has none,
5238
5169
  the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
5239
5170
  for this project and any other, so later runs create new
@@ -5273,7 +5204,7 @@ function parseCliArgs(argv) {
5273
5204
  }
5274
5205
 
5275
5206
  // src/lib/resetState.ts
5276
- import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
5207
+ import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
5277
5208
  import { join as join13 } from "node:path";
5278
5209
  var KEEP = ["wizard.log"];
5279
5210
  async function resetProjectState() {
@@ -5281,7 +5212,7 @@ async function resetProjectState() {
5281
5212
  await forgetResolvedKeys();
5282
5213
  let entries;
5283
5214
  try {
5284
- entries = await readdir4(dir);
5215
+ entries = await readdir3(dir);
5285
5216
  } catch {
5286
5217
  return { dir, removed: [] };
5287
5218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.18.0",
3
+ "version": "0.19.0-rc.111.186",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {