@algolia/wizard 0.23.0 → 0.24.0-rc.117.212

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 (3) hide show
  1. package/README.md +0 -14
  2. package/dist/main.js +669 -518
  3. package/package.json +2 -1
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/ui/App.tsx
7
- import { Box as Box17, Text as Text17, useApp, useInput as useInput7, useWindowSize as useWindowSize8 } from "ink";
7
+ import { Box as Box19, Text as Text19, useApp, useInput as useInput7, useWindowSize as useWindowSize7 } from "ink";
8
8
 
9
9
  // src/core/store.ts
10
10
  import { create } from "zustand";
@@ -251,6 +251,7 @@ var useWizard = create((set, get) => ({
251
251
  _noticeTimer: null,
252
252
  cliOutput: [],
253
253
  targetIndex: null,
254
+ writtenFiles: [],
254
255
  logs: [],
255
256
  error: null,
256
257
  inputReq: null,
@@ -344,6 +345,8 @@ var useWizard = create((set, get) => ({
344
345
  })),
345
346
  clearCliOutput: () => set({ cliOutput: [] }),
346
347
  setTargetIndex: (index) => set({ targetIndex: index }),
348
+ recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
349
+ clearWrittenFiles: () => set({ writtenFiles: [] }),
347
350
  logStart: (kind, name, input) => {
348
351
  const id = nanoid();
349
352
  set((s) => ({
@@ -391,6 +394,7 @@ var useWizard = create((set, get) => ({
391
394
  notices: [],
392
395
  cliOutput: [],
393
396
  targetIndex: null,
397
+ writtenFiles: [],
394
398
  logs: [],
395
399
  error: null,
396
400
  inputReq: null,
@@ -484,7 +488,7 @@ function CliOutput() {
484
488
  }
485
489
 
486
490
  // src/ui/Notices.tsx
487
- import { Box as Box3, Text as Text3, useWindowSize as useWindowSize3 } from "ink";
491
+ import { Box as Box3, Text as Text3 } from "ink";
488
492
  import { useEffect as useEffect2, useState as useState2 } from "react";
489
493
 
490
494
  // src/ui/Table.tsx
@@ -543,31 +547,6 @@ var truncate = (s, width) => s.length <= width ? s : width <= 1 ? s.slice(0, wid
543
547
  // src/ui/Notices.tsx
544
548
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
545
549
  var AGENT_MARKER = "\u2726";
546
- var RESERVED_ROWS2 = 14;
547
- var PANEL_TEXT_WIDTH2 = 45;
548
- function messageLineCount(text) {
549
- return Math.max(1, Math.ceil(text.length / PANEL_TEXT_WIDTH2));
550
- }
551
- function noticeLineCount(notice) {
552
- const messageLines = (notice.messages ?? []).reduce((sum, m) => {
553
- const text = typeof m === "string" ? m : m.text;
554
- return sum + messageLineCount(text);
555
- }, 0);
556
- const tableLines = notice.table ? notice.table.rows.length + 4 : 0;
557
- return messageLines + tableLines;
558
- }
559
- function fitVisibleNotices(notices, windowRows) {
560
- const budget = Math.max(windowRows - RESERVED_ROWS2, 3);
561
- let used = 0;
562
- let count = 0;
563
- for (let i = notices.length - 1; i >= 0; i--) {
564
- const height = noticeLineCount(notices[i]) + (count > 0 ? 1 : 0);
565
- if (count > 0 && used + height > budget) break;
566
- used += height;
567
- count++;
568
- }
569
- return notices.slice(notices.length - count);
570
- }
571
550
  var PULSE_STEPS = 12;
572
551
  var PULSE_STEP_MS = 150;
573
552
  var PULSE_COLORS = Array.from(
@@ -590,8 +569,7 @@ function parseHex(hex) {
590
569
  }
591
570
  function Notices() {
592
571
  const notices = useWizard((s) => s.notices);
593
- const { rows: windowRows } = useWindowSize3();
594
- const visible = fitVisibleNotices(notices, windowRows);
572
+ const latest = notices[notices.length - 1];
595
573
  const [pulseStep, setPulseStep] = useState2(0);
596
574
  useEffect2(() => {
597
575
  let direction = 1;
@@ -605,30 +583,40 @@ function Notices() {
605
583
  }, PULSE_STEP_MS);
606
584
  return () => clearInterval(id);
607
585
  }, []);
608
- if (!visible.length) return null;
586
+ if (!latest) return null;
609
587
  const pulseColor = PULSE_COLORS[pulseStep];
610
- return /* @__PURE__ */ jsx2(Box3, { flexDirection: "column", gap: 1, marginBottom: 1, children: visible.map((notice, i) => {
611
- const isLatest = i === visible.length - 1;
612
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
613
- notice.messages?.map((m, j) => {
614
- const line = typeof m === "string" ? { text: m } : m;
615
- const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
616
- return /* @__PURE__ */ jsxs2(
617
- Text3,
618
- {
619
- color: isLatest && !line.color ? pulseColor : line.color ?? COLORS.dim,
620
- bold: line.bold,
621
- children: [
622
- prefix,
623
- line.text
624
- ]
625
- },
626
- `notice-${i}-${j}`
627
- );
628
- }),
629
- notice.table && /* @__PURE__ */ jsx2(Table, { columns: notice.table.columns, rows: notice.table.rows })
630
- ] }, `notice-${i}`);
631
- }) });
588
+ return /* @__PURE__ */ jsxs2(
589
+ Box3,
590
+ {
591
+ borderColor: COLORS.border,
592
+ borderStyle: "single",
593
+ borderBackgroundColor: COLORS.bg.main,
594
+ borderLeft: false,
595
+ borderRight: false,
596
+ borderBottom: false,
597
+ paddingTop: 1,
598
+ flexDirection: "column",
599
+ children: [
600
+ latest.messages?.map((m, j) => {
601
+ const line = typeof m === "string" ? { text: m } : m;
602
+ const prefix = j === 0 ? `${AGENT_MARKER} ` : " ";
603
+ return /* @__PURE__ */ jsxs2(
604
+ Text3,
605
+ {
606
+ color: !line.color ? pulseColor : line.color ?? COLORS.dim,
607
+ bold: line.bold,
608
+ children: [
609
+ prefix,
610
+ line.text
611
+ ]
612
+ },
613
+ `notice-${j}`
614
+ );
615
+ }),
616
+ latest.table && /* @__PURE__ */ jsx2(Table, { columns: latest.table.columns, rows: latest.table.rows })
617
+ ]
618
+ }
619
+ );
632
620
  }
633
621
 
634
622
  // src/ui/PromptInput.tsx
@@ -709,11 +697,11 @@ function CommandApproval({
709
697
  }
710
698
 
711
699
  // src/ui/SelectPrompt.tsx
712
- import { Box as Box8, Text as Text8, useInput as useInput2, useWindowSize as useWindowSize5 } from "ink";
700
+ import { Box as Box8, Text as Text8, useInput as useInput2, useWindowSize as useWindowSize4 } from "ink";
713
701
  import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState4 } from "react";
714
702
 
715
703
  // src/ui/ScrollView.tsx
716
- import { Box as Box6, Text as Text6, measureElement as measureElement2, useWindowSize as useWindowSize4 } from "ink";
704
+ import { Box as Box6, Text as Text6, measureElement as measureElement2, useWindowSize as useWindowSize3 } from "ink";
717
705
  import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
718
706
  import { jsxs as jsxs5 } from "react/jsx-runtime";
719
707
  var INDICATOR_ROWS = 2;
@@ -730,7 +718,7 @@ function useScrollWindow({
730
718
  followBottom = false
731
719
  }) {
732
720
  const viewportRef = useRef2(null);
733
- const { columns } = useWindowSize4();
721
+ const { columns } = useWindowSize3();
734
722
  const [size, setSize] = useState3(
735
723
  null
736
724
  );
@@ -871,7 +859,7 @@ function SelectPrompt({
871
859
  if (multi) hints.push({ key: "[space]", label: "select" });
872
860
  hints.push({ key: "[enter]", label: "confirm" });
873
861
  const containerRef = useRef3(null);
874
- const { columns } = useWindowSize5();
862
+ const { columns } = useWindowSize4();
875
863
  const [width, setWidth] = useState4(columns);
876
864
  useLayoutEffect2(() => {
877
865
  if (!containerRef.current) return;
@@ -1100,7 +1088,7 @@ function PromptInput() {
1100
1088
  import { dirname as dirname2, join as join3 } from "node:path";
1101
1089
  import { fileURLToPath } from "node:url";
1102
1090
  import { useState as useState6 } from "react";
1103
- import { Box as Box10, Spacer, Text as Text10, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
1091
+ import { Box as Box10, Spacer, Text as Text10, useInput as useInput4, useWindowSize as useWindowSize5 } from "ink";
1104
1092
 
1105
1093
  // src/ui/copy/welcome.ts
1106
1094
  var sidebarItems = [
@@ -1153,7 +1141,7 @@ function SidebarItem({
1153
1141
  function Welcome() {
1154
1142
  const confirmStart = useWizard((s) => s.confirmStart);
1155
1143
  const openLearnMore = useWizard((s) => s.openLearnMore);
1156
- const { rows } = useWindowSize6();
1144
+ const { rows } = useWindowSize5();
1157
1145
  const actions = [
1158
1146
  { label: "start wizard", run: confirmStart },
1159
1147
  { label: "learn more", run: openLearnMore }
@@ -1246,7 +1234,7 @@ function Welcome() {
1246
1234
 
1247
1235
  // src/ui/LearnMore.tsx
1248
1236
  import { Fragment as Fragment2 } from "react";
1249
- import { Box as Box11, Text as Text11, useInput as useInput5, useWindowSize as useWindowSize7 } from "ink";
1237
+ import { Box as Box11, Text as Text11, useInput as useInput5, useWindowSize as useWindowSize6 } from "ink";
1250
1238
 
1251
1239
  // src/ui/copy/learn-more.ts
1252
1240
  var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
@@ -1259,7 +1247,7 @@ var accessItems = [
1259
1247
  {
1260
1248
  tag: "WRITE",
1261
1249
  title: "Code changes",
1262
- description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
1250
+ description: "creates & edits files (search UI, config) directly in your branch."
1263
1251
  },
1264
1252
  {
1265
1253
  tag: "EXEC",
@@ -1274,7 +1262,7 @@ var accessItems = [
1274
1262
  {
1275
1263
  tag: "KEY",
1276
1264
  title: "Credentials",
1277
- description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
1265
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
1278
1266
  }
1279
1267
  ];
1280
1268
  var neverItems = [
@@ -1316,7 +1304,7 @@ function NeverLine({
1316
1304
  function LearnMore() {
1317
1305
  const confirmStart = useWizard((s) => s.confirmStart);
1318
1306
  const backToHome = useWizard((s) => s.backToHome);
1319
- const { columns } = useWindowSize7();
1307
+ const { columns } = useWindowSize6();
1320
1308
  const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1321
1309
  useInput5((_input, key) => {
1322
1310
  if (key.escape) backToHome();
@@ -1526,7 +1514,7 @@ function Ribbon() {
1526
1514
  }
1527
1515
 
1528
1516
  // src/ui/App.tsx
1529
- import { useState as useState7 } from "react";
1517
+ import { useState as useState8 } from "react";
1530
1518
 
1531
1519
  // src/ui/Logs.tsx
1532
1520
  import { Box as Box16, Text as Text16, useInput as useInput6 } from "ink";
@@ -1776,13 +1764,287 @@ function track(event, payload) {
1776
1764
  });
1777
1765
  }
1778
1766
 
1767
+ // src/ui/Tips.tsx
1768
+ import { useEffect as useEffect3, useState as useState7 } from "react";
1769
+ import { Box as Box18, Text as Text18 } from "ink";
1770
+ import terminalLink from "terminal-link";
1771
+
1772
+ // src/ui/Code.tsx
1773
+ import { Box as Box17, Text as Text17 } from "ink";
1774
+ import { jsx as jsx15 } from "react/jsx-runtime";
1775
+ var TOKEN_RE = /("(?:\\.|[^"\\])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}[\]:,])/g;
1776
+ function highlightJson(json) {
1777
+ const parts = json.split(TOKEN_RE);
1778
+ return parts.map((part, i) => {
1779
+ if (!part) return null;
1780
+ if (part[0] === '"') {
1781
+ const next = parts.slice(i + 1).find((p) => p.trim());
1782
+ const isKey = next?.trimStart().startsWith(":");
1783
+ return /* @__PURE__ */ jsx15(Text17, { color: isKey ? "cyan" : "green", children: part }, i);
1784
+ }
1785
+ if (part === "true" || part === "false" || part === "null") {
1786
+ return /* @__PURE__ */ jsx15(Text17, { color: "magenta", children: part }, i);
1787
+ }
1788
+ if (/^-?\d/.test(part)) {
1789
+ return /* @__PURE__ */ jsx15(Text17, { color: "yellow", children: part }, i);
1790
+ }
1791
+ if (/^[{}[\]:,]$/.test(part)) {
1792
+ return /* @__PURE__ */ jsx15(Text17, { dimColor: true, children: part }, i);
1793
+ }
1794
+ return /* @__PURE__ */ jsx15(Text17, { children: part }, i);
1795
+ });
1796
+ }
1797
+ function Code({ children }) {
1798
+ return /* @__PURE__ */ jsx15(Box17, { children: /* @__PURE__ */ jsx15(Text17, { children: highlightJson(children) }) });
1799
+ }
1800
+
1801
+ // src/ui/copy/tips.ts
1802
+ var tips = [
1803
+ {
1804
+ title: "An Application is like your library. Indices are the books in it.",
1805
+ chunks: [
1806
+ {
1807
+ type: "text",
1808
+ value: "Applications hold your API keys and Indices."
1809
+ },
1810
+ {
1811
+ type: "text",
1812
+ value: "Each index is a searchable collection of records (think: products, articles, orders, concerts)."
1813
+ }
1814
+ ]
1815
+ },
1816
+ {
1817
+ title: "A Record is a JSON object inside of an index.",
1818
+ chunks: [
1819
+ {
1820
+ type: "text",
1821
+ value: "Every object you index is just a JSON document with a unique objectID."
1822
+ },
1823
+ {
1824
+ type: "text",
1825
+ value: "No fixed schema, no hard requirements."
1826
+ },
1827
+ {
1828
+ type: "text",
1829
+ value: "Everything else is up to you:"
1830
+ },
1831
+ {
1832
+ type: "code",
1833
+ value: JSON.stringify(
1834
+ {
1835
+ objectID: "sku_48213",
1836
+ name: "Trail Running Shoe",
1837
+ brand: "Northline",
1838
+ categories: ["Footwear", "Running", "Mens"],
1839
+ price: 129.99,
1840
+ in_stock: true,
1841
+ rating: 4.6
1842
+ },
1843
+ null,
1844
+ 2
1845
+ )
1846
+ }
1847
+ ]
1848
+ },
1849
+ {
1850
+ title: "Speed isn't a feature, it's our business",
1851
+ chunks: [
1852
+ {
1853
+ type: "text",
1854
+ value: "Algolia's engine processes most search queries in 1 to 50 milliseconds."
1855
+ },
1856
+ {
1857
+ type: "text",
1858
+ value: "This is why our search-as-you-type experience feels instant."
1859
+ }
1860
+ ]
1861
+ },
1862
+ {
1863
+ title: "Searchable attributes are your relevance dial #1",
1864
+ chunks: [
1865
+ {
1866
+ type: "text",
1867
+ value: "Order matters. Attributes listed first in"
1868
+ },
1869
+ { type: "codeword", value: "searchableAttributes" },
1870
+ { type: "text", value: "carry more ranking weight." },
1871
+ {
1872
+ type: "text",
1873
+ value: "This is the single highest-leverage lever new users don't know exists."
1874
+ }
1875
+ ]
1876
+ },
1877
+ {
1878
+ title: "Facets aren't just filters, they help build your UI",
1879
+ chunks: [
1880
+ { type: "codeword", value: "attributesForFaceting" },
1881
+ {
1882
+ type: "text",
1883
+ value: "unlock category sidebars, price sliders, tag clouds without extra backend work."
1884
+ }
1885
+ ]
1886
+ },
1887
+ {
1888
+ title: "Test relevance in the dashboard before you write a line of ranking code",
1889
+ chunks: [
1890
+ {
1891
+ type: "text",
1892
+ value: "Our Search dashboard has a live preview where you can browse results."
1893
+ },
1894
+ {
1895
+ type: "text",
1896
+ value: "Tune your settings and see how it alters your results in"
1897
+ },
1898
+ {
1899
+ type: "link",
1900
+ value: "real-time.",
1901
+ href: "https://dashboard.algolia.com/explorer/browse"
1902
+ }
1903
+ ]
1904
+ },
1905
+ {
1906
+ title: "Search Analytics helps you discover opportunities",
1907
+ chunks: [
1908
+ {
1909
+ type: "text",
1910
+ value: "No click results for a particular query? Low click-through rates for another?"
1911
+ },
1912
+ {
1913
+ type: "text",
1914
+ value: "Our Search Analytics will help you identify synonyms, rules or other relevancy settings to improve your results."
1915
+ }
1916
+ ]
1917
+ },
1918
+ {
1919
+ title: "Let Algolia act as your recommendation engine",
1920
+ chunks: [
1921
+ {
1922
+ type: "text",
1923
+ value: "Beyond search, Algolia Recommend runs models trained on your existing indices and event data to power your recommendation engine."
1924
+ },
1925
+ {
1926
+ type: "text",
1927
+ value: "You can improve engagement with related, popular or visually similar items."
1928
+ }
1929
+ ]
1930
+ }
1931
+ ];
1932
+
1933
+ // src/ui/Tips.tsx
1934
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
1935
+ var TICK_MS = 16;
1936
+ var CHUNK_HOLD_MS = 2e3;
1937
+ var HOLD_MS = 8e3;
1938
+ function TipTitle({ children }) {
1939
+ return /* @__PURE__ */ jsxs16(Box18, { gap: 1, children: [
1940
+ /* @__PURE__ */ jsx16(Text18, { color: "cyan", children: "\u2726" }),
1941
+ /* @__PURE__ */ jsx16(Text18, { color: "white", bold: true, children })
1942
+ ] });
1943
+ }
1944
+ function InlineSegment({ segment }) {
1945
+ switch (segment.type) {
1946
+ case "highlight":
1947
+ return /* @__PURE__ */ jsx16(Text18, { color: COLORS.success, children: segment.value });
1948
+ case "link":
1949
+ return /* @__PURE__ */ jsx16(Text18, { color: "cyan", underline: true, children: segment.href ? terminalLink(segment.value, segment.href) : segment.value });
1950
+ case "codeword":
1951
+ return /* @__PURE__ */ jsx16(Text18, { color: COLORS.highlight.fg, backgroundColor: COLORS.highlight.bg, children: segment.value });
1952
+ default:
1953
+ return /* @__PURE__ */ jsx16(Text18, { color: COLORS.muted, children: segment.value });
1954
+ }
1955
+ }
1956
+ function TipContent({
1957
+ chunks,
1958
+ revealed
1959
+ }) {
1960
+ let remaining = revealed;
1961
+ const slices = chunks.map((chunk) => {
1962
+ const slice = chunk.value.slice(0, Math.max(0, remaining));
1963
+ remaining -= chunk.value.length;
1964
+ return slice;
1965
+ });
1966
+ const blocks = [];
1967
+ for (let i = 0; i < chunks.length; i++) {
1968
+ const chunk = chunks[i];
1969
+ const slice = slices[i];
1970
+ if (!slice) continue;
1971
+ const segment = {
1972
+ type: chunk.type,
1973
+ value: slice,
1974
+ href: chunk.type === "link" ? chunk.href : void 0
1975
+ };
1976
+ if (chunk.type === "code") {
1977
+ blocks.push({ type: "code", segment });
1978
+ continue;
1979
+ }
1980
+ const last = blocks[blocks.length - 1];
1981
+ if (last?.type === "inline") {
1982
+ last.segments.push(segment);
1983
+ } else {
1984
+ blocks.push({ type: "inline", segments: [segment] });
1985
+ }
1986
+ }
1987
+ return /* @__PURE__ */ jsx16(Box18, { flexDirection: "column", marginLeft: 2, gap: 1, children: blocks.map(
1988
+ (block, i) => block.type === "code" ? /* @__PURE__ */ jsx16(Code, { children: block.segment.value }, i) : /* @__PURE__ */ jsx16(Text18, { children: block.segments.map((segment, j) => /* @__PURE__ */ jsxs16(Text18, { children: [
1989
+ j > 0 && " ",
1990
+ /* @__PURE__ */ jsx16(InlineSegment, { segment })
1991
+ ] }, j)) }, i)
1992
+ ) });
1993
+ }
1994
+ function Tips() {
1995
+ const [tipIndex, setTipIndex] = useState7(0);
1996
+ const [revealed, setRevealed] = useState7(0);
1997
+ const tip = tips[tipIndex];
1998
+ const contentLength = tip.chunks.reduce((sum, c) => sum + c.value.length, 0);
1999
+ const totalLength = tip.title.length + contentLength;
2000
+ const segments = [
2001
+ { type: "title", length: tip.title.length },
2002
+ ...tip.chunks.map((c) => ({ type: c.type, length: c.value.length }))
2003
+ ];
2004
+ const noPauseAfterText = [
2005
+ "highlight",
2006
+ "link",
2007
+ "codeword"
2008
+ ];
2009
+ const chunkBoundaries = [];
2010
+ let cumulative = 0;
2011
+ for (let i = 0; i < segments.length - 1; i++) {
2012
+ cumulative += segments[i].length;
2013
+ const skipPause = noPauseAfterText.includes(
2014
+ segments[i + 1].type
2015
+ );
2016
+ if (!skipPause) chunkBoundaries.push(cumulative);
2017
+ }
2018
+ useEffect3(() => {
2019
+ if (revealed < totalLength) {
2020
+ const delay2 = chunkBoundaries.includes(revealed) ? CHUNK_HOLD_MS : TICK_MS;
2021
+ const timer2 = setTimeout(() => setRevealed((r) => r + 1), delay2);
2022
+ return () => clearTimeout(timer2);
2023
+ }
2024
+ const timer = setTimeout(() => {
2025
+ setTipIndex((i) => {
2026
+ if (i < tips.length - 1) return i + 1;
2027
+ return 0;
2028
+ });
2029
+ setRevealed(0);
2030
+ }, HOLD_MS);
2031
+ return () => clearTimeout(timer);
2032
+ }, [revealed, totalLength]);
2033
+ const titleRevealed = tip.title.slice(0, revealed);
2034
+ const contentRevealed = Math.max(0, revealed - tip.title.length);
2035
+ return /* @__PURE__ */ jsxs16(Box18, { flexDirection: "column", marginBottom: 1, gap: 1, children: [
2036
+ /* @__PURE__ */ jsx16(TipTitle, { children: titleRevealed }),
2037
+ /* @__PURE__ */ jsx16(TipContent, { chunks: tip.chunks, revealed: contentRevealed })
2038
+ ] });
2039
+ }
2040
+
1779
2041
  // src/ui/App.tsx
1780
- import { jsx as jsx15, jsxs as jsxs16 } from "react/jsx-runtime";
2042
+ import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
1781
2043
  function App() {
1782
- const { phase, error, homeScreen, currentStepIndex, steps, inputReq } = useWizard();
2044
+ const { phase, error, homeScreen, currentStepIndex, steps, inputReq, notices } = useWizard();
1783
2045
  const { exit } = useApp();
1784
- const { columns, rows } = useWindowSize8();
1785
- const [showLogs, setShowLogs] = useState7(false);
2046
+ const { columns, rows } = useWindowSize7();
2047
+ const [showLogs, setShowLogs] = useState8(false);
1786
2048
  const finished = phase === "done" || phase === "error";
1787
2049
  const currentStep = steps[currentStepIndex];
1788
2050
  useInput7(
@@ -1824,8 +2086,8 @@ function App() {
1824
2086
  /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
1825
2087
  the whole screen, and the scrolling throws off its cursor arithmetic —
1826
2088
  flicker and leftover rows. */
1827
- /* @__PURE__ */ jsxs16(
1828
- Box17,
2089
+ /* @__PURE__ */ jsxs17(
2090
+ Box19,
1829
2091
  {
1830
2092
  backgroundColor: COLORS.bg.main,
1831
2093
  flexDirection: "row",
@@ -1833,42 +2095,46 @@ function App() {
1833
2095
  height: scrollsPastViewport ? void 0 : rows,
1834
2096
  overflow: scrollsPastViewport ? "visible" : "hidden",
1835
2097
  children: [
1836
- mainWindowVisible && /* @__PURE__ */ jsxs16(
1837
- Box17,
2098
+ mainWindowVisible && /* @__PURE__ */ jsxs17(
2099
+ Box19,
1838
2100
  {
1839
2101
  flexDirection,
1840
2102
  width: "100%",
1841
2103
  maxHeight: rows,
1842
2104
  justifyContent: "space-between",
1843
2105
  children: [
1844
- showLogs ? /* @__PURE__ */ jsx15(Logs, {}) : /* @__PURE__ */ jsxs16(
1845
- Box17,
2106
+ showLogs ? /* @__PURE__ */ jsx17(Logs, {}) : /* @__PURE__ */ jsxs17(
2107
+ Box19,
1846
2108
  {
1847
2109
  flexDirection: "column",
1848
2110
  paddingX: 4,
1849
2111
  paddingY: 2,
1850
2112
  width: showSidebar ? 70 : "100%",
1851
2113
  flexGrow: 1,
2114
+ gap: 1,
1852
2115
  children: [
1853
- phase === "authenticating" && /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", marginBottom: 1, children: [
1854
- /* @__PURE__ */ jsx15(Text17, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
1855
- /* @__PURE__ */ jsx15(Text17, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
2116
+ /* @__PURE__ */ jsxs17(Box19, { flexGrow: 2, flexDirection: "column", children: [
2117
+ phase === "authenticating" && /* @__PURE__ */ jsxs17(Box19, { flexDirection: "column", marginBottom: 1, children: [
2118
+ /* @__PURE__ */ jsx17(Text19, { color: COLORS.strong, bold: true, children: "Signing in to Algolia" }),
2119
+ /* @__PURE__ */ jsx17(Text19, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
2120
+ ] }),
2121
+ /* @__PURE__ */ jsx17(CliOutput, {}),
2122
+ notices.length > 0 && phase === "running" && /* @__PURE__ */ jsx17(Tips, {}),
2123
+ /* @__PURE__ */ jsx17(PromptInput, {}),
2124
+ phase === "error" && error && /* @__PURE__ */ jsx17(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.status.error, children: [
2125
+ "\u2716 ",
2126
+ error
2127
+ ] }) })
1856
2128
  ] }),
1857
- /* @__PURE__ */ jsx15(CliOutput, {}),
1858
- /* @__PURE__ */ jsx15(Notices, {}),
1859
- /* @__PURE__ */ jsx15(PromptInput, {}),
1860
- phase === "error" && error && /* @__PURE__ */ jsx15(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: COLORS.status.error, children: [
1861
- "\u2716 ",
1862
- error
1863
- ] }) })
2129
+ /* @__PURE__ */ jsx17(Notices, {})
1864
2130
  ]
1865
2131
  }
1866
2132
  ),
1867
- showSidebar ? /* @__PURE__ */ jsx15(Sidebar, {}) : /* @__PURE__ */ jsx15(Ribbon, {})
2133
+ showSidebar ? /* @__PURE__ */ jsx17(Sidebar, {}) : /* @__PURE__ */ jsx17(Ribbon, {})
1868
2134
  ]
1869
2135
  }
1870
2136
  ),
1871
- phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx15(LearnMore, {}) : /* @__PURE__ */ jsx15(Welcome, {}))
2137
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx17(LearnMore, {}) : /* @__PURE__ */ jsx17(Welcome, {}))
1872
2138
  ]
1873
2139
  }
1874
2140
  )
@@ -2554,6 +2820,7 @@ function writeFileTool(ctx) {
2554
2820
  }
2555
2821
  await mkdir3(dirname4(resolved2.target), { recursive: true });
2556
2822
  await writeFile3(resolved2.target, content, "utf8");
2823
+ useWizard.getState().recordWrittenFile(resolved2.target);
2557
2824
  return `Wrote to ${filePath}`;
2558
2825
  } catch (err) {
2559
2826
  return `Error writing ${filePath}: ${err.message}`;
@@ -3334,7 +3601,7 @@ function defaultCreateModel() {
3334
3601
  }
3335
3602
  function generateRecordTool(ctx, createModel = defaultCreateModel) {
3336
3603
  return tool10({
3337
- description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. 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.",
3604
+ 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.",
3338
3605
  inputSchema: z17.object({
3339
3606
  entityName: z17.string().describe("Name of the entity to generate records for."),
3340
3607
  attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
@@ -3421,10 +3688,10 @@ import { tool as tool11 } from "ai";
3421
3688
  import z18 from "zod";
3422
3689
  function notifyUserTool() {
3423
3690
  return tool11({
3424
- description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work.`,
3691
+ description: `Give the user a brief, high-level update on what you are currently doing or about to do next. This is for the big picture (e.g. "Reading through your data models", "Writing the search UI") \u2014 not granular detail like individual tool calls, which are already logged separately. Call it when you start a new phase of work or your focus shifts, just not on every step, enough to keep the user engaged. Don't say things like "starting", just describe what you are doing. Don't mention tool calls themselves, just general direction of the work. Only the latest update is shown on screen, in one line, so keep it under 140 characters.`,
3425
3692
  inputSchema: z18.object({
3426
- message: z18.string().describe(
3427
- "Short, plain-language description of what you are doing now."
3693
+ message: z18.string().max(140).describe(
3694
+ "Short, plain-language description of what you are doing now. Under 140 characters \u2014 only the latest update is shown, on one line."
3428
3695
  )
3429
3696
  }),
3430
3697
  execute: async ({ message }) => {
@@ -3700,7 +3967,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3700
3967
  // package.json
3701
3968
  var package_default = {
3702
3969
  name: "@algolia/wizard",
3703
- version: "0.23.0",
3970
+ version: "0.24.0-rc.117.212",
3704
3971
  description: "Magically implement Algolia functionality in your codebase",
3705
3972
  type: "module",
3706
3973
  engines: {
@@ -3762,6 +4029,7 @@ var package_default = {
3762
4029
  nanoid: "^5.1.15",
3763
4030
  pino: "^10.3.1",
3764
4031
  react: "^19.2.7",
4032
+ "terminal-link": "^5.0.0",
3765
4033
  varlock: "^1.5.1",
3766
4034
  zod: "^4.4.3",
3767
4035
  zustand: "^5.0.14"
@@ -4067,11 +4335,10 @@ ${JSON.stringify(s.output, null, 2)}`
4067
4335
  function formatReviewSummary(result) {
4068
4336
  const nextStepLines = result.nextSteps.map((step) => {
4069
4337
  const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
4070
- const isWorktreeCommand = step.includes("/worktrees/");
4071
4338
  return {
4072
4339
  text: `\u2192 ${step}`,
4073
- color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
4074
- bold: isIngestCommand || isWorktreeCommand
4340
+ color: isIngestCommand ? COLORS.brand : void 0,
4341
+ bold: isIngestCommand
4075
4342
  };
4076
4343
  });
4077
4344
  return [
@@ -4106,15 +4373,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4106
4373
 
4107
4374
  // src/actions/implement.ts
4108
4375
  import z29 from "zod";
4109
- import { join as join12 } from "node:path";
4376
+ import { join as join12, relative as relative6 } from "node:path";
4110
4377
 
4111
- // src/lib/worktree.ts
4378
+ // src/lib/git.ts
4112
4379
  import { execFile as execFile2 } from "node:child_process";
4113
- import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4380
+ import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
4114
4381
  import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
4115
4382
  var MAX_BUFFER = 32 * 1024 * 1024;
4116
- var MAX_WIZARD_WORKTREES = 3;
4117
- var WIZARD_BRANCH_PREFIX = "wizard/implement-";
4118
4383
  function git(args) {
4119
4384
  return new Promise((resolve4, reject) => {
4120
4385
  execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
@@ -4137,44 +4402,7 @@ async function assertGitRepoWithHead(repoRoot) {
4137
4402
  );
4138
4403
  }
4139
4404
  }
4140
- async function isWorkingTreeDirty(repoRoot) {
4141
- const out = await git(["-C", repoRoot, "status", "--porcelain"]);
4142
- return out.trim().length > 0;
4143
- }
4144
- async function pruneOldWorktrees(repoRoot) {
4145
- const dir = join10(stateDir(repoRoot), "worktrees");
4146
- const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
4147
- for (const slug of stale) {
4148
- const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
4149
- try {
4150
- await git([
4151
- "-C",
4152
- repoRoot,
4153
- "worktree",
4154
- "remove",
4155
- "--force",
4156
- join10(dir, slug)
4157
- ]);
4158
- await git(["-C", repoRoot, "branch", "-D", branch]);
4159
- } catch (err) {
4160
- logger.warn(
4161
- { branch, err: err.message },
4162
- "createWorktree: failed to prune a stale wizard worktree; continuing"
4163
- );
4164
- }
4165
- }
4166
- }
4167
- async function createWorktree(repoRoot) {
4168
- const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
4169
- const dirSlug = branch.replace(/\//g, "-");
4170
- const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
4171
- await git(["-C", repoRoot, "worktree", "prune"]);
4172
- await pruneOldWorktrees(repoRoot);
4173
- await mkdir6(dirname7(path), { recursive: true });
4174
- await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
4175
- return { path, branch };
4176
- }
4177
- async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
4405
+ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4178
4406
  const trimmed = sourcePath.trim();
4179
4407
  if (!trimmed) {
4180
4408
  return { ok: false, reason: "no file path was provided" };
@@ -4188,7 +4416,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4188
4416
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4189
4417
  }
4190
4418
  const relPath = join10(ingestDir, basename2(source));
4191
- const dest = join10(worktreePath, relPath);
4419
+ const dest = join10(repoRoot, relPath);
4420
+ if (resolve3(source) === resolve3(dest)) {
4421
+ return { ok: true, relPath };
4422
+ }
4192
4423
  try {
4193
4424
  await mkdir6(dirname7(dest), { recursive: true });
4194
4425
  await copyFile(source, dest);
@@ -4203,10 +4434,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
4203
4434
  function hasEnvVar(content, name) {
4204
4435
  return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
4205
4436
  }
4206
- async function readEnvVar(worktreePath, name) {
4437
+ async function readEnvVar(repoRoot, name) {
4207
4438
  let content;
4208
4439
  try {
4209
- content = await readFile8(join10(worktreePath, ".env"), "utf8");
4440
+ content = await readFile8(join10(repoRoot, ".env"), "utf8");
4210
4441
  } catch (err) {
4211
4442
  if (err.code !== "ENOENT") throw err;
4212
4443
  return void 0;
@@ -4220,8 +4451,8 @@ async function readEnvVar(worktreePath, name) {
4220
4451
  if (!value || value.startsWith("<")) return void 0;
4221
4452
  return value;
4222
4453
  }
4223
- async function writeSearchEnvValues(worktreePath, vars) {
4224
- const target = join10(worktreePath, ".env");
4454
+ async function writeSearchEnvValues(repoRoot, vars) {
4455
+ const target = join10(repoRoot, ".env");
4225
4456
  let existing = "";
4226
4457
  try {
4227
4458
  existing = await readFile8(target, "utf8");
@@ -4236,61 +4467,27 @@ async function writeSearchEnvValues(worktreePath, vars) {
4236
4467
  await writeFile7(target, existing + prefix + lines, "utf8");
4237
4468
  return missing.map((v) => v.name);
4238
4469
  }
4239
- async function listChangedFiles(worktreePath) {
4240
- const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
4241
- const entries = raw.split("\0");
4242
- const files = [];
4243
- for (let i = 0; i < entries.length; i += 1) {
4244
- const entry = entries[i];
4245
- if (!entry) continue;
4246
- files.push(entry.slice(3));
4247
- if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
4248
- }
4249
- return files;
4250
- }
4251
4470
  function normalizeFindingPaths(findings) {
4252
4471
  return {
4253
4472
  ...findings,
4254
4473
  ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
4255
4474
  ...e,
4256
- paths: e.paths.map(toWorktreeRelative)
4475
+ paths: e.paths.map(toRootRelative)
4257
4476
  })),
4258
4477
  searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
4259
4478
  confirmedEntities: findings.confirmedEntities?.map((e) => ({
4260
4479
  ...e,
4261
- paths: e.paths.map(toWorktreeRelative)
4480
+ paths: e.paths.map(toRootRelative)
4262
4481
  }))
4263
4482
  };
4264
4483
  }
4265
4484
  function normalizeSearchLocation(path) {
4266
- const normalized = path ? toWorktreeRelative(path).trim() : "";
4485
+ const normalized = path ? toRootRelative(path).trim() : "";
4267
4486
  return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
4268
4487
  }
4269
- function toWorktreeRelative(p) {
4488
+ function toRootRelative(p) {
4270
4489
  return p.replace(/^\/+/, "");
4271
4490
  }
4272
- async function confirmDirtyWorkingTree(ctx, repoRoot) {
4273
- const MAX_LISTED_DIRTY_FILES = 10;
4274
- const dirty = await listChangedFiles(repoRoot);
4275
- const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
4276
- const overflow = dirty.length - shown.length;
4277
- const answer = await ctx.requestUserInput({
4278
- prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
4279
- promptType: "acceptReject",
4280
- options: [],
4281
- messages: [
4282
- `${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
4283
- ...shown.map((file) => ` \u2022 ${file}`),
4284
- ...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
4285
- "Commit or stash them first to include them in the implementation."
4286
- ]
4287
- });
4288
- if (answer !== true) {
4289
- throw new Error(
4290
- "implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
4291
- );
4292
- }
4293
- }
4294
4491
 
4295
4492
  // src/lib/algoliaDocs.ts
4296
4493
  import { readFileSync, readdirSync, existsSync } from "node:fs";
@@ -4350,11 +4547,6 @@ function getFrameworkSpecificDoc(frameworks) {
4350
4547
  return loadAlgoliaDoc("js");
4351
4548
  }
4352
4549
 
4353
- // src/lib/shell.ts
4354
- function shellQuote(value) {
4355
- return "'" + value.replace(/'/g, "'\\''") + "'";
4356
- }
4357
-
4358
4550
  // src/actions/resolveEnvVarPrefix.ts
4359
4551
  import z28 from "zod";
4360
4552
  var resolveEnvVarPrefixSchema = z28.object({
@@ -4374,9 +4566,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
4374
4566
 
4375
4567
  // src/actions/implement.ts
4376
4568
  var implementSchema = z29.object({
4377
- filesChanged: z29.array(z29.string()),
4378
4569
  summary: z29.string(),
4379
- worktreePath: z29.string().optional(),
4380
4570
  ingestCommand: z29.string().optional(),
4381
4571
  ingestScriptRan: z29.boolean().optional(),
4382
4572
  ingestRecordCount: z29.number().optional(),
@@ -4429,8 +4619,6 @@ function frameworksForDoc(language) {
4429
4619
  }
4430
4620
  function baseInstructions(input) {
4431
4621
  return [
4432
- // Agents have renamed this (e.g. appending the project name), which the
4433
- // index-scoped keys then reject with a 403.
4434
4622
  `Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
4435
4623
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4436
4624
  "Make minimal, idiomatic changes; do not touch unrelated code.",
@@ -4448,14 +4636,14 @@ function sourceSpecificInstructions(input) {
4448
4636
  "Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
4449
4637
  ],
4450
4638
  fileUpload: [
4451
- `Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
4639
+ `Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
4452
4640
  "Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
4453
4641
  "Map parsed columns/fields to the confirmed entity attributes.",
4454
4642
  "Never fabricate, hardcode, or substitute a different file."
4455
4643
  ],
4456
4644
  generated: [
4457
4645
  "No real data source exists; use sample records for each confirmed entity.",
4458
- "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.",
4646
+ "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, returning the file path. Do not write records or objectIDs yourself.",
4459
4647
  "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.",
4460
4648
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4461
4649
  ]
@@ -4504,14 +4692,11 @@ function searchInstructions(input) {
4504
4692
  "If a search box already exists, replace it with yours.",
4505
4693
  `Read the index name from the ${publicIndexNameVar(input.publicEnvVarPrefix)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4506
4694
  "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.",
4507
- // The key is provisioned only after verification passes, so the agent never
4508
- // sees one. It must also leave .env alone: the wizard reads that file to
4509
- // decide whether a key already exists, and an agent-invented value there
4510
- // would be reused as if it were real.
4695
+ // The key is provisioned only after verification passes, and the wizard
4696
+ // reads .env to decide whether a key already exists an agent-invented
4697
+ // value there would be reused as if it were real.
4511
4698
  `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.`,
4512
- // Not the agent's to rename: the wizard writes these exact names into
4513
- // ".env" right after this step, so a renamed prefix would leave the code
4514
- // reading a var the wizard never wrote.
4699
+ // The wizard writes these exact names into .env right after this step.
4515
4700
  `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4516
4701
  "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4517
4702
  "Match the styles of the application as closely as possible.",
@@ -4520,10 +4705,10 @@ function searchInstructions(input) {
4520
4705
  }
4521
4706
  function verificationInstructions(input) {
4522
4707
  return [
4523
- "Verify the Algolia implementation changes in the current worktree.",
4708
+ "Verify the Algolia implementation changes.",
4524
4709
  `Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
4525
4710
  "Run the project's own checks (lint, type check, tests) via runShell, using the commands the project actually defines \u2014 its task runner, manifest scripts, or Makefile. Run every check that applies, not just the first.",
4526
- "This worktree starts with no installed dependencies. If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
4711
+ "If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
4527
4712
  "For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
4528
4713
  "Do not make speculative fixes when no checks exist, a check cannot run, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
4529
4714
  "Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
@@ -4618,11 +4803,11 @@ function ingestFailure(attempt, executions) {
4618
4803
  }
4619
4804
  return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
4620
4805
  }
4621
- function makeToolContext(worktree, env = async () => ({})) {
4806
+ function makeToolContext(root, env = async () => ({})) {
4622
4807
  return createToolContext(
4623
4808
  DEFAULT_TOOL_LIMITS,
4624
- worktree,
4625
- createShellContext({ env, approve: storeApproval(worktree) })
4809
+ root,
4810
+ createShellContext({ env, approve: storeApproval(root) })
4626
4811
  );
4627
4812
  }
4628
4813
  function verificationRetryInstructions(verification) {
@@ -4630,7 +4815,7 @@ function verificationRetryInstructions(verification) {
4630
4815
  `Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
4631
4816
  ];
4632
4817
  }
4633
- async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
4818
+ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4634
4819
  const repoRoot = process.cwd();
4635
4820
  const scan = ctx.getStepOutput("project-scan");
4636
4821
  const entities = ctx.getStepOutput(
@@ -4711,9 +4896,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4711
4896
  const targetIndex = selected?.selection;
4712
4897
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4713
4898
  await assertGitRepoWithHead(repoRoot);
4714
- if (await isWorkingTreeDirty(repoRoot)) {
4715
- await confirmDirtyWorkingTree(ctx, repoRoot);
4716
- }
4717
4899
  const normalized = normalizeFindingPaths(findings);
4718
4900
  const confirmed2 = normalized.confirmedEntities;
4719
4901
  const searchLocation = normalized.searchImplementationAnalysis;
@@ -4725,328 +4907,303 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4725
4907
  if (useCases.includes("ingestion")) {
4726
4908
  ingestAppId = appId ?? (await requireApplication()).id;
4727
4909
  }
4728
- const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
4729
- try {
4730
- process.chdir(worktree);
4731
- let uploadFilePath;
4732
- let uploadWarning;
4733
- if (ingestionSource === "fileUpload") {
4734
- const copied = await copyUploadIntoWorktree(
4910
+ let uploadFilePath;
4911
+ let uploadWarning;
4912
+ if (ingestionSource === "fileUpload") {
4913
+ const copied = await copyUploadIntoProject(
4914
+ repoRoot,
4915
+ INGEST_DIR,
4916
+ uploadSourcePath ?? ""
4917
+ );
4918
+ if (copied.ok) {
4919
+ uploadFilePath = copied.relPath;
4920
+ } else {
4921
+ ingestionSource = "generated";
4922
+ uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4923
+ logger.warn(
4924
+ { reason: copied.reason },
4925
+ "implement: file upload unavailable; falling back to generated sample records"
4926
+ );
4927
+ }
4928
+ }
4929
+ const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
4930
+ const input = {
4931
+ findings: normalized,
4932
+ confirmed: confirmed2,
4933
+ searchLocation,
4934
+ targetIndex,
4935
+ language,
4936
+ publicEnvVarPrefix,
4937
+ appId,
4938
+ searchEnvVars: publicSearchEnvVars(publicEnvVarPrefix, targetIndex, appId),
4939
+ ingestDir: INGEST_DIR,
4940
+ ingestionSource,
4941
+ uploadFilePath,
4942
+ searchUiTarget: searchUiTarget(language)
4943
+ };
4944
+ const summaries = [];
4945
+ if (uploadWarning) summaries.push(uploadWarning);
4946
+ let envSearchKey;
4947
+ let envAppIdMismatch = false;
4948
+ if (useCases.includes("search") && appId) {
4949
+ const envAppId = await readEnvVar(
4950
+ repoRoot,
4951
+ publicAppIdVar(publicEnvVarPrefix)
4952
+ );
4953
+ if (envAppId === appId) {
4954
+ envSearchKey = await readEnvVar(
4735
4955
  repoRoot,
4736
- worktree,
4737
- INGEST_DIR,
4738
- uploadSourcePath ?? ""
4956
+ publicSearchKeyVar(publicEnvVarPrefix)
4957
+ );
4958
+ } else if (envAppId) {
4959
+ envAppIdMismatch = true;
4960
+ const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
4961
+ const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
4962
+ summaries.push(
4963
+ `\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
4964
+ );
4965
+ logger.warn(
4966
+ { envAppId, appId },
4967
+ "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4739
4968
  );
4740
- if (copied.ok) {
4741
- uploadFilePath = copied.relPath;
4742
- } else {
4743
- ingestionSource = "generated";
4744
- uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
4745
- logger.warn(
4746
- { reason: copied.reason },
4747
- "implement: file upload unavailable; falling back to generated sample records"
4748
- );
4749
- }
4750
4969
  }
4751
- const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
4752
- const input = {
4753
- findings: normalized,
4754
- confirmed: confirmed2,
4755
- searchLocation,
4756
- targetIndex,
4757
- language,
4758
- publicEnvVarPrefix,
4759
- appId,
4760
- searchEnvVars: publicSearchEnvVars(
4761
- publicEnvVarPrefix,
4762
- targetIndex,
4763
- appId
4970
+ }
4971
+ let finalSearchEnvVars = input.searchEnvVars;
4972
+ let agentRuns = 0;
4973
+ let ingestCommand;
4974
+ let ingestScriptRan = false;
4975
+ let ingestRecordCount;
4976
+ let ingestDurationMs;
4977
+ let ingestOutcomeMessage;
4978
+ const ingestKeyAppId = ingestAppId;
4979
+ const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
4980
+ [APP_ID_VAR]: ingestKeyAppId,
4981
+ [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4982
+ [INDEX_NAME_VAR]: targetIndex
4983
+ })) : void 0;
4984
+ const searchTools = makeToolContext(repoRoot);
4985
+ async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4986
+ if (agentRuns > 0) ctx.recordStepExecution();
4987
+ agentRuns += 1;
4988
+ return runAgent({
4989
+ instructions: buildAgentInstructions(
4990
+ currentUseCase,
4991
+ input,
4992
+ extraInstructions
4764
4993
  ),
4765
- ingestDir: INGEST_DIR,
4766
- ingestionSource,
4767
- uploadFilePath,
4768
- searchUiTarget: searchUiTarget(language)
4769
- };
4770
- const summaries = [];
4771
- if (uploadWarning) summaries.push(uploadWarning);
4772
- let envSearchKey;
4773
- let envAppIdMismatch = false;
4774
- if (useCases.includes("search") && appId) {
4775
- const envAppId = await readEnvVar(
4776
- worktree,
4777
- publicAppIdVar(publicEnvVarPrefix)
4994
+ tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4995
+ outputSchema: implementationOutputSchema,
4996
+ toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4997
+ });
4998
+ }
4999
+ async function runVerificationUseCase() {
5000
+ if (agentRuns > 0) ctx.recordStepExecution();
5001
+ agentRuns += 1;
5002
+ return runAgent({
5003
+ instructions: buildAgentInstructions("verification", input),
5004
+ tools: toolsForUseCase("verification"),
5005
+ outputSchema: verificationOutputSchema,
5006
+ toolContext: searchTools
5007
+ });
5008
+ }
5009
+ if (useCases.includes("ingestion")) {
5010
+ let ingestFailureDetail;
5011
+ const result = await runImplementationUseCase("ingestion");
5012
+ summaries.push(formatSummary("ingestion", result.summary));
5013
+ ingestCommand = result.ingestCommand;
5014
+ const ingestionContext = ingestionTools ?? searchTools;
5015
+ const executions = ingestionContext.shell.executions;
5016
+ const {
5017
+ run: ingestRun,
5018
+ attempt: ingestAttempt,
5019
+ recordCount
5020
+ } = ingestOutcome(executions, ingestCommand);
5021
+ ingestScriptRan = ingestRun != null;
5022
+ ingestRecordCount = recordCount;
5023
+ ingestDurationMs = ingestRun?.durationMs;
5024
+ if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
5025
+ summaries.push(
5026
+ "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
5027
+ );
5028
+ logger.warn(
5029
+ { ingestCommand },
5030
+ "implement: ingestion ran without a reviewScript call"
4778
5031
  );
4779
- if (envAppId === appId) {
4780
- envSearchKey = await readEnvVar(
4781
- worktree,
4782
- publicSearchKeyVar(publicEnvVarPrefix)
4783
- );
4784
- } else if (envAppId) {
4785
- envAppIdMismatch = true;
4786
- const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
4787
- const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
4788
- summaries.push(
4789
- `\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
4790
- );
4791
- logger.warn(
4792
- { envAppId, appId },
4793
- "implement: .env holds credentials for a different Algolia application; not reusing its search key"
4794
- );
4795
- }
4796
- }
4797
- let finalSearchEnvVars = input.searchEnvVars;
4798
- let agentRuns = 0;
4799
- let ingestCommand;
4800
- let ingestScriptRan = false;
4801
- let ingestRecordCount;
4802
- let ingestDurationMs;
4803
- let ingestOutcomeMessage;
4804
- const ingestKeyAppId = ingestAppId;
4805
- const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
4806
- [APP_ID_VAR]: ingestKeyAppId,
4807
- [API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
4808
- [INDEX_NAME_VAR]: targetIndex
4809
- })) : void 0;
4810
- const searchTools = makeToolContext(worktree);
4811
- async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
4812
- if (agentRuns > 0) ctx.recordStepExecution();
4813
- agentRuns += 1;
4814
- return runAgent({
4815
- instructions: buildAgentInstructions(
4816
- currentUseCase,
4817
- input,
4818
- extraInstructions
4819
- ),
4820
- tools: toolsForUseCase(currentUseCase, input.ingestionSource),
4821
- outputSchema: implementationOutputSchema,
4822
- toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
4823
- });
4824
- }
4825
- async function runVerificationUseCase() {
4826
- if (agentRuns > 0) ctx.recordStepExecution();
4827
- agentRuns += 1;
4828
- return runAgent({
4829
- instructions: buildAgentInstructions("verification", input),
4830
- tools: toolsForUseCase("verification"),
4831
- outputSchema: verificationOutputSchema,
4832
- toolContext: searchTools
4833
- });
4834
5032
  }
4835
- if (useCases.includes("ingestion")) {
4836
- let ingestFailureDetail;
4837
- const result = await runImplementationUseCase("ingestion");
4838
- summaries.push(formatSummary("ingestion", result.summary));
4839
- ingestCommand = result.ingestCommand;
4840
- const ingestionContext = ingestionTools ?? searchTools;
4841
- const executions = ingestionContext.shell.executions;
4842
- const {
4843
- run: ingestRun,
4844
- attempt: ingestAttempt,
4845
- recordCount
4846
- } = ingestOutcome(executions, ingestCommand);
4847
- ingestScriptRan = ingestRun != null;
4848
- ingestRecordCount = recordCount;
4849
- ingestDurationMs = ingestRun?.durationMs;
4850
- if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
4851
- summaries.push(
4852
- "\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
4853
- );
4854
- logger.warn(
4855
- { ingestCommand },
4856
- "implement: ingestion ran without a reviewScript call"
4857
- );
5033
+ if (ingestScriptRan) {
5034
+ ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
5035
+ if (ingestRecordCount != null) {
5036
+ track("AI Wizard Ingest Successful", {
5037
+ entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
5038
+ record_count: ingestRecordCount,
5039
+ duration_ms: ingestDurationMs ?? 0
5040
+ });
4858
5041
  }
4859
- if (ingestScriptRan) {
4860
- ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
4861
- if (ingestRecordCount != null) {
4862
- track("AI Wizard Ingest Successful", {
4863
- entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
4864
- record_count: ingestRecordCount,
4865
- duration_ms: ingestDurationMs ?? 0
4866
- });
4867
- }
4868
- } else {
4869
- const { reason, detail } = ingestFailure(ingestAttempt, executions);
4870
- ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
4871
- ingestFailureDetail = detail;
4872
- summaries.push(
4873
- `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
5042
+ } else {
5043
+ const { reason, detail } = ingestFailure(ingestAttempt, executions);
5044
+ ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
5045
+ ingestFailureDetail = detail;
5046
+ summaries.push(
5047
+ `\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
4874
5048
  ${detail}` : ""}`
4875
- );
4876
- logger.warn(
5049
+ );
5050
+ logger.warn(
5051
+ {
5052
+ ingestCommand,
5053
+ reason,
5054
+ approved: ingestAttempt?.approved,
5055
+ exitCode: ingestAttempt?.exitCode,
5056
+ timedOut: ingestAttempt?.timedOut,
5057
+ commandsRun: executions.length
5058
+ },
5059
+ "implement: ingestion script did not complete successfully"
5060
+ );
5061
+ track("Error", {
5062
+ step: "Push Data",
5063
+ error: `ingestion did not complete: ${reason}`,
5064
+ product_area: "AI Wizard"
5065
+ });
5066
+ }
5067
+ const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
5068
+ await ctx.requestUserInput({
5069
+ prompt: "",
5070
+ promptType: "enterToContinue",
5071
+ options: [],
5072
+ // One message per line: Notices.tsx counts a message as one wrapped
5073
+ // line, so an embedded newline overflows the panel's height accounting.
5074
+ messages: [
5075
+ ingestOutcomeMessage,
5076
+ ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
5077
+ ...commandMessages
5078
+ ]
5079
+ });
5080
+ }
5081
+ if (useCases.includes("search")) {
5082
+ let extraInstructions = [];
5083
+ useWizard.getState().clearWrittenFiles();
5084
+ for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
5085
+ if (attempt > 1) {
5086
+ logger.info(
4877
5087
  {
4878
- ingestCommand,
4879
- reason,
4880
- approved: ingestAttempt?.approved,
4881
- exitCode: ingestAttempt?.exitCode,
4882
- timedOut: ingestAttempt?.timedOut,
4883
- commandsRun: executions.length
5088
+ attempt,
5089
+ maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
5090
+ extraInstructions
4884
5091
  },
4885
- "implement: ingestion script did not complete successfully"
5092
+ "implement: retrying search implementation after failed verification"
4886
5093
  );
4887
- track("Error", {
4888
- step: "Push Data",
4889
- error: `ingestion did not complete: ${reason}`,
4890
- product_area: "AI Wizard"
5094
+ }
5095
+ const { summary } = await runImplementationUseCase(
5096
+ "search",
5097
+ extraInstructions
5098
+ );
5099
+ summaries.push(formatSummary("search", summary));
5100
+ const verification = await runVerificationUseCase();
5101
+ summaries.push(formatSummary("verification", verification.summary));
5102
+ if (verification.sufficient) {
5103
+ ctx.setUserInput("implementation", "success");
5104
+ const searchFilesChanged = [
5105
+ ...new Set(useWizard.getState().writtenFiles)
5106
+ ].map((file) => relative6(repoRoot, file));
5107
+ track("AI Wizard Frontend Component Generated", {
5108
+ filePaths: searchFilesChanged
4891
5109
  });
5110
+ track("AI Wizard Wired to UI", {
5111
+ location_heuristic: searchLocation ?? "unknown"
5112
+ });
5113
+ break;
4892
5114
  }
4893
- const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
4894
- if (ingestCommand) {
4895
- commandMessages.push(`Ingestion command: ${ingestCommand}`);
5115
+ if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
5116
+ ctx.setUserInput("implementation", "fail");
5117
+ throw new Error(
5118
+ `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
5119
+ );
4896
5120
  }
4897
- await ctx.requestUserInput({
4898
- prompt: "",
4899
- promptType: "enterToContinue",
4900
- options: [],
4901
- // The wizard never streams command output, so a failed run's tail is the
4902
- // only place the developer sees why it failed. One message per line:
4903
- // the panel's height accounting counts a message as one wrapped line
4904
- // (see Notices.tsx), so an embedded newline overflows it.
4905
- messages: [
4906
- ingestOutcomeMessage,
4907
- ...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
4908
- ...commandMessages
4909
- ]
4910
- });
5121
+ extraInstructions = verificationRetryInstructions(verification);
4911
5122
  }
4912
- if (useCases.includes("search")) {
4913
- let extraInstructions = [];
4914
- const preSearchFiles = new Set(await listChangedFiles(worktree));
4915
- for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
4916
- if (attempt > 1) {
4917
- logger.info(
4918
- {
4919
- attempt,
4920
- maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
4921
- extraInstructions
4922
- },
4923
- "implement: retrying search implementation after failed verification"
4924
- );
4925
- }
4926
- const { summary } = await runImplementationUseCase(
4927
- "search",
4928
- extraInstructions
5123
+ let searchKey;
5124
+ let searchKeyError;
5125
+ if (appId) {
5126
+ try {
5127
+ const resolved2 = await resolveSearchOnlyKey(
5128
+ targetIndex,
5129
+ appId,
5130
+ envSearchKey
5131
+ );
5132
+ searchKey = resolved2.key;
5133
+ summaries.push(
5134
+ resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
5135
+ );
5136
+ } catch (err) {
5137
+ searchKeyError = err.message;
5138
+ logger.warn(
5139
+ { err: searchKeyError },
5140
+ "implement: could not provision a search-only API key; the .env value stays a placeholder"
4929
5141
  );
4930
- summaries.push(formatSummary("search", summary));
4931
- const verification = await runVerificationUseCase();
4932
- summaries.push(formatSummary("verification", verification.summary));
4933
- if (verification.sufficient) {
4934
- ctx.setUserInput("implementation", "success");
4935
- const searchFilesChanged = (await listChangedFiles(worktree)).filter(
4936
- (file) => !preSearchFiles.has(file)
4937
- );
4938
- track("AI Wizard Frontend Component Generated", {
4939
- filePaths: searchFilesChanged
4940
- });
4941
- track("AI Wizard Wired to UI", {
4942
- location_heuristic: searchLocation ?? "unknown"
4943
- });
4944
- break;
4945
- }
4946
- if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
4947
- ctx.setUserInput("implementation", "fail");
4948
- throw new Error(
4949
- `Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
4950
- );
4951
- }
4952
- extraInstructions = verificationRetryInstructions(verification);
4953
- }
4954
- let searchKey;
4955
- let searchKeyError;
4956
- if (appId) {
4957
- try {
4958
- const resolved2 = await resolveSearchOnlyKey(
4959
- targetIndex,
4960
- appId,
4961
- envSearchKey
4962
- );
4963
- searchKey = resolved2.key;
4964
- summaries.push(
4965
- resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
4966
- );
4967
- } catch (err) {
4968
- searchKeyError = err.message;
4969
- logger.warn(
4970
- { err: searchKeyError },
4971
- "implement: could not provision a search-only API key; the .env value stays a placeholder"
4972
- );
4973
- }
4974
5142
  }
4975
- finalSearchEnvVars = publicSearchEnvVars(
4976
- publicEnvVarPrefix,
4977
- targetIndex,
4978
- appId,
4979
- searchKey
4980
- );
4981
- const resolvedSearchEnvVars = finalSearchEnvVars.filter(
4982
- (v) => !v.value.startsWith("<")
5143
+ }
5144
+ finalSearchEnvVars = publicSearchEnvVars(
5145
+ publicEnvVarPrefix,
5146
+ targetIndex,
5147
+ appId,
5148
+ searchKey
5149
+ );
5150
+ const resolvedSearchEnvVars = finalSearchEnvVars.filter(
5151
+ (v) => !v.value.startsWith("<")
5152
+ );
5153
+ if (resolvedSearchEnvVars.length > 0) {
5154
+ const written = await writeSearchEnvValues(
5155
+ repoRoot,
5156
+ resolvedSearchEnvVars
4983
5157
  );
4984
- if (resolvedSearchEnvVars.length > 0) {
4985
- const written = await writeSearchEnvValues(
4986
- worktree,
4987
- resolvedSearchEnvVars
5158
+ if (written.length > 0) {
5159
+ summaries.push(`Wrote ${written.join(", ")} to .env.`);
5160
+ }
5161
+ const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
5162
+ if (ignored === "added") {
5163
+ summaries.push("Added .env to .gitignore.");
5164
+ } else if (ignored === "tracked") {
5165
+ summaries.push(
5166
+ '\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.'
4988
5167
  );
4989
- if (written.length > 0) {
4990
- summaries.push(`Wrote ${written.join(", ")} to .env.`);
4991
- }
4992
- const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
4993
- if (ignored === "added") {
4994
- summaries.push("Added .env to .gitignore.");
4995
- } else if (ignored === "tracked") {
4996
- summaries.push(
4997
- '\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.'
4998
- );
4999
- }
5000
- const stale = [];
5001
- for (const v of resolvedSearchEnvVars) {
5002
- if (written.includes(v.name)) continue;
5003
- const current = await readEnvVar(worktree, v.name);
5004
- if (current && current !== v.value) stale.push(v);
5005
- }
5006
- if (stale.length > 0 && !envAppIdMismatch) {
5007
- summaries.push(
5008
- `\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
5009
- );
5010
- logger.warn(
5011
- { vars: stale.map((v) => v.name) },
5012
- "implement: .env holds different values for the resolved search credentials; not overwriting them"
5013
- );
5014
- }
5015
5168
  }
5016
- const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
5017
- (v) => v.value.startsWith("<")
5018
- );
5019
- if (unresolvedSearchEnvVars.length > 0) {
5169
+ const stale = [];
5170
+ for (const v of resolvedSearchEnvVars) {
5171
+ if (written.includes(v.name)) continue;
5172
+ const current = await readEnvVar(repoRoot, v.name);
5173
+ if (current && current !== v.value) stale.push(v);
5174
+ }
5175
+ if (stale.length > 0 && !envAppIdMismatch) {
5020
5176
  summaries.push(
5021
- `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
5022
- (searchKeyError ? ` Reason: ${searchKeyError}` : "")
5177
+ `\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
5178
+ );
5179
+ logger.warn(
5180
+ { vars: stale.map((v) => v.name) },
5181
+ "implement: .env holds different values for the resolved search credentials; not overwriting them"
5023
5182
  );
5024
5183
  }
5025
- } else {
5026
- ctx.setUserInput("implementation", "success");
5027
5184
  }
5028
- const filesChanged = await listChangedFiles(worktree);
5029
- if (filesChanged.length === 0) {
5030
- logger.warn(
5031
- "implement: agent reported success but no files changed in the worktree"
5185
+ const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
5186
+ (v) => v.value.startsWith("<")
5187
+ );
5188
+ if (unresolvedSearchEnvVars.length > 0) {
5189
+ summaries.push(
5190
+ `Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
5032
5191
  );
5033
5192
  }
5034
- return {
5035
- ingestionSource,
5036
- filesChanged,
5037
- summary: summaries.join("\n\n"),
5038
- worktreePath: worktree,
5039
- ...useCases.includes("ingestion") && ingestCommand ? {
5040
- ingestCommand,
5041
- ingestScriptRan,
5042
- ...ingestRecordCount != null ? { ingestRecordCount } : {},
5043
- ...ingestDurationMs != null ? { ingestDurationMs } : {}
5044
- } : {},
5045
- ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
5046
- };
5047
- } finally {
5048
- process.chdir(repoRoot);
5193
+ } else {
5194
+ ctx.setUserInput("implementation", "success");
5049
5195
  }
5196
+ return {
5197
+ ingestionSource,
5198
+ summary: summaries.join("\n\n"),
5199
+ ...useCases.includes("ingestion") && ingestCommand ? {
5200
+ ingestCommand,
5201
+ ingestScriptRan,
5202
+ ...ingestRecordCount != null ? { ingestRecordCount } : {},
5203
+ ...ingestDurationMs != null ? { ingestDurationMs } : {}
5204
+ } : {},
5205
+ ...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
5206
+ };
5050
5207
  }
5051
5208
 
5052
5209
  // src/workflows/default.ts
@@ -5118,10 +5275,7 @@ var defaultWorkflow = {
5118
5275
  ctx.notify({
5119
5276
  messages: ["Building your Algolia search experience\u2026"]
5120
5277
  });
5121
- const ingestion2 = ctx.getStepOutput(
5122
- "ingestion"
5123
- );
5124
- return implement(ctx, ["search"], ingestion2?.worktreePath);
5278
+ return implement(ctx, ["search"]);
5125
5279
  }
5126
5280
  }),
5127
5281
  defineStep({
@@ -5136,9 +5290,8 @@ var defaultWorkflow = {
5136
5290
  "ingestion"
5137
5291
  );
5138
5292
  return reviewStep(ctx, {
5139
- // The ingestion step already showed the user the exact `ingestCommand`
5140
- // and worktree path as a notice, so nextSteps must not restate it —
5141
- // an LLM-paraphrased command risks being wrong.
5293
+ // ingestCommand was already shown verbatim as a notice; an
5294
+ // LLM-paraphrased restatement in nextSteps risks being wrong.
5142
5295
  nextStepsGuidance: ingestion2?.ingestScriptRan ? "The wizard already ran the ingestion script and records are in the index. Do NOT tell the user to run it again; instead point them at the target index to confirm the records. Do not restate the ingestion command \u2014 the wizard already showed it to them." : "Tell the user to run the ingestion script; do not restate the exact command \u2014 the wizard already showed it to them above."
5143
5296
  });
5144
5297
  }
@@ -5185,7 +5338,6 @@ var selectIndex = {
5185
5338
  selection: "wizard_seed_products"
5186
5339
  };
5187
5340
  var ingestion = {
5188
- filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
5189
5341
  summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
5190
5342
  ingestCommand: "node algolia/ingest.mjs",
5191
5343
  ingestScriptRan: true,
@@ -5197,7 +5349,6 @@ var confirmFramework2 = {
5197
5349
  frameworks: projectScan2.frameworks
5198
5350
  };
5199
5351
  var search = {
5200
- filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
5201
5352
  summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
5202
5353
  ingestionSource: "generated",
5203
5354
  searchEnvVars: [
@@ -5210,7 +5361,7 @@ var review = {
5210
5361
  "Ingested 25 generated Product records into wizard_seed_products.",
5211
5362
  "Added an InstantSearch search experience to the shared header."
5212
5363
  ],
5213
- reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
5364
+ reviewPrompt: "Review the Algolia ingestion and search changes.",
5214
5365
  nextSteps: ["Point the ingestion script at your real product data."]
5215
5366
  };
5216
5367
  var SEEDS = {
@@ -5327,9 +5478,9 @@ Options:
5327
5478
  steps pre-filled with test data. Pass with no value to print
5328
5479
  the step ids. See CONTRIBUTING.md.
5329
5480
  --no-telemetry Send no telemetry or analytics for this run.
5330
- --reset-on-run Wipe this project's wizard state (run state, AI consent,
5331
- worktrees) before starting, so the run behaves like a
5332
- first-ever run. Also drops every API key the wizard has
5481
+ --reset-on-run Wipe this project's wizard state (run state, AI consent)
5482
+ before starting, so the run behaves like a first-ever
5483
+ run. Also drops every API key the wizard has
5333
5484
  stored in your keychain (or, where the platform has none,
5334
5485
  the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
5335
5486
  for this project and any other, so later runs create new
@@ -5369,7 +5520,7 @@ function parseCliArgs(argv) {
5369
5520
  }
5370
5521
 
5371
5522
  // src/lib/resetState.ts
5372
- import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
5523
+ import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
5373
5524
  import { join as join13 } from "node:path";
5374
5525
  var KEEP = ["wizard.log"];
5375
5526
  async function resetProjectState() {
@@ -5377,7 +5528,7 @@ async function resetProjectState() {
5377
5528
  await forgetResolvedKeys();
5378
5529
  let entries;
5379
5530
  try {
5380
- entries = await readdir4(dir);
5531
+ entries = await readdir3(dir);
5381
5532
  } catch {
5382
5533
  return { dir, removed: [] };
5383
5534
  }
@@ -5430,7 +5581,7 @@ function delay(ms) {
5430
5581
  }
5431
5582
 
5432
5583
  // src/main.tsx
5433
- import { jsx as jsx16 } from "react/jsx-runtime";
5584
+ import { jsx as jsx18 } from "react/jsx-runtime";
5434
5585
  async function startup() {
5435
5586
  setProjectRoot(process.cwd());
5436
5587
  let args;
@@ -5480,7 +5631,7 @@ ${formatStepList(workflow)}`);
5480
5631
  }
5481
5632
  async function run(workflow) {
5482
5633
  const store = useWizard.getState();
5483
- const instance = render(/* @__PURE__ */ jsx16(App, {}), { incrementalRendering: true });
5634
+ const instance = render(/* @__PURE__ */ jsx18(App, {}), { incrementalRendering: true });
5484
5635
  await store.waitForStart();
5485
5636
  let user = await getUser();
5486
5637
  if (!user) {