@algolia/wizard 0.10.0 → 0.12.0-rc.104.151
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 +497 -290
- package/package.json +2 -1
package/dist/main.js
CHANGED
|
@@ -17,6 +17,9 @@ function npxArgs(args) {
|
|
|
17
17
|
return ["--yes", "@algolia/cli@latest", ...args];
|
|
18
18
|
}
|
|
19
19
|
var shell = process.platform === "win32";
|
|
20
|
+
function mask(text, secret) {
|
|
21
|
+
return secret ? text.replaceAll(secret, "***") : text;
|
|
22
|
+
}
|
|
20
23
|
function lineSplitter(emit) {
|
|
21
24
|
let buffer = "";
|
|
22
25
|
return {
|
|
@@ -40,9 +43,10 @@ var stderrSink = (stream, line) => {
|
|
|
40
43
|
if (stream === "stdout") return;
|
|
41
44
|
wizardSink(stream, line);
|
|
42
45
|
};
|
|
43
|
-
function runAlgoliaCli(args, { onOutput } = {}) {
|
|
46
|
+
function runAlgoliaCli(args, { onOutput, redact } = {}) {
|
|
44
47
|
const store = useWizard.getState();
|
|
45
|
-
const
|
|
48
|
+
const command = mask(args.join(" "), redact);
|
|
49
|
+
const logId = store.logStart("tool", `algolia ${command}`);
|
|
46
50
|
return new Promise((resolve4, reject) => {
|
|
47
51
|
const child = spawn("npx", npxArgs(args), { shell });
|
|
48
52
|
let stdout = "";
|
|
@@ -71,13 +75,13 @@ function runAlgoliaCli(args, { onOutput } = {}) {
|
|
|
71
75
|
const failed = stderr.trim();
|
|
72
76
|
let detail = "";
|
|
73
77
|
if (failed) {
|
|
74
|
-
detail = `: ${failed}`;
|
|
78
|
+
detail = `: ${mask(failed, redact)}`;
|
|
75
79
|
} else if (stdout.trim()) {
|
|
76
80
|
detail = " (no stderr; stdout withheld \u2014 it may contain credentials)";
|
|
77
81
|
}
|
|
78
82
|
reject(
|
|
79
83
|
new Error(
|
|
80
|
-
`Algolia CLI \`${
|
|
84
|
+
`Algolia CLI \`${command}\` failed (exit ${code})${detail}`
|
|
81
85
|
)
|
|
82
86
|
);
|
|
83
87
|
}
|
|
@@ -158,10 +162,14 @@ import { join, resolve } from "node:path";
|
|
|
158
162
|
function rootDir() {
|
|
159
163
|
return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
|
|
160
164
|
}
|
|
161
|
-
|
|
165
|
+
var pinnedRoot;
|
|
166
|
+
function setProjectRoot(cwd) {
|
|
167
|
+
pinnedRoot = resolve(cwd);
|
|
168
|
+
}
|
|
169
|
+
function projectSlug(cwd = pinnedRoot ?? process.cwd()) {
|
|
162
170
|
return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
|
|
163
171
|
}
|
|
164
|
-
function stateDir(cwd = process.cwd()) {
|
|
172
|
+
function stateDir(cwd = pinnedRoot ?? process.cwd()) {
|
|
165
173
|
return join(rootDir(), projectSlug(cwd));
|
|
166
174
|
}
|
|
167
175
|
|
|
@@ -1023,9 +1031,14 @@ var sidebarItems = [
|
|
|
1023
1031
|
];
|
|
1024
1032
|
|
|
1025
1033
|
// src/ui/Welcome.tsx
|
|
1026
|
-
import Image, {
|
|
1034
|
+
import Image, { TerminalInfoContext, defaultTerminalInfo } from "ink-picture";
|
|
1027
1035
|
import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1028
1036
|
var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
|
|
1037
|
+
var TERMINAL_INFO = {
|
|
1038
|
+
...defaultTerminalInfo,
|
|
1039
|
+
supportsUnicode: true,
|
|
1040
|
+
supportsColor: true
|
|
1041
|
+
};
|
|
1029
1042
|
function SidebarItem({
|
|
1030
1043
|
title,
|
|
1031
1044
|
description
|
|
@@ -1072,7 +1085,7 @@ function Welcome() {
|
|
|
1072
1085
|
flexDirection: "column",
|
|
1073
1086
|
justifyContent: "center",
|
|
1074
1087
|
children: /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", gap: 2, children: [
|
|
1075
|
-
/* @__PURE__ */ jsx6(
|
|
1088
|
+
/* @__PURE__ */ jsx6(TerminalInfoContext.Provider, { value: TERMINAL_INFO, children: /* @__PURE__ */ jsx6(
|
|
1076
1089
|
Image,
|
|
1077
1090
|
{
|
|
1078
1091
|
src: IMAGE_PATH,
|
|
@@ -1743,18 +1756,17 @@ import "zod";
|
|
|
1743
1756
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1744
1757
|
import { join as join5 } from "node:path";
|
|
1745
1758
|
var configFile = () => join5(stateDir(), "config.json");
|
|
1746
|
-
var
|
|
1759
|
+
var defaultConfig = () => ({
|
|
1747
1760
|
version: 1,
|
|
1748
1761
|
aiConsent: false,
|
|
1749
|
-
workflowsRun: []
|
|
1750
|
-
|
|
1751
|
-
};
|
|
1762
|
+
workflowsRun: []
|
|
1763
|
+
});
|
|
1752
1764
|
async function loadConfig() {
|
|
1753
1765
|
try {
|
|
1754
1766
|
const raw = await readFile2(configFile(), "utf8");
|
|
1755
|
-
return { ...
|
|
1767
|
+
return { ...defaultConfig(), ...JSON.parse(raw) };
|
|
1756
1768
|
} catch {
|
|
1757
|
-
return
|
|
1769
|
+
return defaultConfig();
|
|
1758
1770
|
}
|
|
1759
1771
|
}
|
|
1760
1772
|
async function saveConfig(config) {
|
|
@@ -1766,38 +1778,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
|
|
|
1766
1778
|
config.workflowsRun.push({ workflowId, completedAt });
|
|
1767
1779
|
await saveConfig(config);
|
|
1768
1780
|
}
|
|
1769
|
-
function isStoredSearchKey(value) {
|
|
1770
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1771
|
-
const { appId, key } = value;
|
|
1772
|
-
return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
|
|
1773
|
-
}
|
|
1774
|
-
function storedSearchKeys(config) {
|
|
1775
|
-
const stored = config.searchApiKeys;
|
|
1776
|
-
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
1777
|
-
return {};
|
|
1778
|
-
}
|
|
1779
|
-
return stored;
|
|
1780
|
-
}
|
|
1781
|
-
async function getStoredSearchKey(index, appId) {
|
|
1782
|
-
const entry = storedSearchKeys(await loadConfig())[index];
|
|
1783
|
-
if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
|
|
1784
|
-
return entry.key;
|
|
1785
|
-
}
|
|
1786
|
-
async function storeSearchKey(index, appId, key) {
|
|
1787
|
-
const config = await loadConfig();
|
|
1788
|
-
config.searchApiKeys = {
|
|
1789
|
-
...storedSearchKeys(config),
|
|
1790
|
-
[index]: { appId, key }
|
|
1791
|
-
};
|
|
1792
|
-
await saveConfig(config);
|
|
1793
|
-
}
|
|
1794
|
-
async function forgetSearchKey(index) {
|
|
1795
|
-
const config = await loadConfig();
|
|
1796
|
-
const remaining = { ...storedSearchKeys(config) };
|
|
1797
|
-
delete remaining[index];
|
|
1798
|
-
config.searchApiKeys = remaining;
|
|
1799
|
-
await saveConfig(config);
|
|
1800
|
-
}
|
|
1801
1781
|
|
|
1802
1782
|
// src/core/orchestrator.ts
|
|
1803
1783
|
function defineStep(step) {
|
|
@@ -2063,6 +2043,42 @@ function parseJson(text) {
|
|
|
2063
2043
|
}
|
|
2064
2044
|
}
|
|
2065
2045
|
|
|
2046
|
+
// src/lib/envAppId.ts
|
|
2047
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2048
|
+
import { join as join6 } from "node:path";
|
|
2049
|
+
var ENV_FILES = [".env", ".env.local"];
|
|
2050
|
+
var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
|
|
2051
|
+
async function findEnvApplicationId(root = process.cwd()) {
|
|
2052
|
+
for (const file of ENV_FILES) {
|
|
2053
|
+
let content;
|
|
2054
|
+
try {
|
|
2055
|
+
content = await readFile3(join6(root, file), "utf8");
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
if (err.code !== "ENOENT") {
|
|
2058
|
+
logger.warn(
|
|
2059
|
+
{ file, err },
|
|
2060
|
+
"could not read env file for an application id"
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
|
|
2066
|
+
const id = readValue(raw);
|
|
2067
|
+
if (id) {
|
|
2068
|
+
logger.info({ file, name, app: id }, "found an application id in env");
|
|
2069
|
+
return { id, name, file };
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
function readValue(raw) {
|
|
2076
|
+
const trimmed = raw.trim();
|
|
2077
|
+
const quoted = trimmed.match(/^(['"])(.*)\1/);
|
|
2078
|
+
const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
|
|
2079
|
+
return value.length > 0 && !value.startsWith("<") ? value : null;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2066
2082
|
// src/lib/algoliaAppPicker.ts
|
|
2067
2083
|
function secondaryFor(app) {
|
|
2068
2084
|
return app.plan ? { kind: "badge", value: app.plan } : void 0;
|
|
@@ -2077,7 +2093,7 @@ function selectAndReport(app) {
|
|
|
2077
2093
|
);
|
|
2078
2094
|
return selectApplication(app.id);
|
|
2079
2095
|
}
|
|
2080
|
-
async function promptForApplication() {
|
|
2096
|
+
async function promptForApplication(leadIn = []) {
|
|
2081
2097
|
const store = useWizard.getState();
|
|
2082
2098
|
const apps = await listApplications();
|
|
2083
2099
|
if (apps.length === 0) {
|
|
@@ -2091,9 +2107,13 @@ async function promptForApplication() {
|
|
|
2091
2107
|
{ app: only.id },
|
|
2092
2108
|
"single application on the account; selecting it"
|
|
2093
2109
|
);
|
|
2110
|
+
for (const line of leadIn) store.pushCliOutput("stdout", line);
|
|
2094
2111
|
return selectAndReport(only);
|
|
2095
2112
|
}
|
|
2096
|
-
const messages = [
|
|
2113
|
+
const messages = [
|
|
2114
|
+
...leadIn,
|
|
2115
|
+
"Which Algolia application should the wizard work in?"
|
|
2116
|
+
];
|
|
2097
2117
|
for (; ; ) {
|
|
2098
2118
|
const choice = await store.requestUserInput({
|
|
2099
2119
|
prompt: "Select an application",
|
|
@@ -2119,12 +2139,46 @@ async function promptForApplication() {
|
|
|
2119
2139
|
}
|
|
2120
2140
|
}
|
|
2121
2141
|
}
|
|
2142
|
+
async function confirmEnvApplication(env, current) {
|
|
2143
|
+
const useEnv = `Use ${env.id} (from ${env.file})`;
|
|
2144
|
+
const choice = await useWizard.getState().requestUserInput({
|
|
2145
|
+
prompt: "Select an application",
|
|
2146
|
+
promptType: "multipleChoice",
|
|
2147
|
+
options: [
|
|
2148
|
+
useEnv,
|
|
2149
|
+
current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
|
|
2150
|
+
],
|
|
2151
|
+
messages: [
|
|
2152
|
+
`${env.file} already sets ${env.name}=${env.id}.`,
|
|
2153
|
+
"Which Algolia application should the wizard work in?"
|
|
2154
|
+
]
|
|
2155
|
+
});
|
|
2156
|
+
return choice === useEnv;
|
|
2157
|
+
}
|
|
2158
|
+
async function selectEnvApplication(env) {
|
|
2159
|
+
try {
|
|
2160
|
+
return await selectAndReport({ id: env.id, name: "" });
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
logger.warn(
|
|
2163
|
+
{ app: env.id, err: err.message },
|
|
2164
|
+
"could not select the application named in env; falling back to the picker"
|
|
2165
|
+
);
|
|
2166
|
+
return promptForApplication([
|
|
2167
|
+
`Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
|
|
2168
|
+
]);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2122
2171
|
async function ensureApplication() {
|
|
2123
|
-
|
|
2172
|
+
const current = await currentApplication();
|
|
2173
|
+
const env = await findEnvApplicationId();
|
|
2174
|
+
if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
|
|
2175
|
+
return selectEnvApplication(env);
|
|
2176
|
+
}
|
|
2177
|
+
return current ?? await promptForApplication();
|
|
2124
2178
|
}
|
|
2125
2179
|
|
|
2126
2180
|
// src/workflows/default.ts
|
|
2127
|
-
import { z as
|
|
2181
|
+
import { z as z28 } from "zod";
|
|
2128
2182
|
|
|
2129
2183
|
// src/actions/listIndices.ts
|
|
2130
2184
|
import { z as z5 } from "zod";
|
|
@@ -2208,7 +2262,7 @@ import { readdir } from "node:fs/promises";
|
|
|
2208
2262
|
|
|
2209
2263
|
// src/lib/tools/path.ts
|
|
2210
2264
|
import { lstat } from "node:fs/promises";
|
|
2211
|
-
import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as
|
|
2265
|
+
import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
|
|
2212
2266
|
function resolveInRoot(ctx, path) {
|
|
2213
2267
|
const target = resolve2(ctx.cwd, path);
|
|
2214
2268
|
const rel = relative(ctx.root, target);
|
|
@@ -2224,7 +2278,7 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
2224
2278
|
let current = ctx.root;
|
|
2225
2279
|
const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
|
|
2226
2280
|
for (const part of parts) {
|
|
2227
|
-
current =
|
|
2281
|
+
current = join7(current, part);
|
|
2228
2282
|
try {
|
|
2229
2283
|
if ((await lstat(current)).isSymbolicLink()) return true;
|
|
2230
2284
|
} catch (err) {
|
|
@@ -2245,9 +2299,9 @@ function listFilesTool(ctx) {
|
|
|
2245
2299
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2246
2300
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
2247
2301
|
}
|
|
2248
|
-
const
|
|
2249
|
-
if (!
|
|
2250
|
-
const entries = await readdir(
|
|
2302
|
+
const resolved2 = resolveInRoot(ctx, ".");
|
|
2303
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2304
|
+
const entries = await readdir(resolved2.target, { withFileTypes: true });
|
|
2251
2305
|
return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
2252
2306
|
}
|
|
2253
2307
|
});
|
|
@@ -2265,14 +2319,14 @@ function changeDirectoryTool(ctx) {
|
|
|
2265
2319
|
}),
|
|
2266
2320
|
execute: async ({ path }) => {
|
|
2267
2321
|
logger.info({ path }, "called changeDirectory tool");
|
|
2268
|
-
const
|
|
2269
|
-
if (!
|
|
2322
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2323
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2270
2324
|
try {
|
|
2271
|
-
const info = await stat(
|
|
2325
|
+
const info = await stat(resolved2.target);
|
|
2272
2326
|
if (!info.isDirectory()) {
|
|
2273
2327
|
return `Error changing directory to ${path}: not a directory`;
|
|
2274
2328
|
}
|
|
2275
|
-
ctx.cwd =
|
|
2329
|
+
ctx.cwd = resolved2.target;
|
|
2276
2330
|
return `Changed working directory to ${ctx.cwd}`;
|
|
2277
2331
|
} catch (err) {
|
|
2278
2332
|
return `Error changing directory to ${path}: ${err.message}`;
|
|
@@ -2302,7 +2356,7 @@ function reportStatusTool(output) {
|
|
|
2302
2356
|
// src/lib/tools/readFile.ts
|
|
2303
2357
|
import { tool as tool4 } from "ai";
|
|
2304
2358
|
import z9 from "zod";
|
|
2305
|
-
import { readFile as
|
|
2359
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2306
2360
|
|
|
2307
2361
|
// src/lib/tools/env.ts
|
|
2308
2362
|
import { basename } from "node:path";
|
|
@@ -2337,11 +2391,11 @@ function readFileTool(ctx) {
|
|
|
2337
2391
|
return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
|
|
2338
2392
|
}
|
|
2339
2393
|
logger.info({ filePath }, "called readFile tool");
|
|
2340
|
-
const
|
|
2341
|
-
if (!
|
|
2394
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2395
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2342
2396
|
try {
|
|
2343
|
-
const content = await
|
|
2344
|
-
return isEnvFile(
|
|
2397
|
+
const content = await readFile4(resolved2.target, "utf8");
|
|
2398
|
+
return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
|
|
2345
2399
|
} catch (err) {
|
|
2346
2400
|
return `Error reading ${filePath}: ${err.message}`;
|
|
2347
2401
|
}
|
|
@@ -2363,17 +2417,17 @@ function writeFileTool(ctx) {
|
|
|
2363
2417
|
}),
|
|
2364
2418
|
execute: async ({ filePath, content }) => {
|
|
2365
2419
|
logger.info({ filePath }, "called writeFile tool");
|
|
2366
|
-
const
|
|
2367
|
-
if (
|
|
2368
|
-
if (isSecretEnvFile(
|
|
2420
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2421
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2422
|
+
if (isSecretEnvFile(resolved2.target)) {
|
|
2369
2423
|
return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
|
|
2370
2424
|
}
|
|
2371
2425
|
try {
|
|
2372
|
-
if (await hasSymlinkParent(ctx,
|
|
2373
|
-
return `Refused: ${
|
|
2426
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2427
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2374
2428
|
}
|
|
2375
|
-
await mkdir3(dirname4(
|
|
2376
|
-
await writeFile3(
|
|
2429
|
+
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2430
|
+
await writeFile3(resolved2.target, content, "utf8");
|
|
2377
2431
|
return `Wrote to ${filePath}`;
|
|
2378
2432
|
} catch (err) {
|
|
2379
2433
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -2384,12 +2438,93 @@ function writeFileTool(ctx) {
|
|
|
2384
2438
|
|
|
2385
2439
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2386
2440
|
import { tool as tool6 } from "ai";
|
|
2387
|
-
import
|
|
2388
|
-
import { mkdir as mkdir4, readFile as
|
|
2441
|
+
import z13 from "zod";
|
|
2442
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
2389
2443
|
import { dirname as dirname5 } from "node:path";
|
|
2390
2444
|
|
|
2391
2445
|
// src/lib/algoliaApiKey.ts
|
|
2446
|
+
import { z as z12 } from "zod";
|
|
2447
|
+
|
|
2448
|
+
// src/lib/keychain.ts
|
|
2449
|
+
import { deletePassword, getPassword, setPassword } from "cross-keychain";
|
|
2392
2450
|
import { z as z11 } from "zod";
|
|
2451
|
+
var SERVICE = "algolia-wizard";
|
|
2452
|
+
var ACCOUNT = "api-keys";
|
|
2453
|
+
var storedKeysSchema = z11.record(z11.string(), z11.string());
|
|
2454
|
+
function entryId(kind, index, appId) {
|
|
2455
|
+
return `${kind}:${appId}:${index}`;
|
|
2456
|
+
}
|
|
2457
|
+
async function loadKeys() {
|
|
2458
|
+
const raw = await getPassword(SERVICE, ACCOUNT);
|
|
2459
|
+
if (!raw) return {};
|
|
2460
|
+
let payload;
|
|
2461
|
+
try {
|
|
2462
|
+
payload = JSON.parse(raw);
|
|
2463
|
+
} catch {
|
|
2464
|
+
payload = null;
|
|
2465
|
+
}
|
|
2466
|
+
const keys = storedKeysSchema.safeParse(payload);
|
|
2467
|
+
if (!keys.success) {
|
|
2468
|
+
logger.warn("the stored API keys are unreadable; treating them as empty");
|
|
2469
|
+
return {};
|
|
2470
|
+
}
|
|
2471
|
+
return keys.data;
|
|
2472
|
+
}
|
|
2473
|
+
var queue = Promise.resolve();
|
|
2474
|
+
function serialized(op) {
|
|
2475
|
+
const next = queue.then(op);
|
|
2476
|
+
queue = next.catch(() => {
|
|
2477
|
+
});
|
|
2478
|
+
return next;
|
|
2479
|
+
}
|
|
2480
|
+
async function readStoredKey(kind, index, appId) {
|
|
2481
|
+
try {
|
|
2482
|
+
return (await loadKeys())[entryId(kind, index, appId)] ?? null;
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
logger.warn(
|
|
2485
|
+
{ err: err.message, kind, index, appId },
|
|
2486
|
+
"could not read the API key from the keychain"
|
|
2487
|
+
);
|
|
2488
|
+
return null;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
function storeKey(kind, index, appId, value) {
|
|
2492
|
+
return serialized(async () => {
|
|
2493
|
+
const id = entryId(kind, index, appId);
|
|
2494
|
+
try {
|
|
2495
|
+
const keys = await loadKeys();
|
|
2496
|
+
await setPassword(
|
|
2497
|
+
SERVICE,
|
|
2498
|
+
ACCOUNT,
|
|
2499
|
+
JSON.stringify({ ...keys, [id]: value })
|
|
2500
|
+
);
|
|
2501
|
+
if ((await loadKeys())[id] !== value) {
|
|
2502
|
+
throw new Error("the keychain did not store the value");
|
|
2503
|
+
}
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
logger.warn(
|
|
2506
|
+
{ err: err.message, kind, index, appId },
|
|
2507
|
+
"could not store the API key in the keychain; the next run will create another"
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
function deleteStoredKeys() {
|
|
2513
|
+
return serialized(async () => {
|
|
2514
|
+
try {
|
|
2515
|
+
await deletePassword(SERVICE, ACCOUNT);
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
const message = err.message;
|
|
2518
|
+
if (/not found/i.test(message)) return;
|
|
2519
|
+
logger.warn(
|
|
2520
|
+
{ err: message },
|
|
2521
|
+
"could not delete the API keys from the keychain"
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/lib/algoliaApiKey.ts
|
|
2393
2528
|
var WRITE_ACLS = [
|
|
2394
2529
|
"addObject",
|
|
2395
2530
|
"deleteObject",
|
|
@@ -2397,58 +2532,21 @@ var WRITE_ACLS = [
|
|
|
2397
2532
|
"editSettings",
|
|
2398
2533
|
"listIndexes"
|
|
2399
2534
|
];
|
|
2400
|
-
var
|
|
2401
|
-
|
|
2402
|
-
value:
|
|
2403
|
-
acl: z11.array(z11.string()).default([]),
|
|
2404
|
-
indexes: z11.array(z11.string()).default([])
|
|
2405
|
-
});
|
|
2406
|
-
var apiKeyListSchema = z11.object({
|
|
2407
|
-
items: z11.array(apiKeySchema).optional(),
|
|
2408
|
-
keys: z11.array(apiKeySchema).optional()
|
|
2409
|
-
}).transform((o) => o.items ?? o.keys ?? []);
|
|
2410
|
-
var createdKeySchema = z11.object({
|
|
2411
|
-
key: z11.string().min(1).optional(),
|
|
2412
|
-
value: z11.string().min(1).optional()
|
|
2535
|
+
var createdKeySchema = z12.object({
|
|
2536
|
+
key: z12.string().min(1).optional(),
|
|
2537
|
+
value: z12.string().min(1).optional()
|
|
2413
2538
|
}).transform((o) => o.key ?? o.value);
|
|
2414
|
-
function
|
|
2415
|
-
|
|
2416
|
-
}
|
|
2417
|
-
async function resolveWriteKey(index) {
|
|
2418
|
-
const stdout = await runAlgoliaCli(["apikeys", "list", "-o", "json"]);
|
|
2419
|
-
const existing = apiKeyListSchema.parse(JSON.parse(stdout)).find((key) => canReuseForWrites(key, index))?.value;
|
|
2420
|
-
if (existing) {
|
|
2421
|
-
logger.info({ index }, "reusing existing write API key");
|
|
2422
|
-
return existing;
|
|
2423
|
-
}
|
|
2424
|
-
logger.info({ index }, "no reusable write key found; creating one");
|
|
2425
|
-
const created = await runAlgoliaCli([
|
|
2426
|
-
"apikeys",
|
|
2427
|
-
"create",
|
|
2428
|
-
"--indices",
|
|
2429
|
-
index,
|
|
2430
|
-
"--acl",
|
|
2431
|
-
WRITE_ACLS.join(","),
|
|
2432
|
-
"--description",
|
|
2433
|
-
`wizard write key for ${index}`,
|
|
2434
|
-
"-o",
|
|
2435
|
-
"json"
|
|
2436
|
-
]);
|
|
2437
|
-
const writeKey = createdKeySchema.parse(JSON.parse(created));
|
|
2438
|
-
if (!writeKey) throw new Error("apikeys create returned no key value");
|
|
2439
|
-
return writeKey;
|
|
2440
|
-
}
|
|
2441
|
-
async function createSearchOnlyKey(index) {
|
|
2442
|
-
logger.info({ index }, "creating a search-only API key");
|
|
2539
|
+
async function createKey(index, acls, description) {
|
|
2540
|
+
logger.info({ index, acls }, "creating an API key");
|
|
2443
2541
|
const stdout = await runAlgoliaCli([
|
|
2444
2542
|
"apikeys",
|
|
2445
2543
|
"create",
|
|
2446
2544
|
"--acl",
|
|
2447
|
-
"
|
|
2545
|
+
acls.join(","),
|
|
2448
2546
|
"--indices",
|
|
2449
2547
|
index,
|
|
2450
2548
|
"--description",
|
|
2451
|
-
|
|
2549
|
+
description,
|
|
2452
2550
|
"-o",
|
|
2453
2551
|
"json"
|
|
2454
2552
|
]);
|
|
@@ -2462,49 +2560,72 @@ async function createSearchOnlyKey(index) {
|
|
|
2462
2560
|
if (!created) throw new Error("apikeys create returned no key value");
|
|
2463
2561
|
return created;
|
|
2464
2562
|
}
|
|
2465
|
-
async function
|
|
2563
|
+
async function keyExists(key) {
|
|
2466
2564
|
try {
|
|
2467
|
-
await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
|
|
2565
|
+
await runAlgoliaCli(["apikeys", "get", key, "-o", "json"], { redact: key });
|
|
2468
2566
|
return true;
|
|
2469
2567
|
} catch (err) {
|
|
2470
|
-
return !/does not exist
|
|
2568
|
+
return !/does not exist/i.test(err.message);
|
|
2471
2569
|
}
|
|
2472
2570
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2571
|
+
var resolved = /* @__PURE__ */ new Map();
|
|
2572
|
+
async function forgetResolvedKeys() {
|
|
2573
|
+
resolved.clear();
|
|
2574
|
+
await deleteStoredKeys();
|
|
2575
|
+
}
|
|
2576
|
+
function resolveKey(kind, index, appId, acls, description) {
|
|
2577
|
+
const cacheKey = `${kind}:${appId}:${index}`;
|
|
2578
|
+
const cached = resolved.get(cacheKey);
|
|
2579
|
+
if (cached) return cached;
|
|
2580
|
+
const pending = provisionKey(kind, index, appId, acls, description).catch(
|
|
2581
|
+
(err) => {
|
|
2582
|
+
resolved.delete(cacheKey);
|
|
2583
|
+
throw err;
|
|
2584
|
+
}
|
|
2585
|
+
);
|
|
2586
|
+
resolved.set(cacheKey, pending);
|
|
2587
|
+
return pending;
|
|
2588
|
+
}
|
|
2589
|
+
async function provisionKey(kind, index, appId, acls, description) {
|
|
2590
|
+
const stored = await readStoredKey(kind, index, appId);
|
|
2479
2591
|
if (stored) {
|
|
2480
|
-
if (await
|
|
2481
|
-
logger.info({ index, appId }, "reusing the stored
|
|
2482
|
-
return { key: stored, source: "
|
|
2592
|
+
if (await keyExists(stored)) {
|
|
2593
|
+
logger.info({ kind, index, appId }, "reusing the stored API key");
|
|
2594
|
+
return { key: stored, source: "keychain" };
|
|
2483
2595
|
}
|
|
2484
|
-
logger.
|
|
2485
|
-
{ index, appId },
|
|
2486
|
-
"the stored
|
|
2596
|
+
logger.info(
|
|
2597
|
+
{ kind, index, appId },
|
|
2598
|
+
"the stored API key no longer exists; creating another"
|
|
2487
2599
|
);
|
|
2488
|
-
await forgetSearchKey(index);
|
|
2489
2600
|
}
|
|
2490
|
-
const key = await
|
|
2491
|
-
await
|
|
2601
|
+
const key = await createKey(index, acls, description);
|
|
2602
|
+
await storeKey(kind, index, appId, key);
|
|
2492
2603
|
return { key, source: "created" };
|
|
2493
2604
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2605
|
+
function resolveWriteKey(index, appId) {
|
|
2606
|
+
return resolveKey(
|
|
2607
|
+
"write",
|
|
2608
|
+
index,
|
|
2609
|
+
appId,
|
|
2610
|
+
WRITE_ACLS,
|
|
2611
|
+
`Algolia Wizard write key for ${index} index`
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
async function resolveSearchOnlyKey(index, appId, envKey) {
|
|
2615
|
+
if (envKey) return { key: envKey, source: "env" };
|
|
2616
|
+
return resolveKey(
|
|
2617
|
+
"search",
|
|
2618
|
+
index,
|
|
2619
|
+
appId,
|
|
2620
|
+
["search"],
|
|
2621
|
+
`Algolia Wizard search-only key for ${index} index`
|
|
2622
|
+
);
|
|
2503
2623
|
}
|
|
2504
2624
|
|
|
2505
2625
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2506
2626
|
var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
|
|
2507
2627
|
var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
|
|
2628
|
+
var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
|
|
2508
2629
|
function appendEnv(content, entries) {
|
|
2509
2630
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2510
2631
|
const lines = entries.map(([name, value]) => `${name}=${value}
|
|
@@ -2514,53 +2635,111 @@ function appendEnv(content, entries) {
|
|
|
2514
2635
|
function hasEnv(content, name) {
|
|
2515
2636
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
2516
2637
|
}
|
|
2638
|
+
function readEnv(content, name) {
|
|
2639
|
+
const found = content.match(
|
|
2640
|
+
new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*(.*)$`, "m")
|
|
2641
|
+
);
|
|
2642
|
+
if (!found) return null;
|
|
2643
|
+
const raw = found[1].trim();
|
|
2644
|
+
const quoted = raw.match(/^(['"])(.*)\1/);
|
|
2645
|
+
const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, "");
|
|
2646
|
+
return value.length > 0 ? value : null;
|
|
2647
|
+
}
|
|
2648
|
+
function upsertEnv(content, name, value) {
|
|
2649
|
+
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
2650
|
+
return content.replace(
|
|
2651
|
+
new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "gm"),
|
|
2652
|
+
() => `${name}=${value}`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2517
2655
|
function writeCredentialsTool(ctx) {
|
|
2518
2656
|
return tool6({
|
|
2519
|
-
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file
|
|
2520
|
-
inputSchema:
|
|
2521
|
-
filePath:
|
|
2657
|
+
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.`,
|
|
2658
|
+
inputSchema: z13.object({
|
|
2659
|
+
filePath: z13.string().describe(
|
|
2522
2660
|
'Path to the env file to write credentials into (e.g. ".env")'
|
|
2523
2661
|
)
|
|
2524
2662
|
}),
|
|
2525
2663
|
execute: async ({ filePath }) => {
|
|
2526
2664
|
logger.info({ filePath }, "called writeCredentials tool");
|
|
2527
|
-
const
|
|
2528
|
-
if (
|
|
2665
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2666
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2529
2667
|
const targetIndex = useWizard.getState().targetIndex;
|
|
2530
2668
|
if (!targetIndex) {
|
|
2531
2669
|
return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
|
|
2532
2670
|
}
|
|
2533
|
-
let
|
|
2534
|
-
let
|
|
2535
|
-
try {
|
|
2536
|
-
appId = (await requireApplication()).id;
|
|
2537
|
-
writeKey = await resolveWriteKey(targetIndex);
|
|
2538
|
-
} catch (err) {
|
|
2539
|
-
return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
|
|
2540
|
-
}
|
|
2671
|
+
let existing = "";
|
|
2672
|
+
let present;
|
|
2541
2673
|
try {
|
|
2542
|
-
if (await hasSymlinkParent(ctx,
|
|
2543
|
-
return `Refused: ${
|
|
2674
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2675
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2544
2676
|
}
|
|
2545
|
-
let existing = "";
|
|
2546
2677
|
try {
|
|
2547
|
-
existing = await
|
|
2678
|
+
existing = await readFile5(resolved2.target, "utf8");
|
|
2548
2679
|
} catch (err) {
|
|
2549
2680
|
if (err.code !== "ENOENT") throw err;
|
|
2550
2681
|
}
|
|
2551
|
-
|
|
2552
|
-
(name) =>
|
|
2682
|
+
present = [APP_ID_VAR, API_KEY_VAR].filter(
|
|
2683
|
+
(name) => readEnv(existing, name) !== null
|
|
2553
2684
|
);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
return `Error writing credentials to ${filePath}: ${err.message}`;
|
|
2687
|
+
}
|
|
2688
|
+
const credentials = [];
|
|
2689
|
+
const notes = [];
|
|
2690
|
+
const fileAppId = readEnv(existing, APP_ID_VAR);
|
|
2691
|
+
const fileKey = readEnv(existing, API_KEY_VAR);
|
|
2692
|
+
const fileIndex = readEnv(existing, INDEX_NAME_VAR);
|
|
2693
|
+
if (fileAppId === null || fileKey === null) {
|
|
2694
|
+
try {
|
|
2695
|
+
const selected = (await requireApplication()).id;
|
|
2696
|
+
if (fileAppId === null) credentials.push([APP_ID_VAR, selected]);
|
|
2697
|
+
if (fileKey === null) {
|
|
2698
|
+
if (fileAppId !== null && fileAppId !== selected) {
|
|
2699
|
+
notes.push(
|
|
2700
|
+
`No ${API_KEY_VAR} was provisioned: ${filePath} names application ${fileAppId}, but ${selected} is selected. Tell the user to remove ${APP_ID_VAR} from ${filePath} and re-run so a matching pair can be written, or to fill in a ${API_KEY_VAR} for ${fileAppId} by hand.`
|
|
2701
|
+
);
|
|
2702
|
+
} else {
|
|
2703
|
+
credentials.push([
|
|
2704
|
+
API_KEY_VAR,
|
|
2705
|
+
(await resolveWriteKey(targetIndex, selected)).key
|
|
2706
|
+
]);
|
|
2707
|
+
}
|
|
2708
|
+
} else {
|
|
2709
|
+
notes.push(
|
|
2710
|
+
`The existing ${API_KEY_VAR} must belong to application ${selected} or writes will fail.`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
} catch (err) {
|
|
2714
|
+
return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
try {
|
|
2718
|
+
const updated = [
|
|
2719
|
+
...credentials,
|
|
2720
|
+
// Unconditional, unlike the credentials: the write key is scoped to
|
|
2721
|
+
// the run's index, so a stale name left in the file earns a 403.
|
|
2722
|
+
[INDEX_NAME_VAR, targetIndex]
|
|
2723
|
+
].reduce(
|
|
2724
|
+
(content, [name, value]) => upsertEnv(content, name, value),
|
|
2725
|
+
existing
|
|
2726
|
+
);
|
|
2727
|
+
await mkdir4(dirname5(resolved2.target), { recursive: true });
|
|
2728
|
+
await writeFile4(resolved2.target, updated, "utf8");
|
|
2729
|
+
const sentences = [
|
|
2730
|
+
`Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
|
|
2731
|
+
];
|
|
2732
|
+
if (fileIndex !== null && fileIndex !== targetIndex) {
|
|
2733
|
+
sentences.push(
|
|
2734
|
+
`That replaced the ${INDEX_NAME_VAR} already there ("${fileIndex}"), which this run does not target.`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2554
2737
|
if (present.length > 0) {
|
|
2555
|
-
|
|
2738
|
+
sentences.push(
|
|
2739
|
+
`Skipped ${present.join(" and ")}: already defined there.`
|
|
2740
|
+
);
|
|
2556
2741
|
}
|
|
2557
|
-
|
|
2558
|
-
[APP_ID_VAR, appId],
|
|
2559
|
-
[API_KEY_VAR, writeKey]
|
|
2560
|
-
]);
|
|
2561
|
-
await mkdir4(dirname5(resolved.target), { recursive: true });
|
|
2562
|
-
await writeFile4(resolved.target, envWithCredentials, "utf8");
|
|
2563
|
-
return `Wrote Algolia credentials to ${filePath}`;
|
|
2742
|
+
return [...sentences, ...notes].join(" ");
|
|
2564
2743
|
} catch (err) {
|
|
2565
2744
|
return `Error writing credentials to ${filePath}: ${err.message}`;
|
|
2566
2745
|
}
|
|
@@ -2570,16 +2749,16 @@ function writeCredentialsTool(ctx) {
|
|
|
2570
2749
|
|
|
2571
2750
|
// src/lib/tools/searchFiles.ts
|
|
2572
2751
|
import { tool as tool7 } from "ai";
|
|
2573
|
-
import
|
|
2574
|
-
import { readdir as readdir2, readFile as
|
|
2575
|
-
import { join as
|
|
2752
|
+
import z14 from "zod";
|
|
2753
|
+
import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
|
|
2754
|
+
import { join as join8 } from "node:path";
|
|
2576
2755
|
var MAX_QUERY_LENGTH = 1e3;
|
|
2577
2756
|
async function walkFiles(dir) {
|
|
2578
2757
|
const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
2579
2758
|
const out = [];
|
|
2580
2759
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2581
2760
|
if (e.name.startsWith(".") || skip.has(e.name)) continue;
|
|
2582
|
-
const full =
|
|
2761
|
+
const full = join8(dir, e.name);
|
|
2583
2762
|
if (e.isDirectory()) out.push(...await walkFiles(full));
|
|
2584
2763
|
else if (e.isFile()) out.push(full);
|
|
2585
2764
|
}
|
|
@@ -2588,9 +2767,9 @@ async function walkFiles(dir) {
|
|
|
2588
2767
|
function searchFilesTool(ctx) {
|
|
2589
2768
|
return tool7({
|
|
2590
2769
|
description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
|
|
2591
|
-
inputSchema:
|
|
2592
|
-
query:
|
|
2593
|
-
path:
|
|
2770
|
+
inputSchema: z14.object({
|
|
2771
|
+
query: z14.string().describe("JavaScript RegExp pattern to search for"),
|
|
2772
|
+
path: z14.string().optional().describe("Directory to search in (default: cwd)")
|
|
2594
2773
|
}),
|
|
2595
2774
|
execute: async ({ query, path = "." }) => {
|
|
2596
2775
|
logger.info({ query, path }, "called searchFiles tool");
|
|
@@ -2600,8 +2779,8 @@ function searchFilesTool(ctx) {
|
|
|
2600
2779
|
if (query.length > MAX_QUERY_LENGTH) {
|
|
2601
2780
|
return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
|
|
2602
2781
|
}
|
|
2603
|
-
const
|
|
2604
|
-
if (!
|
|
2782
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2783
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2605
2784
|
let re;
|
|
2606
2785
|
try {
|
|
2607
2786
|
re = new RegExp(query);
|
|
@@ -2609,10 +2788,10 @@ function searchFilesTool(ctx) {
|
|
|
2609
2788
|
return `Invalid regex: ${err.message}`;
|
|
2610
2789
|
}
|
|
2611
2790
|
const matches = [];
|
|
2612
|
-
for (const file of await walkFiles(
|
|
2791
|
+
for (const file of await walkFiles(resolved2.target)) {
|
|
2613
2792
|
let content;
|
|
2614
2793
|
try {
|
|
2615
|
-
content = await
|
|
2794
|
+
content = await readFile6(file, "utf8");
|
|
2616
2795
|
} catch {
|
|
2617
2796
|
continue;
|
|
2618
2797
|
}
|
|
@@ -2634,7 +2813,7 @@ function searchFilesTool(ctx) {
|
|
|
2634
2813
|
|
|
2635
2814
|
// src/lib/tools/verifyImplementation.ts
|
|
2636
2815
|
import { tool as tool8 } from "ai";
|
|
2637
|
-
import
|
|
2816
|
+
import z15 from "zod";
|
|
2638
2817
|
|
|
2639
2818
|
// src/lib/tools/utils/runCommand.ts
|
|
2640
2819
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2656,9 +2835,9 @@ function runCommand(command, args, cwd) {
|
|
|
2656
2835
|
}
|
|
2657
2836
|
|
|
2658
2837
|
// src/lib/tools/utils/packageManager.ts
|
|
2659
|
-
import { readFile as
|
|
2838
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2660
2839
|
import { existsSync } from "node:fs";
|
|
2661
|
-
import { join as
|
|
2840
|
+
import { join as join9 } from "node:path";
|
|
2662
2841
|
var LOCKFILES = [
|
|
2663
2842
|
["pnpm-lock.yaml", "pnpm"],
|
|
2664
2843
|
["yarn.lock", "yarn"],
|
|
@@ -2667,13 +2846,13 @@ var LOCKFILES = [
|
|
|
2667
2846
|
["package-lock.json", "npm"]
|
|
2668
2847
|
];
|
|
2669
2848
|
async function readPackageJson(cwd = process.cwd()) {
|
|
2670
|
-
return JSON.parse(await
|
|
2849
|
+
return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
|
|
2671
2850
|
}
|
|
2672
2851
|
function packageManagerFrom(pkg) {
|
|
2673
2852
|
return pkg.packageManager?.split("@")[0] ?? "npm";
|
|
2674
2853
|
}
|
|
2675
2854
|
function packageManagerFromLockfile(cwd) {
|
|
2676
|
-
return LOCKFILES.find(([file]) => existsSync(
|
|
2855
|
+
return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
|
|
2677
2856
|
}
|
|
2678
2857
|
async function detectPackageManager(cwd) {
|
|
2679
2858
|
try {
|
|
@@ -2714,7 +2893,7 @@ async function runRepoVerificationCheck() {
|
|
|
2714
2893
|
function verifyImplementationTool() {
|
|
2715
2894
|
return tool8({
|
|
2716
2895
|
description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
|
|
2717
|
-
inputSchema:
|
|
2896
|
+
inputSchema: z15.object(),
|
|
2718
2897
|
execute: async () => {
|
|
2719
2898
|
logger.info("called verifyImplementation tool");
|
|
2720
2899
|
return runRepoVerificationCheck();
|
|
@@ -2728,7 +2907,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
2728
2907
|
import { nanoid as nanoid2 } from "nanoid";
|
|
2729
2908
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
2730
2909
|
import { dirname as dirname6 } from "node:path";
|
|
2731
|
-
import
|
|
2910
|
+
import z16 from "zod";
|
|
2732
2911
|
var DATA_DIR = ".algolia-wizard/data";
|
|
2733
2912
|
var RECORD_MODEL = "claude-haiku-4-5";
|
|
2734
2913
|
var MAX_RECORDS = 100;
|
|
@@ -2740,17 +2919,17 @@ var anthropic = createAnthropic({
|
|
|
2740
2919
|
function generateRecordTool(ctx) {
|
|
2741
2920
|
return tool9({
|
|
2742
2921
|
description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
|
|
2743
|
-
inputSchema:
|
|
2744
|
-
entityName:
|
|
2745
|
-
attributes:
|
|
2746
|
-
count:
|
|
2747
|
-
hint:
|
|
2922
|
+
inputSchema: z16.object({
|
|
2923
|
+
entityName: z16.string().describe("Name of the entity to generate records for."),
|
|
2924
|
+
attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
|
|
2925
|
+
count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
|
|
2926
|
+
hint: z16.string().optional().describe("Optional context to steer realistic values.")
|
|
2748
2927
|
}),
|
|
2749
2928
|
execute: async ({ entityName, attributes, count, hint }) => {
|
|
2750
2929
|
logger.info({ entityName, count }, "called generateRecord tool");
|
|
2751
2930
|
try {
|
|
2752
|
-
const value =
|
|
2753
|
-
const recordSchema =
|
|
2931
|
+
const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
|
|
2932
|
+
const recordSchema = z16.object(
|
|
2754
2933
|
Object.fromEntries(attributes.map((attr) => [attr, value]))
|
|
2755
2934
|
);
|
|
2756
2935
|
const generateBatch = async (batchCount) => {
|
|
@@ -2760,8 +2939,8 @@ function generateRecordTool(ctx) {
|
|
|
2760
2939
|
const { output } = await generateText({
|
|
2761
2940
|
model: anthropic(RECORD_MODEL),
|
|
2762
2941
|
output: Output.object({
|
|
2763
|
-
schema:
|
|
2764
|
-
records:
|
|
2942
|
+
schema: z16.object({
|
|
2943
|
+
records: z16.array(recordSchema).length(batchCount)
|
|
2765
2944
|
})
|
|
2766
2945
|
}),
|
|
2767
2946
|
prompt: [
|
|
@@ -2797,13 +2976,13 @@ function generateRecordTool(ctx) {
|
|
|
2797
2976
|
}));
|
|
2798
2977
|
const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
2799
2978
|
const relPath = `${DATA_DIR}/${slug}.json`;
|
|
2800
|
-
const
|
|
2801
|
-
if (
|
|
2802
|
-
if (await hasSymlinkParent(ctx,
|
|
2803
|
-
return `Refused: ${
|
|
2979
|
+
const resolved2 = resolveInRoot(ctx, relPath);
|
|
2980
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2981
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2982
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2804
2983
|
}
|
|
2805
|
-
await mkdir5(dirname6(
|
|
2806
|
-
await writeFile5(
|
|
2984
|
+
await mkdir5(dirname6(resolved2.target), { recursive: true });
|
|
2985
|
+
await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
|
|
2807
2986
|
logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
|
|
2808
2987
|
return {
|
|
2809
2988
|
filePath: relPath,
|
|
@@ -2819,12 +2998,12 @@ function generateRecordTool(ctx) {
|
|
|
2819
2998
|
|
|
2820
2999
|
// src/lib/tools/notifyUser.ts
|
|
2821
3000
|
import { tool as tool10 } from "ai";
|
|
2822
|
-
import
|
|
3001
|
+
import z17 from "zod";
|
|
2823
3002
|
function notifyUserTool() {
|
|
2824
3003
|
return tool10({
|
|
2825
3004
|
description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work.`,
|
|
2826
|
-
inputSchema:
|
|
2827
|
-
message:
|
|
3005
|
+
inputSchema: z17.object({
|
|
3006
|
+
message: z17.string().describe(
|
|
2828
3007
|
"Short, plain-language description of what you are doing now."
|
|
2829
3008
|
)
|
|
2830
3009
|
}),
|
|
@@ -3004,10 +3183,10 @@ async function runAgent(req) {
|
|
|
3004
3183
|
}
|
|
3005
3184
|
|
|
3006
3185
|
// src/actions/detectLanguage.ts
|
|
3007
|
-
import
|
|
3008
|
-
var detectLanguageSchema =
|
|
3009
|
-
languages:
|
|
3010
|
-
frameworks:
|
|
3186
|
+
import z20 from "zod";
|
|
3187
|
+
var detectLanguageSchema = z20.object({
|
|
3188
|
+
languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
|
|
3189
|
+
frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
|
|
3011
3190
|
});
|
|
3012
3191
|
var detectLanguage = () => runAgent({
|
|
3013
3192
|
instructions: [
|
|
@@ -3025,31 +3204,31 @@ var detectLanguage = () => runAgent({
|
|
|
3025
3204
|
});
|
|
3026
3205
|
|
|
3027
3206
|
// src/actions/analyzeCodebase.ts
|
|
3028
|
-
import
|
|
3207
|
+
import z21 from "zod";
|
|
3029
3208
|
var READONLY_TOOLS = [
|
|
3030
3209
|
"listFiles",
|
|
3031
3210
|
"changeDirectory",
|
|
3032
3211
|
"readFile",
|
|
3033
3212
|
"searchFiles"
|
|
3034
3213
|
];
|
|
3035
|
-
var ingestionAnalysisSchema =
|
|
3036
|
-
ingestionAnalysis:
|
|
3037
|
-
|
|
3038
|
-
name:
|
|
3039
|
-
paths:
|
|
3214
|
+
var ingestionAnalysisSchema = z21.object({
|
|
3215
|
+
ingestionAnalysis: z21.array(
|
|
3216
|
+
z21.object({
|
|
3217
|
+
name: z21.string(),
|
|
3218
|
+
paths: z21.array(z21.string()),
|
|
3040
3219
|
// indexable fields the agent found for this entity
|
|
3041
|
-
attributes:
|
|
3220
|
+
attributes: z21.array(z21.string())
|
|
3042
3221
|
})
|
|
3043
3222
|
)
|
|
3044
3223
|
});
|
|
3045
|
-
var searchImplementationAnalysisSchema =
|
|
3046
|
-
searchImplementationAnalysis:
|
|
3224
|
+
var searchImplementationAnalysisSchema = z21.object({
|
|
3225
|
+
searchImplementationAnalysis: z21.string()
|
|
3047
3226
|
});
|
|
3048
|
-
var verificationSchema =
|
|
3049
|
-
verification:
|
|
3227
|
+
var verificationSchema = z21.object({
|
|
3228
|
+
verification: z21.array(z21.string())
|
|
3050
3229
|
});
|
|
3051
3230
|
var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
|
|
3052
|
-
var analyzeCodebaseSchema =
|
|
3231
|
+
var analyzeCodebaseSchema = z21.object({
|
|
3053
3232
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
3054
3233
|
searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
|
|
3055
3234
|
verification: verificationSchema.shape.verification.optional(),
|
|
@@ -3111,7 +3290,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3111
3290
|
// package.json
|
|
3112
3291
|
var package_default = {
|
|
3113
3292
|
name: "@algolia/wizard",
|
|
3114
|
-
version: "0.
|
|
3293
|
+
version: "0.12.0-rc.104.151",
|
|
3115
3294
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3116
3295
|
type: "module",
|
|
3117
3296
|
engines: {
|
|
@@ -3162,6 +3341,7 @@ var package_default = {
|
|
|
3162
3341
|
"@hono/node-server": "^2.0.10",
|
|
3163
3342
|
"@segment/analytics-node": "^3.1.0",
|
|
3164
3343
|
ai: "^6.0.190",
|
|
3344
|
+
"cross-keychain": "^1.1.0",
|
|
3165
3345
|
dotenv: "^17.4.2",
|
|
3166
3346
|
hono: "^4.12.27",
|
|
3167
3347
|
ink: "^7.0.5",
|
|
@@ -3229,8 +3409,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
|
|
|
3229
3409
|
}
|
|
3230
3410
|
|
|
3231
3411
|
// src/actions/confirmLanguage.ts
|
|
3232
|
-
import
|
|
3233
|
-
var confirmLanguageSchema =
|
|
3412
|
+
import z23 from "zod";
|
|
3413
|
+
var confirmLanguageSchema = z23.object({
|
|
3234
3414
|
languages: detectLanguageSchema.shape.languages
|
|
3235
3415
|
});
|
|
3236
3416
|
async function confirmLanguage(ctx) {
|
|
@@ -3251,8 +3431,8 @@ async function confirmLanguage(ctx) {
|
|
|
3251
3431
|
}
|
|
3252
3432
|
|
|
3253
3433
|
// src/actions/confirmFramework.ts
|
|
3254
|
-
import
|
|
3255
|
-
var confirmFrameworkSchema =
|
|
3434
|
+
import z24 from "zod";
|
|
3435
|
+
var confirmFrameworkSchema = z24.object({
|
|
3256
3436
|
frameworks: detectLanguageSchema.shape.frameworks
|
|
3257
3437
|
});
|
|
3258
3438
|
var CURATED_FRAMEWORKS = [
|
|
@@ -3380,8 +3560,8 @@ async function promptUser(ctx, params) {
|
|
|
3380
3560
|
}
|
|
3381
3561
|
|
|
3382
3562
|
// src/actions/confirmEntities.ts
|
|
3383
|
-
import
|
|
3384
|
-
var confirmEntitiesSchema =
|
|
3563
|
+
import z25 from "zod";
|
|
3564
|
+
var confirmEntitiesSchema = z25.object({
|
|
3385
3565
|
// Final detection — the focused re-run may supersede project-scan's.
|
|
3386
3566
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
3387
3567
|
confirmedEntities: confirmedEntitiesFieldSchema
|
|
@@ -3451,15 +3631,15 @@ async function confirmEntities(ctx) {
|
|
|
3451
3631
|
}
|
|
3452
3632
|
|
|
3453
3633
|
// src/actions/review.ts
|
|
3454
|
-
import { z as
|
|
3455
|
-
var reviewSchema =
|
|
3634
|
+
import { z as z26 } from "zod";
|
|
3635
|
+
var reviewSchema = z26.object({
|
|
3456
3636
|
// Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
|
|
3457
3637
|
// not one entry per workflow step — a step's raw output can be a long,
|
|
3458
3638
|
// multi-paragraph blob (see implement.ts's summaries.join), and mirroring
|
|
3459
3639
|
// that 1:1 is what made the old per-step summary an unreadable wall of text.
|
|
3460
|
-
summaryPoints:
|
|
3461
|
-
reviewPrompt:
|
|
3462
|
-
nextSteps:
|
|
3640
|
+
summaryPoints: z26.array(z26.string()),
|
|
3641
|
+
reviewPrompt: z26.string(),
|
|
3642
|
+
nextSteps: z26.array(z26.string())
|
|
3463
3643
|
});
|
|
3464
3644
|
function formatCompletedSteps(steps) {
|
|
3465
3645
|
if (!steps.length) return "(no prior steps completed)";
|
|
@@ -3510,16 +3690,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
3510
3690
|
};
|
|
3511
3691
|
|
|
3512
3692
|
// src/actions/implement.ts
|
|
3513
|
-
import
|
|
3693
|
+
import z27 from "zod";
|
|
3514
3694
|
|
|
3515
3695
|
// src/lib/worktree.ts
|
|
3516
3696
|
import { execFile, spawn as spawn3 } from "node:child_process";
|
|
3517
|
-
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as
|
|
3697
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
3518
3698
|
import {
|
|
3519
3699
|
basename as basename2,
|
|
3520
3700
|
dirname as dirname7,
|
|
3521
3701
|
isAbsolute as isAbsolute2,
|
|
3522
|
-
join as
|
|
3702
|
+
join as join10,
|
|
3523
3703
|
relative as relative2,
|
|
3524
3704
|
resolve as resolve3
|
|
3525
3705
|
} from "node:path";
|
|
@@ -3553,7 +3733,7 @@ async function isWorkingTreeDirty(repoRoot) {
|
|
|
3553
3733
|
return out.trim().length > 0;
|
|
3554
3734
|
}
|
|
3555
3735
|
async function pruneOldWorktrees(repoRoot) {
|
|
3556
|
-
const dir =
|
|
3736
|
+
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
3557
3737
|
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
3558
3738
|
for (const slug of stale) {
|
|
3559
3739
|
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
@@ -3564,7 +3744,7 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
3564
3744
|
"worktree",
|
|
3565
3745
|
"remove",
|
|
3566
3746
|
"--force",
|
|
3567
|
-
|
|
3747
|
+
join10(dir, slug)
|
|
3568
3748
|
]);
|
|
3569
3749
|
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
3570
3750
|
} catch (err) {
|
|
@@ -3578,7 +3758,7 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
3578
3758
|
async function createWorktree(repoRoot) {
|
|
3579
3759
|
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
3580
3760
|
const dirSlug = branch.replace(/\//g, "-");
|
|
3581
|
-
const path =
|
|
3761
|
+
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
3582
3762
|
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
3583
3763
|
await pruneOldWorktrees(repoRoot);
|
|
3584
3764
|
await mkdir6(dirname7(path), { recursive: true });
|
|
@@ -3698,8 +3878,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
3698
3878
|
} catch {
|
|
3699
3879
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
3700
3880
|
}
|
|
3701
|
-
const relPath =
|
|
3702
|
-
const dest =
|
|
3881
|
+
const relPath = join10(ingestDir, basename2(source));
|
|
3882
|
+
const dest = join10(worktreePath, relPath);
|
|
3703
3883
|
try {
|
|
3704
3884
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
3705
3885
|
await copyFile(source, dest);
|
|
@@ -3717,7 +3897,7 @@ function hasEnvVar(content, name) {
|
|
|
3717
3897
|
async function readEnvVar(worktreePath, name) {
|
|
3718
3898
|
let content;
|
|
3719
3899
|
try {
|
|
3720
|
-
content = await
|
|
3900
|
+
content = await readFile8(join10(worktreePath, ".env"), "utf8");
|
|
3721
3901
|
} catch (err) {
|
|
3722
3902
|
if (err.code !== "ENOENT") throw err;
|
|
3723
3903
|
return void 0;
|
|
@@ -3732,10 +3912,10 @@ async function readEnvVar(worktreePath, name) {
|
|
|
3732
3912
|
return value;
|
|
3733
3913
|
}
|
|
3734
3914
|
async function writeSearchEnvValues(worktreePath, vars) {
|
|
3735
|
-
const target =
|
|
3915
|
+
const target = join10(worktreePath, ".env");
|
|
3736
3916
|
let existing = "";
|
|
3737
3917
|
try {
|
|
3738
|
-
existing = await
|
|
3918
|
+
existing = await readFile8(target, "utf8");
|
|
3739
3919
|
} catch (err) {
|
|
3740
3920
|
if (err.code !== "ENOENT") throw err;
|
|
3741
3921
|
}
|
|
@@ -3805,13 +3985,13 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
|
3805
3985
|
|
|
3806
3986
|
// src/lib/algoliaDocs.ts
|
|
3807
3987
|
import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
|
|
3808
|
-
import { dirname as dirname8, join as
|
|
3988
|
+
import { dirname as dirname8, join as join11 } from "node:path";
|
|
3809
3989
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3810
|
-
var DOCS_SUBPATH =
|
|
3990
|
+
var DOCS_SUBPATH = join11("docs", "algolia-sdk");
|
|
3811
3991
|
function findDocsDir() {
|
|
3812
3992
|
let dir = dirname8(fileURLToPath2(import.meta.url));
|
|
3813
3993
|
for (; ; ) {
|
|
3814
|
-
const candidate =
|
|
3994
|
+
const candidate = join11(dir, DOCS_SUBPATH);
|
|
3815
3995
|
if (existsSync2(candidate)) return candidate;
|
|
3816
3996
|
const parent = dirname8(dir);
|
|
3817
3997
|
if (parent === dir) return void 0;
|
|
@@ -3834,7 +4014,7 @@ function loadAlgoliaDoc(language) {
|
|
|
3834
4014
|
);
|
|
3835
4015
|
return "";
|
|
3836
4016
|
}
|
|
3837
|
-
return readFileSync(
|
|
4017
|
+
return readFileSync(join11(docsDir, files[0]), "utf8").trim();
|
|
3838
4018
|
}
|
|
3839
4019
|
function getNamedDoc(name, language) {
|
|
3840
4020
|
const docsDir = findDocsDir();
|
|
@@ -3842,7 +4022,7 @@ function getNamedDoc(name, language) {
|
|
|
3842
4022
|
logger.warn("docs/algolia-sdk not found");
|
|
3843
4023
|
return "";
|
|
3844
4024
|
}
|
|
3845
|
-
const file =
|
|
4025
|
+
const file = join11(docsDir, `${name}-${language}.md`);
|
|
3846
4026
|
if (!existsSync2(file)) {
|
|
3847
4027
|
logger.warn({ name, language }, "named SDK reference not found");
|
|
3848
4028
|
return "";
|
|
@@ -3869,34 +4049,34 @@ function shellQuote(value) {
|
|
|
3869
4049
|
}
|
|
3870
4050
|
|
|
3871
4051
|
// src/actions/implement.ts
|
|
3872
|
-
var implementSchema =
|
|
3873
|
-
filesChanged:
|
|
3874
|
-
summary:
|
|
3875
|
-
worktreePath:
|
|
3876
|
-
ingestCommand:
|
|
3877
|
-
ingestScriptRan:
|
|
3878
|
-
ingestRecordCount:
|
|
3879
|
-
ingestDurationMs:
|
|
3880
|
-
ingestionSource:
|
|
3881
|
-
searchEnvVars:
|
|
3882
|
-
|
|
3883
|
-
name:
|
|
3884
|
-
value:
|
|
4052
|
+
var implementSchema = z27.object({
|
|
4053
|
+
filesChanged: z27.array(z27.string()),
|
|
4054
|
+
summary: z27.string(),
|
|
4055
|
+
worktreePath: z27.string().optional(),
|
|
4056
|
+
ingestCommand: z27.string().optional(),
|
|
4057
|
+
ingestScriptRan: z27.boolean().optional(),
|
|
4058
|
+
ingestRecordCount: z27.number().optional(),
|
|
4059
|
+
ingestDurationMs: z27.number().optional(),
|
|
4060
|
+
ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
|
|
4061
|
+
searchEnvVars: z27.array(
|
|
4062
|
+
z27.object({
|
|
4063
|
+
name: z27.string(),
|
|
4064
|
+
value: z27.string()
|
|
3885
4065
|
})
|
|
3886
4066
|
).optional()
|
|
3887
4067
|
});
|
|
3888
|
-
var implementationOutputSchema =
|
|
3889
|
-
summary:
|
|
4068
|
+
var implementationOutputSchema = z27.object({
|
|
4069
|
+
summary: z27.string(),
|
|
3890
4070
|
// Ingestion only: a structured pair the wizard turns into an argv, never a
|
|
3891
4071
|
// free-form command string. `runtime` is allowlisted and `entrypoint` is
|
|
3892
4072
|
// validated worktree-relative, so the agent cannot inject extra commands.
|
|
3893
|
-
runtime:
|
|
3894
|
-
entrypoint:
|
|
4073
|
+
runtime: z27.enum(INGEST_RUNTIMES).optional(),
|
|
4074
|
+
entrypoint: z27.string().optional()
|
|
3895
4075
|
});
|
|
3896
|
-
var verificationOutputSchema =
|
|
3897
|
-
summary:
|
|
3898
|
-
sufficient:
|
|
3899
|
-
additionalInstructions:
|
|
4076
|
+
var verificationOutputSchema = z27.object({
|
|
4077
|
+
summary: z27.string(),
|
|
4078
|
+
sufficient: z27.boolean(),
|
|
4079
|
+
additionalInstructions: z27.string().optional()
|
|
3900
4080
|
});
|
|
3901
4081
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
3902
4082
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
@@ -3941,13 +4121,17 @@ function publicEnvPrefix(language) {
|
|
|
3941
4121
|
}
|
|
3942
4122
|
var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
|
|
3943
4123
|
var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
|
|
4124
|
+
var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
3944
4125
|
function appIdVar(language) {
|
|
3945
4126
|
return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
|
|
3946
4127
|
}
|
|
3947
4128
|
function searchKeyVar(language) {
|
|
3948
4129
|
return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
|
|
3949
4130
|
}
|
|
3950
|
-
function
|
|
4131
|
+
function searchIndexVar(language) {
|
|
4132
|
+
return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
|
|
4133
|
+
}
|
|
4134
|
+
function searchEnvVars(language, index, appId, searchKey) {
|
|
3951
4135
|
return [
|
|
3952
4136
|
{
|
|
3953
4137
|
name: appIdVar(language),
|
|
@@ -3956,12 +4140,21 @@ function searchEnvVars(language, appId, searchKey) {
|
|
|
3956
4140
|
{
|
|
3957
4141
|
name: searchKeyVar(language),
|
|
3958
4142
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4143
|
+
},
|
|
4144
|
+
// Wizard-supplied rather than written into the generated code, because an
|
|
4145
|
+
// agent that retypes the name (appending the project name, re-casing it)
|
|
4146
|
+
// leaves the UI querying an index that does not exist.
|
|
4147
|
+
{
|
|
4148
|
+
name: searchIndexVar(language),
|
|
4149
|
+
value: index
|
|
3959
4150
|
}
|
|
3960
4151
|
];
|
|
3961
4152
|
}
|
|
3962
4153
|
function baseInstructions(input) {
|
|
3963
4154
|
return [
|
|
3964
|
-
|
|
4155
|
+
// Agents have renamed this (e.g. appending the project name), which the
|
|
4156
|
+
// index-scoped keys then reject with a 403.
|
|
4157
|
+
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
3965
4158
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
3966
4159
|
"Make minimal, idiomatic changes; do not touch unrelated code."
|
|
3967
4160
|
];
|
|
@@ -3995,6 +4188,7 @@ function ingestionInstructions(input) {
|
|
|
3995
4188
|
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
3996
4189
|
`Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
3997
4190
|
`Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
|
|
4191
|
+
`Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
|
|
3998
4192
|
"Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
|
|
3999
4193
|
"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.",
|
|
4000
4194
|
getNamedDoc("save-records", "js"),
|
|
@@ -4012,7 +4206,8 @@ function searchInstructions(input) {
|
|
|
4012
4206
|
`Build the search UI for ${input.uiFramework}.`,
|
|
4013
4207
|
"Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
|
|
4014
4208
|
doc,
|
|
4015
|
-
`Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the
|
|
4209
|
+
`Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the target index.`,
|
|
4210
|
+
`Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
|
|
4016
4211
|
// The key is provisioned only after verification passes, so the agent never
|
|
4017
4212
|
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4018
4213
|
// decide whether a key already exists, and an agent-invented value there
|
|
@@ -4022,7 +4217,7 @@ function searchInstructions(input) {
|
|
|
4022
4217
|
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4023
4218
|
// reading a var the wizard never wrote.
|
|
4024
4219
|
`Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4025
|
-
"Read the App ID
|
|
4220
|
+
"Read the App ID, the search-only API key, and the index name from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
|
|
4026
4221
|
'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
|
|
4027
4222
|
"The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
|
|
4028
4223
|
];
|
|
@@ -4206,9 +4401,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4206
4401
|
targetIndex,
|
|
4207
4402
|
language,
|
|
4208
4403
|
appId,
|
|
4209
|
-
|
|
4210
|
-
// every value here is still a placeholder when the agent reads them.
|
|
4211
|
-
searchEnvVars: searchEnvVars(language, appId),
|
|
4404
|
+
searchEnvVars: searchEnvVars(language, targetIndex, appId),
|
|
4212
4405
|
ingestDir: INGEST_DIR,
|
|
4213
4406
|
ingestionSource,
|
|
4214
4407
|
uploadFilePath,
|
|
@@ -4295,7 +4488,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4295
4488
|
}) === true;
|
|
4296
4489
|
if (runNow) {
|
|
4297
4490
|
const ingestApp = await requireApplication();
|
|
4298
|
-
const writeKey = await resolveWriteKey(targetIndex);
|
|
4491
|
+
const writeKey = (await resolveWriteKey(targetIndex, ingestApp.id)).key;
|
|
4299
4492
|
ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
|
|
4300
4493
|
const scriptLogId = ctx.logStart("runIngestScript", {
|
|
4301
4494
|
runtime: ingestRuntime,
|
|
@@ -4308,7 +4501,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4308
4501
|
ingestEntrypoint,
|
|
4309
4502
|
{
|
|
4310
4503
|
[APP_ID_VAR]: ingestApp.id,
|
|
4311
|
-
[API_KEY_VAR]: writeKey
|
|
4504
|
+
[API_KEY_VAR]: writeKey,
|
|
4505
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4312
4506
|
}
|
|
4313
4507
|
);
|
|
4314
4508
|
ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
|
|
@@ -4429,14 +4623,14 @@ ${run2.output}` : status;
|
|
|
4429
4623
|
let searchKeyError;
|
|
4430
4624
|
if (appId) {
|
|
4431
4625
|
try {
|
|
4432
|
-
const
|
|
4626
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4433
4627
|
targetIndex,
|
|
4434
4628
|
appId,
|
|
4435
4629
|
envSearchKey
|
|
4436
4630
|
);
|
|
4437
|
-
searchKey =
|
|
4631
|
+
searchKey = resolved2.key;
|
|
4438
4632
|
summaries.push(
|
|
4439
|
-
|
|
4633
|
+
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}.`
|
|
4440
4634
|
);
|
|
4441
4635
|
} catch (err) {
|
|
4442
4636
|
searchKeyError = err.message;
|
|
@@ -4446,7 +4640,12 @@ ${run2.output}` : status;
|
|
|
4446
4640
|
);
|
|
4447
4641
|
}
|
|
4448
4642
|
}
|
|
4449
|
-
finalSearchEnvVars = searchEnvVars(
|
|
4643
|
+
finalSearchEnvVars = searchEnvVars(
|
|
4644
|
+
language,
|
|
4645
|
+
targetIndex,
|
|
4646
|
+
appId,
|
|
4647
|
+
searchKey
|
|
4648
|
+
);
|
|
4450
4649
|
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4451
4650
|
(v) => !v.value.startsWith("<")
|
|
4452
4651
|
);
|
|
@@ -4555,8 +4754,8 @@ var defaultWorkflow = {
|
|
|
4555
4754
|
defineStep({
|
|
4556
4755
|
id: "select-index",
|
|
4557
4756
|
title: "Set up index",
|
|
4558
|
-
outputSchema:
|
|
4559
|
-
selection:
|
|
4757
|
+
outputSchema: z28.object({
|
|
4758
|
+
selection: z28.string()
|
|
4560
4759
|
}),
|
|
4561
4760
|
run: (ctx) => selectIndexStep(ctx)
|
|
4562
4761
|
}),
|
|
@@ -4798,7 +4997,11 @@ Options:
|
|
|
4798
4997
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
4799
4998
|
--reset-on-run Wipe this project's wizard state (run state, AI consent,
|
|
4800
4999
|
worktrees) before starting, so the run behaves like a
|
|
4801
|
-
first-ever run.
|
|
5000
|
+
first-ever run. Also drops every API key the wizard has
|
|
5001
|
+
stored in your keychain (or, where the platform has none,
|
|
5002
|
+
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5003
|
+
for this project and any other, so later runs create new
|
|
5004
|
+
ones. Your Algolia login is not touched.
|
|
4802
5005
|
-h, --help Print this message.`;
|
|
4803
5006
|
function parseCliArgs(argv) {
|
|
4804
5007
|
const positionals = [];
|
|
@@ -4835,10 +5038,11 @@ function parseCliArgs(argv) {
|
|
|
4835
5038
|
|
|
4836
5039
|
// src/lib/resetState.ts
|
|
4837
5040
|
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
4838
|
-
import { join as
|
|
5041
|
+
import { join as join12 } from "node:path";
|
|
4839
5042
|
var KEEP = ["wizard.log"];
|
|
4840
5043
|
async function resetProjectState() {
|
|
4841
5044
|
const dir = stateDir();
|
|
5045
|
+
await forgetResolvedKeys();
|
|
4842
5046
|
let entries;
|
|
4843
5047
|
try {
|
|
4844
5048
|
entries = await readdir4(dir);
|
|
@@ -4847,7 +5051,9 @@ async function resetProjectState() {
|
|
|
4847
5051
|
}
|
|
4848
5052
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
4849
5053
|
await Promise.all(
|
|
4850
|
-
targets.map(
|
|
5054
|
+
targets.map(
|
|
5055
|
+
(name) => rm2(join12(dir, name), { recursive: true, force: true })
|
|
5056
|
+
)
|
|
4851
5057
|
);
|
|
4852
5058
|
return { dir, removed: targets };
|
|
4853
5059
|
}
|
|
@@ -4855,6 +5061,7 @@ async function resetProjectState() {
|
|
|
4855
5061
|
// src/main.tsx
|
|
4856
5062
|
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
4857
5063
|
async function startup() {
|
|
5064
|
+
setProjectRoot(process.cwd());
|
|
4858
5065
|
let args;
|
|
4859
5066
|
try {
|
|
4860
5067
|
args = parseCliArgs(process.argv.slice(2));
|