@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 readdir2(dir, { withFileTypes: true })) {
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 = join9(dir, e.name);
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 readFile7(file, "utf8");
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 existsSync2 } from "node:fs";
2414
- import { join as join10 } from "node:path";
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 || existsSync2(join10(process.cwd(), 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
- "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
2914
- 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
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 package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
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.72.60",
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:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
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(".algolia-wizard/ingest.sh");
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, spawn as spawn3 } from "node:child_process";
3414
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
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 join11,
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 = join11(stateDir(repoRoot), "worktrees");
3454
- const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
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
- join11(dir, slug)
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 = join11(stateDir(repoRoot), "worktrees", dirSlug);
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 installWorktreeDeps(worktreePath) {
3486
- try {
3487
- await readPackageJson(worktreePath);
3488
- } catch {
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
- var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
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, runtime, entrypoint, env = {}) {
3532
- if (!INGEST_RUNTIMES.includes(runtime)) {
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: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
3749
+ reason: `${profile.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
3538
3750
  };
3539
3751
  }
3540
- const validated = validateIngestEntrypoint(worktreePath, entrypoint);
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
- return new Promise((resolveRun) => {
3562
- let output = "";
3563
- const child = spawn3(runtime, [entrypoint], {
3564
- cwd: worktreePath,
3565
- shell: false,
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 = join11(ingestDir, basename2(source));
3599
- const dest = join11(worktreePath, relPath);
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 = join11(worktreePath, ".env");
3815
+ const target = join12(worktreePath, ".env");
3616
3816
  let existing = "";
3617
3817
  try {
3618
- existing = await readFile8(target, "utf8");
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, readdirSync, existsSync as existsSync3 } from "node:fs";
3736
- import { dirname as dirname8, join as join12 } from "node:path";
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 = join12("docs", "algolia-sdk");
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 = join12(dir, DOCS_SUBPATH);
3743
- if (existsSync3(candidate)) return candidate;
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 loadAlgoliaDoc(language) {
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 = join12(docsDir, `${name}-${language}.md`);
3774
- if (!existsSync3(file)) {
3775
- logger.warn({ name, language }, "named SDK reference not found");
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: how to run the generated script, as a structured pair the
3828
- // wizard turns into an argv (`<runtime> <entrypoint>`) never a free-form
3829
- // command string. `runtime` is constrained to an allowlisted interpreter and
3830
- // `entrypoint` is validated to a worktree-relative path before execution, so
3831
- // the agent cannot inject extra commands or swap the interpreter.
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
- var INGEST_DIR2 = ".algolia-wizard";
3843
- function detectUiFramework(language) {
3844
- const names = language.frameworks.map((f) => f.name.toLowerCase());
3845
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
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 (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) instead of inlining the records as literals.",
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
- `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
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
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
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", "js"),
3936
- 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4103
+ getNamedDoc("save-records", profile.sdk.docKey),
4104
+ dependencyInstruction(toolchain),
3937
4105
  "The summary should be extremely concise.",
3938
- `Return how to run the script as two fields, not a command string: "runtime" (one of ${INGEST_RUNTIMES.join(", ")}) and "entrypoint" (the script path relative to the worktree root, e.g. "${input.ingestDir}/ingest.mjs"). The wizard runs \`<runtime> <entrypoint>\` directly, so the entrypoint must be a plain path with no flags or arguments. Write a script one of those interpreters can run as-is.`,
4106
+ runInstruction,
3939
4107
  ...sourceSpecificInstructions(input)
3940
4108
  ] : []
3941
4109
  ];
3942
4110
  }
3943
4111
  function searchInstructions(input) {
3944
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
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.uiFramework}.`,
3948
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
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
- `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
3951
- "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.",
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 public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
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
- "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
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 "${input.ingestDir}/" unless verifyImplementation reports an actionable issue in its files.`,
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
- const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
4021
- return `${label}: ${summary}`;
4200
+ return `${IMPLEMENT_CONFIG[useCase].label}: ${summary}`;
4022
4201
  }
4023
- function buildIngestCommand(worktree, runtime, entrypoint) {
4024
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
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
- INGEST_DIR2,
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: searchEnvVars(language, appId, searchKey),
4153
- ingestDir: INGEST_DIR2,
4358
+ searchEnvVars: buildSearchEnvVars(
4359
+ language,
4360
+ searchStrategy,
4361
+ appId,
4362
+ searchKey
4363
+ ),
4364
+ ingestDir: INGEST_DIR,
4154
4365
  ingestionSource,
4155
4366
  uploadFilePath,
4156
- // language.frameworks already prefers the confirm-framework step output,
4157
- // so the user's confirmed stack (not just raw detection) picks the flavor.
4158
- uiFramework: detectUiFramework(language)
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
- let installFailed = false;
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
- installFailed = true;
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, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4428
+ const { summary, entrypoint } = await runImplementationUseCase("ingestion");
4210
4429
  summaries.push(formatSummary("ingestion", summary));
4211
- ingestRuntime = runtime;
4212
4430
  ingestEntrypoint = entrypoint;
4213
- if (ingestRuntime && ingestEntrypoint && !installFailed) {
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
- runtime: ingestRuntime,
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
- ingestRuntime,
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
- runtime: ingestRuntime,
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
- runtime: ingestRuntime,
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 (ingestRuntime && ingestEntrypoint) {
4516
+ if (ingestEntrypoint) {
4299
4517
  commandMessages.push(
4300
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
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
- if (useCases.includes("search")) {
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 (installFailed) {
4632
+ if (failedInstalls.size > 0) {
4384
4633
  summaries.push(
4385
- '\u26A0\uFE0F Dependency install in the worktree failed. Run your package manager install in the worktree before the command below, or it will fail with "Cannot find module".'
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") && ingestRuntime && ingestEntrypoint ? {
4642
+ ...useCases.includes("ingestion") && ingestEntrypoint ? {
4394
4643
  ingestCommand: buildIngestCommand(
4395
4644
  worktree,
4396
- ingestRuntime,
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 readdir4, rm as rm2 } from "node:fs/promises";
4726
- import { join as join13 } from "node:path";
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 readdir4(dir);
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(join13(dir, name), { recursive: true, force: true }))
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 JS SDK reference (search)
1
+ # Algolia SDK reference
2
2
 
3
- Authoritative reference for wiring **Algolia search** in JavaScript/TypeScript apps.
4
- Prefer the method shapes documented here over prior knowledge — they are pinned to
5
- the current stable majors and avoid known pitfalls (e.g. v5 type mismatches with
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
- ## Versions
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
- - `algoliasearch` v5 (install `^5`)
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
- Always install the **latest stable** within the major (use a caret range like `^5` /
17
- `^7`); never pin an exact patch.
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
- - Read the Application ID and search-only key from **public env vars**, never hardcode
24
- them inline.
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
- client cache and causes re-renders.
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 — see `instantsearch-setup.md`).
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.72.60",
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:fixtures": "touch .env && tsx --env-file=.env ./fixtures/run-fixtures.ts",
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.