@algolia/wizard 0.9.0-rc.71.62 → 0.9.0-rc.73.63

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
@@ -892,12 +892,12 @@ var sidebarItems = [
892
892
  description: "push 100 records to Algolia in seconds"
893
893
  },
894
894
  {
895
- title: "detect your framework",
896
- description: "React, Vue, Angular, Vanilla JS"
895
+ title: "detect your stack",
896
+ description: "React, Vue, Angular, Rails, Django, Laravel & more"
897
897
  },
898
898
  {
899
899
  title: "scaffold a search UI",
900
- description: "a styled InstantSearch component, wired into your app"
900
+ description: "a styled InstantSearch UI, wired into your app or templates"
901
901
  },
902
902
  {
903
903
  title: "ship it",
@@ -1003,7 +1003,7 @@ var accessItems = [
1003
1003
  {
1004
1004
  tag: "READ",
1005
1005
  title: "Project files",
1006
- description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
1006
+ description: "reads your dependency manifests (package.json\u2026), configs & source to detect your stack. Read-only; nothing is uploaded."
1007
1007
  },
1008
1008
  {
1009
1009
  tag: "WRITE",
@@ -2221,15 +2221,282 @@ 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 readdir2, readFile as readFile6 } from "node:fs/promises";
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
2228
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
2229
+ import { existsSync as existsSync2 } from "node:fs";
2230
+ import { join as join9 } from "node:path";
2231
+
2232
+ // src/lib/tools/utils/packageManager.ts
2233
+ import { readFile as readFile6 } from "node:fs/promises";
2234
+ import { existsSync } from "node:fs";
2225
2235
  import { join as join8 } from "node:path";
2236
+ var LOCKFILES = [
2237
+ ["pnpm-lock.yaml", "pnpm"],
2238
+ ["yarn.lock", "yarn"],
2239
+ ["bun.lockb", "bun"],
2240
+ ["bun.lock", "bun"],
2241
+ ["package-lock.json", "npm"]
2242
+ ];
2243
+ async function readPackageJson(cwd = process.cwd()) {
2244
+ return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2245
+ }
2246
+ function packageManagerFrom(pkg) {
2247
+ return pkg.packageManager?.split("@")[0] ?? "npm";
2248
+ }
2249
+ function packageManagerFromLockfile(cwd) {
2250
+ return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2251
+ }
2252
+ async function detectPackageManager(cwd) {
2253
+ try {
2254
+ const pkg = await readPackageJson(cwd);
2255
+ if (pkg.packageManager) return packageManagerFrom(pkg);
2256
+ } catch {
2257
+ }
2258
+ return packageManagerFromLockfile(cwd) ?? "npm";
2259
+ }
2260
+
2261
+ // src/lib/shell.ts
2262
+ function shellQuote(value) {
2263
+ return "'" + value.replace(/'/g, "'\\''") + "'";
2264
+ }
2265
+
2266
+ // src/lib/languages.ts
2267
+ var ENTRYPOINT_TOKEN = "{entrypoint}";
2268
+ var INGEST_DIR = ".algolia-wizard";
2269
+ var LANGUAGE_PROFILES = {
2270
+ javascript: {
2271
+ id: "javascript",
2272
+ displayName: "JavaScript/TypeScript",
2273
+ aliases: [
2274
+ "javascript",
2275
+ "js",
2276
+ "typescript",
2277
+ "ts",
2278
+ "node",
2279
+ "nodejs",
2280
+ "node.js",
2281
+ "bun",
2282
+ "deno",
2283
+ "ecmascript",
2284
+ "jsx",
2285
+ "tsx"
2286
+ ],
2287
+ manifests: ["package.json"],
2288
+ // The concrete npm-family manager is resolved by detectPackageManager (it
2289
+ // honours the package.json `packageManager` field, which lockfiles can't
2290
+ // express), so one spec covers all four and `resolveToolchain` rewrites the
2291
+ // binary below.
2292
+ packageManagers: [
2293
+ {
2294
+ id: "npm",
2295
+ dependency: { mode: "agent-declares", file: "package.json" },
2296
+ installSteps: [{ argv: ["npm", "install"] }],
2297
+ ingest: {
2298
+ kind: "auto",
2299
+ argv: ["node", ENTRYPOINT_TOKEN],
2300
+ entrypointExtensions: [".mjs", ".cjs", ".js"]
2301
+ }
2302
+ }
2303
+ ],
2304
+ sdk: { packageName: "algoliasearch", versionPin: "^5", docKey: "js" },
2305
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.mjs`,
2306
+ // package.json scripts are repo-defined, so they're resolved at run time by
2307
+ // repoVerification rather than listed here.
2308
+ verification: [],
2309
+ envReadInstruction: "Read them from `process.env`.",
2310
+ skipDirs: ["node_modules", "dist", "build", "coverage", ".next", "out"]
2311
+ }
2312
+ };
2313
+ var DEFAULT_LANGUAGE_ID = "javascript";
2314
+ var JAVASCRIPT = "javascript";
2315
+ var CURATED_LANGUAGES = Object.values(
2316
+ LANGUAGE_PROFILES
2317
+ ).map((profile) => profile.displayName);
2318
+ function isBackendLanguage(profile) {
2319
+ return profile.id !== JAVASCRIPT;
2320
+ }
2321
+ function normalizeLanguageName(name) {
2322
+ return name.toLowerCase().trim().replace(/[\s_-]+/g, "");
2323
+ }
2324
+ var ALIAS_TO_ID = /* @__PURE__ */ new Map();
2325
+ for (const profile of Object.values(LANGUAGE_PROFILES)) {
2326
+ for (const alias of [profile.id, profile.displayName, ...profile.aliases]) {
2327
+ ALIAS_TO_ID.set(normalizeLanguageName(alias), profile.id);
2328
+ }
2329
+ }
2330
+ function resolveLanguageProfile(name) {
2331
+ const id = ALIAS_TO_ID.get(normalizeLanguageName(name));
2332
+ return id ? LANGUAGE_PROFILES[id] : void 0;
2333
+ }
2334
+ function isSameLanguage(a, b) {
2335
+ const x = resolveLanguageProfile(a);
2336
+ const y = resolveLanguageProfile(b);
2337
+ if (x && y) return x.id === y.id;
2338
+ if (x || y) return false;
2339
+ const folded = normalizeLanguageName(a);
2340
+ return folded !== "" && folded === normalizeLanguageName(b);
2341
+ }
2342
+ var BASE_SKIP_DIRS = ["node_modules", ".git", "dist"];
2343
+ var ALL_SKIP_DIRS = /* @__PURE__ */ new Set([
2344
+ ...BASE_SKIP_DIRS,
2345
+ ...Object.values(LANGUAGE_PROFILES).flatMap((p) => p.skipDirs)
2346
+ ]);
2347
+ var ALLOWED_BINARIES = new Set(
2348
+ Object.values(LANGUAGE_PROFILES).flatMap((profile) => [
2349
+ ...profile.packageManagers.flatMap((pm) => [
2350
+ ...pm.installSteps.map((s) => s.argv[0]),
2351
+ ...pm.ingest.kind === "auto" ? [pm.ingest.argv[0]] : []
2352
+ ]),
2353
+ ...profile.verification.map((v) => v.argv[0])
2354
+ ])
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
+ }
2492
+
2493
+ // src/lib/tools/searchFiles.ts
2226
2494
  var MAX_QUERY_LENGTH = 1e3;
2227
2495
  async function walkFiles(dir) {
2228
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2229
2496
  const out = [];
2230
- for (const e of await readdir2(dir, { withFileTypes: true })) {
2231
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2232
- const full = join8(dir, e.name);
2497
+ for (const e of await readdir3(dir, { withFileTypes: true })) {
2498
+ if (e.name.startsWith(".") || ALL_SKIP_DIRS.has(e.name)) continue;
2499
+ const full = join10(dir, e.name);
2233
2500
  if (e.isDirectory()) out.push(...await walkFiles(full));
2234
2501
  else if (e.isFile()) out.push(full);
2235
2502
  }
@@ -2262,7 +2529,7 @@ function searchFilesTool(ctx) {
2262
2529
  for (const file of await walkFiles(resolved.target)) {
2263
2530
  let content;
2264
2531
  try {
2265
- content = await readFile6(file, "utf8");
2532
+ content = await readFile8(file, "utf8");
2266
2533
  } catch {
2267
2534
  continue;
2268
2535
  }
@@ -2286,6 +2553,10 @@ function searchFilesTool(ctx) {
2286
2553
  import { tool as tool8 } from "ai";
2287
2554
  import z11 from "zod";
2288
2555
 
2556
+ // src/lib/tools/repoVerification.ts
2557
+ import { existsSync as existsSync3 } from "node:fs";
2558
+ import { join as join11 } from "node:path";
2559
+
2289
2560
  // src/lib/tools/utils/runCommand.ts
2290
2561
  import { spawn as spawn2 } from "node:child_process";
2291
2562
  var INSTALL_TIMEOUT_MS = 15 * 6e4;
@@ -2337,71 +2608,89 @@ Timed out after ${seconds}s: ${command} ${args.join(" ")}`.trim(),
2337
2608
  });
2338
2609
  }
2339
2610
 
2340
- // src/lib/tools/utils/packageManager.ts
2341
- import { readFile as readFile7 } from "node:fs/promises";
2342
- import { existsSync } from "node:fs";
2343
- import { join as join9 } from "node:path";
2344
- var LOCKFILES = [
2345
- ["pnpm-lock.yaml", "pnpm"],
2346
- ["yarn.lock", "yarn"],
2347
- ["bun.lockb", "bun"],
2348
- ["bun.lock", "bun"],
2349
- ["package-lock.json", "npm"]
2350
- ];
2351
- async function readPackageJson(cwd = process.cwd()) {
2352
- return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
2353
- }
2354
- function packageManagerFrom(pkg) {
2355
- return pkg.packageManager?.split("@")[0] ?? "npm";
2356
- }
2357
- function packageManagerFromLockfile(cwd) {
2358
- return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
2359
- }
2360
- async function detectPackageManager(cwd) {
2361
- try {
2362
- const pkg = await readPackageJson(cwd);
2363
- if (pkg.packageManager) return packageManagerFrom(pkg);
2364
- } catch {
2365
- }
2366
- return packageManagerFromLockfile(cwd) ?? "npm";
2367
- }
2368
-
2369
2611
  // src/lib/tools/repoVerification.ts
2370
2612
  var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
2371
- async function runRepoVerificationCheck() {
2613
+ async function runCheck(command, binary, args) {
2614
+ const { code, output } = await runCommand(binary, args, {
2615
+ timeoutMs: VERIFY_TIMEOUT_MS
2616
+ });
2617
+ return { command, exitCode: code, ok: code === 0, output: output.trim() };
2618
+ }
2619
+ async function javascriptChecks() {
2372
2620
  let pkg;
2373
2621
  try {
2374
2622
  pkg = await readPackageJson();
2375
2623
  } catch (err) {
2376
- const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2377
- return { ok: false, checks: [], limitation };
2624
+ return {
2625
+ limitation: `Could not read package.json to detect verification conventions: ${err.message}`
2626
+ };
2378
2627
  }
2379
2628
  const scripts = pkg.scripts ?? {};
2380
2629
  const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
2381
2630
  if (present.length === 0) {
2382
- const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2383
- return { ok: false, checks: [], limitation };
2631
+ return {
2632
+ limitation: `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`
2633
+ };
2384
2634
  }
2385
2635
  const pm = await detectPackageManager(process.cwd());
2386
2636
  const checks = [];
2387
2637
  for (const script of present) {
2388
- const command = `${pm} run ${script}`;
2389
- const { code, output } = await runCommand(pm, ["run", script], {
2390
- timeoutMs: VERIFY_TIMEOUT_MS
2391
- });
2392
- checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
2638
+ checks.push(
2639
+ await runCheck(`${pm} run ${script}`, pm, ["run", script])
2640
+ );
2393
2641
  }
2394
- return { ok: checks.every((c) => c.ok), checks };
2642
+ return { checks };
2643
+ }
2644
+ async function registryChecks(id) {
2645
+ const profile = LANGUAGE_PROFILES[id];
2646
+ const runnable = profile.verification.filter(
2647
+ (spec) => !spec.requiresFile || existsSync3(join11(process.cwd(), spec.requiresFile))
2648
+ );
2649
+ if (runnable.length === 0) {
2650
+ return {
2651
+ limitation: `No mechanical verification available for ${profile.displayName} in this repo.`
2652
+ };
2653
+ }
2654
+ const checks = [];
2655
+ for (const spec of runnable) {
2656
+ checks.push(
2657
+ await runCheck(spec.argv.join(" "), spec.argv[0], [...spec.argv.slice(1)])
2658
+ );
2659
+ }
2660
+ return { checks };
2661
+ }
2662
+ async function runRepoVerificationCheck(languages = [DEFAULT_LANGUAGE_ID]) {
2663
+ const ids = [...new Set(languages)];
2664
+ if (ids.length === 0) ids.push(DEFAULT_LANGUAGE_ID);
2665
+ const checks = [];
2666
+ const limitations = [];
2667
+ for (const id of ids) {
2668
+ const result = id === JAVASCRIPT ? await javascriptChecks() : await registryChecks(id);
2669
+ if ("checks" in result) checks.push(...result.checks);
2670
+ else limitations.push(result.limitation);
2671
+ }
2672
+ if (checks.length === 0) {
2673
+ return {
2674
+ ok: false,
2675
+ checks: [],
2676
+ limitation: limitations.join(" ") || "No verification checks available."
2677
+ };
2678
+ }
2679
+ return {
2680
+ ok: checks.every((c) => c.ok),
2681
+ checks,
2682
+ ...limitations.length ? { limitation: limitations.join(" ") } : {}
2683
+ };
2395
2684
  }
2396
2685
 
2397
2686
  // src/lib/tools/verifyImplementation.ts
2398
- function verifyImplementationTool() {
2687
+ function verifyImplementationTool(ctx) {
2399
2688
  return tool8({
2400
- description: "Run the repo's mechanical verification check for generated implementation changes. Detects lint/typecheck/check from package.json and returns structured pass/fail evidence for the verifier to interpret.",
2689
+ description: "Run the repo's mechanical verification checks for generated implementation changes. Uses the conventions of the repo's languages (package.json lint/typecheck/check scripts for JavaScript, the equivalent compile/analyze command elsewhere) and returns structured pass/fail evidence for the verifier to interpret.",
2401
2690
  inputSchema: z11.object(),
2402
2691
  execute: async () => {
2403
- logger.info("called verifyImplementation tool");
2404
- return runRepoVerificationCheck();
2692
+ logger.info({ languages: ctx.languages }, "called verifyImplementation tool");
2693
+ return runRepoVerificationCheck(ctx.languages);
2405
2694
  }
2406
2695
  });
2407
2696
  }
@@ -2527,12 +2816,17 @@ var DEFAULT_TOOL_LIMITS = {
2527
2816
  read: 20,
2528
2817
  match: 100
2529
2818
  };
2530
- function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
2819
+ function createToolContext({
2820
+ limits = DEFAULT_TOOL_LIMITS,
2821
+ cwd = process.cwd(),
2822
+ languages = [DEFAULT_LANGUAGE_ID]
2823
+ } = {}) {
2531
2824
  return {
2532
2825
  root: cwd,
2533
2826
  cwd,
2534
2827
  limits,
2535
- counts: { list: 0, search: 0, read: 0 }
2828
+ counts: { list: 0, search: 0, read: 0 },
2829
+ languages: languages.length ? languages : [DEFAULT_LANGUAGE_ID]
2536
2830
  };
2537
2831
  }
2538
2832
 
@@ -2569,7 +2863,7 @@ function createTools(ctx, { output, tools }) {
2569
2863
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2570
2864
  verifyImplementation: withLogging(
2571
2865
  "verifyImplementation",
2572
- verifyImplementationTool()
2866
+ verifyImplementationTool(ctx)
2573
2867
  ),
2574
2868
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2575
2869
  notifyUser: withLogging("notifyUser", notifyUserTool())
@@ -2605,7 +2899,7 @@ async function runAgent(req) {
2605
2899
  baseURL: PROXY_BASE_URL,
2606
2900
  fetch: proxyFetch
2607
2901
  });
2608
- const toolContext = createToolContext();
2902
+ const toolContext = createToolContext({ languages: req.languages });
2609
2903
  const readTools = ["readFile", "searchFiles", "listFiles"];
2610
2904
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2611
2905
  const instructions = [
@@ -2696,8 +2990,11 @@ var detectLanguageSchema = z16.object({
2696
2990
  var detectLanguage = () => runAgent({
2697
2991
  instructions: [
2698
2992
  "Analyze the codebase and determine the programming languages and frameworks used",
2993
+ "Start from the dependency manifests: package.json.",
2994
+ "List the language that owns the backend/data code first \u2014 that is the one an ingestion script will be written in.",
2699
2995
  "If a superset language is found, exclude the subset language. TS-over-JS.",
2700
2996
  "If a meta-framework is used, exclude the framework. Next-over-React.",
2997
+ "Frameworks include backend and server-rendering frameworks (e.g. Rails, Django, Laravel, Symfony, Spring Boot, ASP.NET Core, Flask, Gin, Ktor) as well as frontend ones (React, Vue, Angular, Svelte) and mobile ones (Flutter, SwiftUI).",
2701
2998
  "Return the exact version",
2702
2999
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
2703
3000
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -2746,6 +3043,7 @@ var MODE_CONFIG = {
2746
3043
  "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
2747
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).",
2748
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.",
2749
3047
  "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
2750
3048
  "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
2751
3049
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -2757,8 +3055,9 @@ var MODE_CONFIG = {
2757
3055
  instructions: [
2758
3056
  "Analyze the codebase to determine the single best location to add search UI functionality.",
2759
3057
  "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
2760
- "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
2761
- '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".',
2762
3061
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2763
3062
  "When done, call reportStatus"
2764
3063
  ],
@@ -2767,7 +3066,7 @@ var MODE_CONFIG = {
2767
3066
  verification: {
2768
3067
  instructions: [
2769
3068
  "Analyze the codebase to determine which code-quality tools are available to validate changes.",
2770
- "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.",
2771
3070
  'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
2772
3071
  "Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
2773
3072
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -2795,7 +3094,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2795
3094
  // package.json
2796
3095
  var package_default = {
2797
3096
  name: "@algolia/wizard",
2798
- version: "0.9.0-rc.71.62",
3097
+ version: "0.9.0-rc.73.63",
2799
3098
  description: "Magically implement Algolia functionality in your codebase",
2800
3099
  type: "module",
2801
3100
  engines: {
@@ -2898,82 +3197,185 @@ function parseEntries(raw) {
2898
3197
  return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
2899
3198
  }
2900
3199
  var summarize = (entries) => entries.length ? entries.map((e) => e.name).join(", ") : "none";
2901
- async function askList(ctx, prompt, { required = false } = {}) {
3200
+
3201
+ // src/actions/confirmLanguage.ts
3202
+ import z19 from "zod";
3203
+ var confirmLanguageSchema = z19.object({
3204
+ languages: detectLanguageSchema.shape.languages
3205
+ });
3206
+ var OTHER_OPTION = "Other";
3207
+ function confirmed(languages) {
3208
+ track("AI Wizard Language Confirmed", { languages });
3209
+ return { languages };
3210
+ }
3211
+ async function askOtherLanguage(ctx) {
3212
+ let prompt = "enter the language for your ingestion script";
2902
3213
  for (; ; ) {
2903
3214
  const answer = await ctx.requestUserInput({
2904
3215
  prompt,
2905
3216
  promptType: "textInput",
2906
- options: [],
2907
- helpText: 'Comma-separated, e.g. "TypeScript, Node".'
3217
+ options: []
2908
3218
  });
2909
3219
  if (typeof answer !== "string") {
2910
- throw new Error("askList received an unexpected non-text result");
3220
+ throw new Error("confirmLanguage received an unexpected non-text result");
2911
3221
  }
2912
- const entries = parseEntries(answer);
2913
- if (entries.length || !required) return entries;
2914
- prompt = "Please enter at least one entry:";
3222
+ const name = parseEntries(answer)[0]?.name;
3223
+ if (name) return name;
3224
+ prompt = "please enter a language name:";
2915
3225
  }
2916
3226
  }
2917
-
2918
- // src/actions/confirmLanguage.ts
2919
- import z19 from "zod";
2920
- var confirmLanguageSchema = z19.object({
2921
- languages: detectLanguageSchema.shape.languages
2922
- });
2923
3227
  async function confirmLanguage(ctx) {
2924
3228
  const detected = ctx.getStepOutput("project-scan");
2925
- const answer = await ctx.requestUserInput({
2926
- prompt: "Did we detect your language(s) correctly?",
2927
- promptType: "acceptReject",
2928
- options: ["Yes", "No"],
2929
- messages: [`Languages: ${summarize(detected.languages)}`]
2930
- });
2931
- const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
2932
- required: true
2933
- });
2934
- track("AI Wizard Language Confirmed", {
2935
- languages
3229
+ const detectedLanguages = detected.languages ?? [];
3230
+ const others = (chosen) => detectedLanguages.filter((l) => !isSameLanguage(l.name, chosen));
3231
+ const primary = detectedLanguages[0];
3232
+ if (primary) {
3233
+ const accepted = await ctx.requestUserInput({
3234
+ prompt: `Write the ingestion script in ${primary.name}?`,
3235
+ promptType: "acceptReject",
3236
+ options: [`Confirm ${primary.name}`, "Use a different language"],
3237
+ secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0],
3238
+ messages: detectedLanguages.length > 1 ? [`Detected: ${summarize(detectedLanguages)}`] : []
3239
+ });
3240
+ if (accepted === true) return confirmed(detectedLanguages);
3241
+ }
3242
+ const options = [...CURATED_LANGUAGES];
3243
+ for (const language of detectedLanguages) {
3244
+ if (!options.some((o) => isSameLanguage(o, language.name))) {
3245
+ options.push(language.name);
3246
+ }
3247
+ }
3248
+ options.push(OTHER_OPTION);
3249
+ const detectedFor = (option) => detectedLanguages.find((l) => isSameLanguage(option, l.name));
3250
+ const secondary = options.map(
3251
+ (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
3252
+ );
3253
+ const defaultSelectedIndex = Math.max(
3254
+ options.findIndex((o) => detectedFor(o)),
3255
+ 0
3256
+ );
3257
+ const selection = await ctx.requestUserInput({
3258
+ prompt: "select the language for your ingestion script",
3259
+ promptType: "multipleChoice",
3260
+ options,
3261
+ secondary,
3262
+ defaultSelectedIndex
2936
3263
  });
2937
- return { languages };
3264
+ if (typeof selection !== "string") {
3265
+ throw new Error("confirmLanguage received an unexpected non-text result");
3266
+ }
3267
+ const name = selection === OTHER_OPTION ? await askOtherLanguage(ctx) : selection;
3268
+ const version = detectedFor(name)?.version ?? "unknown";
3269
+ return confirmed([{ name, version }, ...others(name)]);
2938
3270
  }
2939
3271
 
2940
3272
  // src/actions/confirmFramework.ts
2941
3273
  import z20 from "zod";
2942
- var confirmFrameworkSchema = z20.object({
2943
- frameworks: detectLanguageSchema.shape.frameworks
2944
- });
2945
- var CURATED_FRAMEWORKS = [
2946
- "Next.js",
2947
- "React",
2948
- "Vue",
2949
- "Angular",
2950
- "Svelte",
2951
- "Vanilla JS"
3274
+
3275
+ // src/lib/frameworks.ts
3276
+ var BACKEND_ONLY_FRAMEWORK = "Backend only / API";
3277
+ var FRAMEWORKS = [
3278
+ // Frontend — InstantSearch component flavors.
3279
+ { name: "Next.js", strategy: "react", aliases: ["next", "nextjs"] },
3280
+ { name: "React", strategy: "react", aliases: ["reactjs"] },
3281
+ { name: "Vue", strategy: "vue", aliases: ["vuejs", "nuxt", "nuxtjs"] },
3282
+ { name: "Angular", strategy: "angular", aliases: ["angularjs"] },
3283
+ // No Svelte InstantSearch flavor exists, so it uses InstantSearch.js.
3284
+ { name: "Svelte", strategy: "js", aliases: ["sveltekit"] },
3285
+ {
3286
+ name: "Vanilla JS",
3287
+ strategy: "js",
3288
+ aliases: ["vanilla", "javascript", "js", "astro", "vite"]
3289
+ },
3290
+ // Backend — Algolia's official framework integrations. Server-rendered
3291
+ // templates get InstantSearch.js from a CDN.
3292
+ {
3293
+ name: "Rails",
3294
+ strategy: "cdn-template",
3295
+ aliases: ["rubyonrails", "ruby on rails", "erb"]
3296
+ },
3297
+ { name: "Django", strategy: "cdn-template", aliases: ["jinja", "jinja2"] },
3298
+ { name: "Laravel", strategy: "cdn-template", aliases: ["blade"] },
3299
+ { name: "Symfony", strategy: "cdn-template", aliases: ["twig"] },
3300
+ // Mobile — Algolia ships InstantSearch iOS/Android and Dart clients, but the
3301
+ // wizard can't scaffold a native UI, so it points at the docs instead.
3302
+ { name: "Flutter", strategy: "none", aliases: [] },
3303
+ { name: "iOS", strategy: "none", aliases: ["swiftui", "uikit"] },
3304
+ { name: "Android", strategy: "none", aliases: ["jetpack compose", "compose"] },
3305
+ { name: BACKEND_ONLY_FRAMEWORK, strategy: "cdn-template", aliases: [] }
2952
3306
  ];
2953
- var OTHER_OPTION = "Other";
3307
+ var CURATED_FRAMEWORKS = FRAMEWORKS.map(
3308
+ (f) => f.name
3309
+ );
2954
3310
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2955
- var FRAMEWORK_ALIASES = {
2956
- next: "nextjs",
2957
- nextjs: "nextjs",
2958
- react: "react",
2959
- reactjs: "react",
2960
- vue: "vue",
2961
- vuejs: "vue",
2962
- angular: "angular",
2963
- angularjs: "angular",
2964
- svelte: "svelte",
2965
- sveltekit: "svelte",
2966
- vanillajs: "vanillajs",
2967
- vanilla: "vanillajs",
2968
- javascript: "vanillajs",
2969
- js: "vanillajs"
2970
- };
2971
- var isSameFramework = (a, b) => {
2972
- const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
2973
- const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
3311
+ var ALIAS_TO_NAME = /* @__PURE__ */ new Map();
3312
+ for (const framework of FRAMEWORKS) {
3313
+ for (const alias of [framework.name, ...framework.aliases]) {
3314
+ ALIAS_TO_NAME.set(normalize(alias), framework.name);
3315
+ }
3316
+ }
3317
+ var STRATEGY_BY_NAME = new Map(FRAMEWORKS.map((f) => [f.name, f.strategy]));
3318
+ function canonicalFrameworkName(name) {
3319
+ return ALIAS_TO_NAME.get(normalize(name));
3320
+ }
3321
+ function isSameFramework(a, b) {
3322
+ const x = canonicalFrameworkName(a) ?? normalize(a);
3323
+ const y = canonicalFrameworkName(b) ?? normalize(b);
2974
3324
  return x !== "" && x === y;
2975
- };
2976
- function confirmed(name, version) {
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
+ }
3372
+
3373
+ // src/actions/confirmFramework.ts
3374
+ var confirmFrameworkSchema = z20.object({
3375
+ frameworks: detectLanguageSchema.shape.frameworks
3376
+ });
3377
+ var OTHER_OPTION2 = "Other";
3378
+ function confirmed2(name, version) {
2977
3379
  const frameworks = [{ name, version: version ?? "unknown" }];
2978
3380
  track("AI Wizard Frontend Framework Confirmed", { frameworks });
2979
3381
  return { frameworks };
@@ -3001,7 +3403,7 @@ async function confirmFramework(ctx) {
3001
3403
  for (const fw of detectedFrameworks) {
3002
3404
  if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
3003
3405
  }
3004
- options.push(OTHER_OPTION);
3406
+ options.push(OTHER_OPTION2);
3005
3407
  const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
3006
3408
  const primary = detectedFrameworks[0];
3007
3409
  if (primary) {
@@ -3011,7 +3413,7 @@ async function confirmFramework(ctx) {
3011
3413
  options: [`Confirm ${primary.name}`, "Use a different framework"],
3012
3414
  secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
3013
3415
  });
3014
- if (accepted === true) return confirmed(primary.name, primary.version);
3416
+ if (accepted === true) return confirmed2(primary.name, primary.version);
3015
3417
  }
3016
3418
  const secondary = options.map(
3017
3419
  (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
@@ -3021,7 +3423,7 @@ async function confirmFramework(ctx) {
3021
3423
  0
3022
3424
  );
3023
3425
  const selection = await ctx.requestUserInput({
3024
- prompt: "select a framework",
3426
+ prompt: "select the framework that renders your UI",
3025
3427
  promptType: "multipleChoice",
3026
3428
  options,
3027
3429
  secondary,
@@ -3030,10 +3432,10 @@ async function confirmFramework(ctx) {
3030
3432
  if (typeof selection !== "string") {
3031
3433
  throw new Error("confirmFramework received an unexpected non-text result");
3032
3434
  }
3033
- if (selection === OTHER_OPTION) {
3034
- return confirmed(await askOtherFramework(ctx));
3435
+ if (selection === OTHER_OPTION2) {
3436
+ return confirmed2(await askOtherFramework(ctx));
3035
3437
  }
3036
- return confirmed(selection, detectedFor(selection)?.version);
3438
+ return confirmed2(selection, detectedFor(selection)?.version);
3037
3439
  }
3038
3440
 
3039
3441
  // src/actions/promptUser.ts
@@ -3126,15 +3528,15 @@ async function confirmEntities(ctx) {
3126
3528
  onSubmit: () => {
3127
3529
  }
3128
3530
  });
3129
- const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3130
- if (confirmed2.length === 0) {
3531
+ const confirmed3 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3532
+ if (confirmed3.length === 0) {
3131
3533
  throw new Error("User cancelled entity selection \u2014 analysis halted.");
3132
3534
  }
3133
- ctx.setUserInput("confirmedEntities", confirmed2);
3535
+ ctx.setUserInput("confirmedEntities", confirmed3);
3134
3536
  track("AI Wizard Entities Confirmed", {
3135
- entities: toEntitySummary(confirmed2)
3537
+ entities: toEntitySummary(confirmed3)
3136
3538
  });
3137
- return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
3539
+ return { ingestionAnalysis: entities, confirmedEntities: confirmed3 };
3138
3540
  }
3139
3541
 
3140
3542
  // src/actions/review.ts
@@ -3158,7 +3560,7 @@ ${JSON.stringify(s.output, null, 2)}`
3158
3560
  }
3159
3561
  function formatReviewSummary(result) {
3160
3562
  const nextStepLines = result.nextSteps.map((step) => {
3161
- const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3563
+ const isIngestCommand = step.includes("algolia-wizard/");
3162
3564
  const isWorktreeCommand = step.includes("/worktrees/");
3163
3565
  return {
3164
3566
  text: `\u2192 ${step}`,
@@ -3200,13 +3602,14 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3200
3602
  import z24 from "zod";
3201
3603
 
3202
3604
  // src/lib/worktree.ts
3203
- import { execFile, spawn as spawn3 } from "node:child_process";
3204
- 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";
3205
3608
  import {
3206
3609
  basename as basename2,
3207
3610
  dirname as dirname7,
3208
3611
  isAbsolute as isAbsolute2,
3209
- join as join10,
3612
+ join as join12,
3210
3613
  relative as relative2,
3211
3614
  resolve as resolve3
3212
3615
  } from "node:path";
@@ -3240,8 +3643,8 @@ async function isWorkingTreeDirty(repoRoot) {
3240
3643
  return out.trim().length > 0;
3241
3644
  }
3242
3645
  async function pruneOldWorktrees(repoRoot) {
3243
- const dir = join10(stateDir(repoRoot), "worktrees");
3244
- 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);
3245
3648
  for (const slug of stale) {
3246
3649
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
3247
3650
  try {
@@ -3251,7 +3654,7 @@ async function pruneOldWorktrees(repoRoot) {
3251
3654
  "worktree",
3252
3655
  "remove",
3253
3656
  "--force",
3254
- join10(dir, slug)
3657
+ join12(dir, slug)
3255
3658
  ]);
3256
3659
  await git(["-C", repoRoot, "branch", "-D", branch]);
3257
3660
  } catch (err) {
@@ -3265,43 +3668,55 @@ async function pruneOldWorktrees(repoRoot) {
3265
3668
  async function createWorktree(repoRoot) {
3266
3669
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3267
3670
  const dirSlug = branch.replace(/\//g, "-");
3268
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3671
+ const path = join12(stateDir(repoRoot), "worktrees", dirSlug);
3269
3672
  await git(["-C", repoRoot, "worktree", "prune"]);
3270
3673
  await pruneOldWorktrees(repoRoot);
3271
3674
  await mkdir6(dirname7(path), { recursive: true });
3272
3675
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
3273
3676
  return { path, branch };
3274
3677
  }
3275
- async function installWorktreeDeps(worktreePath) {
3276
- try {
3277
- await readPackageJson(worktreePath);
3278
- } catch {
3279
- return { ok: true, output: "no package.json; skipped install" };
3280
- }
3281
- const pm = await detectPackageManager(worktreePath);
3282
- return new Promise((resolve4) => {
3283
- let output = "";
3284
- const child = spawn3(pm, ["install"], {
3285
- cwd: worktreePath,
3286
- stdio: ["ignore", "pipe", "pipe"]
3287
- });
3288
- child.stdout?.on("data", (d) => output += d);
3289
- child.stderr?.on("data", (d) => output += d);
3290
- child.on(
3291
- "error",
3292
- (err) => resolve4({
3293
- ok: false,
3294
- output: `Failed to run ${pm} install: ${err.message}`
3295
- })
3296
- );
3297
- child.on(
3298
- "close",
3299
- (code) => resolve4({ ok: code === 0, output: output.trim() })
3300
- );
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
3301
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() };
3302
3718
  }
3303
- var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3304
- function validateIngestEntrypoint(worktreePath, entrypoint) {
3719
+ function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
3305
3720
  if (!entrypoint || entrypoint.startsWith("-")) {
3306
3721
  return {
3307
3722
  ok: false,
@@ -3316,18 +3731,29 @@ function validateIngestEntrypoint(worktreePath, entrypoint) {
3316
3731
  reason: `entrypoint "${entrypoint}" resolves outside the worktree`
3317
3732
  };
3318
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
+ }
3319
3740
  return { ok: true, target };
3320
3741
  }
3321
- async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3322
- if (!INGEST_RUNTIMES.includes(runtime)) {
3742
+ async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
3743
+ const { ingest, profile, packageManager } = toolchain;
3744
+ if (ingest.kind !== "auto") {
3323
3745
  return {
3324
3746
  ran: false,
3325
3747
  ok: false,
3326
3748
  output: "",
3327
- reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
3749
+ reason: `${profile.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
3328
3750
  };
3329
3751
  }
3330
- const validated = validateIngestEntrypoint(worktreePath, entrypoint);
3752
+ const validated = validateIngestEntrypoint(
3753
+ worktreePath,
3754
+ entrypoint,
3755
+ ingest.entrypointExtensions
3756
+ );
3331
3757
  if (!validated.ok) {
3332
3758
  return { ran: false, ok: false, output: "", reason: validated.reason };
3333
3759
  }
@@ -3348,29 +3774,13 @@ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3348
3774
  reason: `entrypoint "${entrypoint}" does not exist`
3349
3775
  };
3350
3776
  }
3351
- return new Promise((resolveRun) => {
3352
- let output = "";
3353
- const child = spawn3(runtime, [entrypoint], {
3354
- cwd: worktreePath,
3355
- shell: false,
3356
- stdio: ["ignore", "pipe", "pipe"],
3357
- env: { ...process.env, ...env }
3358
- });
3359
- child.stdout?.on("data", (d) => output += d);
3360
- child.stderr?.on("data", (d) => output += d);
3361
- child.on(
3362
- "error",
3363
- (err) => resolveRun({
3364
- ran: true,
3365
- ok: false,
3366
- output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
3367
- })
3368
- );
3369
- child.on(
3370
- "close",
3371
- (code) => resolveRun({ ran: true, ok: code === 0, output: output.trim() })
3372
- );
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
3373
3782
  });
3783
+ return { ran: true, ok: code === 0, output: output.trim() };
3374
3784
  }
3375
3785
  async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
3376
3786
  const trimmed = sourcePath.trim();
@@ -3385,8 +3795,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3385
3795
  } catch {
3386
3796
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3387
3797
  }
3388
- const relPath = join10(ingestDir, basename2(source));
3389
- const dest = join10(worktreePath, relPath);
3798
+ const relPath = join12(ingestDir, basename2(source));
3799
+ const dest = join12(worktreePath, relPath);
3390
3800
  try {
3391
3801
  await mkdir6(dirname7(dest), { recursive: true });
3392
3802
  await copyFile(source, dest);
@@ -3402,10 +3812,10 @@ function hasEnvVar(content, name) {
3402
3812
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3403
3813
  }
3404
3814
  async function writeSearchEnvValues(worktreePath, vars) {
3405
- const target = join10(worktreePath, ".env");
3815
+ const target = join12(worktreePath, ".env");
3406
3816
  let existing = "";
3407
3817
  try {
3408
- existing = await readFile8(target, "utf8");
3818
+ existing = await readFile9(target, "utf8");
3409
3819
  } catch (err) {
3410
3820
  if (err.code !== "ENOENT") throw err;
3411
3821
  }
@@ -3522,69 +3932,33 @@ async function resolveSearchOnlyKey(index) {
3522
3932
  }
3523
3933
 
3524
3934
  // src/lib/algoliaDocs.ts
3525
- import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3526
- import { dirname as dirname8, join as join11 } from "node:path";
3935
+ import { readFileSync, existsSync as existsSync5 } from "node:fs";
3936
+ import { dirname as dirname8, join as join13 } from "node:path";
3527
3937
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3528
- var DOCS_SUBPATH = join11("docs", "algolia-sdk");
3938
+ var DOCS_SUBPATH = join13("docs", "algolia-sdk");
3529
3939
  function findDocsDir() {
3530
3940
  let dir = dirname8(fileURLToPath2(import.meta.url));
3531
3941
  for (; ; ) {
3532
- const candidate = join11(dir, DOCS_SUBPATH);
3533
- if (existsSync2(candidate)) return candidate;
3942
+ const candidate = join13(dir, DOCS_SUBPATH);
3943
+ if (existsSync5(candidate)) return candidate;
3534
3944
  const parent = dirname8(dir);
3535
3945
  if (parent === dir) return void 0;
3536
3946
  dir = parent;
3537
3947
  }
3538
3948
  }
3539
- function loadAlgoliaDoc(language) {
3540
- const docsDir = findDocsDir();
3541
- if (!docsDir) {
3542
- logger.warn(
3543
- "algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
3544
- );
3545
- return "";
3546
- }
3547
- const files = readdirSync(docsDir).filter((f) => f.includes(language));
3548
- if (files.length === 0) {
3549
- logger.warn(
3550
- { language },
3551
- "algoliaDocs: no SDK reference found for language; skipping"
3552
- );
3553
- return "";
3554
- }
3555
- return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3556
- }
3557
- function getNamedDoc(name, language) {
3949
+ function getNamedDoc(name, key) {
3558
3950
  const docsDir = findDocsDir();
3559
3951
  if (!docsDir) {
3560
3952
  logger.warn("docs/algolia-sdk not found");
3561
3953
  return "";
3562
3954
  }
3563
- const file = join11(docsDir, `${name}-${language}.md`);
3564
- if (!existsSync2(file)) {
3565
- 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");
3566
3958
  return "";
3567
3959
  }
3568
3960
  return readFileSync(file, "utf8").trim();
3569
3961
  }
3570
- function getFrameworkSpecificDoc(frameworks) {
3571
- const fw = frameworks.map((f) => f.toLowerCase());
3572
- if (fw.includes("vue") || fw.includes("nuxt")) {
3573
- return loadAlgoliaDoc("vue");
3574
- }
3575
- if (fw.includes("react") || fw.includes("next.js")) {
3576
- return loadAlgoliaDoc("react");
3577
- }
3578
- if (fw.includes("angular")) {
3579
- return loadAlgoliaDoc("angular");
3580
- }
3581
- return loadAlgoliaDoc("js");
3582
- }
3583
-
3584
- // src/lib/shell.ts
3585
- function shellQuote(value) {
3586
- return "'" + value.replace(/'/g, "'\\''") + "'";
3587
- }
3588
3962
 
3589
3963
  // src/actions/implement.ts
3590
3964
  var implementSchema = z24.object({
@@ -3619,12 +3993,11 @@ var implementSchema = z24.object({
3619
3993
  });
3620
3994
  var implementationOutputSchema = z24.object({
3621
3995
  summary: z24.string(),
3622
- // Ingestion only: how to run the generated script, as a structured pair the
3623
- // wizard turns into an argv (`<runtime> <entrypoint>`) never a free-form
3624
- // command string. `runtime` is constrained to an allowlisted interpreter and
3625
- // `entrypoint` is validated to a worktree-relative path before execution, so
3626
- // the agent cannot inject extra commands or swap the interpreter.
3627
- 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.
3628
4001
  entrypoint: z24.string().optional()
3629
4002
  });
3630
4003
  var verificationOutputSchema = z24.object({
@@ -3634,47 +4007,11 @@ var verificationOutputSchema = z24.object({
3634
4007
  });
3635
4008
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3636
4009
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
3637
- var INGEST_DIR = ".algolia-wizard";
3638
- function detectUiFramework(language) {
3639
- const names = language.frameworks.map((f) => f.name.toLowerCase());
3640
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3641
- if (names.some((n) => n.includes("react") || n.includes("next")))
3642
- return "React";
3643
- if (names.some((n) => n.includes("angular"))) return "Angular";
3644
- return "JavaScript";
3645
- }
3646
- function frameworksForDoc(framework) {
3647
- switch (framework) {
3648
- case "React":
3649
- return ["react"];
3650
- case "Vue":
3651
- return ["vue"];
3652
- case "Angular":
3653
- return ["angular"];
3654
- case "JavaScript":
3655
- return [];
3656
- }
3657
- }
3658
- function publicEnvPrefix(language) {
3659
- const frameworkNames = language.frameworks.map(
3660
- (framework) => framework.name.toLowerCase()
4010
+ function buildSearchEnvVars(language, strategy, appId, searchKey) {
4011
+ const prefix = publicEnvPrefix(
4012
+ language.frameworks.map((framework) => framework.name),
4013
+ strategy
3661
4014
  );
3662
- if (frameworkNames.some((name) => name.includes("next"))) {
3663
- return "NEXT_PUBLIC_";
3664
- }
3665
- if (frameworkNames.some((name) => name.includes("nuxt"))) {
3666
- return "NUXT_PUBLIC_";
3667
- }
3668
- if (frameworkNames.some((name) => name.includes("astro"))) {
3669
- return "PUBLIC_";
3670
- }
3671
- if (frameworkNames.some((name) => name.includes("vite"))) {
3672
- return "VITE_";
3673
- }
3674
- return "PUBLIC_";
3675
- }
3676
- function searchEnvVars(language, appId, searchKey) {
3677
- const prefix = publicEnvPrefix(language);
3678
4015
  return [
3679
4016
  {
3680
4017
  name: `${prefix}ALGOLIA_APP_ID`,
@@ -3686,6 +4023,38 @@ function searchEnvVars(language, appId, searchKey) {
3686
4023
  }
3687
4024
  ];
3688
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
+ }
3689
4058
  function baseInstructions(input) {
3690
4059
  return [
3691
4060
  `Target Algolia index: ${input.targetIndex}`,
@@ -3713,37 +4082,48 @@ function sourceSpecificInstructions(input) {
3713
4082
  generated: [
3714
4083
  "No real data source exists; use sample records for each confirmed entity.",
3715
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.",
3716
- "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.",
3717
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."
3718
4087
  ]
3719
4088
  };
3720
4089
  return byLine[input.ingestionSource];
3721
4090
  }
3722
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` : ""}.`;
3723
4096
  return [
3724
4097
  ...input.confirmed && input.confirmed.length ? [
3725
- `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.`,
3726
4099
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3727
- `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.`,
3728
- "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.`,
3729
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.",
3730
- getNamedDoc("save-records", "js"),
3731
- '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),
3732
4105
  "The summary should be extremely concise.",
3733
- `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,
3734
4107
  ...sourceSpecificInstructions(input)
3735
4108
  ] : []
3736
4109
  ];
3737
4110
  }
3738
4111
  function searchInstructions(input) {
3739
- 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.`;
3740
4118
  return [
3741
4119
  "Implement an in-app Algolia search experience.",
3742
- `Build the search UI for ${input.uiFramework}.`,
3743
- "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:",
3744
4122
  doc,
3745
- `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.`,
3746
- "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.",
3747
4127
  // appId always resolves (loadActiveProfile throws otherwise); only the
3748
4128
  // search-only key is best-effort and can fall back to a placeholder.
3749
4129
  `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
@@ -3751,20 +4131,22 @@ function searchInstructions(input) {
3751
4131
  // resolved app id / search-only key into ".env" under these exact names
3752
4132
  // right after this step, so a renamed prefix here would leave the code
3753
4133
  // reading a var the wizard never wrote.
3754
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3755
- '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(", ")}.`,
3756
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."
3757
4136
  ];
3758
4137
  }
3759
4138
  function verificationInstructions(input) {
4139
+ const protectedDirs = [
4140
+ .../* @__PURE__ */ new Set([input.ingestDir, ingestScriptDir(input.ingestionProfile)])
4141
+ ];
3760
4142
  return [
3761
4143
  "Verify the Algolia implementation changes in the current worktree.",
3762
4144
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
3763
- "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.`,
3764
4146
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
3765
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.",
3766
4148
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
3767
- `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.`,
3768
4150
  "Always call reportStatus with status=success once verification has run, even when sufficient=false.",
3769
4151
  "Set sufficient=true only when the implementation is complete and checks pass (or fail for a clearly unrelated reason).",
3770
4152
  "Set sufficient=false when the implementation is incomplete or has implementation-caused failures; include concrete additionalInstructions for the next pass."
@@ -3773,14 +4155,17 @@ function verificationInstructions(input) {
3773
4155
  var IMPLEMENT_CONFIG = {
3774
4156
  ingestion: {
3775
4157
  title: "Algolia ingestion",
4158
+ label: "Ingestion",
3776
4159
  buildInstructions: ingestionInstructions
3777
4160
  },
3778
4161
  search: {
3779
4162
  title: "Algolia search",
4163
+ label: "Search",
3780
4164
  buildInstructions: searchInstructions
3781
4165
  },
3782
4166
  verification: {
3783
4167
  title: "Algolia verification",
4168
+ label: "Verification",
3784
4169
  buildInstructions: verificationInstructions
3785
4170
  }
3786
4171
  };
@@ -3812,11 +4197,10 @@ function buildAgentInstructions(useCase, input, extraInstructions = []) {
3812
4197
  ];
3813
4198
  }
3814
4199
  function formatSummary(useCase, summary) {
3815
- const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3816
- return `${label}: ${summary}`;
4200
+ return `${IMPLEMENT_CONFIG[useCase].label}: ${summary}`;
3817
4201
  }
3818
- function buildIngestCommand(worktree, runtime, entrypoint) {
3819
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4202
+ function buildIngestCommand(worktree, toolchain, entrypoint) {
4203
+ return `cd ${shellQuote(worktree)} && ${describeIngestCommand(toolchain.ingest, entrypoint)}`;
3820
4204
  }
3821
4205
  function parseIngestRecordCount(output) {
3822
4206
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
@@ -3898,7 +4282,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3898
4282
  await confirmDirtyWorkingTree(ctx, repoRoot);
3899
4283
  }
3900
4284
  const normalized = normalizeFindingPaths(findings);
3901
- const confirmed2 = normalized.confirmedEntities;
4285
+ const confirmed3 = normalized.confirmedEntities;
3902
4286
  const searchLocation = normalized.searchImplementationAnalysis;
3903
4287
  let appId;
3904
4288
  let searchKey;
@@ -3936,31 +4320,66 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3936
4320
  );
3937
4321
  }
3938
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
+ );
3939
4350
  const input = {
3940
4351
  findings: normalized,
3941
- confirmed: confirmed2,
4352
+ confirmed: confirmed3,
3942
4353
  searchLocation,
3943
4354
  targetIndex,
3944
4355
  language,
3945
4356
  appId,
3946
4357
  searchKey,
3947
- searchEnvVars: searchEnvVars(language, appId, searchKey),
4358
+ searchEnvVars: buildSearchEnvVars(
4359
+ language,
4360
+ searchStrategy,
4361
+ appId,
4362
+ searchKey
4363
+ ),
3948
4364
  ingestDir: INGEST_DIR,
3949
4365
  ingestionSource,
3950
4366
  uploadFilePath,
3951
- // language.frameworks already prefers the confirm-framework step output,
3952
- // so the user's confirmed stack (not just raw detection) picks the flavor.
3953
- uiFramework: detectUiFramework(language)
4367
+ searchStrategy,
4368
+ frameworkName,
4369
+ ingestionProfile,
4370
+ toolchain,
4371
+ verificationLanguages
3954
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;
3955
4375
  const summaries = [];
3956
4376
  if (uploadWarning) summaries.push(uploadWarning);
3957
4377
  let agentRuns = 0;
3958
- let ingestRuntime;
3959
4378
  let ingestEntrypoint;
3960
4379
  let ingestScriptRan = false;
3961
4380
  let ingestRecordCount;
3962
4381
  let ingestDurationMs;
3963
- let installFailed = false;
4382
+ const failedInstalls = /* @__PURE__ */ new Set();
3964
4383
  let ingestOutcomeMessage;
3965
4384
  async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
3966
4385
  if (agentRuns > 0) ctx.recordStepExecution();
@@ -3974,16 +4393,19 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3974
4393
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
3975
4394
  outputSchema: implementationOutputSchema
3976
4395
  });
4396
+ const useCaseToolchain = toolchainForUseCase(currentUseCase);
4397
+ if (!useCaseToolchain) return result;
3977
4398
  ctx.notify({
3978
4399
  messages: [`Installing dependencies for ${currentUseCase}\u2026`]
3979
4400
  });
3980
4401
  const installLogId = ctx.logStart("installWorktreeDeps", {
3981
- useCase: currentUseCase
4402
+ useCase: currentUseCase,
4403
+ language: useCaseToolchain.profile.id
3982
4404
  });
3983
- const install = await installWorktreeDeps(worktree);
4405
+ const install = await installWorktreeDeps(worktree, useCaseToolchain);
3984
4406
  ctx.logEnd(installLogId, install.ok ? "success" : "error");
3985
4407
  if (!install.ok) {
3986
- installFailed = true;
4408
+ failedInstalls.add(useCaseToolchain.profile.displayName);
3987
4409
  logger.warn(
3988
4410
  { useCase: currentUseCase, output: install.output },
3989
4411
  "implement: dependency install in worktree failed; generated commands may not run until deps are installed"
@@ -3997,15 +4419,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3997
4419
  return runAgent({
3998
4420
  instructions: buildAgentInstructions("verification", input),
3999
4421
  tools: toolsForUseCase("verification"),
4000
- outputSchema: verificationOutputSchema
4422
+ outputSchema: verificationOutputSchema,
4423
+ // So verifyImplementation runs this repo's checks, not just npm scripts.
4424
+ languages: input.verificationLanguages
4001
4425
  });
4002
4426
  }
4003
4427
  if (useCases.includes("ingestion")) {
4004
- const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4428
+ const { summary, entrypoint } = await runImplementationUseCase("ingestion");
4005
4429
  summaries.push(formatSummary("ingestion", summary));
4006
- ingestRuntime = runtime;
4007
4430
  ingestEntrypoint = entrypoint;
4008
- if (ingestRuntime && ingestEntrypoint && !installFailed) {
4431
+ if (ingestEntrypoint && toolchain.ingest.kind === "auto" && failedInstalls.size === 0) {
4009
4432
  ctx.clearNotices();
4010
4433
  const runNow = await ctx.requestUserInput({
4011
4434
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
@@ -4017,13 +4440,13 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4017
4440
  const profile = await loadActiveProfile();
4018
4441
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
4019
4442
  const scriptLogId = ctx.logStart("runIngestScript", {
4020
- runtime: ingestRuntime,
4443
+ language: ingestionProfile.id,
4021
4444
  entrypoint: ingestEntrypoint
4022
4445
  });
4023
4446
  const startedAt = Date.now();
4024
4447
  const run2 = await runIngestScript(
4025
4448
  worktree,
4026
- ingestRuntime,
4449
+ toolchain,
4027
4450
  ingestEntrypoint,
4028
4451
  {
4029
4452
  [APP_ID_VAR]: profile.appId,
@@ -4037,7 +4460,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4037
4460
  ingestRecordCount = parseIngestRecordCount(run2.output);
4038
4461
  if (ingestRecordCount != null) {
4039
4462
  track("AI Wizard Ingest Successful", {
4040
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4463
+ entity_name: confirmed3?.map((e) => e.name).join(", ") || "unknown",
4041
4464
  record_count: ingestRecordCount,
4042
4465
  duration_ms: ingestDurationMs
4043
4466
  });
@@ -4050,7 +4473,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4050
4473
  outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run2.reason}`;
4051
4474
  logger.warn(
4052
4475
  {
4053
- runtime: ingestRuntime,
4476
+ language: ingestionProfile.id,
4054
4477
  entrypoint: ingestEntrypoint,
4055
4478
  reason: run2.reason
4056
4479
  },
@@ -4073,7 +4496,7 @@ ${run2.output}` : status;
4073
4496
  outcomeMessage = `\u274C Ingestion failed.${run2.output ? ` ${run2.output}` : ""}`;
4074
4497
  logger.warn(
4075
4498
  {
4076
- runtime: ingestRuntime,
4499
+ language: ingestionProfile.id,
4077
4500
  entrypoint: ingestEntrypoint,
4078
4501
  output: run2.output
4079
4502
  },
@@ -4090,10 +4513,28 @@ ${run2.output}` : status;
4090
4513
  }
4091
4514
  }
4092
4515
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4093
- if (ingestRuntime && ingestEntrypoint) {
4516
+ if (ingestEntrypoint) {
4094
4517
  commandMessages.push(
4095
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4518
+ `Ingestion command: ${buildIngestCommand(worktree, toolchain, ingestEntrypoint)}`
4096
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
+ }
4097
4538
  }
4098
4539
  await ctx.requestUserInput({
4099
4540
  // No question being asked here, just an acknowledgement — the
@@ -4104,7 +4545,20 @@ ${run2.output}` : status;
4104
4545
  messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4105
4546
  });
4106
4547
  }
4107
- 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) {
4108
4562
  let extraInstructions = [];
4109
4563
  const preSearchFiles = new Set(await listChangedFiles(worktree));
4110
4564
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
@@ -4175,9 +4629,9 @@ ${run2.output}` : status;
4175
4629
  "implement: agent reported success but no files changed in the worktree"
4176
4630
  );
4177
4631
  }
4178
- if (installFailed) {
4632
+ if (failedInstalls.size > 0) {
4179
4633
  summaries.push(
4180
- '\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.`
4181
4635
  );
4182
4636
  }
4183
4637
  return {
@@ -4185,10 +4639,10 @@ ${run2.output}` : status;
4185
4639
  filesChanged,
4186
4640
  summary: summaries.join("\n\n"),
4187
4641
  worktreePath: worktree,
4188
- ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
4642
+ ...useCases.includes("ingestion") && ingestEntrypoint ? {
4189
4643
  ingestCommand: buildIngestCommand(
4190
4644
  worktree,
4191
- ingestRuntime,
4645
+ toolchain,
4192
4646
  ingestEntrypoint
4193
4647
  ),
4194
4648
  ingestScriptRan,
@@ -4517,20 +4971,20 @@ function parseCliArgs(argv) {
4517
4971
  }
4518
4972
 
4519
4973
  // src/lib/resetState.ts
4520
- import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
4521
- import { join as join12 } from "node:path";
4974
+ import { readdir as readdir5, rm as rm2 } from "node:fs/promises";
4975
+ import { join as join14 } from "node:path";
4522
4976
  var KEEP = ["wizard.log"];
4523
4977
  async function resetProjectState() {
4524
4978
  const dir = stateDir();
4525
4979
  let entries;
4526
4980
  try {
4527
- entries = await readdir4(dir);
4981
+ entries = await readdir5(dir);
4528
4982
  } catch {
4529
4983
  return { dir, removed: [] };
4530
4984
  }
4531
4985
  const targets = entries.filter((name) => !KEEP.includes(name));
4532
4986
  await Promise.all(
4533
- targets.map((name) => rm2(join12(dir, name), { recursive: true, force: true }))
4987
+ targets.map((name) => rm2(join14(dir, name), { recursive: true, force: true }))
4534
4988
  );
4535
4989
  return { dir, removed: targets };
4536
4990
  }