@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(
|
|
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
|
|
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:
|
|
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:
|
|
678
|
+
bold: active && !highlightBackground,
|
|
672
679
|
wrap: "truncate",
|
|
673
680
|
children: [
|
|
674
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
|
1852
|
-
var applicationSchema =
|
|
1853
|
-
id:
|
|
1854
|
-
name:
|
|
1855
|
-
plan:
|
|
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 =
|
|
1858
|
-
|
|
1859
|
-
id:
|
|
1860
|
-
name:
|
|
1861
|
-
plan_label:
|
|
1862
|
-
|
|
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
|
|
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
|
|
1853
|
+
import { z as z6 } from "zod";
|
|
1918
1854
|
var SERVICE = "algolia-wizard";
|
|
1919
|
-
var
|
|
1920
|
-
var storedKeysSchema =
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
2003
|
-
key:
|
|
2004
|
-
value:
|
|
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
|
|
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
|
|
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
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
|
2095
|
-
import { join as
|
|
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 =
|
|
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
|
|
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 = "
|
|
2167
|
-
var API_KEY_VAR = "
|
|
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(`^(\\
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
2331
|
-
import { join as
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
3897
|
+
import z28 from "zod";
|
|
3684
3898
|
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
3685
|
-
import { join as
|
|
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,
|
|
3690
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as
|
|
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 =
|
|
3728
|
-
const dest =
|
|
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
|
|
3981
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
3804
3982
|
import { fileURLToPath } from "node:url";
|
|
3805
|
-
var DOCS_SUBPATH =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
3878
|
-
summary:
|
|
3879
|
-
ingestCommand:
|
|
3880
|
-
ingestScriptRan:
|
|
3881
|
-
ingestRecordCount:
|
|
3882
|
-
ingestDurationMs:
|
|
3883
|
-
ingestionSource:
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
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 =
|
|
3892
|
-
summary:
|
|
3893
|
-
ingestCommand:
|
|
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 =
|
|
3896
|
-
summary:
|
|
3897
|
-
sufficient:
|
|
3898
|
-
additionalInstructions:
|
|
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
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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]:
|
|
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
|
|
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
|
-
|
|
4437
|
-
|
|
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
|
-
|
|
4595
|
+
join11(repoRoot, searchConfigFile)
|
|
4470
4596
|
);
|
|
4471
|
-
if (
|
|
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
|
|
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") ? {
|
|
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:
|
|
4565
|
-
selection:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
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
|
-
|
|
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();
|