@algolia/wizard 0.9.0-rc.87.83 → 0.9.0-rc.87.86
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 +229 -221
- package/package.json +2 -1
package/dist/main.js
CHANGED
|
@@ -26,10 +26,14 @@ import { join, resolve } from "node:path";
|
|
|
26
26
|
function rootDir() {
|
|
27
27
|
return process.env.WIZARD_HOME ?? join(homedir(), ".algolia");
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
var pinnedRoot;
|
|
30
|
+
function setProjectRoot(cwd) {
|
|
31
|
+
pinnedRoot = resolve(cwd);
|
|
32
|
+
}
|
|
33
|
+
function projectSlug(cwd = pinnedRoot ?? process.cwd()) {
|
|
30
34
|
return resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+/, "") || "root";
|
|
31
35
|
}
|
|
32
|
-
function stateDir(cwd = process.cwd()) {
|
|
36
|
+
function stateDir(cwd = pinnedRoot ?? process.cwd()) {
|
|
33
37
|
return join(rootDir(), projectSlug(cwd));
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -58,6 +62,11 @@ function npxArgs(args) {
|
|
|
58
62
|
return ["--yes", "@algolia/cli@latest", ...args];
|
|
59
63
|
}
|
|
60
64
|
var shell = process.platform === "win32";
|
|
65
|
+
function childEnv(withoutAdminKey) {
|
|
66
|
+
if (!withoutAdminKey) return void 0;
|
|
67
|
+
const { ALGOLIA_API_KEY: _adminKey, ...rest } = process.env;
|
|
68
|
+
return rest;
|
|
69
|
+
}
|
|
61
70
|
function lineSplitter(emit) {
|
|
62
71
|
let buffer = "";
|
|
63
72
|
return {
|
|
@@ -81,11 +90,14 @@ var stderrSink = (stream, line) => {
|
|
|
81
90
|
if (stream === "stdout") return;
|
|
82
91
|
wizardSink(stream, line);
|
|
83
92
|
};
|
|
84
|
-
function runAlgoliaCli(args, { onOutput } = {}) {
|
|
93
|
+
function runAlgoliaCli(args, { onOutput, withoutAdminKey } = {}) {
|
|
85
94
|
const store = useWizard.getState();
|
|
86
95
|
const logId = store.logStart("tool", `algolia ${args.join(" ")}`);
|
|
87
96
|
return new Promise((resolve4, reject) => {
|
|
88
|
-
const child = spawn("npx", npxArgs(args), {
|
|
97
|
+
const child = spawn("npx", npxArgs(args), {
|
|
98
|
+
shell,
|
|
99
|
+
env: childEnv(withoutAdminKey)
|
|
100
|
+
});
|
|
89
101
|
let stdout = "";
|
|
90
102
|
let stderr = "";
|
|
91
103
|
const splitters = {
|
|
@@ -1813,18 +1825,17 @@ import "zod";
|
|
|
1813
1825
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1814
1826
|
import { join as join5 } from "node:path";
|
|
1815
1827
|
var configFile = () => join5(stateDir(), "config.json");
|
|
1816
|
-
var
|
|
1828
|
+
var defaultConfig = () => ({
|
|
1817
1829
|
version: 1,
|
|
1818
1830
|
aiConsent: false,
|
|
1819
|
-
workflowsRun: []
|
|
1820
|
-
|
|
1821
|
-
};
|
|
1831
|
+
workflowsRun: []
|
|
1832
|
+
});
|
|
1822
1833
|
async function loadConfig() {
|
|
1823
1834
|
try {
|
|
1824
1835
|
const raw = await readFile2(configFile(), "utf8");
|
|
1825
|
-
return { ...
|
|
1836
|
+
return { ...defaultConfig(), ...JSON.parse(raw) };
|
|
1826
1837
|
} catch {
|
|
1827
|
-
return
|
|
1838
|
+
return defaultConfig();
|
|
1828
1839
|
}
|
|
1829
1840
|
}
|
|
1830
1841
|
async function saveConfig(config) {
|
|
@@ -1836,38 +1847,6 @@ async function recordWorkflowRun(workflowId, completedAt) {
|
|
|
1836
1847
|
config.workflowsRun.push({ workflowId, completedAt });
|
|
1837
1848
|
await saveConfig(config);
|
|
1838
1849
|
}
|
|
1839
|
-
function isStoredSearchKey(value) {
|
|
1840
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1841
|
-
const { appId, key } = value;
|
|
1842
|
-
return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
|
|
1843
|
-
}
|
|
1844
|
-
function storedSearchKeys(config) {
|
|
1845
|
-
const stored = config.searchApiKeys;
|
|
1846
|
-
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
1847
|
-
return {};
|
|
1848
|
-
}
|
|
1849
|
-
return stored;
|
|
1850
|
-
}
|
|
1851
|
-
async function getStoredSearchKey(index, appId) {
|
|
1852
|
-
const entry = storedSearchKeys(await loadConfig())[index];
|
|
1853
|
-
if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
|
|
1854
|
-
return entry.key;
|
|
1855
|
-
}
|
|
1856
|
-
async function storeSearchKey(index, appId, key) {
|
|
1857
|
-
const config = await loadConfig();
|
|
1858
|
-
config.searchApiKeys = {
|
|
1859
|
-
...storedSearchKeys(config),
|
|
1860
|
-
[index]: { appId, key }
|
|
1861
|
-
};
|
|
1862
|
-
await saveConfig(config);
|
|
1863
|
-
}
|
|
1864
|
-
async function forgetSearchKey(index) {
|
|
1865
|
-
const config = await loadConfig();
|
|
1866
|
-
const remaining = { ...storedSearchKeys(config) };
|
|
1867
|
-
delete remaining[index];
|
|
1868
|
-
config.searchApiKeys = remaining;
|
|
1869
|
-
await saveConfig(config);
|
|
1870
|
-
}
|
|
1871
1850
|
|
|
1872
1851
|
// src/core/orchestrator.ts
|
|
1873
1852
|
function defineStep(step) {
|
|
@@ -2315,9 +2294,9 @@ function listFilesTool(ctx) {
|
|
|
2315
2294
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2316
2295
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
2317
2296
|
}
|
|
2318
|
-
const
|
|
2319
|
-
if (!
|
|
2320
|
-
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 });
|
|
2321
2300
|
return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
2322
2301
|
}
|
|
2323
2302
|
});
|
|
@@ -2335,14 +2314,14 @@ function changeDirectoryTool(ctx) {
|
|
|
2335
2314
|
}),
|
|
2336
2315
|
execute: async ({ path }) => {
|
|
2337
2316
|
logger.info({ path }, "called changeDirectory tool");
|
|
2338
|
-
const
|
|
2339
|
-
if (!
|
|
2317
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2318
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2340
2319
|
try {
|
|
2341
|
-
const info = await stat(
|
|
2320
|
+
const info = await stat(resolved2.target);
|
|
2342
2321
|
if (!info.isDirectory()) {
|
|
2343
2322
|
return `Error changing directory to ${path}: not a directory`;
|
|
2344
2323
|
}
|
|
2345
|
-
ctx.cwd =
|
|
2324
|
+
ctx.cwd = resolved2.target;
|
|
2346
2325
|
return `Changed working directory to ${ctx.cwd}`;
|
|
2347
2326
|
} catch (err) {
|
|
2348
2327
|
return `Error changing directory to ${path}: ${err.message}`;
|
|
@@ -2407,11 +2386,11 @@ function readFileTool(ctx) {
|
|
|
2407
2386
|
return `Refused: read limit (${ctx.limits.read}) reached. Stop reading and proceed with the information you already have.`;
|
|
2408
2387
|
}
|
|
2409
2388
|
logger.info({ filePath }, "called readFile tool");
|
|
2410
|
-
const
|
|
2411
|
-
if (!
|
|
2389
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2390
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2412
2391
|
try {
|
|
2413
|
-
const content = await readFile3(
|
|
2414
|
-
return isEnvFile(
|
|
2392
|
+
const content = await readFile3(resolved2.target, "utf8");
|
|
2393
|
+
return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
|
|
2415
2394
|
} catch (err) {
|
|
2416
2395
|
return `Error reading ${filePath}: ${err.message}`;
|
|
2417
2396
|
}
|
|
@@ -2433,17 +2412,17 @@ function writeFileTool(ctx) {
|
|
|
2433
2412
|
}),
|
|
2434
2413
|
execute: async ({ filePath, content }) => {
|
|
2435
2414
|
logger.info({ filePath }, "called writeFile tool");
|
|
2436
|
-
const
|
|
2437
|
-
if (
|
|
2438
|
-
if (isSecretEnvFile(
|
|
2415
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2416
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2417
|
+
if (isSecretEnvFile(resolved2.target)) {
|
|
2439
2418
|
return `Refused: ${filePath} holds secrets. Use the writeCredentials tool to set Algolia environment variables, passing this file path.`;
|
|
2440
2419
|
}
|
|
2441
2420
|
try {
|
|
2442
|
-
if (await hasSymlinkParent(ctx,
|
|
2443
|
-
return `Refused: ${
|
|
2421
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2422
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2444
2423
|
}
|
|
2445
|
-
await mkdir3(dirname4(
|
|
2446
|
-
await writeFile3(
|
|
2424
|
+
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2425
|
+
await writeFile3(resolved2.target, content, "utf8");
|
|
2447
2426
|
return `Wrote to ${filePath}`;
|
|
2448
2427
|
} catch (err) {
|
|
2449
2428
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -2460,6 +2439,40 @@ import { dirname as dirname5 } from "node:path";
|
|
|
2460
2439
|
|
|
2461
2440
|
// src/lib/algoliaApiKey.ts
|
|
2462
2441
|
import { z as z11 } from "zod";
|
|
2442
|
+
|
|
2443
|
+
// src/lib/keychain.ts
|
|
2444
|
+
import { getPassword, setPassword } from "cross-keychain";
|
|
2445
|
+
var SERVICE = "algolia-wizard";
|
|
2446
|
+
function account(kind, index, appId) {
|
|
2447
|
+
return `${kind}:${appId}:${index}`;
|
|
2448
|
+
}
|
|
2449
|
+
async function readStoredKey(kind, index, appId) {
|
|
2450
|
+
try {
|
|
2451
|
+
return await getPassword(SERVICE, account(kind, index, appId));
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
logger.warn(
|
|
2454
|
+
{ err: err.message, kind, index, appId },
|
|
2455
|
+
"could not read the API key from the keychain"
|
|
2456
|
+
);
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
async function storeKey(kind, index, appId, value) {
|
|
2461
|
+
const name = account(kind, index, appId);
|
|
2462
|
+
try {
|
|
2463
|
+
await setPassword(SERVICE, name, value);
|
|
2464
|
+
if (await getPassword(SERVICE, name) !== value) {
|
|
2465
|
+
throw new Error("the keychain did not store the value");
|
|
2466
|
+
}
|
|
2467
|
+
} catch (err) {
|
|
2468
|
+
logger.warn(
|
|
2469
|
+
{ err: err.message, kind, index, appId },
|
|
2470
|
+
"could not store the API key in the keychain; the next run will create another"
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// src/lib/algoliaApiKey.ts
|
|
2463
2476
|
var WRITE_ACLS = [
|
|
2464
2477
|
"addObject",
|
|
2465
2478
|
"deleteObject",
|
|
@@ -2467,81 +2480,27 @@ var WRITE_ACLS = [
|
|
|
2467
2480
|
"editSettings",
|
|
2468
2481
|
"listIndexes"
|
|
2469
2482
|
];
|
|
2470
|
-
var WRITE_ACL_SET = new Set(WRITE_ACLS);
|
|
2471
|
-
var apiKeySchema = z11.object({
|
|
2472
|
-
value: z11.string().min(1),
|
|
2473
|
-
acl: z11.array(z11.string()).default([]),
|
|
2474
|
-
indexes: z11.array(z11.string()).default([])
|
|
2475
|
-
});
|
|
2476
|
-
var apiKeyListSchema = z11.union([
|
|
2477
|
-
z11.array(apiKeySchema),
|
|
2478
|
-
z11.object({
|
|
2479
|
-
items: z11.array(apiKeySchema).optional(),
|
|
2480
|
-
keys: z11.array(apiKeySchema).optional()
|
|
2481
|
-
})
|
|
2482
|
-
]).transform((o) => Array.isArray(o) ? o : o.items ?? o.keys ?? []);
|
|
2483
|
-
function parseCliJson(schema, stdout, command) {
|
|
2484
|
-
let raw;
|
|
2485
|
-
try {
|
|
2486
|
-
raw = JSON.parse(stdout);
|
|
2487
|
-
} catch {
|
|
2488
|
-
throw new Error(`Algolia CLI \`${command}\` returned unreadable JSON.`);
|
|
2489
|
-
}
|
|
2490
|
-
const parsed = schema.safeParse(raw);
|
|
2491
|
-
if (!parsed.success) {
|
|
2492
|
-
throw new Error(
|
|
2493
|
-
`Algolia CLI \`${command}\` returned JSON in an unexpected shape.`
|
|
2494
|
-
);
|
|
2495
|
-
}
|
|
2496
|
-
return parsed.data;
|
|
2497
|
-
}
|
|
2498
2483
|
var createdKeySchema = z11.object({
|
|
2499
2484
|
key: z11.string().min(1).optional(),
|
|
2500
2485
|
value: z11.string().min(1).optional()
|
|
2501
2486
|
}).transform((o) => o.key ?? o.value);
|
|
2502
|
-
function
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
index,
|
|
2520
|
-
"--acl",
|
|
2521
|
-
WRITE_ACLS.join(","),
|
|
2522
|
-
"--description",
|
|
2523
|
-
`wizard write key for ${index}`,
|
|
2524
|
-
"-o",
|
|
2525
|
-
"json"
|
|
2526
|
-
]);
|
|
2527
|
-
const writeKey = parseCliJson(createdKeySchema, created, "apikeys create");
|
|
2528
|
-
if (!writeKey) throw new Error("apikeys create returned no key value");
|
|
2529
|
-
return writeKey;
|
|
2530
|
-
}
|
|
2531
|
-
async function createSearchOnlyKey(index) {
|
|
2532
|
-
logger.info({ index }, "creating a search-only API key");
|
|
2533
|
-
const stdout = await runAlgoliaCli([
|
|
2534
|
-
"apikeys",
|
|
2535
|
-
"create",
|
|
2536
|
-
"--acl",
|
|
2537
|
-
"search",
|
|
2538
|
-
"--indices",
|
|
2539
|
-
index,
|
|
2540
|
-
"--description",
|
|
2541
|
-
`Algolia Wizard search-only key for ${index}`,
|
|
2542
|
-
"-o",
|
|
2543
|
-
"json"
|
|
2544
|
-
]);
|
|
2487
|
+
async function createKey(index, acls, description) {
|
|
2488
|
+
logger.info({ index, acls }, "creating an API key");
|
|
2489
|
+
const stdout = await runAlgoliaCli(
|
|
2490
|
+
[
|
|
2491
|
+
"apikeys",
|
|
2492
|
+
"create",
|
|
2493
|
+
"--acl",
|
|
2494
|
+
acls.join(","),
|
|
2495
|
+
"--indices",
|
|
2496
|
+
index,
|
|
2497
|
+
"--description",
|
|
2498
|
+
description,
|
|
2499
|
+
"-o",
|
|
2500
|
+
"json"
|
|
2501
|
+
],
|
|
2502
|
+
{ withoutAdminKey: true }
|
|
2503
|
+
);
|
|
2545
2504
|
let payload;
|
|
2546
2505
|
try {
|
|
2547
2506
|
payload = JSON.parse(stdout);
|
|
@@ -2552,49 +2511,57 @@ async function createSearchOnlyKey(index) {
|
|
|
2552
2511
|
if (!created) throw new Error("apikeys create returned no key value");
|
|
2553
2512
|
return created;
|
|
2554
2513
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
return true;
|
|
2559
|
-
} catch (err) {
|
|
2560
|
-
return !/does not exist|not found|404/i.test(err.message);
|
|
2561
|
-
}
|
|
2514
|
+
var resolved = /* @__PURE__ */ new Map();
|
|
2515
|
+
function forgetResolvedKeys() {
|
|
2516
|
+
resolved.clear();
|
|
2562
2517
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
logger.info({ index, appId }, "reusing the stored search-only API key");
|
|
2572
|
-
return { key: stored, source: "config" };
|
|
2518
|
+
function resolveKey(kind, index, appId, acls, description) {
|
|
2519
|
+
const cacheKey = `${kind}:${appId}:${index}`;
|
|
2520
|
+
const cached = resolved.get(cacheKey);
|
|
2521
|
+
if (cached) return cached;
|
|
2522
|
+
const pending = provisionKey(kind, index, appId, acls, description).catch(
|
|
2523
|
+
(err) => {
|
|
2524
|
+
resolved.delete(cacheKey);
|
|
2525
|
+
throw err;
|
|
2573
2526
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2527
|
+
);
|
|
2528
|
+
resolved.set(cacheKey, pending);
|
|
2529
|
+
return pending;
|
|
2530
|
+
}
|
|
2531
|
+
async function provisionKey(kind, index, appId, acls, description) {
|
|
2532
|
+
const stored = await readStoredKey(kind, index, appId);
|
|
2533
|
+
if (stored) {
|
|
2534
|
+
logger.info({ kind, index, appId }, "reusing the stored API key");
|
|
2535
|
+
return { key: stored, source: "keychain" };
|
|
2579
2536
|
}
|
|
2580
|
-
const key = await
|
|
2581
|
-
await
|
|
2537
|
+
const key = await createKey(index, acls, description);
|
|
2538
|
+
await storeKey(kind, index, appId, key);
|
|
2582
2539
|
return { key, source: "created" };
|
|
2583
2540
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2541
|
+
function resolveWriteKey(index, appId) {
|
|
2542
|
+
return resolveKey(
|
|
2543
|
+
"write",
|
|
2544
|
+
index,
|
|
2545
|
+
appId,
|
|
2546
|
+
WRITE_ACLS,
|
|
2547
|
+
`Algolia Wizard write key for ${index} index`
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
async function resolveSearchOnlyKey(index, appId, envKey) {
|
|
2551
|
+
if (envKey) return { key: envKey, source: "env" };
|
|
2552
|
+
return resolveKey(
|
|
2553
|
+
"search",
|
|
2554
|
+
index,
|
|
2555
|
+
appId,
|
|
2556
|
+
["search"],
|
|
2557
|
+
`Algolia Wizard search-only key for ${index} index`
|
|
2558
|
+
);
|
|
2593
2559
|
}
|
|
2594
2560
|
|
|
2595
2561
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2596
2562
|
var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
|
|
2597
2563
|
var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
|
|
2564
|
+
var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
|
|
2598
2565
|
function appendEnv(content, entries) {
|
|
2599
2566
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2600
2567
|
const lines = entries.map(([name, value]) => `${name}=${value}
|
|
@@ -2604,9 +2571,16 @@ function appendEnv(content, entries) {
|
|
|
2604
2571
|
function hasEnv(content, name) {
|
|
2605
2572
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
2606
2573
|
}
|
|
2574
|
+
function upsertEnv(content, name, value) {
|
|
2575
|
+
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
2576
|
+
return content.replace(
|
|
2577
|
+
new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "m"),
|
|
2578
|
+
`${name}=${value}`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2607
2581
|
function writeCredentialsTool(ctx) {
|
|
2608
2582
|
return tool6({
|
|
2609
|
-
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 (e.g. ".env").
|
|
2583
|
+
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 (e.g. ".env"). Any name the file already defines is left untouched.`,
|
|
2610
2584
|
inputSchema: z12.object({
|
|
2611
2585
|
filePath: z12.string().describe(
|
|
2612
2586
|
'Path to the env file to write credentials into (e.g. ".env")'
|
|
@@ -2614,43 +2588,51 @@ function writeCredentialsTool(ctx) {
|
|
|
2614
2588
|
}),
|
|
2615
2589
|
execute: async ({ filePath }) => {
|
|
2616
2590
|
logger.info({ filePath }, "called writeCredentials tool");
|
|
2617
|
-
const
|
|
2618
|
-
if (
|
|
2591
|
+
const resolved2 = resolveInRoot(ctx, filePath);
|
|
2592
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2619
2593
|
const targetIndex = useWizard.getState().targetIndex;
|
|
2620
2594
|
if (!targetIndex) {
|
|
2621
2595
|
return "Error: no target index is set for this run, so a scoped write key cannot be provisioned.";
|
|
2622
2596
|
}
|
|
2623
|
-
let
|
|
2624
|
-
let
|
|
2625
|
-
try {
|
|
2626
|
-
appId = (await requireApplication()).id;
|
|
2627
|
-
writeKey = await resolveWriteKey(targetIndex);
|
|
2628
|
-
} catch (err) {
|
|
2629
|
-
return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
|
|
2630
|
-
}
|
|
2597
|
+
let existing = "";
|
|
2598
|
+
let present;
|
|
2631
2599
|
try {
|
|
2632
|
-
if (await hasSymlinkParent(ctx,
|
|
2633
|
-
return `Refused: ${
|
|
2600
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2601
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
2634
2602
|
}
|
|
2635
|
-
let existing = "";
|
|
2636
2603
|
try {
|
|
2637
|
-
existing = await readFile4(
|
|
2604
|
+
existing = await readFile4(resolved2.target, "utf8");
|
|
2638
2605
|
} catch (err) {
|
|
2639
2606
|
if (err.code !== "ENOENT") throw err;
|
|
2640
2607
|
}
|
|
2641
|
-
|
|
2608
|
+
present = [APP_ID_VAR, API_KEY_VAR].filter(
|
|
2642
2609
|
(name) => hasEnv(existing, name)
|
|
2643
2610
|
);
|
|
2644
|
-
|
|
2645
|
-
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
return `Error writing credentials to ${filePath}: ${err.message}`;
|
|
2613
|
+
}
|
|
2614
|
+
let credentials = [];
|
|
2615
|
+
if (present.length === 0) {
|
|
2616
|
+
try {
|
|
2617
|
+
const appId = (await requireApplication()).id;
|
|
2618
|
+
credentials = [
|
|
2619
|
+
[APP_ID_VAR, appId],
|
|
2620
|
+
[API_KEY_VAR, (await resolveWriteKey(targetIndex, appId)).key]
|
|
2621
|
+
];
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
|
|
2646
2624
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2625
|
+
}
|
|
2626
|
+
try {
|
|
2627
|
+
const updated = upsertEnv(
|
|
2628
|
+
appendEnv(existing, credentials),
|
|
2629
|
+
INDEX_NAME_VAR,
|
|
2630
|
+
targetIndex
|
|
2631
|
+
);
|
|
2632
|
+
await mkdir4(dirname5(resolved2.target), { recursive: true });
|
|
2633
|
+
await writeFile4(resolved2.target, updated, "utf8");
|
|
2634
|
+
const wrote = `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}`;
|
|
2635
|
+
return present.length > 0 ? `${wrote}. Skipped ${present.join(" and ")}: already defined there.` : wrote;
|
|
2654
2636
|
} catch (err) {
|
|
2655
2637
|
return `Error writing credentials to ${filePath}: ${err.message}`;
|
|
2656
2638
|
}
|
|
@@ -2698,8 +2680,8 @@ function searchFilesTool(ctx) {
|
|
|
2698
2680
|
if (query.length > MAX_QUERY_LENGTH) {
|
|
2699
2681
|
return `Refused: query exceeds ${MAX_QUERY_LENGTH} characters. Use a shorter pattern.`;
|
|
2700
2682
|
}
|
|
2701
|
-
const
|
|
2702
|
-
if (!
|
|
2683
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2684
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2703
2685
|
let re;
|
|
2704
2686
|
try {
|
|
2705
2687
|
re = new RegExp(query);
|
|
@@ -2707,7 +2689,7 @@ function searchFilesTool(ctx) {
|
|
|
2707
2689
|
return `Invalid regex: ${err.message}`;
|
|
2708
2690
|
}
|
|
2709
2691
|
const matches = [];
|
|
2710
|
-
for (const file of await walkFiles(
|
|
2692
|
+
for (const file of await walkFiles(resolved2.target)) {
|
|
2711
2693
|
let content;
|
|
2712
2694
|
try {
|
|
2713
2695
|
content = await readFile5(file, "utf8");
|
|
@@ -2865,19 +2847,19 @@ function runShellTool(ctx) {
|
|
|
2865
2847
|
if (++ctx.counts.shell > ctx.limits.shell) {
|
|
2866
2848
|
return `Refused: command limit (${ctx.limits.shell}) reached. Stop running commands and report what you have.`;
|
|
2867
2849
|
}
|
|
2868
|
-
const
|
|
2869
|
-
if (!
|
|
2870
|
-
logger.info({ command, cwd:
|
|
2871
|
-
const key = approvalKey(
|
|
2850
|
+
const resolved2 = resolveInRoot(ctx, cwd ?? ".");
|
|
2851
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2852
|
+
logger.info({ command, cwd: resolved2.target }, "called runShell tool");
|
|
2853
|
+
const key = approvalKey(resolved2.target, command);
|
|
2872
2854
|
const decision = ctx.shell.approved.has(key) ? "approve" : await ctx.shell.approve({
|
|
2873
2855
|
command,
|
|
2874
|
-
cwd:
|
|
2856
|
+
cwd: resolved2.target,
|
|
2875
2857
|
explanation
|
|
2876
2858
|
});
|
|
2877
2859
|
if (decision === "reject") {
|
|
2878
2860
|
ctx.shell.executions.push({
|
|
2879
2861
|
command,
|
|
2880
|
-
cwd:
|
|
2862
|
+
cwd: resolved2.target,
|
|
2881
2863
|
approved: false
|
|
2882
2864
|
});
|
|
2883
2865
|
logger.info({ command }, "runShell: user rejected the command");
|
|
@@ -2890,7 +2872,7 @@ function runShellTool(ctx) {
|
|
|
2890
2872
|
return {};
|
|
2891
2873
|
});
|
|
2892
2874
|
const run2 = await (ctx.shell.run ?? runShell)(command, {
|
|
2893
|
-
cwd:
|
|
2875
|
+
cwd: resolved2.target,
|
|
2894
2876
|
env,
|
|
2895
2877
|
timeoutMs: ctx.shell.timeoutMs
|
|
2896
2878
|
});
|
|
@@ -2906,7 +2888,7 @@ function runShellTool(ctx) {
|
|
|
2906
2888
|
await markInteraction();
|
|
2907
2889
|
ctx.shell.executions.push({
|
|
2908
2890
|
command,
|
|
2909
|
-
cwd:
|
|
2891
|
+
cwd: resolved2.target,
|
|
2910
2892
|
approved: true,
|
|
2911
2893
|
exitCode: run2.exitCode,
|
|
2912
2894
|
output: run2.output,
|
|
@@ -2997,13 +2979,13 @@ function generateRecordTool(ctx) {
|
|
|
2997
2979
|
}));
|
|
2998
2980
|
const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
2999
2981
|
const relPath = `${DATA_DIR}/${slug}.json`;
|
|
3000
|
-
const
|
|
3001
|
-
if (
|
|
3002
|
-
if (await hasSymlinkParent(ctx,
|
|
3003
|
-
return `Refused: ${
|
|
2982
|
+
const resolved2 = resolveInRoot(ctx, relPath);
|
|
2983
|
+
if (resolved2.ok === false) return resolved2.error;
|
|
2984
|
+
if (await hasSymlinkParent(ctx, resolved2.target)) {
|
|
2985
|
+
return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
|
|
3004
2986
|
}
|
|
3005
|
-
await mkdir5(dirname6(
|
|
3006
|
-
await writeFile5(
|
|
2987
|
+
await mkdir5(dirname6(resolved2.target), { recursive: true });
|
|
2988
|
+
await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
|
|
3007
2989
|
logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
|
|
3008
2990
|
return {
|
|
3009
2991
|
filePath: relPath,
|
|
@@ -3296,7 +3278,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3296
3278
|
// package.json
|
|
3297
3279
|
var package_default = {
|
|
3298
3280
|
name: "@algolia/wizard",
|
|
3299
|
-
version: "0.9.0-rc.87.
|
|
3281
|
+
version: "0.9.0-rc.87.86",
|
|
3300
3282
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3301
3283
|
type: "module",
|
|
3302
3284
|
engines: {
|
|
@@ -3347,6 +3329,7 @@ var package_default = {
|
|
|
3347
3329
|
"@hono/node-server": "^2.0.10",
|
|
3348
3330
|
"@segment/analytics-node": "^3.1.0",
|
|
3349
3331
|
ai: "^6.0.190",
|
|
3332
|
+
"cross-keychain": "^1.1.0",
|
|
3350
3333
|
dotenv: "^17.4.2",
|
|
3351
3334
|
hono: "^4.12.27",
|
|
3352
3335
|
ink: "^7.0.5",
|
|
@@ -4023,13 +4006,17 @@ function publicEnvPrefix(language) {
|
|
|
4023
4006
|
}
|
|
4024
4007
|
var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
|
|
4025
4008
|
var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
|
|
4009
|
+
var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
4026
4010
|
function appIdVar(language) {
|
|
4027
4011
|
return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
|
|
4028
4012
|
}
|
|
4029
4013
|
function searchKeyVar(language) {
|
|
4030
4014
|
return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
|
|
4031
4015
|
}
|
|
4032
|
-
function
|
|
4016
|
+
function searchIndexVar(language) {
|
|
4017
|
+
return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
|
|
4018
|
+
}
|
|
4019
|
+
function searchEnvVars(language, index, appId, searchKey) {
|
|
4033
4020
|
return [
|
|
4034
4021
|
{
|
|
4035
4022
|
name: appIdVar(language),
|
|
@@ -4038,12 +4025,21 @@ function searchEnvVars(language, appId, searchKey) {
|
|
|
4038
4025
|
{
|
|
4039
4026
|
name: searchKeyVar(language),
|
|
4040
4027
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4028
|
+
},
|
|
4029
|
+
// Wizard-supplied rather than written into the generated code, because an
|
|
4030
|
+
// agent that retypes the name (appending the project name, re-casing it)
|
|
4031
|
+
// leaves the UI querying an index that does not exist.
|
|
4032
|
+
{
|
|
4033
|
+
name: searchIndexVar(language),
|
|
4034
|
+
value: index
|
|
4041
4035
|
}
|
|
4042
4036
|
];
|
|
4043
4037
|
}
|
|
4044
4038
|
function baseInstructions(input) {
|
|
4045
4039
|
return [
|
|
4046
|
-
|
|
4040
|
+
// Agents have renamed this (e.g. appending the project name), which the
|
|
4041
|
+
// index-scoped keys then reject with a 403.
|
|
4042
|
+
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4047
4043
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4048
4044
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
4049
4045
|
`Commands run through a shell on ${process.platform}. Write commands that work there.`,
|
|
@@ -4089,6 +4085,7 @@ function ingestionInstructions(input) {
|
|
|
4089
4085
|
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
4090
4086
|
`Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
4091
4087
|
`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.`,
|
|
4088
|
+
`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.`,
|
|
4092
4089
|
"Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
|
|
4093
4090
|
"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.",
|
|
4094
4091
|
...algoliaClientDoc(input),
|
|
@@ -4110,8 +4107,9 @@ function searchInstructions(input) {
|
|
|
4110
4107
|
] : [
|
|
4111
4108
|
"No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
|
|
4112
4109
|
],
|
|
4113
|
-
`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 search input and results list against the
|
|
4114
|
-
|
|
4110
|
+
`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 search input and results list against the target index.`,
|
|
4111
|
+
`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.`,
|
|
4112
|
+
"Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
|
|
4115
4113
|
// The key is provisioned only after verification passes, so the agent never
|
|
4116
4114
|
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4117
4115
|
// decide whether a key already exists, and an agent-invented value there
|
|
@@ -4203,7 +4201,6 @@ function ingestOutcome(executions, ingestCommand) {
|
|
|
4203
4201
|
recordCount: parseIngestRecordCount(withCount[0]?.output ?? "")
|
|
4204
4202
|
};
|
|
4205
4203
|
}
|
|
4206
|
-
var PLACEHOLDER_WRITE_KEY = "ALGOLIA_WRITE_KEY_PLACEHOLDER";
|
|
4207
4204
|
function makeToolContext(worktree, env = async () => ({})) {
|
|
4208
4205
|
return createToolContext(
|
|
4209
4206
|
DEFAULT_TOOL_LIMITS,
|
|
@@ -4328,7 +4325,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4328
4325
|
appId,
|
|
4329
4326
|
// Names only: the search-only key is provisioned after verification, so
|
|
4330
4327
|
// every value here is still a placeholder when the agent reads them.
|
|
4331
|
-
searchEnvVars: searchEnvVars(language, appId),
|
|
4328
|
+
searchEnvVars: searchEnvVars(language, targetIndex, appId),
|
|
4332
4329
|
ingestDir: INGEST_DIR,
|
|
4333
4330
|
ingestionSource,
|
|
4334
4331
|
uploadFilePath,
|
|
@@ -4360,9 +4357,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4360
4357
|
let ingestRecordCount;
|
|
4361
4358
|
let ingestDurationMs;
|
|
4362
4359
|
let ingestOutcomeMessage;
|
|
4363
|
-
const
|
|
4364
|
-
|
|
4365
|
-
[
|
|
4360
|
+
const ingestKeyAppId = ingestAppId;
|
|
4361
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4362
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4363
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4364
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4366
4365
|
})) : void 0;
|
|
4367
4366
|
const searchTools = makeToolContext(worktree);
|
|
4368
4367
|
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
@@ -4482,14 +4481,14 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4482
4481
|
let searchKeyError;
|
|
4483
4482
|
if (appId) {
|
|
4484
4483
|
try {
|
|
4485
|
-
const
|
|
4484
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4486
4485
|
targetIndex,
|
|
4487
4486
|
appId,
|
|
4488
4487
|
envSearchKey
|
|
4489
4488
|
);
|
|
4490
|
-
searchKey =
|
|
4489
|
+
searchKey = resolved2.key;
|
|
4491
4490
|
summaries.push(
|
|
4492
|
-
|
|
4491
|
+
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}.`
|
|
4493
4492
|
);
|
|
4494
4493
|
} catch (err) {
|
|
4495
4494
|
searchKeyError = err.message;
|
|
@@ -4499,7 +4498,12 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4499
4498
|
);
|
|
4500
4499
|
}
|
|
4501
4500
|
}
|
|
4502
|
-
finalSearchEnvVars = searchEnvVars(
|
|
4501
|
+
finalSearchEnvVars = searchEnvVars(
|
|
4502
|
+
language,
|
|
4503
|
+
targetIndex,
|
|
4504
|
+
appId,
|
|
4505
|
+
searchKey
|
|
4506
|
+
);
|
|
4503
4507
|
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4504
4508
|
(v) => !v.value.startsWith("<")
|
|
4505
4509
|
);
|
|
@@ -4883,6 +4887,7 @@ import { join as join10 } from "node:path";
|
|
|
4883
4887
|
var KEEP = ["wizard.log"];
|
|
4884
4888
|
async function resetProjectState() {
|
|
4885
4889
|
const dir = stateDir();
|
|
4890
|
+
forgetResolvedKeys();
|
|
4886
4891
|
let entries;
|
|
4887
4892
|
try {
|
|
4888
4893
|
entries = await readdir4(dir);
|
|
@@ -4891,7 +4896,9 @@ async function resetProjectState() {
|
|
|
4891
4896
|
}
|
|
4892
4897
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
4893
4898
|
await Promise.all(
|
|
4894
|
-
targets.map(
|
|
4899
|
+
targets.map(
|
|
4900
|
+
(name) => rm2(join10(dir, name), { recursive: true, force: true })
|
|
4901
|
+
)
|
|
4895
4902
|
);
|
|
4896
4903
|
return { dir, removed: targets };
|
|
4897
4904
|
}
|
|
@@ -4899,6 +4906,7 @@ async function resetProjectState() {
|
|
|
4899
4906
|
// src/main.tsx
|
|
4900
4907
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
4901
4908
|
async function startup() {
|
|
4909
|
+
setProjectRoot(process.cwd());
|
|
4902
4910
|
let args;
|
|
4903
4911
|
try {
|
|
4904
4912
|
args = parseCliArgs(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@algolia/wizard",
|
|
3
|
-
"version": "0.9.0-rc.87.
|
|
3
|
+
"version": "0.9.0-rc.87.86",
|
|
4
4
|
"description": "Magically implement Algolia functionality in your codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@hono/node-server": "^2.0.10",
|
|
52
52
|
"@segment/analytics-node": "^3.1.0",
|
|
53
53
|
"ai": "^6.0.190",
|
|
54
|
+
"cross-keychain": "^1.1.0",
|
|
54
55
|
"dotenv": "^17.4.2",
|
|
55
56
|
"hono": "^4.12.27",
|
|
56
57
|
"ink": "^7.0.5",
|