veefore 1.1.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 +1954 -1477
- 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, "*"],
|
|
@@ -638,10 +653,22 @@ var STARS = [
|
|
|
638
653
|
[5, 34, "*"],
|
|
639
654
|
[9, 32, "\xB7"]
|
|
640
655
|
];
|
|
641
|
-
async function showWelcome(context) {
|
|
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) {
|
|
|
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 = 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
|
}
|
|
@@ -704,17 +733,216 @@ function renderPanel(facts) {
|
|
|
704
733
|
}
|
|
705
734
|
return lines;
|
|
706
735
|
}
|
|
736
|
+
function clearScreen() {
|
|
737
|
+
if (!isInteractive()) return;
|
|
738
|
+
process.stdout.write("\x1B[H\x1B[2J");
|
|
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
|
+
}
|
|
775
|
+
function compactHeader(context) {
|
|
776
|
+
const facts = Object.fromEntries(context.facts);
|
|
777
|
+
const parts = [
|
|
778
|
+
brand(style.bold("Veefore")),
|
|
779
|
+
style.grey(`v${context.version}`),
|
|
780
|
+
...[facts.Folder, facts["Signed in"], facts.Veefore].filter((v) => Boolean(v))
|
|
781
|
+
];
|
|
782
|
+
out();
|
|
783
|
+
out(fit(` ${parts.join(style.grey(" \xB7 "))}`, columns() - 1));
|
|
784
|
+
out(` ${style.grey("\xB7".repeat(Math.max(10, Math.min(columns() - 2, 72))))}`);
|
|
785
|
+
}
|
|
786
|
+
async function beginScreen(source, options = {}) {
|
|
787
|
+
if (!isInteractive()) return;
|
|
788
|
+
clearScreen();
|
|
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
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async function pressBack(label = "Back") {
|
|
882
|
+
if (!isInteractive()) return;
|
|
883
|
+
flushCapture();
|
|
884
|
+
const stdin = process.stdin;
|
|
885
|
+
const stdout = process.stdout;
|
|
886
|
+
out();
|
|
887
|
+
stdout.write(`${brand("\u2190")} ${style.bold(label)} ${style.grey("\xB7 enter or esc")}`);
|
|
888
|
+
await new Promise((resolve) => {
|
|
889
|
+
const wasRaw = stdin.isRaw;
|
|
890
|
+
const done = () => {
|
|
891
|
+
stdin.off("keypress", onKey);
|
|
892
|
+
stdin.setRawMode(wasRaw);
|
|
893
|
+
stdin.pause();
|
|
894
|
+
stdout.write("\r\x1B[K");
|
|
895
|
+
resolve();
|
|
896
|
+
};
|
|
897
|
+
const onKey = (input, key = {}) => {
|
|
898
|
+
if (key.name === "return" || key.name === "enter" || key.name === "escape" || key.name === "left" || key.name === "backspace" || input === "b" || key.ctrl && key.name === "c") {
|
|
899
|
+
done();
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
emitKeypressEvents(stdin);
|
|
903
|
+
stdin.setRawMode(true);
|
|
904
|
+
stdin.resume();
|
|
905
|
+
stdin.on("keypress", onKey);
|
|
906
|
+
});
|
|
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
|
+
}
|
|
931
|
+
var BACK = /* @__PURE__ */ Symbol("veefore.back");
|
|
707
932
|
var MenuCancelled = class extends CliError {
|
|
708
933
|
constructor() {
|
|
709
934
|
super("Cancelled.", void 0, 130);
|
|
710
935
|
this.name = "MenuCancelled";
|
|
711
936
|
}
|
|
712
937
|
};
|
|
713
|
-
async function select(question,
|
|
938
|
+
async function select(question, real, settings = {}) {
|
|
939
|
+
const options = settings.back ? [...real, { label: settings.backLabel ?? "Back", value: BACK, hint: "\u2190" }] : real;
|
|
714
940
|
const enabled = options.map((option, index) => option.disabled ? -1 : index).filter((i) => i >= 0);
|
|
715
941
|
if (enabled.length === 0) throw new CliError(`Nothing can be chosen for "${question}".`);
|
|
716
|
-
const
|
|
942
|
+
const choosable = enabled.filter((index) => options[index]?.value !== BACK);
|
|
943
|
+
const fallback = choosable.includes(settings.initial ?? -1) ? settings.initial : choosable[0] ?? enabled[0];
|
|
717
944
|
if (!isInteractive()) return options[fallback].value;
|
|
945
|
+
flushCapture();
|
|
718
946
|
let cursor = fallback;
|
|
719
947
|
let rendered = 0;
|
|
720
948
|
const stdin = process.stdin;
|
|
@@ -734,12 +962,23 @@ async function select(question, options, settings = {}) {
|
|
|
734
962
|
const label = option.disabled ? style.grey(`${number} ${text}`) : active ? brand(`${number} ${text}`) : `${number} ${text}`;
|
|
735
963
|
lines.push(fit(`${pointer} ${label}${hint}`, width - 1));
|
|
736
964
|
});
|
|
737
|
-
lines.push(
|
|
965
|
+
lines.push(
|
|
966
|
+
style.grey(
|
|
967
|
+
settings.back ? " \u2191\u2193 move \xB7 enter choose \xB7 \u2190 back \xB7 esc cancel" : " \u2191\u2193 move \xB7 enter choose \xB7 esc cancel"
|
|
968
|
+
)
|
|
969
|
+
);
|
|
738
970
|
if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
|
|
739
971
|
stdout.write(`${lines.join("\n")}
|
|
740
972
|
`);
|
|
741
973
|
rendered = lines.length;
|
|
742
974
|
};
|
|
975
|
+
const repaint = () => {
|
|
976
|
+
if (!settings.redraw) return;
|
|
977
|
+
clearScreen();
|
|
978
|
+
settings.redraw();
|
|
979
|
+
rendered = 0;
|
|
980
|
+
draw();
|
|
981
|
+
};
|
|
743
982
|
const move = (direction) => {
|
|
744
983
|
const position = enabled.indexOf(cursor);
|
|
745
984
|
const next = (position + direction + enabled.length) % enabled.length;
|
|
@@ -750,15 +989,17 @@ async function select(question, options, settings = {}) {
|
|
|
750
989
|
const restoreCursor = () => {
|
|
751
990
|
stdout.write("\x1B[?25h");
|
|
752
991
|
};
|
|
992
|
+
const stopWatching = settings.redraw ? watchFacts(repaint) : void 0;
|
|
753
993
|
const finish = (result) => {
|
|
754
994
|
stdin.off("keypress", onKey);
|
|
755
995
|
stdin.setRawMode(wasRaw);
|
|
756
996
|
stdin.pause();
|
|
997
|
+
stopWatching?.();
|
|
757
998
|
process.off("exit", restoreCursor);
|
|
758
999
|
if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
|
|
759
1000
|
restoreCursor();
|
|
760
1001
|
if (result) {
|
|
761
|
-
if (!settings.quiet) {
|
|
1002
|
+
if (!settings.quiet && result.value !== BACK) {
|
|
762
1003
|
stdout.write(`${style.green("\u2713")} ${question} ${brand(result.label)}
|
|
763
1004
|
`);
|
|
764
1005
|
}
|
|
@@ -777,6 +1018,9 @@ async function select(question, options, settings = {}) {
|
|
|
777
1018
|
const onKey = (input, key = {}) => {
|
|
778
1019
|
if (key.ctrl && key.name === "c" || key.name === "escape") return finish(null);
|
|
779
1020
|
if (key.name === "return" || key.name === "enter") return choose(cursor);
|
|
1021
|
+
if (settings.back && (key.name === "left" || key.name === "backspace")) {
|
|
1022
|
+
return finish({ value: BACK, label: settings.backLabel ?? "Back" });
|
|
1023
|
+
}
|
|
780
1024
|
if (key.name === "up" || key.name === "k") move(-1);
|
|
781
1025
|
else if (key.name === "down" || key.name === "j" || key.name === "tab") move(1);
|
|
782
1026
|
else if (input && /^[1-9]$/.test(input)) {
|
|
@@ -855,11 +1099,38 @@ var symbol = {
|
|
|
855
1099
|
info: style.cyan("\u203A"),
|
|
856
1100
|
bullet: style.grey("\u2022")
|
|
857
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
|
+
}
|
|
858
1121
|
function out(line = "") {
|
|
1122
|
+
if (captured !== null) {
|
|
1123
|
+
captured.push(line);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
859
1126
|
process.stdout.write(`${line}
|
|
860
1127
|
`);
|
|
861
1128
|
}
|
|
862
1129
|
function err(line = "") {
|
|
1130
|
+
if (captured !== null) {
|
|
1131
|
+
captured.push(line);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
863
1134
|
process.stderr.write(`${line}
|
|
864
1135
|
`);
|
|
865
1136
|
}
|
|
@@ -868,15 +1139,15 @@ function heading(text) {
|
|
|
868
1139
|
out(style.bold(text));
|
|
869
1140
|
out(style.grey("\u2500".repeat(Math.min(text.length, 72))));
|
|
870
1141
|
}
|
|
871
|
-
function table(columns2,
|
|
872
|
-
if (
|
|
1142
|
+
function table(columns2, rows2) {
|
|
1143
|
+
if (rows2.length === 0) {
|
|
873
1144
|
out(style.grey(" (none)"));
|
|
874
1145
|
return;
|
|
875
1146
|
}
|
|
876
1147
|
const widths = columns2.map((column) => {
|
|
877
1148
|
const longest = Math.max(
|
|
878
1149
|
column.header.length,
|
|
879
|
-
...
|
|
1150
|
+
...rows2.map((row) => (row[column.key] ?? "").length)
|
|
880
1151
|
);
|
|
881
1152
|
return Math.min(column.width ?? longest, longest);
|
|
882
1153
|
});
|
|
@@ -892,7 +1163,7 @@ function table(columns2, rows) {
|
|
|
892
1163
|
columns2.map((column) => column.header),
|
|
893
1164
|
style.grey
|
|
894
1165
|
);
|
|
895
|
-
for (const row of
|
|
1166
|
+
for (const row of rows2) {
|
|
896
1167
|
renderRow(
|
|
897
1168
|
columns2.map((column) => row[column.key] ?? ""),
|
|
898
1169
|
(text) => text
|
|
@@ -909,6 +1180,7 @@ function keyValue(label, value) {
|
|
|
909
1180
|
out(` ${style.grey(`${label}:`)} ${value}`);
|
|
910
1181
|
}
|
|
911
1182
|
async function ask(question, fallback = "") {
|
|
1183
|
+
flushCapture();
|
|
912
1184
|
if (!process.stdin.isTTY) return fallback;
|
|
913
1185
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
914
1186
|
try {
|
|
@@ -919,6 +1191,7 @@ async function ask(question, fallback = "") {
|
|
|
919
1191
|
}
|
|
920
1192
|
}
|
|
921
1193
|
async function askSecret(question) {
|
|
1194
|
+
flushCapture();
|
|
922
1195
|
if (!process.stdin.isTTY) {
|
|
923
1196
|
throw new Error(
|
|
924
1197
|
"A secret is required but the terminal is not interactive. Provide it through the documented environment variable instead."
|
|
@@ -961,6 +1234,7 @@ async function askSecret(question) {
|
|
|
961
1234
|
});
|
|
962
1235
|
}
|
|
963
1236
|
async function confirm(question, options = {}) {
|
|
1237
|
+
flushCapture();
|
|
964
1238
|
if (options.yes) {
|
|
965
1239
|
out(`${question} ${style.grey("[y/N]")} ${style.amber("y (--yes)")}`);
|
|
966
1240
|
return true;
|
|
@@ -983,6 +1257,12 @@ async function confirm(question, options = {}) {
|
|
|
983
1257
|
return ["y", "yes"].includes(answer.toLowerCase());
|
|
984
1258
|
}
|
|
985
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
|
+
}
|
|
986
1266
|
if (!process.stdout.isTTY) {
|
|
987
1267
|
out(`${symbol.info} ${label}`);
|
|
988
1268
|
return {
|
|
@@ -1106,6 +1386,7 @@ async function resolveContext(flags) {
|
|
|
1106
1386
|
};
|
|
1107
1387
|
}
|
|
1108
1388
|
async function login(controlPlaneUrl, options = {}) {
|
|
1389
|
+
flushCapture();
|
|
1109
1390
|
await assertControlPlane(controlPlaneUrl);
|
|
1110
1391
|
const ciCredentials = Boolean(process.env.VEEFORE_EMAIL && process.env.VEEFORE_PASSWORD);
|
|
1111
1392
|
if (options.method === "password" || options.method === void 0 && ciCredentials) {
|
|
@@ -1682,11 +1963,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
|
|
|
1682
1963
|
out(` ${style.red("This action is irreversible for this identity.")}`);
|
|
1683
1964
|
out();
|
|
1684
1965
|
}
|
|
1685
|
-
async function lifecycle(args, flags,
|
|
1966
|
+
async function lifecycle(args, flags, path13, resultingStatus) {
|
|
1686
1967
|
const context = await resolveContext(flags);
|
|
1687
1968
|
const agentId = positional(args) ?? context.config.agentId;
|
|
1688
1969
|
const agent = await context.api.get(`/v1/agents/${agentId}`);
|
|
1689
|
-
const reasonRequired =
|
|
1970
|
+
const reasonRequired = path13 === "suspend";
|
|
1690
1971
|
const reason = valueFor(args, "--reason");
|
|
1691
1972
|
if (reasonRequired && (!reason || reason.trim().length < 10)) {
|
|
1692
1973
|
throw new CliError(
|
|
@@ -1696,24 +1977,24 @@ async function lifecycle(args, flags, path12, resultingStatus) {
|
|
|
1696
1977
|
}
|
|
1697
1978
|
out();
|
|
1698
1979
|
out(
|
|
1699
|
-
`${symbol.info} ${
|
|
1980
|
+
`${symbol.info} ${path13} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
|
|
1700
1981
|
);
|
|
1701
|
-
if (
|
|
1982
|
+
if (path13 !== "resume") {
|
|
1702
1983
|
const confirmed = await confirm("Continue?", { yes: flags.yes });
|
|
1703
1984
|
if (!confirmed) {
|
|
1704
1985
|
out(style.grey("Cancelled."));
|
|
1705
1986
|
return 1;
|
|
1706
1987
|
}
|
|
1707
1988
|
}
|
|
1708
|
-
const actionSpinner = spinner(`Applying ${
|
|
1989
|
+
const actionSpinner = spinner(`Applying ${path13}`);
|
|
1709
1990
|
try {
|
|
1710
1991
|
await withStepUp(
|
|
1711
1992
|
context.api,
|
|
1712
|
-
() => context.api.post(`/v1/agents/${agentId}/${
|
|
1993
|
+
() => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
|
|
1713
1994
|
);
|
|
1714
1995
|
actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
|
|
1715
1996
|
} catch (error) {
|
|
1716
|
-
actionSpinner.fail(`Could not ${
|
|
1997
|
+
actionSpinner.fail(`Could not ${path13} the agent`);
|
|
1717
1998
|
throw error;
|
|
1718
1999
|
}
|
|
1719
2000
|
out();
|
|
@@ -2017,13 +2298,13 @@ async function exportEvents(args, flags) {
|
|
|
2017
2298
|
}
|
|
2018
2299
|
|
|
2019
2300
|
// src/commands/doctor.ts
|
|
2020
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
2021
|
-
import
|
|
2301
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
2302
|
+
import path4 from "node:path";
|
|
2022
2303
|
|
|
2023
2304
|
// src/project-checks.ts
|
|
2024
2305
|
import { spawnSync } from "node:child_process";
|
|
2025
2306
|
import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
2026
|
-
import
|
|
2307
|
+
import path3 from "node:path";
|
|
2027
2308
|
var MIN_APPROVAL_WAIT_SDK = "1.1.0";
|
|
2028
2309
|
function versionLessThan(a, b) {
|
|
2029
2310
|
if (!a) return true;
|
|
@@ -2040,19 +2321,19 @@ function versionLessThan(a, b) {
|
|
|
2040
2321
|
function installedSdks(root, options = {}) {
|
|
2041
2322
|
const found = [];
|
|
2042
2323
|
const seen = /* @__PURE__ */ new Set();
|
|
2043
|
-
const top =
|
|
2324
|
+
const top = path3.resolve(root);
|
|
2044
2325
|
const candidates = /* @__PURE__ */ new Set([top]);
|
|
2045
2326
|
for (const dir of options.dirs ?? []) {
|
|
2046
|
-
let current =
|
|
2327
|
+
let current = path3.resolve(top, dir);
|
|
2047
2328
|
while (current.startsWith(top)) {
|
|
2048
2329
|
candidates.add(current);
|
|
2049
|
-
const parent =
|
|
2330
|
+
const parent = path3.dirname(current);
|
|
2050
2331
|
if (parent === current) break;
|
|
2051
2332
|
current = parent;
|
|
2052
2333
|
}
|
|
2053
2334
|
}
|
|
2054
2335
|
for (const dir of candidates) {
|
|
2055
|
-
const manifest =
|
|
2336
|
+
const manifest = path3.join(dir, "node_modules", "@veefore", "sdk", "package.json");
|
|
2056
2337
|
if (!existsSync2(manifest)) continue;
|
|
2057
2338
|
let real = manifest;
|
|
2058
2339
|
try {
|
|
@@ -2069,7 +2350,7 @@ function installedSdks(root, options = {}) {
|
|
|
2069
2350
|
}
|
|
2070
2351
|
found.push({
|
|
2071
2352
|
language: "js",
|
|
2072
|
-
location:
|
|
2353
|
+
location: path3.relative(top, path3.dirname(manifest)) || ".",
|
|
2073
2354
|
version,
|
|
2074
2355
|
waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
|
|
2075
2356
|
});
|
|
@@ -2101,7 +2382,7 @@ function upgradeCommand(root, language) {
|
|
|
2101
2382
|
}
|
|
2102
2383
|
function configuredApprovalMode(root) {
|
|
2103
2384
|
for (const name of [".env.local", ".env"]) {
|
|
2104
|
-
const file =
|
|
2385
|
+
const file = path3.join(root, name);
|
|
2105
2386
|
if (!existsSync2(file)) continue;
|
|
2106
2387
|
const match = /^\s*(?:export\s+)?VEEFORE_APPROVAL_MODE\s*=\s*["']?(\w+)["']?/m.exec(
|
|
2107
2388
|
readFileSync2(file, "utf8")
|
|
@@ -2111,7 +2392,7 @@ function configuredApprovalMode(root) {
|
|
|
2111
2392
|
return null;
|
|
2112
2393
|
}
|
|
2113
2394
|
function sdkInstalled(root, language) {
|
|
2114
|
-
if (language === "js") return existsSync2(
|
|
2395
|
+
if (language === "js") return existsSync2(path3.join(root, "node_modules", "@veefore", "sdk"));
|
|
2115
2396
|
const python = projectPython(root);
|
|
2116
2397
|
if (!python) return false;
|
|
2117
2398
|
return spawnSync(python, ["-c", "import veefore"], { cwd: root, stdio: "ignore", timeout: 2e4 }).status === 0;
|
|
@@ -2120,18 +2401,18 @@ function typecheck(root, files) {
|
|
|
2120
2401
|
const name = "TypeScript typecheck";
|
|
2121
2402
|
const projects = files ? [
|
|
2122
2403
|
...new Set(
|
|
2123
|
-
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)
|
|
2124
2405
|
)
|
|
2125
|
-
] : existsSync2(
|
|
2406
|
+
] : existsSync2(path3.join(root, "tsconfig.json")) ? [root] : [];
|
|
2126
2407
|
if (projects.length === 0) return { name, ran: false, ok: true, detail: "no tsconfig.json" };
|
|
2127
2408
|
const binary = process.platform === "win32" ? "tsc.cmd" : "tsc";
|
|
2128
2409
|
for (const project of projects) {
|
|
2129
|
-
const bin = nearestUp(root, project,
|
|
2410
|
+
const bin = nearestUp(root, project, path3.join("node_modules", ".bin", binary));
|
|
2130
2411
|
if (!bin) {
|
|
2131
2412
|
return { name, ran: false, ok: true, detail: "TypeScript is not installed in this project" };
|
|
2132
2413
|
}
|
|
2133
2414
|
const result = spawnSync(
|
|
2134
|
-
|
|
2415
|
+
path3.join(bin, "node_modules", ".bin", binary),
|
|
2135
2416
|
["--noEmit", "-p", "."],
|
|
2136
2417
|
{
|
|
2137
2418
|
cwd: project,
|
|
@@ -2141,7 +2422,7 @@ function typecheck(root, files) {
|
|
|
2141
2422
|
);
|
|
2142
2423
|
if (result.status !== 0) {
|
|
2143
2424
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2144
|
-
const where =
|
|
2425
|
+
const where = path3.relative(root, project) || ".";
|
|
2145
2426
|
return {
|
|
2146
2427
|
name,
|
|
2147
2428
|
ran: true,
|
|
@@ -2154,12 +2435,12 @@ ${output.split("\n").slice(0, 12).join("\n")}`
|
|
|
2154
2435
|
return { name, ran: true, ok: true, detail: "" };
|
|
2155
2436
|
}
|
|
2156
2437
|
function nearestUp(root, start, entry) {
|
|
2157
|
-
const top =
|
|
2158
|
-
let dir =
|
|
2438
|
+
const top = path3.resolve(root);
|
|
2439
|
+
let dir = path3.resolve(start);
|
|
2159
2440
|
for (; ; ) {
|
|
2160
|
-
if (existsSync2(
|
|
2441
|
+
if (existsSync2(path3.join(dir, entry))) return dir;
|
|
2161
2442
|
if (dir === top) return null;
|
|
2162
|
-
const parent =
|
|
2443
|
+
const parent = path3.dirname(dir);
|
|
2163
2444
|
if (parent === dir || !parent.startsWith(top)) return null;
|
|
2164
2445
|
dir = parent;
|
|
2165
2446
|
}
|
|
@@ -2213,20 +2494,20 @@ function pythonSyntax(root, files) {
|
|
|
2213
2494
|
}
|
|
2214
2495
|
function projectPython(root) {
|
|
2215
2496
|
for (const candidate of [".venv/bin/python", "venv/bin/python", ".venv/Scripts/python.exe"]) {
|
|
2216
|
-
if (existsSync2(
|
|
2497
|
+
if (existsSync2(path3.join(root, candidate))) return path3.join(root, candidate);
|
|
2217
2498
|
}
|
|
2218
2499
|
const probe = spawnSync("python3", ["--version"], { stdio: "ignore" });
|
|
2219
2500
|
return probe.status === 0 ? "python3" : null;
|
|
2220
2501
|
}
|
|
2221
2502
|
function installCommand(root, language) {
|
|
2222
2503
|
if (language === "py") {
|
|
2223
|
-
if (existsSync2(
|
|
2224
|
-
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";
|
|
2225
2506
|
return "pip install veefore-py";
|
|
2226
2507
|
}
|
|
2227
|
-
if (existsSync2(
|
|
2228
|
-
if (existsSync2(
|
|
2229
|
-
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"))) {
|
|
2230
2511
|
return "bun add @veefore/sdk";
|
|
2231
2512
|
}
|
|
2232
2513
|
return "npm install @veefore/sdk";
|
|
@@ -2346,8 +2627,8 @@ async function checkTransportSecurity(config, add) {
|
|
|
2346
2627
|
}
|
|
2347
2628
|
function checkLocalSecrets(config, add) {
|
|
2348
2629
|
const configFile = findProjectConfig();
|
|
2349
|
-
const projectDir = configFile ?
|
|
2350
|
-
const envFile =
|
|
2630
|
+
const projectDir = configFile ? path4.dirname(configFile) : process.cwd();
|
|
2631
|
+
const envFile = path4.join(projectDir, ".env.local");
|
|
2351
2632
|
if (!existsSync3(envFile)) {
|
|
2352
2633
|
add({
|
|
2353
2634
|
check: "Local agent credentials",
|
|
@@ -2357,7 +2638,7 @@ function checkLocalSecrets(config, add) {
|
|
|
2357
2638
|
});
|
|
2358
2639
|
return;
|
|
2359
2640
|
}
|
|
2360
|
-
const mode =
|
|
2641
|
+
const mode = statSync2(envFile).mode & 511;
|
|
2361
2642
|
if ((mode & 63) !== 0) {
|
|
2362
2643
|
add({
|
|
2363
2644
|
check: "Local agent credentials",
|
|
@@ -2372,7 +2653,7 @@ function checkLocalSecrets(config, add) {
|
|
|
2372
2653
|
message: `.env.local is mode ${mode.toString(8)} (owner only).`
|
|
2373
2654
|
});
|
|
2374
2655
|
}
|
|
2375
|
-
const gitignore =
|
|
2656
|
+
const gitignore = path4.join(projectDir, ".gitignore");
|
|
2376
2657
|
const ignored = existsSync3(gitignore) && readFileSync3(gitignore, "utf8").split(/\r?\n/).some((line) => line.trim() === ".env.local" || line.trim() === "*.local");
|
|
2377
2658
|
add({
|
|
2378
2659
|
check: "Secrets excluded from version control",
|
|
@@ -2700,8 +2981,8 @@ async function checkCredentialRotation(api, add) {
|
|
|
2700
2981
|
}
|
|
2701
2982
|
async function checkDependencies(add, dir) {
|
|
2702
2983
|
const configFile = findProjectConfig();
|
|
2703
|
-
const projectDir = dir ?? (configFile ?
|
|
2704
|
-
const manifest =
|
|
2984
|
+
const projectDir = dir ?? (configFile ? path4.dirname(configFile) : process.cwd());
|
|
2985
|
+
const manifest = path4.join(projectDir, "package.json");
|
|
2705
2986
|
if (!existsSync3(manifest)) {
|
|
2706
2987
|
add({
|
|
2707
2988
|
check: "SDK dependency",
|
|
@@ -2743,9 +3024,9 @@ async function checkDependencies(add, dir) {
|
|
|
2743
3024
|
}
|
|
2744
3025
|
function checkApprovalWaiting(add, projectDir) {
|
|
2745
3026
|
const configFile = findProjectConfig();
|
|
2746
|
-
const dir = projectDir ?? (configFile ?
|
|
3027
|
+
const dir = projectDir ?? (configFile ? path4.dirname(configFile) : process.cwd());
|
|
2747
3028
|
const python = ["pyproject.toml", "requirements.txt", "setup.py"].some(
|
|
2748
|
-
(name) => existsSync3(
|
|
3029
|
+
(name) => existsSync3(path4.join(dir, name))
|
|
2749
3030
|
);
|
|
2750
3031
|
const sdks = installedSdks(dir, { python });
|
|
2751
3032
|
const old = sdks.filter((sdk) => !sdk.waitsForApproval);
|
|
@@ -3037,11 +3318,11 @@ function isLoosening(from, to) {
|
|
|
3037
3318
|
|
|
3038
3319
|
// src/commands/init.ts
|
|
3039
3320
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3040
|
-
import
|
|
3321
|
+
import path12 from "node:path";
|
|
3041
3322
|
|
|
3042
3323
|
// src/integrate.ts
|
|
3043
3324
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3044
|
-
import
|
|
3325
|
+
import path9 from "node:path";
|
|
3045
3326
|
|
|
3046
3327
|
// ../types/src/permissions.ts
|
|
3047
3328
|
var PERMISSIONS = [
|
|
@@ -4311,21 +4592,21 @@ import {
|
|
|
4311
4592
|
readdirSync,
|
|
4312
4593
|
renameSync as renameSync2,
|
|
4313
4594
|
rmSync as rmSync2,
|
|
4314
|
-
statSync as
|
|
4595
|
+
statSync as statSync3,
|
|
4315
4596
|
writeFileSync as writeFileSync3
|
|
4316
4597
|
} from "node:fs";
|
|
4317
|
-
import
|
|
4318
|
-
var BACKUP_DIR =
|
|
4598
|
+
import path5 from "node:path";
|
|
4599
|
+
var BACKUP_DIR = path5.join(".veefore", "backups");
|
|
4319
4600
|
function sha256(text) {
|
|
4320
4601
|
return createHash2("sha256").update(text).digest("hex");
|
|
4321
4602
|
}
|
|
4322
4603
|
function createBackup(root, changes) {
|
|
4323
4604
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4324
4605
|
const id = createdAt.replace(/[:.]/g, "-");
|
|
4325
|
-
const dir =
|
|
4606
|
+
const dir = path5.join(root, BACKUP_DIR, id);
|
|
4326
4607
|
for (const change of changes) {
|
|
4327
|
-
const target =
|
|
4328
|
-
mkdirSync2(
|
|
4608
|
+
const target = path5.join(dir, "files", change.file);
|
|
4609
|
+
mkdirSync2(path5.dirname(target), { recursive: true });
|
|
4329
4610
|
writeFileSync3(target, change.before, { mode: 384 });
|
|
4330
4611
|
}
|
|
4331
4612
|
const files = changes.map((change) => ({
|
|
@@ -4334,7 +4615,7 @@ function createBackup(root, changes) {
|
|
|
4334
4615
|
afterSha256: sha256(change.after)
|
|
4335
4616
|
}));
|
|
4336
4617
|
writeFileSync3(
|
|
4337
|
-
|
|
4618
|
+
path5.join(dir, "manifest.json"),
|
|
4338
4619
|
`${JSON.stringify({ version: 1, createdAt, files }, null, 2)}
|
|
4339
4620
|
`,
|
|
4340
4621
|
{ mode: 384 }
|
|
@@ -4342,28 +4623,28 @@ function createBackup(root, changes) {
|
|
|
4342
4623
|
return { id, dir, createdAt, files };
|
|
4343
4624
|
}
|
|
4344
4625
|
function writeFilePreservingMode(file, content) {
|
|
4345
|
-
const mode = existsSync4(file) ?
|
|
4626
|
+
const mode = existsSync4(file) ? statSync3(file).mode & 511 : 420;
|
|
4346
4627
|
const temp = `${file}.veefore-tmp-${process.pid}`;
|
|
4347
4628
|
writeFileSync3(temp, content, { mode });
|
|
4348
4629
|
renameSync2(temp, file);
|
|
4349
4630
|
}
|
|
4350
4631
|
function latestBackup(root) {
|
|
4351
|
-
const base2 =
|
|
4632
|
+
const base2 = path5.join(root, BACKUP_DIR);
|
|
4352
4633
|
if (!existsSync4(base2)) return null;
|
|
4353
|
-
const ids = readdirSync(base2).filter((entry) => existsSync4(
|
|
4634
|
+
const ids = readdirSync(base2).filter((entry) => existsSync4(path5.join(base2, entry, "manifest.json"))).sort();
|
|
4354
4635
|
const id = ids[ids.length - 1];
|
|
4355
4636
|
if (!id) return null;
|
|
4356
|
-
const dir =
|
|
4357
|
-
const manifest = JSON.parse(readFileSync4(
|
|
4637
|
+
const dir = path5.join(base2, id);
|
|
4638
|
+
const manifest = JSON.parse(readFileSync4(path5.join(dir, "manifest.json"), "utf8"));
|
|
4358
4639
|
return { id, dir, createdAt: manifest.createdAt, files: manifest.files };
|
|
4359
4640
|
}
|
|
4360
4641
|
function restoreBackup(root, backup, options = {}) {
|
|
4361
4642
|
const restored = [];
|
|
4362
4643
|
const conflicts = [];
|
|
4363
4644
|
for (const entry of backup.files) {
|
|
4364
|
-
const target =
|
|
4645
|
+
const target = path5.join(root, entry.file);
|
|
4365
4646
|
const current = existsSync4(target) ? readFileSync4(target, "utf8") : null;
|
|
4366
|
-
const original = readFileSync4(
|
|
4647
|
+
const original = readFileSync4(path5.join(backup.dir, "files", entry.file), "utf8");
|
|
4367
4648
|
if (current !== null && sha256(current) === entry.beforeSha256) {
|
|
4368
4649
|
restored.push(entry.file);
|
|
4369
4650
|
continue;
|
|
@@ -4383,15 +4664,15 @@ function restoreBackup(root, backup, options = {}) {
|
|
|
4383
4664
|
|
|
4384
4665
|
// src/rewrite.ts
|
|
4385
4666
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4386
|
-
import
|
|
4667
|
+
import path8 from "node:path";
|
|
4387
4668
|
|
|
4388
4669
|
// src/scan.ts
|
|
4389
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as
|
|
4390
|
-
import
|
|
4670
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
4671
|
+
import path7 from "node:path";
|
|
4391
4672
|
|
|
4392
4673
|
// src/framework.ts
|
|
4393
4674
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
4394
|
-
import
|
|
4675
|
+
import path6 from "node:path";
|
|
4395
4676
|
var SIGNATURES = [
|
|
4396
4677
|
{
|
|
4397
4678
|
key: "langchain",
|
|
@@ -4473,8 +4754,8 @@ function detectFramework(dir = process.cwd()) {
|
|
|
4473
4754
|
};
|
|
4474
4755
|
}
|
|
4475
4756
|
}
|
|
4476
|
-
if (nodeDeps.size > 0 || existsSync5(
|
|
4477
|
-
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");
|
|
4478
4759
|
return {
|
|
4479
4760
|
key: "custom",
|
|
4480
4761
|
name: "Custom agent",
|
|
@@ -4496,7 +4777,7 @@ function normalize(name) {
|
|
|
4496
4777
|
return name.toLowerCase().replace(/_/g, "-");
|
|
4497
4778
|
}
|
|
4498
4779
|
function readPackageJsonDependencies(dir) {
|
|
4499
|
-
const file =
|
|
4780
|
+
const file = path6.join(dir, "package.json");
|
|
4500
4781
|
if (!existsSync5(file)) return /* @__PURE__ */ new Set();
|
|
4501
4782
|
try {
|
|
4502
4783
|
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
@@ -4513,12 +4794,12 @@ function readPackageJsonDependencies(dir) {
|
|
|
4513
4794
|
}
|
|
4514
4795
|
function hasPythonProject(dir) {
|
|
4515
4796
|
return ["pyproject.toml", "requirements.txt", "Pipfile", "setup.py"].some(
|
|
4516
|
-
(file) => existsSync5(
|
|
4797
|
+
(file) => existsSync5(path6.join(dir, file))
|
|
4517
4798
|
);
|
|
4518
4799
|
}
|
|
4519
4800
|
function readPythonDependencies(dir) {
|
|
4520
4801
|
const found = /* @__PURE__ */ new Set();
|
|
4521
|
-
const requirements =
|
|
4802
|
+
const requirements = path6.join(dir, "requirements.txt");
|
|
4522
4803
|
if (existsSync5(requirements)) {
|
|
4523
4804
|
for (const line of readFileSync5(requirements, "utf8").split(/\r?\n/)) {
|
|
4524
4805
|
const trimmed = line.trim();
|
|
@@ -4527,7 +4808,7 @@ function readPythonDependencies(dir) {
|
|
|
4527
4808
|
if (name) found.add(normalize(name));
|
|
4528
4809
|
}
|
|
4529
4810
|
}
|
|
4530
|
-
const pyproject =
|
|
4811
|
+
const pyproject = path6.join(dir, "pyproject.toml");
|
|
4531
4812
|
if (existsSync5(pyproject)) {
|
|
4532
4813
|
const text = readFileSync5(pyproject, "utf8");
|
|
4533
4814
|
for (const match of text.matchAll(
|
|
@@ -4559,7 +4840,7 @@ function findEntryPoints(dir = process.cwd(), limit = 5) {
|
|
|
4559
4840
|
"app.py",
|
|
4560
4841
|
"src/main.py"
|
|
4561
4842
|
];
|
|
4562
|
-
const found = candidates.filter((file) => existsSync5(
|
|
4843
|
+
const found = candidates.filter((file) => existsSync5(path6.join(dir, file)));
|
|
4563
4844
|
if (found.length > 0) return found.slice(0, limit);
|
|
4564
4845
|
try {
|
|
4565
4846
|
return readdirSync2(dir).filter((file) => /\.(ts|js|py)$/.test(file)).slice(0, limit);
|
|
@@ -4663,7 +4944,7 @@ function scanProject(root = process.cwd()) {
|
|
|
4663
4944
|
for (const relative of files) {
|
|
4664
4945
|
let text;
|
|
4665
4946
|
try {
|
|
4666
|
-
text = readFileSync6(
|
|
4947
|
+
text = readFileSync6(path7.join(root, relative), "utf8");
|
|
4667
4948
|
} catch {
|
|
4668
4949
|
continue;
|
|
4669
4950
|
}
|
|
@@ -5162,10 +5443,10 @@ function walk(root, dir, out2) {
|
|
|
5162
5443
|
if (entry.startsWith(".") && entry !== ".") {
|
|
5163
5444
|
continue;
|
|
5164
5445
|
}
|
|
5165
|
-
const full =
|
|
5446
|
+
const full = path7.join(dir, entry);
|
|
5166
5447
|
let stats;
|
|
5167
5448
|
try {
|
|
5168
|
-
stats =
|
|
5449
|
+
stats = statSync4(full);
|
|
5169
5450
|
} catch {
|
|
5170
5451
|
continue;
|
|
5171
5452
|
}
|
|
@@ -5175,14 +5456,14 @@ function walk(root, dir, out2) {
|
|
|
5175
5456
|
continue;
|
|
5176
5457
|
}
|
|
5177
5458
|
if (!stats.isFile() || stats.size > MAX_FILE_BYTES) continue;
|
|
5178
|
-
if (!SOURCE_EXTENSIONS.has(
|
|
5459
|
+
if (!SOURCE_EXTENSIONS.has(path7.extname(entry))) continue;
|
|
5179
5460
|
if (/\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$/.test(entry)) continue;
|
|
5180
|
-
out2.push(
|
|
5461
|
+
out2.push(path7.relative(root, full));
|
|
5181
5462
|
}
|
|
5182
5463
|
return false;
|
|
5183
5464
|
}
|
|
5184
5465
|
function readProjectName(root) {
|
|
5185
|
-
const pkg =
|
|
5466
|
+
const pkg = path7.join(root, "package.json");
|
|
5186
5467
|
if (existsSync6(pkg)) {
|
|
5187
5468
|
try {
|
|
5188
5469
|
const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
|
|
@@ -5190,12 +5471,12 @@ function readProjectName(root) {
|
|
|
5190
5471
|
} catch {
|
|
5191
5472
|
}
|
|
5192
5473
|
}
|
|
5193
|
-
const pyproject =
|
|
5474
|
+
const pyproject = path7.join(root, "pyproject.toml");
|
|
5194
5475
|
if (existsSync6(pyproject)) {
|
|
5195
5476
|
const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync6(pyproject, "utf8"))?.[1];
|
|
5196
5477
|
if (name) return name.trim();
|
|
5197
5478
|
}
|
|
5198
|
-
return
|
|
5479
|
+
return path7.basename(root) || null;
|
|
5199
5480
|
}
|
|
5200
5481
|
function stringField(body, field) {
|
|
5201
5482
|
const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
|
|
@@ -5250,7 +5531,7 @@ function planRewrite(root, files, toolKeys) {
|
|
|
5250
5531
|
for (const file of files) {
|
|
5251
5532
|
let before;
|
|
5252
5533
|
try {
|
|
5253
|
-
before = readFileSync7(
|
|
5534
|
+
before = readFileSync7(path8.join(root, file), "utf8");
|
|
5254
5535
|
} catch {
|
|
5255
5536
|
continue;
|
|
5256
5537
|
}
|
|
@@ -6240,13 +6521,13 @@ async function integrate(root, toolKeys, options) {
|
|
|
6240
6521
|
toWrite.map((file) => file.file)
|
|
6241
6522
|
) : null;
|
|
6242
6523
|
for (const file of toWrite) {
|
|
6243
|
-
if (readFileSync8(
|
|
6524
|
+
if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
|
|
6244
6525
|
out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
|
|
6245
6526
|
return { status: "declined", plan, checks: [], backupId: null, manual };
|
|
6246
6527
|
}
|
|
6247
6528
|
}
|
|
6248
6529
|
const backup = createBackup(root, toWrite);
|
|
6249
|
-
for (const file of toWrite) writeFilePreservingMode(
|
|
6530
|
+
for (const file of toWrite) writeFilePreservingMode(path9.join(root, file.file), file.after);
|
|
6250
6531
|
out(
|
|
6251
6532
|
`${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
|
|
6252
6533
|
);
|
|
@@ -6317,7 +6598,7 @@ async function integrate(root, toolKeys, options) {
|
|
|
6317
6598
|
function reportSdkReadiness(root, files) {
|
|
6318
6599
|
const problems = [];
|
|
6319
6600
|
if (files.length === 0) return problems;
|
|
6320
|
-
const dirs = [...new Set(files.map((file) =>
|
|
6601
|
+
const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
|
|
6321
6602
|
const python = files.some((file) => file.endsWith(".py"));
|
|
6322
6603
|
const sdks = installedSdks(root, { dirs, python });
|
|
6323
6604
|
for (const sdk of sdks) {
|
|
@@ -6394,7 +6675,7 @@ async function removeGuards(root, options) {
|
|
|
6394
6675
|
const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
|
|
6395
6676
|
const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
|
|
6396
6677
|
const backup = createBackup(root, toWrite);
|
|
6397
|
-
for (const entry of toWrite) writeFilePreservingMode(
|
|
6678
|
+
for (const entry of toWrite) writeFilePreservingMode(path9.join(root, entry.file), entry.after);
|
|
6398
6679
|
const checks = [];
|
|
6399
6680
|
const py = changed.filter((file) => file.endsWith(".py"));
|
|
6400
6681
|
if (py.length > 0) checks.push(pythonSyntax(root, py));
|
|
@@ -6418,7 +6699,7 @@ function currentText(root, plan, file) {
|
|
|
6418
6699
|
const entry = plan.files.find((candidate) => candidate.file === file);
|
|
6419
6700
|
if (entry) return entry.after;
|
|
6420
6701
|
try {
|
|
6421
|
-
return readFileSync8(
|
|
6702
|
+
return readFileSync8(path9.join(root, file), "utf8");
|
|
6422
6703
|
} catch {
|
|
6423
6704
|
return null;
|
|
6424
6705
|
}
|
|
@@ -6443,7 +6724,7 @@ function applyDispatch(root, plan, file, after, line, items, via) {
|
|
|
6443
6724
|
function readSources(root) {
|
|
6444
6725
|
return listSourceFiles(root).flatMap((file) => {
|
|
6445
6726
|
try {
|
|
6446
|
-
return [{ file, text: readFileSync8(
|
|
6727
|
+
return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
|
|
6447
6728
|
} catch {
|
|
6448
6729
|
return [];
|
|
6449
6730
|
}
|
|
@@ -6565,7 +6846,7 @@ function printPlan(plan) {
|
|
|
6565
6846
|
|
|
6566
6847
|
// src/commands/remove.ts
|
|
6567
6848
|
import { rmSync as rmSync3 } from "node:fs";
|
|
6568
|
-
import
|
|
6849
|
+
import path10 from "node:path";
|
|
6569
6850
|
|
|
6570
6851
|
// src/discover.ts
|
|
6571
6852
|
var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
|
|
@@ -6679,7 +6960,7 @@ async function removeCommand(args, flags, known) {
|
|
|
6679
6960
|
if (!file || !config) {
|
|
6680
6961
|
throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
|
|
6681
6962
|
}
|
|
6682
|
-
const root =
|
|
6963
|
+
const root = path10.dirname(file);
|
|
6683
6964
|
const state = known ?? await agentState(config);
|
|
6684
6965
|
heading("Remove Veefore from this project");
|
|
6685
6966
|
details([
|
|
@@ -7211,6 +7492,48 @@ async function resolveAuthorizedTools(context, filter) {
|
|
|
7211
7492
|
return match;
|
|
7212
7493
|
}
|
|
7213
7494
|
|
|
7495
|
+
// src/panel.ts
|
|
7496
|
+
import { homedir as homedir2 } from "node:os";
|
|
7497
|
+
import path11 from "node:path";
|
|
7498
|
+
|
|
7499
|
+
// src/version.ts
|
|
7500
|
+
var CLI_VERSION = "1.4.0";
|
|
7501
|
+
|
|
7502
|
+
// src/panel.ts
|
|
7503
|
+
function welcomeSource(flags = {}) {
|
|
7504
|
+
return () => {
|
|
7505
|
+
const project = readProjectConfig();
|
|
7506
|
+
const controlPlaneUrl = project?.controlPlaneUrl ?? resolveControlPlaneUrl(flags.controlPlaneUrl);
|
|
7507
|
+
const session = loadSession(controlPlaneUrl);
|
|
7508
|
+
return {
|
|
7509
|
+
version: CLI_VERSION,
|
|
7510
|
+
facts: [
|
|
7511
|
+
["Folder", shorten(tilde(process.cwd()), 34)],
|
|
7512
|
+
["Signed in", session ? session.email : style.grey("not yet")],
|
|
7513
|
+
[
|
|
7514
|
+
"Project",
|
|
7515
|
+
project ? `${project.agentName} \xB7 ${project.environmentName}` : style.grey("not set up here")
|
|
7516
|
+
],
|
|
7517
|
+
["Veefore", hostOf(controlPlaneUrl)]
|
|
7518
|
+
]
|
|
7519
|
+
};
|
|
7520
|
+
};
|
|
7521
|
+
}
|
|
7522
|
+
function tilde(dir) {
|
|
7523
|
+
const home = homedir2();
|
|
7524
|
+
return dir === home || dir.startsWith(`${home}${path11.sep}`) ? `~${dir.slice(home.length)}` : dir;
|
|
7525
|
+
}
|
|
7526
|
+
function hostOf(url) {
|
|
7527
|
+
try {
|
|
7528
|
+
return new URL(url).host;
|
|
7529
|
+
} catch {
|
|
7530
|
+
return url;
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
function shorten(text, width) {
|
|
7534
|
+
return text.length <= width ? text : `\u2026${text.slice(text.length - width + 1)}`;
|
|
7535
|
+
}
|
|
7536
|
+
|
|
7214
7537
|
// src/report.ts
|
|
7215
7538
|
function reportError(error, flags) {
|
|
7216
7539
|
if (error instanceof CliError) {
|
|
@@ -7538,1473 +7861,1627 @@ function sourceLabel(source) {
|
|
|
7538
7861
|
}
|
|
7539
7862
|
}
|
|
7540
7863
|
|
|
7541
|
-
// src/commands/
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
if (existing && !options.force && process.stdin.isTTY && process.stdout.isTTY && !flags.json) {
|
|
7547
|
-
return projectHome(existing, flags);
|
|
7548
|
-
}
|
|
7549
|
-
if (existing && !options.force) {
|
|
7550
|
-
out();
|
|
7551
|
-
out(`${symbol.warn} This project is already connected to Veefore.`);
|
|
7552
|
-
details([
|
|
7553
|
-
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
7554
|
-
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
7555
|
-
["Control plane", existing.controlPlaneUrl]
|
|
7556
|
-
]);
|
|
7557
|
-
out();
|
|
7558
|
-
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));
|
|
7559
7869
|
return 0;
|
|
7560
7870
|
}
|
|
7561
|
-
heading("Veefore
|
|
7562
|
-
const framework = detectFramework();
|
|
7871
|
+
heading("Veefore scan");
|
|
7563
7872
|
out(
|
|
7564
|
-
`${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})`)}`
|
|
7565
7874
|
);
|
|
7566
|
-
if (framework.key === "unknown") {
|
|
7567
|
-
out(
|
|
7568
|
-
style.grey(
|
|
7569
|
-
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
7570
|
-
)
|
|
7571
|
-
);
|
|
7572
|
-
}
|
|
7573
|
-
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
7574
|
-
const controlPlaneUrl = found.controlPlaneUrl;
|
|
7575
|
-
if (found.source !== "entered") {
|
|
7576
|
-
out(
|
|
7577
|
-
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
7578
|
-
);
|
|
7579
|
-
}
|
|
7580
|
-
let session = loadSession(controlPlaneUrl);
|
|
7581
|
-
if (!session) {
|
|
7582
|
-
await login(controlPlaneUrl);
|
|
7583
|
-
session = loadSession(controlPlaneUrl);
|
|
7584
|
-
}
|
|
7585
|
-
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
7586
|
-
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
7587
|
-
const me = await api.get("/v1/auth/session");
|
|
7588
|
-
const organization = await selectOrganization(api, me, options, flags);
|
|
7589
|
-
const workspace = await selectWorkspace(api, me, options, flags);
|
|
7590
|
-
const environment = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
7591
|
-
if (environment.type === "PRODUCTION" && !options.force) {
|
|
7592
|
-
out();
|
|
7593
|
-
out(
|
|
7594
|
-
`${symbol.warn} ${style.amber("You selected a PRODUCTION environment.")} A new agent here starts in ${style.bold("OBSERVE")} mode and performs no enforcement until you switch it.`
|
|
7595
|
-
);
|
|
7596
|
-
const proceed2 = await confirm("Continue with the production environment?", { yes: flags.yes });
|
|
7597
|
-
if (!proceed2) {
|
|
7598
|
-
out(style.grey("Cancelled. Re-run and choose a development environment."));
|
|
7599
|
-
return 1;
|
|
7600
|
-
}
|
|
7601
|
-
}
|
|
7602
|
-
heading("Looking at your project");
|
|
7603
|
-
const scan = scanProject(process.cwd());
|
|
7604
7875
|
printScan(scan);
|
|
7605
|
-
const plan = buildPlan(scan, {
|
|
7606
|
-
agentName: options.agentName,
|
|
7607
|
-
extraCapabilities: options.capabilities
|
|
7608
|
-
});
|
|
7609
|
-
if (!options.agentName && !flags.yes) {
|
|
7610
|
-
plan.agentName = await ask(`Agent name ${style.grey(`[${plan.agentName}]`)}:`, plan.agentName);
|
|
7611
|
-
}
|
|
7612
|
-
printPlan2(plan, environment);
|
|
7613
7876
|
out();
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
7617
|
-
return 1;
|
|
7618
|
-
}
|
|
7619
|
-
heading("Setting up");
|
|
7620
|
-
let created;
|
|
7621
|
-
try {
|
|
7622
|
-
created = await applyPlan(
|
|
7623
|
-
api,
|
|
7624
|
-
plan,
|
|
7625
|
-
{
|
|
7626
|
-
workspaceId: workspace.id,
|
|
7627
|
-
environmentId: environment.id,
|
|
7628
|
-
framework: framework.key === "unknown" ? null : framework.key
|
|
7629
|
-
},
|
|
7630
|
-
void 0,
|
|
7631
|
-
{ newAgent: options.newAgent === true }
|
|
7632
|
-
);
|
|
7633
|
-
} catch (error) {
|
|
7634
|
-
if (error instanceof CliApiError && error.code === "PERMISSION_DENIED") {
|
|
7635
|
-
throw new CliError(
|
|
7636
|
-
"Your role cannot set up agents in this workspace.",
|
|
7637
|
-
'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
|
|
7638
|
-
);
|
|
7639
|
-
}
|
|
7640
|
-
throw error;
|
|
7641
|
-
}
|
|
7642
|
-
let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
|
|
7643
|
-
if (!gatewayUrl) {
|
|
7644
|
-
const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
|
|
7645
|
-
if (published && await gatewayReachable(published)) {
|
|
7646
|
-
gatewayUrl = published;
|
|
7647
|
-
out(
|
|
7648
|
-
`${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
|
|
7649
|
-
);
|
|
7650
|
-
} else {
|
|
7651
|
-
const suggestion = published ?? DEFAULT_GATEWAY;
|
|
7652
|
-
out(
|
|
7653
|
-
`${symbol.warn} ${published ? `The gateway at ${published} is not answering.` : "The control plane did not publish a gateway address."}`
|
|
7654
|
-
);
|
|
7655
|
-
gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
|
|
7656
|
-
}
|
|
7657
|
-
}
|
|
7658
|
-
gatewayUrl = normalizeUrl(gatewayUrl);
|
|
7659
|
-
const config = {
|
|
7660
|
-
version: 1,
|
|
7661
|
-
controlPlaneUrl,
|
|
7662
|
-
gatewayUrl,
|
|
7663
|
-
organizationId: organization.id,
|
|
7664
|
-
organizationName: organization.name,
|
|
7665
|
-
workspaceId: workspace.id,
|
|
7666
|
-
workspaceName: workspace.name,
|
|
7667
|
-
environmentId: environment.id,
|
|
7668
|
-
environmentName: environment.name,
|
|
7669
|
-
environmentType: environment.type,
|
|
7670
|
-
agentId: created.agentId,
|
|
7671
|
-
agentName: created.agentName,
|
|
7672
|
-
keyId: created.keyId,
|
|
7673
|
-
framework: framework.key === "unknown" ? null : framework.key,
|
|
7674
|
-
declaredTools: created.tools.map((tool) => tool.key),
|
|
7675
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7676
|
-
};
|
|
7677
|
-
const configFile = writeProjectConfig(config);
|
|
7678
|
-
out(
|
|
7679
|
-
`${symbol.pass} Wrote ${style.bold("veefore.config.json")} ${style.grey("(safe to commit)")}`
|
|
7680
|
-
);
|
|
7681
|
-
const audience = await discoverAudience(gatewayUrl);
|
|
7682
|
-
ensureGitignore(process.cwd(), [".env.local", ".veefore/"]);
|
|
7683
|
-
const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
|
|
7684
|
-
const envFile = writeLocalEnv({
|
|
7685
|
-
VEEFORE_GATEWAY_URL: gatewayUrl,
|
|
7686
|
-
VEEFORE_GATEWAY_AUDIENCE: audience,
|
|
7687
|
-
VEEFORE_AGENT_ID: created.agentId,
|
|
7688
|
-
VEEFORE_AGENT_KEY_ID: created.keyId,
|
|
7689
|
-
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
7690
|
-
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
7691
|
-
// Written explicitly so the behaviour is visible and editable in one place.
|
|
7692
|
-
VEEFORE_APPROVAL_MODE: approvalMode
|
|
7693
|
-
});
|
|
7694
|
-
if (audience !== gatewayUrl) {
|
|
7695
|
-
out(
|
|
7696
|
-
`${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
|
|
7697
|
-
`(it reports this, rather than the ${gatewayUrl} you dialled)`
|
|
7698
|
-
)}`
|
|
7699
|
-
);
|
|
7700
|
-
}
|
|
7701
|
-
out(
|
|
7702
|
-
`${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
|
|
7703
|
-
);
|
|
7704
|
-
out();
|
|
7705
|
-
out(
|
|
7706
|
-
` ${style.amber("The private key was generated here and written to that file only.")} Veefore`
|
|
7707
|
-
);
|
|
7708
|
-
out(
|
|
7709
|
-
` never received it and cannot recover it. Move it into your secret manager before deploying.`
|
|
7710
|
-
);
|
|
7711
|
-
out(` ${style.grey(`Fingerprint: ${created.fingerprint}`)}`);
|
|
7712
|
-
await configureStarterPolicy(api, workspace.id, flags, options);
|
|
7713
|
-
let integrated = false;
|
|
7714
|
-
if (created.tools.length > 0 && !options.skipCode) {
|
|
7715
|
-
const outcome = await integrate(
|
|
7716
|
-
process.cwd(),
|
|
7717
|
-
created.tools.map((tool) => tool.key),
|
|
7718
|
-
{ yes: flags.yes, quiet: flags.json, api }
|
|
7719
|
-
);
|
|
7720
|
-
integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
|
|
7721
|
-
}
|
|
7722
|
-
heading("Validating the connection");
|
|
7723
|
-
const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
|
|
7724
|
-
for (const check of checks) {
|
|
7725
|
-
const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
|
|
7726
|
-
out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
|
|
7727
|
-
}
|
|
7728
|
-
const failed = checks.filter((check) => check.status === "fail");
|
|
7729
|
-
if (!integrated) {
|
|
7730
|
-
heading("Protect each tool");
|
|
7731
|
-
out();
|
|
7732
|
-
for (const line of integrationSnippet(framework).split("\n")) {
|
|
7733
|
-
out(` ${style.cyan(line)}`);
|
|
7734
|
-
}
|
|
7735
|
-
const entryPoints = findEntryPoints();
|
|
7736
|
-
if (entryPoints.length > 0) {
|
|
7737
|
-
out();
|
|
7738
|
-
out(
|
|
7739
|
-
style.grey(
|
|
7740
|
-
` Likely entry point${entryPoints.length > 1 ? "s" : ""}: ${entryPoints.join(", ")}`
|
|
7741
|
-
)
|
|
7742
|
-
);
|
|
7743
|
-
}
|
|
7744
|
-
if (created.tools.length > 0) {
|
|
7745
|
-
out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
|
|
7746
|
-
}
|
|
7747
|
-
}
|
|
7748
|
-
heading("When a human must approve");
|
|
7749
|
-
for (const line of approvalModeExplanation(approvalMode)) out(line);
|
|
7750
|
-
heading("Next");
|
|
7751
|
-
out(` ${symbol.bullet} ${style.bold("veefore dev")} watch decisions live while you build`);
|
|
7752
|
-
out(
|
|
7753
|
-
` ${symbol.bullet} ${style.bold("veefore doctor")} check the security posture of this setup`
|
|
7754
|
-
);
|
|
7755
|
-
out(` ${symbol.bullet} ${style.bold("veefore status")} see the current mode and agent health`);
|
|
7756
|
-
out();
|
|
7757
|
-
out(
|
|
7758
|
-
style.grey(
|
|
7759
|
-
` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
|
|
7760
|
-
)
|
|
7761
|
-
);
|
|
7762
|
-
out(style.grey(" Review the recommended policy, simulate it, then switch to ENFORCE."));
|
|
7763
|
-
out();
|
|
7764
|
-
if (flags.json) {
|
|
7765
|
-
out(
|
|
7766
|
-
JSON.stringify(
|
|
7767
|
-
{
|
|
7768
|
-
config_file: configFile,
|
|
7769
|
-
env_file: envFile,
|
|
7770
|
-
agent_id: created.agentId,
|
|
7771
|
-
key_id: created.keyId,
|
|
7772
|
-
fingerprint: created.fingerprint,
|
|
7773
|
-
tools: created.tools,
|
|
7774
|
-
checks
|
|
7775
|
-
},
|
|
7776
|
-
null,
|
|
7777
|
-
2
|
|
7778
|
-
)
|
|
7779
|
-
);
|
|
7780
|
-
}
|
|
7781
|
-
return failed.length > 0 ? 1 : 0;
|
|
7782
|
-
}
|
|
7783
|
-
function previousApprovalMode(dir = process.cwd()) {
|
|
7784
|
-
try {
|
|
7785
|
-
const text = readFileSync9(path10.join(dir, ".env.local"), "utf8");
|
|
7786
|
-
const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
|
|
7787
|
-
return value === "wait" || value === "throw" ? value : null;
|
|
7788
|
-
} catch {
|
|
7789
|
-
return null;
|
|
7790
|
-
}
|
|
7791
|
-
}
|
|
7792
|
-
function approvalModeExplanation(mode) {
|
|
7793
|
-
if (mode === "wait") {
|
|
7794
|
-
return [
|
|
7795
|
-
` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
|
|
7796
|
-
` ${symbol.bullet} approved: the tool runs once and returns its real result`,
|
|
7797
|
-
` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
|
|
7798
|
-
style.grey(
|
|
7799
|
-
" The agent process must stay running while it waits (up to the approval's expiry)."
|
|
7800
|
-
),
|
|
7801
|
-
style.grey(
|
|
7802
|
-
" For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
|
|
7803
|
-
),
|
|
7804
|
-
style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
|
|
7805
|
-
];
|
|
7806
|
-
}
|
|
7807
|
-
return [
|
|
7808
|
-
` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
|
|
7809
|
-
" carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
|
|
7810
|
-
" or the action never happens.",
|
|
7811
|
-
style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
|
|
7812
|
-
];
|
|
7813
|
-
}
|
|
7814
|
-
async function selectOrganization(api, me, options, flags) {
|
|
7815
|
-
const organizations = me.organizations.length > 0 ? me.organizations : [
|
|
7816
|
-
{
|
|
7817
|
-
id: me.organization.id,
|
|
7818
|
-
name: me.organization.name,
|
|
7819
|
-
slug: me.organization.slug,
|
|
7820
|
-
roleKey: me.roleKey
|
|
7821
|
-
}
|
|
7822
|
-
];
|
|
7823
|
-
if (options.organizationId) {
|
|
7824
|
-
const match = organizations.find((org) => org.id === options.organizationId);
|
|
7825
|
-
if (!match) {
|
|
7826
|
-
throw new CliError(
|
|
7827
|
-
`You are not a member of organization ${options.organizationId}.`,
|
|
7828
|
-
`Available: ${organizations.map((org) => org.id).join(", ")}`
|
|
7829
|
-
);
|
|
7830
|
-
}
|
|
7831
|
-
return match;
|
|
7832
|
-
}
|
|
7833
|
-
if (organizations.length === 1 || flags.yes) {
|
|
7834
|
-
const chosen2 = organizations[0];
|
|
7835
|
-
if (!chosen2) throw new CliError("Your account is not a member of any organization.");
|
|
7836
|
-
out(`${symbol.pass} Organization: ${style.bold(chosen2.name)}`);
|
|
7837
|
-
return chosen2;
|
|
7838
|
-
}
|
|
7839
|
-
out();
|
|
7840
|
-
const chosen = await select(
|
|
7841
|
-
"Which organization?",
|
|
7842
|
-
organizations.map((org) => ({
|
|
7843
|
-
label: org.name,
|
|
7844
|
-
hint: `${org.roleKey} \xB7 ${org.id}`,
|
|
7845
|
-
value: org
|
|
7846
|
-
})),
|
|
7847
|
-
{ quiet: true }
|
|
7848
|
-
);
|
|
7849
|
-
await api.post("/v1/auth/scope", { organization_id: chosen.id });
|
|
7850
|
-
out(`${symbol.pass} Organization: ${style.bold(chosen.name)}`);
|
|
7851
|
-
return chosen;
|
|
7877
|
+
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
7878
|
+
return 0;
|
|
7852
7879
|
}
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
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."],
|
|
7890
|
+
[
|
|
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."
|
|
7893
|
+
],
|
|
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."]]
|
|
7870
8069
|
}
|
|
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) {
|
|
7871
8084
|
out();
|
|
7872
|
-
|
|
7873
|
-
"
|
|
7874
|
-
workspaces.map((workspace) => ({
|
|
7875
|
-
label: workspace.name,
|
|
7876
|
-
hint: workspace.id,
|
|
7877
|
-
value: workspace
|
|
7878
|
-
})),
|
|
7879
|
-
{ quiet: true }
|
|
8085
|
+
out(
|
|
8086
|
+
`${style.bold("veefore")} ${style.grey(version)} \u2014 identity, authority and policy for AI agents`
|
|
7880
8087
|
);
|
|
7881
|
-
out(
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
const
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
"That workspace has no environments.",
|
|
7890
|
-
"Create one in the console, or ask an admin to."
|
|
7891
|
-
);
|
|
7892
|
-
}
|
|
7893
|
-
if (options.environmentId) {
|
|
7894
|
-
const match = environments.find((environment) => environment.id === options.environmentId);
|
|
7895
|
-
if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
|
|
7896
|
-
return match;
|
|
7897
|
-
}
|
|
7898
|
-
if (flags.yes) {
|
|
7899
|
-
const chosen2 = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
|
|
7900
|
-
out(`${symbol.pass} Environment: ${style.bold(chosen2.name)} (${chosen2.type})`);
|
|
7901
|
-
return chosen2;
|
|
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}`);
|
|
7902
8096
|
}
|
|
7903
8097
|
out();
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
value: environment
|
|
7911
|
-
})),
|
|
7912
|
-
{ quiet: true, initial: development >= 0 ? development : 0 }
|
|
8098
|
+
out(style.bold("GLOBAL OPTIONS"));
|
|
8099
|
+
renderOptions(GLOBAL_OPTIONS);
|
|
8100
|
+
out();
|
|
8101
|
+
out(style.bold("GETTING STARTED"));
|
|
8102
|
+
out(
|
|
8103
|
+
` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
|
|
7913
8104
|
);
|
|
7914
|
-
out(
|
|
7915
|
-
|
|
7916
|
-
}
|
|
7917
|
-
async function discoverAudience(gatewayUrl) {
|
|
7918
|
-
try {
|
|
7919
|
-
const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
|
|
7920
|
-
headers: { accept: "application/json" }
|
|
7921
|
-
});
|
|
7922
|
-
if (!response.ok) return gatewayUrl;
|
|
7923
|
-
const body = await response.json();
|
|
7924
|
-
const audience = body.data?.audience;
|
|
7925
|
-
return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
|
|
7926
|
-
} catch {
|
|
7927
|
-
return gatewayUrl;
|
|
7928
|
-
}
|
|
7929
|
-
}
|
|
7930
|
-
async function configureStarterPolicy(api, workspaceId, flags, options) {
|
|
7931
|
-
if (options.skipPolicy) {
|
|
7932
|
-
out(`${symbol.info} Skipped starter policy (--skip-policy).`);
|
|
7933
|
-
return;
|
|
7934
|
-
}
|
|
7935
|
-
const existing = await api.get(
|
|
7936
|
-
"/v1/policies",
|
|
7937
|
-
{ workspace_id: workspaceId }
|
|
8105
|
+
out(
|
|
8106
|
+
` ${style.grey("2.")} veefore dev ${style.grey("watch what your agent actually does")}`
|
|
7938
8107
|
);
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
|
|
7945
|
-
if (!create) {
|
|
7946
|
-
out(style.grey(" Skipped. Create one later from a template in the console."));
|
|
7947
|
-
return;
|
|
7948
|
-
}
|
|
7949
|
-
const policySpinner = spinner("Creating starter policy from the safe-defaults template");
|
|
7950
|
-
try {
|
|
7951
|
-
const result = await api.post("/v1/policies", {
|
|
7952
|
-
workspace_id: workspaceId,
|
|
7953
|
-
name: "Starter \u2014 safe defaults",
|
|
7954
|
-
template_key: "starter-observe-safe-defaults"
|
|
7955
|
-
});
|
|
7956
|
-
await api.post(`/v1/policies/${result.policy.id}/bindings`, {
|
|
7957
|
-
scope: "WORKSPACE",
|
|
7958
|
-
scope_id: workspaceId
|
|
7959
|
-
});
|
|
7960
|
-
policySpinner.succeed(
|
|
7961
|
-
`Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
|
|
7962
|
-
);
|
|
7963
|
-
keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
|
|
7964
|
-
out(
|
|
7965
|
-
style.grey(
|
|
7966
|
-
" Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
|
|
7967
|
-
)
|
|
7968
|
-
);
|
|
7969
|
-
} catch (error) {
|
|
7970
|
-
policySpinner.fail("Could not create the starter policy");
|
|
7971
|
-
if (error instanceof CliApiError) {
|
|
7972
|
-
out(style.grey(` ${error.message}`));
|
|
7973
|
-
out(style.grey(" You can create one from a template in the console instead."));
|
|
7974
|
-
return;
|
|
7975
|
-
}
|
|
7976
|
-
throw error;
|
|
7977
|
-
}
|
|
7978
|
-
}
|
|
7979
|
-
async function validateConnection(api, gatewayUrl, agentId, environmentId) {
|
|
7980
|
-
const checks = [];
|
|
7981
|
-
try {
|
|
7982
|
-
await api.get("/v1/auth/session");
|
|
7983
|
-
checks.push({ name: "Control plane reachable", status: "pass", message: api.controlPlaneUrl });
|
|
7984
|
-
} catch (error) {
|
|
7985
|
-
checks.push({
|
|
7986
|
-
name: "Control plane reachable",
|
|
7987
|
-
status: "fail",
|
|
7988
|
-
message: error instanceof Error ? error.message : String(error)
|
|
7989
|
-
});
|
|
7990
|
-
}
|
|
7991
|
-
try {
|
|
7992
|
-
const response = await fetch(`${gatewayUrl}/health`, {
|
|
7993
|
-
signal: AbortSignal.timeout(5e3)
|
|
7994
|
-
});
|
|
7995
|
-
const body = await response.json();
|
|
7996
|
-
checks.push({
|
|
7997
|
-
name: "Gateway reachable",
|
|
7998
|
-
status: response.ok ? "pass" : "fail",
|
|
7999
|
-
message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
|
|
8000
|
-
});
|
|
8001
|
-
} catch (error) {
|
|
8002
|
-
checks.push({
|
|
8003
|
-
name: "Gateway reachable",
|
|
8004
|
-
status: "fail",
|
|
8005
|
-
message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
|
|
8006
|
-
});
|
|
8007
|
-
}
|
|
8008
|
-
try {
|
|
8009
|
-
const identities = await api.get(
|
|
8010
|
-
`/v1/agents/${agentId}/identities`
|
|
8011
|
-
);
|
|
8012
|
-
const active = identities.find((identity) => identity.status === "ACTIVE");
|
|
8013
|
-
checks.push({
|
|
8014
|
-
name: "Agent identity active",
|
|
8015
|
-
status: active ? "pass" : "fail",
|
|
8016
|
-
message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
|
|
8017
|
-
});
|
|
8018
|
-
} catch (error) {
|
|
8019
|
-
checks.push({
|
|
8020
|
-
name: "Agent identity active",
|
|
8021
|
-
status: "warn",
|
|
8022
|
-
message: error instanceof Error ? error.message : String(error)
|
|
8023
|
-
});
|
|
8024
|
-
}
|
|
8025
|
-
try {
|
|
8026
|
-
const passport = await api.get(
|
|
8027
|
-
`/v1/passports/${agentId}`
|
|
8028
|
-
);
|
|
8029
|
-
checks.push({
|
|
8030
|
-
name: "Agent passport issued",
|
|
8031
|
-
status: passport ? "pass" : "warn",
|
|
8032
|
-
message: passport ? `version ${passport.version}` : "not issued yet"
|
|
8033
|
-
});
|
|
8034
|
-
} catch {
|
|
8035
|
-
checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
|
|
8036
|
-
}
|
|
8037
|
-
try {
|
|
8038
|
-
const environment = await api.get(
|
|
8039
|
-
`/v1/environments/${environmentId}`
|
|
8040
|
-
);
|
|
8041
|
-
checks.push({
|
|
8042
|
-
name: "Enforcement mode",
|
|
8043
|
-
status: "pass",
|
|
8044
|
-
message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
|
|
8045
|
-
});
|
|
8046
|
-
} catch {
|
|
8047
|
-
checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
|
|
8048
|
-
}
|
|
8049
|
-
return checks;
|
|
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();
|
|
8050
8113
|
}
|
|
8051
|
-
function
|
|
8052
|
-
const
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
case "--name":
|
|
8092
|
-
options.agentName = next();
|
|
8093
|
-
break;
|
|
8094
|
-
case "--capability":
|
|
8095
|
-
options.capabilities = [...options.capabilities ?? [], next()];
|
|
8096
|
-
break;
|
|
8097
|
-
case "--approval-mode": {
|
|
8098
|
-
const value = next().toLowerCase();
|
|
8099
|
-
if (value !== "wait" && value !== "throw") {
|
|
8100
|
-
throw new CliError('--approval-mode must be "wait" or "throw".');
|
|
8101
|
-
}
|
|
8102
|
-
options.approvalMode = value;
|
|
8103
|
-
break;
|
|
8104
|
-
}
|
|
8105
|
-
default:
|
|
8106
|
-
if (arg?.startsWith("-")) {
|
|
8107
|
-
throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
|
|
8108
|
-
}
|
|
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);
|
|
8129
|
+
}
|
|
8130
|
+
if (command.examples && command.examples.length > 0) {
|
|
8131
|
+
out();
|
|
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}`;
|
|
8109
8154
|
}
|
|
8110
8155
|
}
|
|
8111
|
-
|
|
8156
|
+
if (current !== "") lines.push(current);
|
|
8157
|
+
return lines;
|
|
8112
8158
|
}
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
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 });
|
|
8118
8176
|
out();
|
|
8119
|
-
out(style.
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8177
|
+
out(style.bold("Let's get started."));
|
|
8178
|
+
out();
|
|
8179
|
+
};
|
|
8180
|
+
await beginScreen(source, { animate: first, reserve: 11 });
|
|
8181
|
+
first = false;
|
|
8124
8182
|
out();
|
|
8125
|
-
|
|
8183
|
+
out(style.bold("Let's get started."));
|
|
8184
|
+
out();
|
|
8185
|
+
let action;
|
|
8126
8186
|
try {
|
|
8127
|
-
|
|
8187
|
+
action = await select(
|
|
8128
8188
|
"What would you like to do?",
|
|
8129
|
-
|
|
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 }
|
|
8130
8201
|
);
|
|
8131
8202
|
} catch (error) {
|
|
8132
|
-
if (error instanceof MenuCancelled) return
|
|
8203
|
+
if (error instanceof MenuCancelled) return 0;
|
|
8133
8204
|
throw error;
|
|
8134
8205
|
}
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
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;
|
|
8140
8215
|
}
|
|
8141
|
-
|
|
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;
|
|
8142
8230
|
}
|
|
8143
8231
|
}
|
|
8144
|
-
function
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
details([
|
|
8148
|
-
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
8149
|
-
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
8150
|
-
[
|
|
8151
|
-
"Status",
|
|
8152
|
-
state.kind === "ok" ? state.status : state.kind === "signed-out" ? "not signed in" : state.kind === "session-ended" ? "signed out" : state.kind === "missing" ? "agent not found" : "Veefore not reachable"
|
|
8153
|
-
]
|
|
8154
|
-
]);
|
|
8155
|
-
if (state.kind === "session-ended") {
|
|
8156
|
-
out(`${symbol.warn} ${state.message}`);
|
|
8157
|
-
}
|
|
8232
|
+
async function initWithWelcome(args, flags) {
|
|
8233
|
+
await beginScreen(welcomeSource(flags), { animate: true });
|
|
8234
|
+
return initCommand(args, flags);
|
|
8158
8235
|
}
|
|
8159
|
-
function
|
|
8160
|
-
const
|
|
8161
|
-
if (ended) {
|
|
8236
|
+
async function runOnScreen(source, flags, run2, options = {}) {
|
|
8237
|
+
const header = () => {
|
|
8162
8238
|
out();
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
);
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
leaves: true
|
|
8174
|
-
},
|
|
8175
|
-
{
|
|
8176
|
-
label: "Set it up again with a new agent",
|
|
8177
|
-
hint: "the guarded code keeps working",
|
|
8178
|
-
run: () => initCommand(["--force"], flags),
|
|
8179
|
-
leaves: true
|
|
8180
|
-
}
|
|
8181
|
-
];
|
|
8182
|
-
}
|
|
8183
|
-
const choices = [];
|
|
8184
|
-
if (state.kind === "signed-out" || state.kind === "session-ended") {
|
|
8185
|
-
choices.push({
|
|
8186
|
-
label: "Sign in",
|
|
8187
|
-
hint: "veefore login",
|
|
8188
|
-
run: async () => {
|
|
8189
|
-
await login(existing.controlPlaneUrl);
|
|
8190
|
-
return 0;
|
|
8191
|
-
}
|
|
8192
|
-
});
|
|
8193
|
-
}
|
|
8194
|
-
choices.push(
|
|
8195
|
-
{ label: "Watch decisions live", hint: "veefore dev", run: () => devCommand([], flags) },
|
|
8196
|
-
{ label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
|
|
8197
|
-
{
|
|
8198
|
-
label: "Protect tools in the code",
|
|
8199
|
-
hint: "veefore integrate",
|
|
8200
|
-
run: () => integrateCommand([], flags)
|
|
8201
|
-
},
|
|
8202
|
-
{
|
|
8203
|
-
label: "Check every tool is guarded",
|
|
8204
|
-
hint: "veefore integrate --check",
|
|
8205
|
-
run: () => integrateCommand(["--check"], flags)
|
|
8206
|
-
},
|
|
8207
|
-
{ label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
|
|
8208
|
-
{
|
|
8209
|
-
label: "Audit the security posture",
|
|
8210
|
-
hint: "veefore doctor",
|
|
8211
|
-
run: () => doctorCommand([], flags)
|
|
8212
|
-
},
|
|
8213
|
-
{ label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
|
|
8214
|
-
{
|
|
8215
|
-
label: "Re-run setup for this project",
|
|
8216
|
-
hint: "veefore init --force",
|
|
8217
|
-
run: () => initCommand(["--force"], flags),
|
|
8218
|
-
leaves: true
|
|
8219
|
-
},
|
|
8220
|
-
{
|
|
8221
|
-
label: "Remove Veefore from this project",
|
|
8222
|
-
hint: "veefore remove",
|
|
8223
|
-
run: () => removeCommand([], flags, state),
|
|
8224
|
-
leaves: true
|
|
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);
|
|
8225
8249
|
}
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
label: "Sign out",
|
|
8230
|
-
hint: "veefore logout",
|
|
8231
|
-
run: async () => {
|
|
8232
|
-
await logout(existing.controlPlaneUrl);
|
|
8233
|
-
return 0;
|
|
8234
|
-
},
|
|
8235
|
-
leaves: true
|
|
8236
|
-
});
|
|
8250
|
+
const label2 = options.label ?? "Back";
|
|
8251
|
+
await pressBack(typeof label2 === "function" ? label2() : label2);
|
|
8252
|
+
return code2;
|
|
8237
8253
|
}
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
if (flags.json) {
|
|
8245
|
-
out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
|
|
8246
|
-
return 0;
|
|
8254
|
+
beginCapture();
|
|
8255
|
+
let code = 0;
|
|
8256
|
+
try {
|
|
8257
|
+
code = await run2();
|
|
8258
|
+
} catch (error) {
|
|
8259
|
+
code = error instanceof MenuCancelled ? 0 : reportError(error, flags);
|
|
8247
8260
|
}
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
return 0;
|
|
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;
|
|
8256
8268
|
}
|
|
8257
8269
|
|
|
8258
|
-
// src/commands/
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
case void 0:
|
|
8266
|
-
case "list":
|
|
8267
|
-
return listPolicies(rest, flags);
|
|
8268
|
-
case "show":
|
|
8269
|
-
return showPolicy(rest, flags);
|
|
8270
|
-
case "test":
|
|
8271
|
-
return testPolicies(rest, flags);
|
|
8272
|
-
case "deploy":
|
|
8273
|
-
return deployPolicy(rest, flags);
|
|
8274
|
-
case "diff":
|
|
8275
|
-
return diffPolicy(rest, flags);
|
|
8276
|
-
default:
|
|
8277
|
-
throw new CliError(
|
|
8278
|
-
`Unknown subcommand \`veefore policies ${subcommand}\`.`,
|
|
8279
|
-
"Available: list, show, test, deploy, diff."
|
|
8280
|
-
);
|
|
8281
|
-
}
|
|
8282
|
-
}
|
|
8283
|
-
async function listPolicies(args, flags) {
|
|
8284
|
-
const context = await resolveContext(flags);
|
|
8285
|
-
const policies = await context.api.get("/v1/policies", {
|
|
8286
|
-
workspace_id: args.includes("--all") ? void 0 : context.config.workspaceId,
|
|
8287
|
-
status: valueFor(args, "--status")
|
|
8288
|
-
});
|
|
8289
|
-
if (flags.json) {
|
|
8290
|
-
printJson({ items: policies });
|
|
8291
|
-
return 0;
|
|
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);
|
|
8292
8277
|
}
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
latest: policy.latestVersion ? String(policy.latestVersion.versionNumber) : "\u2014",
|
|
8310
|
-
bindings: String(policy.bindings.length),
|
|
8311
|
-
evaluations: String(policy.evaluations24h)
|
|
8312
|
-
}))
|
|
8313
|
-
);
|
|
8314
|
-
const unbound = policies.filter(
|
|
8315
|
-
(policy) => policy.status === "ACTIVE" && policy.bindings.length === 0
|
|
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();
|
|
8292
|
+
out(
|
|
8293
|
+
`${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language}, matched ${framework.evidence})`)}`
|
|
8316
8294
|
);
|
|
8317
|
-
|
|
8318
|
-
out();
|
|
8319
|
-
if (unbound.length > 0) {
|
|
8295
|
+
if (framework.key === "unknown") {
|
|
8320
8296
|
out(
|
|
8321
|
-
|
|
8297
|
+
style.grey(
|
|
8298
|
+
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
8299
|
+
)
|
|
8322
8300
|
);
|
|
8323
8301
|
}
|
|
8324
|
-
|
|
8302
|
+
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
8303
|
+
const controlPlaneUrl = found.controlPlaneUrl;
|
|
8304
|
+
if (found.source !== "entered") {
|
|
8325
8305
|
out(
|
|
8326
|
-
|
|
8306
|
+
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
8327
8307
|
);
|
|
8328
8308
|
}
|
|
8329
|
-
|
|
8330
|
-
|
|
8309
|
+
let session = loadSession(controlPlaneUrl);
|
|
8310
|
+
if (!session) {
|
|
8311
|
+
await login(controlPlaneUrl);
|
|
8312
|
+
session = loadSession(controlPlaneUrl);
|
|
8331
8313
|
}
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
const
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
const
|
|
8341
|
-
|
|
8342
|
-
);
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
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();
|
|
8325
|
+
try {
|
|
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();
|
|
8347
8356
|
}
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
["ID", policy.id],
|
|
8351
|
-
["Key", policy.key],
|
|
8352
|
-
["Status", decisionStyle(policy.status)],
|
|
8353
|
-
["Priority", String(policy.priority)],
|
|
8354
|
-
[
|
|
8355
|
-
"Active version",
|
|
8356
|
-
policy.activeVersion ? `v${policy.activeVersion.versionNumber}` : style.grey("none")
|
|
8357
|
-
],
|
|
8358
|
-
["Bound agents", String(policy.boundAgentCount)],
|
|
8359
|
-
["Evaluations (24h)", `${policy.evaluations24h} ${style.grey(`${policy.blocks24h} blocked`)}`],
|
|
8360
|
-
["Owner", policy.ownerEmail ?? style.grey("unknown")],
|
|
8361
|
-
["Description", policy.description || style.grey("none")]
|
|
8362
|
-
]);
|
|
8363
|
-
if (policy.plainEnglish && policy.plainEnglish.length > 0) {
|
|
8364
|
-
heading("In plain English");
|
|
8365
|
-
for (const line of policy.plainEnglish) out(` ${symbol.bullet} ${line}`);
|
|
8357
|
+
for (const [label, value] of decided) {
|
|
8358
|
+
out(`${symbol.pass} ${label}: ${style.bold(value)}`);
|
|
8366
8359
|
}
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
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?",
|
|
8370
8387
|
[
|
|
8371
|
-
{
|
|
8372
|
-
{
|
|
8373
|
-
{
|
|
8388
|
+
{ label: "Change the agent name", value: "name" },
|
|
8389
|
+
{ label: "Choose a different environment", value: "environment" },
|
|
8390
|
+
{ label: "Leave setup", value: "leave" }
|
|
8374
8391
|
],
|
|
8375
|
-
|
|
8376
|
-
scope: binding.scope,
|
|
8377
|
-
target: binding.scopeId,
|
|
8378
|
-
enabled: binding.enabled ? style.green("yes") : style.red("no")
|
|
8379
|
-
}))
|
|
8392
|
+
{ initial: 0 }
|
|
8380
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
|
+
}
|
|
8381
8402
|
}
|
|
8382
|
-
heading("
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
{
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
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)")}`
|
|
8396
8464
|
);
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
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
|
|
8405
8477
|
});
|
|
8406
|
-
if (
|
|
8407
|
-
out(
|
|
8408
|
-
|
|
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
|
+
);
|
|
8409
8484
|
}
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
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
|
+
)
|
|
8416
8526
|
);
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
policy,
|
|
8421
|
-
result: null,
|
|
8422
|
-
error: error instanceof Error ? error.message : String(error)
|
|
8423
|
-
});
|
|
8527
|
+
}
|
|
8528
|
+
if (created.tools.length > 0) {
|
|
8529
|
+
out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
|
|
8424
8530
|
}
|
|
8425
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();
|
|
8426
8548
|
if (flags.json) {
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
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
|
+
);
|
|
8430
8564
|
}
|
|
8431
|
-
const failed = reports.filter(
|
|
8432
|
-
(report) => report.error !== void 0 || (report.result?.failed ?? 0) > 0
|
|
8433
|
-
);
|
|
8434
8565
|
return failed.length > 0 ? 1 : 0;
|
|
8435
8566
|
}
|
|
8436
|
-
function
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
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.")
|
|
8596
|
+
];
|
|
8597
|
+
}
|
|
8598
|
+
async function selectOrganization(api, me, options, flags) {
|
|
8599
|
+
const organizations = me.organizations.length > 0 ? me.organizations : [
|
|
8600
|
+
{
|
|
8601
|
+
id: me.organization.id,
|
|
8602
|
+
name: me.organization.name,
|
|
8603
|
+
slug: me.organization.slug,
|
|
8604
|
+
roleKey: me.roleKey
|
|
8454
8605
|
}
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
const
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
out(
|
|
8463
|
-
` ${symbol.fail} ${testCase.name}: expected ${decisionStyle(testCase.expected)}, got ${decisionStyle(testCase.actual)}`
|
|
8606
|
+
];
|
|
8607
|
+
if (options.organizationId) {
|
|
8608
|
+
const match = organizations.find((org) => org.id === options.organizationId);
|
|
8609
|
+
if (!match) {
|
|
8610
|
+
throw new CliError(
|
|
8611
|
+
`You are not a member of organization ${options.organizationId}.`,
|
|
8612
|
+
`Available: ${organizations.map((org) => org.id).join(", ")}`
|
|
8464
8613
|
);
|
|
8465
|
-
if (testCase.reason) out(` ${style.grey(testCase.reason)}`);
|
|
8466
8614
|
}
|
|
8615
|
+
return { value: match, asked: false };
|
|
8616
|
+
}
|
|
8617
|
+
if (organizations.length === 1 || flags.yes) {
|
|
8618
|
+
const chosen2 = organizations[0];
|
|
8619
|
+
if (!chosen2) throw new CliError("Your account is not a member of any organization.");
|
|
8620
|
+
return { value: chosen2, asked: false };
|
|
8467
8621
|
}
|
|
8468
8622
|
out();
|
|
8469
|
-
|
|
8470
|
-
|
|
8623
|
+
const chosen = await select(
|
|
8624
|
+
"Which organization?",
|
|
8625
|
+
organizations.map((org) => ({
|
|
8626
|
+
label: org.name,
|
|
8627
|
+
hint: `${org.roleKey} \xB7 ${org.id}`,
|
|
8628
|
+
value: org
|
|
8629
|
+
})),
|
|
8630
|
+
{ quiet: true, back: true, backLabel: "Back" }
|
|
8471
8631
|
);
|
|
8472
|
-
if (
|
|
8473
|
-
|
|
8474
|
-
|
|
8632
|
+
if (chosen === BACK) return BACK;
|
|
8633
|
+
await api.post("/v1/auth/scope", { organization_id: chosen.id });
|
|
8634
|
+
return { value: chosen, asked: true };
|
|
8635
|
+
}
|
|
8636
|
+
async function selectWorkspace(api, me, options, flags) {
|
|
8637
|
+
const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
|
|
8638
|
+
if (workspaces.length === 0) {
|
|
8639
|
+
throw new CliError(
|
|
8640
|
+
"This organization has no workspaces.",
|
|
8641
|
+
"Create one in the console, or ask an admin to."
|
|
8475
8642
|
);
|
|
8476
8643
|
}
|
|
8644
|
+
if (options.workspaceId) {
|
|
8645
|
+
const match = workspaces.find((workspace) => workspace.id === options.workspaceId);
|
|
8646
|
+
if (!match) throw new CliError(`Workspace ${options.workspaceId} was not found.`);
|
|
8647
|
+
return { value: match, asked: false };
|
|
8648
|
+
}
|
|
8649
|
+
if (workspaces.length === 1 || flags.yes) {
|
|
8650
|
+
const chosen2 = workspaces[0];
|
|
8651
|
+
return { value: chosen2, asked: false };
|
|
8652
|
+
}
|
|
8477
8653
|
out();
|
|
8654
|
+
const chosen = await select(
|
|
8655
|
+
"Which workspace?",
|
|
8656
|
+
workspaces.map((workspace) => ({
|
|
8657
|
+
label: workspace.name,
|
|
8658
|
+
hint: workspace.id,
|
|
8659
|
+
value: workspace
|
|
8660
|
+
})),
|
|
8661
|
+
{ quiet: true, back: true }
|
|
8662
|
+
);
|
|
8663
|
+
if (chosen === BACK) return BACK;
|
|
8664
|
+
return { value: chosen, asked: true };
|
|
8478
8665
|
}
|
|
8479
|
-
async function
|
|
8480
|
-
const
|
|
8481
|
-
const
|
|
8482
|
-
if (
|
|
8666
|
+
async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
8667
|
+
const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
|
|
8668
|
+
const environments = all.filter((environment) => environment.workspaceId === workspaceId);
|
|
8669
|
+
if (environments.length === 0) {
|
|
8483
8670
|
throw new CliError(
|
|
8484
|
-
"
|
|
8485
|
-
|
|
8671
|
+
"That workspace has no environments.",
|
|
8672
|
+
"Create one in the console, or ask an admin to."
|
|
8486
8673
|
);
|
|
8487
8674
|
}
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8675
|
+
if (options.environmentId) {
|
|
8676
|
+
const match = environments.find((environment) => environment.id === options.environmentId);
|
|
8677
|
+
if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
|
|
8678
|
+
return { value: match, asked: false };
|
|
8679
|
+
}
|
|
8680
|
+
if (flags.yes) {
|
|
8681
|
+
const chosen = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
|
|
8682
|
+
return { value: chosen, asked: false };
|
|
8683
|
+
}
|
|
8684
|
+
const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
|
|
8685
|
+
for (; ; ) {
|
|
8686
|
+
out();
|
|
8687
|
+
const chosen = await select(
|
|
8688
|
+
"Which environment?",
|
|
8689
|
+
environments.map((environment) => ({
|
|
8690
|
+
label: environment.name,
|
|
8691
|
+
hint: `${environment.type} \xB7 ${environment.gatewayMode ?? "\u2014"} \xB7 ${environment.id}`,
|
|
8692
|
+
value: environment
|
|
8693
|
+
})),
|
|
8694
|
+
{ quiet: true, initial: development >= 0 ? development : 0, back: true }
|
|
8496
8695
|
);
|
|
8696
|
+
if (chosen === BACK) return BACK;
|
|
8697
|
+
if (chosen.type === "PRODUCTION" && !options.force) {
|
|
8698
|
+
out();
|
|
8699
|
+
out(
|
|
8700
|
+
`${symbol.warn} ${style.amber("You selected a PRODUCTION environment.")} A new agent here starts in ${style.bold("OBSERVE")} mode and performs no enforcement until you switch it.`
|
|
8701
|
+
);
|
|
8702
|
+
if (!await confirm("Continue with the production environment?", { yes: flags.yes })) {
|
|
8703
|
+
out(style.grey(" Choose another environment."));
|
|
8704
|
+
continue;
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
return { value: chosen, asked: true };
|
|
8497
8708
|
}
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
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
|
+
}
|
|
8501
8720
|
}
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
["Policy", `${policy.name} (${policy.id})`],
|
|
8506
|
-
[
|
|
8507
|
-
"Currently active",
|
|
8508
|
-
policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
|
|
8509
|
-
],
|
|
8510
|
-
["Deploying", `v${target.versionNumber}`],
|
|
8511
|
-
["Bindings", String(policy.bindings.length)]
|
|
8512
|
-
]);
|
|
8513
|
-
if (policy.bindings.length === 0) {
|
|
8721
|
+
}
|
|
8722
|
+
async function askAgentName(suggested) {
|
|
8723
|
+
for (; ; ) {
|
|
8514
8724
|
out();
|
|
8725
|
+
const choice = await select(
|
|
8726
|
+
"Agent name",
|
|
8727
|
+
[
|
|
8728
|
+
{ label: suggested, hint: "suggested", value: "keep" },
|
|
8729
|
+
{ label: "Type a different name\u2026", value: "type" }
|
|
8730
|
+
],
|
|
8731
|
+
{ quiet: true, back: true }
|
|
8732
|
+
);
|
|
8733
|
+
if (choice === BACK) return BACK;
|
|
8734
|
+
if (choice === "keep") {
|
|
8735
|
+
out(`${symbol.pass} Agent name: ${style.bold(suggested)}`);
|
|
8736
|
+
return suggested;
|
|
8737
|
+
}
|
|
8738
|
+
const typed = (await ask(`Agent name ${style.grey(`[${suggested}]`)}:`, suggested)).trim();
|
|
8739
|
+
if (typed === "") continue;
|
|
8740
|
+
out(`${symbol.pass} Agent name: ${style.bold(typed)}`);
|
|
8741
|
+
return typed;
|
|
8742
|
+
}
|
|
8743
|
+
}
|
|
8744
|
+
async function discoverAudience(gatewayUrl) {
|
|
8745
|
+
try {
|
|
8746
|
+
const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
|
|
8747
|
+
headers: { accept: "application/json" }
|
|
8748
|
+
});
|
|
8749
|
+
if (!response.ok) return gatewayUrl;
|
|
8750
|
+
const body = await response.json();
|
|
8751
|
+
const audience = body.data?.audience;
|
|
8752
|
+
return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
|
|
8753
|
+
} catch {
|
|
8754
|
+
return gatewayUrl;
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
8757
|
+
async function configureStarterPolicy(api, workspaceId, flags, options) {
|
|
8758
|
+
if (options.skipPolicy) {
|
|
8759
|
+
out(`${symbol.info} Skipped starter policy (--skip-policy).`);
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
8762
|
+
const existing = await api.get(
|
|
8763
|
+
"/v1/policies",
|
|
8764
|
+
{ workspace_id: workspaceId }
|
|
8765
|
+
);
|
|
8766
|
+
const count = Array.isArray(existing) ? existing.length : existing.items?.length ?? 0;
|
|
8767
|
+
if (count > 0) {
|
|
8768
|
+
out(`${symbol.pass} Workspace already has ${count} policy/policies. Leaving them alone.`);
|
|
8769
|
+
return;
|
|
8770
|
+
}
|
|
8771
|
+
const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
|
|
8772
|
+
if (!create) {
|
|
8773
|
+
out(style.grey(" Skipped. Create one later from a template in the console."));
|
|
8774
|
+
return;
|
|
8775
|
+
}
|
|
8776
|
+
const policySpinner = spinner("Creating starter policy from the safe-defaults template");
|
|
8777
|
+
try {
|
|
8778
|
+
const result = await api.post("/v1/policies", {
|
|
8779
|
+
workspace_id: workspaceId,
|
|
8780
|
+
name: "Starter \u2014 safe defaults",
|
|
8781
|
+
template_key: "starter-observe-safe-defaults"
|
|
8782
|
+
});
|
|
8783
|
+
await api.post(`/v1/policies/${result.policy.id}/bindings`, {
|
|
8784
|
+
scope: "WORKSPACE",
|
|
8785
|
+
scope_id: workspaceId
|
|
8786
|
+
});
|
|
8787
|
+
policySpinner.succeed(
|
|
8788
|
+
`Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
|
|
8789
|
+
);
|
|
8790
|
+
keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
|
|
8515
8791
|
out(
|
|
8516
|
-
|
|
8792
|
+
style.grey(
|
|
8793
|
+
" Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
|
|
8794
|
+
)
|
|
8517
8795
|
);
|
|
8518
|
-
}
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
if (diff) {
|
|
8525
|
-
heading("Changes");
|
|
8526
|
-
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
8527
|
-
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
8528
|
-
for (const rule of diff.changed) {
|
|
8529
|
-
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
8530
|
-
}
|
|
8531
|
-
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
8532
|
-
out(style.grey(" No rule changes."));
|
|
8533
|
-
}
|
|
8534
|
-
if (diff.riskAssessment) {
|
|
8535
|
-
out();
|
|
8536
|
-
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
8537
|
-
}
|
|
8796
|
+
} catch (error) {
|
|
8797
|
+
policySpinner.fail("Could not create the starter policy");
|
|
8798
|
+
if (error instanceof CliApiError) {
|
|
8799
|
+
out(style.grey(` ${error.message}`));
|
|
8800
|
+
out(style.grey(" You can create one from a template in the console instead."));
|
|
8801
|
+
return;
|
|
8538
8802
|
}
|
|
8803
|
+
throw error;
|
|
8539
8804
|
}
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8805
|
+
}
|
|
8806
|
+
async function validateConnection(api, gatewayUrl, agentId, environmentId) {
|
|
8807
|
+
const checks = [];
|
|
8543
8808
|
try {
|
|
8544
|
-
|
|
8545
|
-
|
|
8809
|
+
await api.get("/v1/auth/session");
|
|
8810
|
+
checks.push({ name: "Control plane reachable", status: "pass", message: api.controlPlaneUrl });
|
|
8811
|
+
} catch (error) {
|
|
8812
|
+
checks.push({
|
|
8813
|
+
name: "Control plane reachable",
|
|
8814
|
+
status: "fail",
|
|
8815
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8816
|
+
});
|
|
8817
|
+
}
|
|
8818
|
+
try {
|
|
8819
|
+
const response = await fetch(`${gatewayUrl}/health`, {
|
|
8820
|
+
signal: AbortSignal.timeout(5e3)
|
|
8821
|
+
});
|
|
8822
|
+
const body = await response.json();
|
|
8823
|
+
checks.push({
|
|
8824
|
+
name: "Gateway reachable",
|
|
8825
|
+
status: response.ok ? "pass" : "fail",
|
|
8826
|
+
message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
|
|
8546
8827
|
});
|
|
8547
|
-
testSpinner.succeed(`${testResult.passed}/${caseCount(testResult)} test case(s) passing`);
|
|
8548
8828
|
} catch (error) {
|
|
8549
|
-
|
|
8550
|
-
|
|
8829
|
+
checks.push({
|
|
8830
|
+
name: "Gateway reachable",
|
|
8831
|
+
status: "fail",
|
|
8832
|
+
message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
|
|
8833
|
+
});
|
|
8551
8834
|
}
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
out(
|
|
8556
|
-
` ${symbol.fail} ${failure.name}: expected ${decisionStyle(failure.expected)}, got ${decisionStyle(failure.actual)}`
|
|
8557
|
-
);
|
|
8558
|
-
}
|
|
8559
|
-
out();
|
|
8560
|
-
throw new CliError(
|
|
8561
|
-
`${testResult.failed} test case(s) fail. This version will not be deployed.`,
|
|
8562
|
-
"Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
|
|
8835
|
+
try {
|
|
8836
|
+
const identities = await api.get(
|
|
8837
|
+
`/v1/agents/${agentId}/identities`
|
|
8563
8838
|
);
|
|
8839
|
+
const active = identities.find((identity) => identity.status === "ACTIVE");
|
|
8840
|
+
checks.push({
|
|
8841
|
+
name: "Agent identity active",
|
|
8842
|
+
status: active ? "pass" : "fail",
|
|
8843
|
+
message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
|
|
8844
|
+
});
|
|
8845
|
+
} catch (error) {
|
|
8846
|
+
checks.push({
|
|
8847
|
+
name: "Agent identity active",
|
|
8848
|
+
status: "warn",
|
|
8849
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8850
|
+
});
|
|
8564
8851
|
}
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
` ${symbol.warn} ${style.amber("This policy has no test cases.")} There is no evidence the new rules behave as intended.`
|
|
8852
|
+
try {
|
|
8853
|
+
const passport = await api.get(
|
|
8854
|
+
`/v1/passports/${agentId}`
|
|
8569
8855
|
);
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
}
|
|
8575
|
-
}
|
|
8576
|
-
|
|
8577
|
-
if (reason.trim().length < 10) {
|
|
8578
|
-
throw new CliError("A reason of at least 10 characters is required.");
|
|
8579
|
-
}
|
|
8580
|
-
if (context.config.environmentType === "PRODUCTION") {
|
|
8581
|
-
out();
|
|
8582
|
-
out(` ${symbol.warn} ${style.amber("This affects a PRODUCTION environment.")}`);
|
|
8583
|
-
}
|
|
8584
|
-
const confirmed = await confirm(`Activate v${target.versionNumber}?`, { yes: flags.yes });
|
|
8585
|
-
if (!confirmed) {
|
|
8586
|
-
out(style.grey("Cancelled. No change was made."));
|
|
8587
|
-
return 1;
|
|
8856
|
+
checks.push({
|
|
8857
|
+
name: "Agent passport issued",
|
|
8858
|
+
status: passport ? "pass" : "warn",
|
|
8859
|
+
message: passport ? `version ${passport.version}` : "not issued yet"
|
|
8860
|
+
});
|
|
8861
|
+
} catch {
|
|
8862
|
+
checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
|
|
8588
8863
|
}
|
|
8589
|
-
const deploySpinner = spinner(`Activating v${target.versionNumber}`);
|
|
8590
8864
|
try {
|
|
8591
|
-
await
|
|
8592
|
-
|
|
8593
|
-
() => context.api.post(`/v1/policies/${policyId}/activate`, {
|
|
8594
|
-
version_id: target.id,
|
|
8595
|
-
reason
|
|
8596
|
-
})
|
|
8865
|
+
const environment = await api.get(
|
|
8866
|
+
`/v1/environments/${environmentId}`
|
|
8597
8867
|
);
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8868
|
+
checks.push({
|
|
8869
|
+
name: "Enforcement mode",
|
|
8870
|
+
status: "pass",
|
|
8871
|
+
message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
|
|
8872
|
+
});
|
|
8873
|
+
} catch {
|
|
8874
|
+
checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
|
|
8602
8875
|
}
|
|
8603
|
-
|
|
8604
|
-
out(` ${symbol.pass} The previous version is marked SUPERSEDED and can be rolled back.`);
|
|
8605
|
-
out(` ${symbol.pass} The change and its diff are recorded in the audit trail.`);
|
|
8606
|
-
out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
|
|
8607
|
-
out();
|
|
8608
|
-
return 0;
|
|
8876
|
+
return checks;
|
|
8609
8877
|
}
|
|
8610
|
-
|
|
8611
|
-
const
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8878
|
+
function parseInitArgs(args) {
|
|
8879
|
+
const options = { force: false, skipPolicy: false, skipCode: false };
|
|
8880
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8881
|
+
const arg = args[index];
|
|
8882
|
+
const next = () => {
|
|
8883
|
+
const value = args[index + 1];
|
|
8884
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
8885
|
+
throw new CliError(`Option ${String(arg)} requires a value.`);
|
|
8886
|
+
}
|
|
8887
|
+
index += 1;
|
|
8888
|
+
return value;
|
|
8889
|
+
};
|
|
8890
|
+
switch (arg) {
|
|
8891
|
+
case "--force":
|
|
8892
|
+
options.force = true;
|
|
8893
|
+
break;
|
|
8894
|
+
case "--skip-policy":
|
|
8895
|
+
options.skipPolicy = true;
|
|
8896
|
+
break;
|
|
8897
|
+
case "--skip-code":
|
|
8898
|
+
options.skipCode = true;
|
|
8899
|
+
break;
|
|
8900
|
+
case "--new-agent":
|
|
8901
|
+
options.newAgent = true;
|
|
8902
|
+
break;
|
|
8903
|
+
case "--control-plane":
|
|
8904
|
+
options.controlPlaneUrl = next();
|
|
8905
|
+
break;
|
|
8906
|
+
case "--gateway":
|
|
8907
|
+
options.gatewayUrl = next();
|
|
8908
|
+
break;
|
|
8909
|
+
case "--organization":
|
|
8910
|
+
options.organizationId = next();
|
|
8911
|
+
break;
|
|
8912
|
+
case "--workspace":
|
|
8913
|
+
options.workspaceId = next();
|
|
8914
|
+
break;
|
|
8915
|
+
case "--environment":
|
|
8916
|
+
options.environmentId = next();
|
|
8917
|
+
break;
|
|
8918
|
+
case "--name":
|
|
8919
|
+
options.agentName = next();
|
|
8920
|
+
break;
|
|
8921
|
+
case "--capability":
|
|
8922
|
+
options.capabilities = [...options.capabilities ?? [], next()];
|
|
8923
|
+
break;
|
|
8924
|
+
case "--approval-mode": {
|
|
8925
|
+
const value = next().toLowerCase();
|
|
8926
|
+
if (value !== "wait" && value !== "throw") {
|
|
8927
|
+
throw new CliError('--approval-mode must be "wait" or "throw".');
|
|
8928
|
+
}
|
|
8929
|
+
options.approvalMode = value;
|
|
8930
|
+
break;
|
|
8931
|
+
}
|
|
8932
|
+
default:
|
|
8933
|
+
if (arg?.startsWith("-")) {
|
|
8934
|
+
throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
|
|
8935
|
+
}
|
|
8936
|
+
}
|
|
8618
8937
|
}
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
return
|
|
8938
|
+
return options;
|
|
8939
|
+
}
|
|
8940
|
+
async function projectHome(existing, flags) {
|
|
8941
|
+
enterApp();
|
|
8942
|
+
try {
|
|
8943
|
+
return await projectHomeLoop(existing, flags);
|
|
8944
|
+
} finally {
|
|
8945
|
+
leaveApp();
|
|
8625
8946
|
}
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8947
|
+
}
|
|
8948
|
+
async function projectHomeLoop(existing, flags) {
|
|
8949
|
+
const source = welcomeSource(flags);
|
|
8950
|
+
let last = 0;
|
|
8951
|
+
for (let round = 0; ; round += 1) {
|
|
8952
|
+
await beginScreen(source, { animate: round === 0, reserve: 18 });
|
|
8953
|
+
const state = await agentState(existing);
|
|
8954
|
+
printProjectSummary(existing, state);
|
|
8955
|
+
const choices = homeChoices(existing, state, flags);
|
|
8956
|
+
choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
|
|
8957
|
+
out();
|
|
8958
|
+
let picked;
|
|
8959
|
+
try {
|
|
8960
|
+
picked = await select(
|
|
8961
|
+
"What would you like to do?",
|
|
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
|
+
}
|
|
8972
|
+
);
|
|
8973
|
+
} catch (error) {
|
|
8974
|
+
if (error instanceof MenuCancelled) return last;
|
|
8975
|
+
throw error;
|
|
8976
|
+
}
|
|
8977
|
+
if (picked.leaves) {
|
|
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
|
+
});
|
|
8987
|
+
}
|
|
8988
|
+
last = await runOnScreen(source, flags, () => picked.run(), {
|
|
8989
|
+
label: "Back to menu",
|
|
8990
|
+
live: picked.live
|
|
8991
|
+
});
|
|
8992
|
+
if (!isInteractive()) return last;
|
|
8631
8993
|
}
|
|
8632
|
-
|
|
8633
|
-
|
|
8994
|
+
}
|
|
8995
|
+
function printProjectSummary(existing, state) {
|
|
8996
|
+
out();
|
|
8997
|
+
details([
|
|
8998
|
+
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
8999
|
+
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
9000
|
+
[
|
|
9001
|
+
"Status",
|
|
9002
|
+
state.kind === "ok" ? decisionStyle(state.status) : state.kind === "signed-out" ? style.amber("not signed in") : state.kind === "session-ended" ? style.amber("signed out") : state.kind === "missing" ? style.red("agent not found") : style.amber("Veefore not reachable")
|
|
9003
|
+
]
|
|
9004
|
+
]);
|
|
9005
|
+
if (state.kind === "session-ended") {
|
|
9006
|
+
out(`${symbol.warn} ${state.message}`);
|
|
8634
9007
|
}
|
|
8635
|
-
|
|
9008
|
+
}
|
|
9009
|
+
function homeChoices(existing, state, flags) {
|
|
9010
|
+
const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
|
|
9011
|
+
if (ended) {
|
|
8636
9012
|
out();
|
|
8637
|
-
out(
|
|
9013
|
+
out(
|
|
9014
|
+
`${symbol.warn} ${style.amber(
|
|
9015
|
+
state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
|
|
9016
|
+
)} Every tool call wrapped in guard() is now refused.`
|
|
9017
|
+
);
|
|
9018
|
+
return [
|
|
9019
|
+
{
|
|
9020
|
+
label: "Remove Veefore from this project",
|
|
9021
|
+
hint: "take guard() out of the code, disconnect",
|
|
9022
|
+
run: () => removeCommand([], flags, state),
|
|
9023
|
+
leaves: true
|
|
9024
|
+
},
|
|
9025
|
+
{
|
|
9026
|
+
label: "Set it up again with a new agent",
|
|
9027
|
+
hint: "the guarded code keeps working",
|
|
9028
|
+
run: () => initCommand(["--force"], flags),
|
|
9029
|
+
leaves: true
|
|
9030
|
+
}
|
|
9031
|
+
];
|
|
8638
9032
|
}
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
"veefore
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
}
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8751
|
-
],
|
|
8752
|
-
examples: [
|
|
8753
|
-
"veefore agents list",
|
|
8754
|
-
'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
|
|
8755
|
-
]
|
|
8756
|
-
},
|
|
8757
|
-
{
|
|
8758
|
-
name: "policies",
|
|
8759
|
-
summary: "List, test and deploy policies.",
|
|
8760
|
-
usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
|
|
8761
|
-
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.",
|
|
8762
|
-
options: [
|
|
8763
|
-
["--version <n>", "Version to deploy, for `deploy`."],
|
|
8764
|
-
["--from <n> --to <n>", "Versions to compare, for `diff`."],
|
|
8765
|
-
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8766
|
-
],
|
|
8767
|
-
examples: [
|
|
8768
|
-
"veefore policies list",
|
|
8769
|
-
"veefore policies test",
|
|
8770
|
-
"veefore policies deploy pol_01ABC"
|
|
8771
|
-
]
|
|
8772
|
-
},
|
|
8773
|
-
{
|
|
8774
|
-
name: "logs",
|
|
8775
|
-
summary: "Read or stream the decision log.",
|
|
8776
|
-
usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
|
|
8777
|
-
description: "Every line is a server-side decision with the policy and rule that produced it.",
|
|
8778
|
-
options: [
|
|
8779
|
-
["--follow, -f", "Stream new decisions as they happen."],
|
|
8780
|
-
["--decision <d>", "Filter by decision, e.g. BLOCK."],
|
|
8781
|
-
["--agent <id>", "Filter by agent."],
|
|
8782
|
-
["--all", "Include every agent in the environment."],
|
|
8783
|
-
["--limit <n>", "Number of recent decisions to show."]
|
|
8784
|
-
]
|
|
8785
|
-
},
|
|
8786
|
-
{
|
|
8787
|
-
name: "audit",
|
|
8788
|
-
summary: "Query, verify and export the audit trail.",
|
|
8789
|
-
usage: "veefore audit <list|show|verify|export> [eventId]",
|
|
8790
|
-
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.",
|
|
8791
|
-
options: [
|
|
8792
|
-
["--severity <s>", "Filter by severity."],
|
|
8793
|
-
["--type <t>", "Filter by event type."],
|
|
8794
|
-
["--from <iso> --to <iso>", "Time range."],
|
|
8795
|
-
["--format <json|csv>", "Export format."],
|
|
8796
|
-
['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
|
|
8797
|
-
["--out <file>", "Write the export to a file (mode 0600)."]
|
|
9033
|
+
const choices = [];
|
|
9034
|
+
if (state.kind === "signed-out" || state.kind === "session-ended") {
|
|
9035
|
+
choices.push({
|
|
9036
|
+
label: "Sign in",
|
|
9037
|
+
hint: "veefore login",
|
|
9038
|
+
run: async () => {
|
|
9039
|
+
await login(existing.controlPlaneUrl);
|
|
9040
|
+
return 0;
|
|
9041
|
+
}
|
|
9042
|
+
});
|
|
9043
|
+
}
|
|
9044
|
+
choices.push(
|
|
9045
|
+
{
|
|
9046
|
+
label: "Watch decisions live",
|
|
9047
|
+
hint: "veefore dev",
|
|
9048
|
+
run: () => devCommand([], flags),
|
|
9049
|
+
live: true
|
|
9050
|
+
},
|
|
9051
|
+
{ label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
|
|
9052
|
+
{
|
|
9053
|
+
label: "Protect tools in the code",
|
|
9054
|
+
hint: "veefore integrate",
|
|
9055
|
+
run: () => integrateCommand([], flags)
|
|
9056
|
+
},
|
|
9057
|
+
{
|
|
9058
|
+
label: "Check every tool is guarded",
|
|
9059
|
+
hint: "veefore integrate --check",
|
|
9060
|
+
run: () => integrateCommand(["--check"], flags)
|
|
9061
|
+
},
|
|
9062
|
+
{ label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
|
|
9063
|
+
{
|
|
9064
|
+
label: "Audit the security posture",
|
|
9065
|
+
hint: "veefore doctor",
|
|
9066
|
+
run: () => doctorCommand([], flags)
|
|
9067
|
+
},
|
|
9068
|
+
{ label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
|
|
9069
|
+
{
|
|
9070
|
+
label: "Re-run setup for this project",
|
|
9071
|
+
hint: "veefore init --force",
|
|
9072
|
+
run: () => initCommand(["--force"], flags),
|
|
9073
|
+
leaves: true
|
|
9074
|
+
},
|
|
9075
|
+
{
|
|
9076
|
+
label: "Remove Veefore from this project",
|
|
9077
|
+
hint: "veefore remove",
|
|
9078
|
+
run: () => removeCommand([], flags, state),
|
|
9079
|
+
leaves: true
|
|
9080
|
+
}
|
|
9081
|
+
);
|
|
9082
|
+
if (state.kind === "ok" || state.kind === "unreachable") {
|
|
9083
|
+
choices.push({
|
|
9084
|
+
label: "Sign out",
|
|
9085
|
+
hint: "veefore logout",
|
|
9086
|
+
run: async () => {
|
|
9087
|
+
await logout(existing.controlPlaneUrl);
|
|
9088
|
+
return 0;
|
|
9089
|
+
}
|
|
9090
|
+
// Deliberately not `leaves`: signing out comes back to this menu, so the
|
|
9091
|
+
// panel and the status redraw as signed out and the next step on offer
|
|
9092
|
+
// is Sign in. Leaving instead would quit on the one action whose result
|
|
9093
|
+
// a person most wants to see confirmed.
|
|
9094
|
+
});
|
|
9095
|
+
}
|
|
9096
|
+
return choices;
|
|
9097
|
+
}
|
|
9098
|
+
|
|
9099
|
+
// src/commands/policies.ts
|
|
9100
|
+
function caseCount(result) {
|
|
9101
|
+
return result.passed + result.failed;
|
|
9102
|
+
}
|
|
9103
|
+
async function policiesCommand(args, flags) {
|
|
9104
|
+
const [subcommand, ...rest] = args;
|
|
9105
|
+
switch (subcommand) {
|
|
9106
|
+
case void 0:
|
|
9107
|
+
case "list":
|
|
9108
|
+
return listPolicies(rest, flags);
|
|
9109
|
+
case "show":
|
|
9110
|
+
return showPolicy(rest, flags);
|
|
9111
|
+
case "test":
|
|
9112
|
+
return testPolicies(rest, flags);
|
|
9113
|
+
case "deploy":
|
|
9114
|
+
return deployPolicy(rest, flags);
|
|
9115
|
+
case "diff":
|
|
9116
|
+
return diffPolicy(rest, flags);
|
|
9117
|
+
default:
|
|
9118
|
+
throw new CliError(
|
|
9119
|
+
`Unknown subcommand \`veefore policies ${subcommand}\`.`,
|
|
9120
|
+
"Available: list, show, test, deploy, diff."
|
|
9121
|
+
);
|
|
9122
|
+
}
|
|
9123
|
+
}
|
|
9124
|
+
async function listPolicies(args, flags) {
|
|
9125
|
+
const context = await resolveContext(flags);
|
|
9126
|
+
const policies = await context.api.get("/v1/policies", {
|
|
9127
|
+
workspace_id: args.includes("--all") ? void 0 : context.config.workspaceId,
|
|
9128
|
+
status: valueFor(args, "--status")
|
|
9129
|
+
});
|
|
9130
|
+
if (flags.json) {
|
|
9131
|
+
printJson({ items: policies });
|
|
9132
|
+
return 0;
|
|
9133
|
+
}
|
|
9134
|
+
heading(`Policies in ${context.config.workspaceName}`);
|
|
9135
|
+
table(
|
|
9136
|
+
[
|
|
9137
|
+
{ header: "NAME", key: "name", width: 30 },
|
|
9138
|
+
{ header: "KEY", key: "key", width: 24 },
|
|
9139
|
+
{ header: "STATUS", key: "status", width: 18 },
|
|
9140
|
+
{ header: "ACTIVE v", key: "active", width: 10, align: "right" },
|
|
9141
|
+
{ header: "LATEST v", key: "latest", width: 10, align: "right" },
|
|
9142
|
+
{ header: "BINDINGS", key: "bindings", width: 9, align: "right" },
|
|
9143
|
+
{ header: "24H", key: "evaluations", width: 7, align: "right" }
|
|
8798
9144
|
],
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
{
|
|
8826
|
-
name: "logout",
|
|
8827
|
-
summary: "Sign out of Veefore on this machine.",
|
|
8828
|
-
usage: "veefore logout [--all]",
|
|
8829
|
-
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.',
|
|
8830
|
-
options: [["--all", "Sign out of every control plane this machine is signed in to."]]
|
|
9145
|
+
policies.map((policy) => ({
|
|
9146
|
+
name: policy.name,
|
|
9147
|
+
key: policy.key,
|
|
9148
|
+
status: decisionStyle(policy.status),
|
|
9149
|
+
active: policy.activeVersion ? String(policy.activeVersion.versionNumber) : "\u2014",
|
|
9150
|
+
latest: policy.latestVersion ? String(policy.latestVersion.versionNumber) : "\u2014",
|
|
9151
|
+
bindings: String(policy.bindings.length),
|
|
9152
|
+
evaluations: String(policy.evaluations24h)
|
|
9153
|
+
}))
|
|
9154
|
+
);
|
|
9155
|
+
const unbound = policies.filter(
|
|
9156
|
+
(policy) => policy.status === "ACTIVE" && policy.bindings.length === 0
|
|
9157
|
+
);
|
|
9158
|
+
const drafts = policies.filter((policy) => policy.status === "DRAFT");
|
|
9159
|
+
out();
|
|
9160
|
+
if (unbound.length > 0) {
|
|
9161
|
+
out(
|
|
9162
|
+
` ${symbol.warn} ${style.amber(`${unbound.length} active policy/policies are not bound to any scope, so they evaluate nothing.`)}`
|
|
9163
|
+
);
|
|
9164
|
+
}
|
|
9165
|
+
if (drafts.length > 0) {
|
|
9166
|
+
out(
|
|
9167
|
+
` ${symbol.info} ${drafts.length} draft(s). Run ${style.bold("veefore policies deploy <policyId>")} once the tests pass.`
|
|
9168
|
+
);
|
|
9169
|
+
}
|
|
9170
|
+
if (policies.length === 0) {
|
|
9171
|
+
out(` ${symbol.warn} No policies exist. Nothing is being evaluated against rules.`);
|
|
8831
9172
|
}
|
|
8832
|
-
];
|
|
8833
|
-
var GLOBAL_OPTIONS = [
|
|
8834
|
-
["--json", "Machine-readable output."],
|
|
8835
|
-
["--yes, -y", "Assume yes for confirmations. Never skips step-up authentication."],
|
|
8836
|
-
["--verbose", "Expanded output."],
|
|
8837
|
-
[
|
|
8838
|
-
"--control-plane <url>",
|
|
8839
|
-
"Use this Veefore instead of the one found automatically (hosted, or the last you signed in to)."
|
|
8840
|
-
],
|
|
8841
|
-
["--gateway <url>", "Override the configured gateway URL."],
|
|
8842
|
-
["--version, -V", "Print the CLI version."],
|
|
8843
|
-
["--help, -h", "Show help."]
|
|
8844
|
-
];
|
|
8845
|
-
function printRootHelp(version) {
|
|
8846
9173
|
out();
|
|
8847
|
-
|
|
8848
|
-
|
|
9174
|
+
return 0;
|
|
9175
|
+
}
|
|
9176
|
+
async function showPolicy(args, flags) {
|
|
9177
|
+
const context = await resolveContext(flags);
|
|
9178
|
+
const policyId = positional(args);
|
|
9179
|
+
if (!policyId)
|
|
9180
|
+
throw new CliError("A policy id is required. Usage: veefore policies show <policyId>");
|
|
9181
|
+
const policy = await context.api.get(
|
|
9182
|
+
`/v1/policies/${policyId}`
|
|
9183
|
+
);
|
|
9184
|
+
const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
|
|
9185
|
+
if (flags.json) {
|
|
9186
|
+
printJson({ policy, versions });
|
|
9187
|
+
return 0;
|
|
9188
|
+
}
|
|
9189
|
+
heading(policy.name);
|
|
9190
|
+
details([
|
|
9191
|
+
["ID", policy.id],
|
|
9192
|
+
["Key", policy.key],
|
|
9193
|
+
["Status", decisionStyle(policy.status)],
|
|
9194
|
+
["Priority", String(policy.priority)],
|
|
9195
|
+
[
|
|
9196
|
+
"Active version",
|
|
9197
|
+
policy.activeVersion ? `v${policy.activeVersion.versionNumber}` : style.grey("none")
|
|
9198
|
+
],
|
|
9199
|
+
["Bound agents", String(policy.boundAgentCount)],
|
|
9200
|
+
["Evaluations (24h)", `${policy.evaluations24h} ${style.grey(`${policy.blocks24h} blocked`)}`],
|
|
9201
|
+
["Owner", policy.ownerEmail ?? style.grey("unknown")],
|
|
9202
|
+
["Description", policy.description || style.grey("none")]
|
|
9203
|
+
]);
|
|
9204
|
+
if (policy.plainEnglish && policy.plainEnglish.length > 0) {
|
|
9205
|
+
heading("In plain English");
|
|
9206
|
+
for (const line of policy.plainEnglish) out(` ${symbol.bullet} ${line}`);
|
|
9207
|
+
}
|
|
9208
|
+
if (policy.bindings.length > 0) {
|
|
9209
|
+
heading("Bindings");
|
|
9210
|
+
table(
|
|
9211
|
+
[
|
|
9212
|
+
{ header: "SCOPE", key: "scope", width: 14 },
|
|
9213
|
+
{ header: "TARGET", key: "target", width: 34 },
|
|
9214
|
+
{ header: "ENABLED", key: "enabled", width: 8 }
|
|
9215
|
+
],
|
|
9216
|
+
policy.bindings.map((binding) => ({
|
|
9217
|
+
scope: binding.scope,
|
|
9218
|
+
target: binding.scopeId,
|
|
9219
|
+
enabled: binding.enabled ? style.green("yes") : style.red("no")
|
|
9220
|
+
}))
|
|
9221
|
+
);
|
|
9222
|
+
}
|
|
9223
|
+
heading("Versions");
|
|
9224
|
+
table(
|
|
9225
|
+
[
|
|
9226
|
+
{ header: "v", key: "version", width: 4, align: "right" },
|
|
9227
|
+
{ header: "STATUS", key: "status", width: 20 },
|
|
9228
|
+
{ header: "CREATED", key: "created", width: 20 },
|
|
9229
|
+
{ header: "SUMMARY", key: "summary", width: 40 }
|
|
9230
|
+
],
|
|
9231
|
+
versions.map((version) => ({
|
|
9232
|
+
version: String(version.versionNumber),
|
|
9233
|
+
status: decisionStyle(version.status),
|
|
9234
|
+
created: version.createdAt.slice(0, 16).replace("T", " "),
|
|
9235
|
+
summary: version.changeSummary ?? ""
|
|
9236
|
+
}))
|
|
8849
9237
|
);
|
|
8850
9238
|
out();
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
const
|
|
8856
|
-
|
|
8857
|
-
|
|
9239
|
+
return 0;
|
|
9240
|
+
}
|
|
9241
|
+
async function testPolicies(args, flags) {
|
|
9242
|
+
const context = await resolveContext(flags);
|
|
9243
|
+
const explicit = positional(args);
|
|
9244
|
+
const targets = explicit ? [await context.api.get(`/v1/policies/${explicit}`)] : await context.api.get("/v1/policies", {
|
|
9245
|
+
workspace_id: context.config.workspaceId
|
|
9246
|
+
});
|
|
9247
|
+
if (targets.length === 0) {
|
|
9248
|
+
out(`${symbol.warn} No policies to test.`);
|
|
9249
|
+
return 0;
|
|
9250
|
+
}
|
|
9251
|
+
const reports = [];
|
|
9252
|
+
for (const policy of targets) {
|
|
9253
|
+
try {
|
|
9254
|
+
const result = await context.api.post(
|
|
9255
|
+
`/v1/policies/${policy.id}/tests/run`,
|
|
9256
|
+
{}
|
|
9257
|
+
);
|
|
9258
|
+
reports.push({ policy, result });
|
|
9259
|
+
} catch (error) {
|
|
9260
|
+
reports.push({
|
|
9261
|
+
policy,
|
|
9262
|
+
result: null,
|
|
9263
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9264
|
+
});
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
if (flags.json) {
|
|
9268
|
+
printJson(reports);
|
|
9269
|
+
} else {
|
|
9270
|
+
renderTestReports(reports);
|
|
9271
|
+
}
|
|
9272
|
+
const failed = reports.filter(
|
|
9273
|
+
(report) => report.error !== void 0 || (report.result?.failed ?? 0) > 0
|
|
9274
|
+
);
|
|
9275
|
+
return failed.length > 0 ? 1 : 0;
|
|
9276
|
+
}
|
|
9277
|
+
function renderTestReports(reports) {
|
|
9278
|
+
heading("Policy tests");
|
|
9279
|
+
let totalCases = 0;
|
|
9280
|
+
let totalFailed = 0;
|
|
9281
|
+
let withoutTests = 0;
|
|
9282
|
+
for (const report of reports) {
|
|
9283
|
+
if (report.error) {
|
|
9284
|
+
out(`${symbol.fail} ${style.bold(report.policy.name)} ${style.grey(report.error)}`);
|
|
9285
|
+
totalFailed += 1;
|
|
9286
|
+
continue;
|
|
9287
|
+
}
|
|
9288
|
+
const result = report.result;
|
|
9289
|
+
if (!result || caseCount(result) === 0) {
|
|
9290
|
+
withoutTests += 1;
|
|
9291
|
+
out(
|
|
9292
|
+
`${symbol.warn} ${style.bold(report.policy.name)} ${style.amber("no test cases recorded")}`
|
|
9293
|
+
);
|
|
9294
|
+
continue;
|
|
9295
|
+
}
|
|
9296
|
+
totalCases += caseCount(result);
|
|
9297
|
+
totalFailed += result.failed;
|
|
9298
|
+
const icon = result.failed === 0 ? symbol.pass : symbol.fail;
|
|
9299
|
+
out(
|
|
9300
|
+
`${icon} ${style.bold(report.policy.name)} ${style.grey(`${result.passed}/${caseCount(result)} passing`)}`
|
|
9301
|
+
);
|
|
9302
|
+
for (const testCase of result.results.filter((entry) => !entry.passed)) {
|
|
9303
|
+
out(
|
|
9304
|
+
` ${symbol.fail} ${testCase.name}: expected ${decisionStyle(testCase.expected)}, got ${decisionStyle(testCase.actual)}`
|
|
9305
|
+
);
|
|
9306
|
+
if (testCase.reason) out(` ${style.grey(testCase.reason)}`);
|
|
9307
|
+
}
|
|
8858
9308
|
}
|
|
8859
9309
|
out();
|
|
8860
|
-
out(style.bold("GLOBAL OPTIONS"));
|
|
8861
|
-
renderOptions(GLOBAL_OPTIONS);
|
|
8862
|
-
out();
|
|
8863
|
-
out(style.bold("GETTING STARTED"));
|
|
8864
9310
|
out(
|
|
8865
|
-
` ${
|
|
8866
|
-
);
|
|
8867
|
-
out(
|
|
8868
|
-
` ${style.grey("2.")} veefore dev ${style.grey("watch what your agent actually does")}`
|
|
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).`)}`
|
|
8869
9312
|
);
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
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
|
+
}
|
|
8874
9318
|
out();
|
|
8875
9319
|
}
|
|
8876
|
-
function
|
|
8877
|
-
const
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
out();
|
|
8885
|
-
out(style.bold("DESCRIPTION"));
|
|
8886
|
-
for (const line of wrap2(command.description, 76)) out(` ${line}`);
|
|
8887
|
-
if (command.options && command.options.length > 0) {
|
|
8888
|
-
out();
|
|
8889
|
-
out(style.bold("OPTIONS"));
|
|
8890
|
-
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
|
+
);
|
|
8891
9328
|
}
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
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
|
+
);
|
|
8896
9338
|
}
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
function renderOptions(options) {
|
|
8901
|
-
const width = Math.max(...options.map(([flag]) => flag.length));
|
|
8902
|
-
for (const [flag, description] of options) {
|
|
8903
|
-
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;
|
|
8904
9342
|
}
|
|
8905
|
-
}
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
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
|
+
}
|
|
8916
9379
|
}
|
|
8917
9380
|
}
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
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) {
|
|
8931
9394
|
out();
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
{
|
|
8937
|
-
label: "Preview what setup would find",
|
|
8938
|
-
hint: "veefore scan \xB7 changes nothing",
|
|
8939
|
-
value: "scan"
|
|
8940
|
-
},
|
|
8941
|
-
{ label: "Sign in", hint: "veefore login", value: "login" },
|
|
8942
|
-
{ label: "Show every command", hint: "veefore help", value: "help" },
|
|
8943
|
-
{ label: "Exit", hint: "esc", value: "exit" }
|
|
8944
|
-
]);
|
|
8945
|
-
} catch (error) {
|
|
8946
|
-
if (error instanceof MenuCancelled) return 0;
|
|
8947
|
-
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
|
+
);
|
|
8948
9399
|
}
|
|
8949
9400
|
out();
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
return 0;
|
|
8965
|
-
}
|
|
8966
|
-
} catch (error) {
|
|
8967
|
-
if (!(error instanceof MenuCancelled)) reportError(error, flags);
|
|
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;
|
|
8968
9415
|
}
|
|
8969
9416
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
const
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
facts: [
|
|
8985
|
-
["Folder", shorten(folder, 34)],
|
|
8986
|
-
["Signed in", session ? session.email : style.grey("not yet")],
|
|
8987
|
-
[
|
|
8988
|
-
"Project",
|
|
8989
|
-
project ? `${project.agentName} \xB7 ${project.environmentName}` : style.grey("not set up here")
|
|
8990
|
-
],
|
|
8991
|
-
["Veefore", hostOf(controlPlaneUrl)]
|
|
8992
|
-
]
|
|
8993
|
-
};
|
|
8994
|
-
}
|
|
8995
|
-
function hostOf(url) {
|
|
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}`);
|
|
8996
9431
|
try {
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
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`);
|
|
9440
|
+
} catch (error) {
|
|
9441
|
+
deploySpinner.fail("Activation failed");
|
|
9442
|
+
throw error;
|
|
9000
9443
|
}
|
|
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;
|
|
9001
9450
|
}
|
|
9002
|
-
function
|
|
9003
|
-
|
|
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;
|
|
9004
9482
|
}
|
|
9005
9483
|
|
|
9006
9484
|
// src/index.ts
|
|
9007
|
-
var CLI_VERSION = "1.1.0";
|
|
9008
9485
|
async function run(argv) {
|
|
9009
9486
|
const { flags, rest } = parseGlobalFlags(argv);
|
|
9010
9487
|
if (rest.includes("--version") || rest.includes("-V")) {
|
|
@@ -9013,9 +9490,9 @@ async function run(argv) {
|
|
|
9013
9490
|
}
|
|
9014
9491
|
const [command, ...args] = rest.filter((arg) => arg !== "--help" && arg !== "-h");
|
|
9015
9492
|
const wantsHelp = rest.includes("--help") || rest.includes("-h");
|
|
9016
|
-
if (command === void 0 && !wantsHelp &&
|
|
9493
|
+
if (command === void 0 && !wantsHelp && isInteractive()) {
|
|
9017
9494
|
try {
|
|
9018
|
-
return flags.json ? await initCommand(args, flags) : await homeCommand(args, flags
|
|
9495
|
+
return flags.json ? await initCommand(args, flags) : await homeCommand(args, flags);
|
|
9019
9496
|
} catch (error) {
|
|
9020
9497
|
return handleError(error, flags);
|
|
9021
9498
|
}
|
|
@@ -9035,7 +9512,7 @@ async function run(argv) {
|
|
|
9035
9512
|
switch (command) {
|
|
9036
9513
|
case "init":
|
|
9037
9514
|
case "setup":
|
|
9038
|
-
return flags.json || !isInteractive() ? await initCommand(args, flags) : await initWithWelcome(args, flags
|
|
9515
|
+
return flags.json || !isInteractive() ? await initCommand(args, flags) : await initWithWelcome(args, flags);
|
|
9039
9516
|
case "scan":
|
|
9040
9517
|
return await scanCommand(args, flags);
|
|
9041
9518
|
case "integrate":
|