@algolia/wizard 0.36.0 → 0.38.0-rc.131.265

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.
package/dist/main.js CHANGED
@@ -258,10 +258,14 @@ var useWizard = create((set, get) => ({
258
258
  error: null,
259
259
  inputReq: null,
260
260
  _resolve: null,
261
+ settingUpAppId: null,
262
+ setSettingUpApp: (appId) => set({ settingUpAppId: appId }),
261
263
  // `endAuth` lands on 'preflight', not 'idle': sign-in happens after the
262
264
  // welcome screen, so going back would gate the run a second time.
263
265
  beginAuth: () => set({ phase: "authenticating", cliOutput: [] }),
264
- endAuth: () => set((s) => s.phase === "authenticating" ? { phase: "preflight" } : {}),
266
+ endAuth: () => set(
267
+ (s) => s.phase === "authenticating" ? { phase: "preflight", cliOutput: [] } : {}
268
+ ),
265
269
  confirmStart: () => set(
266
270
  (s) => s.phase === "idle" ? { phase: "preflight", homeScreen: "home" } : {}
267
271
  ),
@@ -405,7 +409,8 @@ var useWizard = create((set, get) => ({
405
409
  logs: [],
406
410
  error: null,
407
411
  inputReq: null,
408
- _resolve: null
412
+ _resolve: null,
413
+ settingUpAppId: null
409
414
  });
410
415
  }
411
416
  }));
@@ -653,25 +658,27 @@ function SelectRow({
653
658
  labelWidth,
654
659
  highlightBackground = true,
655
660
  usePadding = false,
661
+ disabled = false,
656
662
  children
657
663
  }) {
658
- const labelColor = highlighted ? highlightBackground ? COLORS.highlight.fg : COLORS.success : COLORS.primary;
664
+ const active = highlighted && !disabled;
665
+ const labelColor = disabled ? COLORS.dim : active ? highlightBackground ? COLORS.highlight.fg : COLORS.success : COLORS.primary;
659
666
  return /* @__PURE__ */ jsxs3(
660
667
  Box4,
661
668
  {
662
669
  width,
663
670
  paddingX: usePadding ? 1 : 0,
664
671
  paddingY: usePadding ? 1 : 0,
665
- backgroundColor: highlighted && highlightBackground ? COLORS.highlight.bg : void 0,
672
+ backgroundColor: active && highlightBackground ? COLORS.highlight.bg : void 0,
666
673
  children: [
667
674
  /* @__PURE__ */ jsx3(Box4, { width: labelWidth, children: /* @__PURE__ */ jsxs3(
668
675
  Text4,
669
676
  {
670
677
  color: labelColor,
671
- bold: highlighted && !highlightBackground,
678
+ bold: active && !highlightBackground,
672
679
  wrap: "truncate",
673
680
  children: [
674
- highlighted ? "\u276F " : " ",
681
+ active ? "\u276F " : " ",
675
682
  label
676
683
  ]
677
684
  }
@@ -870,6 +877,14 @@ var ARROW_WIDTH = 4;
870
877
  var COLUMN_GAP = 2;
871
878
  var BAR_PADDING = 2;
872
879
  var ROW_HEIGHT = 3;
880
+ function nextSelectableIndex(start, dir, rowCount, isDisabled) {
881
+ let i = start;
882
+ for (let step = 0; step < rowCount; step++) {
883
+ i = (i + dir + rowCount) % rowCount;
884
+ if (!isDisabled(i)) return i;
885
+ }
886
+ return start;
887
+ }
873
888
  function SelectPrompt({
874
889
  options,
875
890
  onSelect,
@@ -881,15 +896,18 @@ function SelectPrompt({
881
896
  multi,
882
897
  cancelable,
883
898
  secondary,
899
+ disabled,
884
900
  defaultSelectedIndex = 0
885
901
  }) {
886
- const [index, setIndex] = useState5(
887
- () => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
888
- );
889
- const [checked, setChecked] = useState5(() => /* @__PURE__ */ new Set());
890
902
  const hasCancel = Boolean(multi || cancelable);
891
903
  const rows = hasCancel ? [...options, "Cancel"] : options;
892
904
  const cancelIndex = hasCancel ? options.length : -1;
905
+ const isDisabled = (i) => i !== cancelIndex && Boolean(disabled?.[i]);
906
+ const [index, setIndex] = useState5(() => {
907
+ const start = defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0;
908
+ return isDisabled(start) ? nextSelectableIndex(start, 1, rows.length, isDisabled) : start;
909
+ });
910
+ const [checked, setChecked] = useState5(() => /* @__PURE__ */ new Set());
893
911
  const hints = [];
894
912
  if (rows.length > 1) hints.push({ key: "[\u2191] [\u2193]", label: "move" });
895
913
  if (multi) hints.push({ key: "[space]", label: "select" });
@@ -929,9 +947,15 @@ function SelectPrompt({
929
947
  useInput2((input, key) => {
930
948
  if (rows.length === 0) return;
931
949
  if (key.upArrow || input === "k") {
932
- setIndex((i) => (i - 1 + rows.length) % rows.length);
950
+ setIndex((i) => {
951
+ const next = (i - 1 + rows.length) % rows.length;
952
+ return isDisabled(next) ? i : next;
953
+ });
933
954
  } else if (key.downArrow || input === "j") {
934
- setIndex((i) => (i + 1) % rows.length);
955
+ setIndex((i) => {
956
+ const next = (i + 1) % rows.length;
957
+ return isDisabled(next) ? i : next;
958
+ });
935
959
  } else if (multi && input === " " && index !== cancelIndex) {
936
960
  setChecked((prev) => {
937
961
  const next = new Set(prev);
@@ -942,6 +966,7 @@ function SelectPrompt({
942
966
  } else if (key.return) {
943
967
  if (index === cancelIndex) {
944
968
  onSelect(CANCEL);
969
+ } else if (isDisabled(index)) {
945
970
  } else if (!multi) {
946
971
  onSelect(options[index]);
947
972
  } else if (checked.size > 0) {
@@ -966,10 +991,12 @@ function SelectPrompt({
966
991
  const bullet = multi && !isCancel ? checked.has(i) ? "\u25CF " : "\u25CB " : "";
967
992
  const sec = isCancel ? void 0 : secondary?.[i];
968
993
  const isText = sec?.kind === "text";
994
+ const rowDisabled = isDisabled(i);
969
995
  return /* @__PURE__ */ jsxs7(
970
996
  SelectRow,
971
997
  {
972
998
  highlighted,
999
+ disabled: rowDisabled,
973
1000
  width: isText ? "100%" : barWidth,
974
1001
  labelWidth: isText ? labelWidth : barLabelWidth,
975
1002
  label: `${bullet}${option}`,
@@ -979,7 +1006,7 @@ function SelectPrompt({
979
1006
  Text8,
980
1007
  {
981
1008
  wrap: "truncate",
982
- color: highlighted ? COLORS.primary : COLORS.muted,
1009
+ color: rowDisabled ? COLORS.dim : highlighted ? COLORS.primary : COLORS.muted,
983
1010
  children: sec.value
984
1011
  }
985
1012
  ) }),
@@ -1041,6 +1068,7 @@ function PromptInput() {
1041
1068
  table: inputReq.table,
1042
1069
  options: inputReq.options,
1043
1070
  secondary: inputReq.secondary,
1071
+ disabled: inputReq.disabled,
1044
1072
  defaultSelectedIndex: inputReq.defaultSelectedIndex,
1045
1073
  cancelable: inputReq.cancelable,
1046
1074
  error: inputReq.error,
@@ -1112,7 +1140,7 @@ function PromptInput() {
1112
1140
  inputReq.prompt,
1113
1141
  " "
1114
1142
  ] }),
1115
- /* @__PURE__ */ jsx7(
1143
+ /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: /* @__PURE__ */ jsx7(
1116
1144
  TextInput,
1117
1145
  {
1118
1146
  value: draft,
@@ -1122,13 +1150,13 @@ function PromptInput() {
1122
1150
  setDraft("");
1123
1151
  }
1124
1152
  }
1125
- )
1153
+ ) })
1126
1154
  ] })
1127
1155
  ] });
1128
1156
  }
1129
1157
 
1130
1158
  // src/workflows/default.ts
1131
- import { z as z30 } from "zod";
1159
+ import { z as z29 } from "zod";
1132
1160
 
1133
1161
  // src/core/orchestrator.ts
1134
1162
  import "zod";
@@ -1404,6 +1432,10 @@ function reconcileWorkflowState(state, workflow) {
1404
1432
  }
1405
1433
  return state;
1406
1434
  }
1435
+ async function isResumableWorkflow(workflow) {
1436
+ const persisted = await loadWorkflowState(workflow.id);
1437
+ return persisted != null && reconcileWorkflowState(persisted, workflow) != null;
1438
+ }
1407
1439
  function initWorkflowState(workflow, now) {
1408
1440
  return {
1409
1441
  workflowId: workflow.id,
@@ -1600,267 +1632,31 @@ async function listIndices() {
1600
1632
  return items.map((i) => ({ name: i.name, entries: i.entries })).sort((a, b) => a.name.localeCompare(b.name));
1601
1633
  }
1602
1634
 
1603
- // src/actions/selectIndex.ts
1604
- var CREATE_NEW_INDEX = "Create a new index\u2026";
1605
- var selectIndexStep = async (ctx) => {
1606
- const indices = await listIndices();
1607
- const names = indices.map((i) => i.name);
1608
- const hasIndices = names.length > 0;
1609
- let error;
1610
- for (; ; ) {
1611
- const selection = hasIndices ? await ctx.requestUserInput({
1612
- prompt: "Which index do you want to ingest into?",
1613
- promptType: "multipleChoice",
1614
- options: [...names, CREATE_NEW_INDEX],
1615
- messages: ["We found these indices:"],
1616
- error
1617
- }) : await ctx.requestUserInput({
1618
- prompt: "Name the index to create and ingest into:",
1619
- promptType: "textInput",
1620
- options: [],
1621
- messages: [
1622
- "This app has no existing indices \u2014 enter a name to create one."
1623
- ],
1624
- error
1625
- });
1626
- if (typeof selection !== "string") {
1627
- throw new Error("selectIndex received an unexpected non-text result");
1628
- }
1629
- let chosen;
1630
- if (!hasIndices) {
1631
- chosen = selection.trim();
1632
- } else if (selection === CREATE_NEW_INDEX) {
1633
- const name = await ctx.requestUserInput({
1634
- prompt: "Name the new index:",
1635
- promptType: "textInput",
1636
- options: []
1637
- });
1638
- if (typeof name !== "string") {
1639
- throw new Error("selectIndex received an unexpected non-text result");
1640
- }
1641
- chosen = name.trim();
1642
- } else {
1643
- chosen = selection;
1644
- }
1645
- if (!chosen) {
1646
- error = "Index name cannot be empty.";
1647
- continue;
1648
- }
1649
- ctx.setUserInput("index", chosen);
1650
- return { selection: chosen };
1651
- }
1652
- };
1653
-
1654
- // src/lib/agent.ts
1655
- import { ToolLoopAgent, hasToolCall, Output as Output3 } from "ai";
1656
- import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
1657
- import "zod";
1658
-
1659
- // src/lib/tools/index.ts
1660
- import "zod";
1661
-
1662
- // src/lib/tools/listFiles.ts
1663
- import { tool } from "ai";
1664
- import z5 from "zod";
1665
- import { readdir } from "node:fs/promises";
1666
-
1667
- // src/lib/tools/path.ts
1668
- import { lstat } from "node:fs/promises";
1669
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname2, join as join5, sep } from "node:path";
1670
- function resolveInRoot(ctx, path) {
1671
- const target = resolve2(ctx.cwd, path);
1672
- const rel = relative(ctx.root, target);
1673
- if (rel.startsWith("..") || isAbsolute(rel)) {
1674
- return {
1675
- ok: false,
1676
- error: `Refused: ${target} is outside the repo root (${ctx.root}).`
1677
- };
1678
- }
1679
- return { ok: true, target };
1680
- }
1681
- async function hasSymlinkParent(ctx, target) {
1682
- let current = ctx.root;
1683
- const parts = relative(ctx.root, dirname2(target)).split(sep).filter(Boolean);
1684
- for (const part of parts) {
1685
- current = join5(current, part);
1686
- try {
1687
- if ((await lstat(current)).isSymbolicLink()) return true;
1688
- } catch (err) {
1689
- if (err.code === "ENOENT") return false;
1690
- throw err;
1691
- }
1692
- }
1693
- return false;
1694
- }
1695
-
1696
- // src/lib/tools/listFiles.ts
1697
- function listFilesTool(ctx) {
1698
- return tool({
1699
- description: "List files in the current working directory",
1700
- inputSchema: z5.object(),
1701
- execute: async () => {
1702
- logger.info("called listFiles tool");
1703
- if (++ctx.counts.list > ctx.limits.list) {
1704
- return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
1705
- }
1706
- const resolved2 = resolveInRoot(ctx, ".");
1707
- if (!resolved2.ok) return resolved2.error;
1708
- const entries = await readdir(resolved2.target, { withFileTypes: true });
1709
- return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
1710
- }
1711
- });
1712
- }
1713
-
1714
- // src/lib/tools/changeDirectory.ts
1715
- import { tool as tool2 } from "ai";
1716
- import z6 from "zod";
1717
- import { stat } from "node:fs/promises";
1718
- function changeDirectoryTool(ctx) {
1719
- return tool2({
1720
- description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
1721
- inputSchema: z6.object({
1722
- path: z6.string().describe("Directory to change into")
1723
- }),
1724
- execute: async ({ path }) => {
1725
- logger.info({ path }, "called changeDirectory tool");
1726
- const resolved2 = resolveInRoot(ctx, path);
1727
- if (!resolved2.ok) return resolved2.error;
1728
- try {
1729
- const info = await stat(resolved2.target);
1730
- if (!info.isDirectory()) {
1731
- return `Error changing directory to ${path}: not a directory`;
1732
- }
1733
- ctx.cwd = resolved2.target;
1734
- return `Changed working directory to ${ctx.cwd}`;
1735
- } catch (err) {
1736
- return `Error changing directory to ${path}: ${err.message}`;
1737
- }
1738
- }
1739
- });
1740
- }
1741
-
1742
- // src/lib/tools/reportStatus.ts
1743
- import { tool as tool3 } from "ai";
1744
- import z7 from "zod";
1745
- function reportStatusTool(output) {
1746
- return tool3({
1747
- description: "Report the status of your execution. Return a reason in case of failure.",
1748
- inputSchema: z7.object({
1749
- status: z7.enum(["success", "fail"]),
1750
- reason: z7.string().optional(),
1751
- output
1752
- }),
1753
- execute: async ({ status, reason, output: output2 }) => {
1754
- logger.info({ status, reason }, "called reportStatus tool");
1755
- return { status, reason, output: output2 };
1756
- }
1757
- });
1758
- }
1759
-
1760
- // src/lib/tools/readFile.ts
1761
- import { tool as tool4 } from "ai";
1762
- import z8 from "zod";
1763
- import { readFile as readFile3 } from "node:fs/promises";
1764
-
1765
- // src/lib/tools/env.ts
1766
- import { basename } from "node:path";
1767
- function isEnvFile(filePath) {
1768
- const name = basename(filePath);
1769
- return name === ".env" || name.startsWith(".env.");
1770
- }
1771
- function isSecretEnvFile(filePath) {
1772
- if (!isEnvFile(filePath)) return false;
1773
- const name = basename(filePath);
1774
- return !/\.(example|sample|template)$/.test(name);
1775
- }
1776
-
1777
- // src/lib/tools/readFile.ts
1778
- function redactEnvValues(content) {
1779
- return content.split("\n").map((line) => {
1780
- const match = line.match(/^(\s*(?:export\s+)?[\w.-]+\s*=)(.*)$/);
1781
- if (!match) return line;
1782
- const value = match[2].trim();
1783
- if (value === "") return line;
1784
- return `${match[1]}[REDACTED]`;
1785
- }).join("\n");
1786
- }
1787
- function readFileTool(ctx) {
1788
- return tool4({
1789
- description: "Read the contents of a file at the given path",
1790
- inputSchema: z8.object({
1791
- filePath: z8.string().describe("Path to the file to read")
1792
- }),
1793
- execute: async ({ filePath }) => {
1794
- if (++ctx.counts.read > ctx.limits.read) {
1795
- return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
1796
- }
1797
- logger.info({ filePath }, "called readFile tool");
1798
- const resolved2 = resolveInRoot(ctx, filePath);
1799
- if (!resolved2.ok) return resolved2.error;
1800
- try {
1801
- const content = await readFile3(resolved2.target, "utf8");
1802
- return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
1803
- } catch (err) {
1804
- return `Error reading ${filePath}: ${err.message}`;
1805
- }
1806
- }
1807
- });
1808
- }
1809
-
1810
- // src/lib/tools/writeFile.ts
1811
- import { tool as tool5 } from "ai";
1812
- import z9 from "zod";
1813
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
1814
- import { dirname as dirname3 } from "node:path";
1815
- function writeFileTool(ctx) {
1816
- return tool5({
1817
- description: "Write content to a file at the given path, overwriting it. To set Algolia credentials in an env file, use writeCredentials instead of this tool.",
1818
- inputSchema: z9.object({
1819
- filePath: z9.string().describe("Path to the file to write"),
1820
- content: z9.string().describe("Content to write to the file")
1821
- }),
1822
- execute: async ({ filePath, content }) => {
1823
- logger.info({ filePath }, "called writeFile tool");
1824
- const resolved2 = resolveInRoot(ctx, filePath);
1825
- if (resolved2.ok === false) return resolved2.error;
1826
- if (isSecretEnvFile(resolved2.target)) {
1827
- return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
1828
- }
1829
- try {
1830
- if (await hasSymlinkParent(ctx, resolved2.target)) {
1831
- return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
1832
- }
1833
- await mkdir3(dirname3(resolved2.target), { recursive: true });
1834
- await writeFile3(resolved2.target, content, "utf8");
1835
- useWizard.getState().recordWrittenFile(resolved2.target);
1836
- return `Wrote to ${filePath}`;
1837
- } catch (err) {
1838
- return `Error writing ${filePath}: ${err.message}`;
1839
- }
1840
- }
1841
- });
1842
- }
1843
-
1844
- // src/lib/tools/writeAlgoliaCredentials.ts
1845
- import { tool as tool6 } from "ai";
1846
- import z13 from "zod";
1847
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1848
- import { dirname as dirname4, relative as relative3 } from "node:path";
1849
-
1850
1635
  // src/lib/algoliaApp.ts
1851
- import { z as z10 } from "zod";
1852
- var applicationSchema = z10.object({
1853
- id: z10.string().min(1),
1854
- name: z10.string().default(""),
1855
- plan: z10.string().optional()
1636
+ import { z as z5 } from "zod";
1637
+ var applicationSchema = z5.object({
1638
+ id: z5.string().min(1),
1639
+ name: z5.string().default(""),
1640
+ plan: z5.string().optional()
1856
1641
  });
1857
- var listSchema = z10.array(
1858
- z10.object({
1859
- id: z10.string().min(1),
1860
- name: z10.string().default(""),
1861
- plan_label: z10.string().optional()
1862
- }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
1642
+ var listSchema = z5.array(
1643
+ z5.object({
1644
+ id: z5.string().min(1),
1645
+ name: z5.string().default(""),
1646
+ plan_label: z5.string().optional(),
1647
+ status: z5.string().optional(),
1648
+ acl: z5.array(z5.string()).optional()
1649
+ }).transform(({ id, name, plan_label, status, acl }) => ({
1650
+ id,
1651
+ name,
1652
+ plan: plan_label,
1653
+ status,
1654
+ acl
1655
+ }))
1863
1656
  );
1657
+ function canSelectApplication(app) {
1658
+ return app.status === "active" && (app.acl ?? []).includes("keys");
1659
+ }
1864
1660
  async function currentApplication() {
1865
1661
  let raw;
1866
1662
  try {
@@ -1909,20 +1705,160 @@ function parseJson(text) {
1909
1705
  }
1910
1706
  }
1911
1707
 
1708
+ // src/lib/envAppId.ts
1709
+ import { readFile as readFile3 } from "node:fs/promises";
1710
+ import { join as join5 } from "node:path";
1711
+ var ENV_FILES = [".env", ".env.local"];
1712
+ var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
1713
+ async function findEnvApplicationId(root = process.cwd()) {
1714
+ for (const file of ENV_FILES) {
1715
+ let content;
1716
+ try {
1717
+ content = await readFile3(join5(root, file), "utf8");
1718
+ } catch (err) {
1719
+ if (err.code !== "ENOENT") {
1720
+ logger.warn(
1721
+ { file, err },
1722
+ "could not read env file for an application id"
1723
+ );
1724
+ }
1725
+ continue;
1726
+ }
1727
+ for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
1728
+ const id = readValue(raw);
1729
+ if (id) {
1730
+ logger.info({ file, name, app: id }, "found an application id in env");
1731
+ return { id, name, file };
1732
+ }
1733
+ }
1734
+ }
1735
+ return null;
1736
+ }
1737
+ function readValue(raw) {
1738
+ const trimmed = raw.trim();
1739
+ const quoted = trimmed.match(/^(['"])(.*)\1/);
1740
+ const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
1741
+ return value.length > 0 && !value.startsWith("<") ? value : null;
1742
+ }
1743
+
1744
+ // src/lib/algoliaAppPicker.ts
1745
+ function blockReasonFor(app) {
1746
+ return app.status !== "active" ? "Inactive" : "Missing permissions";
1747
+ }
1748
+ function secondaryFor(app) {
1749
+ if (!canSelectApplication(app)) {
1750
+ return { kind: "text", value: blockReasonFor(app) };
1751
+ }
1752
+ return app.plan ? { kind: "badge", value: app.plan } : void 0;
1753
+ }
1754
+ function labelFor(app) {
1755
+ return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
1756
+ }
1757
+ async function selectAndReport(app) {
1758
+ const store = useWizard.getState();
1759
+ store.setSettingUpApp(app.id);
1760
+ try {
1761
+ return await selectApplication(app.id);
1762
+ } finally {
1763
+ store.setSettingUpApp(null);
1764
+ }
1765
+ }
1766
+ async function promptForApplication(leadIn = []) {
1767
+ const store = useWizard.getState();
1768
+ const unordered = await listApplications();
1769
+ if (unordered.length === 0) {
1770
+ throw new Error(
1771
+ "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
1772
+ );
1773
+ }
1774
+ const apps = [
1775
+ ...unordered.filter(canSelectApplication),
1776
+ ...unordered.filter((app) => !canSelectApplication(app))
1777
+ ];
1778
+ const messages = [...leadIn];
1779
+ for (; ; ) {
1780
+ const choice = await store.requestUserInput({
1781
+ prompt: "Which Algolia application should the wizard work in?",
1782
+ promptType: "multipleChoice",
1783
+ options: apps.map(labelFor),
1784
+ secondary: apps.map(secondaryFor),
1785
+ disabled: apps.map((app) => !canSelectApplication(app)),
1786
+ messages
1787
+ });
1788
+ const chosen = apps.find((app) => labelFor(app) === choice);
1789
+ if (!chosen || !canSelectApplication(chosen)) {
1790
+ throw new Error("Application picker received an unexpected selection");
1791
+ }
1792
+ try {
1793
+ return await selectAndReport(chosen);
1794
+ } catch (err) {
1795
+ logger.warn(
1796
+ { app: chosen.id, err: err.message },
1797
+ "application select failed; re-prompting"
1798
+ );
1799
+ messages.push(
1800
+ `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
1801
+ );
1802
+ }
1803
+ }
1804
+ }
1805
+ async function confirmEnvApplication(env, current) {
1806
+ const useEnv = `Use ${env.id} (from ${env.file})`;
1807
+ const choice = await useWizard.getState().requestUserInput({
1808
+ prompt: "Select an application",
1809
+ promptType: "multipleChoice",
1810
+ options: [
1811
+ useEnv,
1812
+ current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
1813
+ ],
1814
+ messages: [
1815
+ `${env.file} already sets ${env.name}=${env.id}.`,
1816
+ "Which Algolia application would you like to use?"
1817
+ ]
1818
+ });
1819
+ if (choice === useEnv) return selectEnvApplication(env);
1820
+ return current ? selectAndReport(current) : null;
1821
+ }
1822
+ async function selectEnvApplication(env) {
1823
+ try {
1824
+ return await selectAndReport({ id: env.id, name: "" });
1825
+ } catch (err) {
1826
+ logger.warn(
1827
+ { app: env.id, err: err.message },
1828
+ "could not select the application named in env; falling back to the picker"
1829
+ );
1830
+ return promptForApplication([
1831
+ `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
1832
+ ]);
1833
+ }
1834
+ }
1835
+ async function ensureApplication(isResuming) {
1836
+ const current = await currentApplication();
1837
+ if (isResuming) {
1838
+ return current ?? await promptForApplication();
1839
+ }
1840
+ const env = await findEnvApplicationId();
1841
+ if (env && env.id !== current?.id) {
1842
+ const chosen = await confirmEnvApplication(env, current);
1843
+ if (chosen) return chosen;
1844
+ }
1845
+ return promptForApplication();
1846
+ }
1847
+
1912
1848
  // src/lib/algoliaApiKey.ts
1913
- import { z as z12 } from "zod";
1849
+ import { z as z7 } from "zod";
1914
1850
 
1915
1851
  // src/lib/keychain.ts
1916
1852
  import { deletePassword, getPassword, setPassword } from "cross-keychain";
1917
- import { z as z11 } from "zod";
1853
+ import { z as z6 } from "zod";
1918
1854
  var SERVICE = "algolia-wizard";
1919
- var ACCOUNT = "api-keys";
1920
- var storedKeysSchema = z11.record(z11.string(), z11.string());
1855
+ var account = (userId) => `api-keys:${userId}`;
1856
+ var storedKeysSchema = z6.record(z6.string(), z6.string());
1921
1857
  function entryId(kind, index, appId) {
1922
1858
  return `${kind}:${appId}:${index}`;
1923
1859
  }
1924
- async function loadKeys() {
1925
- const raw = await getPassword(SERVICE, ACCOUNT);
1860
+ async function loadKeys(userId) {
1861
+ const raw = await getPassword(SERVICE, account(userId));
1926
1862
  if (!raw) return {};
1927
1863
  let payload;
1928
1864
  try {
@@ -1944,47 +1880,47 @@ function serialized(op) {
1944
1880
  });
1945
1881
  return next;
1946
1882
  }
1947
- async function readStoredKey(kind, index, appId) {
1883
+ async function readStoredKey(kind, userId, index, appId) {
1948
1884
  try {
1949
- return (await loadKeys())[entryId(kind, index, appId)] ?? null;
1885
+ return (await loadKeys(userId))[entryId(kind, index, appId)] ?? null;
1950
1886
  } catch (err) {
1951
1887
  logger.warn(
1952
- { err: err.message, kind, index, appId },
1888
+ { err: err.message, kind, index, appId, userId },
1953
1889
  "could not read the API key from the keychain"
1954
1890
  );
1955
1891
  return null;
1956
1892
  }
1957
1893
  }
1958
- function storeKey(kind, index, appId, value) {
1894
+ function storeKey(kind, userId, index, appId, value) {
1959
1895
  return serialized(async () => {
1960
1896
  const id = entryId(kind, index, appId);
1961
1897
  try {
1962
- const keys = await loadKeys();
1898
+ const keys = await loadKeys(userId);
1963
1899
  await setPassword(
1964
1900
  SERVICE,
1965
- ACCOUNT,
1901
+ account(userId),
1966
1902
  JSON.stringify({ ...keys, [id]: value })
1967
1903
  );
1968
- if ((await loadKeys())[id] !== value) {
1904
+ if ((await loadKeys(userId))[id] !== value) {
1969
1905
  throw new Error("the keychain did not store the value");
1970
1906
  }
1971
1907
  } catch (err) {
1972
1908
  logger.warn(
1973
- { err: err.message, kind, index, appId },
1909
+ { err: err.message, kind, index, appId, userId },
1974
1910
  "could not store the API key in the keychain; the next run will create another"
1975
1911
  );
1976
1912
  }
1977
1913
  });
1978
1914
  }
1979
- function deleteStoredKeys() {
1915
+ function deleteStoredKeys(userId) {
1980
1916
  return serialized(async () => {
1981
1917
  try {
1982
- await deletePassword(SERVICE, ACCOUNT);
1918
+ await deletePassword(SERVICE, account(userId));
1983
1919
  } catch (err) {
1984
1920
  const message = err.message;
1985
1921
  if (/not found/i.test(message)) return;
1986
1922
  logger.warn(
1987
- { err: message },
1923
+ { err: message, userId },
1988
1924
  "could not delete the API keys from the keychain"
1989
1925
  );
1990
1926
  }
@@ -1992,6 +1928,16 @@ function deleteStoredKeys() {
1992
1928
  }
1993
1929
 
1994
1930
  // src/lib/algoliaApiKey.ts
1931
+ function requireUserId() {
1932
+ const userId = useWizard.getState().user?.userId;
1933
+ if (!userId) {
1934
+ throw new Error("No Algolia user is signed in; cannot scope the API key.");
1935
+ }
1936
+ return userId;
1937
+ }
1938
+ async function currentUserId() {
1939
+ return useWizard.getState().user?.userId ?? (await getUser())?.userId ?? null;
1940
+ }
1995
1941
  var WRITE_ACLS = [
1996
1942
  "addObject",
1997
1943
  "deleteObject",
@@ -1999,9 +1945,9 @@ var WRITE_ACLS = [
1999
1945
  "editSettings",
2000
1946
  "listIndexes"
2001
1947
  ];
2002
- var createdKeySchema = z12.object({
2003
- key: z12.string().min(1).optional(),
2004
- value: z12.string().min(1).optional()
1948
+ var createdKeySchema = z7.object({
1949
+ key: z7.string().min(1).optional(),
1950
+ value: z7.string().min(1).optional()
2005
1951
  }).transform((o) => o.key ?? o.value);
2006
1952
  async function createKey(index, acls, description) {
2007
1953
  logger.info({ index, acls }, "creating an API key");
@@ -2038,7 +1984,8 @@ async function keyExists(key) {
2038
1984
  var resolved = /* @__PURE__ */ new Map();
2039
1985
  async function forgetResolvedKeys() {
2040
1986
  resolved.clear();
2041
- await deleteStoredKeys();
1987
+ const userId = await currentUserId();
1988
+ if (userId) await deleteStoredKeys(userId);
2042
1989
  }
2043
1990
  function resolveKey(kind, index, appId, acls, description) {
2044
1991
  const cacheKey = `${kind}:${appId}:${index}`;
@@ -2054,7 +2001,8 @@ function resolveKey(kind, index, appId, acls, description) {
2054
2001
  return pending;
2055
2002
  }
2056
2003
  async function provisionKey(kind, index, appId, acls, description) {
2057
- const stored = await readStoredKey(kind, index, appId);
2004
+ const userId = requireUserId();
2005
+ const stored = await readStoredKey(kind, userId, index, appId);
2058
2006
  if (stored) {
2059
2007
  if (await keyExists(stored)) {
2060
2008
  logger.info({ kind, index, appId }, "reusing the stored API key");
@@ -2066,7 +2014,7 @@ async function provisionKey(kind, index, appId, acls, description) {
2066
2014
  );
2067
2015
  }
2068
2016
  const key = await createKey(index, acls, description);
2069
- await storeKey(kind, index, appId, key);
2017
+ await storeKey(kind, userId, index, appId, key);
2070
2018
  return { key, source: "created" };
2071
2019
  }
2072
2020
  function resolveWriteKey(index, appId) {
@@ -2078,21 +2026,299 @@ function resolveWriteKey(index, appId) {
2078
2026
  `Algolia Wizard write key for ${index} index`
2079
2027
  );
2080
2028
  }
2081
- async function resolveSearchOnlyKey(index, appId, envKey) {
2082
- if (envKey) return { key: envKey, source: "env" };
2083
- return resolveKey(
2084
- "search",
2085
- index,
2086
- appId,
2087
- ["search"],
2088
- `Algolia Wizard search-only key for ${index} index`
2089
- );
2029
+ async function resolveSearchOnlyKey(index, appId) {
2030
+ return resolveKey(
2031
+ "search",
2032
+ index,
2033
+ appId,
2034
+ ["search"],
2035
+ `Algolia Wizard search-only key for ${index} index`
2036
+ );
2037
+ }
2038
+
2039
+ // src/actions/selectIndex.ts
2040
+ var CREATE_NEW_INDEX = "Create a new index\u2026";
2041
+ async function canIssueKeys(index, appId) {
2042
+ try {
2043
+ await resolveWriteKey(index, appId);
2044
+ await resolveSearchOnlyKey(index, appId);
2045
+ return { ok: true };
2046
+ } catch (err) {
2047
+ return { ok: false, error: err.message };
2048
+ }
2049
+ }
2050
+ var selectIndexStep = async (ctx) => {
2051
+ let appId = (await requireApplication()).id;
2052
+ for (; ; ) {
2053
+ const indices = await listIndices();
2054
+ const names = indices.map((i) => i.name);
2055
+ const hasIndices = names.length > 0;
2056
+ let error;
2057
+ let chosen;
2058
+ while (chosen === void 0) {
2059
+ const selection = hasIndices ? await ctx.requestUserInput({
2060
+ prompt: "Which index do you want to ingest into?",
2061
+ promptType: "multipleChoice",
2062
+ options: [...names, CREATE_NEW_INDEX],
2063
+ messages: ["We found these indices:"],
2064
+ error
2065
+ }) : await ctx.requestUserInput({
2066
+ prompt: "Name the index to create and ingest into:",
2067
+ promptType: "textInput",
2068
+ options: [],
2069
+ messages: [
2070
+ "This app has no existing indices \u2014 enter a name to create one."
2071
+ ],
2072
+ error
2073
+ });
2074
+ if (typeof selection !== "string") {
2075
+ throw new Error("selectIndex received an unexpected non-text result");
2076
+ }
2077
+ let candidate;
2078
+ if (!hasIndices) {
2079
+ candidate = selection.trim();
2080
+ } else if (selection === CREATE_NEW_INDEX) {
2081
+ const name = await ctx.requestUserInput({
2082
+ prompt: "Name the new index:",
2083
+ promptType: "textInput",
2084
+ options: []
2085
+ });
2086
+ if (typeof name !== "string") {
2087
+ throw new Error("selectIndex received an unexpected non-text result");
2088
+ }
2089
+ candidate = name.trim();
2090
+ } else {
2091
+ candidate = selection;
2092
+ }
2093
+ if (!candidate) {
2094
+ error = "Index name cannot be empty.";
2095
+ continue;
2096
+ }
2097
+ chosen = candidate;
2098
+ }
2099
+ const issued = await canIssueKeys(chosen, appId);
2100
+ if (issued.ok) {
2101
+ ctx.setUserInput("index", chosen);
2102
+ return { selection: chosen };
2103
+ }
2104
+ logger.warn(
2105
+ { appId, index: chosen, err: issued.error },
2106
+ "selectIndex: could not mint API keys in the selected application; asking for another"
2107
+ );
2108
+ const reselected = await promptForApplication([
2109
+ `Could not create an Algolia API key in application ${appId} (${issued.error}).`,
2110
+ "Pick a different application to work in."
2111
+ ]);
2112
+ appId = reselected.id;
2113
+ }
2114
+ };
2115
+
2116
+ // src/lib/agent.ts
2117
+ import { ToolLoopAgent, hasToolCall, Output as Output3 } from "ai";
2118
+ import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
2119
+ import "zod";
2120
+
2121
+ // src/lib/tools/index.ts
2122
+ import "zod";
2123
+
2124
+ // src/lib/tools/listFiles.ts
2125
+ import { tool } from "ai";
2126
+ import z8 from "zod";
2127
+ import { readdir } from "node:fs/promises";
2128
+
2129
+ // src/lib/tools/path.ts
2130
+ import { lstat } from "node:fs/promises";
2131
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname2, join as join6, sep } from "node:path";
2132
+ function resolveInRoot(ctx, path) {
2133
+ const target = resolve2(ctx.cwd, path);
2134
+ const rel = relative(ctx.root, target);
2135
+ if (rel.startsWith("..") || isAbsolute(rel)) {
2136
+ return {
2137
+ ok: false,
2138
+ error: `Refused: ${target} is outside the repo root (${ctx.root}).`
2139
+ };
2140
+ }
2141
+ return { ok: true, target };
2142
+ }
2143
+ async function hasSymlinkParent(ctx, target) {
2144
+ let current = ctx.root;
2145
+ const parts = relative(ctx.root, dirname2(target)).split(sep).filter(Boolean);
2146
+ for (const part of parts) {
2147
+ current = join6(current, part);
2148
+ try {
2149
+ if ((await lstat(current)).isSymbolicLink()) return true;
2150
+ } catch (err) {
2151
+ if (err.code === "ENOENT") return false;
2152
+ throw err;
2153
+ }
2154
+ }
2155
+ return false;
2156
+ }
2157
+
2158
+ // src/lib/tools/listFiles.ts
2159
+ function listFilesTool(ctx) {
2160
+ return tool({
2161
+ description: 'List files in a directory (default: the current working directory). Pass path to list a subdirectory directly \u2014 e.g. "packages/api" \u2014 without first changeDirectory-ing into it.',
2162
+ inputSchema: z8.object({
2163
+ path: z8.string().optional().describe("Directory to list, relative to cwd (default: cwd)")
2164
+ }),
2165
+ execute: async ({ path = "." }) => {
2166
+ logger.info({ path }, "called listFiles tool");
2167
+ if (++ctx.counts.list > ctx.limits.list) {
2168
+ return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
2169
+ }
2170
+ const resolved2 = resolveInRoot(ctx, path);
2171
+ if (!resolved2.ok) return resolved2.error;
2172
+ try {
2173
+ const entries = await readdir(resolved2.target, { withFileTypes: true });
2174
+ return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2175
+ } catch (err) {
2176
+ return `Error listing ${path}: ${err.message}`;
2177
+ }
2178
+ }
2179
+ });
2180
+ }
2181
+
2182
+ // src/lib/tools/changeDirectory.ts
2183
+ import { tool as tool2 } from "ai";
2184
+ import z9 from "zod";
2185
+ import { stat } from "node:fs/promises";
2186
+ function changeDirectoryTool(ctx) {
2187
+ return tool2({
2188
+ description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
2189
+ inputSchema: z9.object({
2190
+ path: z9.string().describe("Directory to change into")
2191
+ }),
2192
+ execute: async ({ path }) => {
2193
+ logger.info({ path }, "called changeDirectory tool");
2194
+ const resolved2 = resolveInRoot(ctx, path);
2195
+ if (!resolved2.ok) return resolved2.error;
2196
+ try {
2197
+ const info = await stat(resolved2.target);
2198
+ if (!info.isDirectory()) {
2199
+ return `Error changing directory to ${path}: not a directory`;
2200
+ }
2201
+ ctx.cwd = resolved2.target;
2202
+ return `Changed working directory to ${ctx.cwd}`;
2203
+ } catch (err) {
2204
+ return `Error changing directory to ${path}: ${err.message}`;
2205
+ }
2206
+ }
2207
+ });
2208
+ }
2209
+
2210
+ // src/lib/tools/reportStatus.ts
2211
+ import { tool as tool3 } from "ai";
2212
+ import z10 from "zod";
2213
+ function reportStatusTool(output) {
2214
+ return tool3({
2215
+ description: "Report the status of your execution. Return a reason in case of failure.",
2216
+ inputSchema: z10.object({
2217
+ status: z10.enum(["success", "fail"]),
2218
+ reason: z10.string().optional(),
2219
+ output
2220
+ }),
2221
+ execute: async ({ status, reason, output: output2 }) => {
2222
+ logger.info({ status, reason }, "called reportStatus tool");
2223
+ return { status, reason, output: output2 };
2224
+ }
2225
+ });
2226
+ }
2227
+
2228
+ // src/lib/tools/readFile.ts
2229
+ import { tool as tool4 } from "ai";
2230
+ import z11 from "zod";
2231
+ import { readFile as readFile4 } from "node:fs/promises";
2232
+
2233
+ // src/lib/tools/env.ts
2234
+ import { basename } from "node:path";
2235
+ function isEnvFile(filePath) {
2236
+ const name = basename(filePath);
2237
+ return name === ".env" || name.startsWith(".env.");
2238
+ }
2239
+ function isSecretEnvFile(filePath) {
2240
+ if (!isEnvFile(filePath)) return false;
2241
+ const name = basename(filePath);
2242
+ return !/\.(example|sample|template)$/.test(name);
2243
+ }
2244
+
2245
+ // src/lib/tools/readFile.ts
2246
+ function redactEnvValues(content) {
2247
+ return content.split("\n").map((line) => {
2248
+ const match = line.match(/^(\s*(?:export\s+)?[\w.-]+\s*=)(.*)$/);
2249
+ if (!match) return line;
2250
+ const value = match[2].trim();
2251
+ if (value === "") return line;
2252
+ return `${match[1]}[REDACTED]`;
2253
+ }).join("\n");
2254
+ }
2255
+ function readFileTool(ctx) {
2256
+ return tool4({
2257
+ description: "Read the contents of a file at the given path",
2258
+ inputSchema: z11.object({
2259
+ filePath: z11.string().describe("Path to the file to read")
2260
+ }),
2261
+ execute: async ({ filePath }) => {
2262
+ if (++ctx.counts.read > ctx.limits.read) {
2263
+ return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
2264
+ }
2265
+ logger.info({ filePath }, "called readFile tool");
2266
+ const resolved2 = resolveInRoot(ctx, filePath);
2267
+ if (!resolved2.ok) return resolved2.error;
2268
+ try {
2269
+ const content = await readFile4(resolved2.target, "utf8");
2270
+ return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2271
+ } catch (err) {
2272
+ return `Error reading ${filePath}: ${err.message}`;
2273
+ }
2274
+ }
2275
+ });
2276
+ }
2277
+
2278
+ // src/lib/tools/writeFile.ts
2279
+ import { tool as tool5 } from "ai";
2280
+ import z12 from "zod";
2281
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2282
+ import { dirname as dirname3 } from "node:path";
2283
+ function writeFileTool(ctx) {
2284
+ return tool5({
2285
+ description: "Write content to a file at the given path, overwriting it. Writes to secret env files (.env, .env.local, etc.) are refused by this tool \u2014 call it anyway and the tool will tell you how to proceed.",
2286
+ inputSchema: z12.object({
2287
+ filePath: z12.string().describe("Path to the file to write"),
2288
+ content: z12.string().describe("Content to write to the file")
2289
+ }),
2290
+ execute: async ({ filePath, content }) => {
2291
+ logger.info({ filePath }, "called writeFile tool");
2292
+ const resolved2 = resolveInRoot(ctx, filePath);
2293
+ if (resolved2.ok === false) return resolved2.error;
2294
+ if (isSecretEnvFile(resolved2.target)) {
2295
+ return `Refused: ${filePath} holds secrets and cannot be written directly. If a writeCredentials tool is available, use it to set Algolia credentials. If not, tell the user to set the value manually \u2014 do not reproduce the secret value in your response.`;
2296
+ }
2297
+ try {
2298
+ if (await hasSymlinkParent(ctx, resolved2.target)) {
2299
+ return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2300
+ }
2301
+ await mkdir3(dirname3(resolved2.target), { recursive: true });
2302
+ await writeFile3(resolved2.target, content, "utf8");
2303
+ useWizard.getState().recordWrittenFile(resolved2.target);
2304
+ return `Wrote to ${filePath}`;
2305
+ } catch (err) {
2306
+ return `Error writing ${filePath}: ${err.message}`;
2307
+ }
2308
+ }
2309
+ });
2090
2310
  }
2091
2311
 
2312
+ // src/lib/tools/writeAlgoliaCredentials.ts
2313
+ import { tool as tool6 } from "ai";
2314
+ import z13 from "zod";
2315
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
2316
+ import { dirname as dirname4, relative as relative3 } from "node:path";
2317
+
2092
2318
  // src/lib/gitignore.ts
2093
2319
  import { execFile } from "node:child_process";
2094
- import { lstat as lstat2, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
2095
- import { join as join6, relative as relative2 } from "node:path";
2320
+ import { lstat as lstat2, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2321
+ import { join as join7, relative as relative2 } from "node:path";
2096
2322
  var GIT_ENV_OVERRIDES = [
2097
2323
  "GIT_DIR",
2098
2324
  "GIT_WORK_TREE",
@@ -2118,6 +2344,12 @@ function isIgnoredByRule(root, relPath) {
2118
2344
  function isTracked(root, relPath) {
2119
2345
  return gitSucceeds(root, ["ls-files", "--error-unmatch", "--", relPath]);
2120
2346
  }
2347
+ async function gitIgnoreStatus(root, target) {
2348
+ const { ignoredByRule, tracked } = await inspect(root, target);
2349
+ if (ignoredByRule === void 0) return "unknown";
2350
+ if (tracked) return "tracked";
2351
+ return ignoredByRule ? "covered" : "needsRule";
2352
+ }
2121
2353
  async function inspect(root, target) {
2122
2354
  const relPath = relative2(root, target);
2123
2355
  if (!relPath || relPath.startsWith("..")) {
@@ -2138,7 +2370,7 @@ async function ensureGitIgnored(root, target) {
2138
2370
  if (ignoredByRule === void 0) return "unknown";
2139
2371
  if (ignoredByRule) return tracked ? "tracked" : "covered";
2140
2372
  const pattern = relative2(root, target);
2141
- const gitIgnore = join6(root, ".gitignore");
2373
+ const gitIgnore = join7(root, ".gitignore");
2142
2374
  try {
2143
2375
  const link = await lstat2(gitIgnore).catch(() => null);
2144
2376
  if (link?.isSymbolicLink()) {
@@ -2148,7 +2380,7 @@ async function ensureGitIgnored(root, target) {
2148
2380
  );
2149
2381
  return "unknown";
2150
2382
  }
2151
- const existing = link ? await readFile4(gitIgnore, "utf8") : "";
2383
+ const existing = link ? await readFile5(gitIgnore, "utf8") : "";
2152
2384
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2153
2385
  await writeFile4(gitIgnore, `${existing}${prefix}${pattern}
2154
2386
  `, "utf8");
@@ -2163,31 +2395,9 @@ async function ensureGitIgnored(root, target) {
2163
2395
  }
2164
2396
 
2165
2397
  // src/lib/tools/writeAlgoliaCredentials.ts
2166
- var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2167
- var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
2398
+ var APP_ID_VAR = "ALGOLIA_APP_ID";
2399
+ var API_KEY_VAR = "ALGOLIA_WRITE_KEY";
2168
2400
  var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
2169
- var PUBLIC_APP_ID_SUFFIX = "ALGOLIA_APP_ID";
2170
- var PUBLIC_SEARCH_KEY_SUFFIX = "ALGOLIA_SEARCH_KEY";
2171
- var PUBLIC_INDEX_NAME_SUFFIX = "ALGOLIA_INDEX_NAME";
2172
- function publicAppIdVar(prefix) {
2173
- return `${prefix}${PUBLIC_APP_ID_SUFFIX}`;
2174
- }
2175
- function publicSearchKeyVar(prefix) {
2176
- return `${prefix}${PUBLIC_SEARCH_KEY_SUFFIX}`;
2177
- }
2178
- function publicIndexNameVar(prefix) {
2179
- return `${prefix}${PUBLIC_INDEX_NAME_SUFFIX}`;
2180
- }
2181
- function publicSearchEnvVars(prefix, index, appId, searchKey) {
2182
- return [
2183
- { name: publicAppIdVar(prefix), value: appId ?? "<your-algolia-app-id>" },
2184
- {
2185
- name: publicSearchKeyVar(prefix),
2186
- value: searchKey ?? "<your-algolia-search-only-api-key>"
2187
- },
2188
- { name: publicIndexNameVar(prefix), value: index }
2189
- ];
2190
- }
2191
2401
  function appendEnv(content, entries) {
2192
2402
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2193
2403
  const lines = entries.map(([name, value]) => `${name}=${value}
@@ -2195,11 +2405,13 @@ function appendEnv(content, entries) {
2195
2405
  return content + prefix + lines;
2196
2406
  }
2197
2407
  function hasEnv(content, name) {
2198
- return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
2408
+ return new RegExp(`^([ \\t]*(?:export[ \\t]+)?${name})[ \\t]*=`, "m").test(
2409
+ content
2410
+ );
2199
2411
  }
2200
2412
  function readEnv(content, name) {
2201
2413
  const found = content.match(
2202
- new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*(.*)$`, "m")
2414
+ new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`, "m")
2203
2415
  );
2204
2416
  if (!found) return null;
2205
2417
  const raw = found[1].trim();
@@ -2210,16 +2422,16 @@ function readEnv(content, name) {
2210
2422
  function upsertEnv(content, name, value) {
2211
2423
  if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
2212
2424
  return content.replace(
2213
- new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "gm"),
2425
+ new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=.*$`, "gm"),
2214
2426
  () => `${name}=${value}`
2215
2427
  );
2216
2428
  }
2217
2429
  function writeCredentialsTool(ctx) {
2218
2430
  return tool6({
2219
- 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. The env file is added to .gitignore automatically; do not edit .gitignore yourself.`,
2431
+ 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. The env file is added to .gitignore automatically; do not edit .gitignore yourself. If the script or app that reads these credentials lives in a subdirectory (e.g. a package in a monorepo), an env file at the repo root is the wrong default \u2014 a script only loads env vars from its own directory (or one it's explicitly configured to read), so check every directory from the script's own up to the repo root, not just those two: its own directory, each ancestor in between (a shared workspace-level directory above the immediate package is common), and the root. Use whichever of those already holds real credentials; only fall back to the repo root when none of them do. Never invent a brand-new file in one of those directories when a real one already exists in another \u2014 that leaves the real one stale and the new one wrong. Listing just the script's own directory and the very top-level root is not enough to find a workspace-level file in between; check the intermediate ones too. If instructions describe a location that doesn't match the project you actually find (e.g. a path outside the repo, or a convention the project doesn't follow), don't stop and ask before doing anything \u2014 call this tool on the real, in-repo file the script actually reads (that's always the safe default), then note the mismatch afterward. Ending your turn with only a question and no call to this tool leaves the project unconfigured.`,
2220
2432
  inputSchema: z13.object({
2221
2433
  filePath: z13.string().describe(
2222
- 'Path to the env file to write credentials into (e.g. ".env")'
2434
+ 'Path to the env file to write credentials into, relative to the repo root (e.g. ".env", or "packages/api/.env" when the consuming script lives in that package)'
2223
2435
  )
2224
2436
  }),
2225
2437
  execute: async ({ filePath }) => {
@@ -2237,7 +2449,7 @@ function writeCredentialsTool(ctx) {
2237
2449
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2238
2450
  }
2239
2451
  try {
2240
- existing = await readFile5(resolved2.target, "utf8");
2452
+ existing = await readFile6(resolved2.target, "utf8");
2241
2453
  } catch (err) {
2242
2454
  if (err.code !== "ENOENT") throw err;
2243
2455
  }
@@ -2327,8 +2539,8 @@ async function gitIgnoreOutcome(ctx, target) {
2327
2539
  // src/lib/tools/searchFiles.ts
2328
2540
  import { tool as tool7 } from "ai";
2329
2541
  import z14 from "zod";
2330
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2331
- import { join as join7 } from "node:path";
2542
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
2543
+ import { join as join8 } from "node:path";
2332
2544
  var MAX_QUERY_LENGTH = 1e3;
2333
2545
  var SKIP_DIRS = /* @__PURE__ */ new Set([
2334
2546
  "node_modules",
@@ -2343,7 +2555,7 @@ async function walkFiles(dir) {
2343
2555
  const out = [];
2344
2556
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2345
2557
  if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2346
- const full = join7(dir, e.name);
2558
+ const full = join8(dir, e.name);
2347
2559
  if (e.isDirectory()) out.push(...await walkFiles(full));
2348
2560
  else if (e.isFile()) out.push(full);
2349
2561
  }
@@ -2376,7 +2588,7 @@ function searchFilesTool(ctx) {
2376
2588
  for (const file of await walkFiles(resolved2.target)) {
2377
2589
  let content;
2378
2590
  try {
2379
- content = await readFile6(file, "utf8");
2591
+ content = await readFile7(file, "utf8");
2380
2592
  } catch {
2381
2593
  continue;
2382
2594
  }
@@ -3180,6 +3392,7 @@ var detectLanguage = () => runAgent({
3180
3392
  "Return the exact version",
3181
3393
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
3182
3394
  `Determine publicEnvVarPrefix: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to the detected framework's known convention for exposing env vars to client-side code; use "" when the project has no such convention (e.g. a backend-only project).`,
3395
+ "A brand-new project has no existing env var usage to find \u2014 one or two targeted checks (e.g. .env/.env.example, or a grep for the bundler's public-prefix convention) are enough to confirm that. Do not keep searching once those turn up nothing; fall back to the framework convention immediately.",
3183
3396
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
3184
3397
  "When done, call reportStatus"
3185
3398
  ],
@@ -3275,7 +3488,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3275
3488
  // package.json
3276
3489
  var package_default = {
3277
3490
  name: "@algolia/wizard",
3278
- version: "0.36.0",
3491
+ version: "0.38.0-rc.131.265",
3279
3492
  description: "Magically implement Algolia functionality in your codebase",
3280
3493
  type: "module",
3281
3494
  engines: {
@@ -3299,6 +3512,7 @@ var package_default = {
3299
3512
  reset: "tsx ./scripts/reset-state.ts",
3300
3513
  "test:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
3301
3514
  "test:tools": "tsx ./tool-evals/toolEval.ts",
3515
+ "test:tools:improve": "tsx ./tool-evals/improveFromPlan.ts",
3302
3516
  test: "vitest",
3303
3517
  typecheck: "tsc --noEmit -p tsconfig.json"
3304
3518
  },
@@ -3680,14 +3894,14 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3680
3894
  };
3681
3895
 
3682
3896
  // src/actions/implement.ts
3683
- import z29 from "zod";
3897
+ import z28 from "zod";
3684
3898
  import { mkdir as mkdir7 } from "node:fs/promises";
3685
- import { join as join10, relative as relative6 } from "node:path";
3899
+ import { join as join11, relative as relative6 } from "node:path";
3686
3900
 
3687
3901
  // src/lib/git.ts
3688
3902
  import { execFile as execFile2 } from "node:child_process";
3689
- import { copyFile, mkdir as mkdir6, readFile as readFile7, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
3690
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
3903
+ import { copyFile, mkdir as mkdir6, stat as stat3 } from "node:fs/promises";
3904
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
3691
3905
  var MAX_BUFFER = 32 * 1024 * 1024;
3692
3906
  function git(args) {
3693
3907
  return new Promise((resolve4, reject) => {
@@ -3724,8 +3938,8 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
3724
3938
  } catch {
3725
3939
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3726
3940
  }
3727
- const relPath = join8(ingestDir, basename2(source));
3728
- const dest = join8(repoRoot, relPath);
3941
+ const relPath = join9(ingestDir, basename2(source));
3942
+ const dest = join9(repoRoot, relPath);
3729
3943
  if (resolve3(source) === resolve3(dest)) {
3730
3944
  return { ok: true, relPath };
3731
3945
  }
@@ -3740,42 +3954,6 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
3740
3954
  }
3741
3955
  return { ok: true, relPath };
3742
3956
  }
3743
- function hasEnvVar(content, name) {
3744
- return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3745
- }
3746
- async function readEnvVar(repoRoot, name) {
3747
- let content;
3748
- try {
3749
- content = await readFile7(join8(repoRoot, ".env"), "utf8");
3750
- } catch (err) {
3751
- if (err.code !== "ENOENT") throw err;
3752
- return void 0;
3753
- }
3754
- const match = new RegExp(
3755
- `^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
3756
- "m"
3757
- ).exec(content);
3758
- if (!match) return void 0;
3759
- const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
3760
- if (!value || value.startsWith("<")) return void 0;
3761
- return value;
3762
- }
3763
- async function writeSearchEnvValues(repoRoot, vars) {
3764
- const target = join8(repoRoot, ".env");
3765
- let existing = "";
3766
- try {
3767
- existing = await readFile7(target, "utf8");
3768
- } catch (err) {
3769
- if (err.code !== "ENOENT") throw err;
3770
- }
3771
- const missing = vars.filter((v) => !hasEnvVar(existing, v.name));
3772
- if (missing.length === 0) return [];
3773
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3774
- const lines = missing.map(({ name, value }) => `${name}=${value}
3775
- `).join("");
3776
- await writeFile7(target, existing + prefix + lines, "utf8");
3777
- return missing.map((v) => v.name);
3778
- }
3779
3957
  function normalizeFindingPaths(findings) {
3780
3958
  return {
3781
3959
  ...findings,
@@ -3800,13 +3978,13 @@ function toRootRelative(p) {
3800
3978
 
3801
3979
  // src/lib/algoliaDocs.ts
3802
3980
  import { readFileSync, readdirSync, existsSync } from "node:fs";
3803
- import { dirname as dirname7, join as join9 } from "node:path";
3981
+ import { dirname as dirname7, join as join10 } from "node:path";
3804
3982
  import { fileURLToPath } from "node:url";
3805
- var DOCS_SUBPATH = join9("docs", "algolia-sdk");
3983
+ var DOCS_SUBPATH = join10("docs", "algolia-sdk");
3806
3984
  function findDocsDir() {
3807
3985
  let dir = dirname7(fileURLToPath(import.meta.url));
3808
3986
  for (; ; ) {
3809
- const candidate = join9(dir, DOCS_SUBPATH);
3987
+ const candidate = join10(dir, DOCS_SUBPATH);
3810
3988
  if (existsSync(candidate)) return candidate;
3811
3989
  const parent = dirname7(dir);
3812
3990
  if (parent === dir) return void 0;
@@ -3829,7 +4007,7 @@ function loadAlgoliaDoc(language) {
3829
4007
  );
3830
4008
  return "";
3831
4009
  }
3832
- return readFileSync(join9(docsDir, files[0]), "utf8").trim();
4010
+ return readFileSync(join10(docsDir, files[0]), "utf8").trim();
3833
4011
  }
3834
4012
  function getNamedDoc(name, language) {
3835
4013
  const docsDir = findDocsDir();
@@ -3837,7 +4015,7 @@ function getNamedDoc(name, language) {
3837
4015
  logger.warn("docs/algolia-sdk not found");
3838
4016
  return "";
3839
4017
  }
3840
- const file = join9(docsDir, `${name}-${language}.md`);
4018
+ const file = join10(docsDir, `${name}-${language}.md`);
3841
4019
  if (!existsSync(file)) {
3842
4020
  logger.warn({ name, language }, "named SDK reference not found");
3843
4021
  return "";
@@ -3856,46 +4034,36 @@ function getFrameworkSpecificDoc(frameworks) {
3856
4034
  return loadAlgoliaDoc("js");
3857
4035
  }
3858
4036
 
3859
- // src/actions/resolveEnvVarPrefix.ts
3860
- import z28 from "zod";
3861
- var resolveEnvVarPrefixSchema = z28.object({
3862
- publicEnvVarPrefix: detectLanguageSchema.shape.publicEnvVarPrefix
3863
- });
3864
- var resolveEnvVarPrefix = (frameworkName) => runAgent({
3865
- instructions: [
3866
- `The developer corrected the project's framework to "${frameworkName}".`,
3867
- `Determine publicEnvVarPrefix for this framework: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to this framework's known convention for exposing env vars to client-side code; use "" when the framework has no such convention (e.g. a backend-only framework).`,
3868
- 'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
3869
- "When done, call reportStatus"
3870
- ],
3871
- tools: ["listFiles", "changeDirectory", "readFile", "searchFiles"],
3872
- outputSchema: resolveEnvVarPrefixSchema,
3873
- modelSize: "small"
3874
- });
3875
-
3876
4037
  // src/actions/implement.ts
3877
- var implementSchema = z29.object({
3878
- summary: z29.string(),
3879
- ingestCommand: z29.string().optional(),
3880
- ingestScriptRan: z29.boolean().optional(),
3881
- ingestRecordCount: z29.number().optional(),
3882
- ingestDurationMs: z29.number().optional(),
3883
- ingestionSource: z29.enum(["local", "fileUpload", "generated"]),
3884
- searchEnvVars: z29.array(
3885
- z29.object({
3886
- name: z29.string(),
3887
- value: z29.string()
3888
- })
3889
- ).optional()
4038
+ var implementSchema = z28.object({
4039
+ summary: z28.string(),
4040
+ ingestCommand: z28.string().optional(),
4041
+ ingestScriptRan: z28.boolean().optional(),
4042
+ ingestRecordCount: z28.number().optional(),
4043
+ ingestDurationMs: z28.number().optional(),
4044
+ ingestionSource: z28.enum(["local", "fileUpload", "generated"]),
4045
+ searchConfig: z28.object({
4046
+ filePath: z28.string().optional(),
4047
+ vars: z28.array(
4048
+ z28.object({
4049
+ name: z28.string(),
4050
+ value: z28.string()
4051
+ })
4052
+ )
4053
+ }).optional()
3890
4054
  });
3891
- var implementationOutputSchema = z29.object({
3892
- summary: z29.string(),
3893
- ingestCommand: z29.string().optional()
4055
+ var implementationOutputSchema = z28.object({
4056
+ summary: z28.string(),
4057
+ ingestCommand: z28.string().optional(),
4058
+ // Only for the search use case: the path of whatever module the agent
4059
+ // defined the Algolia config constants in, so the wizard can check it
4060
+ // won't end up gitignored (it's public, meant to be committed).
4061
+ searchConfigFile: z28.string().optional()
3894
4062
  });
3895
- var verificationOutputSchema = z29.object({
3896
- summary: z29.string(),
3897
- sufficient: z29.boolean(),
3898
- additionalInstructions: z29.string().optional()
4063
+ var verificationOutputSchema = z28.object({
4064
+ summary: z28.string(),
4065
+ sufficient: z28.boolean(),
4066
+ additionalInstructions: z28.string().optional()
3899
4067
  });
3900
4068
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3901
4069
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
@@ -3909,6 +4077,10 @@ function isJsProject(language) {
3909
4077
  (name) => JS_LANGUAGES.some((js) => name.includes(js))
3910
4078
  );
3911
4079
  }
4080
+ var SEARCH_CONFIG_APP_ID = "ALGOLIA_APP_ID";
4081
+ var SEARCH_CONFIG_SEARCH_KEY = "ALGOLIA_SEARCH_API_KEY";
4082
+ var SEARCH_CONFIG_INDEX_NAME = "ALGOLIA_INDEX_NAME";
4083
+ var SEARCH_KEY_PLACEHOLDER = "<your-algolia-search-only-api-key>";
3912
4084
  var UI_FRAMEWORKS = [
3913
4085
  { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
3914
4086
  { match: ["react", "next"], target: "React", doc: "react" },
@@ -3979,6 +4151,7 @@ function ingestionInstructions(input) {
3979
4151
  "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.",
3980
4152
  ...algoliaClientDoc(input),
3981
4153
  "Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
4154
+ `If the script loads its env vars from a file (e.g. via dotenv or an equivalent for its language) rather than the process environment directly, decide that up front and call writeCredentials on that file before you finish the script \u2014 do not wait to discover the need for it by having a writeFile call refused.`,
3982
4155
  "When the script is finished, call reviewScript with its path and wait: running it writes records to a live index, so the developer reads it first. Do not run it before that call returns.",
3983
4156
  'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
3984
4157
  "The summary should be extremely concise.",
@@ -4000,17 +4173,14 @@ function searchInstructions(input) {
4000
4173
  `Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
4001
4174
  `Import and render that new component from ${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.`,
4002
4175
  "If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
4003
- `Read the index name from the ${publicIndexNameVar(input.publicEnvVarPrefix)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4004
- "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.",
4005
- // The key is provisioned only after verification passes, and the wizard
4006
- // reads .env to decide whether a key already exists an agent-invented
4007
- // value there would be reused as if it were real.
4008
- `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.`,
4009
- // The wizard writes these exact names into .env right after this step.
4010
- `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4176
+ "When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
4177
+ `Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
4178
+ `Set ${SEARCH_CONFIG_APP_ID} to "${input.appId}" and ${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
4179
+ input.searchKey ? `Set ${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
4180
+ 'Report the repo-relative path of that module as "searchConfigFile" in your final status.',
4011
4181
  "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4012
4182
  "Match the styles of the application as closely as possible.",
4013
- "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."
4183
+ "The summary should be extremely concise; do not mention manual testing steps."
4014
4184
  ];
4015
4185
  }
4016
4186
  function verificationInstructions(input) {
@@ -4141,21 +4311,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4141
4311
  languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
4142
4312
  frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
4143
4313
  };
4144
- const normalizeFrameworkName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
4145
- const confirmedPrimaryFramework = language.frameworks[0]?.name;
4146
- const frameworkWasCorrected = confirmedPrimaryFramework !== void 0 && !scan.frameworks.some(
4147
- (fw) => normalizeFrameworkName(fw.name) === normalizeFrameworkName(confirmedPrimaryFramework)
4148
- );
4149
- const publicEnvVarPrefixPromise = frameworkWasCorrected ? resolveEnvVarPrefix(confirmedPrimaryFramework).then(
4150
- (r) => r.publicEnvVarPrefix,
4151
- (err) => {
4152
- logger.warn(
4153
- { err, framework: confirmedPrimaryFramework },
4154
- "implement: could not re-resolve publicEnvVarPrefix after a framework correction; using the stale scan value"
4155
- );
4156
- return scan.publicEnvVarPrefix;
4157
- }
4158
- ) : Promise.resolve(scan.publicEnvVarPrefix);
4159
4314
  const selected = ctx.getStepOutput(
4160
4315
  "select-index"
4161
4316
  );
@@ -4207,7 +4362,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4207
4362
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4208
4363
  await assertGitRepoWithHead(repoRoot);
4209
4364
  if (useCases.includes("ingestion")) {
4210
- await mkdir7(join10(repoRoot, INGEST_DIR), { recursive: true });
4365
+ await mkdir7(join11(repoRoot, INGEST_DIR), { recursive: true });
4211
4366
  }
4212
4367
  const normalized = normalizeFindingPaths(findings);
4213
4368
  const confirmed2 = normalized.confirmedEntities;
@@ -4220,6 +4375,10 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4220
4375
  if (useCases.includes("ingestion")) {
4221
4376
  ingestAppId = appId ?? (await requireApplication()).id;
4222
4377
  }
4378
+ let ingestWriteKey;
4379
+ if (useCases.includes("ingestion") && ingestAppId) {
4380
+ ingestWriteKey = (await resolveWriteKey(targetIndex, ingestAppId)).key;
4381
+ }
4223
4382
  let uploadFilePath;
4224
4383
  let uploadWarning;
4225
4384
  if (ingestionSource === "fileUpload") {
@@ -4239,49 +4398,42 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4239
4398
  );
4240
4399
  }
4241
4400
  }
4242
- const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
4401
+ const summaries = [];
4402
+ if (uploadWarning) summaries.push(uploadWarning);
4403
+ let searchKey;
4404
+ let searchKeyError;
4405
+ if (useCases.includes("search") && appId) {
4406
+ try {
4407
+ const resolved2 = await resolveSearchOnlyKey(targetIndex, appId);
4408
+ searchKey = resolved2.key;
4409
+ summaries.push(
4410
+ 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}.`
4411
+ );
4412
+ } catch (err) {
4413
+ searchKeyError = err.message;
4414
+ summaries.push(
4415
+ `Could not provision a search-only Algolia API key (${searchKeyError}) \u2014 the search agent will scaffold a placeholder with a TODO for you to fill in.`
4416
+ );
4417
+ logger.warn(
4418
+ { err: searchKeyError },
4419
+ "implement: could not provision a search-only API key; the agent will scaffold a placeholder"
4420
+ );
4421
+ }
4422
+ }
4243
4423
  const input = {
4244
4424
  findings: normalized,
4245
4425
  confirmed: confirmed2,
4246
4426
  searchLocation,
4247
4427
  targetIndex,
4248
4428
  language,
4249
- publicEnvVarPrefix,
4250
4429
  appId,
4251
- searchEnvVars: publicSearchEnvVars(publicEnvVarPrefix, targetIndex, appId),
4430
+ searchKey,
4431
+ searchKeyError,
4252
4432
  ingestDir: INGEST_DIR,
4253
4433
  ingestionSource,
4254
4434
  uploadFilePath,
4255
4435
  searchUiTarget: searchUiTarget(language)
4256
4436
  };
4257
- const summaries = [];
4258
- if (uploadWarning) summaries.push(uploadWarning);
4259
- let envSearchKey;
4260
- let envAppIdMismatch = false;
4261
- if (useCases.includes("search") && appId) {
4262
- const envAppId = await readEnvVar(
4263
- repoRoot,
4264
- publicAppIdVar(publicEnvVarPrefix)
4265
- );
4266
- if (envAppId === appId) {
4267
- envSearchKey = await readEnvVar(
4268
- repoRoot,
4269
- publicSearchKeyVar(publicEnvVarPrefix)
4270
- );
4271
- } else if (envAppId) {
4272
- envAppIdMismatch = true;
4273
- const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
4274
- const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
4275
- summaries.push(
4276
- `\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
4277
- );
4278
- logger.warn(
4279
- { envAppId, appId },
4280
- "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4281
- );
4282
- }
4283
- }
4284
- let finalSearchEnvVars = input.searchEnvVars;
4285
4437
  let agentRuns = 0;
4286
4438
  let ingestCommand;
4287
4439
  let ingestScriptRan = false;
@@ -4289,9 +4441,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4289
4441
  let ingestDurationMs;
4290
4442
  let ingestOutcomeMessage;
4291
4443
  const ingestKeyAppId = ingestAppId;
4292
- const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
4444
+ const ingestionTools = ingestKeyAppId && ingestWriteKey ? makeToolContext(repoRoot, async () => ({
4293
4445
  [APP_ID_VAR]: ingestKeyAppId,
4294
- [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4446
+ [API_KEY_VAR]: ingestWriteKey,
4295
4447
  [INDEX_NAME_VAR]: targetIndex
4296
4448
  })) : void 0;
4297
4449
  const searchTools = makeToolContext(repoRoot);
@@ -4391,6 +4543,7 @@ ${detail}` : ""}`
4391
4543
  ]
4392
4544
  });
4393
4545
  }
4546
+ let searchConfigFile;
4394
4547
  if (useCases.includes("search")) {
4395
4548
  let extraInstructions = [];
4396
4549
  useWizard.getState().clearWrittenFiles();
@@ -4405,11 +4558,14 @@ ${detail}` : ""}`
4405
4558
  "implement: retrying search implementation after failed verification"
4406
4559
  );
4407
4560
  }
4408
- const { summary } = await runImplementationUseCase(
4561
+ const searchResult = await runImplementationUseCase(
4409
4562
  "search",
4410
4563
  extraInstructions
4411
4564
  );
4412
- summaries.push(formatSummary("search", summary));
4565
+ summaries.push(formatSummary("search", searchResult.summary));
4566
+ if (searchResult.searchConfigFile) {
4567
+ searchConfigFile = searchResult.searchConfigFile;
4568
+ }
4413
4569
  const verification = await runVerificationUseCase();
4414
4570
  summaries.push(formatSummary("verification", verification.summary));
4415
4571
  if (verification.sufficient) {
@@ -4433,76 +4589,17 @@ ${detail}` : ""}`
4433
4589
  }
4434
4590
  extraInstructions = verificationRetryInstructions(verification);
4435
4591
  }
4436
- let searchKey;
4437
- let searchKeyError;
4438
- if (appId) {
4439
- try {
4440
- const resolved2 = await resolveSearchOnlyKey(
4441
- targetIndex,
4442
- appId,
4443
- envSearchKey
4444
- );
4445
- searchKey = resolved2.key;
4446
- summaries.push(
4447
- 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}.`
4448
- );
4449
- } catch (err) {
4450
- searchKeyError = err.message;
4451
- logger.warn(
4452
- { err: searchKeyError },
4453
- "implement: could not provision a search-only API key; the .env value stays a placeholder"
4454
- );
4455
- }
4456
- }
4457
- finalSearchEnvVars = publicSearchEnvVars(
4458
- publicEnvVarPrefix,
4459
- targetIndex,
4460
- appId,
4461
- searchKey
4462
- );
4463
- const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4464
- (v) => !v.value.startsWith("<")
4465
- );
4466
- if (resolvedSearchEnvVars.length > 0) {
4467
- const written = await writeSearchEnvValues(
4592
+ if (searchConfigFile) {
4593
+ const ignoreStatus = await gitIgnoreStatus(
4468
4594
  repoRoot,
4469
- resolvedSearchEnvVars
4595
+ join11(repoRoot, searchConfigFile)
4470
4596
  );
4471
- if (written.length > 0) {
4472
- summaries.push(`Wrote ${written.join(", ")} to .env.`);
4473
- }
4474
- const ignored = await ensureGitIgnored(repoRoot, join10(repoRoot, ".env"));
4475
- if (ignored === "added") {
4476
- summaries.push("Added .env to .gitignore.");
4477
- } else if (ignored === "tracked") {
4478
- summaries.push(
4479
- '\u26A0\uFE0F .env is tracked by git, so a .gitignore rule cannot un-stage it. Run "git rm --cached .env" before committing, or the credentials go into history.'
4480
- );
4481
- }
4482
- const stale = [];
4483
- for (const v of resolvedSearchEnvVars) {
4484
- if (written.includes(v.name)) continue;
4485
- const current = await readEnvVar(repoRoot, v.name);
4486
- if (current && current !== v.value) stale.push(v);
4487
- }
4488
- if (stale.length > 0 && !envAppIdMismatch) {
4597
+ if (ignoreStatus === "covered") {
4489
4598
  summaries.push(
4490
- `\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.`
4491
- );
4492
- logger.warn(
4493
- { vars: stale.map((v) => v.name) },
4494
- "implement: .env holds different values for the resolved search credentials; not overwriting them"
4599
+ `\u26A0\uFE0F ${searchConfigFile} is gitignored, so this public, safe-to-share search config won't reach teammates or CI. Remove whatever .gitignore rule covers it.`
4495
4600
  );
4496
4601
  }
4497
4602
  }
4498
- const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
4499
- (v) => v.value.startsWith("<")
4500
- );
4501
- if (unresolvedSearchEnvVars.length > 0) {
4502
- summaries.push(
4503
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
4504
- );
4505
- }
4506
4603
  } else {
4507
4604
  ctx.setUserInput("implementation", "success");
4508
4605
  }
@@ -4515,7 +4612,19 @@ ${detail}` : ""}`
4515
4612
  ...ingestRecordCount != null ? { ingestRecordCount } : {},
4516
4613
  ...ingestDurationMs != null ? { ingestDurationMs } : {}
4517
4614
  } : {},
4518
- ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
4615
+ ...useCases.includes("search") ? {
4616
+ searchConfig: {
4617
+ filePath: searchConfigFile,
4618
+ vars: [
4619
+ { name: SEARCH_CONFIG_APP_ID, value: appId ?? "" },
4620
+ {
4621
+ name: SEARCH_CONFIG_SEARCH_KEY,
4622
+ value: searchKey ?? SEARCH_KEY_PLACEHOLDER
4623
+ },
4624
+ { name: SEARCH_CONFIG_INDEX_NAME, value: targetIndex }
4625
+ ]
4626
+ }
4627
+ } : {}
4519
4628
  };
4520
4629
  }
4521
4630
 
@@ -4561,8 +4670,8 @@ var defaultWorkflow = {
4561
4670
  defineStep({
4562
4671
  id: "select-index",
4563
4672
  title: "Set up index",
4564
- outputSchema: z30.object({
4565
- selection: z30.string()
4673
+ outputSchema: z29.object({
4674
+ selection: z29.string()
4566
4675
  }),
4567
4676
  run: (ctx) => selectIndexStep(ctx)
4568
4677
  }),
@@ -4629,7 +4738,7 @@ function getWorkflow(id) {
4629
4738
  }
4630
4739
 
4631
4740
  // src/ui/Welcome.tsx
4632
- import { dirname as dirname8, join as join11 } from "node:path";
4741
+ import { dirname as dirname8, join as join12 } from "node:path";
4633
4742
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4634
4743
  import { useState as useState7 } from "react";
4635
4744
  import { Box as Box10, Spacer, Text as Text10, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
@@ -4661,7 +4770,7 @@ var sidebarItems = [
4661
4770
  // src/ui/Welcome.tsx
4662
4771
  import Image, { TerminalInfoContext, defaultTerminalInfo } from "ink-picture";
4663
4772
  import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
4664
- var IMAGE_PATH = join11(dirname8(fileURLToPath2(import.meta.url)), "algolia.png");
4773
+ var IMAGE_PATH = join12(dirname8(fileURLToPath2(import.meta.url)), "algolia.png");
4665
4774
  var TERMINAL_INFO = {
4666
4775
  ...defaultTerminalInfo,
4667
4776
  supportsUnicode: true,
@@ -5489,7 +5598,8 @@ function App() {
5489
5598
  steps,
5490
5599
  inputReq,
5491
5600
  user,
5492
- workflow
5601
+ workflow,
5602
+ settingUpAppId
5493
5603
  } = useWizard();
5494
5604
  const { exit } = useApp();
5495
5605
  const { columns, rows } = useWindowSize7();
@@ -5504,7 +5614,7 @@ function App() {
5504
5614
  const isCommandApprovalPrompt = isAwaitingUserInput && inputReq?.promptType === "commandApproval";
5505
5615
  const promptPending = isAwaitingUserInput && !isCommandApprovalPrompt;
5506
5616
  const holdForTip = stepHasTips && promptPending && tipState === "revealing";
5507
- const showTips = stepHasTips && (phase === "running" || isCommandApprovalPrompt || holdForTip);
5617
+ const showTips = stepHasTips && (phase === "running" && currentStep?.status === "running" || isCommandApprovalPrompt || holdForTip);
5508
5618
  const showNoticesInMain = !stepHasTips;
5509
5619
  const showNotices = !isAwaitingUserInput || holdForTip;
5510
5620
  useInput6(
@@ -5589,6 +5699,12 @@ function App() {
5589
5699
  ] }),
5590
5700
  /* @__PURE__ */ jsx17(Text19, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
5591
5701
  ] }),
5702
+ settingUpAppId && /* @__PURE__ */ jsx17(Box19, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
5703
+ /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
5704
+ " Setting up wizard using app",
5705
+ " ",
5706
+ settingUpAppId
5707
+ ] }) }),
5592
5708
  /* @__PURE__ */ jsx17(CliOutput, {}),
5593
5709
  showTips && currentStep && /* @__PURE__ */ jsx17(
5594
5710
  Tips,
@@ -5621,140 +5737,6 @@ function App() {
5621
5737
  );
5622
5738
  }
5623
5739
 
5624
- // src/lib/envAppId.ts
5625
- import { readFile as readFile8 } from "node:fs/promises";
5626
- import { join as join12 } from "node:path";
5627
- var ENV_FILES = [".env", ".env.local"];
5628
- var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
5629
- async function findEnvApplicationId(root = process.cwd()) {
5630
- for (const file of ENV_FILES) {
5631
- let content;
5632
- try {
5633
- content = await readFile8(join12(root, file), "utf8");
5634
- } catch (err) {
5635
- if (err.code !== "ENOENT") {
5636
- logger.warn(
5637
- { file, err },
5638
- "could not read env file for an application id"
5639
- );
5640
- }
5641
- continue;
5642
- }
5643
- for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
5644
- const id = readValue(raw);
5645
- if (id) {
5646
- logger.info({ file, name, app: id }, "found an application id in env");
5647
- return { id, name, file };
5648
- }
5649
- }
5650
- }
5651
- return null;
5652
- }
5653
- function readValue(raw) {
5654
- const trimmed = raw.trim();
5655
- const quoted = trimmed.match(/^(['"])(.*)\1/);
5656
- const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
5657
- return value.length > 0 && !value.startsWith("<") ? value : null;
5658
- }
5659
-
5660
- // src/lib/algoliaAppPicker.ts
5661
- function secondaryFor(app) {
5662
- return app.plan ? { kind: "badge", value: app.plan } : void 0;
5663
- }
5664
- function labelFor(app) {
5665
- return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
5666
- }
5667
- function selectAndReport(app) {
5668
- useWizard.getState().pushCliOutput(
5669
- "stdout",
5670
- `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
5671
- );
5672
- return selectApplication(app.id);
5673
- }
5674
- async function promptForApplication(leadIn = []) {
5675
- const store = useWizard.getState();
5676
- const apps = await listApplications();
5677
- if (apps.length === 0) {
5678
- throw new Error(
5679
- "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
5680
- );
5681
- }
5682
- if (apps.length === 1) {
5683
- const only = apps[0];
5684
- logger.info(
5685
- { app: only.id },
5686
- "single application on the account; selecting it"
5687
- );
5688
- for (const line of leadIn) store.pushCliOutput("stdout", line);
5689
- return selectAndReport(only);
5690
- }
5691
- const messages = [
5692
- ...leadIn,
5693
- "Which Algolia application should the wizard work in?"
5694
- ];
5695
- for (; ; ) {
5696
- const choice = await store.requestUserInput({
5697
- prompt: "Select an application",
5698
- promptType: "multipleChoice",
5699
- options: apps.map(labelFor),
5700
- secondary: apps.map(secondaryFor),
5701
- messages
5702
- });
5703
- const chosen = apps.find((app) => labelFor(app) === choice);
5704
- if (!chosen) {
5705
- throw new Error("Application picker received an unexpected selection");
5706
- }
5707
- try {
5708
- return await selectAndReport(chosen);
5709
- } catch (err) {
5710
- logger.warn(
5711
- { app: chosen.id, err: err.message },
5712
- "application select failed; re-prompting"
5713
- );
5714
- messages.push(
5715
- `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
5716
- );
5717
- }
5718
- }
5719
- }
5720
- async function confirmEnvApplication(env, current) {
5721
- const useEnv = `Use ${env.id} (from ${env.file})`;
5722
- const choice = await useWizard.getState().requestUserInput({
5723
- prompt: "Select an application",
5724
- promptType: "multipleChoice",
5725
- options: [
5726
- useEnv,
5727
- current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
5728
- ],
5729
- messages: [
5730
- `${env.file} already sets ${env.name}=${env.id}.`,
5731
- "Which Algolia application should the wizard work in?"
5732
- ]
5733
- });
5734
- return choice === useEnv;
5735
- }
5736
- async function selectEnvApplication(env) {
5737
- try {
5738
- return await selectAndReport({ id: env.id, name: "" });
5739
- } catch (err) {
5740
- logger.warn(
5741
- { app: env.id, err: err.message },
5742
- "could not select the application named in env; falling back to the picker"
5743
- );
5744
- return promptForApplication([
5745
- `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
5746
- ]);
5747
- }
5748
- }
5749
- async function ensureApplication() {
5750
- const current = await currentApplication();
5751
- const env = await findEnvApplicationId();
5752
- if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
5753
- return selectEnvApplication(env);
5754
- }
5755
- return current ?? await promptForApplication();
5756
- }
5757
-
5758
5740
  // src/lib/seed.ts
5759
5741
  var projectScan2 = {
5760
5742
  languages: [{ name: "TypeScript", version: "5.7.2" }],
@@ -5799,10 +5781,14 @@ var confirmFramework2 = {
5799
5781
  var search = {
5800
5782
  summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
5801
5783
  ingestionSource: "generated",
5802
- searchEnvVars: [
5803
- { name: "NEXT_PUBLIC_ALGOLIA_APP_ID", value: "SEEDAPPID" },
5804
- { name: "NEXT_PUBLIC_ALGOLIA_SEARCH_KEY", value: "seedsearchkey" }
5805
- ]
5784
+ searchConfig: {
5785
+ filePath: "src/algolia.config.ts",
5786
+ vars: [
5787
+ { name: "ALGOLIA_APP_ID", value: "SEEDAPPID" },
5788
+ { name: "ALGOLIA_SEARCH_API_KEY", value: "seedsearchkey" },
5789
+ { name: "ALGOLIA_INDEX_NAME", value: "wizard_seed_products" }
5790
+ ]
5791
+ }
5806
5792
  };
5807
5793
  var review = {
5808
5794
  summaryPoints: [
@@ -6113,7 +6099,8 @@ async function run(workflow) {
6113
6099
  store.setUser(user);
6114
6100
  let app;
6115
6101
  try {
6116
- app = await ensureApplication();
6102
+ const resuming = await isResumableWorkflow(workflow);
6103
+ app = await ensureApplication(resuming);
6117
6104
  } catch (err) {
6118
6105
  store.setError(err instanceof Error ? err.message : String(err));
6119
6106
  await instance.waitUntilExit();