@algolia/wizard 0.80.0 → 0.82.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 +579 -151
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2306,7 +2306,7 @@ async function ensureApplication(resumeAppId) {
2306
2306
  // package.json
2307
2307
  var package_default = {
2308
2308
  name: "@algolia/wizard",
2309
- version: "0.80.0",
2309
+ version: "0.82.0",
2310
2310
  description: "Magically implement Algolia functionality in your codebase",
2311
2311
  type: "module",
2312
2312
  engines: {
@@ -3566,30 +3566,379 @@ async function gitIgnoreOutcome(ctx, target) {
3566
3566
  }
3567
3567
 
3568
3568
  // src/lib/tools/searchFiles.ts
3569
+ import { execFile as execFile2 } from "node:child_process";
3569
3570
  import { tool as tool7 } from "ai";
3570
3571
  import z14 from "zod";
3571
- import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
3572
- import { join as join8 } from "node:path";
3572
+ import { lstat as lstat3, readdir as readdir2, readFile as readFile7, realpath, stat as stat2 } from "node:fs/promises";
3573
+ import { isAbsolute as isAbsolute2, join as join8, relative as relative4, sep as sep2 } from "node:path";
3574
+ import { performance } from "node:perf_hooks";
3575
+ import { Worker } from "node:worker_threads";
3576
+ var MAX_GIT_OUTPUT_BYTES = 32e6;
3573
3577
  var MAX_QUERY_LENGTH = 1e3;
3578
+ var MAX_MATCH_TEXT_CHARACTERS = 500;
3579
+ var MAX_RESULT_CHARACTERS = 32e3;
3580
+ var REGEX_TIMEOUT_MS = 100;
3581
+ var REGEX_SEARCH_DEADLINE_MS = 2e3;
3582
+ var LINE_NUMBER_OFFSET = 1;
3583
+ var NEWLINE_CHARACTER_COUNT = 1;
3574
3584
  var SKIP_DIRS = /* @__PURE__ */ new Set([
3575
3585
  "node_modules",
3576
3586
  "dist",
3577
3587
  "build",
3588
+ "coverage",
3578
3589
  "vendor",
3579
3590
  "venv",
3580
3591
  "__pycache__",
3581
3592
  "target"
3582
3593
  ]);
3583
- async function walkFiles(dir) {
3594
+ var SKIP_FILES = /* @__PURE__ */ new Set([
3595
+ "bun.lock",
3596
+ "bun.lockb",
3597
+ "Cargo.lock",
3598
+ "composer.lock",
3599
+ "Gemfile.lock",
3600
+ "package-lock.json",
3601
+ "npm-shrinkwrap.json",
3602
+ "Pipfile.lock",
3603
+ "Podfile.lock",
3604
+ "pnpm-lock.yaml",
3605
+ "poetry.lock",
3606
+ "uv.lock",
3607
+ "yarn.lock"
3608
+ ]);
3609
+ var REGEX_WORKER_SOURCE = `
3610
+ const { parentPort, workerData } = require('node:worker_threads')
3611
+
3612
+ function matchingLineSnippet(line, matchIndex) {
3613
+ if (line.length <= workerData.maxMatchTextCharacters) return line
3614
+
3615
+ const contextBefore = Math.floor(workerData.maxMatchTextCharacters / 2)
3616
+ const start = Math.max(0, matchIndex - contextBefore)
3617
+ const end = Math.min(
3618
+ line.length,
3619
+ start + workerData.maxMatchTextCharacters,
3620
+ )
3621
+ const prefix = start > 0 ? '... ' : ''
3622
+ const suffix = end < line.length ? ' ...' : ''
3623
+ return \`\${prefix}\${line.slice(start, end)}\${suffix} (line truncated)\`
3624
+ }
3625
+
3626
+ let expression
3627
+ try {
3628
+ expression = new RegExp(workerData.query)
3629
+ } catch (error) {
3630
+ parentPort.postMessage({
3631
+ type: 'invalid',
3632
+ message: error instanceof Error ? error.message : String(error),
3633
+ })
3634
+ parentPort.close()
3635
+ }
3636
+
3637
+ if (expression !== undefined) {
3638
+ parentPort.postMessage({ type: 'ready' })
3639
+ parentPort.on('message', ({ id, content, limit }) => {
3640
+ const matches = []
3641
+ const lines = content.split('\\n')
3642
+ parentPort.postMessage({ type: 'started', id })
3643
+ for (const [lineIndex, line] of lines.entries()) {
3644
+ const matchIndex = line.search(expression)
3645
+ if (matchIndex >= 0) {
3646
+ matches.push({
3647
+ lineIndex,
3648
+ text: matchingLineSnippet(line, matchIndex),
3649
+ })
3650
+ if (matches.length >= limit) break
3651
+ }
3652
+ }
3653
+ parentPort.postMessage({ type: 'matches', id, matches })
3654
+ })
3655
+ }
3656
+ `;
3657
+ var InvalidRegexError = class extends Error {
3658
+ };
3659
+ var RegexSearchWorker = class _RegexSearchWorker {
3660
+ worker;
3661
+ ready;
3662
+ resolveReady;
3663
+ rejectReady;
3664
+ readySettled = false;
3665
+ stopped = false;
3666
+ nextId = 0;
3667
+ pending;
3668
+ termination;
3669
+ constructor(query) {
3670
+ this.ready = new Promise((resolve6, reject) => {
3671
+ this.resolveReady = resolve6;
3672
+ this.rejectReady = reject;
3673
+ });
3674
+ this.worker = new Worker(REGEX_WORKER_SOURCE, {
3675
+ eval: true,
3676
+ workerData: {
3677
+ query,
3678
+ maxMatchTextCharacters: MAX_MATCH_TEXT_CHARACTERS
3679
+ }
3680
+ });
3681
+ this.worker.on("message", (message) => {
3682
+ this.handleMessage(message);
3683
+ });
3684
+ this.worker.on("error", this.handleWorkerError);
3685
+ this.worker.on("exit", (code) => {
3686
+ if (this.termination === void 0 && !this.stopped) {
3687
+ this.fail(new Error(`Regex worker exited unexpectedly with code ${code}`));
3688
+ }
3689
+ });
3690
+ }
3691
+ static async create(query) {
3692
+ const searchWorker = new _RegexSearchWorker(query);
3693
+ try {
3694
+ await searchWorker.ready;
3695
+ return searchWorker;
3696
+ } catch (error) {
3697
+ await searchWorker.close();
3698
+ throw error;
3699
+ }
3700
+ }
3701
+ match(content, limit, timeoutMs) {
3702
+ if (this.stopped) throw new Error("Regex worker is not available");
3703
+ return new Promise((resolve6, reject) => {
3704
+ const id = ++this.nextId;
3705
+ const pending = { id, timeoutMs, resolve: resolve6, reject };
3706
+ this.pending = pending;
3707
+ try {
3708
+ this.worker.postMessage({ id, content, limit });
3709
+ } catch (error) {
3710
+ this.pending = void 0;
3711
+ reject(error instanceof Error ? error : new Error(String(error)));
3712
+ return;
3713
+ }
3714
+ });
3715
+ }
3716
+ async close() {
3717
+ this.stopped = true;
3718
+ const pending = this.pending;
3719
+ if (pending?.timer !== void 0) clearTimeout(pending.timer);
3720
+ this.termination ??= this.worker.terminate();
3721
+ await this.termination.catch(() => void 0);
3722
+ }
3723
+ handleWorkerError = (error) => {
3724
+ this.fail(error);
3725
+ };
3726
+ handleMessage(message) {
3727
+ if (message.type === "ready") {
3728
+ if (!this.readySettled) {
3729
+ this.readySettled = true;
3730
+ this.resolveReady();
3731
+ }
3732
+ return;
3733
+ }
3734
+ if (message.type === "invalid") {
3735
+ if (!this.readySettled) {
3736
+ this.readySettled = true;
3737
+ this.stopped = true;
3738
+ this.rejectReady(
3739
+ new InvalidRegexError(message.message ?? "Invalid regular expression")
3740
+ );
3741
+ }
3742
+ return;
3743
+ }
3744
+ const pending = this.pending;
3745
+ if (message.type === "started" && message.id !== void 0 && pending !== void 0 && pending.id === message.id) {
3746
+ const startedAt = performance.now();
3747
+ pending.startedAt = startedAt;
3748
+ pending.timer = setTimeout(() => {
3749
+ const current = this.pending;
3750
+ if (current === void 0 || current.id !== message.id) return;
3751
+ this.pending = void 0;
3752
+ this.stopped = true;
3753
+ this.termination = this.worker.terminate();
3754
+ pending.resolve({
3755
+ locations: null,
3756
+ executionMs: performance.now() - startedAt
3757
+ });
3758
+ }, pending.timeoutMs);
3759
+ return;
3760
+ }
3761
+ if (message.type !== "matches" || message.id === void 0 || message.matches === void 0 || pending === void 0 || pending.id !== message.id || pending.startedAt === void 0) {
3762
+ return;
3763
+ }
3764
+ this.pending = void 0;
3765
+ if (pending.timer !== void 0) clearTimeout(pending.timer);
3766
+ pending.resolve({
3767
+ locations: message.matches,
3768
+ executionMs: performance.now() - pending.startedAt
3769
+ });
3770
+ }
3771
+ fail(error) {
3772
+ if (this.stopped) return;
3773
+ this.stopped = true;
3774
+ if (!this.readySettled) {
3775
+ this.readySettled = true;
3776
+ this.rejectReady(error);
3777
+ }
3778
+ const pending = this.pending;
3779
+ this.pending = void 0;
3780
+ if (pending !== void 0) {
3781
+ if (pending.timer !== void 0) clearTimeout(pending.timer);
3782
+ pending.reject(error);
3783
+ }
3784
+ }
3785
+ };
3786
+ function isSearchableGitPath(path, skipGeneratedDirs) {
3787
+ const parts = path.split("/");
3788
+ const fileName = path.slice(path.lastIndexOf("/") + LINE_NUMBER_OFFSET);
3789
+ return !SKIP_FILES.has(fileName) && parts.every(
3790
+ (part) => !part.startsWith(".") && (!skipGeneratedDirs || !SKIP_DIRS.has(part))
3791
+ );
3792
+ }
3793
+ function parseGitFiles(root, output, skipGeneratedDirs) {
3794
+ return output.split("\0").filter(Boolean).filter((path) => isSearchableGitPath(path, skipGeneratedDirs)).map((path) => join8(root, path));
3795
+ }
3796
+ function gitIsUnavailable(error, stderr) {
3797
+ return "code" in error && error.code === "ENOENT" || stderr.includes("not a git repository");
3798
+ }
3799
+ async function listGitFiles(root, pathspec, args, skipGeneratedDirs) {
3800
+ const env = { ...process.env };
3801
+ env.GIT_DIR = void 0;
3802
+ env.GIT_WORK_TREE = void 0;
3803
+ env.GIT_INDEX_FILE = void 0;
3804
+ env.GIT_OBJECT_DIRECTORY = void 0;
3805
+ env.GIT_COMMON_DIR = void 0;
3806
+ return new Promise((resolve6, reject) => {
3807
+ execFile2(
3808
+ "git",
3809
+ ["-C", root, "ls-files", ...args, "-z", "--", pathspec],
3810
+ { env, maxBuffer: MAX_GIT_OUTPUT_BYTES },
3811
+ (error, stdout, stderr) => {
3812
+ if (error === null) {
3813
+ resolve6({
3814
+ available: true,
3815
+ files: parseGitFiles(
3816
+ root,
3817
+ stdout.toString(),
3818
+ skipGeneratedDirs
3819
+ )
3820
+ });
3821
+ return;
3822
+ }
3823
+ if (gitIsUnavailable(error, stderr.toString())) {
3824
+ resolve6({ available: false, files: [] });
3825
+ return;
3826
+ }
3827
+ reject(error);
3828
+ }
3829
+ );
3830
+ });
3831
+ }
3832
+ async function gitVisibleFiles(root, target) {
3833
+ const pathspec = relative4(root, target).split(sep2).join("/") || ".";
3834
+ const [tracked, untracked] = await Promise.all([
3835
+ listGitFiles(root, pathspec, ["--cached"], false),
3836
+ listGitFiles(
3837
+ root,
3838
+ pathspec,
3839
+ ["--others", "--exclude-standard"],
3840
+ true
3841
+ )
3842
+ ]);
3843
+ if (!tracked.available || !untracked.available) {
3844
+ return { available: false, files: [] };
3845
+ }
3846
+ return {
3847
+ available: true,
3848
+ files: [.../* @__PURE__ */ new Set([...tracked.files, ...untracked.files])]
3849
+ };
3850
+ }
3851
+ function isInsideRoot(root, target) {
3852
+ const fromRoot = relative4(root, target);
3853
+ return fromRoot !== ".." && !fromRoot.startsWith(`..${sep2}`) && !isAbsolute2(fromRoot);
3854
+ }
3855
+ async function walkFiles(root, dir) {
3856
+ const [rootPath, dirPath, dirInfo] = await Promise.all([
3857
+ realpath(root),
3858
+ realpath(dir),
3859
+ lstat3(dir)
3860
+ ]);
3861
+ if (dirInfo.isSymbolicLink() || !isInsideRoot(rootPath, dirPath)) {
3862
+ return [];
3863
+ }
3584
3864
  const out = [];
3585
3865
  for (const e of await readdir2(dir, { withFileTypes: true })) {
3586
3866
  if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
3587
3867
  const full = join8(dir, e.name);
3588
- if (e.isDirectory()) out.push(...await walkFiles(full));
3589
- else if (e.isFile()) out.push(full);
3868
+ if (e.isDirectory()) out.push(...await walkFiles(root, full));
3869
+ else if (e.isFile() && !SKIP_FILES.has(e.name)) out.push(full);
3590
3870
  }
3591
3871
  return out;
3592
3872
  }
3873
+ async function expandNestedRepositories(files) {
3874
+ const expanded = [];
3875
+ for (const file of files) {
3876
+ const info = await lstat3(file).catch(() => null);
3877
+ if (info?.isFile() || info?.isSymbolicLink()) {
3878
+ expanded.push(file);
3879
+ continue;
3880
+ }
3881
+ if (!info?.isDirectory()) continue;
3882
+ try {
3883
+ const nested = await gitVisibleFiles(file, file);
3884
+ expanded.push(
3885
+ ...nested.available ? nested.files : await walkFiles(file, file)
3886
+ );
3887
+ } catch (error) {
3888
+ logger.warn(
3889
+ { error, root: file },
3890
+ "searchFiles: nested Git file scan failed, using safe walker"
3891
+ );
3892
+ expanded.push(...await walkFiles(file, file));
3893
+ }
3894
+ }
3895
+ return expanded;
3896
+ }
3897
+ async function gitRootForTarget(root, target) {
3898
+ let repositoryRoot = root;
3899
+ let current = root;
3900
+ const parts = relative4(root, target).split(sep2).filter(Boolean);
3901
+ for (const part of parts) {
3902
+ current = join8(current, part);
3903
+ const info = await lstat3(current).catch(() => null);
3904
+ if (!info?.isDirectory()) return repositoryRoot;
3905
+ const marker = await lstat3(join8(current, ".git")).catch(() => null);
3906
+ if (marker?.isDirectory() || marker?.isFile()) repositoryRoot = current;
3907
+ }
3908
+ return repositoryRoot;
3909
+ }
3910
+ async function safeSearchPath(rootPath, file) {
3911
+ const [info, filePath] = await Promise.all([lstat3(file), realpath(file)]);
3912
+ return info.isFile() && isInsideRoot(rootPath, filePath) ? filePath : null;
3913
+ }
3914
+ async function searchableFiles(root, target) {
3915
+ try {
3916
+ const gitRoot = await gitRootForTarget(root, target);
3917
+ const result = await gitVisibleFiles(gitRoot, target);
3918
+ return result.available ? expandNestedRepositories(result.files) : walkFiles(root, target);
3919
+ } catch (error) {
3920
+ logger.warn(
3921
+ { error, root, target },
3922
+ "searchFiles: Git file scan failed, using safe walker"
3923
+ );
3924
+ return walkFiles(root, target);
3925
+ }
3926
+ }
3927
+ function formatSearchResult(matches, skippedFiles, deadlineReached, workerFailed, finalMarker) {
3928
+ const lines = [...matches];
3929
+ if (skippedFiles > 0) {
3930
+ const fileLabel = skippedFiles === 1 ? "file" : "files";
3931
+ const deadlineMarker = deadlineReached ? ", regex deadline reached" : "";
3932
+ lines.push(
3933
+ `... (partial results: skipped ${skippedFiles} timed-out ${fileLabel}${deadlineMarker})`
3934
+ );
3935
+ }
3936
+ if (workerFailed) {
3937
+ lines.push("... (partial results: regex worker stopped unexpectedly)");
3938
+ }
3939
+ if (finalMarker !== void 0) lines.push(finalMarker);
3940
+ return lines.length > 0 ? lines.join("\n") : "No matches";
3941
+ }
3593
3942
  function searchFilesTool(ctx) {
3594
3943
  return tool7({
3595
3944
  description: "Search file contents for a JavaScript regular expression (RegExp syntax, not grep/PCRE). Returns matching lines as file:line:text.",
@@ -3607,32 +3956,124 @@ function searchFilesTool(ctx) {
3607
3956
  }
3608
3957
  const resolved2 = resolveInRoot(ctx, path);
3609
3958
  if (!resolved2.ok) return resolved2.error;
3610
- let re;
3611
3959
  try {
3612
- re = new RegExp(query);
3960
+ await stat2(resolved2.target);
3613
3961
  } catch (err) {
3614
- return `Invalid regex: ${err.message}`;
3962
+ return `Error searching ${path}: ${err.message}`;
3963
+ }
3964
+ let regexWorker;
3965
+ try {
3966
+ regexWorker = await RegexSearchWorker.create(query);
3967
+ } catch (error) {
3968
+ const message = error instanceof Error ? error.message : String(error);
3969
+ if (error instanceof InvalidRegexError) {
3970
+ return `Invalid regex: ${message}`;
3971
+ }
3972
+ logger.warn({ error }, "searchFiles: regex worker failed to start");
3973
+ return "Refused: regex worker could not start. Retry the search.";
3615
3974
  }
3616
3975
  const matches = [];
3617
- for (const file of await walkFiles(resolved2.target)) {
3618
- let content;
3976
+ let resultCharacters = 0;
3977
+ let skippedFiles = 0;
3978
+ let remainingRegexTime = REGEX_SEARCH_DEADLINE_MS;
3979
+ let deadlineReached = false;
3980
+ let workerFailed = false;
3981
+ try {
3619
3982
  try {
3620
- content = await readFile7(file, "utf8");
3621
- } catch {
3622
- continue;
3623
- }
3624
- const lines = content.split("\n");
3625
- for (let i = 0; i < lines.length; i++) {
3626
- if (re.test(lines[i])) {
3627
- matches.push(`${file}:${i + 1}:${lines[i]}`);
3628
- if (matches.length >= ctx.limits.match) {
3629
- matches.push(`... (truncated at ${ctx.limits.match} matches)`);
3630
- return matches.join("\n");
3983
+ const files = await searchableFiles(ctx.root, resolved2.target);
3984
+ const rootPath = await realpath(ctx.root);
3985
+ for (const [fileIndex, file] of files.entries()) {
3986
+ const moreFilesRemain = fileIndex < files.length - LINE_NUMBER_OFFSET;
3987
+ if (remainingRegexTime <= 0) {
3988
+ deadlineReached = true;
3989
+ break;
3990
+ }
3991
+ let content;
3992
+ try {
3993
+ const filePath = await safeSearchPath(rootPath, file);
3994
+ if (filePath === null) continue;
3995
+ content = await readFile7(filePath, "utf8");
3996
+ } catch {
3997
+ continue;
3998
+ }
3999
+ let result;
4000
+ try {
4001
+ result = await regexWorker.match(
4002
+ content,
4003
+ ctx.limits.match - matches.length,
4004
+ Math.min(REGEX_TIMEOUT_MS, remainingRegexTime)
4005
+ );
4006
+ } catch (error) {
4007
+ logger.warn(
4008
+ { error, file },
4009
+ "searchFiles: regex worker stopped during search"
4010
+ );
4011
+ workerFailed = true;
4012
+ break;
4013
+ }
4014
+ const { executionMs, locations } = result;
4015
+ remainingRegexTime -= executionMs;
4016
+ if (locations === null) {
4017
+ skippedFiles += LINE_NUMBER_OFFSET;
4018
+ if (!moreFilesRemain) break;
4019
+ if (remainingRegexTime <= 0) {
4020
+ deadlineReached = true;
4021
+ break;
4022
+ }
4023
+ await regexWorker.close();
4024
+ try {
4025
+ regexWorker = await RegexSearchWorker.create(query);
4026
+ } catch (error) {
4027
+ logger.warn(
4028
+ { error, file },
4029
+ "searchFiles: regex worker failed to restart"
4030
+ );
4031
+ workerFailed = true;
4032
+ break;
4033
+ }
4034
+ continue;
4035
+ }
4036
+ for (const { lineIndex, text } of locations) {
4037
+ const lineNumber = lineIndex + LINE_NUMBER_OFFSET;
4038
+ const match = `${file}:${lineNumber}:${text}`;
4039
+ const nextResultCharacters = resultCharacters + match.length + NEWLINE_CHARACTER_COUNT;
4040
+ if (nextResultCharacters > MAX_RESULT_CHARACTERS) {
4041
+ matches.push(
4042
+ `... (truncated at ${MAX_RESULT_CHARACTERS} characters)`
4043
+ );
4044
+ return formatSearchResult(
4045
+ matches,
4046
+ skippedFiles,
4047
+ deadlineReached,
4048
+ workerFailed
4049
+ );
4050
+ }
4051
+ matches.push(match);
4052
+ resultCharacters = nextResultCharacters;
4053
+ if (matches.length >= ctx.limits.match) {
4054
+ return formatSearchResult(
4055
+ matches,
4056
+ skippedFiles,
4057
+ deadlineReached,
4058
+ workerFailed,
4059
+ `... (truncated at ${ctx.limits.match} matches)`
4060
+ );
4061
+ }
3631
4062
  }
3632
4063
  }
4064
+ } catch (error) {
4065
+ logger.warn({ error }, "searchFiles: safe file scan failed");
4066
+ return "Refused: Could not list searchable files safely. Fix file access and retry.";
3633
4067
  }
4068
+ } finally {
4069
+ await regexWorker.close();
3634
4070
  }
3635
- return matches.length ? matches.join("\n") : "No matches";
4071
+ return formatSearchResult(
4072
+ matches,
4073
+ skippedFiles,
4074
+ deadlineReached,
4075
+ workerFailed
4076
+ );
3636
4077
  }
3637
4078
  });
3638
4079
  }
@@ -3641,14 +4082,14 @@ function searchFilesTool(ctx) {
3641
4082
  import { tool as tool9, generateText, Output } from "ai";
3642
4083
  import { createAnthropic } from "@ai-sdk/anthropic";
3643
4084
  import z16 from "zod";
3644
- import { relative as relative5 } from "node:path";
4085
+ import { relative as relative6 } from "node:path";
3645
4086
 
3646
4087
  // src/lib/tools/reviewScript.ts
3647
4088
  import { tool as tool8 } from "ai";
3648
4089
  import z15 from "zod";
3649
4090
  import { createHash as createHash2 } from "node:crypto";
3650
- import { readFile as readFile8, stat as stat2 } from "node:fs/promises";
3651
- import { basename as basename2, relative as relative4, resolve as resolve3 } from "node:path";
4091
+ import { readFile as readFile8, stat as stat3 } from "node:fs/promises";
4092
+ import { basename as basename2, relative as relative5, resolve as resolve3 } from "node:path";
3652
4093
 
3653
4094
  // src/lib/tools/utils/prompt.ts
3654
4095
  var chain = Promise.resolve();
@@ -3747,7 +4188,7 @@ function commandDiffersFromReviewedScript(ctx, command, cwd) {
3747
4188
  if (approval.command === command) return true;
3748
4189
  const scriptReferences = [
3749
4190
  basename2(approval.filePath),
3750
- relative4(ctx.root, approval.filePath),
4191
+ relative5(ctx.root, approval.filePath),
3751
4192
  ".algolia-wizard"
3752
4193
  ];
3753
4194
  return scriptReferences.some(
@@ -3796,19 +4237,19 @@ function reviewScriptTool(ctx) {
3796
4237
  if (resolved2.ok === false) return resolved2.error;
3797
4238
  const resolvedCwd = resolveInRoot(ctx, cwd);
3798
4239
  if (resolvedCwd.ok === false) return resolvedCwd.error;
3799
- const displayCwd = relative4(ctx.root, resolvedCwd.target) || ".";
3800
- const displayFilePath = relative4(ctx.root, resolved2.target);
4240
+ const displayCwd = relative5(ctx.root, resolvedCwd.target) || ".";
4241
+ const displayFilePath = relative5(ctx.root, resolved2.target);
3801
4242
  const ingestionSummary = usesGeneratedRecords ? `This will push ${entityName} records from generated sample data to Algolia. You can replace the sample dataset with your real data source in ${displayFilePath}.` : `This will push ${entityName} records from your selected source to Algolia.`;
3802
4243
  return serializePrompt(async () => {
3803
4244
  revokeCommandApprovals(ctx, command, resolvedCwd.target);
3804
- const isFile = await stat2(resolved2.target).then(
4245
+ const isFile = await stat3(resolved2.target).then(
3805
4246
  (s) => s.isFile(),
3806
4247
  () => false
3807
4248
  );
3808
4249
  if (!isFile) {
3809
4250
  return `Refused: ${filePath} is not a file. Write the script first, then review the path you wrote.`;
3810
4251
  }
3811
- const relativeScriptPath = relative4(resolvedCwd.target, resolved2.target);
4252
+ const relativeScriptPath = relative5(resolvedCwd.target, resolved2.target);
3812
4253
  if (!commandReferencesScript(
3813
4254
  command,
3814
4255
  resolved2.target,
@@ -3968,7 +4409,7 @@ function storeApproval(root) {
3968
4409
  return async (req) => {
3969
4410
  const store = useWizard.getState();
3970
4411
  if (store.isCommandApproved(req.command, req.cwd)) return "approve";
3971
- const rel = relative5(root, req.cwd);
4412
+ const rel = relative6(root, req.cwd);
3972
4413
  const answer = await store.requestUserInput({
3973
4414
  prompt: "Run this command?",
3974
4415
  promptType: "commandApproval",
@@ -4194,8 +4635,8 @@ function defaultCreateModel() {
4194
4635
  }
4195
4636
  function approvedCommandHistory(approvedCommands) {
4196
4637
  return Array.from(approvedCommands).map((entry) => {
4197
- const sep3 = entry.indexOf("\0");
4198
- return { cwd: entry.slice(0, sep3), command: entry.slice(sep3 + 1) };
4638
+ const sep4 = entry.indexOf("\0");
4639
+ return { cwd: entry.slice(0, sep4), command: entry.slice(sep4 + 1) };
4199
4640
  });
4200
4641
  }
4201
4642
  async function classifyCommandSafety(createModel, command, cwd, explanation, approvedHistory) {
@@ -4312,14 +4753,14 @@ async function runReviewedCommand(ctx, command, cwd) {
4312
4753
  const approval = getReviewedCommandApproval(ctx, command, cwd);
4313
4754
  if (!approval) return void 0;
4314
4755
  if (!await reviewedCommandApprovalIsCurrent(approval)) {
4315
- return `Refused: ${relative5(ctx.root, approval.filePath)} changed after review. Call reviewScript again before running it.`;
4756
+ return `Refused: ${relative6(ctx.root, approval.filePath)} changed after review. Call reviewScript again before running it.`;
4316
4757
  }
4317
4758
  const [env, secrets] = await Promise.all([
4318
4759
  resolveShellEnv(ctx, command),
4319
4760
  resolveSecretShellEnv(ctx, command)
4320
4761
  ]);
4321
4762
  if (!await reviewedCommandApprovalIsCurrent(approval)) {
4322
- return `Refused: ${relative5(ctx.root, approval.filePath)} changed after review. Call reviewScript again before running it.`;
4763
+ return `Refused: ${relative6(ctx.root, approval.filePath)} changed after review. Call reviewScript again before running it.`;
4323
4764
  }
4324
4765
  const result = await runAndRecord(
4325
4766
  ctx,
@@ -4923,10 +5364,13 @@ var MODE_CONFIG = {
4923
5364
  ingestion: {
4924
5365
  instructions: [
4925
5366
  "Analyze the codebase to find the data entities (models) that should be ingested into Algolia.",
5367
+ "Assume this is a new Algolia customer project. Existing Algolia dependencies, credentials, indices, and integration code are not required.",
5368
+ "This workflow creates the Algolia integration, so its absence is expected and is not a failure.",
5369
+ "Infer entity candidates from domain models, database schemas, data-access code, API handlers, and representative application data.",
4926
5370
  "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).",
4927
5371
  "Inspect the source of each entity to extract real field names for attributes \u2014 do not guess or leave attributes empty.",
4928
5372
  "Prefer domain models (e.g. Document, Product, User) over framework or infrastructure types.",
4929
- "Use as few tools as possible, but do not guess. If you cannot find any entities, return an empty array.",
5373
+ "Use as few tools as possible, but do not guess. If no domain entities exist, report success with an empty array.",
4930
5374
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
4931
5375
  "When done, call reportStatus"
4932
5376
  ],
@@ -5389,16 +5833,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
5389
5833
  // src/actions/implement.ts
5390
5834
  import z28 from "zod";
5391
5835
  import { access, mkdir as mkdir5, readFile as readFile9 } from "node:fs/promises";
5392
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join11, relative as relative6, resolve as resolve5, sep as sep2 } from "node:path";
5836
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join11, relative as relative7, resolve as resolve5, sep as sep3 } from "node:path";
5393
5837
 
5394
5838
  // src/lib/git.ts
5395
- import { execFile as execFile2 } from "node:child_process";
5396
- import { copyFile, mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
5397
- import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute2, join as join9, resolve as resolve4 } from "node:path";
5839
+ import { execFile as execFile3 } from "node:child_process";
5840
+ import { copyFile, mkdir as mkdir4, stat as stat4 } from "node:fs/promises";
5841
+ import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute3, join as join9, resolve as resolve4 } from "node:path";
5398
5842
  var MAX_BUFFER = 32 * 1024 * 1024;
5399
5843
  function git(args) {
5400
5844
  return new Promise((resolve6, reject) => {
5401
- execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
5845
+ execFile3("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
5402
5846
  if (err)
5403
5847
  return reject(
5404
5848
  new Error(
@@ -5423,9 +5867,9 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
5423
5867
  if (!trimmed) {
5424
5868
  return { ok: false, reason: "no file path was provided" };
5425
5869
  }
5426
- const source = isAbsolute2(trimmed) ? trimmed : resolve4(repoRoot, trimmed);
5870
+ const source = isAbsolute3(trimmed) ? trimmed : resolve4(repoRoot, trimmed);
5427
5871
  try {
5428
- if (!(await stat3(source)).isFile()) {
5872
+ if (!(await stat4(source)).isFile()) {
5429
5873
  return { ok: false, reason: `"${sourcePath}" is not a file` };
5430
5874
  }
5431
5875
  } catch {
@@ -5819,7 +6263,7 @@ function parseIngestRecordCount(output) {
5819
6263
  return Number.isFinite(count) ? count : void 0;
5820
6264
  }
5821
6265
  function relativeIngestCwd(execution, repoRoot) {
5822
- const cwd = relative6(repoRoot, execution.cwd);
6266
+ const cwd = relative7(repoRoot, execution.cwd);
5823
6267
  return cwd && cwd !== "." ? cwd : ".";
5824
6268
  }
5825
6269
  function reportedCommandWithCwdNote(execution, repoRoot) {
@@ -5936,8 +6380,8 @@ async function frontendHasPackageJson(repoRoot, searchLocation) {
5936
6380
  let directory = repoRoot;
5937
6381
  if (searchLocation) {
5938
6382
  const candidate = dirname5(resolve5(repoRoot, searchLocation));
5939
- const candidateRelative = relative6(repoRoot, candidate);
5940
- const outsideRoot = candidateRelative === ".." || candidateRelative.startsWith(`..${sep2}`) || isAbsolute3(candidateRelative);
6383
+ const candidateRelative = relative7(repoRoot, candidate);
6384
+ const outsideRoot = candidateRelative === ".." || candidateRelative.startsWith(`..${sep3}`) || isAbsolute4(candidateRelative);
5941
6385
  if (!outsideRoot) directory = candidate;
5942
6386
  }
5943
6387
  for (; ; ) {
@@ -6169,7 +6613,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
6169
6613
  if (agentRuns > 0) ctx.recordStepExecution();
6170
6614
  agentRuns += 1;
6171
6615
  const writtenFiles = [...new Set(useWizard.getState().writtenFiles)].map(
6172
- (file) => relative6(repoRoot, file)
6616
+ (file) => relative7(repoRoot, file)
6173
6617
  );
6174
6618
  return runAgent({
6175
6619
  operation: "search-validation",
@@ -6309,7 +6753,7 @@ ${detail}` : ""}`
6309
6753
  const trackComponentGenerated = (mounted_successfully, attempts) => {
6310
6754
  const searchFilesChanged = [
6311
6755
  ...new Set(useWizard.getState().writtenFiles)
6312
- ].map((file) => relative6(repoRoot, file));
6756
+ ].map((file) => relative7(repoRoot, file));
6313
6757
  track("AI Wizard Frontend Component Generated", {
6314
6758
  file_paths: searchFilesChanged,
6315
6759
  framework,
@@ -6344,7 +6788,7 @@ ${detail}` : ""}`
6344
6788
  buildCheckPlan ??= await resolveBuildCheckPlan(ctx, repoRoot, input);
6345
6789
  const searchFilesChanged = [
6346
6790
  ...new Set(useWizard.getState().writtenFiles)
6347
- ].map((file) => relative6(repoRoot, file));
6791
+ ].map((file) => relative7(repoRoot, file));
6348
6792
  const validation = await runValidationUseCase(
6349
6793
  validationRunInstructions(buildCheckPlan, searchFilesChanged, attempt)
6350
6794
  );
@@ -6435,17 +6879,12 @@ var defaultWorkflow = {
6435
6879
  id: "default",
6436
6880
  title: "Default Workflow",
6437
6881
  description: "This workflow explores your repo and implements Algolia on your behalf",
6882
+ tips: true,
6438
6883
  steps: [
6439
6884
  defineStep({
6440
6885
  id: "project-scan",
6441
6886
  title: "project scan",
6442
6887
  outputSchema: projectScanSchema,
6443
- tips: [
6444
- "apps-and-indices",
6445
- "records-are-json",
6446
- "speed",
6447
- "searchable-attributes"
6448
- ],
6449
6888
  run: (ctx) => {
6450
6889
  ctx.notify({
6451
6890
  messages: [
@@ -6481,7 +6920,6 @@ var defaultWorkflow = {
6481
6920
  id: "ingestion",
6482
6921
  title: "ingest records",
6483
6922
  outputSchema: implementSchema,
6484
- tips: ["facets", "test-relevance", "search-analytics", "recommend"],
6485
6923
  run: (ctx) => {
6486
6924
  ctx.notify({
6487
6925
  messages: [
@@ -6502,7 +6940,6 @@ var defaultWorkflow = {
6502
6940
  id: "search",
6503
6941
  title: "create search ui",
6504
6942
  outputSchema: implementSchema,
6505
- tips: ["typo-tolerance", "synonyms", "multi-index-search", "rules"],
6506
6943
  run: (ctx) => {
6507
6944
  ctx.notify({
6508
6945
  messages: ["Building your Algolia search experience\u2026"]
@@ -6513,6 +6950,7 @@ var defaultWorkflow = {
6513
6950
  defineStep({
6514
6951
  id: "review",
6515
6952
  title: "done",
6953
+ hideTips: true,
6516
6954
  outputSchema: reviewSchema,
6517
6955
  run: (ctx) => {
6518
6956
  ctx.notify({
@@ -7175,7 +7613,6 @@ var playbookWorkflow = {
7175
7613
  id: "select-index",
7176
7614
  title: "Select index",
7177
7615
  outputSchema: selectIndexSchema,
7178
- tips: ["apps-and-indices", "records-are-json", "speed"],
7179
7616
  run: (ctx) => selectIndexStep2(ctx)
7180
7617
  }),
7181
7618
  defineStep({
@@ -7183,7 +7620,6 @@ var playbookWorkflow = {
7183
7620
  title: "Confirm sample index creation",
7184
7621
  outputSchema: confirmIndexSchema,
7185
7622
  visible: false,
7186
- tips: ["apps-and-indices", "records-are-json", "speed"],
7187
7623
  run: (ctx) => confirmIndexStep(ctx)
7188
7624
  }),
7189
7625
  defineStep({
@@ -7788,7 +8224,6 @@ function Code({ children }) {
7788
8224
  // src/ui/copy/tips.ts
7789
8225
  var tips = [
7790
8226
  {
7791
- id: "apps-and-indices",
7792
8227
  title: "An Application is like your library. Indices are the books in it.",
7793
8228
  chunks: [
7794
8229
  {
@@ -7802,7 +8237,6 @@ var tips = [
7802
8237
  ]
7803
8238
  },
7804
8239
  {
7805
- id: "records-are-json",
7806
8240
  title: "A Record is a JSON object inside of an index.",
7807
8241
  chunks: [
7808
8242
  {
@@ -7836,7 +8270,6 @@ var tips = [
7836
8270
  ]
7837
8271
  },
7838
8272
  {
7839
- id: "speed",
7840
8273
  title: "Speed isn't a feature, it's our business",
7841
8274
  chunks: [
7842
8275
  {
@@ -7850,7 +8283,6 @@ var tips = [
7850
8283
  ]
7851
8284
  },
7852
8285
  {
7853
- id: "searchable-attributes",
7854
8286
  title: "Searchable attributes are your relevance dial #1",
7855
8287
  chunks: [
7856
8288
  {
@@ -7866,7 +8298,6 @@ var tips = [
7866
8298
  ]
7867
8299
  },
7868
8300
  {
7869
- id: "facets",
7870
8301
  title: "Facets aren't just filters, they help build your UI",
7871
8302
  chunks: [
7872
8303
  { type: "codeword", value: "attributesForFaceting" },
@@ -7877,7 +8308,6 @@ var tips = [
7877
8308
  ]
7878
8309
  },
7879
8310
  {
7880
- id: "test-relevance",
7881
8311
  title: "Test relevance in the dashboard before you write a line of ranking code",
7882
8312
  chunks: [
7883
8313
  {
@@ -7896,7 +8326,6 @@ var tips = [
7896
8326
  ]
7897
8327
  },
7898
8328
  {
7899
- id: "search-analytics",
7900
8329
  title: "Search Analytics helps you discover opportunities",
7901
8330
  chunks: [
7902
8331
  {
@@ -7910,7 +8339,6 @@ var tips = [
7910
8339
  ]
7911
8340
  },
7912
8341
  {
7913
- id: "typo-tolerance",
7914
8342
  title: "Your users will misspell things. Algolia already handles it.",
7915
8343
  chunks: [
7916
8344
  {
@@ -7924,7 +8352,6 @@ var tips = [
7924
8352
  ]
7925
8353
  },
7926
8354
  {
7927
- id: "synonyms",
7928
8355
  title: "Synonyms teach the engine your words",
7929
8356
  chunks: [
7930
8357
  {
@@ -7942,7 +8369,6 @@ var tips = [
7942
8369
  ]
7943
8370
  },
7944
8371
  {
7945
- id: "multi-index-search",
7946
8372
  title: "One search box, many indices",
7947
8373
  chunks: [
7948
8374
  {
@@ -7956,7 +8382,6 @@ var tips = [
7956
8382
  ]
7957
8383
  },
7958
8384
  {
7959
- id: "rules",
7960
8385
  title: "Rules let you put a thumb on the scale",
7961
8386
  chunks: [
7962
8387
  {
@@ -7974,7 +8399,6 @@ var tips = [
7974
8399
  ]
7975
8400
  },
7976
8401
  {
7977
- id: "recommend",
7978
8402
  title: "Let Algolia act as your recommendation engine",
7979
8403
  chunks: [
7980
8404
  {
@@ -7988,18 +8412,12 @@ var tips = [
7988
8412
  ]
7989
8413
  }
7990
8414
  ];
7991
- var tipsById = Object.fromEntries(
7992
- tips.map((tip) => [tip.id, tip])
7993
- );
7994
- function tipsByIds(ids) {
7995
- return ids.map((id) => tipsById[id]).filter((tip) => tip !== void 0);
7996
- }
7997
8415
 
7998
8416
  // src/ui/Tips.tsx
7999
8417
  import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
8000
8418
  var TICK_MS = 16;
8001
8419
  var CHUNK_HOLD_MS = 2e3;
8002
- var HOLD_MS = 12e3;
8420
+ var TIP_DURATION_MS = 25e3;
8003
8421
  function TipTitle({ children }) {
8004
8422
  return /* @__PURE__ */ jsxs19(Box21, { gap: 1, children: [
8005
8423
  /* @__PURE__ */ jsx20(Text22, { color: "cyan", children: "\u2726" }),
@@ -8056,45 +8474,53 @@ function TipContent({
8056
8474
  ] }, j)) }, i)
8057
8475
  ) });
8058
8476
  }
8059
- var progress = { stepId: null, tipIndex: 0, revealed: 0 };
8477
+ var noPauseAfterText = [
8478
+ "highlight",
8479
+ "link",
8480
+ "codeword"
8481
+ ];
8482
+ function revealSchedule(tip) {
8483
+ const segments = [
8484
+ { type: "text", value: tip.title },
8485
+ ...tip.chunks
8486
+ ];
8487
+ const schedule = [];
8488
+ let at = 0;
8489
+ segments.forEach((segment, i) => {
8490
+ for (let c = 0; c < segment.value.length; c++) {
8491
+ const pause = i > 0 && c === 0 && !noPauseAfterText.includes(segment.type);
8492
+ at += pause ? CHUNK_HOLD_MS : TICK_MS;
8493
+ schedule.push(at);
8494
+ }
8495
+ });
8496
+ return schedule;
8497
+ }
8498
+ var clock = { tipIndex: 0, elapsedMs: 0, complete: false };
8060
8499
  function Tips({
8061
- stepId,
8062
- ids,
8063
- onRevealStateChange
8500
+ onRevealStateChange,
8501
+ onComplete
8064
8502
  }) {
8065
- const tips2 = tipsByIds(ids);
8066
- const resuming = progress.stepId === stepId;
8067
- if (!resuming) {
8068
- progress.stepId = stepId;
8069
- progress.tipIndex = 0;
8070
- progress.revealed = 0;
8071
- }
8072
- const [tipIndex, setTipIndex] = useState11(resuming ? progress.tipIndex : 0);
8073
- const [revealed, setRevealed] = useState11(resuming ? progress.revealed : 0);
8074
- const tip = tips2.length > 0 ? tips2[tipIndex % tips2.length] : void 0;
8075
- const contentLength = tip?.chunks.reduce((sum, c) => sum + c.value.length, 0) ?? 0;
8076
- const totalLength = (tip?.title.length ?? 0) + contentLength;
8077
- const segments = tip ? [
8078
- { type: "title", length: tip.title.length },
8079
- ...tip.chunks.map((c) => ({ type: c.type, length: c.value.length }))
8080
- ] : [];
8081
- const noPauseAfterText = [
8082
- "highlight",
8083
- "link",
8084
- "codeword"
8085
- ];
8086
- const chunkBoundaries = [];
8087
- let cumulative = 0;
8088
- for (let i = 0; i < segments.length - 1; i++) {
8089
- cumulative += segments[i].length;
8090
- const skipPause = noPauseAfterText.includes(
8091
- segments[i + 1].type
8092
- );
8093
- if (!skipPause) chunkBoundaries.push(cumulative);
8094
- }
8095
- const revealState = tips2.length === 0 ? "idle" : revealed < totalLength ? "revealing" : "revealed";
8503
+ const [position, setPosition] = useState11(() => {
8504
+ const now = Date.now();
8505
+ return {
8506
+ tipIndex: clock.tipIndex,
8507
+ startedAt: now - clock.elapsedMs,
8508
+ now,
8509
+ complete: clock.complete
8510
+ };
8511
+ });
8512
+ const tip = tips[position.tipIndex];
8513
+ const elapsedMs = position.now - position.startedAt;
8514
+ const schedule = revealSchedule(tip);
8515
+ const revealed = schedule.filter((at) => at <= elapsedMs).length;
8516
+ const fullyTyped = revealed === schedule.length;
8517
+ const nextAt = fullyTyped ? TIP_DURATION_MS : schedule[revealed];
8518
+ const midSentence = !fullyTyped && revealed > 0 && nextAt - schedule[revealed - 1] === TICK_MS;
8519
+ const revealState = position.complete ? "idle" : midSentence ? "typing" : "resting";
8096
8520
  const onRevealStateChangeRef = useRef4(onRevealStateChange);
8097
8521
  onRevealStateChangeRef.current = onRevealStateChange;
8522
+ const onCompleteRef = useRef4(onComplete);
8523
+ onCompleteRef.current = onComplete;
8098
8524
  const reportedRef = useRef4(null);
8099
8525
  useEffect3(() => {
8100
8526
  if (reportedRef.current === revealState) return;
@@ -8103,30 +8529,35 @@ function Tips({
8103
8529
  }, [revealState]);
8104
8530
  useEffect3(() => () => onRevealStateChangeRef.current?.("idle"), []);
8105
8531
  useEffect3(() => {
8106
- if (tips2.length === 0) return;
8107
- if (revealed < totalLength) {
8108
- const delay2 = chunkBoundaries.includes(revealed) ? CHUNK_HOLD_MS : TICK_MS;
8109
- const timer2 = setTimeout(() => {
8110
- setRevealed((r) => {
8111
- const next = r + 1;
8112
- progress.revealed = next;
8113
- return next;
8114
- });
8115
- }, delay2);
8116
- return () => clearTimeout(timer2);
8532
+ if (position.complete) {
8533
+ onCompleteRef.current?.();
8534
+ return;
8117
8535
  }
8118
8536
  const timer = setTimeout(() => {
8119
- setTipIndex((i) => {
8120
- const next = (i + 1) % tips2.length;
8121
- progress.tipIndex = next;
8122
- return next;
8123
- });
8124
- setRevealed(0);
8125
- progress.revealed = 0;
8126
- }, HOLD_MS);
8127
- return () => clearTimeout(timer);
8128
- }, [revealed, totalLength, tips2.length]);
8129
- if (!tip) return null;
8537
+ const now = Date.now();
8538
+ const tipComplete = now - position.startedAt >= TIP_DURATION_MS;
8539
+ if (tipComplete && position.tipIndex === tips.length - 1) {
8540
+ clock.complete = true;
8541
+ setPosition({ ...position, now, complete: true });
8542
+ return;
8543
+ }
8544
+ setPosition(
8545
+ tipComplete ? {
8546
+ tipIndex: position.tipIndex + 1,
8547
+ startedAt: now,
8548
+ now,
8549
+ complete: false
8550
+ } : { ...position, now }
8551
+ );
8552
+ }, nextAt - elapsedMs);
8553
+ return () => {
8554
+ clearTimeout(timer);
8555
+ if (clock.complete) return;
8556
+ clock.tipIndex = position.tipIndex;
8557
+ clock.elapsedMs = Date.now() - position.startedAt;
8558
+ };
8559
+ }, [position, nextAt, elapsedMs]);
8560
+ if (position.complete) return null;
8130
8561
  const titleRevealed = tip.title.slice(0, revealed);
8131
8562
  const contentRevealed = Math.max(0, revealed - tip.title.length);
8132
8563
  return /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", marginBottom: 1, gap: 1, children: [
@@ -8194,17 +8625,16 @@ function App() {
8194
8625
  const compactRun = isCompactTerminal(columns, rows);
8195
8626
  const [showLogs, setShowLogs] = useState12(false);
8196
8627
  const [tipState, setTipState] = useState12("idle");
8628
+ const [tipsComplete, setTipsComplete] = useState12(false);
8197
8629
  const finished = phase === "done" || phase === "error";
8198
8630
  const currentStep = steps[currentStepIndex];
8199
- const stepDef = workflow ? getWorkflow(workflow.id)?.steps.find((s) => s.id === currentStep?.id) : void 0;
8200
- const tipIds = stepDef?.tips ?? [];
8201
- const stepHasTips = tipIds.length > 0;
8631
+ const workflowDef = workflow ? getWorkflow(workflow.id) : void 0;
8632
+ const stepHidesTips = workflowDef?.steps.find((s) => s.id === currentStep?.id)?.hideTips === true;
8633
+ const tipsActive = workflowDef?.tips === true && !tipsComplete && !stepHidesTips;
8202
8634
  const isAwaitingUserInput = phase === "awaitingInput" && !!inputReq;
8203
- const isCommandApprovalPrompt = isAwaitingUserInput && inputReq?.promptType === "commandApproval";
8204
- const promptPending = isAwaitingUserInput && !isCommandApprovalPrompt;
8205
- const holdForTip = !compactRun && stepHasTips && promptPending && tipState === "revealing";
8206
- const showTips = !compactRun && stepHasTips && (phase === "running" && currentStep?.status === "running" && !loadingMessage && !settingUpAppId || isCommandApprovalPrompt);
8207
- const showNoticesInMain = !stepHasTips;
8635
+ const holdForTip = !compactRun && tipsActive && isAwaitingUserInput && tipState === "typing";
8636
+ const showTips = !compactRun && tipsActive && (phase === "running" && currentStep?.status === "running" && !loadingMessage && !settingUpAppId || holdForTip);
8637
+ const showNoticesInMain = !tipsActive;
8208
8638
  const showNotices = !compactRun && (!isAwaitingUserInput || holdForTip) && !review2;
8209
8639
  useInput9(
8210
8640
  (_input, key) => {
@@ -8299,12 +8729,11 @@ function App() {
8299
8729
  loadingMessage
8300
8730
  ] }) }),
8301
8731
  /* @__PURE__ */ jsx21(CliOutput, {}),
8302
- showTips && currentStep && /* @__PURE__ */ jsx21(
8732
+ showTips && /* @__PURE__ */ jsx21(
8303
8733
  Tips,
8304
8734
  {
8305
- stepId: currentStep.id,
8306
- ids: tipIds,
8307
- onRevealStateChange: setTipState
8735
+ onRevealStateChange: setTipState,
8736
+ onComplete: () => setTipsComplete(true)
8308
8737
  }
8309
8738
  ),
8310
8739
  showNoticesInMain && showNotices && /* @__PURE__ */ jsx21(Notices, { showAll: true, border: false }),
@@ -8315,8 +8744,7 @@ function App() {
8315
8744
  error
8316
8745
  ] }) })
8317
8746
  ] }),
8318
- !showNoticesInMain && showNotices && /* @__PURE__ */ jsx21(Notices, {}),
8319
- showTips && !holdForTip && /* @__PURE__ */ jsx21(PromptInput, {})
8747
+ !showNoticesInMain && showNotices && /* @__PURE__ */ jsx21(Notices, {})
8320
8748
  ]
8321
8749
  }
8322
8750
  ),
@@ -8563,7 +8991,7 @@ async function resetProjectState() {
8563
8991
  // package.json with { type: 'json' }
8564
8992
  var package_default2 = {
8565
8993
  name: "@algolia/wizard",
8566
- version: "0.80.0",
8994
+ version: "0.82.0",
8567
8995
  description: "Magically implement Algolia functionality in your codebase",
8568
8996
  type: "module",
8569
8997
  engines: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.80.0",
3
+ "version": "0.82.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {