@algolia/wizard 0.9.0-rc.87.83 → 0.9.0-rc.87.86

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 +229 -221
  2. package/package.json +2 -1
package/dist/main.js CHANGED
@@ -26,10 +26,14 @@ import { join, resolve } from "node:path";
26
26
  function rootDir() {
27
27
  return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
28
28
  }
29
- function projectSlug(cwd = process.cwd()) {
29
+ var pinnedRoot;
30
+ function setProjectRoot(cwd) {
31
+ pinnedRoot = resolve(cwd);
32
+ }
33
+ function projectSlug(cwd = pinnedRoot ?? process.cwd()) {
30
34
  return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
31
35
  }
32
- function stateDir(cwd = process.cwd()) {
36
+ function stateDir(cwd = pinnedRoot ?? process.cwd()) {
33
37
  return join(rootDir(), projectSlug(cwd));
34
38
  }
35
39
 
@@ -58,6 +62,11 @@ function npxArgs(args) {
58
62
  return ["--yes", "@algolia/cli@latest", ...args];
59
63
  }
60
64
  var shell = process.platform === "win32";
65
+ function childEnv(withoutAdminKey) {
66
+ if (!withoutAdminKey) return void 0;
67
+ const { ALGOLIA_API_KEY: _adminKey, ...rest } = process.env;
68
+ return rest;
69
+ }
61
70
  function lineSplitter(emit) {
62
71
  let buffer = "";
63
72
  return {
@@ -81,11 +90,14 @@ var stderrSink = (stream, line) => {
81
90
  if (stream === "stdout") return;
82
91
  wizardSink(stream, line);
83
92
  };
84
- function runAlgoliaCli(args, { onOutput } = {}) {
93
+ function runAlgoliaCli(args, { onOutput, withoutAdminKey } = {}) {
85
94
  const store = useWizard.getState();
86
95
  const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
87
96
  return new Promise((resolve4, reject) => {
88
- const child = spawn("npx", npxArgs(args), { shell });
97
+ const child = spawn("npx", npxArgs(args), {
98
+ shell,
99
+ env: childEnv(withoutAdminKey)
100
+ });
89
101
  let stdout = "";
90
102
  let stderr = "";
91
103
  const splitters = {
@@ -1813,18 +1825,17 @@ import "zod";
1813
1825
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1814
1826
  import { join as join5 } from "node:path";
1815
1827
  var configFile = () => join5(stateDir(), "config.json");
1816
- var DEFAULT_CONFIG = {
1828
+ var defaultConfig = () => ({
1817
1829
  version: 1,
1818
1830
  aiConsent: false,
1819
- workflowsRun: [],
1820
- searchApiKeys: {}
1821
- };
1831
+ workflowsRun: []
1832
+ });
1822
1833
  async function loadConfig() {
1823
1834
  try {
1824
1835
  const raw = await readFile2(configFile(), "utf8");
1825
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1836
+ return { ...defaultConfig(), ...JSON.parse(raw) };
1826
1837
  } catch {
1827
- return { ...DEFAULT_CONFIG };
1838
+ return defaultConfig();
1828
1839
  }
1829
1840
  }
1830
1841
  async function saveConfig(config) {
@@ -1836,38 +1847,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
1836
1847
  config.workflowsRun.push({ workflowId, completedAt });
1837
1848
  await saveConfig(config);
1838
1849
  }
1839
- function isStoredSearchKey(value) {
1840
- if (typeof value !== "object" || value === null) return false;
1841
- const { appId, key } = value;
1842
- return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
1843
- }
1844
- function storedSearchKeys(config) {
1845
- const stored = config.searchApiKeys;
1846
- if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
1847
- return {};
1848
- }
1849
- return stored;
1850
- }
1851
- async function getStoredSearchKey(index, appId) {
1852
- const entry = storedSearchKeys(await loadConfig())[index];
1853
- if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
1854
- return entry.key;
1855
- }
1856
- async function storeSearchKey(index, appId, key) {
1857
- const config = await loadConfig();
1858
- config.searchApiKeys = {
1859
- ...storedSearchKeys(config),
1860
- [index]: { appId, key }
1861
- };
1862
- await saveConfig(config);
1863
- }
1864
- async function forgetSearchKey(index) {
1865
- const config = await loadConfig();
1866
- const remaining = { ...storedSearchKeys(config) };
1867
- delete remaining[index];
1868
- config.searchApiKeys = remaining;
1869
- await saveConfig(config);
1870
- }
1871
1850
 
1872
1851
  // src/core/orchestrator.ts
1873
1852
  function defineStep(step) {
@@ -2315,9 +2294,9 @@ function listFilesTool(ctx) {
2315
2294
  if (++ctx.counts.list > ctx.limits.list) {
2316
2295
  return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
2317
2296
  }
2318
- const resolved = resolveInRoot(ctx, ".");
2319
- if (!resolved.ok) return resolved.error;
2320
- 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 });
2321
2300
  return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2322
2301
  }
2323
2302
  });
@@ -2335,14 +2314,14 @@ function changeDirectoryTool(ctx) {
2335
2314
  }),
2336
2315
  execute: async ({ path }) => {
2337
2316
  logger.info({ path }, "called changeDirectory tool");
2338
- const resolved = resolveInRoot(ctx, path);
2339
- if (!resolved.ok) return resolved.error;
2317
+ const resolved2 = resolveInRoot(ctx, path);
2318
+ if (!resolved2.ok) return resolved2.error;
2340
2319
  try {
2341
- const info = await stat(resolved.target);
2320
+ const info = await stat(resolved2.target);
2342
2321
  if (!info.isDirectory()) {
2343
2322
  return `Error changing directory to ${path}: not a directory`;
2344
2323
  }
2345
- ctx.cwd = resolved.target;
2324
+ ctx.cwd = resolved2.target;
2346
2325
  return `Changed working directory to ${ctx.cwd}`;
2347
2326
  } catch (err) {
2348
2327
  return `Error changing directory to ${path}: ${err.message}`;
@@ -2407,11 +2386,11 @@ function readFileTool(ctx) {
2407
2386
  return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
2408
2387
  }
2409
2388
  logger.info({ filePath }, "called readFile tool");
2410
- const resolved = resolveInRoot(ctx, filePath);
2411
- if (!resolved.ok) return resolved.error;
2389
+ const resolved2 = resolveInRoot(ctx, filePath);
2390
+ if (!resolved2.ok) return resolved2.error;
2412
2391
  try {
2413
- const content = await readFile3(resolved.target, "utf8");
2414
- return isEnvFile(resolved.target) ? redactEnvValues(content) : content;
2392
+ const content = await readFile3(resolved2.target, "utf8");
2393
+ return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2415
2394
  } catch (err) {
2416
2395
  return `Error reading ${filePath}: ${err.message}`;
2417
2396
  }
@@ -2433,17 +2412,17 @@ function writeFileTool(ctx) {
2433
2412
  }),
2434
2413
  execute: async ({ filePath, content }) => {
2435
2414
  logger.info({ filePath }, "called writeFile tool");
2436
- const resolved = resolveInRoot(ctx, filePath);
2437
- if (resolved.ok === false) return resolved.error;
2438
- if (isSecretEnvFile(resolved.target)) {
2415
+ const resolved2 = resolveInRoot(ctx, filePath);
2416
+ if (resolved2.ok === false) return resolved2.error;
2417
+ if (isSecretEnvFile(resolved2.target)) {
2439
2418
  return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
2440
2419
  }
2441
2420
  try {
2442
- if (await hasSymlinkParent(ctx, resolved.target)) {
2443
- 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}).`;
2444
2423
  }
2445
- await mkdir3(dirname4(resolved.target), { recursive: true });
2446
- await writeFile3(resolved.target, content, "utf8");
2424
+ await mkdir3(dirname4(resolved2.target), { recursive: true });
2425
+ await writeFile3(resolved2.target, content, "utf8");
2447
2426
  return `Wrote to ${filePath}`;
2448
2427
  } catch (err) {
2449
2428
  return `Error writing ${filePath}: ${err.message}`;
@@ -2460,6 +2439,40 @@ import { dirname as dirname5 } from "node:path";
2460
2439
 
2461
2440
  // src/lib/algoliaApiKey.ts
2462
2441
  import { z as z11 } from "zod";
2442
+
2443
+ // src/lib/keychain.ts
2444
+ import { getPassword, setPassword } from "cross-keychain";
2445
+ var SERVICE = "algolia-wizard";
2446
+ function account(kind, index, appId) {
2447
+ return `${kind}:${appId}:${index}`;
2448
+ }
2449
+ async function readStoredKey(kind, index, appId) {
2450
+ try {
2451
+ return await getPassword(SERVICE, account(kind, index, appId));
2452
+ } catch (err) {
2453
+ logger.warn(
2454
+ { err: err.message, kind, index, appId },
2455
+ "could not read the API key from the keychain"
2456
+ );
2457
+ return null;
2458
+ }
2459
+ }
2460
+ async function storeKey(kind, index, appId, value) {
2461
+ const name = account(kind, index, appId);
2462
+ try {
2463
+ await setPassword(SERVICE, name, value);
2464
+ if (await getPassword(SERVICE, name) !== value) {
2465
+ throw new Error("the keychain did not store the value");
2466
+ }
2467
+ } catch (err) {
2468
+ logger.warn(
2469
+ { err: err.message, kind, index, appId },
2470
+ "could not store the API key in the keychain; the next run will create another"
2471
+ );
2472
+ }
2473
+ }
2474
+
2475
+ // src/lib/algoliaApiKey.ts
2463
2476
  var WRITE_ACLS = [
2464
2477
  "addObject",
2465
2478
  "deleteObject",
@@ -2467,81 +2480,27 @@ var WRITE_ACLS = [
2467
2480
  "editSettings",
2468
2481
  "listIndexes"
2469
2482
  ];
2470
- var WRITE_ACL_SET = new Set(WRITE_ACLS);
2471
- var apiKeySchema = z11.object({
2472
- value: z11.string().min(1),
2473
- acl: z11.array(z11.string()).default([]),
2474
- indexes: z11.array(z11.string()).default([])
2475
- });
2476
- var apiKeyListSchema = z11.union([
2477
- z11.array(apiKeySchema),
2478
- z11.object({
2479
- items: z11.array(apiKeySchema).optional(),
2480
- keys: z11.array(apiKeySchema).optional()
2481
- })
2482
- ]).transform((o) => Array.isArray(o) ? o : o.items ?? o.keys ?? []);
2483
- function parseCliJson(schema, stdout, command) {
2484
- let raw;
2485
- try {
2486
- raw = JSON.parse(stdout);
2487
- } catch {
2488
- throw new Error(`Algolia CLI \`${command}\` returned unreadable JSON.`);
2489
- }
2490
- const parsed = schema.safeParse(raw);
2491
- if (!parsed.success) {
2492
- throw new Error(
2493
- `Algolia CLI \`${command}\` returned JSON in an unexpected shape.`
2494
- );
2495
- }
2496
- return parsed.data;
2497
- }
2498
2483
  var createdKeySchema = z11.object({
2499
2484
  key: z11.string().min(1).optional(),
2500
2485
  value: z11.string().min(1).optional()
2501
2486
  }).transform((o) => o.key ?? o.value);
2502
- function canReuseForWrites(key, index) {
2503
- return WRITE_ACLS.every((acl) => key.acl.includes(acl)) && key.acl.every((acl) => WRITE_ACL_SET.has(acl)) && key.indexes.includes(index);
2504
- }
2505
- async function resolveWriteKey(index) {
2506
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
2507
- const existing = parseCliJson(apiKeyListSchema, stdout, "apikeys list").find(
2508
- (key) => canReuseForWrites(key, index)
2509
- )?.value;
2510
- if (existing) {
2511
- logger.info({ index }, "reusing existing write API key");
2512
- return existing;
2513
- }
2514
- logger.info({ index }, "no reusable write key found; creating one");
2515
- const created = await runAlgoliaCli([
2516
- "apikeys",
2517
- "create",
2518
- "--indices",
2519
- index,
2520
- "--acl",
2521
- WRITE_ACLS.join(","),
2522
- "--description",
2523
- `wizard write key for ${index}`,
2524
- "-o",
2525
- "json"
2526
- ]);
2527
- const writeKey = parseCliJson(createdKeySchema, created, "apikeys create");
2528
- if (!writeKey) throw new Error("apikeys create returned no key value");
2529
- return writeKey;
2530
- }
2531
- async function createSearchOnlyKey(index) {
2532
- logger.info({ index }, "creating a search-only API key");
2533
- const stdout = await runAlgoliaCli([
2534
- "apikeys",
2535
- "create",
2536
- "--acl",
2537
- "search",
2538
- "--indices",
2539
- index,
2540
- "--description",
2541
- `Algolia Wizard search-only key for ${index}`,
2542
- "-o",
2543
- "json"
2544
- ]);
2487
+ async function createKey(index, acls, description) {
2488
+ logger.info({ index, acls }, "creating an API key");
2489
+ const stdout = await runAlgoliaCli(
2490
+ [
2491
+ "apikeys",
2492
+ "create",
2493
+ "--acl",
2494
+ acls.join(","),
2495
+ "--indices",
2496
+ index,
2497
+ "--description",
2498
+ description,
2499
+ "-o",
2500
+ "json"
2501
+ ],
2502
+ { withoutAdminKey: true }
2503
+ );
2545
2504
  let payload;
2546
2505
  try {
2547
2506
  payload = JSON.parse(stdout);
@@ -2552,49 +2511,57 @@ async function createSearchOnlyKey(index) {
2552
2511
  if (!created) throw new Error("apikeys create returned no key value");
2553
2512
  return created;
2554
2513
  }
2555
- async function apiKeyExists(key) {
2556
- try {
2557
- await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
2558
- return true;
2559
- } catch (err) {
2560
- return !/does not exist|not found|404/i.test(err.message);
2561
- }
2514
+ var resolved = /* @__PURE__ */ new Map();
2515
+ function forgetResolvedKeys() {
2516
+ resolved.clear();
2562
2517
  }
2563
- async function resolveSearchOnlyKey(index, appId, envKey) {
2564
- if (envKey) {
2565
- await recordSearchKey(index, appId, envKey);
2566
- return { key: envKey, source: "env" };
2567
- }
2568
- const stored = await getStoredSearchKey(index, appId);
2569
- if (stored) {
2570
- if (await apiKeyExists(stored)) {
2571
- logger.info({ index, appId }, "reusing the stored search-only API key");
2572
- return { key: stored, source: "config" };
2518
+ function resolveKey(kind, index, appId, acls, description) {
2519
+ const cacheKey = `${kind}:${appId}:${index}`;
2520
+ const cached = resolved.get(cacheKey);
2521
+ if (cached) return cached;
2522
+ const pending = provisionKey(kind, index, appId, acls, description).catch(
2523
+ (err) => {
2524
+ resolved.delete(cacheKey);
2525
+ throw err;
2573
2526
  }
2574
- logger.warn(
2575
- { index, appId },
2576
- "the stored search-only API key no longer exists; creating a replacement"
2577
- );
2578
- await forgetSearchKey(index);
2527
+ );
2528
+ resolved.set(cacheKey, pending);
2529
+ return pending;
2530
+ }
2531
+ async function provisionKey(kind, index, appId, acls, description) {
2532
+ const stored = await readStoredKey(kind, index, appId);
2533
+ if (stored) {
2534
+ logger.info({ kind, index, appId }, "reusing the stored API key");
2535
+ return { key: stored, source: "keychain" };
2579
2536
  }
2580
- const key = await createSearchOnlyKey(index);
2581
- await recordSearchKey(index, appId, key);
2537
+ const key = await createKey(index, acls, description);
2538
+ await storeKey(kind, index, appId, key);
2582
2539
  return { key, source: "created" };
2583
2540
  }
2584
- async function recordSearchKey(index, appId, key) {
2585
- try {
2586
- await storeSearchKey(index, appId, key);
2587
- } catch (err) {
2588
- logger.warn(
2589
- { err: err.message, index },
2590
- "could not record the search-only API key; a later run may create another"
2591
- );
2592
- }
2541
+ function resolveWriteKey(index, appId) {
2542
+ return resolveKey(
2543
+ "write",
2544
+ index,
2545
+ appId,
2546
+ WRITE_ACLS,
2547
+ `Algolia Wizard write key for ${index} index`
2548
+ );
2549
+ }
2550
+ async function resolveSearchOnlyKey(index, appId, envKey) {
2551
+ if (envKey) return { key: envKey, source: "env" };
2552
+ return resolveKey(
2553
+ "search",
2554
+ index,
2555
+ appId,
2556
+ ["search"],
2557
+ `Algolia Wizard search-only key for ${index} index`
2558
+ );
2593
2559
  }
2594
2560
 
2595
2561
  // src/lib/tools/writeAlgoliaCredentials.ts
2596
2562
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2597
2563
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2564
+ var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
2598
2565
  function appendEnv(content, entries) {
2599
2566
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2600
2567
  const lines = entries.map(([name, value]) => `${name}=${value}
@@ -2604,9 +2571,16 @@ function appendEnv(content, entries) {
2604
2571
  function hasEnv(content, name) {
2605
2572
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
2606
2573
  }
2574
+ function upsertEnv(content, name, value) {
2575
+ if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
2576
+ return content.replace(
2577
+ new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "m"),
2578
+ `${name}=${value}`
2579
+ );
2580
+ }
2607
2581
  function writeCredentialsTool(ctx) {
2608
2582
  return tool6({
2609
- 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.`,
2583
+ 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 (e.g. ".env"). Any name the file already defines is left untouched.`,
2610
2584
  inputSchema: z12.object({
2611
2585
  filePath: z12.string().describe(
2612
2586
  'Path to the env file to write credentials into (e.g. ".env")'
@@ -2614,43 +2588,51 @@ function writeCredentialsTool(ctx) {
2614
2588
  }),
2615
2589
  execute: async ({ filePath }) => {
2616
2590
  logger.info({ filePath }, "called writeCredentials tool");
2617
- const resolved = resolveInRoot(ctx, filePath);
2618
- if (resolved.ok === false) return resolved.error;
2591
+ const resolved2 = resolveInRoot(ctx, filePath);
2592
+ if (resolved2.ok === false) return resolved2.error;
2619
2593
  const targetIndex = useWizard.getState().targetIndex;
2620
2594
  if (!targetIndex) {
2621
2595
  return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
2622
2596
  }
2623
- let appId;
2624
- let writeKey;
2625
- try {
2626
- appId = (await requireApplication()).id;
2627
- writeKey = await resolveWriteKey(targetIndex);
2628
- } catch (err) {
2629
- return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2630
- }
2597
+ let existing = "";
2598
+ let present;
2631
2599
  try {
2632
- if (await hasSymlinkParent(ctx, resolved.target)) {
2633
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2600
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2601
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2634
2602
  }
2635
- let existing = "";
2636
2603
  try {
2637
- existing = await readFile4(resolved.target, "utf8");
2604
+ existing = await readFile4(resolved2.target, "utf8");
2638
2605
  } catch (err) {
2639
2606
  if (err.code !== "ENOENT") throw err;
2640
2607
  }
2641
- const present = [APP_ID_VAR, API_KEY_VAR].filter(
2608
+ present = [APP_ID_VAR, API_KEY_VAR].filter(
2642
2609
  (name) => hasEnv(existing, name)
2643
2610
  );
2644
- if (present.length > 0) {
2645
- return `Skipped: ${filePath} already defines ${present.join(" and ")}. Leaving existing credentials untouched.`;
2611
+ } catch (err) {
2612
+ return `Error writing credentials to ${filePath}: ${err.message}`;
2613
+ }
2614
+ let credentials = [];
2615
+ if (present.length === 0) {
2616
+ try {
2617
+ const appId = (await requireApplication()).id;
2618
+ credentials = [
2619
+ [APP_ID_VAR, appId],
2620
+ [API_KEY_VAR, (await resolveWriteKey(targetIndex, appId)).key]
2621
+ ];
2622
+ } catch (err) {
2623
+ return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2646
2624
  }
2647
- const envWithCredentials = appendEnv(existing, [
2648
- [APP_ID_VAR, appId],
2649
- [API_KEY_VAR, writeKey]
2650
- ]);
2651
- await mkdir4(dirname5(resolved.target), { recursive: true });
2652
- await writeFile4(resolved.target, envWithCredentials, "utf8");
2653
- return `Wrote Algolia credentials to ${filePath}`;
2625
+ }
2626
+ try {
2627
+ const updated = upsertEnv(
2628
+ appendEnv(existing, credentials),
2629
+ INDEX_NAME_VAR,
2630
+ targetIndex
2631
+ );
2632
+ await mkdir4(dirname5(resolved2.target), { recursive: true });
2633
+ await writeFile4(resolved2.target, updated, "utf8");
2634
+ const wrote = `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}`;
2635
+ return present.length > 0 ? `${wrote}. Skipped ${present.join(" and ")}: already defined there.` : wrote;
2654
2636
  } catch (err) {
2655
2637
  return `Error writing credentials to ${filePath}: ${err.message}`;
2656
2638
  }
@@ -2698,8 +2680,8 @@ function searchFilesTool(ctx) {
2698
2680
  if (query.length > MAX_QUERY_LENGTH) {
2699
2681
  return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
2700
2682
  }
2701
- const resolved = resolveInRoot(ctx, path);
2702
- if (!resolved.ok) return resolved.error;
2683
+ const resolved2 = resolveInRoot(ctx, path);
2684
+ if (!resolved2.ok) return resolved2.error;
2703
2685
  let re;
2704
2686
  try {
2705
2687
  re = new RegExp(query);
@@ -2707,7 +2689,7 @@ function searchFilesTool(ctx) {
2707
2689
  return `Invalid regex: ${err.message}`;
2708
2690
  }
2709
2691
  const matches = [];
2710
- for (const file of await walkFiles(resolved.target)) {
2692
+ for (const file of await walkFiles(resolved2.target)) {
2711
2693
  let content;
2712
2694
  try {
2713
2695
  content = await readFile5(file, "utf8");
@@ -2865,19 +2847,19 @@ function runShellTool(ctx) {
2865
2847
  if (++ctx.counts.shell > ctx.limits.shell) {
2866
2848
  return `Refused: command limit (${ctx.limits.shell}) reached. Stop running commands and report what you have.`;
2867
2849
  }
2868
- const resolved = resolveInRoot(ctx, cwd ?? ".");
2869
- if (!resolved.ok) return resolved.error;
2870
- logger.info({ command, cwd: resolved.target }, "called runShell tool");
2871
- const key = approvalKey(resolved.target, command);
2850
+ const resolved2 = resolveInRoot(ctx, cwd ?? ".");
2851
+ if (!resolved2.ok) return resolved2.error;
2852
+ logger.info({ command, cwd: resolved2.target }, "called runShell tool");
2853
+ const key = approvalKey(resolved2.target, command);
2872
2854
  const decision = ctx.shell.approved.has(key) ? "approve" : await ctx.shell.approve({
2873
2855
  command,
2874
- cwd: resolved.target,
2856
+ cwd: resolved2.target,
2875
2857
  explanation
2876
2858
  });
2877
2859
  if (decision === "reject") {
2878
2860
  ctx.shell.executions.push({
2879
2861
  command,
2880
- cwd: resolved.target,
2862
+ cwd: resolved2.target,
2881
2863
  approved: false
2882
2864
  });
2883
2865
  logger.info({ command }, "runShell: user rejected the command");
@@ -2890,7 +2872,7 @@ function runShellTool(ctx) {
2890
2872
  return {};
2891
2873
  });
2892
2874
  const run2 = await (ctx.shell.run ?? runShell)(command, {
2893
- cwd: resolved.target,
2875
+ cwd: resolved2.target,
2894
2876
  env,
2895
2877
  timeoutMs: ctx.shell.timeoutMs
2896
2878
  });
@@ -2906,7 +2888,7 @@ function runShellTool(ctx) {
2906
2888
  await markInteraction();
2907
2889
  ctx.shell.executions.push({
2908
2890
  command,
2909
- cwd: resolved.target,
2891
+ cwd: resolved2.target,
2910
2892
  approved: true,
2911
2893
  exitCode: run2.exitCode,
2912
2894
  output: run2.output,
@@ -2997,13 +2979,13 @@ function generateRecordTool(ctx) {
2997
2979
  }));
2998
2980
  const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2999
2981
  const relPath = `${DATA_DIR}/${slug}.json`;
3000
- const resolved = resolveInRoot(ctx, relPath);
3001
- if (resolved.ok === false) return resolved.error;
3002
- if (await hasSymlinkParent(ctx, resolved.target)) {
3003
- return `Refused: ${resolved.target} is outside the repo root (${ctx.root}).`;
2982
+ const resolved2 = resolveInRoot(ctx, relPath);
2983
+ if (resolved2.ok === false) return resolved2.error;
2984
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2985
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
3004
2986
  }
3005
- await mkdir5(dirname6(resolved.target), { recursive: true });
3006
- await writeFile5(resolved.target, JSON.stringify(records, null, 2), "utf8");
2987
+ await mkdir5(dirname6(resolved2.target), { recursive: true });
2988
+ await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
3007
2989
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
3008
2990
  return {
3009
2991
  filePath: relPath,
@@ -3296,7 +3278,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3296
3278
  // package.json
3297
3279
  var package_default = {
3298
3280
  name: "@algolia/wizard",
3299
- version: "0.9.0-rc.87.83",
3281
+ version: "0.9.0-rc.87.86",
3300
3282
  description: "Magically implement Algolia functionality in your codebase",
3301
3283
  type: "module",
3302
3284
  engines: {
@@ -3347,6 +3329,7 @@ var package_default = {
3347
3329
  "@hono/node-server": "^2.0.10",
3348
3330
  "@segment/analytics-node": "^3.1.0",
3349
3331
  ai: "^6.0.190",
3332
+ "cross-keychain": "^1.1.0",
3350
3333
  dotenv: "^17.4.2",
3351
3334
  hono: "^4.12.27",
3352
3335
  ink: "^7.0.5",
@@ -4023,13 +4006,17 @@ function publicEnvPrefix(language) {
4023
4006
  }
4024
4007
  var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4025
4008
  var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
4009
+ var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
4026
4010
  function appIdVar(language) {
4027
4011
  return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
4028
4012
  }
4029
4013
  function searchKeyVar(language) {
4030
4014
  return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
4031
4015
  }
4032
- function searchEnvVars(language, appId, searchKey) {
4016
+ function searchIndexVar(language) {
4017
+ return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
4018
+ }
4019
+ function searchEnvVars(language, index, appId, searchKey) {
4033
4020
  return [
4034
4021
  {
4035
4022
  name: appIdVar(language),
@@ -4038,12 +4025,21 @@ function searchEnvVars(language, appId, searchKey) {
4038
4025
  {
4039
4026
  name: searchKeyVar(language),
4040
4027
  value: searchKey ?? "<your-algolia-search-only-api-key>"
4028
+ },
4029
+ // Wizard-supplied rather than written into the generated code, because an
4030
+ // agent that retypes the name (appending the project name, re-casing it)
4031
+ // leaves the UI querying an index that does not exist.
4032
+ {
4033
+ name: searchIndexVar(language),
4034
+ value: index
4041
4035
  }
4042
4036
  ];
4043
4037
  }
4044
4038
  function baseInstructions(input) {
4045
4039
  return [
4046
- `Target Algolia index: ${input.targetIndex}`,
4040
+ // Agents have renamed this (e.g. appending the project name), which the
4041
+ // index-scoped keys then reject with a 403.
4042
+ `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
4047
4043
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4048
4044
  "Make minimal, idiomatic changes; do not touch unrelated code.",
4049
4045
  `Commands run through a shell on ${process.platform}. Write commands that work there.`,
@@ -4089,6 +4085,7 @@ function ingestionInstructions(input) {
4089
4085
  `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
4090
4086
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
4091
4087
  `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.`,
4088
+ `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.`,
4092
4089
  "Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
4093
4090
  "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.",
4094
4091
  ...algoliaClientDoc(input),
@@ -4110,8 +4107,9 @@ function searchInstructions(input) {
4110
4107
  ] : [
4111
4108
  "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
4112
4109
  ],
4113
- `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 search input and results list against the "${input.targetIndex}" index.`,
4114
- "Read the App ID and a search-only API key from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4110
+ `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 search input and results list against the target index.`,
4111
+ `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.`,
4112
+ "Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4115
4113
  // The key is provisioned only after verification passes, so the agent never
4116
4114
  // sees one. It must also leave .env alone: the wizard reads that file to
4117
4115
  // decide whether a key already exists, and an agent-invented value there
@@ -4203,7 +4201,6 @@ function ingestOutcome(executions, ingestCommand) {
4203
4201
  recordCount: parseIngestRecordCount(withCount[0]?.output ?? "")
4204
4202
  };
4205
4203
  }
4206
- var PLACEHOLDER_WRITE_KEY = "ALGOLIA_WRITE_KEY_PLACEHOLDER";
4207
4204
  function makeToolContext(worktree, env = async () => ({})) {
4208
4205
  return createToolContext(
4209
4206
  DEFAULT_TOOL_LIMITS,
@@ -4328,7 +4325,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4328
4325
  appId,
4329
4326
  // Names only: the search-only key is provisioned after verification, so
4330
4327
  // every value here is still a placeholder when the agent reads them.
4331
- searchEnvVars: searchEnvVars(language, appId),
4328
+ searchEnvVars: searchEnvVars(language, targetIndex, appId),
4332
4329
  ingestDir: INGEST_DIR,
4333
4330
  ingestionSource,
4334
4331
  uploadFilePath,
@@ -4360,9 +4357,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4360
4357
  let ingestRecordCount;
4361
4358
  let ingestDurationMs;
4362
4359
  let ingestOutcomeMessage;
4363
- const ingestionTools = ingestAppId ? makeToolContext(worktree, async () => ({
4364
- [APP_ID_VAR]: ingestAppId,
4365
- [API_KEY_VAR]: PLACEHOLDER_WRITE_KEY
4360
+ const ingestKeyAppId = ingestAppId;
4361
+ const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
4362
+ [APP_ID_VAR]: ingestKeyAppId,
4363
+ [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4364
+ [INDEX_NAME_VAR]: targetIndex
4366
4365
  })) : void 0;
4367
4366
  const searchTools = makeToolContext(worktree);
4368
4367
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
@@ -4482,14 +4481,14 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4482
4481
  let searchKeyError;
4483
4482
  if (appId) {
4484
4483
  try {
4485
- const resolved = await resolveSearchOnlyKey(
4484
+ const resolved2 = await resolveSearchOnlyKey(
4486
4485
  targetIndex,
4487
4486
  appId,
4488
4487
  envSearchKey
4489
4488
  );
4490
- searchKey = resolved.key;
4489
+ searchKey = resolved2.key;
4491
4490
  summaries.push(
4492
- 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}.`
4491
+ 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}.`
4493
4492
  );
4494
4493
  } catch (err) {
4495
4494
  searchKeyError = err.message;
@@ -4499,7 +4498,12 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4499
4498
  );
4500
4499
  }
4501
4500
  }
4502
- finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4501
+ finalSearchEnvVars = searchEnvVars(
4502
+ language,
4503
+ targetIndex,
4504
+ appId,
4505
+ searchKey
4506
+ );
4503
4507
  const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4504
4508
  (v) => !v.value.startsWith("<")
4505
4509
  );
@@ -4883,6 +4887,7 @@ import { join as join10 } from "node:path";
4883
4887
  var KEEP = ["wizard.log"];
4884
4888
  async function resetProjectState() {
4885
4889
  const dir = stateDir();
4890
+ forgetResolvedKeys();
4886
4891
  let entries;
4887
4892
  try {
4888
4893
  entries = await readdir4(dir);
@@ -4891,7 +4896,9 @@ async function resetProjectState() {
4891
4896
  }
4892
4897
  const targets = entries.filter((name) => !KEEP.includes(name));
4893
4898
  await Promise.all(
4894
- targets.map((name) => rm2(join10(dir, name), { recursive: true, force: true }))
4899
+ targets.map(
4900
+ (name) => rm2(join10(dir, name), { recursive: true, force: true })
4901
+ )
4895
4902
  );
4896
4903
  return { dir, removed: targets };
4897
4904
  }
@@ -4899,6 +4906,7 @@ async function resetProjectState() {
4899
4906
  // src/main.tsx
4900
4907
  import { jsx as jsx15 } from "react/jsx-runtime";
4901
4908
  async function startup() {
4909
+ setProjectRoot(process.cwd());
4902
4910
  let args;
4903
4911
  try {
4904
4912
  args = parseCliArgs(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.9.0-rc.87.83",
3
+ "version": "0.9.0-rc.87.86",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {
@@ -51,6 +51,7 @@
51
51
  "@hono/node-server": "^2.0.10",
52
52
  "@segment/analytics-node": "^3.1.0",
53
53
  "ai": "^6.0.190",
54
+ "cross-keychain": "^1.1.0",
54
55
  "dotenv": "^17.4.2",
55
56
  "hono": "^4.12.27",
56
57
  "ink": "^7.0.5",