@algolia/wizard 0.14.0 → 0.16.0

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
@@ -1079,12 +1079,12 @@ var sidebarItems = [
1079
1079
  description: "push 100 records to Algolia in seconds"
1080
1080
  },
1081
1081
  {
1082
- title: "detect your framework",
1083
- description: "React, Vue, Angular, Vanilla JS"
1082
+ title: "detect your stack",
1083
+ description: "whatever language and framework you already use"
1084
1084
  },
1085
1085
  {
1086
1086
  title: "scaffold a search UI",
1087
- description: "a styled InstantSearch component, wired into your app"
1087
+ description: "a search box and results, wired into your app"
1088
1088
  },
1089
1089
  {
1090
1090
  title: "ship it",
@@ -2507,8 +2507,8 @@ function writeFileTool(ctx) {
2507
2507
  // src/lib/tools/writeAlgoliaCredentials.ts
2508
2508
  import { tool as tool6 } from "ai";
2509
2509
  import z13 from "zod";
2510
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2511
- import { dirname as dirname5 } from "node:path";
2510
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
2511
+ import { dirname as dirname5, relative as relative3 } from "node:path";
2512
2512
 
2513
2513
  // src/lib/algoliaApiKey.ts
2514
2514
  import { z as z12 } from "zod";
@@ -2690,6 +2690,79 @@ async function resolveSearchOnlyKey(index, appId, envKey) {
2690
2690
  );
2691
2691
  }
2692
2692
 
2693
+ // src/lib/gitignore.ts
2694
+ import { execFile } from "node:child_process";
2695
+ import { lstat as lstat2, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2696
+ import { join as join8, relative as relative2 } from "node:path";
2697
+ var GIT_ENV_OVERRIDES = [
2698
+ "GIT_DIR",
2699
+ "GIT_WORK_TREE",
2700
+ "GIT_INDEX_FILE",
2701
+ "GIT_OBJECT_DIRECTORY",
2702
+ "GIT_COMMON_DIR"
2703
+ ];
2704
+ function gitSucceeds(root, args) {
2705
+ const env = { ...process.env };
2706
+ for (const key of GIT_ENV_OVERRIDES) delete env[key];
2707
+ return new Promise((resolve4) => {
2708
+ execFile("git", ["-C", root, ...args], { env }, (err) => {
2709
+ if (!err) return resolve4(true);
2710
+ resolve4(
2711
+ err.code === 1 ? false : void 0
2712
+ );
2713
+ });
2714
+ });
2715
+ }
2716
+ function isIgnoredByRule(root, relPath) {
2717
+ return gitSucceeds(root, ["check-ignore", "-q", "--no-index", "--", relPath]);
2718
+ }
2719
+ function isTracked(root, relPath) {
2720
+ return gitSucceeds(root, ["ls-files", "--error-unmatch", "--", relPath]);
2721
+ }
2722
+ async function inspect(root, target) {
2723
+ const relPath = relative2(root, target);
2724
+ if (!relPath || relPath.startsWith("..")) {
2725
+ return { ignoredByRule: void 0, tracked: false };
2726
+ }
2727
+ const ignoredByRule = await isIgnoredByRule(root, relPath);
2728
+ if (ignoredByRule === void 0) {
2729
+ logger.warn(
2730
+ { root, relPath },
2731
+ "gitignore: git check-ignore could not answer; not reporting on this path"
2732
+ );
2733
+ return { ignoredByRule, tracked: false };
2734
+ }
2735
+ return { ignoredByRule, tracked: await isTracked(root, relPath) };
2736
+ }
2737
+ async function ensureGitIgnored(root, target) {
2738
+ const { ignoredByRule, tracked } = await inspect(root, target);
2739
+ if (ignoredByRule === void 0) return "unknown";
2740
+ if (ignoredByRule) return tracked ? "tracked" : "covered";
2741
+ const pattern = relative2(root, target);
2742
+ const gitIgnore = join8(root, ".gitignore");
2743
+ try {
2744
+ const link = await lstat2(gitIgnore).catch(() => null);
2745
+ if (link?.isSymbolicLink()) {
2746
+ logger.warn(
2747
+ { gitIgnore },
2748
+ "gitignore: root .gitignore is a symlink; not writing to it"
2749
+ );
2750
+ return "unknown";
2751
+ }
2752
+ const existing = link ? await readFile5(gitIgnore, "utf8") : "";
2753
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2754
+ await writeFile4(gitIgnore, `${existing}${prefix}${pattern}
2755
+ `, "utf8");
2756
+ return tracked ? "tracked" : "added";
2757
+ } catch (err) {
2758
+ logger.warn(
2759
+ { gitIgnore, pattern, err: err.message },
2760
+ "gitignore: could not add the pattern; continuing"
2761
+ );
2762
+ return "unknown";
2763
+ }
2764
+ }
2765
+
2693
2766
  // src/lib/tools/writeAlgoliaCredentials.ts
2694
2767
  var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
2695
2768
  var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
@@ -2722,7 +2795,7 @@ function upsertEnv(content, name, value) {
2722
2795
  }
2723
2796
  function writeCredentialsTool(ctx) {
2724
2797
  return tool6({
2725
- description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application.`,
2798
+ description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself.`,
2726
2799
  inputSchema: z13.object({
2727
2800
  filePath: z13.string().describe(
2728
2801
  'Path to the env file to write credentials into (e.g. ".env")'
@@ -2743,7 +2816,7 @@ function writeCredentialsTool(ctx) {
2743
2816
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2744
2817
  }
2745
2818
  try {
2746
- existing = await readFile5(resolved2.target, "utf8");
2819
+ existing = await readFile6(resolved2.target, "utf8");
2747
2820
  } catch (err) {
2748
2821
  if (err.code !== "ENOENT") throw err;
2749
2822
  }
@@ -2782,6 +2855,7 @@ function writeCredentialsTool(ctx) {
2782
2855
  return `Error: could not resolve Algolia credentials (${err.message}). Ask the user to authenticate with the Algolia CLI first.`;
2783
2856
  }
2784
2857
  }
2858
+ let wrote;
2785
2859
  try {
2786
2860
  const updated = [
2787
2861
  ...credentials,
@@ -2793,7 +2867,7 @@ function writeCredentialsTool(ctx) {
2793
2867
  existing
2794
2868
  );
2795
2869
  await mkdir4(dirname5(resolved2.target), { recursive: true });
2796
- await writeFile4(resolved2.target, updated, "utf8");
2870
+ await writeFile5(resolved2.target, updated, "utf8");
2797
2871
  const sentences = [
2798
2872
  `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
2799
2873
  ];
@@ -2807,26 +2881,48 @@ function writeCredentialsTool(ctx) {
2807
2881
  `Skipped ${present.join(" and ")}: already defined there.`
2808
2882
  );
2809
2883
  }
2810
- return [...sentences, ...notes].join(" ");
2884
+ wrote = [...sentences, ...notes].join(" ");
2811
2885
  } catch (err) {
2812
2886
  return `Error writing credentials to ${filePath}: ${err.message}`;
2813
2887
  }
2888
+ return wrote + await gitIgnoreOutcome(ctx, resolved2.target);
2814
2889
  }
2815
2890
  });
2816
2891
  }
2892
+ async function gitIgnoreOutcome(ctx, target) {
2893
+ const name = relative3(ctx.root, target);
2894
+ switch (await ensureGitIgnored(ctx.root, target)) {
2895
+ case "added":
2896
+ return ` Added "${name}" to .gitignore so the credentials are not stageable.`;
2897
+ case "tracked":
2898
+ return ` Warning: ${name} is already tracked by git, and a .gitignore rule cannot un-stage it. Tell the user with notifyUser to run "git rm --cached ${name}" before committing.`;
2899
+ case "unknown":
2900
+ return ` Warning: could not confirm ${name} is gitignored. Tell the user with notifyUser to check before committing.`;
2901
+ case "covered":
2902
+ return "";
2903
+ }
2904
+ }
2817
2905
 
2818
2906
  // src/lib/tools/searchFiles.ts
2819
2907
  import { tool as tool7 } from "ai";
2820
2908
  import z14 from "zod";
2821
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2822
- import { join as join8 } from "node:path";
2909
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
2910
+ import { join as join9 } from "node:path";
2823
2911
  var MAX_QUERY_LENGTH = 1e3;
2912
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2913
+ "node_modules",
2914
+ "dist",
2915
+ "build",
2916
+ "vendor",
2917
+ "venv",
2918
+ "__pycache__",
2919
+ "target"
2920
+ ]);
2824
2921
  async function walkFiles(dir) {
2825
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2826
2922
  const out = [];
2827
2923
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2828
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2829
- const full = join8(dir, e.name);
2924
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2925
+ const full = join9(dir, e.name);
2830
2926
  if (e.isDirectory()) out.push(...await walkFiles(full));
2831
2927
  else if (e.isFile()) out.push(full);
2832
2928
  }
@@ -2859,7 +2955,7 @@ function searchFilesTool(ctx) {
2859
2955
  for (const file of await walkFiles(resolved2.target)) {
2860
2956
  let content;
2861
2957
  try {
2862
- content = await readFile6(file, "utf8");
2958
+ content = await readFile7(file, "utf8");
2863
2959
  } catch {
2864
2960
  continue;
2865
2961
  }
@@ -2882,7 +2978,7 @@ function searchFilesTool(ctx) {
2882
2978
  // src/lib/tools/runShell.ts
2883
2979
  import { tool as tool8 } from "ai";
2884
2980
  import z15 from "zod";
2885
- import { relative as relative2 } from "node:path";
2981
+ import { relative as relative4 } from "node:path";
2886
2982
 
2887
2983
  // src/lib/tools/utils/runShell.ts
2888
2984
  import { spawn as spawn2 } from "node:child_process";
@@ -2979,7 +3075,7 @@ function runShell(command, opts) {
2979
3075
  // src/lib/tools/runShell.ts
2980
3076
  function storeApproval(root) {
2981
3077
  return async (req) => {
2982
- const rel = relative2(root, req.cwd);
3078
+ const rel = relative4(root, req.cwd);
2983
3079
  const answer = await useWizard.getState().requestUserInput({
2984
3080
  prompt: "Run this command?",
2985
3081
  promptType: "commandApproval",
@@ -3073,7 +3169,7 @@ function runShellTool(ctx) {
3073
3169
  import { tool as tool9, generateText, Output, NoObjectGeneratedError } from "ai";
3074
3170
  import { createAnthropic } from "@ai-sdk/anthropic";
3075
3171
  import { nanoid as nanoid2 } from "nanoid";
3076
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
3172
+ import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
3077
3173
  import { dirname as dirname6 } from "node:path";
3078
3174
  import z16 from "zod";
3079
3175
  var DATA_DIR = ".algolia-wizard/data";
@@ -3150,12 +3246,12 @@ function generateRecordTool(ctx) {
3150
3246
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
3151
3247
  }
3152
3248
  await mkdir5(dirname6(resolved2.target), { recursive: true });
3153
- await writeFile5(resolved2.target, JSON.stringify(records, null, 2), "utf8");
3249
+ await writeFile6(resolved2.target, JSON.stringify(records, null, 2), "utf8");
3154
3250
  logger.info({ entityName, count: records.length, relPath }, "generateRecord wrote records to disk");
3155
3251
  return {
3156
3252
  filePath: relPath,
3157
3253
  count: records.length,
3158
- message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) \u2014 do not inline the records as literals.`
3254
+ message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime using your language's standard JSON support \u2014 do not inline the records as literals.`
3159
3255
  };
3160
3256
  } catch (err) {
3161
3257
  return `Error generating records: ${err.message}`;
@@ -3344,8 +3440,8 @@ var detectLanguageSchema = z20.object({
3344
3440
  var detectLanguage = () => runAgent({
3345
3441
  instructions: [
3346
3442
  "Analyze the codebase and determine the programming languages and frameworks used",
3347
- "If a superset language is found, exclude the subset language. TS-over-JS.",
3348
- "If a meta-framework is used, exclude the framework. Next-over-React.",
3443
+ "If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
3444
+ "If a meta-framework is used, exclude the framework it builds on (e.g. Next.js over React, Rails over Rack).",
3349
3445
  "Return the exact version",
3350
3446
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
3351
3447
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -3443,7 +3539,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3443
3539
  // package.json
3444
3540
  var package_default = {
3445
3541
  name: "@algolia/wizard",
3446
- version: "0.14.0",
3542
+ version: "0.16.0",
3447
3543
  description: "Magically implement Algolia functionality in your codebase",
3448
3544
  type: "module",
3449
3545
  engines: {
@@ -3592,9 +3688,11 @@ var CURATED_FRAMEWORKS = [
3592
3688
  "Next.js",
3593
3689
  "React",
3594
3690
  "Vue",
3595
- "Angular",
3596
- "Svelte",
3597
- "Vanilla JS"
3691
+ "Vanilla JS",
3692
+ "Django",
3693
+ "Laravel",
3694
+ "Rails",
3695
+ "Symfony"
3598
3696
  ];
3599
3697
  var OTHER_OPTION = "Other";
3600
3698
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -3607,12 +3705,15 @@ var FRAMEWORK_ALIASES = {
3607
3705
  vuejs: "vue",
3608
3706
  angular: "angular",
3609
3707
  angularjs: "angular",
3610
- svelte: "svelte",
3611
- sveltekit: "svelte",
3612
3708
  vanillajs: "vanillajs",
3613
3709
  vanilla: "vanillajs",
3614
3710
  javascript: "vanillajs",
3615
- js: "vanillajs"
3711
+ js: "vanillajs",
3712
+ django: "django",
3713
+ laravel: "laravel",
3714
+ rails: "rails",
3715
+ rubyonrails: "rails",
3716
+ symfony: "symfony"
3616
3717
  };
3617
3718
  var isSameFramework = (a, b) => {
3618
3719
  const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
@@ -3844,17 +3945,18 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3844
3945
 
3845
3946
  // src/actions/implement.ts
3846
3947
  import z27 from "zod";
3948
+ import { join as join12 } from "node:path";
3847
3949
 
3848
3950
  // src/lib/worktree.ts
3849
- import { execFile } from "node:child_process";
3850
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile7, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3851
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
3951
+ import { execFile as execFile2 } from "node:child_process";
3952
+ import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
3953
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
3852
3954
  var MAX_BUFFER = 32 * 1024 * 1024;
3853
3955
  var MAX_WIZARD_WORKTREES = 3;
3854
3956
  var WIZARD_BRANCH_PREFIX = "wizard/implement-";
3855
3957
  function git(args) {
3856
3958
  return new Promise((resolve4, reject) => {
3857
- execFile("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
3959
+ execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
3858
3960
  if (err)
3859
3961
  return reject(
3860
3962
  new Error(
@@ -3879,7 +3981,7 @@ async function isWorkingTreeDirty(repoRoot) {
3879
3981
  return out.trim().length > 0;
3880
3982
  }
3881
3983
  async function pruneOldWorktrees(repoRoot) {
3882
- const dir = join9(stateDir(repoRoot), "worktrees");
3984
+ const dir = join10(stateDir(repoRoot), "worktrees");
3883
3985
  const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
3884
3986
  for (const slug of stale) {
3885
3987
  const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
@@ -3890,7 +3992,7 @@ async function pruneOldWorktrees(repoRoot) {
3890
3992
  "worktree",
3891
3993
  "remove",
3892
3994
  "--force",
3893
- join9(dir, slug)
3995
+ join10(dir, slug)
3894
3996
  ]);
3895
3997
  await git(["-C", repoRoot, "branch", "-D", branch]);
3896
3998
  } catch (err) {
@@ -3904,7 +4006,7 @@ async function pruneOldWorktrees(repoRoot) {
3904
4006
  async function createWorktree(repoRoot) {
3905
4007
  const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
3906
4008
  const dirSlug = branch.replace(/\//g, "-");
3907
- const path = join9(stateDir(repoRoot), "worktrees", dirSlug);
4009
+ const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
3908
4010
  await git(["-C", repoRoot, "worktree", "prune"]);
3909
4011
  await pruneOldWorktrees(repoRoot);
3910
4012
  await mkdir6(dirname7(path), { recursive: true });
@@ -3924,8 +4026,8 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
3924
4026
  } catch {
3925
4027
  return { ok: false, reason: `"${sourcePath}" does not exist` };
3926
4028
  }
3927
- const relPath = join9(ingestDir, basename2(source));
3928
- const dest = join9(worktreePath, relPath);
4029
+ const relPath = join10(ingestDir, basename2(source));
4030
+ const dest = join10(worktreePath, relPath);
3929
4031
  try {
3930
4032
  await mkdir6(dirname7(dest), { recursive: true });
3931
4033
  await copyFile(source, dest);
@@ -3943,7 +4045,7 @@ function hasEnvVar(content, name) {
3943
4045
  async function readEnvVar(worktreePath, name) {
3944
4046
  let content;
3945
4047
  try {
3946
- content = await readFile7(join9(worktreePath, ".env"), "utf8");
4048
+ content = await readFile8(join10(worktreePath, ".env"), "utf8");
3947
4049
  } catch (err) {
3948
4050
  if (err.code !== "ENOENT") throw err;
3949
4051
  return void 0;
@@ -3958,10 +4060,10 @@ async function readEnvVar(worktreePath, name) {
3958
4060
  return value;
3959
4061
  }
3960
4062
  async function writeSearchEnvValues(worktreePath, vars) {
3961
- const target = join9(worktreePath, ".env");
4063
+ const target = join10(worktreePath, ".env");
3962
4064
  let existing = "";
3963
4065
  try {
3964
- existing = await readFile7(target, "utf8");
4066
+ existing = await readFile8(target, "utf8");
3965
4067
  } catch (err) {
3966
4068
  if (err.code !== "ENOENT") throw err;
3967
4069
  }
@@ -3970,7 +4072,7 @@ async function writeSearchEnvValues(worktreePath, vars) {
3970
4072
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3971
4073
  const lines = missing.map(({ name, value }) => `${name}=${value}
3972
4074
  `).join("");
3973
- await writeFile6(target, existing + prefix + lines, "utf8");
4075
+ await writeFile7(target, existing + prefix + lines, "utf8");
3974
4076
  return missing.map((v) => v.name);
3975
4077
  }
3976
4078
  async function listChangedFiles(worktreePath) {
@@ -4031,13 +4133,13 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
4031
4133
 
4032
4134
  // src/lib/algoliaDocs.ts
4033
4135
  import { readFileSync, readdirSync, existsSync } from "node:fs";
4034
- import { dirname as dirname8, join as join10 } from "node:path";
4136
+ import { dirname as dirname8, join as join11 } from "node:path";
4035
4137
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4036
- var DOCS_SUBPATH = join10("docs", "algolia-sdk");
4138
+ var DOCS_SUBPATH = join11("docs", "algolia-sdk");
4037
4139
  function findDocsDir() {
4038
4140
  let dir = dirname8(fileURLToPath2(import.meta.url));
4039
4141
  for (; ; ) {
4040
- const candidate = join10(dir, DOCS_SUBPATH);
4142
+ const candidate = join11(dir, DOCS_SUBPATH);
4041
4143
  if (existsSync(candidate)) return candidate;
4042
4144
  const parent = dirname8(dir);
4043
4145
  if (parent === dir) return void 0;
@@ -4060,7 +4162,7 @@ function loadAlgoliaDoc(language) {
4060
4162
  );
4061
4163
  return "";
4062
4164
  }
4063
- return readFileSync(join10(docsDir, files[0]), "utf8").trim();
4165
+ return readFileSync(join11(docsDir, files[0]), "utf8").trim();
4064
4166
  }
4065
4167
  function getNamedDoc(name, language) {
4066
4168
  const docsDir = findDocsDir();
@@ -4068,7 +4170,7 @@ function getNamedDoc(name, language) {
4068
4170
  logger.warn("docs/algolia-sdk not found");
4069
4171
  return "";
4070
4172
  }
4071
- const file = join10(docsDir, `${name}-${language}.md`);
4173
+ const file = join11(docsDir, `${name}-${language}.md`);
4072
4174
  if (!existsSync(file)) {
4073
4175
  logger.warn({ name, language }, "named SDK reference not found");
4074
4176
  return "";
@@ -4076,6 +4178,7 @@ function getNamedDoc(name, language) {
4076
4178
  return readFileSync(file, "utf8").trim();
4077
4179
  }
4078
4180
  function getFrameworkSpecificDoc(frameworks) {
4181
+ if (frameworks.length === 0) return "";
4079
4182
  const fw = frameworks.map((f) => f.toLowerCase());
4080
4183
  if (fw.includes("vue") || fw.includes("nuxt")) {
4081
4184
  return loadAlgoliaDoc("vue");
@@ -4083,9 +4186,6 @@ function getFrameworkSpecificDoc(frameworks) {
4083
4186
  if (fw.includes("react") || fw.includes("next.js")) {
4084
4187
  return loadAlgoliaDoc("react");
4085
4188
  }
4086
- if (fw.includes("angular")) {
4087
- return loadAlgoliaDoc("angular");
4088
- }
4089
4189
  return loadAlgoliaDoc("js");
4090
4190
  }
4091
4191
 
@@ -4123,30 +4223,34 @@ var verificationOutputSchema = z27.object({
4123
4223
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
4124
4224
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
4125
4225
  var INGEST_DIR = ".algolia-wizard";
4126
- function detectUiFramework(language) {
4127
- const names = language.frameworks.map((f) => f.name.toLowerCase());
4128
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
4129
- if (names.some((n) => n.includes("react") || n.includes("next")))
4130
- return "React";
4131
- if (names.some((n) => n.includes("angular"))) return "Angular";
4132
- return "JavaScript";
4133
- }
4134
- function frameworksForDoc(framework) {
4135
- switch (framework) {
4136
- case "React":
4137
- return ["react"];
4138
- case "Vue":
4139
- return ["vue"];
4140
- case "Angular":
4141
- return ["angular"];
4142
- case "JavaScript":
4143
- return [];
4144
- }
4226
+ var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
4227
+ function lower(entries) {
4228
+ return entries.map((entry) => entry.name.toLowerCase());
4145
4229
  }
4146
- function publicEnvPrefix(language) {
4147
- const frameworkNames = language.frameworks.map(
4148
- (framework) => framework.name.toLowerCase()
4230
+ function isJsProject(language) {
4231
+ return lower(language.languages).some(
4232
+ (name) => JS_LANGUAGES.some((js) => name.includes(js))
4149
4233
  );
4234
+ }
4235
+ var UI_FRAMEWORKS = [
4236
+ { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
4237
+ { match: ["react", "next"], target: "React", doc: "react" },
4238
+ { match: ["angular"], target: "Angular" }
4239
+ ];
4240
+ function matchUiFramework(language) {
4241
+ const names = lower(language.frameworks);
4242
+ return UI_FRAMEWORKS.find(
4243
+ (ui) => ui.match.some((needle) => names.some((name) => name.includes(needle)))
4244
+ );
4245
+ }
4246
+ function searchUiTarget(language) {
4247
+ return matchUiFramework(language)?.target ?? language.frameworks[0]?.name ?? (isJsProject(language) ? "JavaScript" : "this project");
4248
+ }
4249
+ function frameworksForDoc(language) {
4250
+ return [matchUiFramework(language)?.doc ?? "js"];
4251
+ }
4252
+ function publicEnvPrefix(language) {
4253
+ const frameworkNames = lower(language.frameworks);
4150
4254
  if (frameworkNames.some((name) => name.includes("next"))) {
4151
4255
  return "NEXT_PUBLIC_";
4152
4256
  }
@@ -4159,7 +4263,7 @@ function publicEnvPrefix(language) {
4159
4263
  if (frameworkNames.some((name) => name.includes("vite"))) {
4160
4264
  return "VITE_";
4161
4265
  }
4162
- return "PUBLIC_";
4266
+ return isJsProject(language) ? "PUBLIC_" : "";
4163
4267
  }
4164
4268
  var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4165
4269
  var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
@@ -4200,6 +4304,7 @@ function baseInstructions(input) {
4200
4304
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4201
4305
  "Make minimal, idiomatic changes; do not touch unrelated code.",
4202
4306
  `Commands run through a shell on ${process.platform}. Write commands that work there.`,
4307
+ "Use the project's own tooling for every command \u2014 its package manager, task runner, and test/lint commands. Do not assume a JavaScript toolchain.",
4203
4308
  "runShell needs the developer to approve each command, so give every call a clear `explanation` naming what it does and any side effect. If a command is rejected, do not retry it \u2014 take a different approach or report the limitation."
4204
4309
  ];
4205
4310
  }
@@ -4220,12 +4325,21 @@ function sourceSpecificInstructions(input) {
4220
4325
  generated: [
4221
4326
  "No real data source exists; use sample records for each confirmed entity.",
4222
4327
  "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.",
4223
- "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.",
4328
+ "In the script, read and parse each returned file path at runtime using your language's standard JSON support, instead of inlining the records as literals.",
4224
4329
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4225
4330
  ]
4226
4331
  };
4227
4332
  return byLine[input.ingestionSource];
4228
4333
  }
4334
+ function algoliaClientDoc(input) {
4335
+ const doc = getNamedDoc("save-records", "js");
4336
+ if (!doc) return [];
4337
+ if (isJsProject(input.language)) return [doc];
4338
+ return [
4339
+ "The reference below is written in JavaScript. Use it for the method names, arguments, and record shape, then translate to this project's language and its official Algolia client:",
4340
+ doc
4341
+ ];
4342
+ }
4229
4343
  function ingestionInstructions(input) {
4230
4344
  return [
4231
4345
  ...input.confirmed && input.confirmed.length ? [
@@ -4233,10 +4347,10 @@ function ingestionInstructions(input) {
4233
4347
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
4234
4348
  `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.`,
4235
4349
  `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
4236
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4350
+ "Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
4237
4351
  "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.",
4238
- getNamedDoc("save-records", "js"),
4239
- 'Install the Algolia client via runShell and add it to package.json "dependencies" with a valid version range, so the dependency is not just installed ad hoc.',
4352
+ ...algoliaClientDoc(input),
4353
+ "Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
4240
4354
  'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
4241
4355
  "The summary should be extremely concise.",
4242
4356
  ...sourceSpecificInstructions(input)
@@ -4244,20 +4358,31 @@ function ingestionInstructions(input) {
4244
4358
  ];
4245
4359
  }
4246
4360
  function searchInstructions(input) {
4247
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4361
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.language));
4248
4362
  return [
4249
4363
  "Implement an in-app Algolia search experience.",
4250
- `Build the search UI for ${input.uiFramework}.`,
4251
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4252
- doc,
4253
- `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.`,
4364
+ `Build the search UI for ${input.searchUiTarget}.`,
4365
+ ...doc ? [
4366
+ "Follow the Algolia SDK reference below for client setup and search UI wiring; prefer it over prior knowledge:",
4367
+ doc
4368
+ ] : [
4369
+ "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
4370
+ ],
4371
+ `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 search input and results list against the target index.`,
4254
4372
  "If a search box already exists, replace it with yours.",
4255
- `Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Do not fabricate the index name.`,
4373
+ `Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4374
+ "Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4375
+ // The key is provisioned only after verification passes, so the agent never
4376
+ // sees one. It must also leave .env alone: the wizard reads that file to
4377
+ // decide whether a key already exists, and an agent-invented value there
4378
+ // would be reused as if it were real.
4256
4379
  `Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
4257
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4258
- "The search-only key is safe to expose client-side.",
4259
- 'Install any Algolia/InstantSearch packages you import via runShell, and add them to package.json "dependencies" with a valid version range.',
4260
- "Match the styles of the application as closely as possible",
4380
+ // Not the agent's to rename: the wizard writes these exact names into
4381
+ // ".env" right after this step, so a renamed prefix would leave the code
4382
+ // reading a var the wizard never wrote.
4383
+ `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4384
+ "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4385
+ "Match the styles of the application as closely as possible.",
4261
4386
  "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."
4262
4387
  ];
4263
4388
  }
@@ -4486,7 +4611,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4486
4611
  ingestDir: INGEST_DIR,
4487
4612
  ingestionSource,
4488
4613
  uploadFilePath,
4489
- uiFramework: detectUiFramework(language)
4614
+ searchUiTarget: searchUiTarget(language)
4490
4615
  };
4491
4616
  const summaries = [];
4492
4617
  if (uploadWarning) summaries.push(uploadWarning);
@@ -4692,6 +4817,14 @@ ${detail}` : ""}`
4692
4817
  if (written.length > 0) {
4693
4818
  summaries.push(`Wrote ${written.join(", ")} to .env.`);
4694
4819
  }
4820
+ const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
4821
+ if (ignored === "added") {
4822
+ summaries.push("Added .env to .gitignore.");
4823
+ } else if (ignored === "tracked") {
4824
+ summaries.push(
4825
+ '\u26A0\uFE0F .env is tracked by git, so a .gitignore rule cannot un-stage it. Run "git rm --cached .env" before committing, or the credentials go into history.'
4826
+ );
4827
+ }
4695
4828
  const stale = [];
4696
4829
  for (const v of resolvedSearchEnvVars) {
4697
4830
  if (written.includes(v.name)) continue;
@@ -5064,7 +5197,7 @@ function parseCliArgs(argv) {
5064
5197
 
5065
5198
  // src/lib/resetState.ts
5066
5199
  import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
5067
- import { join as join11 } from "node:path";
5200
+ import { join as join13 } from "node:path";
5068
5201
  var KEEP = ["wizard.log"];
5069
5202
  async function resetProjectState() {
5070
5203
  const dir = stateDir();
@@ -5078,7 +5211,7 @@ async function resetProjectState() {
5078
5211
  const targets = entries.filter((name) => !KEEP.includes(name));
5079
5212
  await Promise.all(
5080
5213
  targets.map(
5081
- (name) => rm2(join11(dir, name), { recursive: true, force: true })
5214
+ (name) => rm2(join13(dir, name), { recursive: true, force: true })
5082
5215
  )
5083
5216
  );
5084
5217
  return { dir, removed: targets };
@@ -10,8 +10,8 @@ InstantSearch).
10
10
  - `algoliasearch` v5 (install `^5`)
11
11
  - `react-instantsearch` v7 (install `^7`)
12
12
  - `vue-instantsearch` v4 (install `^4`)
13
- - `instantsearch.js` v4 (install `^4`) — also the recommended choice for Angular
14
- - `angular-instantsearch` DEPRECATED/archived (Sep 2024); do not use, prefer `instantsearch.js`
13
+ - `instantsearch.js` v4 (install `^4`) — also the choice for any framework without
14
+ its own flavor (Angular, Svelte); `angular-instantsearch` is archived, never use it
15
15
 
16
16
  Always install the **latest stable** within the major (use a caret range like `^5` /
17
17
  `^7`); never pin an exact patch.
@@ -30,7 +30,7 @@ Always install the **latest stable** within the major (use a caret range like `^
30
30
 
31
31
  ## Files
32
32
 
33
- - `instantsearch-setup.md` — framework-specific InstantSearch wiring (React, Vue,
34
- Angular, vanilla). Use this for the in-app search UI.
33
+ - `instantsearch-setup-*.md` — InstantSearch wiring per framework (React, Vue, and
34
+ vanilla for everything else). Use this for the in-app search UI.
35
35
  - `search-single-index.md` — direct/manual search via the core client
36
36
  (`searchSingleIndex`), for cases where InstantSearch is not used.
@@ -31,3 +31,14 @@ search.addWidgets([
31
31
 
32
32
  search.start()
33
33
  ```
34
+
35
+ ## Frameworks without their own flavor
36
+
37
+ Only React and Vue have a maintained InstantSearch wrapper. For any other
38
+ framework — Angular, Svelte, Solid — drive `instantsearch.js` directly from a
39
+ component, mounting widgets against element refs and calling `search.dispose()`
40
+ on teardown.
41
+
42
+ Do NOT install `angular-instantsearch` or a community `*-instantsearch` package:
43
+ `angular-instantsearch` is deprecated and archived (last release v4.4.3, Sep 2024),
44
+ is incompatible with Angular v13+, and receives no security fixes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,80 +0,0 @@
1
- # InstantSearch setup
2
-
3
- Install `instantsearch.js` (v4)
4
-
5
- ## Search client: always use the lite client
6
-
7
- Import the client from `algoliasearch/lite` and alias it to `algoliasearch`
8
-
9
- ```ts
10
- import { liteClient as algoliasearch } from 'algoliasearch/lite'
11
-
12
- // Instantiate ONCE, outside components, with a stable reference.
13
- const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
14
- ```
15
-
16
- ## Angular (use `instantsearch.js`)
17
-
18
- Do NOT use `angular-instantsearch`: it is deprecated and archived (last release
19
- v4.4.3, Sep 2024), not compatible with Ivy / modern Angular (v13+), and receives no
20
- security fixes. Algolia recommends driving `instantsearch.js` directly from an Angular
21
- component instead.
22
-
23
- ```ts
24
- import {
25
- Component,
26
- ElementRef,
27
- OnDestroy,
28
- OnInit,
29
- ViewChild,
30
- } from '@angular/core'
31
- import { liteClient as algoliasearch } from 'algoliasearch/lite'
32
- import instantsearch from 'instantsearch.js'
33
- import { searchBox, hits } from 'instantsearch.js/es/widgets'
34
-
35
- @Component({
36
- selector: 'app-search',
37
- template: `<div #searchbox></div>
38
- <div #hits></div>`,
39
- })
40
- export class SearchComponent implements OnInit, OnDestroy {
41
- @ViewChild('searchbox', { static: true }) searchbox!: ElementRef
42
- @ViewChild('hits', { static: true }) hits!: ElementRef
43
-
44
- private search = instantsearch({
45
- indexName: 'INDEX_NAME',
46
- searchClient: algoliasearch(APP_ID, SEARCH_ONLY_KEY),
47
- })
48
-
49
- ngOnInit() {
50
- this.search.addWidgets([
51
- searchBox({ container: this.searchbox.nativeElement }),
52
- hits({ container: this.hits.nativeElement }),
53
- ])
54
- this.search.start()
55
- }
56
-
57
- ngOnDestroy() {
58
- this.search.dispose()
59
- }
60
- }
61
- ```
62
-
63
- ## Vanilla (`instantsearch.js` v4)
64
-
65
- ```js
66
- import { liteClient as algoliasearch } from 'algoliasearch/lite'
67
- import instantsearch from 'instantsearch.js'
68
- import { searchBox, hits } from 'instantsearch.js/es/widgets'
69
-
70
- const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
71
-
72
- const search = instantsearch({ indexName: 'INDEX_NAME', searchClient })
73
-
74
- search.addWidgets([
75
- searchBox({ container: '#searchbox' }),
76
- hits({ container: '#hits' }),
77
- ])
78
-
79
- search.start()
80
- ```