@algolia/wizard 0.35.0-rc.126.250 → 0.35.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.
Files changed (2) hide show
  1. package/dist/main.js +131 -261
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -253,7 +253,6 @@ var useWizard = create((set, get) => ({
253
253
  cliOutput: [],
254
254
  targetIndex: null,
255
255
  writtenFiles: [],
256
- approvedCommands: /* @__PURE__ */ new Set(),
257
256
  logs: [],
258
257
  error: null,
259
258
  inputReq: null,
@@ -349,10 +348,6 @@ var useWizard = create((set, get) => ({
349
348
  setTargetIndex: (index) => set({ targetIndex: index }),
350
349
  recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
351
350
  clearWrittenFiles: () => set({ writtenFiles: [] }),
352
- isCommandApproved: (command, cwd) => get().approvedCommands.has(`${cwd}\0${command}`),
353
- approveCommand: (command, cwd) => set((s) => ({
354
- approvedCommands: new Set(s.approvedCommands).add(`${cwd}\0${command}`)
355
- })),
356
351
  logStart: (kind, name, input) => {
357
352
  const id = nanoid();
358
353
  set((s) => ({
@@ -401,7 +396,6 @@ var useWizard = create((set, get) => ({
401
396
  cliOutput: [],
402
397
  targetIndex: null,
403
398
  writtenFiles: [],
404
- approvedCommands: /* @__PURE__ */ new Set(),
405
399
  logs: [],
406
400
  error: null,
407
401
  inputReq: null,
@@ -1652,8 +1646,8 @@ var selectIndexStep = async (ctx) => {
1652
1646
  };
1653
1647
 
1654
1648
  // src/lib/agent.ts
1655
- import { ToolLoopAgent, hasToolCall, Output as Output3 } from "ai";
1656
- import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
1649
+ import { ToolLoopAgent, hasToolCall, Output as Output2 } from "ai";
1650
+ import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1657
1651
  import "zod";
1658
1652
 
1659
1653
  // src/lib/tools/index.ts
@@ -2397,8 +2391,7 @@ function searchFilesTool(ctx) {
2397
2391
  }
2398
2392
 
2399
2393
  // src/lib/tools/runShell.ts
2400
- import { tool as tool8, generateText, Output } from "ai";
2401
- import { createAnthropic } from "@ai-sdk/anthropic";
2394
+ import { tool as tool8 } from "ai";
2402
2395
  import z15 from "zod";
2403
2396
  import { relative as relative4 } from "node:path";
2404
2397
 
@@ -2507,10 +2500,8 @@ function runShell(command, opts) {
2507
2500
  // src/lib/tools/runShell.ts
2508
2501
  function storeApproval(root) {
2509
2502
  return async (req) => {
2510
- const store = useWizard.getState();
2511
- if (store.isCommandApproved(req.command, req.cwd)) return "approve";
2512
2503
  const rel = relative4(root, req.cwd);
2513
- const answer = await store.requestUserInput({
2504
+ const answer = await useWizard.getState().requestUserInput({
2514
2505
  prompt: "Run this command?",
2515
2506
  promptType: "commandApproval",
2516
2507
  options: [],
@@ -2519,195 +2510,20 @@ function storeApproval(root) {
2519
2510
  cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
2520
2511
  }
2521
2512
  });
2522
- if (answer !== "approve") return "reject";
2523
- store.approveCommand(req.command, req.cwd);
2524
- return "approve";
2513
+ return answer === "approve" ? "approve" : "reject";
2525
2514
  };
2526
2515
  }
2527
2516
  var EXPLORATORY_COMMANDS = /* @__PURE__ */ new Set(["ls", "find", "tree", "dir"]);
2528
- function commandSegments(command) {
2529
- return command.split(/&&|;|\|/).map((segment) => segment.trim());
2530
- }
2531
2517
  function isExploratoryCommand(command) {
2532
- return commandSegments(command).map((segment) => segment.split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
2533
- }
2534
- var READ_ONLY_BINARIES = /* @__PURE__ */ new Set([
2535
- "cat",
2536
- "head",
2537
- "tail",
2538
- "wc",
2539
- "pwd",
2540
- "echo",
2541
- "date",
2542
- "whoami",
2543
- "hostname",
2544
- "uname",
2545
- "which",
2546
- "file",
2547
- "stat",
2548
- "grep",
2549
- "egrep",
2550
- "fgrep",
2551
- "rg",
2552
- "diff"
2553
- ]);
2554
- var READ_ONLY_NO_ARGS_BINARIES = /* @__PURE__ */ new Set(["env", "printenv"]);
2555
- var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
2556
- "status",
2557
- "log",
2558
- "diff",
2559
- "show",
2560
- "describe",
2561
- "blame",
2562
- "ls-files",
2563
- "rev-parse",
2564
- "cat-file",
2565
- "shortlog",
2566
- "ls-remote"
2567
- ]);
2568
- var VERSION_CHECK_BINARIES = /* @__PURE__ */ new Set([
2569
- "node",
2570
- "tsc",
2571
- "npm",
2572
- "pnpm",
2573
- "yarn",
2574
- "python",
2575
- "python3",
2576
- "ruby",
2577
- "go",
2578
- "cargo",
2579
- "rustc",
2580
- "php",
2581
- "composer",
2582
- "java",
2583
- "mvn",
2584
- "gradle",
2585
- "bundle",
2586
- "git"
2587
- ]);
2588
- var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v", "-V"]);
2589
- var FD_DUP_REDIRECT = /\d*>&\d+/g;
2590
- function stripQuoted(segment) {
2591
- let result = "";
2592
- let quote = null;
2593
- let i = 0;
2594
- while (i < segment.length) {
2595
- const char = segment[i];
2596
- if (quote === "'") {
2597
- if (char === "'") quote = null;
2598
- i++;
2599
- } else if (char === "\\") {
2600
- i += 2;
2601
- } else if (quote) {
2602
- if (char === quote) quote = null;
2603
- i++;
2604
- } else if (char === '"' || char === "'") {
2605
- quote = char;
2606
- i++;
2607
- } else {
2608
- result += char;
2609
- i++;
2610
- }
2611
- }
2612
- return quote === null ? result : segment;
2613
- }
2614
- function hasFileRedirect(segment) {
2615
- return stripQuoted(segment.replace(FD_DUP_REDIRECT, "")).includes(">");
2616
- }
2617
- function hasShellInjectionRisk(segment) {
2618
- if (segment.includes("$(") || segment.includes("<(") || segment.includes("`")) {
2619
- return true;
2620
- }
2621
- return stripQuoted(segment.replace(FD_DUP_REDIRECT, "")).includes("&");
2622
- }
2623
- function fastPathSegmentSafety(segment) {
2624
- if (!segment) return true;
2625
- if (hasFileRedirect(segment)) return false;
2626
- if (hasShellInjectionRisk(segment)) return false;
2627
- const [cmd0, ...rest] = segment.split(/\s+/);
2628
- if (cmd0 === void 0) return true;
2629
- if (VERSION_CHECK_BINARIES.has(cmd0) && rest.length === 1 && VERSION_FLAGS.has(rest[0])) {
2630
- return true;
2631
- }
2632
- if (READ_ONLY_BINARIES.has(cmd0)) return true;
2633
- if (READ_ONLY_NO_ARGS_BINARIES.has(cmd0)) {
2634
- return rest.length === 0 ? true : void 0;
2635
- }
2636
- if (cmd0 === "git") {
2637
- return GIT_READ_ONLY_SUBCOMMANDS.has(rest[0] ?? "") ? true : void 0;
2638
- }
2639
- return void 0;
2640
- }
2641
- function fastPathSafety(command) {
2642
- const results = commandSegments(command).map(fastPathSegmentSafety);
2643
- if (results.some((r) => r === false)) return false;
2644
- if (results.every((r) => r === true)) return true;
2645
- return void 0;
2646
- }
2647
- var CLASSIFIER_MODEL = "claude-haiku-4-5";
2648
- var commandSafetySchema = z15.object({
2649
- safe: z15.boolean(),
2650
- reason: z15.string().describe("One short sentence explaining the verdict.")
2651
- });
2652
- function defaultCreateModel() {
2653
- const token = getAuthToken();
2654
- if (!token) {
2655
- throw new Error("Not authenticated: no user token available");
2656
- }
2657
- return createAnthropic({
2658
- apiKey: token,
2659
- baseURL: PROXY_BASE_URL,
2660
- fetch: proxyFetch
2661
- });
2518
+ return command.split(/&&|;|\|/).map((segment) => segment.trim().split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
2662
2519
  }
2663
- function approvedCommandHistory(approvedCommands) {
2664
- return Array.from(approvedCommands).map((entry) => {
2665
- const sep2 = entry.indexOf("\0");
2666
- return { cwd: entry.slice(0, sep2), command: entry.slice(sep2 + 1) };
2667
- });
2668
- }
2669
- async function classifyCommandSafety(createModel, command, cwd, explanation, approvedHistory) {
2670
- try {
2671
- const anthropic = createModel();
2672
- const { output } = await generateText({
2673
- model: anthropic(CLASSIFIER_MODEL),
2674
- temperature: 0,
2675
- output: Output.object({ schema: commandSafetySchema }),
2676
- prompt: [
2677
- "A coding agent wants to run this shell command in a user's project without asking for approval first. The command may be in any programming language or ecosystem.",
2678
- "Judge it SAFE only if it cannot modify, delete, move, rename, or install anything the project cares about, cannot push/publish/deploy/commit anything, and cannot make a network request that changes remote state.",
2679
- "Three broad categories are safe, and most commands you will see fall into one of them:",
2680
- "(1) Reporting or listing existing state, with no side effect \u2014 e.g. `git status`, `npm ls`, `npm outdated`, `pip show`, `pip freeze`, `cargo tree`, `docker ps`, `docker images`, `kubectl get pods`, `terraform state list`. This includes a read-only GET-style query to a remote registry or API that only fetches public metadata and changes nothing remote (e.g. `npm view <package>`, `pip index versions <package>`, `curl` with no -X/--request other than GET and no -d/--data) \u2014 safe because nothing changes, not because it stays local.",
2681
- '(2) A "preview" or "check" mode that reports what a change WOULD do, or reports a problem, without making the change. This is a general pattern, not a fixed list \u2014 ANY command that takes a dry-run/check/plan/validate/diff/list-only flag is safe when that flag is present, regardless of whether the same flag would normally appear on a "safe" kind of command: `terraform plan`, `terraform validate`, `terraform fmt -check`, `black --check`, `prettier --check`, `isort --check`, `gofmt -l` (list-only), `stylelint` with no `--fix`, and equally `git push --dry-run`, `npm publish --dry-run`, `kubectl apply --dry-run=client` \u2014 these last three are otherwise-unsafe operations (push, publish, cluster changes) that the dry-run flag turns into a report. The corresponding command WITHOUT that flag (e.g. `terraform apply`, `black .`, `gofmt -w`, `git push`) is a different, unsafe command \u2014 the flag is what makes the difference, for any tool, not just the ones named here. This cuts both ways, so do not assume a bare invocation is the safe one: `cargo fmt`, `black`, `prettier`, `isort`, `rustfmt`, and `terraform fmt` all REWRITE FILES by default and need an explicit check/diff/dry-run flag to become safe, while `gofmt`, `eslint`, `stylelint`, `rubocop`, and `ruff check` default to a safe check-only mode and need an explicit fix/write flag to become unsafe \u2014 the same-looking bare command is safe for one group and unsafe for the other, so judge each by what its own flags actually say, not by resemblance to a tool you already judged.',
2682
- "(3) Running the project's own tests, type checker, linter, or build/compile step (e.g. `npm run build`, `yarn build`, `vite build`, `webpack`, `tsc`, `cargo build`, `go build`, `mvn compile`), as long as it is not passed an autofix/write/update flag (e.g. --fix, -u, --write, rubocop -a) \u2014 this holds even though the point of a build step is to write compiled output to a build/dist/target directory inside the project (e.g. a target/, build/, dist/, or __pycache__ directory): writing that output does not make the command unsafe. It stops being safe the moment the command chain goes past building \u2014 a step that also deploys, publishes, uploads, or pushes the build (e.g. `next build && vercel deploy`, `npm run build && npm publish`) is unsafe for that later segment even though the build segment itself is fine; judge each chained segment on its own, same as elsewhere in this list.",
2683
- "Deleting or removing anything is unsafe, even something in a cache or build directory (e.g. `rm -rf __pycache__`, `cargo clean`, `git clean`), and even if the command otherwise fits one of the three categories above. Installing, uninstalling, or upgrading a dependency, changing a database schema, or writing to a path outside the project is also unsafe. A command chained with && or ; is only safe if every part of the chain is safe on its own.",
2684
- "One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe.",
2685
- ...approvedHistory.length > 0 ? [
2686
- "The user has already explicitly approved these exact commands earlier in this session (working directory in parentheses, then the command):",
2687
- approvedHistory.map((h) => `- (${h.cwd}) ${h.command}`).join("\n"),
2688
- "If the new command performs the same action and side-effect profile as one of these \u2014 differing only in a trivial way, such as a different file path, package name, or argument value that does not change what kind of action it is \u2014 judge it SAFE on that precedent, even if it would not otherwise fit categories (1)-(3) above. Do not stretch this to a command that merely shares a binary name, or superficially resembles one on the list while doing something riskier or of a different kind (e.g. an approved `rm build/tmp.log` does not license a new `rm -rf src/`, and an approved `git push origin feature-x` does not license `git push --force`)."
2689
- ] : [],
2690
- `Command: ${command}`,
2691
- `Working directory: ${cwd}`,
2692
- `Stated purpose: ${explanation}`
2693
- ].join("\n")
2694
- });
2695
- if (!output.safe) {
2696
- logger.info(
2697
- { command, reason: output.reason },
2698
- "runShell: classifier judged command unsafe, requiring approval"
2699
- );
2700
- }
2701
- return output.safe;
2702
- } catch (err) {
2703
- logger.warn(
2704
- { err, command },
2705
- "runShell: command safety classifier failed, requiring approval"
2706
- );
2707
- return false;
2520
+ async function approveAndRun(ctx, command, cwd, explanation) {
2521
+ const decision = await ctx.shell.approve({ command, cwd, explanation });
2522
+ if (decision === "reject") {
2523
+ ctx.shell.executions.push({ command, cwd, approved: false });
2524
+ logger.info({ command }, "runShell: user rejected the command");
2525
+ return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2708
2526
  }
2709
- }
2710
- async function runAndRecord(ctx, command, cwd) {
2711
2527
  useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
2712
2528
  const env = await ctx.shell.env().catch((err) => {
2713
2529
  logger.warn({ err, command }, "runShell: could not resolve command env");
@@ -2743,18 +2559,9 @@ async function runAndRecord(ctx, command, cwd) {
2743
2559
  output: run2.output
2744
2560
  };
2745
2561
  }
2746
- async function approveAndRun(ctx, command, cwd, explanation) {
2747
- const decision = await ctx.shell.approve({ command, cwd, explanation });
2748
- if (decision === "reject") {
2749
- ctx.shell.executions.push({ command, cwd, approved: false });
2750
- logger.info({ command }, "runShell: user rejected the command");
2751
- return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2752
- }
2753
- return runAndRecord(ctx, command, cwd);
2754
- }
2755
- function runShellTool(ctx, createModel = defaultCreateModel) {
2562
+ function runShellTool(ctx) {
2756
2563
  return tool8({
2757
- description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. Do not use this to find or read files \u2014 use listFiles, searchFiles, and readFile instead of ls/find/cat/head/tail/grep/rg. This tool refuses ls/find/tree/dir outright; a read command like cat is not refused (some read-only commands run without approval, see below), but it's still the wrong tool for reading a file \u2014 the dedicated tools exist for that and won't count against this tool's command budget. The user approves any command that could change the project before it runs, so write a clear `explanation`. A command judged read-only (inspection, or running tests/typecheck/lint without an autofix flag, in any language) runs immediately without approval. If the user rejects a command, do not retry it \u2014 propose a different approach.",
2564
+ description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. To inspect the project, use listFiles or searchFiles instead of ls/find \u2014 this tool refuses those. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
2758
2565
  inputSchema: z15.object({
2759
2566
  command: z15.string().describe(
2760
2567
  "The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
@@ -2773,29 +2580,9 @@ function runShellTool(ctx, createModel = defaultCreateModel) {
2773
2580
  const resolved2 = resolveInRoot(ctx, cwd ?? ".");
2774
2581
  if (!resolved2.ok) return resolved2.error;
2775
2582
  if (isExploratoryCommand(command)) {
2776
- return "Refused: use listFiles or searchFiles to find files, and readFile to read one, instead of ls/find/tree/dir.";
2583
+ return "Refused: use listFiles or searchFiles to inspect the project instead of ls/find/tree.";
2777
2584
  }
2778
2585
  logger.info({ command, cwd: resolved2.target }, "called runShell tool");
2779
- const fast = fastPathSafety(command);
2780
- let isSafe = fast;
2781
- if (isSafe === void 0) {
2782
- const store = useWizard.getState();
2783
- isSafe = store.isCommandApproved(command, resolved2.target);
2784
- if (!isSafe) {
2785
- isSafe = await classifyCommandSafety(
2786
- createModel,
2787
- command,
2788
- resolved2.target,
2789
- explanation,
2790
- approvedCommandHistory(store.approvedCommands)
2791
- );
2792
- }
2793
- }
2794
- if (isSafe) {
2795
- return serializePrompt(
2796
- () => runAndRecord(ctx, command, resolved2.target)
2797
- );
2798
- }
2799
2586
  return serializePrompt(
2800
2587
  () => approveAndRun(ctx, command, resolved2.target, explanation)
2801
2588
  );
@@ -2845,8 +2632,8 @@ function reviewScriptTool(ctx) {
2845
2632
  }
2846
2633
 
2847
2634
  // src/lib/tools/generateRecord.ts
2848
- import { tool as tool10, generateText as generateText2, Output as Output2, NoObjectGeneratedError } from "ai";
2849
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
2635
+ import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
2636
+ import { createAnthropic } from "@ai-sdk/anthropic";
2850
2637
  import { nanoid as nanoid2 } from "nanoid";
2851
2638
  import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
2852
2639
  import { dirname as dirname5 } from "node:path";
@@ -2856,18 +2643,18 @@ var RECORD_MODEL = "claude-haiku-4-5";
2856
2643
  var MAX_RECORDS = 100;
2857
2644
  var BATCH_SIZE = 10;
2858
2645
  var MAX_BATCH_ATTEMPTS = 3;
2859
- function defaultCreateModel2() {
2646
+ function defaultCreateModel() {
2860
2647
  const token = getAuthToken();
2861
2648
  if (!token) {
2862
2649
  throw new Error("Not authenticated: no user token available");
2863
2650
  }
2864
- return createAnthropic2({
2651
+ return createAnthropic({
2865
2652
  apiKey: token,
2866
2653
  baseURL: PROXY_BASE_URL,
2867
2654
  fetch: proxyFetch
2868
2655
  });
2869
2656
  }
2870
- function generateRecordTool(ctx, createModel = defaultCreateModel2) {
2657
+ function generateRecordTool(ctx, createModel = defaultCreateModel) {
2871
2658
  return tool10({
2872
2659
  description: "Generate realistic sample records for an entity and write them to a JSON file. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
2873
2660
  inputSchema: z17.object({
@@ -2888,9 +2675,9 @@ function generateRecordTool(ctx, createModel = defaultCreateModel2) {
2888
2675
  let lastError;
2889
2676
  for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt++) {
2890
2677
  try {
2891
- const { output } = await generateText2({
2678
+ const { output } = await generateText({
2892
2679
  model: anthropic(RECORD_MODEL),
2893
- output: Output2.object({
2680
+ output: Output.object({
2894
2681
  schema: z17.object({
2895
2682
  records: z17.array(recordSchema).length(batchCount)
2896
2683
  })
@@ -3055,7 +2842,7 @@ async function runAgentAttempt(req, attempt) {
3055
2842
  if (!token) {
3056
2843
  throw new Error("Not authenticated: no user token available");
3057
2844
  }
3058
- const anthropic = createAnthropic3({
2845
+ const anthropic = createAnthropic2({
3059
2846
  apiKey: token,
3060
2847
  baseURL: PROXY_BASE_URL,
3061
2848
  fetch: proxyFetch
@@ -3092,7 +2879,7 @@ async function runAgentAttempt(req, attempt) {
3092
2879
  }
3093
2880
  };
3094
2881
  }),
3095
- output: Output3.object({ schema: req.outputSchema }),
2882
+ output: Output2.object({ schema: req.outputSchema }),
3096
2883
  tools: createTools(toolContext, {
3097
2884
  output: req.outputSchema,
3098
2885
  tools: req.tools
@@ -3275,7 +3062,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3275
3062
  // package.json
3276
3063
  var package_default = {
3277
3064
  name: "@algolia/wizard",
3278
- version: "0.35.0-rc.126.250",
3065
+ version: "0.35.0",
3279
3066
  description: "Magically implement Algolia functionality in your codebase",
3280
3067
  type: "module",
3281
3068
  engines: {
@@ -3681,7 +3468,6 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
3681
3468
 
3682
3469
  // src/actions/implement.ts
3683
3470
  import z29 from "zod";
3684
- import { mkdir as mkdir7 } from "node:fs/promises";
3685
3471
  import { join as join10, relative as relative6 } from "node:path";
3686
3472
 
3687
3473
  // src/lib/git.ts
@@ -3971,7 +3757,7 @@ function algoliaClientDoc(input) {
3971
3757
  function ingestionInstructions(input) {
3972
3758
  return [
3973
3759
  ...input.confirmed && input.confirmed.length ? [
3974
- `Create an ingestion script under "${input.ingestDir}/" at the repo root. That directory already exists \u2014 writeFile creates any nested path itself, so never run a shell command just to create a directory.`,
3760
+ `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3975
3761
  `Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
3976
3762
  `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.`,
3977
3763
  `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.`,
@@ -4206,9 +3992,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4206
3992
  const targetIndex = selected?.selection;
4207
3993
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4208
3994
  await assertGitRepoWithHead(repoRoot);
4209
- if (useCases.includes("ingestion")) {
4210
- await mkdir7(join10(repoRoot, INGEST_DIR), { recursive: true });
4211
- }
4212
3995
  const normalized = normalizeFindingPaths(findings);
4213
3996
  const confirmed2 = normalized.confirmedEntities;
4214
3997
  const searchLocation = normalized.searchImplementationAnalysis;
@@ -4529,7 +4312,12 @@ var defaultWorkflow = {
4529
4312
  id: "project-scan",
4530
4313
  title: "project scan",
4531
4314
  outputSchema: projectScanSchema,
4532
- tips: ["apps-and-indices", "records-are-json", "speed"],
4315
+ tips: [
4316
+ "apps-and-indices",
4317
+ "records-are-json",
4318
+ "speed",
4319
+ "searchable-attributes"
4320
+ ],
4533
4321
  run: (ctx) => {
4534
4322
  ctx.notify({
4535
4323
  messages: [
@@ -4565,7 +4353,7 @@ var defaultWorkflow = {
4565
4353
  id: "ingestion",
4566
4354
  title: "ingest records",
4567
4355
  outputSchema: implementSchema,
4568
- tips: ["records-are-json", "searchable-attributes", "facets"],
4356
+ tips: ["facets", "test-relevance", "search-analytics", "recommend"],
4569
4357
  run: (ctx) => {
4570
4358
  ctx.notify({
4571
4359
  messages: [
@@ -4586,13 +4374,7 @@ var defaultWorkflow = {
4586
4374
  id: "search",
4587
4375
  title: "create search ui",
4588
4376
  outputSchema: implementSchema,
4589
- tips: [
4590
- "test-relevance",
4591
- "search-analytics",
4592
- "recommend",
4593
- "searchable-attributes",
4594
- "facets"
4595
- ],
4377
+ tips: ["typo-tolerance", "synonyms", "multi-index-search", "rules"],
4596
4378
  run: (ctx) => {
4597
4379
  ctx.notify({
4598
4380
  messages: ["Building your Algolia search experience\u2026"]
@@ -5096,7 +4878,7 @@ function Logs() {
5096
4878
  }
5097
4879
 
5098
4880
  // src/ui/Tips.tsx
5099
- import { useEffect as useEffect3, useState as useState8 } from "react";
4881
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState8 } from "react";
5100
4882
  import { Box as Box18, Text as Text18 } from "ink";
5101
4883
  import terminalLink from "terminal-link";
5102
4884
 
@@ -5120,7 +4902,7 @@ function highlightJson(json) {
5120
4902
  return /* @__PURE__ */ jsx15(Text17, { color: "yellow", children: part }, i);
5121
4903
  }
5122
4904
  if (/^[{}[\]:,]$/.test(part)) {
5123
- return /* @__PURE__ */ jsx15(Text17, { dimColor: true, children: part }, i);
4905
+ return /* @__PURE__ */ jsx15(Text17, { color: COLORS.dim, children: part }, i);
5124
4906
  }
5125
4907
  return /* @__PURE__ */ jsx15(Text17, { children: part }, i);
5126
4908
  });
@@ -5253,6 +5035,70 @@ var tips = [
5253
5035
  }
5254
5036
  ]
5255
5037
  },
5038
+ {
5039
+ id: "typo-tolerance",
5040
+ title: "Your users will misspell things. Algolia already handles it.",
5041
+ chunks: [
5042
+ {
5043
+ type: "text",
5044
+ value: 'Typo tolerance is on out of the box. Someone searching "rstaurnt" still gets restaurants.'
5045
+ },
5046
+ {
5047
+ type: "text",
5048
+ value: "Exact matches always rank above one-typo matches, which rank above two."
5049
+ }
5050
+ ]
5051
+ },
5052
+ {
5053
+ id: "synonyms",
5054
+ title: "Synonyms teach the engine your words",
5055
+ chunks: [
5056
+ {
5057
+ type: "text",
5058
+ value: "Pants, trousers, slacks: one synonym entry and all three find each other."
5059
+ },
5060
+ {
5061
+ type: "text",
5062
+ value: 'One-way synonyms go further. "tablet" can find iPad without iPad returning every tablet.'
5063
+ },
5064
+ {
5065
+ type: "text",
5066
+ value: 'And a placeholder like "Case for iPhone <model>" covers a whole product line without duplicating records.'
5067
+ }
5068
+ ]
5069
+ },
5070
+ {
5071
+ id: "multi-index-search",
5072
+ title: "One search box, many indices",
5073
+ chunks: [
5074
+ {
5075
+ type: "text",
5076
+ value: "Federated search queries several indices at once, so a single dropdown can show products, categories and articles side by side."
5077
+ },
5078
+ {
5079
+ type: "text",
5080
+ value: "Each source gets its own layout and its own hit count: 4 categories, 8 products, whatever fits."
5081
+ }
5082
+ ]
5083
+ },
5084
+ {
5085
+ id: "rules",
5086
+ title: "Rules let you put a thumb on the scale",
5087
+ chunks: [
5088
+ {
5089
+ type: "text",
5090
+ value: "When a query matches a condition, a Rule can pin a result to the top, hide one, boost a brand or attach a banner."
5091
+ },
5092
+ {
5093
+ type: "text",
5094
+ value: 'A search for "sale" can filter to discounted items. A search for "iphone" can pin your best-selling case.'
5095
+ },
5096
+ {
5097
+ type: "text",
5098
+ value: "The Visual Editor in the dashboard does all of it by drag and drop, no reindex needed."
5099
+ }
5100
+ ]
5101
+ },
5256
5102
  {
5257
5103
  id: "recommend",
5258
5104
  title: "Let Algolia act as your recommendation engine",
@@ -5279,7 +5125,7 @@ function tipsByIds(ids) {
5279
5125
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
5280
5126
  var TICK_MS = 16;
5281
5127
  var CHUNK_HOLD_MS = 2e3;
5282
- var HOLD_MS = 8e3;
5128
+ var HOLD_MS = 12e3;
5283
5129
  function TipTitle({ children }) {
5284
5130
  return /* @__PURE__ */ jsxs16(Box18, { gap: 1, children: [
5285
5131
  /* @__PURE__ */ jsx16(Text18, { color: "cyan", children: "\u2726" }),
@@ -5337,7 +5183,11 @@ function TipContent({
5337
5183
  ) });
5338
5184
  }
5339
5185
  var progress = { stepId: null, tipIndex: 0, revealed: 0 };
5340
- function Tips({ stepId, ids }) {
5186
+ function Tips({
5187
+ stepId,
5188
+ ids,
5189
+ onRevealStateChange
5190
+ }) {
5341
5191
  const tips2 = tipsByIds(ids);
5342
5192
  const resuming = progress.stepId === stepId;
5343
5193
  if (!resuming) {
@@ -5368,6 +5218,16 @@ function Tips({ stepId, ids }) {
5368
5218
  );
5369
5219
  if (!skipPause) chunkBoundaries.push(cumulative);
5370
5220
  }
5221
+ const revealState = tips2.length === 0 ? "idle" : revealed < totalLength ? "revealing" : "revealed";
5222
+ const onRevealStateChangeRef = useRef4(onRevealStateChange);
5223
+ onRevealStateChangeRef.current = onRevealStateChange;
5224
+ const reportedRef = useRef4(null);
5225
+ useEffect3(() => {
5226
+ if (reportedRef.current === revealState) return;
5227
+ reportedRef.current = revealState;
5228
+ onRevealStateChangeRef.current?.(revealState);
5229
+ }, [revealState]);
5230
+ useEffect3(() => () => onRevealStateChangeRef.current?.("idle"), []);
5371
5231
  useEffect3(() => {
5372
5232
  if (tips2.length === 0) return;
5373
5233
  if (revealed < totalLength) {
@@ -5417,6 +5277,7 @@ function App() {
5417
5277
  const { exit } = useApp();
5418
5278
  const { columns, rows } = useWindowSize7();
5419
5279
  const [showLogs, setShowLogs] = useState9(false);
5280
+ const [tipState, setTipState] = useState9("idle");
5420
5281
  const finished = phase === "done" || phase === "error";
5421
5282
  const currentStep = steps[currentStepIndex];
5422
5283
  const stepDef = workflow ? getWorkflow(workflow.id)?.steps.find((s) => s.id === currentStep?.id) : void 0;
@@ -5424,9 +5285,11 @@ function App() {
5424
5285
  const stepHasTips = tipIds.length > 0;
5425
5286
  const isAwaitingUserInput = phase === "awaitingInput" && !!inputReq;
5426
5287
  const isCommandApprovalPrompt = isAwaitingUserInput && inputReq?.promptType === "commandApproval";
5427
- const showTips = stepHasTips && (phase === "running" || isCommandApprovalPrompt);
5288
+ const promptPending = isAwaitingUserInput && !isCommandApprovalPrompt;
5289
+ const holdForTip = stepHasTips && promptPending && tipState === "revealing";
5290
+ const showTips = stepHasTips && (phase === "running" || isCommandApprovalPrompt || holdForTip);
5428
5291
  const showNoticesInMain = !stepHasTips;
5429
- const showNotices = !isAwaitingUserInput;
5292
+ const showNotices = !isAwaitingUserInput || holdForTip;
5430
5293
  useInput6(
5431
5294
  (_input, key) => {
5432
5295
  if (key.return) {
@@ -5446,7 +5309,7 @@ function App() {
5446
5309
  });
5447
5310
  }
5448
5311
  });
5449
- const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
5312
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && !holdForTip && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
5450
5313
  useInput6((_input, key) => {
5451
5314
  if (escOwnedElsewhere) return;
5452
5315
  if (key.escape) {
@@ -5510,7 +5373,14 @@ function App() {
5510
5373
  /* @__PURE__ */ jsx17(Text19, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
5511
5374
  ] }),
5512
5375
  /* @__PURE__ */ jsx17(CliOutput, {}),
5513
- showTips && currentStep && /* @__PURE__ */ jsx17(Tips, { stepId: currentStep.id, ids: tipIds }),
5376
+ showTips && currentStep && /* @__PURE__ */ jsx17(
5377
+ Tips,
5378
+ {
5379
+ stepId: currentStep.id,
5380
+ ids: tipIds,
5381
+ onRevealStateChange: setTipState
5382
+ }
5383
+ ),
5514
5384
  showNoticesInMain && showNotices && /* @__PURE__ */ jsx17(Notices, { showAll: true, border: false }),
5515
5385
  !showTips && /* @__PURE__ */ jsx17(PromptInput, {}),
5516
5386
  phase === "error" && error && /* @__PURE__ */ jsx17(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.status.error, children: [
@@ -5519,7 +5389,7 @@ function App() {
5519
5389
  ] }) })
5520
5390
  ] }),
5521
5391
  !showNoticesInMain && showNotices && /* @__PURE__ */ jsx17(Notices, {}),
5522
- showTips && /* @__PURE__ */ jsx17(PromptInput, {})
5392
+ showTips && !holdForTip && /* @__PURE__ */ jsx17(PromptInput, {})
5523
5393
  ]
5524
5394
  }
5525
5395
  ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.35.0-rc.126.250",
3
+ "version": "0.35.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {