@algolia/wizard 0.5.0 → 0.6.0-rc.51.22

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
@@ -796,12 +796,12 @@ var sidebarItems = [
796
796
  description: "push 100 records to Algolia in seconds"
797
797
  },
798
798
  {
799
- title: "detect your framework",
800
- description: "React, Vue, Angular, Vanilla JS"
799
+ title: "detect your stack",
800
+ description: "React, Vue, Angular, Rails, Django, Laravel & more"
801
801
  },
802
802
  {
803
803
  title: "scaffold a search UI",
804
- description: "a styled InstantSearch component, wired into your app"
804
+ description: "a styled InstantSearch UI, wired into your app or templates"
805
805
  },
806
806
  {
807
807
  title: "ship it",
@@ -907,7 +907,7 @@ var accessItems = [
907
907
  {
908
908
  tag: "READ",
909
909
  title: "Project files",
910
- description: "reads package.json, configs & source to detect your stack. Read-only; nothing is uploaded."
910
+ description: "reads your dependency manifests (package.json, Gemfile, go.mod, pom.xml\u2026), configs & source to detect your stack. Read-only; nothing is uploaded."
911
911
  },
912
912
  {
913
913
  tag: "WRITE",
@@ -2155,15 +2155,647 @@ function writeCredentialsTool(ctx) {
2155
2155
  // src/lib/tools/searchFiles.ts
2156
2156
  import { tool as tool7 } from "ai";
2157
2157
  import z10 from "zod";
2158
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2158
+ import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
2159
+ import { join as join10 } from "node:path";
2160
+
2161
+ // src/lib/languages.ts
2162
+ import { readdir as readdir2 } from "node:fs/promises";
2163
+ import { existsSync as existsSync2 } from "node:fs";
2164
+ import { join as join9 } from "node:path";
2165
+
2166
+ // src/lib/tools/utils/packageManager.ts
2167
+ import { readFile as readFile6 } from "node:fs/promises";
2168
+ import { existsSync } from "node:fs";
2159
2169
  import { join as join8 } from "node:path";
2170
+ var LOCKFILES = [
2171
+ ["pnpm-lock.yaml", "pnpm"],
2172
+ ["yarn.lock", "yarn"],
2173
+ ["bun.lockb", "bun"],
2174
+ ["bun.lock", "bun"],
2175
+ ["package-lock.json", "npm"]
2176
+ ];
2177
+ async function readPackageJson(cwd = process.cwd()) {
2178
+ return JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
2179
+ }
2180
+ function packageManagerFrom(pkg) {
2181
+ return pkg.packageManager?.split("@")[0] ?? "npm";
2182
+ }
2183
+ function packageManagerFromLockfile(cwd) {
2184
+ return LOCKFILES.find(([file]) => existsSync(join8(cwd, file)))?.[1];
2185
+ }
2186
+ async function detectPackageManager(cwd) {
2187
+ try {
2188
+ const pkg = await readPackageJson(cwd);
2189
+ if (pkg.packageManager) return packageManagerFrom(pkg);
2190
+ } catch {
2191
+ }
2192
+ return packageManagerFromLockfile(cwd) ?? "npm";
2193
+ }
2194
+
2195
+ // src/lib/shell.ts
2196
+ function shellQuote(value) {
2197
+ return "'" + value.replace(/'/g, "'\\''") + "'";
2198
+ }
2199
+
2200
+ // src/lib/languages.ts
2201
+ var ENTRYPOINT_TOKEN = "{entrypoint}";
2202
+ var INGEST_DIR = ".algolia-wizard";
2203
+ var PY_VENV = `${INGEST_DIR}/.venv`;
2204
+ var PY_VENV_PYTHON = `${PY_VENV}/bin/python`;
2205
+ var PY_REQUIREMENTS = `${INGEST_DIR}/requirements.txt`;
2206
+ var CSHARP_PROJECT = `${INGEST_DIR}/ingest/ingest.csproj`;
2207
+ var SWIFT_PACKAGE_DIR = `${INGEST_DIR}/Ingest`;
2208
+ var LANGUAGE_PROFILES = {
2209
+ javascript: {
2210
+ id: "javascript",
2211
+ displayName: "JavaScript/TypeScript",
2212
+ aliases: [
2213
+ "javascript",
2214
+ "js",
2215
+ "typescript",
2216
+ "ts",
2217
+ "node",
2218
+ "nodejs",
2219
+ "node.js",
2220
+ "bun",
2221
+ "deno",
2222
+ "ecmascript",
2223
+ "jsx",
2224
+ "tsx"
2225
+ ],
2226
+ manifests: ["package.json"],
2227
+ // The concrete npm-family manager is resolved by detectPackageManager (it
2228
+ // honours the package.json `packageManager` field, which lockfiles can't
2229
+ // express), so one spec covers all four and `resolveToolchain` rewrites the
2230
+ // binary below.
2231
+ packageManagers: [
2232
+ {
2233
+ id: "npm",
2234
+ dependency: { mode: "agent-declares", file: "package.json" },
2235
+ installSteps: [{ argv: ["npm", "install"] }],
2236
+ ingest: {
2237
+ kind: "auto",
2238
+ argv: ["node", ENTRYPOINT_TOKEN],
2239
+ entrypointExtensions: [".mjs", ".cjs", ".js"]
2240
+ }
2241
+ }
2242
+ ],
2243
+ sdk: { packageName: "algoliasearch", versionPin: "^5", docKey: "js" },
2244
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.mjs`,
2245
+ // package.json scripts are repo-defined, so they're resolved at run time by
2246
+ // repoVerification rather than listed here.
2247
+ verification: [],
2248
+ envReadInstruction: "Read them from `process.env`.",
2249
+ skipDirs: ["node_modules", "dist", "build", "coverage", ".next", "out"]
2250
+ },
2251
+ python: {
2252
+ id: "python",
2253
+ displayName: "Python",
2254
+ aliases: ["python", "python3", "py", "cpython"],
2255
+ manifests: [
2256
+ "pyproject.toml",
2257
+ "requirements.txt",
2258
+ "setup.py",
2259
+ "setup.cfg",
2260
+ "Pipfile"
2261
+ ],
2262
+ // Deliberately one path for every Python repo: a wizard-owned venv under
2263
+ // .algolia-wizard. Reusing the project's uv/poetry environment would mean
2264
+ // mutating the developer's real dependency manifest and lockfile, and the
2265
+ // declare-here/install-there split is the main way ingestion silently ends
2266
+ // up without the SDK installed. The tradeoff: the script can import the
2267
+ // Algolia client and anything it declares itself, but not the project's own
2268
+ // packages (see the optional root-requirements step below).
2269
+ packageManagers: [
2270
+ {
2271
+ id: "pip-venv",
2272
+ dependency: { mode: "agent-declares", file: PY_REQUIREMENTS },
2273
+ installSteps: [
2274
+ { argv: ["python3", "-m", "venv", PY_VENV] },
2275
+ {
2276
+ argv: [PY_VENV_PYTHON, "-m", "pip", "install", "-r", PY_REQUIREMENTS]
2277
+ },
2278
+ // Best-effort access to the project's own dependencies (DB drivers,
2279
+ // ORMs) when the repo pins them the classic way.
2280
+ {
2281
+ argv: [
2282
+ PY_VENV_PYTHON,
2283
+ "-m",
2284
+ "pip",
2285
+ "install",
2286
+ "-r",
2287
+ "requirements.txt"
2288
+ ],
2289
+ requiresFile: "requirements.txt",
2290
+ optional: true
2291
+ }
2292
+ ],
2293
+ ingest: {
2294
+ kind: "auto",
2295
+ argv: [PY_VENV_PYTHON, ENTRYPOINT_TOKEN],
2296
+ entrypointExtensions: [".py"]
2297
+ }
2298
+ }
2299
+ ],
2300
+ sdk: {
2301
+ packageName: "algoliasearch",
2302
+ versionPin: ">=4,<5",
2303
+ docKey: "python"
2304
+ },
2305
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.py`,
2306
+ verification: [
2307
+ {
2308
+ label: "python compileall",
2309
+ argv: ["python3", "-m", "compileall", "-q", INGEST_DIR]
2310
+ }
2311
+ ],
2312
+ envReadInstruction: "Read them from `os.environ`.",
2313
+ skipDirs: [
2314
+ "venv",
2315
+ "__pycache__",
2316
+ "site-packages",
2317
+ "dist",
2318
+ "build",
2319
+ "htmlcov"
2320
+ ]
2321
+ },
2322
+ ruby: {
2323
+ id: "ruby",
2324
+ displayName: "Ruby",
2325
+ aliases: ["ruby", "rb", "rails", "ruby on rails", "rubyonrails"],
2326
+ manifests: ["Gemfile", "*.gemspec"],
2327
+ packageManagers: [
2328
+ {
2329
+ id: "bundler",
2330
+ dependency: { mode: "agent-declares", file: "Gemfile" },
2331
+ installSteps: [{ argv: ["bundle", "install"] }],
2332
+ ingest: {
2333
+ kind: "auto",
2334
+ argv: ["bundle", "exec", "ruby", ENTRYPOINT_TOKEN],
2335
+ entrypointExtensions: [".rb"]
2336
+ }
2337
+ }
2338
+ ],
2339
+ sdk: { packageName: "algolia", versionPin: "~> 3.0", docKey: "ruby" },
2340
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.rb`,
2341
+ // Ruby has no directory-level syntax check (`ruby -c` is one file at a
2342
+ // time), so verification relies on the agent's own review here.
2343
+ verification: [],
2344
+ envReadInstruction: "Read them from `ENV.fetch('NAME')`.",
2345
+ skipDirs: ["vendor", "tmp", "log", "coverage"]
2346
+ },
2347
+ php: {
2348
+ id: "php",
2349
+ displayName: "PHP",
2350
+ aliases: ["php", "laravel", "symfony"],
2351
+ manifests: ["composer.json"],
2352
+ packageManagers: [
2353
+ {
2354
+ id: "composer",
2355
+ // `composer require` both declares and installs, and unlike editing
2356
+ // composer.json by hand it can't leave composer.lock out of date (which
2357
+ // makes a later `composer install` refuse to run).
2358
+ dependency: { mode: "wizard-installs" },
2359
+ installSteps: [
2360
+ {
2361
+ argv: [
2362
+ "composer",
2363
+ "require",
2364
+ "algolia/algoliasearch-client-php:^4",
2365
+ "--no-interaction",
2366
+ // Repo post-install scripts are the project's code, not ours to
2367
+ // trigger; Laravel's package:discover also fails in a bare tree.
2368
+ "--no-scripts"
2369
+ ]
2370
+ }
2371
+ ],
2372
+ ingest: {
2373
+ kind: "auto",
2374
+ argv: ["php", ENTRYPOINT_TOKEN],
2375
+ entrypointExtensions: [".php"]
2376
+ }
2377
+ }
2378
+ ],
2379
+ sdk: {
2380
+ packageName: "algolia/algoliasearch-client-php",
2381
+ versionPin: "^4",
2382
+ docKey: "php"
2383
+ },
2384
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.php`,
2385
+ verification: [],
2386
+ envReadInstruction: "Read them from `getenv('NAME')`.",
2387
+ skipDirs: ["vendor", "node_modules"]
2388
+ },
2389
+ go: {
2390
+ id: "go",
2391
+ displayName: "Go",
2392
+ aliases: ["go", "golang"],
2393
+ manifests: ["go.mod"],
2394
+ packageManagers: [
2395
+ {
2396
+ id: "gomod",
2397
+ // Imports in the generated file are the declaration; `go mod tidy`
2398
+ // resolves and fetches them.
2399
+ dependency: { mode: "code-imports" },
2400
+ installSteps: [{ argv: ["go", "mod", "tidy"] }],
2401
+ ingest: {
2402
+ kind: "auto",
2403
+ argv: ["go", "run", ENTRYPOINT_TOKEN],
2404
+ entrypointExtensions: [".go"]
2405
+ }
2406
+ }
2407
+ ],
2408
+ sdk: {
2409
+ packageName: "github.com/algolia/algoliasearch-client-go/v4",
2410
+ versionPin: "v4",
2411
+ docKey: "go"
2412
+ },
2413
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.go`,
2414
+ verification: [
2415
+ { label: "go vet", argv: ["go", "vet", "./..."], requiresFile: "go.mod" }
2416
+ ],
2417
+ envReadInstruction: "Read them from `os.Getenv`.",
2418
+ skipDirs: ["vendor", "bin"]
2419
+ },
2420
+ java: {
2421
+ id: "java",
2422
+ displayName: "Java",
2423
+ aliases: ["java"],
2424
+ manifests: ["pom.xml", "build.gradle", "build.gradle.kts"],
2425
+ packageManagers: [
2426
+ {
2427
+ id: "maven",
2428
+ detectFiles: ["pom.xml"],
2429
+ dependency: { mode: "agent-declares", file: "pom.xml" },
2430
+ installSteps: [{ argv: ["mvn", "-q", "-DskipTests", "compile"] }],
2431
+ // The main class is a wizard constant the instructions require the agent
2432
+ // to use, so execution can't be redirected by agent output. Runnable only
2433
+ // because the install step above compiles src/main/java first — which is
2434
+ // why the entrypoint lives there rather than under .algolia-wizard/.
2435
+ ingest: {
2436
+ kind: "auto",
2437
+ argv: [
2438
+ "mvn",
2439
+ "-q",
2440
+ "org.codehaus.mojo:exec-maven-plugin:3.5.0:java",
2441
+ "-Dexec.mainClass=AlgoliaWizardIngest"
2442
+ ],
2443
+ entrypointExtensions: [".java"]
2444
+ }
2445
+ },
2446
+ {
2447
+ id: "gradle",
2448
+ detectFiles: ["build.gradle", "build.gradle.kts"],
2449
+ dependency: { mode: "agent-declares", file: "build.gradle" },
2450
+ installSteps: [],
2451
+ // Auto-running means executing the repo's own ./gradlew wrapper; out of
2452
+ // scope for now, so the wizard writes the code and prints the command.
2453
+ ingest: {
2454
+ kind: "manual",
2455
+ entrypointExtensions: [".java"],
2456
+ runCommand: "./gradlew runAlgoliaIngest"
2457
+ }
2458
+ }
2459
+ ],
2460
+ sdk: {
2461
+ packageName: "com.algolia:algoliasearch",
2462
+ versionPin: "4.+",
2463
+ docKey: "java",
2464
+ alsoRequires: "The class must be named AlgoliaWizardIngest, in the default package (no `package` statement), with a `public static void main`."
2465
+ },
2466
+ // Not under .algolia-wizard/: Maven and Gradle only compile src/main/<lang>,
2467
+ // so a class outside it never makes it onto the classpath and the run command
2468
+ // fails with "class not found".
2469
+ ingestEntrypointExample: "src/main/java/AlgoliaWizardIngest.java",
2470
+ verification: [
2471
+ {
2472
+ label: "mvn compile",
2473
+ argv: ["mvn", "-q", "-DskipTests", "compile"],
2474
+ requiresFile: "pom.xml"
2475
+ }
2476
+ ],
2477
+ envReadInstruction: "Read them from `System.getenv`.",
2478
+ skipDirs: ["target", "build", "out"]
2479
+ },
2480
+ kotlin: {
2481
+ id: "kotlin",
2482
+ displayName: "Kotlin",
2483
+ aliases: ["kotlin", "kt", "ktor"],
2484
+ manifests: ["build.gradle.kts", "build.gradle", "pom.xml"],
2485
+ packageManagers: [
2486
+ {
2487
+ id: "gradle",
2488
+ dependency: { mode: "agent-declares", file: "build.gradle.kts" },
2489
+ installSteps: [],
2490
+ ingest: {
2491
+ kind: "manual",
2492
+ entrypointExtensions: [".kt"],
2493
+ runCommand: "./gradlew runAlgoliaIngest"
2494
+ }
2495
+ }
2496
+ ],
2497
+ sdk: {
2498
+ packageName: "com.algolia:algoliasearch-client-kotlin",
2499
+ versionPin: "3.+",
2500
+ docKey: "kotlin",
2501
+ // The published client's commonMain ships only ktor-client-core; without an
2502
+ // engine the script compiles and then fails at its first request.
2503
+ alsoRequires: "The Kotlin client bundles no HTTP engine, so also declare one (e.g. io.ktor:ktor-client-okhttp). Name the object AlgoliaWizardIngest in the default package, with a @JvmStatic main."
2504
+ },
2505
+ ingestEntrypointExample: "src/main/kotlin/AlgoliaWizardIngest.kt",
2506
+ verification: [],
2507
+ envReadInstruction: "Read them from `System.getenv`.",
2508
+ skipDirs: ["build", "out"]
2509
+ },
2510
+ scala: {
2511
+ id: "scala",
2512
+ displayName: "Scala",
2513
+ aliases: ["scala", "sbt"],
2514
+ manifests: ["build.sbt", "build.sc"],
2515
+ packageManagers: [
2516
+ {
2517
+ id: "sbt",
2518
+ dependency: { mode: "agent-declares", file: "build.sbt" },
2519
+ installSteps: [],
2520
+ ingest: {
2521
+ kind: "manual",
2522
+ entrypointExtensions: [".scala"],
2523
+ runCommand: 'sbt "runMain AlgoliaWizardIngest"'
2524
+ }
2525
+ }
2526
+ ],
2527
+ sdk: {
2528
+ packageName: "com.algolia:algoliasearch-scala_2.13",
2529
+ versionPin: "2.+",
2530
+ docKey: "scala",
2531
+ alsoRequires: "Name the object AlgoliaWizardIngest in the default package (no `package` statement) so `runMain AlgoliaWizardIngest` resolves it."
2532
+ },
2533
+ ingestEntrypointExample: "src/main/scala/AlgoliaWizardIngest.scala",
2534
+ verification: [],
2535
+ envReadInstruction: "Read them from `sys.env`.",
2536
+ // `project/` holds sbt's build definition, but the name is generic enough
2537
+ // that some repos use it for source; scanning it is cheap, missing source
2538
+ // is not.
2539
+ skipDirs: ["target"]
2540
+ },
2541
+ csharp: {
2542
+ id: "csharp",
2543
+ displayName: "C#",
2544
+ aliases: ["c#", "csharp", "cs", ".net", "dotnet", "net", "asp.net"],
2545
+ manifests: ["*.csproj", "*.sln", "global.json"],
2546
+ packageManagers: [
2547
+ {
2548
+ id: "dotnet",
2549
+ // A self-contained project under .algolia-wizard keeps the ingest script
2550
+ // out of the repo's own build graph.
2551
+ dependency: { mode: "agent-declares", file: CSHARP_PROJECT },
2552
+ installSteps: [{ argv: ["dotnet", "restore", CSHARP_PROJECT] }],
2553
+ ingest: {
2554
+ kind: "auto",
2555
+ argv: ["dotnet", "run", "--project", ENTRYPOINT_TOKEN],
2556
+ entrypointExtensions: [".csproj"]
2557
+ }
2558
+ }
2559
+ ],
2560
+ sdk: {
2561
+ packageName: "Algolia.Search",
2562
+ versionPin: "7.*",
2563
+ docKey: "csharp"
2564
+ },
2565
+ ingestEntrypointExample: CSHARP_PROJECT,
2566
+ verification: [
2567
+ {
2568
+ label: "dotnet build",
2569
+ argv: ["dotnet", "build", CSHARP_PROJECT, "--nologo"],
2570
+ requiresFile: CSHARP_PROJECT
2571
+ }
2572
+ ],
2573
+ envReadInstruction: 'Read them from `Environment.GetEnvironmentVariable("NAME")`.',
2574
+ // Deliberately not `packages`: modern .NET uses PackageReference, and
2575
+ // `packages/` is where pnpm/Lerna/Turborepo monorepos keep all their source —
2576
+ // skipping it would hide the entities the scan is looking for.
2577
+ skipDirs: ["bin", "obj"]
2578
+ },
2579
+ swift: {
2580
+ id: "swift",
2581
+ displayName: "Swift",
2582
+ aliases: ["swift", "swiftui", "ios", "vapor"],
2583
+ manifests: ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
2584
+ packageManagers: [
2585
+ {
2586
+ id: "swiftpm",
2587
+ dependency: {
2588
+ mode: "agent-declares",
2589
+ file: `${SWIFT_PACKAGE_DIR}/Package.swift`
2590
+ },
2591
+ // `swift build` resolves and fetches; a cold build of the client is slow
2592
+ // (minutes), which is why the caller degrades to the manual command when
2593
+ // this fails.
2594
+ installSteps: [
2595
+ {
2596
+ argv: ["swift", "build", "--package-path", SWIFT_PACKAGE_DIR],
2597
+ requiresFile: `${SWIFT_PACKAGE_DIR}/Package.swift`
2598
+ }
2599
+ ],
2600
+ ingest: {
2601
+ kind: "auto",
2602
+ argv: ["swift", "run", "--package-path", SWIFT_PACKAGE_DIR],
2603
+ entrypointExtensions: [".swift"]
2604
+ }
2605
+ }
2606
+ ],
2607
+ sdk: {
2608
+ packageName: "algoliasearch-client-swift",
2609
+ // SwiftPM range syntax, not an exact version — a bare "9.0.0" in a
2610
+ // Package.swift dependency pins the patch.
2611
+ versionPin: 'from: "9.0.0"',
2612
+ docKey: "swift"
2613
+ },
2614
+ ingestEntrypointExample: `${SWIFT_PACKAGE_DIR}/Sources/Ingest/main.swift`,
2615
+ verification: [
2616
+ {
2617
+ label: "swift build",
2618
+ argv: ["swift", "build", "--package-path", SWIFT_PACKAGE_DIR],
2619
+ requiresFile: `${SWIFT_PACKAGE_DIR}/Package.swift`
2620
+ }
2621
+ ],
2622
+ envReadInstruction: "Read them from `ProcessInfo.processInfo.environment`.",
2623
+ skipDirs: ["Pods", "DerivedData", "Carthage", ".build"]
2624
+ },
2625
+ dart: {
2626
+ id: "dart",
2627
+ displayName: "Dart",
2628
+ aliases: ["dart", "flutter"],
2629
+ manifests: ["pubspec.yaml"],
2630
+ packageManagers: [
2631
+ {
2632
+ id: "flutter-pub",
2633
+ detectFiles: [".metadata"],
2634
+ dependency: { mode: "agent-declares", file: "pubspec.yaml" },
2635
+ installSteps: [{ argv: ["flutter", "pub", "get"] }],
2636
+ ingest: {
2637
+ kind: "auto",
2638
+ argv: ["dart", "run", ENTRYPOINT_TOKEN],
2639
+ entrypointExtensions: [".dart"]
2640
+ }
2641
+ },
2642
+ {
2643
+ id: "pub",
2644
+ dependency: { mode: "agent-declares", file: "pubspec.yaml" },
2645
+ installSteps: [{ argv: ["dart", "pub", "get"] }],
2646
+ ingest: {
2647
+ kind: "auto",
2648
+ argv: ["dart", "run", ENTRYPOINT_TOKEN],
2649
+ entrypointExtensions: [".dart"]
2650
+ }
2651
+ }
2652
+ ],
2653
+ sdk: {
2654
+ packageName: "algolia_client_search",
2655
+ versionPin: "^1.0.0",
2656
+ docKey: "dart"
2657
+ },
2658
+ ingestEntrypointExample: `${INGEST_DIR}/ingest.dart`,
2659
+ verification: [
2660
+ {
2661
+ label: "dart analyze",
2662
+ argv: ["dart", "analyze", INGEST_DIR],
2663
+ requiresFile: "pubspec.yaml"
2664
+ }
2665
+ ],
2666
+ envReadInstruction: "Read them from `Platform.environment`.",
2667
+ skipDirs: ["build"]
2668
+ }
2669
+ };
2670
+ var DEFAULT_LANGUAGE_ID = "javascript";
2671
+ function normalizeLanguageName(name) {
2672
+ return name.toLowerCase().trim().replace(/[\s_-]+/g, "");
2673
+ }
2674
+ var ALIAS_TO_ID = /* @__PURE__ */ new Map();
2675
+ for (const profile2 of Object.values(LANGUAGE_PROFILES)) {
2676
+ for (const alias of [profile2.id, profile2.displayName, ...profile2.aliases]) {
2677
+ ALIAS_TO_ID.set(normalizeLanguageName(alias), profile2.id);
2678
+ }
2679
+ }
2680
+ function resolveLanguageProfile(name) {
2681
+ const id = ALIAS_TO_ID.get(normalizeLanguageName(name));
2682
+ return id ? LANGUAGE_PROFILES[id] : void 0;
2683
+ }
2684
+ var BASE_SKIP_DIRS = ["node_modules", ".git", "dist"];
2685
+ var ALL_SKIP_DIRS = /* @__PURE__ */ new Set([
2686
+ ...BASE_SKIP_DIRS,
2687
+ ...Object.values(LANGUAGE_PROFILES).flatMap((p) => p.skipDirs)
2688
+ ]);
2689
+ var ALLOWED_BINARIES = new Set(
2690
+ Object.values(LANGUAGE_PROFILES).flatMap((profile2) => [
2691
+ ...profile2.packageManagers.flatMap((pm) => [
2692
+ ...pm.installSteps.map((s) => s.argv[0]),
2693
+ ...pm.ingest.kind === "auto" ? [pm.ingest.argv[0]] : []
2694
+ ]),
2695
+ ...profile2.verification.map((v) => v.argv[0])
2696
+ ])
2697
+ );
2698
+ var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
2699
+ function isWorktreeRelativeCommand(command) {
2700
+ return command.includes("/");
2701
+ }
2702
+ function withCommand(argv, command) {
2703
+ return [command, ...argv.slice(1)];
2704
+ }
2705
+ async function manifestPresent(root, manifest, listing) {
2706
+ if (!manifest.startsWith("*.")) return existsSync2(join9(root, manifest));
2707
+ if (!listing.entries) {
2708
+ const entries = await readdir2(root).catch(() => []);
2709
+ listing.entries = Array.isArray(entries) ? entries : [];
2710
+ }
2711
+ const suffix = manifest.slice(1);
2712
+ return listing.entries.some((e) => e.endsWith(suffix));
2713
+ }
2714
+ async function profileManifestPresent(root, profile2, listing) {
2715
+ for (const manifest of profile2.manifests) {
2716
+ if (await manifestPresent(root, manifest, listing)) return true;
2717
+ }
2718
+ return false;
2719
+ }
2720
+ async function detectProfilesFromManifests(root) {
2721
+ const listing = {};
2722
+ const found = [];
2723
+ for (const profile2 of Object.values(LANGUAGE_PROFILES)) {
2724
+ if (await profileManifestPresent(root, profile2, listing)) found.push(profile2);
2725
+ }
2726
+ return found;
2727
+ }
2728
+ async function hasProfileManifest(root, profile2) {
2729
+ return profileManifestPresent(root, profile2, {});
2730
+ }
2731
+ async function resolveToolchain(root, profile2) {
2732
+ const matched = profile2.packageManagers.find(
2733
+ (pm) => [...pm.lockfiles ?? [], ...pm.detectFiles ?? []].some(
2734
+ (f) => existsSync2(join9(root, f))
2735
+ )
2736
+ );
2737
+ const packageManager = matched ?? profile2.packageManagers[0];
2738
+ let { installSteps, ingest } = packageManager;
2739
+ installSteps = installSteps.map(
2740
+ (step) => isWorktreeRelativeCommand(step.argv[0]) ? { ...step, argv: withCommand(step.argv, join9(root, step.argv[0])) } : step
2741
+ );
2742
+ if (ingest.kind === "auto" && isWorktreeRelativeCommand(ingest.argv[0])) {
2743
+ ingest = {
2744
+ ...ingest,
2745
+ argv: withCommand(ingest.argv, join9(root, ingest.argv[0]))
2746
+ };
2747
+ }
2748
+ if (profile2.id === "javascript") {
2749
+ const pm = await detectPackageManager(root);
2750
+ if (JS_PACKAGE_MANAGERS.has(pm)) {
2751
+ installSteps = installSteps.map((step) => ({
2752
+ ...step,
2753
+ argv: withCommand(step.argv, pm)
2754
+ }));
2755
+ if (pm === "bun" && ingest.kind === "auto") {
2756
+ ingest = { ...ingest, argv: withCommand(ingest.argv, "bun") };
2757
+ }
2758
+ }
2759
+ }
2760
+ return { profile: profile2, packageManager, installSteps, ingest };
2761
+ }
2762
+ function resolveIngestArgv(ingest, entrypoint) {
2763
+ if (ingest.kind !== "auto") {
2764
+ throw new Error("resolveIngestArgv called for a manual-run toolchain");
2765
+ }
2766
+ return ingest.argv.map(
2767
+ (part) => part === ENTRYPOINT_TOKEN ? entrypoint : part
2768
+ );
2769
+ }
2770
+ function describeIngestCommand(ingest, entrypoint) {
2771
+ if (ingest.kind !== "auto") return ingest.runCommand;
2772
+ return ingest.argv.map((part) => part === ENTRYPOINT_TOKEN ? shellQuote(entrypoint) : part).join(" ");
2773
+ }
2774
+ function ingestScriptDir(profile2) {
2775
+ const parts = profile2.ingestEntrypointExample.split("/");
2776
+ return parts.slice(0, -1).join("/") || ".";
2777
+ }
2778
+ function dependencyInstruction(toolchain) {
2779
+ const { profile: profile2, packageManager } = toolchain;
2780
+ const { packageName, versionPin } = profile2.sdk;
2781
+ const also = profile2.sdk.alsoRequires ? ` ${profile2.sdk.alsoRequires}` : "";
2782
+ switch (packageManager.dependency.mode) {
2783
+ case "wizard-installs":
2784
+ 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}`;
2785
+ case "code-imports":
2786
+ 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}`;
2787
+ case "agent-declares":
2788
+ 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}`;
2789
+ }
2790
+ }
2791
+
2792
+ // src/lib/tools/searchFiles.ts
2160
2793
  var MAX_QUERY_LENGTH = 1e3;
2161
2794
  async function walkFiles(dir) {
2162
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2163
2795
  const out = [];
2164
- for (const e of await readdir2(dir, { withFileTypes: true })) {
2165
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2166
- const full = join8(dir, e.name);
2796
+ for (const e of await readdir3(dir, { withFileTypes: true })) {
2797
+ if (e.name.startsWith(".") || ALL_SKIP_DIRS.has(e.name)) continue;
2798
+ const full = join10(dir, e.name);
2167
2799
  if (e.isDirectory()) out.push(...await walkFiles(full));
2168
2800
  else if (e.isFile()) out.push(full);
2169
2801
  }
@@ -2196,7 +2828,7 @@ function searchFilesTool(ctx) {
2196
2828
  for (const file of await walkFiles(resolved.target)) {
2197
2829
  let content;
2198
2830
  try {
2199
- content = await readFile6(file, "utf8");
2831
+ content = await readFile7(file, "utf8");
2200
2832
  } catch {
2201
2833
  continue;
2202
2834
  }
@@ -2220,6 +2852,10 @@ function searchFilesTool(ctx) {
2220
2852
  import { tool as tool8 } from "ai";
2221
2853
  import z11 from "zod";
2222
2854
 
2855
+ // src/lib/tools/repoVerification.ts
2856
+ import { existsSync as existsSync3 } from "node:fs";
2857
+ import { join as join11 } from "node:path";
2858
+
2223
2859
  // src/lib/tools/utils/runCommand.ts
2224
2860
  import { spawn as spawn2 } from "node:child_process";
2225
2861
  function runCommand(command, args, cwd) {
@@ -2239,69 +2875,89 @@ function runCommand(command, args, cwd) {
2239
2875
  });
2240
2876
  }
2241
2877
 
2242
- // src/lib/tools/utils/packageManager.ts
2243
- import { readFile as readFile7 } from "node:fs/promises";
2244
- import { existsSync } from "node:fs";
2245
- import { join as join9 } from "node:path";
2246
- var LOCKFILES = [
2247
- ["pnpm-lock.yaml", "pnpm"],
2248
- ["yarn.lock", "yarn"],
2249
- ["bun.lockb", "bun"],
2250
- ["bun.lock", "bun"],
2251
- ["package-lock.json", "npm"]
2252
- ];
2253
- async function readPackageJson(cwd = process.cwd()) {
2254
- return JSON.parse(await readFile7(join9(cwd, "package.json"), "utf8"));
2255
- }
2256
- function packageManagerFrom(pkg) {
2257
- return pkg.packageManager?.split("@")[0] ?? "npm";
2258
- }
2259
- function packageManagerFromLockfile(cwd) {
2260
- return LOCKFILES.find(([file]) => existsSync(join9(cwd, file)))?.[1];
2261
- }
2262
- async function detectPackageManager(cwd) {
2263
- try {
2264
- const pkg = await readPackageJson(cwd);
2265
- if (pkg.packageManager) return packageManagerFrom(pkg);
2266
- } catch {
2267
- }
2268
- return packageManagerFromLockfile(cwd) ?? "npm";
2269
- }
2270
-
2271
2878
  // src/lib/tools/repoVerification.ts
2272
2879
  var VERIFICATION_SCRIPT_CANDIDATES = ["lint", "typecheck", "check"];
2273
- async function runRepoVerificationCheck() {
2880
+ async function runCheck(command, binary, args) {
2881
+ const { code, output } = await runCommand(binary, args);
2882
+ return { command, exitCode: code, ok: code === 0, output: output.trim() };
2883
+ }
2884
+ async function javascriptChecks() {
2274
2885
  let pkg;
2275
2886
  try {
2276
2887
  pkg = await readPackageJson();
2277
2888
  } catch (err) {
2278
- const limitation = `Could not read package.json to detect verification conventions: ${err.message}`;
2279
- return { ok: false, checks: [], limitation };
2889
+ return {
2890
+ limitation: `Could not read package.json to detect verification conventions: ${err.message}`
2891
+ };
2280
2892
  }
2281
2893
  const scripts = pkg.scripts ?? {};
2282
2894
  const present = VERIFICATION_SCRIPT_CANDIDATES.filter((s) => s in scripts);
2283
2895
  if (present.length === 0) {
2284
- const limitation = `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`;
2285
- return { ok: false, checks: [], limitation };
2896
+ return {
2897
+ limitation: `No verification script found in package.json (looked for: ${VERIFICATION_SCRIPT_CANDIDATES.join(", ")}).`
2898
+ };
2286
2899
  }
2287
2900
  const pm = await detectPackageManager(process.cwd());
2288
2901
  const checks = [];
2289
2902
  for (const script of present) {
2290
- const command = `${pm} run ${script}`;
2291
- const { code, output } = await runCommand(pm, ["run", script]);
2292
- checks.push({ command, exitCode: code, ok: code === 0, output: output.trim() });
2903
+ checks.push(
2904
+ await runCheck(`${pm} run ${script}`, pm, ["run", script])
2905
+ );
2293
2906
  }
2294
- return { ok: checks.every((c) => c.ok), checks };
2907
+ return { checks };
2908
+ }
2909
+ async function runRepoVerificationCheck(languages = [DEFAULT_LANGUAGE_ID]) {
2910
+ const ids = [...new Set(languages)];
2911
+ if (ids.length === 0) ids.push(DEFAULT_LANGUAGE_ID);
2912
+ const checks = [];
2913
+ const limitations = [];
2914
+ for (const id of ids) {
2915
+ if (id === DEFAULT_LANGUAGE_ID) {
2916
+ const result = await javascriptChecks();
2917
+ if ("checks" in result) checks.push(...result.checks);
2918
+ else limitations.push(result.limitation);
2919
+ continue;
2920
+ }
2921
+ const profile2 = LANGUAGE_PROFILES[id];
2922
+ const runnable = profile2.verification.filter(
2923
+ (spec) => !spec.requiresFile || existsSync3(join11(process.cwd(), spec.requiresFile))
2924
+ );
2925
+ if (runnable.length === 0) {
2926
+ limitations.push(
2927
+ `No mechanical verification available for ${profile2.displayName} in this repo.`
2928
+ );
2929
+ continue;
2930
+ }
2931
+ for (const spec of runnable) {
2932
+ checks.push(
2933
+ await runCheck(spec.argv.join(" "), spec.argv[0], [
2934
+ ...spec.argv.slice(1)
2935
+ ])
2936
+ );
2937
+ }
2938
+ }
2939
+ if (checks.length === 0) {
2940
+ return {
2941
+ ok: false,
2942
+ checks: [],
2943
+ limitation: limitations.join(" ") || "No verification checks available."
2944
+ };
2945
+ }
2946
+ return {
2947
+ ok: checks.every((c) => c.ok),
2948
+ checks,
2949
+ ...limitations.length ? { limitation: limitations.join(" ") } : {}
2950
+ };
2295
2951
  }
2296
2952
 
2297
2953
  // src/lib/tools/verifyImplementation.ts
2298
- function verifyImplementationTool() {
2954
+ function verifyImplementationTool(ctx) {
2299
2955
  return tool8({
2300
- 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.",
2956
+ 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.",
2301
2957
  inputSchema: z11.object(),
2302
2958
  execute: async () => {
2303
- logger.info("called verifyImplementation tool");
2304
- return runRepoVerificationCheck();
2959
+ logger.info({ languages: ctx.languages }, "called verifyImplementation tool");
2960
+ return runRepoVerificationCheck(ctx.languages);
2305
2961
  }
2306
2962
  });
2307
2963
  }
@@ -2427,12 +3083,13 @@ var DEFAULT_TOOL_LIMITS = {
2427
3083
  read: 20,
2428
3084
  match: 100
2429
3085
  };
2430
- function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd()) {
3086
+ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), languages = [DEFAULT_LANGUAGE_ID]) {
2431
3087
  return {
2432
3088
  root: cwd,
2433
3089
  cwd,
2434
3090
  limits,
2435
- counts: { list: 0, search: 0, read: 0 }
3091
+ counts: { list: 0, search: 0, read: 0 },
3092
+ languages: languages.length ? languages : [DEFAULT_LANGUAGE_ID]
2436
3093
  };
2437
3094
  }
2438
3095
 
@@ -2469,7 +3126,7 @@ function createTools(ctx, { output, tools }) {
2469
3126
  searchFiles: withLogging("searchFiles", searchFilesTool(ctx)),
2470
3127
  verifyImplementation: withLogging(
2471
3128
  "verifyImplementation",
2472
- verifyImplementationTool()
3129
+ verifyImplementationTool(ctx)
2473
3130
  ),
2474
3131
  generateRecord: withLogging("generateRecord", generateRecordTool(ctx)),
2475
3132
  notifyUser: withLogging("notifyUser", notifyUserTool())
@@ -2505,7 +3162,11 @@ async function runAgent(req) {
2505
3162
  baseURL: PROXY_BASE_URL,
2506
3163
  fetch: proxyFetch
2507
3164
  });
2508
- const toolContext = createToolContext();
3165
+ const toolContext = createToolContext(
3166
+ void 0,
3167
+ void 0,
3168
+ req.languages
3169
+ );
2509
3170
  const readTools = ["readFile", "searchFiles", "listFiles"];
2510
3171
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
2511
3172
  const instructions = [
@@ -2596,8 +3257,11 @@ var detectLanguageSchema = z16.object({
2596
3257
  var detectLanguage = () => runAgent({
2597
3258
  instructions: [
2598
3259
  "Analyze the codebase and determine the programming languages and frameworks used",
2599
- "If a superset language is found, exclude the subset language. TS-over-JS.",
3260
+ "Start from the dependency manifests: package.json, pyproject.toml, requirements.txt, Gemfile, composer.json, go.mod, pom.xml, build.gradle(.kts), *.csproj, build.sbt, Package.swift, pubspec.yaml.",
3261
+ "List the language that owns the backend/data code first \u2014 that is the one an ingestion script will be written in.",
3262
+ "If a superset language is found, exclude the subset language. TS-over-JS. Kotlin-over-Java when Kotlin is primary.",
2600
3263
  "If a meta-framework is used, exclude the framework. Next-over-React.",
3264
+ "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).",
2601
3265
  "Return the exact version",
2602
3266
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
2603
3267
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -2646,6 +3310,7 @@ var MODE_CONFIG = {
2646
3310
  "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
2647
3311
  "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).",
2648
3312
  "Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
3313
+ "Entities live wherever the stack keeps them: TypeScript interfaces or a Prisma schema, Django models.py, Rails app/models, Laravel Eloquent models, JPA @Entity classes, Go structs, C# entity classes, Pydantic models.",
2649
3314
  "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
2650
3315
  "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
2651
3316
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
@@ -2657,8 +3322,9 @@ var MODE_CONFIG = {
2657
3322
  instructions: [
2658
3323
  "Analyze the codebase to determine the single best location to add search UI functionality.",
2659
3324
  "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
2660
- "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
2661
- 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
3325
+ "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).",
3326
+ "Return one file path as searchImplementationAnalysis.",
3327
+ 'Use as few tools as possible, but do not guess. If the project renders no UI at all (an API-only service), say "unknown".',
2662
3328
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2663
3329
  "When done, call reportStatus"
2664
3330
  ],
@@ -2667,8 +3333,8 @@ var MODE_CONFIG = {
2667
3333
  verification: {
2668
3334
  instructions: [
2669
3335
  "Analyze the codebase to determine which code-quality tools are available to validate changes.",
2670
- "Look at package.json scripts, config files (e.g. .eslintrc, tsconfig, prettier), and dev dependencies.",
2671
- 'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"].',
3336
+ "Look at the dependency manifest and config files for the project's languages: package.json scripts with tsconfig/eslint/prettier, pyproject.toml or setup.cfg (ruff, mypy, black), Gemfile with .rubocop.yml, composer.json scripts (phpstan, pint), go.mod with a golangci-lint config, Maven/Gradle verification tasks, .NET analyzers, analysis_options.yaml.",
3337
+ 'Return the tool names as an array, e.g. ["eslint", "prettier", "tsc"] or ["ruff", "mypy"].',
2672
3338
  "Use as few tools as possible, but do not guess. If you cannot find any, return an empty array.",
2673
3339
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
2674
3340
  "When done, call reportStatus"
@@ -2695,7 +3361,7 @@ async function runAnalysis(mode, extraInstructions = []) {
2695
3361
  // package.json
2696
3362
  var package_default = {
2697
3363
  name: "@algolia/wizard",
2698
- version: "0.5.0",
3364
+ version: "0.6.0-rc.51.22",
2699
3365
  description: "Magically implement Algolia functionality in your codebase",
2700
3366
  type: "module",
2701
3367
  engines: {
@@ -2798,82 +3464,175 @@ function parseEntries(raw) {
2798
3464
  return raw.split(",").map(clean).filter(Boolean).slice(0, MAX_ENTRIES).map((name) => ({ name, version: "unknown" }));
2799
3465
  }
2800
3466
  var summarize = (entries) => entries.length ? entries.map((e) => e.name).join(", ") : "none";
2801
- async function askList(ctx, prompt, { required = false } = {}) {
3467
+
3468
+ // src/actions/confirmLanguage.ts
3469
+ import z19 from "zod";
3470
+ var confirmLanguageSchema = z19.object({
3471
+ languages: detectLanguageSchema.shape.languages
3472
+ });
3473
+ var OTHER_OPTION = "Other";
3474
+ var CURATED_LANGUAGES = Object.values(LANGUAGE_PROFILES).map(
3475
+ (profile2) => profile2.displayName
3476
+ );
3477
+ function isSameLanguage(a, b) {
3478
+ const x = resolveLanguageProfile(a);
3479
+ const y = resolveLanguageProfile(b);
3480
+ if (x && y) return x.id === y.id;
3481
+ if (x || y) return false;
3482
+ const fold = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
3483
+ return fold(a) !== "" && fold(a) === fold(b);
3484
+ }
3485
+ function confirmed(languages) {
3486
+ track("AI Wizard Language Confirmed", { languages });
3487
+ return { languages };
3488
+ }
3489
+ async function askOtherLanguage(ctx) {
3490
+ let prompt = "enter the language for your ingestion script";
2802
3491
  for (; ; ) {
2803
3492
  const answer = await ctx.requestUserInput({
2804
3493
  prompt,
2805
3494
  promptType: "textInput",
2806
- options: [],
2807
- helpText: 'Comma-separated, e.g. "TypeScript, Node".'
3495
+ options: []
2808
3496
  });
2809
3497
  if (typeof answer !== "string") {
2810
- throw new Error("askList received an unexpected non-text result");
3498
+ throw new Error("confirmLanguage received an unexpected non-text result");
2811
3499
  }
2812
- const entries = parseEntries(answer);
2813
- if (entries.length || !required) return entries;
2814
- prompt = "Please enter at least one entry:";
3500
+ const name = parseEntries(answer)[0]?.name;
3501
+ if (name) return name;
3502
+ prompt = "please enter a language name:";
2815
3503
  }
2816
3504
  }
2817
-
2818
- // src/actions/confirmLanguage.ts
2819
- import z19 from "zod";
2820
- var confirmLanguageSchema = z19.object({
2821
- languages: detectLanguageSchema.shape.languages
2822
- });
2823
3505
  async function confirmLanguage(ctx) {
2824
3506
  const detected = ctx.getStepOutput("project-scan");
2825
- const answer = await ctx.requestUserInput({
2826
- prompt: "Did we detect your language(s) correctly?",
2827
- promptType: "acceptReject",
2828
- options: ["Yes", "No"],
2829
- messages: [`Languages: ${summarize(detected.languages)}`]
2830
- });
2831
- const languages = answer === true ? detected.languages : await askList(ctx, "List the languages your project uses:", {
2832
- required: true
2833
- });
2834
- track("AI Wizard Language Confirmed", {
2835
- languages
3507
+ const detectedLanguages = detected.languages ?? [];
3508
+ const others = (chosen) => detectedLanguages.filter((l) => !isSameLanguage(l.name, chosen));
3509
+ const primary = detectedLanguages[0];
3510
+ if (primary) {
3511
+ const accepted = await ctx.requestUserInput({
3512
+ prompt: `Write the ingestion script in ${primary.name}?`,
3513
+ promptType: "acceptReject",
3514
+ options: [`Confirm ${primary.name}`, "Use a different language"],
3515
+ secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0],
3516
+ messages: detectedLanguages.length > 1 ? [`Detected: ${summarize(detectedLanguages)}`] : []
3517
+ });
3518
+ if (accepted === true) return confirmed(detectedLanguages);
3519
+ }
3520
+ const options = [...CURATED_LANGUAGES];
3521
+ for (const language of detectedLanguages) {
3522
+ if (!options.some((o) => isSameLanguage(o, language.name))) {
3523
+ options.push(language.name);
3524
+ }
3525
+ }
3526
+ options.push(OTHER_OPTION);
3527
+ const detectedFor = (option) => detectedLanguages.find((l) => isSameLanguage(option, l.name));
3528
+ const secondary = options.map(
3529
+ (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
3530
+ );
3531
+ const defaultSelectedIndex = Math.max(
3532
+ options.findIndex((o) => detectedFor(o)),
3533
+ 0
3534
+ );
3535
+ const selection = await ctx.requestUserInput({
3536
+ prompt: "select the language for your ingestion script",
3537
+ promptType: "multipleChoice",
3538
+ options,
3539
+ secondary,
3540
+ defaultSelectedIndex
2836
3541
  });
2837
- return { languages };
3542
+ if (typeof selection !== "string") {
3543
+ throw new Error("confirmLanguage received an unexpected non-text result");
3544
+ }
3545
+ const name = selection === OTHER_OPTION ? await askOtherLanguage(ctx) : selection;
3546
+ const version = detectedFor(name)?.version ?? "unknown";
3547
+ return confirmed([{ name, version }, ...others(name)]);
2838
3548
  }
2839
3549
 
2840
3550
  // src/actions/confirmFramework.ts
2841
3551
  import z20 from "zod";
2842
- var confirmFrameworkSchema = z20.object({
2843
- frameworks: detectLanguageSchema.shape.frameworks
2844
- });
2845
- var CURATED_FRAMEWORKS = [
2846
- "Next.js",
2847
- "React",
2848
- "Vue",
2849
- "Angular",
2850
- "Svelte",
2851
- "Vanilla JS"
3552
+
3553
+ // src/lib/frameworks.ts
3554
+ var BACKEND_ONLY_FRAMEWORK = "Backend only / API";
3555
+ var FRAMEWORKS = [
3556
+ // Frontend — InstantSearch component flavors.
3557
+ { name: "Next.js", strategy: "react", aliases: ["next", "nextjs"] },
3558
+ { name: "React", strategy: "react", aliases: ["reactjs"] },
3559
+ { name: "Vue", strategy: "vue", aliases: ["vuejs", "nuxt", "nuxtjs"] },
3560
+ { name: "Angular", strategy: "angular", aliases: ["angularjs"] },
3561
+ // No Svelte InstantSearch flavor exists, so it uses InstantSearch.js.
3562
+ { name: "Svelte", strategy: "js", aliases: ["sveltekit"] },
3563
+ {
3564
+ name: "Vanilla JS",
3565
+ strategy: "js",
3566
+ aliases: ["vanilla", "javascript", "js", "astro", "vite"]
3567
+ },
3568
+ // Backend — Algolia's official framework integrations. Server-rendered
3569
+ // templates get InstantSearch.js from a CDN.
3570
+ {
3571
+ name: "Rails",
3572
+ strategy: "cdn-template",
3573
+ aliases: ["rubyonrails", "ruby on rails", "erb"]
3574
+ },
3575
+ { name: "Django", strategy: "cdn-template", aliases: ["jinja", "jinja2"] },
3576
+ { name: "Laravel", strategy: "cdn-template", aliases: ["blade"] },
3577
+ { name: "Symfony", strategy: "cdn-template", aliases: ["twig"] },
3578
+ // Mobile — Algolia ships InstantSearch iOS/Android and Dart clients, but the
3579
+ // wizard can't scaffold a native UI, so it points at the docs instead.
3580
+ { name: "Flutter", strategy: "none", aliases: [] },
3581
+ { name: "iOS", strategy: "none", aliases: ["swiftui", "uikit"] },
3582
+ { name: "Android", strategy: "none", aliases: ["jetpack compose", "compose"] },
3583
+ { name: BACKEND_ONLY_FRAMEWORK, strategy: "cdn-template", aliases: [] }
2852
3584
  ];
2853
- var OTHER_OPTION = "Other";
3585
+ var CURATED_FRAMEWORKS = FRAMEWORKS.map(
3586
+ (f) => f.name
3587
+ );
2854
3588
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2855
- var FRAMEWORK_ALIASES = {
2856
- next: "nextjs",
2857
- nextjs: "nextjs",
2858
- react: "react",
2859
- reactjs: "react",
2860
- vue: "vue",
2861
- vuejs: "vue",
2862
- angular: "angular",
2863
- angularjs: "angular",
2864
- svelte: "svelte",
2865
- sveltekit: "svelte",
2866
- vanillajs: "vanillajs",
2867
- vanilla: "vanillajs",
2868
- javascript: "vanillajs",
2869
- js: "vanillajs"
2870
- };
2871
- var isSameFramework = (a, b) => {
2872
- const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
2873
- const y = FRAMEWORK_ALIASES[normalize(b)] ?? normalize(b);
3589
+ var ALIAS_TO_NAME = /* @__PURE__ */ new Map();
3590
+ for (const framework of FRAMEWORKS) {
3591
+ for (const alias of [framework.name, ...framework.aliases]) {
3592
+ ALIAS_TO_NAME.set(normalize(alias), framework.name);
3593
+ }
3594
+ }
3595
+ var STRATEGY_BY_NAME = new Map(FRAMEWORKS.map((f) => [f.name, f.strategy]));
3596
+ function canonicalFrameworkName(name) {
3597
+ return ALIAS_TO_NAME.get(normalize(name));
3598
+ }
3599
+ function isSameFramework(a, b) {
3600
+ const x = canonicalFrameworkName(a) ?? normalize(a);
3601
+ const y = canonicalFrameworkName(b) ?? normalize(b);
2874
3602
  return x !== "" && x === y;
2875
- };
2876
- function confirmed(name, version) {
3603
+ }
3604
+ function resolveSearchStrategy(frameworkName, hasJavaScriptInStack) {
3605
+ const canonical = frameworkName ? canonicalFrameworkName(frameworkName) : void 0;
3606
+ const strategy = canonical ? STRATEGY_BY_NAME.get(canonical) : void 0;
3607
+ if (strategy) return strategy;
3608
+ return hasJavaScriptInStack ? "js" : "cdn-template";
3609
+ }
3610
+ function searchDocKey(strategy) {
3611
+ return strategy === "cdn-template" ? "templates" : strategy;
3612
+ }
3613
+ function describeSearchTarget(strategy, frameworkName) {
3614
+ switch (strategy) {
3615
+ case "react":
3616
+ return "React (react-instantsearch)";
3617
+ case "vue":
3618
+ return "Vue (vue-instantsearch)";
3619
+ case "angular":
3620
+ return "Angular (angular-instantsearch)";
3621
+ case "js":
3622
+ return "plain JavaScript (InstantSearch.js)";
3623
+ case "cdn-template":
3624
+ return `${frameworkName ?? "server-rendered"} templates (InstantSearch.js via CDN)`;
3625
+ case "none":
3626
+ return frameworkName ?? "a native mobile app";
3627
+ }
3628
+ }
3629
+
3630
+ // src/actions/confirmFramework.ts
3631
+ var confirmFrameworkSchema = z20.object({
3632
+ frameworks: detectLanguageSchema.shape.frameworks
3633
+ });
3634
+ var OTHER_OPTION2 = "Other";
3635
+ function confirmed2(name, version) {
2877
3636
  const frameworks = [{ name, version: version ?? "unknown" }];
2878
3637
  track("AI Wizard Frontend Framework Confirmed", { frameworks });
2879
3638
  return { frameworks };
@@ -2901,7 +3660,7 @@ async function confirmFramework(ctx) {
2901
3660
  for (const fw of detectedFrameworks) {
2902
3661
  if (!options.some((o) => isSameFramework(o, fw.name))) options.push(fw.name);
2903
3662
  }
2904
- options.push(OTHER_OPTION);
3663
+ options.push(OTHER_OPTION2);
2905
3664
  const detectedFor = (option) => detectedFrameworks.find((fw) => isSameFramework(option, fw.name));
2906
3665
  const primary = detectedFrameworks[0];
2907
3666
  if (primary) {
@@ -2911,7 +3670,7 @@ async function confirmFramework(ctx) {
2911
3670
  options: [`Confirm ${primary.name}`, "Use a different framework"],
2912
3671
  secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
2913
3672
  });
2914
- if (accepted === true) return confirmed(primary.name, primary.version);
3673
+ if (accepted === true) return confirmed2(primary.name, primary.version);
2915
3674
  }
2916
3675
  const secondary = options.map(
2917
3676
  (o) => detectedFor(o) ? { kind: "badge", value: "[DETECTED]" } : void 0
@@ -2921,7 +3680,7 @@ async function confirmFramework(ctx) {
2921
3680
  0
2922
3681
  );
2923
3682
  const selection = await ctx.requestUserInput({
2924
- prompt: "select a framework",
3683
+ prompt: "select the framework that renders your UI",
2925
3684
  promptType: "multipleChoice",
2926
3685
  options,
2927
3686
  secondary,
@@ -2930,10 +3689,10 @@ async function confirmFramework(ctx) {
2930
3689
  if (typeof selection !== "string") {
2931
3690
  throw new Error("confirmFramework received an unexpected non-text result");
2932
3691
  }
2933
- if (selection === OTHER_OPTION) {
2934
- return confirmed(await askOtherFramework(ctx));
3692
+ if (selection === OTHER_OPTION2) {
3693
+ return confirmed2(await askOtherFramework(ctx));
2935
3694
  }
2936
- return confirmed(selection, detectedFor(selection)?.version);
3695
+ return confirmed2(selection, detectedFor(selection)?.version);
2937
3696
  }
2938
3697
 
2939
3698
  // src/actions/promptUser.ts
@@ -3026,15 +3785,15 @@ async function confirmEntities(ctx) {
3026
3785
  onSubmit: () => {
3027
3786
  }
3028
3787
  });
3029
- const confirmed2 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3030
- if (confirmed2.length === 0) {
3788
+ const confirmed3 = typeof selection === "string" ? entities.filter((e) => e.name === selection) : [];
3789
+ if (confirmed3.length === 0) {
3031
3790
  throw new Error("User cancelled entity selection \u2014 analysis halted.");
3032
3791
  }
3033
- ctx.setUserInput("confirmedEntities", confirmed2);
3792
+ ctx.setUserInput("confirmedEntities", confirmed3);
3034
3793
  track("AI Wizard Entities Confirmed", {
3035
- entities: toEntitySummary(confirmed2)
3794
+ entities: toEntitySummary(confirmed3)
3036
3795
  });
3037
- return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
3796
+ return { ingestionAnalysis: entities, confirmedEntities: confirmed3 };
3038
3797
  }
3039
3798
 
3040
3799
  // src/actions/review.ts
@@ -3058,7 +3817,7 @@ ${JSON.stringify(s.output, null, 2)}`
3058
3817
  }
3059
3818
  function formatReviewSummary(result) {
3060
3819
  const nextStepLines = result.nextSteps.map((step) => {
3061
- const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
3820
+ const isIngestCommand = step.includes(".algolia-wizard/") || step.includes("AlgoliaWizardIngest");
3062
3821
  const isWorktreeCommand = step.includes("/worktrees/");
3063
3822
  return {
3064
3823
  text: `\u2192 ${step}`,
@@ -3101,12 +3860,13 @@ import z24 from "zod";
3101
3860
 
3102
3861
  // src/lib/worktree.ts
3103
3862
  import { execFile, spawn as spawn3 } from "node:child_process";
3104
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3863
+ import { existsSync as existsSync4 } from "node:fs";
3864
+ import { copyFile, mkdir as mkdir6, readdir as readdir4, readFile as readFile8, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3105
3865
  import {
3106
3866
  basename as basename2,
3107
3867
  dirname as dirname7,
3108
3868
  isAbsolute as isAbsolute2,
3109
- join as join10,
3869
+ join as join12,
3110
3870
  relative as relative2,
3111
3871
  resolve as resolve3
3112
3872
  } from "node:path";
@@ -3140,8 +3900,8 @@ async function isWorkingTreeDirty(repoRoot) {
3140
3900
  return out.trim().length > 0;
3141
3901
  }
3142
3902
  async function pruneOldWorktrees(repoRoot) {
3143
- const dir = join10(stateDir(repoRoot), "worktrees");
3144
- const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3903
+ const dir = join12(stateDir(repoRoot), "worktrees");
3904
+ const stale = (await readdir4(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3145
3905
  for (const slug of stale) {
3146
3906
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
3147
3907
  try {
@@ -3151,7 +3911,7 @@ async function pruneOldWorktrees(repoRoot) {
3151
3911
  "worktree",
3152
3912
  "remove",
3153
3913
  "--force",
3154
- join10(dir, slug)
3914
+ join12(dir, slug)
3155
3915
  ]);
3156
3916
  await git(["-C", repoRoot, "branch", "-D", branch]);
3157
3917
  } catch (err) {
@@ -3165,24 +3925,19 @@ async function pruneOldWorktrees(repoRoot) {
3165
3925
  async function createWorktree(repoRoot) {
3166
3926
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3167
3927
  const dirSlug = branch.replace(/\//g, "-");
3168
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3928
+ const path = join12(stateDir(repoRoot), "worktrees", dirSlug);
3169
3929
  await git(["-C", repoRoot, "worktree", "prune"]);
3170
3930
  await pruneOldWorktrees(repoRoot);
3171
3931
  await mkdir6(dirname7(path), { recursive: true });
3172
3932
  await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
3173
3933
  return { path, branch };
3174
3934
  }
3175
- async function installWorktreeDeps(worktreePath) {
3176
- try {
3177
- await readPackageJson(worktreePath);
3178
- } catch {
3179
- return { ok: true, output: "no package.json; skipped install" };
3180
- }
3181
- const pm = await detectPackageManager(worktreePath);
3935
+ function spawnStep(worktreePath, argv) {
3182
3936
  return new Promise((resolve4) => {
3183
3937
  let output = "";
3184
- const child = spawn3(pm, ["install"], {
3938
+ const child = spawn3(argv[0], [...argv.slice(1)], {
3185
3939
  cwd: worktreePath,
3940
+ shell: false,
3186
3941
  stdio: ["ignore", "pipe", "pipe"]
3187
3942
  });
3188
3943
  child.stdout?.on("data", (d) => output += d);
@@ -3191,7 +3946,7 @@ async function installWorktreeDeps(worktreePath) {
3191
3946
  "error",
3192
3947
  (err) => resolve4({
3193
3948
  ok: false,
3194
- output: `Failed to run ${pm} install: ${err.message}`
3949
+ output: `Failed to run ${argv.join(" ")}: ${err.message}`
3195
3950
  })
3196
3951
  );
3197
3952
  child.on(
@@ -3200,8 +3955,41 @@ async function installWorktreeDeps(worktreePath) {
3200
3955
  );
3201
3956
  });
3202
3957
  }
3203
- var INGEST_RUNTIMES = ["node", "python", "python3", "bun"];
3204
- function validateIngestEntrypoint(worktreePath, entrypoint) {
3958
+ async function installWorktreeDeps(worktreePath, toolchain) {
3959
+ const { profile: profile2, installSteps, packageManager } = toolchain;
3960
+ const declared = packageManager.dependency.mode === "agent-declares" ? packageManager.dependency.file : void 0;
3961
+ const haveSomethingToInstall = await hasProfileManifest(worktreePath, profile2) || declared !== void 0 && existsSync4(join12(worktreePath, declared));
3962
+ if (!haveSomethingToInstall) {
3963
+ return {
3964
+ ok: true,
3965
+ output: `no ${profile2.displayName} manifest; skipped install`
3966
+ };
3967
+ }
3968
+ if (installSteps.length === 0) {
3969
+ return {
3970
+ ok: true,
3971
+ output: `${profile2.displayName} (${toolchain.packageManager.id}) has no wizard-run install step`
3972
+ };
3973
+ }
3974
+ const outputs = [];
3975
+ for (const step of installSteps) {
3976
+ if (step.requiresFile && !existsSync4(join12(worktreePath, step.requiresFile)))
3977
+ continue;
3978
+ const result = await spawnStep(worktreePath, step.argv);
3979
+ if (result.output) outputs.push(result.output);
3980
+ if (result.ok) continue;
3981
+ if (step.optional) {
3982
+ logger.warn(
3983
+ { step: step.argv.join(" "), output: result.output },
3984
+ "installWorktreeDeps: optional install step failed; continuing"
3985
+ );
3986
+ continue;
3987
+ }
3988
+ return { ok: false, output: outputs.join("\n").trim() };
3989
+ }
3990
+ return { ok: true, output: outputs.join("\n").trim() };
3991
+ }
3992
+ function validateIngestEntrypoint(worktreePath, entrypoint, allowedExtensions) {
3205
3993
  if (!entrypoint || entrypoint.startsWith("-")) {
3206
3994
  return {
3207
3995
  ok: false,
@@ -3216,18 +4004,29 @@ function validateIngestEntrypoint(worktreePath, entrypoint) {
3216
4004
  reason: `entrypoint "${entrypoint}" resolves outside the worktree`
3217
4005
  };
3218
4006
  }
4007
+ if (allowedExtensions?.length && !allowedExtensions.some((ext) => entrypoint.endsWith(ext))) {
4008
+ return {
4009
+ ok: false,
4010
+ reason: `entrypoint "${entrypoint}" is not one of ${allowedExtensions.join(", ")}`
4011
+ };
4012
+ }
3219
4013
  return { ok: true, target };
3220
4014
  }
3221
- async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3222
- if (!INGEST_RUNTIMES.includes(runtime)) {
4015
+ async function runIngestScript(worktreePath, toolchain, entrypoint, env = {}) {
4016
+ const { ingest, profile: profile2, packageManager } = toolchain;
4017
+ if (ingest.kind !== "auto") {
3223
4018
  return {
3224
4019
  ran: false,
3225
4020
  ok: false,
3226
4021
  output: "",
3227
- reason: `runtime "${runtime}" is not an allowed interpreter (${INGEST_RUNTIMES.join(", ")})`
4022
+ reason: `${profile2.displayName} (${packageManager.id}) projects must be run manually: ${ingest.runCommand}`
3228
4023
  };
3229
4024
  }
3230
- const validated = validateIngestEntrypoint(worktreePath, entrypoint);
4025
+ const validated = validateIngestEntrypoint(
4026
+ worktreePath,
4027
+ entrypoint,
4028
+ ingest.entrypointExtensions
4029
+ );
3231
4030
  if (!validated.ok) {
3232
4031
  return { ran: false, ok: false, output: "", reason: validated.reason };
3233
4032
  }
@@ -3248,9 +4047,10 @@ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3248
4047
  reason: `entrypoint "${entrypoint}" does not exist`
3249
4048
  };
3250
4049
  }
4050
+ const argv = resolveIngestArgv(ingest, entrypoint);
3251
4051
  return new Promise((resolveRun) => {
3252
4052
  let output = "";
3253
- const child = spawn3(runtime, [entrypoint], {
4053
+ const child = spawn3(argv[0], argv.slice(1), {
3254
4054
  cwd: worktreePath,
3255
4055
  shell: false,
3256
4056
  stdio: ["ignore", "pipe", "pipe"],
@@ -3263,7 +4063,7 @@ async function runIngestScript(worktreePath, runtime, entrypoint, env = {}) {
3263
4063
  (err) => resolveRun({
3264
4064
  ran: true,
3265
4065
  ok: false,
3266
- output: `Failed to run ${runtime} ${entrypoint}: ${err.message}`
4066
+ output: `Failed to run ${argv.join(" ")}: ${err.message}`
3267
4067
  })
3268
4068
  );
3269
4069
  child.on(
@@ -3285,8 +4085,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3285
4085
  } catch {
3286
4086
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3287
4087
  }
3288
- const relPath = join10(ingestDir, basename2(source));
3289
- const dest = join10(worktreePath, relPath);
4088
+ const relPath = join12(ingestDir, basename2(source));
4089
+ const dest = join12(worktreePath, relPath);
3290
4090
  try {
3291
4091
  await mkdir6(dirname7(dest), { recursive: true });
3292
4092
  await copyFile(source, dest);
@@ -3302,7 +4102,7 @@ function hasEnvVar(content, name) {
3302
4102
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
3303
4103
  }
3304
4104
  async function writeSearchEnvValues(worktreePath, vars) {
3305
- const target = join10(worktreePath, ".env");
4105
+ const target = join12(worktreePath, ".env");
3306
4106
  let existing = "";
3307
4107
  try {
3308
4108
  existing = await readFile8(target, "utf8");
@@ -3422,69 +4222,33 @@ async function resolveSearchOnlyKey(index) {
3422
4222
  }
3423
4223
 
3424
4224
  // src/lib/algoliaDocs.ts
3425
- import { readFileSync, readdirSync, existsSync as existsSync2 } from "node:fs";
3426
- import { dirname as dirname8, join as join11 } from "node:path";
4225
+ import { readFileSync, existsSync as existsSync5 } from "node:fs";
4226
+ import { dirname as dirname8, join as join13 } from "node:path";
3427
4227
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3428
- var DOCS_SUBPATH = join11("docs", "algolia-sdk");
4228
+ var DOCS_SUBPATH = join13("docs", "algolia-sdk");
3429
4229
  function findDocsDir() {
3430
4230
  let dir = dirname8(fileURLToPath2(import.meta.url));
3431
4231
  for (; ; ) {
3432
- const candidate = join11(dir, DOCS_SUBPATH);
3433
- if (existsSync2(candidate)) return candidate;
4232
+ const candidate = join13(dir, DOCS_SUBPATH);
4233
+ if (existsSync5(candidate)) return candidate;
3434
4234
  const parent = dirname8(dir);
3435
4235
  if (parent === dir) return void 0;
3436
4236
  dir = parent;
3437
4237
  }
3438
4238
  }
3439
- function loadAlgoliaDoc(language) {
3440
- const docsDir = findDocsDir();
3441
- if (!docsDir) {
3442
- logger.warn(
3443
- "algoliaDocs: docs/algolia-sdk not found; skipping SDK reference"
3444
- );
3445
- return "";
3446
- }
3447
- const files = readdirSync(docsDir).filter((f) => f.includes(language));
3448
- if (files.length === 0) {
3449
- logger.warn(
3450
- { language },
3451
- "algoliaDocs: no SDK reference found for language; skipping"
3452
- );
3453
- return "";
3454
- }
3455
- return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3456
- }
3457
- function getNamedDoc(name, language) {
4239
+ function getNamedDoc(name, key) {
3458
4240
  const docsDir = findDocsDir();
3459
4241
  if (!docsDir) {
3460
4242
  logger.warn("docs/algolia-sdk not found");
3461
4243
  return "";
3462
4244
  }
3463
- const file = join11(docsDir, `${name}-${language}.md`);
3464
- if (!existsSync2(file)) {
3465
- logger.warn({ name, language }, "named SDK reference not found");
4245
+ const file = join13(docsDir, `${name}-${key}.md`);
4246
+ if (!existsSync5(file)) {
4247
+ logger.warn({ name, key }, "named SDK reference not found");
3466
4248
  return "";
3467
4249
  }
3468
4250
  return readFileSync(file, "utf8").trim();
3469
4251
  }
3470
- function getFrameworkSpecificDoc(frameworks) {
3471
- const fw = frameworks.map((f) => f.toLowerCase());
3472
- if (fw.includes("vue") || fw.includes("nuxt")) {
3473
- return loadAlgoliaDoc("vue");
3474
- }
3475
- if (fw.includes("react") || fw.includes("next.js")) {
3476
- return loadAlgoliaDoc("react");
3477
- }
3478
- if (fw.includes("angular")) {
3479
- return loadAlgoliaDoc("angular");
3480
- }
3481
- return loadAlgoliaDoc("js");
3482
- }
3483
-
3484
- // src/lib/shell.ts
3485
- function shellQuote(value) {
3486
- return "'" + value.replace(/'/g, "'\\''") + "'";
3487
- }
3488
4252
 
3489
4253
  // src/actions/implement.ts
3490
4254
  var implementSchema = z24.object({
@@ -3519,12 +4283,11 @@ var implementSchema = z24.object({
3519
4283
  });
3520
4284
  var implementationOutputSchema = z24.object({
3521
4285
  summary: z24.string(),
3522
- // Ingestion only: how to run the generated script, as a structured pair the
3523
- // wizard turns into an argv (`<runtime> <entrypoint>`) never a free-form
3524
- // command string. `runtime` is constrained to an allowlisted interpreter and
3525
- // `entrypoint` is validated to a worktree-relative path before execution, so
3526
- // the agent cannot inject extra commands or swap the interpreter.
3527
- runtime: z24.enum(INGEST_RUNTIMES).optional(),
4286
+ // Ingestion only: the script the wizard should run, as a bare path — never a
4287
+ // command string, and never the interpreter. The command comes from the
4288
+ // resolved language toolchain (a registry constant); this path is validated to
4289
+ // a worktree-relative file with a runnable extension and substituted into it.
4290
+ // So the agent contributes no part of the command that gets executed.
3528
4291
  entrypoint: z24.string().optional()
3529
4292
  });
3530
4293
  var verificationOutputSchema = z24.object({
@@ -3534,28 +4297,9 @@ var verificationOutputSchema = z24.object({
3534
4297
  });
3535
4298
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
3536
4299
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
3537
- var INGEST_DIR = ".algolia-wizard";
3538
- function detectUiFramework(language) {
3539
- const names = language.frameworks.map((f) => f.name.toLowerCase());
3540
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
3541
- if (names.some((n) => n.includes("react") || n.includes("next")))
3542
- return "React";
3543
- if (names.some((n) => n.includes("angular"))) return "Angular";
3544
- return "JavaScript";
3545
- }
3546
- function frameworksForDoc(framework) {
3547
- switch (framework) {
3548
- case "React":
3549
- return ["react"];
3550
- case "Vue":
3551
- return ["vue"];
3552
- case "Angular":
3553
- return ["angular"];
3554
- case "JavaScript":
3555
- return [];
3556
- }
3557
- }
3558
- function publicEnvPrefix(language) {
4300
+ var INGEST_DIR2 = ".algolia-wizard";
4301
+ function publicEnvPrefix(language, strategy) {
4302
+ if (strategy === "cdn-template" || strategy === "none") return "";
3559
4303
  const frameworkNames = language.frameworks.map(
3560
4304
  (framework) => framework.name.toLowerCase()
3561
4305
  );
@@ -3573,8 +4317,8 @@ function publicEnvPrefix(language) {
3573
4317
  }
3574
4318
  return "PUBLIC_";
3575
4319
  }
3576
- function searchEnvVars(language, appId, searchKey) {
3577
- const prefix = publicEnvPrefix(language);
4320
+ function searchEnvVars(language, strategy, appId, searchKey) {
4321
+ const prefix = publicEnvPrefix(language, strategy);
3578
4322
  return [
3579
4323
  {
3580
4324
  name: `${prefix}ALGOLIA_APP_ID`,
@@ -3586,6 +4330,42 @@ function searchEnvVars(language, appId, searchKey) {
3586
4330
  }
3587
4331
  ];
3588
4332
  }
4333
+ async function resolveIngestionProfile(ctx, language, repoRoot) {
4334
+ const fallback = LANGUAGE_PROFILES[DEFAULT_LANGUAGE_ID];
4335
+ const confirmed3 = language.languages.map((l) => resolveLanguageProfile(l.name)).filter((p) => p !== void 0);
4336
+ const onDisk = await detectProfilesFromManifests(repoRoot);
4337
+ const onDiskIds = new Set(onDisk.map((p) => p.id));
4338
+ const candidates = [
4339
+ ...new Map(
4340
+ confirmed3.filter((p) => onDiskIds.has(p.id)).map((p) => [p.id, p])
4341
+ ).values()
4342
+ ];
4343
+ if (candidates.length === 0) {
4344
+ const chosen = confirmed3[0] ?? onDisk[0] ?? fallback;
4345
+ logger.warn(
4346
+ {
4347
+ confirmed: language.languages.map((l) => l.name),
4348
+ onDisk: onDisk.map((p) => p.id),
4349
+ chosen: chosen.id
4350
+ },
4351
+ "implement: no confirmed language matched a manifest on disk; falling back"
4352
+ );
4353
+ return chosen;
4354
+ }
4355
+ if (candidates.length === 1) return candidates[0];
4356
+ const backends = candidates.filter((p) => p.id !== DEFAULT_LANGUAGE_ID);
4357
+ if (backends.length === 1) return backends[0];
4358
+ if (backends.length === 0) return candidates[0];
4359
+ const options = backends.map((p) => p.displayName);
4360
+ const selection = await ctx.requestUserInput({
4361
+ prompt: "Which language should the ingestion script use?",
4362
+ promptType: "multipleChoice",
4363
+ options,
4364
+ defaultSelectedIndex: 0
4365
+ });
4366
+ const picked = typeof selection === "string" ? backends.find((p) => p.displayName === selection) : void 0;
4367
+ return picked ?? backends[0];
4368
+ }
3589
4369
  function baseInstructions(input) {
3590
4370
  return [
3591
4371
  `Target Algolia index: ${input.targetIndex}`,
@@ -3613,37 +4393,48 @@ function sourceSpecificInstructions(input) {
3613
4393
  generated: [
3614
4394
  "No real data source exists; use sample records for each confirmed entity.",
3615
4395
  "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.",
3616
- "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.",
4396
+ "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.",
3617
4397
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
3618
4398
  ]
3619
4399
  };
3620
4400
  return byLine[input.ingestionSource];
3621
4401
  }
3622
4402
  function ingestionInstructions(input) {
4403
+ const { ingestionProfile: profile2, toolchain } = input;
4404
+ const { ingest } = toolchain;
4405
+ const extensions = ingest.entrypointExtensions.join(", ");
4406
+ const runInstruction = ingest.kind === "auto" ? `Return "entrypoint": the script path relative to the worktree root (e.g. "${profile2.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard runs it with \`${describeIngestCommand(ingest, profile2.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. "${profile2.ingestEntrypointExample}"), ending in one of ${extensions}. The wizard does NOT run ${profile2.displayName} ${toolchain.packageManager.id} projects itself \u2014 it tells the developer to run \`${describeIngestCommand(ingest, profile2.ingestEntrypointExample)}\`, so also add whatever build configuration that command needs.`;
3623
4407
  return [
3624
4408
  ...input.confirmed && input.confirmed.length ? [
3625
- `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
4409
+ `Write the ingestion script in ${profile2.displayName}, at "${ingestScriptDir(profile2)}/" in the repo.`,
3626
4410
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3627
- `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.`,
3628
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4411
+ `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. ${profile2.envReadInstruction} The wizard sets these when it runs the script.`,
4412
+ `Use the official Algolia ${profile2.displayName} client (${profile2.sdk.packageName}). Do not use the raw HTTP API, and do not use a client for another language.`,
3629
4413
  "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.",
3630
- getNamedDoc("save-records", "js"),
3631
- 'Add algoliasearch to package.json "dependencies" with a valid version range; the wizard installs the worktree deps after you finish.',
4414
+ getNamedDoc("save-records", profile2.sdk.docKey),
4415
+ dependencyInstruction(toolchain),
3632
4416
  "The summary should be extremely concise.",
3633
- `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.`,
4417
+ runInstruction,
3634
4418
  ...sourceSpecificInstructions(input)
3635
4419
  ] : []
3636
4420
  ];
3637
4421
  }
3638
4422
  function searchInstructions(input) {
3639
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4423
+ const doc = getNamedDoc(
4424
+ "instantsearch-setup",
4425
+ searchDocKey(input.searchStrategy)
4426
+ );
4427
+ const isTemplate = input.searchStrategy === "cdn-template";
4428
+ 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.`;
3640
4429
  return [
3641
4430
  "Implement an in-app Algolia search experience.",
3642
- `Build the search UI for ${input.uiFramework}.`,
3643
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4431
+ `Build the search UI for ${describeSearchTarget(input.searchStrategy, input.frameworkName)}.`,
4432
+ "Follow the Algolia reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
3644
4433
  doc,
3645
- `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.`,
3646
- "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.",
4434
+ placement,
4435
+ `It needs at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
4436
+ 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.',
4437
+ 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.",
3647
4438
  // appId always resolves (loadActiveProfile throws otherwise); only the
3648
4439
  // search-only key is best-effort and can fall back to a placeholder.
3649
4440
  `Values: App ID "${input.appId}", search-only key ${input.searchKey ? `"${input.searchKey}"` : "(placeholder for the developer to fill in)"}.`,
@@ -3651,8 +4442,7 @@ function searchInstructions(input) {
3651
4442
  // resolved app id / search-only key into ".env" under these exact names
3652
4443
  // right after this step, so a renamed prefix here would leave the code
3653
4444
  // reading a var the wizard never wrote.
3654
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3655
- '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.',
4445
+ `Use exactly these env var names: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
3656
4446
  "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."
3657
4447
  ];
3658
4448
  }
@@ -3660,7 +4450,7 @@ function verificationInstructions(input) {
3660
4450
  return [
3661
4451
  "Verify the Algolia implementation changes in the current worktree.",
3662
4452
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
3663
- "Call verifyImplementation at least once; it runs every repo-defined lint/typecheck/check script and returns per-check results plus an aggregate ok.",
4453
+ `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.`,
3664
4454
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run verifyImplementation.",
3665
4455
  "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.",
3666
4456
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
@@ -3715,8 +4505,8 @@ function formatSummary(useCase, summary) {
3715
4505
  const label = useCase === "ingestion" ? "Ingestion" : useCase === "search" ? "Search" : "Verification";
3716
4506
  return `${label}: ${summary}`;
3717
4507
  }
3718
- function buildIngestCommand(worktree, runtime, entrypoint) {
3719
- return `cd ${shellQuote(worktree)} && ${runtime} ${shellQuote(entrypoint)}`;
4508
+ function buildIngestCommand(worktree, toolchain, entrypoint) {
4509
+ return `cd ${shellQuote(worktree)} && ${describeIngestCommand(toolchain.ingest, entrypoint)}`;
3720
4510
  }
3721
4511
  function parseIngestRecordCount(output) {
3722
4512
  const match = output.match(/ALGOLIA_WIZARD_RECORD_COUNT=(\d+)/);
@@ -3798,7 +4588,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3798
4588
  await confirmDirtyWorkingTree(ctx, repoRoot);
3799
4589
  }
3800
4590
  const normalized = normalizeFindingPaths(findings);
3801
- const confirmed2 = normalized.confirmedEntities;
4591
+ const confirmed3 = normalized.confirmedEntities;
3802
4592
  const searchLocation = normalized.searchImplementationAnalysis;
3803
4593
  let appId;
3804
4594
  let searchKey;
@@ -3822,7 +4612,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3822
4612
  const copied = await copyUploadIntoWorktree(
3823
4613
  repoRoot,
3824
4614
  worktree,
3825
- INGEST_DIR,
4615
+ INGEST_DIR2,
3826
4616
  uploadSourcePath ?? ""
3827
4617
  );
3828
4618
  if (copied.ok) {
@@ -3836,26 +4626,59 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3836
4626
  );
3837
4627
  }
3838
4628
  }
4629
+ const ingestionProfile = await resolveIngestionProfile(
4630
+ ctx,
4631
+ language,
4632
+ worktree
4633
+ );
4634
+ const toolchain = await resolveToolchain(worktree, ingestionProfile);
4635
+ const verificationLanguages = [
4636
+ .../* @__PURE__ */ new Set([
4637
+ ingestionProfile.id,
4638
+ ...(await detectProfilesFromManifests(worktree)).map((p) => p.id)
4639
+ ])
4640
+ ];
4641
+ const frameworkName = language.frameworks[0]?.name;
4642
+ const searchStrategy = resolveSearchStrategy(
4643
+ frameworkName,
4644
+ verificationLanguages.includes(DEFAULT_LANGUAGE_ID)
4645
+ );
4646
+ logger.info(
4647
+ {
4648
+ language: ingestionProfile.id,
4649
+ packageManager: toolchain.packageManager.id,
4650
+ ingest: toolchain.ingest.kind,
4651
+ framework: frameworkName,
4652
+ searchStrategy
4653
+ },
4654
+ "implement: resolved ingestion toolchain and search strategy"
4655
+ );
3839
4656
  const input = {
3840
4657
  findings: normalized,
3841
- confirmed: confirmed2,
4658
+ confirmed: confirmed3,
3842
4659
  searchLocation,
3843
4660
  targetIndex,
3844
4661
  language,
3845
4662
  appId,
3846
4663
  searchKey,
3847
- searchEnvVars: searchEnvVars(language, appId, searchKey),
3848
- ingestDir: INGEST_DIR,
4664
+ searchEnvVars: searchEnvVars(language, searchStrategy, appId, searchKey),
4665
+ ingestDir: INGEST_DIR2,
3849
4666
  ingestionSource,
3850
4667
  uploadFilePath,
3851
- // language.frameworks already prefers the confirm-framework step output,
3852
- // so the user's confirmed stack (not just raw detection) picks the flavor.
3853
- uiFramework: detectUiFramework(language)
4668
+ searchStrategy,
4669
+ frameworkName,
4670
+ ingestionProfile,
4671
+ toolchain,
4672
+ verificationLanguages
3854
4673
  };
4674
+ const searchToolchain = searchStrategy === "cdn-template" || searchStrategy === "none" ? void 0 : ingestionProfile.id === DEFAULT_LANGUAGE_ID ? toolchain : await resolveToolchain(
4675
+ worktree,
4676
+ LANGUAGE_PROFILES[DEFAULT_LANGUAGE_ID]
4677
+ );
4678
+ const toolchainForUseCase = (useCase) => useCase === "search" ? searchToolchain : toolchain;
3855
4679
  const summaries = [];
3856
4680
  if (uploadWarning) summaries.push(uploadWarning);
3857
4681
  let agentRuns = 0;
3858
- let ingestRuntime;
3859
4682
  let ingestEntrypoint;
3860
4683
  let ingestScriptRan = false;
3861
4684
  let ingestRecordCount;
@@ -3874,13 +4697,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3874
4697
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
3875
4698
  outputSchema: implementationOutputSchema
3876
4699
  });
4700
+ const useCaseToolchain = toolchainForUseCase(currentUseCase);
4701
+ if (!useCaseToolchain) return result;
3877
4702
  ctx.notify({
3878
4703
  messages: [`Installing dependencies for ${currentUseCase}\u2026`]
3879
4704
  });
3880
4705
  const installLogId = ctx.logStart("installWorktreeDeps", {
3881
- useCase: currentUseCase
4706
+ useCase: currentUseCase,
4707
+ language: useCaseToolchain.profile.id
3882
4708
  });
3883
- const install = await installWorktreeDeps(worktree);
4709
+ const install = await installWorktreeDeps(worktree, useCaseToolchain);
3884
4710
  ctx.logEnd(installLogId, install.ok ? "success" : "error");
3885
4711
  if (!install.ok) {
3886
4712
  installFailed = true;
@@ -3897,15 +4723,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3897
4723
  return runAgent({
3898
4724
  instructions: buildAgentInstructions("verification", input),
3899
4725
  tools: toolsForUseCase("verification"),
3900
- outputSchema: verificationOutputSchema
4726
+ outputSchema: verificationOutputSchema,
4727
+ // So verifyImplementation runs this repo's checks, not just npm scripts.
4728
+ languages: input.verificationLanguages
3901
4729
  });
3902
4730
  }
3903
4731
  if (useCases.includes("ingestion")) {
3904
- const { summary, runtime, entrypoint } = await runImplementationUseCase("ingestion");
4732
+ const { summary, entrypoint } = await runImplementationUseCase("ingestion");
3905
4733
  summaries.push(formatSummary("ingestion", summary));
3906
- ingestRuntime = runtime;
3907
4734
  ingestEntrypoint = entrypoint;
3908
- if (ingestRuntime && ingestEntrypoint && !installFailed) {
4735
+ if (ingestEntrypoint && toolchain.ingest.kind === "auto" && !installFailed) {
3909
4736
  ctx.clearNotices();
3910
4737
  const runNow = await ctx.requestUserInput({
3911
4738
  prompt: `Run the ingestion script now? This writes records to the "${targetIndex}" index.`,
@@ -3917,13 +4744,13 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3917
4744
  const profile2 = await loadActiveProfile();
3918
4745
  ctx.notify({ messages: [`Writing records to "${targetIndex}"\u2026`] });
3919
4746
  const scriptLogId = ctx.logStart("runIngestScript", {
3920
- runtime: ingestRuntime,
4747
+ language: ingestionProfile.id,
3921
4748
  entrypoint: ingestEntrypoint
3922
4749
  });
3923
4750
  const startedAt = Date.now();
3924
4751
  const run = await runIngestScript(
3925
4752
  worktree,
3926
- ingestRuntime,
4753
+ toolchain,
3927
4754
  ingestEntrypoint,
3928
4755
  {
3929
4756
  [APP_ID_VAR]: profile2.appId,
@@ -3937,7 +4764,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3937
4764
  ingestRecordCount = parseIngestRecordCount(run.output);
3938
4765
  if (ingestRecordCount != null) {
3939
4766
  track("AI Wizard Ingest Successful", {
3940
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4767
+ entity_name: confirmed3?.map((e) => e.name).join(", ") || "unknown",
3941
4768
  record_count: ingestRecordCount,
3942
4769
  duration_ms: ingestDurationMs
3943
4770
  });
@@ -3950,7 +4777,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
3950
4777
  outcomeMessage = `\u26A0\uFE0F The ingestion script did not run: ${run.reason}`;
3951
4778
  logger.warn(
3952
4779
  {
3953
- runtime: ingestRuntime,
4780
+ language: ingestionProfile.id,
3954
4781
  entrypoint: ingestEntrypoint,
3955
4782
  reason: run.reason
3956
4783
  },
@@ -3973,7 +4800,7 @@ ${run.output}` : status;
3973
4800
  outcomeMessage = `\u274C Ingestion failed.${run.output ? ` ${run.output}` : ""}`;
3974
4801
  logger.warn(
3975
4802
  {
3976
- runtime: ingestRuntime,
4803
+ language: ingestionProfile.id,
3977
4804
  entrypoint: ingestEntrypoint,
3978
4805
  output: run.output
3979
4806
  },
@@ -3990,10 +4817,15 @@ ${run.output}` : status;
3990
4817
  }
3991
4818
  }
3992
4819
  const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
3993
- if (ingestRuntime && ingestEntrypoint) {
4820
+ if (ingestEntrypoint) {
3994
4821
  commandMessages.push(
3995
- `Ingestion command: ${buildIngestCommand(worktree, ingestRuntime, ingestEntrypoint)}`
4822
+ `Ingestion command: ${buildIngestCommand(worktree, toolchain, ingestEntrypoint)}`
3996
4823
  );
4824
+ if (toolchain.ingest.kind === "manual") {
4825
+ commandMessages.push(
4826
+ `The wizard does not run ${ingestionProfile.displayName} ${toolchain.packageManager.id} projects \u2014 run the command above yourself to ingest.`
4827
+ );
4828
+ }
3997
4829
  }
3998
4830
  await ctx.requestUserInput({
3999
4831
  // No question being asked here, just an acknowledgement — the
@@ -4004,7 +4836,21 @@ ${run.output}` : status;
4004
4836
  messages: ingestOutcomeMessage ? [ingestOutcomeMessage, ...commandMessages] : commandMessages
4005
4837
  });
4006
4838
  }
4007
- if (useCases.includes("search")) {
4839
+ const skipSearch = useCases.includes("search") && input.searchStrategy === "none";
4840
+ if (skipSearch) {
4841
+ const target = describeSearchTarget(
4842
+ input.searchStrategy,
4843
+ input.frameworkName
4844
+ );
4845
+ summaries.push(
4846
+ `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).`
4847
+ );
4848
+ ctx.setUserInput("implementation", "success");
4849
+ track("AI Wizard Search UI Skipped", {
4850
+ framework: input.frameworkName ?? "unknown"
4851
+ });
4852
+ }
4853
+ if (useCases.includes("search") && !skipSearch) {
4008
4854
  let extraInstructions = [];
4009
4855
  const preSearchFiles = new Set(await listChangedFiles(worktree));
4010
4856
  for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
@@ -4077,7 +4923,7 @@ ${run.output}` : status;
4077
4923
  }
4078
4924
  if (installFailed) {
4079
4925
  summaries.push(
4080
- '\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".'
4926
+ `\u26A0\uFE0F Dependency install in the worktree failed. Install the ${ingestionProfile.displayName} dependencies in the worktree before the command below, or it will fail on a missing package.`
4081
4927
  );
4082
4928
  }
4083
4929
  return {
@@ -4085,10 +4931,10 @@ ${run.output}` : status;
4085
4931
  filesChanged,
4086
4932
  summary: summaries.join("\n\n"),
4087
4933
  worktreePath: worktree,
4088
- ...useCases.includes("ingestion") && ingestRuntime && ingestEntrypoint ? {
4934
+ ...useCases.includes("ingestion") && ingestEntrypoint ? {
4089
4935
  ingestCommand: buildIngestCommand(
4090
4936
  worktree,
4091
- ingestRuntime,
4937
+ toolchain,
4092
4938
  ingestEntrypoint
4093
4939
  ),
4094
4940
  ingestScriptRan,