@algolia/wizard 0.10.0 → 0.11.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 +490 -288
  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
 
@@ -1743,18 +1751,17 @@ import "zod";
1743
1751
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1744
1752
  import { join as join5 } from "node:path";
1745
1753
  var configFile = () => join5(stateDir(), "config.json");
1746
- var DEFAULT_CONFIG = {
1754
+ var defaultConfig = () => ({
1747
1755
  version: 1,
1748
1756
  aiConsent: false,
1749
- workflowsRun: [],
1750
- searchApiKeys: {}
1751
- };
1757
+ workflowsRun: []
1758
+ });
1752
1759
  async function loadConfig() {
1753
1760
  try {
1754
1761
  const raw = await readFile2(configFile(), "utf8");
1755
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1762
+ return { ...defaultConfig(), ...JSON.parse(raw) };
1756
1763
  } catch {
1757
- return { ...DEFAULT_CONFIG };
1764
+ return defaultConfig();
1758
1765
  }
1759
1766
  }
1760
1767
  async function saveConfig(config) {
@@ -1766,38 +1773,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
1766
1773
  config.workflowsRun.push({ workflowId, completedAt });
1767
1774
  await saveConfig(config);
1768
1775
  }
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
1776
 
1802
1777
  // src/core/orchestrator.ts
1803
1778
  function defineStep(step) {
@@ -2063,6 +2038,42 @@ function parseJson(text) {
2063
2038
  }
2064
2039
  }
2065
2040
 
2041
+ // src/lib/envAppId.ts
2042
+ import { readFile as readFile3 } from "node:fs/promises";
2043
+ import { join as join6 } from "node:path";
2044
+ var ENV_FILES = [".env", ".env.local"];
2045
+ var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
2046
+ async function findEnvApplicationId(root = process.cwd()) {
2047
+ for (const file of ENV_FILES) {
2048
+ let content;
2049
+ try {
2050
+ content = await readFile3(join6(root, file), "utf8");
2051
+ } catch (err) {
2052
+ if (err.code !== "ENOENT") {
2053
+ logger.warn(
2054
+ { file, err },
2055
+ "could not read env file for an application id"
2056
+ );
2057
+ }
2058
+ continue;
2059
+ }
2060
+ for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
2061
+ const id = readValue(raw);
2062
+ if (id) {
2063
+ logger.info({ file, name, app: id }, "found an application id in env");
2064
+ return { id, name, file };
2065
+ }
2066
+ }
2067
+ }
2068
+ return null;
2069
+ }
2070
+ function readValue(raw) {
2071
+ const trimmed = raw.trim();
2072
+ const quoted = trimmed.match(/^(['"])(.*)\1/);
2073
+ const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
2074
+ return value.length > 0 && !value.startsWith("<") ? value : null;
2075
+ }
2076
+
2066
2077
  // src/lib/algoliaAppPicker.ts
2067
2078
  function secondaryFor(app) {
2068
2079
  return app.plan ? { kind: "badge", value: app.plan } : void 0;
@@ -2077,7 +2088,7 @@ function selectAndReport(app) {
2077
2088
  );
2078
2089
  return selectApplication(app.id);
2079
2090
  }
2080
- async function promptForApplication() {
2091
+ async function promptForApplication(leadIn = []) {
2081
2092
  const store = useWizard.getState();
2082
2093
  const apps = await listApplications();
2083
2094
  if (apps.length === 0) {
@@ -2091,9 +2102,13 @@ async function promptForApplication() {
2091
2102
  { app: only.id },
2092
2103
  "single application on the account; selecting it"
2093
2104
  );
2105
+ for (const line of leadIn) store.pushCliOutput("stdout", line);
2094
2106
  return selectAndReport(only);
2095
2107
  }
2096
- const messages = ["Which Algolia application should the wizard work in?"];
2108
+ const messages = [
2109
+ ...leadIn,
2110
+ "Which Algolia application should the wizard work in?"
2111
+ ];
2097
2112
  for (; ; ) {
2098
2113
  const choice = await store.requestUserInput({
2099
2114
  prompt: "Select an application",
@@ -2119,12 +2134,46 @@ async function promptForApplication() {
2119
2134
  }
2120
2135
  }
2121
2136
  }
2137
+ async function confirmEnvApplication(env, current) {
2138
+ const useEnv = `Use ${env.id} (from ${env.file})`;
2139
+ const choice = await useWizard.getState().requestUserInput({
2140
+ prompt: "Select an application",
2141
+ promptType: "multipleChoice",
2142
+ options: [
2143
+ useEnv,
2144
+ current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
2145
+ ],
2146
+ messages: [
2147
+ `${env.file} already sets ${env.name}=${env.id}.`,
2148
+ "Which Algolia application should the wizard work in?"
2149
+ ]
2150
+ });
2151
+ return choice === useEnv;
2152
+ }
2153
+ async function selectEnvApplication(env) {
2154
+ try {
2155
+ return await selectAndReport({ id: env.id, name: "" });
2156
+ } catch (err) {
2157
+ logger.warn(
2158
+ { app: env.id, err: err.message },
2159
+ "could not select the application named in env; falling back to the picker"
2160
+ );
2161
+ return promptForApplication([
2162
+ `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
2163
+ ]);
2164
+ }
2165
+ }
2122
2166
  async function ensureApplication() {
2123
- return await currentApplication() ?? await promptForApplication();
2167
+ const current = await currentApplication();
2168
+ const env = await findEnvApplicationId();
2169
+ if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
2170
+ return selectEnvApplication(env);
2171
+ }
2172
+ return current ?? await promptForApplication();
2124
2173
  }
2125
2174
 
2126
2175
  // src/workflows/default.ts
2127
- import { z as z27 } from "zod";
2176
+ import { z as z28 } from "zod";
2128
2177
 
2129
2178
  // src/actions/listIndices.ts
2130
2179
  import { z as z5 } from "zod";
@@ -2208,7 +2257,7 @@ import { readdir } from "node:fs/promises";
2208
2257
 
2209
2258
  // src/lib/tools/path.ts
2210
2259
  import { lstat } from "node:fs/promises";
2211
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join6, sep } from "node:path";
2260
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
2212
2261
  function resolveInRoot(ctx, path) {
2213
2262
  const target = resolve2(ctx.cwd, path);
2214
2263
  const rel = relative(ctx.root, target);
@@ -2224,7 +2273,7 @@ async function hasSymlinkParent(ctx, target) {
2224
2273
  let current = ctx.root;
2225
2274
  const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
2226
2275
  for (const part of parts) {
2227
- current = join6(current, part);
2276
+ current = join7(current, part);
2228
2277
  try {
2229
2278
  if ((await lstat(current)).isSymbolicLink()) return true;
2230
2279
  } catch (err) {
@@ -2245,9 +2294,9 @@ function listFilesTool(ctx) {
2245
2294
  if (++ctx.counts.list > ctx.limits.list) {
2246
2295
  return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
2247
2296
  }
2248
- const resolved = resolveInRoot(ctx, ".");
2249
- if (!resolved.ok) return resolved.error;
2250
- const entries = await readdir(resolved.target, { withFileTypes: true });
2297
+ const resolved2 = resolveInRoot(ctx, ".");
2298
+ if (!resolved2.ok) return resolved2.error;
2299
+ const entries = await readdir(resolved2.target, { withFileTypes: true });
2251
2300
  return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2252
2301
  }
2253
2302
  });
@@ -2265,14 +2314,14 @@ function changeDirectoryTool(ctx) {
2265
2314
  }),
2266
2315
  execute: async ({ path }) => {
2267
2316
  logger.info({ path }, "called changeDirectory tool");
2268
- const resolved = resolveInRoot(ctx, path);
2269
- if (!resolved.ok) return resolved.error;
2317
+ const resolved2 = resolveInRoot(ctx, path);
2318
+ if (!resolved2.ok) return resolved2.error;
2270
2319
  try {
2271
- const info = await stat(resolved.target);
2320
+ const info = await stat(resolved2.target);
2272
2321
  if (!info.isDirectory()) {
2273
2322
  return `Error changing directory to ${path}: not a directory`;
2274
2323
  }
2275
- ctx.cwd = resolved.target;
2324
+ ctx.cwd = resolved2.target;
2276
2325
  return `Changed working directory to ${ctx.cwd}`;
2277
2326
  } catch (err) {
2278
2327
  return `Error changing directory to ${path}: ${err.message}`;
@@ -2302,7 +2351,7 @@ function reportStatusTool(output) {
2302
2351
  // src/lib/tools/readFile.ts
2303
2352
  import { tool as tool4 } from "ai";
2304
2353
  import z9 from "zod";
2305
- import { readFile as readFile3 } from "node:fs/promises";
2354
+ import { readFile as readFile4 } from "node:fs/promises";
2306
2355
 
2307
2356
  // src/lib/tools/env.ts
2308
2357
  import { basename } from "node:path";
@@ -2337,11 +2386,11 @@ function readFileTool(ctx) {
2337
2386
  return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
2338
2387
  }
2339
2388
  logger.info({ filePath }, "called readFile tool");
2340
- const resolved = resolveInRoot(ctx, filePath);
2341
- if (!resolved.ok) return resolved.error;
2389
+ const resolved2 = resolveInRoot(ctx, filePath);
2390
+ if (!resolved2.ok) return resolved2.error;
2342
2391
  try {
2343
- const content = await readFile3(resolved.target, "utf8");
2344
- return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2392
+ const content = await readFile4(resolved2.target, "utf8");
2393
+ return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2345
2394
  } catch (err) {
2346
2395
  return `Error reading ${filePath}: ${err.message}`;
2347
2396
  }
@@ -2363,17 +2412,17 @@ function writeFileTool(ctx) {
2363
2412
  }),
2364
2413
  execute: async ({ filePath, content }) => {
2365
2414
  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)) {
2415
+ const resolved2 = resolveInRoot(ctx, filePath);
2416
+ if (resolved2.ok === false) return resolved2.error;
2417
+ if (isSecretEnvFile(resolved2.target)) {
2369
2418
  return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
2370
2419
  }
2371
2420
  try {
2372
- if (await hasSymlinkParent(ctx, resolved.target)) {
2373
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2421
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2422
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2374
2423
  }
2375
- await mkdir3(dirname4(resolved.target), { recursive: true });
2376
- await writeFile3(resolved.target, content, "utf8");
2424
+ await mkdir3(dirname4(resolved2.target), { recursive: true });
2425
+ await writeFile3(resolved2.target, content, "utf8");
2377
2426
  return `Wrote to ${filePath}`;
2378
2427
  } catch (err) {
2379
2428
  return `Error writing ${filePath}: ${err.message}`;
@@ -2384,12 +2433,93 @@ function writeFileTool(ctx) {
2384
2433
 
2385
2434
  // src/lib/tools/writeAlgoliaCredentials.ts
2386
2435
  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";
2436
+ import z13 from "zod";
2437
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2389
2438
  import { dirname as dirname5 } from "node:path";
2390
2439
 
2391
2440
  // src/lib/algoliaApiKey.ts
2441
+ import { z as z12 } from "zod";
2442
+
2443
+ // src/lib/keychain.ts
2444
+ import { deletePassword, getPassword, setPassword } from "cross-keychain";
2392
2445
  import { z as z11 } from "zod";
2446
+ var SERVICE = "algolia-wizard";
2447
+ var ACCOUNT = "api-keys";
2448
+ var storedKeysSchema = z11.record(z11.string(), z11.string());
2449
+ function entryId(kind, index, appId) {
2450
+ return `${kind}:${appId}:${index}`;
2451
+ }
2452
+ async function loadKeys() {
2453
+ const raw = await getPassword(SERVICE, ACCOUNT);
2454
+ if (!raw) return {};
2455
+ let payload;
2456
+ try {
2457
+ payload = JSON.parse(raw);
2458
+ } catch {
2459
+ payload = null;
2460
+ }
2461
+ const keys = storedKeysSchema.safeParse(payload);
2462
+ if (!keys.success) {
2463
+ logger.warn("the stored API keys are unreadable; treating them as empty");
2464
+ return {};
2465
+ }
2466
+ return keys.data;
2467
+ }
2468
+ var queue = Promise.resolve();
2469
+ function serialized(op) {
2470
+ const next = queue.then(op);
2471
+ queue = next.catch(() => {
2472
+ });
2473
+ return next;
2474
+ }
2475
+ async function readStoredKey(kind, index, appId) {
2476
+ try {
2477
+ return (await loadKeys())[entryId(kind, index, appId)] ?? null;
2478
+ } catch (err) {
2479
+ logger.warn(
2480
+ { err: err.message, kind, index, appId },
2481
+ "could not read the API key from the keychain"
2482
+ );
2483
+ return null;
2484
+ }
2485
+ }
2486
+ function storeKey(kind, index, appId, value) {
2487
+ return serialized(async () => {
2488
+ const id = entryId(kind, index, appId);
2489
+ try {
2490
+ const keys = await loadKeys();
2491
+ await setPassword(
2492
+ SERVICE,
2493
+ ACCOUNT,
2494
+ JSON.stringify({ ...keys, [id]: value })
2495
+ );
2496
+ if ((await loadKeys())[id] !== value) {
2497
+ throw new Error("the keychain did not store the value");
2498
+ }
2499
+ } catch (err) {
2500
+ logger.warn(
2501
+ { err: err.message, kind, index, appId },
2502
+ "could not store the API key in the keychain; the next run will create another"
2503
+ );
2504
+ }
2505
+ });
2506
+ }
2507
+ function deleteStoredKeys() {
2508
+ return serialized(async () => {
2509
+ try {
2510
+ await deletePassword(SERVICE, ACCOUNT);
2511
+ } catch (err) {
2512
+ const message = err.message;
2513
+ if (/not found/i.test(message)) return;
2514
+ logger.warn(
2515
+ { err: message },
2516
+ "could not delete the API keys from the keychain"
2517
+ );
2518
+ }
2519
+ });
2520
+ }
2521
+
2522
+ // src/lib/algoliaApiKey.ts
2393
2523
  var WRITE_ACLS = [
2394
2524
  "addObject",
2395
2525
  "deleteObject",
@@ -2397,58 +2527,21 @@ var WRITE_ACLS = [
2397
2527
  "editSettings",
2398
2528
  "listIndexes"
2399
2529
  ];
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()
2530
+ var createdKeySchema = z12.object({
2531
+ key: z12.string().min(1).optional(),
2532
+ value: z12.string().min(1).optional()
2413
2533
  }).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");
2534
+ async function createKey(index, acls, description) {
2535
+ logger.info({ index, acls }, "creating an API key");
2443
2536
  const stdout = await runAlgoliaCli([
2444
2537
  "apikeys",
2445
2538
  "create",
2446
2539
  "--acl",
2447
- "search",
2540
+ acls.join(","),
2448
2541
  "--indices",
2449
2542
  index,
2450
2543
  "--description",
2451
- `Algolia Wizard search-only key for ${index}`,
2544
+ description,
2452
2545
  "-o",
2453
2546
  "json"
2454
2547
  ]);
@@ -2462,49 +2555,72 @@ async function createSearchOnlyKey(index) {
2462
2555
  if (!created) throw new Error("apikeys create returned no key value");
2463
2556
  return created;
2464
2557
  }
2465
- async function apiKeyExists(key) {
2558
+ async function keyExists(key) {
2466
2559
  try {
2467
- await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
2560
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"], { redact: key });
2468
2561
  return true;
2469
2562
  } catch (err) {
2470
- return !/does not exist|not found|404/i.test(err.message);
2563
+ return !/does not exist/i.test(err.message);
2471
2564
  }
2472
2565
  }
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);
2566
+ var resolved = /* @__PURE__ */ new Map();
2567
+ async function forgetResolvedKeys() {
2568
+ resolved.clear();
2569
+ await deleteStoredKeys();
2570
+ }
2571
+ function resolveKey(kind, index, appId, acls, description) {
2572
+ const cacheKey = `${kind}:${appId}:${index}`;
2573
+ const cached = resolved.get(cacheKey);
2574
+ if (cached) return cached;
2575
+ const pending = provisionKey(kind, index, appId, acls, description).catch(
2576
+ (err) => {
2577
+ resolved.delete(cacheKey);
2578
+ throw err;
2579
+ }
2580
+ );
2581
+ resolved.set(cacheKey, pending);
2582
+ return pending;
2583
+ }
2584
+ async function provisionKey(kind, index, appId, acls, description) {
2585
+ const stored = await readStoredKey(kind, index, appId);
2479
2586
  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" };
2587
+ if (await keyExists(stored)) {
2588
+ logger.info({ kind, index, appId }, "reusing the stored API key");
2589
+ return { key: stored, source: "keychain" };
2483
2590
  }
2484
- logger.warn(
2485
- { index, appId },
2486
- "the stored search-only API key no longer exists; creating a replacement"
2591
+ logger.info(
2592
+ { kind, index, appId },
2593
+ "the stored API key no longer exists; creating another"
2487
2594
  );
2488
- await forgetSearchKey(index);
2489
2595
  }
2490
- const key = await createSearchOnlyKey(index);
2491
- await recordSearchKey(index, appId, key);
2596
+ const key = await createKey(index, acls, description);
2597
+ await storeKey(kind, index, appId, key);
2492
2598
  return { key, source: "created" };
2493
2599
  }
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
- }
2600
+ function resolveWriteKey(index, appId) {
2601
+ return resolveKey(
2602
+ "write",
2603
+ index,
2604
+ appId,
2605
+ WRITE_ACLS,
2606
+ `Algolia Wizard write key for ${index} index`
2607
+ );
2608
+ }
2609
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2610
+ if (envKey) return { key: envKey, source: "env" };
2611
+ return resolveKey(
2612
+ "search",
2613
+ index,
2614
+ appId,
2615
+ ["search"],
2616
+ `Algolia Wizard search-only key for ${index} index`
2617
+ );
2503
2618
  }
2504
2619
 
2505
2620
  // src/lib/tools/writeAlgoliaCredentials.ts
2506
2621
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2507
2622
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2623
+ var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
2508
2624
  function appendEnv(content, entries) {
2509
2625
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2510
2626
  const lines = entries.map(([name, value]) => `${name}=${value}
@@ -2514,53 +2630,111 @@ function appendEnv(content, entries) {
2514
2630
  function hasEnv(content, name) {
2515
2631
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
2516
2632
  }
2633
+ function readEnv(content, name) {
2634
+ const found = content.match(
2635
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*(.*)$`, "m")
2636
+ );
2637
+ if (!found) return null;
2638
+ const raw = found[1].trim();
2639
+ const quoted = raw.match(/^(['"])(.*)\1/);
2640
+ const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, "");
2641
+ return value.length > 0 ? value : null;
2642
+ }
2643
+ function upsertEnv(content, name, value) {
2644
+ if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
2645
+ return content.replace(
2646
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "gm"),
2647
+ () => `${name}=${value}`
2648
+ );
2649
+ }
2517
2650
  function writeCredentialsTool(ctx) {
2518
2651
  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(
2652
+ 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.`,
2653
+ inputSchema: z13.object({
2654
+ filePath: z13.string().describe(
2522
2655
  'Path to the env file to write credentials into (e.g. ".env")'
2523
2656
  )
2524
2657
  }),
2525
2658
  execute: async ({ filePath }) => {
2526
2659
  logger.info({ filePath }, "called writeCredentials tool");
2527
- const resolved = resolveInRoot(ctx, filePath);
2528
- if (resolved.ok === false) return resolved.error;
2660
+ const resolved2 = resolveInRoot(ctx, filePath);
2661
+ if (resolved2.ok === false) return resolved2.error;
2529
2662
  const targetIndex = useWizard.getState().targetIndex;
2530
2663
  if (!targetIndex) {
2531
2664
  return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2532
2665
  }
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
- }
2666
+ let existing = "";
2667
+ let present;
2541
2668
  try {
2542
- if (await hasSymlinkParent(ctx, resolved.target)) {
2543
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2669
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2670
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2544
2671
  }
2545
- let existing = "";
2546
2672
  try {
2547
- existing = await readFile4(resolved.target, "utf8");
2673
+ existing = await readFile5(resolved2.target, "utf8");
2548
2674
  } catch (err) {
2549
2675
  if (err.code !== "ENOENT") throw err;
2550
2676
  }
2551
- const present = [APP_ID_VAR, API_KEY_VAR].filter(
2552
- (name) => hasEnv(existing, name)
2677
+ present = [APP_ID_VAR, API_KEY_VAR].filter(
2678
+ (name) => readEnv(existing, name) !== null
2553
2679
  );
2680
+ } catch (err) {
2681
+ return `Error writing credentials to ${filePath}: ${err.message}`;
2682
+ }
2683
+ const credentials = [];
2684
+ const notes = [];
2685
+ const fileAppId = readEnv(existing, APP_ID_VAR);
2686
+ const fileKey = readEnv(existing, API_KEY_VAR);
2687
+ const fileIndex = readEnv(existing, INDEX_NAME_VAR);
2688
+ if (fileAppId === null || fileKey === null) {
2689
+ try {
2690
+ const selected = (await requireApplication()).id;
2691
+ if (fileAppId === null) credentials.push([APP_ID_VAR, selected]);
2692
+ if (fileKey === null) {
2693
+ if (fileAppId !== null && fileAppId !== selected) {
2694
+ notes.push(
2695
+ `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.`
2696
+ );
2697
+ } else {
2698
+ credentials.push([
2699
+ API_KEY_VAR,
2700
+ (await resolveWriteKey(targetIndex, selected)).key
2701
+ ]);
2702
+ }
2703
+ } else {
2704
+ notes.push(
2705
+ `The existing ${API_KEY_VAR} must belong to application ${selected} or writes will fail.`
2706
+ );
2707
+ }
2708
+ } catch (err) {
2709
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2710
+ }
2711
+ }
2712
+ try {
2713
+ const updated = [
2714
+ ...credentials,
2715
+ // Unconditional, unlike the credentials: the write key is scoped to
2716
+ // the run's index, so a stale name left in the file earns a 403.
2717
+ [INDEX_NAME_VAR, targetIndex]
2718
+ ].reduce(
2719
+ (content, [name, value]) => upsertEnv(content, name, value),
2720
+ existing
2721
+ );
2722
+ await mkdir4(dirname5(resolved2.target), { recursive: true });
2723
+ await writeFile4(resolved2.target, updated, "utf8");
2724
+ const sentences = [
2725
+ `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
2726
+ ];
2727
+ if (fileIndex !== null && fileIndex !== targetIndex) {
2728
+ sentences.push(
2729
+ `That replaced the ${INDEX_NAME_VAR} already there ("${fileIndex}"), which this run does not target.`
2730
+ );
2731
+ }
2554
2732
  if (present.length > 0) {
2555
- return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2733
+ sentences.push(
2734
+ `Skipped ${present.join(" and ")}: already defined there.`
2735
+ );
2556
2736
  }
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}`;
2737
+ return [...sentences, ...notes].join(" ");
2564
2738
  } catch (err) {
2565
2739
  return `Error writing credentials to ${filePath}: ${err.message}`;
2566
2740
  }
@@ -2570,16 +2744,16 @@ function writeCredentialsTool(ctx) {
2570
2744
 
2571
2745
  // src/lib/tools/searchFiles.ts
2572
2746
  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";
2747
+ import z14 from "zod";
2748
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2749
+ import { join as join8 } from "node:path";
2576
2750
  var MAX_QUERY_LENGTH = 1e3;
2577
2751
  async function walkFiles(dir) {
2578
2752
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2579
2753
  const out = [];
2580
2754
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2581
2755
  if (e.name.startsWith(".") || skip.has(e.name)) continue;
2582
- const full = join7(dir, e.name);
2756
+ const full = join8(dir, e.name);
2583
2757
  if (e.isDirectory()) out.push(...await walkFiles(full));
2584
2758
  else if (e.isFile()) out.push(full);
2585
2759
  }
@@ -2588,9 +2762,9 @@ async function walkFiles(dir) {
2588
2762
  function searchFilesTool(ctx) {
2589
2763
  return tool7({
2590
2764
  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)")
2765
+ inputSchema: z14.object({
2766
+ query: z14.string().describe("JavaScript RegExp pattern to search for"),
2767
+ path: z14.string().optional().describe("Directory to search in (default: cwd)")
2594
2768
  }),
2595
2769
  execute: async ({ query, path = "." }) => {
2596
2770
  logger.info({ query, path }, "called searchFiles tool");
@@ -2600,8 +2774,8 @@ function searchFilesTool(ctx) {
2600
2774
  if (query.length > MAX_QUERY_LENGTH) {
2601
2775
  return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
2602
2776
  }
2603
- const resolved = resolveInRoot(ctx, path);
2604
- if (!resolved.ok) return resolved.error;
2777
+ const resolved2 = resolveInRoot(ctx, path);
2778
+ if (!resolved2.ok) return resolved2.error;
2605
2779
  let re;
2606
2780
  try {
2607
2781
  re = new RegExp(query);
@@ -2609,10 +2783,10 @@ function searchFilesTool(ctx) {
2609
2783
  return `Invalid regex: ${err.message}`;
2610
2784
  }
2611
2785
  const matches = [];
2612
- for (const file of await walkFiles(resolved.target)) {
2786
+ for (const file of await walkFiles(resolved2.target)) {
2613
2787
  let content;
2614
2788
  try {
2615
- content = await readFile5(file, "utf8");
2789
+ content = await readFile6(file, "utf8");
2616
2790
  } catch {
2617
2791
  continue;
2618
2792
  }
@@ -2634,7 +2808,7 @@ function searchFilesTool(ctx) {
2634
2808
 
2635
2809
  // src/lib/tools/verifyImplementation.ts
2636
2810
  import { tool as tool8 } from "ai";
2637
- import z14 from "zod";
2811
+ import z15 from "zod";
2638
2812
 
2639
2813
  // src/lib/tools/utils/runCommand.ts
2640
2814
  import { spawn as spawn2 } from "node:child_process";
@@ -2656,9 +2830,9 @@ function runCommand(command, args, cwd) {
2656
2830
  }
2657
2831
 
2658
2832
  // src/lib/tools/utils/packageManager.ts
2659
- import { readFile as readFile6 } from "node:fs/promises";
2833
+ import { readFile as readFile7 } from "node:fs/promises";
2660
2834
  import { existsSync } from "node:fs";
2661
- import { join as join8 } from "node:path";
2835
+ import { join as join9 } from "node:path";
2662
2836
  var LOCKFILES = [
2663
2837
  ["pnpm-lock.yaml", "pnpm"],
2664
2838
  ["yarn.lock", "yarn"],
@@ -2667,13 +2841,13 @@ var LOCKFILES = [
2667
2841
  ["package-lock.json", "npm"]
2668
2842
  ];
2669
2843
  async function readPackageJson(cwd = process.cwd()) {
2670
- return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2844
+ return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
2671
2845
  }
2672
2846
  function packageManagerFrom(pkg) {
2673
2847
  return pkg.packageManager?.split("@")[0] ?? "npm";
2674
2848
  }
2675
2849
  function packageManagerFromLockfile(cwd) {
2676
- return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2850
+ return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
2677
2851
  }
2678
2852
  async function detectPackageManager(cwd) {
2679
2853
  try {
@@ -2714,7 +2888,7 @@ async function runRepoVerificationCheck() {
2714
2888
  function verifyImplementationTool() {
2715
2889
  return tool8({
2716
2890
  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(),
2891
+ inputSchema: z15.object(),
2718
2892
  execute: async () => {
2719
2893
  logger.info("called verifyImplementation tool");
2720
2894
  return runRepoVerificationCheck();
@@ -2728,7 +2902,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
2728
2902
  import { nanoid as nanoid2 } from "nanoid";
2729
2903
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2730
2904
  import { dirname as dirname6 } from "node:path";
2731
- import z15 from "zod";
2905
+ import z16 from "zod";
2732
2906
  var DATA_DIR = ".algolia-wizard/data";
2733
2907
  var RECORD_MODEL = "claude-haiku-4-5";
2734
2908
  var MAX_RECORDS = 100;
@@ -2740,17 +2914,17 @@ var anthropic = createAnthropic({
2740
2914
  function generateRecordTool(ctx) {
2741
2915
  return tool9({
2742
2916
  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.")
2917
+ inputSchema: z16.object({
2918
+ entityName: z16.string().describe("Name of the entity to generate records for."),
2919
+ attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
2920
+ count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
2921
+ hint: z16.string().optional().describe("Optional context to steer realistic values.")
2748
2922
  }),
2749
2923
  execute: async ({ entityName, attributes, count, hint }) => {
2750
2924
  logger.info({ entityName, count }, "called generateRecord tool");
2751
2925
  try {
2752
- const value = z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]);
2753
- const recordSchema = z15.object(
2926
+ const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
2927
+ const recordSchema = z16.object(
2754
2928
  Object.fromEntries(attributes.map((attr) => [attr, value]))
2755
2929
  );
2756
2930
  const generateBatch = async (batchCount) => {
@@ -2760,8 +2934,8 @@ function generateRecordTool(ctx) {
2760
2934
  const { output } = await generateText({
2761
2935
  model: anthropic(RECORD_MODEL),
2762
2936
  output: Output.object({
2763
- schema: z15.object({
2764
- records: z15.array(recordSchema).length(batchCount)
2937
+ schema: z16.object({
2938
+ records: z16.array(recordSchema).length(batchCount)
2765
2939
  })
2766
2940
  }),
2767
2941
  prompt: [
@@ -2797,13 +2971,13 @@ function generateRecordTool(ctx) {
2797
2971
  }));
2798
2972
  const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2799
2973
  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}).`;
2974
+ const resolved2 = resolveInRoot(ctx, relPath);
2975
+ if (resolved2.ok === false) return resolved2.error;
2976
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2977
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2804
2978
  }
2805
- await mkdir5(dirname6(resolved.target), { recursive: true });
2806
- await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2979
+ await mkdir5(dirname6(resolved2.target), { recursive: true });
2980
+ await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
2807
2981
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
2808
2982
  return {
2809
2983
  filePath: relPath,
@@ -2819,12 +2993,12 @@ function generateRecordTool(ctx) {
2819
2993
 
2820
2994
  // src/lib/tools/notifyUser.ts
2821
2995
  import { tool as tool10 } from "ai";
2822
- import z16 from "zod";
2996
+ import z17 from "zod";
2823
2997
  function notifyUserTool() {
2824
2998
  return tool10({
2825
2999
  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(
3000
+ inputSchema: z17.object({
3001
+ message: z17.string().describe(
2828
3002
  "Short, plain-language description of what you are doing now."
2829
3003
  )
2830
3004
  }),
@@ -3004,10 +3178,10 @@ async function runAgent(req) {
3004
3178
  }
3005
3179
 
3006
3180
  // 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() }))
3181
+ import z20 from "zod";
3182
+ var detectLanguageSchema = z20.object({
3183
+ languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
3184
+ frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
3011
3185
  });
3012
3186
  var detectLanguage = () => runAgent({
3013
3187
  instructions: [
@@ -3025,31 +3199,31 @@ var detectLanguage = () => runAgent({
3025
3199
  });
3026
3200
 
3027
3201
  // src/actions/analyzeCodebase.ts
3028
- import z20 from "zod";
3202
+ import z21 from "zod";
3029
3203
  var READONLY_TOOLS = [
3030
3204
  "listFiles",
3031
3205
  "changeDirectory",
3032
3206
  "readFile",
3033
3207
  "searchFiles"
3034
3208
  ];
3035
- var ingestionAnalysisSchema = z20.object({
3036
- ingestionAnalysis: z20.array(
3037
- z20.object({
3038
- name: z20.string(),
3039
- paths: z20.array(z20.string()),
3209
+ var ingestionAnalysisSchema = z21.object({
3210
+ ingestionAnalysis: z21.array(
3211
+ z21.object({
3212
+ name: z21.string(),
3213
+ paths: z21.array(z21.string()),
3040
3214
  // indexable fields the agent found for this entity
3041
- attributes: z20.array(z20.string())
3215
+ attributes: z21.array(z21.string())
3042
3216
  })
3043
3217
  )
3044
3218
  });
3045
- var searchImplementationAnalysisSchema = z20.object({
3046
- searchImplementationAnalysis: z20.string()
3219
+ var searchImplementationAnalysisSchema = z21.object({
3220
+ searchImplementationAnalysis: z21.string()
3047
3221
  });
3048
- var verificationSchema = z20.object({
3049
- verification: z20.array(z20.string())
3222
+ var verificationSchema = z21.object({
3223
+ verification: z21.array(z21.string())
3050
3224
  });
3051
3225
  var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
3052
- var analyzeCodebaseSchema = z20.object({
3226
+ var analyzeCodebaseSchema = z21.object({
3053
3227
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3054
3228
  searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
3055
3229
  verification: verificationSchema.shape.verification.optional(),
@@ -3111,7 +3285,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3111
3285
  // package.json
3112
3286
  var package_default = {
3113
3287
  name: "@algolia/wizard",
3114
- version: "0.10.0",
3288
+ version: "0.11.0",
3115
3289
  description: "Magically implement Algolia functionality in your codebase",
3116
3290
  type: "module",
3117
3291
  engines: {
@@ -3162,6 +3336,7 @@ var package_default = {
3162
3336
  "@hono/node-server": "^2.0.10",
3163
3337
  "@segment/analytics-node": "^3.1.0",
3164
3338
  ai: "^6.0.190",
3339
+ "cross-keychain": "^1.1.0",
3165
3340
  dotenv: "^17.4.2",
3166
3341
  hono: "^4.12.27",
3167
3342
  ink: "^7.0.5",
@@ -3229,8 +3404,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
3229
3404
  }
3230
3405
 
3231
3406
  // src/actions/confirmLanguage.ts
3232
- import z22 from "zod";
3233
- var confirmLanguageSchema = z22.object({
3407
+ import z23 from "zod";
3408
+ var confirmLanguageSchema = z23.object({
3234
3409
  languages: detectLanguageSchema.shape.languages
3235
3410
  });
3236
3411
  async function confirmLanguage(ctx) {
@@ -3251,8 +3426,8 @@ async function confirmLanguage(ctx) {
3251
3426
  }
3252
3427
 
3253
3428
  // src/actions/confirmFramework.ts
3254
- import z23 from "zod";
3255
- var confirmFrameworkSchema = z23.object({
3429
+ import z24 from "zod";
3430
+ var confirmFrameworkSchema = z24.object({
3256
3431
  frameworks: detectLanguageSchema.shape.frameworks
3257
3432
  });
3258
3433
  var CURATED_FRAMEWORKS = [
@@ -3380,8 +3555,8 @@ async function promptUser(ctx, params) {
3380
3555
  }
3381
3556
 
3382
3557
  // src/actions/confirmEntities.ts
3383
- import z24 from "zod";
3384
- var confirmEntitiesSchema = z24.object({
3558
+ import z25 from "zod";
3559
+ var confirmEntitiesSchema = z25.object({
3385
3560
  // Final detection — the focused re-run may supersede project-scan's.
3386
3561
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
3387
3562
  confirmedEntities: confirmedEntitiesFieldSchema
@@ -3451,15 +3626,15 @@ async function confirmEntities(ctx) {
3451
3626
  }
3452
3627
 
3453
3628
  // src/actions/review.ts
3454
- import { z as z25 } from "zod";
3455
- var reviewSchema = z25.object({
3629
+ import { z as z26 } from "zod";
3630
+ var reviewSchema = z26.object({
3456
3631
  // Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
3457
3632
  // not one entry per workflow step — a step's raw output can be a long,
3458
3633
  // multi-paragraph blob (see implement.ts's summaries.join), and mirroring
3459
3634
  // 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())
3635
+ summaryPoints: z26.array(z26.string()),
3636
+ reviewPrompt: z26.string(),
3637
+ nextSteps: z26.array(z26.string())
3463
3638
  });
3464
3639
  function formatCompletedSteps(steps) {
3465
3640
  if (!steps.length) return "(no prior steps completed)";
@@ -3510,16 +3685,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3510
3685
  };
3511
3686
 
3512
3687
  // src/actions/implement.ts
3513
- import z26 from "zod";
3688
+ import z27 from "zod";
3514
3689
 
3515
3690
  // src/lib/worktree.ts
3516
3691
  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";
3692
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3518
3693
  import {
3519
3694
  basename as basename2,
3520
3695
  dirname as dirname7,
3521
3696
  isAbsolute as isAbsolute2,
3522
- join as join9,
3697
+ join as join10,
3523
3698
  relative as relative2,
3524
3699
  resolve as resolve3
3525
3700
  } from "node:path";
@@ -3553,7 +3728,7 @@ async function isWorkingTreeDirty(repoRoot) {
3553
3728
  return out.trim().length > 0;
3554
3729
  }
3555
3730
  async function pruneOldWorktrees(repoRoot) {
3556
- const dir = join9(stateDir(repoRoot), "worktrees");
3731
+ const dir = join10(stateDir(repoRoot), "worktrees");
3557
3732
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3558
3733
  for (const slug of stale) {
3559
3734
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3564,7 +3739,7 @@ async function pruneOldWorktrees(repoRoot) {
3564
3739
  "worktree",
3565
3740
  "remove",
3566
3741
  "--force",
3567
- join9(dir, slug)
3742
+ join10(dir, slug)
3568
3743
  ]);
3569
3744
  await git(["-C", repoRoot, "branch", "-D", branch]);
3570
3745
  } catch (err) {
@@ -3578,7 +3753,7 @@ async function pruneOldWorktrees(repoRoot) {
3578
3753
  async function createWorktree(repoRoot) {
3579
3754
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3580
3755
  const dirSlug = branch.replace(/\//g, "-");
3581
- const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
3756
+ const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3582
3757
  await git(["-C", repoRoot, "worktree", "prune"]);
3583
3758
  await pruneOldWorktrees(repoRoot);
3584
3759
  await mkdir6(dirname7(path), { recursive: true });
@@ -3698,8 +3873,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3698
3873
  } catch {
3699
3874
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3700
3875
  }
3701
- const relPath = join9(ingestDir, basename2(source));
3702
- const dest = join9(worktreePath, relPath);
3876
+ const relPath = join10(ingestDir, basename2(source));
3877
+ const dest = join10(worktreePath, relPath);
3703
3878
  try {
3704
3879
  await mkdir6(dirname7(dest), { recursive: true });
3705
3880
  await copyFile(source, dest);
@@ -3717,7 +3892,7 @@ function hasEnvVar(content, name) {
3717
3892
  async function readEnvVar(worktreePath, name) {
3718
3893
  let content;
3719
3894
  try {
3720
- content = await readFile7(join9(worktreePath, ".env"), "utf8");
3895
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
3721
3896
  } catch (err) {
3722
3897
  if (err.code !== "ENOENT") throw err;
3723
3898
  return void 0;
@@ -3732,10 +3907,10 @@ async function readEnvVar(worktreePath, name) {
3732
3907
  return value;
3733
3908
  }
3734
3909
  async function writeSearchEnvValues(worktreePath, vars) {
3735
- const target = join9(worktreePath, ".env");
3910
+ const target = join10(worktreePath, ".env");
3736
3911
  let existing = "";
3737
3912
  try {
3738
- existing = await readFile7(target, "utf8");
3913
+ existing = await readFile8(target, "utf8");
3739
3914
  } catch (err) {
3740
3915
  if (err.code !== "ENOENT") throw err;
3741
3916
  }
@@ -3805,13 +3980,13 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3805
3980
 
3806
3981
  // src/lib/algoliaDocs.ts
3807
3982
  import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3808
- import { dirname as dirname8, join as join10 } from "node:path";
3983
+ import { dirname as dirname8, join as join11 } from "node:path";
3809
3984
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3810
- var DOCS_SUBPATH = join10("docs", "algolia-sdk");
3985
+ var DOCS_SUBPATH = join11("docs", "algolia-sdk");
3811
3986
  function findDocsDir() {
3812
3987
  let dir = dirname8(fileURLToPath2(import.meta.url));
3813
3988
  for (; ; ) {
3814
- const candidate = join10(dir, DOCS_SUBPATH);
3989
+ const candidate = join11(dir, DOCS_SUBPATH);
3815
3990
  if (existsSync2(candidate)) return candidate;
3816
3991
  const parent = dirname8(dir);
3817
3992
  if (parent === dir) return void 0;
@@ -3834,7 +4009,7 @@ function loadAlgoliaDoc(language) {
3834
4009
  );
3835
4010
  return "";
3836
4011
  }
3837
- return readFileSync(join10(docsDir, files[0]), "utf8").trim();
4012
+ return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3838
4013
  }
3839
4014
  function getNamedDoc(name, language) {
3840
4015
  const docsDir = findDocsDir();
@@ -3842,7 +4017,7 @@ function getNamedDoc(name, language) {
3842
4017
  logger.warn("docs/algolia-sdk not found");
3843
4018
  return "";
3844
4019
  }
3845
- const file = join10(docsDir, `${name}-${language}.md`);
4020
+ const file = join11(docsDir, `${name}-${language}.md`);
3846
4021
  if (!existsSync2(file)) {
3847
4022
  logger.warn({ name, language }, "named SDK reference not found");
3848
4023
  return "";
@@ -3869,34 +4044,34 @@ function shellQuote(value) {
3869
4044
  }
3870
4045
 
3871
4046
  // 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()
4047
+ var implementSchema = z27.object({
4048
+ filesChanged: z27.array(z27.string()),
4049
+ summary: z27.string(),
4050
+ worktreePath: z27.string().optional(),
4051
+ ingestCommand: z27.string().optional(),
4052
+ ingestScriptRan: z27.boolean().optional(),
4053
+ ingestRecordCount: z27.number().optional(),
4054
+ ingestDurationMs: z27.number().optional(),
4055
+ ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
4056
+ searchEnvVars: z27.array(
4057
+ z27.object({
4058
+ name: z27.string(),
4059
+ value: z27.string()
3885
4060
  })
3886
4061
  ).optional()
3887
4062
  });
3888
- var implementationOutputSchema = z26.object({
3889
- summary: z26.string(),
4063
+ var implementationOutputSchema = z27.object({
4064
+ summary: z27.string(),
3890
4065
  // Ingestion only: a structured pair the wizard turns into an argv, never a
3891
4066
  // free-form command string. `runtime` is allowlisted and `entrypoint` is
3892
4067
  // validated worktree-relative, so the agent cannot inject extra commands.
3893
- runtime: z26.enum(INGEST_RUNTIMES).optional(),
3894
- entrypoint: z26.string().optional()
4068
+ runtime: z27.enum(INGEST_RUNTIMES).optional(),
4069
+ entrypoint: z27.string().optional()
3895
4070
  });
3896
- var verificationOutputSchema = z26.object({
3897
- summary: z26.string(),
3898
- sufficient: z26.boolean(),
3899
- additionalInstructions: z26.string().optional()
4071
+ var verificationOutputSchema = z27.object({
4072
+ summary: z27.string(),
4073
+ sufficient: z27.boolean(),
4074
+ additionalInstructions: z27.string().optional()
3900
4075
  });
3901
4076
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3902
4077
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -3941,13 +4116,17 @@ function publicEnvPrefix(language) {
3941
4116
  }
3942
4117
  var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
3943
4118
  var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4119
+ var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
3944
4120
  function appIdVar(language) {
3945
4121
  return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
3946
4122
  }
3947
4123
  function searchKeyVar(language) {
3948
4124
  return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3949
4125
  }
3950
- function searchEnvVars(language, appId, searchKey) {
4126
+ function searchIndexVar(language) {
4127
+ return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
4128
+ }
4129
+ function searchEnvVars(language, index, appId, searchKey) {
3951
4130
  return [
3952
4131
  {
3953
4132
  name: appIdVar(language),
@@ -3956,12 +4135,21 @@ function searchEnvVars(language, appId, searchKey) {
3956
4135
  {
3957
4136
  name: searchKeyVar(language),
3958
4137
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4138
+ },
4139
+ // Wizard-supplied rather than written into the generated code, because an
4140
+ // agent that retypes the name (appending the project name, re-casing it)
4141
+ // leaves the UI querying an index that does not exist.
4142
+ {
4143
+ name: searchIndexVar(language),
4144
+ value: index
3959
4145
  }
3960
4146
  ];
3961
4147
  }
3962
4148
  function baseInstructions(input) {
3963
4149
  return [
3964
- `Target Algolia index: ${input.targetIndex}`,
4150
+ // Agents have renamed this (e.g. appending the project name), which the
4151
+ // index-scoped keys then reject with a 403.
4152
+ `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
3965
4153
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
3966
4154
  "Make minimal, idiomatic changes; do not touch unrelated code."
3967
4155
  ];
@@ -3995,6 +4183,7 @@ function ingestionInstructions(input) {
3995
4183
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3996
4184
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3997
4185
  `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.`,
4186
+ `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
4187
  "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
3999
4188
  "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
4189
  getNamedDoc("save-records", "js"),
@@ -4012,7 +4201,8 @@ function searchInstructions(input) {
4012
4201
  `Build the search UI for ${input.uiFramework}.`,
4013
4202
  "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4014
4203
  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.`,
4204
+ `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.`,
4205
+ `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
4206
  // The key is provisioned only after verification passes, so the agent never
4017
4207
  // sees one. It must also leave .env alone: the wizard reads that file to
4018
4208
  // decide whether a key already exists, and an agent-invented value there
@@ -4022,7 +4212,7 @@ function searchInstructions(input) {
4022
4212
  // ".env" right after this step, so a renamed prefix would leave the code
4023
4213
  // reading a var the wizard never wrote.
4024
4214
  `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.",
4215
+ "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
4216
  '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
4217
  "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
4218
  ];
@@ -4206,9 +4396,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4206
4396
  targetIndex,
4207
4397
  language,
4208
4398
  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),
4399
+ searchEnvVars: searchEnvVars(language, targetIndex, appId),
4212
4400
  ingestDir: INGEST_DIR,
4213
4401
  ingestionSource,
4214
4402
  uploadFilePath,
@@ -4295,7 +4483,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4295
4483
  }) === true;
4296
4484
  if (runNow) {
4297
4485
  const ingestApp = await requireApplication();
4298
- const writeKey = await resolveWriteKey(targetIndex);
4486
+ const writeKey = (await resolveWriteKey(targetIndex, ingestApp.id)).key;
4299
4487
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
4300
4488
  const scriptLogId = ctx.logStart("runIngestScript", {
4301
4489
  runtime: ingestRuntime,
@@ -4308,7 +4496,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4308
4496
  ingestEntrypoint,
4309
4497
  {
4310
4498
  [APP_ID_VAR]: ingestApp.id,
4311
- [API_KEY_VAR]: writeKey
4499
+ [API_KEY_VAR]: writeKey,
4500
+ [INDEX_NAME_VAR]: targetIndex
4312
4501
  }
4313
4502
  );
4314
4503
  ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
@@ -4429,14 +4618,14 @@ ${run2.output}` : status;
4429
4618
  let searchKeyError;
4430
4619
  if (appId) {
4431
4620
  try {
4432
- const resolved = await resolveSearchOnlyKey(
4621
+ const resolved2 = await resolveSearchOnlyKey(
4433
4622
  targetIndex,
4434
4623
  appId,
4435
4624
  envSearchKey
4436
4625
  );
4437
- searchKey = resolved.key;
4626
+ searchKey = resolved2.key;
4438
4627
  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}.`
4628
+ 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
4629
  );
4441
4630
  } catch (err) {
4442
4631
  searchKeyError = err.message;
@@ -4446,7 +4635,12 @@ ${run2.output}` : status;
4446
4635
  );
4447
4636
  }
4448
4637
  }
4449
- finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4638
+ finalSearchEnvVars = searchEnvVars(
4639
+ language,
4640
+ targetIndex,
4641
+ appId,
4642
+ searchKey
4643
+ );
4450
4644
  const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4451
4645
  (v) => !v.value.startsWith("<")
4452
4646
  );
@@ -4555,8 +4749,8 @@ var defaultWorkflow = {
4555
4749
  defineStep({
4556
4750
  id: "select-index",
4557
4751
  title: "Set up index",
4558
- outputSchema: z27.object({
4559
- selection: z27.string()
4752
+ outputSchema: z28.object({
4753
+ selection: z28.string()
4560
4754
  }),
4561
4755
  run: (ctx) => selectIndexStep(ctx)
4562
4756
  }),
@@ -4798,7 +4992,11 @@ Options:
4798
4992
  --no-telemetry Send no telemetry or analytics for this run.
4799
4993
  --reset-on-run Wipe this project's wizard state (run state, AI consent,
4800
4994
  worktrees) before starting, so the run behaves like a
4801
- first-ever run. Algolia credentials are not touched.
4995
+ first-ever run. Also drops every API key the wizard has
4996
+ stored in your keychain (or, where the platform has none,
4997
+ the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
4998
+ for this project and any other, so later runs create new
4999
+ ones. Your Algolia login is not touched.
4802
5000
  -h, --help Print this message.`;
4803
5001
  function parseCliArgs(argv) {
4804
5002
  const positionals = [];
@@ -4835,10 +5033,11 @@ function parseCliArgs(argv) {
4835
5033
 
4836
5034
  // src/lib/resetState.ts
4837
5035
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4838
- import { join as join11 } from "node:path";
5036
+ import { join as join12 } from "node:path";
4839
5037
  var KEEP = ["wizard.log"];
4840
5038
  async function resetProjectState() {
4841
5039
  const dir = stateDir();
5040
+ await forgetResolvedKeys();
4842
5041
  let entries;
4843
5042
  try {
4844
5043
  entries = await readdir4(dir);
@@ -4847,7 +5046,9 @@ async function resetProjectState() {
4847
5046
  }
4848
5047
  const targets = entries.filter((name) => !KEEP.includes(name));
4849
5048
  await Promise.all(
4850
- targets.map((name) => rm2(join11(dir, name), { recursive: true, force: true }))
5049
+ targets.map(
5050
+ (name) => rm2(join12(dir, name), { recursive: true, force: true })
5051
+ )
4851
5052
  );
4852
5053
  return { dir, removed: targets };
4853
5054
  }
@@ -4855,6 +5056,7 @@ async function resetProjectState() {
4855
5056
  // src/main.tsx
4856
5057
  import { jsx as jsx14 } from "react/jsx-runtime";
4857
5058
  async function startup() {
5059
+ setProjectRoot(process.cwd());
4858
5060
  let args;
4859
5061
  try {
4860
5062
  args = parseCliArgs(process.argv.slice(2));