@algolia/wizard 0.19.0-rc.111.189 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -0
  2. package/dist/main.js +461 -307
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,8 +12,22 @@ npx @algolia/wizard
12
12
 
13
13
  # run a specific workflow by id
14
14
  npx @algolia/wizard <workflow-id>
15
+
16
+ # see all options
17
+ npx @algolia/wizard --help
15
18
  ```
16
19
 
20
+ ### Options
21
+
22
+ | Flag | Effect |
23
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `--seed <step-id>` | Start the workflow at the step with this id (e.g. `--seed ingestion`), with the earlier steps pre-filled with test data. Pass with no value to print the step ids. |
25
+ | `--no-telemetry` | Send no telemetry or analytics for this run. |
26
+ | `--reset-on-run` | Wipe this project's wizard state (run state, AI-changes consent, worktrees) before starting, so the run behaves like a first-ever run. Credentials are untouched. |
27
+ | `-h`, `--help` | Print usage. |
28
+
29
+ `--seed` pre-fills the earlier steps with fabricated data so a single step can be exercised without running the whole workflow — useful for testing a step, not for a real implementation. It replaces any in-progress run for that workflow and pre-grants the AI-changes consent. See [Starting mid-workflow](CONTRIBUTING.md#starting-mid-workflow---seed).
30
+
17
31
  On first run, Wizard checks whether you're signed in to the Algolia CLI. If you aren't, it runs `algolia auth login --non-interactive` — the browser opens for sign-in, and no prompts land in the wizard's terminal. It then asks which Algolia application to work in (skipping the question when the account has only one) and makes it current with `algolia application select`.
18
32
 
19
33
  You'll also be asked once to consent to AI-authored changes to the repository.
package/dist/main.js CHANGED
@@ -251,7 +251,6 @@ var useWizard = create((set, get) => ({
251
251
  _noticeTimer: null,
252
252
  cliOutput: [],
253
253
  targetIndex: null,
254
- writtenFiles: [],
255
254
  logs: [],
256
255
  error: null,
257
256
  inputReq: null,
@@ -345,8 +344,6 @@ var useWizard = create((set, get) => ({
345
344
  })),
346
345
  clearCliOutput: () => set({ cliOutput: [] }),
347
346
  setTargetIndex: (index) => set({ targetIndex: index }),
348
- recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
349
- clearWrittenFiles: () => set({ writtenFiles: [] }),
350
347
  logStart: (kind, name, input) => {
351
348
  const id = nanoid();
352
349
  set((s) => ({
@@ -394,7 +391,6 @@ var useWizard = create((set, get) => ({
394
391
  notices: [],
395
392
  cliOutput: [],
396
393
  targetIndex: null,
397
- writtenFiles: [],
398
394
  logs: [],
399
395
  error: null,
400
396
  inputReq: null,
@@ -1204,7 +1200,7 @@ var accessItems = [
1204
1200
  {
1205
1201
  tag: "WRITE",
1206
1202
  title: "Code changes",
1207
- description: "creates & edits files (search UI, config) directly in your branch. You are asked to confirm first if it has uncommitted changes."
1203
+ description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1208
1204
  },
1209
1205
  {
1210
1206
  tag: "EXEC",
@@ -1219,7 +1215,7 @@ var accessItems = [
1219
1215
  {
1220
1216
  tag: "KEY",
1221
1217
  title: "Credentials",
1222
- description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
1218
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1223
1219
  }
1224
1220
  ];
1225
1221
  var neverItems = [
@@ -2500,7 +2496,6 @@ function writeFileTool(ctx) {
2500
2496
  }
2501
2497
  await mkdir3(dirname4(resolved2.target), { recursive: true });
2502
2498
  await writeFile3(resolved2.target, content, "utf8");
2503
- useWizard.getState().recordWrittenFile(resolved2.target);
2504
2499
  return `Wrote to ${filePath}`;
2505
2500
  } catch (err) {
2506
2501
  return `Error writing ${filePath}: ${err.message}`;
@@ -3251,7 +3246,7 @@ var anthropic = createAnthropic({
3251
3246
  });
3252
3247
  function generateRecordTool(ctx) {
3253
3248
  return tool10({
3254
- 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.",
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.",
3255
3250
  inputSchema: z17.object({
3256
3251
  entityName: z17.string().describe("Name of the entity to generate records for."),
3257
3252
  attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
@@ -3609,7 +3604,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3609
3604
  // package.json
3610
3605
  var package_default = {
3611
3606
  name: "@algolia/wizard",
3612
- version: "0.19.0-rc.111.189",
3607
+ version: "0.19.0",
3613
3608
  description: "Magically implement Algolia functionality in your codebase",
3614
3609
  type: "module",
3615
3610
  engines: {
@@ -3976,10 +3971,11 @@ ${JSON.stringify(s.output, null, 2)}`
3976
3971
  function formatReviewSummary(result) {
3977
3972
  const nextStepLines = result.nextSteps.map((step) => {
3978
3973
  const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3974
+ const isWorktreeCommand = step.includes("/worktrees/");
3979
3975
  return {
3980
3976
  text: `\u2192 ${step}`,
3981
- color: isIngestCommand ? COLORS.brand : void 0,
3982
- bold: isIngestCommand
3977
+ color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
3978
+ bold: isIngestCommand || isWorktreeCommand
3983
3979
  };
3984
3980
  });
3985
3981
  return [
@@ -4014,13 +4010,15 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4014
4010
 
4015
4011
  // src/actions/implement.ts
4016
4012
  import z28 from "zod";
4017
- import { join as join12, relative as relative6 } from "node:path";
4013
+ import { join as join12 } from "node:path";
4018
4014
 
4019
- // src/lib/git.ts
4015
+ // src/lib/worktree.ts
4020
4016
  import { execFile as execFile2 } from "node:child_process";
4021
- import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4017
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4022
4018
  import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
4023
4019
  var MAX_BUFFER = 32 * 1024 * 1024;
4020
+ var MAX_WIZARD_WORKTREES = 3;
4021
+ var WIZARD_BRANCH_PREFIX = "wizard/implement-";
4024
4022
  function git(args) {
4025
4023
  return new Promise((resolve4, reject) => {
4026
4024
  execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
@@ -4043,7 +4041,44 @@ async function assertGitRepoWithHead(repoRoot) {
4043
4041
  );
4044
4042
  }
4045
4043
  }
4046
- async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4044
+ async function isWorkingTreeDirty(repoRoot) {
4045
+ const out = await git(["-C", repoRoot, "status", "--porcelain"]);
4046
+ return out.trim().length > 0;
4047
+ }
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) {
4047
4082
  const trimmed = sourcePath.trim();
4048
4083
  if (!trimmed) {
4049
4084
  return { ok: false, reason: "no file path was provided" };
@@ -4057,10 +4092,7 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4057
4092
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4058
4093
  }
4059
4094
  const relPath = join10(ingestDir, basename2(source));
4060
- const dest = join10(repoRoot, relPath);
4061
- if (resolve3(source) === resolve3(dest)) {
4062
- return { ok: true, relPath };
4063
- }
4095
+ const dest = join10(worktreePath, relPath);
4064
4096
  try {
4065
4097
  await mkdir6(dirname7(dest), { recursive: true });
4066
4098
  await copyFile(source, dest);
@@ -4075,10 +4107,10 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4075
4107
  function hasEnvVar(content, name) {
4076
4108
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
4077
4109
  }
4078
- async function readEnvVar(repoRoot, name) {
4110
+ async function readEnvVar(worktreePath, name) {
4079
4111
  let content;
4080
4112
  try {
4081
- content = await readFile8(join10(repoRoot, ".env"), "utf8");
4113
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
4082
4114
  } catch (err) {
4083
4115
  if (err.code !== "ENOENT") throw err;
4084
4116
  return void 0;
@@ -4092,8 +4124,8 @@ async function readEnvVar(repoRoot, name) {
4092
4124
  if (!value || value.startsWith("<")) return void 0;
4093
4125
  return value;
4094
4126
  }
4095
- async function writeSearchEnvValues(repoRoot, vars) {
4096
- const target = join10(repoRoot, ".env");
4127
+ async function writeSearchEnvValues(worktreePath, vars) {
4128
+ const target = join10(worktreePath, ".env");
4097
4129
  let existing = "";
4098
4130
  try {
4099
4131
  existing = await readFile8(target, "utf8");
@@ -4108,27 +4140,61 @@ async function writeSearchEnvValues(repoRoot, vars) {
4108
4140
  await writeFile7(target, existing + prefix + lines, "utf8");
4109
4141
  return missing.map((v) => v.name);
4110
4142
  }
4143
+ async function listChangedFiles(worktreePath) {
4144
+ const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
4145
+ const entries = raw.split("\0");
4146
+ const files = [];
4147
+ for (let i = 0; i < entries.length; i += 1) {
4148
+ const entry = entries[i];
4149
+ if (!entry) continue;
4150
+ files.push(entry.slice(3));
4151
+ if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
4152
+ }
4153
+ return files;
4154
+ }
4111
4155
  function normalizeFindingPaths(findings) {
4112
4156
  return {
4113
4157
  ...findings,
4114
4158
  ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
4115
4159
  ...e,
4116
- paths: e.paths.map(toRootRelative)
4160
+ paths: e.paths.map(toWorktreeRelative)
4117
4161
  })),
4118
4162
  searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
4119
4163
  confirmedEntities: findings.confirmedEntities?.map((e) => ({
4120
4164
  ...e,
4121
- paths: e.paths.map(toRootRelative)
4165
+ paths: e.paths.map(toWorktreeRelative)
4122
4166
  }))
4123
4167
  };
4124
4168
  }
4125
4169
  function normalizeSearchLocation(path) {
4126
- const normalized = path ? toRootRelative(path).trim() : "";
4170
+ const normalized = path ? toWorktreeRelative(path).trim() : "";
4127
4171
  return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
4128
4172
  }
4129
- function toRootRelative(p) {
4173
+ function toWorktreeRelative(p) {
4130
4174
  return p.replace(/^\/+/, "");
4131
4175
  }
4176
+ async function confirmDirtyWorkingTree(ctx, repoRoot) {
4177
+ const MAX_LISTED_DIRTY_FILES = 10;
4178
+ const dirty = await listChangedFiles(repoRoot);
4179
+ const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
4180
+ const overflow = dirty.length - shown.length;
4181
+ const answer = await ctx.requestUserInput({
4182
+ prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
4183
+ promptType: "acceptReject",
4184
+ options: [],
4185
+ messages: [
4186
+ `${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
4187
+ ...shown.map((file) => ` \u2022 ${file}`),
4188
+ ...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
4189
+ "Commit or stash them first to include them in the implementation."
4190
+ ]
4191
+ });
4192
+ if (answer !== true) {
4193
+ throw new Error(
4194
+ "implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
4195
+ );
4196
+ }
4197
+ }
4132
4198
 
4133
4199
  // src/lib/algoliaDocs.ts
4134
4200
  import { readFileSync, readdirSync, existsSync } from "node:fs";
@@ -4188,9 +4254,16 @@ function getFrameworkSpecificDoc(frameworks) {
4188
4254
  return loadAlgoliaDoc("js");
4189
4255
  }
4190
4256
 
4257
+ // src/lib/shell.ts
4258
+ function shellQuote(value) {
4259
+ return "'" + value.replace(/'/g, "'\\''") + "'";
4260
+ }
4261
+
4191
4262
  // src/actions/implement.ts
4192
4263
  var implementSchema = z28.object({
4264
+ filesChanged: z28.array(z28.string()),
4193
4265
  summary: z28.string(),
4266
+ worktreePath: z28.string().optional(),
4194
4267
  ingestCommand: z28.string().optional(),
4195
4268
  ingestScriptRan: z28.boolean().optional(),
4196
4269
  ingestRecordCount: z28.number().optional(),
@@ -4279,8 +4352,9 @@ function searchEnvVars(language, index, appId, searchKey) {
4279
4352
  name: searchKeyVar(language),
4280
4353
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4281
4354
  },
4282
- // Wizard-supplied rather than written into the generated code: an agent
4283
- // that retypes the name leaves the UI querying an index that doesn't exist.
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.
4284
4358
  {
4285
4359
  name: searchIndexVar(language),
4286
4360
  value: index
@@ -4289,6 +4363,8 @@ function searchEnvVars(language, index, appId, searchKey) {
4289
4363
  }
4290
4364
  function baseInstructions(input) {
4291
4365
  return [
4366
+ // Agents have renamed this (e.g. appending the project name), which the
4367
+ // index-scoped keys then reject with a 403.
4292
4368
  `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
4293
4369
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4294
4370
  "Make minimal, idiomatic changes; do not touch unrelated code.",
@@ -4306,14 +4382,14 @@ function sourceSpecificInstructions(input) {
4306
4382
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
4307
4383
  ],
4308
4384
  fileUpload: [
4309
- `Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
4385
+ `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
4310
4386
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
4311
4387
  "Map parsed columns/fields to the confirmed entity attributes.",
4312
4388
  "Never fabricate, hardcode, or substitute a different file."
4313
4389
  ],
4314
4390
  generated: [
4315
4391
  "No real data source exists; use sample records for each confirmed entity.",
4316
- "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.",
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.",
4317
4393
  "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.",
4318
4394
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4319
4395
  ]
@@ -4362,11 +4438,14 @@ function searchInstructions(input) {
4362
4438
  "If a search box already exists, replace it with yours.",
4363
4439
  `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.`,
4364
4440
  "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.",
4365
- // The key is provisioned only after verification passes, and the wizard
4366
- // reads .env to decide whether a key already exists an agent-invented
4367
- // value there would be reused as if it were real.
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.
4368
4445
  `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.`,
4369
- // The wizard writes these exact names into .env right after this step.
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.
4370
4449
  `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4371
4450
  "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4372
4451
  "Match the styles of the application as closely as possible.",
@@ -4375,10 +4454,10 @@ function searchInstructions(input) {
4375
4454
  }
4376
4455
  function verificationInstructions(input) {
4377
4456
  return [
4378
- "Verify the Algolia implementation changes.",
4457
+ "Verify the Algolia implementation changes in the current worktree.",
4379
4458
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
4380
4459
  "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.",
4381
- "If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
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.",
4382
4461
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4383
4462
  "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.",
4384
4463
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
@@ -4473,11 +4552,11 @@ function ingestFailure(attempt, executions) {
4473
4552
  }
4474
4553
  return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
4475
4554
  }
4476
- function makeToolContext(root, env = async () => ({})) {
4555
+ function makeToolContext(worktree, env = async () => ({})) {
4477
4556
  return createToolContext(
4478
4557
  DEFAULT_TOOL_LIMITS,
4479
- root,
4480
- createShellContext({ env, approve: storeApproval(root) })
4558
+ worktree,
4559
+ createShellContext({ env, approve: storeApproval(worktree) })
4481
4560
  );
4482
4561
  }
4483
4562
  function verificationRetryInstructions(verification) {
@@ -4485,7 +4564,7 @@ function verificationRetryInstructions(verification) {
4485
4564
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
4486
4565
  ];
4487
4566
  }
4488
- async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4567
+ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
4489
4568
  const repoRoot = process.cwd();
4490
4569
  const scan = ctx.getStepOutput("project-scan");
4491
4570
  const entities = ctx.getStepOutput(
@@ -4551,6 +4630,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4551
4630
  const targetIndex = selected?.selection;
4552
4631
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4553
4632
  await assertGitRepoWithHead(repoRoot);
4633
+ if (await isWorkingTreeDirty(repoRoot)) {
4634
+ await confirmDirtyWorkingTree(ctx, repoRoot);
4635
+ }
4554
4636
  const normalized = normalizeFindingPaths(findings);
4555
4637
  const confirmed2 = normalized.confirmedEntities;
4556
4638
  const searchLocation = normalized.searchImplementationAnalysis;
@@ -4562,288 +4644,314 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4562
4644
  if (useCases.includes("ingestion")) {
4563
4645
  ingestAppId = appId ?? (await requireApplication()).id;
4564
4646
  }
4565
- let uploadFilePath;
4566
- let uploadWarning;
4567
- if (ingestionSource === "fileUpload") {
4568
- const copied = await copyUploadIntoProject(
4569
- repoRoot,
4570
- INGEST_DIR,
4571
- uploadSourcePath ?? ""
4572
- );
4573
- if (copied.ok) {
4574
- uploadFilePath = copied.relPath;
4575
- } else {
4576
- ingestionSource = "generated";
4577
- uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4578
- logger.warn(
4579
- { reason: copied.reason },
4580
- "implement: file upload unavailable; falling back to generated sample records"
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 ?? ""
4581
4658
  );
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
+ }
4582
4669
  }
4583
- }
4584
- const input = {
4585
- findings: normalized,
4586
- confirmed: confirmed2,
4587
- searchLocation,
4588
- targetIndex,
4589
- language,
4590
- appId,
4591
- searchEnvVars: searchEnvVars(language, targetIndex, appId),
4592
- ingestDir: INGEST_DIR,
4593
- ingestionSource,
4594
- uploadFilePath,
4595
- searchUiTarget: searchUiTarget(language)
4596
- };
4597
- const summaries = [];
4598
- if (uploadWarning) summaries.push(uploadWarning);
4599
- let envSearchKey;
4600
- let envAppIdMismatch = false;
4601
- if (useCases.includes("search") && appId) {
4602
- const envAppId = await readEnvVar(repoRoot, appIdVar(language));
4603
- if (envAppId === appId) {
4604
- envSearchKey = await readEnvVar(repoRoot, searchKeyVar(language));
4605
- } else if (envAppId) {
4606
- envAppIdMismatch = true;
4607
- summaries.push(
4608
- `\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.`
4609
- );
4610
- logger.warn(
4611
- { envAppId, appId },
4612
- "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4613
- );
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
+ }
4614
4701
  }
4615
- }
4616
- let finalSearchEnvVars = input.searchEnvVars;
4617
- let agentRuns = 0;
4618
- let ingestCommand;
4619
- let ingestScriptRan = false;
4620
- let ingestRecordCount;
4621
- let ingestDurationMs;
4622
- let ingestOutcomeMessage;
4623
- const ingestKeyAppId = ingestAppId;
4624
- const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
4625
- [APP_ID_VAR]: ingestKeyAppId,
4626
- [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4627
- [INDEX_NAME_VAR]: targetIndex
4628
- })) : void 0;
4629
- const searchTools = makeToolContext(repoRoot);
4630
- async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4631
- if (agentRuns > 0) ctx.recordStepExecution();
4632
- agentRuns += 1;
4633
- return runAgent({
4634
- instructions: buildAgentInstructions(
4635
- currentUseCase,
4636
- input,
4637
- extraInstructions
4638
- ),
4639
- tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4640
- outputSchema: implementationOutputSchema,
4641
- toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4642
- });
4643
- }
4644
- async function runVerificationUseCase() {
4645
- if (agentRuns > 0) ctx.recordStepExecution();
4646
- agentRuns += 1;
4647
- return runAgent({
4648
- instructions: buildAgentInstructions("verification", input),
4649
- tools: toolsForUseCase("verification"),
4650
- outputSchema: verificationOutputSchema,
4651
- toolContext: searchTools
4652
- });
4653
- }
4654
- if (useCases.includes("ingestion")) {
4655
- let ingestFailureDetail;
4656
- const result = await runImplementationUseCase("ingestion");
4657
- summaries.push(formatSummary("ingestion", result.summary));
4658
- ingestCommand = result.ingestCommand;
4659
- const ingestionContext = ingestionTools ?? searchTools;
4660
- const executions = ingestionContext.shell.executions;
4661
- const {
4662
- run: ingestRun,
4663
- attempt: ingestAttempt,
4664
- recordCount
4665
- } = ingestOutcome(executions, ingestCommand);
4666
- ingestScriptRan = ingestRun != null;
4667
- ingestRecordCount = recordCount;
4668
- ingestDurationMs = ingestRun?.durationMs;
4669
- if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4670
- summaries.push(
4671
- "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
4672
- );
4673
- logger.warn(
4674
- { ingestCommand },
4675
- "implement: ingestion ran without a reviewScript call"
4676
- );
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
+ });
4677
4729
  }
4678
- if (ingestScriptRan) {
4679
- ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4680
- if (ingestRecordCount != null) {
4681
- track("AI Wizard Ingest Successful", {
4682
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4683
- record_count: ingestRecordCount,
4684
- duration_ms: ingestDurationMs ?? 0
4685
- });
4686
- }
4687
- } else {
4688
- const { reason, detail } = ingestFailure(ingestAttempt, executions);
4689
- ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4690
- ingestFailureDetail = detail;
4691
- summaries.push(
4692
- `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4693
- ${detail}` : ""}`
4694
- );
4695
- logger.warn(
4696
- {
4697
- ingestCommand,
4698
- reason,
4699
- approved: ingestAttempt?.approved,
4700
- exitCode: ingestAttempt?.exitCode,
4701
- timedOut: ingestAttempt?.timedOut,
4702
- commandsRun: executions.length
4703
- },
4704
- "implement: ingestion script did not complete successfully"
4705
- );
4706
- track("Error", {
4707
- step: "Push Data",
4708
- error: `ingestion did not complete: ${reason}`,
4709
- product_area: "AI Wizard"
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
4710
4738
  });
4711
4739
  }
4712
- const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
4713
- await ctx.requestUserInput({
4714
- prompt: "",
4715
- promptType: "enterToContinue",
4716
- options: [],
4717
- // One message per line: Notices.tsx counts a message as one wrapped
4718
- // line, so an embedded newline overflows the panel's height accounting.
4719
- messages: [
4720
- ingestOutcomeMessage,
4721
- ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
4722
- ...commandMessages
4723
- ]
4724
- });
4725
- }
4726
- if (useCases.includes("search")) {
4727
- let extraInstructions = [];
4728
- useWizard.getState().clearWrittenFiles();
4729
- for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
4730
- if (attempt > 1) {
4731
- logger.info(
4732
- {
4733
- attempt,
4734
- maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
4735
- extraInstructions
4736
- },
4737
- "implement: retrying search implementation after failed verification"
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."
4738
4758
  );
4739
- }
4740
- const { summary } = await runImplementationUseCase(
4741
- "search",
4742
- extraInstructions
4743
- );
4744
- summaries.push(formatSummary("search", summary));
4745
- const verification = await runVerificationUseCase();
4746
- summaries.push(formatSummary("verification", verification.summary));
4747
- if (verification.sufficient) {
4748
- ctx.setUserInput("implementation", "success");
4749
- const searchFilesChanged = [
4750
- ...new Set(useWizard.getState().writtenFiles)
4751
- ].map((file) => relative6(repoRoot, file));
4752
- track("AI Wizard Frontend Component Generated", {
4753
- filePaths: searchFilesChanged
4754
- });
4755
- track("AI Wizard Wired to UI", {
4756
- location_heuristic: searchLocation ?? "unknown"
4757
- });
4758
- break;
4759
- }
4760
- if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
4761
- ctx.setUserInput("implementation", "fail");
4762
- throw new Error(
4763
- `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
4759
+ logger.warn(
4760
+ { ingestCommand },
4761
+ "implement: ingestion ran without a reviewScript call"
4764
4762
  );
4765
4763
  }
4766
- extraInstructions = verificationRetryInstructions(verification);
4767
- }
4768
- let searchKey;
4769
- let searchKeyError;
4770
- if (appId) {
4771
- try {
4772
- const resolved2 = await resolveSearchOnlyKey(
4773
- targetIndex,
4774
- appId,
4775
- envSearchKey
4776
- );
4777
- searchKey = resolved2.key;
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;
4778
4777
  summaries.push(
4779
- 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}.`
4778
+ `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4779
+ ${detail}` : ""}`
4780
4780
  );
4781
- } catch (err) {
4782
- searchKeyError = err.message;
4783
4781
  logger.warn(
4784
- { err: searchKeyError },
4785
- "implement: could not provision a search-only API key; the .env value stays a placeholder"
4782
+ {
4783
+ ingestCommand,
4784
+ reason,
4785
+ approved: ingestAttempt?.approved,
4786
+ exitCode: ingestAttempt?.exitCode,
4787
+ timedOut: ingestAttempt?.timedOut,
4788
+ commandsRun: executions.length
4789
+ },
4790
+ "implement: ingestion script did not complete successfully"
4786
4791
  );
4792
+ track("Error", {
4793
+ step: "Push Data",
4794
+ error: `ingestion did not complete: ${reason}`,
4795
+ product_area: "AI Wizard"
4796
+ });
4787
4797
  }
4788
- }
4789
- finalSearchEnvVars = searchEnvVars(language, targetIndex, appId, searchKey);
4790
- const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4791
- (v) => !v.value.startsWith("<")
4792
- );
4793
- if (resolvedSearchEnvVars.length > 0) {
4794
- const written = await writeSearchEnvValues(
4795
- repoRoot,
4796
- resolvedSearchEnvVars
4797
- );
4798
- if (written.length > 0) {
4799
- summaries.push(`Wrote ${written.join(", ")} to .env.`);
4798
+ const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4799
+ if (ingestCommand) {
4800
+ commandMessages.push(`Ingestion command: ${ingestCommand}`);
4800
4801
  }
4801
- const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
4802
- if (ignored === "added") {
4803
- summaries.push("Added .env to .gitignore.");
4804
- } else if (ignored === "tracked") {
4805
- summaries.push(
4806
- '\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.'
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
+ });
4816
+ }
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
4834
  );
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
+ }
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
+ }
4808
4879
  }
4809
- const stale = [];
4810
- for (const v of resolvedSearchEnvVars) {
4811
- if (written.includes(v.name)) continue;
4812
- const current = await readEnvVar(repoRoot, v.name);
4813
- if (current && current !== v.value) stale.push(v);
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
4893
+ );
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
+ }
4814
4920
  }
4815
- if (stale.length > 0 && !envAppIdMismatch) {
4921
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4922
+ (v) => v.value.startsWith("<")
4923
+ );
4924
+ if (unresolvedSearchEnvVars.length > 0) {
4816
4925
  summaries.push(
4817
- `\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.`
4818
- );
4819
- logger.warn(
4820
- { vars: stale.map((v) => v.name) },
4821
- "implement: .env holds different values for the resolved search credentials; not overwriting them"
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}` : "")
4822
4928
  );
4823
4929
  }
4930
+ } else {
4931
+ ctx.setUserInput("implementation", "success");
4824
4932
  }
4825
- const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4826
- (v) => v.value.startsWith("<")
4827
- );
4828
- if (unresolvedSearchEnvVars.length > 0) {
4829
- summaries.push(
4830
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
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"
4831
4937
  );
4832
4938
  }
4833
- } else {
4834
- ctx.setUserInput("implementation", "success");
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);
4835
4954
  }
4836
- return {
4837
- ingestionSource,
4838
- summary: summaries.join("\n\n"),
4839
- ...useCases.includes("ingestion") && ingestCommand ? {
4840
- ingestCommand,
4841
- ingestScriptRan,
4842
- ...ingestRecordCount != null ? { ingestRecordCount } : {},
4843
- ...ingestDurationMs != null ? { ingestDurationMs } : {}
4844
- } : {},
4845
- ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4846
- };
4847
4955
  }
4848
4956
 
4849
4957
  // src/workflows/default.ts
@@ -4915,7 +5023,10 @@ var defaultWorkflow = {
4915
5023
  ctx.notify({
4916
5024
  messages: ["Building your Algolia search experience\u2026"]
4917
5025
  });
4918
- return implement(ctx, ["search"]);
5026
+ const ingestion2 = ctx.getStepOutput(
5027
+ "ingestion"
5028
+ );
5029
+ return implement(ctx, ["search"], ingestion2?.worktreePath);
4919
5030
  }
4920
5031
  }),
4921
5032
  defineStep({
@@ -4930,8 +5041,9 @@ var defaultWorkflow = {
4930
5041
  "ingestion"
4931
5042
  );
4932
5043
  return reviewStep(ctx, {
4933
- // ingestCommand was already shown verbatim as a notice; an
4934
- // LLM-paraphrased restatement in nextSteps risks being wrong.
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.
4935
5047
  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."
4936
5048
  });
4937
5049
  }
@@ -4977,6 +5089,7 @@ var selectIndex = {
4977
5089
  selection: "wizard_seed_products"
4978
5090
  };
4979
5091
  var ingestion = {
5092
+ filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
4980
5093
  summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
4981
5094
  ingestCommand: "node algolia/ingest.mjs",
4982
5095
  ingestScriptRan: true,
@@ -4988,6 +5101,7 @@ var confirmFramework2 = {
4988
5101
  frameworks: projectScan2.frameworks
4989
5102
  };
4990
5103
  var search = {
5104
+ filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
4991
5105
  summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
4992
5106
  ingestionSource: "generated",
4993
5107
  searchEnvVars: [
@@ -5000,7 +5114,7 @@ var review = {
5000
5114
  "Ingested 25 generated Product records into wizard_seed_products.",
5001
5115
  "Added an InstantSearch search experience to the shared header."
5002
5116
  ],
5003
- reviewPrompt: "Review the Algolia ingestion and search changes.",
5117
+ reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
5004
5118
  nextSteps: ["Point the ingestion script at your real product data."]
5005
5119
  };
5006
5120
  var SEEDS = {
@@ -5117,9 +5231,9 @@ Options:
5117
5231
  steps pre-filled with test data. Pass with no value to print
5118
5232
  the step ids. See CONTRIBUTING.md.
5119
5233
  --no-telemetry Send no telemetry or analytics for this run.
5120
- --reset-on-run Wipe this project's wizard state (run state, AI consent)
5121
- before starting, so the run behaves like a first-ever
5122
- run. Also drops every API key the wizard has
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
5123
5237
  stored in your keychain (or, where the platform has none,
5124
5238
  the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
5125
5239
  for this project and any other, so later runs create new
@@ -5159,7 +5273,7 @@ function parseCliArgs(argv) {
5159
5273
  }
5160
5274
 
5161
5275
  // src/lib/resetState.ts
5162
- import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
5276
+ import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
5163
5277
  import { join as join13 } from "node:path";
5164
5278
  var KEEP = ["wizard.log"];
5165
5279
  async function resetProjectState() {
@@ -5167,7 +5281,7 @@ async function resetProjectState() {
5167
5281
  await forgetResolvedKeys();
5168
5282
  let entries;
5169
5283
  try {
5170
- entries = await readdir3(dir);
5284
+ entries = await readdir4(dir);
5171
5285
  } catch {
5172
5286
  return { dir, removed: [] };
5173
5287
  }
@@ -5180,6 +5294,45 @@ async function resetProjectState() {
5180
5294
  return { dir, removed: targets };
5181
5295
  }
5182
5296
 
5297
+ // src/lib/terminalSize.ts
5298
+ var MIN_ROWS = 30;
5299
+ var MIN_COLUMNS = 100;
5300
+ async function requestTerminalSize(stdout = process.stdout, {
5301
+ rows = MIN_ROWS,
5302
+ columns = MIN_COLUMNS,
5303
+ timeoutMs = 150,
5304
+ settleMs = 120
5305
+ } = {}) {
5306
+ if (!stdout.isTTY) return;
5307
+ const currentRows = stdout.rows ?? 0;
5308
+ const currentColumns = stdout.columns ?? 0;
5309
+ if (currentRows >= rows && currentColumns >= columns) return;
5310
+ stdout.write(
5311
+ `\x1B[8;${Math.max(currentRows, rows)};${Math.max(currentColumns, columns)}t`
5312
+ );
5313
+ const resized = await waitForResize(stdout, timeoutMs);
5314
+ if (!resized) return;
5315
+ await delay(settleMs);
5316
+ stdout.write("\x1B[2J\x1B[H");
5317
+ }
5318
+ function waitForResize(stdout, timeoutMs) {
5319
+ return new Promise((resolve4) => {
5320
+ const finish = (didResize) => () => {
5321
+ clearTimeout(timer);
5322
+ stdout.off("resize", onResize);
5323
+ resolve4(didResize);
5324
+ };
5325
+ const onResize = finish(true);
5326
+ const timer = setTimeout(finish(false), timeoutMs);
5327
+ stdout.once("resize", onResize);
5328
+ });
5329
+ }
5330
+ function delay(ms) {
5331
+ return new Promise((resolve4) => {
5332
+ setTimeout(resolve4, ms);
5333
+ });
5334
+ }
5335
+
5183
5336
  // src/main.tsx
5184
5337
  import { jsx as jsx15 } from "react/jsx-runtime";
5185
5338
  async function startup() {
@@ -5264,6 +5417,7 @@ async function run(workflow) {
5264
5417
  }
5265
5418
  runWorkflow(workflow, app.id);
5266
5419
  }
5420
+ await requestTerminalSize();
5267
5421
  var started = await startup();
5268
5422
  if (typeof started === "number") {
5269
5423
  process.exitCode = started;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.19.0-rc.111.189",
3
+ "version": "0.19.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {