@algolia/wizard 0.30.0 → 0.32.0-rc.125.247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { render } from "ink";
|
|
5
5
|
|
|
6
6
|
// src/ui/App.tsx
|
|
7
|
-
import { Box as Box19, Text as Text19, useApp, useInput as
|
|
7
|
+
import { Box as Box19, Text as Text19, useApp, useInput as useInput6, useWindowSize as useWindowSize7 } from "ink";
|
|
8
8
|
import Spinner2 from "ink-spinner";
|
|
9
9
|
|
|
10
10
|
// src/core/store.ts
|
|
@@ -253,6 +253,7 @@ var useWizard = create((set, get) => ({
|
|
|
253
253
|
cliOutput: [],
|
|
254
254
|
targetIndex: null,
|
|
255
255
|
writtenFiles: [],
|
|
256
|
+
approvedCommands: /* @__PURE__ */ new Set(),
|
|
256
257
|
logs: [],
|
|
257
258
|
error: null,
|
|
258
259
|
inputReq: null,
|
|
@@ -348,6 +349,10 @@ var useWizard = create((set, get) => ({
|
|
|
348
349
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
349
350
|
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
350
351
|
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
352
|
+
isCommandApproved: (command, cwd) => get().approvedCommands.has(`${cwd}\0${command}`),
|
|
353
|
+
approveCommand: (command, cwd) => set((s) => ({
|
|
354
|
+
approvedCommands: new Set(s.approvedCommands).add(`${cwd}\0${command}`)
|
|
355
|
+
})),
|
|
351
356
|
logStart: (kind, name, input) => {
|
|
352
357
|
const id = nanoid();
|
|
353
358
|
set((s) => ({
|
|
@@ -396,6 +401,7 @@ var useWizard = create((set, get) => ({
|
|
|
396
401
|
cliOutput: [],
|
|
397
402
|
targetIndex: null,
|
|
398
403
|
writtenFiles: [],
|
|
404
|
+
approvedCommands: /* @__PURE__ */ new Set(),
|
|
399
405
|
logs: [],
|
|
400
406
|
error: null,
|
|
401
407
|
inputReq: null,
|
|
@@ -626,50 +632,107 @@ function Notices({
|
|
|
626
632
|
}
|
|
627
633
|
|
|
628
634
|
// src/ui/PromptInput.tsx
|
|
629
|
-
import { Box as Box9, Text as Text9
|
|
635
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
630
636
|
import TextInput from "ink-text-input";
|
|
631
|
-
import { useState as
|
|
637
|
+
import { useState as useState6 } from "react";
|
|
632
638
|
|
|
633
639
|
// src/ui/CommandApproval.tsx
|
|
640
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
641
|
+
|
|
642
|
+
// src/ui/DecisionSelect.tsx
|
|
634
643
|
import { Box as Box5, Text as Text5, useInput } from "ink";
|
|
644
|
+
import { useState as useState3 } from "react";
|
|
635
645
|
|
|
636
|
-
// src/ui/
|
|
646
|
+
// src/ui/SelectRow.tsx
|
|
637
647
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
638
|
-
import {
|
|
639
|
-
function
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
648
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
649
|
+
function SelectRow({
|
|
650
|
+
highlighted,
|
|
651
|
+
label,
|
|
652
|
+
width,
|
|
653
|
+
labelWidth,
|
|
654
|
+
highlightBackground = true,
|
|
655
|
+
usePadding = false,
|
|
656
|
+
children
|
|
643
657
|
}) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
658
|
+
const labelColor = highlighted ? highlightBackground ? COLORS.highlight.fg : COLORS.success : COLORS.primary;
|
|
659
|
+
return /* @__PURE__ */ jsxs3(
|
|
660
|
+
Box4,
|
|
661
|
+
{
|
|
662
|
+
width,
|
|
663
|
+
paddingX: usePadding ? 1 : 0,
|
|
664
|
+
paddingY: usePadding ? 1 : 0,
|
|
665
|
+
backgroundColor: highlighted && highlightBackground ? COLORS.highlight.bg : void 0,
|
|
666
|
+
children: [
|
|
667
|
+
/* @__PURE__ */ jsx3(Box4, { width: labelWidth, children: /* @__PURE__ */ jsxs3(
|
|
668
|
+
Text4,
|
|
669
|
+
{
|
|
670
|
+
color: labelColor,
|
|
671
|
+
bold: highlighted && !highlightBackground,
|
|
672
|
+
wrap: "truncate",
|
|
673
|
+
children: [
|
|
674
|
+
highlighted ? "\u276F " : " ",
|
|
675
|
+
label
|
|
676
|
+
]
|
|
677
|
+
}
|
|
678
|
+
) }),
|
|
679
|
+
children
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/ui/DecisionSelect.tsx
|
|
686
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
687
|
+
function DecisionSelect({
|
|
688
|
+
actions,
|
|
689
|
+
onDecide
|
|
690
|
+
}) {
|
|
691
|
+
const [index, setIndex] = useState3(0);
|
|
692
|
+
useInput((input, key) => {
|
|
693
|
+
if (key.upArrow || input === "k") {
|
|
694
|
+
setIndex((i) => (i - 1 + actions.length) % actions.length);
|
|
695
|
+
} else if (key.downArrow || input === "j") {
|
|
696
|
+
setIndex((i) => (i + 1) % actions.length);
|
|
697
|
+
} else if (key.return) {
|
|
698
|
+
onDecide(actions[index].value);
|
|
699
|
+
} else if (key.escape) {
|
|
700
|
+
onDecide(actions[1].value);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
|
|
704
|
+
/* @__PURE__ */ jsx4(Box5, { flexDirection: "column", children: actions.map((action, i) => /* @__PURE__ */ jsx4(
|
|
705
|
+
SelectRow,
|
|
706
|
+
{
|
|
707
|
+
highlighted: i === index,
|
|
708
|
+
highlightBackground: false,
|
|
709
|
+
label: action.label
|
|
710
|
+
},
|
|
711
|
+
action.label
|
|
712
|
+
)) }),
|
|
713
|
+
/* @__PURE__ */ jsxs4(Box5, { gap: 2, children: [
|
|
714
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
715
|
+
/* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, children: "[\u2191] [\u2193]" }),
|
|
716
|
+
/* @__PURE__ */ jsx4(Text5, { color: COLORS.dim, children: " move" })
|
|
717
|
+
] }),
|
|
718
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
719
|
+
/* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, children: "[enter]" }),
|
|
720
|
+
/* @__PURE__ */ jsx4(Text5, { color: COLORS.dim, children: " confirm" })
|
|
721
|
+
] })
|
|
655
722
|
] })
|
|
656
723
|
] });
|
|
657
724
|
}
|
|
658
725
|
|
|
659
726
|
// src/ui/CommandApproval.tsx
|
|
660
|
-
import { jsx as
|
|
727
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
661
728
|
function CommandApproval({
|
|
662
729
|
command,
|
|
663
730
|
onDecide
|
|
664
731
|
}) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", gap: 1, children: [
|
|
670
|
-
/* @__PURE__ */ jsx4(Text5, { color: COLORS.primary, bold: true, children: "Run this command?" }),
|
|
671
|
-
/* @__PURE__ */ jsxs4(
|
|
672
|
-
Box5,
|
|
732
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", gap: 1, children: [
|
|
733
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.primary, bold: true, children: "Run this command?" }),
|
|
734
|
+
/* @__PURE__ */ jsxs5(
|
|
735
|
+
Box6,
|
|
673
736
|
{
|
|
674
737
|
flexDirection: "column",
|
|
675
738
|
paddingLeft: 2,
|
|
@@ -680,36 +743,42 @@ function CommandApproval({
|
|
|
680
743
|
borderRight: false,
|
|
681
744
|
gap: 1,
|
|
682
745
|
children: [
|
|
683
|
-
/* @__PURE__ */
|
|
684
|
-
/* @__PURE__ */
|
|
685
|
-
/* @__PURE__ */
|
|
746
|
+
/* @__PURE__ */ jsxs5(Box6, { children: [
|
|
747
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: "$ " }),
|
|
748
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.strong, wrap: "wrap", children: command.command })
|
|
686
749
|
] }),
|
|
687
|
-
/* @__PURE__ */
|
|
688
|
-
/* @__PURE__ */
|
|
689
|
-
/* @__PURE__ */
|
|
750
|
+
/* @__PURE__ */ jsxs5(Box6, { gap: 1, children: [
|
|
751
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: "in:" }),
|
|
752
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, wrap: "wrap", children: command.cwd })
|
|
690
753
|
] }),
|
|
691
|
-
command.explanation && /* @__PURE__ */
|
|
692
|
-
/* @__PURE__ */
|
|
693
|
-
/* @__PURE__ */
|
|
754
|
+
command.explanation && /* @__PURE__ */ jsxs5(Box6, { gap: 1, children: [
|
|
755
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.muted, children: "why:" }),
|
|
756
|
+
/* @__PURE__ */ jsx5(Text6, { color: COLORS.accent, wrap: "wrap", children: command.explanation })
|
|
694
757
|
] })
|
|
695
758
|
]
|
|
696
759
|
}
|
|
697
760
|
),
|
|
698
|
-
/* @__PURE__ */
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
761
|
+
/* @__PURE__ */ jsx5(
|
|
762
|
+
DecisionSelect,
|
|
763
|
+
{
|
|
764
|
+
actions: [
|
|
765
|
+
{ label: "approve", value: "approve" },
|
|
766
|
+
{ label: "reject", value: "reject" }
|
|
767
|
+
],
|
|
768
|
+
onDecide
|
|
769
|
+
}
|
|
770
|
+
)
|
|
702
771
|
] });
|
|
703
772
|
}
|
|
704
773
|
|
|
705
774
|
// src/ui/SelectPrompt.tsx
|
|
706
775
|
import { Box as Box8, Text as Text8, useInput as useInput2, useWindowSize as useWindowSize4 } from "ink";
|
|
707
|
-
import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as
|
|
776
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
|
|
708
777
|
|
|
709
778
|
// src/ui/ScrollView.tsx
|
|
710
|
-
import { Box as
|
|
711
|
-
import { useCallback, useLayoutEffect, useRef as useRef2, useState as
|
|
712
|
-
import { jsxs as
|
|
779
|
+
import { Box as Box7, Text as Text7, measureElement as measureElement2, useWindowSize as useWindowSize3 } from "ink";
|
|
780
|
+
import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
|
|
781
|
+
import { jsxs as jsxs6 } from "react/jsx-runtime";
|
|
713
782
|
var INDICATOR_ROWS = 2;
|
|
714
783
|
function fittedWidth(node, columns) {
|
|
715
784
|
let left = 0;
|
|
@@ -725,7 +794,7 @@ function useScrollWindow({
|
|
|
725
794
|
}) {
|
|
726
795
|
const viewportRef = useRef2(null);
|
|
727
796
|
const { columns } = useWindowSize3();
|
|
728
|
-
const [size, setSize] =
|
|
797
|
+
const [size, setSize] = useState4(
|
|
729
798
|
null
|
|
730
799
|
);
|
|
731
800
|
useLayoutEffect(() => {
|
|
@@ -738,7 +807,7 @@ function useScrollWindow({
|
|
|
738
807
|
});
|
|
739
808
|
const capacity = size === null || itemCount * rowHeight <= size.height ? itemCount : Math.max(Math.floor((size.height - INDICATOR_ROWS) / rowHeight), 1);
|
|
740
809
|
const maxOffset = Math.max(itemCount - capacity, 0);
|
|
741
|
-
const [offset, setOffset] =
|
|
810
|
+
const [offset, setOffset] = useState4(0);
|
|
742
811
|
const prevMaxOffsetRef = useRef2(0);
|
|
743
812
|
useLayoutEffect(() => {
|
|
744
813
|
const wasAtBottom = offset >= prevMaxOffsetRef.current;
|
|
@@ -779,14 +848,14 @@ function useScrollWindow({
|
|
|
779
848
|
};
|
|
780
849
|
}
|
|
781
850
|
function ScrollView({ scroll, children }) {
|
|
782
|
-
return /* @__PURE__ */
|
|
783
|
-
scroll.hiddenAbove > 0 && /* @__PURE__ */
|
|
851
|
+
return /* @__PURE__ */ jsxs6(Box7, { ref: scroll.viewportRef, flexDirection: "column", flexGrow: 1, children: [
|
|
852
|
+
scroll.hiddenAbove > 0 && /* @__PURE__ */ jsxs6(Text7, { color: COLORS.dim, children: [
|
|
784
853
|
"\u2191 ",
|
|
785
854
|
scroll.hiddenAbove,
|
|
786
855
|
" more"
|
|
787
856
|
] }),
|
|
788
857
|
children,
|
|
789
|
-
scroll.hiddenBelow > 0 && /* @__PURE__ */
|
|
858
|
+
scroll.hiddenBelow > 0 && /* @__PURE__ */ jsxs6(Text7, { color: COLORS.dim, children: [
|
|
790
859
|
"\u2193 ",
|
|
791
860
|
scroll.hiddenBelow,
|
|
792
861
|
" more"
|
|
@@ -794,45 +863,6 @@ function ScrollView({ scroll, children }) {
|
|
|
794
863
|
] });
|
|
795
864
|
}
|
|
796
865
|
|
|
797
|
-
// src/ui/SelectRow.tsx
|
|
798
|
-
import { Box as Box7, Text as Text7 } from "ink";
|
|
799
|
-
import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
800
|
-
function SelectRow({
|
|
801
|
-
highlighted,
|
|
802
|
-
label,
|
|
803
|
-
width,
|
|
804
|
-
labelWidth,
|
|
805
|
-
highlightBackground = true,
|
|
806
|
-
usePadding = false,
|
|
807
|
-
children
|
|
808
|
-
}) {
|
|
809
|
-
const labelColor = highlighted ? highlightBackground ? COLORS.highlight.fg : COLORS.success : void 0;
|
|
810
|
-
return /* @__PURE__ */ jsxs6(
|
|
811
|
-
Box7,
|
|
812
|
-
{
|
|
813
|
-
width,
|
|
814
|
-
paddingX: usePadding ? 1 : 0,
|
|
815
|
-
paddingY: usePadding ? 1 : 0,
|
|
816
|
-
backgroundColor: highlighted && highlightBackground ? COLORS.highlight.bg : void 0,
|
|
817
|
-
children: [
|
|
818
|
-
/* @__PURE__ */ jsx5(Box7, { width: labelWidth, children: /* @__PURE__ */ jsxs6(
|
|
819
|
-
Text7,
|
|
820
|
-
{
|
|
821
|
-
color: labelColor,
|
|
822
|
-
bold: highlighted && !highlightBackground,
|
|
823
|
-
wrap: "truncate",
|
|
824
|
-
children: [
|
|
825
|
-
highlighted ? "\u276F " : " ",
|
|
826
|
-
label
|
|
827
|
-
]
|
|
828
|
-
}
|
|
829
|
-
) }),
|
|
830
|
-
children
|
|
831
|
-
]
|
|
832
|
-
}
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
866
|
// src/ui/SelectPrompt.tsx
|
|
837
867
|
import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
838
868
|
var CANCEL = "cancel";
|
|
@@ -853,10 +883,10 @@ function SelectPrompt({
|
|
|
853
883
|
secondary,
|
|
854
884
|
defaultSelectedIndex = 0
|
|
855
885
|
}) {
|
|
856
|
-
const [index, setIndex] =
|
|
886
|
+
const [index, setIndex] = useState5(
|
|
857
887
|
() => defaultSelectedIndex > 0 && defaultSelectedIndex < options.length ? defaultSelectedIndex : 0
|
|
858
888
|
);
|
|
859
|
-
const [checked, setChecked] =
|
|
889
|
+
const [checked, setChecked] = useState5(() => /* @__PURE__ */ new Set());
|
|
860
890
|
const hasCancel = Boolean(multi || cancelable);
|
|
861
891
|
const rows = hasCancel ? [...options, "Cancel"] : options;
|
|
862
892
|
const cancelIndex = hasCancel ? options.length : -1;
|
|
@@ -866,7 +896,7 @@ function SelectPrompt({
|
|
|
866
896
|
hints.push({ key: "[enter]", label: "confirm" });
|
|
867
897
|
const containerRef = useRef3(null);
|
|
868
898
|
const { columns } = useWindowSize4();
|
|
869
|
-
const [width, setWidth] =
|
|
899
|
+
const [width, setWidth] = useState5(columns);
|
|
870
900
|
useLayoutEffect2(() => {
|
|
871
901
|
if (!containerRef.current) return;
|
|
872
902
|
const measured = fittedWidth(containerRef.current, columns);
|
|
@@ -978,22 +1008,24 @@ function EnterToContinuePrompt({
|
|
|
978
1008
|
messages,
|
|
979
1009
|
onDecide
|
|
980
1010
|
}) {
|
|
981
|
-
useInput3((_input, key) => {
|
|
982
|
-
if (key.return) onDecide(true);
|
|
983
|
-
else if (key.escape) onDecide(false);
|
|
984
|
-
});
|
|
985
1011
|
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", gap: 1, children: [
|
|
986
1012
|
messages?.map((m, i) => /* @__PURE__ */ jsx7(Text9, { color: COLORS.muted, children: m }, `msg-${i}`)),
|
|
987
1013
|
question && /* @__PURE__ */ jsx7(Text9, { color: COLORS.primary, children: question }),
|
|
988
|
-
/* @__PURE__ */
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1014
|
+
/* @__PURE__ */ jsx7(
|
|
1015
|
+
DecisionSelect,
|
|
1016
|
+
{
|
|
1017
|
+
actions: [
|
|
1018
|
+
{ label: "continue", value: true },
|
|
1019
|
+
{ label: "decline", value: false }
|
|
1020
|
+
],
|
|
1021
|
+
onDecide
|
|
1022
|
+
}
|
|
1023
|
+
)
|
|
992
1024
|
] });
|
|
993
1025
|
}
|
|
994
1026
|
function PromptInput() {
|
|
995
1027
|
const { phase, inputReq, submitInput } = useWizard();
|
|
996
|
-
const [draft, setDraft] =
|
|
1028
|
+
const [draft, setDraft] = useState6("");
|
|
997
1029
|
if (phase === "done" || phase === "error") {
|
|
998
1030
|
return /* @__PURE__ */ jsx7(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text9, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
|
|
999
1031
|
}
|
|
@@ -1093,8 +1125,8 @@ function PromptInput() {
|
|
|
1093
1125
|
// src/ui/Welcome.tsx
|
|
1094
1126
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1095
1127
|
import { fileURLToPath } from "node:url";
|
|
1096
|
-
import { useState as
|
|
1097
|
-
import { Box as Box10, Spacer, Text as Text10, useInput as
|
|
1128
|
+
import { useState as useState7 } from "react";
|
|
1129
|
+
import { Box as Box10, Spacer, Text as Text10, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
|
|
1098
1130
|
|
|
1099
1131
|
// src/ui/copy/welcome.ts
|
|
1100
1132
|
var sidebarItems = [
|
|
@@ -1152,8 +1184,8 @@ function Welcome() {
|
|
|
1152
1184
|
{ label: "start wizard", run: confirmStart },
|
|
1153
1185
|
{ label: "learn more", run: openLearnMore }
|
|
1154
1186
|
];
|
|
1155
|
-
const [index, setIndex] =
|
|
1156
|
-
|
|
1187
|
+
const [index, setIndex] = useState7(0);
|
|
1188
|
+
useInput3((input, key) => {
|
|
1157
1189
|
if (key.upArrow || input === "k") {
|
|
1158
1190
|
setIndex((i) => (i - 1 + actions.length) % actions.length);
|
|
1159
1191
|
} else if (key.downArrow || input === "j") {
|
|
@@ -1239,8 +1271,8 @@ function Welcome() {
|
|
|
1239
1271
|
}
|
|
1240
1272
|
|
|
1241
1273
|
// src/ui/LearnMore.tsx
|
|
1242
|
-
import { Fragment
|
|
1243
|
-
import { Box as Box11, Text as Text11, useInput as
|
|
1274
|
+
import { Fragment } from "react";
|
|
1275
|
+
import { Box as Box11, Text as Text11, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
|
|
1244
1276
|
|
|
1245
1277
|
// src/ui/copy/learn-more.ts
|
|
1246
1278
|
var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
|
|
@@ -1312,7 +1344,7 @@ function LearnMore() {
|
|
|
1312
1344
|
const backToHome = useWizard((s) => s.backToHome);
|
|
1313
1345
|
const { columns } = useWindowSize6();
|
|
1314
1346
|
const dividerWidth = Math.max(0, columns - PADDING_X * 2);
|
|
1315
|
-
|
|
1347
|
+
useInput4((_input, key) => {
|
|
1316
1348
|
if (key.escape) backToHome();
|
|
1317
1349
|
else if (key.return) confirmStart();
|
|
1318
1350
|
});
|
|
@@ -1347,7 +1379,7 @@ function LearnMore() {
|
|
|
1347
1379
|
segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
|
|
1348
1380
|
}
|
|
1349
1381
|
),
|
|
1350
|
-
neverItems.map((item) => /* @__PURE__ */ jsxs10(
|
|
1382
|
+
neverItems.map((item) => /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
1351
1383
|
/* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
|
|
1352
1384
|
/* @__PURE__ */ jsx9(
|
|
1353
1385
|
NeverLine,
|
|
@@ -1520,10 +1552,10 @@ function Ribbon() {
|
|
|
1520
1552
|
}
|
|
1521
1553
|
|
|
1522
1554
|
// src/ui/App.tsx
|
|
1523
|
-
import { useState as
|
|
1555
|
+
import { useState as useState9 } from "react";
|
|
1524
1556
|
|
|
1525
1557
|
// src/ui/Logs.tsx
|
|
1526
|
-
import { Box as Box16, Text as Text16, useInput as
|
|
1558
|
+
import { Box as Box16, Text as Text16, useInput as useInput5 } from "ink";
|
|
1527
1559
|
import { jsx as jsx14, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1528
1560
|
var KIND_COLOR = {
|
|
1529
1561
|
tool: COLORS.primary,
|
|
@@ -1555,7 +1587,7 @@ function formatTimestamp(ms) {
|
|
|
1555
1587
|
function Logs() {
|
|
1556
1588
|
const logs = useWizard((s) => s.logs);
|
|
1557
1589
|
const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
|
|
1558
|
-
|
|
1590
|
+
useInput5((_input, key) => {
|
|
1559
1591
|
if (key.upArrow) scroll.scrollBy(-1);
|
|
1560
1592
|
else if (key.downArrow) scroll.scrollBy(1);
|
|
1561
1593
|
});
|
|
@@ -1771,7 +1803,7 @@ function track(event, payload) {
|
|
|
1771
1803
|
}
|
|
1772
1804
|
|
|
1773
1805
|
// src/ui/Tips.tsx
|
|
1774
|
-
import { useEffect as useEffect3, useState as
|
|
1806
|
+
import { useEffect as useEffect3, useState as useState8 } from "react";
|
|
1775
1807
|
import { Box as Box18, Text as Text18 } from "ink";
|
|
1776
1808
|
import terminalLink from "terminal-link";
|
|
1777
1809
|
|
|
@@ -1998,8 +2030,8 @@ function TipContent({
|
|
|
1998
2030
|
) });
|
|
1999
2031
|
}
|
|
2000
2032
|
function Tips() {
|
|
2001
|
-
const [tipIndex, setTipIndex] =
|
|
2002
|
-
const [revealed, setRevealed] =
|
|
2033
|
+
const [tipIndex, setTipIndex] = useState8(0);
|
|
2034
|
+
const [revealed, setRevealed] = useState8(0);
|
|
2003
2035
|
const tip = tips[tipIndex];
|
|
2004
2036
|
const contentLength = tip.chunks.reduce((sum, c) => sum + c.value.length, 0);
|
|
2005
2037
|
const totalLength = tip.title.length + contentLength;
|
|
@@ -2059,7 +2091,7 @@ function App() {
|
|
|
2059
2091
|
} = useWizard();
|
|
2060
2092
|
const { exit } = useApp();
|
|
2061
2093
|
const { columns, rows } = useWindowSize7();
|
|
2062
|
-
const [showLogs, setShowLogs] =
|
|
2094
|
+
const [showLogs, setShowLogs] = useState9(false);
|
|
2063
2095
|
const finished = phase === "done" || phase === "error";
|
|
2064
2096
|
const currentStep = steps[currentStepIndex];
|
|
2065
2097
|
const isInitialAnalysisStep = currentStepIndex === 0;
|
|
@@ -2067,7 +2099,7 @@ function App() {
|
|
|
2067
2099
|
const showTips = phase === "running" && isInitialAnalysisStep && notices.length > 0;
|
|
2068
2100
|
const showNoticesInMain = !isInitialAnalysisStep;
|
|
2069
2101
|
const showNotices = !isAwaitingUserInput;
|
|
2070
|
-
|
|
2102
|
+
useInput6(
|
|
2071
2103
|
(_input, key) => {
|
|
2072
2104
|
if (key.return) {
|
|
2073
2105
|
exit();
|
|
@@ -2075,7 +2107,7 @@ function App() {
|
|
|
2075
2107
|
},
|
|
2076
2108
|
{ isActive: finished }
|
|
2077
2109
|
);
|
|
2078
|
-
|
|
2110
|
+
useInput6((_input, key) => {
|
|
2079
2111
|
if (phase === "idle" || phase === "authenticating") return;
|
|
2080
2112
|
if (key.tab) {
|
|
2081
2113
|
setShowLogs(!showLogs);
|
|
@@ -2087,7 +2119,7 @@ function App() {
|
|
|
2087
2119
|
}
|
|
2088
2120
|
});
|
|
2089
2121
|
const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
|
|
2090
|
-
|
|
2122
|
+
useInput6((_input, key) => {
|
|
2091
2123
|
if (escOwnedElsewhere) return;
|
|
2092
2124
|
if (key.escape) {
|
|
2093
2125
|
track("AI Wizard Interaction", {
|
|
@@ -2614,7 +2646,7 @@ async function ensureApplication() {
|
|
|
2614
2646
|
}
|
|
2615
2647
|
|
|
2616
2648
|
// src/workflows/default.ts
|
|
2617
|
-
import { z as
|
|
2649
|
+
import { z as z29 } from "zod";
|
|
2618
2650
|
|
|
2619
2651
|
// src/actions/listIndices.ts
|
|
2620
2652
|
import { z as z5 } from "zod";
|
|
@@ -2684,8 +2716,8 @@ var selectIndexStep = async (ctx) => {
|
|
|
2684
2716
|
};
|
|
2685
2717
|
|
|
2686
2718
|
// src/lib/agent.ts
|
|
2687
|
-
import { ToolLoopAgent, hasToolCall, Output as
|
|
2688
|
-
import { createAnthropic as
|
|
2719
|
+
import { ToolLoopAgent, hasToolCall, Output as Output3 } from "ai";
|
|
2720
|
+
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
2689
2721
|
import "zod";
|
|
2690
2722
|
|
|
2691
2723
|
// src/lib/tools/index.ts
|
|
@@ -2728,17 +2760,23 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
2728
2760
|
// src/lib/tools/listFiles.ts
|
|
2729
2761
|
function listFilesTool(ctx) {
|
|
2730
2762
|
return tool({
|
|
2731
|
-
description:
|
|
2732
|
-
inputSchema: z6.object(
|
|
2733
|
-
|
|
2734
|
-
|
|
2763
|
+
description: 'List files in a directory (default: the current working directory). Pass path to list a subdirectory directly \u2014 e.g. "packages/api" \u2014 without first changeDirectory-ing into it.',
|
|
2764
|
+
inputSchema: z6.object({
|
|
2765
|
+
path: z6.string().optional().describe("Directory to list, relative to cwd (default: cwd)")
|
|
2766
|
+
}),
|
|
2767
|
+
execute: async ({ path = "." }) => {
|
|
2768
|
+
logger.info({ path }, "called listFiles tool");
|
|
2735
2769
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2736
2770
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
2737
2771
|
}
|
|
2738
|
-
const resolved2 = resolveInRoot(ctx,
|
|
2772
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2739
2773
|
if (!resolved2.ok) return resolved2.error;
|
|
2740
|
-
|
|
2741
|
-
|
|
2774
|
+
try {
|
|
2775
|
+
const entries = await readdir(resolved2.target, { withFileTypes: true });
|
|
2776
|
+
return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
return `Error listing ${path}: ${err.message}`;
|
|
2779
|
+
}
|
|
2742
2780
|
}
|
|
2743
2781
|
});
|
|
2744
2782
|
}
|
|
@@ -3048,8 +3086,7 @@ function resolveWriteKey(index, appId) {
|
|
|
3048
3086
|
`Algolia Wizard write key for ${index} index`
|
|
3049
3087
|
);
|
|
3050
3088
|
}
|
|
3051
|
-
async function resolveSearchOnlyKey(index, appId
|
|
3052
|
-
if (envKey) return { key: envKey, source: "env" };
|
|
3089
|
+
async function resolveSearchOnlyKey(index, appId) {
|
|
3053
3090
|
return resolveKey(
|
|
3054
3091
|
"search",
|
|
3055
3092
|
index,
|
|
@@ -3088,6 +3125,12 @@ function isIgnoredByRule(root, relPath) {
|
|
|
3088
3125
|
function isTracked(root, relPath) {
|
|
3089
3126
|
return gitSucceeds(root, ["ls-files", "--error-unmatch", "--", relPath]);
|
|
3090
3127
|
}
|
|
3128
|
+
async function gitIgnoreStatus(root, target) {
|
|
3129
|
+
const { ignoredByRule, tracked } = await inspect(root, target);
|
|
3130
|
+
if (ignoredByRule === void 0) return "unknown";
|
|
3131
|
+
if (tracked) return "tracked";
|
|
3132
|
+
return ignoredByRule ? "covered" : "needsRule";
|
|
3133
|
+
}
|
|
3091
3134
|
async function inspect(root, target) {
|
|
3092
3135
|
const relPath = relative2(root, target);
|
|
3093
3136
|
if (!relPath || relPath.startsWith("..")) {
|
|
@@ -3133,31 +3176,9 @@ async function ensureGitIgnored(root, target) {
|
|
|
3133
3176
|
}
|
|
3134
3177
|
|
|
3135
3178
|
// src/lib/tools/writeAlgoliaCredentials.ts
|
|
3136
|
-
var APP_ID_VAR = "
|
|
3137
|
-
var API_KEY_VAR = "
|
|
3179
|
+
var APP_ID_VAR = "ALGOLIA_APP_ID";
|
|
3180
|
+
var API_KEY_VAR = "ALGOLIA_WRITE_KEY";
|
|
3138
3181
|
var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
|
|
3139
|
-
var PUBLIC_APP_ID_SUFFIX = "ALGOLIA_APP_ID";
|
|
3140
|
-
var PUBLIC_SEARCH_KEY_SUFFIX = "ALGOLIA_SEARCH_KEY";
|
|
3141
|
-
var PUBLIC_INDEX_NAME_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
3142
|
-
function publicAppIdVar(prefix) {
|
|
3143
|
-
return `${prefix}${PUBLIC_APP_ID_SUFFIX}`;
|
|
3144
|
-
}
|
|
3145
|
-
function publicSearchKeyVar(prefix) {
|
|
3146
|
-
return `${prefix}${PUBLIC_SEARCH_KEY_SUFFIX}`;
|
|
3147
|
-
}
|
|
3148
|
-
function publicIndexNameVar(prefix) {
|
|
3149
|
-
return `${prefix}${PUBLIC_INDEX_NAME_SUFFIX}`;
|
|
3150
|
-
}
|
|
3151
|
-
function publicSearchEnvVars(prefix, index, appId, searchKey) {
|
|
3152
|
-
return [
|
|
3153
|
-
{ name: publicAppIdVar(prefix), value: appId ?? "<your-algolia-app-id>" },
|
|
3154
|
-
{
|
|
3155
|
-
name: publicSearchKeyVar(prefix),
|
|
3156
|
-
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
3157
|
-
},
|
|
3158
|
-
{ name: publicIndexNameVar(prefix), value: index }
|
|
3159
|
-
];
|
|
3160
|
-
}
|
|
3161
3182
|
function appendEnv(content, entries) {
|
|
3162
3183
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
3163
3184
|
const lines = entries.map(([name, value]) => `${name}=${value}
|
|
@@ -3165,11 +3186,13 @@ function appendEnv(content, entries) {
|
|
|
3165
3186
|
return content + prefix + lines;
|
|
3166
3187
|
}
|
|
3167
3188
|
function hasEnv(content, name) {
|
|
3168
|
-
return new RegExp(`^(\\
|
|
3189
|
+
return new RegExp(`^([ \\t]*(?:export[ \\t]+)?${name})[ \\t]*=`, "m").test(
|
|
3190
|
+
content
|
|
3191
|
+
);
|
|
3169
3192
|
}
|
|
3170
3193
|
function readEnv(content, name) {
|
|
3171
3194
|
const found = content.match(
|
|
3172
|
-
new RegExp(
|
|
3195
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`, "m")
|
|
3173
3196
|
);
|
|
3174
3197
|
if (!found) return null;
|
|
3175
3198
|
const raw = found[1].trim();
|
|
@@ -3180,16 +3203,16 @@ function readEnv(content, name) {
|
|
|
3180
3203
|
function upsertEnv(content, name, value) {
|
|
3181
3204
|
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
3182
3205
|
return content.replace(
|
|
3183
|
-
new RegExp(
|
|
3206
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=.*$`, "gm"),
|
|
3184
3207
|
() => `${name}=${value}`
|
|
3185
3208
|
);
|
|
3186
3209
|
}
|
|
3187
3210
|
function writeCredentialsTool(ctx) {
|
|
3188
3211
|
return tool6({
|
|
3189
|
-
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself.`,
|
|
3212
|
+
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself. If the script or app that reads these credentials lives in a subdirectory (e.g. a package in a monorepo), an env file at the repo root is the wrong default \u2014 a script only loads env vars from its own directory (or one it's explicitly configured to read), so check every directory from the script's own up to the repo root, not just those two: its own directory, each ancestor in between (a shared workspace-level directory above the immediate package is common), and the root. Use whichever of those already holds real credentials; only fall back to the repo root when none of them do. Never invent a brand-new file in one of those directories when a real one already exists in another \u2014 that leaves the real one stale and the new one wrong. Listing just the script's own directory and the very top-level root is not enough to find a workspace-level file in between; check the intermediate ones too. If instructions describe a location that doesn't match the project you actually find (e.g. a path outside the repo, or a convention the project doesn't follow), don't stop and ask before doing anything \u2014 call this tool on the real, in-repo file the script actually reads (that's always the safe default), then note the mismatch afterward. Ending your turn with only a question and no call to this tool leaves the project unconfigured.`,
|
|
3190
3213
|
inputSchema: z13.object({
|
|
3191
3214
|
filePath: z13.string().describe(
|
|
3192
|
-
'Path to the env file to write credentials into (e.g. ".env")'
|
|
3215
|
+
'Path to the env file to write credentials into, relative to the repo root (e.g. ".env", or "packages/api/.env" when the consuming script lives in that package)'
|
|
3193
3216
|
)
|
|
3194
3217
|
}),
|
|
3195
3218
|
execute: async ({ filePath }) => {
|
|
@@ -3367,7 +3390,8 @@ function searchFilesTool(ctx) {
|
|
|
3367
3390
|
}
|
|
3368
3391
|
|
|
3369
3392
|
// src/lib/tools/runShell.ts
|
|
3370
|
-
import { tool as tool8 } from "ai";
|
|
3393
|
+
import { tool as tool8, generateText, Output } from "ai";
|
|
3394
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3371
3395
|
import z15 from "zod";
|
|
3372
3396
|
import { relative as relative4 } from "node:path";
|
|
3373
3397
|
|
|
@@ -3476,8 +3500,10 @@ function runShell(command, opts) {
|
|
|
3476
3500
|
// src/lib/tools/runShell.ts
|
|
3477
3501
|
function storeApproval(root) {
|
|
3478
3502
|
return async (req) => {
|
|
3503
|
+
const store = useWizard.getState();
|
|
3504
|
+
if (store.isCommandApproved(req.command, req.cwd)) return "approve";
|
|
3479
3505
|
const rel = relative4(root, req.cwd);
|
|
3480
|
-
const answer = await
|
|
3506
|
+
const answer = await store.requestUserInput({
|
|
3481
3507
|
prompt: "Run this command?",
|
|
3482
3508
|
promptType: "commandApproval",
|
|
3483
3509
|
options: [],
|
|
@@ -3486,20 +3512,170 @@ function storeApproval(root) {
|
|
|
3486
3512
|
cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
|
|
3487
3513
|
}
|
|
3488
3514
|
});
|
|
3489
|
-
|
|
3515
|
+
if (answer !== "approve") return "reject";
|
|
3516
|
+
store.approveCommand(req.command, req.cwd);
|
|
3517
|
+
return "approve";
|
|
3490
3518
|
};
|
|
3491
3519
|
}
|
|
3492
3520
|
var EXPLORATORY_COMMANDS = /* @__PURE__ */ new Set(["ls", "find", "tree", "dir"]);
|
|
3521
|
+
function commandSegments(command) {
|
|
3522
|
+
return command.split(/&&|;|\|/).map((segment) => segment.trim());
|
|
3523
|
+
}
|
|
3493
3524
|
function isExploratoryCommand(command) {
|
|
3494
|
-
return command
|
|
3525
|
+
return commandSegments(command).map((segment) => segment.split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
|
|
3526
|
+
}
|
|
3527
|
+
var READ_ONLY_BINARIES = /* @__PURE__ */ new Set([
|
|
3528
|
+
"cat",
|
|
3529
|
+
"head",
|
|
3530
|
+
"tail",
|
|
3531
|
+
"wc",
|
|
3532
|
+
"pwd",
|
|
3533
|
+
"echo",
|
|
3534
|
+
"date",
|
|
3535
|
+
"whoami",
|
|
3536
|
+
"hostname",
|
|
3537
|
+
"uname",
|
|
3538
|
+
"which",
|
|
3539
|
+
"file",
|
|
3540
|
+
"stat",
|
|
3541
|
+
"grep",
|
|
3542
|
+
"egrep",
|
|
3543
|
+
"fgrep",
|
|
3544
|
+
"rg",
|
|
3545
|
+
"diff"
|
|
3546
|
+
]);
|
|
3547
|
+
var READ_ONLY_NO_ARGS_BINARIES = /* @__PURE__ */ new Set(["env", "printenv"]);
|
|
3548
|
+
var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
3549
|
+
"status",
|
|
3550
|
+
"log",
|
|
3551
|
+
"diff",
|
|
3552
|
+
"show",
|
|
3553
|
+
"describe",
|
|
3554
|
+
"blame",
|
|
3555
|
+
"ls-files",
|
|
3556
|
+
"rev-parse",
|
|
3557
|
+
"cat-file",
|
|
3558
|
+
"shortlog",
|
|
3559
|
+
"ls-remote"
|
|
3560
|
+
]);
|
|
3561
|
+
var VERSION_CHECK_BINARIES = /* @__PURE__ */ new Set([
|
|
3562
|
+
"node",
|
|
3563
|
+
"tsc",
|
|
3564
|
+
"npm",
|
|
3565
|
+
"pnpm",
|
|
3566
|
+
"yarn",
|
|
3567
|
+
"python",
|
|
3568
|
+
"python3",
|
|
3569
|
+
"ruby",
|
|
3570
|
+
"go",
|
|
3571
|
+
"cargo",
|
|
3572
|
+
"rustc",
|
|
3573
|
+
"php",
|
|
3574
|
+
"composer",
|
|
3575
|
+
"java",
|
|
3576
|
+
"mvn",
|
|
3577
|
+
"gradle",
|
|
3578
|
+
"bundle",
|
|
3579
|
+
"git"
|
|
3580
|
+
]);
|
|
3581
|
+
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v", "-V"]);
|
|
3582
|
+
function hasFileRedirect(segment) {
|
|
3583
|
+
return segment.replace(/\d>&\d/g, "").includes(">");
|
|
3495
3584
|
}
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3585
|
+
function hasShellInjectionRisk(segment) {
|
|
3586
|
+
if (segment.includes("$(") || segment.includes("<(") || segment.includes("`")) {
|
|
3587
|
+
return true;
|
|
3588
|
+
}
|
|
3589
|
+
return segment.replace(/\d>&\d/g, "").includes("&");
|
|
3590
|
+
}
|
|
3591
|
+
function fastPathSegmentSafety(segment) {
|
|
3592
|
+
if (!segment) return true;
|
|
3593
|
+
if (hasFileRedirect(segment)) return false;
|
|
3594
|
+
if (hasShellInjectionRisk(segment)) return false;
|
|
3595
|
+
const [cmd0, ...rest] = segment.split(/\s+/);
|
|
3596
|
+
if (cmd0 === void 0) return true;
|
|
3597
|
+
if (VERSION_CHECK_BINARIES.has(cmd0) && rest.length === 1 && VERSION_FLAGS.has(rest[0])) {
|
|
3598
|
+
return true;
|
|
3502
3599
|
}
|
|
3600
|
+
if (READ_ONLY_BINARIES.has(cmd0)) return true;
|
|
3601
|
+
if (READ_ONLY_NO_ARGS_BINARIES.has(cmd0)) {
|
|
3602
|
+
return rest.length === 0 ? true : void 0;
|
|
3603
|
+
}
|
|
3604
|
+
if (cmd0 === "git") {
|
|
3605
|
+
return GIT_READ_ONLY_SUBCOMMANDS.has(rest[0] ?? "") ? true : void 0;
|
|
3606
|
+
}
|
|
3607
|
+
return void 0;
|
|
3608
|
+
}
|
|
3609
|
+
function fastPathSafety(command) {
|
|
3610
|
+
const results = commandSegments(command).map(fastPathSegmentSafety);
|
|
3611
|
+
if (results.some((r) => r === false)) return false;
|
|
3612
|
+
if (results.every((r) => r === true)) return true;
|
|
3613
|
+
return void 0;
|
|
3614
|
+
}
|
|
3615
|
+
var CLASSIFIER_MODEL = "claude-haiku-4-5";
|
|
3616
|
+
var commandSafetySchema = z15.object({
|
|
3617
|
+
safe: z15.boolean(),
|
|
3618
|
+
reason: z15.string().describe("One short sentence explaining the verdict.")
|
|
3619
|
+
});
|
|
3620
|
+
function defaultCreateModel() {
|
|
3621
|
+
const token = getAuthToken();
|
|
3622
|
+
if (!token) {
|
|
3623
|
+
throw new Error("Not authenticated: no user token available");
|
|
3624
|
+
}
|
|
3625
|
+
return createAnthropic({
|
|
3626
|
+
apiKey: token,
|
|
3627
|
+
baseURL: PROXY_BASE_URL,
|
|
3628
|
+
fetch: proxyFetch
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
function approvedCommandHistory(approvedCommands) {
|
|
3632
|
+
return Array.from(approvedCommands).map((entry) => {
|
|
3633
|
+
const sep2 = entry.indexOf("\0");
|
|
3634
|
+
return { cwd: entry.slice(0, sep2), command: entry.slice(sep2 + 1) };
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
async function classifyCommandSafety(createModel, command, cwd, explanation, approvedHistory) {
|
|
3638
|
+
try {
|
|
3639
|
+
const anthropic = createModel();
|
|
3640
|
+
const { output } = await generateText({
|
|
3641
|
+
model: anthropic(CLASSIFIER_MODEL),
|
|
3642
|
+
temperature: 0,
|
|
3643
|
+
output: Output.object({ schema: commandSafetySchema }),
|
|
3644
|
+
prompt: [
|
|
3645
|
+
"A coding agent wants to run this shell command in a user's project without asking for approval first. The command may be in any programming language or ecosystem.",
|
|
3646
|
+
"Judge it SAFE only if it cannot modify, delete, move, rename, or install anything the project cares about, cannot push/publish/deploy/commit anything, and cannot make a network request that changes remote state.",
|
|
3647
|
+
"Three broad categories are safe, and most commands you will see fall into one of them:",
|
|
3648
|
+
"(1) Reporting or listing existing state, with no side effect \u2014 e.g. `git status`, `npm ls`, `npm outdated`, `pip show`, `pip freeze`, `cargo tree`, `docker ps`, `docker images`, `kubectl get pods`, `terraform state list`. This includes a read-only GET-style query to a remote registry or API that only fetches public metadata and changes nothing remote (e.g. `npm view <package>`, `pip index versions <package>`, `curl` with no -X/--request other than GET and no -d/--data) \u2014 safe because nothing changes, not because it stays local.",
|
|
3649
|
+
'(2) A "preview" or "check" mode that reports what a change WOULD do, or reports a problem, without making the change. This is a general pattern, not a fixed list \u2014 ANY command that takes a dry-run/check/plan/validate/diff/list-only flag is safe when that flag is present, regardless of whether the same flag would normally appear on a "safe" kind of command: `terraform plan`, `terraform validate`, `terraform fmt -check`, `black --check`, `prettier --check`, `isort --check`, `gofmt -l` (list-only), `stylelint` with no `--fix`, and equally `git push --dry-run`, `npm publish --dry-run`, `kubectl apply --dry-run=client` \u2014 these last three are otherwise-unsafe operations (push, publish, cluster changes) that the dry-run flag turns into a report. The corresponding command WITHOUT that flag (e.g. `terraform apply`, `black .`, `gofmt -w`, `git push`) is a different, unsafe command \u2014 the flag is what makes the difference, for any tool, not just the ones named here. This cuts both ways, so do not assume a bare invocation is the safe one: `cargo fmt`, `black`, `prettier`, `isort`, `rustfmt`, and `terraform fmt` all REWRITE FILES by default and need an explicit check/diff/dry-run flag to become safe, while `gofmt`, `eslint`, `stylelint`, `rubocop`, and `ruff check` default to a safe check-only mode and need an explicit fix/write flag to become unsafe \u2014 the same-looking bare command is safe for one group and unsafe for the other, so judge each by what its own flags actually say, not by resemblance to a tool you already judged.',
|
|
3650
|
+
"(3) Running the project's own tests, type checker, linter, or build/compile step (e.g. `npm run build`, `yarn build`, `vite build`, `webpack`, `tsc`, `cargo build`, `go build`, `mvn compile`), as long as it is not passed an autofix/write/update flag (e.g. --fix, -u, --write, rubocop -a) \u2014 this holds even though the point of a build step is to write compiled output to a build/dist/target directory inside the project (e.g. a target/, build/, dist/, or __pycache__ directory): writing that output does not make the command unsafe. It stops being safe the moment the command chain goes past building \u2014 a step that also deploys, publishes, uploads, or pushes the build (e.g. `next build && vercel deploy`, `npm run build && npm publish`) is unsafe for that later segment even though the build segment itself is fine; judge each chained segment on its own, same as elsewhere in this list.",
|
|
3651
|
+
"Deleting or removing anything is unsafe, even something in a cache or build directory (e.g. `rm -rf __pycache__`, `cargo clean`, `git clean`), and even if the command otherwise fits one of the three categories above. Installing, uninstalling, or upgrading a dependency, changing a database schema, or writing to a path outside the project is also unsafe. A command chained with && or ; is only safe if every part of the chain is safe on its own.",
|
|
3652
|
+
"One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe.",
|
|
3653
|
+
...approvedHistory.length > 0 ? [
|
|
3654
|
+
"The user has already explicitly approved these exact commands earlier in this session (working directory in parentheses, then the command):",
|
|
3655
|
+
approvedHistory.map((h) => `- (${h.cwd}) ${h.command}`).join("\n"),
|
|
3656
|
+
"If the new command performs the same action and side-effect profile as one of these \u2014 differing only in a trivial way, such as a different file path, package name, or argument value that does not change what kind of action it is \u2014 judge it SAFE on that precedent, even if it would not otherwise fit categories (1)-(3) above. Do not stretch this to a command that merely shares a binary name, or superficially resembles one on the list while doing something riskier or of a different kind (e.g. an approved `rm build/tmp.log` does not license a new `rm -rf src/`, and an approved `git push origin feature-x` does not license `git push --force`)."
|
|
3657
|
+
] : [],
|
|
3658
|
+
`Command: ${command}`,
|
|
3659
|
+
`Working directory: ${cwd}`,
|
|
3660
|
+
`Stated purpose: ${explanation}`
|
|
3661
|
+
].join("\n")
|
|
3662
|
+
});
|
|
3663
|
+
if (!output.safe) {
|
|
3664
|
+
logger.info(
|
|
3665
|
+
{ command, reason: output.reason },
|
|
3666
|
+
"runShell: classifier judged command unsafe, requiring approval"
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
return output.safe;
|
|
3670
|
+
} catch (err) {
|
|
3671
|
+
logger.warn(
|
|
3672
|
+
{ err, command },
|
|
3673
|
+
"runShell: command safety classifier failed, requiring approval"
|
|
3674
|
+
);
|
|
3675
|
+
return false;
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
async function runAndRecord(ctx, command, cwd) {
|
|
3503
3679
|
useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
|
|
3504
3680
|
const env = await ctx.shell.env().catch((err) => {
|
|
3505
3681
|
logger.warn({ err, command }, "runShell: could not resolve command env");
|
|
@@ -3535,9 +3711,18 @@ async function approveAndRun(ctx, command, cwd, explanation) {
|
|
|
3535
3711
|
output: run2.output
|
|
3536
3712
|
};
|
|
3537
3713
|
}
|
|
3538
|
-
function
|
|
3714
|
+
async function approveAndRun(ctx, command, cwd, explanation) {
|
|
3715
|
+
const decision = await ctx.shell.approve({ command, cwd, explanation });
|
|
3716
|
+
if (decision === "reject") {
|
|
3717
|
+
ctx.shell.executions.push({ command, cwd, approved: false });
|
|
3718
|
+
logger.info({ command }, "runShell: user rejected the command");
|
|
3719
|
+
return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
|
|
3720
|
+
}
|
|
3721
|
+
return runAndRecord(ctx, command, cwd);
|
|
3722
|
+
}
|
|
3723
|
+
function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
3539
3724
|
return tool8({
|
|
3540
|
-
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands.
|
|
3725
|
+
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. Do not use this to find or read files \u2014 use listFiles, searchFiles, and readFile instead of ls/find/cat/head/tail/grep/rg. This tool refuses ls/find/tree/dir outright; a read command like cat is not refused (some read-only commands run without approval, see below), but it's still the wrong tool for reading a file \u2014 the dedicated tools exist for that and won't count against this tool's command budget. The user approves any command that could change the project before it runs, so write a clear `explanation`. A command judged read-only (inspection, or running tests/typecheck/lint without an autofix flag, in any language) runs immediately without approval. If the user rejects a command, do not retry it \u2014 propose a different approach.",
|
|
3541
3726
|
inputSchema: z15.object({
|
|
3542
3727
|
command: z15.string().describe(
|
|
3543
3728
|
"The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
|
|
@@ -3556,9 +3741,29 @@ function runShellTool(ctx) {
|
|
|
3556
3741
|
const resolved2 = resolveInRoot(ctx, cwd ?? ".");
|
|
3557
3742
|
if (!resolved2.ok) return resolved2.error;
|
|
3558
3743
|
if (isExploratoryCommand(command)) {
|
|
3559
|
-
return "Refused: use listFiles or searchFiles to
|
|
3744
|
+
return "Refused: use listFiles or searchFiles to find files, and readFile to read one, instead of ls/find/tree/dir.";
|
|
3560
3745
|
}
|
|
3561
3746
|
logger.info({ command, cwd: resolved2.target }, "called runShell tool");
|
|
3747
|
+
const fast = fastPathSafety(command);
|
|
3748
|
+
let isSafe = fast;
|
|
3749
|
+
if (isSafe === void 0) {
|
|
3750
|
+
const store = useWizard.getState();
|
|
3751
|
+
isSafe = store.isCommandApproved(command, resolved2.target);
|
|
3752
|
+
if (!isSafe) {
|
|
3753
|
+
isSafe = await classifyCommandSafety(
|
|
3754
|
+
createModel,
|
|
3755
|
+
command,
|
|
3756
|
+
resolved2.target,
|
|
3757
|
+
explanation,
|
|
3758
|
+
approvedCommandHistory(store.approvedCommands)
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
if (isSafe) {
|
|
3763
|
+
return serializePrompt(
|
|
3764
|
+
() => runAndRecord(ctx, command, resolved2.target)
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3562
3767
|
return serializePrompt(
|
|
3563
3768
|
() => approveAndRun(ctx, command, resolved2.target, explanation)
|
|
3564
3769
|
);
|
|
@@ -3608,8 +3813,8 @@ function reviewScriptTool(ctx) {
|
|
|
3608
3813
|
}
|
|
3609
3814
|
|
|
3610
3815
|
// src/lib/tools/generateRecord.ts
|
|
3611
|
-
import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
|
|
3612
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3816
|
+
import { tool as tool10, generateText as generateText2, Output as Output2, NoObjectGeneratedError } from "ai";
|
|
3817
|
+
import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
|
|
3613
3818
|
import { nanoid as nanoid2 } from "nanoid";
|
|
3614
3819
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
|
|
3615
3820
|
import { dirname as dirname6 } from "node:path";
|
|
@@ -3619,18 +3824,18 @@ var RECORD_MODEL = "claude-haiku-4-5";
|
|
|
3619
3824
|
var MAX_RECORDS = 100;
|
|
3620
3825
|
var BATCH_SIZE = 10;
|
|
3621
3826
|
var MAX_BATCH_ATTEMPTS = 3;
|
|
3622
|
-
function
|
|
3827
|
+
function defaultCreateModel2() {
|
|
3623
3828
|
const token = getAuthToken();
|
|
3624
3829
|
if (!token) {
|
|
3625
3830
|
throw new Error("Not authenticated: no user token available");
|
|
3626
3831
|
}
|
|
3627
|
-
return
|
|
3832
|
+
return createAnthropic2({
|
|
3628
3833
|
apiKey: token,
|
|
3629
3834
|
baseURL: PROXY_BASE_URL,
|
|
3630
3835
|
fetch: proxyFetch
|
|
3631
3836
|
});
|
|
3632
3837
|
}
|
|
3633
|
-
function generateRecordTool(ctx, createModel =
|
|
3838
|
+
function generateRecordTool(ctx, createModel = defaultCreateModel2) {
|
|
3634
3839
|
return tool10({
|
|
3635
3840
|
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.",
|
|
3636
3841
|
inputSchema: z17.object({
|
|
@@ -3651,9 +3856,9 @@ function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
|
3651
3856
|
let lastError;
|
|
3652
3857
|
for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt++) {
|
|
3653
3858
|
try {
|
|
3654
|
-
const { output } = await
|
|
3859
|
+
const { output } = await generateText2({
|
|
3655
3860
|
model: anthropic(RECORD_MODEL),
|
|
3656
|
-
output:
|
|
3861
|
+
output: Output2.object({
|
|
3657
3862
|
schema: z17.object({
|
|
3658
3863
|
records: z17.array(recordSchema).length(batchCount)
|
|
3659
3864
|
})
|
|
@@ -3818,7 +4023,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
3818
4023
|
if (!token) {
|
|
3819
4024
|
throw new Error("Not authenticated: no user token available");
|
|
3820
4025
|
}
|
|
3821
|
-
const anthropic =
|
|
4026
|
+
const anthropic = createAnthropic3({
|
|
3822
4027
|
apiKey: token,
|
|
3823
4028
|
baseURL: PROXY_BASE_URL,
|
|
3824
4029
|
fetch: proxyFetch
|
|
@@ -3855,7 +4060,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
3855
4060
|
}
|
|
3856
4061
|
};
|
|
3857
4062
|
}),
|
|
3858
|
-
output:
|
|
4063
|
+
output: Output3.object({ schema: req.outputSchema }),
|
|
3859
4064
|
tools: createTools(toolContext, {
|
|
3860
4065
|
output: req.outputSchema,
|
|
3861
4066
|
tools: req.tools
|
|
@@ -3943,6 +4148,7 @@ var detectLanguage = () => runAgent({
|
|
|
3943
4148
|
"Return the exact version",
|
|
3944
4149
|
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
3945
4150
|
`Determine publicEnvVarPrefix: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to the detected framework's known convention for exposing env vars to client-side code; use "" when the project has no such convention (e.g. a backend-only project).`,
|
|
4151
|
+
"A brand-new project has no existing env var usage to find \u2014 one or two targeted checks (e.g. .env/.env.example, or a grep for the bundler's public-prefix convention) are enough to confirm that. Do not keep searching once those turn up nothing; fall back to the framework convention immediately.",
|
|
3946
4152
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
3947
4153
|
"When done, call reportStatus"
|
|
3948
4154
|
],
|
|
@@ -4038,7 +4244,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
4038
4244
|
// package.json
|
|
4039
4245
|
var package_default = {
|
|
4040
4246
|
name: "@algolia/wizard",
|
|
4041
|
-
version: "0.
|
|
4247
|
+
version: "0.32.0-rc.125.247",
|
|
4042
4248
|
description: "Magically implement Algolia functionality in your codebase",
|
|
4043
4249
|
type: "module",
|
|
4044
4250
|
engines: {
|
|
@@ -4443,12 +4649,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4443
4649
|
};
|
|
4444
4650
|
|
|
4445
4651
|
// src/actions/implement.ts
|
|
4446
|
-
import
|
|
4652
|
+
import z28 from "zod";
|
|
4653
|
+
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
4447
4654
|
import { join as join12, relative as relative6 } from "node:path";
|
|
4448
4655
|
|
|
4449
4656
|
// src/lib/git.ts
|
|
4450
4657
|
import { execFile as execFile2 } from "node:child_process";
|
|
4451
|
-
import { copyFile, mkdir as mkdir6,
|
|
4658
|
+
import { copyFile, mkdir as mkdir6, stat as stat3 } from "node:fs/promises";
|
|
4452
4659
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4453
4660
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4454
4661
|
function git(args) {
|
|
@@ -4502,42 +4709,6 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4502
4709
|
}
|
|
4503
4710
|
return { ok: true, relPath };
|
|
4504
4711
|
}
|
|
4505
|
-
function hasEnvVar(content, name) {
|
|
4506
|
-
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4507
|
-
}
|
|
4508
|
-
async function readEnvVar(repoRoot, name) {
|
|
4509
|
-
let content;
|
|
4510
|
-
try {
|
|
4511
|
-
content = await readFile8(join10(repoRoot, ".env"), "utf8");
|
|
4512
|
-
} catch (err) {
|
|
4513
|
-
if (err.code !== "ENOENT") throw err;
|
|
4514
|
-
return void 0;
|
|
4515
|
-
}
|
|
4516
|
-
const match = new RegExp(
|
|
4517
|
-
`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
|
|
4518
|
-
"m"
|
|
4519
|
-
).exec(content);
|
|
4520
|
-
if (!match) return void 0;
|
|
4521
|
-
const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
4522
|
-
if (!value || value.startsWith("<")) return void 0;
|
|
4523
|
-
return value;
|
|
4524
|
-
}
|
|
4525
|
-
async function writeSearchEnvValues(repoRoot, vars) {
|
|
4526
|
-
const target = join10(repoRoot, ".env");
|
|
4527
|
-
let existing = "";
|
|
4528
|
-
try {
|
|
4529
|
-
existing = await readFile8(target, "utf8");
|
|
4530
|
-
} catch (err) {
|
|
4531
|
-
if (err.code !== "ENOENT") throw err;
|
|
4532
|
-
}
|
|
4533
|
-
const missing = vars.filter((v) => !hasEnvVar(existing, v.name));
|
|
4534
|
-
if (missing.length === 0) return [];
|
|
4535
|
-
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
4536
|
-
const lines = missing.map(({ name, value }) => `${name}=${value}
|
|
4537
|
-
`).join("");
|
|
4538
|
-
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4539
|
-
return missing.map((v) => v.name);
|
|
4540
|
-
}
|
|
4541
4712
|
function normalizeFindingPaths(findings) {
|
|
4542
4713
|
return {
|
|
4543
4714
|
...findings,
|
|
@@ -4618,46 +4789,36 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4618
4789
|
return loadAlgoliaDoc("js");
|
|
4619
4790
|
}
|
|
4620
4791
|
|
|
4621
|
-
// src/actions/resolveEnvVarPrefix.ts
|
|
4622
|
-
import z28 from "zod";
|
|
4623
|
-
var resolveEnvVarPrefixSchema = z28.object({
|
|
4624
|
-
publicEnvVarPrefix: detectLanguageSchema.shape.publicEnvVarPrefix
|
|
4625
|
-
});
|
|
4626
|
-
var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
4627
|
-
instructions: [
|
|
4628
|
-
`The developer corrected the project's framework to "${frameworkName}".`,
|
|
4629
|
-
`Determine publicEnvVarPrefix for this framework: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to this framework's known convention for exposing env vars to client-side code; use "" when the framework has no such convention (e.g. a backend-only framework).`,
|
|
4630
|
-
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
4631
|
-
"When done, call reportStatus"
|
|
4632
|
-
],
|
|
4633
|
-
tools: ["listFiles", "changeDirectory", "readFile", "searchFiles"],
|
|
4634
|
-
outputSchema: resolveEnvVarPrefixSchema,
|
|
4635
|
-
modelSize: "small"
|
|
4636
|
-
});
|
|
4637
|
-
|
|
4638
4792
|
// src/actions/implement.ts
|
|
4639
|
-
var implementSchema =
|
|
4640
|
-
summary:
|
|
4641
|
-
ingestCommand:
|
|
4642
|
-
ingestScriptRan:
|
|
4643
|
-
ingestRecordCount:
|
|
4644
|
-
ingestDurationMs:
|
|
4645
|
-
ingestionSource:
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4793
|
+
var implementSchema = z28.object({
|
|
4794
|
+
summary: z28.string(),
|
|
4795
|
+
ingestCommand: z28.string().optional(),
|
|
4796
|
+
ingestScriptRan: z28.boolean().optional(),
|
|
4797
|
+
ingestRecordCount: z28.number().optional(),
|
|
4798
|
+
ingestDurationMs: z28.number().optional(),
|
|
4799
|
+
ingestionSource: z28.enum(["local", "fileUpload", "generated"]),
|
|
4800
|
+
searchConfig: z28.object({
|
|
4801
|
+
filePath: z28.string().optional(),
|
|
4802
|
+
vars: z28.array(
|
|
4803
|
+
z28.object({
|
|
4804
|
+
name: z28.string(),
|
|
4805
|
+
value: z28.string()
|
|
4806
|
+
})
|
|
4807
|
+
)
|
|
4808
|
+
}).optional()
|
|
4652
4809
|
});
|
|
4653
|
-
var implementationOutputSchema =
|
|
4654
|
-
summary:
|
|
4655
|
-
ingestCommand:
|
|
4810
|
+
var implementationOutputSchema = z28.object({
|
|
4811
|
+
summary: z28.string(),
|
|
4812
|
+
ingestCommand: z28.string().optional(),
|
|
4813
|
+
// Only for the search use case: the path of whatever module the agent
|
|
4814
|
+
// defined the Algolia config constants in, so the wizard can check it
|
|
4815
|
+
// won't end up gitignored (it's public, meant to be committed).
|
|
4816
|
+
searchConfigFile: z28.string().optional()
|
|
4656
4817
|
});
|
|
4657
|
-
var verificationOutputSchema =
|
|
4658
|
-
summary:
|
|
4659
|
-
sufficient:
|
|
4660
|
-
additionalInstructions:
|
|
4818
|
+
var verificationOutputSchema = z28.object({
|
|
4819
|
+
summary: z28.string(),
|
|
4820
|
+
sufficient: z28.boolean(),
|
|
4821
|
+
additionalInstructions: z28.string().optional()
|
|
4661
4822
|
});
|
|
4662
4823
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
4663
4824
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
@@ -4671,6 +4832,10 @@ function isJsProject(language) {
|
|
|
4671
4832
|
(name) => JS_LANGUAGES.some((js) => name.includes(js))
|
|
4672
4833
|
);
|
|
4673
4834
|
}
|
|
4835
|
+
var SEARCH_CONFIG_APP_ID = "ALGOLIA_APP_ID";
|
|
4836
|
+
var SEARCH_CONFIG_SEARCH_KEY = "ALGOLIA_SEARCH_API_KEY";
|
|
4837
|
+
var SEARCH_CONFIG_INDEX_NAME = "ALGOLIA_INDEX_NAME";
|
|
4838
|
+
var SEARCH_KEY_PLACEHOLDER = "<your-algolia-search-only-api-key>";
|
|
4674
4839
|
var UI_FRAMEWORKS = [
|
|
4675
4840
|
{ match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
|
|
4676
4841
|
{ match: ["react", "next"], target: "React", doc: "react" },
|
|
@@ -4733,7 +4898,7 @@ function algoliaClientDoc(input) {
|
|
|
4733
4898
|
function ingestionInstructions(input) {
|
|
4734
4899
|
return [
|
|
4735
4900
|
...input.confirmed && input.confirmed.length ? [
|
|
4736
|
-
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
4901
|
+
`Create an ingestion script under "${input.ingestDir}/" at the repo root. That directory already exists \u2014 writeFile creates any nested path itself, so never run a shell command just to create a directory.`,
|
|
4737
4902
|
`Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
4738
4903
|
`Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
|
|
4739
4904
|
`Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
|
|
@@ -4741,6 +4906,7 @@ function ingestionInstructions(input) {
|
|
|
4741
4906
|
"After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
|
|
4742
4907
|
...algoliaClientDoc(input),
|
|
4743
4908
|
"Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
|
|
4909
|
+
`If the script loads its env vars from a file (e.g. via dotenv or an equivalent for its language) rather than the process environment directly, decide that up front and call writeCredentials on that file before you finish the script \u2014 do not wait to discover the need for it by having a writeFile call refused.`,
|
|
4744
4910
|
"When the script is finished, call reviewScript with its path and wait: running it writes records to a live index, so the developer reads it first. Do not run it before that call returns.",
|
|
4745
4911
|
'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
|
|
4746
4912
|
"The summary should be extremely concise.",
|
|
@@ -4759,19 +4925,17 @@ function searchInstructions(input) {
|
|
|
4759
4925
|
] : [
|
|
4760
4926
|
"No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
|
|
4761
4927
|
],
|
|
4762
|
-
`
|
|
4763
|
-
"
|
|
4764
|
-
|
|
4765
|
-
"
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
// The wizard writes these exact names into .env right after this step.
|
|
4771
|
-
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4928
|
+
`Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
|
|
4929
|
+
`Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
|
|
4930
|
+
"If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
|
|
4931
|
+
"When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
|
|
4932
|
+
`Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
|
|
4933
|
+
`Set ${SEARCH_CONFIG_APP_ID} to "${input.appId}" and ${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
|
|
4934
|
+
input.searchKey ? `Set ${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
|
|
4935
|
+
'Report the repo-relative path of that module as "searchConfigFile" in your final status.',
|
|
4772
4936
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4773
4937
|
"Match the styles of the application as closely as possible.",
|
|
4774
|
-
"The summary should be extremely concise; do not mention
|
|
4938
|
+
"The summary should be extremely concise; do not mention manual testing steps."
|
|
4775
4939
|
];
|
|
4776
4940
|
}
|
|
4777
4941
|
function verificationInstructions(input) {
|
|
@@ -4902,21 +5066,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4902
5066
|
languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
|
|
4903
5067
|
frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
|
|
4904
5068
|
};
|
|
4905
|
-
const normalizeFrameworkName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4906
|
-
const confirmedPrimaryFramework = language.frameworks[0]?.name;
|
|
4907
|
-
const frameworkWasCorrected = confirmedPrimaryFramework !== void 0 && !scan.frameworks.some(
|
|
4908
|
-
(fw) => normalizeFrameworkName(fw.name) === normalizeFrameworkName(confirmedPrimaryFramework)
|
|
4909
|
-
);
|
|
4910
|
-
const publicEnvVarPrefixPromise = frameworkWasCorrected ? resolveEnvVarPrefix(confirmedPrimaryFramework).then(
|
|
4911
|
-
(r) => r.publicEnvVarPrefix,
|
|
4912
|
-
(err) => {
|
|
4913
|
-
logger.warn(
|
|
4914
|
-
{ err, framework: confirmedPrimaryFramework },
|
|
4915
|
-
"implement: could not re-resolve publicEnvVarPrefix after a framework correction; using the stale scan value"
|
|
4916
|
-
);
|
|
4917
|
-
return scan.publicEnvVarPrefix;
|
|
4918
|
-
}
|
|
4919
|
-
) : Promise.resolve(scan.publicEnvVarPrefix);
|
|
4920
5069
|
const selected = ctx.getStepOutput(
|
|
4921
5070
|
"select-index"
|
|
4922
5071
|
);
|
|
@@ -4967,6 +5116,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4967
5116
|
const targetIndex = selected?.selection;
|
|
4968
5117
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4969
5118
|
await assertGitRepoWithHead(repoRoot);
|
|
5119
|
+
if (useCases.includes("ingestion")) {
|
|
5120
|
+
await mkdir7(join12(repoRoot, INGEST_DIR), { recursive: true });
|
|
5121
|
+
}
|
|
4970
5122
|
const normalized = normalizeFindingPaths(findings);
|
|
4971
5123
|
const confirmed2 = normalized.confirmedEntities;
|
|
4972
5124
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4997,49 +5149,42 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4997
5149
|
);
|
|
4998
5150
|
}
|
|
4999
5151
|
}
|
|
5000
|
-
const
|
|
5152
|
+
const summaries = [];
|
|
5153
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
5154
|
+
let searchKey;
|
|
5155
|
+
let searchKeyError;
|
|
5156
|
+
if (useCases.includes("search") && appId) {
|
|
5157
|
+
try {
|
|
5158
|
+
const resolved2 = await resolveSearchOnlyKey(targetIndex, appId);
|
|
5159
|
+
searchKey = resolved2.key;
|
|
5160
|
+
summaries.push(
|
|
5161
|
+
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}.`
|
|
5162
|
+
);
|
|
5163
|
+
} catch (err) {
|
|
5164
|
+
searchKeyError = err.message;
|
|
5165
|
+
summaries.push(
|
|
5166
|
+
`Could not provision a search-only Algolia API key (${searchKeyError}) \u2014 the search agent will scaffold a placeholder with a TODO for you to fill in.`
|
|
5167
|
+
);
|
|
5168
|
+
logger.warn(
|
|
5169
|
+
{ err: searchKeyError },
|
|
5170
|
+
"implement: could not provision a search-only API key; the agent will scaffold a placeholder"
|
|
5171
|
+
);
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5001
5174
|
const input = {
|
|
5002
5175
|
findings: normalized,
|
|
5003
5176
|
confirmed: confirmed2,
|
|
5004
5177
|
searchLocation,
|
|
5005
5178
|
targetIndex,
|
|
5006
5179
|
language,
|
|
5007
|
-
publicEnvVarPrefix,
|
|
5008
5180
|
appId,
|
|
5009
|
-
|
|
5181
|
+
searchKey,
|
|
5182
|
+
searchKeyError,
|
|
5010
5183
|
ingestDir: INGEST_DIR,
|
|
5011
5184
|
ingestionSource,
|
|
5012
5185
|
uploadFilePath,
|
|
5013
5186
|
searchUiTarget: searchUiTarget(language)
|
|
5014
5187
|
};
|
|
5015
|
-
const summaries = [];
|
|
5016
|
-
if (uploadWarning) summaries.push(uploadWarning);
|
|
5017
|
-
let envSearchKey;
|
|
5018
|
-
let envAppIdMismatch = false;
|
|
5019
|
-
if (useCases.includes("search") && appId) {
|
|
5020
|
-
const envAppId = await readEnvVar(
|
|
5021
|
-
repoRoot,
|
|
5022
|
-
publicAppIdVar(publicEnvVarPrefix)
|
|
5023
|
-
);
|
|
5024
|
-
if (envAppId === appId) {
|
|
5025
|
-
envSearchKey = await readEnvVar(
|
|
5026
|
-
repoRoot,
|
|
5027
|
-
publicSearchKeyVar(publicEnvVarPrefix)
|
|
5028
|
-
);
|
|
5029
|
-
} else if (envAppId) {
|
|
5030
|
-
envAppIdMismatch = true;
|
|
5031
|
-
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
5032
|
-
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
5033
|
-
summaries.push(
|
|
5034
|
-
`\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.`
|
|
5035
|
-
);
|
|
5036
|
-
logger.warn(
|
|
5037
|
-
{ envAppId, appId },
|
|
5038
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
5039
|
-
);
|
|
5040
|
-
}
|
|
5041
|
-
}
|
|
5042
|
-
let finalSearchEnvVars = input.searchEnvVars;
|
|
5043
5188
|
let agentRuns = 0;
|
|
5044
5189
|
let ingestCommand;
|
|
5045
5190
|
let ingestScriptRan = false;
|
|
@@ -5149,6 +5294,7 @@ ${detail}` : ""}`
|
|
|
5149
5294
|
]
|
|
5150
5295
|
});
|
|
5151
5296
|
}
|
|
5297
|
+
let searchConfigFile;
|
|
5152
5298
|
if (useCases.includes("search")) {
|
|
5153
5299
|
let extraInstructions = [];
|
|
5154
5300
|
useWizard.getState().clearWrittenFiles();
|
|
@@ -5163,11 +5309,14 @@ ${detail}` : ""}`
|
|
|
5163
5309
|
"implement: retrying search implementation after failed verification"
|
|
5164
5310
|
);
|
|
5165
5311
|
}
|
|
5166
|
-
const
|
|
5312
|
+
const searchResult = await runImplementationUseCase(
|
|
5167
5313
|
"search",
|
|
5168
5314
|
extraInstructions
|
|
5169
5315
|
);
|
|
5170
|
-
summaries.push(formatSummary("search", summary));
|
|
5316
|
+
summaries.push(formatSummary("search", searchResult.summary));
|
|
5317
|
+
if (searchResult.searchConfigFile) {
|
|
5318
|
+
searchConfigFile = searchResult.searchConfigFile;
|
|
5319
|
+
}
|
|
5171
5320
|
const verification = await runVerificationUseCase();
|
|
5172
5321
|
summaries.push(formatSummary("verification", verification.summary));
|
|
5173
5322
|
if (verification.sufficient) {
|
|
@@ -5191,76 +5340,17 @@ ${detail}` : ""}`
|
|
|
5191
5340
|
}
|
|
5192
5341
|
extraInstructions = verificationRetryInstructions(verification);
|
|
5193
5342
|
}
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
if (appId) {
|
|
5197
|
-
try {
|
|
5198
|
-
const resolved2 = await resolveSearchOnlyKey(
|
|
5199
|
-
targetIndex,
|
|
5200
|
-
appId,
|
|
5201
|
-
envSearchKey
|
|
5202
|
-
);
|
|
5203
|
-
searchKey = resolved2.key;
|
|
5204
|
-
summaries.push(
|
|
5205
|
-
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}.`
|
|
5206
|
-
);
|
|
5207
|
-
} catch (err) {
|
|
5208
|
-
searchKeyError = err.message;
|
|
5209
|
-
logger.warn(
|
|
5210
|
-
{ err: searchKeyError },
|
|
5211
|
-
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
5212
|
-
);
|
|
5213
|
-
}
|
|
5214
|
-
}
|
|
5215
|
-
finalSearchEnvVars = publicSearchEnvVars(
|
|
5216
|
-
publicEnvVarPrefix,
|
|
5217
|
-
targetIndex,
|
|
5218
|
-
appId,
|
|
5219
|
-
searchKey
|
|
5220
|
-
);
|
|
5221
|
-
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
5222
|
-
(v) => !v.value.startsWith("<")
|
|
5223
|
-
);
|
|
5224
|
-
if (resolvedSearchEnvVars.length > 0) {
|
|
5225
|
-
const written = await writeSearchEnvValues(
|
|
5343
|
+
if (searchConfigFile) {
|
|
5344
|
+
const ignoreStatus = await gitIgnoreStatus(
|
|
5226
5345
|
repoRoot,
|
|
5227
|
-
|
|
5346
|
+
join12(repoRoot, searchConfigFile)
|
|
5228
5347
|
);
|
|
5229
|
-
if (
|
|
5230
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
5231
|
-
}
|
|
5232
|
-
const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
|
|
5233
|
-
if (ignored === "added") {
|
|
5234
|
-
summaries.push("Added .env to .gitignore.");
|
|
5235
|
-
} else if (ignored === "tracked") {
|
|
5236
|
-
summaries.push(
|
|
5237
|
-
'\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.'
|
|
5238
|
-
);
|
|
5239
|
-
}
|
|
5240
|
-
const stale = [];
|
|
5241
|
-
for (const v of resolvedSearchEnvVars) {
|
|
5242
|
-
if (written.includes(v.name)) continue;
|
|
5243
|
-
const current = await readEnvVar(repoRoot, v.name);
|
|
5244
|
-
if (current && current !== v.value) stale.push(v);
|
|
5245
|
-
}
|
|
5246
|
-
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5348
|
+
if (ignoreStatus === "covered") {
|
|
5247
5349
|
summaries.push(
|
|
5248
|
-
`\u26A0\uFE0F
|
|
5249
|
-
);
|
|
5250
|
-
logger.warn(
|
|
5251
|
-
{ vars: stale.map((v) => v.name) },
|
|
5252
|
-
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
5350
|
+
`\u26A0\uFE0F ${searchConfigFile} is gitignored, so this public, safe-to-share search config won't reach teammates or CI. Remove whatever .gitignore rule covers it.`
|
|
5253
5351
|
);
|
|
5254
5352
|
}
|
|
5255
5353
|
}
|
|
5256
|
-
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
5257
|
-
(v) => v.value.startsWith("<")
|
|
5258
|
-
);
|
|
5259
|
-
if (unresolvedSearchEnvVars.length > 0) {
|
|
5260
|
-
summaries.push(
|
|
5261
|
-
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5262
|
-
);
|
|
5263
|
-
}
|
|
5264
5354
|
} else {
|
|
5265
5355
|
ctx.setUserInput("implementation", "success");
|
|
5266
5356
|
}
|
|
@@ -5273,7 +5363,19 @@ ${detail}` : ""}`
|
|
|
5273
5363
|
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
5274
5364
|
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
5275
5365
|
} : {},
|
|
5276
|
-
...useCases.includes("search") ? {
|
|
5366
|
+
...useCases.includes("search") ? {
|
|
5367
|
+
searchConfig: {
|
|
5368
|
+
filePath: searchConfigFile,
|
|
5369
|
+
vars: [
|
|
5370
|
+
{ name: SEARCH_CONFIG_APP_ID, value: appId ?? "" },
|
|
5371
|
+
{
|
|
5372
|
+
name: SEARCH_CONFIG_SEARCH_KEY,
|
|
5373
|
+
value: searchKey ?? SEARCH_KEY_PLACEHOLDER
|
|
5374
|
+
},
|
|
5375
|
+
{ name: SEARCH_CONFIG_INDEX_NAME, value: targetIndex }
|
|
5376
|
+
]
|
|
5377
|
+
}
|
|
5378
|
+
} : {}
|
|
5277
5379
|
};
|
|
5278
5380
|
}
|
|
5279
5381
|
|
|
@@ -5313,8 +5415,8 @@ var defaultWorkflow = {
|
|
|
5313
5415
|
defineStep({
|
|
5314
5416
|
id: "select-index",
|
|
5315
5417
|
title: "Set up index",
|
|
5316
|
-
outputSchema:
|
|
5317
|
-
selection:
|
|
5418
|
+
outputSchema: z29.object({
|
|
5419
|
+
selection: z29.string()
|
|
5318
5420
|
}),
|
|
5319
5421
|
run: (ctx) => selectIndexStep(ctx)
|
|
5320
5422
|
}),
|
|
@@ -5422,10 +5524,14 @@ var confirmFramework2 = {
|
|
|
5422
5524
|
var search = {
|
|
5423
5525
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
5424
5526
|
ingestionSource: "generated",
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5527
|
+
searchConfig: {
|
|
5528
|
+
filePath: "src/algolia.config.ts",
|
|
5529
|
+
vars: [
|
|
5530
|
+
{ name: "ALGOLIA_APP_ID", value: "SEEDAPPID" },
|
|
5531
|
+
{ name: "ALGOLIA_SEARCH_API_KEY", value: "seedsearchkey" },
|
|
5532
|
+
{ name: "ALGOLIA_INDEX_NAME", value: "wizard_seed_products" }
|
|
5533
|
+
]
|
|
5534
|
+
}
|
|
5429
5535
|
};
|
|
5430
5536
|
var review = {
|
|
5431
5537
|
summaryPoints: [
|