@algolia/wizard 0.7.0 → 0.8.0-rc.49.42

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 +182 -57
  2. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -1568,7 +1568,8 @@ var configFile = () => join5(stateDir(), "config.json");
1568
1568
  var DEFAULT_CONFIG = {
1569
1569
  version: 1,
1570
1570
  aiConsent: false,
1571
- workflowsRun: []
1571
+ workflowsRun: [],
1572
+ searchApiKeys: {}
1572
1573
  };
1573
1574
  async function loadConfig() {
1574
1575
  try {
@@ -1587,6 +1588,38 @@ async function recordWorkflowRun(workflowId, completedAt) {
1587
1588
  config.workflowsRun.push({ workflowId, completedAt });
1588
1589
  await saveConfig(config);
1589
1590
  }
1591
+ function isStoredSearchKey(value) {
1592
+ if (typeof value !== "object" || value === null) return false;
1593
+ const { appId, key } = value;
1594
+ return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
1595
+ }
1596
+ function storedSearchKeys(config) {
1597
+ const stored = config.searchApiKeys;
1598
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
1599
+ return {};
1600
+ }
1601
+ return stored;
1602
+ }
1603
+ async function getStoredSearchKey(index, appId) {
1604
+ const entry = storedSearchKeys(await loadConfig())[index];
1605
+ if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
1606
+ return entry.key;
1607
+ }
1608
+ async function storeSearchKey(index, appId, key) {
1609
+ const config = await loadConfig();
1610
+ config.searchApiKeys = {
1611
+ ...storedSearchKeys(config),
1612
+ [index]: { appId, key }
1613
+ };
1614
+ await saveConfig(config);
1615
+ }
1616
+ async function forgetSearchKey(index) {
1617
+ const config = await loadConfig();
1618
+ const remaining = { ...storedSearchKeys(config) };
1619
+ delete remaining[index];
1620
+ config.searchApiKeys = remaining;
1621
+ await saveConfig(config);
1622
+ }
1590
1623
 
1591
1624
  // src/core/orchestrator.ts
1592
1625
  function defineStep(step) {
@@ -2701,7 +2734,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2701
2734
  // package.json
2702
2735
  var package_default = {
2703
2736
  name: "@algolia/wizard",
2704
- version: "0.7.0",
2737
+ version: "0.8.0-rc.49.42",
2705
2738
  description: "Magically implement Algolia functionality in your codebase",
2706
2739
  type: "module",
2707
2740
  engines: {
@@ -2749,7 +2782,7 @@ var package_default = {
2749
2782
  dependencies: {
2750
2783
  "@ai-sdk/anthropic": "^3.0.81",
2751
2784
  "@ai-sdk/openai-compatible": "^2.0.47",
2752
- "@algolia/cli": "^5.11.0",
2785
+ "@algolia/cli": "^5.15.0",
2753
2786
  "@hono/node-server": "^2.0.10",
2754
2787
  "@mishieck/ink-titled-box": "^0.4.2",
2755
2788
  "@segment/analytics-node": "^3.1.0",
@@ -3307,6 +3340,23 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3307
3340
  function hasEnvVar(content, name) {
3308
3341
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3309
3342
  }
3343
+ async function readEnvVar(worktreePath, name) {
3344
+ let content;
3345
+ try {
3346
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
3347
+ } catch (err) {
3348
+ if (err.code !== "ENOENT") throw err;
3349
+ return void 0;
3350
+ }
3351
+ const match = new RegExp(
3352
+ `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3353
+ "m"
3354
+ ).exec(content);
3355
+ if (!match) return void 0;
3356
+ const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3357
+ if (!value || value.startsWith("<")) return void 0;
3358
+ return value;
3359
+ }
3310
3360
  async function writeSearchEnvValues(worktreePath, vars) {
3311
3361
  const target = join10(worktreePath, ".env");
3312
3362
  let existing = "";
@@ -3381,50 +3431,72 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3381
3431
 
3382
3432
  // src/lib/algoliaApiKey.ts
3383
3433
  import { z as z23 } from "zod";
3384
- var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
3385
- var apiKeySchema = z23.object({
3386
- value: z23.string().min(1),
3387
- acl: z23.array(z23.string()).default([]),
3388
- indexes: z23.array(z23.string()).default([])
3389
- });
3390
- var apiKeyListSchema = z23.object({
3391
- items: z23.array(apiKeySchema).optional(),
3392
- keys: z23.array(apiKeySchema).optional()
3393
- }).transform((o) => o.items ?? o.keys ?? []);
3394
3434
  var createdKeySchema = z23.object({
3395
3435
  key: z23.string().min(1).optional(),
3396
3436
  value: z23.string().min(1).optional()
3397
- });
3398
- function canReuse(key, index) {
3399
- return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
3400
- }
3401
- async function createSearchKey(index) {
3437
+ }).transform((o) => o.key ?? o.value);
3438
+ async function createSearchOnlyKey(index) {
3439
+ logger.info({ index }, "creating a search-only API key");
3402
3440
  const stdout = await runAlgoliaCli([
3403
3441
  "apikeys",
3404
3442
  "create",
3443
+ "--acl",
3444
+ "search",
3405
3445
  "--indices",
3406
3446
  index,
3407
- "--acl",
3408
- "search,browse",
3409
3447
  "--description",
3410
- `wizard search-only key for ${index}`,
3448
+ `Algolia Wizard search-only key for ${index}`,
3411
3449
  "-o",
3412
3450
  "json"
3413
3451
  ]);
3414
- const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
3415
- const created = key ?? value;
3452
+ let payload;
3453
+ try {
3454
+ payload = JSON.parse(stdout);
3455
+ } catch {
3456
+ throw new Error("apikeys create returned output that is not valid JSON");
3457
+ }
3458
+ const created = createdKeySchema.parse(payload);
3416
3459
  if (!created) throw new Error("apikeys create returned no key value");
3417
3460
  return created;
3418
3461
  }
3419
- async function resolveSearchOnlyKey(index) {
3420
- const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
3421
- const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
3422
- if (existing) {
3423
- logger.info({ index }, "reusing existing search-only API key");
3424
- return existing;
3462
+ async function apiKeyExists(key) {
3463
+ try {
3464
+ await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
3465
+ return true;
3466
+ } catch (err) {
3467
+ return !/does not exist|not found|404/i.test(err.message);
3468
+ }
3469
+ }
3470
+ async function resolveSearchOnlyKey(index, appId, envKey) {
3471
+ if (envKey) {
3472
+ await recordSearchKey(index, appId, envKey);
3473
+ return { key: envKey, source: "env" };
3474
+ }
3475
+ const stored = await getStoredSearchKey(index, appId);
3476
+ if (stored) {
3477
+ if (await apiKeyExists(stored)) {
3478
+ logger.info({ index, appId }, "reusing the stored search-only API key");
3479
+ return { key: stored, source: "config" };
3480
+ }
3481
+ logger.warn(
3482
+ { index, appId },
3483
+ "the stored search-only API key no longer exists; creating a replacement"
3484
+ );
3485
+ await forgetSearchKey(index);
3486
+ }
3487
+ const key = await createSearchOnlyKey(index);
3488
+ await recordSearchKey(index, appId, key);
3489
+ return { key, source: "created" };
3490
+ }
3491
+ async function recordSearchKey(index, appId, key) {
3492
+ try {
3493
+ await storeSearchKey(index, appId, key);
3494
+ } catch (err) {
3495
+ logger.warn(
3496
+ { err: err.message, index },
3497
+ "could not record the search-only API key; a later run may create another"
3498
+ );
3425
3499
  }
3426
- logger.info({ index }, "no reusable search-only key found; creating one");
3427
- return createSearchKey(index);
3428
3500
  }
3429
3501
 
3430
3502
  // src/lib/algoliaDocs.ts
@@ -3579,15 +3651,22 @@ function publicEnvPrefix(language) {
3579
3651
  }
3580
3652
  return "PUBLIC_";
3581
3653
  }
3654
+ var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
3655
+ var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
3656
+ function appIdVar(language) {
3657
+ return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
3658
+ }
3659
+ function searchKeyVar(language) {
3660
+ return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
3661
+ }
3582
3662
  function searchEnvVars(language, appId, searchKey) {
3583
- const prefix = publicEnvPrefix(language);
3584
3663
  return [
3585
3664
  {
3586
- name: `${prefix}ALGOLIA_APP_ID`,
3665
+ name: appIdVar(language),
3587
3666
  value: appId ?? "<your-algolia-app-id>"
3588
3667
  },
3589
3668
  {
3590
- name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
3669
+ name: searchKeyVar(language),
3591
3670
  value: searchKey ?? "<your-algolia-search-only-api-key>"
3592
3671
  }
3593
3672
  ];
@@ -3649,15 +3728,13 @@ function searchInstructions(input) {
3649
3728
  "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3650
3729
  doc,
3651
3730
  `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.`,
3652
- "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.",
3653
- // appId always resolves (loadActiveProfile throws otherwise); only the
3654
- // search-only key is best-effort and can fall back to a placeholder.
3655
- `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
3656
- // Names are fixed, not the agent's to rename: the wizard writes the
3657
- // resolved app id / search-only key into ".env" under these exact names
3658
- // right after this step, so a renamed prefix here would leave the code
3659
- // reading a var the wizard never wrote.
3731
+ // The key is provisioned only after verification passes, so the agent never
3732
+ // sees one. It must also leave .env alone: the wizard reads that file to
3733
+ // decide whether a key already exists, and an agent-invented value there
3734
+ // would be reused as if it were real.
3735
+ `Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
3660
3736
  `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3737
+ "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.",
3661
3738
  '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.',
3662
3739
  "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."
3663
3740
  ];
@@ -3807,17 +3884,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3807
3884
  const confirmed2 = normalized.confirmedEntities;
3808
3885
  const searchLocation = normalized.searchImplementationAnalysis;
3809
3886
  let appId;
3810
- let searchKey;
3811
3887
  if (useCases.includes("search")) {
3812
3888
  appId = (await loadActiveProfile()).appId;
3813
- try {
3814
- searchKey = await resolveSearchOnlyKey(targetIndex);
3815
- } catch (err) {
3816
- logger.warn(
3817
- { err: err.message },
3818
- "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
3819
- );
3820
- }
3821
3889
  }
3822
3890
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
3823
3891
  try {
@@ -3849,8 +3917,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3849
3917
  targetIndex,
3850
3918
  language,
3851
3919
  appId,
3852
- searchKey,
3853
- searchEnvVars: searchEnvVars(language, appId, searchKey),
3920
+ // Names only: the search-only key is provisioned after verification, so
3921
+ // every value here is still a placeholder when the agent reads them.
3922
+ searchEnvVars: searchEnvVars(language, appId),
3854
3923
  ingestDir: INGEST_DIR,
3855
3924
  ingestionSource,
3856
3925
  uploadFilePath,
@@ -3860,6 +3929,24 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3860
3929
  };
3861
3930
  const summaries = [];
3862
3931
  if (uploadWarning) summaries.push(uploadWarning);
3932
+ let envSearchKey;
3933
+ let envAppIdMismatch = false;
3934
+ if (useCases.includes("search") && appId) {
3935
+ const envAppId = await readEnvVar(worktree, appIdVar(language));
3936
+ if (envAppId === appId) {
3937
+ envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
3938
+ } else if (envAppId) {
3939
+ envAppIdMismatch = true;
3940
+ summaries.push(
3941
+ `\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
3942
+ );
3943
+ logger.warn(
3944
+ { envAppId, appId },
3945
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
3946
+ );
3947
+ }
3948
+ }
3949
+ let finalSearchEnvVars = input.searchEnvVars;
3863
3950
  let agentRuns = 0;
3864
3951
  let ingestRuntime;
3865
3952
  let ingestEntrypoint;
@@ -4052,7 +4139,29 @@ ${run2.output}` : status;
4052
4139
  }
4053
4140
  extraInstructions = verificationRetryInstructions(verification);
4054
4141
  }
4055
- const resolvedSearchEnvVars = input.searchEnvVars.filter(
4142
+ let searchKey;
4143
+ let searchKeyError;
4144
+ if (appId) {
4145
+ try {
4146
+ const resolved = await resolveSearchOnlyKey(
4147
+ targetIndex,
4148
+ appId,
4149
+ envSearchKey
4150
+ );
4151
+ searchKey = resolved.key;
4152
+ summaries.push(
4153
+ 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}.`
4154
+ );
4155
+ } catch (err) {
4156
+ searchKeyError = err.message;
4157
+ logger.warn(
4158
+ { err: searchKeyError },
4159
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4160
+ );
4161
+ }
4162
+ }
4163
+ finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
4164
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4056
4165
  (v) => !v.value.startsWith("<")
4057
4166
  );
4058
4167
  if (resolvedSearchEnvVars.length > 0) {
@@ -4063,13 +4172,29 @@ ${run2.output}` : status;
4063
4172
  if (written.length > 0) {
4064
4173
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4065
4174
  }
4175
+ const stale = [];
4176
+ for (const v of resolvedSearchEnvVars) {
4177
+ if (written.includes(v.name)) continue;
4178
+ const current = await readEnvVar(worktree, v.name);
4179
+ if (current && current !== v.value) stale.push(v);
4180
+ }
4181
+ if (stale.length > 0 && !envAppIdMismatch) {
4182
+ summaries.push(
4183
+ `\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
4184
+ );
4185
+ logger.warn(
4186
+ { vars: stale.map((v) => v.name) },
4187
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
4188
+ );
4189
+ }
4066
4190
  }
4067
- const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4191
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4068
4192
  (v) => v.value.startsWith("<")
4069
4193
  );
4070
4194
  if (unresolvedSearchEnvVars.length > 0) {
4071
4195
  summaries.push(
4072
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4196
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
4197
+ (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4073
4198
  );
4074
4199
  }
4075
4200
  } else {
@@ -4101,7 +4226,7 @@ ${run2.output}` : status;
4101
4226
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4102
4227
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4103
4228
  } : {},
4104
- ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4229
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4105
4230
  };
4106
4231
  } finally {
4107
4232
  process.chdir(repoRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.7.0",
3
+ "version": "0.8.0-rc.49.42",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,7 +48,7 @@
48
48
  "dependencies": {
49
49
  "@ai-sdk/anthropic": "^3.0.81",
50
50
  "@ai-sdk/openai-compatible": "^2.0.47",
51
- "@algolia/cli": "^5.11.0",
51
+ "@algolia/cli": "^5.15.0",
52
52
  "@hono/node-server": "^2.0.10",
53
53
  "@mishieck/ink-titled-box": "^0.4.2",
54
54
  "@segment/analytics-node": "^3.1.0",