veefore 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/veefore.mjs +1266 -960
- package/package.json +1 -1
package/dist/veefore.mjs
CHANGED
|
@@ -236,29 +236,29 @@ var ApiClient = class {
|
|
|
236
236
|
setToken(token) {
|
|
237
237
|
this.token = token;
|
|
238
238
|
}
|
|
239
|
-
async get(
|
|
239
|
+
async get(path13, query = {}) {
|
|
240
240
|
const search = new URLSearchParams();
|
|
241
241
|
for (const [key, value] of Object.entries(query)) {
|
|
242
242
|
if (value !== void 0) search.set(key, String(value));
|
|
243
243
|
}
|
|
244
244
|
const suffix = search.toString() === "" ? "" : `?${search.toString()}`;
|
|
245
|
-
return this.send("GET", `${
|
|
245
|
+
return this.send("GET", `${path13}${suffix}`);
|
|
246
246
|
}
|
|
247
|
-
async post(
|
|
248
|
-
return this.send("POST",
|
|
247
|
+
async post(path13, body) {
|
|
248
|
+
return this.send("POST", path13, body);
|
|
249
249
|
}
|
|
250
|
-
async patch(
|
|
251
|
-
return this.send("PATCH",
|
|
250
|
+
async patch(path13, body) {
|
|
251
|
+
return this.send("PATCH", path13, body);
|
|
252
252
|
}
|
|
253
|
-
async delete(
|
|
254
|
-
return this.send("DELETE",
|
|
253
|
+
async delete(path13) {
|
|
254
|
+
return this.send("DELETE", path13);
|
|
255
255
|
}
|
|
256
256
|
/** Raw request for endpoints that return a non-JSON body (exports). */
|
|
257
|
-
async raw(method,
|
|
258
|
-
return this.fetchWithTimeout(method,
|
|
257
|
+
async raw(method, path13, body) {
|
|
258
|
+
return this.fetchWithTimeout(method, path13, body);
|
|
259
259
|
}
|
|
260
|
-
async send(method,
|
|
261
|
-
const response = await this.fetchWithTimeout(method,
|
|
260
|
+
async send(method, path13, body) {
|
|
261
|
+
const response = await this.fetchWithTimeout(method, path13, body);
|
|
262
262
|
const text = await response.text();
|
|
263
263
|
const parsed = parseJson(text);
|
|
264
264
|
if (response.ok) {
|
|
@@ -266,14 +266,14 @@ var ApiClient = class {
|
|
|
266
266
|
if (parsed && "data" in parsed) return parsed.data;
|
|
267
267
|
throw new CliApiError(
|
|
268
268
|
"UNEXPECTED_RESPONSE",
|
|
269
|
-
`The control plane returned an unexpected body for ${method} ${
|
|
269
|
+
`The control plane returned an unexpected body for ${method} ${path13}.`,
|
|
270
270
|
response.status,
|
|
271
271
|
response.headers.get("x-request-id")
|
|
272
272
|
);
|
|
273
273
|
}
|
|
274
274
|
const error = new CliApiError(
|
|
275
275
|
parsed?.error?.code ?? `HTTP_${response.status}`,
|
|
276
|
-
parsed?.error?.message ?? `${method} ${
|
|
276
|
+
parsed?.error?.message ?? `${method} ${path13} failed with ${response.status} ${response.statusText}.`,
|
|
277
277
|
response.status,
|
|
278
278
|
parsed?.error?.request_id ?? response.headers.get("x-request-id"),
|
|
279
279
|
parsed?.error?.details ?? {}
|
|
@@ -291,7 +291,7 @@ var ApiClient = class {
|
|
|
291
291
|
}
|
|
292
292
|
throw error;
|
|
293
293
|
}
|
|
294
|
-
async fetchWithTimeout(method,
|
|
294
|
+
async fetchWithTimeout(method, path13, body) {
|
|
295
295
|
const controller = new AbortController();
|
|
296
296
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
297
297
|
const headers = { accept: "application/json" };
|
|
@@ -300,7 +300,7 @@ var ApiClient = class {
|
|
|
300
300
|
if (this.workspaceId) headers["x-veefore-workspace"] = this.workspaceId;
|
|
301
301
|
if (this.environmentId) headers["x-veefore-environment"] = this.environmentId;
|
|
302
302
|
try {
|
|
303
|
-
return await fetch(`${this.controlPlaneUrl}${
|
|
303
|
+
return await fetch(`${this.controlPlaneUrl}${path13}`, {
|
|
304
304
|
method,
|
|
305
305
|
headers,
|
|
306
306
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
@@ -568,6 +568,8 @@ function sleep(ms) {
|
|
|
568
568
|
import { createInterface } from "node:readline/promises";
|
|
569
569
|
|
|
570
570
|
// src/tui.ts
|
|
571
|
+
import { statSync } from "node:fs";
|
|
572
|
+
import path2 from "node:path";
|
|
571
573
|
import { emitKeypressEvents } from "node:readline";
|
|
572
574
|
|
|
573
575
|
// src/logo.ts
|
|
@@ -584,6 +586,16 @@ var LOGO = [
|
|
|
584
586
|
" \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
585
587
|
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588"
|
|
586
588
|
];
|
|
589
|
+
var LOGO_SMALL = [
|
|
590
|
+
"\u2580\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2588 \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2588\u2580",
|
|
591
|
+
" \u2580\u2584 \u2584\u2584 \u2580\u2588 \u2588\u2580 \u2584\u2584 \u2584\u2588",
|
|
592
|
+
" \u2588\u2584 \u2588 \u2588\u2580 \u2588 \u2584\u2588",
|
|
593
|
+
" \u2588\u2584 \u2588 \u2588\u2580 \u2588 \u2584\u2588",
|
|
594
|
+
" \u2588 \u2580\u2588\u2584\u2580 \u2588\u2580 \u2588",
|
|
595
|
+
" \u2588 \u2580\u2588 \u2588\u2580 \u2588",
|
|
596
|
+
" \u2580\u2588 \u2584\u2580 \u2588\u2580",
|
|
597
|
+
" \u2580\u2588\u2584\u2584\u2588\u2588 \u2588\u2580"
|
|
598
|
+
];
|
|
587
599
|
|
|
588
600
|
// src/tui.ts
|
|
589
601
|
function isInteractive() {
|
|
@@ -612,8 +624,8 @@ var BRAND_TOP = [150, 172, 236];
|
|
|
612
624
|
var BRAND_BOTTOM = [84, 108, 186];
|
|
613
625
|
var BRAND = [118, 140, 214];
|
|
614
626
|
var brand = (text) => paint(BRAND, text);
|
|
615
|
-
function gradient(row,
|
|
616
|
-
const t =
|
|
627
|
+
function gradient(row, rows2) {
|
|
628
|
+
const t = rows2 <= 1 ? 0 : row / (rows2 - 1);
|
|
617
629
|
const mix = (a, b) => Math.round(a + (b - a) * t);
|
|
618
630
|
return [
|
|
619
631
|
mix(BRAND_TOP[0], BRAND_BOTTOM[0]),
|
|
@@ -628,6 +640,9 @@ function visibleLength(text) {
|
|
|
628
640
|
function columns() {
|
|
629
641
|
return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
|
630
642
|
}
|
|
643
|
+
function rows() {
|
|
644
|
+
return process.stdout.rows && process.stdout.rows > 0 ? process.stdout.rows : 24;
|
|
645
|
+
}
|
|
631
646
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
632
647
|
var STARS = [
|
|
633
648
|
[0, -3, "*"],
|
|
@@ -640,8 +655,20 @@ var STARS = [
|
|
|
640
655
|
];
|
|
641
656
|
async function showWelcome(context, options = {}) {
|
|
642
657
|
if (!isInteractive()) return;
|
|
658
|
+
const lines = welcomeLines(context, options.reserve ?? 0);
|
|
659
|
+
if ((options.animate ?? true) && process.env.VEEFORE_NO_ANIMATION === void 0) {
|
|
660
|
+
for (const line of lines) {
|
|
661
|
+
out(line);
|
|
662
|
+
await sleep2(14);
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
for (const line of lines) out(line);
|
|
667
|
+
}
|
|
668
|
+
function welcomeLines(context, reserve) {
|
|
643
669
|
const width = Math.min(columns(), 100);
|
|
644
|
-
const
|
|
670
|
+
const art = rows() >= LOGO.length + 6 + reserve ? LOGO : LOGO_SMALL;
|
|
671
|
+
const logoWidth = Math.max(...art.map((line) => [...line].length));
|
|
645
672
|
const indent = 5;
|
|
646
673
|
const gap = 6;
|
|
647
674
|
const panelWidth = Math.max(
|
|
@@ -649,43 +676,45 @@ async function showWelcome(context, options = {}) {
|
|
|
649
676
|
...context.facts.map(([label, value]) => label.length + 2 + value.length)
|
|
650
677
|
);
|
|
651
678
|
const sideBySide = width >= indent + logoWidth + gap + panelWidth + 2;
|
|
652
|
-
const showLogo = width >= indent + logoWidth + 2;
|
|
653
|
-
const rule = style.grey("\xB7".repeat(Math.max(10, Math.min(width - 2, 72))));
|
|
654
|
-
const animate = (options.animate ?? true) && process.env.VEEFORE_NO_ANIMATION === void 0;
|
|
655
|
-
out();
|
|
656
|
-
out(` ${brand(style.bold("Welcome to Veefore"))} ${style.grey(`v${context.version}`)}`);
|
|
657
|
-
out(` ${rule}`);
|
|
658
|
-
out();
|
|
659
679
|
const panel = renderPanel(context.facts);
|
|
680
|
+
const showLogo = width >= indent + logoWidth + 2 && (sideBySide || rows() >= art.length + 6 + panel.length + 2 + reserve);
|
|
681
|
+
const rule = style.grey("\xB7".repeat(Math.max(10, Math.min(width - 2, 72))));
|
|
682
|
+
const lines = [];
|
|
683
|
+
const add = (line = "") => void lines.push(line);
|
|
684
|
+
add();
|
|
685
|
+
add(` ${brand(style.bold("Welcome to Veefore"))} ${style.grey(`v${context.version}`)}`);
|
|
686
|
+
add(` ${rule}`);
|
|
687
|
+
add();
|
|
660
688
|
if (showLogo) {
|
|
661
|
-
for (let row = 0; row <
|
|
662
|
-
const line =
|
|
663
|
-
const left = starsFor(row, indent, (column) => column < 0, -indent);
|
|
664
|
-
const right = starsFor(row, 0, (column) => column >= logoWidth, logoWidth);
|
|
665
|
-
const
|
|
666
|
-
let text = `${left}${
|
|
689
|
+
for (let row = 0; row < art.length; row += 1) {
|
|
690
|
+
const line = art[row] ?? "";
|
|
691
|
+
const left = starsFor(row, indent, (column) => column < 0, -indent, art.length);
|
|
692
|
+
const right = starsFor(row, 0, (column) => column >= logoWidth, logoWidth, art.length);
|
|
693
|
+
const painted = paint(gradient(row, art.length), line.padEnd(logoWidth));
|
|
694
|
+
let text = `${left}${painted}`;
|
|
667
695
|
if (sideBySide) {
|
|
668
|
-
const top = Math.max(0, Math.floor((
|
|
696
|
+
const top = Math.max(0, Math.floor((art.length - panel.length) / 2));
|
|
669
697
|
const side = panel[row - top];
|
|
670
698
|
text += side === void 0 ? right : `${" ".repeat(gap)}${side}`;
|
|
671
699
|
} else {
|
|
672
700
|
text += right;
|
|
673
701
|
}
|
|
674
|
-
|
|
675
|
-
if (animate) await sleep2(14);
|
|
702
|
+
add(text.replace(/\s+$/, ""));
|
|
676
703
|
}
|
|
677
704
|
}
|
|
678
705
|
if (!sideBySide) {
|
|
679
|
-
|
|
680
|
-
for (const line of panel)
|
|
706
|
+
add();
|
|
707
|
+
for (const line of panel) add(fit(` ${line}`, width - 1));
|
|
681
708
|
}
|
|
682
|
-
|
|
683
|
-
|
|
709
|
+
add();
|
|
710
|
+
add(` ${rule}`);
|
|
711
|
+
return lines;
|
|
684
712
|
}
|
|
685
|
-
function starsFor(row, width, inBand, offset) {
|
|
713
|
+
function starsFor(row, width, inBand, offset, artRows) {
|
|
686
714
|
const cells = Array.from({ length: Math.max(width, 6) }, () => " ");
|
|
687
715
|
for (const [starRow, column, glyph] of STARS) {
|
|
688
|
-
|
|
716
|
+
const scaled = Math.round(starRow * (artRows - 1) / (LOGO.length - 1));
|
|
717
|
+
if (scaled !== row || !inBand(column)) continue;
|
|
689
718
|
const index = column - offset;
|
|
690
719
|
if (index >= 0 && index < cells.length) cells[index] = style.grey(glyph);
|
|
691
720
|
}
|
|
@@ -708,6 +737,41 @@ function clearScreen() {
|
|
|
708
737
|
if (!isInteractive()) return;
|
|
709
738
|
process.stdout.write("\x1B[H\x1B[2J");
|
|
710
739
|
}
|
|
740
|
+
var appDepth = 0;
|
|
741
|
+
var restoreInstalled = false;
|
|
742
|
+
function enterApp() {
|
|
743
|
+
if (!isInteractive()) return;
|
|
744
|
+
appDepth += 1;
|
|
745
|
+
if (appDepth > 1) return;
|
|
746
|
+
if (!restoreInstalled) {
|
|
747
|
+
restoreInstalled = true;
|
|
748
|
+
process.on("exit", () => {
|
|
749
|
+
if (appDepth > 0) process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
process.stdout.write("\x1B[?1049h\x1B[H\x1B[2J");
|
|
753
|
+
}
|
|
754
|
+
function leaveApp() {
|
|
755
|
+
if (!isInteractive() || appDepth === 0) return;
|
|
756
|
+
appDepth -= 1;
|
|
757
|
+
if (appDepth > 0) return;
|
|
758
|
+
process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
759
|
+
}
|
|
760
|
+
async function onOrdinaryScreen(body) {
|
|
761
|
+
const depth = appDepth;
|
|
762
|
+
if (depth > 0) {
|
|
763
|
+
appDepth = 0;
|
|
764
|
+
process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
return await body();
|
|
768
|
+
} finally {
|
|
769
|
+
if (depth > 0) {
|
|
770
|
+
appDepth = depth;
|
|
771
|
+
process.stdout.write("\x1B[?1049h\x1B[H\x1B[2J");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
711
775
|
function compactHeader(context) {
|
|
712
776
|
const facts = Object.fromEntries(context.facts);
|
|
713
777
|
const parts = [
|
|
@@ -722,11 +786,101 @@ function compactHeader(context) {
|
|
|
722
786
|
async function beginScreen(source, options = {}) {
|
|
723
787
|
if (!isInteractive()) return;
|
|
724
788
|
clearScreen();
|
|
725
|
-
|
|
726
|
-
|
|
789
|
+
const reserve = options.reserve ?? 0;
|
|
790
|
+
if (options.compact || rows() < LOGO_SMALL.length + 6 + reserve) compactHeader(source());
|
|
791
|
+
else await showWelcome(source(), { animate: options.animate ?? false, reserve });
|
|
792
|
+
}
|
|
793
|
+
function drawScreen(source, options = {}) {
|
|
794
|
+
if (!isInteractive()) return;
|
|
795
|
+
clearScreen();
|
|
796
|
+
const reserve = options.reserve ?? 0;
|
|
797
|
+
const context = source();
|
|
798
|
+
if (options.compact || rows() < LOGO_SMALL.length + 6 + reserve) compactHeader(context);
|
|
799
|
+
else for (const line of welcomeLines(context, reserve)) out(line);
|
|
800
|
+
}
|
|
801
|
+
async function showOutput(lines, options) {
|
|
802
|
+
if (!isInteractive()) {
|
|
803
|
+
for (const line of lines) process.stdout.write(`${line}
|
|
804
|
+
`);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const stdout = process.stdout;
|
|
808
|
+
const stdin = process.stdin;
|
|
809
|
+
const body = lines.length > 0 ? lines : [style.grey(" (no output)")];
|
|
810
|
+
let headerRows = 0;
|
|
811
|
+
const measure = () => {
|
|
812
|
+
let count = 0;
|
|
813
|
+
const original = stdout.write.bind(stdout);
|
|
814
|
+
stdout.write = ((chunk) => {
|
|
815
|
+
count += String(chunk).split("\n").length - 1;
|
|
816
|
+
return true;
|
|
817
|
+
});
|
|
818
|
+
try {
|
|
819
|
+
options.drawHeader();
|
|
820
|
+
} finally {
|
|
821
|
+
stdout.write = original;
|
|
822
|
+
}
|
|
823
|
+
headerRows = count;
|
|
824
|
+
};
|
|
825
|
+
measure();
|
|
826
|
+
let top = 0;
|
|
827
|
+
const viewport = () => Math.max(3, rows() - headerRows - 2);
|
|
828
|
+
const draw = () => {
|
|
829
|
+
const height = viewport();
|
|
830
|
+
const maxTop = Math.max(0, body.length - height);
|
|
831
|
+
top = Math.min(Math.max(0, top), maxTop);
|
|
832
|
+
clearScreen();
|
|
833
|
+
options.drawHeader();
|
|
834
|
+
for (const line of body.slice(top, top + height)) {
|
|
835
|
+
stdout.write(`${fit(line, columns() - 1)}
|
|
836
|
+
`);
|
|
837
|
+
}
|
|
838
|
+
const hidden = body.length > height;
|
|
839
|
+
const position = hidden ? `${Math.min(top + height, body.length)}/${body.length}` : `${body.length} line${body.length === 1 ? "" : "s"}`;
|
|
840
|
+
const keys = hidden ? "\u2191\u2193 scroll \xB7 " : "";
|
|
841
|
+
stdout.write(
|
|
842
|
+
`${brand("\u2190")} ${style.bold(options.label ?? "Back")} ${style.grey(`\xB7 ${keys}enter or esc \xB7 ${position}`)}`
|
|
843
|
+
);
|
|
844
|
+
};
|
|
845
|
+
await new Promise((resolve) => {
|
|
846
|
+
const wasRaw = stdin.isRaw;
|
|
847
|
+
const done = () => {
|
|
848
|
+
stdin.off("keypress", onKey);
|
|
849
|
+
stdin.setRawMode(wasRaw);
|
|
850
|
+
stdin.pause();
|
|
851
|
+
process.stdout.off("resize", onResize);
|
|
852
|
+
stdout.write("\r\x1B[K");
|
|
853
|
+
resolve();
|
|
854
|
+
};
|
|
855
|
+
const onResize = () => {
|
|
856
|
+
measure();
|
|
857
|
+
draw();
|
|
858
|
+
};
|
|
859
|
+
const onKey = (input, key = {}) => {
|
|
860
|
+
const height = viewport();
|
|
861
|
+
if (key.name === "return" || key.name === "enter" || key.name === "escape" || key.name === "left" || input === "b" || key.ctrl && key.name === "c") {
|
|
862
|
+
return done();
|
|
863
|
+
}
|
|
864
|
+
if (key.name === "down" || input === "j") top += 1;
|
|
865
|
+
else if (key.name === "up" || input === "k") top -= 1;
|
|
866
|
+
else if (key.name === "pagedown" || input === " ") top += height;
|
|
867
|
+
else if (key.name === "pageup") top -= height;
|
|
868
|
+
else if (input === "G") top = body.length;
|
|
869
|
+
else if (input === "g") top = 0;
|
|
870
|
+
else return;
|
|
871
|
+
draw();
|
|
872
|
+
};
|
|
873
|
+
emitKeypressEvents(stdin);
|
|
874
|
+
stdin.setRawMode(true);
|
|
875
|
+
stdin.resume();
|
|
876
|
+
stdin.on("keypress", onKey);
|
|
877
|
+
process.stdout.on("resize", onResize);
|
|
878
|
+
draw();
|
|
879
|
+
});
|
|
727
880
|
}
|
|
728
881
|
async function pressBack(label = "Back") {
|
|
729
882
|
if (!isInteractive()) return;
|
|
883
|
+
flushCapture();
|
|
730
884
|
const stdin = process.stdin;
|
|
731
885
|
const stdout = process.stdout;
|
|
732
886
|
out();
|
|
@@ -751,6 +905,29 @@ async function pressBack(label = "Back") {
|
|
|
751
905
|
stdin.on("keypress", onKey);
|
|
752
906
|
});
|
|
753
907
|
}
|
|
908
|
+
function watchFacts(onChange) {
|
|
909
|
+
const files = [
|
|
910
|
+
CREDENTIALS_FILE,
|
|
911
|
+
findProjectConfig() ?? path2.join(process.cwd(), PROJECT_CONFIG_FILE)
|
|
912
|
+
];
|
|
913
|
+
const signature = () => files.map((file) => {
|
|
914
|
+
try {
|
|
915
|
+
const stats = statSync(file);
|
|
916
|
+
return `${stats.mtimeMs}:${stats.size}`;
|
|
917
|
+
} catch {
|
|
918
|
+
return "absent";
|
|
919
|
+
}
|
|
920
|
+
}).join("|");
|
|
921
|
+
let last = signature();
|
|
922
|
+
const timer = setInterval(() => {
|
|
923
|
+
const now = signature();
|
|
924
|
+
if (now === last) return;
|
|
925
|
+
last = now;
|
|
926
|
+
onChange();
|
|
927
|
+
}, 500);
|
|
928
|
+
timer.unref();
|
|
929
|
+
return () => clearInterval(timer);
|
|
930
|
+
}
|
|
754
931
|
var BACK = /* @__PURE__ */ Symbol("veefore.back");
|
|
755
932
|
var MenuCancelled = class extends CliError {
|
|
756
933
|
constructor() {
|
|
@@ -765,6 +942,7 @@ async function select(question, real, settings = {}) {
|
|
|
765
942
|
const choosable = enabled.filter((index) => options[index]?.value !== BACK);
|
|
766
943
|
const fallback = choosable.includes(settings.initial ?? -1) ? settings.initial : choosable[0] ?? enabled[0];
|
|
767
944
|
if (!isInteractive()) return options[fallback].value;
|
|
945
|
+
flushCapture();
|
|
768
946
|
let cursor = fallback;
|
|
769
947
|
let rendered = 0;
|
|
770
948
|
const stdin = process.stdin;
|
|
@@ -794,6 +972,13 @@ async function select(question, real, settings = {}) {
|
|
|
794
972
|
`);
|
|
795
973
|
rendered = lines.length;
|
|
796
974
|
};
|
|
975
|
+
const repaint = () => {
|
|
976
|
+
if (!settings.redraw) return;
|
|
977
|
+
clearScreen();
|
|
978
|
+
settings.redraw();
|
|
979
|
+
rendered = 0;
|
|
980
|
+
draw();
|
|
981
|
+
};
|
|
797
982
|
const move = (direction) => {
|
|
798
983
|
const position = enabled.indexOf(cursor);
|
|
799
984
|
const next = (position + direction + enabled.length) % enabled.length;
|
|
@@ -804,10 +989,12 @@ async function select(question, real, settings = {}) {
|
|
|
804
989
|
const restoreCursor = () => {
|
|
805
990
|
stdout.write("\x1B[?25h");
|
|
806
991
|
};
|
|
992
|
+
const stopWatching = settings.redraw ? watchFacts(repaint) : void 0;
|
|
807
993
|
const finish = (result) => {
|
|
808
994
|
stdin.off("keypress", onKey);
|
|
809
995
|
stdin.setRawMode(wasRaw);
|
|
810
996
|
stdin.pause();
|
|
997
|
+
stopWatching?.();
|
|
811
998
|
process.off("exit", restoreCursor);
|
|
812
999
|
if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
|
|
813
1000
|
restoreCursor();
|
|
@@ -912,11 +1099,38 @@ var symbol = {
|
|
|
912
1099
|
info: style.cyan("\u203A"),
|
|
913
1100
|
bullet: style.grey("\u2022")
|
|
914
1101
|
};
|
|
1102
|
+
var captured = null;
|
|
1103
|
+
function beginCapture() {
|
|
1104
|
+
captured = [];
|
|
1105
|
+
}
|
|
1106
|
+
function endCapture() {
|
|
1107
|
+
const lines = captured ?? [];
|
|
1108
|
+
captured = null;
|
|
1109
|
+
return lines;
|
|
1110
|
+
}
|
|
1111
|
+
function flushCapture() {
|
|
1112
|
+
if (captured === null) return;
|
|
1113
|
+
const lines = captured;
|
|
1114
|
+
captured = null;
|
|
1115
|
+
for (const line of lines) process.stdout.write(`${line}
|
|
1116
|
+
`);
|
|
1117
|
+
}
|
|
1118
|
+
function isCapturing() {
|
|
1119
|
+
return captured !== null;
|
|
1120
|
+
}
|
|
915
1121
|
function out(line = "") {
|
|
1122
|
+
if (captured !== null) {
|
|
1123
|
+
captured.push(line);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
916
1126
|
process.stdout.write(`${line}
|
|
917
1127
|
`);
|
|
918
1128
|
}
|
|
919
1129
|
function err(line = "") {
|
|
1130
|
+
if (captured !== null) {
|
|
1131
|
+
captured.push(line);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
920
1134
|
process.stderr.write(`${line}
|
|
921
1135
|
`);
|
|
922
1136
|
}
|
|
@@ -925,15 +1139,15 @@ function heading(text) {
|
|
|
925
1139
|
out(style.bold(text));
|
|
926
1140
|
out(style.grey("\u2500".repeat(Math.min(text.length, 72))));
|
|
927
1141
|
}
|
|
928
|
-
function table(columns2,
|
|
929
|
-
if (
|
|
1142
|
+
function table(columns2, rows2) {
|
|
1143
|
+
if (rows2.length === 0) {
|
|
930
1144
|
out(style.grey(" (none)"));
|
|
931
1145
|
return;
|
|
932
1146
|
}
|
|
933
1147
|
const widths = columns2.map((column) => {
|
|
934
1148
|
const longest = Math.max(
|
|
935
1149
|
column.header.length,
|
|
936
|
-
...
|
|
1150
|
+
...rows2.map((row) => (row[column.key] ?? "").length)
|
|
937
1151
|
);
|
|
938
1152
|
return Math.min(column.width ?? longest, longest);
|
|
939
1153
|
});
|
|
@@ -949,7 +1163,7 @@ function table(columns2, rows) {
|
|
|
949
1163
|
columns2.map((column) => column.header),
|
|
950
1164
|
style.grey
|
|
951
1165
|
);
|
|
952
|
-
for (const row of
|
|
1166
|
+
for (const row of rows2) {
|
|
953
1167
|
renderRow(
|
|
954
1168
|
columns2.map((column) => row[column.key] ?? ""),
|
|
955
1169
|
(text) => text
|
|
@@ -966,6 +1180,7 @@ function keyValue(label, value) {
|
|
|
966
1180
|
out(` ${style.grey(`${label}:`)} ${value}`);
|
|
967
1181
|
}
|
|
968
1182
|
async function ask(question, fallback = "") {
|
|
1183
|
+
flushCapture();
|
|
969
1184
|
if (!process.stdin.isTTY) return fallback;
|
|
970
1185
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
971
1186
|
try {
|
|
@@ -976,6 +1191,7 @@ async function ask(question, fallback = "") {
|
|
|
976
1191
|
}
|
|
977
1192
|
}
|
|
978
1193
|
async function askSecret(question) {
|
|
1194
|
+
flushCapture();
|
|
979
1195
|
if (!process.stdin.isTTY) {
|
|
980
1196
|
throw new Error(
|
|
981
1197
|
"A secret is required but the terminal is not interactive. Provide it through the documented environment variable instead."
|
|
@@ -1018,6 +1234,7 @@ async function askSecret(question) {
|
|
|
1018
1234
|
});
|
|
1019
1235
|
}
|
|
1020
1236
|
async function confirm(question, options = {}) {
|
|
1237
|
+
flushCapture();
|
|
1021
1238
|
if (options.yes) {
|
|
1022
1239
|
out(`${question} ${style.grey("[y/N]")} ${style.amber("y (--yes)")}`);
|
|
1023
1240
|
return true;
|
|
@@ -1040,6 +1257,12 @@ async function confirm(question, options = {}) {
|
|
|
1040
1257
|
return ["y", "yes"].includes(answer.toLowerCase());
|
|
1041
1258
|
}
|
|
1042
1259
|
function spinner(label) {
|
|
1260
|
+
if (isCapturing()) {
|
|
1261
|
+
return {
|
|
1262
|
+
succeed: (text) => out(`${symbol.pass} ${text ?? label}`),
|
|
1263
|
+
fail: (text) => out(`${symbol.fail} ${text ?? label}`)
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1043
1266
|
if (!process.stdout.isTTY) {
|
|
1044
1267
|
out(`${symbol.info} ${label}`);
|
|
1045
1268
|
return {
|
|
@@ -1163,6 +1386,7 @@ async function resolveContext(flags) {
|
|
|
1163
1386
|
};
|
|
1164
1387
|
}
|
|
1165
1388
|
async function login(controlPlaneUrl, options = {}) {
|
|
1389
|
+
flushCapture();
|
|
1166
1390
|
await assertControlPlane(controlPlaneUrl);
|
|
1167
1391
|
const ciCredentials = Boolean(process.env.VEEFORE_EMAIL && process.env.VEEFORE_PASSWORD);
|
|
1168
1392
|
if (options.method === "password" || options.method === void 0 && ciCredentials) {
|
|
@@ -1739,11 +1963,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
|
|
|
1739
1963
|
out(` ${style.red("This action is irreversible for this identity.")}`);
|
|
1740
1964
|
out();
|
|
1741
1965
|
}
|
|
1742
|
-
async function lifecycle(args, flags,
|
|
1966
|
+
async function lifecycle(args, flags, path13, resultingStatus) {
|
|
1743
1967
|
const context = await resolveContext(flags);
|
|
1744
1968
|
const agentId = positional(args) ?? context.config.agentId;
|
|
1745
1969
|
const agent = await context.api.get(`/v1/agents/${agentId}`);
|
|
1746
|
-
const reasonRequired =
|
|
1970
|
+
const reasonRequired = path13 === "suspend";
|
|
1747
1971
|
const reason = valueFor(args, "--reason");
|
|
1748
1972
|
if (reasonRequired && (!reason || reason.trim().length < 10)) {
|
|
1749
1973
|
throw new CliError(
|
|
@@ -1753,24 +1977,24 @@ async function lifecycle(args, flags, path12, resultingStatus) {
|
|
|
1753
1977
|
}
|
|
1754
1978
|
out();
|
|
1755
1979
|
out(
|
|
1756
|
-
`${symbol.info} ${
|
|
1980
|
+
`${symbol.info} ${path13} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
|
|
1757
1981
|
);
|
|
1758
|
-
if (
|
|
1982
|
+
if (path13 !== "resume") {
|
|
1759
1983
|
const confirmed = await confirm("Continue?", { yes: flags.yes });
|
|
1760
1984
|
if (!confirmed) {
|
|
1761
1985
|
out(style.grey("Cancelled."));
|
|
1762
1986
|
return 1;
|
|
1763
1987
|
}
|
|
1764
1988
|
}
|
|
1765
|
-
const actionSpinner = spinner(`Applying ${
|
|
1989
|
+
const actionSpinner = spinner(`Applying ${path13}`);
|
|
1766
1990
|
try {
|
|
1767
1991
|
await withStepUp(
|
|
1768
1992
|
context.api,
|
|
1769
|
-
() => context.api.post(`/v1/agents/${agentId}/${
|
|
1993
|
+
() => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
|
|
1770
1994
|
);
|
|
1771
1995
|
actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
|
|
1772
1996
|
} catch (error) {
|
|
1773
|
-
actionSpinner.fail(`Could not ${
|
|
1997
|
+
actionSpinner.fail(`Could not ${path13} the agent`);
|
|
1774
1998
|
throw error;
|
|
1775
1999
|
}
|
|
1776
2000
|
out();
|
|
@@ -2074,13 +2298,13 @@ async function exportEvents(args, flags) {
|
|
|
2074
2298
|
}
|
|
2075
2299
|
|
|
2076
2300
|
// src/commands/doctor.ts
|
|
2077
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
2078
|
-
import
|
|
2301
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
2302
|
+
import path4 from "node:path";
|
|
2079
2303
|
|
|
2080
2304
|
// src/project-checks.ts
|
|
2081
2305
|
import { spawnSync } from "node:child_process";
|
|
2082
2306
|
import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
2083
|
-
import
|
|
2307
|
+
import path3 from "node:path";
|
|
2084
2308
|
var MIN_APPROVAL_WAIT_SDK = "1.1.0";
|
|
2085
2309
|
function versionLessThan(a, b) {
|
|
2086
2310
|
if (!a) return true;
|
|
@@ -2097,19 +2321,19 @@ function versionLessThan(a, b) {
|
|
|
2097
2321
|
function installedSdks(root, options = {}) {
|
|
2098
2322
|
const found = [];
|
|
2099
2323
|
const seen = /* @__PURE__ */ new Set();
|
|
2100
|
-
const top =
|
|
2324
|
+
const top = path3.resolve(root);
|
|
2101
2325
|
const candidates = /* @__PURE__ */ new Set([top]);
|
|
2102
2326
|
for (const dir of options.dirs ?? []) {
|
|
2103
|
-
let current =
|
|
2327
|
+
let current = path3.resolve(top, dir);
|
|
2104
2328
|
while (current.startsWith(top)) {
|
|
2105
2329
|
candidates.add(current);
|
|
2106
|
-
const parent =
|
|
2330
|
+
const parent = path3.dirname(current);
|
|
2107
2331
|
if (parent === current) break;
|
|
2108
2332
|
current = parent;
|
|
2109
2333
|
}
|
|
2110
2334
|
}
|
|
2111
2335
|
for (const dir of candidates) {
|
|
2112
|
-
const manifest =
|
|
2336
|
+
const manifest = path3.join(dir, "node_modules", "@veefore", "sdk", "package.json");
|
|
2113
2337
|
if (!existsSync2(manifest)) continue;
|
|
2114
2338
|
let real = manifest;
|
|
2115
2339
|
try {
|
|
@@ -2126,7 +2350,7 @@ function installedSdks(root, options = {}) {
|
|
|
2126
2350
|
}
|
|
2127
2351
|
found.push({
|
|
2128
2352
|
language: "js",
|
|
2129
|
-
location:
|
|
2353
|
+
location: path3.relative(top, path3.dirname(manifest)) || ".",
|
|
2130
2354
|
version,
|
|
2131
2355
|
waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
|
|
2132
2356
|
});
|
|
@@ -2158,7 +2382,7 @@ function upgradeCommand(root, language) {
|
|
|
2158
2382
|
}
|
|
2159
2383
|
function configuredApprovalMode(root) {
|
|
2160
2384
|
for (const name of [".env.local", ".env"]) {
|
|
2161
|
-
const file =
|
|
2385
|
+
const file = path3.join(root, name);
|
|
2162
2386
|
if (!existsSync2(file)) continue;
|
|
2163
2387
|
const match = /^\s*(?:export\s+)?VEEFORE_APPROVAL_MODE\s*=\s*["']?(\w+)["']?/m.exec(
|
|
2164
2388
|
readFileSync2(file, "utf8")
|
|
@@ -2168,7 +2392,7 @@ function configuredApprovalMode(root) {
|
|
|
2168
2392
|
return null;
|
|
2169
2393
|
}
|
|
2170
2394
|
function sdkInstalled(root, language) {
|
|
2171
|
-
if (language === "js") return existsSync2(
|
|
2395
|
+
if (language === "js") return existsSync2(path3.join(root, "node_modules", "@veefore", "sdk"));
|
|
2172
2396
|
const python = projectPython(root);
|
|
2173
2397
|
if (!python) return false;
|
|
2174
2398
|
return spawnSync(python, ["-c", "import veefore"], { cwd: root, stdio: "ignore", timeout: 2e4 }).status === 0;
|
|
@@ -2177,18 +2401,18 @@ function typecheck(root, files) {
|
|
|
2177
2401
|
const name = "TypeScript typecheck";
|
|
2178
2402
|
const projects = files ? [
|
|
2179
2403
|
...new Set(
|
|
2180
|
-
files.filter((file) => /\.(m|c)?tsx?$/.test(file)).map((file) => nearestUp(root,
|
|
2404
|
+
files.filter((file) => /\.(m|c)?tsx?$/.test(file)).map((file) => nearestUp(root, path3.dirname(path3.join(root, file)), "tsconfig.json")).filter((dir) => dir !== null)
|
|
2181
2405
|
)
|
|
2182
|
-
] : existsSync2(
|
|
2406
|
+
] : existsSync2(path3.join(root, "tsconfig.json")) ? [root] : [];
|
|
2183
2407
|
if (projects.length === 0) return { name, ran: false, ok: true, detail: "no tsconfig.json" };
|
|
2184
2408
|
const binary = process.platform === "win32" ? "tsc.cmd" : "tsc";
|
|
2185
2409
|
for (const project of projects) {
|
|
2186
|
-
const bin = nearestUp(root, project,
|
|
2410
|
+
const bin = nearestUp(root, project, path3.join("node_modules", ".bin", binary));
|
|
2187
2411
|
if (!bin) {
|
|
2188
2412
|
return { name, ran: false, ok: true, detail: "TypeScript is not installed in this project" };
|
|
2189
2413
|
}
|
|
2190
2414
|
const result = spawnSync(
|
|
2191
|
-
|
|
2415
|
+
path3.join(bin, "node_modules", ".bin", binary),
|
|
2192
2416
|
["--noEmit", "-p", "."],
|
|
2193
2417
|
{
|
|
2194
2418
|
cwd: project,
|
|
@@ -2198,7 +2422,7 @@ function typecheck(root, files) {
|
|
|
2198
2422
|
);
|
|
2199
2423
|
if (result.status !== 0) {
|
|
2200
2424
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2201
|
-
const where =
|
|
2425
|
+
const where = path3.relative(root, project) || ".";
|
|
2202
2426
|
return {
|
|
2203
2427
|
name,
|
|
2204
2428
|
ran: true,
|
|
@@ -2211,12 +2435,12 @@ ${output.split("\n").slice(0, 12).join("\n")}`
|
|
|
2211
2435
|
return { name, ran: true, ok: true, detail: "" };
|
|
2212
2436
|
}
|
|
2213
2437
|
function nearestUp(root, start, entry) {
|
|
2214
|
-
const top =
|
|
2215
|
-
let dir =
|
|
2438
|
+
const top = path3.resolve(root);
|
|
2439
|
+
let dir = path3.resolve(start);
|
|
2216
2440
|
for (; ; ) {
|
|
2217
|
-
if (existsSync2(
|
|
2441
|
+
if (existsSync2(path3.join(dir, entry))) return dir;
|
|
2218
2442
|
if (dir === top) return null;
|
|
2219
|
-
const parent =
|
|
2443
|
+
const parent = path3.dirname(dir);
|
|
2220
2444
|
if (parent === dir || !parent.startsWith(top)) return null;
|
|
2221
2445
|
dir = parent;
|
|
2222
2446
|
}
|
|
@@ -2270,20 +2494,20 @@ function pythonSyntax(root, files) {
|
|
|
2270
2494
|
}
|
|
2271
2495
|
function projectPython(root) {
|
|
2272
2496
|
for (const candidate of [".venv/bin/python", "venv/bin/python", ".venv/Scripts/python.exe"]) {
|
|
2273
|
-
if (existsSync2(
|
|
2497
|
+
if (existsSync2(path3.join(root, candidate))) return path3.join(root, candidate);
|
|
2274
2498
|
}
|
|
2275
2499
|
const probe = spawnSync("python3", ["--version"], { stdio: "ignore" });
|
|
2276
2500
|
return probe.status === 0 ? "python3" : null;
|
|
2277
2501
|
}
|
|
2278
2502
|
function installCommand(root, language) {
|
|
2279
2503
|
if (language === "py") {
|
|
2280
|
-
if (existsSync2(
|
|
2281
|
-
if (existsSync2(
|
|
2504
|
+
if (existsSync2(path3.join(root, "uv.lock"))) return "uv add veefore-py";
|
|
2505
|
+
if (existsSync2(path3.join(root, "poetry.lock"))) return "poetry add veefore-py";
|
|
2282
2506
|
return "pip install veefore-py";
|
|
2283
2507
|
}
|
|
2284
|
-
if (existsSync2(
|
|
2285
|
-
if (existsSync2(
|
|
2286
|
-
if (existsSync2(
|
|
2508
|
+
if (existsSync2(path3.join(root, "pnpm-lock.yaml"))) return "pnpm add @veefore/sdk";
|
|
2509
|
+
if (existsSync2(path3.join(root, "yarn.lock"))) return "yarn add @veefore/sdk";
|
|
2510
|
+
if (existsSync2(path3.join(root, "bun.lockb")) || existsSync2(path3.join(root, "bun.lock"))) {
|
|
2287
2511
|
return "bun add @veefore/sdk";
|
|
2288
2512
|
}
|
|
2289
2513
|
return "npm install @veefore/sdk";
|
|
@@ -2403,8 +2627,8 @@ async function checkTransportSecurity(config, add) {
|
|
|
2403
2627
|
}
|
|
2404
2628
|
function checkLocalSecrets(config, add) {
|
|
2405
2629
|
const configFile = findProjectConfig();
|
|
2406
|
-
const projectDir = configFile ?
|
|
2407
|
-
const envFile =
|
|
2630
|
+
const projectDir = configFile ? path4.dirname(configFile) : process.cwd();
|
|
2631
|
+
const envFile = path4.join(projectDir, ".env.local");
|
|
2408
2632
|
if (!existsSync3(envFile)) {
|
|
2409
2633
|
add({
|
|
2410
2634
|
check: "Local agent credentials",
|
|
@@ -2414,7 +2638,7 @@ function checkLocalSecrets(config, add) {
|
|
|
2414
2638
|
});
|
|
2415
2639
|
return;
|
|
2416
2640
|
}
|
|
2417
|
-
const mode =
|
|
2641
|
+
const mode = statSync2(envFile).mode & 511;
|
|
2418
2642
|
if ((mode & 63) !== 0) {
|
|
2419
2643
|
add({
|
|
2420
2644
|
check: "Local agent credentials",
|
|
@@ -2429,7 +2653,7 @@ function checkLocalSecrets(config, add) {
|
|
|
2429
2653
|
message: `.env.local is mode ${mode.toString(8)} (owner only).`
|
|
2430
2654
|
});
|
|
2431
2655
|
}
|
|
2432
|
-
const gitignore =
|
|
2656
|
+
const gitignore = path4.join(projectDir, ".gitignore");
|
|
2433
2657
|
const ignored = existsSync3(gitignore) && readFileSync3(gitignore, "utf8").split(/\r?\n/).some((line) => line.trim() === ".env.local" || line.trim() === "*.local");
|
|
2434
2658
|
add({
|
|
2435
2659
|
check: "Secrets excluded from version control",
|
|
@@ -2757,8 +2981,8 @@ async function checkCredentialRotation(api, add) {
|
|
|
2757
2981
|
}
|
|
2758
2982
|
async function checkDependencies(add, dir) {
|
|
2759
2983
|
const configFile = findProjectConfig();
|
|
2760
|
-
const projectDir = dir ?? (configFile ?
|
|
2761
|
-
const manifest =
|
|
2984
|
+
const projectDir = dir ?? (configFile ? path4.dirname(configFile) : process.cwd());
|
|
2985
|
+
const manifest = path4.join(projectDir, "package.json");
|
|
2762
2986
|
if (!existsSync3(manifest)) {
|
|
2763
2987
|
add({
|
|
2764
2988
|
check: "SDK dependency",
|
|
@@ -2800,9 +3024,9 @@ async function checkDependencies(add, dir) {
|
|
|
2800
3024
|
}
|
|
2801
3025
|
function checkApprovalWaiting(add, projectDir) {
|
|
2802
3026
|
const configFile = findProjectConfig();
|
|
2803
|
-
const dir = projectDir ?? (configFile ?
|
|
3027
|
+
const dir = projectDir ?? (configFile ? path4.dirname(configFile) : process.cwd());
|
|
2804
3028
|
const python = ["pyproject.toml", "requirements.txt", "setup.py"].some(
|
|
2805
|
-
(name) => existsSync3(
|
|
3029
|
+
(name) => existsSync3(path4.join(dir, name))
|
|
2806
3030
|
);
|
|
2807
3031
|
const sdks = installedSdks(dir, { python });
|
|
2808
3032
|
const old = sdks.filter((sdk) => !sdk.waitsForApproval);
|
|
@@ -3094,11 +3318,11 @@ function isLoosening(from, to) {
|
|
|
3094
3318
|
|
|
3095
3319
|
// src/commands/init.ts
|
|
3096
3320
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3097
|
-
import
|
|
3321
|
+
import path12 from "node:path";
|
|
3098
3322
|
|
|
3099
3323
|
// src/integrate.ts
|
|
3100
3324
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3101
|
-
import
|
|
3325
|
+
import path9 from "node:path";
|
|
3102
3326
|
|
|
3103
3327
|
// ../types/src/permissions.ts
|
|
3104
3328
|
var PERMISSIONS = [
|
|
@@ -4368,21 +4592,21 @@ import {
|
|
|
4368
4592
|
readdirSync,
|
|
4369
4593
|
renameSync as renameSync2,
|
|
4370
4594
|
rmSync as rmSync2,
|
|
4371
|
-
statSync as
|
|
4595
|
+
statSync as statSync3,
|
|
4372
4596
|
writeFileSync as writeFileSync3
|
|
4373
4597
|
} from "node:fs";
|
|
4374
|
-
import
|
|
4375
|
-
var BACKUP_DIR =
|
|
4598
|
+
import path5 from "node:path";
|
|
4599
|
+
var BACKUP_DIR = path5.join(".veefore", "backups");
|
|
4376
4600
|
function sha256(text) {
|
|
4377
4601
|
return createHash2("sha256").update(text).digest("hex");
|
|
4378
4602
|
}
|
|
4379
4603
|
function createBackup(root, changes) {
|
|
4380
4604
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4381
4605
|
const id = createdAt.replace(/[:.]/g, "-");
|
|
4382
|
-
const dir =
|
|
4606
|
+
const dir = path5.join(root, BACKUP_DIR, id);
|
|
4383
4607
|
for (const change of changes) {
|
|
4384
|
-
const target =
|
|
4385
|
-
mkdirSync2(
|
|
4608
|
+
const target = path5.join(dir, "files", change.file);
|
|
4609
|
+
mkdirSync2(path5.dirname(target), { recursive: true });
|
|
4386
4610
|
writeFileSync3(target, change.before, { mode: 384 });
|
|
4387
4611
|
}
|
|
4388
4612
|
const files = changes.map((change) => ({
|
|
@@ -4391,7 +4615,7 @@ function createBackup(root, changes) {
|
|
|
4391
4615
|
afterSha256: sha256(change.after)
|
|
4392
4616
|
}));
|
|
4393
4617
|
writeFileSync3(
|
|
4394
|
-
|
|
4618
|
+
path5.join(dir, "manifest.json"),
|
|
4395
4619
|
`${JSON.stringify({ version: 1, createdAt, files }, null, 2)}
|
|
4396
4620
|
`,
|
|
4397
4621
|
{ mode: 384 }
|
|
@@ -4399,28 +4623,28 @@ function createBackup(root, changes) {
|
|
|
4399
4623
|
return { id, dir, createdAt, files };
|
|
4400
4624
|
}
|
|
4401
4625
|
function writeFilePreservingMode(file, content) {
|
|
4402
|
-
const mode = existsSync4(file) ?
|
|
4626
|
+
const mode = existsSync4(file) ? statSync3(file).mode & 511 : 420;
|
|
4403
4627
|
const temp = `${file}.veefore-tmp-${process.pid}`;
|
|
4404
4628
|
writeFileSync3(temp, content, { mode });
|
|
4405
4629
|
renameSync2(temp, file);
|
|
4406
4630
|
}
|
|
4407
4631
|
function latestBackup(root) {
|
|
4408
|
-
const base2 =
|
|
4632
|
+
const base2 = path5.join(root, BACKUP_DIR);
|
|
4409
4633
|
if (!existsSync4(base2)) return null;
|
|
4410
|
-
const ids = readdirSync(base2).filter((entry) => existsSync4(
|
|
4634
|
+
const ids = readdirSync(base2).filter((entry) => existsSync4(path5.join(base2, entry, "manifest.json"))).sort();
|
|
4411
4635
|
const id = ids[ids.length - 1];
|
|
4412
4636
|
if (!id) return null;
|
|
4413
|
-
const dir =
|
|
4414
|
-
const manifest = JSON.parse(readFileSync4(
|
|
4637
|
+
const dir = path5.join(base2, id);
|
|
4638
|
+
const manifest = JSON.parse(readFileSync4(path5.join(dir, "manifest.json"), "utf8"));
|
|
4415
4639
|
return { id, dir, createdAt: manifest.createdAt, files: manifest.files };
|
|
4416
4640
|
}
|
|
4417
4641
|
function restoreBackup(root, backup, options = {}) {
|
|
4418
4642
|
const restored = [];
|
|
4419
4643
|
const conflicts = [];
|
|
4420
4644
|
for (const entry of backup.files) {
|
|
4421
|
-
const target =
|
|
4645
|
+
const target = path5.join(root, entry.file);
|
|
4422
4646
|
const current = existsSync4(target) ? readFileSync4(target, "utf8") : null;
|
|
4423
|
-
const original = readFileSync4(
|
|
4647
|
+
const original = readFileSync4(path5.join(backup.dir, "files", entry.file), "utf8");
|
|
4424
4648
|
if (current !== null && sha256(current) === entry.beforeSha256) {
|
|
4425
4649
|
restored.push(entry.file);
|
|
4426
4650
|
continue;
|
|
@@ -4440,15 +4664,15 @@ function restoreBackup(root, backup, options = {}) {
|
|
|
4440
4664
|
|
|
4441
4665
|
// src/rewrite.ts
|
|
4442
4666
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4443
|
-
import
|
|
4667
|
+
import path8 from "node:path";
|
|
4444
4668
|
|
|
4445
4669
|
// src/scan.ts
|
|
4446
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as
|
|
4447
|
-
import
|
|
4670
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
4671
|
+
import path7 from "node:path";
|
|
4448
4672
|
|
|
4449
4673
|
// src/framework.ts
|
|
4450
4674
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
4451
|
-
import
|
|
4675
|
+
import path6 from "node:path";
|
|
4452
4676
|
var SIGNATURES = [
|
|
4453
4677
|
{
|
|
4454
4678
|
key: "langchain",
|
|
@@ -4530,8 +4754,8 @@ function detectFramework(dir = process.cwd()) {
|
|
|
4530
4754
|
};
|
|
4531
4755
|
}
|
|
4532
4756
|
}
|
|
4533
|
-
if (nodeDeps.size > 0 || existsSync5(
|
|
4534
|
-
const typescript = existsSync5(
|
|
4757
|
+
if (nodeDeps.size > 0 || existsSync5(path6.join(dir, "package.json"))) {
|
|
4758
|
+
const typescript = existsSync5(path6.join(dir, "tsconfig.json")) || nodeDeps.has("typescript");
|
|
4535
4759
|
return {
|
|
4536
4760
|
key: "custom",
|
|
4537
4761
|
name: "Custom agent",
|
|
@@ -4553,7 +4777,7 @@ function normalize(name) {
|
|
|
4553
4777
|
return name.toLowerCase().replace(/_/g, "-");
|
|
4554
4778
|
}
|
|
4555
4779
|
function readPackageJsonDependencies(dir) {
|
|
4556
|
-
const file =
|
|
4780
|
+
const file = path6.join(dir, "package.json");
|
|
4557
4781
|
if (!existsSync5(file)) return /* @__PURE__ */ new Set();
|
|
4558
4782
|
try {
|
|
4559
4783
|
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
@@ -4570,12 +4794,12 @@ function readPackageJsonDependencies(dir) {
|
|
|
4570
4794
|
}
|
|
4571
4795
|
function hasPythonProject(dir) {
|
|
4572
4796
|
return ["pyproject.toml", "requirements.txt", "Pipfile", "setup.py"].some(
|
|
4573
|
-
(file) => existsSync5(
|
|
4797
|
+
(file) => existsSync5(path6.join(dir, file))
|
|
4574
4798
|
);
|
|
4575
4799
|
}
|
|
4576
4800
|
function readPythonDependencies(dir) {
|
|
4577
4801
|
const found = /* @__PURE__ */ new Set();
|
|
4578
|
-
const requirements =
|
|
4802
|
+
const requirements = path6.join(dir, "requirements.txt");
|
|
4579
4803
|
if (existsSync5(requirements)) {
|
|
4580
4804
|
for (const line of readFileSync5(requirements, "utf8").split(/\r?\n/)) {
|
|
4581
4805
|
const trimmed = line.trim();
|
|
@@ -4584,7 +4808,7 @@ function readPythonDependencies(dir) {
|
|
|
4584
4808
|
if (name) found.add(normalize(name));
|
|
4585
4809
|
}
|
|
4586
4810
|
}
|
|
4587
|
-
const pyproject =
|
|
4811
|
+
const pyproject = path6.join(dir, "pyproject.toml");
|
|
4588
4812
|
if (existsSync5(pyproject)) {
|
|
4589
4813
|
const text = readFileSync5(pyproject, "utf8");
|
|
4590
4814
|
for (const match of text.matchAll(
|
|
@@ -4616,7 +4840,7 @@ function findEntryPoints(dir = process.cwd(), limit = 5) {
|
|
|
4616
4840
|
"app.py",
|
|
4617
4841
|
"src/main.py"
|
|
4618
4842
|
];
|
|
4619
|
-
const found = candidates.filter((file) => existsSync5(
|
|
4843
|
+
const found = candidates.filter((file) => existsSync5(path6.join(dir, file)));
|
|
4620
4844
|
if (found.length > 0) return found.slice(0, limit);
|
|
4621
4845
|
try {
|
|
4622
4846
|
return readdirSync2(dir).filter((file) => /\.(ts|js|py)$/.test(file)).slice(0, limit);
|
|
@@ -4720,7 +4944,7 @@ function scanProject(root = process.cwd()) {
|
|
|
4720
4944
|
for (const relative of files) {
|
|
4721
4945
|
let text;
|
|
4722
4946
|
try {
|
|
4723
|
-
text = readFileSync6(
|
|
4947
|
+
text = readFileSync6(path7.join(root, relative), "utf8");
|
|
4724
4948
|
} catch {
|
|
4725
4949
|
continue;
|
|
4726
4950
|
}
|
|
@@ -5219,10 +5443,10 @@ function walk(root, dir, out2) {
|
|
|
5219
5443
|
if (entry.startsWith(".") && entry !== ".") {
|
|
5220
5444
|
continue;
|
|
5221
5445
|
}
|
|
5222
|
-
const full =
|
|
5446
|
+
const full = path7.join(dir, entry);
|
|
5223
5447
|
let stats;
|
|
5224
5448
|
try {
|
|
5225
|
-
stats =
|
|
5449
|
+
stats = statSync4(full);
|
|
5226
5450
|
} catch {
|
|
5227
5451
|
continue;
|
|
5228
5452
|
}
|
|
@@ -5232,14 +5456,14 @@ function walk(root, dir, out2) {
|
|
|
5232
5456
|
continue;
|
|
5233
5457
|
}
|
|
5234
5458
|
if (!stats.isFile() || stats.size > MAX_FILE_BYTES) continue;
|
|
5235
|
-
if (!SOURCE_EXTENSIONS.has(
|
|
5459
|
+
if (!SOURCE_EXTENSIONS.has(path7.extname(entry))) continue;
|
|
5236
5460
|
if (/\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$/.test(entry)) continue;
|
|
5237
|
-
out2.push(
|
|
5461
|
+
out2.push(path7.relative(root, full));
|
|
5238
5462
|
}
|
|
5239
5463
|
return false;
|
|
5240
5464
|
}
|
|
5241
5465
|
function readProjectName(root) {
|
|
5242
|
-
const pkg =
|
|
5466
|
+
const pkg = path7.join(root, "package.json");
|
|
5243
5467
|
if (existsSync6(pkg)) {
|
|
5244
5468
|
try {
|
|
5245
5469
|
const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
|
|
@@ -5247,12 +5471,12 @@ function readProjectName(root) {
|
|
|
5247
5471
|
} catch {
|
|
5248
5472
|
}
|
|
5249
5473
|
}
|
|
5250
|
-
const pyproject =
|
|
5474
|
+
const pyproject = path7.join(root, "pyproject.toml");
|
|
5251
5475
|
if (existsSync6(pyproject)) {
|
|
5252
5476
|
const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync6(pyproject, "utf8"))?.[1];
|
|
5253
5477
|
if (name) return name.trim();
|
|
5254
5478
|
}
|
|
5255
|
-
return
|
|
5479
|
+
return path7.basename(root) || null;
|
|
5256
5480
|
}
|
|
5257
5481
|
function stringField(body, field) {
|
|
5258
5482
|
const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
|
|
@@ -5307,7 +5531,7 @@ function planRewrite(root, files, toolKeys) {
|
|
|
5307
5531
|
for (const file of files) {
|
|
5308
5532
|
let before;
|
|
5309
5533
|
try {
|
|
5310
|
-
before = readFileSync7(
|
|
5534
|
+
before = readFileSync7(path8.join(root, file), "utf8");
|
|
5311
5535
|
} catch {
|
|
5312
5536
|
continue;
|
|
5313
5537
|
}
|
|
@@ -6297,13 +6521,13 @@ async function integrate(root, toolKeys, options) {
|
|
|
6297
6521
|
toWrite.map((file) => file.file)
|
|
6298
6522
|
) : null;
|
|
6299
6523
|
for (const file of toWrite) {
|
|
6300
|
-
if (readFileSync8(
|
|
6524
|
+
if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
|
|
6301
6525
|
out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
|
|
6302
6526
|
return { status: "declined", plan, checks: [], backupId: null, manual };
|
|
6303
6527
|
}
|
|
6304
6528
|
}
|
|
6305
6529
|
const backup = createBackup(root, toWrite);
|
|
6306
|
-
for (const file of toWrite) writeFilePreservingMode(
|
|
6530
|
+
for (const file of toWrite) writeFilePreservingMode(path9.join(root, file.file), file.after);
|
|
6307
6531
|
out(
|
|
6308
6532
|
`${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
|
|
6309
6533
|
);
|
|
@@ -6374,7 +6598,7 @@ async function integrate(root, toolKeys, options) {
|
|
|
6374
6598
|
function reportSdkReadiness(root, files) {
|
|
6375
6599
|
const problems = [];
|
|
6376
6600
|
if (files.length === 0) return problems;
|
|
6377
|
-
const dirs = [...new Set(files.map((file) =>
|
|
6601
|
+
const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
|
|
6378
6602
|
const python = files.some((file) => file.endsWith(".py"));
|
|
6379
6603
|
const sdks = installedSdks(root, { dirs, python });
|
|
6380
6604
|
for (const sdk of sdks) {
|
|
@@ -6451,7 +6675,7 @@ async function removeGuards(root, options) {
|
|
|
6451
6675
|
const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
|
|
6452
6676
|
const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
|
|
6453
6677
|
const backup = createBackup(root, toWrite);
|
|
6454
|
-
for (const entry of toWrite) writeFilePreservingMode(
|
|
6678
|
+
for (const entry of toWrite) writeFilePreservingMode(path9.join(root, entry.file), entry.after);
|
|
6455
6679
|
const checks = [];
|
|
6456
6680
|
const py = changed.filter((file) => file.endsWith(".py"));
|
|
6457
6681
|
if (py.length > 0) checks.push(pythonSyntax(root, py));
|
|
@@ -6475,7 +6699,7 @@ function currentText(root, plan, file) {
|
|
|
6475
6699
|
const entry = plan.files.find((candidate) => candidate.file === file);
|
|
6476
6700
|
if (entry) return entry.after;
|
|
6477
6701
|
try {
|
|
6478
|
-
return readFileSync8(
|
|
6702
|
+
return readFileSync8(path9.join(root, file), "utf8");
|
|
6479
6703
|
} catch {
|
|
6480
6704
|
return null;
|
|
6481
6705
|
}
|
|
@@ -6500,7 +6724,7 @@ function applyDispatch(root, plan, file, after, line, items, via) {
|
|
|
6500
6724
|
function readSources(root) {
|
|
6501
6725
|
return listSourceFiles(root).flatMap((file) => {
|
|
6502
6726
|
try {
|
|
6503
|
-
return [{ file, text: readFileSync8(
|
|
6727
|
+
return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
|
|
6504
6728
|
} catch {
|
|
6505
6729
|
return [];
|
|
6506
6730
|
}
|
|
@@ -6622,7 +6846,7 @@ function printPlan(plan) {
|
|
|
6622
6846
|
|
|
6623
6847
|
// src/commands/remove.ts
|
|
6624
6848
|
import { rmSync as rmSync3 } from "node:fs";
|
|
6625
|
-
import
|
|
6849
|
+
import path10 from "node:path";
|
|
6626
6850
|
|
|
6627
6851
|
// src/discover.ts
|
|
6628
6852
|
var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
|
|
@@ -6736,7 +6960,7 @@ async function removeCommand(args, flags, known) {
|
|
|
6736
6960
|
if (!file || !config) {
|
|
6737
6961
|
throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
|
|
6738
6962
|
}
|
|
6739
|
-
const root =
|
|
6963
|
+
const root = path10.dirname(file);
|
|
6740
6964
|
const state = known ?? await agentState(config);
|
|
6741
6965
|
heading("Remove Veefore from this project");
|
|
6742
6966
|
details([
|
|
@@ -7270,10 +7494,10 @@ async function resolveAuthorizedTools(context, filter) {
|
|
|
7270
7494
|
|
|
7271
7495
|
// src/panel.ts
|
|
7272
7496
|
import { homedir as homedir2 } from "node:os";
|
|
7273
|
-
import
|
|
7497
|
+
import path11 from "node:path";
|
|
7274
7498
|
|
|
7275
7499
|
// src/version.ts
|
|
7276
|
-
var CLI_VERSION = "1.
|
|
7500
|
+
var CLI_VERSION = "1.4.0";
|
|
7277
7501
|
|
|
7278
7502
|
// src/panel.ts
|
|
7279
7503
|
function welcomeSource(flags = {}) {
|
|
@@ -7297,7 +7521,7 @@ function welcomeSource(flags = {}) {
|
|
|
7297
7521
|
}
|
|
7298
7522
|
function tilde(dir) {
|
|
7299
7523
|
const home = homedir2();
|
|
7300
|
-
return dir === home || dir.startsWith(`${home}${
|
|
7524
|
+
return dir === home || dir.startsWith(`${home}${path11.sep}`) ? `~${dir.slice(home.length)}` : dir;
|
|
7301
7525
|
}
|
|
7302
7526
|
function hostOf(url) {
|
|
7303
7527
|
try {
|
|
@@ -7637,317 +7861,738 @@ function sourceLabel(source) {
|
|
|
7637
7861
|
}
|
|
7638
7862
|
}
|
|
7639
7863
|
|
|
7640
|
-
// src/commands/
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
if (existing && !options.force && isInteractive() && !flags.json) {
|
|
7646
|
-
return projectHome(existing, flags);
|
|
7647
|
-
}
|
|
7648
|
-
if (existing && !options.force) {
|
|
7649
|
-
out();
|
|
7650
|
-
out(`${symbol.warn} This project is already connected to Veefore.`);
|
|
7651
|
-
details([
|
|
7652
|
-
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
7653
|
-
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
7654
|
-
["Control plane", existing.controlPlaneUrl]
|
|
7655
|
-
]);
|
|
7656
|
-
out();
|
|
7657
|
-
out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
|
|
7864
|
+
// src/commands/scan.ts
|
|
7865
|
+
async function scanCommand(_args, flags) {
|
|
7866
|
+
const scan = scanProject(process.cwd());
|
|
7867
|
+
if (flags.json) {
|
|
7868
|
+
out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
|
|
7658
7869
|
return 0;
|
|
7659
7870
|
}
|
|
7660
|
-
heading("Veefore
|
|
7661
|
-
const framework = detectFramework();
|
|
7871
|
+
heading("Veefore scan");
|
|
7662
7872
|
out(
|
|
7663
|
-
`${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language},
|
|
7873
|
+
`${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
|
|
7664
7874
|
);
|
|
7665
|
-
if (framework.key === "unknown") {
|
|
7666
|
-
out(
|
|
7667
|
-
style.grey(
|
|
7668
|
-
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
7669
|
-
)
|
|
7670
|
-
);
|
|
7671
|
-
}
|
|
7672
|
-
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
7673
|
-
const controlPlaneUrl = found.controlPlaneUrl;
|
|
7674
|
-
if (found.source !== "entered") {
|
|
7675
|
-
out(
|
|
7676
|
-
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
7677
|
-
);
|
|
7678
|
-
}
|
|
7679
|
-
let session = loadSession(controlPlaneUrl);
|
|
7680
|
-
if (!session) {
|
|
7681
|
-
await login(controlPlaneUrl);
|
|
7682
|
-
session = loadSession(controlPlaneUrl);
|
|
7683
|
-
}
|
|
7684
|
-
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
7685
|
-
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
7686
|
-
let me = await api.get("/v1/auth/session");
|
|
7687
|
-
const STEPS = ["organization", "workspace", "environment"];
|
|
7688
|
-
const asked = /* @__PURE__ */ new Map();
|
|
7689
|
-
let organization;
|
|
7690
|
-
let workspace;
|
|
7691
|
-
let environment;
|
|
7692
|
-
for (let at = 0; at < STEPS.length; ) {
|
|
7693
|
-
const step = STEPS[at];
|
|
7694
|
-
const answer = step === "organization" ? await selectOrganization(api, me, options, flags) : step === "workspace" ? await selectWorkspace(api, me, options, flags) : await selectEnvironment(api, me, workspace.id, options, flags);
|
|
7695
|
-
if (answer === BACK) {
|
|
7696
|
-
let previous = at - 1;
|
|
7697
|
-
while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
|
|
7698
|
-
if (previous < 0) {
|
|
7699
|
-
throw new MenuCancelled();
|
|
7700
|
-
}
|
|
7701
|
-
at = previous;
|
|
7702
|
-
continue;
|
|
7703
|
-
}
|
|
7704
|
-
asked.set(step, answer.asked);
|
|
7705
|
-
if (step === "organization") {
|
|
7706
|
-
organization = answer.value;
|
|
7707
|
-
if (answer.asked) me = await api.get("/v1/auth/session");
|
|
7708
|
-
} else if (step === "workspace") {
|
|
7709
|
-
workspace = answer.value;
|
|
7710
|
-
} else {
|
|
7711
|
-
environment = answer.value;
|
|
7712
|
-
}
|
|
7713
|
-
at += 1;
|
|
7714
|
-
}
|
|
7715
|
-
heading("Looking at your project");
|
|
7716
|
-
const scan = scanProject(process.cwd());
|
|
7717
7875
|
printScan(scan);
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
printPlan2(plan, environment);
|
|
7733
|
-
out();
|
|
7734
|
-
const proceed = await confirm("Set this up?", { yes: flags.yes });
|
|
7735
|
-
if (proceed) break;
|
|
7736
|
-
if (flags.yes || options.agentName) {
|
|
7737
|
-
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
7738
|
-
return 1;
|
|
7739
|
-
}
|
|
7740
|
-
const next = await select(
|
|
7741
|
-
"What would you like to do?",
|
|
7876
|
+
out();
|
|
7877
|
+
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
7878
|
+
return 0;
|
|
7879
|
+
}
|
|
7880
|
+
|
|
7881
|
+
// src/help.ts
|
|
7882
|
+
var COMMANDS = [
|
|
7883
|
+
{
|
|
7884
|
+
name: "init",
|
|
7885
|
+
summary: "Set up this project with Veefore. Also runs as plain `veefore`.",
|
|
7886
|
+
usage: "veefore init [options]",
|
|
7887
|
+
description: "Connects to hosted Veefore, or to the self-hosted Veefore named with --control-plane (remembered afterwards), signs you in through the browser, scans the project locally (framework, tools, the hosts they call, the credentials they read \u2014 names only, never values), shows what it will set up and asks once. It then creates an agent with a key pair generated on this machine, registers each tool with its own capability and authorizes it for this agent, adds a starter policy in OBSERVE mode, writes veefore.config.json and .env.local, and validates the connection. The private key is written to .env.local only and is never sent, printed or recoverable.",
|
|
7888
|
+
options: [
|
|
7889
|
+
["--force", "Replace an existing veefore.config.json."],
|
|
7742
7890
|
[
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
{ label: "Leave setup", value: "leave" }
|
|
7891
|
+
"--new-agent",
|
|
7892
|
+
"Create a separate agent even if one with this name exists. By default the existing agent is reused with a new key, so its delegations and limits keep applying."
|
|
7746
7893
|
],
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7894
|
+
["--control-plane <url>", "Self-hosted Veefore address. Needed once; remembered after."],
|
|
7895
|
+
["--gateway <url>", "Gateway base URL that agents will call."],
|
|
7896
|
+
["--organization <id>", "Skip the organization prompt."],
|
|
7897
|
+
["--workspace <id>", "Skip the workspace prompt."],
|
|
7898
|
+
["--environment <id>", "Skip the environment prompt."],
|
|
7899
|
+
["--name <name>", "Agent display name."],
|
|
7900
|
+
["--capability <key>", "Grant a capability. Repeatable."],
|
|
7901
|
+
["--skip-policy", "Do not create a starter policy."],
|
|
7902
|
+
["--skip-code", "Do not offer to change your code; run `veefore integrate` later."],
|
|
7903
|
+
[
|
|
7904
|
+
"--approval-mode <wait|throw>",
|
|
7905
|
+
"When approval is required, guarded tools wait for the decision (default) or return at once."
|
|
7906
|
+
]
|
|
7907
|
+
],
|
|
7908
|
+
examples: ["veefore init", "veefore init --name finance-agent --capability refund.create"]
|
|
7909
|
+
},
|
|
7910
|
+
{
|
|
7911
|
+
name: "scan",
|
|
7912
|
+
summary: "Preview what setup would find, without changing anything.",
|
|
7913
|
+
usage: "veefore scan [--json]",
|
|
7914
|
+
description: "Scans the project locally and lists its framework, tools, the external hosts each tool calls and the credentials the code reads. Reads source files only: .env files and other hidden files are never opened, and secret values are never read. Needs no sign-in.",
|
|
7915
|
+
options: [["--json", "Machine-readable scan and the setup plan it implies."]]
|
|
7916
|
+
},
|
|
7917
|
+
{
|
|
7918
|
+
name: "integrate",
|
|
7919
|
+
summary: "Wrap your registered tools in guard() so Veefore decides every call.",
|
|
7920
|
+
usage: "veefore integrate [--dry-run] [--check] [--assist=manual|own|veefore|off] [--yes]",
|
|
7921
|
+
description: "Finds each tool registered for this agent in your code and wraps its function in guard() (TypeScript/JavaScript) or adds @guard above it (Python). Your function bodies are not edited. Shows a diff and asks before writing, backs up every file it changes, then runs your TypeScript compiler (--noEmit), node --check or a Python syntax check; if a check newly fails, every file is restored automatically. For tools it will not change on its own, you choose: make the change yourself from exact instructions, use AI with your own Anthropic or OpenAI key (code goes only to that provider), or use Veefore AI if enabled. Before anything is sent, secrets are replaced with placeholders and you see what goes where and must agree; an AI change is accepted only if it keeps every line of your code and adds nothing but the guard, and you still review the diff.",
|
|
7922
|
+
options: [
|
|
7923
|
+
["--dry-run", "Show the diff and write nothing."],
|
|
7924
|
+
["--check", "Report which registered tools are guarded in the code. Exits 1 if any are not."],
|
|
7925
|
+
["--assist=<how>", "manual (default when not interactive), own, veefore, or off."],
|
|
7926
|
+
["--yes", "Apply without asking."]
|
|
7927
|
+
],
|
|
7928
|
+
examples: [
|
|
7929
|
+
"veefore integrate --dry-run",
|
|
7930
|
+
"veefore integrate --assist=manual",
|
|
7931
|
+
"ANTHROPIC_API_KEY=\u2026 veefore integrate --assist=own",
|
|
7932
|
+
"veefore integrate --check"
|
|
7933
|
+
]
|
|
7934
|
+
},
|
|
7935
|
+
{
|
|
7936
|
+
name: "undo",
|
|
7937
|
+
summary: "Restore the files the last `veefore integrate` changed.",
|
|
7938
|
+
usage: "veefore undo [--force]",
|
|
7939
|
+
description: "Restores each file from the backup taken before the last rewrite. A file you edited afterwards is left alone and listed, unless --force is given.",
|
|
7940
|
+
options: [["--force", "Restore files even if they were edited after the rewrite."]]
|
|
7941
|
+
},
|
|
7942
|
+
{
|
|
7943
|
+
name: "remove",
|
|
7944
|
+
summary: "Take Veefore out of this project: guard() from the code, then disconnect.",
|
|
7945
|
+
usage: "veefore remove [--dry-run] [--keep-agent]",
|
|
7946
|
+
description: "Offers to revoke the agent in Veefore if it is still active, removes every guard() this CLI added (showing the diff, asking once, backing up and rolling back if your checks fail), then deletes veefore.config.json and the Veefore block in .env.local, including the private key. Edits you made after setup are kept. `veefore` offers this automatically when the agent was revoked in the console.",
|
|
7947
|
+
options: [
|
|
7948
|
+
["--dry-run", "Show what would change and write nothing."],
|
|
7949
|
+
["--keep-agent", "Do not offer to revoke the agent in Veefore."]
|
|
7950
|
+
],
|
|
7951
|
+
examples: ["veefore remove --dry-run", "veefore remove"]
|
|
7952
|
+
},
|
|
7953
|
+
{
|
|
7954
|
+
name: "dev",
|
|
7955
|
+
summary: "Watch decisions live while you build.",
|
|
7956
|
+
usage: "veefore dev [--all]",
|
|
7957
|
+
description: "Confirms the gateway is reachable, reports the enforcement mode, then streams every decision for this agent with the next concrete step for each outcome.",
|
|
7958
|
+
options: [["--all", "Stream decisions for every agent in the environment."]]
|
|
7959
|
+
},
|
|
7960
|
+
{
|
|
7961
|
+
name: "doctor",
|
|
7962
|
+
summary: "Audit the security posture of this setup.",
|
|
7963
|
+
usage: "veefore doctor [--strict]",
|
|
7964
|
+
description: "Checks authentication, MFA, TLS, local secret permissions, identity and passport validity, gateway health, dangerous tools, network restrictions, policy coverage, production data access, MCP transport and authorization, audit chain integrity, credential rotation, and SDK version alignment.",
|
|
7965
|
+
options: [["--strict", "Exit non-zero on warnings as well as failures."]]
|
|
7966
|
+
},
|
|
7967
|
+
{
|
|
7968
|
+
name: "status",
|
|
7969
|
+
summary: "Show the current environment, mode and agent health.",
|
|
7970
|
+
usage: "veefore status",
|
|
7971
|
+
description: "Prints the active organization, workspace and environment, the enforcement mode, the agent lifecycle state, gateway health, and the last few decisions."
|
|
7972
|
+
},
|
|
7973
|
+
{
|
|
7974
|
+
name: "test",
|
|
7975
|
+
summary: "Verify enforcement works, without performing any action.",
|
|
7976
|
+
usage: "veefore test [--tool <key>]",
|
|
7977
|
+
description: "Runs a dry-run probe across every tool this agent is authorized for, checks that policy test cases pass, and fails if a dangerous tool is permitted only by the default decision. Suitable as a CI gate.",
|
|
7978
|
+
options: [["--tool <key>", "Probe a single tool."]]
|
|
7979
|
+
},
|
|
7980
|
+
{
|
|
7981
|
+
name: "agents",
|
|
7982
|
+
summary: "Inspect and control the agent fleet.",
|
|
7983
|
+
usage: "veefore agents <list|show|pause|resume|suspend|revoke|rotate-key> [agentId]",
|
|
7984
|
+
description: "Lifecycle control. `revoke` is irreversible for the identity and shows the full blast radius before asking for confirmation. `rotate-key` issues a new key pair and invalidates every outstanding session for that agent.",
|
|
7985
|
+
options: [
|
|
7986
|
+
["--all", "List across the whole organization, not just this environment."],
|
|
7987
|
+
["--status <status>", "Filter by lifecycle status."],
|
|
7988
|
+
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
7989
|
+
],
|
|
7990
|
+
examples: [
|
|
7991
|
+
"veefore agents list",
|
|
7992
|
+
'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
|
|
7993
|
+
]
|
|
7994
|
+
},
|
|
7995
|
+
{
|
|
7996
|
+
name: "policies",
|
|
7997
|
+
summary: "List, test and deploy policies.",
|
|
7998
|
+
usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
|
|
7999
|
+
description: "`test` runs the recorded test cases. `deploy` activates a version and refuses to do so while any test case fails; it requires a reason and step-up authentication.",
|
|
8000
|
+
options: [
|
|
8001
|
+
["--version <n>", "Version to deploy, for `deploy`."],
|
|
8002
|
+
["--from <n> --to <n>", "Versions to compare, for `diff`."],
|
|
8003
|
+
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8004
|
+
],
|
|
8005
|
+
examples: [
|
|
8006
|
+
"veefore policies list",
|
|
8007
|
+
"veefore policies test",
|
|
8008
|
+
"veefore policies deploy pol_01ABC"
|
|
8009
|
+
]
|
|
8010
|
+
},
|
|
8011
|
+
{
|
|
8012
|
+
name: "logs",
|
|
8013
|
+
summary: "Read or stream the decision log.",
|
|
8014
|
+
usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
|
|
8015
|
+
description: "Every line is a server-side decision with the policy and rule that produced it.",
|
|
8016
|
+
options: [
|
|
8017
|
+
["--follow, -f", "Stream new decisions as they happen."],
|
|
8018
|
+
["--decision <d>", "Filter by decision, e.g. BLOCK."],
|
|
8019
|
+
["--agent <id>", "Filter by agent."],
|
|
8020
|
+
["--all", "Include every agent in the environment."],
|
|
8021
|
+
["--limit <n>", "Number of recent decisions to show."]
|
|
8022
|
+
]
|
|
8023
|
+
},
|
|
8024
|
+
{
|
|
8025
|
+
name: "audit",
|
|
8026
|
+
summary: "Query, verify and export the audit trail.",
|
|
8027
|
+
usage: "veefore audit <list|show|verify|export> [eventId]",
|
|
8028
|
+
description: "`verify` re-checks the hash chain and exits non-zero if it is broken. `export` returns a JSON or CSV file together with a content hash an auditor can use to prove the file is unaltered.",
|
|
8029
|
+
options: [
|
|
8030
|
+
["--severity <s>", "Filter by severity."],
|
|
8031
|
+
["--type <t>", "Filter by event type."],
|
|
8032
|
+
["--from <iso> --to <iso>", "Time range."],
|
|
8033
|
+
["--format <json|csv>", "Export format."],
|
|
8034
|
+
['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
|
|
8035
|
+
["--out <file>", "Write the export to a file (mode 0600)."]
|
|
8036
|
+
],
|
|
8037
|
+
examples: [
|
|
8038
|
+
"veefore audit verify",
|
|
8039
|
+
'veefore audit export --format csv --reason "Quarterly SOC 2 evidence" --out audit.csv'
|
|
8040
|
+
]
|
|
8041
|
+
},
|
|
8042
|
+
{
|
|
8043
|
+
name: "deploy",
|
|
8044
|
+
summary: "Promote the environment to enforcement.",
|
|
8045
|
+
usage: "veefore deploy [--mode ENFORCE|SIMULATE|OBSERVE]",
|
|
8046
|
+
description: "Runs preflight checks, replays recent observed traffic to show exactly what starts being refused, then requires confirmation, a reason and step-up authentication. Reducing enforcement always requires a reason and is recorded as a CRITICAL audit event.",
|
|
8047
|
+
options: [
|
|
8048
|
+
["--mode <mode>", "Target mode. Defaults to ENFORCE."],
|
|
8049
|
+
['--reason "<text>"', "Reason recorded in the audit trail."],
|
|
8050
|
+
["--skip-preflight", "Incident use only. Recorded in the audit reason."]
|
|
8051
|
+
]
|
|
8052
|
+
},
|
|
8053
|
+
{
|
|
8054
|
+
name: "login",
|
|
8055
|
+
summary: "Sign in through your browser and store a session token.",
|
|
8056
|
+
usage: "veefore login [--device] [--password] [--control-plane <url>]",
|
|
8057
|
+
description: "Opens the console in your browser, where you sign in (or create an account) and approve this terminal. The terminal never sees your password. Over SSH, or with --device, it prints a link and a code to enter in any browser instead. Only the resulting session token is stored, in ~/.veefore/credentials.json at mode 0600, and the session can be revoked from your profile. In CI, set VEEFORE_EMAIL and VEEFORE_PASSWORD.",
|
|
8058
|
+
options: [
|
|
8059
|
+
["--device", "Show a link and a code instead of opening a browser here."],
|
|
8060
|
+
["--password", "Type email and password in the terminal instead of using the browser."]
|
|
8061
|
+
]
|
|
8062
|
+
},
|
|
8063
|
+
{
|
|
8064
|
+
name: "logout",
|
|
8065
|
+
summary: "Sign out of Veefore on this machine.",
|
|
8066
|
+
usage: "veefore logout [--all]",
|
|
8067
|
+
description: 'Ends the session on the server and deletes the stored token. Signing out of the console with "Also sign out of the Veefore CLI" ticked does the same for every terminal you approved; the next command here then says you were signed out and asks you to sign in again.',
|
|
8068
|
+
options: [["--all", "Sign out of every control plane this machine is signed in to."]]
|
|
7796
8069
|
}
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
framework: framework.key === "unknown" ? null : framework.key,
|
|
7813
|
-
declaredTools: created.tools.map((tool) => tool.key),
|
|
7814
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7815
|
-
};
|
|
7816
|
-
const configFile = writeProjectConfig(config);
|
|
8070
|
+
];
|
|
8071
|
+
var GLOBAL_OPTIONS = [
|
|
8072
|
+
["--json", "Machine-readable output."],
|
|
8073
|
+
["--yes, -y", "Assume yes for confirmations. Never skips step-up authentication."],
|
|
8074
|
+
["--verbose", "Expanded output."],
|
|
8075
|
+
[
|
|
8076
|
+
"--control-plane <url>",
|
|
8077
|
+
"Use this Veefore instead of the one found automatically (hosted, or the last you signed in to)."
|
|
8078
|
+
],
|
|
8079
|
+
["--gateway <url>", "Override the configured gateway URL."],
|
|
8080
|
+
["--version, -V", "Print the CLI version."],
|
|
8081
|
+
["--help, -h", "Show help."]
|
|
8082
|
+
];
|
|
8083
|
+
function printRootHelp(version) {
|
|
8084
|
+
out();
|
|
7817
8085
|
out(
|
|
7818
|
-
`${
|
|
8086
|
+
`${style.bold("veefore")} ${style.grey(version)} \u2014 identity, authority and policy for AI agents`
|
|
7819
8087
|
);
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
7829
|
-
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
7830
|
-
// Written explicitly so the behaviour is visible and editable in one place.
|
|
7831
|
-
VEEFORE_APPROVAL_MODE: approvalMode
|
|
7832
|
-
});
|
|
7833
|
-
if (audience !== gatewayUrl) {
|
|
7834
|
-
out(
|
|
7835
|
-
`${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
|
|
7836
|
-
`(it reports this, rather than the ${gatewayUrl} you dialled)`
|
|
7837
|
-
)}`
|
|
7838
|
-
);
|
|
8088
|
+
out();
|
|
8089
|
+
out(style.bold("USAGE"));
|
|
8090
|
+
out(" veefore <command> [options]");
|
|
8091
|
+
out();
|
|
8092
|
+
out(style.bold("COMMANDS"));
|
|
8093
|
+
const width = Math.max(...COMMANDS.map((command) => command.name.length));
|
|
8094
|
+
for (const command of COMMANDS) {
|
|
8095
|
+
out(` ${style.cyan(command.name.padEnd(width))} ${command.summary}`);
|
|
7839
8096
|
}
|
|
7840
|
-
out(
|
|
7841
|
-
`${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
|
|
7842
|
-
);
|
|
7843
8097
|
out();
|
|
8098
|
+
out(style.bold("GLOBAL OPTIONS"));
|
|
8099
|
+
renderOptions(GLOBAL_OPTIONS);
|
|
8100
|
+
out();
|
|
8101
|
+
out(style.bold("GETTING STARTED"));
|
|
7844
8102
|
out(
|
|
7845
|
-
` ${style.
|
|
8103
|
+
` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
|
|
7846
8104
|
);
|
|
7847
8105
|
out(
|
|
7848
|
-
`
|
|
8106
|
+
` ${style.grey("2.")} veefore dev ${style.grey("watch what your agent actually does")}`
|
|
7849
8107
|
);
|
|
7850
|
-
out(` ${style.grey(
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
}
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
8108
|
+
out(` ${style.grey("3.")} veefore doctor ${style.grey("check the security posture")}`);
|
|
8109
|
+
out(` ${style.grey("4.")} veefore deploy ${style.grey("turn enforcement on")}`);
|
|
8110
|
+
out();
|
|
8111
|
+
out(style.grey(" Run `veefore <command> --help` for detail on any command."));
|
|
8112
|
+
out();
|
|
8113
|
+
}
|
|
8114
|
+
function printCommandHelp(name) {
|
|
8115
|
+
const command = COMMANDS.find((entry) => entry.name === name);
|
|
8116
|
+
if (!command) return false;
|
|
8117
|
+
out();
|
|
8118
|
+
out(`${style.bold(`veefore ${command.name}`)} \u2014 ${command.summary}`);
|
|
8119
|
+
out();
|
|
8120
|
+
out(style.bold("USAGE"));
|
|
8121
|
+
out(` ${command.usage}`);
|
|
8122
|
+
out();
|
|
8123
|
+
out(style.bold("DESCRIPTION"));
|
|
8124
|
+
for (const line of wrap2(command.description, 76)) out(` ${line}`);
|
|
8125
|
+
if (command.options && command.options.length > 0) {
|
|
8126
|
+
out();
|
|
8127
|
+
out(style.bold("OPTIONS"));
|
|
8128
|
+
renderOptions(command.options);
|
|
7866
8129
|
}
|
|
7867
|
-
|
|
7868
|
-
if (!integrated) {
|
|
7869
|
-
heading("Protect each tool");
|
|
8130
|
+
if (command.examples && command.examples.length > 0) {
|
|
7870
8131
|
out();
|
|
7871
|
-
|
|
7872
|
-
|
|
8132
|
+
out(style.bold("EXAMPLES"));
|
|
8133
|
+
for (const example of command.examples) out(` ${style.grey("$")} ${example}`);
|
|
8134
|
+
}
|
|
8135
|
+
out();
|
|
8136
|
+
return true;
|
|
8137
|
+
}
|
|
8138
|
+
function renderOptions(options) {
|
|
8139
|
+
const width = Math.max(...options.map(([flag]) => flag.length));
|
|
8140
|
+
for (const [flag, description] of options) {
|
|
8141
|
+
out(` ${style.cyan(flag.padEnd(width))} ${description}`);
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
function wrap2(text, width) {
|
|
8145
|
+
const words2 = text.split(/\s+/);
|
|
8146
|
+
const lines = [];
|
|
8147
|
+
let current = "";
|
|
8148
|
+
for (const word of words2) {
|
|
8149
|
+
if (current.length + word.length + 1 > width) {
|
|
8150
|
+
lines.push(current);
|
|
8151
|
+
current = word;
|
|
8152
|
+
} else {
|
|
8153
|
+
current = current === "" ? word : `${current} ${word}`;
|
|
7873
8154
|
}
|
|
7874
|
-
|
|
7875
|
-
|
|
8155
|
+
}
|
|
8156
|
+
if (current !== "") lines.push(current);
|
|
8157
|
+
return lines;
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
// src/home.ts
|
|
8161
|
+
async function homeCommand(args, flags) {
|
|
8162
|
+
enterApp();
|
|
8163
|
+
try {
|
|
8164
|
+
return await homeLoop(args, flags);
|
|
8165
|
+
} finally {
|
|
8166
|
+
leaveApp();
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
async function homeLoop(args, flags) {
|
|
8170
|
+
const source = welcomeSource(flags);
|
|
8171
|
+
let first = true;
|
|
8172
|
+
for (; ; ) {
|
|
8173
|
+
if (readProjectConfig()) return initCommand(args, flags);
|
|
8174
|
+
const body = () => {
|
|
8175
|
+
drawScreen(source, { reserve: 11 });
|
|
7876
8176
|
out();
|
|
7877
|
-
out(
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
8177
|
+
out(style.bold("Let's get started."));
|
|
8178
|
+
out();
|
|
8179
|
+
};
|
|
8180
|
+
await beginScreen(source, { animate: first, reserve: 11 });
|
|
8181
|
+
first = false;
|
|
8182
|
+
out();
|
|
8183
|
+
out(style.bold("Let's get started."));
|
|
8184
|
+
out();
|
|
8185
|
+
let action;
|
|
8186
|
+
try {
|
|
8187
|
+
action = await select(
|
|
8188
|
+
"What would you like to do?",
|
|
8189
|
+
[
|
|
8190
|
+
{ label: "Set up this project", hint: "recommended \xB7 veefore init", value: "setup" },
|
|
8191
|
+
{
|
|
8192
|
+
label: "Preview what setup would find",
|
|
8193
|
+
hint: "veefore scan \xB7 changes nothing",
|
|
8194
|
+
value: "scan"
|
|
8195
|
+
},
|
|
8196
|
+
{ label: "Sign in", hint: "veefore login", value: "login" },
|
|
8197
|
+
{ label: "Show every command", hint: "veefore help", value: "help" },
|
|
8198
|
+
{ label: "Exit", hint: "esc", value: "exit" }
|
|
8199
|
+
],
|
|
8200
|
+
{ redraw: body }
|
|
7881
8201
|
);
|
|
8202
|
+
} catch (error) {
|
|
8203
|
+
if (error instanceof MenuCancelled) return 0;
|
|
8204
|
+
throw error;
|
|
7882
8205
|
}
|
|
7883
|
-
if (
|
|
7884
|
-
|
|
8206
|
+
if (action === "exit") return 0;
|
|
8207
|
+
if (action === "setup") {
|
|
8208
|
+
await runOnScreen(source, flags, () => initCommand(args, flags), {
|
|
8209
|
+
// Setup done: "Continue", and the next pass opens the project home
|
|
8210
|
+
// with the panel filled in. Otherwise "Back to menu", to read why and
|
|
8211
|
+
// choose again.
|
|
8212
|
+
label: () => readProjectConfig() ? "Continue" : "Back to menu"
|
|
8213
|
+
});
|
|
8214
|
+
continue;
|
|
7885
8215
|
}
|
|
8216
|
+
const code = await runOnScreen(source, flags, async () => {
|
|
8217
|
+
switch (action) {
|
|
8218
|
+
case "scan":
|
|
8219
|
+
return scanCommand([], flags);
|
|
8220
|
+
case "login":
|
|
8221
|
+
await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
|
|
8222
|
+
return 0;
|
|
8223
|
+
case "help":
|
|
8224
|
+
printRootHelp(source().version);
|
|
8225
|
+
return 0;
|
|
8226
|
+
}
|
|
8227
|
+
return 0;
|
|
8228
|
+
});
|
|
8229
|
+
if (!isInteractive()) return code;
|
|
7886
8230
|
}
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
)
|
|
7894
|
-
|
|
7895
|
-
|
|
8231
|
+
}
|
|
8232
|
+
async function initWithWelcome(args, flags) {
|
|
8233
|
+
await beginScreen(welcomeSource(flags), { animate: true });
|
|
8234
|
+
return initCommand(args, flags);
|
|
8235
|
+
}
|
|
8236
|
+
async function runOnScreen(source, flags, run2, options = {}) {
|
|
8237
|
+
const header = () => {
|
|
8238
|
+
out();
|
|
8239
|
+
compactHeader(source());
|
|
8240
|
+
};
|
|
8241
|
+
if (options.live) {
|
|
8242
|
+
clearScreen();
|
|
8243
|
+
header();
|
|
8244
|
+
let code2 = 0;
|
|
8245
|
+
try {
|
|
8246
|
+
code2 = await run2();
|
|
8247
|
+
} catch (error) {
|
|
8248
|
+
code2 = error instanceof MenuCancelled ? 0 : reportError(error, flags);
|
|
8249
|
+
}
|
|
8250
|
+
const label2 = options.label ?? "Back";
|
|
8251
|
+
await pressBack(typeof label2 === "function" ? label2() : label2);
|
|
8252
|
+
return code2;
|
|
8253
|
+
}
|
|
8254
|
+
beginCapture();
|
|
8255
|
+
let code = 0;
|
|
8256
|
+
try {
|
|
8257
|
+
code = await run2();
|
|
8258
|
+
} catch (error) {
|
|
8259
|
+
code = error instanceof MenuCancelled ? 0 : reportError(error, flags);
|
|
8260
|
+
}
|
|
8261
|
+
const lines = endCapture();
|
|
8262
|
+
const label = options.label ?? "Back";
|
|
8263
|
+
await showOutput(lines, {
|
|
8264
|
+
drawHeader: header,
|
|
8265
|
+
label: typeof label === "function" ? label() : label
|
|
8266
|
+
});
|
|
8267
|
+
return code;
|
|
8268
|
+
}
|
|
8269
|
+
|
|
8270
|
+
// src/commands/init.ts
|
|
8271
|
+
var DEFAULT_GATEWAY = "http://localhost:4010";
|
|
8272
|
+
async function initCommand(args, flags) {
|
|
8273
|
+
const options = parseInitArgs(args);
|
|
8274
|
+
const existing = readProjectConfig();
|
|
8275
|
+
if (existing && !options.force && isInteractive() && !flags.json) {
|
|
8276
|
+
return projectHome(existing, flags);
|
|
8277
|
+
}
|
|
8278
|
+
if (existing && !options.force) {
|
|
8279
|
+
out();
|
|
8280
|
+
out(`${symbol.warn} This project is already connected to Veefore.`);
|
|
8281
|
+
details([
|
|
8282
|
+
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
8283
|
+
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
8284
|
+
["Control plane", existing.controlPlaneUrl]
|
|
8285
|
+
]);
|
|
8286
|
+
out();
|
|
8287
|
+
out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
|
|
8288
|
+
return 0;
|
|
8289
|
+
}
|
|
8290
|
+
heading("Veefore init");
|
|
8291
|
+
const framework = detectFramework();
|
|
7896
8292
|
out(
|
|
7897
|
-
style.grey(
|
|
7898
|
-
` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
|
|
7899
|
-
)
|
|
8293
|
+
`${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language}, matched ${framework.evidence})`)}`
|
|
7900
8294
|
);
|
|
7901
|
-
|
|
7902
|
-
out();
|
|
7903
|
-
if (flags.json) {
|
|
8295
|
+
if (framework.key === "unknown") {
|
|
7904
8296
|
out(
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
config_file: configFile,
|
|
7908
|
-
env_file: envFile,
|
|
7909
|
-
agent_id: created.agentId,
|
|
7910
|
-
key_id: created.keyId,
|
|
7911
|
-
fingerprint: created.fingerprint,
|
|
7912
|
-
tools: created.tools,
|
|
7913
|
-
checks
|
|
7914
|
-
},
|
|
7915
|
-
null,
|
|
7916
|
-
2
|
|
8297
|
+
style.grey(
|
|
8298
|
+
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
7917
8299
|
)
|
|
7918
8300
|
);
|
|
7919
8301
|
}
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
8302
|
+
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
8303
|
+
const controlPlaneUrl = found.controlPlaneUrl;
|
|
8304
|
+
if (found.source !== "entered") {
|
|
8305
|
+
out(
|
|
8306
|
+
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
8307
|
+
);
|
|
8308
|
+
}
|
|
8309
|
+
let session = loadSession(controlPlaneUrl);
|
|
8310
|
+
if (!session) {
|
|
8311
|
+
await login(controlPlaneUrl);
|
|
8312
|
+
session = loadSession(controlPlaneUrl);
|
|
8313
|
+
}
|
|
8314
|
+
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
8315
|
+
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
8316
|
+
let me = await api.get("/v1/auth/session");
|
|
8317
|
+
const STEPS = ["organization", "workspace", "environment"];
|
|
8318
|
+
const asked = /* @__PURE__ */ new Map();
|
|
8319
|
+
let organization;
|
|
8320
|
+
let workspace;
|
|
8321
|
+
let environment;
|
|
8322
|
+
const source = welcomeSource(flags);
|
|
8323
|
+
const decided = [];
|
|
8324
|
+
enterApp();
|
|
7923
8325
|
try {
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
8326
|
+
for (let at = 0; at < STEPS.length; ) {
|
|
8327
|
+
const step = STEPS[at];
|
|
8328
|
+
decided.length = at;
|
|
8329
|
+
await stepScreen(source, decided, at + 1, STEPS.length);
|
|
8330
|
+
const answer = step === "organization" ? await selectOrganization(api, me, options, flags) : step === "workspace" ? await selectWorkspace(api, me, options, flags) : await selectEnvironment(api, me, workspace.id, options, flags);
|
|
8331
|
+
if (answer === BACK) {
|
|
8332
|
+
let previous = at - 1;
|
|
8333
|
+
while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
|
|
8334
|
+
if (previous < 0) {
|
|
8335
|
+
throw new MenuCancelled();
|
|
8336
|
+
}
|
|
8337
|
+
at = previous;
|
|
8338
|
+
continue;
|
|
8339
|
+
}
|
|
8340
|
+
asked.set(step, answer.asked);
|
|
8341
|
+
if (step === "organization") {
|
|
8342
|
+
organization = answer.value;
|
|
8343
|
+
decided[at] = ["Organization", organization.name];
|
|
8344
|
+
if (answer.asked) me = await api.get("/v1/auth/session");
|
|
8345
|
+
} else if (step === "workspace") {
|
|
8346
|
+
workspace = answer.value;
|
|
8347
|
+
decided[at] = ["Workspace", workspace.name];
|
|
8348
|
+
} else {
|
|
8349
|
+
environment = answer.value;
|
|
8350
|
+
decided[at] = ["Environment", `${environment.name} (${environment.type})`];
|
|
8351
|
+
}
|
|
8352
|
+
at += 1;
|
|
8353
|
+
}
|
|
8354
|
+
} finally {
|
|
8355
|
+
leaveApp();
|
|
7929
8356
|
}
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
if (mode === "wait") {
|
|
7933
|
-
return [
|
|
7934
|
-
` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
|
|
7935
|
-
` ${symbol.bullet} approved: the tool runs once and returns its real result`,
|
|
7936
|
-
` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
|
|
7937
|
-
style.grey(
|
|
7938
|
-
" The agent process must stay running while it waits (up to the approval's expiry)."
|
|
7939
|
-
),
|
|
7940
|
-
style.grey(
|
|
7941
|
-
" For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
|
|
7942
|
-
),
|
|
7943
|
-
style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
|
|
7944
|
-
];
|
|
8357
|
+
for (const [label, value] of decided) {
|
|
8358
|
+
out(`${symbol.pass} ${label}: ${style.bold(value)}`);
|
|
7945
8359
|
}
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
8360
|
+
heading("Looking at your project");
|
|
8361
|
+
const scan = scanProject(process.cwd());
|
|
8362
|
+
printScan(scan);
|
|
8363
|
+
const plan = buildPlan(scan, {
|
|
8364
|
+
agentName: options.agentName,
|
|
8365
|
+
extraCapabilities: options.capabilities
|
|
8366
|
+
});
|
|
8367
|
+
for (; ; ) {
|
|
8368
|
+
if (!options.agentName && !flags.yes) {
|
|
8369
|
+
const name = await askAgentName(plan.agentName);
|
|
8370
|
+
if (name === BACK) {
|
|
8371
|
+
const again = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
8372
|
+
if (again !== BACK) environment = again.value;
|
|
8373
|
+
continue;
|
|
8374
|
+
}
|
|
8375
|
+
plan.agentName = name;
|
|
8376
|
+
}
|
|
8377
|
+
printPlan2(plan, environment);
|
|
8378
|
+
out();
|
|
8379
|
+
const proceed = await confirm("Set this up?", { yes: flags.yes });
|
|
8380
|
+
if (proceed) break;
|
|
8381
|
+
if (flags.yes || options.agentName) {
|
|
8382
|
+
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
8383
|
+
return 1;
|
|
8384
|
+
}
|
|
8385
|
+
const next = await select(
|
|
8386
|
+
"What would you like to do?",
|
|
8387
|
+
[
|
|
8388
|
+
{ label: "Change the agent name", value: "name" },
|
|
8389
|
+
{ label: "Choose a different environment", value: "environment" },
|
|
8390
|
+
{ label: "Leave setup", value: "leave" }
|
|
8391
|
+
],
|
|
8392
|
+
{ initial: 0 }
|
|
8393
|
+
);
|
|
8394
|
+
if (next === "leave") {
|
|
8395
|
+
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
8396
|
+
return 1;
|
|
8397
|
+
}
|
|
8398
|
+
if (next === "environment") {
|
|
8399
|
+
const again = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
8400
|
+
if (again !== BACK) environment = again.value;
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
heading("Setting up");
|
|
8404
|
+
let created;
|
|
8405
|
+
try {
|
|
8406
|
+
created = await applyPlan(
|
|
8407
|
+
api,
|
|
8408
|
+
plan,
|
|
8409
|
+
{
|
|
8410
|
+
workspaceId: workspace.id,
|
|
8411
|
+
environmentId: environment.id,
|
|
8412
|
+
framework: framework.key === "unknown" ? null : framework.key
|
|
8413
|
+
},
|
|
8414
|
+
void 0,
|
|
8415
|
+
{ newAgent: options.newAgent === true }
|
|
8416
|
+
);
|
|
8417
|
+
} catch (error) {
|
|
8418
|
+
if (error instanceof CliApiError && error.code === "PERMISSION_DENIED") {
|
|
8419
|
+
throw new CliError(
|
|
8420
|
+
"Your role cannot set up agents in this workspace.",
|
|
8421
|
+
'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
|
|
8422
|
+
);
|
|
8423
|
+
}
|
|
8424
|
+
throw error;
|
|
8425
|
+
}
|
|
8426
|
+
let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
|
|
8427
|
+
if (!gatewayUrl) {
|
|
8428
|
+
const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
|
|
8429
|
+
if (published && await gatewayReachable(published)) {
|
|
8430
|
+
gatewayUrl = published;
|
|
8431
|
+
out(
|
|
8432
|
+
`${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
|
|
8433
|
+
);
|
|
8434
|
+
} else {
|
|
8435
|
+
const suggestion = published ?? DEFAULT_GATEWAY;
|
|
8436
|
+
out(
|
|
8437
|
+
`${symbol.warn} ${published ? `The gateway at ${published} is not answering.` : "The control plane did not publish a gateway address."}`
|
|
8438
|
+
);
|
|
8439
|
+
gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
gatewayUrl = normalizeUrl(gatewayUrl);
|
|
8443
|
+
const config = {
|
|
8444
|
+
version: 1,
|
|
8445
|
+
controlPlaneUrl,
|
|
8446
|
+
gatewayUrl,
|
|
8447
|
+
organizationId: organization.id,
|
|
8448
|
+
organizationName: organization.name,
|
|
8449
|
+
workspaceId: workspace.id,
|
|
8450
|
+
workspaceName: workspace.name,
|
|
8451
|
+
environmentId: environment.id,
|
|
8452
|
+
environmentName: environment.name,
|
|
8453
|
+
environmentType: environment.type,
|
|
8454
|
+
agentId: created.agentId,
|
|
8455
|
+
agentName: created.agentName,
|
|
8456
|
+
keyId: created.keyId,
|
|
8457
|
+
framework: framework.key === "unknown" ? null : framework.key,
|
|
8458
|
+
declaredTools: created.tools.map((tool) => tool.key),
|
|
8459
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8460
|
+
};
|
|
8461
|
+
const configFile = writeProjectConfig(config);
|
|
8462
|
+
out(
|
|
8463
|
+
`${symbol.pass} Wrote ${style.bold("veefore.config.json")} ${style.grey("(safe to commit)")}`
|
|
8464
|
+
);
|
|
8465
|
+
const audience = await discoverAudience(gatewayUrl);
|
|
8466
|
+
ensureGitignore(process.cwd(), [".env.local", ".veefore/"]);
|
|
8467
|
+
const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
|
|
8468
|
+
const envFile = writeLocalEnv({
|
|
8469
|
+
VEEFORE_GATEWAY_URL: gatewayUrl,
|
|
8470
|
+
VEEFORE_GATEWAY_AUDIENCE: audience,
|
|
8471
|
+
VEEFORE_AGENT_ID: created.agentId,
|
|
8472
|
+
VEEFORE_AGENT_KEY_ID: created.keyId,
|
|
8473
|
+
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
8474
|
+
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
8475
|
+
// Written explicitly so the behaviour is visible and editable in one place.
|
|
8476
|
+
VEEFORE_APPROVAL_MODE: approvalMode
|
|
8477
|
+
});
|
|
8478
|
+
if (audience !== gatewayUrl) {
|
|
8479
|
+
out(
|
|
8480
|
+
`${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
|
|
8481
|
+
`(it reports this, rather than the ${gatewayUrl} you dialled)`
|
|
8482
|
+
)}`
|
|
8483
|
+
);
|
|
8484
|
+
}
|
|
8485
|
+
out(
|
|
8486
|
+
`${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
|
|
8487
|
+
);
|
|
8488
|
+
out();
|
|
8489
|
+
out(
|
|
8490
|
+
` ${style.amber("The private key was generated here and written to that file only.")} Veefore`
|
|
8491
|
+
);
|
|
8492
|
+
out(
|
|
8493
|
+
` never received it and cannot recover it. Move it into your secret manager before deploying.`
|
|
8494
|
+
);
|
|
8495
|
+
out(` ${style.grey(`Fingerprint: ${created.fingerprint}`)}`);
|
|
8496
|
+
await configureStarterPolicy(api, workspace.id, flags, options);
|
|
8497
|
+
let integrated = false;
|
|
8498
|
+
if (created.tools.length > 0 && !options.skipCode) {
|
|
8499
|
+
const outcome = await integrate(
|
|
8500
|
+
process.cwd(),
|
|
8501
|
+
created.tools.map((tool) => tool.key),
|
|
8502
|
+
{ yes: flags.yes, quiet: flags.json, api }
|
|
8503
|
+
);
|
|
8504
|
+
integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
|
|
8505
|
+
}
|
|
8506
|
+
heading("Validating the connection");
|
|
8507
|
+
const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
|
|
8508
|
+
for (const check of checks) {
|
|
8509
|
+
const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
|
|
8510
|
+
out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
|
|
8511
|
+
}
|
|
8512
|
+
const failed = checks.filter((check) => check.status === "fail");
|
|
8513
|
+
if (!integrated) {
|
|
8514
|
+
heading("Protect each tool");
|
|
8515
|
+
out();
|
|
8516
|
+
for (const line of integrationSnippet(framework).split("\n")) {
|
|
8517
|
+
out(` ${style.cyan(line)}`);
|
|
8518
|
+
}
|
|
8519
|
+
const entryPoints = findEntryPoints();
|
|
8520
|
+
if (entryPoints.length > 0) {
|
|
8521
|
+
out();
|
|
8522
|
+
out(
|
|
8523
|
+
style.grey(
|
|
8524
|
+
` Likely entry point${entryPoints.length > 1 ? "s" : ""}: ${entryPoints.join(", ")}`
|
|
8525
|
+
)
|
|
8526
|
+
);
|
|
8527
|
+
}
|
|
8528
|
+
if (created.tools.length > 0) {
|
|
8529
|
+
out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
heading("When a human must approve");
|
|
8533
|
+
for (const line of approvalModeExplanation(approvalMode)) out(line);
|
|
8534
|
+
heading("Next");
|
|
8535
|
+
out(` ${symbol.bullet} ${style.bold("veefore dev")} watch decisions live while you build`);
|
|
8536
|
+
out(
|
|
8537
|
+
` ${symbol.bullet} ${style.bold("veefore doctor")} check the security posture of this setup`
|
|
8538
|
+
);
|
|
8539
|
+
out(` ${symbol.bullet} ${style.bold("veefore status")} see the current mode and agent health`);
|
|
8540
|
+
out();
|
|
8541
|
+
out(
|
|
8542
|
+
style.grey(
|
|
8543
|
+
` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
|
|
8544
|
+
)
|
|
8545
|
+
);
|
|
8546
|
+
out(style.grey(" Review the recommended policy, simulate it, then switch to ENFORCE."));
|
|
8547
|
+
out();
|
|
8548
|
+
if (flags.json) {
|
|
8549
|
+
out(
|
|
8550
|
+
JSON.stringify(
|
|
8551
|
+
{
|
|
8552
|
+
config_file: configFile,
|
|
8553
|
+
env_file: envFile,
|
|
8554
|
+
agent_id: created.agentId,
|
|
8555
|
+
key_id: created.keyId,
|
|
8556
|
+
fingerprint: created.fingerprint,
|
|
8557
|
+
tools: created.tools,
|
|
8558
|
+
checks
|
|
8559
|
+
},
|
|
8560
|
+
null,
|
|
8561
|
+
2
|
|
8562
|
+
)
|
|
8563
|
+
);
|
|
8564
|
+
}
|
|
8565
|
+
return failed.length > 0 ? 1 : 0;
|
|
8566
|
+
}
|
|
8567
|
+
function previousApprovalMode(dir = process.cwd()) {
|
|
8568
|
+
try {
|
|
8569
|
+
const text = readFileSync9(path12.join(dir, ".env.local"), "utf8");
|
|
8570
|
+
const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
|
|
8571
|
+
return value === "wait" || value === "throw" ? value : null;
|
|
8572
|
+
} catch {
|
|
8573
|
+
return null;
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
8576
|
+
function approvalModeExplanation(mode) {
|
|
8577
|
+
if (mode === "wait") {
|
|
8578
|
+
return [
|
|
8579
|
+
` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
|
|
8580
|
+
` ${symbol.bullet} approved: the tool runs once and returns its real result`,
|
|
8581
|
+
` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
|
|
8582
|
+
style.grey(
|
|
8583
|
+
" The agent process must stay running while it waits (up to the approval's expiry)."
|
|
8584
|
+
),
|
|
8585
|
+
style.grey(
|
|
8586
|
+
" For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
|
|
8587
|
+
),
|
|
8588
|
+
style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
|
|
8589
|
+
];
|
|
8590
|
+
}
|
|
8591
|
+
return [
|
|
8592
|
+
` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
|
|
8593
|
+
" carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
|
|
8594
|
+
" or the action never happens.",
|
|
8595
|
+
style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
|
|
7951
8596
|
];
|
|
7952
8597
|
}
|
|
7953
8598
|
async function selectOrganization(api, me, options, flags) {
|
|
@@ -7972,7 +8617,6 @@ async function selectOrganization(api, me, options, flags) {
|
|
|
7972
8617
|
if (organizations.length === 1 || flags.yes) {
|
|
7973
8618
|
const chosen2 = organizations[0];
|
|
7974
8619
|
if (!chosen2) throw new CliError("Your account is not a member of any organization.");
|
|
7975
|
-
out(`${symbol.pass} Organization: ${style.bold(chosen2.name)}`);
|
|
7976
8620
|
return { value: chosen2, asked: false };
|
|
7977
8621
|
}
|
|
7978
8622
|
out();
|
|
@@ -7987,7 +8631,6 @@ async function selectOrganization(api, me, options, flags) {
|
|
|
7987
8631
|
);
|
|
7988
8632
|
if (chosen === BACK) return BACK;
|
|
7989
8633
|
await api.post("/v1/auth/scope", { organization_id: chosen.id });
|
|
7990
|
-
out(`${symbol.pass} Organization: ${style.bold(chosen.name)}`);
|
|
7991
8634
|
return { value: chosen, asked: true };
|
|
7992
8635
|
}
|
|
7993
8636
|
async function selectWorkspace(api, me, options, flags) {
|
|
@@ -8005,7 +8648,6 @@ async function selectWorkspace(api, me, options, flags) {
|
|
|
8005
8648
|
}
|
|
8006
8649
|
if (workspaces.length === 1 || flags.yes) {
|
|
8007
8650
|
const chosen2 = workspaces[0];
|
|
8008
|
-
out(`${symbol.pass} Workspace: ${style.bold(chosen2.name)}`);
|
|
8009
8651
|
return { value: chosen2, asked: false };
|
|
8010
8652
|
}
|
|
8011
8653
|
out();
|
|
@@ -8019,7 +8661,6 @@ async function selectWorkspace(api, me, options, flags) {
|
|
|
8019
8661
|
{ quiet: true, back: true }
|
|
8020
8662
|
);
|
|
8021
8663
|
if (chosen === BACK) return BACK;
|
|
8022
|
-
out(`${symbol.pass} Workspace: ${style.bold(chosen.name)}`);
|
|
8023
8664
|
return { value: chosen, asked: true };
|
|
8024
8665
|
}
|
|
8025
8666
|
async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
@@ -8038,7 +8679,6 @@ async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
|
8038
8679
|
}
|
|
8039
8680
|
if (flags.yes) {
|
|
8040
8681
|
const chosen = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
|
|
8041
|
-
out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
|
|
8042
8682
|
return { value: chosen, asked: false };
|
|
8043
8683
|
}
|
|
8044
8684
|
const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
|
|
@@ -8064,10 +8704,21 @@ async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
|
8064
8704
|
continue;
|
|
8065
8705
|
}
|
|
8066
8706
|
}
|
|
8067
|
-
out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
|
|
8068
8707
|
return { value: chosen, asked: true };
|
|
8069
8708
|
}
|
|
8070
8709
|
}
|
|
8710
|
+
async function stepScreen(source, decided, step, total) {
|
|
8711
|
+
await beginScreen(source, { compact: true });
|
|
8712
|
+
out();
|
|
8713
|
+
out(` ${style.bold("Set up this project")} ${style.grey(`step ${step} of ${total}`)}`);
|
|
8714
|
+
if (decided.length > 0) {
|
|
8715
|
+
out();
|
|
8716
|
+
const width = Math.max(...decided.map(([label]) => label.length));
|
|
8717
|
+
for (const [label, value] of decided) {
|
|
8718
|
+
out(` ${symbol.pass} ${style.grey(label.padEnd(width))} ${value}`);
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
}
|
|
8071
8722
|
async function askAgentName(suggested) {
|
|
8072
8723
|
for (; ; ) {
|
|
8073
8724
|
out();
|
|
@@ -8287,10 +8938,18 @@ function parseInitArgs(args) {
|
|
|
8287
8938
|
return options;
|
|
8288
8939
|
}
|
|
8289
8940
|
async function projectHome(existing, flags) {
|
|
8941
|
+
enterApp();
|
|
8942
|
+
try {
|
|
8943
|
+
return await projectHomeLoop(existing, flags);
|
|
8944
|
+
} finally {
|
|
8945
|
+
leaveApp();
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
async function projectHomeLoop(existing, flags) {
|
|
8290
8949
|
const source = welcomeSource(flags);
|
|
8291
8950
|
let last = 0;
|
|
8292
8951
|
for (let round = 0; ; round += 1) {
|
|
8293
|
-
await beginScreen(source, { animate: round === 0 });
|
|
8952
|
+
await beginScreen(source, { animate: round === 0, reserve: 18 });
|
|
8294
8953
|
const state = await agentState(existing);
|
|
8295
8954
|
printProjectSummary(existing, state);
|
|
8296
8955
|
const choices = homeChoices(existing, state, flags);
|
|
@@ -8300,28 +8959,37 @@ async function projectHome(existing, flags) {
|
|
|
8300
8959
|
try {
|
|
8301
8960
|
picked = await select(
|
|
8302
8961
|
"What would you like to do?",
|
|
8303
|
-
choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice }))
|
|
8962
|
+
choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice })),
|
|
8963
|
+
{
|
|
8964
|
+
// Repainted as soon as a sign-in, sign-out or project change is
|
|
8965
|
+
// noticed, so the panel and the status never sit stale.
|
|
8966
|
+
redraw: () => {
|
|
8967
|
+
drawScreen(source, { reserve: 18 });
|
|
8968
|
+
printProjectSummary(existing, state);
|
|
8969
|
+
out();
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8304
8972
|
);
|
|
8305
8973
|
} catch (error) {
|
|
8306
8974
|
if (error instanceof MenuCancelled) return last;
|
|
8307
8975
|
throw error;
|
|
8308
8976
|
}
|
|
8309
8977
|
if (picked.leaves) {
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
last = await picked.run();
|
|
8320
|
-
} catch (error) {
|
|
8321
|
-
last = error instanceof MenuCancelled ? last : reportError(error, flags);
|
|
8978
|
+
return onOrdinaryScreen(async () => {
|
|
8979
|
+
clearScreen();
|
|
8980
|
+
compactHeader(source());
|
|
8981
|
+
try {
|
|
8982
|
+
return await picked.run();
|
|
8983
|
+
} catch (error) {
|
|
8984
|
+
return error instanceof MenuCancelled ? last : reportError(error, flags);
|
|
8985
|
+
}
|
|
8986
|
+
});
|
|
8322
8987
|
}
|
|
8988
|
+
last = await runOnScreen(source, flags, () => picked.run(), {
|
|
8989
|
+
label: "Back to menu",
|
|
8990
|
+
live: picked.live
|
|
8991
|
+
});
|
|
8323
8992
|
if (!isInteractive()) return last;
|
|
8324
|
-
await pressBack("Back to menu");
|
|
8325
8993
|
}
|
|
8326
8994
|
}
|
|
8327
8995
|
function printProjectSummary(existing, state) {
|
|
@@ -8374,7 +9042,12 @@ function homeChoices(existing, state, flags) {
|
|
|
8374
9042
|
});
|
|
8375
9043
|
}
|
|
8376
9044
|
choices.push(
|
|
8377
|
-
{
|
|
9045
|
+
{
|
|
9046
|
+
label: "Watch decisions live",
|
|
9047
|
+
hint: "veefore dev",
|
|
9048
|
+
run: () => devCommand([], flags),
|
|
9049
|
+
live: true
|
|
9050
|
+
},
|
|
8378
9051
|
{ label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
|
|
8379
9052
|
{
|
|
8380
9053
|
label: "Protect tools in the code",
|
|
@@ -8423,23 +9096,6 @@ function homeChoices(existing, state, flags) {
|
|
|
8423
9096
|
return choices;
|
|
8424
9097
|
}
|
|
8425
9098
|
|
|
8426
|
-
// src/commands/scan.ts
|
|
8427
|
-
async function scanCommand(_args, flags) {
|
|
8428
|
-
const scan = scanProject(process.cwd());
|
|
8429
|
-
if (flags.json) {
|
|
8430
|
-
out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
|
|
8431
|
-
return 0;
|
|
8432
|
-
}
|
|
8433
|
-
heading("Veefore scan");
|
|
8434
|
-
out(
|
|
8435
|
-
`${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
|
|
8436
|
-
);
|
|
8437
|
-
printScan(scan);
|
|
8438
|
-
out();
|
|
8439
|
-
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
8440
|
-
return 0;
|
|
8441
|
-
}
|
|
8442
|
-
|
|
8443
9099
|
// src/commands/policies.ts
|
|
8444
9100
|
function caseCount(result) {
|
|
8445
9101
|
return result.passed + result.failed;
|
|
@@ -8648,531 +9304,181 @@ function renderTestReports(reports) {
|
|
|
8648
9304
|
` ${symbol.fail} ${testCase.name}: expected ${decisionStyle(testCase.expected)}, got ${decisionStyle(testCase.actual)}`
|
|
8649
9305
|
);
|
|
8650
9306
|
if (testCase.reason) out(` ${style.grey(testCase.reason)}`);
|
|
8651
|
-
}
|
|
8652
|
-
}
|
|
8653
|
-
out();
|
|
8654
|
-
out(
|
|
8655
|
-
totalFailed === 0 ? ` ${symbol.pass} ${style.green(`All ${totalCases} test case(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${totalFailed} failure(s) across ${totalCases} test case(s).`)}`
|
|
8656
|
-
);
|
|
8657
|
-
if (withoutTests > 0) {
|
|
8658
|
-
out(
|
|
8659
|
-
` ${symbol.warn} ${style.amber(`${withoutTests} policy/policies have no test cases.`)} Add cases so activation can be proven safe.`
|
|
8660
|
-
);
|
|
8661
|
-
}
|
|
8662
|
-
out();
|
|
8663
|
-
}
|
|
8664
|
-
async function deployPolicy(args, flags) {
|
|
8665
|
-
const context = await resolveContext(flags);
|
|
8666
|
-
const policyId = positional(args);
|
|
8667
|
-
if (!policyId) {
|
|
8668
|
-
throw new CliError(
|
|
8669
|
-
"A policy id is required.",
|
|
8670
|
-
'Usage: veefore policies deploy <policyId> [--version <n>] [--reason "\u2026"]'
|
|
8671
|
-
);
|
|
8672
|
-
}
|
|
8673
|
-
const policy = await context.api.get(`/v1/policies/${policyId}`);
|
|
8674
|
-
const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
|
|
8675
|
-
const requested = valueFor(args, "--version");
|
|
8676
|
-
const target = requested ? versions.find((version) => String(version.versionNumber) === requested) : versions.find((version) => version.status === "DRAFT") ?? versions.find((version) => version.status === "TESTED");
|
|
8677
|
-
if (!target) {
|
|
8678
|
-
throw new CliError(
|
|
8679
|
-
requested ? `Version ${requested} was not found for this policy.` : "There is no draft version to deploy.",
|
|
8680
|
-
"Edit the policy in the console to create a new draft, then deploy it."
|
|
8681
|
-
);
|
|
8682
|
-
}
|
|
8683
|
-
if (target.versionNumber === policy.activeVersion?.versionNumber) {
|
|
8684
|
-
out(`${symbol.info} Version ${target.versionNumber} is already active. Nothing to do.`);
|
|
8685
|
-
return 0;
|
|
8686
|
-
}
|
|
8687
|
-
heading(`Deploy ${policy.name} v${target.versionNumber}`);
|
|
8688
|
-
details([
|
|
8689
|
-
["Environment", `${context.config.environmentName} (${context.config.environmentType})`],
|
|
8690
|
-
["Policy", `${policy.name} (${policy.id})`],
|
|
8691
|
-
[
|
|
8692
|
-
"Currently active",
|
|
8693
|
-
policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
|
|
8694
|
-
],
|
|
8695
|
-
["Deploying", `v${target.versionNumber}`],
|
|
8696
|
-
["Bindings", String(policy.bindings.length)]
|
|
8697
|
-
]);
|
|
8698
|
-
if (policy.bindings.length === 0) {
|
|
8699
|
-
out();
|
|
8700
|
-
out(
|
|
8701
|
-
` ${symbol.warn} ${style.amber("This policy is not bound to any scope.")} Activating it will not change any decision until you bind it.`
|
|
8702
|
-
);
|
|
8703
|
-
}
|
|
8704
|
-
if (policy.activeVersion !== null) {
|
|
8705
|
-
const diff = await context.api.get(`/v1/policies/${policyId}/diff`, {
|
|
8706
|
-
from: policy.activeVersion.versionNumber,
|
|
8707
|
-
to: target.versionNumber
|
|
8708
|
-
}).catch(() => null);
|
|
8709
|
-
if (diff) {
|
|
8710
|
-
heading("Changes");
|
|
8711
|
-
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
8712
|
-
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
8713
|
-
for (const rule of diff.changed) {
|
|
8714
|
-
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
8715
|
-
}
|
|
8716
|
-
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
8717
|
-
out(style.grey(" No rule changes."));
|
|
8718
|
-
}
|
|
8719
|
-
if (diff.riskAssessment) {
|
|
8720
|
-
out();
|
|
8721
|
-
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
8722
|
-
}
|
|
8723
|
-
}
|
|
8724
|
-
}
|
|
8725
|
-
heading("Tests");
|
|
8726
|
-
const testSpinner = spinner("Running recorded test cases");
|
|
8727
|
-
let testResult = null;
|
|
8728
|
-
try {
|
|
8729
|
-
testResult = await context.api.post(`/v1/policies/${policyId}/tests/run`, {
|
|
8730
|
-
version_id: target.id
|
|
8731
|
-
});
|
|
8732
|
-
testSpinner.succeed(`${testResult.passed}/${caseCount(testResult)} test case(s) passing`);
|
|
8733
|
-
} catch (error) {
|
|
8734
|
-
testSpinner.fail("Could not run the test cases");
|
|
8735
|
-
throw error;
|
|
8736
|
-
}
|
|
8737
|
-
if (testResult.failed > 0) {
|
|
8738
|
-
out();
|
|
8739
|
-
for (const failure of testResult.results.filter((entry) => !entry.passed)) {
|
|
8740
|
-
out(
|
|
8741
|
-
` ${symbol.fail} ${failure.name}: expected ${decisionStyle(failure.expected)}, got ${decisionStyle(failure.actual)}`
|
|
8742
|
-
);
|
|
8743
|
-
}
|
|
8744
|
-
out();
|
|
8745
|
-
throw new CliError(
|
|
8746
|
-
`${testResult.failed} test case(s) fail. This version will not be deployed.`,
|
|
8747
|
-
"Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
|
|
8748
|
-
);
|
|
8749
|
-
}
|
|
8750
|
-
if (caseCount(testResult) === 0) {
|
|
8751
|
-
out();
|
|
8752
|
-
out(
|
|
8753
|
-
` ${symbol.warn} ${style.amber("This policy has no test cases.")} There is no evidence the new rules behave as intended.`
|
|
8754
|
-
);
|
|
8755
|
-
const proceed = await confirm("Deploy anyway?", { yes: flags.yes });
|
|
8756
|
-
if (!proceed) {
|
|
8757
|
-
out(style.grey("Cancelled. Add test cases first."));
|
|
8758
|
-
return 1;
|
|
8759
|
-
}
|
|
8760
|
-
}
|
|
8761
|
-
const reason = valueFor(args, "--reason") ?? await ask("Reason for this change (recorded in the audit trail, min 10 characters):");
|
|
8762
|
-
if (reason.trim().length < 10) {
|
|
8763
|
-
throw new CliError("A reason of at least 10 characters is required.");
|
|
8764
|
-
}
|
|
8765
|
-
if (context.config.environmentType === "PRODUCTION") {
|
|
8766
|
-
out();
|
|
8767
|
-
out(` ${symbol.warn} ${style.amber("This affects a PRODUCTION environment.")}`);
|
|
8768
|
-
}
|
|
8769
|
-
const confirmed = await confirm(`Activate v${target.versionNumber}?`, { yes: flags.yes });
|
|
8770
|
-
if (!confirmed) {
|
|
8771
|
-
out(style.grey("Cancelled. No change was made."));
|
|
8772
|
-
return 1;
|
|
8773
|
-
}
|
|
8774
|
-
const deploySpinner = spinner(`Activating v${target.versionNumber}`);
|
|
8775
|
-
try {
|
|
8776
|
-
await withStepUp(
|
|
8777
|
-
context.api,
|
|
8778
|
-
() => context.api.post(`/v1/policies/${policyId}/activate`, {
|
|
8779
|
-
version_id: target.id,
|
|
8780
|
-
reason
|
|
8781
|
-
})
|
|
8782
|
-
);
|
|
8783
|
-
deploySpinner.succeed(`${policy.name} v${target.versionNumber} is ACTIVE`);
|
|
8784
|
-
} catch (error) {
|
|
8785
|
-
deploySpinner.fail("Activation failed");
|
|
8786
|
-
throw error;
|
|
8787
|
-
}
|
|
8788
|
-
out();
|
|
8789
|
-
out(` ${symbol.pass} The previous version is marked SUPERSEDED and can be rolled back.`);
|
|
8790
|
-
out(` ${symbol.pass} The change and its diff are recorded in the audit trail.`);
|
|
8791
|
-
out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
|
|
8792
|
-
out();
|
|
8793
|
-
return 0;
|
|
8794
|
-
}
|
|
8795
|
-
async function diffPolicy(args, flags) {
|
|
8796
|
-
const context = await resolveContext(flags);
|
|
8797
|
-
const policyId = positional(args);
|
|
8798
|
-
if (!policyId) {
|
|
8799
|
-
throw new CliError(
|
|
8800
|
-
"A policy id is required.",
|
|
8801
|
-
"Usage: veefore policies diff <policyId> --from <n> --to <n>"
|
|
8802
|
-
);
|
|
8803
|
-
}
|
|
8804
|
-
const from = valueFor(args, "--from");
|
|
8805
|
-
const to = valueFor(args, "--to");
|
|
8806
|
-
const diff = await context.api.get(`/v1/policies/${policyId}/diff`, { from, to });
|
|
8807
|
-
if (flags.json) {
|
|
8808
|
-
printJson(diff);
|
|
8809
|
-
return 0;
|
|
8810
|
-
}
|
|
8811
|
-
heading(`Policy diff v${diff.from} \u2192 v${diff.to}`);
|
|
8812
|
-
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
8813
|
-
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
8814
|
-
for (const rule of diff.changed) {
|
|
8815
|
-
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
8816
|
-
}
|
|
8817
|
-
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
8818
|
-
out(style.grey(" No differences."));
|
|
8819
|
-
}
|
|
8820
|
-
if (diff.riskAssessment) {
|
|
8821
|
-
out();
|
|
8822
|
-
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
8823
|
-
}
|
|
8824
|
-
out();
|
|
8825
|
-
return 0;
|
|
8826
|
-
}
|
|
8827
|
-
|
|
8828
|
-
// src/help.ts
|
|
8829
|
-
var COMMANDS = [
|
|
8830
|
-
{
|
|
8831
|
-
name: "init",
|
|
8832
|
-
summary: "Set up this project with Veefore. Also runs as plain `veefore`.",
|
|
8833
|
-
usage: "veefore init [options]",
|
|
8834
|
-
description: "Connects to hosted Veefore, or to the self-hosted Veefore named with --control-plane (remembered afterwards), signs you in through the browser, scans the project locally (framework, tools, the hosts they call, the credentials they read \u2014 names only, never values), shows what it will set up and asks once. It then creates an agent with a key pair generated on this machine, registers each tool with its own capability and authorizes it for this agent, adds a starter policy in OBSERVE mode, writes veefore.config.json and .env.local, and validates the connection. The private key is written to .env.local only and is never sent, printed or recoverable.",
|
|
8835
|
-
options: [
|
|
8836
|
-
["--force", "Replace an existing veefore.config.json."],
|
|
8837
|
-
[
|
|
8838
|
-
"--new-agent",
|
|
8839
|
-
"Create a separate agent even if one with this name exists. By default the existing agent is reused with a new key, so its delegations and limits keep applying."
|
|
8840
|
-
],
|
|
8841
|
-
["--control-plane <url>", "Self-hosted Veefore address. Needed once; remembered after."],
|
|
8842
|
-
["--gateway <url>", "Gateway base URL that agents will call."],
|
|
8843
|
-
["--organization <id>", "Skip the organization prompt."],
|
|
8844
|
-
["--workspace <id>", "Skip the workspace prompt."],
|
|
8845
|
-
["--environment <id>", "Skip the environment prompt."],
|
|
8846
|
-
["--name <name>", "Agent display name."],
|
|
8847
|
-
["--capability <key>", "Grant a capability. Repeatable."],
|
|
8848
|
-
["--skip-policy", "Do not create a starter policy."],
|
|
8849
|
-
["--skip-code", "Do not offer to change your code; run `veefore integrate` later."],
|
|
8850
|
-
[
|
|
8851
|
-
"--approval-mode <wait|throw>",
|
|
8852
|
-
"When approval is required, guarded tools wait for the decision (default) or return at once."
|
|
8853
|
-
]
|
|
8854
|
-
],
|
|
8855
|
-
examples: ["veefore init", "veefore init --name finance-agent --capability refund.create"]
|
|
8856
|
-
},
|
|
8857
|
-
{
|
|
8858
|
-
name: "scan",
|
|
8859
|
-
summary: "Preview what setup would find, without changing anything.",
|
|
8860
|
-
usage: "veefore scan [--json]",
|
|
8861
|
-
description: "Scans the project locally and lists its framework, tools, the external hosts each tool calls and the credentials the code reads. Reads source files only: .env files and other hidden files are never opened, and secret values are never read. Needs no sign-in.",
|
|
8862
|
-
options: [["--json", "Machine-readable scan and the setup plan it implies."]]
|
|
8863
|
-
},
|
|
8864
|
-
{
|
|
8865
|
-
name: "integrate",
|
|
8866
|
-
summary: "Wrap your registered tools in guard() so Veefore decides every call.",
|
|
8867
|
-
usage: "veefore integrate [--dry-run] [--check] [--assist=manual|own|veefore|off] [--yes]",
|
|
8868
|
-
description: "Finds each tool registered for this agent in your code and wraps its function in guard() (TypeScript/JavaScript) or adds @guard above it (Python). Your function bodies are not edited. Shows a diff and asks before writing, backs up every file it changes, then runs your TypeScript compiler (--noEmit), node --check or a Python syntax check; if a check newly fails, every file is restored automatically. For tools it will not change on its own, you choose: make the change yourself from exact instructions, use AI with your own Anthropic or OpenAI key (code goes only to that provider), or use Veefore AI if enabled. Before anything is sent, secrets are replaced with placeholders and you see what goes where and must agree; an AI change is accepted only if it keeps every line of your code and adds nothing but the guard, and you still review the diff.",
|
|
8869
|
-
options: [
|
|
8870
|
-
["--dry-run", "Show the diff and write nothing."],
|
|
8871
|
-
["--check", "Report which registered tools are guarded in the code. Exits 1 if any are not."],
|
|
8872
|
-
["--assist=<how>", "manual (default when not interactive), own, veefore, or off."],
|
|
8873
|
-
["--yes", "Apply without asking."]
|
|
8874
|
-
],
|
|
8875
|
-
examples: [
|
|
8876
|
-
"veefore integrate --dry-run",
|
|
8877
|
-
"veefore integrate --assist=manual",
|
|
8878
|
-
"ANTHROPIC_API_KEY=\u2026 veefore integrate --assist=own",
|
|
8879
|
-
"veefore integrate --check"
|
|
8880
|
-
]
|
|
8881
|
-
},
|
|
8882
|
-
{
|
|
8883
|
-
name: "undo",
|
|
8884
|
-
summary: "Restore the files the last `veefore integrate` changed.",
|
|
8885
|
-
usage: "veefore undo [--force]",
|
|
8886
|
-
description: "Restores each file from the backup taken before the last rewrite. A file you edited afterwards is left alone and listed, unless --force is given.",
|
|
8887
|
-
options: [["--force", "Restore files even if they were edited after the rewrite."]]
|
|
8888
|
-
},
|
|
8889
|
-
{
|
|
8890
|
-
name: "remove",
|
|
8891
|
-
summary: "Take Veefore out of this project: guard() from the code, then disconnect.",
|
|
8892
|
-
usage: "veefore remove [--dry-run] [--keep-agent]",
|
|
8893
|
-
description: "Offers to revoke the agent in Veefore if it is still active, removes every guard() this CLI added (showing the diff, asking once, backing up and rolling back if your checks fail), then deletes veefore.config.json and the Veefore block in .env.local, including the private key. Edits you made after setup are kept. `veefore` offers this automatically when the agent was revoked in the console.",
|
|
8894
|
-
options: [
|
|
8895
|
-
["--dry-run", "Show what would change and write nothing."],
|
|
8896
|
-
["--keep-agent", "Do not offer to revoke the agent in Veefore."]
|
|
8897
|
-
],
|
|
8898
|
-
examples: ["veefore remove --dry-run", "veefore remove"]
|
|
8899
|
-
},
|
|
8900
|
-
{
|
|
8901
|
-
name: "dev",
|
|
8902
|
-
summary: "Watch decisions live while you build.",
|
|
8903
|
-
usage: "veefore dev [--all]",
|
|
8904
|
-
description: "Confirms the gateway is reachable, reports the enforcement mode, then streams every decision for this agent with the next concrete step for each outcome.",
|
|
8905
|
-
options: [["--all", "Stream decisions for every agent in the environment."]]
|
|
8906
|
-
},
|
|
8907
|
-
{
|
|
8908
|
-
name: "doctor",
|
|
8909
|
-
summary: "Audit the security posture of this setup.",
|
|
8910
|
-
usage: "veefore doctor [--strict]",
|
|
8911
|
-
description: "Checks authentication, MFA, TLS, local secret permissions, identity and passport validity, gateway health, dangerous tools, network restrictions, policy coverage, production data access, MCP transport and authorization, audit chain integrity, credential rotation, and SDK version alignment.",
|
|
8912
|
-
options: [["--strict", "Exit non-zero on warnings as well as failures."]]
|
|
8913
|
-
},
|
|
8914
|
-
{
|
|
8915
|
-
name: "status",
|
|
8916
|
-
summary: "Show the current environment, mode and agent health.",
|
|
8917
|
-
usage: "veefore status",
|
|
8918
|
-
description: "Prints the active organization, workspace and environment, the enforcement mode, the agent lifecycle state, gateway health, and the last few decisions."
|
|
8919
|
-
},
|
|
8920
|
-
{
|
|
8921
|
-
name: "test",
|
|
8922
|
-
summary: "Verify enforcement works, without performing any action.",
|
|
8923
|
-
usage: "veefore test [--tool <key>]",
|
|
8924
|
-
description: "Runs a dry-run probe across every tool this agent is authorized for, checks that policy test cases pass, and fails if a dangerous tool is permitted only by the default decision. Suitable as a CI gate.",
|
|
8925
|
-
options: [["--tool <key>", "Probe a single tool."]]
|
|
8926
|
-
},
|
|
8927
|
-
{
|
|
8928
|
-
name: "agents",
|
|
8929
|
-
summary: "Inspect and control the agent fleet.",
|
|
8930
|
-
usage: "veefore agents <list|show|pause|resume|suspend|revoke|rotate-key> [agentId]",
|
|
8931
|
-
description: "Lifecycle control. `revoke` is irreversible for the identity and shows the full blast radius before asking for confirmation. `rotate-key` issues a new key pair and invalidates every outstanding session for that agent.",
|
|
8932
|
-
options: [
|
|
8933
|
-
["--all", "List across the whole organization, not just this environment."],
|
|
8934
|
-
["--status <status>", "Filter by lifecycle status."],
|
|
8935
|
-
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8936
|
-
],
|
|
8937
|
-
examples: [
|
|
8938
|
-
"veefore agents list",
|
|
8939
|
-
'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
|
|
8940
|
-
]
|
|
8941
|
-
},
|
|
8942
|
-
{
|
|
8943
|
-
name: "policies",
|
|
8944
|
-
summary: "List, test and deploy policies.",
|
|
8945
|
-
usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
|
|
8946
|
-
description: "`test` runs the recorded test cases. `deploy` activates a version and refuses to do so while any test case fails; it requires a reason and step-up authentication.",
|
|
8947
|
-
options: [
|
|
8948
|
-
["--version <n>", "Version to deploy, for `deploy`."],
|
|
8949
|
-
["--from <n> --to <n>", "Versions to compare, for `diff`."],
|
|
8950
|
-
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8951
|
-
],
|
|
8952
|
-
examples: [
|
|
8953
|
-
"veefore policies list",
|
|
8954
|
-
"veefore policies test",
|
|
8955
|
-
"veefore policies deploy pol_01ABC"
|
|
8956
|
-
]
|
|
8957
|
-
},
|
|
8958
|
-
{
|
|
8959
|
-
name: "logs",
|
|
8960
|
-
summary: "Read or stream the decision log.",
|
|
8961
|
-
usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
|
|
8962
|
-
description: "Every line is a server-side decision with the policy and rule that produced it.",
|
|
8963
|
-
options: [
|
|
8964
|
-
["--follow, -f", "Stream new decisions as they happen."],
|
|
8965
|
-
["--decision <d>", "Filter by decision, e.g. BLOCK."],
|
|
8966
|
-
["--agent <id>", "Filter by agent."],
|
|
8967
|
-
["--all", "Include every agent in the environment."],
|
|
8968
|
-
["--limit <n>", "Number of recent decisions to show."]
|
|
8969
|
-
]
|
|
8970
|
-
},
|
|
8971
|
-
{
|
|
8972
|
-
name: "audit",
|
|
8973
|
-
summary: "Query, verify and export the audit trail.",
|
|
8974
|
-
usage: "veefore audit <list|show|verify|export> [eventId]",
|
|
8975
|
-
description: "`verify` re-checks the hash chain and exits non-zero if it is broken. `export` returns a JSON or CSV file together with a content hash an auditor can use to prove the file is unaltered.",
|
|
8976
|
-
options: [
|
|
8977
|
-
["--severity <s>", "Filter by severity."],
|
|
8978
|
-
["--type <t>", "Filter by event type."],
|
|
8979
|
-
["--from <iso> --to <iso>", "Time range."],
|
|
8980
|
-
["--format <json|csv>", "Export format."],
|
|
8981
|
-
['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
|
|
8982
|
-
["--out <file>", "Write the export to a file (mode 0600)."]
|
|
8983
|
-
],
|
|
8984
|
-
examples: [
|
|
8985
|
-
"veefore audit verify",
|
|
8986
|
-
'veefore audit export --format csv --reason "Quarterly SOC 2 evidence" --out audit.csv'
|
|
8987
|
-
]
|
|
8988
|
-
},
|
|
8989
|
-
{
|
|
8990
|
-
name: "deploy",
|
|
8991
|
-
summary: "Promote the environment to enforcement.",
|
|
8992
|
-
usage: "veefore deploy [--mode ENFORCE|SIMULATE|OBSERVE]",
|
|
8993
|
-
description: "Runs preflight checks, replays recent observed traffic to show exactly what starts being refused, then requires confirmation, a reason and step-up authentication. Reducing enforcement always requires a reason and is recorded as a CRITICAL audit event.",
|
|
8994
|
-
options: [
|
|
8995
|
-
["--mode <mode>", "Target mode. Defaults to ENFORCE."],
|
|
8996
|
-
['--reason "<text>"', "Reason recorded in the audit trail."],
|
|
8997
|
-
["--skip-preflight", "Incident use only. Recorded in the audit reason."]
|
|
8998
|
-
]
|
|
8999
|
-
},
|
|
9000
|
-
{
|
|
9001
|
-
name: "login",
|
|
9002
|
-
summary: "Sign in through your browser and store a session token.",
|
|
9003
|
-
usage: "veefore login [--device] [--password] [--control-plane <url>]",
|
|
9004
|
-
description: "Opens the console in your browser, where you sign in (or create an account) and approve this terminal. The terminal never sees your password. Over SSH, or with --device, it prints a link and a code to enter in any browser instead. Only the resulting session token is stored, in ~/.veefore/credentials.json at mode 0600, and the session can be revoked from your profile. In CI, set VEEFORE_EMAIL and VEEFORE_PASSWORD.",
|
|
9005
|
-
options: [
|
|
9006
|
-
["--device", "Show a link and a code instead of opening a browser here."],
|
|
9007
|
-
["--password", "Type email and password in the terminal instead of using the browser."]
|
|
9008
|
-
]
|
|
9009
|
-
},
|
|
9010
|
-
{
|
|
9011
|
-
name: "logout",
|
|
9012
|
-
summary: "Sign out of Veefore on this machine.",
|
|
9013
|
-
usage: "veefore logout [--all]",
|
|
9014
|
-
description: 'Ends the session on the server and deletes the stored token. Signing out of the console with "Also sign out of the Veefore CLI" ticked does the same for every terminal you approved; the next command here then says you were signed out and asks you to sign in again.',
|
|
9015
|
-
options: [["--all", "Sign out of every control plane this machine is signed in to."]]
|
|
9016
|
-
}
|
|
9017
|
-
];
|
|
9018
|
-
var GLOBAL_OPTIONS = [
|
|
9019
|
-
["--json", "Machine-readable output."],
|
|
9020
|
-
["--yes, -y", "Assume yes for confirmations. Never skips step-up authentication."],
|
|
9021
|
-
["--verbose", "Expanded output."],
|
|
9022
|
-
[
|
|
9023
|
-
"--control-plane <url>",
|
|
9024
|
-
"Use this Veefore instead of the one found automatically (hosted, or the last you signed in to)."
|
|
9025
|
-
],
|
|
9026
|
-
["--gateway <url>", "Override the configured gateway URL."],
|
|
9027
|
-
["--version, -V", "Print the CLI version."],
|
|
9028
|
-
["--help, -h", "Show help."]
|
|
9029
|
-
];
|
|
9030
|
-
function printRootHelp(version) {
|
|
9031
|
-
out();
|
|
9032
|
-
out(
|
|
9033
|
-
`${style.bold("veefore")} ${style.grey(version)} \u2014 identity, authority and policy for AI agents`
|
|
9034
|
-
);
|
|
9035
|
-
out();
|
|
9036
|
-
out(style.bold("USAGE"));
|
|
9037
|
-
out(" veefore <command> [options]");
|
|
9038
|
-
out();
|
|
9039
|
-
out(style.bold("COMMANDS"));
|
|
9040
|
-
const width = Math.max(...COMMANDS.map((command) => command.name.length));
|
|
9041
|
-
for (const command of COMMANDS) {
|
|
9042
|
-
out(` ${style.cyan(command.name.padEnd(width))} ${command.summary}`);
|
|
9043
|
-
}
|
|
9044
|
-
out();
|
|
9045
|
-
out(style.bold("GLOBAL OPTIONS"));
|
|
9046
|
-
renderOptions(GLOBAL_OPTIONS);
|
|
9047
|
-
out();
|
|
9048
|
-
out(style.bold("GETTING STARTED"));
|
|
9049
|
-
out(
|
|
9050
|
-
` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
|
|
9051
|
-
);
|
|
9307
|
+
}
|
|
9308
|
+
}
|
|
9309
|
+
out();
|
|
9052
9310
|
out(
|
|
9053
|
-
` ${style.
|
|
9311
|
+
totalFailed === 0 ? ` ${symbol.pass} ${style.green(`All ${totalCases} test case(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${totalFailed} failure(s) across ${totalCases} test case(s).`)}`
|
|
9054
9312
|
);
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9313
|
+
if (withoutTests > 0) {
|
|
9314
|
+
out(
|
|
9315
|
+
` ${symbol.warn} ${style.amber(`${withoutTests} policy/policies have no test cases.`)} Add cases so activation can be proven safe.`
|
|
9316
|
+
);
|
|
9317
|
+
}
|
|
9059
9318
|
out();
|
|
9060
9319
|
}
|
|
9061
|
-
function
|
|
9062
|
-
const
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
out();
|
|
9070
|
-
out(style.bold("DESCRIPTION"));
|
|
9071
|
-
for (const line of wrap2(command.description, 76)) out(` ${line}`);
|
|
9072
|
-
if (command.options && command.options.length > 0) {
|
|
9073
|
-
out();
|
|
9074
|
-
out(style.bold("OPTIONS"));
|
|
9075
|
-
renderOptions(command.options);
|
|
9320
|
+
async function deployPolicy(args, flags) {
|
|
9321
|
+
const context = await resolveContext(flags);
|
|
9322
|
+
const policyId = positional(args);
|
|
9323
|
+
if (!policyId) {
|
|
9324
|
+
throw new CliError(
|
|
9325
|
+
"A policy id is required.",
|
|
9326
|
+
'Usage: veefore policies deploy <policyId> [--version <n>] [--reason "\u2026"]'
|
|
9327
|
+
);
|
|
9076
9328
|
}
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9329
|
+
const policy = await context.api.get(`/v1/policies/${policyId}`);
|
|
9330
|
+
const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
|
|
9331
|
+
const requested = valueFor(args, "--version");
|
|
9332
|
+
const target = requested ? versions.find((version) => String(version.versionNumber) === requested) : versions.find((version) => version.status === "DRAFT") ?? versions.find((version) => version.status === "TESTED");
|
|
9333
|
+
if (!target) {
|
|
9334
|
+
throw new CliError(
|
|
9335
|
+
requested ? `Version ${requested} was not found for this policy.` : "There is no draft version to deploy.",
|
|
9336
|
+
"Edit the policy in the console to create a new draft, then deploy it."
|
|
9337
|
+
);
|
|
9081
9338
|
}
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
function renderOptions(options) {
|
|
9086
|
-
const width = Math.max(...options.map(([flag]) => flag.length));
|
|
9087
|
-
for (const [flag, description] of options) {
|
|
9088
|
-
out(` ${style.cyan(flag.padEnd(width))} ${description}`);
|
|
9339
|
+
if (target.versionNumber === policy.activeVersion?.versionNumber) {
|
|
9340
|
+
out(`${symbol.info} Version ${target.versionNumber} is already active. Nothing to do.`);
|
|
9341
|
+
return 0;
|
|
9089
9342
|
}
|
|
9090
|
-
}
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9343
|
+
heading(`Deploy ${policy.name} v${target.versionNumber}`);
|
|
9344
|
+
details([
|
|
9345
|
+
["Environment", `${context.config.environmentName} (${context.config.environmentType})`],
|
|
9346
|
+
["Policy", `${policy.name} (${policy.id})`],
|
|
9347
|
+
[
|
|
9348
|
+
"Currently active",
|
|
9349
|
+
policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
|
|
9350
|
+
],
|
|
9351
|
+
["Deploying", `v${target.versionNumber}`],
|
|
9352
|
+
["Bindings", String(policy.bindings.length)]
|
|
9353
|
+
]);
|
|
9354
|
+
if (policy.bindings.length === 0) {
|
|
9355
|
+
out();
|
|
9356
|
+
out(
|
|
9357
|
+
` ${symbol.warn} ${style.amber("This policy is not bound to any scope.")} Activating it will not change any decision until you bind it.`
|
|
9358
|
+
);
|
|
9359
|
+
}
|
|
9360
|
+
if (policy.activeVersion !== null) {
|
|
9361
|
+
const diff = await context.api.get(`/v1/policies/${policyId}/diff`, {
|
|
9362
|
+
from: policy.activeVersion.versionNumber,
|
|
9363
|
+
to: target.versionNumber
|
|
9364
|
+
}).catch(() => null);
|
|
9365
|
+
if (diff) {
|
|
9366
|
+
heading("Changes");
|
|
9367
|
+
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
9368
|
+
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
9369
|
+
for (const rule of diff.changed) {
|
|
9370
|
+
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
9371
|
+
}
|
|
9372
|
+
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
9373
|
+
out(style.grey(" No rule changes."));
|
|
9374
|
+
}
|
|
9375
|
+
if (diff.riskAssessment) {
|
|
9376
|
+
out();
|
|
9377
|
+
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
9378
|
+
}
|
|
9101
9379
|
}
|
|
9102
9380
|
}
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
out(style.bold("Let's get started."));
|
|
9381
|
+
heading("Tests");
|
|
9382
|
+
const testSpinner = spinner("Running recorded test cases");
|
|
9383
|
+
let testResult = null;
|
|
9384
|
+
try {
|
|
9385
|
+
testResult = await context.api.post(`/v1/policies/${policyId}/tests/run`, {
|
|
9386
|
+
version_id: target.id
|
|
9387
|
+
});
|
|
9388
|
+
testSpinner.succeed(`${testResult.passed}/${caseCount(testResult)} test case(s) passing`);
|
|
9389
|
+
} catch (error) {
|
|
9390
|
+
testSpinner.fail("Could not run the test cases");
|
|
9391
|
+
throw error;
|
|
9392
|
+
}
|
|
9393
|
+
if (testResult.failed > 0) {
|
|
9117
9394
|
out();
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
{
|
|
9123
|
-
label: "Preview what setup would find",
|
|
9124
|
-
hint: "veefore scan \xB7 changes nothing",
|
|
9125
|
-
value: "scan"
|
|
9126
|
-
},
|
|
9127
|
-
{ label: "Sign in", hint: "veefore login", value: "login" },
|
|
9128
|
-
{ label: "Show every command", hint: "veefore help", value: "help" },
|
|
9129
|
-
{ label: "Exit", hint: "esc", value: "exit" }
|
|
9130
|
-
]);
|
|
9131
|
-
} catch (error) {
|
|
9132
|
-
if (error instanceof MenuCancelled) return 0;
|
|
9133
|
-
throw error;
|
|
9395
|
+
for (const failure of testResult.results.filter((entry) => !entry.passed)) {
|
|
9396
|
+
out(
|
|
9397
|
+
` ${symbol.fail} ${failure.name}: expected ${decisionStyle(failure.expected)}, got ${decisionStyle(failure.actual)}`
|
|
9398
|
+
);
|
|
9134
9399
|
}
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9400
|
+
out();
|
|
9401
|
+
throw new CliError(
|
|
9402
|
+
`${testResult.failed} test case(s) fail. This version will not be deployed.`,
|
|
9403
|
+
"Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
|
|
9404
|
+
);
|
|
9405
|
+
}
|
|
9406
|
+
if (caseCount(testResult) === 0) {
|
|
9407
|
+
out();
|
|
9408
|
+
out(
|
|
9409
|
+
` ${symbol.warn} ${style.amber("This policy has no test cases.")} There is no evidence the new rules behave as intended.`
|
|
9410
|
+
);
|
|
9411
|
+
const proceed = await confirm("Deploy anyway?", { yes: flags.yes });
|
|
9412
|
+
if (!proceed) {
|
|
9413
|
+
out(style.grey("Cancelled. Add test cases first."));
|
|
9414
|
+
return 1;
|
|
9144
9415
|
}
|
|
9145
|
-
const code = await runOnScreen(source, flags, async () => {
|
|
9146
|
-
switch (action) {
|
|
9147
|
-
case "scan":
|
|
9148
|
-
return scanCommand([], flags);
|
|
9149
|
-
case "login":
|
|
9150
|
-
await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
|
|
9151
|
-
return 0;
|
|
9152
|
-
case "help":
|
|
9153
|
-
printRootHelp(source().version);
|
|
9154
|
-
return 0;
|
|
9155
|
-
}
|
|
9156
|
-
return 0;
|
|
9157
|
-
});
|
|
9158
|
-
if (!isInteractive()) return code;
|
|
9159
9416
|
}
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9417
|
+
const reason = valueFor(args, "--reason") ?? await ask("Reason for this change (recorded in the audit trail, min 10 characters):");
|
|
9418
|
+
if (reason.trim().length < 10) {
|
|
9419
|
+
throw new CliError("A reason of at least 10 characters is required.");
|
|
9420
|
+
}
|
|
9421
|
+
if (context.config.environmentType === "PRODUCTION") {
|
|
9422
|
+
out();
|
|
9423
|
+
out(` ${symbol.warn} ${style.amber("This affects a PRODUCTION environment.")}`);
|
|
9424
|
+
}
|
|
9425
|
+
const confirmed = await confirm(`Activate v${target.versionNumber}?`, { yes: flags.yes });
|
|
9426
|
+
if (!confirmed) {
|
|
9427
|
+
out(style.grey("Cancelled. No change was made."));
|
|
9428
|
+
return 1;
|
|
9429
|
+
}
|
|
9430
|
+
const deploySpinner = spinner(`Activating v${target.versionNumber}`);
|
|
9168
9431
|
try {
|
|
9169
|
-
|
|
9432
|
+
await withStepUp(
|
|
9433
|
+
context.api,
|
|
9434
|
+
() => context.api.post(`/v1/policies/${policyId}/activate`, {
|
|
9435
|
+
version_id: target.id,
|
|
9436
|
+
reason
|
|
9437
|
+
})
|
|
9438
|
+
);
|
|
9439
|
+
deploySpinner.succeed(`${policy.name} v${target.versionNumber} is ACTIVE`);
|
|
9170
9440
|
} catch (error) {
|
|
9171
|
-
|
|
9441
|
+
deploySpinner.fail("Activation failed");
|
|
9442
|
+
throw error;
|
|
9172
9443
|
}
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9444
|
+
out();
|
|
9445
|
+
out(` ${symbol.pass} The previous version is marked SUPERSEDED and can be rolled back.`);
|
|
9446
|
+
out(` ${symbol.pass} The change and its diff are recorded in the audit trail.`);
|
|
9447
|
+
out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
|
|
9448
|
+
out();
|
|
9449
|
+
return 0;
|
|
9450
|
+
}
|
|
9451
|
+
async function diffPolicy(args, flags) {
|
|
9452
|
+
const context = await resolveContext(flags);
|
|
9453
|
+
const policyId = positional(args);
|
|
9454
|
+
if (!policyId) {
|
|
9455
|
+
throw new CliError(
|
|
9456
|
+
"A policy id is required.",
|
|
9457
|
+
"Usage: veefore policies diff <policyId> --from <n> --to <n>"
|
|
9458
|
+
);
|
|
9459
|
+
}
|
|
9460
|
+
const from = valueFor(args, "--from");
|
|
9461
|
+
const to = valueFor(args, "--to");
|
|
9462
|
+
const diff = await context.api.get(`/v1/policies/${policyId}/diff`, { from, to });
|
|
9463
|
+
if (flags.json) {
|
|
9464
|
+
printJson(diff);
|
|
9465
|
+
return 0;
|
|
9466
|
+
}
|
|
9467
|
+
heading(`Policy diff v${diff.from} \u2192 v${diff.to}`);
|
|
9468
|
+
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
9469
|
+
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
9470
|
+
for (const rule of diff.changed) {
|
|
9471
|
+
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
9472
|
+
}
|
|
9473
|
+
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
9474
|
+
out(style.grey(" No differences."));
|
|
9475
|
+
}
|
|
9476
|
+
if (diff.riskAssessment) {
|
|
9477
|
+
out();
|
|
9478
|
+
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
9479
|
+
}
|
|
9480
|
+
out();
|
|
9481
|
+
return 0;
|
|
9176
9482
|
}
|
|
9177
9483
|
|
|
9178
9484
|
// src/index.ts
|