@algolia/wizard 0.10.0 → 0.11.0
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 +490 -288
- 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
|
|
|
@@ -1743,18 +1751,17 @@ import "zod";
|
|
|
1743
1751
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1744
1752
|
import { join as join5 } from "node:path";
|
|
1745
1753
|
var configFile = () => join5(stateDir(), "config.json");
|
|
1746
|
-
var
|
|
1754
|
+
var defaultConfig = () => ({
|
|
1747
1755
|
version: 1,
|
|
1748
1756
|
aiConsent: false,
|
|
1749
|
-
workflowsRun: []
|
|
1750
|
-
|
|
1751
|
-
};
|
|
1757
|
+
workflowsRun: []
|
|
1758
|
+
});
|
|
1752
1759
|
async function loadConfig() {
|
|
1753
1760
|
try {
|
|
1754
1761
|
const raw = await readFile2(configFile(), "utf8");
|
|
1755
|
-
return { ...
|
|
1762
|
+
return { ...defaultConfig(), ...JSON.parse(raw) };
|
|
1756
1763
|
} catch {
|
|
1757
|
-
return
|
|
1764
|
+
return defaultConfig();
|
|
1758
1765
|
}
|
|
1759
1766
|
}
|
|
1760
1767
|
async function saveConfig(config) {
|
|
@@ -1766,38 +1773,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
|
|
|
1766
1773
|
config.workflowsRun.push({ workflowId, completedAt });
|
|
1767
1774
|
await saveConfig(config);
|
|
1768
1775
|
}
|
|
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
1776
|
|
|
1802
1777
|
// src/core/orchestrator.ts
|
|
1803
1778
|
function defineStep(step) {
|
|
@@ -2063,6 +2038,42 @@ function parseJson(text) {
|
|
|
2063
2038
|
}
|
|
2064
2039
|
}
|
|
2065
2040
|
|
|
2041
|
+
// src/lib/envAppId.ts
|
|
2042
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2043
|
+
import { join as join6 } from "node:path";
|
|
2044
|
+
var ENV_FILES = [".env", ".env.local"];
|
|
2045
|
+
var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
|
|
2046
|
+
async function findEnvApplicationId(root = process.cwd()) {
|
|
2047
|
+
for (const file of ENV_FILES) {
|
|
2048
|
+
let content;
|
|
2049
|
+
try {
|
|
2050
|
+
content = await readFile3(join6(root, file), "utf8");
|
|
2051
|
+
} catch (err) {
|
|
2052
|
+
if (err.code !== "ENOENT") {
|
|
2053
|
+
logger.warn(
|
|
2054
|
+
{ file, err },
|
|
2055
|
+
"could not read env file for an application id"
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
|
|
2061
|
+
const id = readValue(raw);
|
|
2062
|
+
if (id) {
|
|
2063
|
+
logger.info({ file, name, app: id }, "found an application id in env");
|
|
2064
|
+
return { id, name, file };
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return null;
|
|
2069
|
+
}
|
|
2070
|
+
function readValue(raw) {
|
|
2071
|
+
const trimmed = raw.trim();
|
|
2072
|
+
const quoted = trimmed.match(/^(['"])(.*)\1/);
|
|
2073
|
+
const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
|
|
2074
|
+
return value.length > 0 && !value.startsWith("<") ? value : null;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2066
2077
|
// src/lib/algoliaAppPicker.ts
|
|
2067
2078
|
function secondaryFor(app) {
|
|
2068
2079
|
return app.plan ? { kind: "badge", value: app.plan } : void 0;
|
|
@@ -2077,7 +2088,7 @@ function selectAndReport(app) {
|
|
|
2077
2088
|
);
|
|
2078
2089
|
return selectApplication(app.id);
|
|
2079
2090
|
}
|
|
2080
|
-
async function promptForApplication() {
|
|
2091
|
+
async function promptForApplication(leadIn = []) {
|
|
2081
2092
|
const store = useWizard.getState();
|
|
2082
2093
|
const apps = await listApplications();
|
|
2083
2094
|
if (apps.length === 0) {
|
|
@@ -2091,9 +2102,13 @@ async function promptForApplication() {
|
|
|
2091
2102
|
{ app: only.id },
|
|
2092
2103
|
"single application on the account; selecting it"
|
|
2093
2104
|
);
|
|
2105
|
+
for (const line of leadIn) store.pushCliOutput("stdout", line);
|
|
2094
2106
|
return selectAndReport(only);
|
|
2095
2107
|
}
|
|
2096
|
-
const messages = [
|
|
2108
|
+
const messages = [
|
|
2109
|
+
...leadIn,
|
|
2110
|
+
"Which Algolia application should the wizard work in?"
|
|
2111
|
+
];
|
|
2097
2112
|
for (; ; ) {
|
|
2098
2113
|
const choice = await store.requestUserInput({
|
|
2099
2114
|
prompt: "Select an application",
|
|
@@ -2119,12 +2134,46 @@ async function promptForApplication() {
|
|
|
2119
2134
|
}
|
|
2120
2135
|
}
|
|
2121
2136
|
}
|
|
2137
|
+
async function confirmEnvApplication(env, current) {
|
|
2138
|
+
const useEnv = `Use ${env.id} (from ${env.file})`;
|
|
2139
|
+
const choice = await useWizard.getState().requestUserInput({
|
|
2140
|
+
prompt: "Select an application",
|
|
2141
|
+
promptType: "multipleChoice",
|
|
2142
|
+
options: [
|
|
2143
|
+
useEnv,
|
|
2144
|
+
current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
|
|
2145
|
+
],
|
|
2146
|
+
messages: [
|
|
2147
|
+
`${env.file} already sets ${env.name}=${env.id}.`,
|
|
2148
|
+
"Which Algolia application should the wizard work in?"
|
|
2149
|
+
]
|
|
2150
|
+
});
|
|
2151
|
+
return choice === useEnv;
|
|
2152
|
+
}
|
|
2153
|
+
async function selectEnvApplication(env) {
|
|
2154
|
+
try {
|
|
2155
|
+
return await selectAndReport({ id: env.id, name: "" });
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
logger.warn(
|
|
2158
|
+
{ app: env.id, err: err.message },
|
|
2159
|
+
"could not select the application named in env; falling back to the picker"
|
|
2160
|
+
);
|
|
2161
|
+
return promptForApplication([
|
|
2162
|
+
`Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
|
|
2163
|
+
]);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2122
2166
|
async function ensureApplication() {
|
|
2123
|
-
|
|
2167
|
+
const current = await currentApplication();
|
|
2168
|
+
const env = await findEnvApplicationId();
|
|
2169
|
+
if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
|
|
2170
|
+
return selectEnvApplication(env);
|
|
2171
|
+
}
|
|
2172
|
+
return current ?? await promptForApplication();
|
|
2124
2173
|
}
|
|
2125
2174
|
|
|
2126
2175
|
// src/workflows/default.ts
|
|
2127
|
-
import { z as
|
|
2176
|
+
import { z as z28 } from "zod";
|
|
2128
2177
|
|
|
2129
2178
|
// src/actions/listIndices.ts
|
|
2130
2179
|
import { z as z5 } from "zod";
|
|
@@ -2208,7 +2257,7 @@ import { readdir } from "node:fs/promises";
|
|
|
2208
2257
|
|
|
2209
2258
|
// src/lib/tools/path.ts
|
|
2210
2259
|
import { lstat } from "node:fs/promises";
|
|
2211
|
-
import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as
|
|
2260
|
+
import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
|
|
2212
2261
|
function resolveInRoot(ctx, path) {
|
|
2213
2262
|
const target = resolve2(ctx.cwd, path);
|
|
2214
2263
|
const rel = relative(ctx.root, target);
|
|
@@ -2224,7 +2273,7 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
2224
2273
|
let current = ctx.root;
|
|
2225
2274
|
const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
|
|
2226
2275
|
for (const part of parts) {
|
|
2227
|
-
current =
|
|
2276
|
+
current = join7(current, part);
|
|
2228
2277
|
try {
|
|
2229
2278
|
if ((await lstat(current)).isSymbolicLink()) return true;
|
|
2230
2279
|
} catch (err) {
|
|
@@ -2245,9 +2294,9 @@ function listFilesTool(ctx) {
|
|
|
2245
2294
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2246
2295
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
2247
2296
|
}
|
|
2248
|
-
const
|
|
2249
|
-
if (!
|
|
2250
|
-
const entries = await readdir(
|
|
2297
|
+
const resolved2 = resolveInRoot(ctx, ".");
|
|
2298
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2299
|
+
const entries = await readdir(resolved2.target, { withFileTypes: true });
|
|
2251
2300
|
return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
2252
2301
|
}
|
|
2253
2302
|
});
|
|
@@ -2265,14 +2314,14 @@ function changeDirectoryTool(ctx) {
|
|
|
2265
2314
|
}),
|
|
2266
2315
|
execute: async ({ path }) => {
|
|
2267
2316
|
logger.info({ path }, "called changeDirectory tool");
|
|
2268
|
-
const
|
|
2269
|
-
if (!
|
|
2317
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2318
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2270
2319
|
try {
|
|
2271
|
-
const info = await stat(
|
|
2320
|
+
const info = await stat(resolved2.target);
|
|
2272
2321
|
if (!info.isDirectory()) {
|
|
2273
2322
|
return `Error changing directory to ${path}: not a directory`;
|
|
2274
2323
|
}
|
|
2275
|
-
ctx.cwd =
|
|
2324
|
+
ctx.cwd = resolved2.target;
|
|
2276
2325
|
return `Changed working directory to ${ctx.cwd}`;
|
|
2277
2326
|
} catch (err) {
|
|
2278
2327
|
return `Error changing directory to ${path}: ${err.message}`;
|
|
@@ -2302,7 +2351,7 @@ function reportStatusTool(output) {
|
|
|
2302
2351
|
// src/lib/tools/readFile.ts
|
|
2303
2352
|
import { tool as tool4 } from "ai";
|
|
2304
2353
|
import z9 from "zod";
|
|
2305
|
-
import { readFile as
|
|
2354
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2306
2355
|
|
|
2307
2356
|
// src/lib/tools/env.ts
|
|
2308
2357
|
import { basename } from "node:path";
|
|
@@ -2337,11 +2386,11 @@ function readFileTool(ctx) {
|
|
|
2337
2386
|
return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
|
|
2338
2387
|
}
|
|
2339
2388
|
logger.info({ filePath }, "called readFile tool");
|
|
2340
|
-
const
|
|
2341
|
-
if (!
|
|
2389
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2390
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2342
2391
|
try {
|
|
2343
|
-
const content = await
|
|
2344
|
-
return isEnvFile(
|
|
2392
|
+
const content = await readFile4(resolved2.target, "utf8");
|
|
2393
|
+
return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
|
|
2345
2394
|
} catch (err) {
|
|
2346
2395
|
return `Error reading ${filePath}: ${err.message}`;
|
|
2347
2396
|
}
|
|
@@ -2363,17 +2412,17 @@ function writeFileTool(ctx) {
|
|
|
2363
2412
|
}),
|
|
2364
2413
|
execute: async ({ filePath, content }) => {
|
|
2365
2414
|
logger.info({ filePath }, "called writeFile tool");
|
|
2366
|
-
const
|
|
2367
|
-
if (
|
|
2368
|
-
if (isSecretEnvFile(
|
|
2415
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2416
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2417
|
+
if (isSecretEnvFile(resolved2.target)) {
|
|
2369
2418
|
return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
|
|
2370
2419
|
}
|
|
2371
2420
|
try {
|
|
2372
|
-
if (await hasSymlinkParent(ctx,
|
|
2373
|
-
return `Refused: ${
|
|
2421
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2422
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2374
2423
|
}
|
|
2375
|
-
await mkdir3(dirname4(
|
|
2376
|
-
await writeFile3(
|
|
2424
|
+
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2425
|
+
await writeFile3(resolved2.target, content, "utf8");
|
|
2377
2426
|
return `Wrote to ${filePath}`;
|
|
2378
2427
|
} catch (err) {
|
|
2379
2428
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -2384,12 +2433,93 @@ function writeFileTool(ctx) {
|
|
|
2384
2433
|
|
|
2385
2434
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2386
2435
|
import { tool as tool6 } from "ai";
|
|
2387
|
-
import
|
|
2388
|
-
import { mkdir as mkdir4, readFile as
|
|
2436
|
+
import z13 from "zod";
|
|
2437
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
2389
2438
|
import { dirname as dirname5 } from "node:path";
|
|
2390
2439
|
|
|
2391
2440
|
// src/lib/algoliaApiKey.ts
|
|
2441
|
+
import { z as z12 } from "zod";
|
|
2442
|
+
|
|
2443
|
+
// src/lib/keychain.ts
|
|
2444
|
+
import { deletePassword, getPassword, setPassword } from "cross-keychain";
|
|
2392
2445
|
import { z as z11 } from "zod";
|
|
2446
|
+
var SERVICE = "algolia-wizard";
|
|
2447
|
+
var ACCOUNT = "api-keys";
|
|
2448
|
+
var storedKeysSchema = z11.record(z11.string(), z11.string());
|
|
2449
|
+
function entryId(kind, index, appId) {
|
|
2450
|
+
return `${kind}:${appId}:${index}`;
|
|
2451
|
+
}
|
|
2452
|
+
async function loadKeys() {
|
|
2453
|
+
const raw = await getPassword(SERVICE, ACCOUNT);
|
|
2454
|
+
if (!raw) return {};
|
|
2455
|
+
let payload;
|
|
2456
|
+
try {
|
|
2457
|
+
payload = JSON.parse(raw);
|
|
2458
|
+
} catch {
|
|
2459
|
+
payload = null;
|
|
2460
|
+
}
|
|
2461
|
+
const keys = storedKeysSchema.safeParse(payload);
|
|
2462
|
+
if (!keys.success) {
|
|
2463
|
+
logger.warn("the stored API keys are unreadable; treating them as empty");
|
|
2464
|
+
return {};
|
|
2465
|
+
}
|
|
2466
|
+
return keys.data;
|
|
2467
|
+
}
|
|
2468
|
+
var queue = Promise.resolve();
|
|
2469
|
+
function serialized(op) {
|
|
2470
|
+
const next = queue.then(op);
|
|
2471
|
+
queue = next.catch(() => {
|
|
2472
|
+
});
|
|
2473
|
+
return next;
|
|
2474
|
+
}
|
|
2475
|
+
async function readStoredKey(kind, index, appId) {
|
|
2476
|
+
try {
|
|
2477
|
+
return (await loadKeys())[entryId(kind, index, appId)] ?? null;
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
logger.warn(
|
|
2480
|
+
{ err: err.message, kind, index, appId },
|
|
2481
|
+
"could not read the API key from the keychain"
|
|
2482
|
+
);
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
function storeKey(kind, index, appId, value) {
|
|
2487
|
+
return serialized(async () => {
|
|
2488
|
+
const id = entryId(kind, index, appId);
|
|
2489
|
+
try {
|
|
2490
|
+
const keys = await loadKeys();
|
|
2491
|
+
await setPassword(
|
|
2492
|
+
SERVICE,
|
|
2493
|
+
ACCOUNT,
|
|
2494
|
+
JSON.stringify({ ...keys, [id]: value })
|
|
2495
|
+
);
|
|
2496
|
+
if ((await loadKeys())[id] !== value) {
|
|
2497
|
+
throw new Error("the keychain did not store the value");
|
|
2498
|
+
}
|
|
2499
|
+
} catch (err) {
|
|
2500
|
+
logger.warn(
|
|
2501
|
+
{ err: err.message, kind, index, appId },
|
|
2502
|
+
"could not store the API key in the keychain; the next run will create another"
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
function deleteStoredKeys() {
|
|
2508
|
+
return serialized(async () => {
|
|
2509
|
+
try {
|
|
2510
|
+
await deletePassword(SERVICE, ACCOUNT);
|
|
2511
|
+
} catch (err) {
|
|
2512
|
+
const message = err.message;
|
|
2513
|
+
if (/not found/i.test(message)) return;
|
|
2514
|
+
logger.warn(
|
|
2515
|
+
{ err: message },
|
|
2516
|
+
"could not delete the API keys from the keychain"
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/lib/algoliaApiKey.ts
|
|
2393
2523
|
var WRITE_ACLS = [
|
|
2394
2524
|
"addObject",
|
|
2395
2525
|
"deleteObject",
|
|
@@ -2397,58 +2527,21 @@ var WRITE_ACLS = [
|
|
|
2397
2527
|
"editSettings",
|
|
2398
2528
|
"listIndexes"
|
|
2399
2529
|
];
|
|
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()
|
|
2530
|
+
var createdKeySchema = z12.object({
|
|
2531
|
+
key: z12.string().min(1).optional(),
|
|
2532
|
+
value: z12.string().min(1).optional()
|
|
2413
2533
|
}).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");
|
|
2534
|
+
async function createKey(index, acls, description) {
|
|
2535
|
+
logger.info({ index, acls }, "creating an API key");
|
|
2443
2536
|
const stdout = await runAlgoliaCli([
|
|
2444
2537
|
"apikeys",
|
|
2445
2538
|
"create",
|
|
2446
2539
|
"--acl",
|
|
2447
|
-
"
|
|
2540
|
+
acls.join(","),
|
|
2448
2541
|
"--indices",
|
|
2449
2542
|
index,
|
|
2450
2543
|
"--description",
|
|
2451
|
-
|
|
2544
|
+
description,
|
|
2452
2545
|
"-o",
|
|
2453
2546
|
"json"
|
|
2454
2547
|
]);
|
|
@@ -2462,49 +2555,72 @@ async function createSearchOnlyKey(index) {
|
|
|
2462
2555
|
if (!created) throw new Error("apikeys create returned no key value");
|
|
2463
2556
|
return created;
|
|
2464
2557
|
}
|
|
2465
|
-
async function
|
|
2558
|
+
async function keyExists(key) {
|
|
2466
2559
|
try {
|
|
2467
|
-
await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
|
|
2560
|
+
await runAlgoliaCli(["apikeys", "get", key, "-o", "json"], { redact: key });
|
|
2468
2561
|
return true;
|
|
2469
2562
|
} catch (err) {
|
|
2470
|
-
return !/does not exist
|
|
2563
|
+
return !/does not exist/i.test(err.message);
|
|
2471
2564
|
}
|
|
2472
2565
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2566
|
+
var resolved = /* @__PURE__ */ new Map();
|
|
2567
|
+
async function forgetResolvedKeys() {
|
|
2568
|
+
resolved.clear();
|
|
2569
|
+
await deleteStoredKeys();
|
|
2570
|
+
}
|
|
2571
|
+
function resolveKey(kind, index, appId, acls, description) {
|
|
2572
|
+
const cacheKey = `${kind}:${appId}:${index}`;
|
|
2573
|
+
const cached = resolved.get(cacheKey);
|
|
2574
|
+
if (cached) return cached;
|
|
2575
|
+
const pending = provisionKey(kind, index, appId, acls, description).catch(
|
|
2576
|
+
(err) => {
|
|
2577
|
+
resolved.delete(cacheKey);
|
|
2578
|
+
throw err;
|
|
2579
|
+
}
|
|
2580
|
+
);
|
|
2581
|
+
resolved.set(cacheKey, pending);
|
|
2582
|
+
return pending;
|
|
2583
|
+
}
|
|
2584
|
+
async function provisionKey(kind, index, appId, acls, description) {
|
|
2585
|
+
const stored = await readStoredKey(kind, index, appId);
|
|
2479
2586
|
if (stored) {
|
|
2480
|
-
if (await
|
|
2481
|
-
logger.info({ index, appId }, "reusing the stored
|
|
2482
|
-
return { key: stored, source: "
|
|
2587
|
+
if (await keyExists(stored)) {
|
|
2588
|
+
logger.info({ kind, index, appId }, "reusing the stored API key");
|
|
2589
|
+
return { key: stored, source: "keychain" };
|
|
2483
2590
|
}
|
|
2484
|
-
logger.
|
|
2485
|
-
{ index, appId },
|
|
2486
|
-
"the stored
|
|
2591
|
+
logger.info(
|
|
2592
|
+
{ kind, index, appId },
|
|
2593
|
+
"the stored API key no longer exists; creating another"
|
|
2487
2594
|
);
|
|
2488
|
-
await forgetSearchKey(index);
|
|
2489
2595
|
}
|
|
2490
|
-
const key = await
|
|
2491
|
-
await
|
|
2596
|
+
const key = await createKey(index, acls, description);
|
|
2597
|
+
await storeKey(kind, index, appId, key);
|
|
2492
2598
|
return { key, source: "created" };
|
|
2493
2599
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2600
|
+
function resolveWriteKey(index, appId) {
|
|
2601
|
+
return resolveKey(
|
|
2602
|
+
"write",
|
|
2603
|
+
index,
|
|
2604
|
+
appId,
|
|
2605
|
+
WRITE_ACLS,
|
|
2606
|
+
`Algolia Wizard write key for ${index} index`
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
async function resolveSearchOnlyKey(index, appId, envKey) {
|
|
2610
|
+
if (envKey) return { key: envKey, source: "env" };
|
|
2611
|
+
return resolveKey(
|
|
2612
|
+
"search",
|
|
2613
|
+
index,
|
|
2614
|
+
appId,
|
|
2615
|
+
["search"],
|
|
2616
|
+
`Algolia Wizard search-only key for ${index} index`
|
|
2617
|
+
);
|
|
2503
2618
|
}
|
|
2504
2619
|
|
|
2505
2620
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2506
2621
|
var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
|
|
2507
2622
|
var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
|
|
2623
|
+
var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
|
|
2508
2624
|
function appendEnv(content, entries) {
|
|
2509
2625
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2510
2626
|
const lines = entries.map(([name, value]) => `${name}=${value}
|
|
@@ -2514,53 +2630,111 @@ function appendEnv(content, entries) {
|
|
|
2514
2630
|
function hasEnv(content, name) {
|
|
2515
2631
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
2516
2632
|
}
|
|
2633
|
+
function readEnv(content, name) {
|
|
2634
|
+
const found = content.match(
|
|
2635
|
+
new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*(.*)$`, "m")
|
|
2636
|
+
);
|
|
2637
|
+
if (!found) return null;
|
|
2638
|
+
const raw = found[1].trim();
|
|
2639
|
+
const quoted = raw.match(/^(['"])(.*)\1/);
|
|
2640
|
+
const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, "");
|
|
2641
|
+
return value.length > 0 ? value : null;
|
|
2642
|
+
}
|
|
2643
|
+
function upsertEnv(content, name, value) {
|
|
2644
|
+
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
2645
|
+
return content.replace(
|
|
2646
|
+
new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "gm"),
|
|
2647
|
+
() => `${name}=${value}`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2517
2650
|
function writeCredentialsTool(ctx) {
|
|
2518
2651
|
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:
|
|
2652
|
+
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.`,
|
|
2653
|
+
inputSchema: z13.object({
|
|
2654
|
+
filePath: z13.string().describe(
|
|
2522
2655
|
'Path to the env file to write credentials into (e.g. ".env")'
|
|
2523
2656
|
)
|
|
2524
2657
|
}),
|
|
2525
2658
|
execute: async ({ filePath }) => {
|
|
2526
2659
|
logger.info({ filePath }, "called writeCredentials tool");
|
|
2527
|
-
const
|
|
2528
|
-
if (
|
|
2660
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2661
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2529
2662
|
const targetIndex = useWizard.getState().targetIndex;
|
|
2530
2663
|
if (!targetIndex) {
|
|
2531
2664
|
return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
|
|
2532
2665
|
}
|
|
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
|
-
}
|
|
2666
|
+
let existing = "";
|
|
2667
|
+
let present;
|
|
2541
2668
|
try {
|
|
2542
|
-
if (await hasSymlinkParent(ctx,
|
|
2543
|
-
return `Refused: ${
|
|
2669
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2670
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2544
2671
|
}
|
|
2545
|
-
let existing = "";
|
|
2546
2672
|
try {
|
|
2547
|
-
existing = await
|
|
2673
|
+
existing = await readFile5(resolved2.target, "utf8");
|
|
2548
2674
|
} catch (err) {
|
|
2549
2675
|
if (err.code !== "ENOENT") throw err;
|
|
2550
2676
|
}
|
|
2551
|
-
|
|
2552
|
-
(name) =>
|
|
2677
|
+
present = [APP_ID_VAR, API_KEY_VAR].filter(
|
|
2678
|
+
(name) => readEnv(existing, name) !== null
|
|
2553
2679
|
);
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
return `Error writing credentials to ${filePath}: ${err.message}`;
|
|
2682
|
+
}
|
|
2683
|
+
const credentials = [];
|
|
2684
|
+
const notes = [];
|
|
2685
|
+
const fileAppId = readEnv(existing, APP_ID_VAR);
|
|
2686
|
+
const fileKey = readEnv(existing, API_KEY_VAR);
|
|
2687
|
+
const fileIndex = readEnv(existing, INDEX_NAME_VAR);
|
|
2688
|
+
if (fileAppId === null || fileKey === null) {
|
|
2689
|
+
try {
|
|
2690
|
+
const selected = (await requireApplication()).id;
|
|
2691
|
+
if (fileAppId === null) credentials.push([APP_ID_VAR, selected]);
|
|
2692
|
+
if (fileKey === null) {
|
|
2693
|
+
if (fileAppId !== null && fileAppId !== selected) {
|
|
2694
|
+
notes.push(
|
|
2695
|
+
`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.`
|
|
2696
|
+
);
|
|
2697
|
+
} else {
|
|
2698
|
+
credentials.push([
|
|
2699
|
+
API_KEY_VAR,
|
|
2700
|
+
(await resolveWriteKey(targetIndex, selected)).key
|
|
2701
|
+
]);
|
|
2702
|
+
}
|
|
2703
|
+
} else {
|
|
2704
|
+
notes.push(
|
|
2705
|
+
`The existing ${API_KEY_VAR} must belong to application ${selected} or writes will fail.`
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
} catch (err) {
|
|
2709
|
+
return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
try {
|
|
2713
|
+
const updated = [
|
|
2714
|
+
...credentials,
|
|
2715
|
+
// Unconditional, unlike the credentials: the write key is scoped to
|
|
2716
|
+
// the run's index, so a stale name left in the file earns a 403.
|
|
2717
|
+
[INDEX_NAME_VAR, targetIndex]
|
|
2718
|
+
].reduce(
|
|
2719
|
+
(content, [name, value]) => upsertEnv(content, name, value),
|
|
2720
|
+
existing
|
|
2721
|
+
);
|
|
2722
|
+
await mkdir4(dirname5(resolved2.target), { recursive: true });
|
|
2723
|
+
await writeFile4(resolved2.target, updated, "utf8");
|
|
2724
|
+
const sentences = [
|
|
2725
|
+
`Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
|
|
2726
|
+
];
|
|
2727
|
+
if (fileIndex !== null && fileIndex !== targetIndex) {
|
|
2728
|
+
sentences.push(
|
|
2729
|
+
`That replaced the ${INDEX_NAME_VAR} already there ("${fileIndex}"), which this run does not target.`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2554
2732
|
if (present.length > 0) {
|
|
2555
|
-
|
|
2733
|
+
sentences.push(
|
|
2734
|
+
`Skipped ${present.join(" and ")}: already defined there.`
|
|
2735
|
+
);
|
|
2556
2736
|
}
|
|
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}`;
|
|
2737
|
+
return [...sentences, ...notes].join(" ");
|
|
2564
2738
|
} catch (err) {
|
|
2565
2739
|
return `Error writing credentials to ${filePath}: ${err.message}`;
|
|
2566
2740
|
}
|
|
@@ -2570,16 +2744,16 @@ function writeCredentialsTool(ctx) {
|
|
|
2570
2744
|
|
|
2571
2745
|
// src/lib/tools/searchFiles.ts
|
|
2572
2746
|
import { tool as tool7 } from "ai";
|
|
2573
|
-
import
|
|
2574
|
-
import { readdir as readdir2, readFile as
|
|
2575
|
-
import { join as
|
|
2747
|
+
import z14 from "zod";
|
|
2748
|
+
import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
|
|
2749
|
+
import { join as join8 } from "node:path";
|
|
2576
2750
|
var MAX_QUERY_LENGTH = 1e3;
|
|
2577
2751
|
async function walkFiles(dir) {
|
|
2578
2752
|
const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
2579
2753
|
const out = [];
|
|
2580
2754
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2581
2755
|
if (e.name.startsWith(".") || skip.has(e.name)) continue;
|
|
2582
|
-
const full =
|
|
2756
|
+
const full = join8(dir, e.name);
|
|
2583
2757
|
if (e.isDirectory()) out.push(...await walkFiles(full));
|
|
2584
2758
|
else if (e.isFile()) out.push(full);
|
|
2585
2759
|
}
|
|
@@ -2588,9 +2762,9 @@ async function walkFiles(dir) {
|
|
|
2588
2762
|
function searchFilesTool(ctx) {
|
|
2589
2763
|
return tool7({
|
|
2590
2764
|
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:
|
|
2765
|
+
inputSchema: z14.object({
|
|
2766
|
+
query: z14.string().describe("JavaScript RegExp pattern to search for"),
|
|
2767
|
+
path: z14.string().optional().describe("Directory to search in (default: cwd)")
|
|
2594
2768
|
}),
|
|
2595
2769
|
execute: async ({ query, path = "." }) => {
|
|
2596
2770
|
logger.info({ query, path }, "called searchFiles tool");
|
|
@@ -2600,8 +2774,8 @@ function searchFilesTool(ctx) {
|
|
|
2600
2774
|
if (query.length > MAX_QUERY_LENGTH) {
|
|
2601
2775
|
return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
|
|
2602
2776
|
}
|
|
2603
|
-
const
|
|
2604
|
-
if (!
|
|
2777
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2778
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2605
2779
|
let re;
|
|
2606
2780
|
try {
|
|
2607
2781
|
re = new RegExp(query);
|
|
@@ -2609,10 +2783,10 @@ function searchFilesTool(ctx) {
|
|
|
2609
2783
|
return `Invalid regex: ${err.message}`;
|
|
2610
2784
|
}
|
|
2611
2785
|
const matches = [];
|
|
2612
|
-
for (const file of await walkFiles(
|
|
2786
|
+
for (const file of await walkFiles(resolved2.target)) {
|
|
2613
2787
|
let content;
|
|
2614
2788
|
try {
|
|
2615
|
-
content = await
|
|
2789
|
+
content = await readFile6(file, "utf8");
|
|
2616
2790
|
} catch {
|
|
2617
2791
|
continue;
|
|
2618
2792
|
}
|
|
@@ -2634,7 +2808,7 @@ function searchFilesTool(ctx) {
|
|
|
2634
2808
|
|
|
2635
2809
|
// src/lib/tools/verifyImplementation.ts
|
|
2636
2810
|
import { tool as tool8 } from "ai";
|
|
2637
|
-
import
|
|
2811
|
+
import z15 from "zod";
|
|
2638
2812
|
|
|
2639
2813
|
// src/lib/tools/utils/runCommand.ts
|
|
2640
2814
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2656,9 +2830,9 @@ function runCommand(command, args, cwd) {
|
|
|
2656
2830
|
}
|
|
2657
2831
|
|
|
2658
2832
|
// src/lib/tools/utils/packageManager.ts
|
|
2659
|
-
import { readFile as
|
|
2833
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2660
2834
|
import { existsSync } from "node:fs";
|
|
2661
|
-
import { join as
|
|
2835
|
+
import { join as join9 } from "node:path";
|
|
2662
2836
|
var LOCKFILES = [
|
|
2663
2837
|
["pnpm-lock.yaml", "pnpm"],
|
|
2664
2838
|
["yarn.lock", "yarn"],
|
|
@@ -2667,13 +2841,13 @@ var LOCKFILES = [
|
|
|
2667
2841
|
["package-lock.json", "npm"]
|
|
2668
2842
|
];
|
|
2669
2843
|
async function readPackageJson(cwd = process.cwd()) {
|
|
2670
|
-
return JSON.parse(await
|
|
2844
|
+
return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
|
|
2671
2845
|
}
|
|
2672
2846
|
function packageManagerFrom(pkg) {
|
|
2673
2847
|
return pkg.packageManager?.split("@")[0] ?? "npm";
|
|
2674
2848
|
}
|
|
2675
2849
|
function packageManagerFromLockfile(cwd) {
|
|
2676
|
-
return LOCKFILES.find(([file]) => existsSync(
|
|
2850
|
+
return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
|
|
2677
2851
|
}
|
|
2678
2852
|
async function detectPackageManager(cwd) {
|
|
2679
2853
|
try {
|
|
@@ -2714,7 +2888,7 @@ async function runRepoVerificationCheck() {
|
|
|
2714
2888
|
function verifyImplementationTool() {
|
|
2715
2889
|
return tool8({
|
|
2716
2890
|
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:
|
|
2891
|
+
inputSchema: z15.object(),
|
|
2718
2892
|
execute: async () => {
|
|
2719
2893
|
logger.info("called verifyImplementation tool");
|
|
2720
2894
|
return runRepoVerificationCheck();
|
|
@@ -2728,7 +2902,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
2728
2902
|
import { nanoid as nanoid2 } from "nanoid";
|
|
2729
2903
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
2730
2904
|
import { dirname as dirname6 } from "node:path";
|
|
2731
|
-
import
|
|
2905
|
+
import z16 from "zod";
|
|
2732
2906
|
var DATA_DIR = ".algolia-wizard/data";
|
|
2733
2907
|
var RECORD_MODEL = "claude-haiku-4-5";
|
|
2734
2908
|
var MAX_RECORDS = 100;
|
|
@@ -2740,17 +2914,17 @@ var anthropic = createAnthropic({
|
|
|
2740
2914
|
function generateRecordTool(ctx) {
|
|
2741
2915
|
return tool9({
|
|
2742
2916
|
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:
|
|
2917
|
+
inputSchema: z16.object({
|
|
2918
|
+
entityName: z16.string().describe("Name of the entity to generate records for."),
|
|
2919
|
+
attributes: z16.array(z16.string()).describe("Attribute names each record must contain."),
|
|
2920
|
+
count: z16.number().int().min(1).max(MAX_RECORDS).default(10).describe(`How many records to generate (max ${MAX_RECORDS}).`),
|
|
2921
|
+
hint: z16.string().optional().describe("Optional context to steer realistic values.")
|
|
2748
2922
|
}),
|
|
2749
2923
|
execute: async ({ entityName, attributes, count, hint }) => {
|
|
2750
2924
|
logger.info({ entityName, count }, "called generateRecord tool");
|
|
2751
2925
|
try {
|
|
2752
|
-
const value =
|
|
2753
|
-
const recordSchema =
|
|
2926
|
+
const value = z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]);
|
|
2927
|
+
const recordSchema = z16.object(
|
|
2754
2928
|
Object.fromEntries(attributes.map((attr) => [attr, value]))
|
|
2755
2929
|
);
|
|
2756
2930
|
const generateBatch = async (batchCount) => {
|
|
@@ -2760,8 +2934,8 @@ function generateRecordTool(ctx) {
|
|
|
2760
2934
|
const { output } = await generateText({
|
|
2761
2935
|
model: anthropic(RECORD_MODEL),
|
|
2762
2936
|
output: Output.object({
|
|
2763
|
-
schema:
|
|
2764
|
-
records:
|
|
2937
|
+
schema: z16.object({
|
|
2938
|
+
records: z16.array(recordSchema).length(batchCount)
|
|
2765
2939
|
})
|
|
2766
2940
|
}),
|
|
2767
2941
|
prompt: [
|
|
@@ -2797,13 +2971,13 @@ function generateRecordTool(ctx) {
|
|
|
2797
2971
|
}));
|
|
2798
2972
|
const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
2799
2973
|
const relPath = `${DATA_DIR}/${slug}.json`;
|
|
2800
|
-
const
|
|
2801
|
-
if (
|
|
2802
|
-
if (await hasSymlinkParent(ctx,
|
|
2803
|
-
return `Refused: ${
|
|
2974
|
+
const resolved2 = resolveInRoot(ctx, relPath);
|
|
2975
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2976
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2977
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2804
2978
|
}
|
|
2805
|
-
await mkdir5(dirname6(
|
|
2806
|
-
await writeFile5(
|
|
2979
|
+
await mkdir5(dirname6(resolved2.target), { recursive: true });
|
|
2980
|
+
await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
|
|
2807
2981
|
logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
|
|
2808
2982
|
return {
|
|
2809
2983
|
filePath: relPath,
|
|
@@ -2819,12 +2993,12 @@ function generateRecordTool(ctx) {
|
|
|
2819
2993
|
|
|
2820
2994
|
// src/lib/tools/notifyUser.ts
|
|
2821
2995
|
import { tool as tool10 } from "ai";
|
|
2822
|
-
import
|
|
2996
|
+
import z17 from "zod";
|
|
2823
2997
|
function notifyUserTool() {
|
|
2824
2998
|
return tool10({
|
|
2825
2999
|
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:
|
|
3000
|
+
inputSchema: z17.object({
|
|
3001
|
+
message: z17.string().describe(
|
|
2828
3002
|
"Short, plain-language description of what you are doing now."
|
|
2829
3003
|
)
|
|
2830
3004
|
}),
|
|
@@ -3004,10 +3178,10 @@ async function runAgent(req) {
|
|
|
3004
3178
|
}
|
|
3005
3179
|
|
|
3006
3180
|
// src/actions/detectLanguage.ts
|
|
3007
|
-
import
|
|
3008
|
-
var detectLanguageSchema =
|
|
3009
|
-
languages:
|
|
3010
|
-
frameworks:
|
|
3181
|
+
import z20 from "zod";
|
|
3182
|
+
var detectLanguageSchema = z20.object({
|
|
3183
|
+
languages: z20.array(z20.object({ name: z20.string(), version: z20.string() })),
|
|
3184
|
+
frameworks: z20.array(z20.object({ name: z20.string(), version: z20.string() }))
|
|
3011
3185
|
});
|
|
3012
3186
|
var detectLanguage = () => runAgent({
|
|
3013
3187
|
instructions: [
|
|
@@ -3025,31 +3199,31 @@ var detectLanguage = () => runAgent({
|
|
|
3025
3199
|
});
|
|
3026
3200
|
|
|
3027
3201
|
// src/actions/analyzeCodebase.ts
|
|
3028
|
-
import
|
|
3202
|
+
import z21 from "zod";
|
|
3029
3203
|
var READONLY_TOOLS = [
|
|
3030
3204
|
"listFiles",
|
|
3031
3205
|
"changeDirectory",
|
|
3032
3206
|
"readFile",
|
|
3033
3207
|
"searchFiles"
|
|
3034
3208
|
];
|
|
3035
|
-
var ingestionAnalysisSchema =
|
|
3036
|
-
ingestionAnalysis:
|
|
3037
|
-
|
|
3038
|
-
name:
|
|
3039
|
-
paths:
|
|
3209
|
+
var ingestionAnalysisSchema = z21.object({
|
|
3210
|
+
ingestionAnalysis: z21.array(
|
|
3211
|
+
z21.object({
|
|
3212
|
+
name: z21.string(),
|
|
3213
|
+
paths: z21.array(z21.string()),
|
|
3040
3214
|
// indexable fields the agent found for this entity
|
|
3041
|
-
attributes:
|
|
3215
|
+
attributes: z21.array(z21.string())
|
|
3042
3216
|
})
|
|
3043
3217
|
)
|
|
3044
3218
|
});
|
|
3045
|
-
var searchImplementationAnalysisSchema =
|
|
3046
|
-
searchImplementationAnalysis:
|
|
3219
|
+
var searchImplementationAnalysisSchema = z21.object({
|
|
3220
|
+
searchImplementationAnalysis: z21.string()
|
|
3047
3221
|
});
|
|
3048
|
-
var verificationSchema =
|
|
3049
|
-
verification:
|
|
3222
|
+
var verificationSchema = z21.object({
|
|
3223
|
+
verification: z21.array(z21.string())
|
|
3050
3224
|
});
|
|
3051
3225
|
var confirmedEntitiesFieldSchema = ingestionAnalysisSchema.shape.ingestionAnalysis.optional();
|
|
3052
|
-
var analyzeCodebaseSchema =
|
|
3226
|
+
var analyzeCodebaseSchema = z21.object({
|
|
3053
3227
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
3054
3228
|
searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional(),
|
|
3055
3229
|
verification: verificationSchema.shape.verification.optional(),
|
|
@@ -3111,7 +3285,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3111
3285
|
// package.json
|
|
3112
3286
|
var package_default = {
|
|
3113
3287
|
name: "@algolia/wizard",
|
|
3114
|
-
version: "0.
|
|
3288
|
+
version: "0.11.0",
|
|
3115
3289
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3116
3290
|
type: "module",
|
|
3117
3291
|
engines: {
|
|
@@ -3162,6 +3336,7 @@ var package_default = {
|
|
|
3162
3336
|
"@hono/node-server": "^2.0.10",
|
|
3163
3337
|
"@segment/analytics-node": "^3.1.0",
|
|
3164
3338
|
ai: "^6.0.190",
|
|
3339
|
+
"cross-keychain": "^1.1.0",
|
|
3165
3340
|
dotenv: "^17.4.2",
|
|
3166
3341
|
hono: "^4.12.27",
|
|
3167
3342
|
ink: "^7.0.5",
|
|
@@ -3229,8 +3404,8 @@ async function askList(ctx, prompt, { required = false } = {}) {
|
|
|
3229
3404
|
}
|
|
3230
3405
|
|
|
3231
3406
|
// src/actions/confirmLanguage.ts
|
|
3232
|
-
import
|
|
3233
|
-
var confirmLanguageSchema =
|
|
3407
|
+
import z23 from "zod";
|
|
3408
|
+
var confirmLanguageSchema = z23.object({
|
|
3234
3409
|
languages: detectLanguageSchema.shape.languages
|
|
3235
3410
|
});
|
|
3236
3411
|
async function confirmLanguage(ctx) {
|
|
@@ -3251,8 +3426,8 @@ async function confirmLanguage(ctx) {
|
|
|
3251
3426
|
}
|
|
3252
3427
|
|
|
3253
3428
|
// src/actions/confirmFramework.ts
|
|
3254
|
-
import
|
|
3255
|
-
var confirmFrameworkSchema =
|
|
3429
|
+
import z24 from "zod";
|
|
3430
|
+
var confirmFrameworkSchema = z24.object({
|
|
3256
3431
|
frameworks: detectLanguageSchema.shape.frameworks
|
|
3257
3432
|
});
|
|
3258
3433
|
var CURATED_FRAMEWORKS = [
|
|
@@ -3380,8 +3555,8 @@ async function promptUser(ctx, params) {
|
|
|
3380
3555
|
}
|
|
3381
3556
|
|
|
3382
3557
|
// src/actions/confirmEntities.ts
|
|
3383
|
-
import
|
|
3384
|
-
var confirmEntitiesSchema =
|
|
3558
|
+
import z25 from "zod";
|
|
3559
|
+
var confirmEntitiesSchema = z25.object({
|
|
3385
3560
|
// Final detection — the focused re-run may supersede project-scan's.
|
|
3386
3561
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
3387
3562
|
confirmedEntities: confirmedEntitiesFieldSchema
|
|
@@ -3451,15 +3626,15 @@ async function confirmEntities(ctx) {
|
|
|
3451
3626
|
}
|
|
3452
3627
|
|
|
3453
3628
|
// src/actions/review.ts
|
|
3454
|
-
import { z as
|
|
3455
|
-
var reviewSchema =
|
|
3629
|
+
import { z as z26 } from "zod";
|
|
3630
|
+
var reviewSchema = z26.object({
|
|
3456
3631
|
// Broad, high-level takeaways grouped by theme (e.g. ingestion, search UI),
|
|
3457
3632
|
// not one entry per workflow step — a step's raw output can be a long,
|
|
3458
3633
|
// multi-paragraph blob (see implement.ts's summaries.join), and mirroring
|
|
3459
3634
|
// that 1:1 is what made the old per-step summary an unreadable wall of text.
|
|
3460
|
-
summaryPoints:
|
|
3461
|
-
reviewPrompt:
|
|
3462
|
-
nextSteps:
|
|
3635
|
+
summaryPoints: z26.array(z26.string()),
|
|
3636
|
+
reviewPrompt: z26.string(),
|
|
3637
|
+
nextSteps: z26.array(z26.string())
|
|
3463
3638
|
});
|
|
3464
3639
|
function formatCompletedSteps(steps) {
|
|
3465
3640
|
if (!steps.length) return "(no prior steps completed)";
|
|
@@ -3510,16 +3685,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
3510
3685
|
};
|
|
3511
3686
|
|
|
3512
3687
|
// src/actions/implement.ts
|
|
3513
|
-
import
|
|
3688
|
+
import z27 from "zod";
|
|
3514
3689
|
|
|
3515
3690
|
// src/lib/worktree.ts
|
|
3516
3691
|
import { execFile, spawn as spawn3 } from "node:child_process";
|
|
3517
|
-
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as
|
|
3692
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
3518
3693
|
import {
|
|
3519
3694
|
basename as basename2,
|
|
3520
3695
|
dirname as dirname7,
|
|
3521
3696
|
isAbsolute as isAbsolute2,
|
|
3522
|
-
join as
|
|
3697
|
+
join as join10,
|
|
3523
3698
|
relative as relative2,
|
|
3524
3699
|
resolve as resolve3
|
|
3525
3700
|
} from "node:path";
|
|
@@ -3553,7 +3728,7 @@ async function isWorkingTreeDirty(repoRoot) {
|
|
|
3553
3728
|
return out.trim().length > 0;
|
|
3554
3729
|
}
|
|
3555
3730
|
async function pruneOldWorktrees(repoRoot) {
|
|
3556
|
-
const dir =
|
|
3731
|
+
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
3557
3732
|
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
3558
3733
|
for (const slug of stale) {
|
|
3559
3734
|
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
@@ -3564,7 +3739,7 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
3564
3739
|
"worktree",
|
|
3565
3740
|
"remove",
|
|
3566
3741
|
"--force",
|
|
3567
|
-
|
|
3742
|
+
join10(dir, slug)
|
|
3568
3743
|
]);
|
|
3569
3744
|
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
3570
3745
|
} catch (err) {
|
|
@@ -3578,7 +3753,7 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
3578
3753
|
async function createWorktree(repoRoot) {
|
|
3579
3754
|
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
3580
3755
|
const dirSlug = branch.replace(/\//g, "-");
|
|
3581
|
-
const path =
|
|
3756
|
+
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
3582
3757
|
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
3583
3758
|
await pruneOldWorktrees(repoRoot);
|
|
3584
3759
|
await mkdir6(dirname7(path), { recursive: true });
|
|
@@ -3698,8 +3873,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
3698
3873
|
} catch {
|
|
3699
3874
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
3700
3875
|
}
|
|
3701
|
-
const relPath =
|
|
3702
|
-
const dest =
|
|
3876
|
+
const relPath = join10(ingestDir, basename2(source));
|
|
3877
|
+
const dest = join10(worktreePath, relPath);
|
|
3703
3878
|
try {
|
|
3704
3879
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
3705
3880
|
await copyFile(source, dest);
|
|
@@ -3717,7 +3892,7 @@ function hasEnvVar(content, name) {
|
|
|
3717
3892
|
async function readEnvVar(worktreePath, name) {
|
|
3718
3893
|
let content;
|
|
3719
3894
|
try {
|
|
3720
|
-
content = await
|
|
3895
|
+
content = await readFile8(join10(worktreePath, ".env"), "utf8");
|
|
3721
3896
|
} catch (err) {
|
|
3722
3897
|
if (err.code !== "ENOENT") throw err;
|
|
3723
3898
|
return void 0;
|
|
@@ -3732,10 +3907,10 @@ async function readEnvVar(worktreePath, name) {
|
|
|
3732
3907
|
return value;
|
|
3733
3908
|
}
|
|
3734
3909
|
async function writeSearchEnvValues(worktreePath, vars) {
|
|
3735
|
-
const target =
|
|
3910
|
+
const target = join10(worktreePath, ".env");
|
|
3736
3911
|
let existing = "";
|
|
3737
3912
|
try {
|
|
3738
|
-
existing = await
|
|
3913
|
+
existing = await readFile8(target, "utf8");
|
|
3739
3914
|
} catch (err) {
|
|
3740
3915
|
if (err.code !== "ENOENT") throw err;
|
|
3741
3916
|
}
|
|
@@ -3805,13 +3980,13 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
|
3805
3980
|
|
|
3806
3981
|
// src/lib/algoliaDocs.ts
|
|
3807
3982
|
import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
|
|
3808
|
-
import { dirname as dirname8, join as
|
|
3983
|
+
import { dirname as dirname8, join as join11 } from "node:path";
|
|
3809
3984
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3810
|
-
var DOCS_SUBPATH =
|
|
3985
|
+
var DOCS_SUBPATH = join11("docs", "algolia-sdk");
|
|
3811
3986
|
function findDocsDir() {
|
|
3812
3987
|
let dir = dirname8(fileURLToPath2(import.meta.url));
|
|
3813
3988
|
for (; ; ) {
|
|
3814
|
-
const candidate =
|
|
3989
|
+
const candidate = join11(dir, DOCS_SUBPATH);
|
|
3815
3990
|
if (existsSync2(candidate)) return candidate;
|
|
3816
3991
|
const parent = dirname8(dir);
|
|
3817
3992
|
if (parent === dir) return void 0;
|
|
@@ -3834,7 +4009,7 @@ function loadAlgoliaDoc(language) {
|
|
|
3834
4009
|
);
|
|
3835
4010
|
return "";
|
|
3836
4011
|
}
|
|
3837
|
-
return readFileSync(
|
|
4012
|
+
return readFileSync(join11(docsDir, files[0]), "utf8").trim();
|
|
3838
4013
|
}
|
|
3839
4014
|
function getNamedDoc(name, language) {
|
|
3840
4015
|
const docsDir = findDocsDir();
|
|
@@ -3842,7 +4017,7 @@ function getNamedDoc(name, language) {
|
|
|
3842
4017
|
logger.warn("docs/algolia-sdk not found");
|
|
3843
4018
|
return "";
|
|
3844
4019
|
}
|
|
3845
|
-
const file =
|
|
4020
|
+
const file = join11(docsDir, `${name}-${language}.md`);
|
|
3846
4021
|
if (!existsSync2(file)) {
|
|
3847
4022
|
logger.warn({ name, language }, "named SDK reference not found");
|
|
3848
4023
|
return "";
|
|
@@ -3869,34 +4044,34 @@ function shellQuote(value) {
|
|
|
3869
4044
|
}
|
|
3870
4045
|
|
|
3871
4046
|
// 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:
|
|
4047
|
+
var implementSchema = z27.object({
|
|
4048
|
+
filesChanged: z27.array(z27.string()),
|
|
4049
|
+
summary: z27.string(),
|
|
4050
|
+
worktreePath: z27.string().optional(),
|
|
4051
|
+
ingestCommand: z27.string().optional(),
|
|
4052
|
+
ingestScriptRan: z27.boolean().optional(),
|
|
4053
|
+
ingestRecordCount: z27.number().optional(),
|
|
4054
|
+
ingestDurationMs: z27.number().optional(),
|
|
4055
|
+
ingestionSource: z27.enum(["local", "fileUpload", "generated"]),
|
|
4056
|
+
searchEnvVars: z27.array(
|
|
4057
|
+
z27.object({
|
|
4058
|
+
name: z27.string(),
|
|
4059
|
+
value: z27.string()
|
|
3885
4060
|
})
|
|
3886
4061
|
).optional()
|
|
3887
4062
|
});
|
|
3888
|
-
var implementationOutputSchema =
|
|
3889
|
-
summary:
|
|
4063
|
+
var implementationOutputSchema = z27.object({
|
|
4064
|
+
summary: z27.string(),
|
|
3890
4065
|
// Ingestion only: a structured pair the wizard turns into an argv, never a
|
|
3891
4066
|
// free-form command string. `runtime` is allowlisted and `entrypoint` is
|
|
3892
4067
|
// validated worktree-relative, so the agent cannot inject extra commands.
|
|
3893
|
-
runtime:
|
|
3894
|
-
entrypoint:
|
|
4068
|
+
runtime: z27.enum(INGEST_RUNTIMES).optional(),
|
|
4069
|
+
entrypoint: z27.string().optional()
|
|
3895
4070
|
});
|
|
3896
|
-
var verificationOutputSchema =
|
|
3897
|
-
summary:
|
|
3898
|
-
sufficient:
|
|
3899
|
-
additionalInstructions:
|
|
4071
|
+
var verificationOutputSchema = z27.object({
|
|
4072
|
+
summary: z27.string(),
|
|
4073
|
+
sufficient: z27.boolean(),
|
|
4074
|
+
additionalInstructions: z27.string().optional()
|
|
3900
4075
|
});
|
|
3901
4076
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
3902
4077
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
@@ -3941,13 +4116,17 @@ function publicEnvPrefix(language) {
|
|
|
3941
4116
|
}
|
|
3942
4117
|
var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
|
|
3943
4118
|
var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
|
|
4119
|
+
var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
3944
4120
|
function appIdVar(language) {
|
|
3945
4121
|
return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
|
|
3946
4122
|
}
|
|
3947
4123
|
function searchKeyVar(language) {
|
|
3948
4124
|
return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
|
|
3949
4125
|
}
|
|
3950
|
-
function
|
|
4126
|
+
function searchIndexVar(language) {
|
|
4127
|
+
return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
|
|
4128
|
+
}
|
|
4129
|
+
function searchEnvVars(language, index, appId, searchKey) {
|
|
3951
4130
|
return [
|
|
3952
4131
|
{
|
|
3953
4132
|
name: appIdVar(language),
|
|
@@ -3956,12 +4135,21 @@ function searchEnvVars(language, appId, searchKey) {
|
|
|
3956
4135
|
{
|
|
3957
4136
|
name: searchKeyVar(language),
|
|
3958
4137
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4138
|
+
},
|
|
4139
|
+
// Wizard-supplied rather than written into the generated code, because an
|
|
4140
|
+
// agent that retypes the name (appending the project name, re-casing it)
|
|
4141
|
+
// leaves the UI querying an index that does not exist.
|
|
4142
|
+
{
|
|
4143
|
+
name: searchIndexVar(language),
|
|
4144
|
+
value: index
|
|
3959
4145
|
}
|
|
3960
4146
|
];
|
|
3961
4147
|
}
|
|
3962
4148
|
function baseInstructions(input) {
|
|
3963
4149
|
return [
|
|
3964
|
-
|
|
4150
|
+
// Agents have renamed this (e.g. appending the project name), which the
|
|
4151
|
+
// index-scoped keys then reject with a 403.
|
|
4152
|
+
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
3965
4153
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
3966
4154
|
"Make minimal, idiomatic changes; do not touch unrelated code."
|
|
3967
4155
|
];
|
|
@@ -3995,6 +4183,7 @@ function ingestionInstructions(input) {
|
|
|
3995
4183
|
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
3996
4184
|
`Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
3997
4185
|
`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.`,
|
|
4186
|
+
`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
4187
|
"Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
|
|
3999
4188
|
"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
4189
|
getNamedDoc("save-records", "js"),
|
|
@@ -4012,7 +4201,8 @@ function searchInstructions(input) {
|
|
|
4012
4201
|
`Build the search UI for ${input.uiFramework}.`,
|
|
4013
4202
|
"Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
|
|
4014
4203
|
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
|
|
4204
|
+
`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.`,
|
|
4205
|
+
`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
4206
|
// The key is provisioned only after verification passes, so the agent never
|
|
4017
4207
|
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4018
4208
|
// decide whether a key already exists, and an agent-invented value there
|
|
@@ -4022,7 +4212,7 @@ function searchInstructions(input) {
|
|
|
4022
4212
|
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4023
4213
|
// reading a var the wizard never wrote.
|
|
4024
4214
|
`Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4025
|
-
"Read the App ID
|
|
4215
|
+
"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
4216
|
'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
4217
|
"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
4218
|
];
|
|
@@ -4206,9 +4396,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4206
4396
|
targetIndex,
|
|
4207
4397
|
language,
|
|
4208
4398
|
appId,
|
|
4209
|
-
|
|
4210
|
-
// every value here is still a placeholder when the agent reads them.
|
|
4211
|
-
searchEnvVars: searchEnvVars(language, appId),
|
|
4399
|
+
searchEnvVars: searchEnvVars(language, targetIndex, appId),
|
|
4212
4400
|
ingestDir: INGEST_DIR,
|
|
4213
4401
|
ingestionSource,
|
|
4214
4402
|
uploadFilePath,
|
|
@@ -4295,7 +4483,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4295
4483
|
}) === true;
|
|
4296
4484
|
if (runNow) {
|
|
4297
4485
|
const ingestApp = await requireApplication();
|
|
4298
|
-
const writeKey = await resolveWriteKey(targetIndex);
|
|
4486
|
+
const writeKey = (await resolveWriteKey(targetIndex, ingestApp.id)).key;
|
|
4299
4487
|
ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
|
|
4300
4488
|
const scriptLogId = ctx.logStart("runIngestScript", {
|
|
4301
4489
|
runtime: ingestRuntime,
|
|
@@ -4308,7 +4496,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4308
4496
|
ingestEntrypoint,
|
|
4309
4497
|
{
|
|
4310
4498
|
[APP_ID_VAR]: ingestApp.id,
|
|
4311
|
-
[API_KEY_VAR]: writeKey
|
|
4499
|
+
[API_KEY_VAR]: writeKey,
|
|
4500
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4312
4501
|
}
|
|
4313
4502
|
);
|
|
4314
4503
|
ctx.logEnd(scriptLogId, run2.ok ? "success" : "error");
|
|
@@ -4429,14 +4618,14 @@ ${run2.output}` : status;
|
|
|
4429
4618
|
let searchKeyError;
|
|
4430
4619
|
if (appId) {
|
|
4431
4620
|
try {
|
|
4432
|
-
const
|
|
4621
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4433
4622
|
targetIndex,
|
|
4434
4623
|
appId,
|
|
4435
4624
|
envSearchKey
|
|
4436
4625
|
);
|
|
4437
|
-
searchKey =
|
|
4626
|
+
searchKey = resolved2.key;
|
|
4438
4627
|
summaries.push(
|
|
4439
|
-
|
|
4628
|
+
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
4629
|
);
|
|
4441
4630
|
} catch (err) {
|
|
4442
4631
|
searchKeyError = err.message;
|
|
@@ -4446,7 +4635,12 @@ ${run2.output}` : status;
|
|
|
4446
4635
|
);
|
|
4447
4636
|
}
|
|
4448
4637
|
}
|
|
4449
|
-
finalSearchEnvVars = searchEnvVars(
|
|
4638
|
+
finalSearchEnvVars = searchEnvVars(
|
|
4639
|
+
language,
|
|
4640
|
+
targetIndex,
|
|
4641
|
+
appId,
|
|
4642
|
+
searchKey
|
|
4643
|
+
);
|
|
4450
4644
|
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4451
4645
|
(v) => !v.value.startsWith("<")
|
|
4452
4646
|
);
|
|
@@ -4555,8 +4749,8 @@ var defaultWorkflow = {
|
|
|
4555
4749
|
defineStep({
|
|
4556
4750
|
id: "select-index",
|
|
4557
4751
|
title: "Set up index",
|
|
4558
|
-
outputSchema:
|
|
4559
|
-
selection:
|
|
4752
|
+
outputSchema: z28.object({
|
|
4753
|
+
selection: z28.string()
|
|
4560
4754
|
}),
|
|
4561
4755
|
run: (ctx) => selectIndexStep(ctx)
|
|
4562
4756
|
}),
|
|
@@ -4798,7 +4992,11 @@ Options:
|
|
|
4798
4992
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
4799
4993
|
--reset-on-run Wipe this project's wizard state (run state, AI consent,
|
|
4800
4994
|
worktrees) before starting, so the run behaves like a
|
|
4801
|
-
first-ever run.
|
|
4995
|
+
first-ever run. Also drops every API key the wizard has
|
|
4996
|
+
stored in your keychain (or, where the platform has none,
|
|
4997
|
+
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
4998
|
+
for this project and any other, so later runs create new
|
|
4999
|
+
ones. Your Algolia login is not touched.
|
|
4802
5000
|
-h, --help Print this message.`;
|
|
4803
5001
|
function parseCliArgs(argv) {
|
|
4804
5002
|
const positionals = [];
|
|
@@ -4835,10 +5033,11 @@ function parseCliArgs(argv) {
|
|
|
4835
5033
|
|
|
4836
5034
|
// src/lib/resetState.ts
|
|
4837
5035
|
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
4838
|
-
import { join as
|
|
5036
|
+
import { join as join12 } from "node:path";
|
|
4839
5037
|
var KEEP = ["wizard.log"];
|
|
4840
5038
|
async function resetProjectState() {
|
|
4841
5039
|
const dir = stateDir();
|
|
5040
|
+
await forgetResolvedKeys();
|
|
4842
5041
|
let entries;
|
|
4843
5042
|
try {
|
|
4844
5043
|
entries = await readdir4(dir);
|
|
@@ -4847,7 +5046,9 @@ async function resetProjectState() {
|
|
|
4847
5046
|
}
|
|
4848
5047
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
4849
5048
|
await Promise.all(
|
|
4850
|
-
targets.map(
|
|
5049
|
+
targets.map(
|
|
5050
|
+
(name) => rm2(join12(dir, name), { recursive: true, force: true })
|
|
5051
|
+
)
|
|
4851
5052
|
);
|
|
4852
5053
|
return { dir, removed: targets };
|
|
4853
5054
|
}
|
|
@@ -4855,6 +5056,7 @@ async function resetProjectState() {
|
|
|
4855
5056
|
// src/main.tsx
|
|
4856
5057
|
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
4857
5058
|
async function startup() {
|
|
5059
|
+
setProjectRoot(process.cwd());
|
|
4858
5060
|
let args;
|
|
4859
5061
|
try {
|
|
4860
5062
|
args = parseCliArgs(process.argv.slice(2));
|