@algolia/wizard 0.34.0 → 0.35.0-rc.125.253
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js
CHANGED
|
@@ -253,6 +253,7 @@ var useWizard = create((set, get) => ({
|
|
|
253
253
|
cliOutput: [],
|
|
254
254
|
targetIndex: null,
|
|
255
255
|
writtenFiles: [],
|
|
256
|
+
approvedCommands: /* @__PURE__ */ new Set(),
|
|
256
257
|
logs: [],
|
|
257
258
|
error: null,
|
|
258
259
|
inputReq: null,
|
|
@@ -348,6 +349,10 @@ var useWizard = create((set, get) => ({
|
|
|
348
349
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
349
350
|
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
350
351
|
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
352
|
+
isCommandApproved: (command, cwd) => get().approvedCommands.has(`${cwd}\0${command}`),
|
|
353
|
+
approveCommand: (command, cwd) => set((s) => ({
|
|
354
|
+
approvedCommands: new Set(s.approvedCommands).add(`${cwd}\0${command}`)
|
|
355
|
+
})),
|
|
351
356
|
logStart: (kind, name, input) => {
|
|
352
357
|
const id = nanoid();
|
|
353
358
|
set((s) => ({
|
|
@@ -396,6 +401,7 @@ var useWizard = create((set, get) => ({
|
|
|
396
401
|
cliOutput: [],
|
|
397
402
|
targetIndex: null,
|
|
398
403
|
writtenFiles: [],
|
|
404
|
+
approvedCommands: /* @__PURE__ */ new Set(),
|
|
399
405
|
logs: [],
|
|
400
406
|
error: null,
|
|
401
407
|
inputReq: null,
|
|
@@ -1122,7 +1128,7 @@ function PromptInput() {
|
|
|
1122
1128
|
}
|
|
1123
1129
|
|
|
1124
1130
|
// src/workflows/default.ts
|
|
1125
|
-
import { z as
|
|
1131
|
+
import { z as z29 } from "zod";
|
|
1126
1132
|
|
|
1127
1133
|
// src/core/orchestrator.ts
|
|
1128
1134
|
import "zod";
|
|
@@ -1646,8 +1652,8 @@ var selectIndexStep = async (ctx) => {
|
|
|
1646
1652
|
};
|
|
1647
1653
|
|
|
1648
1654
|
// src/lib/agent.ts
|
|
1649
|
-
import { ToolLoopAgent, hasToolCall, Output as
|
|
1650
|
-
import { createAnthropic as
|
|
1655
|
+
import { ToolLoopAgent, hasToolCall, Output as Output3 } from "ai";
|
|
1656
|
+
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
1651
1657
|
import "zod";
|
|
1652
1658
|
|
|
1653
1659
|
// src/lib/tools/index.ts
|
|
@@ -1690,17 +1696,23 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
1690
1696
|
// src/lib/tools/listFiles.ts
|
|
1691
1697
|
function listFilesTool(ctx) {
|
|
1692
1698
|
return tool({
|
|
1693
|
-
description:
|
|
1694
|
-
inputSchema: z5.object(
|
|
1695
|
-
|
|
1696
|
-
|
|
1699
|
+
description: 'List files in a directory (default: the current working directory). Pass path to list a subdirectory directly \u2014 e.g. "packages/api" \u2014 without first changeDirectory-ing into it.',
|
|
1700
|
+
inputSchema: z5.object({
|
|
1701
|
+
path: z5.string().optional().describe("Directory to list, relative to cwd (default: cwd)")
|
|
1702
|
+
}),
|
|
1703
|
+
execute: async ({ path = "." }) => {
|
|
1704
|
+
logger.info({ path }, "called listFiles tool");
|
|
1697
1705
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
1698
1706
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
1699
1707
|
}
|
|
1700
|
-
const resolved2 = resolveInRoot(ctx,
|
|
1708
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
1701
1709
|
if (!resolved2.ok) return resolved2.error;
|
|
1702
|
-
|
|
1703
|
-
|
|
1710
|
+
try {
|
|
1711
|
+
const entries = await readdir(resolved2.target, { withFileTypes: true });
|
|
1712
|
+
return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
return `Error listing ${path}: ${err.message}`;
|
|
1715
|
+
}
|
|
1704
1716
|
}
|
|
1705
1717
|
});
|
|
1706
1718
|
}
|
|
@@ -2072,8 +2084,7 @@ function resolveWriteKey(index, appId) {
|
|
|
2072
2084
|
`Algolia Wizard write key for ${index} index`
|
|
2073
2085
|
);
|
|
2074
2086
|
}
|
|
2075
|
-
async function resolveSearchOnlyKey(index, appId
|
|
2076
|
-
if (envKey) return { key: envKey, source: "env" };
|
|
2087
|
+
async function resolveSearchOnlyKey(index, appId) {
|
|
2077
2088
|
return resolveKey(
|
|
2078
2089
|
"search",
|
|
2079
2090
|
index,
|
|
@@ -2112,6 +2123,12 @@ function isIgnoredByRule(root, relPath) {
|
|
|
2112
2123
|
function isTracked(root, relPath) {
|
|
2113
2124
|
return gitSucceeds(root, ["ls-files", "--error-unmatch", "--", relPath]);
|
|
2114
2125
|
}
|
|
2126
|
+
async function gitIgnoreStatus(root, target) {
|
|
2127
|
+
const { ignoredByRule, tracked } = await inspect(root, target);
|
|
2128
|
+
if (ignoredByRule === void 0) return "unknown";
|
|
2129
|
+
if (tracked) return "tracked";
|
|
2130
|
+
return ignoredByRule ? "covered" : "needsRule";
|
|
2131
|
+
}
|
|
2115
2132
|
async function inspect(root, target) {
|
|
2116
2133
|
const relPath = relative2(root, target);
|
|
2117
2134
|
if (!relPath || relPath.startsWith("..")) {
|
|
@@ -2157,31 +2174,9 @@ async function ensureGitIgnored(root, target) {
|
|
|
2157
2174
|
}
|
|
2158
2175
|
|
|
2159
2176
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
2160
|
-
var APP_ID_VAR = "
|
|
2161
|
-
var API_KEY_VAR = "
|
|
2177
|
+
var APP_ID_VAR = "ALGOLIA_APP_ID";
|
|
2178
|
+
var API_KEY_VAR = "ALGOLIA_WRITE_KEY";
|
|
2162
2179
|
var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
|
|
2163
|
-
var PUBLIC_APP_ID_SUFFIX = "ALGOLIA_APP_ID";
|
|
2164
|
-
var PUBLIC_SEARCH_KEY_SUFFIX = "ALGOLIA_SEARCH_KEY";
|
|
2165
|
-
var PUBLIC_INDEX_NAME_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
2166
|
-
function publicAppIdVar(prefix) {
|
|
2167
|
-
return `${prefix}${PUBLIC_APP_ID_SUFFIX}`;
|
|
2168
|
-
}
|
|
2169
|
-
function publicSearchKeyVar(prefix) {
|
|
2170
|
-
return `${prefix}${PUBLIC_SEARCH_KEY_SUFFIX}`;
|
|
2171
|
-
}
|
|
2172
|
-
function publicIndexNameVar(prefix) {
|
|
2173
|
-
return `${prefix}${PUBLIC_INDEX_NAME_SUFFIX}`;
|
|
2174
|
-
}
|
|
2175
|
-
function publicSearchEnvVars(prefix, index, appId, searchKey) {
|
|
2176
|
-
return [
|
|
2177
|
-
{ name: publicAppIdVar(prefix), value: appId ?? "<your-algolia-app-id>" },
|
|
2178
|
-
{
|
|
2179
|
-
name: publicSearchKeyVar(prefix),
|
|
2180
|
-
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
2181
|
-
},
|
|
2182
|
-
{ name: publicIndexNameVar(prefix), value: index }
|
|
2183
|
-
];
|
|
2184
|
-
}
|
|
2185
2180
|
function appendEnv(content, entries) {
|
|
2186
2181
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2187
2182
|
const lines = entries.map(([name, value]) => `${name}=${value}
|
|
@@ -2189,11 +2184,13 @@ function appendEnv(content, entries) {
|
|
|
2189
2184
|
return content + prefix + lines;
|
|
2190
2185
|
}
|
|
2191
2186
|
function hasEnv(content, name) {
|
|
2192
|
-
return new RegExp(`^(\\
|
|
2187
|
+
return new RegExp(`^([ \\t]*(?:export[ \\t]+)?${name})[ \\t]*=`, "m").test(
|
|
2188
|
+
content
|
|
2189
|
+
);
|
|
2193
2190
|
}
|
|
2194
2191
|
function readEnv(content, name) {
|
|
2195
2192
|
const found = content.match(
|
|
2196
|
-
new RegExp(
|
|
2193
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`, "m")
|
|
2197
2194
|
);
|
|
2198
2195
|
if (!found) return null;
|
|
2199
2196
|
const raw = found[1].trim();
|
|
@@ -2204,16 +2201,16 @@ function readEnv(content, name) {
|
|
|
2204
2201
|
function upsertEnv(content, name, value) {
|
|
2205
2202
|
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
2206
2203
|
return content.replace(
|
|
2207
|
-
new RegExp(
|
|
2204
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=.*$`, "gm"),
|
|
2208
2205
|
() => `${name}=${value}`
|
|
2209
2206
|
);
|
|
2210
2207
|
}
|
|
2211
2208
|
function writeCredentialsTool(ctx) {
|
|
2212
2209
|
return tool6({
|
|
2213
|
-
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself.`,
|
|
2210
|
+
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself. If the script or app that reads these credentials lives in a subdirectory (e.g. a package in a monorepo), an env file at the repo root is the wrong default \u2014 a script only loads env vars from its own directory (or one it's explicitly configured to read), so check every directory from the script's own up to the repo root, not just those two: its own directory, each ancestor in between (a shared workspace-level directory above the immediate package is common), and the root. Use whichever of those already holds real credentials; only fall back to the repo root when none of them do. Never invent a brand-new file in one of those directories when a real one already exists in another \u2014 that leaves the real one stale and the new one wrong. Listing just the script's own directory and the very top-level root is not enough to find a workspace-level file in between; check the intermediate ones too. If instructions describe a location that doesn't match the project you actually find (e.g. a path outside the repo, or a convention the project doesn't follow), don't stop and ask before doing anything \u2014 call this tool on the real, in-repo file the script actually reads (that's always the safe default), then note the mismatch afterward. Ending your turn with only a question and no call to this tool leaves the project unconfigured.`,
|
|
2214
2211
|
inputSchema: z13.object({
|
|
2215
2212
|
filePath: z13.string().describe(
|
|
2216
|
-
'Path to the env file to write credentials into (e.g. ".env")'
|
|
2213
|
+
'Path to the env file to write credentials into, relative to the repo root (e.g. ".env", or "packages/api/.env" when the consuming script lives in that package)'
|
|
2217
2214
|
)
|
|
2218
2215
|
}),
|
|
2219
2216
|
execute: async ({ filePath }) => {
|
|
@@ -2391,7 +2388,8 @@ function searchFilesTool(ctx) {
|
|
|
2391
2388
|
}
|
|
2392
2389
|
|
|
2393
2390
|
// src/lib/tools/runShell.ts
|
|
2394
|
-
import { tool as tool8 } from "ai";
|
|
2391
|
+
import { tool as tool8, generateText, Output } from "ai";
|
|
2392
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2395
2393
|
import z15 from "zod";
|
|
2396
2394
|
import { relative as relative4 } from "node:path";
|
|
2397
2395
|
|
|
@@ -2500,8 +2498,10 @@ function runShell(command, opts) {
|
|
|
2500
2498
|
// src/lib/tools/runShell.ts
|
|
2501
2499
|
function storeApproval(root) {
|
|
2502
2500
|
return async (req) => {
|
|
2501
|
+
const store = useWizard.getState();
|
|
2502
|
+
if (store.isCommandApproved(req.command, req.cwd)) return "approve";
|
|
2503
2503
|
const rel = relative4(root, req.cwd);
|
|
2504
|
-
const answer = await
|
|
2504
|
+
const answer = await store.requestUserInput({
|
|
2505
2505
|
prompt: "Run this command?",
|
|
2506
2506
|
promptType: "commandApproval",
|
|
2507
2507
|
options: [],
|
|
@@ -2510,20 +2510,195 @@ function storeApproval(root) {
|
|
|
2510
2510
|
cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
|
|
2511
2511
|
}
|
|
2512
2512
|
});
|
|
2513
|
-
|
|
2513
|
+
if (answer !== "approve") return "reject";
|
|
2514
|
+
store.approveCommand(req.command, req.cwd);
|
|
2515
|
+
return "approve";
|
|
2514
2516
|
};
|
|
2515
2517
|
}
|
|
2516
2518
|
var EXPLORATORY_COMMANDS = /* @__PURE__ */ new Set(["ls", "find", "tree", "dir"]);
|
|
2519
|
+
function commandSegments(command) {
|
|
2520
|
+
return command.split(/&&|;|\|/).map((segment) => segment.trim());
|
|
2521
|
+
}
|
|
2517
2522
|
function isExploratoryCommand(command) {
|
|
2518
|
-
return command
|
|
2523
|
+
return commandSegments(command).map((segment) => segment.split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
|
|
2524
|
+
}
|
|
2525
|
+
var READ_ONLY_BINARIES = /* @__PURE__ */ new Set([
|
|
2526
|
+
"cat",
|
|
2527
|
+
"head",
|
|
2528
|
+
"tail",
|
|
2529
|
+
"wc",
|
|
2530
|
+
"pwd",
|
|
2531
|
+
"echo",
|
|
2532
|
+
"date",
|
|
2533
|
+
"whoami",
|
|
2534
|
+
"hostname",
|
|
2535
|
+
"uname",
|
|
2536
|
+
"which",
|
|
2537
|
+
"file",
|
|
2538
|
+
"stat",
|
|
2539
|
+
"grep",
|
|
2540
|
+
"egrep",
|
|
2541
|
+
"fgrep",
|
|
2542
|
+
"rg",
|
|
2543
|
+
"diff"
|
|
2544
|
+
]);
|
|
2545
|
+
var READ_ONLY_NO_ARGS_BINARIES = /* @__PURE__ */ new Set(["env", "printenv"]);
|
|
2546
|
+
var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
2547
|
+
"status",
|
|
2548
|
+
"log",
|
|
2549
|
+
"diff",
|
|
2550
|
+
"show",
|
|
2551
|
+
"describe",
|
|
2552
|
+
"blame",
|
|
2553
|
+
"ls-files",
|
|
2554
|
+
"rev-parse",
|
|
2555
|
+
"cat-file",
|
|
2556
|
+
"shortlog",
|
|
2557
|
+
"ls-remote"
|
|
2558
|
+
]);
|
|
2559
|
+
var VERSION_CHECK_BINARIES = /* @__PURE__ */ new Set([
|
|
2560
|
+
"node",
|
|
2561
|
+
"tsc",
|
|
2562
|
+
"npm",
|
|
2563
|
+
"pnpm",
|
|
2564
|
+
"yarn",
|
|
2565
|
+
"python",
|
|
2566
|
+
"python3",
|
|
2567
|
+
"ruby",
|
|
2568
|
+
"go",
|
|
2569
|
+
"cargo",
|
|
2570
|
+
"rustc",
|
|
2571
|
+
"php",
|
|
2572
|
+
"composer",
|
|
2573
|
+
"java",
|
|
2574
|
+
"mvn",
|
|
2575
|
+
"gradle",
|
|
2576
|
+
"bundle",
|
|
2577
|
+
"git"
|
|
2578
|
+
]);
|
|
2579
|
+
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v", "-V"]);
|
|
2580
|
+
var FD_DUP_REDIRECT = /\d*>&\d+/g;
|
|
2581
|
+
function stripQuoted(segment) {
|
|
2582
|
+
let result = "";
|
|
2583
|
+
let quote = null;
|
|
2584
|
+
let i = 0;
|
|
2585
|
+
while (i < segment.length) {
|
|
2586
|
+
const char = segment[i];
|
|
2587
|
+
if (quote === "'") {
|
|
2588
|
+
if (char === "'") quote = null;
|
|
2589
|
+
i++;
|
|
2590
|
+
} else if (char === "\\") {
|
|
2591
|
+
i += 2;
|
|
2592
|
+
} else if (quote) {
|
|
2593
|
+
if (char === quote) quote = null;
|
|
2594
|
+
i++;
|
|
2595
|
+
} else if (char === '"' || char === "'") {
|
|
2596
|
+
quote = char;
|
|
2597
|
+
i++;
|
|
2598
|
+
} else {
|
|
2599
|
+
result += char;
|
|
2600
|
+
i++;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return quote === null ? result : segment;
|
|
2519
2604
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
return
|
|
2605
|
+
function hasFileRedirect(segment) {
|
|
2606
|
+
return stripQuoted(segment.replace(FD_DUP_REDIRECT, "")).includes(">");
|
|
2607
|
+
}
|
|
2608
|
+
function hasShellInjectionRisk(segment) {
|
|
2609
|
+
if (segment.includes("$(") || segment.includes("<(") || segment.includes("`")) {
|
|
2610
|
+
return true;
|
|
2611
|
+
}
|
|
2612
|
+
return stripQuoted(segment.replace(FD_DUP_REDIRECT, "")).includes("&");
|
|
2613
|
+
}
|
|
2614
|
+
function fastPathSegmentSafety(segment) {
|
|
2615
|
+
if (!segment) return true;
|
|
2616
|
+
if (hasFileRedirect(segment)) return false;
|
|
2617
|
+
if (hasShellInjectionRisk(segment)) return false;
|
|
2618
|
+
const [cmd0, ...rest] = segment.split(/\s+/);
|
|
2619
|
+
if (cmd0 === void 0) return true;
|
|
2620
|
+
if (VERSION_CHECK_BINARIES.has(cmd0) && rest.length === 1 && VERSION_FLAGS.has(rest[0])) {
|
|
2621
|
+
return true;
|
|
2622
|
+
}
|
|
2623
|
+
if (READ_ONLY_BINARIES.has(cmd0)) return true;
|
|
2624
|
+
if (READ_ONLY_NO_ARGS_BINARIES.has(cmd0)) {
|
|
2625
|
+
return rest.length === 0 ? true : void 0;
|
|
2626
|
+
}
|
|
2627
|
+
if (cmd0 === "git") {
|
|
2628
|
+
return GIT_READ_ONLY_SUBCOMMANDS.has(rest[0] ?? "") ? true : void 0;
|
|
2629
|
+
}
|
|
2630
|
+
return void 0;
|
|
2631
|
+
}
|
|
2632
|
+
function fastPathSafety(command) {
|
|
2633
|
+
const results = commandSegments(command).map(fastPathSegmentSafety);
|
|
2634
|
+
if (results.some((r) => r === false)) return false;
|
|
2635
|
+
if (results.every((r) => r === true)) return true;
|
|
2636
|
+
return void 0;
|
|
2637
|
+
}
|
|
2638
|
+
var CLASSIFIER_MODEL = "claude-haiku-4-5";
|
|
2639
|
+
var commandSafetySchema = z15.object({
|
|
2640
|
+
safe: z15.boolean(),
|
|
2641
|
+
reason: z15.string().describe("One short sentence explaining the verdict.")
|
|
2642
|
+
});
|
|
2643
|
+
function defaultCreateModel() {
|
|
2644
|
+
const token = getAuthToken();
|
|
2645
|
+
if (!token) {
|
|
2646
|
+
throw new Error("Not authenticated: no user token available");
|
|
2526
2647
|
}
|
|
2648
|
+
return createAnthropic({
|
|
2649
|
+
apiKey: token,
|
|
2650
|
+
baseURL: PROXY_BASE_URL,
|
|
2651
|
+
fetch: proxyFetch
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
function approvedCommandHistory(approvedCommands) {
|
|
2655
|
+
return Array.from(approvedCommands).map((entry) => {
|
|
2656
|
+
const sep2 = entry.indexOf("\0");
|
|
2657
|
+
return { cwd: entry.slice(0, sep2), command: entry.slice(sep2 + 1) };
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
async function classifyCommandSafety(createModel, command, cwd, explanation, approvedHistory) {
|
|
2661
|
+
try {
|
|
2662
|
+
const anthropic = createModel();
|
|
2663
|
+
const { output } = await generateText({
|
|
2664
|
+
model: anthropic(CLASSIFIER_MODEL),
|
|
2665
|
+
temperature: 0,
|
|
2666
|
+
output: Output.object({ schema: commandSafetySchema }),
|
|
2667
|
+
prompt: [
|
|
2668
|
+
"A coding agent wants to run this shell command in a user's project without asking for approval first. The command may be in any programming language or ecosystem.",
|
|
2669
|
+
"Judge it SAFE only if it cannot modify, delete, move, rename, or install anything the project cares about, cannot push/publish/deploy/commit anything, and cannot make a network request that changes remote state.",
|
|
2670
|
+
"Three broad categories are safe, and most commands you will see fall into one of them:",
|
|
2671
|
+
"(1) Reporting or listing existing state, with no side effect \u2014 e.g. `git status`, `npm ls`, `npm outdated`, `pip show`, `pip freeze`, `cargo tree`, `docker ps`, `docker images`, `kubectl get pods`, `terraform state list`. This includes a read-only GET-style query to a remote registry or API that only fetches public metadata and changes nothing remote (e.g. `npm view <package>`, `pip index versions <package>`, `curl` with no -X/--request other than GET and no -d/--data) \u2014 safe because nothing changes, not because it stays local.",
|
|
2672
|
+
'(2) A "preview" or "check" mode that reports what a change WOULD do, or reports a problem, without making the change. This is a general pattern, not a fixed list \u2014 ANY command that takes a dry-run/check/plan/validate/diff/list-only flag is safe when that flag is present, regardless of whether the same flag would normally appear on a "safe" kind of command: `terraform plan`, `terraform validate`, `terraform fmt -check`, `black --check`, `prettier --check`, `isort --check`, `gofmt -l` (list-only), `stylelint` with no `--fix`, and equally `git push --dry-run`, `npm publish --dry-run`, `kubectl apply --dry-run=client` \u2014 these last three are otherwise-unsafe operations (push, publish, cluster changes) that the dry-run flag turns into a report. The corresponding command WITHOUT that flag (e.g. `terraform apply`, `black .`, `gofmt -w`, `git push`) is a different, unsafe command \u2014 the flag is what makes the difference, for any tool, not just the ones named here. This cuts both ways, so do not assume a bare invocation is the safe one: `cargo fmt`, `black`, `prettier`, `isort`, `rustfmt`, and `terraform fmt` all REWRITE FILES by default and need an explicit check/diff/dry-run flag to become safe, while `gofmt`, `eslint`, `stylelint`, `rubocop`, and `ruff check` default to a safe check-only mode and need an explicit fix/write flag to become unsafe \u2014 the same-looking bare command is safe for one group and unsafe for the other, so judge each by what its own flags actually say, not by resemblance to a tool you already judged.',
|
|
2673
|
+
"(3) Running the project's own tests, type checker, linter, or build/compile step (e.g. `npm run build`, `yarn build`, `vite build`, `webpack`, `tsc`, `cargo build`, `go build`, `mvn compile`), as long as it is not passed an autofix/write/update flag (e.g. --fix, -u, --write, rubocop -a) \u2014 this holds even though the point of a build step is to write compiled output to a build/dist/target directory inside the project (e.g. a target/, build/, dist/, or __pycache__ directory): writing that output does not make the command unsafe. It stops being safe the moment the command chain goes past building \u2014 a step that also deploys, publishes, uploads, or pushes the build (e.g. `next build && vercel deploy`, `npm run build && npm publish`) is unsafe for that later segment even though the build segment itself is fine; judge each chained segment on its own, same as elsewhere in this list.",
|
|
2674
|
+
"Deleting or removing anything is unsafe, even something in a cache or build directory (e.g. `rm -rf __pycache__`, `cargo clean`, `git clean`), and even if the command otherwise fits one of the three categories above. Installing, uninstalling, or upgrading a dependency, changing a database schema, or writing to a path outside the project is also unsafe. A command chained with && or ; is only safe if every part of the chain is safe on its own.",
|
|
2675
|
+
"One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe.",
|
|
2676
|
+
...approvedHistory.length > 0 ? [
|
|
2677
|
+
"The user has already explicitly approved these exact commands earlier in this session (working directory in parentheses, then the command):",
|
|
2678
|
+
approvedHistory.map((h) => `- (${h.cwd}) ${h.command}`).join("\n"),
|
|
2679
|
+
"If the new command performs the same action and side-effect profile as one of these \u2014 differing only in a trivial way, such as a different file path, package name, or argument value that does not change what kind of action it is \u2014 judge it SAFE on that precedent, even if it would not otherwise fit categories (1)-(3) above. Do not stretch this to a command that merely shares a binary name, or superficially resembles one on the list while doing something riskier or of a different kind (e.g. an approved `rm build/tmp.log` does not license a new `rm -rf src/`, and an approved `git push origin feature-x` does not license `git push --force`)."
|
|
2680
|
+
] : [],
|
|
2681
|
+
`Command: ${command}`,
|
|
2682
|
+
`Working directory: ${cwd}`,
|
|
2683
|
+
`Stated purpose: ${explanation}`
|
|
2684
|
+
].join("\n")
|
|
2685
|
+
});
|
|
2686
|
+
if (!output.safe) {
|
|
2687
|
+
logger.info(
|
|
2688
|
+
{ command, reason: output.reason },
|
|
2689
|
+
"runShell: classifier judged command unsafe, requiring approval"
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
return output.safe;
|
|
2693
|
+
} catch (err) {
|
|
2694
|
+
logger.warn(
|
|
2695
|
+
{ err, command },
|
|
2696
|
+
"runShell: command safety classifier failed, requiring approval"
|
|
2697
|
+
);
|
|
2698
|
+
return false;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
async function runAndRecord(ctx, command, cwd) {
|
|
2527
2702
|
useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
|
|
2528
2703
|
const env = await ctx.shell.env().catch((err) => {
|
|
2529
2704
|
logger.warn({ err, command }, "runShell: could not resolve command env");
|
|
@@ -2559,9 +2734,18 @@ async function approveAndRun(ctx, command, cwd, explanation) {
|
|
|
2559
2734
|
output: run2.output
|
|
2560
2735
|
};
|
|
2561
2736
|
}
|
|
2562
|
-
function
|
|
2737
|
+
async function approveAndRun(ctx, command, cwd, explanation) {
|
|
2738
|
+
const decision = await ctx.shell.approve({ command, cwd, explanation });
|
|
2739
|
+
if (decision === "reject") {
|
|
2740
|
+
ctx.shell.executions.push({ command, cwd, approved: false });
|
|
2741
|
+
logger.info({ command }, "runShell: user rejected the command");
|
|
2742
|
+
return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
|
|
2743
|
+
}
|
|
2744
|
+
return runAndRecord(ctx, command, cwd);
|
|
2745
|
+
}
|
|
2746
|
+
function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
2563
2747
|
return tool8({
|
|
2564
|
-
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands.
|
|
2748
|
+
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. Do not use this to find or read files \u2014 use listFiles, searchFiles, and readFile instead of ls/find/cat/head/tail/grep/rg. This tool refuses ls/find/tree/dir outright; a read command like cat is not refused (some read-only commands run without approval, see below), but it's still the wrong tool for reading a file \u2014 the dedicated tools exist for that and won't count against this tool's command budget. The user approves any command that could change the project before it runs, so write a clear `explanation`. A command judged read-only (inspection, or running tests/typecheck/lint without an autofix flag, in any language) runs immediately without approval. If the user rejects a command, do not retry it \u2014 propose a different approach.",
|
|
2565
2749
|
inputSchema: z15.object({
|
|
2566
2750
|
command: z15.string().describe(
|
|
2567
2751
|
"The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
|
|
@@ -2580,9 +2764,29 @@ function runShellTool(ctx) {
|
|
|
2580
2764
|
const resolved2 = resolveInRoot(ctx, cwd ?? ".");
|
|
2581
2765
|
if (!resolved2.ok) return resolved2.error;
|
|
2582
2766
|
if (isExploratoryCommand(command)) {
|
|
2583
|
-
return "Refused: use listFiles or searchFiles to
|
|
2767
|
+
return "Refused: use listFiles or searchFiles to find files, and readFile to read one, instead of ls/find/tree/dir.";
|
|
2584
2768
|
}
|
|
2585
2769
|
logger.info({ command, cwd: resolved2.target }, "called runShell tool");
|
|
2770
|
+
const fast = fastPathSafety(command);
|
|
2771
|
+
let isSafe = fast;
|
|
2772
|
+
if (isSafe === void 0) {
|
|
2773
|
+
const store = useWizard.getState();
|
|
2774
|
+
isSafe = store.isCommandApproved(command, resolved2.target);
|
|
2775
|
+
if (!isSafe) {
|
|
2776
|
+
isSafe = await classifyCommandSafety(
|
|
2777
|
+
createModel,
|
|
2778
|
+
command,
|
|
2779
|
+
resolved2.target,
|
|
2780
|
+
explanation,
|
|
2781
|
+
approvedCommandHistory(store.approvedCommands)
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
if (isSafe) {
|
|
2786
|
+
return serializePrompt(
|
|
2787
|
+
() => runAndRecord(ctx, command, resolved2.target)
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2586
2790
|
return serializePrompt(
|
|
2587
2791
|
() => approveAndRun(ctx, command, resolved2.target, explanation)
|
|
2588
2792
|
);
|
|
@@ -2632,8 +2836,8 @@ function reviewScriptTool(ctx) {
|
|
|
2632
2836
|
}
|
|
2633
2837
|
|
|
2634
2838
|
// src/lib/tools/generateRecord.ts
|
|
2635
|
-
import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
|
|
2636
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2839
|
+
import { tool as tool10, generateText as generateText2, Output as Output2, NoObjectGeneratedError } from "ai";
|
|
2840
|
+
import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
|
|
2637
2841
|
import { nanoid as nanoid2 } from "nanoid";
|
|
2638
2842
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
|
|
2639
2843
|
import { dirname as dirname5 } from "node:path";
|
|
@@ -2643,18 +2847,18 @@ var RECORD_MODEL = "claude-haiku-4-5";
|
|
|
2643
2847
|
var MAX_RECORDS = 100;
|
|
2644
2848
|
var BATCH_SIZE = 10;
|
|
2645
2849
|
var MAX_BATCH_ATTEMPTS = 3;
|
|
2646
|
-
function
|
|
2850
|
+
function defaultCreateModel2() {
|
|
2647
2851
|
const token = getAuthToken();
|
|
2648
2852
|
if (!token) {
|
|
2649
2853
|
throw new Error("Not authenticated: no user token available");
|
|
2650
2854
|
}
|
|
2651
|
-
return
|
|
2855
|
+
return createAnthropic2({
|
|
2652
2856
|
apiKey: token,
|
|
2653
2857
|
baseURL: PROXY_BASE_URL,
|
|
2654
2858
|
fetch: proxyFetch
|
|
2655
2859
|
});
|
|
2656
2860
|
}
|
|
2657
|
-
function generateRecordTool(ctx, createModel =
|
|
2861
|
+
function generateRecordTool(ctx, createModel = defaultCreateModel2) {
|
|
2658
2862
|
return tool10({
|
|
2659
2863
|
description: "Generate realistic sample records for an entity and write them to a JSON file. 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.",
|
|
2660
2864
|
inputSchema: z17.object({
|
|
@@ -2675,9 +2879,9 @@ function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
|
2675
2879
|
let lastError;
|
|
2676
2880
|
for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt++) {
|
|
2677
2881
|
try {
|
|
2678
|
-
const { output } = await
|
|
2882
|
+
const { output } = await generateText2({
|
|
2679
2883
|
model: anthropic(RECORD_MODEL),
|
|
2680
|
-
output:
|
|
2884
|
+
output: Output2.object({
|
|
2681
2885
|
schema: z17.object({
|
|
2682
2886
|
records: z17.array(recordSchema).length(batchCount)
|
|
2683
2887
|
})
|
|
@@ -2842,7 +3046,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
2842
3046
|
if (!token) {
|
|
2843
3047
|
throw new Error("Not authenticated: no user token available");
|
|
2844
3048
|
}
|
|
2845
|
-
const anthropic =
|
|
3049
|
+
const anthropic = createAnthropic3({
|
|
2846
3050
|
apiKey: token,
|
|
2847
3051
|
baseURL: PROXY_BASE_URL,
|
|
2848
3052
|
fetch: proxyFetch
|
|
@@ -2879,7 +3083,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
2879
3083
|
}
|
|
2880
3084
|
};
|
|
2881
3085
|
}),
|
|
2882
|
-
output:
|
|
3086
|
+
output: Output3.object({ schema: req.outputSchema }),
|
|
2883
3087
|
tools: createTools(toolContext, {
|
|
2884
3088
|
output: req.outputSchema,
|
|
2885
3089
|
tools: req.tools
|
|
@@ -2967,6 +3171,7 @@ var detectLanguage = () => runAgent({
|
|
|
2967
3171
|
"Return the exact version",
|
|
2968
3172
|
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
2969
3173
|
`Determine publicEnvVarPrefix: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to the detected framework's known convention for exposing env vars to client-side code; use "" when the project has no such convention (e.g. a backend-only project).`,
|
|
3174
|
+
"A brand-new project has no existing env var usage to find \u2014 one or two targeted checks (e.g. .env/.env.example, or a grep for the bundler's public-prefix convention) are enough to confirm that. Do not keep searching once those turn up nothing; fall back to the framework convention immediately.",
|
|
2970
3175
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
2971
3176
|
"When done, call reportStatus"
|
|
2972
3177
|
],
|
|
@@ -3062,7 +3267,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3062
3267
|
// package.json
|
|
3063
3268
|
var package_default = {
|
|
3064
3269
|
name: "@algolia/wizard",
|
|
3065
|
-
version: "0.
|
|
3270
|
+
version: "0.35.0-rc.125.253",
|
|
3066
3271
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3067
3272
|
type: "module",
|
|
3068
3273
|
engines: {
|
|
@@ -3467,12 +3672,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
3467
3672
|
};
|
|
3468
3673
|
|
|
3469
3674
|
// src/actions/implement.ts
|
|
3470
|
-
import
|
|
3675
|
+
import z28 from "zod";
|
|
3676
|
+
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
3471
3677
|
import { join as join10, relative as relative6 } from "node:path";
|
|
3472
3678
|
|
|
3473
3679
|
// src/lib/git.ts
|
|
3474
3680
|
import { execFile as execFile2 } from "node:child_process";
|
|
3475
|
-
import { copyFile, mkdir as mkdir6,
|
|
3681
|
+
import { copyFile, mkdir as mkdir6, stat as stat3 } from "node:fs/promises";
|
|
3476
3682
|
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
|
|
3477
3683
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
3478
3684
|
function git(args) {
|
|
@@ -3526,42 +3732,6 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
3526
3732
|
}
|
|
3527
3733
|
return { ok: true, relPath };
|
|
3528
3734
|
}
|
|
3529
|
-
function hasEnvVar(content, name) {
|
|
3530
|
-
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
3531
|
-
}
|
|
3532
|
-
async function readEnvVar(repoRoot, name) {
|
|
3533
|
-
let content;
|
|
3534
|
-
try {
|
|
3535
|
-
content = await readFile7(join8(repoRoot, ".env"), "utf8");
|
|
3536
|
-
} catch (err) {
|
|
3537
|
-
if (err.code !== "ENOENT") throw err;
|
|
3538
|
-
return void 0;
|
|
3539
|
-
}
|
|
3540
|
-
const match = new RegExp(
|
|
3541
|
-
`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
|
|
3542
|
-
"m"
|
|
3543
|
-
).exec(content);
|
|
3544
|
-
if (!match) return void 0;
|
|
3545
|
-
const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
3546
|
-
if (!value || value.startsWith("<")) return void 0;
|
|
3547
|
-
return value;
|
|
3548
|
-
}
|
|
3549
|
-
async function writeSearchEnvValues(repoRoot, vars) {
|
|
3550
|
-
const target = join8(repoRoot, ".env");
|
|
3551
|
-
let existing = "";
|
|
3552
|
-
try {
|
|
3553
|
-
existing = await readFile7(target, "utf8");
|
|
3554
|
-
} catch (err) {
|
|
3555
|
-
if (err.code !== "ENOENT") throw err;
|
|
3556
|
-
}
|
|
3557
|
-
const missing = vars.filter((v) => !hasEnvVar(existing, v.name));
|
|
3558
|
-
if (missing.length === 0) return [];
|
|
3559
|
-
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
3560
|
-
const lines = missing.map(({ name, value }) => `${name}=${value}
|
|
3561
|
-
`).join("");
|
|
3562
|
-
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
3563
|
-
return missing.map((v) => v.name);
|
|
3564
|
-
}
|
|
3565
3735
|
function normalizeFindingPaths(findings) {
|
|
3566
3736
|
return {
|
|
3567
3737
|
...findings,
|
|
@@ -3642,46 +3812,36 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
3642
3812
|
return loadAlgoliaDoc("js");
|
|
3643
3813
|
}
|
|
3644
3814
|
|
|
3645
|
-
// src/actions/resolveEnvVarPrefix.ts
|
|
3646
|
-
import z28 from "zod";
|
|
3647
|
-
var resolveEnvVarPrefixSchema = z28.object({
|
|
3648
|
-
publicEnvVarPrefix: detectLanguageSchema.shape.publicEnvVarPrefix
|
|
3649
|
-
});
|
|
3650
|
-
var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
3651
|
-
instructions: [
|
|
3652
|
-
`The developer corrected the project's framework to "${frameworkName}".`,
|
|
3653
|
-
`Determine publicEnvVarPrefix for this framework: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to this framework's known convention for exposing env vars to client-side code; use "" when the framework has no such convention (e.g. a backend-only framework).`,
|
|
3654
|
-
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
3655
|
-
"When done, call reportStatus"
|
|
3656
|
-
],
|
|
3657
|
-
tools: ["listFiles", "changeDirectory", "readFile", "searchFiles"],
|
|
3658
|
-
outputSchema: resolveEnvVarPrefixSchema,
|
|
3659
|
-
modelSize: "small"
|
|
3660
|
-
});
|
|
3661
|
-
|
|
3662
3815
|
// src/actions/implement.ts
|
|
3663
|
-
var implementSchema =
|
|
3664
|
-
summary:
|
|
3665
|
-
ingestCommand:
|
|
3666
|
-
ingestScriptRan:
|
|
3667
|
-
ingestRecordCount:
|
|
3668
|
-
ingestDurationMs:
|
|
3669
|
-
ingestionSource:
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3816
|
+
var implementSchema = z28.object({
|
|
3817
|
+
summary: z28.string(),
|
|
3818
|
+
ingestCommand: z28.string().optional(),
|
|
3819
|
+
ingestScriptRan: z28.boolean().optional(),
|
|
3820
|
+
ingestRecordCount: z28.number().optional(),
|
|
3821
|
+
ingestDurationMs: z28.number().optional(),
|
|
3822
|
+
ingestionSource: z28.enum(["local", "fileUpload", "generated"]),
|
|
3823
|
+
searchConfig: z28.object({
|
|
3824
|
+
filePath: z28.string().optional(),
|
|
3825
|
+
vars: z28.array(
|
|
3826
|
+
z28.object({
|
|
3827
|
+
name: z28.string(),
|
|
3828
|
+
value: z28.string()
|
|
3829
|
+
})
|
|
3830
|
+
)
|
|
3831
|
+
}).optional()
|
|
3676
3832
|
});
|
|
3677
|
-
var implementationOutputSchema =
|
|
3678
|
-
summary:
|
|
3679
|
-
ingestCommand:
|
|
3833
|
+
var implementationOutputSchema = z28.object({
|
|
3834
|
+
summary: z28.string(),
|
|
3835
|
+
ingestCommand: z28.string().optional(),
|
|
3836
|
+
// Only for the search use case: the path of whatever module the agent
|
|
3837
|
+
// defined the Algolia config constants in, so the wizard can check it
|
|
3838
|
+
// won't end up gitignored (it's public, meant to be committed).
|
|
3839
|
+
searchConfigFile: z28.string().optional()
|
|
3680
3840
|
});
|
|
3681
|
-
var verificationOutputSchema =
|
|
3682
|
-
summary:
|
|
3683
|
-
sufficient:
|
|
3684
|
-
additionalInstructions:
|
|
3841
|
+
var verificationOutputSchema = z28.object({
|
|
3842
|
+
summary: z28.string(),
|
|
3843
|
+
sufficient: z28.boolean(),
|
|
3844
|
+
additionalInstructions: z28.string().optional()
|
|
3685
3845
|
});
|
|
3686
3846
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
3687
3847
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
@@ -3695,6 +3855,10 @@ function isJsProject(language) {
|
|
|
3695
3855
|
(name) => JS_LANGUAGES.some((js) => name.includes(js))
|
|
3696
3856
|
);
|
|
3697
3857
|
}
|
|
3858
|
+
var SEARCH_CONFIG_APP_ID = "ALGOLIA_APP_ID";
|
|
3859
|
+
var SEARCH_CONFIG_SEARCH_KEY = "ALGOLIA_SEARCH_API_KEY";
|
|
3860
|
+
var SEARCH_CONFIG_INDEX_NAME = "ALGOLIA_INDEX_NAME";
|
|
3861
|
+
var SEARCH_KEY_PLACEHOLDER = "<your-algolia-search-only-api-key>";
|
|
3698
3862
|
var UI_FRAMEWORKS = [
|
|
3699
3863
|
{ match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
|
|
3700
3864
|
{ match: ["react", "next"], target: "React", doc: "react" },
|
|
@@ -3757,7 +3921,7 @@ function algoliaClientDoc(input) {
|
|
|
3757
3921
|
function ingestionInstructions(input) {
|
|
3758
3922
|
return [
|
|
3759
3923
|
...input.confirmed && input.confirmed.length ? [
|
|
3760
|
-
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
3924
|
+
`Create an ingestion script under "${input.ingestDir}/" at the repo root. That directory already exists \u2014 writeFile creates any nested path itself, so never run a shell command just to create a directory.`,
|
|
3761
3925
|
`Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
3762
3926
|
`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.`,
|
|
3763
3927
|
`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.`,
|
|
@@ -3765,6 +3929,7 @@ function ingestionInstructions(input) {
|
|
|
3765
3929
|
"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.",
|
|
3766
3930
|
...algoliaClientDoc(input),
|
|
3767
3931
|
"Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
|
|
3932
|
+
`If the script loads its env vars from a file (e.g. via dotenv or an equivalent for its language) rather than the process environment directly, decide that up front and call writeCredentials on that file before you finish the script \u2014 do not wait to discover the need for it by having a writeFile call refused.`,
|
|
3768
3933
|
"When the script is finished, call reviewScript with its path and wait: running it writes records to a live index, so the developer reads it first. Do not run it before that call returns.",
|
|
3769
3934
|
'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
|
|
3770
3935
|
"The summary should be extremely concise.",
|
|
@@ -3786,17 +3951,14 @@ function searchInstructions(input) {
|
|
|
3786
3951
|
`Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
|
|
3787
3952
|
`Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
|
|
3788
3953
|
"If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
`Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
|
|
3795
|
-
// The wizard writes these exact names into .env right after this step.
|
|
3796
|
-
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
3954
|
+
"When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
|
|
3955
|
+
`Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
|
|
3956
|
+
`Set ${SEARCH_CONFIG_APP_ID} to "${input.appId}" and ${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
|
|
3957
|
+
input.searchKey ? `Set ${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
|
|
3958
|
+
'Report the repo-relative path of that module as "searchConfigFile" in your final status.',
|
|
3797
3959
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
3798
3960
|
"Match the styles of the application as closely as possible.",
|
|
3799
|
-
"The summary should be extremely concise; do not mention
|
|
3961
|
+
"The summary should be extremely concise; do not mention manual testing steps."
|
|
3800
3962
|
];
|
|
3801
3963
|
}
|
|
3802
3964
|
function verificationInstructions(input) {
|
|
@@ -3927,21 +4089,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
3927
4089
|
languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
|
|
3928
4090
|
frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
|
|
3929
4091
|
};
|
|
3930
|
-
const normalizeFrameworkName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3931
|
-
const confirmedPrimaryFramework = language.frameworks[0]?.name;
|
|
3932
|
-
const frameworkWasCorrected = confirmedPrimaryFramework !== void 0 && !scan.frameworks.some(
|
|
3933
|
-
(fw) => normalizeFrameworkName(fw.name) === normalizeFrameworkName(confirmedPrimaryFramework)
|
|
3934
|
-
);
|
|
3935
|
-
const publicEnvVarPrefixPromise = frameworkWasCorrected ? resolveEnvVarPrefix(confirmedPrimaryFramework).then(
|
|
3936
|
-
(r) => r.publicEnvVarPrefix,
|
|
3937
|
-
(err) => {
|
|
3938
|
-
logger.warn(
|
|
3939
|
-
{ err, framework: confirmedPrimaryFramework },
|
|
3940
|
-
"implement: could not re-resolve publicEnvVarPrefix after a framework correction; using the stale scan value"
|
|
3941
|
-
);
|
|
3942
|
-
return scan.publicEnvVarPrefix;
|
|
3943
|
-
}
|
|
3944
|
-
) : Promise.resolve(scan.publicEnvVarPrefix);
|
|
3945
4092
|
const selected = ctx.getStepOutput(
|
|
3946
4093
|
"select-index"
|
|
3947
4094
|
);
|
|
@@ -3992,6 +4139,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
3992
4139
|
const targetIndex = selected?.selection;
|
|
3993
4140
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
3994
4141
|
await assertGitRepoWithHead(repoRoot);
|
|
4142
|
+
if (useCases.includes("ingestion")) {
|
|
4143
|
+
await mkdir7(join10(repoRoot, INGEST_DIR), { recursive: true });
|
|
4144
|
+
}
|
|
3995
4145
|
const normalized = normalizeFindingPaths(findings);
|
|
3996
4146
|
const confirmed2 = normalized.confirmedEntities;
|
|
3997
4147
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4022,49 +4172,42 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4022
4172
|
);
|
|
4023
4173
|
}
|
|
4024
4174
|
}
|
|
4025
|
-
const
|
|
4175
|
+
const summaries = [];
|
|
4176
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4177
|
+
let searchKey;
|
|
4178
|
+
let searchKeyError;
|
|
4179
|
+
if (useCases.includes("search") && appId) {
|
|
4180
|
+
try {
|
|
4181
|
+
const resolved2 = await resolveSearchOnlyKey(targetIndex, appId);
|
|
4182
|
+
searchKey = resolved2.key;
|
|
4183
|
+
summaries.push(
|
|
4184
|
+
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}.`
|
|
4185
|
+
);
|
|
4186
|
+
} catch (err) {
|
|
4187
|
+
searchKeyError = err.message;
|
|
4188
|
+
summaries.push(
|
|
4189
|
+
`Could not provision a search-only Algolia API key (${searchKeyError}) \u2014 the search agent will scaffold a placeholder with a TODO for you to fill in.`
|
|
4190
|
+
);
|
|
4191
|
+
logger.warn(
|
|
4192
|
+
{ err: searchKeyError },
|
|
4193
|
+
"implement: could not provision a search-only API key; the agent will scaffold a placeholder"
|
|
4194
|
+
);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4026
4197
|
const input = {
|
|
4027
4198
|
findings: normalized,
|
|
4028
4199
|
confirmed: confirmed2,
|
|
4029
4200
|
searchLocation,
|
|
4030
4201
|
targetIndex,
|
|
4031
4202
|
language,
|
|
4032
|
-
publicEnvVarPrefix,
|
|
4033
4203
|
appId,
|
|
4034
|
-
|
|
4204
|
+
searchKey,
|
|
4205
|
+
searchKeyError,
|
|
4035
4206
|
ingestDir: INGEST_DIR,
|
|
4036
4207
|
ingestionSource,
|
|
4037
4208
|
uploadFilePath,
|
|
4038
4209
|
searchUiTarget: searchUiTarget(language)
|
|
4039
4210
|
};
|
|
4040
|
-
const summaries = [];
|
|
4041
|
-
if (uploadWarning) summaries.push(uploadWarning);
|
|
4042
|
-
let envSearchKey;
|
|
4043
|
-
let envAppIdMismatch = false;
|
|
4044
|
-
if (useCases.includes("search") && appId) {
|
|
4045
|
-
const envAppId = await readEnvVar(
|
|
4046
|
-
repoRoot,
|
|
4047
|
-
publicAppIdVar(publicEnvVarPrefix)
|
|
4048
|
-
);
|
|
4049
|
-
if (envAppId === appId) {
|
|
4050
|
-
envSearchKey = await readEnvVar(
|
|
4051
|
-
repoRoot,
|
|
4052
|
-
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4053
|
-
);
|
|
4054
|
-
} else if (envAppId) {
|
|
4055
|
-
envAppIdMismatch = true;
|
|
4056
|
-
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4057
|
-
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4058
|
-
summaries.push(
|
|
4059
|
-
`\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
|
|
4060
|
-
);
|
|
4061
|
-
logger.warn(
|
|
4062
|
-
{ envAppId, appId },
|
|
4063
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4064
|
-
);
|
|
4065
|
-
}
|
|
4066
|
-
}
|
|
4067
|
-
let finalSearchEnvVars = input.searchEnvVars;
|
|
4068
4211
|
let agentRuns = 0;
|
|
4069
4212
|
let ingestCommand;
|
|
4070
4213
|
let ingestScriptRan = false;
|
|
@@ -4174,6 +4317,7 @@ ${detail}` : ""}`
|
|
|
4174
4317
|
]
|
|
4175
4318
|
});
|
|
4176
4319
|
}
|
|
4320
|
+
let searchConfigFile;
|
|
4177
4321
|
if (useCases.includes("search")) {
|
|
4178
4322
|
let extraInstructions = [];
|
|
4179
4323
|
useWizard.getState().clearWrittenFiles();
|
|
@@ -4188,11 +4332,14 @@ ${detail}` : ""}`
|
|
|
4188
4332
|
"implement: retrying search implementation after failed verification"
|
|
4189
4333
|
);
|
|
4190
4334
|
}
|
|
4191
|
-
const
|
|
4335
|
+
const searchResult = await runImplementationUseCase(
|
|
4192
4336
|
"search",
|
|
4193
4337
|
extraInstructions
|
|
4194
4338
|
);
|
|
4195
|
-
summaries.push(formatSummary("search", summary));
|
|
4339
|
+
summaries.push(formatSummary("search", searchResult.summary));
|
|
4340
|
+
if (searchResult.searchConfigFile) {
|
|
4341
|
+
searchConfigFile = searchResult.searchConfigFile;
|
|
4342
|
+
}
|
|
4196
4343
|
const verification = await runVerificationUseCase();
|
|
4197
4344
|
summaries.push(formatSummary("verification", verification.summary));
|
|
4198
4345
|
if (verification.sufficient) {
|
|
@@ -4216,76 +4363,17 @@ ${detail}` : ""}`
|
|
|
4216
4363
|
}
|
|
4217
4364
|
extraInstructions = verificationRetryInstructions(verification);
|
|
4218
4365
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
if (appId) {
|
|
4222
|
-
try {
|
|
4223
|
-
const resolved2 = await resolveSearchOnlyKey(
|
|
4224
|
-
targetIndex,
|
|
4225
|
-
appId,
|
|
4226
|
-
envSearchKey
|
|
4227
|
-
);
|
|
4228
|
-
searchKey = resolved2.key;
|
|
4229
|
-
summaries.push(
|
|
4230
|
-
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}.`
|
|
4231
|
-
);
|
|
4232
|
-
} catch (err) {
|
|
4233
|
-
searchKeyError = err.message;
|
|
4234
|
-
logger.warn(
|
|
4235
|
-
{ err: searchKeyError },
|
|
4236
|
-
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4237
|
-
);
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
finalSearchEnvVars = publicSearchEnvVars(
|
|
4241
|
-
publicEnvVarPrefix,
|
|
4242
|
-
targetIndex,
|
|
4243
|
-
appId,
|
|
4244
|
-
searchKey
|
|
4245
|
-
);
|
|
4246
|
-
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4247
|
-
(v) => !v.value.startsWith("<")
|
|
4248
|
-
);
|
|
4249
|
-
if (resolvedSearchEnvVars.length > 0) {
|
|
4250
|
-
const written = await writeSearchEnvValues(
|
|
4366
|
+
if (searchConfigFile) {
|
|
4367
|
+
const ignoreStatus = await gitIgnoreStatus(
|
|
4251
4368
|
repoRoot,
|
|
4252
|
-
|
|
4369
|
+
join10(repoRoot, searchConfigFile)
|
|
4253
4370
|
);
|
|
4254
|
-
if (
|
|
4255
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4256
|
-
}
|
|
4257
|
-
const ignored = await ensureGitIgnored(repoRoot, join10(repoRoot, ".env"));
|
|
4258
|
-
if (ignored === "added") {
|
|
4259
|
-
summaries.push("Added .env to .gitignore.");
|
|
4260
|
-
} else if (ignored === "tracked") {
|
|
4371
|
+
if (ignoreStatus === "covered") {
|
|
4261
4372
|
summaries.push(
|
|
4262
|
-
|
|
4263
|
-
);
|
|
4264
|
-
}
|
|
4265
|
-
const stale = [];
|
|
4266
|
-
for (const v of resolvedSearchEnvVars) {
|
|
4267
|
-
if (written.includes(v.name)) continue;
|
|
4268
|
-
const current = await readEnvVar(repoRoot, v.name);
|
|
4269
|
-
if (current && current !== v.value) stale.push(v);
|
|
4270
|
-
}
|
|
4271
|
-
if (stale.length > 0 && !envAppIdMismatch) {
|
|
4272
|
-
summaries.push(
|
|
4273
|
-
`\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
|
|
4274
|
-
);
|
|
4275
|
-
logger.warn(
|
|
4276
|
-
{ vars: stale.map((v) => v.name) },
|
|
4277
|
-
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
4373
|
+
`\u26A0\uFE0F ${searchConfigFile} is gitignored, so this public, safe-to-share search config won't reach teammates or CI. Remove whatever .gitignore rule covers it.`
|
|
4278
4374
|
);
|
|
4279
4375
|
}
|
|
4280
4376
|
}
|
|
4281
|
-
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4282
|
-
(v) => v.value.startsWith("<")
|
|
4283
|
-
);
|
|
4284
|
-
if (unresolvedSearchEnvVars.length > 0) {
|
|
4285
|
-
summaries.push(
|
|
4286
|
-
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4287
|
-
);
|
|
4288
|
-
}
|
|
4289
4377
|
} else {
|
|
4290
4378
|
ctx.setUserInput("implementation", "success");
|
|
4291
4379
|
}
|
|
@@ -4298,7 +4386,19 @@ ${detail}` : ""}`
|
|
|
4298
4386
|
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4299
4387
|
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4300
4388
|
} : {},
|
|
4301
|
-
...useCases.includes("search") ? {
|
|
4389
|
+
...useCases.includes("search") ? {
|
|
4390
|
+
searchConfig: {
|
|
4391
|
+
filePath: searchConfigFile,
|
|
4392
|
+
vars: [
|
|
4393
|
+
{ name: SEARCH_CONFIG_APP_ID, value: appId ?? "" },
|
|
4394
|
+
{
|
|
4395
|
+
name: SEARCH_CONFIG_SEARCH_KEY,
|
|
4396
|
+
value: searchKey ?? SEARCH_KEY_PLACEHOLDER
|
|
4397
|
+
},
|
|
4398
|
+
{ name: SEARCH_CONFIG_INDEX_NAME, value: targetIndex }
|
|
4399
|
+
]
|
|
4400
|
+
}
|
|
4401
|
+
} : {}
|
|
4302
4402
|
};
|
|
4303
4403
|
}
|
|
4304
4404
|
|
|
@@ -4339,8 +4439,8 @@ var defaultWorkflow = {
|
|
|
4339
4439
|
defineStep({
|
|
4340
4440
|
id: "select-index",
|
|
4341
4441
|
title: "Set up index",
|
|
4342
|
-
outputSchema:
|
|
4343
|
-
selection:
|
|
4442
|
+
outputSchema: z29.object({
|
|
4443
|
+
selection: z29.string()
|
|
4344
4444
|
}),
|
|
4345
4445
|
run: (ctx) => selectIndexStep(ctx)
|
|
4346
4446
|
}),
|
|
@@ -5318,7 +5418,7 @@ function App() {
|
|
|
5318
5418
|
}
|
|
5319
5419
|
|
|
5320
5420
|
// src/lib/envAppId.ts
|
|
5321
|
-
import { readFile as
|
|
5421
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
5322
5422
|
import { join as join12 } from "node:path";
|
|
5323
5423
|
var ENV_FILES = [".env", ".env.local"];
|
|
5324
5424
|
var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
|
|
@@ -5326,7 +5426,7 @@ async function findEnvApplicationId(root = process.cwd()) {
|
|
|
5326
5426
|
for (const file of ENV_FILES) {
|
|
5327
5427
|
let content;
|
|
5328
5428
|
try {
|
|
5329
|
-
content = await
|
|
5429
|
+
content = await readFile7(join12(root, file), "utf8");
|
|
5330
5430
|
} catch (err) {
|
|
5331
5431
|
if (err.code !== "ENOENT") {
|
|
5332
5432
|
logger.warn(
|
|
@@ -5495,10 +5595,14 @@ var confirmFramework2 = {
|
|
|
5495
5595
|
var search = {
|
|
5496
5596
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
5497
5597
|
ingestionSource: "generated",
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5598
|
+
searchConfig: {
|
|
5599
|
+
filePath: "src/algolia.config.ts",
|
|
5600
|
+
vars: [
|
|
5601
|
+
{ name: "ALGOLIA_APP_ID", value: "SEEDAPPID" },
|
|
5602
|
+
{ name: "ALGOLIA_SEARCH_API_KEY", value: "seedsearchkey" },
|
|
5603
|
+
{ name: "ALGOLIA_INDEX_NAME", value: "wizard_seed_products" }
|
|
5604
|
+
]
|
|
5605
|
+
}
|
|
5502
5606
|
};
|
|
5503
5607
|
var review = {
|
|
5504
5608
|
summaryPoints: [
|
|
@@ -4,13 +4,18 @@ Install `instantsearch.js` (v4)
|
|
|
4
4
|
|
|
5
5
|
## Search client: always use the lite client
|
|
6
6
|
|
|
7
|
-
Import the client from `algoliasearch/lite` and alias it to `algoliasearch
|
|
7
|
+
Import the client from `algoliasearch/lite` and alias it to `algoliasearch`.
|
|
8
|
+
`ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
|
|
9
|
+
the constants exported from the project's shared client-side config module —
|
|
10
|
+
these are public values, safe to commit; never hardcode them inline or read
|
|
11
|
+
them from an env var.
|
|
8
12
|
|
|
9
13
|
```ts
|
|
10
14
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
15
|
+
import { ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY } from '<the project's shared config module>'
|
|
11
16
|
|
|
12
17
|
// Instantiate ONCE, outside components, with a stable reference.
|
|
13
|
-
const searchClient = algoliasearch(
|
|
18
|
+
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
14
19
|
```
|
|
15
20
|
|
|
16
21
|
## Vanilla (`instantsearch.js` v4)
|
|
@@ -19,11 +24,16 @@ const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
|
|
|
19
24
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
20
25
|
import instantsearch from 'instantsearch.js'
|
|
21
26
|
import { searchBox, hits } from 'instantsearch.js/es/widgets'
|
|
27
|
+
import {
|
|
28
|
+
ALGOLIA_APP_ID,
|
|
29
|
+
ALGOLIA_SEARCH_API_KEY,
|
|
30
|
+
ALGOLIA_INDEX_NAME,
|
|
31
|
+
} from '<the project's shared config module>'
|
|
22
32
|
|
|
23
|
-
const searchClient = algoliasearch(
|
|
33
|
+
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
24
34
|
|
|
25
35
|
const search = instantsearch({
|
|
26
|
-
indexName:
|
|
36
|
+
indexName: ALGOLIA_INDEX_NAME,
|
|
27
37
|
searchClient,
|
|
28
38
|
insights: true,
|
|
29
39
|
})
|
|
@@ -53,7 +63,7 @@ case, install `search-insights` from npm and pass its default export as
|
|
|
53
63
|
import aa from 'search-insights'
|
|
54
64
|
|
|
55
65
|
const search = instantsearch({
|
|
56
|
-
indexName:
|
|
66
|
+
indexName: ALGOLIA_INDEX_NAME,
|
|
57
67
|
searchClient,
|
|
58
68
|
insights: { insightsClient: aa },
|
|
59
69
|
})
|
|
@@ -4,13 +4,18 @@ Install `react-instantsearch` (v7)
|
|
|
4
4
|
|
|
5
5
|
## Search client: always use the lite client
|
|
6
6
|
|
|
7
|
-
Import the client from `algoliasearch/lite` and alias it to `algoliasearch
|
|
7
|
+
Import the client from `algoliasearch/lite` and alias it to `algoliasearch`.
|
|
8
|
+
`ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
|
|
9
|
+
the constants exported from the project's shared client-side config module —
|
|
10
|
+
these are public values, safe to commit; never hardcode them inline or read
|
|
11
|
+
them from an env var.
|
|
8
12
|
|
|
9
13
|
```ts
|
|
10
14
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
15
|
+
import { ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY } from '<the project's shared config module>'
|
|
11
16
|
|
|
12
17
|
// Instantiate ONCE, outside components, with a stable reference.
|
|
13
|
-
const searchClient = algoliasearch(
|
|
18
|
+
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
14
19
|
```
|
|
15
20
|
|
|
16
21
|
## React (`react-instantsearch` v7)
|
|
@@ -18,19 +23,19 @@ const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
|
|
|
18
23
|
```tsx
|
|
19
24
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
20
25
|
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
|
|
26
|
+
import {
|
|
27
|
+
ALGOLIA_APP_ID,
|
|
28
|
+
ALGOLIA_SEARCH_API_KEY,
|
|
29
|
+
ALGOLIA_INDEX_NAME,
|
|
30
|
+
} from '<the project's shared config module>'
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
// convention: NEXT_PUBLIC_* for Next.js, etc.)
|
|
24
|
-
const searchClient = algoliasearch(
|
|
25
|
-
import.meta.env.VITE_ALGOLIA_APP_ID,
|
|
26
|
-
import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY,
|
|
27
|
-
)
|
|
32
|
+
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
28
33
|
|
|
29
34
|
export function Search() {
|
|
30
35
|
return (
|
|
31
36
|
<InstantSearch
|
|
32
37
|
searchClient={searchClient}
|
|
33
|
-
indexName=
|
|
38
|
+
indexName={ALGOLIA_INDEX_NAME}
|
|
34
39
|
insights={true}
|
|
35
40
|
>
|
|
36
41
|
<SearchBox />
|
|
@@ -4,13 +4,18 @@ Install `vue-instantsearch` (v4)
|
|
|
4
4
|
|
|
5
5
|
## Search client: always use the lite client
|
|
6
6
|
|
|
7
|
-
Import the client from `algoliasearch/lite` and alias it to `algoliasearch
|
|
7
|
+
Import the client from `algoliasearch/lite` and alias it to `algoliasearch`.
|
|
8
|
+
`ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
|
|
9
|
+
the constants exported from the project's shared client-side config module —
|
|
10
|
+
these are public values, safe to commit; never hardcode them inline or read
|
|
11
|
+
them from an env var.
|
|
8
12
|
|
|
9
13
|
```ts
|
|
10
14
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
15
|
+
import { ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY } from '<the project's shared config module>'
|
|
11
16
|
|
|
12
17
|
// Instantiate ONCE, outside components, with a stable reference.
|
|
13
|
-
const searchClient = algoliasearch(
|
|
18
|
+
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
14
19
|
```
|
|
15
20
|
|
|
16
21
|
## Vue (`vue-instantsearch` v4)
|
|
@@ -19,7 +24,7 @@ const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
|
|
|
19
24
|
<template>
|
|
20
25
|
<ais-instant-search
|
|
21
26
|
:search-client="searchClient"
|
|
22
|
-
index-name="
|
|
27
|
+
:index-name="ALGOLIA_INDEX_NAME"
|
|
23
28
|
:insights="true"
|
|
24
29
|
>
|
|
25
30
|
<ais-search-box />
|
|
@@ -29,11 +34,13 @@ const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
|
|
|
29
34
|
|
|
30
35
|
<script setup>
|
|
31
36
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
37
|
+
import {
|
|
38
|
+
ALGOLIA_APP_ID,
|
|
39
|
+
ALGOLIA_SEARCH_API_KEY,
|
|
40
|
+
ALGOLIA_INDEX_NAME,
|
|
41
|
+
} from '<the project's shared config module>'
|
|
32
42
|
|
|
33
|
-
const searchClient = algoliasearch(
|
|
34
|
-
import.meta.env.VITE_ALGOLIA_APP_ID,
|
|
35
|
-
import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY,
|
|
36
|
-
)
|
|
43
|
+
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
37
44
|
</script>
|
|
38
45
|
```
|
|
39
46
|
|
|
@@ -5,10 +5,19 @@ route, or programmatic queries). For UI, prefer `instantsearch-setup-<framework>
|
|
|
5
5
|
|
|
6
6
|
## Client
|
|
7
7
|
|
|
8
|
+
`ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
|
|
9
|
+
the constants exported from the project's shared client-side config module —
|
|
10
|
+
these are public values, safe to commit; never hardcode them inline or read
|
|
11
|
+
them from an env var.
|
|
12
|
+
|
|
8
13
|
```ts
|
|
9
14
|
import { algoliasearch } from 'algoliasearch'
|
|
15
|
+
import {
|
|
16
|
+
ALGOLIA_APP_ID,
|
|
17
|
+
ALGOLIA_SEARCH_API_KEY,
|
|
18
|
+
} from '<the project's shared config module>'
|
|
10
19
|
|
|
11
|
-
const client = algoliasearch(
|
|
20
|
+
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
12
21
|
```
|
|
13
22
|
|
|
14
23
|
In v5 there is no `client.initIndex(...)`. Index methods take the index name as a
|
|
@@ -18,7 +27,7 @@ parameter on the client. Every method takes a single options object.
|
|
|
18
27
|
|
|
19
28
|
```ts
|
|
20
29
|
const { hits, nbHits } = await client.searchSingleIndex({
|
|
21
|
-
indexName:
|
|
30
|
+
indexName: ALGOLIA_INDEX_NAME,
|
|
22
31
|
searchParams: { query: 'shoes', hitsPerPage: 20, page: 0 },
|
|
23
32
|
})
|
|
24
33
|
```
|
|
@@ -28,7 +37,7 @@ const { hits, nbHits } = await client.searchSingleIndex({
|
|
|
28
37
|
```ts
|
|
29
38
|
const { results } = await client.search({
|
|
30
39
|
requests: [
|
|
31
|
-
{ indexName:
|
|
40
|
+
{ indexName: ALGOLIA_INDEX_NAME, query: 'shoes' },
|
|
32
41
|
{ indexName: 'OTHER_INDEX', query: 'shoes' },
|
|
33
42
|
],
|
|
34
43
|
})
|