@algolia/wizard 0.8.0-rc.49.42 → 0.8.0-rc.58.44

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 +149 -233
  2. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -532,6 +532,8 @@ var CANCEL = "cancel";
532
532
  var ARROW_WIDTH = 4;
533
533
  var COLUMN_GAP = 2;
534
534
  var BAR_PADDING = 2;
535
+ var ROW_HEIGHT = 3;
536
+ var INDICATOR_ROWS = 2;
535
537
  function fittedWidth(node, columns) {
536
538
  let left = 0;
537
539
  for (let n = node; n; n = n.parentNode) {
@@ -564,13 +566,18 @@ function SelectPrompt({
564
566
  if (multi) hints.push({ key: "[space]", label: "select" });
565
567
  hints.push({ key: "[enter]", label: "confirm" });
566
568
  const containerRef = useRef2(null);
567
- const { columns } = useWindowSize3();
569
+ const viewportRef = useRef2(null);
570
+ const { columns, rows: windowRows } = useWindowSize3();
568
571
  const [width, setWidth] = useState3(columns);
572
+ const [viewportHeight, setViewportHeight] = useState3(null);
569
573
  useLayoutEffect(() => {
570
574
  if (containerRef.current) {
571
575
  setWidth(fittedWidth(containerRef.current, columns));
572
576
  }
573
- }, [columns]);
577
+ if (viewportRef.current) {
578
+ setViewportHeight(measureElement2(viewportRef.current).height);
579
+ }
580
+ }, [columns, windowRows, error, question, helpText, messages, table]);
574
581
  const inner = Math.max(width - BAR_PADDING, 0);
575
582
  const labelWidth = Math.min(
576
583
  ARROW_WIDTH + (multi ? 2 : 0) + Math.max(0, ...rows.map((opt) => opt.length)) + COLUMN_GAP,
@@ -586,6 +593,22 @@ function SelectPrompt({
586
593
  const barWidth = Math.min(labelWidth + badgeWidth + BAR_PADDING, width);
587
594
  const barLabelWidth = Math.max(barWidth - BAR_PADDING - badgeWidth, 0);
588
595
  const textWidth = inner - labelWidth;
596
+ const capacity = viewportHeight === null || rows.length * ROW_HEIGHT <= viewportHeight ? rows.length : Math.max(Math.floor((viewportHeight - INDICATOR_ROWS) / ROW_HEIGHT), 1);
597
+ const maxOffset = Math.max(rows.length - capacity, 0);
598
+ const [offset, setOffset] = useState3(0);
599
+ useLayoutEffect(() => {
600
+ setOffset((o) => {
601
+ const clamped = Math.min(o, maxOffset);
602
+ if (index < clamped) return index;
603
+ if (index >= clamped + capacity) {
604
+ return Math.min(index - capacity + 1, maxOffset);
605
+ }
606
+ return clamped;
607
+ });
608
+ }, [index, capacity, maxOffset]);
609
+ const visible = rows.slice(offset, offset + capacity);
610
+ const hiddenAbove = offset;
611
+ const hiddenBelow = rows.length - offset - visible.length;
589
612
  useInput((input, key) => {
590
613
  if (rows.length === 0) return;
591
614
  if (key.upArrow || input === "k") {
@@ -610,56 +633,71 @@ function SelectPrompt({
610
633
  }
611
634
  });
612
635
  return /* @__PURE__ */ jsx4(Box4, { ref: containerRef, flexGrow: 1, children: /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 1, width, children: [
613
- error && /* @__PURE__ */ jsx4(Text4, { color: COLORS.danger, children: error }),
614
- messages?.map((m, i) => /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: m }, `msg-${i}`)),
615
- table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
616
- /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
617
- question && /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: question }),
618
- helpText && /* @__PURE__ */ jsx4(Text4, { color: COLORS.dim, children: helpText })
636
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 1, flexShrink: 0, children: [
637
+ error && /* @__PURE__ */ jsx4(Text4, { color: COLORS.danger, children: error }),
638
+ messages?.map((m, i) => /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: m }, `msg-${i}`)),
639
+ table && /* @__PURE__ */ jsx4(Table, { columns: table.columns, rows: table.rows }),
640
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
641
+ question && /* @__PURE__ */ jsx4(Text4, { color: COLORS.muted, children: question }),
642
+ helpText && /* @__PURE__ */ jsx4(Text4, { color: COLORS.dim, children: helpText })
643
+ ] })
619
644
  ] }),
620
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: rows.map((option, i) => {
621
- const highlighted = i === index;
622
- const isCancel = i === cancelIndex;
623
- const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
624
- const sec = isCancel ? void 0 : secondary?.[i];
625
- const labelColor = highlighted ? COLORS.highlight.fg : void 0;
626
- const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, wrap: "truncate", children: [
627
- highlighted ? "\u276F " : " ",
628
- bullet,
629
- option
630
- ] });
631
- const isText = sec?.kind === "text";
632
- return /* @__PURE__ */ jsxs3(
633
- Box4,
634
- {
635
- width: isText ? "100%" : barWidth,
636
- paddingX: 1,
637
- paddingY: 1,
638
- backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
639
- children: [
640
- /* @__PURE__ */ jsx4(Box4, { width: isText ? labelWidth : barLabelWidth, children: label }),
641
- isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box4, { width: textWidth, children: /* @__PURE__ */ jsx4(
642
- Text4,
643
- {
644
- wrap: "truncate",
645
- color: highlighted ? COLORS.primary : COLORS.muted,
646
- children: sec.value
647
- }
648
- ) }),
649
- sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box4, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
650
- ]
651
- },
652
- `row-${i}`
653
- );
654
- }) }),
655
- /* @__PURE__ */ jsx4(Text4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
645
+ /* @__PURE__ */ jsxs3(Box4, { ref: viewportRef, flexDirection: "column", flexGrow: 1, children: [
646
+ hiddenAbove > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
647
+ "\u2191 ",
648
+ hiddenAbove,
649
+ " more"
650
+ ] }),
651
+ visible.map((option, visibleIndex) => {
652
+ const i = offset + visibleIndex;
653
+ const highlighted = i === index;
654
+ const isCancel = i === cancelIndex;
655
+ const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
656
+ const sec = isCancel ? void 0 : secondary?.[i];
657
+ const labelColor = highlighted ? COLORS.highlight.fg : void 0;
658
+ const label = /* @__PURE__ */ jsxs3(Text4, { color: labelColor, wrap: "truncate", children: [
659
+ highlighted ? "\u276F " : " ",
660
+ bullet,
661
+ option
662
+ ] });
663
+ const isText = sec?.kind === "text";
664
+ return /* @__PURE__ */ jsxs3(
665
+ Box4,
666
+ {
667
+ width: isText ? "100%" : barWidth,
668
+ paddingX: 1,
669
+ paddingY: 1,
670
+ backgroundColor: highlighted ? COLORS.highlight.bg : void 0,
671
+ children: [
672
+ /* @__PURE__ */ jsx4(Box4, { width: isText ? labelWidth : barLabelWidth, children: label }),
673
+ isText && textWidth > 0 && /* @__PURE__ */ jsx4(Box4, { width: textWidth, children: /* @__PURE__ */ jsx4(
674
+ Text4,
675
+ {
676
+ wrap: "truncate",
677
+ color: highlighted ? COLORS.primary : COLORS.muted,
678
+ children: sec.value
679
+ }
680
+ ) }),
681
+ sec?.kind === "badge" && /* @__PURE__ */ jsx4(Box4, { width: badgeWidth, justifyContent: "flex-end", children: /* @__PURE__ */ jsx4(Text4, { color: COLORS.badge, wrap: "truncate", children: sec.value }) })
682
+ ]
683
+ },
684
+ `row-${i}`
685
+ );
686
+ }),
687
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
688
+ "\u2193 ",
689
+ hiddenBelow,
690
+ " more"
691
+ ] })
692
+ ] }),
693
+ /* @__PURE__ */ jsx4(Box4, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
656
694
  i > 0 ? " " : "",
657
695
  /* @__PURE__ */ jsx4(Text4, { color: COLORS.primary, children: key }),
658
696
  /* @__PURE__ */ jsxs3(Text4, { color: COLORS.dim, children: [
659
697
  " ",
660
698
  label
661
699
  ] })
662
- ] }, label)) })
700
+ ] }, label)) }) })
663
701
  ] }) });
664
702
  }
665
703
 
@@ -692,7 +730,7 @@ function PromptInput() {
692
730
  }
693
731
  if (phase !== "awaitingInput" || !inputReq) return null;
694
732
  if (inputReq.promptType === "multipleChoice") {
695
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
733
+ return /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
696
734
  SelectPrompt,
697
735
  {
698
736
  question: inputReq.prompt,
@@ -709,7 +747,7 @@ function PromptInput() {
709
747
  ) });
710
748
  }
711
749
  if (inputReq.promptType === "multiSelect") {
712
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
750
+ return /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
713
751
  SelectPrompt,
714
752
  {
715
753
  multi: true,
@@ -724,7 +762,7 @@ function PromptInput() {
724
762
  ) });
725
763
  }
726
764
  if (inputReq.promptType === "notice") {
727
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
765
+ return /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
728
766
  SelectPrompt,
729
767
  {
730
768
  question: inputReq.prompt,
@@ -746,7 +784,7 @@ function PromptInput() {
746
784
  }
747
785
  if (inputReq.promptType === "acceptReject") {
748
786
  const labels = inputReq.options?.length ? inputReq.options : ACCEPT_REJECT_OPTIONS;
749
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
787
+ return /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, children: /* @__PURE__ */ jsx5(
750
788
  SelectPrompt,
751
789
  {
752
790
  question: inputReq.prompt,
@@ -1527,7 +1565,10 @@ function App() {
1527
1565
  justifyContent: "space-between",
1528
1566
  children: [
1529
1567
  showLogs ? /* @__PURE__ */ jsx13(Logs, {}) : (
1530
- /* Fill the width beside the sidebar; row layout only (would grow vertically when stacked). */
1568
+ /* Fill the space the sidebar/ribbon leaves width beside the
1569
+ sidebar, height above the ribbon. The height matters even
1570
+ stacked: it is what the prompt's scrolling list measures itself
1571
+ against (see SelectPrompt). */
1531
1572
  /* @__PURE__ */ jsxs12(
1532
1573
  Box13,
1533
1574
  {
@@ -1535,7 +1576,7 @@ function App() {
1535
1576
  paddingX: 4,
1536
1577
  paddingY: 2,
1537
1578
  width: showSidebar ? 70 : "100%",
1538
- flexGrow: showSidebar ? 1 : 0,
1579
+ flexGrow: 1,
1539
1580
  children: [
1540
1581
  /* @__PURE__ */ jsx13(Notices, {}),
1541
1582
  /* @__PURE__ */ jsx13(PromptInput, {}),
@@ -1568,8 +1609,7 @@ var configFile = () => join5(stateDir(), "config.json");
1568
1609
  var DEFAULT_CONFIG = {
1569
1610
  version: 1,
1570
1611
  aiConsent: false,
1571
- workflowsRun: [],
1572
- searchApiKeys: {}
1612
+ workflowsRun: []
1573
1613
  };
1574
1614
  async function loadConfig() {
1575
1615
  try {
@@ -1588,38 +1628,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
1588
1628
  config.workflowsRun.push({ workflowId, completedAt });
1589
1629
  await saveConfig(config);
1590
1630
  }
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
- }
1623
1631
 
1624
1632
  // src/core/orchestrator.ts
1625
1633
  function defineStep(step) {
@@ -2734,7 +2742,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2734
2742
  // package.json
2735
2743
  var package_default = {
2736
2744
  name: "@algolia/wizard",
2737
- version: "0.8.0-rc.49.42",
2745
+ version: "0.8.0-rc.58.44",
2738
2746
  description: "Magically implement Algolia functionality in your codebase",
2739
2747
  type: "module",
2740
2748
  engines: {
@@ -2782,7 +2790,7 @@ var package_default = {
2782
2790
  dependencies: {
2783
2791
  "@ai-sdk/anthropic": "^3.0.81",
2784
2792
  "@ai-sdk/openai-compatible": "^2.0.47",
2785
- "@algolia/cli": "^5.15.0",
2793
+ "@algolia/cli": "^5.11.0",
2786
2794
  "@hono/node-server": "^2.0.10",
2787
2795
  "@mishieck/ink-titled-box": "^0.4.2",
2788
2796
  "@segment/analytics-node": "^3.1.0",
@@ -3340,23 +3348,6 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3340
3348
  function hasEnvVar(content, name) {
3341
3349
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3342
3350
  }
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
- }
3360
3351
  async function writeSearchEnvValues(worktreePath, vars) {
3361
3352
  const target = join10(worktreePath, ".env");
3362
3353
  let existing = "";
@@ -3431,72 +3422,50 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
3431
3422
 
3432
3423
  // src/lib/algoliaApiKey.ts
3433
3424
  import { z as z23 } from "zod";
3425
+ var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
3426
+ var apiKeySchema = z23.object({
3427
+ value: z23.string().min(1),
3428
+ acl: z23.array(z23.string()).default([]),
3429
+ indexes: z23.array(z23.string()).default([])
3430
+ });
3431
+ var apiKeyListSchema = z23.object({
3432
+ items: z23.array(apiKeySchema).optional(),
3433
+ keys: z23.array(apiKeySchema).optional()
3434
+ }).transform((o) => o.items ?? o.keys ?? []);
3434
3435
  var createdKeySchema = z23.object({
3435
3436
  key: z23.string().min(1).optional(),
3436
3437
  value: z23.string().min(1).optional()
3437
- }).transform((o) => o.key ?? o.value);
3438
- async function createSearchOnlyKey(index) {
3439
- logger.info({ index }, "creating a search-only API key");
3438
+ });
3439
+ function canReuse(key, index) {
3440
+ return key.acl.includes("search") && key.acl.every((acl) => SAFE_ACLS.has(acl)) && (key.indexes.length === 0 || key.indexes.includes("*") || key.indexes.includes(index));
3441
+ }
3442
+ async function createSearchKey(index) {
3440
3443
  const stdout = await runAlgoliaCli([
3441
3444
  "apikeys",
3442
3445
  "create",
3443
- "--acl",
3444
- "search",
3445
3446
  "--indices",
3446
3447
  index,
3448
+ "--acl",
3449
+ "search,browse",
3447
3450
  "--description",
3448
- `Algolia Wizard search-only key for ${index}`,
3451
+ `wizard search-only key for ${index}`,
3449
3452
  "-o",
3450
3453
  "json"
3451
3454
  ]);
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);
3455
+ const { key, value } = createdKeySchema.parse(JSON.parse(stdout));
3456
+ const created = key ?? value;
3459
3457
  if (!created) throw new Error("apikeys create returned no key value");
3460
3458
  return created;
3461
3459
  }
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
- );
3460
+ async function resolveSearchOnlyKey(index) {
3461
+ const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
3462
+ const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuse(key, index))?.value;
3463
+ if (existing) {
3464
+ logger.info({ index }, "reusing existing search-only API key");
3465
+ return existing;
3499
3466
  }
3467
+ logger.info({ index }, "no reusable search-only key found; creating one");
3468
+ return createSearchKey(index);
3500
3469
  }
3501
3470
 
3502
3471
  // src/lib/algoliaDocs.ts
@@ -3651,22 +3620,15 @@ function publicEnvPrefix(language) {
3651
3620
  }
3652
3621
  return "PUBLIC_";
3653
3622
  }
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
- }
3662
3623
  function searchEnvVars(language, appId, searchKey) {
3624
+ const prefix = publicEnvPrefix(language);
3663
3625
  return [
3664
3626
  {
3665
- name: appIdVar(language),
3627
+ name: `${prefix}ALGOLIA_APP_ID`,
3666
3628
  value: appId ?? "<your-algolia-app-id>"
3667
3629
  },
3668
3630
  {
3669
- name: searchKeyVar(language),
3631
+ name: `${prefix}ALGOLIA_SEARCH_API_KEY`,
3670
3632
  value: searchKey ?? "<your-algolia-search-only-api-key>"
3671
3633
  }
3672
3634
  ];
@@ -3728,13 +3690,15 @@ function searchInstructions(input) {
3728
3690
  "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3729
3691
  doc,
3730
3692
  `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.`,
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.`,
3736
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3737
3693
  "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.",
3694
+ // appId always resolves (loadActiveProfile throws otherwise); only the
3695
+ // search-only key is best-effort and can fall back to a placeholder.
3696
+ `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
3697
+ // Names are fixed, not the agent's to rename: the wizard writes the
3698
+ // resolved app id / search-only key into ".env" under these exact names
3699
+ // right after this step, so a renamed prefix here would leave the code
3700
+ // reading a var the wizard never wrote.
3701
+ `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3738
3702
  '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.',
3739
3703
  "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."
3740
3704
  ];
@@ -3884,8 +3848,17 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3884
3848
  const confirmed2 = normalized.confirmedEntities;
3885
3849
  const searchLocation = normalized.searchImplementationAnalysis;
3886
3850
  let appId;
3851
+ let searchKey;
3887
3852
  if (useCases.includes("search")) {
3888
3853
  appId = (await loadActiveProfile()).appId;
3854
+ try {
3855
+ searchKey = await resolveSearchOnlyKey(targetIndex);
3856
+ } catch (err) {
3857
+ logger.warn(
3858
+ { err: err.message },
3859
+ "implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
3860
+ );
3861
+ }
3889
3862
  }
3890
3863
  const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
3891
3864
  try {
@@ -3917,9 +3890,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3917
3890
  targetIndex,
3918
3891
  language,
3919
3892
  appId,
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),
3893
+ searchKey,
3894
+ searchEnvVars: searchEnvVars(language, appId, searchKey),
3923
3895
  ingestDir: INGEST_DIR,
3924
3896
  ingestionSource,
3925
3897
  uploadFilePath,
@@ -3929,24 +3901,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3929
3901
  };
3930
3902
  const summaries = [];
3931
3903
  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;
3950
3904
  let agentRuns = 0;
3951
3905
  let ingestRuntime;
3952
3906
  let ingestEntrypoint;
@@ -4139,29 +4093,7 @@ ${run2.output}` : status;
4139
4093
  }
4140
4094
  extraInstructions = verificationRetryInstructions(verification);
4141
4095
  }
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(
4096
+ const resolvedSearchEnvVars = input.searchEnvVars.filter(
4165
4097
  (v) => !v.value.startsWith("<")
4166
4098
  );
4167
4099
  if (resolvedSearchEnvVars.length > 0) {
@@ -4172,29 +4104,13 @@ ${run2.output}` : status;
4172
4104
  if (written.length > 0) {
4173
4105
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4174
4106
  }
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
- }
4190
4107
  }
4191
- const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4108
+ const unresolvedSearchEnvVars = input.searchEnvVars.filter(
4192
4109
  (v) => v.value.startsWith("<")
4193
4110
  );
4194
4111
  if (unresolvedSearchEnvVars.length > 0) {
4195
4112
  summaries.push(
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}` : "")
4113
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
4198
4114
  );
4199
4115
  }
4200
4116
  } else {
@@ -4226,7 +4142,7 @@ ${run2.output}` : status;
4226
4142
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4227
4143
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4228
4144
  } : {},
4229
- ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4145
+ ...useCases.includes("search") ? { searchEnvVars: input.searchEnvVars } : {}
4230
4146
  };
4231
4147
  } finally {
4232
4148
  process.chdir(repoRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.8.0-rc.49.42",
3
+ "version": "0.8.0-rc.58.44",
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.15.0",
51
+ "@algolia/cli": "^5.11.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",