@algolia/wizard 0.15.0 → 0.17.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 (2) hide show
  1. package/dist/main.js +288 -114
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2246,7 +2246,7 @@ async function ensureApplication() {
2246
2246
  }
2247
2247
 
2248
2248
  // src/workflows/default.ts
2249
- import { z as z28 } from "zod";
2249
+ import { z as z29 } from "zod";
2250
2250
 
2251
2251
  // src/actions/listIndices.ts
2252
2252
  import { z as z5 } from "zod";
@@ -2507,8 +2507,8 @@ function writeFileTool(ctx) {
2507
2507
  // src/lib/tools/writeAlgoliaCredentials.ts
2508
2508
  import { tool as tool6 } from "ai";
2509
2509
  import z13 from "zod";
2510
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2511
- import { dirname as dirname5 } from "node:path";
2510
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
2511
+ import { dirname as dirname5, relative as relative3 } from "node:path";
2512
2512
 
2513
2513
  // src/lib/algoliaApiKey.ts
2514
2514
  import { z as z12 } from "zod";
@@ -2690,6 +2690,79 @@ async function resolveSearchOnlyKey(index, appId, envKey) {
2690
2690
  );
2691
2691
  }
2692
2692
 
2693
+ // src/lib/gitignore.ts
2694
+ import { execFile } from "node:child_process";
2695
+ import { lstat as lstat2, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2696
+ import { join as join8, relative as relative2 } from "node:path";
2697
+ var GIT_ENV_OVERRIDES = [
2698
+ "GIT_DIR",
2699
+ "GIT_WORK_TREE",
2700
+ "GIT_INDEX_FILE",
2701
+ "GIT_OBJECT_DIRECTORY",
2702
+ "GIT_COMMON_DIR"
2703
+ ];
2704
+ function gitSucceeds(root, args) {
2705
+ const env = { ...process.env };
2706
+ for (const key of GIT_ENV_OVERRIDES) delete env[key];
2707
+ return new Promise((resolve4) => {
2708
+ execFile("git", ["-C", root, ...args], { env }, (err) => {
2709
+ if (!err) return resolve4(true);
2710
+ resolve4(
2711
+ err.code === 1 ? false : void 0
2712
+ );
2713
+ });
2714
+ });
2715
+ }
2716
+ function isIgnoredByRule(root, relPath) {
2717
+ return gitSucceeds(root, ["check-ignore", "-q", "--no-index", "--", relPath]);
2718
+ }
2719
+ function isTracked(root, relPath) {
2720
+ return gitSucceeds(root, ["ls-files", "--error-unmatch", "--", relPath]);
2721
+ }
2722
+ async function inspect(root, target) {
2723
+ const relPath = relative2(root, target);
2724
+ if (!relPath || relPath.startsWith("..")) {
2725
+ return { ignoredByRule: void 0, tracked: false };
2726
+ }
2727
+ const ignoredByRule = await isIgnoredByRule(root, relPath);
2728
+ if (ignoredByRule === void 0) {
2729
+ logger.warn(
2730
+ { root, relPath },
2731
+ "gitignore: git check-ignore could not answer; not reporting on this path"
2732
+ );
2733
+ return { ignoredByRule, tracked: false };
2734
+ }
2735
+ return { ignoredByRule, tracked: await isTracked(root, relPath) };
2736
+ }
2737
+ async function ensureGitIgnored(root, target) {
2738
+ const { ignoredByRule, tracked } = await inspect(root, target);
2739
+ if (ignoredByRule === void 0) return "unknown";
2740
+ if (ignoredByRule) return tracked ? "tracked" : "covered";
2741
+ const pattern = relative2(root, target);
2742
+ const gitIgnore = join8(root, ".gitignore");
2743
+ try {
2744
+ const link = await lstat2(gitIgnore).catch(() => null);
2745
+ if (link?.isSymbolicLink()) {
2746
+ logger.warn(
2747
+ { gitIgnore },
2748
+ "gitignore: root .gitignore is a symlink; not writing to it"
2749
+ );
2750
+ return "unknown";
2751
+ }
2752
+ const existing = link ? await readFile5(gitIgnore, "utf8") : "";
2753
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2754
+ await writeFile4(gitIgnore, `${existing}${prefix}${pattern}
2755
+ `, "utf8");
2756
+ return tracked ? "tracked" : "added";
2757
+ } catch (err) {
2758
+ logger.warn(
2759
+ { gitIgnore, pattern, err: err.message },
2760
+ "gitignore: could not add the pattern; continuing"
2761
+ );
2762
+ return "unknown";
2763
+ }
2764
+ }
2765
+
2693
2766
  // src/lib/tools/writeAlgoliaCredentials.ts
2694
2767
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2695
2768
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
@@ -2722,7 +2795,7 @@ function upsertEnv(content, name, value) {
2722
2795
  }
2723
2796
  function writeCredentialsTool(ctx) {
2724
2797
  return tool6({
2725
- description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application.`,
2798
+ description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself.`,
2726
2799
  inputSchema: z13.object({
2727
2800
  filePath: z13.string().describe(
2728
2801
  'Path to the env file to write credentials into (e.g. ".env")'
@@ -2743,7 +2816,7 @@ function writeCredentialsTool(ctx) {
2743
2816
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2744
2817
  }
2745
2818
  try {
2746
- existing = await readFile5(resolved2.target, "utf8");
2819
+ existing = await readFile6(resolved2.target, "utf8");
2747
2820
  } catch (err) {
2748
2821
  if (err.code !== "ENOENT") throw err;
2749
2822
  }
@@ -2782,6 +2855,7 @@ function writeCredentialsTool(ctx) {
2782
2855
  return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2783
2856
  }
2784
2857
  }
2858
+ let wrote;
2785
2859
  try {
2786
2860
  const updated = [
2787
2861
  ...credentials,
@@ -2793,7 +2867,7 @@ function writeCredentialsTool(ctx) {
2793
2867
  existing
2794
2868
  );
2795
2869
  await mkdir4(dirname5(resolved2.target), { recursive: true });
2796
- await writeFile4(resolved2.target, updated, "utf8");
2870
+ await writeFile5(resolved2.target, updated, "utf8");
2797
2871
  const sentences = [
2798
2872
  `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
2799
2873
  ];
@@ -2807,19 +2881,33 @@ function writeCredentialsTool(ctx) {
2807
2881
  `Skipped ${present.join(" and ")}: already defined there.`
2808
2882
  );
2809
2883
  }
2810
- return [...sentences, ...notes].join(" ");
2884
+ wrote = [...sentences, ...notes].join(" ");
2811
2885
  } catch (err) {
2812
2886
  return `Error writing credentials to ${filePath}: ${err.message}`;
2813
2887
  }
2888
+ return wrote + await gitIgnoreOutcome(ctx, resolved2.target);
2814
2889
  }
2815
2890
  });
2816
2891
  }
2892
+ async function gitIgnoreOutcome(ctx, target) {
2893
+ const name = relative3(ctx.root, target);
2894
+ switch (await ensureGitIgnored(ctx.root, target)) {
2895
+ case "added":
2896
+ return ` Added "${name}" to .gitignore so the credentials are not stageable.`;
2897
+ case "tracked":
2898
+ return ` Warning: ${name} is already tracked by git, and a .gitignore rule cannot un-stage it. Tell the user with notifyUser to run "git rm --cached ${name}" before committing.`;
2899
+ case "unknown":
2900
+ return ` Warning: could not confirm ${name} is gitignored. Tell the user with notifyUser to check before committing.`;
2901
+ case "covered":
2902
+ return "";
2903
+ }
2904
+ }
2817
2905
 
2818
2906
  // src/lib/tools/searchFiles.ts
2819
2907
  import { tool as tool7 } from "ai";
2820
2908
  import z14 from "zod";
2821
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2822
- import { join as join8 } from "node:path";
2909
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
2910
+ import { join as join9 } from "node:path";
2823
2911
  var MAX_QUERY_LENGTH = 1e3;
2824
2912
  var SKIP_DIRS = /* @__PURE__ */ new Set([
2825
2913
  "node_modules",
@@ -2834,7 +2922,7 @@ async function walkFiles(dir) {
2834
2922
  const out = [];
2835
2923
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2836
2924
  if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2837
- const full = join8(dir, e.name);
2925
+ const full = join9(dir, e.name);
2838
2926
  if (e.isDirectory()) out.push(...await walkFiles(full));
2839
2927
  else if (e.isFile()) out.push(full);
2840
2928
  }
@@ -2867,7 +2955,7 @@ function searchFilesTool(ctx) {
2867
2955
  for (const file of await walkFiles(resolved2.target)) {
2868
2956
  let content;
2869
2957
  try {
2870
- content = await readFile6(file, "utf8");
2958
+ content = await readFile7(file, "utf8");
2871
2959
  } catch {
2872
2960
  continue;
2873
2961
  }
@@ -2890,7 +2978,16 @@ function searchFilesTool(ctx) {
2890
2978
  // src/lib/tools/runShell.ts
2891
2979
  import { tool as tool8 } from "ai";
2892
2980
  import z15 from "zod";
2893
- import { relative as relative2 } from "node:path";
2981
+ import { relative as relative4 } from "node:path";
2982
+
2983
+ // src/lib/tools/utils/prompt.ts
2984
+ var chain = Promise.resolve();
2985
+ function serializePrompt(work) {
2986
+ const result = chain.then(work);
2987
+ chain = result.catch(() => {
2988
+ });
2989
+ return result;
2990
+ }
2894
2991
 
2895
2992
  // src/lib/tools/utils/runShell.ts
2896
2993
  import { spawn as spawn2 } from "node:child_process";
@@ -2922,7 +3019,8 @@ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), sh
2922
3019
  cwd,
2923
3020
  limits: { ...limits },
2924
3021
  counts: { list: 0, search: 0, read: 0, shell: 0 },
2925
- shell: shell2
3022
+ shell: shell2,
3023
+ reviewed: []
2926
3024
  };
2927
3025
  }
2928
3026
 
@@ -2987,7 +3085,7 @@ function runShell(command, opts) {
2987
3085
  // src/lib/tools/runShell.ts
2988
3086
  function storeApproval(root) {
2989
3087
  return async (req) => {
2990
- const rel = relative2(root, req.cwd);
3088
+ const rel = relative4(root, req.cwd);
2991
3089
  const answer = await useWizard.getState().requestUserInput({
2992
3090
  prompt: "Run this command?",
2993
3091
  promptType: "commandApproval",
@@ -3000,13 +3098,6 @@ function storeApproval(root) {
3000
3098
  return answer === "approve" ? "approve" : "reject";
3001
3099
  };
3002
3100
  }
3003
- var shellChain = Promise.resolve();
3004
- function serialize(work) {
3005
- const result = shellChain.then(work);
3006
- shellChain = result.catch(() => {
3007
- });
3008
- return result;
3009
- }
3010
3101
  async function approveAndRun(ctx, command, cwd, explanation) {
3011
3102
  const decision = await ctx.shell.approve({ command, cwd, explanation });
3012
3103
  if (decision === "reject") {
@@ -3070,20 +3161,81 @@ function runShellTool(ctx) {
3070
3161
  const resolved2 = resolveInRoot(ctx, cwd ?? ".");
3071
3162
  if (!resolved2.ok) return resolved2.error;
3072
3163
  logger.info({ command, cwd: resolved2.target }, "called runShell tool");
3073
- return serialize(
3164
+ return serializePrompt(
3074
3165
  () => approveAndRun(ctx, command, resolved2.target, explanation)
3075
3166
  );
3076
3167
  }
3077
3168
  });
3078
3169
  }
3079
3170
 
3171
+ // src/lib/tools/reviewScript.ts
3172
+ import { tool as tool9 } from "ai";
3173
+ import z16 from "zod";
3174
+ import { stat as stat2 } from "node:fs/promises";
3175
+ import { relative as relative5 } from "node:path";
3176
+
3177
+ // src/lib/editor.ts
3178
+ import { spawn as spawn3 } from "node:child_process";
3179
+ function openInEditor(filePath) {
3180
+ const { command, args } = process.platform === "darwin" ? (
3181
+ // -t forces the default *text* editor; plain `open` on an extension
3182
+ // with no handler (.mjs, .py) pops a "choose an application" dialog.
3183
+ { command: "open", args: ["-t", filePath] }
3184
+ ) : process.platform === "win32" ? { command: "cmd", args: ["/c", "start", "", filePath] } : { command: "xdg-open", args: [filePath] };
3185
+ try {
3186
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
3187
+ child.on("error", () => {
3188
+ });
3189
+ child.unref();
3190
+ } catch {
3191
+ }
3192
+ }
3193
+
3194
+ // src/lib/tools/reviewScript.ts
3195
+ function reviewScriptTool(ctx) {
3196
+ return tool9({
3197
+ description: "Show a script you have written to the user and wait while they read it. Call this as soon as the script is finished and before you run it for the first time, so nothing with a side effect happens behind their back. It opens the file in their editor and returns once they have confirmed.",
3198
+ inputSchema: z16.object({
3199
+ filePath: z16.string().describe(
3200
+ "Path to the script to review, relative to the project root."
3201
+ )
3202
+ }),
3203
+ execute: async ({ filePath }) => {
3204
+ logger.info({ filePath }, "called reviewScript tool");
3205
+ const resolved2 = resolveInRoot(ctx, filePath);
3206
+ if (resolved2.ok === false) return resolved2.error;
3207
+ const isFile = await stat2(resolved2.target).then(
3208
+ (s) => s.isFile(),
3209
+ () => false
3210
+ );
3211
+ if (!isFile) {
3212
+ return `Refused: ${filePath} is not a file. Write the script first, then review the path you wrote.`;
3213
+ }
3214
+ return serializePrompt(async () => {
3215
+ openInEditor(resolved2.target);
3216
+ await useWizard.getState().requestUserInput({
3217
+ prompt: "",
3218
+ promptType: "enterToContinue",
3219
+ options: [],
3220
+ messages: [
3221
+ `Please review ${relative5(ctx.root, resolved2.target)} before it runs:`,
3222
+ resolved2.target
3223
+ ]
3224
+ });
3225
+ ctx.reviewed.push(resolved2.target);
3226
+ return "The user has reviewed the script. You may run it now.";
3227
+ });
3228
+ }
3229
+ });
3230
+ }
3231
+
3080
3232
  // src/lib/tools/generateRecord.ts
3081
- import { tool as tool9, generateText, Output, NoObjectGeneratedError } from "ai";
3233
+ import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
3082
3234
  import { createAnthropic } from "@ai-sdk/anthropic";
3083
3235
  import { nanoid as nanoid2 } from "nanoid";
3084
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
3236
+ import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
3085
3237
  import { dirname as dirname6 } from "node:path";
3086
- import z16 from "zod";
3238
+ import z17 from "zod";
3087
3239
  var DATA_DIR = ".algolia-wizard/data";
3088
3240
  var RECORD_MODEL = "claude-haiku-4-5";
3089
3241
  var MAX_RECORDS = 100;
@@ -3093,19 +3245,19 @@ var anthropic = createAnthropic({
3093
3245
  apiKey: process.env.PROVIDER_API_KEY ?? ""
3094
3246
  });
3095
3247
  function generateRecordTool(ctx) {
3096
- return tool9({
3248
+ return tool10({
3097
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.",
3098
- inputSchema: z16.object({
3099
- entityName: z16.string().describe("Name of the entity to generate records for."),
3100
- attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
3101
- count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
3102
- hint: z16.string().optional().describe("Optional context to steer realistic values.")
3250
+ inputSchema: z17.object({
3251
+ entityName: z17.string().describe("Name of the entity to generate records for."),
3252
+ attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
3253
+ count: z17.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
3254
+ hint: z17.string().optional().describe("Optional context to steer realistic values.")
3103
3255
  }),
3104
3256
  execute: async ({ entityName, attributes, count, hint }) => {
3105
3257
  logger.info({ entityName, count }, "called generateRecord tool");
3106
3258
  try {
3107
- const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
3108
- const recordSchema = z16.object(
3259
+ const value = z17.union([z17.string(), z17.number(), z17.boolean(), z17.null()]);
3260
+ const recordSchema = z17.object(
3109
3261
  Object.fromEntries(attributes.map((attr) => [attr, value]))
3110
3262
  );
3111
3263
  const generateBatch = async (batchCount) => {
@@ -3115,8 +3267,8 @@ function generateRecordTool(ctx) {
3115
3267
  const { output } = await generateText({
3116
3268
  model: anthropic(RECORD_MODEL),
3117
3269
  output: Output.object({
3118
- schema: z16.object({
3119
- records: z16.array(recordSchema).length(batchCount)
3270
+ schema: z17.object({
3271
+ records: z17.array(recordSchema).length(batchCount)
3120
3272
  })
3121
3273
  }),
3122
3274
  prompt: [
@@ -3158,7 +3310,7 @@ function generateRecordTool(ctx) {
3158
3310
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
3159
3311
  }
3160
3312
  await mkdir5(dirname6(resolved2.target), { recursive: true });
3161
- await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
3313
+ await writeFile6(resolved2.target, JSON.stringify(records, null, 2), "utf8");
3162
3314
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
3163
3315
  return {
3164
3316
  filePath: relPath,
@@ -3173,13 +3325,13 @@ function generateRecordTool(ctx) {
3173
3325
  }
3174
3326
 
3175
3327
  // src/lib/tools/notifyUser.ts
3176
- import { tool as tool10 } from "ai";
3177
- import z17 from "zod";
3328
+ import { tool as tool11 } from "ai";
3329
+ import z18 from "zod";
3178
3330
  function notifyUserTool() {
3179
- return tool10({
3331
+ return tool11({
3180
3332
  description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work.`,
3181
- inputSchema: z17.object({
3182
- message: z17.string().describe(
3333
+ inputSchema: z18.object({
3334
+ message: z18.string().describe(
3183
3335
  "Short, plain-language description of what you are doing now."
3184
3336
  )
3185
3337
  }),
@@ -3223,6 +3375,7 @@ function createTools(ctx, { output, tools }) {
3223
3375
  ),
3224
3376
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
3225
3377
  runShell: withLogging("runShell", runShellTool(ctx)),
3378
+ reviewScript: withLogging("reviewScript", reviewScriptTool(ctx)),
3226
3379
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
3227
3380
  notifyUser: withLogging("notifyUser", notifyUserTool())
3228
3381
  };
@@ -3344,10 +3497,10 @@ async function runAgent(req) {
3344
3497
  }
3345
3498
 
3346
3499
  // src/actions/detectLanguage.ts
3347
- import z20 from "zod";
3348
- var detectLanguageSchema = z20.object({
3349
- languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
3350
- frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
3500
+ import z21 from "zod";
3501
+ var detectLanguageSchema = z21.object({
3502
+ languages: z21.array(z21.object({ name: z21.string(), version: z21.string() })),
3503
+ frameworks: z21.array(z21.object({ name: z21.string(), version: z21.string() }))
3351
3504
  });
3352
3505
  var detectLanguage = () => runAgent({
3353
3506
  instructions: [
@@ -3365,31 +3518,31 @@ var detectLanguage = () => runAgent({
3365
3518
  });
3366
3519
 
3367
3520
  // src/actions/analyzeCodebase.ts
3368
- import z21 from "zod";
3521
+ import z22 from "zod";
3369
3522
  var READONLY_TOOLS = [
3370
3523
  "listFiles",
3371
3524
  "changeDirectory",
3372
3525
  "readFile",
3373
3526
  "searchFiles"
3374
3527
  ];
3375
- var ingestionAnalysisSchema = z21.object({
3376
- ingestionAnalysis: z21.array(
3377
- z21.object({
3378
- name: z21.string(),
3379
- paths: z21.array(z21.string()),
3528
+ var ingestionAnalysisSchema = z22.object({
3529
+ ingestionAnalysis: z22.array(
3530
+ z22.object({
3531
+ name: z22.string(),
3532
+ paths: z22.array(z22.string()),
3380
3533
  // indexable fields the agent found for this entity
3381
- attributes: z21.array(z21.string())
3534
+ attributes: z22.array(z22.string())
3382
3535
  })
3383
3536
  )
3384
3537
  });
3385
- var searchImplementationAnalysisSchema = z21.object({
3386
- searchImplementationAnalysis: z21.string()
3538
+ var searchImplementationAnalysisSchema = z22.object({
3539
+ searchImplementationAnalysis: z22.string()
3387
3540
  });
3388
- var verificationSchema = z21.object({
3389
- verification: z21.array(z21.string())
3541
+ var verificationSchema = z22.object({
3542
+ verification: z22.array(z22.string())
3390
3543
  });
3391
3544
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
3392
- var analyzeCodebaseSchema = z21.object({
3545
+ var analyzeCodebaseSchema = z22.object({
3393
3546
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3394
3547
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
3395
3548
  verification: verificationSchema.shape.verification.optional(),
@@ -3451,7 +3604,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3451
3604
  // package.json
3452
3605
  var package_default = {
3453
3606
  name: "@algolia/wizard",
3454
- version: "0.15.0",
3607
+ version: "0.17.0",
3455
3608
  description: "Magically implement Algolia functionality in your codebase",
3456
3609
  type: "module",
3457
3610
  engines: {
@@ -3570,8 +3723,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
3570
3723
  }
3571
3724
 
3572
3725
  // src/actions/confirmLanguage.ts
3573
- import z23 from "zod";
3574
- var confirmLanguageSchema = z23.object({
3726
+ import z24 from "zod";
3727
+ var confirmLanguageSchema = z24.object({
3575
3728
  languages: detectLanguageSchema.shape.languages
3576
3729
  });
3577
3730
  async function confirmLanguage(ctx) {
@@ -3592,8 +3745,8 @@ async function confirmLanguage(ctx) {
3592
3745
  }
3593
3746
 
3594
3747
  // src/actions/confirmFramework.ts
3595
- import z24 from "zod";
3596
- var confirmFrameworkSchema = z24.object({
3748
+ import z25 from "zod";
3749
+ var confirmFrameworkSchema = z25.object({
3597
3750
  frameworks: detectLanguageSchema.shape.frameworks
3598
3751
  });
3599
3752
  var CURATED_FRAMEWORKS = [
@@ -3726,8 +3879,8 @@ async function promptUser(ctx, params) {
3726
3879
  }
3727
3880
 
3728
3881
  // src/actions/confirmEntities.ts
3729
- import z25 from "zod";
3730
- var confirmEntitiesSchema = z25.object({
3882
+ import z26 from "zod";
3883
+ var confirmEntitiesSchema = z26.object({
3731
3884
  // Final detection — the focused re-run may supersede project-scan's.
3732
3885
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3733
3886
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3797,15 +3950,15 @@ async function confirmEntities(ctx) {
3797
3950
  }
3798
3951
 
3799
3952
  // src/actions/review.ts
3800
- import { z as z26 } from "zod";
3801
- var reviewSchema = z26.object({
3953
+ import { z as z27 } from "zod";
3954
+ var reviewSchema = z27.object({
3802
3955
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3803
3956
  // not one entry per workflow step — a step's raw output can be a long,
3804
3957
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3805
3958
  // that 1:1 is what made the old per-step summary an unreadable wall of text.
3806
- summaryPoints: z26.array(z26.string()),
3807
- reviewPrompt: z26.string(),
3808
- nextSteps: z26.array(z26.string())
3959
+ summaryPoints: z27.array(z27.string()),
3960
+ reviewPrompt: z27.string(),
3961
+ nextSteps: z27.array(z27.string())
3809
3962
  });
3810
3963
  function formatCompletedSteps(steps) {
3811
3964
  if (!steps.length) return "(no prior steps completed)";
@@ -3856,18 +4009,19 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3856
4009
  };
3857
4010
 
3858
4011
  // src/actions/implement.ts
3859
- import z27 from "zod";
4012
+ import z28 from "zod";
4013
+ import { join as join12 } from "node:path";
3860
4014
 
3861
4015
  // src/lib/worktree.ts
3862
- import { execFile } from "node:child_process";
3863
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3864
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
4016
+ 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";
4018
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
3865
4019
  var MAX_BUFFER = 32 * 1024 * 1024;
3866
4020
  var MAX_WIZARD_WORKTREES = 3;
3867
4021
  var WIZARD_BRANCH_PREFIX = "wizard/implement-";
3868
4022
  function git(args) {
3869
4023
  return new Promise((resolve4, reject) => {
3870
- execFile("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
4024
+ execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
3871
4025
  if (err)
3872
4026
  return reject(
3873
4027
  new Error(
@@ -3892,7 +4046,7 @@ async function isWorkingTreeDirty(repoRoot) {
3892
4046
  return out.trim().length > 0;
3893
4047
  }
3894
4048
  async function pruneOldWorktrees(repoRoot) {
3895
- const dir = join9(stateDir(repoRoot), "worktrees");
4049
+ const dir = join10(stateDir(repoRoot), "worktrees");
3896
4050
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3897
4051
  for (const slug of stale) {
3898
4052
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3903,7 +4057,7 @@ async function pruneOldWorktrees(repoRoot) {
3903
4057
  "worktree",
3904
4058
  "remove",
3905
4059
  "--force",
3906
- join9(dir, slug)
4060
+ join10(dir, slug)
3907
4061
  ]);
3908
4062
  await git(["-C", repoRoot, "branch", "-D", branch]);
3909
4063
  } catch (err) {
@@ -3917,7 +4071,7 @@ async function pruneOldWorktrees(repoRoot) {
3917
4071
  async function createWorktree(repoRoot) {
3918
4072
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3919
4073
  const dirSlug = branch.replace(/\//g, "-");
3920
- const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
4074
+ const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3921
4075
  await git(["-C", repoRoot, "worktree", "prune"]);
3922
4076
  await pruneOldWorktrees(repoRoot);
3923
4077
  await mkdir6(dirname7(path), { recursive: true });
@@ -3931,14 +4085,14 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3931
4085
  }
3932
4086
  const source = isAbsolute2(trimmed) ? trimmed : resolve3(repoRoot, trimmed);
3933
4087
  try {
3934
- if (!(await stat2(source)).isFile()) {
4088
+ if (!(await stat3(source)).isFile()) {
3935
4089
  return { ok: false, reason: `"${sourcePath}" is not a file` };
3936
4090
  }
3937
4091
  } catch {
3938
4092
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3939
4093
  }
3940
- const relPath = join9(ingestDir, basename2(source));
3941
- const dest = join9(worktreePath, relPath);
4094
+ const relPath = join10(ingestDir, basename2(source));
4095
+ const dest = join10(worktreePath, relPath);
3942
4096
  try {
3943
4097
  await mkdir6(dirname7(dest), { recursive: true });
3944
4098
  await copyFile(source, dest);
@@ -3956,7 +4110,7 @@ function hasEnvVar(content, name) {
3956
4110
  async function readEnvVar(worktreePath, name) {
3957
4111
  let content;
3958
4112
  try {
3959
- content = await readFile7(join9(worktreePath, ".env"), "utf8");
4113
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
3960
4114
  } catch (err) {
3961
4115
  if (err.code !== "ENOENT") throw err;
3962
4116
  return void 0;
@@ -3971,10 +4125,10 @@ async function readEnvVar(worktreePath, name) {
3971
4125
  return value;
3972
4126
  }
3973
4127
  async function writeSearchEnvValues(worktreePath, vars) {
3974
- const target = join9(worktreePath, ".env");
4128
+ const target = join10(worktreePath, ".env");
3975
4129
  let existing = "";
3976
4130
  try {
3977
- existing = await readFile7(target, "utf8");
4131
+ existing = await readFile8(target, "utf8");
3978
4132
  } catch (err) {
3979
4133
  if (err.code !== "ENOENT") throw err;
3980
4134
  }
@@ -3983,7 +4137,7 @@ async function writeSearchEnvValues(worktreePath, vars) {
3983
4137
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3984
4138
  const lines = missing.map(({ name, value }) => `${name}=${value}
3985
4139
  `).join("");
3986
- await writeFile6(target, existing + prefix + lines, "utf8");
4140
+ await writeFile7(target, existing + prefix + lines, "utf8");
3987
4141
  return missing.map((v) => v.name);
3988
4142
  }
3989
4143
  async function listChangedFiles(worktreePath) {
@@ -4044,13 +4198,13 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
4044
4198
 
4045
4199
  // src/lib/algoliaDocs.ts
4046
4200
  import { readFileSync, readdirSync, existsSync } from "node:fs";
4047
- import { dirname as dirname8, join as join10 } from "node:path";
4201
+ import { dirname as dirname8, join as join11 } from "node:path";
4048
4202
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4049
- var DOCS_SUBPATH = join10("docs", "algolia-sdk");
4203
+ var DOCS_SUBPATH = join11("docs", "algolia-sdk");
4050
4204
  function findDocsDir() {
4051
4205
  let dir = dirname8(fileURLToPath2(import.meta.url));
4052
4206
  for (; ; ) {
4053
- const candidate = join10(dir, DOCS_SUBPATH);
4207
+ const candidate = join11(dir, DOCS_SUBPATH);
4054
4208
  if (existsSync(candidate)) return candidate;
4055
4209
  const parent = dirname8(dir);
4056
4210
  if (parent === dir) return void 0;
@@ -4073,7 +4227,7 @@ function loadAlgoliaDoc(language) {
4073
4227
  );
4074
4228
  return "";
4075
4229
  }
4076
- return readFileSync(join10(docsDir, files[0]), "utf8").trim();
4230
+ return readFileSync(join11(docsDir, files[0]), "utf8").trim();
4077
4231
  }
4078
4232
  function getNamedDoc(name, language) {
4079
4233
  const docsDir = findDocsDir();
@@ -4081,7 +4235,7 @@ function getNamedDoc(name, language) {
4081
4235
  logger.warn("docs/algolia-sdk not found");
4082
4236
  return "";
4083
4237
  }
4084
- const file = join10(docsDir, `${name}-${language}.md`);
4238
+ const file = join11(docsDir, `${name}-${language}.md`);
4085
4239
  if (!existsSync(file)) {
4086
4240
  logger.warn({ name, language }, "named SDK reference not found");
4087
4241
  return "";
@@ -4106,30 +4260,30 @@ function shellQuote(value) {
4106
4260
  }
4107
4261
 
4108
4262
  // src/actions/implement.ts
4109
- var implementSchema = z27.object({
4110
- filesChanged: z27.array(z27.string()),
4111
- summary: z27.string(),
4112
- worktreePath: z27.string().optional(),
4113
- ingestCommand: z27.string().optional(),
4114
- ingestScriptRan: z27.boolean().optional(),
4115
- ingestRecordCount: z27.number().optional(),
4116
- ingestDurationMs: z27.number().optional(),
4117
- ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
4118
- searchEnvVars: z27.array(
4119
- z27.object({
4120
- name: z27.string(),
4121
- value: z27.string()
4263
+ var implementSchema = z28.object({
4264
+ filesChanged: z28.array(z28.string()),
4265
+ summary: z28.string(),
4266
+ worktreePath: z28.string().optional(),
4267
+ ingestCommand: z28.string().optional(),
4268
+ ingestScriptRan: z28.boolean().optional(),
4269
+ ingestRecordCount: z28.number().optional(),
4270
+ ingestDurationMs: z28.number().optional(),
4271
+ ingestionSource: z28.enum(["local", "fileUpload", "generated"]),
4272
+ searchEnvVars: z28.array(
4273
+ z28.object({
4274
+ name: z28.string(),
4275
+ value: z28.string()
4122
4276
  })
4123
4277
  ).optional()
4124
4278
  });
4125
- var implementationOutputSchema = z27.object({
4126
- summary: z27.string(),
4127
- ingestCommand: z27.string().optional()
4279
+ var implementationOutputSchema = z28.object({
4280
+ summary: z28.string(),
4281
+ ingestCommand: z28.string().optional()
4128
4282
  });
4129
- var verificationOutputSchema = z27.object({
4130
- summary: z27.string(),
4131
- sufficient: z27.boolean(),
4132
- additionalInstructions: z27.string().optional()
4283
+ var verificationOutputSchema = z28.object({
4284
+ summary: z28.string(),
4285
+ sufficient: z28.boolean(),
4286
+ additionalInstructions: z28.string().optional()
4133
4287
  });
4134
4288
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
4135
4289
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -4262,6 +4416,7 @@ function ingestionInstructions(input) {
4262
4416
  "After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
4263
4417
  ...algoliaClientDoc(input),
4264
4418
  "Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
4419
+ "When the script is finished, call reviewScript with its path and wait: running it writes records to a live index, so the developer reads it first. Do not run it before that call returns.",
4265
4420
  'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
4266
4421
  "The summary should be extremely concise.",
4267
4422
  ...sourceSpecificInstructions(input)
@@ -4332,6 +4487,7 @@ var useCaseToolMap = {
4332
4487
  "writeFile",
4333
4488
  "writeCredentials",
4334
4489
  "runShell",
4490
+ "reviewScript",
4335
4491
  "notifyUser"
4336
4492
  ],
4337
4493
  search: [...FS_READ_TOOLS, "writeFile", "runShell", "notifyUser"],
@@ -4586,7 +4742,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4586
4742
  const result = await runImplementationUseCase("ingestion");
4587
4743
  summaries.push(formatSummary("ingestion", result.summary));
4588
4744
  ingestCommand = result.ingestCommand;
4589
- const executions = (ingestionTools ?? searchTools).shell.executions;
4745
+ const ingestionContext = ingestionTools ?? searchTools;
4746
+ const executions = ingestionContext.shell.executions;
4590
4747
  const {
4591
4748
  run: ingestRun,
4592
4749
  attempt: ingestAttempt,
@@ -4595,6 +4752,15 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4595
4752
  ingestScriptRan = ingestRun != null;
4596
4753
  ingestRecordCount = recordCount;
4597
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
+ );
4763
+ }
4598
4764
  if (ingestScriptRan) {
4599
4765
  ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4600
4766
  if (ingestRecordCount != null) {
@@ -4728,6 +4894,14 @@ ${detail}` : ""}`
4728
4894
  if (written.length > 0) {
4729
4895
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4730
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
+ }
4731
4905
  const stale = [];
4732
4906
  for (const v of resolvedSearchEnvVars) {
4733
4907
  if (written.includes(v.name)) continue;
@@ -4816,8 +4990,8 @@ var defaultWorkflow = {
4816
4990
  defineStep({
4817
4991
  id: "select-index",
4818
4992
  title: "Set up index",
4819
- outputSchema: z28.object({
4820
- selection: z28.string()
4993
+ outputSchema: z29.object({
4994
+ selection: z29.string()
4821
4995
  }),
4822
4996
  run: (ctx) => selectIndexStep(ctx)
4823
4997
  }),
@@ -5100,7 +5274,7 @@ function parseCliArgs(argv) {
5100
5274
 
5101
5275
  // src/lib/resetState.ts
5102
5276
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
5103
- import { join as join11 } from "node:path";
5277
+ import { join as join13 } from "node:path";
5104
5278
  var KEEP = ["wizard.log"];
5105
5279
  async function resetProjectState() {
5106
5280
  const dir = stateDir();
@@ -5114,7 +5288,7 @@ async function resetProjectState() {
5114
5288
  const targets = entries.filter((name) => !KEEP.includes(name));
5115
5289
  await Promise.all(
5116
5290
  targets.map(
5117
- (name) => rm2(join11(dir, name), { recursive: true, force: true })
5291
+ (name) => rm2(join13(dir, name), { recursive: true, force: true })
5118
5292
  )
5119
5293
  );
5120
5294
  return { dir, removed: targets };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {