@algolia/wizard 0.10.0 → 0.12.0-rc.104.151

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 +497 -290
  2. package/package.json +2 -1
package/dist/main.js CHANGED
@@ -17,6 +17,9 @@ function npxArgs(args) {
17
17
  return ["--yes", "@algolia/cli@latest", ...args];
18
18
  }
19
19
  var shell = process.platform === "win32";
20
+ function mask(text, secret) {
21
+ return secret ? text.replaceAll(secret, "***") : text;
22
+ }
20
23
  function lineSplitter(emit) {
21
24
  let buffer = "";
22
25
  return {
@@ -40,9 +43,10 @@ var stderrSink = (stream, line) => {
40
43
  if (stream === "stdout") return;
41
44
  wizardSink(stream, line);
42
45
  };
43
- function runAlgoliaCli(args, { onOutput } = {}) {
46
+ function runAlgoliaCli(args, { onOutput, redact } = {}) {
44
47
  const store = useWizard.getState();
45
- const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
48
+ const command = mask(args.join(" "), redact);
49
+ const logId = store.logStart("tool", `algolia ${command}`);
46
50
  return new Promise((resolve4, reject) => {
47
51
  const child = spawn("npx", npxArgs(args), { shell });
48
52
  let stdout = "";
@@ -71,13 +75,13 @@ function runAlgoliaCli(args, { onOutput } = {}) {
71
75
  const failed = stderr.trim();
72
76
  let detail = "";
73
77
  if (failed) {
74
- detail = `: ${failed}`;
78
+ detail = `: ${mask(failed, redact)}`;
75
79
  } else if (stdout.trim()) {
76
80
  detail = " (no stderr; stdout withheld \u2014 it may contain credentials)";
77
81
  }
78
82
  reject(
79
83
  new Error(
80
- `Algolia CLI \`${args.join(" ")}\` failed (exit ${code})${detail}`
84
+ `Algolia CLI \`${command}\` failed (exit ${code})${detail}`
81
85
  )
82
86
  );
83
87
  }
@@ -158,10 +162,14 @@ import { join, resolve } from "node:path";
158
162
  function rootDir() {
159
163
  return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
160
164
  }
161
- function projectSlug(cwd = process.cwd()) {
165
+ var pinnedRoot;
166
+ function setProjectRoot(cwd) {
167
+ pinnedRoot = resolve(cwd);
168
+ }
169
+ function projectSlug(cwd = pinnedRoot ?? process.cwd()) {
162
170
  return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
163
171
  }
164
- function stateDir(cwd = process.cwd()) {
172
+ function stateDir(cwd = pinnedRoot ?? process.cwd()) {
165
173
  return join(rootDir(), projectSlug(cwd));
166
174
  }
167
175
 
@@ -1023,9 +1031,14 @@ var sidebarItems = [
1023
1031
  ];
1024
1032
 
1025
1033
  // src/ui/Welcome.tsx
1026
- import Image, { InkPictureProvider } from "ink-picture";
1034
+ import Image, { TerminalInfoContext, defaultTerminalInfo } from "ink-picture";
1027
1035
  import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
1028
1036
  var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
1037
+ var TERMINAL_INFO = {
1038
+ ...defaultTerminalInfo,
1039
+ supportsUnicode: true,
1040
+ supportsColor: true
1041
+ };
1029
1042
  function SidebarItem({
1030
1043
  title,
1031
1044
  description
@@ -1072,7 +1085,7 @@ function Welcome() {
1072
1085
  flexDirection: "column",
1073
1086
  justifyContent: "center",
1074
1087
  children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
1075
- /* @__PURE__ */ jsx6(InkPictureProvider, { children: /* @__PURE__ */ jsx6(
1088
+ /* @__PURE__ */ jsx6(TerminalInfoContext.Provider, { value: TERMINAL_INFO, children: /* @__PURE__ */ jsx6(
1076
1089
  Image,
1077
1090
  {
1078
1091
  src: IMAGE_PATH,
@@ -1743,18 +1756,17 @@ import "zod";
1743
1756
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1744
1757
  import { join as join5 } from "node:path";
1745
1758
  var configFile = () => join5(stateDir(), "config.json");
1746
- var DEFAULT_CONFIG = {
1759
+ var defaultConfig = () => ({
1747
1760
  version: 1,
1748
1761
  aiConsent: false,
1749
- workflowsRun: [],
1750
- searchApiKeys: {}
1751
- };
1762
+ workflowsRun: []
1763
+ });
1752
1764
  async function loadConfig() {
1753
1765
  try {
1754
1766
  const raw = await readFile2(configFile(), "utf8");
1755
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1767
+ return { ...defaultConfig(), ...JSON.parse(raw) };
1756
1768
  } catch {
1757
- return { ...DEFAULT_CONFIG };
1769
+ return defaultConfig();
1758
1770
  }
1759
1771
  }
1760
1772
  async function saveConfig(config) {
@@ -1766,38 +1778,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
1766
1778
  config.workflowsRun.push({ workflowId, completedAt });
1767
1779
  await saveConfig(config);
1768
1780
  }
1769
- function isStoredSearchKey(value) {
1770
- if (typeof value !== "object" || value === null) return false;
1771
- const { appId, key } = value;
1772
- return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
1773
- }
1774
- function storedSearchKeys(config) {
1775
- const stored = config.searchApiKeys;
1776
- if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
1777
- return {};
1778
- }
1779
- return stored;
1780
- }
1781
- async function getStoredSearchKey(index, appId) {
1782
- const entry = storedSearchKeys(await loadConfig())[index];
1783
- if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
1784
- return entry.key;
1785
- }
1786
- async function storeSearchKey(index, appId, key) {
1787
- const config = await loadConfig();
1788
- config.searchApiKeys = {
1789
- ...storedSearchKeys(config),
1790
- [index]: { appId, key }
1791
- };
1792
- await saveConfig(config);
1793
- }
1794
- async function forgetSearchKey(index) {
1795
- const config = await loadConfig();
1796
- const remaining = { ...storedSearchKeys(config) };
1797
- delete remaining[index];
1798
- config.searchApiKeys = remaining;
1799
- await saveConfig(config);
1800
- }
1801
1781
 
1802
1782
  // src/core/orchestrator.ts
1803
1783
  function defineStep(step) {
@@ -2063,6 +2043,42 @@ function parseJson(text) {
2063
2043
  }
2064
2044
  }
2065
2045
 
2046
+ // src/lib/envAppId.ts
2047
+ import { readFile as readFile3 } from "node:fs/promises";
2048
+ import { join as join6 } from "node:path";
2049
+ var ENV_FILES = [".env", ".env.local"];
2050
+ var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
2051
+ async function findEnvApplicationId(root = process.cwd()) {
2052
+ for (const file of ENV_FILES) {
2053
+ let content;
2054
+ try {
2055
+ content = await readFile3(join6(root, file), "utf8");
2056
+ } catch (err) {
2057
+ if (err.code !== "ENOENT") {
2058
+ logger.warn(
2059
+ { file, err },
2060
+ "could not read env file for an application id"
2061
+ );
2062
+ }
2063
+ continue;
2064
+ }
2065
+ for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
2066
+ const id = readValue(raw);
2067
+ if (id) {
2068
+ logger.info({ file, name, app: id }, "found an application id in env");
2069
+ return { id, name, file };
2070
+ }
2071
+ }
2072
+ }
2073
+ return null;
2074
+ }
2075
+ function readValue(raw) {
2076
+ const trimmed = raw.trim();
2077
+ const quoted = trimmed.match(/^(['"])(.*)\1/);
2078
+ const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
2079
+ return value.length > 0 && !value.startsWith("<") ? value : null;
2080
+ }
2081
+
2066
2082
  // src/lib/algoliaAppPicker.ts
2067
2083
  function secondaryFor(app) {
2068
2084
  return app.plan ? { kind: "badge", value: app.plan } : void 0;
@@ -2077,7 +2093,7 @@ function selectAndReport(app) {
2077
2093
  );
2078
2094
  return selectApplication(app.id);
2079
2095
  }
2080
- async function promptForApplication() {
2096
+ async function promptForApplication(leadIn = []) {
2081
2097
  const store = useWizard.getState();
2082
2098
  const apps = await listApplications();
2083
2099
  if (apps.length === 0) {
@@ -2091,9 +2107,13 @@ async function promptForApplication() {
2091
2107
  { app: only.id },
2092
2108
  "single application on the account; selecting it"
2093
2109
  );
2110
+ for (const line of leadIn) store.pushCliOutput("stdout", line);
2094
2111
  return selectAndReport(only);
2095
2112
  }
2096
- const messages = ["Which Algolia application should the wizard work in?"];
2113
+ const messages = [
2114
+ ...leadIn,
2115
+ "Which Algolia application should the wizard work in?"
2116
+ ];
2097
2117
  for (; ; ) {
2098
2118
  const choice = await store.requestUserInput({
2099
2119
  prompt: "Select an application",
@@ -2119,12 +2139,46 @@ async function promptForApplication() {
2119
2139
  }
2120
2140
  }
2121
2141
  }
2142
+ async function confirmEnvApplication(env, current) {
2143
+ const useEnv = `Use ${env.id} (from ${env.file})`;
2144
+ const choice = await useWizard.getState().requestUserInput({
2145
+ prompt: "Select an application",
2146
+ promptType: "multipleChoice",
2147
+ options: [
2148
+ useEnv,
2149
+ current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
2150
+ ],
2151
+ messages: [
2152
+ `${env.file} already sets ${env.name}=${env.id}.`,
2153
+ "Which Algolia application should the wizard work in?"
2154
+ ]
2155
+ });
2156
+ return choice === useEnv;
2157
+ }
2158
+ async function selectEnvApplication(env) {
2159
+ try {
2160
+ return await selectAndReport({ id: env.id, name: "" });
2161
+ } catch (err) {
2162
+ logger.warn(
2163
+ { app: env.id, err: err.message },
2164
+ "could not select the application named in env; falling back to the picker"
2165
+ );
2166
+ return promptForApplication([
2167
+ `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
2168
+ ]);
2169
+ }
2170
+ }
2122
2171
  async function ensureApplication() {
2123
- return await currentApplication() ?? await promptForApplication();
2172
+ const current = await currentApplication();
2173
+ const env = await findEnvApplicationId();
2174
+ if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
2175
+ return selectEnvApplication(env);
2176
+ }
2177
+ return current ?? await promptForApplication();
2124
2178
  }
2125
2179
 
2126
2180
  // src/workflows/default.ts
2127
- import { z as z27 } from "zod";
2181
+ import { z as z28 } from "zod";
2128
2182
 
2129
2183
  // src/actions/listIndices.ts
2130
2184
  import { z as z5 } from "zod";
@@ -2208,7 +2262,7 @@ import { readdir } from "node:fs/promises";
2208
2262
 
2209
2263
  // src/lib/tools/path.ts
2210
2264
  import { lstat } from "node:fs/promises";
2211
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join6, sep } from "node:path";
2265
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
2212
2266
  function resolveInRoot(ctx, path) {
2213
2267
  const target = resolve2(ctx.cwd, path);
2214
2268
  const rel = relative(ctx.root, target);
@@ -2224,7 +2278,7 @@ async function hasSymlinkParent(ctx, target) {
2224
2278
  let current = ctx.root;
2225
2279
  const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
2226
2280
  for (const part of parts) {
2227
- current = join6(current, part);
2281
+ current = join7(current, part);
2228
2282
  try {
2229
2283
  if ((await lstat(current)).isSymbolicLink()) return true;
2230
2284
  } catch (err) {
@@ -2245,9 +2299,9 @@ function listFilesTool(ctx) {
2245
2299
  if (++ctx.counts.list > ctx.limits.list) {
2246
2300
  return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
2247
2301
  }
2248
- const resolved = resolveInRoot(ctx, ".");
2249
- if (!resolved.ok) return resolved.error;
2250
- const entries = await readdir(resolved.target, { withFileTypes: true });
2302
+ const resolved2 = resolveInRoot(ctx, ".");
2303
+ if (!resolved2.ok) return resolved2.error;
2304
+ const entries = await readdir(resolved2.target, { withFileTypes: true });
2251
2305
  return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2252
2306
  }
2253
2307
  });
@@ -2265,14 +2319,14 @@ function changeDirectoryTool(ctx) {
2265
2319
  }),
2266
2320
  execute: async ({ path }) => {
2267
2321
  logger.info({ path }, "called changeDirectory tool");
2268
- const resolved = resolveInRoot(ctx, path);
2269
- if (!resolved.ok) return resolved.error;
2322
+ const resolved2 = resolveInRoot(ctx, path);
2323
+ if (!resolved2.ok) return resolved2.error;
2270
2324
  try {
2271
- const info = await stat(resolved.target);
2325
+ const info = await stat(resolved2.target);
2272
2326
  if (!info.isDirectory()) {
2273
2327
  return `Error changing directory to ${path}: not a directory`;
2274
2328
  }
2275
- ctx.cwd = resolved.target;
2329
+ ctx.cwd = resolved2.target;
2276
2330
  return `Changed working directory to ${ctx.cwd}`;
2277
2331
  } catch (err) {
2278
2332
  return `Error changing directory to ${path}: ${err.message}`;
@@ -2302,7 +2356,7 @@ function reportStatusTool(output) {
2302
2356
  // src/lib/tools/readFile.ts
2303
2357
  import { tool as tool4 } from "ai";
2304
2358
  import z9 from "zod";
2305
- import { readFile as readFile3 } from "node:fs/promises";
2359
+ import { readFile as readFile4 } from "node:fs/promises";
2306
2360
 
2307
2361
  // src/lib/tools/env.ts
2308
2362
  import { basename } from "node:path";
@@ -2337,11 +2391,11 @@ function readFileTool(ctx) {
2337
2391
  return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
2338
2392
  }
2339
2393
  logger.info({ filePath }, "called readFile tool");
2340
- const resolved = resolveInRoot(ctx, filePath);
2341
- if (!resolved.ok) return resolved.error;
2394
+ const resolved2 = resolveInRoot(ctx, filePath);
2395
+ if (!resolved2.ok) return resolved2.error;
2342
2396
  try {
2343
- const content = await readFile3(resolved.target, "utf8");
2344
- return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2397
+ const content = await readFile4(resolved2.target, "utf8");
2398
+ return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2345
2399
  } catch (err) {
2346
2400
  return `Error reading ${filePath}: ${err.message}`;
2347
2401
  }
@@ -2363,17 +2417,17 @@ function writeFileTool(ctx) {
2363
2417
  }),
2364
2418
  execute: async ({ filePath, content }) => {
2365
2419
  logger.info({ filePath }, "called writeFile tool");
2366
- const resolved = resolveInRoot(ctx, filePath);
2367
- if (resolved.ok === false) return resolved.error;
2368
- if (isSecretEnvFile(resolved.target)) {
2420
+ const resolved2 = resolveInRoot(ctx, filePath);
2421
+ if (resolved2.ok === false) return resolved2.error;
2422
+ if (isSecretEnvFile(resolved2.target)) {
2369
2423
  return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
2370
2424
  }
2371
2425
  try {
2372
- if (await hasSymlinkParent(ctx, resolved.target)) {
2373
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2426
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2427
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2374
2428
  }
2375
- await mkdir3(dirname4(resolved.target), { recursive: true });
2376
- await writeFile3(resolved.target, content, "utf8");
2429
+ await mkdir3(dirname4(resolved2.target), { recursive: true });
2430
+ await writeFile3(resolved2.target, content, "utf8");
2377
2431
  return `Wrote to ${filePath}`;
2378
2432
  } catch (err) {
2379
2433
  return `Error writing ${filePath}: ${err.message}`;
@@ -2384,12 +2438,93 @@ function writeFileTool(ctx) {
2384
2438
 
2385
2439
  // src/lib/tools/writeAlgoliaCredentials.ts
2386
2440
  import { tool as tool6 } from "ai";
2387
- import z12 from "zod";
2388
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
2441
+ import z13 from "zod";
2442
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2389
2443
  import { dirname as dirname5 } from "node:path";
2390
2444
 
2391
2445
  // src/lib/algoliaApiKey.ts
2446
+ import { z as z12 } from "zod";
2447
+
2448
+ // src/lib/keychain.ts
2449
+ import { deletePassword, getPassword, setPassword } from "cross-keychain";
2392
2450
  import { z as z11 } from "zod";
2451
+ var SERVICE = "algolia-wizard";
2452
+ var ACCOUNT = "api-keys";
2453
+ var storedKeysSchema = z11.record(z11.string(), z11.string());
2454
+ function entryId(kind, index, appId) {
2455
+ return `${kind}:${appId}:${index}`;
2456
+ }
2457
+ async function loadKeys() {
2458
+ const raw = await getPassword(SERVICE, ACCOUNT);
2459
+ if (!raw) return {};
2460
+ let payload;
2461
+ try {
2462
+ payload = JSON.parse(raw);
2463
+ } catch {
2464
+ payload = null;
2465
+ }
2466
+ const keys = storedKeysSchema.safeParse(payload);
2467
+ if (!keys.success) {
2468
+ logger.warn("the stored API keys are unreadable; treating them as empty");
2469
+ return {};
2470
+ }
2471
+ return keys.data;
2472
+ }
2473
+ var queue = Promise.resolve();
2474
+ function serialized(op) {
2475
+ const next = queue.then(op);
2476
+ queue = next.catch(() => {
2477
+ });
2478
+ return next;
2479
+ }
2480
+ async function readStoredKey(kind, index, appId) {
2481
+ try {
2482
+ return (await loadKeys())[entryId(kind, index, appId)] ?? null;
2483
+ } catch (err) {
2484
+ logger.warn(
2485
+ { err: err.message, kind, index, appId },
2486
+ "could not read the API key from the keychain"
2487
+ );
2488
+ return null;
2489
+ }
2490
+ }
2491
+ function storeKey(kind, index, appId, value) {
2492
+ return serialized(async () => {
2493
+ const id = entryId(kind, index, appId);
2494
+ try {
2495
+ const keys = await loadKeys();
2496
+ await setPassword(
2497
+ SERVICE,
2498
+ ACCOUNT,
2499
+ JSON.stringify({ ...keys, [id]: value })
2500
+ );
2501
+ if ((await loadKeys())[id] !== value) {
2502
+ throw new Error("the keychain did not store the value");
2503
+ }
2504
+ } catch (err) {
2505
+ logger.warn(
2506
+ { err: err.message, kind, index, appId },
2507
+ "could not store the API key in the keychain; the next run will create another"
2508
+ );
2509
+ }
2510
+ });
2511
+ }
2512
+ function deleteStoredKeys() {
2513
+ return serialized(async () => {
2514
+ try {
2515
+ await deletePassword(SERVICE, ACCOUNT);
2516
+ } catch (err) {
2517
+ const message = err.message;
2518
+ if (/not found/i.test(message)) return;
2519
+ logger.warn(
2520
+ { err: message },
2521
+ "could not delete the API keys from the keychain"
2522
+ );
2523
+ }
2524
+ });
2525
+ }
2526
+
2527
+ // src/lib/algoliaApiKey.ts
2393
2528
  var WRITE_ACLS = [
2394
2529
  "addObject",
2395
2530
  "deleteObject",
@@ -2397,58 +2532,21 @@ var WRITE_ACLS = [
2397
2532
  "editSettings",
2398
2533
  "listIndexes"
2399
2534
  ];
2400
- var WRITE_ACL_SET = new Set(WRITE_ACLS);
2401
- var apiKeySchema = z11.object({
2402
- value: z11.string().min(1),
2403
- acl: z11.array(z11.string()).default([]),
2404
- indexes: z11.array(z11.string()).default([])
2405
- });
2406
- var apiKeyListSchema = z11.object({
2407
- items: z11.array(apiKeySchema).optional(),
2408
- keys: z11.array(apiKeySchema).optional()
2409
- }).transform((o) => o.items ?? o.keys ?? []);
2410
- var createdKeySchema = z11.object({
2411
- key: z11.string().min(1).optional(),
2412
- value: z11.string().min(1).optional()
2535
+ var createdKeySchema = z12.object({
2536
+ key: z12.string().min(1).optional(),
2537
+ value: z12.string().min(1).optional()
2413
2538
  }).transform((o) => o.key ?? o.value);
2414
- function canReuseForWrites(key, index) {
2415
- return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2416
- }
2417
- async function resolveWriteKey(index) {
2418
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2419
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuseForWrites(key, index))?.value;
2420
- if (existing) {
2421
- logger.info({ index }, "reusing existing write API key");
2422
- return existing;
2423
- }
2424
- logger.info({ index }, "no reusable write key found; creating one");
2425
- const created = await runAlgoliaCli([
2426
- "apikeys",
2427
- "create",
2428
- "--indices",
2429
- index,
2430
- "--acl",
2431
- WRITE_ACLS.join(","),
2432
- "--description",
2433
- `wizard write key for ${index}`,
2434
- "-o",
2435
- "json"
2436
- ]);
2437
- const writeKey = createdKeySchema.parse(JSON.parse(created));
2438
- if (!writeKey) throw new Error("apikeys create returned no key value");
2439
- return writeKey;
2440
- }
2441
- async function createSearchOnlyKey(index) {
2442
- logger.info({ index }, "creating a search-only API key");
2539
+ async function createKey(index, acls, description) {
2540
+ logger.info({ index, acls }, "creating an API key");
2443
2541
  const stdout = await runAlgoliaCli([
2444
2542
  "apikeys",
2445
2543
  "create",
2446
2544
  "--acl",
2447
- "search",
2545
+ acls.join(","),
2448
2546
  "--indices",
2449
2547
  index,
2450
2548
  "--description",
2451
- `Algolia Wizard search-only key for ${index}`,
2549
+ description,
2452
2550
  "-o",
2453
2551
  "json"
2454
2552
  ]);
@@ -2462,49 +2560,72 @@ async function createSearchOnlyKey(index) {
2462
2560
  if (!created) throw new Error("apikeys create returned no key value");
2463
2561
  return created;
2464
2562
  }
2465
- async function apiKeyExists(key) {
2563
+ async function keyExists(key) {
2466
2564
  try {
2467
- await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
2565
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"], { redact: key });
2468
2566
  return true;
2469
2567
  } catch (err) {
2470
- return !/does not exist|not found|404/i.test(err.message);
2568
+ return !/does not exist/i.test(err.message);
2471
2569
  }
2472
2570
  }
2473
- async function resolveSearchOnlyKey(index, appId, envKey) {
2474
- if (envKey) {
2475
- await recordSearchKey(index, appId, envKey);
2476
- return { key: envKey, source: "env" };
2477
- }
2478
- const stored = await getStoredSearchKey(index, appId);
2571
+ var resolved = /* @__PURE__ */ new Map();
2572
+ async function forgetResolvedKeys() {
2573
+ resolved.clear();
2574
+ await deleteStoredKeys();
2575
+ }
2576
+ function resolveKey(kind, index, appId, acls, description) {
2577
+ const cacheKey = `${kind}:${appId}:${index}`;
2578
+ const cached = resolved.get(cacheKey);
2579
+ if (cached) return cached;
2580
+ const pending = provisionKey(kind, index, appId, acls, description).catch(
2581
+ (err) => {
2582
+ resolved.delete(cacheKey);
2583
+ throw err;
2584
+ }
2585
+ );
2586
+ resolved.set(cacheKey, pending);
2587
+ return pending;
2588
+ }
2589
+ async function provisionKey(kind, index, appId, acls, description) {
2590
+ const stored = await readStoredKey(kind, index, appId);
2479
2591
  if (stored) {
2480
- if (await apiKeyExists(stored)) {
2481
- logger.info({ index, appId }, "reusing the stored search-only API key");
2482
- return { key: stored, source: "config" };
2592
+ if (await keyExists(stored)) {
2593
+ logger.info({ kind, index, appId }, "reusing the stored API key");
2594
+ return { key: stored, source: "keychain" };
2483
2595
  }
2484
- logger.warn(
2485
- { index, appId },
2486
- "the stored search-only API key no longer exists; creating a replacement"
2596
+ logger.info(
2597
+ { kind, index, appId },
2598
+ "the stored API key no longer exists; creating another"
2487
2599
  );
2488
- await forgetSearchKey(index);
2489
2600
  }
2490
- const key = await createSearchOnlyKey(index);
2491
- await recordSearchKey(index, appId, key);
2601
+ const key = await createKey(index, acls, description);
2602
+ await storeKey(kind, index, appId, key);
2492
2603
  return { key, source: "created" };
2493
2604
  }
2494
- async function recordSearchKey(index, appId, key) {
2495
- try {
2496
- await storeSearchKey(index, appId, key);
2497
- } catch (err) {
2498
- logger.warn(
2499
- { err: err.message, index },
2500
- "could not record the search-only API key; a later run may create another"
2501
- );
2502
- }
2605
+ function resolveWriteKey(index, appId) {
2606
+ return resolveKey(
2607
+ "write",
2608
+ index,
2609
+ appId,
2610
+ WRITE_ACLS,
2611
+ `Algolia Wizard write key for ${index} index`
2612
+ );
2613
+ }
2614
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2615
+ if (envKey) return { key: envKey, source: "env" };
2616
+ return resolveKey(
2617
+ "search",
2618
+ index,
2619
+ appId,
2620
+ ["search"],
2621
+ `Algolia Wizard search-only key for ${index} index`
2622
+ );
2503
2623
  }
2504
2624
 
2505
2625
  // src/lib/tools/writeAlgoliaCredentials.ts
2506
2626
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2507
2627
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2628
+ var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
2508
2629
  function appendEnv(content, entries) {
2509
2630
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2510
2631
  const lines = entries.map(([name, value]) => `${name}=${value}
@@ -2514,53 +2635,111 @@ function appendEnv(content, entries) {
2514
2635
  function hasEnv(content, name) {
2515
2636
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
2516
2637
  }
2638
+ function readEnv(content, name) {
2639
+ const found = content.match(
2640
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*(.*)$`, "m")
2641
+ );
2642
+ if (!found) return null;
2643
+ const raw = found[1].trim();
2644
+ const quoted = raw.match(/^(['"])(.*)\1/);
2645
+ const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, "");
2646
+ return value.length > 0 ? value : null;
2647
+ }
2648
+ function upsertEnv(content, name, value) {
2649
+ if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
2650
+ return content.replace(
2651
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "gm"),
2652
+ () => `${name}=${value}`
2653
+ );
2654
+ }
2517
2655
  function writeCredentialsTool(ctx) {
2518
2656
  return tool6({
2519
- description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_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 (e.g. ".env"). If the file already defines ${APP_ID_VAR} or ${API_KEY_VAR}, the write is skipped and existing values are left untouched.`,
2520
- inputSchema: z12.object({
2521
- filePath: z12.string().describe(
2657
+ 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.`,
2658
+ inputSchema: z13.object({
2659
+ filePath: z13.string().describe(
2522
2660
  'Path to the env file to write credentials into (e.g. ".env")'
2523
2661
  )
2524
2662
  }),
2525
2663
  execute: async ({ filePath }) => {
2526
2664
  logger.info({ filePath }, "called writeCredentials tool");
2527
- const resolved = resolveInRoot(ctx, filePath);
2528
- if (resolved.ok === false) return resolved.error;
2665
+ const resolved2 = resolveInRoot(ctx, filePath);
2666
+ if (resolved2.ok === false) return resolved2.error;
2529
2667
  const targetIndex = useWizard.getState().targetIndex;
2530
2668
  if (!targetIndex) {
2531
2669
  return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2532
2670
  }
2533
- let appId;
2534
- let writeKey;
2535
- try {
2536
- appId = (await requireApplication()).id;
2537
- writeKey = await resolveWriteKey(targetIndex);
2538
- } catch (err) {
2539
- return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2540
- }
2671
+ let existing = "";
2672
+ let present;
2541
2673
  try {
2542
- if (await hasSymlinkParent(ctx, resolved.target)) {
2543
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2674
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2675
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2544
2676
  }
2545
- let existing = "";
2546
2677
  try {
2547
- existing = await readFile4(resolved.target, "utf8");
2678
+ existing = await readFile5(resolved2.target, "utf8");
2548
2679
  } catch (err) {
2549
2680
  if (err.code !== "ENOENT") throw err;
2550
2681
  }
2551
- const present = [APP_ID_VAR, API_KEY_VAR].filter(
2552
- (name) => hasEnv(existing, name)
2682
+ present = [APP_ID_VAR, API_KEY_VAR].filter(
2683
+ (name) => readEnv(existing, name) !== null
2553
2684
  );
2685
+ } catch (err) {
2686
+ return `Error writing credentials to ${filePath}: ${err.message}`;
2687
+ }
2688
+ const credentials = [];
2689
+ const notes = [];
2690
+ const fileAppId = readEnv(existing, APP_ID_VAR);
2691
+ const fileKey = readEnv(existing, API_KEY_VAR);
2692
+ const fileIndex = readEnv(existing, INDEX_NAME_VAR);
2693
+ if (fileAppId === null || fileKey === null) {
2694
+ try {
2695
+ const selected = (await requireApplication()).id;
2696
+ if (fileAppId === null) credentials.push([APP_ID_VAR, selected]);
2697
+ if (fileKey === null) {
2698
+ if (fileAppId !== null && fileAppId !== selected) {
2699
+ notes.push(
2700
+ `No ${API_KEY_VAR} was provisioned: ${filePath} names application ${fileAppId}, but ${selected} is selected. Tell the user to remove ${APP_ID_VAR} from ${filePath} and re-run so a matching pair can be written, or to fill in a ${API_KEY_VAR} for ${fileAppId} by hand.`
2701
+ );
2702
+ } else {
2703
+ credentials.push([
2704
+ API_KEY_VAR,
2705
+ (await resolveWriteKey(targetIndex, selected)).key
2706
+ ]);
2707
+ }
2708
+ } else {
2709
+ notes.push(
2710
+ `The existing ${API_KEY_VAR} must belong to application ${selected} or writes will fail.`
2711
+ );
2712
+ }
2713
+ } catch (err) {
2714
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2715
+ }
2716
+ }
2717
+ try {
2718
+ const updated = [
2719
+ ...credentials,
2720
+ // Unconditional, unlike the credentials: the write key is scoped to
2721
+ // the run's index, so a stale name left in the file earns a 403.
2722
+ [INDEX_NAME_VAR, targetIndex]
2723
+ ].reduce(
2724
+ (content, [name, value]) => upsertEnv(content, name, value),
2725
+ existing
2726
+ );
2727
+ await mkdir4(dirname5(resolved2.target), { recursive: true });
2728
+ await writeFile4(resolved2.target, updated, "utf8");
2729
+ const sentences = [
2730
+ `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
2731
+ ];
2732
+ if (fileIndex !== null && fileIndex !== targetIndex) {
2733
+ sentences.push(
2734
+ `That replaced the ${INDEX_NAME_VAR} already there ("${fileIndex}"), which this run does not target.`
2735
+ );
2736
+ }
2554
2737
  if (present.length > 0) {
2555
- return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2738
+ sentences.push(
2739
+ `Skipped ${present.join(" and ")}: already defined there.`
2740
+ );
2556
2741
  }
2557
- const envWithCredentials = appendEnv(existing, [
2558
- [APP_ID_VAR, appId],
2559
- [API_KEY_VAR, writeKey]
2560
- ]);
2561
- await mkdir4(dirname5(resolved.target), { recursive: true });
2562
- await writeFile4(resolved.target, envWithCredentials, "utf8");
2563
- return `Wrote Algolia credentials to ${filePath}`;
2742
+ return [...sentences, ...notes].join(" ");
2564
2743
  } catch (err) {
2565
2744
  return `Error writing credentials to ${filePath}: ${err.message}`;
2566
2745
  }
@@ -2570,16 +2749,16 @@ function writeCredentialsTool(ctx) {
2570
2749
 
2571
2750
  // src/lib/tools/searchFiles.ts
2572
2751
  import { tool as tool7 } from "ai";
2573
- import z13 from "zod";
2574
- import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
2575
- import { join as join7 } from "node:path";
2752
+ import z14 from "zod";
2753
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2754
+ import { join as join8 } from "node:path";
2576
2755
  var MAX_QUERY_LENGTH = 1e3;
2577
2756
  async function walkFiles(dir) {
2578
2757
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2579
2758
  const out = [];
2580
2759
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2581
2760
  if (e.name.startsWith(".") || skip.has(e.name)) continue;
2582
- const full = join7(dir, e.name);
2761
+ const full = join8(dir, e.name);
2583
2762
  if (e.isDirectory()) out.push(...await walkFiles(full));
2584
2763
  else if (e.isFile()) out.push(full);
2585
2764
  }
@@ -2588,9 +2767,9 @@ async function walkFiles(dir) {
2588
2767
  function searchFilesTool(ctx) {
2589
2768
  return tool7({
2590
2769
  description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
2591
- inputSchema: z13.object({
2592
- query: z13.string().describe("JavaScript RegExp pattern to search for"),
2593
- path: z13.string().optional().describe("Directory to search in (default: cwd)")
2770
+ inputSchema: z14.object({
2771
+ query: z14.string().describe("JavaScript RegExp pattern to search for"),
2772
+ path: z14.string().optional().describe("Directory to search in (default: cwd)")
2594
2773
  }),
2595
2774
  execute: async ({ query, path = "." }) => {
2596
2775
  logger.info({ query, path }, "called searchFiles tool");
@@ -2600,8 +2779,8 @@ function searchFilesTool(ctx) {
2600
2779
  if (query.length > MAX_QUERY_LENGTH) {
2601
2780
  return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
2602
2781
  }
2603
- const resolved = resolveInRoot(ctx, path);
2604
- if (!resolved.ok) return resolved.error;
2782
+ const resolved2 = resolveInRoot(ctx, path);
2783
+ if (!resolved2.ok) return resolved2.error;
2605
2784
  let re;
2606
2785
  try {
2607
2786
  re = new RegExp(query);
@@ -2609,10 +2788,10 @@ function searchFilesTool(ctx) {
2609
2788
  return `Invalid regex: ${err.message}`;
2610
2789
  }
2611
2790
  const matches = [];
2612
- for (const file of await walkFiles(resolved.target)) {
2791
+ for (const file of await walkFiles(resolved2.target)) {
2613
2792
  let content;
2614
2793
  try {
2615
- content = await readFile5(file, "utf8");
2794
+ content = await readFile6(file, "utf8");
2616
2795
  } catch {
2617
2796
  continue;
2618
2797
  }
@@ -2634,7 +2813,7 @@ function searchFilesTool(ctx) {
2634
2813
 
2635
2814
  // src/lib/tools/verifyImplementation.ts
2636
2815
  import { tool as tool8 } from "ai";
2637
- import z14 from "zod";
2816
+ import z15 from "zod";
2638
2817
 
2639
2818
  // src/lib/tools/utils/runCommand.ts
2640
2819
  import { spawn as spawn2 } from "node:child_process";
@@ -2656,9 +2835,9 @@ function runCommand(command, args, cwd) {
2656
2835
  }
2657
2836
 
2658
2837
  // src/lib/tools/utils/packageManager.ts
2659
- import { readFile as readFile6 } from "node:fs/promises";
2838
+ import { readFile as readFile7 } from "node:fs/promises";
2660
2839
  import { existsSync } from "node:fs";
2661
- import { join as join8 } from "node:path";
2840
+ import { join as join9 } from "node:path";
2662
2841
  var LOCKFILES = [
2663
2842
  ["pnpm-lock.yaml", "pnpm"],
2664
2843
  ["yarn.lock", "yarn"],
@@ -2667,13 +2846,13 @@ var LOCKFILES = [
2667
2846
  ["package-lock.json", "npm"]
2668
2847
  ];
2669
2848
  async function readPackageJson(cwd = process.cwd()) {
2670
- return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2849
+ return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
2671
2850
  }
2672
2851
  function packageManagerFrom(pkg) {
2673
2852
  return pkg.packageManager?.split("@")[0] ?? "npm";
2674
2853
  }
2675
2854
  function packageManagerFromLockfile(cwd) {
2676
- return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2855
+ return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
2677
2856
  }
2678
2857
  async function detectPackageManager(cwd) {
2679
2858
  try {
@@ -2714,7 +2893,7 @@ async function runRepoVerificationCheck() {
2714
2893
  function verifyImplementationTool() {
2715
2894
  return tool8({
2716
2895
  description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
2717
- inputSchema: z14.object(),
2896
+ inputSchema: z15.object(),
2718
2897
  execute: async () => {
2719
2898
  logger.info("called verifyImplementation tool");
2720
2899
  return runRepoVerificationCheck();
@@ -2728,7 +2907,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
2728
2907
  import { nanoid as nanoid2 } from "nanoid";
2729
2908
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2730
2909
  import { dirname as dirname6 } from "node:path";
2731
- import z15 from "zod";
2910
+ import z16 from "zod";
2732
2911
  var DATA_DIR = ".algolia-wizard/data";
2733
2912
  var RECORD_MODEL = "claude-haiku-4-5";
2734
2913
  var MAX_RECORDS = 100;
@@ -2740,17 +2919,17 @@ var anthropic = createAnthropic({
2740
2919
  function generateRecordTool(ctx) {
2741
2920
  return tool9({
2742
2921
  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.",
2743
- inputSchema: z15.object({
2744
- entityName: z15.string().describe("Name of the entity to generate records for."),
2745
- attributes: z15.array(z15.string()).describe("Attribute names each record must contain."),
2746
- count: z15.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2747
- hint: z15.string().optional().describe("Optional context to steer realistic values.")
2922
+ inputSchema: z16.object({
2923
+ entityName: z16.string().describe("Name of the entity to generate records for."),
2924
+ attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
2925
+ count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2926
+ hint: z16.string().optional().describe("Optional context to steer realistic values.")
2748
2927
  }),
2749
2928
  execute: async ({ entityName, attributes, count, hint }) => {
2750
2929
  logger.info({ entityName, count }, "called generateRecord tool");
2751
2930
  try {
2752
- const value = z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]);
2753
- const recordSchema = z15.object(
2931
+ const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
2932
+ const recordSchema = z16.object(
2754
2933
  Object.fromEntries(attributes.map((attr) => [attr, value]))
2755
2934
  );
2756
2935
  const generateBatch = async (batchCount) => {
@@ -2760,8 +2939,8 @@ function generateRecordTool(ctx) {
2760
2939
  const { output } = await generateText({
2761
2940
  model: anthropic(RECORD_MODEL),
2762
2941
  output: Output.object({
2763
- schema: z15.object({
2764
- records: z15.array(recordSchema).length(batchCount)
2942
+ schema: z16.object({
2943
+ records: z16.array(recordSchema).length(batchCount)
2765
2944
  })
2766
2945
  }),
2767
2946
  prompt: [
@@ -2797,13 +2976,13 @@ function generateRecordTool(ctx) {
2797
2976
  }));
2798
2977
  const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2799
2978
  const relPath = `${DATA_DIR}/${slug}.json`;
2800
- const resolved = resolveInRoot(ctx, relPath);
2801
- if (resolved.ok === false) return resolved.error;
2802
- if (await hasSymlinkParent(ctx, resolved.target)) {
2803
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2979
+ const resolved2 = resolveInRoot(ctx, relPath);
2980
+ if (resolved2.ok === false) return resolved2.error;
2981
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2982
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2804
2983
  }
2805
- await mkdir5(dirname6(resolved.target), { recursive: true });
2806
- await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2984
+ await mkdir5(dirname6(resolved2.target), { recursive: true });
2985
+ await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
2807
2986
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
2808
2987
  return {
2809
2988
  filePath: relPath,
@@ -2819,12 +2998,12 @@ function generateRecordTool(ctx) {
2819
2998
 
2820
2999
  // src/lib/tools/notifyUser.ts
2821
3000
  import { tool as tool10 } from "ai";
2822
- import z16 from "zod";
3001
+ import z17 from "zod";
2823
3002
  function notifyUserTool() {
2824
3003
  return tool10({
2825
3004
  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.`,
2826
- inputSchema: z16.object({
2827
- message: z16.string().describe(
3005
+ inputSchema: z17.object({
3006
+ message: z17.string().describe(
2828
3007
  "Short, plain-language description of what you are doing now."
2829
3008
  )
2830
3009
  }),
@@ -3004,10 +3183,10 @@ async function runAgent(req) {
3004
3183
  }
3005
3184
 
3006
3185
  // src/actions/detectLanguage.ts
3007
- import z19 from "zod";
3008
- var detectLanguageSchema = z19.object({
3009
- languages: z19.array(z19.object({ name: z19.string(), version: z19.string() })),
3010
- frameworks: z19.array(z19.object({ name: z19.string(), version: z19.string() }))
3186
+ import z20 from "zod";
3187
+ var detectLanguageSchema = z20.object({
3188
+ languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
3189
+ frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
3011
3190
  });
3012
3191
  var detectLanguage = () => runAgent({
3013
3192
  instructions: [
@@ -3025,31 +3204,31 @@ var detectLanguage = () => runAgent({
3025
3204
  });
3026
3205
 
3027
3206
  // src/actions/analyzeCodebase.ts
3028
- import z20 from "zod";
3207
+ import z21 from "zod";
3029
3208
  var READONLY_TOOLS = [
3030
3209
  "listFiles",
3031
3210
  "changeDirectory",
3032
3211
  "readFile",
3033
3212
  "searchFiles"
3034
3213
  ];
3035
- var ingestionAnalysisSchema = z20.object({
3036
- ingestionAnalysis: z20.array(
3037
- z20.object({
3038
- name: z20.string(),
3039
- paths: z20.array(z20.string()),
3214
+ var ingestionAnalysisSchema = z21.object({
3215
+ ingestionAnalysis: z21.array(
3216
+ z21.object({
3217
+ name: z21.string(),
3218
+ paths: z21.array(z21.string()),
3040
3219
  // indexable fields the agent found for this entity
3041
- attributes: z20.array(z20.string())
3220
+ attributes: z21.array(z21.string())
3042
3221
  })
3043
3222
  )
3044
3223
  });
3045
- var searchImplementationAnalysisSchema = z20.object({
3046
- searchImplementationAnalysis: z20.string()
3224
+ var searchImplementationAnalysisSchema = z21.object({
3225
+ searchImplementationAnalysis: z21.string()
3047
3226
  });
3048
- var verificationSchema = z20.object({
3049
- verification: z20.array(z20.string())
3227
+ var verificationSchema = z21.object({
3228
+ verification: z21.array(z21.string())
3050
3229
  });
3051
3230
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
3052
- var analyzeCodebaseSchema = z20.object({
3231
+ var analyzeCodebaseSchema = z21.object({
3053
3232
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3054
3233
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
3055
3234
  verification: verificationSchema.shape.verification.optional(),
@@ -3111,7 +3290,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3111
3290
  // package.json
3112
3291
  var package_default = {
3113
3292
  name: "@algolia/wizard",
3114
- version: "0.10.0",
3293
+ version: "0.12.0-rc.104.151",
3115
3294
  description: "Magically implement Algolia functionality in your codebase",
3116
3295
  type: "module",
3117
3296
  engines: {
@@ -3162,6 +3341,7 @@ var package_default = {
3162
3341
  "@hono/node-server": "^2.0.10",
3163
3342
  "@segment/analytics-node": "^3.1.0",
3164
3343
  ai: "^6.0.190",
3344
+ "cross-keychain": "^1.1.0",
3165
3345
  dotenv: "^17.4.2",
3166
3346
  hono: "^4.12.27",
3167
3347
  ink: "^7.0.5",
@@ -3229,8 +3409,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
3229
3409
  }
3230
3410
 
3231
3411
  // src/actions/confirmLanguage.ts
3232
- import z22 from "zod";
3233
- var confirmLanguageSchema = z22.object({
3412
+ import z23 from "zod";
3413
+ var confirmLanguageSchema = z23.object({
3234
3414
  languages: detectLanguageSchema.shape.languages
3235
3415
  });
3236
3416
  async function confirmLanguage(ctx) {
@@ -3251,8 +3431,8 @@ async function confirmLanguage(ctx) {
3251
3431
  }
3252
3432
 
3253
3433
  // src/actions/confirmFramework.ts
3254
- import z23 from "zod";
3255
- var confirmFrameworkSchema = z23.object({
3434
+ import z24 from "zod";
3435
+ var confirmFrameworkSchema = z24.object({
3256
3436
  frameworks: detectLanguageSchema.shape.frameworks
3257
3437
  });
3258
3438
  var CURATED_FRAMEWORKS = [
@@ -3380,8 +3560,8 @@ async function promptUser(ctx, params) {
3380
3560
  }
3381
3561
 
3382
3562
  // src/actions/confirmEntities.ts
3383
- import z24 from "zod";
3384
- var confirmEntitiesSchema = z24.object({
3563
+ import z25 from "zod";
3564
+ var confirmEntitiesSchema = z25.object({
3385
3565
  // Final detection — the focused re-run may supersede project-scan's.
3386
3566
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3387
3567
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3451,15 +3631,15 @@ async function confirmEntities(ctx) {
3451
3631
  }
3452
3632
 
3453
3633
  // src/actions/review.ts
3454
- import { z as z25 } from "zod";
3455
- var reviewSchema = z25.object({
3634
+ import { z as z26 } from "zod";
3635
+ var reviewSchema = z26.object({
3456
3636
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3457
3637
  // not one entry per workflow step — a step's raw output can be a long,
3458
3638
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3459
3639
  // that 1:1 is what made the old per-step summary an unreadable wall of text.
3460
- summaryPoints: z25.array(z25.string()),
3461
- reviewPrompt: z25.string(),
3462
- nextSteps: z25.array(z25.string())
3640
+ summaryPoints: z26.array(z26.string()),
3641
+ reviewPrompt: z26.string(),
3642
+ nextSteps: z26.array(z26.string())
3463
3643
  });
3464
3644
  function formatCompletedSteps(steps) {
3465
3645
  if (!steps.length) return "(no prior steps completed)";
@@ -3510,16 +3690,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3510
3690
  };
3511
3691
 
3512
3692
  // src/actions/implement.ts
3513
- import z26 from "zod";
3693
+ import z27 from "zod";
3514
3694
 
3515
3695
  // src/lib/worktree.ts
3516
3696
  import { execFile, spawn as spawn3 } from "node:child_process";
3517
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3697
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3518
3698
  import {
3519
3699
  basename as basename2,
3520
3700
  dirname as dirname7,
3521
3701
  isAbsolute as isAbsolute2,
3522
- join as join9,
3702
+ join as join10,
3523
3703
  relative as relative2,
3524
3704
  resolve as resolve3
3525
3705
  } from "node:path";
@@ -3553,7 +3733,7 @@ async function isWorkingTreeDirty(repoRoot) {
3553
3733
  return out.trim().length > 0;
3554
3734
  }
3555
3735
  async function pruneOldWorktrees(repoRoot) {
3556
- const dir = join9(stateDir(repoRoot), "worktrees");
3736
+ const dir = join10(stateDir(repoRoot), "worktrees");
3557
3737
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3558
3738
  for (const slug of stale) {
3559
3739
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3564,7 +3744,7 @@ async function pruneOldWorktrees(repoRoot) {
3564
3744
  "worktree",
3565
3745
  "remove",
3566
3746
  "--force",
3567
- join9(dir, slug)
3747
+ join10(dir, slug)
3568
3748
  ]);
3569
3749
  await git(["-C", repoRoot, "branch", "-D", branch]);
3570
3750
  } catch (err) {
@@ -3578,7 +3758,7 @@ async function pruneOldWorktrees(repoRoot) {
3578
3758
  async function createWorktree(repoRoot) {
3579
3759
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3580
3760
  const dirSlug = branch.replace(/\//g, "-");
3581
- const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
3761
+ const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3582
3762
  await git(["-C", repoRoot, "worktree", "prune"]);
3583
3763
  await pruneOldWorktrees(repoRoot);
3584
3764
  await mkdir6(dirname7(path), { recursive: true });
@@ -3698,8 +3878,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3698
3878
  } catch {
3699
3879
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3700
3880
  }
3701
- const relPath = join9(ingestDir, basename2(source));
3702
- const dest = join9(worktreePath, relPath);
3881
+ const relPath = join10(ingestDir, basename2(source));
3882
+ const dest = join10(worktreePath, relPath);
3703
3883
  try {
3704
3884
  await mkdir6(dirname7(dest), { recursive: true });
3705
3885
  await copyFile(source, dest);
@@ -3717,7 +3897,7 @@ function hasEnvVar(content, name) {
3717
3897
  async function readEnvVar(worktreePath, name) {
3718
3898
  let content;
3719
3899
  try {
3720
- content = await readFile7(join9(worktreePath, ".env"), "utf8");
3900
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
3721
3901
  } catch (err) {
3722
3902
  if (err.code !== "ENOENT") throw err;
3723
3903
  return void 0;
@@ -3732,10 +3912,10 @@ async function readEnvVar(worktreePath, name) {
3732
3912
  return value;
3733
3913
  }
3734
3914
  async function writeSearchEnvValues(worktreePath, vars) {
3735
- const target = join9(worktreePath, ".env");
3915
+ const target = join10(worktreePath, ".env");
3736
3916
  let existing = "";
3737
3917
  try {
3738
- existing = await readFile7(target, "utf8");
3918
+ existing = await readFile8(target, "utf8");
3739
3919
  } catch (err) {
3740
3920
  if (err.code !== "ENOENT") throw err;
3741
3921
  }
@@ -3805,13 +3985,13 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3805
3985
 
3806
3986
  // src/lib/algoliaDocs.ts
3807
3987
  import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3808
- import { dirname as dirname8, join as join10 } from "node:path";
3988
+ import { dirname as dirname8, join as join11 } from "node:path";
3809
3989
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3810
- var DOCS_SUBPATH = join10("docs", "algolia-sdk");
3990
+ var DOCS_SUBPATH = join11("docs", "algolia-sdk");
3811
3991
  function findDocsDir() {
3812
3992
  let dir = dirname8(fileURLToPath2(import.meta.url));
3813
3993
  for (; ; ) {
3814
- const candidate = join10(dir, DOCS_SUBPATH);
3994
+ const candidate = join11(dir, DOCS_SUBPATH);
3815
3995
  if (existsSync2(candidate)) return candidate;
3816
3996
  const parent = dirname8(dir);
3817
3997
  if (parent === dir) return void 0;
@@ -3834,7 +4014,7 @@ function loadAlgoliaDoc(language) {
3834
4014
  );
3835
4015
  return "";
3836
4016
  }
3837
- return readFileSync(join10(docsDir, files[0]), "utf8").trim();
4017
+ return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3838
4018
  }
3839
4019
  function getNamedDoc(name, language) {
3840
4020
  const docsDir = findDocsDir();
@@ -3842,7 +4022,7 @@ function getNamedDoc(name, language) {
3842
4022
  logger.warn("docs/algolia-sdk not found");
3843
4023
  return "";
3844
4024
  }
3845
- const file = join10(docsDir, `${name}-${language}.md`);
4025
+ const file = join11(docsDir, `${name}-${language}.md`);
3846
4026
  if (!existsSync2(file)) {
3847
4027
  logger.warn({ name, language }, "named SDK reference not found");
3848
4028
  return "";
@@ -3869,34 +4049,34 @@ function shellQuote(value) {
3869
4049
  }
3870
4050
 
3871
4051
  // src/actions/implement.ts
3872
- var implementSchema = z26.object({
3873
- filesChanged: z26.array(z26.string()),
3874
- summary: z26.string(),
3875
- worktreePath: z26.string().optional(),
3876
- ingestCommand: z26.string().optional(),
3877
- ingestScriptRan: z26.boolean().optional(),
3878
- ingestRecordCount: z26.number().optional(),
3879
- ingestDurationMs: z26.number().optional(),
3880
- ingestionSource: z26.enum(["local", "fileUpload", "generated"]),
3881
- searchEnvVars: z26.array(
3882
- z26.object({
3883
- name: z26.string(),
3884
- value: z26.string()
4052
+ var implementSchema = z27.object({
4053
+ filesChanged: z27.array(z27.string()),
4054
+ summary: z27.string(),
4055
+ worktreePath: z27.string().optional(),
4056
+ ingestCommand: z27.string().optional(),
4057
+ ingestScriptRan: z27.boolean().optional(),
4058
+ ingestRecordCount: z27.number().optional(),
4059
+ ingestDurationMs: z27.number().optional(),
4060
+ ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
4061
+ searchEnvVars: z27.array(
4062
+ z27.object({
4063
+ name: z27.string(),
4064
+ value: z27.string()
3885
4065
  })
3886
4066
  ).optional()
3887
4067
  });
3888
- var implementationOutputSchema = z26.object({
3889
- summary: z26.string(),
4068
+ var implementationOutputSchema = z27.object({
4069
+ summary: z27.string(),
3890
4070
  // Ingestion only: a structured pair the wizard turns into an argv, never a
3891
4071
  // free-form command string. `runtime` is allowlisted and `entrypoint` is
3892
4072
  // validated worktree-relative, so the agent cannot inject extra commands.
3893
- runtime: z26.enum(INGEST_RUNTIMES).optional(),
3894
- entrypoint: z26.string().optional()
4073
+ runtime: z27.enum(INGEST_RUNTIMES).optional(),
4074
+ entrypoint: z27.string().optional()
3895
4075
  });
3896
- var verificationOutputSchema = z26.object({
3897
- summary: z26.string(),
3898
- sufficient: z26.boolean(),
3899
- additionalInstructions: z26.string().optional()
4076
+ var verificationOutputSchema = z27.object({
4077
+ summary: z27.string(),
4078
+ sufficient: z27.boolean(),
4079
+ additionalInstructions: z27.string().optional()
3900
4080
  });
3901
4081
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3902
4082
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -3941,13 +4121,17 @@ function publicEnvPrefix(language) {
3941
4121
  }
3942
4122
  var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
3943
4123
  var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4124
+ var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
3944
4125
  function appIdVar(language) {
3945
4126
  return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
3946
4127
  }
3947
4128
  function searchKeyVar(language) {
3948
4129
  return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3949
4130
  }
3950
- function searchEnvVars(language, appId, searchKey) {
4131
+ function searchIndexVar(language) {
4132
+ return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
4133
+ }
4134
+ function searchEnvVars(language, index, appId, searchKey) {
3951
4135
  return [
3952
4136
  {
3953
4137
  name: appIdVar(language),
@@ -3956,12 +4140,21 @@ function searchEnvVars(language, appId, searchKey) {
3956
4140
  {
3957
4141
  name: searchKeyVar(language),
3958
4142
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4143
+ },
4144
+ // Wizard-supplied rather than written into the generated code, because an
4145
+ // agent that retypes the name (appending the project name, re-casing it)
4146
+ // leaves the UI querying an index that does not exist.
4147
+ {
4148
+ name: searchIndexVar(language),
4149
+ value: index
3959
4150
  }
3960
4151
  ];
3961
4152
  }
3962
4153
  function baseInstructions(input) {
3963
4154
  return [
3964
- `Target Algolia index: ${input.targetIndex}`,
4155
+ // Agents have renamed this (e.g. appending the project name), which the
4156
+ // index-scoped keys then reject with a 403.
4157
+ `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
3965
4158
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3966
4159
  "Make minimal, idiomatic changes; do not touch unrelated code."
3967
4160
  ];
@@ -3995,6 +4188,7 @@ function ingestionInstructions(input) {
3995
4188
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3996
4189
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3997
4190
  `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
4191
+ `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
3998
4192
  "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
3999
4193
  "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.",
4000
4194
  getNamedDoc("save-records", "js"),
@@ -4012,7 +4206,8 @@ function searchInstructions(input) {
4012
4206
  `Build the search UI for ${input.uiFramework}.`,
4013
4207
  "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4014
4208
  doc,
4015
- `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
4209
+ `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the target index.`,
4210
+ `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.`,
4016
4211
  // The key is provisioned only after verification passes, so the agent never
4017
4212
  // sees one. It must also leave .env alone: the wizard reads that file to
4018
4213
  // decide whether a key already exists, and an agent-invented value there
@@ -4022,7 +4217,7 @@ function searchInstructions(input) {
4022
4217
  // ".env" right after this step, so a renamed prefix would leave the code
4023
4218
  // reading a var the wizard never wrote.
4024
4219
  `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4025
- "Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
4220
+ "Read the App ID, the search-only API key, and the index name from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
4026
4221
  'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
4027
4222
  "The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
4028
4223
  ];
@@ -4206,9 +4401,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4206
4401
  targetIndex,
4207
4402
  language,
4208
4403
  appId,
4209
- // Names only: the search-only key is provisioned after verification, so
4210
- // every value here is still a placeholder when the agent reads them.
4211
- searchEnvVars: searchEnvVars(language, appId),
4404
+ searchEnvVars: searchEnvVars(language, targetIndex, appId),
4212
4405
  ingestDir: INGEST_DIR,
4213
4406
  ingestionSource,
4214
4407
  uploadFilePath,
@@ -4295,7 +4488,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4295
4488
  }) === true;
4296
4489
  if (runNow) {
4297
4490
  const ingestApp = await requireApplication();
4298
- const writeKey = await resolveWriteKey(targetIndex);
4491
+ const writeKey = (await resolveWriteKey(targetIndex, ingestApp.id)).key;
4299
4492
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
4300
4493
  const scriptLogId = ctx.logStart("runIngestScript", {
4301
4494
  runtime: ingestRuntime,
@@ -4308,7 +4501,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4308
4501
  ingestEntrypoint,
4309
4502
  {
4310
4503
  [APP_ID_VAR]: ingestApp.id,
4311
- [API_KEY_VAR]: writeKey
4504
+ [API_KEY_VAR]: writeKey,
4505
+ [INDEX_NAME_VAR]: targetIndex
4312
4506
  }
4313
4507
  );
4314
4508
  ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
@@ -4429,14 +4623,14 @@ ${run2.output}` : status;
4429
4623
  let searchKeyError;
4430
4624
  if (appId) {
4431
4625
  try {
4432
- const resolved = await resolveSearchOnlyKey(
4626
+ const resolved2 = await resolveSearchOnlyKey(
4433
4627
  targetIndex,
4434
4628
  appId,
4435
4629
  envSearchKey
4436
4630
  );
4437
- searchKey = resolved.key;
4631
+ searchKey = resolved2.key;
4438
4632
  summaries.push(
4439
- resolved.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}.`
4633
+ 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}.`
4440
4634
  );
4441
4635
  } catch (err) {
4442
4636
  searchKeyError = err.message;
@@ -4446,7 +4640,12 @@ ${run2.output}` : status;
4446
4640
  );
4447
4641
  }
4448
4642
  }
4449
- finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4643
+ finalSearchEnvVars = searchEnvVars(
4644
+ language,
4645
+ targetIndex,
4646
+ appId,
4647
+ searchKey
4648
+ );
4450
4649
  const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4451
4650
  (v) => !v.value.startsWith("<")
4452
4651
  );
@@ -4555,8 +4754,8 @@ var defaultWorkflow = {
4555
4754
  defineStep({
4556
4755
  id: "select-index",
4557
4756
  title: "Set up index",
4558
- outputSchema: z27.object({
4559
- selection: z27.string()
4757
+ outputSchema: z28.object({
4758
+ selection: z28.string()
4560
4759
  }),
4561
4760
  run: (ctx) => selectIndexStep(ctx)
4562
4761
  }),
@@ -4798,7 +4997,11 @@ Options:
4798
4997
  --no-telemetry Send no telemetry or analytics for this run.
4799
4998
  --reset-on-run Wipe this project's wizard state (run state, AI consent,
4800
4999
  worktrees) before starting, so the run behaves like a
4801
- first-ever run. Algolia credentials are not touched.
5000
+ first-ever run. Also drops every API key the wizard has
5001
+ stored in your keychain (or, where the platform has none,
5002
+ the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
5003
+ for this project and any other, so later runs create new
5004
+ ones. Your Algolia login is not touched.
4802
5005
  -h, --help Print this message.`;
4803
5006
  function parseCliArgs(argv) {
4804
5007
  const positionals = [];
@@ -4835,10 +5038,11 @@ function parseCliArgs(argv) {
4835
5038
 
4836
5039
  // src/lib/resetState.ts
4837
5040
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4838
- import { join as join11 } from "node:path";
5041
+ import { join as join12 } from "node:path";
4839
5042
  var KEEP = ["wizard.log"];
4840
5043
  async function resetProjectState() {
4841
5044
  const dir = stateDir();
5045
+ await forgetResolvedKeys();
4842
5046
  let entries;
4843
5047
  try {
4844
5048
  entries = await readdir4(dir);
@@ -4847,7 +5051,9 @@ async function resetProjectState() {
4847
5051
  }
4848
5052
  const targets = entries.filter((name) => !KEEP.includes(name));
4849
5053
  await Promise.all(
4850
- targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
5054
+ targets.map(
5055
+ (name) => rm2(join12(dir, name), { recursive: true, force: true })
5056
+ )
4851
5057
  );
4852
5058
  return { dir, removed: targets };
4853
5059
  }
@@ -4855,6 +5061,7 @@ async function resetProjectState() {
4855
5061
  // src/main.tsx
4856
5062
  import { jsx as jsx14 } from "react/jsx-runtime";
4857
5063
  async function startup() {
5064
+ setProjectRoot(process.cwd());
4858
5065
  let args;
4859
5066
  try {
4860
5067
  args = parseCliArgs(process.argv.slice(2));