@algolia/wizard 0.9.0-rc.72.60 → 0.9.0-rc.74.64
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
|
@@ -2221,7 +2221,12 @@ function writeCredentialsTool(ctx) {
|
|
|
2221
2221
|
// src/lib/tools/searchFiles.ts
|
|
2222
2222
|
import { tool as tool7 } from "ai";
|
|
2223
2223
|
import z10 from "zod";
|
|
2224
|
+
import { readdir as readdir3, readFile as readFile8 } from "node:fs/promises";
|
|
2225
|
+
import { join as join10 } from "node:path";
|
|
2226
|
+
|
|
2227
|
+
// src/lib/languages.ts
|
|
2224
2228
|
import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
|
|
2229
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
2225
2230
|
import { join as join9 } from "node:path";
|
|
2226
2231
|
|
|
2227
2232
|
// src/lib/tools/utils/packageManager.ts
|
|
@@ -2310,6 +2315,9 @@ var JAVASCRIPT = "javascript";
|
|
|
2310
2315
|
var CURATED_LANGUAGES = Object.values(
|
|
2311
2316
|
LANGUAGE_PROFILES
|
|
2312
2317
|
).map((profile) => profile.displayName);
|
|
2318
|
+
function isBackendLanguage(profile) {
|
|
2319
|
+
return profile.id !== JAVASCRIPT;
|
|
2320
|
+
}
|
|
2313
2321
|
function normalizeLanguageName(name) {
|
|
2314
2322
|
return name.toLowerCase().trim().replace(/[\s_-]+/g, "");
|
|
2315
2323
|
}
|
|
@@ -2345,14 +2353,150 @@ var ALLOWED_BINARIES = new Set(
|
|
|
2345
2353
|
...profile.verification.map((v) => v.argv[0])
|
|
2346
2354
|
])
|
|
2347
2355
|
);
|
|
2356
|
+
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
2357
|
+
function isWorktreeRelativeCommand(command) {
|
|
2358
|
+
return command.includes("/");
|
|
2359
|
+
}
|
|
2360
|
+
function withCommand(argv, command) {
|
|
2361
|
+
return [command, ...argv.slice(1)];
|
|
2362
|
+
}
|
|
2363
|
+
function resolveDeclaredManifest(root, packageManager) {
|
|
2364
|
+
const { dependency } = packageManager;
|
|
2365
|
+
if (dependency.mode !== "agent-declares" || !dependency.alternatives?.length) {
|
|
2366
|
+
return packageManager;
|
|
2367
|
+
}
|
|
2368
|
+
const present = [dependency.file, ...dependency.alternatives].find(
|
|
2369
|
+
(file) => existsSync2(join9(root, file))
|
|
2370
|
+
);
|
|
2371
|
+
if (!present || present === dependency.file) return packageManager;
|
|
2372
|
+
return { ...packageManager, dependency: { ...dependency, file: present } };
|
|
2373
|
+
}
|
|
2374
|
+
async function manifestPresent(root, manifest, listing) {
|
|
2375
|
+
if (!manifest.startsWith("*.")) return existsSync2(join9(root, manifest));
|
|
2376
|
+
if (!listing.entries) {
|
|
2377
|
+
const entries = await readdir2(root).catch(() => []);
|
|
2378
|
+
listing.entries = Array.isArray(entries) ? entries : [];
|
|
2379
|
+
}
|
|
2380
|
+
const suffix = manifest.slice(1);
|
|
2381
|
+
return listing.entries.some((e) => e.endsWith(suffix));
|
|
2382
|
+
}
|
|
2383
|
+
async function profileManifestPresent(root, profile, listing) {
|
|
2384
|
+
for (const manifest of profile.manifests) {
|
|
2385
|
+
if (await manifestPresent(root, manifest, listing)) return true;
|
|
2386
|
+
}
|
|
2387
|
+
return false;
|
|
2388
|
+
}
|
|
2389
|
+
async function detectProfilesFromManifests(root) {
|
|
2390
|
+
const listing = {};
|
|
2391
|
+
const found = [];
|
|
2392
|
+
for (const profile of Object.values(LANGUAGE_PROFILES)) {
|
|
2393
|
+
if (await profileManifestPresent(root, profile, listing)) found.push(profile);
|
|
2394
|
+
}
|
|
2395
|
+
return found;
|
|
2396
|
+
}
|
|
2397
|
+
async function hasProfileManifest(root, profile) {
|
|
2398
|
+
return profileManifestPresent(root, profile, {});
|
|
2399
|
+
}
|
|
2400
|
+
async function pickIngestionCandidates(root, confirmedNames) {
|
|
2401
|
+
const confirmed3 = confirmedNames.map((name) => resolveLanguageProfile(name)).filter((p) => p !== void 0);
|
|
2402
|
+
const onDisk = await detectProfilesFromManifests(root);
|
|
2403
|
+
const onDiskIds = new Set(onDisk.map((p) => p.id));
|
|
2404
|
+
const candidates = [
|
|
2405
|
+
...new Map(
|
|
2406
|
+
confirmed3.filter((p) => onDiskIds.has(p.id)).map((p) => [p.id, p])
|
|
2407
|
+
).values()
|
|
2408
|
+
];
|
|
2409
|
+
return { candidates, confirmed: confirmed3, onDisk };
|
|
2410
|
+
}
|
|
2411
|
+
async function resolveToolchain(root, profile) {
|
|
2412
|
+
const signals = (pm) => [
|
|
2413
|
+
...pm.lockfiles ?? [],
|
|
2414
|
+
...pm.detectFiles ?? []
|
|
2415
|
+
];
|
|
2416
|
+
const matched = profile.packageManagers.find(
|
|
2417
|
+
(pm) => signals(pm).some((f) => existsSync2(join9(root, f)))
|
|
2418
|
+
);
|
|
2419
|
+
const fallback = profile.packageManagers.find((pm) => signals(pm).length === 0) ?? profile.packageManagers[0];
|
|
2420
|
+
const packageManager = resolveDeclaredManifest(root, matched ?? fallback);
|
|
2421
|
+
let { installSteps, ingest } = packageManager;
|
|
2422
|
+
installSteps = installSteps.map(
|
|
2423
|
+
(step) => isWorktreeRelativeCommand(step.argv[0]) ? { ...step, argv: withCommand(step.argv, join9(root, step.argv[0])) } : step
|
|
2424
|
+
);
|
|
2425
|
+
if (ingest.kind === "auto" && isWorktreeRelativeCommand(ingest.argv[0])) {
|
|
2426
|
+
ingest = {
|
|
2427
|
+
...ingest,
|
|
2428
|
+
argv: withCommand(ingest.argv, join9(root, ingest.argv[0]))
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
if (profile.id === "javascript") {
|
|
2432
|
+
const pm = await detectPackageManager(root);
|
|
2433
|
+
if (JS_PACKAGE_MANAGERS.has(pm)) {
|
|
2434
|
+
installSteps = installSteps.map((step) => ({
|
|
2435
|
+
...step,
|
|
2436
|
+
argv: withCommand(step.argv, pm)
|
|
2437
|
+
}));
|
|
2438
|
+
if (pm === "bun" && ingest.kind === "auto") {
|
|
2439
|
+
ingest = { ...ingest, argv: withCommand(ingest.argv, "bun") };
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return { profile, packageManager, installSteps, ingest };
|
|
2444
|
+
}
|
|
2445
|
+
function resolveIngestArgv(ingest, entrypoint) {
|
|
2446
|
+
if (ingest.kind !== "auto") {
|
|
2447
|
+
throw new Error("resolveIngestArgv called for a manual-run toolchain");
|
|
2448
|
+
}
|
|
2449
|
+
return ingest.argv.map(
|
|
2450
|
+
(part) => part === ENTRYPOINT_TOKEN ? entrypoint : part
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
function describeIngestCommand(ingest, entrypoint) {
|
|
2454
|
+
if (ingest.kind !== "auto") return ingest.runCommand;
|
|
2455
|
+
return ingest.argv.map((part) => part === ENTRYPOINT_TOKEN ? shellQuote(entrypoint) : part).join(" ");
|
|
2456
|
+
}
|
|
2457
|
+
function ingestScriptDir(profile) {
|
|
2458
|
+
const parts = profile.ingestEntrypointExample.split("/");
|
|
2459
|
+
return parts.slice(0, -1).join("/") || ".";
|
|
2460
|
+
}
|
|
2461
|
+
function localSourceLimitation(root, profile) {
|
|
2462
|
+
const caveat = profile.localSourceCaveat;
|
|
2463
|
+
if (!caveat) return void 0;
|
|
2464
|
+
return existsSync2(join9(root, caveat.unless)) ? void 0 : caveat.message;
|
|
2465
|
+
}
|
|
2466
|
+
async function missingBuildTask(root, toolchain) {
|
|
2467
|
+
const { ingest, packageManager } = toolchain;
|
|
2468
|
+
if (ingest.kind !== "manual" || !ingest.requiresBuildTask) return void 0;
|
|
2469
|
+
if (packageManager.dependency.mode !== "agent-declares") return void 0;
|
|
2470
|
+
const buildFile = join9(root, packageManager.dependency.file);
|
|
2471
|
+
const contents = await readFile7(buildFile, "utf8").catch(() => void 0);
|
|
2472
|
+
if (contents === void 0) return void 0;
|
|
2473
|
+
return contents.includes(ingest.requiresBuildTask) ? void 0 : ingest.requiresBuildTask;
|
|
2474
|
+
}
|
|
2475
|
+
function sdkVersionPin(profile, packageManager) {
|
|
2476
|
+
return packageManager.sdkVersionPin ?? profile.sdk.versionPin;
|
|
2477
|
+
}
|
|
2478
|
+
function dependencyInstruction(toolchain) {
|
|
2479
|
+
const { profile, packageManager } = toolchain;
|
|
2480
|
+
const { packageName } = profile.sdk;
|
|
2481
|
+
const versionPin = sdkVersionPin(profile, packageManager);
|
|
2482
|
+
const also = profile.sdk.alsoRequires ? ` ${profile.sdk.alsoRequires}` : "";
|
|
2483
|
+
switch (packageManager.dependency.mode) {
|
|
2484
|
+
case "wizard-installs":
|
|
2485
|
+
return `The wizard installs ${packageName} ${versionPin} in the worktree after you finish \u2014 import it directly and do not edit dependency manifests for it.${also}`;
|
|
2486
|
+
case "code-imports":
|
|
2487
|
+
return `Import ${packageName} in the script; the wizard resolves and fetches it in the worktree after you finish. Do not edit dependency manifests by hand.${also}`;
|
|
2488
|
+
case "agent-declares":
|
|
2489
|
+
return `Declare ${packageName} ${versionPin} in "${packageManager.dependency.file}" (create the file if needed), plus any other dependency your script imports; the wizard installs them in the worktree after you finish.${also}`;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2348
2492
|
|
|
2349
2493
|
// src/lib/tools/searchFiles.ts
|
|
2350
2494
|
var MAX_QUERY_LENGTH = 1e3;
|
|
2351
2495
|
async function walkFiles(dir) {
|
|
2352
2496
|
const out = [];
|
|
2353
|
-
for (const e of await
|
|
2497
|
+
for (const e of await readdir3(dir, { withFileTypes: true })) {
|
|
2354
2498
|
if (e.name.startsWith(".") || ALL_SKIP_DIRS.has(e.name)) continue;
|
|
2355
|
-
const full =
|
|
2499
|
+
const full = join10(dir, e.name);
|
|
2356
2500
|
if (e.isDirectory()) out.push(...await walkFiles(full));
|
|
2357
2501
|
else if (e.isFile()) out.push(full);
|
|
2358
2502
|
}
|
|
@@ -2385,7 +2529,7 @@ function searchFilesTool(ctx) {
|
|
|
2385
2529
|
for (const file of await walkFiles(resolved.target)) {
|
|
2386
2530
|
let content;
|
|
2387
2531
|
try {
|
|
2388
|
-
content = await
|
|
2532
|
+
content = await readFile8(file, "utf8");
|
|
2389
2533
|
} catch {
|
|
2390
2534
|
continue;
|
|
2391
2535
|
}
|
|
@@ -2410,8 +2554,8 @@ import { tool as tool8 } from "ai";
|
|
|
2410
2554
|
import z11 from "zod";
|
|
2411
2555
|
|
|
2412
2556
|
// src/lib/tools/repoVerification.ts
|
|
2413
|
-
import { existsSync as
|
|
2414
|
-
import { join as
|
|
2557
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
2558
|
+
import { join as join11 } from "node:path";
|
|
2415
2559
|
|
|
2416
2560
|
// src/lib/tools/utils/runCommand.ts
|
|
2417
2561
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2500,7 +2644,7 @@ async function javascriptChecks() {
|
|
|
2500
2644
|
async function registryChecks(id) {
|
|
2501
2645
|
const profile = LANGUAGE_PROFILES[id];
|
|
2502
2646
|
const runnable = profile.verification.filter(
|
|
2503
|
-
(spec) => !spec.requiresFile ||
|
|
2647
|
+
(spec) => !spec.requiresFile || existsSync3(join11(process.cwd(), spec.requiresFile))
|
|
2504
2648
|
);
|
|
2505
2649
|
if (runnable.length === 0) {
|
|
2506
2650
|
return {
|
|
@@ -2899,6 +3043,7 @@ var MODE_CONFIG = {
|
|
|
2899
3043
|
"Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
|
|
2900
3044
|
"For each entity, return its name, the file path(s) where it is defined, and its indexable attribute keys (the fields a user would search or filter on).",
|
|
2901
3045
|
"Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
|
|
3046
|
+
"Entities live wherever the stack keeps them: TypeScript interfaces or a Prisma schema.",
|
|
2902
3047
|
"Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
|
|
2903
3048
|
"Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
|
|
2904
3049
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
@@ -2910,8 +3055,9 @@ var MODE_CONFIG = {
|
|
|
2910
3055
|
instructions: [
|
|
2911
3056
|
"Analyze the codebase to determine the single best location to add search UI functionality.",
|
|
2912
3057
|
"Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
|
|
2913
|
-
"
|
|
2914
|
-
|
|
3058
|
+
"It may be a client-side component or a server-rendered template \u2014 return whichever the project actually renders its UI from (e.g. /layouts/header.tsx, app/views/layouts/application.html.erb, templates/base.html, resources/views/layouts/app.blade.php, templates/base.html.twig).",
|
|
3059
|
+
"Return one file path as searchImplementationAnalysis.",
|
|
3060
|
+
'Use as few tools as possible, but do not guess. If the project renders no UI at all (an API-only service), say "unknown".',
|
|
2915
3061
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
2916
3062
|
"When done, call reportStatus"
|
|
2917
3063
|
],
|
|
@@ -2920,7 +3066,7 @@ var MODE_CONFIG = {
|
|
|
2920
3066
|
verification: {
|
|
2921
3067
|
instructions: [
|
|
2922
3068
|
"Analyze the codebase to determine which code-quality tools are available to validate changes.",
|
|
2923
|
-
"Look at
|
|
3069
|
+
"Look at the dependency manifest and config files for the project's languages: package.json scripts with tsconfig/eslint/prettier.",
|
|
2924
3070
|
'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
|
|
2925
3071
|
"Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
|
|
2926
3072
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
@@ -2948,7 +3094,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
2948
3094
|
// package.json
|
|
2949
3095
|
var package_default = {
|
|
2950
3096
|
name: "@algolia/wizard",
|
|
2951
|
-
version: "0.9.0-rc.
|
|
3097
|
+
version: "0.9.0-rc.74.64",
|
|
2952
3098
|
description: "Magically implement Algolia functionality in your codebase",
|
|
2953
3099
|
type: "module",
|
|
2954
3100
|
engines: {
|
|
@@ -2970,7 +3116,7 @@ var package_default = {
|
|
|
2970
3116
|
prepare: "husky",
|
|
2971
3117
|
prepublishOnly: "pnpm build",
|
|
2972
3118
|
reset: "tsx ./scripts/reset-state.ts",
|
|
2973
|
-
"test:
|
|
3119
|
+
"test:toolchains": "tsx ./scripts/verify-toolchains.ts",
|
|
2974
3120
|
"test:tools": "tsx ./tool-evals/toolEval.ts",
|
|
2975
3121
|
test: "vitest",
|
|
2976
3122
|
typecheck: "tsc --noEmit -p tsconfig.json"
|
|
@@ -3177,6 +3323,52 @@ function isSameFramework(a, b) {
|
|
|
3177
3323
|
const y = canonicalFrameworkName(b) ?? normalize(b);
|
|
3178
3324
|
return x !== "" && x === y;
|
|
3179
3325
|
}
|
|
3326
|
+
function resolveSearchStrategy(frameworkName, hasJavaScriptInStack) {
|
|
3327
|
+
const canonical = frameworkName ? canonicalFrameworkName(frameworkName) : void 0;
|
|
3328
|
+
const strategy = canonical ? STRATEGY_BY_NAME.get(canonical) : void 0;
|
|
3329
|
+
if (strategy) return strategy;
|
|
3330
|
+
return hasJavaScriptInStack ? "js" : "cdn-template";
|
|
3331
|
+
}
|
|
3332
|
+
function searchDocKey(strategy) {
|
|
3333
|
+
return strategy === "cdn-template" ? "templates" : strategy;
|
|
3334
|
+
}
|
|
3335
|
+
function bundlesJavaScript(strategy) {
|
|
3336
|
+
return strategy !== "cdn-template" && strategy !== "none";
|
|
3337
|
+
}
|
|
3338
|
+
function canScaffoldSearchUI(strategy) {
|
|
3339
|
+
return strategy !== "none";
|
|
3340
|
+
}
|
|
3341
|
+
var ENV_PREFIXES = [
|
|
3342
|
+
{ aliases: ["next", "nextjs"], prefix: "NEXT_PUBLIC_" },
|
|
3343
|
+
{ aliases: ["nuxt", "nuxtjs"], prefix: "NUXT_PUBLIC_" },
|
|
3344
|
+
{ aliases: ["astro"], prefix: "PUBLIC_" },
|
|
3345
|
+
{ aliases: ["vite"], prefix: "VITE_" }
|
|
3346
|
+
];
|
|
3347
|
+
var DEFAULT_ENV_PREFIX = "PUBLIC_";
|
|
3348
|
+
function publicEnvPrefix(frameworkNames, strategy) {
|
|
3349
|
+
if (!bundlesJavaScript(strategy)) return "";
|
|
3350
|
+
const present = new Set(frameworkNames.map(normalize));
|
|
3351
|
+
for (const { aliases, prefix } of ENV_PREFIXES) {
|
|
3352
|
+
if (aliases.some((alias) => present.has(alias))) return prefix;
|
|
3353
|
+
}
|
|
3354
|
+
return DEFAULT_ENV_PREFIX;
|
|
3355
|
+
}
|
|
3356
|
+
function describeSearchTarget(strategy, frameworkName) {
|
|
3357
|
+
switch (strategy) {
|
|
3358
|
+
case "react":
|
|
3359
|
+
return "React (react-instantsearch)";
|
|
3360
|
+
case "vue":
|
|
3361
|
+
return "Vue (vue-instantsearch)";
|
|
3362
|
+
case "angular":
|
|
3363
|
+
return "Angular (angular-instantsearch)";
|
|
3364
|
+
case "js":
|
|
3365
|
+
return "plain JavaScript (InstantSearch.js)";
|
|
3366
|
+
case "cdn-template":
|
|
3367
|
+
return `${frameworkName ?? "server-rendered"} templates (InstantSearch.js via CDN)`;
|
|
3368
|
+
case "none":
|
|
3369
|
+
return frameworkName ?? "a native mobile app";
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3180
3372
|
|
|
3181
3373
|
// src/actions/confirmFramework.ts
|
|
3182
3374
|
var confirmFrameworkSchema = z20.object({
|
|
@@ -3368,7 +3560,7 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
3368
3560
|
}
|
|
3369
3561
|
function formatReviewSummary(result) {
|
|
3370
3562
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
3371
|
-
const isIngestCommand = step.includes("
|
|
3563
|
+
const isIngestCommand = step.includes("algolia-wizard/");
|
|
3372
3564
|
const isWorktreeCommand = step.includes("/worktrees/");
|
|
3373
3565
|
return {
|
|
3374
3566
|
text: `\u2192 ${step}`,
|
|
@@ -3410,13 +3602,14 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
3410
3602
|
import z24 from "zod";
|
|
3411
3603
|
|
|
3412
3604
|
// src/lib/worktree.ts
|
|
3413
|
-
import { execFile
|
|
3414
|
-
import {
|
|
3605
|
+
import { execFile } from "node:child_process";
|
|
3606
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
3607
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
3415
3608
|
import {
|
|
3416
3609
|
basename as basename2,
|
|
3417
3610
|
dirname as dirname7,
|
|
3418
3611
|
isAbsolute as isAbsolute2,
|
|
3419
|
-
join as
|
|
3612
|
+
join as join12,
|
|
3420
3613
|
relative as relative2,
|
|
3421
3614
|
resolve as resolve3
|
|
3422
3615
|
} from "node:path";
|
|
@@ -3450,8 +3643,8 @@ async function isWorkingTreeDirty(repoRoot) {
|
|
|
3450
3643
|
return out.trim().length > 0;
|
|
3451
3644
|
}
|
|
3452
3645
|
async function pruneOldWorktrees(repoRoot) {
|
|
3453
|
-
const dir =
|
|
3454
|
-
const stale = (await
|
|
3646
|
+
const dir = join12(stateDir(repoRoot), "worktrees");
|
|
3647
|
+
const stale = (await readdir4(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
3455
3648
|
for (const slug of stale) {
|
|
3456
3649
|
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
3457
3650
|
try {
|
|
@@ -3461,7 +3654,7 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
3461
3654
|
"worktree",
|
|
3462
3655
|
"remove",
|
|
3463
3656
|
"--force",
|
|
3464
|
-
|
|
3657
|
+
join12(dir, slug)
|
|
3465
3658
|
]);
|
|
3466
3659
|
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
3467
3660
|
} catch (err) {
|
|
@@ -3475,43 +3668,55 @@ async function pruneOldWorktrees(repoRoot) {
|
|
|
3475
3668
|
async function createWorktree(repoRoot) {
|
|
3476
3669
|
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
3477
3670
|
const dirSlug = branch.replace(/\//g, "-");
|
|
3478
|
-
const path =
|
|
3671
|
+
const path = join12(stateDir(repoRoot), "worktrees", dirSlug);
|
|
3479
3672
|
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
3480
3673
|
await pruneOldWorktrees(repoRoot);
|
|
3481
3674
|
await mkdir6(dirname7(path), { recursive: true });
|
|
3482
3675
|
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
3483
3676
|
return { path, branch };
|
|
3484
3677
|
}
|
|
3485
|
-
async function
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
return { ok: true, output: "no package.json; skipped install" };
|
|
3490
|
-
}
|
|
3491
|
-
const pm = await detectPackageManager(worktreePath);
|
|
3492
|
-
return new Promise((resolve4) => {
|
|
3493
|
-
let output = "";
|
|
3494
|
-
const child = spawn3(pm, ["install"], {
|
|
3495
|
-
cwd: worktreePath,
|
|
3496
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3497
|
-
});
|
|
3498
|
-
child.stdout?.on("data", (d) => output += d);
|
|
3499
|
-
child.stderr?.on("data", (d) => output += d);
|
|
3500
|
-
child.on(
|
|
3501
|
-
"error",
|
|
3502
|
-
(err) => resolve4({
|
|
3503
|
-
ok: false,
|
|
3504
|
-
output: `Failed to run ${pm} install: ${err.message}`
|
|
3505
|
-
})
|
|
3506
|
-
);
|
|
3507
|
-
child.on(
|
|
3508
|
-
"close",
|
|
3509
|
-
(code) => resolve4({ ok: code === 0, output: output.trim() })
|
|
3510
|
-
);
|
|
3678
|
+
async function spawnStep(worktreePath, argv) {
|
|
3679
|
+
const { code, output } = await runCommand(argv[0], [...argv.slice(1)], {
|
|
3680
|
+
cwd: worktreePath,
|
|
3681
|
+
timeoutMs: INSTALL_TIMEOUT_MS
|
|
3511
3682
|
});
|
|
3683
|
+
return { ok: code === 0, output: output.trim() };
|
|
3684
|
+
}
|
|
3685
|
+
async function installWorktreeDeps(worktreePath, toolchain) {
|
|
3686
|
+
const { profile, installSteps, packageManager } = toolchain;
|
|
3687
|
+
const declared = packageManager.dependency.mode === "agent-declares" ? packageManager.dependency.file : void 0;
|
|
3688
|
+
const haveSomethingToInstall = await hasProfileManifest(worktreePath, profile) || declared !== void 0 && existsSync4(join12(worktreePath, declared));
|
|
3689
|
+
if (!haveSomethingToInstall) {
|
|
3690
|
+
return {
|
|
3691
|
+
ok: true,
|
|
3692
|
+
output: `no ${profile.displayName} manifest; skipped install`
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
if (installSteps.length === 0) {
|
|
3696
|
+
return {
|
|
3697
|
+
ok: true,
|
|
3698
|
+
output: `${profile.displayName} (${toolchain.packageManager.id}) has no wizard-run install step`
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
const outputs = [];
|
|
3702
|
+
for (const step of installSteps) {
|
|
3703
|
+
if (step.requiresFile && !existsSync4(join12(worktreePath, step.requiresFile)))
|
|
3704
|
+
continue;
|
|
3705
|
+
const result = await spawnStep(worktreePath, step.argv);
|
|
3706
|
+
if (result.output) outputs.push(result.output);
|
|
3707
|
+
if (result.ok) continue;
|
|
3708
|
+
if (step.optional) {
|
|
3709
|
+
logger.warn(
|
|
3710
|
+
{ step: step.argv.join(" "), output: result.output },
|
|
3711
|
+
"installWorktreeDeps: optional install step failed; continuing"
|
|
3712
|
+
);
|
|
3713
|
+
continue;
|
|
3714
|
+
}
|
|
3715
|
+
return { ok: false, output: outputs.join("\n").trim() };
|
|
3716
|
+
}
|
|
3717
|
+
return { ok: true, output: outputs.join("\n").trim() };
|
|
3512
3718
|
}
|
|
3513
|
-
|
|
3514
|
-
function validateIngestEntrypoint(worktreePath, entrypoint) {
|
|
3719
|
+
function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
|
|
3515
3720
|
if (!entrypoint || entrypoint.startsWith("-")) {
|
|
3516
3721
|
return {
|
|
3517
3722
|
ok: false,
|
|
@@ -3526,18 +3731,29 @@ function validateIngestEntrypoint(worktreePath, entrypoint) {
|
|
|
3526
3731
|
reason: `entrypoint "${entrypoint}" resolves outside the worktree`
|
|
3527
3732
|
};
|
|
3528
3733
|
}
|
|
3734
|
+
if (allowedExtensions?.length && !allowedExtensions.some((ext) => entrypoint.endsWith(ext))) {
|
|
3735
|
+
return {
|
|
3736
|
+
ok: false,
|
|
3737
|
+
reason: `entrypoint "${entrypoint}" is not one of ${allowedExtensions.join(", ")}`
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3529
3740
|
return { ok: true, target };
|
|
3530
3741
|
}
|
|
3531
|
-
async function runIngestScript(worktreePath,
|
|
3532
|
-
|
|
3742
|
+
async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
|
|
3743
|
+
const { ingest, profile, packageManager } = toolchain;
|
|
3744
|
+
if (ingest.kind !== "auto") {
|
|
3533
3745
|
return {
|
|
3534
3746
|
ran: false,
|
|
3535
3747
|
ok: false,
|
|
3536
3748
|
output: "",
|
|
3537
|
-
reason:
|
|
3749
|
+
reason: `${profile.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
|
|
3538
3750
|
};
|
|
3539
3751
|
}
|
|
3540
|
-
const validated = validateIngestEntrypoint(
|
|
3752
|
+
const validated = validateIngestEntrypoint(
|
|
3753
|
+
worktreePath,
|
|
3754
|
+
entrypoint,
|
|
3755
|
+
ingest.entrypointExtensions
|
|
3756
|
+
);
|
|
3541
3757
|
if (!validated.ok) {
|
|
3542
3758
|
return { ran: false, ok: false, output: "", reason: validated.reason };
|
|
3543
3759
|
}
|
|
@@ -3558,29 +3774,13 @@ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
|
|
|
3558
3774
|
reason: `entrypoint "${entrypoint}" does not exist`
|
|
3559
3775
|
};
|
|
3560
3776
|
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3567
|
-
env: { ...process.env, ...env }
|
|
3568
|
-
});
|
|
3569
|
-
child.stdout?.on("data", (d) => output += d);
|
|
3570
|
-
child.stderr?.on("data", (d) => output += d);
|
|
3571
|
-
child.on(
|
|
3572
|
-
"error",
|
|
3573
|
-
(err) => resolveRun({
|
|
3574
|
-
ran: true,
|
|
3575
|
-
ok: false,
|
|
3576
|
-
output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
|
|
3577
|
-
})
|
|
3578
|
-
);
|
|
3579
|
-
child.on(
|
|
3580
|
-
"close",
|
|
3581
|
-
(code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
|
|
3582
|
-
);
|
|
3777
|
+
const argv = resolveIngestArgv(ingest, entrypoint);
|
|
3778
|
+
const { code, output } = await runCommand(argv[0], argv.slice(1), {
|
|
3779
|
+
cwd: worktreePath,
|
|
3780
|
+
env,
|
|
3781
|
+
timeoutMs: INGEST_TIMEOUT_MS
|
|
3583
3782
|
});
|
|
3783
|
+
return { ran: true, ok: code === 0, output: output.trim() };
|
|
3584
3784
|
}
|
|
3585
3785
|
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
3586
3786
|
const trimmed = sourcePath.trim();
|
|
@@ -3595,8 +3795,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
3595
3795
|
} catch {
|
|
3596
3796
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
3597
3797
|
}
|
|
3598
|
-
const relPath =
|
|
3599
|
-
const dest =
|
|
3798
|
+
const relPath = join12(ingestDir, basename2(source));
|
|
3799
|
+
const dest = join12(worktreePath, relPath);
|
|
3600
3800
|
try {
|
|
3601
3801
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
3602
3802
|
await copyFile(source, dest);
|
|
@@ -3612,10 +3812,10 @@ function hasEnvVar(content, name) {
|
|
|
3612
3812
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
3613
3813
|
}
|
|
3614
3814
|
async function writeSearchEnvValues(worktreePath, vars) {
|
|
3615
|
-
const target =
|
|
3815
|
+
const target = join12(worktreePath, ".env");
|
|
3616
3816
|
let existing = "";
|
|
3617
3817
|
try {
|
|
3618
|
-
existing = await
|
|
3818
|
+
existing = await readFile9(target, "utf8");
|
|
3619
3819
|
} catch (err) {
|
|
3620
3820
|
if (err.code !== "ENOENT") throw err;
|
|
3621
3821
|
}
|
|
@@ -3732,64 +3932,33 @@ async function resolveSearchOnlyKey(index) {
|
|
|
3732
3932
|
}
|
|
3733
3933
|
|
|
3734
3934
|
// src/lib/algoliaDocs.ts
|
|
3735
|
-
import { readFileSync,
|
|
3736
|
-
import { dirname as dirname8, join as
|
|
3935
|
+
import { readFileSync, existsSync as existsSync5 } from "node:fs";
|
|
3936
|
+
import { dirname as dirname8, join as join13 } from "node:path";
|
|
3737
3937
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3738
|
-
var DOCS_SUBPATH =
|
|
3938
|
+
var DOCS_SUBPATH = join13("docs", "algolia-sdk");
|
|
3739
3939
|
function findDocsDir() {
|
|
3740
3940
|
let dir = dirname8(fileURLToPath2(import.meta.url));
|
|
3741
3941
|
for (; ; ) {
|
|
3742
|
-
const candidate =
|
|
3743
|
-
if (
|
|
3942
|
+
const candidate = join13(dir, DOCS_SUBPATH);
|
|
3943
|
+
if (existsSync5(candidate)) return candidate;
|
|
3744
3944
|
const parent = dirname8(dir);
|
|
3745
3945
|
if (parent === dir) return void 0;
|
|
3746
3946
|
dir = parent;
|
|
3747
3947
|
}
|
|
3748
3948
|
}
|
|
3749
|
-
function
|
|
3750
|
-
const docsDir = findDocsDir();
|
|
3751
|
-
if (!docsDir) {
|
|
3752
|
-
logger.warn(
|
|
3753
|
-
"algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
|
|
3754
|
-
);
|
|
3755
|
-
return "";
|
|
3756
|
-
}
|
|
3757
|
-
const files = readdirSync(docsDir).filter((f) => f.includes(language));
|
|
3758
|
-
if (files.length === 0) {
|
|
3759
|
-
logger.warn(
|
|
3760
|
-
{ language },
|
|
3761
|
-
"algoliaDocs: no SDK reference found for language; skipping"
|
|
3762
|
-
);
|
|
3763
|
-
return "";
|
|
3764
|
-
}
|
|
3765
|
-
return readFileSync(join12(docsDir, files[0]), "utf8").trim();
|
|
3766
|
-
}
|
|
3767
|
-
function getNamedDoc(name, language) {
|
|
3949
|
+
function getNamedDoc(name, key) {
|
|
3768
3950
|
const docsDir = findDocsDir();
|
|
3769
3951
|
if (!docsDir) {
|
|
3770
3952
|
logger.warn("docs/algolia-sdk not found");
|
|
3771
3953
|
return "";
|
|
3772
3954
|
}
|
|
3773
|
-
const file =
|
|
3774
|
-
if (!
|
|
3775
|
-
logger.warn({ name,
|
|
3955
|
+
const file = join13(docsDir, `${name}-${key}.md`);
|
|
3956
|
+
if (!existsSync5(file)) {
|
|
3957
|
+
logger.warn({ name, key }, "named SDK reference not found");
|
|
3776
3958
|
return "";
|
|
3777
3959
|
}
|
|
3778
3960
|
return readFileSync(file, "utf8").trim();
|
|
3779
3961
|
}
|
|
3780
|
-
function getFrameworkSpecificDoc(frameworks) {
|
|
3781
|
-
const fw = frameworks.map((f) => f.toLowerCase());
|
|
3782
|
-
if (fw.includes("vue") || fw.includes("nuxt")) {
|
|
3783
|
-
return loadAlgoliaDoc("vue");
|
|
3784
|
-
}
|
|
3785
|
-
if (fw.includes("react") || fw.includes("next.js")) {
|
|
3786
|
-
return loadAlgoliaDoc("react");
|
|
3787
|
-
}
|
|
3788
|
-
if (fw.includes("angular")) {
|
|
3789
|
-
return loadAlgoliaDoc("angular");
|
|
3790
|
-
}
|
|
3791
|
-
return loadAlgoliaDoc("js");
|
|
3792
|
-
}
|
|
3793
3962
|
|
|
3794
3963
|
// src/actions/implement.ts
|
|
3795
3964
|
var implementSchema = z24.object({
|
|
@@ -3824,12 +3993,11 @@ var implementSchema = z24.object({
|
|
|
3824
3993
|
});
|
|
3825
3994
|
var implementationOutputSchema = z24.object({
|
|
3826
3995
|
summary: z24.string(),
|
|
3827
|
-
// Ingestion only:
|
|
3828
|
-
//
|
|
3829
|
-
//
|
|
3830
|
-
//
|
|
3831
|
-
// the agent
|
|
3832
|
-
runtime: z24.enum(INGEST_RUNTIMES).optional(),
|
|
3996
|
+
// Ingestion only: the script the wizard should run, as a bare path — never a
|
|
3997
|
+
// command string, and never the interpreter. The command comes from the
|
|
3998
|
+
// resolved language toolchain (a registry constant); this path is validated to
|
|
3999
|
+
// a worktree-relative file with a runnable extension and substituted into it.
|
|
4000
|
+
// So the agent contributes no part of the command that gets executed.
|
|
3833
4001
|
entrypoint: z24.string().optional()
|
|
3834
4002
|
});
|
|
3835
4003
|
var verificationOutputSchema = z24.object({
|
|
@@ -3839,47 +4007,11 @@ var verificationOutputSchema = z24.object({
|
|
|
3839
4007
|
});
|
|
3840
4008
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
3841
4009
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
if (names.some((n) => n.includes("react") || n.includes("next")))
|
|
3847
|
-
return "React";
|
|
3848
|
-
if (names.some((n) => n.includes("angular"))) return "Angular";
|
|
3849
|
-
return "JavaScript";
|
|
3850
|
-
}
|
|
3851
|
-
function frameworksForDoc(framework) {
|
|
3852
|
-
switch (framework) {
|
|
3853
|
-
case "React":
|
|
3854
|
-
return ["react"];
|
|
3855
|
-
case "Vue":
|
|
3856
|
-
return ["vue"];
|
|
3857
|
-
case "Angular":
|
|
3858
|
-
return ["angular"];
|
|
3859
|
-
case "JavaScript":
|
|
3860
|
-
return [];
|
|
3861
|
-
}
|
|
3862
|
-
}
|
|
3863
|
-
function publicEnvPrefix(language) {
|
|
3864
|
-
const frameworkNames = language.frameworks.map(
|
|
3865
|
-
(framework) => framework.name.toLowerCase()
|
|
4010
|
+
function buildSearchEnvVars(language, strategy, appId, searchKey) {
|
|
4011
|
+
const prefix = publicEnvPrefix(
|
|
4012
|
+
language.frameworks.map((framework) => framework.name),
|
|
4013
|
+
strategy
|
|
3866
4014
|
);
|
|
3867
|
-
if (frameworkNames.some((name) => name.includes("next"))) {
|
|
3868
|
-
return "NEXT_PUBLIC_";
|
|
3869
|
-
}
|
|
3870
|
-
if (frameworkNames.some((name) => name.includes("nuxt"))) {
|
|
3871
|
-
return "NUXT_PUBLIC_";
|
|
3872
|
-
}
|
|
3873
|
-
if (frameworkNames.some((name) => name.includes("astro"))) {
|
|
3874
|
-
return "PUBLIC_";
|
|
3875
|
-
}
|
|
3876
|
-
if (frameworkNames.some((name) => name.includes("vite"))) {
|
|
3877
|
-
return "VITE_";
|
|
3878
|
-
}
|
|
3879
|
-
return "PUBLIC_";
|
|
3880
|
-
}
|
|
3881
|
-
function searchEnvVars(language, appId, searchKey) {
|
|
3882
|
-
const prefix = publicEnvPrefix(language);
|
|
3883
4015
|
return [
|
|
3884
4016
|
{
|
|
3885
4017
|
name: `${prefix}ALGOLIA_APP_ID`,
|
|
@@ -3891,6 +4023,38 @@ function searchEnvVars(language, appId, searchKey) {
|
|
|
3891
4023
|
}
|
|
3892
4024
|
];
|
|
3893
4025
|
}
|
|
4026
|
+
async function resolveIngestionProfile(ctx, language, repoRoot) {
|
|
4027
|
+
const { candidates, confirmed: confirmed3, onDisk } = await pickIngestionCandidates(
|
|
4028
|
+
repoRoot,
|
|
4029
|
+
language.languages.map((l) => l.name)
|
|
4030
|
+
);
|
|
4031
|
+
if (candidates.length === 0) {
|
|
4032
|
+
const chosen = confirmed3[0] ?? onDisk[0] ?? LANGUAGE_PROFILES[DEFAULT_LANGUAGE_ID];
|
|
4033
|
+
logger.warn(
|
|
4034
|
+
{
|
|
4035
|
+
confirmed: language.languages.map((l) => l.name),
|
|
4036
|
+
onDisk: onDisk.map((p) => p.id),
|
|
4037
|
+
chosen: chosen.id
|
|
4038
|
+
},
|
|
4039
|
+
"implement: no confirmed language matched a manifest on disk; falling back"
|
|
4040
|
+
);
|
|
4041
|
+
return chosen;
|
|
4042
|
+
}
|
|
4043
|
+
if (candidates.length === 1) return candidates[0];
|
|
4044
|
+
const backends = candidates.filter(isBackendLanguage);
|
|
4045
|
+
if (backends.length === 1) return backends[0];
|
|
4046
|
+
if (backends.length === 0) return candidates[0];
|
|
4047
|
+
if (isBackendLanguage(candidates[0])) return candidates[0];
|
|
4048
|
+
const options = backends.map((p) => p.displayName);
|
|
4049
|
+
const selection = await ctx.requestUserInput({
|
|
4050
|
+
prompt: "Which language should the ingestion script use?",
|
|
4051
|
+
promptType: "multipleChoice",
|
|
4052
|
+
options,
|
|
4053
|
+
defaultSelectedIndex: 0
|
|
4054
|
+
});
|
|
4055
|
+
const picked = typeof selection === "string" ? backends.find((p) => p.displayName === selection) : void 0;
|
|
4056
|
+
return picked ?? backends[0];
|
|
4057
|
+
}
|
|
3894
4058
|
function baseInstructions(input) {
|
|
3895
4059
|
return [
|
|
3896
4060
|
`Target Algolia index: ${input.targetIndex}`,
|
|
@@ -3918,37 +4082,48 @@ function sourceSpecificInstructions(input) {
|
|
|
3918
4082
|
generated: [
|
|
3919
4083
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
3920
4084
|
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
|
|
3921
|
-
"In the script, read and parse each returned file path at runtime
|
|
4085
|
+
"In the script, read and parse each returned JSON file path at runtime using the idiomatic file read for the language you are writing in (the SDK reference above shows one) instead of inlining the records as literals.",
|
|
3922
4086
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
3923
4087
|
]
|
|
3924
4088
|
};
|
|
3925
4089
|
return byLine[input.ingestionSource];
|
|
3926
4090
|
}
|
|
3927
4091
|
function ingestionInstructions(input) {
|
|
4092
|
+
const { ingestionProfile: profile, toolchain } = input;
|
|
4093
|
+
const { ingest } = toolchain;
|
|
4094
|
+
const extensions = ingest.entrypointExtensions.join(", ");
|
|
4095
|
+
const runInstruction = ingest.kind === "auto" ? `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard runs it with \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so it must be a plain path with no flags or arguments and must run as-is under that command.` : `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard does NOT run ${profile.displayName} ${toolchain.packageManager.id} projects itself \u2014 it tells the developer to run \`${describeIngestCommand(ingest, profile.ingestEntrypointExample)}\`, so also add whatever build configuration that command needs${ingest.kind === "manual" && ingest.requiresBuildTask ? `, including a "${ingest.requiresBuildTask}" task in "${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"}" that runs the script` : ""}.`;
|
|
3928
4096
|
return [
|
|
3929
4097
|
...input.confirmed && input.confirmed.length ? [
|
|
3930
|
-
`
|
|
4098
|
+
`Write the ingestion script in ${profile.displayName}, at "${ingestScriptDir(profile)}/" in the repo.`,
|
|
3931
4099
|
`Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
3932
|
-
`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.`,
|
|
3933
|
-
|
|
4100
|
+
`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. ${profile.envReadInstruction} The wizard sets these when it runs the script.`,
|
|
4101
|
+
`Use the official Algolia ${profile.displayName} client (${profile.sdk.packageName}). Do not use the raw HTTP API, and do not use a client for another language.`,
|
|
3934
4102
|
"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.",
|
|
3935
|
-
getNamedDoc("save-records",
|
|
3936
|
-
|
|
4103
|
+
getNamedDoc("save-records", profile.sdk.docKey),
|
|
4104
|
+
dependencyInstruction(toolchain),
|
|
3937
4105
|
"The summary should be extremely concise.",
|
|
3938
|
-
|
|
4106
|
+
runInstruction,
|
|
3939
4107
|
...sourceSpecificInstructions(input)
|
|
3940
4108
|
] : []
|
|
3941
4109
|
];
|
|
3942
4110
|
}
|
|
3943
4111
|
function searchInstructions(input) {
|
|
3944
|
-
const doc =
|
|
4112
|
+
const doc = getNamedDoc(
|
|
4113
|
+
"instantsearch-setup",
|
|
4114
|
+
searchDocKey(input.searchStrategy)
|
|
4115
|
+
);
|
|
4116
|
+
const isTemplate = input.searchStrategy === "cdn-template";
|
|
4117
|
+
const placement = isTemplate ? input.searchLocation ? `Add the search UI to the server-rendered template at "${input.searchLocation}" \u2014 ideally a shared layout, so it is reachable across the app.` : `This project has no shared template to host the UI, so create a standalone page at "${input.ingestDir}/search-demo.html" the developer can open directly, and add a TODO explaining how to move the snippet into their own layout.` : `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.`;
|
|
3945
4118
|
return [
|
|
3946
4119
|
"Implement an in-app Algolia search experience.",
|
|
3947
|
-
`Build the search UI for ${input.
|
|
3948
|
-
"Follow the Algolia
|
|
4120
|
+
`Build the search UI for ${describeSearchTarget(input.searchStrategy, input.frameworkName)}.`,
|
|
4121
|
+
"Follow the Algolia reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
|
|
3949
4122
|
doc,
|
|
3950
|
-
|
|
3951
|
-
|
|
4123
|
+
placement,
|
|
4124
|
+
`It needs at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
|
|
4125
|
+
isTemplate ? "Load InstantSearch from a CDN with script tags as shown in the reference. Do not add JavaScript package dependencies, a bundler, or a build step." : 'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
|
|
4126
|
+
isTemplate ? "Read the App ID and search-only API key from server-side configuration/environment and render them into the page (e.g. as data- attributes the script reads); never hardcode them, and never put a write/admin key in HTML." : "Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
|
|
3952
4127
|
// appId always resolves (loadActiveProfile throws otherwise); only the
|
|
3953
4128
|
// search-only key is best-effort and can fall back to a placeholder.
|
|
3954
4129
|
`Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
|
|
@@ -3956,20 +4131,22 @@ function searchInstructions(input) {
|
|
|
3956
4131
|
// resolved app id / search-only key into ".env" under these exact names
|
|
3957
4132
|
// right after this step, so a renamed prefix here would leave the code
|
|
3958
4133
|
// reading a var the wizard never wrote.
|
|
3959
|
-
`Use exactly these
|
|
3960
|
-
'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
|
|
4134
|
+
`Use exactly these env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
3961
4135
|
"The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
|
|
3962
4136
|
];
|
|
3963
4137
|
}
|
|
3964
4138
|
function verificationInstructions(input) {
|
|
4139
|
+
const protectedDirs = [
|
|
4140
|
+
.../* @__PURE__ */ new Set([input.ingestDir, ingestScriptDir(input.ingestionProfile)])
|
|
4141
|
+
];
|
|
3965
4142
|
return [
|
|
3966
4143
|
"Verify the Algolia implementation changes in the current worktree.",
|
|
3967
4144
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
3968
|
-
|
|
4145
|
+
`Call verifyImplementation at least once; it runs the mechanical checks available for this repo's languages (${input.verificationLanguages.join(", ")}) and returns per-check results plus an aggregate ok.`,
|
|
3969
4146
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
|
|
3970
4147
|
"Do not make speculative fixes when verifyImplementation cannot run, no checks exist, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
|
|
3971
4148
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
3972
|
-
`Do not modify "${
|
|
4149
|
+
`Do not modify ${protectedDirs.map((dir) => `"${dir}/"`).join(" or ")} unless verifyImplementation reports an actionable issue in its files.`,
|
|
3973
4150
|
"Always call reportStatus with status=success once verification has run, even when sufficient=false.",
|
|
3974
4151
|
"Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
|
|
3975
4152
|
"Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
|
|
@@ -3978,14 +4155,17 @@ function verificationInstructions(input) {
|
|
|
3978
4155
|
var IMPLEMENT_CONFIG = {
|
|
3979
4156
|
ingestion: {
|
|
3980
4157
|
title: "Algolia ingestion",
|
|
4158
|
+
label: "Ingestion",
|
|
3981
4159
|
buildInstructions: ingestionInstructions
|
|
3982
4160
|
},
|
|
3983
4161
|
search: {
|
|
3984
4162
|
title: "Algolia search",
|
|
4163
|
+
label: "Search",
|
|
3985
4164
|
buildInstructions: searchInstructions
|
|
3986
4165
|
},
|
|
3987
4166
|
verification: {
|
|
3988
4167
|
title: "Algolia verification",
|
|
4168
|
+
label: "Verification",
|
|
3989
4169
|
buildInstructions: verificationInstructions
|
|
3990
4170
|
}
|
|
3991
4171
|
};
|
|
@@ -4017,11 +4197,10 @@ function buildAgentInstructions(useCase, input, extraInstructions = []) {
|
|
|
4017
4197
|
];
|
|
4018
4198
|
}
|
|
4019
4199
|
function formatSummary(useCase, summary) {
|
|
4020
|
-
|
|
4021
|
-
return `${label}: ${summary}`;
|
|
4200
|
+
return `${IMPLEMENT_CONFIG[useCase].label}: ${summary}`;
|
|
4022
4201
|
}
|
|
4023
|
-
function buildIngestCommand(worktree,
|
|
4024
|
-
return `cd ${shellQuote(worktree)} && ${
|
|
4202
|
+
function buildIngestCommand(worktree, toolchain, entrypoint) {
|
|
4203
|
+
return `cd ${shellQuote(worktree)} && ${describeIngestCommand(toolchain.ingest, entrypoint)}`;
|
|
4025
4204
|
}
|
|
4026
4205
|
function parseIngestRecordCount(output) {
|
|
4027
4206
|
const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
|
|
@@ -4127,7 +4306,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4127
4306
|
const copied = await copyUploadIntoWorktree(
|
|
4128
4307
|
repoRoot,
|
|
4129
4308
|
worktree,
|
|
4130
|
-
|
|
4309
|
+
INGEST_DIR,
|
|
4131
4310
|
uploadSourcePath ?? ""
|
|
4132
4311
|
);
|
|
4133
4312
|
if (copied.ok) {
|
|
@@ -4141,6 +4320,33 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4141
4320
|
);
|
|
4142
4321
|
}
|
|
4143
4322
|
}
|
|
4323
|
+
const ingestionProfile = await resolveIngestionProfile(
|
|
4324
|
+
ctx,
|
|
4325
|
+
language,
|
|
4326
|
+
worktree
|
|
4327
|
+
);
|
|
4328
|
+
const toolchain = await resolveToolchain(worktree, ingestionProfile);
|
|
4329
|
+
const verificationLanguages = [
|
|
4330
|
+
.../* @__PURE__ */ new Set([
|
|
4331
|
+
ingestionProfile.id,
|
|
4332
|
+
...(await detectProfilesFromManifests(worktree)).map((p) => p.id)
|
|
4333
|
+
])
|
|
4334
|
+
];
|
|
4335
|
+
const frameworkName = language.frameworks[0]?.name;
|
|
4336
|
+
const searchStrategy = resolveSearchStrategy(
|
|
4337
|
+
frameworkName,
|
|
4338
|
+
verificationLanguages.includes(JAVASCRIPT)
|
|
4339
|
+
);
|
|
4340
|
+
logger.info(
|
|
4341
|
+
{
|
|
4342
|
+
language: ingestionProfile.id,
|
|
4343
|
+
packageManager: toolchain.packageManager.id,
|
|
4344
|
+
ingest: toolchain.ingest.kind,
|
|
4345
|
+
framework: frameworkName,
|
|
4346
|
+
searchStrategy
|
|
4347
|
+
},
|
|
4348
|
+
"implement: resolved ingestion toolchain and search strategy"
|
|
4349
|
+
);
|
|
4144
4350
|
const input = {
|
|
4145
4351
|
findings: normalized,
|
|
4146
4352
|
confirmed: confirmed3,
|
|
@@ -4149,23 +4355,31 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4149
4355
|
language,
|
|
4150
4356
|
appId,
|
|
4151
4357
|
searchKey,
|
|
4152
|
-
searchEnvVars:
|
|
4153
|
-
|
|
4358
|
+
searchEnvVars: buildSearchEnvVars(
|
|
4359
|
+
language,
|
|
4360
|
+
searchStrategy,
|
|
4361
|
+
appId,
|
|
4362
|
+
searchKey
|
|
4363
|
+
),
|
|
4364
|
+
ingestDir: INGEST_DIR,
|
|
4154
4365
|
ingestionSource,
|
|
4155
4366
|
uploadFilePath,
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4367
|
+
searchStrategy,
|
|
4368
|
+
frameworkName,
|
|
4369
|
+
ingestionProfile,
|
|
4370
|
+
toolchain,
|
|
4371
|
+
verificationLanguages
|
|
4159
4372
|
};
|
|
4373
|
+
const searchToolchain = !bundlesJavaScript(searchStrategy) ? void 0 : ingestionProfile.id === JAVASCRIPT ? toolchain : await resolveToolchain(worktree, LANGUAGE_PROFILES[JAVASCRIPT]);
|
|
4374
|
+
const toolchainForUseCase = (useCase) => useCase === "search" ? searchToolchain : toolchain;
|
|
4160
4375
|
const summaries = [];
|
|
4161
4376
|
if (uploadWarning) summaries.push(uploadWarning);
|
|
4162
4377
|
let agentRuns = 0;
|
|
4163
|
-
let ingestRuntime;
|
|
4164
4378
|
let ingestEntrypoint;
|
|
4165
4379
|
let ingestScriptRan = false;
|
|
4166
4380
|
let ingestRecordCount;
|
|
4167
4381
|
let ingestDurationMs;
|
|
4168
|
-
|
|
4382
|
+
const failedInstalls = /* @__PURE__ */ new Set();
|
|
4169
4383
|
let ingestOutcomeMessage;
|
|
4170
4384
|
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4171
4385
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
@@ -4179,16 +4393,19 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4179
4393
|
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4180
4394
|
outputSchema: implementationOutputSchema
|
|
4181
4395
|
});
|
|
4396
|
+
const useCaseToolchain = toolchainForUseCase(currentUseCase);
|
|
4397
|
+
if (!useCaseToolchain) return result;
|
|
4182
4398
|
ctx.notify({
|
|
4183
4399
|
messages: [`Installing dependencies for ${currentUseCase}\u2026`]
|
|
4184
4400
|
});
|
|
4185
4401
|
const installLogId = ctx.logStart("installWorktreeDeps", {
|
|
4186
|
-
useCase: currentUseCase
|
|
4402
|
+
useCase: currentUseCase,
|
|
4403
|
+
language: useCaseToolchain.profile.id
|
|
4187
4404
|
});
|
|
4188
|
-
const install = await installWorktreeDeps(worktree);
|
|
4405
|
+
const install = await installWorktreeDeps(worktree, useCaseToolchain);
|
|
4189
4406
|
ctx.logEnd(installLogId, install.ok ? "success" : "error");
|
|
4190
4407
|
if (!install.ok) {
|
|
4191
|
-
|
|
4408
|
+
failedInstalls.add(useCaseToolchain.profile.displayName);
|
|
4192
4409
|
logger.warn(
|
|
4193
4410
|
{ useCase: currentUseCase, output: install.output },
|
|
4194
4411
|
"implement: dependency install in worktree failed; generated commands may not run until deps are installed"
|
|
@@ -4202,15 +4419,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4202
4419
|
return runAgent({
|
|
4203
4420
|
instructions: buildAgentInstructions("verification", input),
|
|
4204
4421
|
tools: toolsForUseCase("verification"),
|
|
4205
|
-
outputSchema: verificationOutputSchema
|
|
4422
|
+
outputSchema: verificationOutputSchema,
|
|
4423
|
+
// So verifyImplementation runs this repo's checks, not just npm scripts.
|
|
4424
|
+
languages: input.verificationLanguages
|
|
4206
4425
|
});
|
|
4207
4426
|
}
|
|
4208
4427
|
if (useCases.includes("ingestion")) {
|
|
4209
|
-
const { summary,
|
|
4428
|
+
const { summary, entrypoint } = await runImplementationUseCase("ingestion");
|
|
4210
4429
|
summaries.push(formatSummary("ingestion", summary));
|
|
4211
|
-
ingestRuntime = runtime;
|
|
4212
4430
|
ingestEntrypoint = entrypoint;
|
|
4213
|
-
if (
|
|
4431
|
+
if (ingestEntrypoint && toolchain.ingest.kind === "auto" && failedInstalls.size === 0) {
|
|
4214
4432
|
ctx.clearNotices();
|
|
4215
4433
|
const runNow = await ctx.requestUserInput({
|
|
4216
4434
|
prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
|
|
@@ -4222,13 +4440,13 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4222
4440
|
const profile = await loadActiveProfile();
|
|
4223
4441
|
ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
|
|
4224
4442
|
const scriptLogId = ctx.logStart("runIngestScript", {
|
|
4225
|
-
|
|
4443
|
+
language: ingestionProfile.id,
|
|
4226
4444
|
entrypoint: ingestEntrypoint
|
|
4227
4445
|
});
|
|
4228
4446
|
const startedAt = Date.now();
|
|
4229
4447
|
const run2 = await runIngestScript(
|
|
4230
4448
|
worktree,
|
|
4231
|
-
|
|
4449
|
+
toolchain,
|
|
4232
4450
|
ingestEntrypoint,
|
|
4233
4451
|
{
|
|
4234
4452
|
[APP_ID_VAR]: profile.appId,
|
|
@@ -4255,7 +4473,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4255
4473
|
outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
|
|
4256
4474
|
logger.warn(
|
|
4257
4475
|
{
|
|
4258
|
-
|
|
4476
|
+
language: ingestionProfile.id,
|
|
4259
4477
|
entrypoint: ingestEntrypoint,
|
|
4260
4478
|
reason: run2.reason
|
|
4261
4479
|
},
|
|
@@ -4278,7 +4496,7 @@ ${run2.output}` : status;
|
|
|
4278
4496
|
outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
|
|
4279
4497
|
logger.warn(
|
|
4280
4498
|
{
|
|
4281
|
-
|
|
4499
|
+
language: ingestionProfile.id,
|
|
4282
4500
|
entrypoint: ingestEntrypoint,
|
|
4283
4501
|
output: run2.output
|
|
4284
4502
|
},
|
|
@@ -4295,10 +4513,28 @@ ${run2.output}` : status;
|
|
|
4295
4513
|
}
|
|
4296
4514
|
}
|
|
4297
4515
|
const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
|
|
4298
|
-
if (
|
|
4516
|
+
if (ingestEntrypoint) {
|
|
4299
4517
|
commandMessages.push(
|
|
4300
|
-
`Ingestion command: ${buildIngestCommand(worktree,
|
|
4518
|
+
`Ingestion command: ${buildIngestCommand(worktree, toolchain, ingestEntrypoint)}`
|
|
4301
4519
|
);
|
|
4520
|
+
if (toolchain.ingest.kind === "manual") {
|
|
4521
|
+
commandMessages.push(
|
|
4522
|
+
`The wizard does not run ${ingestionProfile.displayName} ${toolchain.packageManager.id} projects \u2014 run the command above yourself to ingest.`
|
|
4523
|
+
);
|
|
4524
|
+
const missingTask = await missingBuildTask(worktree, toolchain);
|
|
4525
|
+
if (missingTask) {
|
|
4526
|
+
const warning = `\u26A0\uFE0F The command above needs a "${missingTask}" task, which is not in ${toolchain.packageManager.dependency.mode === "agent-declares" ? toolchain.packageManager.dependency.file : "the build file"} \u2014 add it before running, or run the script through your IDE instead.`;
|
|
4527
|
+
commandMessages.push(warning);
|
|
4528
|
+
summaries.push(warning);
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
if (ingestionSource === "local") {
|
|
4533
|
+
const limitation = localSourceLimitation(worktree, ingestionProfile);
|
|
4534
|
+
if (limitation) {
|
|
4535
|
+
commandMessages.push(`\u26A0\uFE0F ${limitation}`);
|
|
4536
|
+
summaries.push(`\u26A0\uFE0F ${limitation}`);
|
|
4537
|
+
}
|
|
4302
4538
|
}
|
|
4303
4539
|
await ctx.requestUserInput({
|
|
4304
4540
|
// No question being asked here, just an acknowledgement — the
|
|
@@ -4309,7 +4545,20 @@ ${run2.output}` : status;
|
|
|
4309
4545
|
messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
|
|
4310
4546
|
});
|
|
4311
4547
|
}
|
|
4312
|
-
|
|
4548
|
+
const skipSearch = useCases.includes("search") && !canScaffoldSearchUI(input.searchStrategy);
|
|
4549
|
+
if (skipSearch) {
|
|
4550
|
+
const target = describeSearchTarget(
|
|
4551
|
+
input.searchStrategy,
|
|
4552
|
+
input.frameworkName
|
|
4553
|
+
);
|
|
4554
|
+
summaries.push(
|
|
4555
|
+
`Search UI skipped: the wizard can't scaffold a native search UI for ${target}. Your records are in the "${targetIndex}" index \u2014 build the UI with Algolia's mobile InstantSearch libraries (https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/ios/ for iOS, .../android for Android).`
|
|
4556
|
+
);
|
|
4557
|
+
track("AI Wizard Search UI Skipped", {
|
|
4558
|
+
framework: input.frameworkName ?? "unknown"
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4561
|
+
if (useCases.includes("search") && !skipSearch) {
|
|
4313
4562
|
let extraInstructions = [];
|
|
4314
4563
|
const preSearchFiles = new Set(await listChangedFiles(worktree));
|
|
4315
4564
|
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
@@ -4380,9 +4629,9 @@ ${run2.output}` : status;
|
|
|
4380
4629
|
"implement: agent reported success but no files changed in the worktree"
|
|
4381
4630
|
);
|
|
4382
4631
|
}
|
|
4383
|
-
if (
|
|
4632
|
+
if (failedInstalls.size > 0) {
|
|
4384
4633
|
summaries.push(
|
|
4385
|
-
|
|
4634
|
+
`\u26A0\uFE0F Dependency install in the worktree failed. Install the ${[...failedInstalls].join(" and ")} dependencies in the worktree before the command below, or it will fail on a missing package.`
|
|
4386
4635
|
);
|
|
4387
4636
|
}
|
|
4388
4637
|
return {
|
|
@@ -4390,10 +4639,10 @@ ${run2.output}` : status;
|
|
|
4390
4639
|
filesChanged,
|
|
4391
4640
|
summary: summaries.join("\n\n"),
|
|
4392
4641
|
worktreePath: worktree,
|
|
4393
|
-
...useCases.includes("ingestion") &&
|
|
4642
|
+
...useCases.includes("ingestion") && ingestEntrypoint ? {
|
|
4394
4643
|
ingestCommand: buildIngestCommand(
|
|
4395
4644
|
worktree,
|
|
4396
|
-
|
|
4645
|
+
toolchain,
|
|
4397
4646
|
ingestEntrypoint
|
|
4398
4647
|
),
|
|
4399
4648
|
ingestScriptRan,
|
|
@@ -4722,20 +4971,20 @@ function parseCliArgs(argv) {
|
|
|
4722
4971
|
}
|
|
4723
4972
|
|
|
4724
4973
|
// src/lib/resetState.ts
|
|
4725
|
-
import { readdir as
|
|
4726
|
-
import { join as
|
|
4974
|
+
import { readdir as readdir5, rm as rm2 } from "node:fs/promises";
|
|
4975
|
+
import { join as join14 } from "node:path";
|
|
4727
4976
|
var KEEP = ["wizard.log"];
|
|
4728
4977
|
async function resetProjectState() {
|
|
4729
4978
|
const dir = stateDir();
|
|
4730
4979
|
let entries;
|
|
4731
4980
|
try {
|
|
4732
|
-
entries = await
|
|
4981
|
+
entries = await readdir5(dir);
|
|
4733
4982
|
} catch {
|
|
4734
4983
|
return { dir, removed: [] };
|
|
4735
4984
|
}
|
|
4736
4985
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
4737
4986
|
await Promise.all(
|
|
4738
|
-
targets.map((name) => rm2(
|
|
4987
|
+
targets.map((name) => rm2(join14(dir, name), { recursive: true, force: true }))
|
|
4739
4988
|
);
|
|
4740
4989
|
return { dir, removed: targets };
|
|
4741
4990
|
}
|
|
@@ -1,36 +1,49 @@
|
|
|
1
|
-
# Algolia
|
|
1
|
+
# Algolia SDK reference
|
|
2
2
|
|
|
3
|
-
Authoritative reference for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
InstantSearch).
|
|
3
|
+
Authoritative reference for the code the wizard generates. Prefer the method shapes
|
|
4
|
+
documented here over prior knowledge — they are pinned to the current stable majors
|
|
5
|
+
and avoid known pitfalls (e.g. v5 type mismatches with InstantSearch).
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Docs are loaded by exact name (`<name>-<key>.md`), keyed off the language registry in
|
|
8
|
+
`src/lib/languages.ts` and the framework registry in `src/lib/frameworks.ts`. Adding a
|
|
9
|
+
language means adding its `save-records-<key>.md`, or the doc-coverage test fails.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
- `react-instantsearch` v7 (install `^7`)
|
|
12
|
-
- `vue-instantsearch` v4 (install `^4`)
|
|
13
|
-
- `instantsearch.js` v4 (install `^4`) — also the recommended choice for Angular
|
|
14
|
-
- `angular-instantsearch` — DEPRECATED/archived (Sep 2024); do not use, prefer `instantsearch.js`
|
|
11
|
+
## Ingestion clients
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
One `save-records-<key>.md` per language Algolia ships an official API client for.
|
|
14
|
+
Install the latest stable within the major; never pin an exact patch.
|
|
15
|
+
|
|
16
|
+
| Language | Package | Version |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| JavaScript/TypeScript | `algoliasearch` | `^5` |
|
|
19
|
+
|
|
20
|
+
## Search UI
|
|
21
|
+
|
|
22
|
+
| Strategy | Doc | Packages |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| React (incl. Next.js) | `instantsearch-setup-react.md` | `react-instantsearch` v7 |
|
|
25
|
+
| Vue (incl. Nuxt) | `instantsearch-setup-vue.md` | `vue-instantsearch` v4 |
|
|
26
|
+
| Angular | `instantsearch-setup-angular.md` | `instantsearch.js` v4 |
|
|
27
|
+
| Plain JavaScript | `instantsearch-setup-js.md` | `instantsearch.js` v4 |
|
|
28
|
+
| Server-rendered templates | `instantsearch-setup-templates.md` | CDN script tags, no bundler |
|
|
29
|
+
|
|
30
|
+
`angular-instantsearch` is DEPRECATED/archived (Sep 2024); use `instantsearch.js`.
|
|
31
|
+
|
|
32
|
+
Server-rendered templates cover Algolia's official framework integrations — Rails,
|
|
33
|
+
Django, Laravel, Symfony — and any backend with no JavaScript build.
|
|
18
34
|
|
|
19
35
|
## Golden rules
|
|
20
36
|
|
|
37
|
+
- Ingestion is a **write** op: it needs a write key and must stay server-side. Read the
|
|
38
|
+
credentials from the `ALGOLIA_APPLICATION_ID` and `ALGOLIA_WRITE_API_KEY` env vars.
|
|
21
39
|
- Use a **search-only API key** in any browser/client code. It is safe to expose.
|
|
22
|
-
NEVER ship an admin or any write-capable key to the client
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
NEVER ship an admin or any write-capable key to the client, and never render one into
|
|
41
|
+
HTML.
|
|
42
|
+
- Read the Application ID and search-only key from env vars or server-side config, never
|
|
43
|
+
hardcode them inline.
|
|
25
44
|
- Instantiate the search client **once, outside your components**, and pass a stable
|
|
26
|
-
reference. Do not inline `algoliasearch(...)` as a prop value — it breaks the
|
|
27
|
-
|
|
45
|
+
reference. Do not inline `algoliasearch(...)` as a prop value — it breaks the client
|
|
46
|
+
cache and causes re-renders.
|
|
28
47
|
- For InstantSearch, import the client from `algoliasearch/lite` (smaller bundle and
|
|
29
|
-
correct types
|
|
30
|
-
|
|
31
|
-
## Files
|
|
32
|
-
|
|
33
|
-
- `instantsearch-setup.md` — framework-specific InstantSearch wiring (React, Vue,
|
|
34
|
-
Angular, vanilla). Use this for the in-app search UI.
|
|
35
|
-
- `search-single-index.md` — direct/manual search via the core client
|
|
36
|
-
(`searchSingleIndex`), for cases where InstantSearch is not used.
|
|
48
|
+
correct types).
|
|
49
|
+
- Every record needs an `objectID`. `saveObjects` auto-batches in groups of 1,000.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# InstantSearch setup (CDN, server-rendered templates)
|
|
2
|
+
|
|
3
|
+
Use this when HTML is rendered server-side and there is no bundler or npm build
|
|
4
|
+
step: Rails ERB, Django templates, Laravel Blade, Symfony Twig.
|
|
5
|
+
|
|
6
|
+
## CDN tags (shared layout)
|
|
7
|
+
|
|
8
|
+
```html
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@8/themes/satellite-min.css">
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@5/dist/lite/builds/browser.umd.js"></script>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4/dist/instantsearch.production.min.js"></script>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Globals these expose:
|
|
15
|
+
|
|
16
|
+
- `window['algoliasearch/lite']` → `{ liteClient }`. The lite UMD build's global is
|
|
17
|
+
the literal string key `algoliasearch/lite`, **not** `algoliasearch.liteClient`.
|
|
18
|
+
(`dist/algoliasearch.umd.js` is the full write-capable client under
|
|
19
|
+
`window.algoliasearch` — don't use it for search UI.)
|
|
20
|
+
- `window.instantsearch` → the callable factory, with `.widgets`, `.connectors`.
|
|
21
|
+
|
|
22
|
+
Floating `@5`/`@4`/`@8` track the newest patch. Adding SRI `integrity` requires
|
|
23
|
+
exact pins instead (Algolia's install page publishes hashes for
|
|
24
|
+
`algoliasearch@5.56.0`, `instantsearch.js@4.108.0`, `instantsearch.css@8.18.0`).
|
|
25
|
+
|
|
26
|
+
## Containers
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<div id="search"
|
|
30
|
+
data-algolia-app-id="APP_ID"
|
|
31
|
+
data-algolia-search-key="SEARCH_ONLY_KEY"
|
|
32
|
+
data-algolia-index="products">
|
|
33
|
+
<div id="searchbox"></div>
|
|
34
|
+
<div id="hits"></div>
|
|
35
|
+
</div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Init
|
|
39
|
+
|
|
40
|
+
Inline `<script>` must run after both CDN scripts — put it at the end of `<body>`,
|
|
41
|
+
not in `<head>`.
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<script>
|
|
45
|
+
const el = document.getElementById('search')
|
|
46
|
+
const { liteClient: algoliasearch } = window['algoliasearch/lite']
|
|
47
|
+
const searchClient = algoliasearch(
|
|
48
|
+
el.dataset.algoliaAppId,
|
|
49
|
+
el.dataset.algoliaSearchKey
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const search = instantsearch({ indexName: el.dataset.algoliaIndex, searchClient })
|
|
53
|
+
|
|
54
|
+
search.addWidgets([
|
|
55
|
+
instantsearch.widgets.searchBox({ container: '#searchbox' }),
|
|
56
|
+
instantsearch.widgets.hits({
|
|
57
|
+
container: '#hits',
|
|
58
|
+
templates: {
|
|
59
|
+
item(hit, { html, components }) {
|
|
60
|
+
return html`
|
|
61
|
+
<article>
|
|
62
|
+
<h3>${components.Highlight({ attribute: 'name', hit })}</h3>
|
|
63
|
+
<p>${hit.description}</p>
|
|
64
|
+
</article>
|
|
65
|
+
`
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
search.start()
|
|
72
|
+
</script>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`html` arrives as a property of the template function's second argument — it is
|
|
76
|
+
not an import and not a global. It escapes interpolated values, so it is the XSS
|
|
77
|
+
guard: never string-concatenate hit values or assign them to `innerHTML`.
|
|
78
|
+
|
|
79
|
+
## Injecting credentials from server config
|
|
80
|
+
|
|
81
|
+
Render App ID and the search-only key into `data-` attributes (every engine below
|
|
82
|
+
escapes attribute output), then read them from `dataset` — attribute dashes become
|
|
83
|
+
camelCase: `data-algolia-app-id` → `el.dataset.algoliaAppId`. Never interpolate a
|
|
84
|
+
credential into a JS string literal.
|
|
85
|
+
|
|
86
|
+
- ERB: `data-algolia-app-id="<%= Rails.application.credentials.algolia[:app_id] %>"`
|
|
87
|
+
- Django: `data-algolia-app-id="{{ algolia_app_id }}"` (from the view context)
|
|
88
|
+
- Blade: `data-algolia-app-id="{{ config('services.algolia.app_id') }}"`
|
|
89
|
+
- Twig: `data-algolia-app-id="{{ algolia_app_id }}"` (from a Twig global/parameter)
|
|
90
|
+
|
|
91
|
+
Only ever a **search-only** API key client-side. A write or admin key must never
|
|
92
|
+
appear in HTML, a `data-` attribute, or inline JS.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@algolia/wizard",
|
|
3
|
-
"version": "0.9.0-rc.
|
|
3
|
+
"version": "0.9.0-rc.74.64",
|
|
4
4
|
"description": "Magically implement Algolia functionality in your codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prepare": "husky",
|
|
23
23
|
"prepublishOnly": "pnpm build",
|
|
24
24
|
"reset": "tsx ./scripts/reset-state.ts",
|
|
25
|
-
"test:
|
|
25
|
+
"test:toolchains": "tsx ./scripts/verify-toolchains.ts",
|
|
26
26
|
"test:tools": "tsx ./tool-evals/toolEval.ts",
|
|
27
27
|
"test": "vitest",
|
|
28
28
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Direct search with the core client (v5)
|
|
2
|
-
|
|
3
|
-
Use this only when NOT using InstantSearch (e.g. a custom search box, a server
|
|
4
|
-
route, or programmatic queries). For UI, prefer `instantsearch-setup-<framework>.md`.
|
|
5
|
-
|
|
6
|
-
## Client
|
|
7
|
-
|
|
8
|
-
```ts
|
|
9
|
-
import { algoliasearch } from 'algoliasearch'
|
|
10
|
-
|
|
11
|
-
const client = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
In v5 there is no `client.initIndex(...)`. Index methods take the index name as a
|
|
15
|
-
parameter on the client. Every method takes a single options object.
|
|
16
|
-
|
|
17
|
-
## Search a single index
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
const { hits, nbHits } = await client.searchSingleIndex({
|
|
21
|
-
indexName: 'INDEX_NAME',
|
|
22
|
-
searchParams: { query: 'shoes', hitsPerPage: 20, page: 0 },
|
|
23
|
-
})
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Search multiple indices / queries in one request
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
const { results } = await client.search({
|
|
30
|
-
requests: [
|
|
31
|
-
{ indexName: 'INDEX_NAME', query: 'shoes' },
|
|
32
|
-
{ indexName: 'OTHER_INDEX', query: 'shoes' },
|
|
33
|
-
],
|
|
34
|
-
})
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Notes
|
|
38
|
-
|
|
39
|
-
- `searchSingleIndex` returns up to 1,000 hits. For larger exports use the `browse`
|
|
40
|
-
operation instead.
|
|
41
|
-
- Keep using a **search-only** key for any client-exposed search. Use an admin key
|
|
42
|
-
only in trusted server-side code.
|