veefore 1.2.0 → 1.4.1
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 +1847 -1537
- 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
|
|
@@ -612,8 +614,8 @@ var BRAND_TOP = [150, 172, 236];
|
|
|
612
614
|
var BRAND_BOTTOM = [84, 108, 186];
|
|
613
615
|
var BRAND = [118, 140, 214];
|
|
614
616
|
var brand = (text) => paint(BRAND, text);
|
|
615
|
-
function gradient(row,
|
|
616
|
-
const t =
|
|
617
|
+
function gradient(row, rows2) {
|
|
618
|
+
const t = rows2 <= 1 ? 0 : row / (rows2 - 1);
|
|
617
619
|
const mix = (a, b) => Math.round(a + (b - a) * t);
|
|
618
620
|
return [
|
|
619
621
|
mix(BRAND_TOP[0], BRAND_BOTTOM[0]),
|
|
@@ -628,6 +630,9 @@ function visibleLength(text) {
|
|
|
628
630
|
function columns() {
|
|
629
631
|
return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
|
630
632
|
}
|
|
633
|
+
function rows() {
|
|
634
|
+
return process.stdout.rows && process.stdout.rows > 0 ? process.stdout.rows : 24;
|
|
635
|
+
}
|
|
631
636
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
632
637
|
var STARS = [
|
|
633
638
|
[0, -3, "*"],
|
|
@@ -640,8 +645,20 @@ var STARS = [
|
|
|
640
645
|
];
|
|
641
646
|
async function showWelcome(context, options = {}) {
|
|
642
647
|
if (!isInteractive()) return;
|
|
648
|
+
const lines = welcomeLines(context);
|
|
649
|
+
if ((options.animate ?? true) && process.env.VEEFORE_NO_ANIMATION === void 0) {
|
|
650
|
+
for (const line of lines) {
|
|
651
|
+
out(line);
|
|
652
|
+
await sleep2(14);
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
for (const line of lines) out(line);
|
|
657
|
+
}
|
|
658
|
+
function welcomeLines(context) {
|
|
643
659
|
const width = Math.min(columns(), 100);
|
|
644
|
-
const
|
|
660
|
+
const art = LOGO;
|
|
661
|
+
const logoWidth = Math.max(...art.map((line) => [...line].length));
|
|
645
662
|
const indent = 5;
|
|
646
663
|
const gap = 6;
|
|
647
664
|
const panelWidth = Math.max(
|
|
@@ -649,43 +666,45 @@ async function showWelcome(context, options = {}) {
|
|
|
649
666
|
...context.facts.map(([label, value]) => label.length + 2 + value.length)
|
|
650
667
|
);
|
|
651
668
|
const sideBySide = width >= indent + logoWidth + gap + panelWidth + 2;
|
|
669
|
+
const panel = renderPanel(context.facts);
|
|
652
670
|
const showLogo = width >= indent + logoWidth + 2;
|
|
653
671
|
const rule = style.grey("\xB7".repeat(Math.max(10, Math.min(width - 2, 72))));
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
672
|
+
const lines = [];
|
|
673
|
+
const add = (line = "") => void lines.push(line);
|
|
674
|
+
add();
|
|
675
|
+
add(` ${brand(style.bold("Welcome to Veefore"))} ${style.grey(`v${context.version}`)}`);
|
|
676
|
+
add(` ${rule}`);
|
|
677
|
+
add();
|
|
660
678
|
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}${
|
|
679
|
+
for (let row = 0; row < art.length; row += 1) {
|
|
680
|
+
const line = art[row] ?? "";
|
|
681
|
+
const left = starsFor(row, indent, (column) => column < 0, -indent, art.length);
|
|
682
|
+
const right = starsFor(row, 0, (column) => column >= logoWidth, logoWidth, art.length);
|
|
683
|
+
const painted = paint(gradient(row, art.length), line.padEnd(logoWidth));
|
|
684
|
+
let text = `${left}${painted}`;
|
|
667
685
|
if (sideBySide) {
|
|
668
|
-
const top = Math.max(0, Math.floor((
|
|
686
|
+
const top = Math.max(0, Math.floor((art.length - panel.length) / 2));
|
|
669
687
|
const side = panel[row - top];
|
|
670
688
|
text += side === void 0 ? right : `${" ".repeat(gap)}${side}`;
|
|
671
689
|
} else {
|
|
672
690
|
text += right;
|
|
673
691
|
}
|
|
674
|
-
|
|
675
|
-
if (animate) await sleep2(14);
|
|
692
|
+
add(text.replace(/\s+$/, ""));
|
|
676
693
|
}
|
|
677
694
|
}
|
|
678
695
|
if (!sideBySide) {
|
|
679
|
-
|
|
680
|
-
for (const line of panel)
|
|
696
|
+
add();
|
|
697
|
+
for (const line of panel) add(fit(` ${line}`, width - 1));
|
|
681
698
|
}
|
|
682
|
-
|
|
683
|
-
|
|
699
|
+
add();
|
|
700
|
+
add(` ${rule}`);
|
|
701
|
+
return lines;
|
|
684
702
|
}
|
|
685
|
-
function starsFor(row, width, inBand, offset) {
|
|
703
|
+
function starsFor(row, width, inBand, offset, artRows) {
|
|
686
704
|
const cells = Array.from({ length: Math.max(width, 6) }, () => " ");
|
|
687
705
|
for (const [starRow, column, glyph] of STARS) {
|
|
688
|
-
|
|
706
|
+
const scaled = Math.round(starRow * (artRows - 1) / (LOGO.length - 1));
|
|
707
|
+
if (scaled !== row || !inBand(column)) continue;
|
|
689
708
|
const index = column - offset;
|
|
690
709
|
if (index >= 0 && index < cells.length) cells[index] = style.grey(glyph);
|
|
691
710
|
}
|
|
@@ -706,27 +725,139 @@ function renderPanel(facts) {
|
|
|
706
725
|
}
|
|
707
726
|
function clearScreen() {
|
|
708
727
|
if (!isInteractive()) return;
|
|
709
|
-
process.stdout.write("\x1B[H\x1B[
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
728
|
+
process.stdout.write("\x1B[H\x1B[J");
|
|
729
|
+
}
|
|
730
|
+
var appDepth = 0;
|
|
731
|
+
var restoreInstalled = false;
|
|
732
|
+
function enterApp() {
|
|
733
|
+
if (!isInteractive()) return;
|
|
734
|
+
appDepth += 1;
|
|
735
|
+
if (appDepth > 1) return;
|
|
736
|
+
if (!restoreInstalled) {
|
|
737
|
+
restoreInstalled = true;
|
|
738
|
+
process.on("exit", () => {
|
|
739
|
+
if (appDepth > 0) process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
process.stdout.write("\x1B[?1049h\x1B[?25l\x1B[H\x1B[J");
|
|
743
|
+
}
|
|
744
|
+
function leaveApp() {
|
|
745
|
+
if (!isInteractive() || appDepth === 0) return;
|
|
746
|
+
appDepth -= 1;
|
|
747
|
+
if (appDepth > 0) return;
|
|
748
|
+
process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
749
|
+
}
|
|
750
|
+
function inApp() {
|
|
751
|
+
return appDepth > 0;
|
|
752
|
+
}
|
|
753
|
+
async function onOrdinaryScreen(body) {
|
|
754
|
+
const depth = appDepth;
|
|
755
|
+
if (depth > 0) {
|
|
756
|
+
appDepth = 0;
|
|
757
|
+
process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
return await body();
|
|
761
|
+
} finally {
|
|
762
|
+
if (depth > 0) {
|
|
763
|
+
appDepth = depth;
|
|
764
|
+
process.stdout.write("\x1B[?1049h\x1B[H\x1B[J");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
721
767
|
}
|
|
722
768
|
async function beginScreen(source, options = {}) {
|
|
723
769
|
if (!isInteractive()) return;
|
|
724
770
|
clearScreen();
|
|
725
|
-
|
|
726
|
-
|
|
771
|
+
await showWelcome(source(), { animate: options.animate ?? false });
|
|
772
|
+
}
|
|
773
|
+
function drawScreen(source) {
|
|
774
|
+
if (!isInteractive()) return;
|
|
775
|
+
clearScreen();
|
|
776
|
+
for (const line of welcomeLines(source())) out(line);
|
|
777
|
+
}
|
|
778
|
+
async function showOutput(lines, options) {
|
|
779
|
+
if (!isInteractive()) {
|
|
780
|
+
for (const line of lines) process.stdout.write(`${line}
|
|
781
|
+
`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const stdout = process.stdout;
|
|
785
|
+
const stdin = process.stdin;
|
|
786
|
+
const body = lines.length > 0 ? lines : [style.grey(" (no output)")];
|
|
787
|
+
let headerRows = 0;
|
|
788
|
+
const measure = () => {
|
|
789
|
+
let count = 0;
|
|
790
|
+
const original = stdout.write.bind(stdout);
|
|
791
|
+
stdout.write = ((chunk) => {
|
|
792
|
+
count += String(chunk).split("\n").length - 1;
|
|
793
|
+
return true;
|
|
794
|
+
});
|
|
795
|
+
try {
|
|
796
|
+
options.drawHeader();
|
|
797
|
+
} finally {
|
|
798
|
+
stdout.write = original;
|
|
799
|
+
}
|
|
800
|
+
headerRows = count;
|
|
801
|
+
};
|
|
802
|
+
measure();
|
|
803
|
+
let top = 0;
|
|
804
|
+
const viewport = () => Math.max(3, rows() - headerRows - 2);
|
|
805
|
+
const draw = () => {
|
|
806
|
+
const height = viewport();
|
|
807
|
+
const maxTop = Math.max(0, body.length - height);
|
|
808
|
+
top = Math.min(Math.max(0, top), maxTop);
|
|
809
|
+
clearScreen();
|
|
810
|
+
options.drawHeader();
|
|
811
|
+
for (const line of body.slice(top, top + height)) {
|
|
812
|
+
stdout.write(`${fit(line, columns() - 1)}
|
|
813
|
+
`);
|
|
814
|
+
}
|
|
815
|
+
const hidden = body.length > height;
|
|
816
|
+
const position = hidden ? `${Math.min(top + height, body.length)}/${body.length}` : `${body.length} line${body.length === 1 ? "" : "s"}`;
|
|
817
|
+
const keys = hidden ? "\u2191\u2193 scroll \xB7 " : "";
|
|
818
|
+
stdout.write(
|
|
819
|
+
`${brand("\u2190")} ${style.bold(options.label ?? "Back")} ${style.grey(`\xB7 ${keys}enter or esc \xB7 ${position}`)}`
|
|
820
|
+
);
|
|
821
|
+
};
|
|
822
|
+
await new Promise((resolve) => {
|
|
823
|
+
const wasRaw = stdin.isRaw;
|
|
824
|
+
const done = () => {
|
|
825
|
+
stdin.off("keypress", onKey);
|
|
826
|
+
stdin.setRawMode(wasRaw);
|
|
827
|
+
stdin.pause();
|
|
828
|
+
process.stdout.off("resize", onResize);
|
|
829
|
+
stdout.write("\r\x1B[K");
|
|
830
|
+
resolve();
|
|
831
|
+
};
|
|
832
|
+
const onResize = () => {
|
|
833
|
+
measure();
|
|
834
|
+
draw();
|
|
835
|
+
};
|
|
836
|
+
const onKey = (input, key = {}) => {
|
|
837
|
+
const height = viewport();
|
|
838
|
+
if (key.name === "return" || key.name === "enter" || key.name === "escape" || key.name === "left" || input === "b" || key.ctrl && key.name === "c") {
|
|
839
|
+
return done();
|
|
840
|
+
}
|
|
841
|
+
if (key.name === "down" || input === "j") top += 1;
|
|
842
|
+
else if (key.name === "up" || input === "k") top -= 1;
|
|
843
|
+
else if (key.name === "pagedown" || input === " ") top += height;
|
|
844
|
+
else if (key.name === "pageup") top -= height;
|
|
845
|
+
else if (input === "G") top = body.length;
|
|
846
|
+
else if (input === "g") top = 0;
|
|
847
|
+
else return;
|
|
848
|
+
draw();
|
|
849
|
+
};
|
|
850
|
+
emitKeypressEvents(stdin);
|
|
851
|
+
stdin.setRawMode(true);
|
|
852
|
+
stdin.resume();
|
|
853
|
+
stdin.on("keypress", onKey);
|
|
854
|
+
process.stdout.on("resize", onResize);
|
|
855
|
+
draw();
|
|
856
|
+
});
|
|
727
857
|
}
|
|
728
858
|
async function pressBack(label = "Back") {
|
|
729
859
|
if (!isInteractive()) return;
|
|
860
|
+
flushCapture();
|
|
730
861
|
const stdin = process.stdin;
|
|
731
862
|
const stdout = process.stdout;
|
|
732
863
|
out();
|
|
@@ -751,6 +882,29 @@ async function pressBack(label = "Back") {
|
|
|
751
882
|
stdin.on("keypress", onKey);
|
|
752
883
|
});
|
|
753
884
|
}
|
|
885
|
+
function watchFacts(onChange) {
|
|
886
|
+
const files = [
|
|
887
|
+
CREDENTIALS_FILE,
|
|
888
|
+
findProjectConfig() ?? path2.join(process.cwd(), PROJECT_CONFIG_FILE)
|
|
889
|
+
];
|
|
890
|
+
const signature = () => files.map((file) => {
|
|
891
|
+
try {
|
|
892
|
+
const stats = statSync(file);
|
|
893
|
+
return `${stats.mtimeMs}:${stats.size}`;
|
|
894
|
+
} catch {
|
|
895
|
+
return "absent";
|
|
896
|
+
}
|
|
897
|
+
}).join("|");
|
|
898
|
+
let last = signature();
|
|
899
|
+
const timer = setInterval(() => {
|
|
900
|
+
const now = signature();
|
|
901
|
+
if (now === last) return;
|
|
902
|
+
last = now;
|
|
903
|
+
onChange();
|
|
904
|
+
}, 500);
|
|
905
|
+
timer.unref();
|
|
906
|
+
return () => clearInterval(timer);
|
|
907
|
+
}
|
|
754
908
|
var BACK = /* @__PURE__ */ Symbol("veefore.back");
|
|
755
909
|
var MenuCancelled = class extends CliError {
|
|
756
910
|
constructor() {
|
|
@@ -765,6 +919,7 @@ async function select(question, real, settings = {}) {
|
|
|
765
919
|
const choosable = enabled.filter((index) => options[index]?.value !== BACK);
|
|
766
920
|
const fallback = choosable.includes(settings.initial ?? -1) ? settings.initial : choosable[0] ?? enabled[0];
|
|
767
921
|
if (!isInteractive()) return options[fallback].value;
|
|
922
|
+
flushCapture();
|
|
768
923
|
let cursor = fallback;
|
|
769
924
|
let rendered = 0;
|
|
770
925
|
const stdin = process.stdin;
|
|
@@ -794,6 +949,13 @@ async function select(question, real, settings = {}) {
|
|
|
794
949
|
`);
|
|
795
950
|
rendered = lines.length;
|
|
796
951
|
};
|
|
952
|
+
const repaint = () => {
|
|
953
|
+
if (!settings.redraw) return;
|
|
954
|
+
clearScreen();
|
|
955
|
+
settings.redraw();
|
|
956
|
+
rendered = 0;
|
|
957
|
+
draw();
|
|
958
|
+
};
|
|
797
959
|
const move = (direction) => {
|
|
798
960
|
const position = enabled.indexOf(cursor);
|
|
799
961
|
const next = (position + direction + enabled.length) % enabled.length;
|
|
@@ -802,12 +964,19 @@ async function select(question, real, settings = {}) {
|
|
|
802
964
|
return new Promise((resolve, reject) => {
|
|
803
965
|
const wasRaw = stdin.isRaw;
|
|
804
966
|
const restoreCursor = () => {
|
|
805
|
-
stdout.write("\x1B[?25h");
|
|
967
|
+
if (!inApp()) stdout.write("\x1B[?25h");
|
|
968
|
+
};
|
|
969
|
+
const stopWatching = settings.redraw ? watchFacts(repaint) : void 0;
|
|
970
|
+
const onResize = () => {
|
|
971
|
+
if (settings.redraw) repaint();
|
|
972
|
+
else draw();
|
|
806
973
|
};
|
|
807
974
|
const finish = (result) => {
|
|
808
975
|
stdin.off("keypress", onKey);
|
|
809
976
|
stdin.setRawMode(wasRaw);
|
|
810
977
|
stdin.pause();
|
|
978
|
+
stopWatching?.();
|
|
979
|
+
process.stdout.off("resize", onResize);
|
|
811
980
|
process.off("exit", restoreCursor);
|
|
812
981
|
if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
|
|
813
982
|
restoreCursor();
|
|
@@ -857,6 +1026,7 @@ async function select(question, real, settings = {}) {
|
|
|
857
1026
|
stdin.resume();
|
|
858
1027
|
stdout.write("\x1B[?25l");
|
|
859
1028
|
stdin.on("keypress", onKey);
|
|
1029
|
+
process.stdout.on("resize", onResize);
|
|
860
1030
|
draw();
|
|
861
1031
|
});
|
|
862
1032
|
}
|
|
@@ -912,11 +1082,38 @@ var symbol = {
|
|
|
912
1082
|
info: style.cyan("\u203A"),
|
|
913
1083
|
bullet: style.grey("\u2022")
|
|
914
1084
|
};
|
|
1085
|
+
var captured = null;
|
|
1086
|
+
function beginCapture() {
|
|
1087
|
+
captured = [];
|
|
1088
|
+
}
|
|
1089
|
+
function endCapture() {
|
|
1090
|
+
const lines = captured ?? [];
|
|
1091
|
+
captured = null;
|
|
1092
|
+
return lines;
|
|
1093
|
+
}
|
|
1094
|
+
function flushCapture() {
|
|
1095
|
+
if (captured === null) return;
|
|
1096
|
+
const lines = captured;
|
|
1097
|
+
captured = null;
|
|
1098
|
+
for (const line of lines) process.stdout.write(`${line}
|
|
1099
|
+
`);
|
|
1100
|
+
}
|
|
1101
|
+
function isCapturing() {
|
|
1102
|
+
return captured !== null;
|
|
1103
|
+
}
|
|
915
1104
|
function out(line = "") {
|
|
1105
|
+
if (captured !== null) {
|
|
1106
|
+
captured.push(line);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
916
1109
|
process.stdout.write(`${line}
|
|
917
1110
|
`);
|
|
918
1111
|
}
|
|
919
1112
|
function err(line = "") {
|
|
1113
|
+
if (captured !== null) {
|
|
1114
|
+
captured.push(line);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
920
1117
|
process.stderr.write(`${line}
|
|
921
1118
|
`);
|
|
922
1119
|
}
|
|
@@ -925,15 +1122,15 @@ function heading(text) {
|
|
|
925
1122
|
out(style.bold(text));
|
|
926
1123
|
out(style.grey("\u2500".repeat(Math.min(text.length, 72))));
|
|
927
1124
|
}
|
|
928
|
-
function table(columns2,
|
|
929
|
-
if (
|
|
1125
|
+
function table(columns2, rows2) {
|
|
1126
|
+
if (rows2.length === 0) {
|
|
930
1127
|
out(style.grey(" (none)"));
|
|
931
1128
|
return;
|
|
932
1129
|
}
|
|
933
1130
|
const widths = columns2.map((column) => {
|
|
934
1131
|
const longest = Math.max(
|
|
935
1132
|
column.header.length,
|
|
936
|
-
...
|
|
1133
|
+
...rows2.map((row) => (row[column.key] ?? "").length)
|
|
937
1134
|
);
|
|
938
1135
|
return Math.min(column.width ?? longest, longest);
|
|
939
1136
|
});
|
|
@@ -949,7 +1146,7 @@ function table(columns2, rows) {
|
|
|
949
1146
|
columns2.map((column) => column.header),
|
|
950
1147
|
style.grey
|
|
951
1148
|
);
|
|
952
|
-
for (const row of
|
|
1149
|
+
for (const row of rows2) {
|
|
953
1150
|
renderRow(
|
|
954
1151
|
columns2.map((column) => row[column.key] ?? ""),
|
|
955
1152
|
(text) => text
|
|
@@ -966,6 +1163,7 @@ function keyValue(label, value) {
|
|
|
966
1163
|
out(` ${style.grey(`${label}:`)} ${value}`);
|
|
967
1164
|
}
|
|
968
1165
|
async function ask(question, fallback = "") {
|
|
1166
|
+
flushCapture();
|
|
969
1167
|
if (!process.stdin.isTTY) return fallback;
|
|
970
1168
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
971
1169
|
try {
|
|
@@ -976,6 +1174,7 @@ async function ask(question, fallback = "") {
|
|
|
976
1174
|
}
|
|
977
1175
|
}
|
|
978
1176
|
async function askSecret(question) {
|
|
1177
|
+
flushCapture();
|
|
979
1178
|
if (!process.stdin.isTTY) {
|
|
980
1179
|
throw new Error(
|
|
981
1180
|
"A secret is required but the terminal is not interactive. Provide it through the documented environment variable instead."
|
|
@@ -1018,6 +1217,7 @@ async function askSecret(question) {
|
|
|
1018
1217
|
});
|
|
1019
1218
|
}
|
|
1020
1219
|
async function confirm(question, options = {}) {
|
|
1220
|
+
flushCapture();
|
|
1021
1221
|
if (options.yes) {
|
|
1022
1222
|
out(`${question} ${style.grey("[y/N]")} ${style.amber("y (--yes)")}`);
|
|
1023
1223
|
return true;
|
|
@@ -1040,6 +1240,12 @@ async function confirm(question, options = {}) {
|
|
|
1040
1240
|
return ["y", "yes"].includes(answer.toLowerCase());
|
|
1041
1241
|
}
|
|
1042
1242
|
function spinner(label) {
|
|
1243
|
+
if (isCapturing()) {
|
|
1244
|
+
return {
|
|
1245
|
+
succeed: (text) => out(`${symbol.pass} ${text ?? label}`),
|
|
1246
|
+
fail: (text) => out(`${symbol.fail} ${text ?? label}`)
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1043
1249
|
if (!process.stdout.isTTY) {
|
|
1044
1250
|
out(`${symbol.info} ${label}`);
|
|
1045
1251
|
return {
|
|
@@ -1163,6 +1369,7 @@ async function resolveContext(flags) {
|
|
|
1163
1369
|
};
|
|
1164
1370
|
}
|
|
1165
1371
|
async function login(controlPlaneUrl, options = {}) {
|
|
1372
|
+
flushCapture();
|
|
1166
1373
|
await assertControlPlane(controlPlaneUrl);
|
|
1167
1374
|
const ciCredentials = Boolean(process.env.VEEFORE_EMAIL && process.env.VEEFORE_PASSWORD);
|
|
1168
1375
|
if (options.method === "password" || options.method === void 0 && ciCredentials) {
|
|
@@ -1739,11 +1946,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
|
|
|
1739
1946
|
out(` ${style.red("This action is irreversible for this identity.")}`);
|
|
1740
1947
|
out();
|
|
1741
1948
|
}
|
|
1742
|
-
async function lifecycle(args, flags,
|
|
1949
|
+
async function lifecycle(args, flags, path13, resultingStatus) {
|
|
1743
1950
|
const context = await resolveContext(flags);
|
|
1744
1951
|
const agentId = positional(args) ?? context.config.agentId;
|
|
1745
1952
|
const agent = await context.api.get(`/v1/agents/${agentId}`);
|
|
1746
|
-
const reasonRequired =
|
|
1953
|
+
const reasonRequired = path13 === "suspend";
|
|
1747
1954
|
const reason = valueFor(args, "--reason");
|
|
1748
1955
|
if (reasonRequired && (!reason || reason.trim().length < 10)) {
|
|
1749
1956
|
throw new CliError(
|
|
@@ -1753,24 +1960,24 @@ async function lifecycle(args, flags, path12, resultingStatus) {
|
|
|
1753
1960
|
}
|
|
1754
1961
|
out();
|
|
1755
1962
|
out(
|
|
1756
|
-
`${symbol.info} ${
|
|
1963
|
+
`${symbol.info} ${path13} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
|
|
1757
1964
|
);
|
|
1758
|
-
if (
|
|
1965
|
+
if (path13 !== "resume") {
|
|
1759
1966
|
const confirmed = await confirm("Continue?", { yes: flags.yes });
|
|
1760
1967
|
if (!confirmed) {
|
|
1761
1968
|
out(style.grey("Cancelled."));
|
|
1762
1969
|
return 1;
|
|
1763
1970
|
}
|
|
1764
1971
|
}
|
|
1765
|
-
const actionSpinner = spinner(`Applying ${
|
|
1972
|
+
const actionSpinner = spinner(`Applying ${path13}`);
|
|
1766
1973
|
try {
|
|
1767
1974
|
await withStepUp(
|
|
1768
1975
|
context.api,
|
|
1769
|
-
() => context.api.post(`/v1/agents/${agentId}/${
|
|
1976
|
+
() => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
|
|
1770
1977
|
);
|
|
1771
1978
|
actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
|
|
1772
1979
|
} catch (error) {
|
|
1773
|
-
actionSpinner.fail(`Could not ${
|
|
1980
|
+
actionSpinner.fail(`Could not ${path13} the agent`);
|
|
1774
1981
|
throw error;
|
|
1775
1982
|
}
|
|
1776
1983
|
out();
|
|
@@ -2074,13 +2281,13 @@ async function exportEvents(args, flags) {
|
|
|
2074
2281
|
}
|
|
2075
2282
|
|
|
2076
2283
|
// src/commands/doctor.ts
|
|
2077
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
2078
|
-
import
|
|
2284
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
2285
|
+
import path4 from "node:path";
|
|
2079
2286
|
|
|
2080
2287
|
// src/project-checks.ts
|
|
2081
2288
|
import { spawnSync } from "node:child_process";
|
|
2082
2289
|
import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
2083
|
-
import
|
|
2290
|
+
import path3 from "node:path";
|
|
2084
2291
|
var MIN_APPROVAL_WAIT_SDK = "1.1.0";
|
|
2085
2292
|
function versionLessThan(a, b) {
|
|
2086
2293
|
if (!a) return true;
|
|
@@ -2097,19 +2304,19 @@ function versionLessThan(a, b) {
|
|
|
2097
2304
|
function installedSdks(root, options = {}) {
|
|
2098
2305
|
const found = [];
|
|
2099
2306
|
const seen = /* @__PURE__ */ new Set();
|
|
2100
|
-
const top =
|
|
2307
|
+
const top = path3.resolve(root);
|
|
2101
2308
|
const candidates = /* @__PURE__ */ new Set([top]);
|
|
2102
2309
|
for (const dir of options.dirs ?? []) {
|
|
2103
|
-
let current =
|
|
2310
|
+
let current = path3.resolve(top, dir);
|
|
2104
2311
|
while (current.startsWith(top)) {
|
|
2105
2312
|
candidates.add(current);
|
|
2106
|
-
const parent =
|
|
2313
|
+
const parent = path3.dirname(current);
|
|
2107
2314
|
if (parent === current) break;
|
|
2108
2315
|
current = parent;
|
|
2109
2316
|
}
|
|
2110
2317
|
}
|
|
2111
2318
|
for (const dir of candidates) {
|
|
2112
|
-
const manifest =
|
|
2319
|
+
const manifest = path3.join(dir, "node_modules", "@veefore", "sdk", "package.json");
|
|
2113
2320
|
if (!existsSync2(manifest)) continue;
|
|
2114
2321
|
let real = manifest;
|
|
2115
2322
|
try {
|
|
@@ -2126,7 +2333,7 @@ function installedSdks(root, options = {}) {
|
|
|
2126
2333
|
}
|
|
2127
2334
|
found.push({
|
|
2128
2335
|
language: "js",
|
|
2129
|
-
location:
|
|
2336
|
+
location: path3.relative(top, path3.dirname(manifest)) || ".",
|
|
2130
2337
|
version,
|
|
2131
2338
|
waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
|
|
2132
2339
|
});
|
|
@@ -2158,7 +2365,7 @@ function upgradeCommand(root, language) {
|
|
|
2158
2365
|
}
|
|
2159
2366
|
function configuredApprovalMode(root) {
|
|
2160
2367
|
for (const name of [".env.local", ".env"]) {
|
|
2161
|
-
const file =
|
|
2368
|
+
const file = path3.join(root, name);
|
|
2162
2369
|
if (!existsSync2(file)) continue;
|
|
2163
2370
|
const match = /^\s*(?:export\s+)?VEEFORE_APPROVAL_MODE\s*=\s*["']?(\w+)["']?/m.exec(
|
|
2164
2371
|
readFileSync2(file, "utf8")
|
|
@@ -2168,7 +2375,7 @@ function configuredApprovalMode(root) {
|
|
|
2168
2375
|
return null;
|
|
2169
2376
|
}
|
|
2170
2377
|
function sdkInstalled(root, language) {
|
|
2171
|
-
if (language === "js") return existsSync2(
|
|
2378
|
+
if (language === "js") return existsSync2(path3.join(root, "node_modules", "@veefore", "sdk"));
|
|
2172
2379
|
const python = projectPython(root);
|
|
2173
2380
|
if (!python) return false;
|
|
2174
2381
|
return spawnSync(python, ["-c", "import veefore"], { cwd: root, stdio: "ignore", timeout: 2e4 }).status === 0;
|
|
@@ -2177,18 +2384,18 @@ function typecheck(root, files) {
|
|
|
2177
2384
|
const name = "TypeScript typecheck";
|
|
2178
2385
|
const projects = files ? [
|
|
2179
2386
|
...new Set(
|
|
2180
|
-
files.filter((file) => /\.(m|c)?tsx?$/.test(file)).map((file) => nearestUp(root,
|
|
2387
|
+
files.filter((file) => /\.(m|c)?tsx?$/.test(file)).map((file) => nearestUp(root, path3.dirname(path3.join(root, file)), "tsconfig.json")).filter((dir) => dir !== null)
|
|
2181
2388
|
)
|
|
2182
|
-
] : existsSync2(
|
|
2389
|
+
] : existsSync2(path3.join(root, "tsconfig.json")) ? [root] : [];
|
|
2183
2390
|
if (projects.length === 0) return { name, ran: false, ok: true, detail: "no tsconfig.json" };
|
|
2184
2391
|
const binary = process.platform === "win32" ? "tsc.cmd" : "tsc";
|
|
2185
2392
|
for (const project of projects) {
|
|
2186
|
-
const bin = nearestUp(root, project,
|
|
2393
|
+
const bin = nearestUp(root, project, path3.join("node_modules", ".bin", binary));
|
|
2187
2394
|
if (!bin) {
|
|
2188
2395
|
return { name, ran: false, ok: true, detail: "TypeScript is not installed in this project" };
|
|
2189
2396
|
}
|
|
2190
2397
|
const result = spawnSync(
|
|
2191
|
-
|
|
2398
|
+
path3.join(bin, "node_modules", ".bin", binary),
|
|
2192
2399
|
["--noEmit", "-p", "."],
|
|
2193
2400
|
{
|
|
2194
2401
|
cwd: project,
|
|
@@ -2198,7 +2405,7 @@ function typecheck(root, files) {
|
|
|
2198
2405
|
);
|
|
2199
2406
|
if (result.status !== 0) {
|
|
2200
2407
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2201
|
-
const where =
|
|
2408
|
+
const where = path3.relative(root, project) || ".";
|
|
2202
2409
|
return {
|
|
2203
2410
|
name,
|
|
2204
2411
|
ran: true,
|
|
@@ -2211,12 +2418,12 @@ ${output.split("\n").slice(0, 12).join("\n")}`
|
|
|
2211
2418
|
return { name, ran: true, ok: true, detail: "" };
|
|
2212
2419
|
}
|
|
2213
2420
|
function nearestUp(root, start, entry) {
|
|
2214
|
-
const top =
|
|
2215
|
-
let dir =
|
|
2421
|
+
const top = path3.resolve(root);
|
|
2422
|
+
let dir = path3.resolve(start);
|
|
2216
2423
|
for (; ; ) {
|
|
2217
|
-
if (existsSync2(
|
|
2424
|
+
if (existsSync2(path3.join(dir, entry))) return dir;
|
|
2218
2425
|
if (dir === top) return null;
|
|
2219
|
-
const parent =
|
|
2426
|
+
const parent = path3.dirname(dir);
|
|
2220
2427
|
if (parent === dir || !parent.startsWith(top)) return null;
|
|
2221
2428
|
dir = parent;
|
|
2222
2429
|
}
|
|
@@ -2270,20 +2477,20 @@ function pythonSyntax(root, files) {
|
|
|
2270
2477
|
}
|
|
2271
2478
|
function projectPython(root) {
|
|
2272
2479
|
for (const candidate of [".venv/bin/python", "venv/bin/python", ".venv/Scripts/python.exe"]) {
|
|
2273
|
-
if (existsSync2(
|
|
2480
|
+
if (existsSync2(path3.join(root, candidate))) return path3.join(root, candidate);
|
|
2274
2481
|
}
|
|
2275
2482
|
const probe = spawnSync("python3", ["--version"], { stdio: "ignore" });
|
|
2276
2483
|
return probe.status === 0 ? "python3" : null;
|
|
2277
2484
|
}
|
|
2278
2485
|
function installCommand(root, language) {
|
|
2279
2486
|
if (language === "py") {
|
|
2280
|
-
if (existsSync2(
|
|
2281
|
-
if (existsSync2(
|
|
2487
|
+
if (existsSync2(path3.join(root, "uv.lock"))) return "uv add veefore-py";
|
|
2488
|
+
if (existsSync2(path3.join(root, "poetry.lock"))) return "poetry add veefore-py";
|
|
2282
2489
|
return "pip install veefore-py";
|
|
2283
2490
|
}
|
|
2284
|
-
if (existsSync2(
|
|
2285
|
-
if (existsSync2(
|
|
2286
|
-
if (existsSync2(
|
|
2491
|
+
if (existsSync2(path3.join(root, "pnpm-lock.yaml"))) return "pnpm add @veefore/sdk";
|
|
2492
|
+
if (existsSync2(path3.join(root, "yarn.lock"))) return "yarn add @veefore/sdk";
|
|
2493
|
+
if (existsSync2(path3.join(root, "bun.lockb")) || existsSync2(path3.join(root, "bun.lock"))) {
|
|
2287
2494
|
return "bun add @veefore/sdk";
|
|
2288
2495
|
}
|
|
2289
2496
|
return "npm install @veefore/sdk";
|
|
@@ -2403,8 +2610,8 @@ async function checkTransportSecurity(config, add) {
|
|
|
2403
2610
|
}
|
|
2404
2611
|
function checkLocalSecrets(config, add) {
|
|
2405
2612
|
const configFile = findProjectConfig();
|
|
2406
|
-
const projectDir = configFile ?
|
|
2407
|
-
const envFile =
|
|
2613
|
+
const projectDir = configFile ? path4.dirname(configFile) : process.cwd();
|
|
2614
|
+
const envFile = path4.join(projectDir, ".env.local");
|
|
2408
2615
|
if (!existsSync3(envFile)) {
|
|
2409
2616
|
add({
|
|
2410
2617
|
check: "Local agent credentials",
|
|
@@ -2414,7 +2621,7 @@ function checkLocalSecrets(config, add) {
|
|
|
2414
2621
|
});
|
|
2415
2622
|
return;
|
|
2416
2623
|
}
|
|
2417
|
-
const mode =
|
|
2624
|
+
const mode = statSync2(envFile).mode & 511;
|
|
2418
2625
|
if ((mode & 63) !== 0) {
|
|
2419
2626
|
add({
|
|
2420
2627
|
check: "Local agent credentials",
|
|
@@ -2429,7 +2636,7 @@ function checkLocalSecrets(config, add) {
|
|
|
2429
2636
|
message: `.env.local is mode ${mode.toString(8)} (owner only).`
|
|
2430
2637
|
});
|
|
2431
2638
|
}
|
|
2432
|
-
const gitignore =
|
|
2639
|
+
const gitignore = path4.join(projectDir, ".gitignore");
|
|
2433
2640
|
const ignored = existsSync3(gitignore) && readFileSync3(gitignore, "utf8").split(/\r?\n/).some((line) => line.trim() === ".env.local" || line.trim() === "*.local");
|
|
2434
2641
|
add({
|
|
2435
2642
|
check: "Secrets excluded from version control",
|
|
@@ -2757,8 +2964,8 @@ async function checkCredentialRotation(api, add) {
|
|
|
2757
2964
|
}
|
|
2758
2965
|
async function checkDependencies(add, dir) {
|
|
2759
2966
|
const configFile = findProjectConfig();
|
|
2760
|
-
const projectDir = dir ?? (configFile ?
|
|
2761
|
-
const manifest =
|
|
2967
|
+
const projectDir = dir ?? (configFile ? path4.dirname(configFile) : process.cwd());
|
|
2968
|
+
const manifest = path4.join(projectDir, "package.json");
|
|
2762
2969
|
if (!existsSync3(manifest)) {
|
|
2763
2970
|
add({
|
|
2764
2971
|
check: "SDK dependency",
|
|
@@ -2800,9 +3007,9 @@ async function checkDependencies(add, dir) {
|
|
|
2800
3007
|
}
|
|
2801
3008
|
function checkApprovalWaiting(add, projectDir) {
|
|
2802
3009
|
const configFile = findProjectConfig();
|
|
2803
|
-
const dir = projectDir ?? (configFile ?
|
|
3010
|
+
const dir = projectDir ?? (configFile ? path4.dirname(configFile) : process.cwd());
|
|
2804
3011
|
const python = ["pyproject.toml", "requirements.txt", "setup.py"].some(
|
|
2805
|
-
(name) => existsSync3(
|
|
3012
|
+
(name) => existsSync3(path4.join(dir, name))
|
|
2806
3013
|
);
|
|
2807
3014
|
const sdks = installedSdks(dir, { python });
|
|
2808
3015
|
const old = sdks.filter((sdk) => !sdk.waitsForApproval);
|
|
@@ -3094,11 +3301,11 @@ function isLoosening(from, to) {
|
|
|
3094
3301
|
|
|
3095
3302
|
// src/commands/init.ts
|
|
3096
3303
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3097
|
-
import
|
|
3304
|
+
import path12 from "node:path";
|
|
3098
3305
|
|
|
3099
3306
|
// src/integrate.ts
|
|
3100
3307
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3101
|
-
import
|
|
3308
|
+
import path9 from "node:path";
|
|
3102
3309
|
|
|
3103
3310
|
// ../types/src/permissions.ts
|
|
3104
3311
|
var PERMISSIONS = [
|
|
@@ -3435,6 +3642,11 @@ var EVENT_TYPES = [
|
|
|
3435
3642
|
"user.profile_updated",
|
|
3436
3643
|
"onboarding.completed",
|
|
3437
3644
|
"session.revoked",
|
|
3645
|
+
// Sign-in is served from its own origin, so a session cannot be carried
|
|
3646
|
+
// across to the console by cookie; it is re-issued there from a one-time
|
|
3647
|
+
// code. Recorded at both ends, so the trail shows one session ending and
|
|
3648
|
+
// its replacement beginning rather than an unexplained gap.
|
|
3649
|
+
"session.handed_over",
|
|
3438
3650
|
"cli.authorization_approved",
|
|
3439
3651
|
"cli.authorization_denied",
|
|
3440
3652
|
"cli.session_issued",
|
|
@@ -4368,21 +4580,21 @@ import {
|
|
|
4368
4580
|
readdirSync,
|
|
4369
4581
|
renameSync as renameSync2,
|
|
4370
4582
|
rmSync as rmSync2,
|
|
4371
|
-
statSync as
|
|
4583
|
+
statSync as statSync3,
|
|
4372
4584
|
writeFileSync as writeFileSync3
|
|
4373
4585
|
} from "node:fs";
|
|
4374
|
-
import
|
|
4375
|
-
var BACKUP_DIR =
|
|
4586
|
+
import path5 from "node:path";
|
|
4587
|
+
var BACKUP_DIR = path5.join(".veefore", "backups");
|
|
4376
4588
|
function sha256(text) {
|
|
4377
4589
|
return createHash2("sha256").update(text).digest("hex");
|
|
4378
4590
|
}
|
|
4379
4591
|
function createBackup(root, changes) {
|
|
4380
4592
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4381
4593
|
const id = createdAt.replace(/[:.]/g, "-");
|
|
4382
|
-
const dir =
|
|
4594
|
+
const dir = path5.join(root, BACKUP_DIR, id);
|
|
4383
4595
|
for (const change of changes) {
|
|
4384
|
-
const target =
|
|
4385
|
-
mkdirSync2(
|
|
4596
|
+
const target = path5.join(dir, "files", change.file);
|
|
4597
|
+
mkdirSync2(path5.dirname(target), { recursive: true });
|
|
4386
4598
|
writeFileSync3(target, change.before, { mode: 384 });
|
|
4387
4599
|
}
|
|
4388
4600
|
const files = changes.map((change) => ({
|
|
@@ -4391,7 +4603,7 @@ function createBackup(root, changes) {
|
|
|
4391
4603
|
afterSha256: sha256(change.after)
|
|
4392
4604
|
}));
|
|
4393
4605
|
writeFileSync3(
|
|
4394
|
-
|
|
4606
|
+
path5.join(dir, "manifest.json"),
|
|
4395
4607
|
`${JSON.stringify({ version: 1, createdAt, files }, null, 2)}
|
|
4396
4608
|
`,
|
|
4397
4609
|
{ mode: 384 }
|
|
@@ -4399,28 +4611,28 @@ function createBackup(root, changes) {
|
|
|
4399
4611
|
return { id, dir, createdAt, files };
|
|
4400
4612
|
}
|
|
4401
4613
|
function writeFilePreservingMode(file, content) {
|
|
4402
|
-
const mode = existsSync4(file) ?
|
|
4614
|
+
const mode = existsSync4(file) ? statSync3(file).mode & 511 : 420;
|
|
4403
4615
|
const temp = `${file}.veefore-tmp-${process.pid}`;
|
|
4404
4616
|
writeFileSync3(temp, content, { mode });
|
|
4405
4617
|
renameSync2(temp, file);
|
|
4406
4618
|
}
|
|
4407
4619
|
function latestBackup(root) {
|
|
4408
|
-
const base2 =
|
|
4620
|
+
const base2 = path5.join(root, BACKUP_DIR);
|
|
4409
4621
|
if (!existsSync4(base2)) return null;
|
|
4410
|
-
const ids = readdirSync(base2).filter((entry) => existsSync4(
|
|
4622
|
+
const ids = readdirSync(base2).filter((entry) => existsSync4(path5.join(base2, entry, "manifest.json"))).sort();
|
|
4411
4623
|
const id = ids[ids.length - 1];
|
|
4412
4624
|
if (!id) return null;
|
|
4413
|
-
const dir =
|
|
4414
|
-
const manifest = JSON.parse(readFileSync4(
|
|
4625
|
+
const dir = path5.join(base2, id);
|
|
4626
|
+
const manifest = JSON.parse(readFileSync4(path5.join(dir, "manifest.json"), "utf8"));
|
|
4415
4627
|
return { id, dir, createdAt: manifest.createdAt, files: manifest.files };
|
|
4416
4628
|
}
|
|
4417
4629
|
function restoreBackup(root, backup, options = {}) {
|
|
4418
4630
|
const restored = [];
|
|
4419
4631
|
const conflicts = [];
|
|
4420
4632
|
for (const entry of backup.files) {
|
|
4421
|
-
const target =
|
|
4633
|
+
const target = path5.join(root, entry.file);
|
|
4422
4634
|
const current = existsSync4(target) ? readFileSync4(target, "utf8") : null;
|
|
4423
|
-
const original = readFileSync4(
|
|
4635
|
+
const original = readFileSync4(path5.join(backup.dir, "files", entry.file), "utf8");
|
|
4424
4636
|
if (current !== null && sha256(current) === entry.beforeSha256) {
|
|
4425
4637
|
restored.push(entry.file);
|
|
4426
4638
|
continue;
|
|
@@ -4440,15 +4652,15 @@ function restoreBackup(root, backup, options = {}) {
|
|
|
4440
4652
|
|
|
4441
4653
|
// src/rewrite.ts
|
|
4442
4654
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4443
|
-
import
|
|
4655
|
+
import path8 from "node:path";
|
|
4444
4656
|
|
|
4445
4657
|
// src/scan.ts
|
|
4446
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as
|
|
4447
|
-
import
|
|
4658
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
4659
|
+
import path7 from "node:path";
|
|
4448
4660
|
|
|
4449
4661
|
// src/framework.ts
|
|
4450
4662
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
4451
|
-
import
|
|
4663
|
+
import path6 from "node:path";
|
|
4452
4664
|
var SIGNATURES = [
|
|
4453
4665
|
{
|
|
4454
4666
|
key: "langchain",
|
|
@@ -4530,8 +4742,8 @@ function detectFramework(dir = process.cwd()) {
|
|
|
4530
4742
|
};
|
|
4531
4743
|
}
|
|
4532
4744
|
}
|
|
4533
|
-
if (nodeDeps.size > 0 || existsSync5(
|
|
4534
|
-
const typescript = existsSync5(
|
|
4745
|
+
if (nodeDeps.size > 0 || existsSync5(path6.join(dir, "package.json"))) {
|
|
4746
|
+
const typescript = existsSync5(path6.join(dir, "tsconfig.json")) || nodeDeps.has("typescript");
|
|
4535
4747
|
return {
|
|
4536
4748
|
key: "custom",
|
|
4537
4749
|
name: "Custom agent",
|
|
@@ -4553,7 +4765,7 @@ function normalize(name) {
|
|
|
4553
4765
|
return name.toLowerCase().replace(/_/g, "-");
|
|
4554
4766
|
}
|
|
4555
4767
|
function readPackageJsonDependencies(dir) {
|
|
4556
|
-
const file =
|
|
4768
|
+
const file = path6.join(dir, "package.json");
|
|
4557
4769
|
if (!existsSync5(file)) return /* @__PURE__ */ new Set();
|
|
4558
4770
|
try {
|
|
4559
4771
|
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
@@ -4570,12 +4782,12 @@ function readPackageJsonDependencies(dir) {
|
|
|
4570
4782
|
}
|
|
4571
4783
|
function hasPythonProject(dir) {
|
|
4572
4784
|
return ["pyproject.toml", "requirements.txt", "Pipfile", "setup.py"].some(
|
|
4573
|
-
(file) => existsSync5(
|
|
4785
|
+
(file) => existsSync5(path6.join(dir, file))
|
|
4574
4786
|
);
|
|
4575
4787
|
}
|
|
4576
4788
|
function readPythonDependencies(dir) {
|
|
4577
4789
|
const found = /* @__PURE__ */ new Set();
|
|
4578
|
-
const requirements =
|
|
4790
|
+
const requirements = path6.join(dir, "requirements.txt");
|
|
4579
4791
|
if (existsSync5(requirements)) {
|
|
4580
4792
|
for (const line of readFileSync5(requirements, "utf8").split(/\r?\n/)) {
|
|
4581
4793
|
const trimmed = line.trim();
|
|
@@ -4584,7 +4796,7 @@ function readPythonDependencies(dir) {
|
|
|
4584
4796
|
if (name) found.add(normalize(name));
|
|
4585
4797
|
}
|
|
4586
4798
|
}
|
|
4587
|
-
const pyproject =
|
|
4799
|
+
const pyproject = path6.join(dir, "pyproject.toml");
|
|
4588
4800
|
if (existsSync5(pyproject)) {
|
|
4589
4801
|
const text = readFileSync5(pyproject, "utf8");
|
|
4590
4802
|
for (const match of text.matchAll(
|
|
@@ -4616,7 +4828,7 @@ function findEntryPoints(dir = process.cwd(), limit = 5) {
|
|
|
4616
4828
|
"app.py",
|
|
4617
4829
|
"src/main.py"
|
|
4618
4830
|
];
|
|
4619
|
-
const found = candidates.filter((file) => existsSync5(
|
|
4831
|
+
const found = candidates.filter((file) => existsSync5(path6.join(dir, file)));
|
|
4620
4832
|
if (found.length > 0) return found.slice(0, limit);
|
|
4621
4833
|
try {
|
|
4622
4834
|
return readdirSync2(dir).filter((file) => /\.(ts|js|py)$/.test(file)).slice(0, limit);
|
|
@@ -4720,7 +4932,7 @@ function scanProject(root = process.cwd()) {
|
|
|
4720
4932
|
for (const relative of files) {
|
|
4721
4933
|
let text;
|
|
4722
4934
|
try {
|
|
4723
|
-
text = readFileSync6(
|
|
4935
|
+
text = readFileSync6(path7.join(root, relative), "utf8");
|
|
4724
4936
|
} catch {
|
|
4725
4937
|
continue;
|
|
4726
4938
|
}
|
|
@@ -5219,10 +5431,10 @@ function walk(root, dir, out2) {
|
|
|
5219
5431
|
if (entry.startsWith(".") && entry !== ".") {
|
|
5220
5432
|
continue;
|
|
5221
5433
|
}
|
|
5222
|
-
const full =
|
|
5434
|
+
const full = path7.join(dir, entry);
|
|
5223
5435
|
let stats;
|
|
5224
5436
|
try {
|
|
5225
|
-
stats =
|
|
5437
|
+
stats = statSync4(full);
|
|
5226
5438
|
} catch {
|
|
5227
5439
|
continue;
|
|
5228
5440
|
}
|
|
@@ -5232,14 +5444,14 @@ function walk(root, dir, out2) {
|
|
|
5232
5444
|
continue;
|
|
5233
5445
|
}
|
|
5234
5446
|
if (!stats.isFile() || stats.size > MAX_FILE_BYTES) continue;
|
|
5235
|
-
if (!SOURCE_EXTENSIONS.has(
|
|
5447
|
+
if (!SOURCE_EXTENSIONS.has(path7.extname(entry))) continue;
|
|
5236
5448
|
if (/\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$/.test(entry)) continue;
|
|
5237
|
-
out2.push(
|
|
5449
|
+
out2.push(path7.relative(root, full));
|
|
5238
5450
|
}
|
|
5239
5451
|
return false;
|
|
5240
5452
|
}
|
|
5241
5453
|
function readProjectName(root) {
|
|
5242
|
-
const pkg =
|
|
5454
|
+
const pkg = path7.join(root, "package.json");
|
|
5243
5455
|
if (existsSync6(pkg)) {
|
|
5244
5456
|
try {
|
|
5245
5457
|
const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
|
|
@@ -5247,12 +5459,12 @@ function readProjectName(root) {
|
|
|
5247
5459
|
} catch {
|
|
5248
5460
|
}
|
|
5249
5461
|
}
|
|
5250
|
-
const pyproject =
|
|
5462
|
+
const pyproject = path7.join(root, "pyproject.toml");
|
|
5251
5463
|
if (existsSync6(pyproject)) {
|
|
5252
5464
|
const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync6(pyproject, "utf8"))?.[1];
|
|
5253
5465
|
if (name) return name.trim();
|
|
5254
5466
|
}
|
|
5255
|
-
return
|
|
5467
|
+
return path7.basename(root) || null;
|
|
5256
5468
|
}
|
|
5257
5469
|
function stringField(body, field) {
|
|
5258
5470
|
const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
|
|
@@ -5307,7 +5519,7 @@ function planRewrite(root, files, toolKeys) {
|
|
|
5307
5519
|
for (const file of files) {
|
|
5308
5520
|
let before;
|
|
5309
5521
|
try {
|
|
5310
|
-
before = readFileSync7(
|
|
5522
|
+
before = readFileSync7(path8.join(root, file), "utf8");
|
|
5311
5523
|
} catch {
|
|
5312
5524
|
continue;
|
|
5313
5525
|
}
|
|
@@ -6297,13 +6509,13 @@ async function integrate(root, toolKeys, options) {
|
|
|
6297
6509
|
toWrite.map((file) => file.file)
|
|
6298
6510
|
) : null;
|
|
6299
6511
|
for (const file of toWrite) {
|
|
6300
|
-
if (readFileSync8(
|
|
6512
|
+
if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
|
|
6301
6513
|
out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
|
|
6302
6514
|
return { status: "declined", plan, checks: [], backupId: null, manual };
|
|
6303
6515
|
}
|
|
6304
6516
|
}
|
|
6305
6517
|
const backup = createBackup(root, toWrite);
|
|
6306
|
-
for (const file of toWrite) writeFilePreservingMode(
|
|
6518
|
+
for (const file of toWrite) writeFilePreservingMode(path9.join(root, file.file), file.after);
|
|
6307
6519
|
out(
|
|
6308
6520
|
`${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
|
|
6309
6521
|
);
|
|
@@ -6374,7 +6586,7 @@ async function integrate(root, toolKeys, options) {
|
|
|
6374
6586
|
function reportSdkReadiness(root, files) {
|
|
6375
6587
|
const problems = [];
|
|
6376
6588
|
if (files.length === 0) return problems;
|
|
6377
|
-
const dirs = [...new Set(files.map((file) =>
|
|
6589
|
+
const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
|
|
6378
6590
|
const python = files.some((file) => file.endsWith(".py"));
|
|
6379
6591
|
const sdks = installedSdks(root, { dirs, python });
|
|
6380
6592
|
for (const sdk of sdks) {
|
|
@@ -6451,7 +6663,7 @@ async function removeGuards(root, options) {
|
|
|
6451
6663
|
const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
|
|
6452
6664
|
const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
|
|
6453
6665
|
const backup = createBackup(root, toWrite);
|
|
6454
|
-
for (const entry of toWrite) writeFilePreservingMode(
|
|
6666
|
+
for (const entry of toWrite) writeFilePreservingMode(path9.join(root, entry.file), entry.after);
|
|
6455
6667
|
const checks = [];
|
|
6456
6668
|
const py = changed.filter((file) => file.endsWith(".py"));
|
|
6457
6669
|
if (py.length > 0) checks.push(pythonSyntax(root, py));
|
|
@@ -6475,7 +6687,7 @@ function currentText(root, plan, file) {
|
|
|
6475
6687
|
const entry = plan.files.find((candidate) => candidate.file === file);
|
|
6476
6688
|
if (entry) return entry.after;
|
|
6477
6689
|
try {
|
|
6478
|
-
return readFileSync8(
|
|
6690
|
+
return readFileSync8(path9.join(root, file), "utf8");
|
|
6479
6691
|
} catch {
|
|
6480
6692
|
return null;
|
|
6481
6693
|
}
|
|
@@ -6500,7 +6712,7 @@ function applyDispatch(root, plan, file, after, line, items, via) {
|
|
|
6500
6712
|
function readSources(root) {
|
|
6501
6713
|
return listSourceFiles(root).flatMap((file) => {
|
|
6502
6714
|
try {
|
|
6503
|
-
return [{ file, text: readFileSync8(
|
|
6715
|
+
return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
|
|
6504
6716
|
} catch {
|
|
6505
6717
|
return [];
|
|
6506
6718
|
}
|
|
@@ -6622,7 +6834,7 @@ function printPlan(plan) {
|
|
|
6622
6834
|
|
|
6623
6835
|
// src/commands/remove.ts
|
|
6624
6836
|
import { rmSync as rmSync3 } from "node:fs";
|
|
6625
|
-
import
|
|
6837
|
+
import path10 from "node:path";
|
|
6626
6838
|
|
|
6627
6839
|
// src/discover.ts
|
|
6628
6840
|
var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
|
|
@@ -6736,7 +6948,7 @@ async function removeCommand(args, flags, known) {
|
|
|
6736
6948
|
if (!file || !config) {
|
|
6737
6949
|
throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
|
|
6738
6950
|
}
|
|
6739
|
-
const root =
|
|
6951
|
+
const root = path10.dirname(file);
|
|
6740
6952
|
const state = known ?? await agentState(config);
|
|
6741
6953
|
heading("Remove Veefore from this project");
|
|
6742
6954
|
details([
|
|
@@ -7270,10 +7482,10 @@ async function resolveAuthorizedTools(context, filter) {
|
|
|
7270
7482
|
|
|
7271
7483
|
// src/panel.ts
|
|
7272
7484
|
import { homedir as homedir2 } from "node:os";
|
|
7273
|
-
import
|
|
7485
|
+
import path11 from "node:path";
|
|
7274
7486
|
|
|
7275
7487
|
// src/version.ts
|
|
7276
|
-
var CLI_VERSION = "1.
|
|
7488
|
+
var CLI_VERSION = "1.4.1";
|
|
7277
7489
|
|
|
7278
7490
|
// src/panel.ts
|
|
7279
7491
|
function welcomeSource(flags = {}) {
|
|
@@ -7297,7 +7509,7 @@ function welcomeSource(flags = {}) {
|
|
|
7297
7509
|
}
|
|
7298
7510
|
function tilde(dir) {
|
|
7299
7511
|
const home = homedir2();
|
|
7300
|
-
return dir === home || dir.startsWith(`${home}${
|
|
7512
|
+
return dir === home || dir.startsWith(`${home}${path11.sep}`) ? `~${dir.slice(home.length)}` : dir;
|
|
7301
7513
|
}
|
|
7302
7514
|
function hostOf(url) {
|
|
7303
7515
|
try {
|
|
@@ -7637,1542 +7849,1640 @@ function sourceLabel(source) {
|
|
|
7637
7849
|
}
|
|
7638
7850
|
}
|
|
7639
7851
|
|
|
7640
|
-
// src/commands/
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
if (existing && !options.force && isInteractive() && !flags.json) {
|
|
7646
|
-
return projectHome(existing, flags);
|
|
7647
|
-
}
|
|
7648
|
-
if (existing && !options.force) {
|
|
7649
|
-
out();
|
|
7650
|
-
out(`${symbol.warn} This project is already connected to Veefore.`);
|
|
7651
|
-
details([
|
|
7652
|
-
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
7653
|
-
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
7654
|
-
["Control plane", existing.controlPlaneUrl]
|
|
7655
|
-
]);
|
|
7656
|
-
out();
|
|
7657
|
-
out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
|
|
7852
|
+
// src/commands/scan.ts
|
|
7853
|
+
async function scanCommand(_args, flags) {
|
|
7854
|
+
const scan = scanProject(process.cwd());
|
|
7855
|
+
if (flags.json) {
|
|
7856
|
+
out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
|
|
7658
7857
|
return 0;
|
|
7659
7858
|
}
|
|
7660
|
-
heading("Veefore
|
|
7661
|
-
const framework = detectFramework();
|
|
7859
|
+
heading("Veefore scan");
|
|
7662
7860
|
out(
|
|
7663
|
-
`${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language},
|
|
7861
|
+
`${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
|
|
7664
7862
|
);
|
|
7665
|
-
if (framework.key === "unknown") {
|
|
7666
|
-
out(
|
|
7667
|
-
style.grey(
|
|
7668
|
-
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
7669
|
-
)
|
|
7670
|
-
);
|
|
7671
|
-
}
|
|
7672
|
-
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
7673
|
-
const controlPlaneUrl = found.controlPlaneUrl;
|
|
7674
|
-
if (found.source !== "entered") {
|
|
7675
|
-
out(
|
|
7676
|
-
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
7677
|
-
);
|
|
7678
|
-
}
|
|
7679
|
-
let session = loadSession(controlPlaneUrl);
|
|
7680
|
-
if (!session) {
|
|
7681
|
-
await login(controlPlaneUrl);
|
|
7682
|
-
session = loadSession(controlPlaneUrl);
|
|
7683
|
-
}
|
|
7684
|
-
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
7685
|
-
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
7686
|
-
let me = await api.get("/v1/auth/session");
|
|
7687
|
-
const STEPS = ["organization", "workspace", "environment"];
|
|
7688
|
-
const asked = /* @__PURE__ */ new Map();
|
|
7689
|
-
let organization;
|
|
7690
|
-
let workspace;
|
|
7691
|
-
let environment;
|
|
7692
|
-
for (let at = 0; at < STEPS.length; ) {
|
|
7693
|
-
const step = STEPS[at];
|
|
7694
|
-
const answer = step === "organization" ? await selectOrganization(api, me, options, flags) : step === "workspace" ? await selectWorkspace(api, me, options, flags) : await selectEnvironment(api, me, workspace.id, options, flags);
|
|
7695
|
-
if (answer === BACK) {
|
|
7696
|
-
let previous = at - 1;
|
|
7697
|
-
while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
|
|
7698
|
-
if (previous < 0) {
|
|
7699
|
-
throw new MenuCancelled();
|
|
7700
|
-
}
|
|
7701
|
-
at = previous;
|
|
7702
|
-
continue;
|
|
7703
|
-
}
|
|
7704
|
-
asked.set(step, answer.asked);
|
|
7705
|
-
if (step === "organization") {
|
|
7706
|
-
organization = answer.value;
|
|
7707
|
-
if (answer.asked) me = await api.get("/v1/auth/session");
|
|
7708
|
-
} else if (step === "workspace") {
|
|
7709
|
-
workspace = answer.value;
|
|
7710
|
-
} else {
|
|
7711
|
-
environment = answer.value;
|
|
7712
|
-
}
|
|
7713
|
-
at += 1;
|
|
7714
|
-
}
|
|
7715
|
-
heading("Looking at your project");
|
|
7716
|
-
const scan = scanProject(process.cwd());
|
|
7717
7863
|
printScan(scan);
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
printPlan2(plan, environment);
|
|
7733
|
-
out();
|
|
7734
|
-
const proceed = await confirm("Set this up?", { yes: flags.yes });
|
|
7735
|
-
if (proceed) break;
|
|
7736
|
-
if (flags.yes || options.agentName) {
|
|
7737
|
-
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
7738
|
-
return 1;
|
|
7739
|
-
}
|
|
7740
|
-
const next = await select(
|
|
7741
|
-
"What would you like to do?",
|
|
7864
|
+
out();
|
|
7865
|
+
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
7866
|
+
return 0;
|
|
7867
|
+
}
|
|
7868
|
+
|
|
7869
|
+
// src/help.ts
|
|
7870
|
+
var COMMANDS = [
|
|
7871
|
+
{
|
|
7872
|
+
name: "init",
|
|
7873
|
+
summary: "Set up this project with Veefore. Also runs as plain `veefore`.",
|
|
7874
|
+
usage: "veefore init [options]",
|
|
7875
|
+
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.",
|
|
7876
|
+
options: [
|
|
7877
|
+
["--force", "Replace an existing veefore.config.json."],
|
|
7742
7878
|
[
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
{ label: "Leave setup", value: "leave" }
|
|
7879
|
+
"--new-agent",
|
|
7880
|
+
"Create a separate agent even if one with this name exists. By default the existing agent is reused with a new key, so its delegations and limits keep applying."
|
|
7746
7881
|
],
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7882
|
+
["--control-plane <url>", "Self-hosted Veefore address. Needed once; remembered after."],
|
|
7883
|
+
["--gateway <url>", "Gateway base URL that agents will call."],
|
|
7884
|
+
["--organization <id>", "Skip the organization prompt."],
|
|
7885
|
+
["--workspace <id>", "Skip the workspace prompt."],
|
|
7886
|
+
["--environment <id>", "Skip the environment prompt."],
|
|
7887
|
+
["--name <name>", "Agent display name."],
|
|
7888
|
+
["--capability <key>", "Grant a capability. Repeatable."],
|
|
7889
|
+
["--skip-policy", "Do not create a starter policy."],
|
|
7890
|
+
["--skip-code", "Do not offer to change your code; run `veefore integrate` later."],
|
|
7891
|
+
[
|
|
7892
|
+
"--approval-mode <wait|throw>",
|
|
7893
|
+
"When approval is required, guarded tools wait for the decision (default) or return at once."
|
|
7894
|
+
]
|
|
7895
|
+
],
|
|
7896
|
+
examples: ["veefore init", "veefore init --name finance-agent --capability refund.create"]
|
|
7897
|
+
},
|
|
7898
|
+
{
|
|
7899
|
+
name: "scan",
|
|
7900
|
+
summary: "Preview what setup would find, without changing anything.",
|
|
7901
|
+
usage: "veefore scan [--json]",
|
|
7902
|
+
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.",
|
|
7903
|
+
options: [["--json", "Machine-readable scan and the setup plan it implies."]]
|
|
7904
|
+
},
|
|
7905
|
+
{
|
|
7906
|
+
name: "integrate",
|
|
7907
|
+
summary: "Wrap your registered tools in guard() so Veefore decides every call.",
|
|
7908
|
+
usage: "veefore integrate [--dry-run] [--check] [--assist=manual|own|veefore|off] [--yes]",
|
|
7909
|
+
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.",
|
|
7910
|
+
options: [
|
|
7911
|
+
["--dry-run", "Show the diff and write nothing."],
|
|
7912
|
+
["--check", "Report which registered tools are guarded in the code. Exits 1 if any are not."],
|
|
7913
|
+
["--assist=<how>", "manual (default when not interactive), own, veefore, or off."],
|
|
7914
|
+
["--yes", "Apply without asking."]
|
|
7915
|
+
],
|
|
7916
|
+
examples: [
|
|
7917
|
+
"veefore integrate --dry-run",
|
|
7918
|
+
"veefore integrate --assist=manual",
|
|
7919
|
+
"ANTHROPIC_API_KEY=\u2026 veefore integrate --assist=own",
|
|
7920
|
+
"veefore integrate --check"
|
|
7921
|
+
]
|
|
7922
|
+
},
|
|
7923
|
+
{
|
|
7924
|
+
name: "undo",
|
|
7925
|
+
summary: "Restore the files the last `veefore integrate` changed.",
|
|
7926
|
+
usage: "veefore undo [--force]",
|
|
7927
|
+
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.",
|
|
7928
|
+
options: [["--force", "Restore files even if they were edited after the rewrite."]]
|
|
7929
|
+
},
|
|
7930
|
+
{
|
|
7931
|
+
name: "remove",
|
|
7932
|
+
summary: "Take Veefore out of this project: guard() from the code, then disconnect.",
|
|
7933
|
+
usage: "veefore remove [--dry-run] [--keep-agent]",
|
|
7934
|
+
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.",
|
|
7935
|
+
options: [
|
|
7936
|
+
["--dry-run", "Show what would change and write nothing."],
|
|
7937
|
+
["--keep-agent", "Do not offer to revoke the agent in Veefore."]
|
|
7938
|
+
],
|
|
7939
|
+
examples: ["veefore remove --dry-run", "veefore remove"]
|
|
7940
|
+
},
|
|
7941
|
+
{
|
|
7942
|
+
name: "dev",
|
|
7943
|
+
summary: "Watch decisions live while you build.",
|
|
7944
|
+
usage: "veefore dev [--all]",
|
|
7945
|
+
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.",
|
|
7946
|
+
options: [["--all", "Stream decisions for every agent in the environment."]]
|
|
7947
|
+
},
|
|
7948
|
+
{
|
|
7949
|
+
name: "doctor",
|
|
7950
|
+
summary: "Audit the security posture of this setup.",
|
|
7951
|
+
usage: "veefore doctor [--strict]",
|
|
7952
|
+
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.",
|
|
7953
|
+
options: [["--strict", "Exit non-zero on warnings as well as failures."]]
|
|
7954
|
+
},
|
|
7955
|
+
{
|
|
7956
|
+
name: "status",
|
|
7957
|
+
summary: "Show the current environment, mode and agent health.",
|
|
7958
|
+
usage: "veefore status",
|
|
7959
|
+
description: "Prints the active organization, workspace and environment, the enforcement mode, the agent lifecycle state, gateway health, and the last few decisions."
|
|
7960
|
+
},
|
|
7961
|
+
{
|
|
7962
|
+
name: "test",
|
|
7963
|
+
summary: "Verify enforcement works, without performing any action.",
|
|
7964
|
+
usage: "veefore test [--tool <key>]",
|
|
7965
|
+
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.",
|
|
7966
|
+
options: [["--tool <key>", "Probe a single tool."]]
|
|
7967
|
+
},
|
|
7968
|
+
{
|
|
7969
|
+
name: "agents",
|
|
7970
|
+
summary: "Inspect and control the agent fleet.",
|
|
7971
|
+
usage: "veefore agents <list|show|pause|resume|suspend|revoke|rotate-key> [agentId]",
|
|
7972
|
+
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.",
|
|
7973
|
+
options: [
|
|
7974
|
+
["--all", "List across the whole organization, not just this environment."],
|
|
7975
|
+
["--status <status>", "Filter by lifecycle status."],
|
|
7976
|
+
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
7977
|
+
],
|
|
7978
|
+
examples: [
|
|
7979
|
+
"veefore agents list",
|
|
7980
|
+
'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
|
|
7981
|
+
]
|
|
7982
|
+
},
|
|
7983
|
+
{
|
|
7984
|
+
name: "policies",
|
|
7985
|
+
summary: "List, test and deploy policies.",
|
|
7986
|
+
usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
|
|
7987
|
+
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.",
|
|
7988
|
+
options: [
|
|
7989
|
+
["--version <n>", "Version to deploy, for `deploy`."],
|
|
7990
|
+
["--from <n> --to <n>", "Versions to compare, for `diff`."],
|
|
7991
|
+
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
7992
|
+
],
|
|
7993
|
+
examples: [
|
|
7994
|
+
"veefore policies list",
|
|
7995
|
+
"veefore policies test",
|
|
7996
|
+
"veefore policies deploy pol_01ABC"
|
|
7997
|
+
]
|
|
7998
|
+
},
|
|
7999
|
+
{
|
|
8000
|
+
name: "logs",
|
|
8001
|
+
summary: "Read or stream the decision log.",
|
|
8002
|
+
usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
|
|
8003
|
+
description: "Every line is a server-side decision with the policy and rule that produced it.",
|
|
8004
|
+
options: [
|
|
8005
|
+
["--follow, -f", "Stream new decisions as they happen."],
|
|
8006
|
+
["--decision <d>", "Filter by decision, e.g. BLOCK."],
|
|
8007
|
+
["--agent <id>", "Filter by agent."],
|
|
8008
|
+
["--all", "Include every agent in the environment."],
|
|
8009
|
+
["--limit <n>", "Number of recent decisions to show."]
|
|
8010
|
+
]
|
|
8011
|
+
},
|
|
8012
|
+
{
|
|
8013
|
+
name: "audit",
|
|
8014
|
+
summary: "Query, verify and export the audit trail.",
|
|
8015
|
+
usage: "veefore audit <list|show|verify|export> [eventId]",
|
|
8016
|
+
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.",
|
|
8017
|
+
options: [
|
|
8018
|
+
["--severity <s>", "Filter by severity."],
|
|
8019
|
+
["--type <t>", "Filter by event type."],
|
|
8020
|
+
["--from <iso> --to <iso>", "Time range."],
|
|
8021
|
+
["--format <json|csv>", "Export format."],
|
|
8022
|
+
['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
|
|
8023
|
+
["--out <file>", "Write the export to a file (mode 0600)."]
|
|
8024
|
+
],
|
|
8025
|
+
examples: [
|
|
8026
|
+
"veefore audit verify",
|
|
8027
|
+
'veefore audit export --format csv --reason "Quarterly SOC 2 evidence" --out audit.csv'
|
|
8028
|
+
]
|
|
8029
|
+
},
|
|
8030
|
+
{
|
|
8031
|
+
name: "deploy",
|
|
8032
|
+
summary: "Promote the environment to enforcement.",
|
|
8033
|
+
usage: "veefore deploy [--mode ENFORCE|SIMULATE|OBSERVE]",
|
|
8034
|
+
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.",
|
|
8035
|
+
options: [
|
|
8036
|
+
["--mode <mode>", "Target mode. Defaults to ENFORCE."],
|
|
8037
|
+
['--reason "<text>"', "Reason recorded in the audit trail."],
|
|
8038
|
+
["--skip-preflight", "Incident use only. Recorded in the audit reason."]
|
|
8039
|
+
]
|
|
8040
|
+
},
|
|
8041
|
+
{
|
|
8042
|
+
name: "login",
|
|
8043
|
+
summary: "Sign in through your browser and store a session token.",
|
|
8044
|
+
usage: "veefore login [--device] [--password] [--control-plane <url>]",
|
|
8045
|
+
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.",
|
|
8046
|
+
options: [
|
|
8047
|
+
["--device", "Show a link and a code instead of opening a browser here."],
|
|
8048
|
+
["--password", "Type email and password in the terminal instead of using the browser."]
|
|
8049
|
+
]
|
|
8050
|
+
},
|
|
8051
|
+
{
|
|
8052
|
+
name: "logout",
|
|
8053
|
+
summary: "Sign out of Veefore on this machine.",
|
|
8054
|
+
usage: "veefore logout [--all]",
|
|
8055
|
+
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.',
|
|
8056
|
+
options: [["--all", "Sign out of every control plane this machine is signed in to."]]
|
|
7796
8057
|
}
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
framework: framework.key === "unknown" ? null : framework.key,
|
|
7813
|
-
declaredTools: created.tools.map((tool) => tool.key),
|
|
7814
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7815
|
-
};
|
|
7816
|
-
const configFile = writeProjectConfig(config);
|
|
8058
|
+
];
|
|
8059
|
+
var GLOBAL_OPTIONS = [
|
|
8060
|
+
["--json", "Machine-readable output."],
|
|
8061
|
+
["--yes, -y", "Assume yes for confirmations. Never skips step-up authentication."],
|
|
8062
|
+
["--verbose", "Expanded output."],
|
|
8063
|
+
[
|
|
8064
|
+
"--control-plane <url>",
|
|
8065
|
+
"Use this Veefore instead of the one found automatically (hosted, or the last you signed in to)."
|
|
8066
|
+
],
|
|
8067
|
+
["--gateway <url>", "Override the configured gateway URL."],
|
|
8068
|
+
["--version, -V", "Print the CLI version."],
|
|
8069
|
+
["--help, -h", "Show help."]
|
|
8070
|
+
];
|
|
8071
|
+
function printRootHelp(version) {
|
|
8072
|
+
out();
|
|
7817
8073
|
out(
|
|
7818
|
-
`${
|
|
8074
|
+
`${style.bold("veefore")} ${style.grey(version)} \u2014 identity, authority and policy for AI agents`
|
|
7819
8075
|
);
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
7829
|
-
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
7830
|
-
// Written explicitly so the behaviour is visible and editable in one place.
|
|
7831
|
-
VEEFORE_APPROVAL_MODE: approvalMode
|
|
7832
|
-
});
|
|
7833
|
-
if (audience !== gatewayUrl) {
|
|
7834
|
-
out(
|
|
7835
|
-
`${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
|
|
7836
|
-
`(it reports this, rather than the ${gatewayUrl} you dialled)`
|
|
7837
|
-
)}`
|
|
7838
|
-
);
|
|
8076
|
+
out();
|
|
8077
|
+
out(style.bold("USAGE"));
|
|
8078
|
+
out(" veefore <command> [options]");
|
|
8079
|
+
out();
|
|
8080
|
+
out(style.bold("COMMANDS"));
|
|
8081
|
+
const width = Math.max(...COMMANDS.map((command) => command.name.length));
|
|
8082
|
+
for (const command of COMMANDS) {
|
|
8083
|
+
out(` ${style.cyan(command.name.padEnd(width))} ${command.summary}`);
|
|
7839
8084
|
}
|
|
7840
|
-
out(
|
|
7841
|
-
`${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
|
|
7842
|
-
);
|
|
7843
8085
|
out();
|
|
8086
|
+
out(style.bold("GLOBAL OPTIONS"));
|
|
8087
|
+
renderOptions(GLOBAL_OPTIONS);
|
|
8088
|
+
out();
|
|
8089
|
+
out(style.bold("GETTING STARTED"));
|
|
7844
8090
|
out(
|
|
7845
|
-
` ${style.
|
|
8091
|
+
` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
|
|
7846
8092
|
);
|
|
7847
8093
|
out(
|
|
7848
|
-
`
|
|
8094
|
+
` ${style.grey("2.")} veefore dev ${style.grey("watch what your agent actually does")}`
|
|
7849
8095
|
);
|
|
7850
|
-
out(` ${style.grey(
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
}
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
8096
|
+
out(` ${style.grey("3.")} veefore doctor ${style.grey("check the security posture")}`);
|
|
8097
|
+
out(` ${style.grey("4.")} veefore deploy ${style.grey("turn enforcement on")}`);
|
|
8098
|
+
out();
|
|
8099
|
+
out(style.grey(" Run `veefore <command> --help` for detail on any command."));
|
|
8100
|
+
out();
|
|
8101
|
+
}
|
|
8102
|
+
function printCommandHelp(name) {
|
|
8103
|
+
const command = COMMANDS.find((entry) => entry.name === name);
|
|
8104
|
+
if (!command) return false;
|
|
8105
|
+
out();
|
|
8106
|
+
out(`${style.bold(`veefore ${command.name}`)} \u2014 ${command.summary}`);
|
|
8107
|
+
out();
|
|
8108
|
+
out(style.bold("USAGE"));
|
|
8109
|
+
out(` ${command.usage}`);
|
|
8110
|
+
out();
|
|
8111
|
+
out(style.bold("DESCRIPTION"));
|
|
8112
|
+
for (const line of wrap2(command.description, 76)) out(` ${line}`);
|
|
8113
|
+
if (command.options && command.options.length > 0) {
|
|
8114
|
+
out();
|
|
8115
|
+
out(style.bold("OPTIONS"));
|
|
8116
|
+
renderOptions(command.options);
|
|
7866
8117
|
}
|
|
7867
|
-
|
|
7868
|
-
if (!integrated) {
|
|
7869
|
-
heading("Protect each tool");
|
|
8118
|
+
if (command.examples && command.examples.length > 0) {
|
|
7870
8119
|
out();
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
8120
|
+
out(style.bold("EXAMPLES"));
|
|
8121
|
+
for (const example of command.examples) out(` ${style.grey("$")} ${example}`);
|
|
8122
|
+
}
|
|
8123
|
+
out();
|
|
8124
|
+
return true;
|
|
8125
|
+
}
|
|
8126
|
+
function renderOptions(options) {
|
|
8127
|
+
const width = Math.max(...options.map(([flag]) => flag.length));
|
|
8128
|
+
for (const [flag, description] of options) {
|
|
8129
|
+
out(` ${style.cyan(flag.padEnd(width))} ${description}`);
|
|
8130
|
+
}
|
|
8131
|
+
}
|
|
8132
|
+
function wrap2(text, width) {
|
|
8133
|
+
const words2 = text.split(/\s+/);
|
|
8134
|
+
const lines = [];
|
|
8135
|
+
let current = "";
|
|
8136
|
+
for (const word of words2) {
|
|
8137
|
+
if (current.length + word.length + 1 > width) {
|
|
8138
|
+
lines.push(current);
|
|
8139
|
+
current = word;
|
|
8140
|
+
} else {
|
|
8141
|
+
current = current === "" ? word : `${current} ${word}`;
|
|
7885
8142
|
}
|
|
7886
8143
|
}
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
);
|
|
7894
|
-
|
|
8144
|
+
if (current !== "") lines.push(current);
|
|
8145
|
+
return lines;
|
|
8146
|
+
}
|
|
8147
|
+
|
|
8148
|
+
// src/home.ts
|
|
8149
|
+
async function homeCommand(args, flags) {
|
|
8150
|
+
enterApp();
|
|
8151
|
+
try {
|
|
8152
|
+
return await homeLoop(args, flags);
|
|
8153
|
+
} finally {
|
|
8154
|
+
leaveApp();
|
|
8155
|
+
printFarewell();
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
var farewellPrinted = false;
|
|
8159
|
+
function printFarewell() {
|
|
8160
|
+
if (!isInteractive() || inApp() || farewellPrinted) return;
|
|
8161
|
+
const project = readProjectConfig();
|
|
8162
|
+
if (!project) return;
|
|
8163
|
+
farewellPrinted = true;
|
|
7895
8164
|
out();
|
|
7896
8165
|
out(
|
|
7897
|
-
style.grey(
|
|
7898
|
-
` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
|
|
7899
|
-
)
|
|
8166
|
+
`${brand(style.bold("Veefore"))} ${style.grey("\xB7")} ${style.bold(project.agentName)} ${style.grey(`\xB7 ${project.environmentName} \xB7 ${project.controlPlaneUrl}`)}`
|
|
7900
8167
|
);
|
|
7901
|
-
out(style.grey("
|
|
8168
|
+
out(style.grey(" veefore dev watch decisions live"));
|
|
8169
|
+
out(style.grey(" veefore doctor check the security posture"));
|
|
8170
|
+
out(style.grey(" veefore open this again"));
|
|
7902
8171
|
out();
|
|
7903
|
-
if (flags.json) {
|
|
7904
|
-
out(
|
|
7905
|
-
JSON.stringify(
|
|
7906
|
-
{
|
|
7907
|
-
config_file: configFile,
|
|
7908
|
-
env_file: envFile,
|
|
7909
|
-
agent_id: created.agentId,
|
|
7910
|
-
key_id: created.keyId,
|
|
7911
|
-
fingerprint: created.fingerprint,
|
|
7912
|
-
tools: created.tools,
|
|
7913
|
-
checks
|
|
7914
|
-
},
|
|
7915
|
-
null,
|
|
7916
|
-
2
|
|
7917
|
-
)
|
|
7918
|
-
);
|
|
7919
|
-
}
|
|
7920
|
-
return failed.length > 0 ? 1 : 0;
|
|
7921
8172
|
}
|
|
7922
|
-
function
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
8173
|
+
async function homeLoop(args, flags) {
|
|
8174
|
+
const source = welcomeSource(flags);
|
|
8175
|
+
let first = true;
|
|
8176
|
+
for (; ; ) {
|
|
8177
|
+
if (readProjectConfig()) return initCommand(args, flags);
|
|
8178
|
+
const body = () => {
|
|
8179
|
+
drawScreen(source);
|
|
8180
|
+
out();
|
|
8181
|
+
out(style.bold("Let's get started."));
|
|
8182
|
+
out();
|
|
8183
|
+
};
|
|
8184
|
+
await beginScreen(source, { animate: first });
|
|
8185
|
+
first = false;
|
|
8186
|
+
out();
|
|
8187
|
+
out(style.bold("Let's get started."));
|
|
8188
|
+
out();
|
|
8189
|
+
let action;
|
|
8190
|
+
try {
|
|
8191
|
+
action = await select(
|
|
8192
|
+
"What would you like to do?",
|
|
8193
|
+
[
|
|
8194
|
+
{ label: "Set up this project", hint: "recommended \xB7 veefore init", value: "setup" },
|
|
8195
|
+
{
|
|
8196
|
+
label: "Preview what setup would find",
|
|
8197
|
+
hint: "veefore scan \xB7 changes nothing",
|
|
8198
|
+
value: "scan"
|
|
8199
|
+
},
|
|
8200
|
+
{ label: "Sign in", hint: "veefore login", value: "login" },
|
|
8201
|
+
{ label: "Show every command", hint: "veefore help", value: "help" },
|
|
8202
|
+
{ label: "Exit", hint: "esc", value: "exit" }
|
|
8203
|
+
],
|
|
8204
|
+
{ redraw: body }
|
|
8205
|
+
);
|
|
8206
|
+
} catch (error) {
|
|
8207
|
+
if (error instanceof MenuCancelled) return 0;
|
|
8208
|
+
throw error;
|
|
8209
|
+
}
|
|
8210
|
+
if (action === "exit") return 0;
|
|
8211
|
+
if (action === "setup") {
|
|
8212
|
+
await runOnScreen(source, flags, () => initCommand(args, flags), {
|
|
8213
|
+
// Setup done: "Continue", and the next pass opens the project home
|
|
8214
|
+
// with the panel filled in. Otherwise "Back to menu", to read why and
|
|
8215
|
+
// choose again.
|
|
8216
|
+
label: () => readProjectConfig() ? "Continue" : "Back to menu",
|
|
8217
|
+
// Setup asks questions and draws its own screens as it goes, so it has
|
|
8218
|
+
// to reach the terminal as it runs. Collecting it would hold those
|
|
8219
|
+
// screens back and then print them all at once.
|
|
8220
|
+
live: true
|
|
8221
|
+
});
|
|
8222
|
+
continue;
|
|
8223
|
+
}
|
|
8224
|
+
const code = await runOnScreen(source, flags, async () => {
|
|
8225
|
+
switch (action) {
|
|
8226
|
+
case "scan":
|
|
8227
|
+
return scanCommand([], flags);
|
|
8228
|
+
case "login":
|
|
8229
|
+
await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
|
|
8230
|
+
return 0;
|
|
8231
|
+
case "help":
|
|
8232
|
+
printRootHelp(source().version);
|
|
8233
|
+
return 0;
|
|
8234
|
+
}
|
|
8235
|
+
return 0;
|
|
8236
|
+
});
|
|
8237
|
+
if (!isInteractive()) return code;
|
|
7929
8238
|
}
|
|
7930
8239
|
}
|
|
7931
|
-
function
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
|
|
7935
|
-
` ${symbol.bullet} approved: the tool runs once and returns its real result`,
|
|
7936
|
-
` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
|
|
7937
|
-
style.grey(
|
|
7938
|
-
" The agent process must stay running while it waits (up to the approval's expiry)."
|
|
7939
|
-
),
|
|
7940
|
-
style.grey(
|
|
7941
|
-
" For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
|
|
7942
|
-
),
|
|
7943
|
-
style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
|
|
7944
|
-
];
|
|
7945
|
-
}
|
|
7946
|
-
return [
|
|
7947
|
-
` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
|
|
7948
|
-
" carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
|
|
7949
|
-
" or the action never happens.",
|
|
7950
|
-
style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
|
|
7951
|
-
];
|
|
8240
|
+
async function initWithWelcome(args, flags) {
|
|
8241
|
+
await beginScreen(welcomeSource(flags), { animate: true });
|
|
8242
|
+
return initCommand(args, flags);
|
|
7952
8243
|
}
|
|
7953
|
-
async function
|
|
7954
|
-
const
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
}
|
|
7961
|
-
|
|
7962
|
-
if (options.organizationId) {
|
|
7963
|
-
const match = organizations.find((org) => org.id === options.organizationId);
|
|
7964
|
-
if (!match) {
|
|
7965
|
-
throw new CliError(
|
|
7966
|
-
`You are not a member of organization ${options.organizationId}.`,
|
|
7967
|
-
`Available: ${organizations.map((org) => org.id).join(", ")}`
|
|
7968
|
-
);
|
|
8244
|
+
async function runOnScreen(source, flags, run2, options = {}) {
|
|
8245
|
+
const header = () => drawScreen(source);
|
|
8246
|
+
if (options.live) {
|
|
8247
|
+
header();
|
|
8248
|
+
let code2 = 0;
|
|
8249
|
+
try {
|
|
8250
|
+
code2 = await run2();
|
|
8251
|
+
} catch (error) {
|
|
8252
|
+
code2 = error instanceof MenuCancelled ? 0 : reportError(error, flags);
|
|
7969
8253
|
}
|
|
7970
|
-
|
|
8254
|
+
const label2 = options.label ?? "Back";
|
|
8255
|
+
await pressBack(typeof label2 === "function" ? label2() : label2);
|
|
8256
|
+
return code2;
|
|
7971
8257
|
}
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
8258
|
+
beginCapture();
|
|
8259
|
+
let code = 0;
|
|
8260
|
+
try {
|
|
8261
|
+
code = await run2();
|
|
8262
|
+
} catch (error) {
|
|
8263
|
+
code = error instanceof MenuCancelled ? 0 : reportError(error, flags);
|
|
7977
8264
|
}
|
|
7978
|
-
|
|
7979
|
-
const
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
})),
|
|
7986
|
-
{ quiet: true, back: true, backLabel: "Back" }
|
|
7987
|
-
);
|
|
7988
|
-
if (chosen === BACK) return BACK;
|
|
7989
|
-
await api.post("/v1/auth/scope", { organization_id: chosen.id });
|
|
7990
|
-
out(`${symbol.pass} Organization: ${style.bold(chosen.name)}`);
|
|
7991
|
-
return { value: chosen, asked: true };
|
|
8265
|
+
const lines = endCapture();
|
|
8266
|
+
const label = options.label ?? "Back";
|
|
8267
|
+
await showOutput(lines, {
|
|
8268
|
+
drawHeader: header,
|
|
8269
|
+
label: typeof label === "function" ? label() : label
|
|
8270
|
+
});
|
|
8271
|
+
return code;
|
|
7992
8272
|
}
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8273
|
+
|
|
8274
|
+
// src/commands/init.ts
|
|
8275
|
+
var DEFAULT_GATEWAY = "http://localhost:4010";
|
|
8276
|
+
async function initCommand(args, flags) {
|
|
8277
|
+
const options = parseInitArgs(args);
|
|
8278
|
+
const existing = readProjectConfig();
|
|
8279
|
+
if (existing && !options.force && isInteractive() && !flags.json) {
|
|
8280
|
+
return projectHome(existing, flags);
|
|
8000
8281
|
}
|
|
8001
|
-
if (options.
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8282
|
+
if (existing && !options.force) {
|
|
8283
|
+
out();
|
|
8284
|
+
out(`${symbol.warn} This project is already connected to Veefore.`);
|
|
8285
|
+
details([
|
|
8286
|
+
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
8287
|
+
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
8288
|
+
["Control plane", existing.controlPlaneUrl]
|
|
8289
|
+
]);
|
|
8290
|
+
out();
|
|
8291
|
+
out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
|
|
8292
|
+
return 0;
|
|
8010
8293
|
}
|
|
8011
|
-
|
|
8012
|
-
const
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
label: workspace.name,
|
|
8016
|
-
hint: workspace.id,
|
|
8017
|
-
value: workspace
|
|
8018
|
-
})),
|
|
8019
|
-
{ quiet: true, back: true }
|
|
8294
|
+
heading("Veefore init");
|
|
8295
|
+
const framework = detectFramework();
|
|
8296
|
+
out(
|
|
8297
|
+
`${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language}, matched ${framework.evidence})`)}`
|
|
8020
8298
|
);
|
|
8021
|
-
if (
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
|
|
8027
|
-
const environments = all.filter((environment) => environment.workspaceId === workspaceId);
|
|
8028
|
-
if (environments.length === 0) {
|
|
8029
|
-
throw new CliError(
|
|
8030
|
-
"That workspace has no environments.",
|
|
8031
|
-
"Create one in the console, or ask an admin to."
|
|
8299
|
+
if (framework.key === "unknown") {
|
|
8300
|
+
out(
|
|
8301
|
+
style.grey(
|
|
8302
|
+
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
8303
|
+
)
|
|
8032
8304
|
);
|
|
8033
8305
|
}
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8306
|
+
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
8307
|
+
const controlPlaneUrl = found.controlPlaneUrl;
|
|
8308
|
+
if (found.source !== "entered") {
|
|
8309
|
+
out(
|
|
8310
|
+
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
8311
|
+
);
|
|
8038
8312
|
}
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8313
|
+
let session = loadSession(controlPlaneUrl);
|
|
8314
|
+
if (!session) {
|
|
8315
|
+
await login(controlPlaneUrl);
|
|
8316
|
+
session = loadSession(controlPlaneUrl);
|
|
8043
8317
|
}
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
)
|
|
8062
|
-
|
|
8063
|
-
|
|
8318
|
+
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
8319
|
+
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
8320
|
+
let me = await api.get("/v1/auth/session");
|
|
8321
|
+
const STEPS = ["organization", "workspace", "environment"];
|
|
8322
|
+
const asked = /* @__PURE__ */ new Map();
|
|
8323
|
+
let organization;
|
|
8324
|
+
let workspace;
|
|
8325
|
+
let environment;
|
|
8326
|
+
const source = welcomeSource(flags);
|
|
8327
|
+
const decided = [];
|
|
8328
|
+
enterApp();
|
|
8329
|
+
try {
|
|
8330
|
+
for (let at = 0; at < STEPS.length; ) {
|
|
8331
|
+
const step = STEPS[at];
|
|
8332
|
+
decided.length = at;
|
|
8333
|
+
await stepScreen(source, decided, at + 1, STEPS.length);
|
|
8334
|
+
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);
|
|
8335
|
+
if (answer === BACK) {
|
|
8336
|
+
let previous = at - 1;
|
|
8337
|
+
while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
|
|
8338
|
+
if (previous < 0) {
|
|
8339
|
+
throw new MenuCancelled();
|
|
8340
|
+
}
|
|
8341
|
+
at = previous;
|
|
8064
8342
|
continue;
|
|
8065
8343
|
}
|
|
8344
|
+
asked.set(step, answer.asked);
|
|
8345
|
+
if (step === "organization") {
|
|
8346
|
+
organization = answer.value;
|
|
8347
|
+
decided[at] = ["Organization", organization.name];
|
|
8348
|
+
if (answer.asked) me = await api.get("/v1/auth/session");
|
|
8349
|
+
} else if (step === "workspace") {
|
|
8350
|
+
workspace = answer.value;
|
|
8351
|
+
decided[at] = ["Workspace", workspace.name];
|
|
8352
|
+
} else {
|
|
8353
|
+
environment = answer.value;
|
|
8354
|
+
decided[at] = ["Environment", `${environment.name} (${environment.type})`];
|
|
8355
|
+
}
|
|
8356
|
+
at += 1;
|
|
8066
8357
|
}
|
|
8067
|
-
|
|
8068
|
-
|
|
8358
|
+
} finally {
|
|
8359
|
+
leaveApp();
|
|
8069
8360
|
}
|
|
8070
|
-
|
|
8071
|
-
|
|
8361
|
+
for (const [label, value] of decided) {
|
|
8362
|
+
out(`${symbol.pass} ${label}: ${style.bold(value)}`);
|
|
8363
|
+
}
|
|
8364
|
+
heading("Looking at your project");
|
|
8365
|
+
const scan = scanProject(process.cwd());
|
|
8366
|
+
printScan(scan);
|
|
8367
|
+
const plan = buildPlan(scan, {
|
|
8368
|
+
agentName: options.agentName,
|
|
8369
|
+
extraCapabilities: options.capabilities
|
|
8370
|
+
});
|
|
8072
8371
|
for (; ; ) {
|
|
8372
|
+
if (!options.agentName && !flags.yes) {
|
|
8373
|
+
const name = await askAgentName(plan.agentName);
|
|
8374
|
+
if (name === BACK) {
|
|
8375
|
+
const again = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
8376
|
+
if (again !== BACK) environment = again.value;
|
|
8377
|
+
continue;
|
|
8378
|
+
}
|
|
8379
|
+
plan.agentName = name;
|
|
8380
|
+
}
|
|
8381
|
+
printPlan2(plan, environment);
|
|
8073
8382
|
out();
|
|
8074
|
-
const
|
|
8075
|
-
|
|
8383
|
+
const proceed = await confirm("Set this up?", { yes: flags.yes });
|
|
8384
|
+
if (proceed) break;
|
|
8385
|
+
if (flags.yes || options.agentName) {
|
|
8386
|
+
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
8387
|
+
return 1;
|
|
8388
|
+
}
|
|
8389
|
+
const next = await select(
|
|
8390
|
+
"What would you like to do?",
|
|
8076
8391
|
[
|
|
8077
|
-
{ label:
|
|
8078
|
-
{ label: "
|
|
8392
|
+
{ label: "Change the agent name", value: "name" },
|
|
8393
|
+
{ label: "Choose a different environment", value: "environment" },
|
|
8394
|
+
{ label: "Leave setup", value: "leave" }
|
|
8079
8395
|
],
|
|
8080
|
-
{
|
|
8396
|
+
{ initial: 0 }
|
|
8081
8397
|
);
|
|
8082
|
-
if (
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8398
|
+
if (next === "leave") {
|
|
8399
|
+
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
8400
|
+
return 1;
|
|
8401
|
+
}
|
|
8402
|
+
if (next === "environment") {
|
|
8403
|
+
const again = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
8404
|
+
if (again !== BACK) environment = again.value;
|
|
8086
8405
|
}
|
|
8087
|
-
const typed = (await ask(`Agent name ${style.grey(`[${suggested}]`)}:`, suggested)).trim();
|
|
8088
|
-
if (typed === "") continue;
|
|
8089
|
-
out(`${symbol.pass} Agent name: ${style.bold(typed)}`);
|
|
8090
|
-
return typed;
|
|
8091
|
-
}
|
|
8092
|
-
}
|
|
8093
|
-
async function discoverAudience(gatewayUrl) {
|
|
8094
|
-
try {
|
|
8095
|
-
const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
|
|
8096
|
-
headers: { accept: "application/json" }
|
|
8097
|
-
});
|
|
8098
|
-
if (!response.ok) return gatewayUrl;
|
|
8099
|
-
const body = await response.json();
|
|
8100
|
-
const audience = body.data?.audience;
|
|
8101
|
-
return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
|
|
8102
|
-
} catch {
|
|
8103
|
-
return gatewayUrl;
|
|
8104
|
-
}
|
|
8105
|
-
}
|
|
8106
|
-
async function configureStarterPolicy(api, workspaceId, flags, options) {
|
|
8107
|
-
if (options.skipPolicy) {
|
|
8108
|
-
out(`${symbol.info} Skipped starter policy (--skip-policy).`);
|
|
8109
|
-
return;
|
|
8110
|
-
}
|
|
8111
|
-
const existing = await api.get(
|
|
8112
|
-
"/v1/policies",
|
|
8113
|
-
{ workspace_id: workspaceId }
|
|
8114
|
-
);
|
|
8115
|
-
const count = Array.isArray(existing) ? existing.length : existing.items?.length ?? 0;
|
|
8116
|
-
if (count > 0) {
|
|
8117
|
-
out(`${symbol.pass} Workspace already has ${count} policy/policies. Leaving them alone.`);
|
|
8118
|
-
return;
|
|
8119
|
-
}
|
|
8120
|
-
const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
|
|
8121
|
-
if (!create) {
|
|
8122
|
-
out(style.grey(" Skipped. Create one later from a template in the console."));
|
|
8123
|
-
return;
|
|
8124
8406
|
}
|
|
8125
|
-
|
|
8407
|
+
heading("Setting up");
|
|
8408
|
+
let created;
|
|
8126
8409
|
try {
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
`Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
|
|
8138
|
-
);
|
|
8139
|
-
keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
|
|
8140
|
-
out(
|
|
8141
|
-
style.grey(
|
|
8142
|
-
" Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
|
|
8143
|
-
)
|
|
8410
|
+
created = await applyPlan(
|
|
8411
|
+
api,
|
|
8412
|
+
plan,
|
|
8413
|
+
{
|
|
8414
|
+
workspaceId: workspace.id,
|
|
8415
|
+
environmentId: environment.id,
|
|
8416
|
+
framework: framework.key === "unknown" ? null : framework.key
|
|
8417
|
+
},
|
|
8418
|
+
void 0,
|
|
8419
|
+
{ newAgent: options.newAgent === true }
|
|
8144
8420
|
);
|
|
8145
8421
|
} catch (error) {
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8422
|
+
if (error instanceof CliApiError && error.code === "PERMISSION_DENIED") {
|
|
8423
|
+
throw new CliError(
|
|
8424
|
+
"Your role cannot set up agents in this workspace.",
|
|
8425
|
+
'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
|
|
8426
|
+
);
|
|
8151
8427
|
}
|
|
8152
8428
|
throw error;
|
|
8153
8429
|
}
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8430
|
+
let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
|
|
8431
|
+
if (!gatewayUrl) {
|
|
8432
|
+
const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
|
|
8433
|
+
if (published && await gatewayReachable(published)) {
|
|
8434
|
+
gatewayUrl = published;
|
|
8435
|
+
out(
|
|
8436
|
+
`${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
|
|
8437
|
+
);
|
|
8438
|
+
} else {
|
|
8439
|
+
const suggestion = published ?? DEFAULT_GATEWAY;
|
|
8440
|
+
out(
|
|
8441
|
+
`${symbol.warn} ${published ? `The gateway at ${published} is not answering.` : "The control plane did not publish a gateway address."}`
|
|
8442
|
+
);
|
|
8443
|
+
gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
|
|
8444
|
+
}
|
|
8166
8445
|
}
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
}
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8446
|
+
gatewayUrl = normalizeUrl(gatewayUrl);
|
|
8447
|
+
const config = {
|
|
8448
|
+
version: 1,
|
|
8449
|
+
controlPlaneUrl,
|
|
8450
|
+
gatewayUrl,
|
|
8451
|
+
organizationId: organization.id,
|
|
8452
|
+
organizationName: organization.name,
|
|
8453
|
+
workspaceId: workspace.id,
|
|
8454
|
+
workspaceName: workspace.name,
|
|
8455
|
+
environmentId: environment.id,
|
|
8456
|
+
environmentName: environment.name,
|
|
8457
|
+
environmentType: environment.type,
|
|
8458
|
+
agentId: created.agentId,
|
|
8459
|
+
agentName: created.agentName,
|
|
8460
|
+
keyId: created.keyId,
|
|
8461
|
+
framework: framework.key === "unknown" ? null : framework.key,
|
|
8462
|
+
declaredTools: created.tools.map((tool) => tool.key),
|
|
8463
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8464
|
+
};
|
|
8465
|
+
const configFile = writeProjectConfig(config);
|
|
8466
|
+
out(
|
|
8467
|
+
`${symbol.pass} Wrote ${style.bold("veefore.config.json")} ${style.grey("(safe to commit)")}`
|
|
8468
|
+
);
|
|
8469
|
+
const audience = await discoverAudience(gatewayUrl);
|
|
8470
|
+
ensureGitignore(process.cwd(), [".env.local", ".veefore/"]);
|
|
8471
|
+
const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
|
|
8472
|
+
const envFile = writeLocalEnv({
|
|
8473
|
+
VEEFORE_GATEWAY_URL: gatewayUrl,
|
|
8474
|
+
VEEFORE_GATEWAY_AUDIENCE: audience,
|
|
8475
|
+
VEEFORE_AGENT_ID: created.agentId,
|
|
8476
|
+
VEEFORE_AGENT_KEY_ID: created.keyId,
|
|
8477
|
+
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
8478
|
+
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
8479
|
+
// Written explicitly so the behaviour is visible and editable in one place.
|
|
8480
|
+
VEEFORE_APPROVAL_MODE: approvalMode
|
|
8481
|
+
});
|
|
8482
|
+
if (audience !== gatewayUrl) {
|
|
8483
|
+
out(
|
|
8484
|
+
`${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
|
|
8485
|
+
`(it reports this, rather than the ${gatewayUrl} you dialled)`
|
|
8486
|
+
)}`
|
|
8204
8487
|
);
|
|
8205
|
-
checks.push({
|
|
8206
|
-
name: "Agent passport issued",
|
|
8207
|
-
status: passport ? "pass" : "warn",
|
|
8208
|
-
message: passport ? `version ${passport.version}` : "not issued yet"
|
|
8209
|
-
});
|
|
8210
|
-
} catch {
|
|
8211
|
-
checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
|
|
8212
8488
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8489
|
+
out(
|
|
8490
|
+
`${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
|
|
8491
|
+
);
|
|
8492
|
+
out();
|
|
8493
|
+
out(
|
|
8494
|
+
` ${style.amber("The private key was generated here and written to that file only.")} Veefore`
|
|
8495
|
+
);
|
|
8496
|
+
out(
|
|
8497
|
+
` never received it and cannot recover it. Move it into your secret manager before deploying.`
|
|
8498
|
+
);
|
|
8499
|
+
out(` ${style.grey(`Fingerprint: ${created.fingerprint}`)}`);
|
|
8500
|
+
await configureStarterPolicy(api, workspace.id, flags, options);
|
|
8501
|
+
let integrated = false;
|
|
8502
|
+
if (created.tools.length > 0 && !options.skipCode) {
|
|
8503
|
+
const outcome = await integrate(
|
|
8504
|
+
process.cwd(),
|
|
8505
|
+
created.tools.map((tool) => tool.key),
|
|
8506
|
+
{ yes: flags.yes, quiet: flags.json, api }
|
|
8216
8507
|
);
|
|
8217
|
-
|
|
8218
|
-
name: "Enforcement mode",
|
|
8219
|
-
status: "pass",
|
|
8220
|
-
message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
|
|
8221
|
-
});
|
|
8222
|
-
} catch {
|
|
8223
|
-
checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
|
|
8508
|
+
integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
|
|
8224
8509
|
}
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
const arg = args[index];
|
|
8231
|
-
const next = () => {
|
|
8232
|
-
const value = args[index + 1];
|
|
8233
|
-
if (value === void 0 || value.startsWith("-")) {
|
|
8234
|
-
throw new CliError(`Option ${String(arg)} requires a value.`);
|
|
8235
|
-
}
|
|
8236
|
-
index += 1;
|
|
8237
|
-
return value;
|
|
8238
|
-
};
|
|
8239
|
-
switch (arg) {
|
|
8240
|
-
case "--force":
|
|
8241
|
-
options.force = true;
|
|
8242
|
-
break;
|
|
8243
|
-
case "--skip-policy":
|
|
8244
|
-
options.skipPolicy = true;
|
|
8245
|
-
break;
|
|
8246
|
-
case "--skip-code":
|
|
8247
|
-
options.skipCode = true;
|
|
8248
|
-
break;
|
|
8249
|
-
case "--new-agent":
|
|
8250
|
-
options.newAgent = true;
|
|
8251
|
-
break;
|
|
8252
|
-
case "--control-plane":
|
|
8253
|
-
options.controlPlaneUrl = next();
|
|
8254
|
-
break;
|
|
8255
|
-
case "--gateway":
|
|
8256
|
-
options.gatewayUrl = next();
|
|
8257
|
-
break;
|
|
8258
|
-
case "--organization":
|
|
8259
|
-
options.organizationId = next();
|
|
8260
|
-
break;
|
|
8261
|
-
case "--workspace":
|
|
8262
|
-
options.workspaceId = next();
|
|
8263
|
-
break;
|
|
8264
|
-
case "--environment":
|
|
8265
|
-
options.environmentId = next();
|
|
8266
|
-
break;
|
|
8267
|
-
case "--name":
|
|
8268
|
-
options.agentName = next();
|
|
8269
|
-
break;
|
|
8270
|
-
case "--capability":
|
|
8271
|
-
options.capabilities = [...options.capabilities ?? [], next()];
|
|
8272
|
-
break;
|
|
8273
|
-
case "--approval-mode": {
|
|
8274
|
-
const value = next().toLowerCase();
|
|
8275
|
-
if (value !== "wait" && value !== "throw") {
|
|
8276
|
-
throw new CliError('--approval-mode must be "wait" or "throw".');
|
|
8277
|
-
}
|
|
8278
|
-
options.approvalMode = value;
|
|
8279
|
-
break;
|
|
8280
|
-
}
|
|
8281
|
-
default:
|
|
8282
|
-
if (arg?.startsWith("-")) {
|
|
8283
|
-
throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
|
|
8284
|
-
}
|
|
8285
|
-
}
|
|
8510
|
+
heading("Validating the connection");
|
|
8511
|
+
const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
|
|
8512
|
+
for (const check of checks) {
|
|
8513
|
+
const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
|
|
8514
|
+
out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
|
|
8286
8515
|
}
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
const source = welcomeSource(flags);
|
|
8291
|
-
let last = 0;
|
|
8292
|
-
for (let round = 0; ; round += 1) {
|
|
8293
|
-
await beginScreen(source, { animate: round === 0 });
|
|
8294
|
-
const state = await agentState(existing);
|
|
8295
|
-
printProjectSummary(existing, state);
|
|
8296
|
-
const choices = homeChoices(existing, state, flags);
|
|
8297
|
-
choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
|
|
8516
|
+
const failed = checks.filter((check) => check.status === "fail");
|
|
8517
|
+
if (!integrated) {
|
|
8518
|
+
heading("Protect each tool");
|
|
8298
8519
|
out();
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
picked = await select(
|
|
8302
|
-
"What would you like to do?",
|
|
8303
|
-
choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice }))
|
|
8304
|
-
);
|
|
8305
|
-
} catch (error) {
|
|
8306
|
-
if (error instanceof MenuCancelled) return last;
|
|
8307
|
-
throw error;
|
|
8520
|
+
for (const line of integrationSnippet(framework).split("\n")) {
|
|
8521
|
+
out(` ${style.cyan(line)}`);
|
|
8308
8522
|
}
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8523
|
+
const entryPoints = findEntryPoints();
|
|
8524
|
+
if (entryPoints.length > 0) {
|
|
8525
|
+
out();
|
|
8526
|
+
out(
|
|
8527
|
+
style.grey(
|
|
8528
|
+
` Likely entry point${entryPoints.length > 1 ? "s" : ""}: ${entryPoints.join(", ")}`
|
|
8529
|
+
)
|
|
8530
|
+
);
|
|
8316
8531
|
}
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
last = await picked.run();
|
|
8320
|
-
} catch (error) {
|
|
8321
|
-
last = error instanceof MenuCancelled ? last : reportError(error, flags);
|
|
8532
|
+
if (created.tools.length > 0) {
|
|
8533
|
+
out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
|
|
8322
8534
|
}
|
|
8323
|
-
if (!isInteractive()) return last;
|
|
8324
|
-
await pressBack("Back to menu");
|
|
8325
8535
|
}
|
|
8326
|
-
|
|
8327
|
-
|
|
8536
|
+
heading("When a human must approve");
|
|
8537
|
+
for (const line of approvalModeExplanation(approvalMode)) out(line);
|
|
8538
|
+
heading("Next");
|
|
8539
|
+
out(` ${symbol.bullet} ${style.bold("veefore dev")} watch decisions live while you build`);
|
|
8540
|
+
out(
|
|
8541
|
+
` ${symbol.bullet} ${style.bold("veefore doctor")} check the security posture of this setup`
|
|
8542
|
+
);
|
|
8543
|
+
out(` ${symbol.bullet} ${style.bold("veefore status")} see the current mode and agent health`);
|
|
8328
8544
|
out();
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
if (state.kind === "session-ended") {
|
|
8338
|
-
out(`${symbol.warn} ${state.message}`);
|
|
8339
|
-
}
|
|
8340
|
-
}
|
|
8341
|
-
function homeChoices(existing, state, flags) {
|
|
8342
|
-
const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
|
|
8343
|
-
if (ended) {
|
|
8344
|
-
out();
|
|
8545
|
+
out(
|
|
8546
|
+
style.grey(
|
|
8547
|
+
` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
|
|
8548
|
+
)
|
|
8549
|
+
);
|
|
8550
|
+
out(style.grey(" Review the recommended policy, simulate it, then switch to ENFORCE."));
|
|
8551
|
+
out();
|
|
8552
|
+
if (flags.json) {
|
|
8345
8553
|
out(
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
run: () => initCommand(["--force"], flags),
|
|
8361
|
-
leaves: true
|
|
8362
|
-
}
|
|
8363
|
-
];
|
|
8364
|
-
}
|
|
8365
|
-
const choices = [];
|
|
8366
|
-
if (state.kind === "signed-out" || state.kind === "session-ended") {
|
|
8367
|
-
choices.push({
|
|
8368
|
-
label: "Sign in",
|
|
8369
|
-
hint: "veefore login",
|
|
8370
|
-
run: async () => {
|
|
8371
|
-
await login(existing.controlPlaneUrl);
|
|
8372
|
-
return 0;
|
|
8373
|
-
}
|
|
8374
|
-
});
|
|
8375
|
-
}
|
|
8376
|
-
choices.push(
|
|
8377
|
-
{ label: "Watch decisions live", hint: "veefore dev", run: () => devCommand([], flags) },
|
|
8378
|
-
{ label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
|
|
8379
|
-
{
|
|
8380
|
-
label: "Protect tools in the code",
|
|
8381
|
-
hint: "veefore integrate",
|
|
8382
|
-
run: () => integrateCommand([], flags)
|
|
8383
|
-
},
|
|
8384
|
-
{
|
|
8385
|
-
label: "Check every tool is guarded",
|
|
8386
|
-
hint: "veefore integrate --check",
|
|
8387
|
-
run: () => integrateCommand(["--check"], flags)
|
|
8388
|
-
},
|
|
8389
|
-
{ label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
|
|
8390
|
-
{
|
|
8391
|
-
label: "Audit the security posture",
|
|
8392
|
-
hint: "veefore doctor",
|
|
8393
|
-
run: () => doctorCommand([], flags)
|
|
8394
|
-
},
|
|
8395
|
-
{ label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
|
|
8396
|
-
{
|
|
8397
|
-
label: "Re-run setup for this project",
|
|
8398
|
-
hint: "veefore init --force",
|
|
8399
|
-
run: () => initCommand(["--force"], flags),
|
|
8400
|
-
leaves: true
|
|
8401
|
-
},
|
|
8402
|
-
{
|
|
8403
|
-
label: "Remove Veefore from this project",
|
|
8404
|
-
hint: "veefore remove",
|
|
8405
|
-
run: () => removeCommand([], flags, state),
|
|
8406
|
-
leaves: true
|
|
8407
|
-
}
|
|
8408
|
-
);
|
|
8409
|
-
if (state.kind === "ok" || state.kind === "unreachable") {
|
|
8410
|
-
choices.push({
|
|
8411
|
-
label: "Sign out",
|
|
8412
|
-
hint: "veefore logout",
|
|
8413
|
-
run: async () => {
|
|
8414
|
-
await logout(existing.controlPlaneUrl);
|
|
8415
|
-
return 0;
|
|
8416
|
-
}
|
|
8417
|
-
// Deliberately not `leaves`: signing out comes back to this menu, so the
|
|
8418
|
-
// panel and the status redraw as signed out and the next step on offer
|
|
8419
|
-
// is Sign in. Leaving instead would quit on the one action whose result
|
|
8420
|
-
// a person most wants to see confirmed.
|
|
8421
|
-
});
|
|
8554
|
+
JSON.stringify(
|
|
8555
|
+
{
|
|
8556
|
+
config_file: configFile,
|
|
8557
|
+
env_file: envFile,
|
|
8558
|
+
agent_id: created.agentId,
|
|
8559
|
+
key_id: created.keyId,
|
|
8560
|
+
fingerprint: created.fingerprint,
|
|
8561
|
+
tools: created.tools,
|
|
8562
|
+
checks
|
|
8563
|
+
},
|
|
8564
|
+
null,
|
|
8565
|
+
2
|
|
8566
|
+
)
|
|
8567
|
+
);
|
|
8422
8568
|
}
|
|
8423
|
-
return
|
|
8569
|
+
return failed.length > 0 ? 1 : 0;
|
|
8424
8570
|
}
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
return
|
|
8571
|
+
function previousApprovalMode(dir = process.cwd()) {
|
|
8572
|
+
try {
|
|
8573
|
+
const text = readFileSync9(path12.join(dir, ".env.local"), "utf8");
|
|
8574
|
+
const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
|
|
8575
|
+
return value === "wait" || value === "throw" ? value : null;
|
|
8576
|
+
} catch {
|
|
8577
|
+
return null;
|
|
8432
8578
|
}
|
|
8433
|
-
heading("Veefore scan");
|
|
8434
|
-
out(
|
|
8435
|
-
`${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
|
|
8436
|
-
);
|
|
8437
|
-
printScan(scan);
|
|
8438
|
-
out();
|
|
8439
|
-
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
8440
|
-
return 0;
|
|
8441
8579
|
}
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8580
|
+
function approvalModeExplanation(mode) {
|
|
8581
|
+
if (mode === "wait") {
|
|
8582
|
+
return [
|
|
8583
|
+
` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
|
|
8584
|
+
` ${symbol.bullet} approved: the tool runs once and returns its real result`,
|
|
8585
|
+
` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
|
|
8586
|
+
style.grey(
|
|
8587
|
+
" The agent process must stay running while it waits (up to the approval's expiry)."
|
|
8588
|
+
),
|
|
8589
|
+
style.grey(
|
|
8590
|
+
" For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
|
|
8591
|
+
),
|
|
8592
|
+
style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
|
|
8593
|
+
];
|
|
8594
|
+
}
|
|
8595
|
+
return [
|
|
8596
|
+
` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
|
|
8597
|
+
" carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
|
|
8598
|
+
" or the action never happens.",
|
|
8599
|
+
style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
|
|
8600
|
+
];
|
|
8446
8601
|
}
|
|
8447
|
-
async function
|
|
8448
|
-
const
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
case "diff":
|
|
8460
|
-
return diffPolicy(rest, flags);
|
|
8461
|
-
default:
|
|
8602
|
+
async function selectOrganization(api, me, options, flags) {
|
|
8603
|
+
const organizations = me.organizations.length > 0 ? me.organizations : [
|
|
8604
|
+
{
|
|
8605
|
+
id: me.organization.id,
|
|
8606
|
+
name: me.organization.name,
|
|
8607
|
+
slug: me.organization.slug,
|
|
8608
|
+
roleKey: me.roleKey
|
|
8609
|
+
}
|
|
8610
|
+
];
|
|
8611
|
+
if (options.organizationId) {
|
|
8612
|
+
const match = organizations.find((org) => org.id === options.organizationId);
|
|
8613
|
+
if (!match) {
|
|
8462
8614
|
throw new CliError(
|
|
8463
|
-
`
|
|
8464
|
-
|
|
8615
|
+
`You are not a member of organization ${options.organizationId}.`,
|
|
8616
|
+
`Available: ${organizations.map((org) => org.id).join(", ")}`
|
|
8465
8617
|
);
|
|
8618
|
+
}
|
|
8619
|
+
return { value: match, asked: false };
|
|
8466
8620
|
}
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
workspace_id: args.includes("--all") ? void 0 : context.config.workspaceId,
|
|
8472
|
-
status: valueFor(args, "--status")
|
|
8473
|
-
});
|
|
8474
|
-
if (flags.json) {
|
|
8475
|
-
printJson({ items: policies });
|
|
8476
|
-
return 0;
|
|
8621
|
+
if (organizations.length === 1 || flags.yes) {
|
|
8622
|
+
const chosen2 = organizations[0];
|
|
8623
|
+
if (!chosen2) throw new CliError("Your account is not a member of any organization.");
|
|
8624
|
+
return { value: chosen2, asked: false };
|
|
8477
8625
|
}
|
|
8478
|
-
heading(`Policies in ${context.config.workspaceName}`);
|
|
8479
|
-
table(
|
|
8480
|
-
[
|
|
8481
|
-
{ header: "NAME", key: "name", width: 30 },
|
|
8482
|
-
{ header: "KEY", key: "key", width: 24 },
|
|
8483
|
-
{ header: "STATUS", key: "status", width: 18 },
|
|
8484
|
-
{ header: "ACTIVE v", key: "active", width: 10, align: "right" },
|
|
8485
|
-
{ header: "LATEST v", key: "latest", width: 10, align: "right" },
|
|
8486
|
-
{ header: "BINDINGS", key: "bindings", width: 9, align: "right" },
|
|
8487
|
-
{ header: "24H", key: "evaluations", width: 7, align: "right" }
|
|
8488
|
-
],
|
|
8489
|
-
policies.map((policy) => ({
|
|
8490
|
-
name: policy.name,
|
|
8491
|
-
key: policy.key,
|
|
8492
|
-
status: decisionStyle(policy.status),
|
|
8493
|
-
active: policy.activeVersion ? String(policy.activeVersion.versionNumber) : "\u2014",
|
|
8494
|
-
latest: policy.latestVersion ? String(policy.latestVersion.versionNumber) : "\u2014",
|
|
8495
|
-
bindings: String(policy.bindings.length),
|
|
8496
|
-
evaluations: String(policy.evaluations24h)
|
|
8497
|
-
}))
|
|
8498
|
-
);
|
|
8499
|
-
const unbound = policies.filter(
|
|
8500
|
-
(policy) => policy.status === "ACTIVE" && policy.bindings.length === 0
|
|
8501
|
-
);
|
|
8502
|
-
const drafts = policies.filter((policy) => policy.status === "DRAFT");
|
|
8503
8626
|
out();
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8627
|
+
const chosen = await select(
|
|
8628
|
+
"Which organization?",
|
|
8629
|
+
organizations.map((org) => ({
|
|
8630
|
+
label: org.name,
|
|
8631
|
+
hint: `${org.roleKey} \xB7 ${org.id}`,
|
|
8632
|
+
value: org
|
|
8633
|
+
})),
|
|
8634
|
+
{ quiet: true, back: true, backLabel: "Back" }
|
|
8635
|
+
);
|
|
8636
|
+
if (chosen === BACK) return BACK;
|
|
8637
|
+
await api.post("/v1/auth/scope", { organization_id: chosen.id });
|
|
8638
|
+
return { value: chosen, asked: true };
|
|
8639
|
+
}
|
|
8640
|
+
async function selectWorkspace(api, me, options, flags) {
|
|
8641
|
+
const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
|
|
8642
|
+
if (workspaces.length === 0) {
|
|
8643
|
+
throw new CliError(
|
|
8644
|
+
"This organization has no workspaces.",
|
|
8645
|
+
"Create one in the console, or ask an admin to."
|
|
8507
8646
|
);
|
|
8508
8647
|
}
|
|
8509
|
-
if (
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8648
|
+
if (options.workspaceId) {
|
|
8649
|
+
const match = workspaces.find((workspace) => workspace.id === options.workspaceId);
|
|
8650
|
+
if (!match) throw new CliError(`Workspace ${options.workspaceId} was not found.`);
|
|
8651
|
+
return { value: match, asked: false };
|
|
8513
8652
|
}
|
|
8514
|
-
if (
|
|
8515
|
-
|
|
8653
|
+
if (workspaces.length === 1 || flags.yes) {
|
|
8654
|
+
const chosen2 = workspaces[0];
|
|
8655
|
+
return { value: chosen2, asked: false };
|
|
8516
8656
|
}
|
|
8517
8657
|
out();
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
`/v1/policies/${policyId}`
|
|
8658
|
+
const chosen = await select(
|
|
8659
|
+
"Which workspace?",
|
|
8660
|
+
workspaces.map((workspace) => ({
|
|
8661
|
+
label: workspace.name,
|
|
8662
|
+
hint: workspace.id,
|
|
8663
|
+
value: workspace
|
|
8664
|
+
})),
|
|
8665
|
+
{ quiet: true, back: true }
|
|
8527
8666
|
);
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
printJson({ policy, versions });
|
|
8531
|
-
return 0;
|
|
8532
|
-
}
|
|
8533
|
-
heading(policy.name);
|
|
8534
|
-
details([
|
|
8535
|
-
["ID", policy.id],
|
|
8536
|
-
["Key", policy.key],
|
|
8537
|
-
["Status", decisionStyle(policy.status)],
|
|
8538
|
-
["Priority", String(policy.priority)],
|
|
8539
|
-
[
|
|
8540
|
-
"Active version",
|
|
8541
|
-
policy.activeVersion ? `v${policy.activeVersion.versionNumber}` : style.grey("none")
|
|
8542
|
-
],
|
|
8543
|
-
["Bound agents", String(policy.boundAgentCount)],
|
|
8544
|
-
["Evaluations (24h)", `${policy.evaluations24h} ${style.grey(`${policy.blocks24h} blocked`)}`],
|
|
8545
|
-
["Owner", policy.ownerEmail ?? style.grey("unknown")],
|
|
8546
|
-
["Description", policy.description || style.grey("none")]
|
|
8547
|
-
]);
|
|
8548
|
-
if (policy.plainEnglish && policy.plainEnglish.length > 0) {
|
|
8549
|
-
heading("In plain English");
|
|
8550
|
-
for (const line of policy.plainEnglish) out(` ${symbol.bullet} ${line}`);
|
|
8551
|
-
}
|
|
8552
|
-
if (policy.bindings.length > 0) {
|
|
8553
|
-
heading("Bindings");
|
|
8554
|
-
table(
|
|
8555
|
-
[
|
|
8556
|
-
{ header: "SCOPE", key: "scope", width: 14 },
|
|
8557
|
-
{ header: "TARGET", key: "target", width: 34 },
|
|
8558
|
-
{ header: "ENABLED", key: "enabled", width: 8 }
|
|
8559
|
-
],
|
|
8560
|
-
policy.bindings.map((binding) => ({
|
|
8561
|
-
scope: binding.scope,
|
|
8562
|
-
target: binding.scopeId,
|
|
8563
|
-
enabled: binding.enabled ? style.green("yes") : style.red("no")
|
|
8564
|
-
}))
|
|
8565
|
-
);
|
|
8566
|
-
}
|
|
8567
|
-
heading("Versions");
|
|
8568
|
-
table(
|
|
8569
|
-
[
|
|
8570
|
-
{ header: "v", key: "version", width: 4, align: "right" },
|
|
8571
|
-
{ header: "STATUS", key: "status", width: 20 },
|
|
8572
|
-
{ header: "CREATED", key: "created", width: 20 },
|
|
8573
|
-
{ header: "SUMMARY", key: "summary", width: 40 }
|
|
8574
|
-
],
|
|
8575
|
-
versions.map((version) => ({
|
|
8576
|
-
version: String(version.versionNumber),
|
|
8577
|
-
status: decisionStyle(version.status),
|
|
8578
|
-
created: version.createdAt.slice(0, 16).replace("T", " "),
|
|
8579
|
-
summary: version.changeSummary ?? ""
|
|
8580
|
-
}))
|
|
8581
|
-
);
|
|
8582
|
-
out();
|
|
8583
|
-
return 0;
|
|
8667
|
+
if (chosen === BACK) return BACK;
|
|
8668
|
+
return { value: chosen, asked: true };
|
|
8584
8669
|
}
|
|
8585
|
-
async function
|
|
8586
|
-
const
|
|
8587
|
-
const
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
return 0;
|
|
8670
|
+
async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
8671
|
+
const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
|
|
8672
|
+
const environments = all.filter((environment) => environment.workspaceId === workspaceId);
|
|
8673
|
+
if (environments.length === 0) {
|
|
8674
|
+
throw new CliError(
|
|
8675
|
+
"That workspace has no environments.",
|
|
8676
|
+
"Create one in the console, or ask an admin to."
|
|
8677
|
+
);
|
|
8594
8678
|
}
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
`/v1/policies/${policy.id}/tests/run`,
|
|
8600
|
-
{}
|
|
8601
|
-
);
|
|
8602
|
-
reports.push({ policy, result });
|
|
8603
|
-
} catch (error) {
|
|
8604
|
-
reports.push({
|
|
8605
|
-
policy,
|
|
8606
|
-
result: null,
|
|
8607
|
-
error: error instanceof Error ? error.message : String(error)
|
|
8608
|
-
});
|
|
8609
|
-
}
|
|
8679
|
+
if (options.environmentId) {
|
|
8680
|
+
const match = environments.find((environment) => environment.id === options.environmentId);
|
|
8681
|
+
if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
|
|
8682
|
+
return { value: match, asked: false };
|
|
8610
8683
|
}
|
|
8611
|
-
if (flags.
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
renderTestReports(reports);
|
|
8684
|
+
if (flags.yes) {
|
|
8685
|
+
const chosen = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
|
|
8686
|
+
return { value: chosen, asked: false };
|
|
8615
8687
|
}
|
|
8616
|
-
const
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
if (report.error) {
|
|
8628
|
-
out(`${symbol.fail} ${style.bold(report.policy.name)} ${style.grey(report.error)}`);
|
|
8629
|
-
totalFailed += 1;
|
|
8630
|
-
continue;
|
|
8631
|
-
}
|
|
8632
|
-
const result = report.result;
|
|
8633
|
-
if (!result || caseCount(result) === 0) {
|
|
8634
|
-
withoutTests += 1;
|
|
8635
|
-
out(
|
|
8636
|
-
`${symbol.warn} ${style.bold(report.policy.name)} ${style.amber("no test cases recorded")}`
|
|
8637
|
-
);
|
|
8638
|
-
continue;
|
|
8639
|
-
}
|
|
8640
|
-
totalCases += caseCount(result);
|
|
8641
|
-
totalFailed += result.failed;
|
|
8642
|
-
const icon = result.failed === 0 ? symbol.pass : symbol.fail;
|
|
8643
|
-
out(
|
|
8644
|
-
`${icon} ${style.bold(report.policy.name)} ${style.grey(`${result.passed}/${caseCount(result)} passing`)}`
|
|
8688
|
+
const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
|
|
8689
|
+
for (; ; ) {
|
|
8690
|
+
out();
|
|
8691
|
+
const chosen = await select(
|
|
8692
|
+
"Which environment?",
|
|
8693
|
+
environments.map((environment) => ({
|
|
8694
|
+
label: environment.name,
|
|
8695
|
+
hint: `${environment.type} \xB7 ${environment.gatewayMode ?? "\u2014"} \xB7 ${environment.id}`,
|
|
8696
|
+
value: environment
|
|
8697
|
+
})),
|
|
8698
|
+
{ quiet: true, initial: development >= 0 ? development : 0, back: true }
|
|
8645
8699
|
);
|
|
8646
|
-
|
|
8700
|
+
if (chosen === BACK) return BACK;
|
|
8701
|
+
if (chosen.type === "PRODUCTION" && !options.force) {
|
|
8702
|
+
out();
|
|
8647
8703
|
out(
|
|
8648
|
-
|
|
8704
|
+
`${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.`
|
|
8649
8705
|
);
|
|
8650
|
-
if (
|
|
8706
|
+
if (!await confirm("Continue with the production environment?", { yes: flags.yes })) {
|
|
8707
|
+
out(style.grey(" Choose another environment."));
|
|
8708
|
+
continue;
|
|
8709
|
+
}
|
|
8651
8710
|
}
|
|
8711
|
+
return { value: chosen, asked: true };
|
|
8652
8712
|
}
|
|
8713
|
+
}
|
|
8714
|
+
async function stepScreen(source, decided, step, total) {
|
|
8715
|
+
await beginScreen(source);
|
|
8653
8716
|
out();
|
|
8654
|
-
out(
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
`
|
|
8660
|
-
|
|
8717
|
+
out(` ${style.bold("Set up this project")} ${style.grey(`step ${step} of ${total}`)}`);
|
|
8718
|
+
if (decided.length > 0) {
|
|
8719
|
+
out();
|
|
8720
|
+
const width = Math.max(...decided.map(([label]) => label.length));
|
|
8721
|
+
for (const [label, value] of decided) {
|
|
8722
|
+
out(` ${symbol.pass} ${style.grey(label.padEnd(width))} ${value}`);
|
|
8723
|
+
}
|
|
8661
8724
|
}
|
|
8662
|
-
out();
|
|
8663
8725
|
}
|
|
8664
|
-
async function
|
|
8665
|
-
|
|
8666
|
-
const policyId = positional(args);
|
|
8667
|
-
if (!policyId) {
|
|
8668
|
-
throw new CliError(
|
|
8669
|
-
"A policy id is required.",
|
|
8670
|
-
'Usage: veefore policies deploy <policyId> [--version <n>] [--reason "\u2026"]'
|
|
8671
|
-
);
|
|
8672
|
-
}
|
|
8673
|
-
const policy = await context.api.get(`/v1/policies/${policyId}`);
|
|
8674
|
-
const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
|
|
8675
|
-
const requested = valueFor(args, "--version");
|
|
8676
|
-
const target = requested ? versions.find((version) => String(version.versionNumber) === requested) : versions.find((version) => version.status === "DRAFT") ?? versions.find((version) => version.status === "TESTED");
|
|
8677
|
-
if (!target) {
|
|
8678
|
-
throw new CliError(
|
|
8679
|
-
requested ? `Version ${requested} was not found for this policy.` : "There is no draft version to deploy.",
|
|
8680
|
-
"Edit the policy in the console to create a new draft, then deploy it."
|
|
8681
|
-
);
|
|
8682
|
-
}
|
|
8683
|
-
if (target.versionNumber === policy.activeVersion?.versionNumber) {
|
|
8684
|
-
out(`${symbol.info} Version ${target.versionNumber} is already active. Nothing to do.`);
|
|
8685
|
-
return 0;
|
|
8686
|
-
}
|
|
8687
|
-
heading(`Deploy ${policy.name} v${target.versionNumber}`);
|
|
8688
|
-
details([
|
|
8689
|
-
["Environment", `${context.config.environmentName} (${context.config.environmentType})`],
|
|
8690
|
-
["Policy", `${policy.name} (${policy.id})`],
|
|
8691
|
-
[
|
|
8692
|
-
"Currently active",
|
|
8693
|
-
policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
|
|
8694
|
-
],
|
|
8695
|
-
["Deploying", `v${target.versionNumber}`],
|
|
8696
|
-
["Bindings", String(policy.bindings.length)]
|
|
8697
|
-
]);
|
|
8698
|
-
if (policy.bindings.length === 0) {
|
|
8726
|
+
async function askAgentName(suggested) {
|
|
8727
|
+
for (; ; ) {
|
|
8699
8728
|
out();
|
|
8700
|
-
|
|
8701
|
-
|
|
8729
|
+
const choice = await select(
|
|
8730
|
+
"Agent name",
|
|
8731
|
+
[
|
|
8732
|
+
{ label: suggested, hint: "suggested", value: "keep" },
|
|
8733
|
+
{ label: "Type a different name\u2026", value: "type" }
|
|
8734
|
+
],
|
|
8735
|
+
{ quiet: true, back: true }
|
|
8702
8736
|
);
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
to: target.versionNumber
|
|
8708
|
-
}).catch(() => null);
|
|
8709
|
-
if (diff) {
|
|
8710
|
-
heading("Changes");
|
|
8711
|
-
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
8712
|
-
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
8713
|
-
for (const rule of diff.changed) {
|
|
8714
|
-
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
8715
|
-
}
|
|
8716
|
-
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
8717
|
-
out(style.grey(" No rule changes."));
|
|
8718
|
-
}
|
|
8719
|
-
if (diff.riskAssessment) {
|
|
8720
|
-
out();
|
|
8721
|
-
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
8722
|
-
}
|
|
8737
|
+
if (choice === BACK) return BACK;
|
|
8738
|
+
if (choice === "keep") {
|
|
8739
|
+
out(`${symbol.pass} Agent name: ${style.bold(suggested)}`);
|
|
8740
|
+
return suggested;
|
|
8723
8741
|
}
|
|
8742
|
+
const typed = (await ask(`Agent name ${style.grey(`[${suggested}]`)}:`, suggested)).trim();
|
|
8743
|
+
if (typed === "") continue;
|
|
8744
|
+
out(`${symbol.pass} Agent name: ${style.bold(typed)}`);
|
|
8745
|
+
return typed;
|
|
8724
8746
|
}
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
let testResult = null;
|
|
8747
|
+
}
|
|
8748
|
+
async function discoverAudience(gatewayUrl) {
|
|
8728
8749
|
try {
|
|
8729
|
-
|
|
8730
|
-
|
|
8750
|
+
const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
|
|
8751
|
+
headers: { accept: "application/json" }
|
|
8731
8752
|
});
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8753
|
+
if (!response.ok) return gatewayUrl;
|
|
8754
|
+
const body = await response.json();
|
|
8755
|
+
const audience = body.data?.audience;
|
|
8756
|
+
return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
|
|
8757
|
+
} catch {
|
|
8758
|
+
return gatewayUrl;
|
|
8736
8759
|
}
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
);
|
|
8743
|
-
}
|
|
8744
|
-
out();
|
|
8745
|
-
throw new CliError(
|
|
8746
|
-
`${testResult.failed} test case(s) fail. This version will not be deployed.`,
|
|
8747
|
-
"Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
|
|
8748
|
-
);
|
|
8760
|
+
}
|
|
8761
|
+
async function configureStarterPolicy(api, workspaceId, flags, options) {
|
|
8762
|
+
if (options.skipPolicy) {
|
|
8763
|
+
out(`${symbol.info} Skipped starter policy (--skip-policy).`);
|
|
8764
|
+
return;
|
|
8749
8765
|
}
|
|
8750
|
-
|
|
8751
|
-
|
|
8766
|
+
const existing = await api.get(
|
|
8767
|
+
"/v1/policies",
|
|
8768
|
+
{ workspace_id: workspaceId }
|
|
8769
|
+
);
|
|
8770
|
+
const count = Array.isArray(existing) ? existing.length : existing.items?.length ?? 0;
|
|
8771
|
+
if (count > 0) {
|
|
8772
|
+
out(`${symbol.pass} Workspace already has ${count} policy/policies. Leaving them alone.`);
|
|
8773
|
+
return;
|
|
8774
|
+
}
|
|
8775
|
+
const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
|
|
8776
|
+
if (!create) {
|
|
8777
|
+
out(style.grey(" Skipped. Create one later from a template in the console."));
|
|
8778
|
+
return;
|
|
8779
|
+
}
|
|
8780
|
+
const policySpinner = spinner("Creating starter policy from the safe-defaults template");
|
|
8781
|
+
try {
|
|
8782
|
+
const result = await api.post("/v1/policies", {
|
|
8783
|
+
workspace_id: workspaceId,
|
|
8784
|
+
name: "Starter \u2014 safe defaults",
|
|
8785
|
+
template_key: "starter-observe-safe-defaults"
|
|
8786
|
+
});
|
|
8787
|
+
await api.post(`/v1/policies/${result.policy.id}/bindings`, {
|
|
8788
|
+
scope: "WORKSPACE",
|
|
8789
|
+
scope_id: workspaceId
|
|
8790
|
+
});
|
|
8791
|
+
policySpinner.succeed(
|
|
8792
|
+
`Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
|
|
8793
|
+
);
|
|
8794
|
+
keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
|
|
8752
8795
|
out(
|
|
8753
|
-
|
|
8796
|
+
style.grey(
|
|
8797
|
+
" Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
|
|
8798
|
+
)
|
|
8754
8799
|
);
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8800
|
+
} catch (error) {
|
|
8801
|
+
policySpinner.fail("Could not create the starter policy");
|
|
8802
|
+
if (error instanceof CliApiError) {
|
|
8803
|
+
out(style.grey(` ${error.message}`));
|
|
8804
|
+
out(style.grey(" You can create one from a template in the console instead."));
|
|
8805
|
+
return;
|
|
8759
8806
|
}
|
|
8807
|
+
throw error;
|
|
8760
8808
|
}
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8809
|
+
}
|
|
8810
|
+
async function validateConnection(api, gatewayUrl, agentId, environmentId) {
|
|
8811
|
+
const checks = [];
|
|
8812
|
+
try {
|
|
8813
|
+
await api.get("/v1/auth/session");
|
|
8814
|
+
checks.push({ name: "Control plane reachable", status: "pass", message: api.controlPlaneUrl });
|
|
8815
|
+
} catch (error) {
|
|
8816
|
+
checks.push({
|
|
8817
|
+
name: "Control plane reachable",
|
|
8818
|
+
status: "fail",
|
|
8819
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8820
|
+
});
|
|
8768
8821
|
}
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8822
|
+
try {
|
|
8823
|
+
const response = await fetch(`${gatewayUrl}/health`, {
|
|
8824
|
+
signal: AbortSignal.timeout(5e3)
|
|
8825
|
+
});
|
|
8826
|
+
const body = await response.json();
|
|
8827
|
+
checks.push({
|
|
8828
|
+
name: "Gateway reachable",
|
|
8829
|
+
status: response.ok ? "pass" : "fail",
|
|
8830
|
+
message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
|
|
8831
|
+
});
|
|
8832
|
+
} catch (error) {
|
|
8833
|
+
checks.push({
|
|
8834
|
+
name: "Gateway reachable",
|
|
8835
|
+
status: "fail",
|
|
8836
|
+
message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
|
|
8837
|
+
});
|
|
8773
8838
|
}
|
|
8774
|
-
const deploySpinner = spinner(`Activating v${target.versionNumber}`);
|
|
8775
8839
|
try {
|
|
8776
|
-
await
|
|
8777
|
-
|
|
8778
|
-
() => context.api.post(`/v1/policies/${policyId}/activate`, {
|
|
8779
|
-
version_id: target.id,
|
|
8780
|
-
reason
|
|
8781
|
-
})
|
|
8840
|
+
const identities = await api.get(
|
|
8841
|
+
`/v1/agents/${agentId}/identities`
|
|
8782
8842
|
);
|
|
8783
|
-
|
|
8843
|
+
const active = identities.find((identity) => identity.status === "ACTIVE");
|
|
8844
|
+
checks.push({
|
|
8845
|
+
name: "Agent identity active",
|
|
8846
|
+
status: active ? "pass" : "fail",
|
|
8847
|
+
message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
|
|
8848
|
+
});
|
|
8784
8849
|
} catch (error) {
|
|
8785
|
-
|
|
8786
|
-
|
|
8850
|
+
checks.push({
|
|
8851
|
+
name: "Agent identity active",
|
|
8852
|
+
status: "warn",
|
|
8853
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8854
|
+
});
|
|
8787
8855
|
}
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
|
|
8792
|
-
out();
|
|
8793
|
-
return 0;
|
|
8794
|
-
}
|
|
8795
|
-
async function diffPolicy(args, flags) {
|
|
8796
|
-
const context = await resolveContext(flags);
|
|
8797
|
-
const policyId = positional(args);
|
|
8798
|
-
if (!policyId) {
|
|
8799
|
-
throw new CliError(
|
|
8800
|
-
"A policy id is required.",
|
|
8801
|
-
"Usage: veefore policies diff <policyId> --from <n> --to <n>"
|
|
8856
|
+
try {
|
|
8857
|
+
const passport = await api.get(
|
|
8858
|
+
`/v1/passports/${agentId}`
|
|
8802
8859
|
);
|
|
8860
|
+
checks.push({
|
|
8861
|
+
name: "Agent passport issued",
|
|
8862
|
+
status: passport ? "pass" : "warn",
|
|
8863
|
+
message: passport ? `version ${passport.version}` : "not issued yet"
|
|
8864
|
+
});
|
|
8865
|
+
} catch {
|
|
8866
|
+
checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
|
|
8803
8867
|
}
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
8868
|
+
try {
|
|
8869
|
+
const environment = await api.get(
|
|
8870
|
+
`/v1/environments/${environmentId}`
|
|
8871
|
+
);
|
|
8872
|
+
checks.push({
|
|
8873
|
+
name: "Enforcement mode",
|
|
8874
|
+
status: "pass",
|
|
8875
|
+
message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
|
|
8876
|
+
});
|
|
8877
|
+
} catch {
|
|
8878
|
+
checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
|
|
8816
8879
|
}
|
|
8817
|
-
|
|
8818
|
-
|
|
8880
|
+
return checks;
|
|
8881
|
+
}
|
|
8882
|
+
function parseInitArgs(args) {
|
|
8883
|
+
const options = { force: false, skipPolicy: false, skipCode: false };
|
|
8884
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8885
|
+
const arg = args[index];
|
|
8886
|
+
const next = () => {
|
|
8887
|
+
const value = args[index + 1];
|
|
8888
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
8889
|
+
throw new CliError(`Option ${String(arg)} requires a value.`);
|
|
8890
|
+
}
|
|
8891
|
+
index += 1;
|
|
8892
|
+
return value;
|
|
8893
|
+
};
|
|
8894
|
+
switch (arg) {
|
|
8895
|
+
case "--force":
|
|
8896
|
+
options.force = true;
|
|
8897
|
+
break;
|
|
8898
|
+
case "--skip-policy":
|
|
8899
|
+
options.skipPolicy = true;
|
|
8900
|
+
break;
|
|
8901
|
+
case "--skip-code":
|
|
8902
|
+
options.skipCode = true;
|
|
8903
|
+
break;
|
|
8904
|
+
case "--new-agent":
|
|
8905
|
+
options.newAgent = true;
|
|
8906
|
+
break;
|
|
8907
|
+
case "--control-plane":
|
|
8908
|
+
options.controlPlaneUrl = next();
|
|
8909
|
+
break;
|
|
8910
|
+
case "--gateway":
|
|
8911
|
+
options.gatewayUrl = next();
|
|
8912
|
+
break;
|
|
8913
|
+
case "--organization":
|
|
8914
|
+
options.organizationId = next();
|
|
8915
|
+
break;
|
|
8916
|
+
case "--workspace":
|
|
8917
|
+
options.workspaceId = next();
|
|
8918
|
+
break;
|
|
8919
|
+
case "--environment":
|
|
8920
|
+
options.environmentId = next();
|
|
8921
|
+
break;
|
|
8922
|
+
case "--name":
|
|
8923
|
+
options.agentName = next();
|
|
8924
|
+
break;
|
|
8925
|
+
case "--capability":
|
|
8926
|
+
options.capabilities = [...options.capabilities ?? [], next()];
|
|
8927
|
+
break;
|
|
8928
|
+
case "--approval-mode": {
|
|
8929
|
+
const value = next().toLowerCase();
|
|
8930
|
+
if (value !== "wait" && value !== "throw") {
|
|
8931
|
+
throw new CliError('--approval-mode must be "wait" or "throw".');
|
|
8932
|
+
}
|
|
8933
|
+
options.approvalMode = value;
|
|
8934
|
+
break;
|
|
8935
|
+
}
|
|
8936
|
+
default:
|
|
8937
|
+
if (arg?.startsWith("-")) {
|
|
8938
|
+
throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
|
|
8939
|
+
}
|
|
8940
|
+
}
|
|
8819
8941
|
}
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8942
|
+
return options;
|
|
8943
|
+
}
|
|
8944
|
+
async function projectHome(existing, flags) {
|
|
8945
|
+
enterApp();
|
|
8946
|
+
try {
|
|
8947
|
+
return await projectHomeLoop(existing, flags);
|
|
8948
|
+
} finally {
|
|
8949
|
+
leaveApp();
|
|
8950
|
+
printFarewell();
|
|
8823
8951
|
}
|
|
8824
|
-
out();
|
|
8825
|
-
return 0;
|
|
8826
8952
|
}
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
{
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
"
|
|
8880
|
-
|
|
8881
|
-
},
|
|
8882
|
-
{
|
|
8883
|
-
name: "undo",
|
|
8884
|
-
summary: "Restore the files the last `veefore integrate` changed.",
|
|
8885
|
-
usage: "veefore undo [--force]",
|
|
8886
|
-
description: "Restores each file from the backup taken before the last rewrite. A file you edited afterwards is left alone and listed, unless --force is given.",
|
|
8887
|
-
options: [["--force", "Restore files even if they were edited after the rewrite."]]
|
|
8888
|
-
},
|
|
8889
|
-
{
|
|
8890
|
-
name: "remove",
|
|
8891
|
-
summary: "Take Veefore out of this project: guard() from the code, then disconnect.",
|
|
8892
|
-
usage: "veefore remove [--dry-run] [--keep-agent]",
|
|
8893
|
-
description: "Offers to revoke the agent in Veefore if it is still active, removes every guard() this CLI added (showing the diff, asking once, backing up and rolling back if your checks fail), then deletes veefore.config.json and the Veefore block in .env.local, including the private key. Edits you made after setup are kept. `veefore` offers this automatically when the agent was revoked in the console.",
|
|
8894
|
-
options: [
|
|
8895
|
-
["--dry-run", "Show what would change and write nothing."],
|
|
8896
|
-
["--keep-agent", "Do not offer to revoke the agent in Veefore."]
|
|
8897
|
-
],
|
|
8898
|
-
examples: ["veefore remove --dry-run", "veefore remove"]
|
|
8899
|
-
},
|
|
8900
|
-
{
|
|
8901
|
-
name: "dev",
|
|
8902
|
-
summary: "Watch decisions live while you build.",
|
|
8903
|
-
usage: "veefore dev [--all]",
|
|
8904
|
-
description: "Confirms the gateway is reachable, reports the enforcement mode, then streams every decision for this agent with the next concrete step for each outcome.",
|
|
8905
|
-
options: [["--all", "Stream decisions for every agent in the environment."]]
|
|
8906
|
-
},
|
|
8907
|
-
{
|
|
8908
|
-
name: "doctor",
|
|
8909
|
-
summary: "Audit the security posture of this setup.",
|
|
8910
|
-
usage: "veefore doctor [--strict]",
|
|
8911
|
-
description: "Checks authentication, MFA, TLS, local secret permissions, identity and passport validity, gateway health, dangerous tools, network restrictions, policy coverage, production data access, MCP transport and authorization, audit chain integrity, credential rotation, and SDK version alignment.",
|
|
8912
|
-
options: [["--strict", "Exit non-zero on warnings as well as failures."]]
|
|
8913
|
-
},
|
|
8914
|
-
{
|
|
8915
|
-
name: "status",
|
|
8916
|
-
summary: "Show the current environment, mode and agent health.",
|
|
8917
|
-
usage: "veefore status",
|
|
8918
|
-
description: "Prints the active organization, workspace and environment, the enforcement mode, the agent lifecycle state, gateway health, and the last few decisions."
|
|
8919
|
-
},
|
|
8920
|
-
{
|
|
8921
|
-
name: "test",
|
|
8922
|
-
summary: "Verify enforcement works, without performing any action.",
|
|
8923
|
-
usage: "veefore test [--tool <key>]",
|
|
8924
|
-
description: "Runs a dry-run probe across every tool this agent is authorized for, checks that policy test cases pass, and fails if a dangerous tool is permitted only by the default decision. Suitable as a CI gate.",
|
|
8925
|
-
options: [["--tool <key>", "Probe a single tool."]]
|
|
8926
|
-
},
|
|
8927
|
-
{
|
|
8928
|
-
name: "agents",
|
|
8929
|
-
summary: "Inspect and control the agent fleet.",
|
|
8930
|
-
usage: "veefore agents <list|show|pause|resume|suspend|revoke|rotate-key> [agentId]",
|
|
8931
|
-
description: "Lifecycle control. `revoke` is irreversible for the identity and shows the full blast radius before asking for confirmation. `rotate-key` issues a new key pair and invalidates every outstanding session for that agent.",
|
|
8932
|
-
options: [
|
|
8933
|
-
["--all", "List across the whole organization, not just this environment."],
|
|
8934
|
-
["--status <status>", "Filter by lifecycle status."],
|
|
8935
|
-
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8936
|
-
],
|
|
8937
|
-
examples: [
|
|
8938
|
-
"veefore agents list",
|
|
8939
|
-
'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
|
|
8940
|
-
]
|
|
8941
|
-
},
|
|
8942
|
-
{
|
|
8943
|
-
name: "policies",
|
|
8944
|
-
summary: "List, test and deploy policies.",
|
|
8945
|
-
usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
|
|
8946
|
-
description: "`test` runs the recorded test cases. `deploy` activates a version and refuses to do so while any test case fails; it requires a reason and step-up authentication.",
|
|
8947
|
-
options: [
|
|
8948
|
-
["--version <n>", "Version to deploy, for `deploy`."],
|
|
8949
|
-
["--from <n> --to <n>", "Versions to compare, for `diff`."],
|
|
8950
|
-
['--reason "<text>"', "Reason recorded in the audit trail."]
|
|
8951
|
-
],
|
|
8952
|
-
examples: [
|
|
8953
|
-
"veefore policies list",
|
|
8954
|
-
"veefore policies test",
|
|
8955
|
-
"veefore policies deploy pol_01ABC"
|
|
8956
|
-
]
|
|
8957
|
-
},
|
|
8958
|
-
{
|
|
8959
|
-
name: "logs",
|
|
8960
|
-
summary: "Read or stream the decision log.",
|
|
8961
|
-
usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
|
|
8962
|
-
description: "Every line is a server-side decision with the policy and rule that produced it.",
|
|
8963
|
-
options: [
|
|
8964
|
-
["--follow, -f", "Stream new decisions as they happen."],
|
|
8965
|
-
["--decision <d>", "Filter by decision, e.g. BLOCK."],
|
|
8966
|
-
["--agent <id>", "Filter by agent."],
|
|
8967
|
-
["--all", "Include every agent in the environment."],
|
|
8968
|
-
["--limit <n>", "Number of recent decisions to show."]
|
|
8969
|
-
]
|
|
8970
|
-
},
|
|
8971
|
-
{
|
|
8972
|
-
name: "audit",
|
|
8973
|
-
summary: "Query, verify and export the audit trail.",
|
|
8974
|
-
usage: "veefore audit <list|show|verify|export> [eventId]",
|
|
8975
|
-
description: "`verify` re-checks the hash chain and exits non-zero if it is broken. `export` returns a JSON or CSV file together with a content hash an auditor can use to prove the file is unaltered.",
|
|
8976
|
-
options: [
|
|
8977
|
-
["--severity <s>", "Filter by severity."],
|
|
8978
|
-
["--type <t>", "Filter by event type."],
|
|
8979
|
-
["--from <iso> --to <iso>", "Time range."],
|
|
8980
|
-
["--format <json|csv>", "Export format."],
|
|
8981
|
-
['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
|
|
8982
|
-
["--out <file>", "Write the export to a file (mode 0600)."]
|
|
8983
|
-
],
|
|
8984
|
-
examples: [
|
|
8985
|
-
"veefore audit verify",
|
|
8986
|
-
'veefore audit export --format csv --reason "Quarterly SOC 2 evidence" --out audit.csv'
|
|
8987
|
-
]
|
|
8988
|
-
},
|
|
8989
|
-
{
|
|
8990
|
-
name: "deploy",
|
|
8991
|
-
summary: "Promote the environment to enforcement.",
|
|
8992
|
-
usage: "veefore deploy [--mode ENFORCE|SIMULATE|OBSERVE]",
|
|
8993
|
-
description: "Runs preflight checks, replays recent observed traffic to show exactly what starts being refused, then requires confirmation, a reason and step-up authentication. Reducing enforcement always requires a reason and is recorded as a CRITICAL audit event.",
|
|
8994
|
-
options: [
|
|
8995
|
-
["--mode <mode>", "Target mode. Defaults to ENFORCE."],
|
|
8996
|
-
['--reason "<text>"', "Reason recorded in the audit trail."],
|
|
8997
|
-
["--skip-preflight", "Incident use only. Recorded in the audit reason."]
|
|
8998
|
-
]
|
|
8999
|
-
},
|
|
9000
|
-
{
|
|
9001
|
-
name: "login",
|
|
9002
|
-
summary: "Sign in through your browser and store a session token.",
|
|
9003
|
-
usage: "veefore login [--device] [--password] [--control-plane <url>]",
|
|
9004
|
-
description: "Opens the console in your browser, where you sign in (or create an account) and approve this terminal. The terminal never sees your password. Over SSH, or with --device, it prints a link and a code to enter in any browser instead. Only the resulting session token is stored, in ~/.veefore/credentials.json at mode 0600, and the session can be revoked from your profile. In CI, set VEEFORE_EMAIL and VEEFORE_PASSWORD.",
|
|
9005
|
-
options: [
|
|
9006
|
-
["--device", "Show a link and a code instead of opening a browser here."],
|
|
9007
|
-
["--password", "Type email and password in the terminal instead of using the browser."]
|
|
8953
|
+
async function projectHomeLoop(existing, flags) {
|
|
8954
|
+
const source = welcomeSource(flags);
|
|
8955
|
+
let last = 0;
|
|
8956
|
+
for (let round = 0; ; round += 1) {
|
|
8957
|
+
await beginScreen(source, { animate: round === 0 });
|
|
8958
|
+
const state = await agentState(existing);
|
|
8959
|
+
printProjectSummary(existing, state);
|
|
8960
|
+
const choices = homeChoices(existing, state, flags);
|
|
8961
|
+
choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
|
|
8962
|
+
out();
|
|
8963
|
+
let picked;
|
|
8964
|
+
try {
|
|
8965
|
+
picked = await select(
|
|
8966
|
+
"What would you like to do?",
|
|
8967
|
+
choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice })),
|
|
8968
|
+
{
|
|
8969
|
+
// Repainted as soon as a sign-in, sign-out or project change is
|
|
8970
|
+
// noticed, so the panel and the status never sit stale.
|
|
8971
|
+
redraw: () => {
|
|
8972
|
+
drawScreen(source);
|
|
8973
|
+
printProjectSummary(existing, state);
|
|
8974
|
+
out();
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
);
|
|
8978
|
+
} catch (error) {
|
|
8979
|
+
if (error instanceof MenuCancelled) return last;
|
|
8980
|
+
throw error;
|
|
8981
|
+
}
|
|
8982
|
+
if (picked.leaves) {
|
|
8983
|
+
return onOrdinaryScreen(async () => {
|
|
8984
|
+
drawScreen(source);
|
|
8985
|
+
try {
|
|
8986
|
+
return await picked.run();
|
|
8987
|
+
} catch (error) {
|
|
8988
|
+
return error instanceof MenuCancelled ? last : reportError(error, flags);
|
|
8989
|
+
}
|
|
8990
|
+
});
|
|
8991
|
+
}
|
|
8992
|
+
last = await runOnScreen(source, flags, () => picked.run(), {
|
|
8993
|
+
label: "Back to menu",
|
|
8994
|
+
live: picked.live
|
|
8995
|
+
});
|
|
8996
|
+
if (!isInteractive()) return last;
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8999
|
+
function printProjectSummary(existing, state) {
|
|
9000
|
+
out();
|
|
9001
|
+
details([
|
|
9002
|
+
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
9003
|
+
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
9004
|
+
[
|
|
9005
|
+
"Status",
|
|
9006
|
+
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")
|
|
9008
9007
|
]
|
|
9009
|
-
|
|
9010
|
-
{
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9008
|
+
]);
|
|
9009
|
+
if (state.kind === "session-ended") {
|
|
9010
|
+
out(`${symbol.warn} ${state.message}`);
|
|
9011
|
+
}
|
|
9012
|
+
}
|
|
9013
|
+
function homeChoices(existing, state, flags) {
|
|
9014
|
+
const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
|
|
9015
|
+
if (ended) {
|
|
9016
|
+
out();
|
|
9017
|
+
out(
|
|
9018
|
+
`${symbol.warn} ${style.amber(
|
|
9019
|
+
state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
|
|
9020
|
+
)} Every tool call wrapped in guard() is now refused.`
|
|
9021
|
+
);
|
|
9022
|
+
return [
|
|
9023
|
+
{
|
|
9024
|
+
label: "Remove Veefore from this project",
|
|
9025
|
+
hint: "take guard() out of the code, disconnect",
|
|
9026
|
+
run: () => removeCommand([], flags, state),
|
|
9027
|
+
leaves: true
|
|
9028
|
+
},
|
|
9029
|
+
{
|
|
9030
|
+
label: "Set it up again with a new agent",
|
|
9031
|
+
hint: "the guarded code keeps working",
|
|
9032
|
+
run: () => initCommand(["--force"], flags),
|
|
9033
|
+
leaves: true
|
|
9034
|
+
}
|
|
9035
|
+
];
|
|
9036
|
+
}
|
|
9037
|
+
const choices = [];
|
|
9038
|
+
if (state.kind === "signed-out" || state.kind === "session-ended") {
|
|
9039
|
+
choices.push({
|
|
9040
|
+
label: "Sign in",
|
|
9041
|
+
hint: "veefore login",
|
|
9042
|
+
run: async () => {
|
|
9043
|
+
await login(existing.controlPlaneUrl);
|
|
9044
|
+
return 0;
|
|
9045
|
+
}
|
|
9046
|
+
});
|
|
9047
|
+
}
|
|
9048
|
+
choices.push(
|
|
9049
|
+
{
|
|
9050
|
+
label: "Watch decisions live",
|
|
9051
|
+
hint: "veefore dev",
|
|
9052
|
+
run: () => devCommand([], flags),
|
|
9053
|
+
live: true
|
|
9054
|
+
},
|
|
9055
|
+
{ label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
|
|
9056
|
+
{
|
|
9057
|
+
label: "Protect tools in the code",
|
|
9058
|
+
hint: "veefore integrate",
|
|
9059
|
+
run: () => integrateCommand([], flags)
|
|
9060
|
+
},
|
|
9061
|
+
{
|
|
9062
|
+
label: "Check every tool is guarded",
|
|
9063
|
+
hint: "veefore integrate --check",
|
|
9064
|
+
run: () => integrateCommand(["--check"], flags)
|
|
9065
|
+
},
|
|
9066
|
+
{ label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
|
|
9067
|
+
{
|
|
9068
|
+
label: "Audit the security posture",
|
|
9069
|
+
hint: "veefore doctor",
|
|
9070
|
+
run: () => doctorCommand([], flags)
|
|
9071
|
+
},
|
|
9072
|
+
{ label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
|
|
9073
|
+
{
|
|
9074
|
+
label: "Re-run setup for this project",
|
|
9075
|
+
hint: "veefore init --force",
|
|
9076
|
+
run: () => initCommand(["--force"], flags),
|
|
9077
|
+
leaves: true
|
|
9078
|
+
},
|
|
9079
|
+
{
|
|
9080
|
+
label: "Remove Veefore from this project",
|
|
9081
|
+
hint: "veefore remove",
|
|
9082
|
+
run: () => removeCommand([], flags, state),
|
|
9083
|
+
leaves: true
|
|
9084
|
+
}
|
|
9085
|
+
);
|
|
9086
|
+
if (state.kind === "ok" || state.kind === "unreachable") {
|
|
9087
|
+
choices.push({
|
|
9088
|
+
label: "Sign out",
|
|
9089
|
+
hint: "veefore logout",
|
|
9090
|
+
run: async () => {
|
|
9091
|
+
await logout(existing.controlPlaneUrl);
|
|
9092
|
+
return 0;
|
|
9093
|
+
}
|
|
9094
|
+
// Deliberately not `leaves`: signing out comes back to this menu, so the
|
|
9095
|
+
// panel and the status redraw as signed out and the next step on offer
|
|
9096
|
+
// is Sign in. Leaving instead would quit on the one action whose result
|
|
9097
|
+
// a person most wants to see confirmed.
|
|
9098
|
+
});
|
|
9016
9099
|
}
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
]
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9100
|
+
return choices;
|
|
9101
|
+
}
|
|
9102
|
+
|
|
9103
|
+
// src/commands/policies.ts
|
|
9104
|
+
function caseCount(result) {
|
|
9105
|
+
return result.passed + result.failed;
|
|
9106
|
+
}
|
|
9107
|
+
async function policiesCommand(args, flags) {
|
|
9108
|
+
const [subcommand, ...rest] = args;
|
|
9109
|
+
switch (subcommand) {
|
|
9110
|
+
case void 0:
|
|
9111
|
+
case "list":
|
|
9112
|
+
return listPolicies(rest, flags);
|
|
9113
|
+
case "show":
|
|
9114
|
+
return showPolicy(rest, flags);
|
|
9115
|
+
case "test":
|
|
9116
|
+
return testPolicies(rest, flags);
|
|
9117
|
+
case "deploy":
|
|
9118
|
+
return deployPolicy(rest, flags);
|
|
9119
|
+
case "diff":
|
|
9120
|
+
return diffPolicy(rest, flags);
|
|
9121
|
+
default:
|
|
9122
|
+
throw new CliError(
|
|
9123
|
+
`Unknown subcommand \`veefore policies ${subcommand}\`.`,
|
|
9124
|
+
"Available: list, show, test, deploy, diff."
|
|
9125
|
+
);
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9128
|
+
async function listPolicies(args, flags) {
|
|
9129
|
+
const context = await resolveContext(flags);
|
|
9130
|
+
const policies = await context.api.get("/v1/policies", {
|
|
9131
|
+
workspace_id: args.includes("--all") ? void 0 : context.config.workspaceId,
|
|
9132
|
+
status: valueFor(args, "--status")
|
|
9133
|
+
});
|
|
9134
|
+
if (flags.json) {
|
|
9135
|
+
printJson({ items: policies });
|
|
9136
|
+
return 0;
|
|
9137
|
+
}
|
|
9138
|
+
heading(`Policies in ${context.config.workspaceName}`);
|
|
9139
|
+
table(
|
|
9140
|
+
[
|
|
9141
|
+
{ header: "NAME", key: "name", width: 30 },
|
|
9142
|
+
{ header: "KEY", key: "key", width: 24 },
|
|
9143
|
+
{ header: "STATUS", key: "status", width: 18 },
|
|
9144
|
+
{ header: "ACTIVE v", key: "active", width: 10, align: "right" },
|
|
9145
|
+
{ header: "LATEST v", key: "latest", width: 10, align: "right" },
|
|
9146
|
+
{ header: "BINDINGS", key: "bindings", width: 9, align: "right" },
|
|
9147
|
+
{ header: "24H", key: "evaluations", width: 7, align: "right" }
|
|
9148
|
+
],
|
|
9149
|
+
policies.map((policy) => ({
|
|
9150
|
+
name: policy.name,
|
|
9151
|
+
key: policy.key,
|
|
9152
|
+
status: decisionStyle(policy.status),
|
|
9153
|
+
active: policy.activeVersion ? String(policy.activeVersion.versionNumber) : "\u2014",
|
|
9154
|
+
latest: policy.latestVersion ? String(policy.latestVersion.versionNumber) : "\u2014",
|
|
9155
|
+
bindings: String(policy.bindings.length),
|
|
9156
|
+
evaluations: String(policy.evaluations24h)
|
|
9157
|
+
}))
|
|
9158
|
+
);
|
|
9159
|
+
const unbound = policies.filter(
|
|
9160
|
+
(policy) => policy.status === "ACTIVE" && policy.bindings.length === 0
|
|
9034
9161
|
);
|
|
9162
|
+
const drafts = policies.filter((policy) => policy.status === "DRAFT");
|
|
9035
9163
|
out();
|
|
9036
|
-
|
|
9037
|
-
|
|
9164
|
+
if (unbound.length > 0) {
|
|
9165
|
+
out(
|
|
9166
|
+
` ${symbol.warn} ${style.amber(`${unbound.length} active policy/policies are not bound to any scope, so they evaluate nothing.`)}`
|
|
9167
|
+
);
|
|
9168
|
+
}
|
|
9169
|
+
if (drafts.length > 0) {
|
|
9170
|
+
out(
|
|
9171
|
+
` ${symbol.info} ${drafts.length} draft(s). Run ${style.bold("veefore policies deploy <policyId>")} once the tests pass.`
|
|
9172
|
+
);
|
|
9173
|
+
}
|
|
9174
|
+
if (policies.length === 0) {
|
|
9175
|
+
out(` ${symbol.warn} No policies exist. Nothing is being evaluated against rules.`);
|
|
9176
|
+
}
|
|
9038
9177
|
out();
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9178
|
+
return 0;
|
|
9179
|
+
}
|
|
9180
|
+
async function showPolicy(args, flags) {
|
|
9181
|
+
const context = await resolveContext(flags);
|
|
9182
|
+
const policyId = positional(args);
|
|
9183
|
+
if (!policyId)
|
|
9184
|
+
throw new CliError("A policy id is required. Usage: veefore policies show <policyId>");
|
|
9185
|
+
const policy = await context.api.get(
|
|
9186
|
+
`/v1/policies/${policyId}`
|
|
9187
|
+
);
|
|
9188
|
+
const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
|
|
9189
|
+
if (flags.json) {
|
|
9190
|
+
printJson({ policy, versions });
|
|
9191
|
+
return 0;
|
|
9192
|
+
}
|
|
9193
|
+
heading(policy.name);
|
|
9194
|
+
details([
|
|
9195
|
+
["ID", policy.id],
|
|
9196
|
+
["Key", policy.key],
|
|
9197
|
+
["Status", decisionStyle(policy.status)],
|
|
9198
|
+
["Priority", String(policy.priority)],
|
|
9199
|
+
[
|
|
9200
|
+
"Active version",
|
|
9201
|
+
policy.activeVersion ? `v${policy.activeVersion.versionNumber}` : style.grey("none")
|
|
9202
|
+
],
|
|
9203
|
+
["Bound agents", String(policy.boundAgentCount)],
|
|
9204
|
+
["Evaluations (24h)", `${policy.evaluations24h} ${style.grey(`${policy.blocks24h} blocked`)}`],
|
|
9205
|
+
["Owner", policy.ownerEmail ?? style.grey("unknown")],
|
|
9206
|
+
["Description", policy.description || style.grey("none")]
|
|
9207
|
+
]);
|
|
9208
|
+
if (policy.plainEnglish && policy.plainEnglish.length > 0) {
|
|
9209
|
+
heading("In plain English");
|
|
9210
|
+
for (const line of policy.plainEnglish) out(` ${symbol.bullet} ${line}`);
|
|
9211
|
+
}
|
|
9212
|
+
if (policy.bindings.length > 0) {
|
|
9213
|
+
heading("Bindings");
|
|
9214
|
+
table(
|
|
9215
|
+
[
|
|
9216
|
+
{ header: "SCOPE", key: "scope", width: 14 },
|
|
9217
|
+
{ header: "TARGET", key: "target", width: 34 },
|
|
9218
|
+
{ header: "ENABLED", key: "enabled", width: 8 }
|
|
9219
|
+
],
|
|
9220
|
+
policy.bindings.map((binding) => ({
|
|
9221
|
+
scope: binding.scope,
|
|
9222
|
+
target: binding.scopeId,
|
|
9223
|
+
enabled: binding.enabled ? style.green("yes") : style.red("no")
|
|
9224
|
+
}))
|
|
9225
|
+
);
|
|
9043
9226
|
}
|
|
9227
|
+
heading("Versions");
|
|
9228
|
+
table(
|
|
9229
|
+
[
|
|
9230
|
+
{ header: "v", key: "version", width: 4, align: "right" },
|
|
9231
|
+
{ header: "STATUS", key: "status", width: 20 },
|
|
9232
|
+
{ header: "CREATED", key: "created", width: 20 },
|
|
9233
|
+
{ header: "SUMMARY", key: "summary", width: 40 }
|
|
9234
|
+
],
|
|
9235
|
+
versions.map((version) => ({
|
|
9236
|
+
version: String(version.versionNumber),
|
|
9237
|
+
status: decisionStyle(version.status),
|
|
9238
|
+
created: version.createdAt.slice(0, 16).replace("T", " "),
|
|
9239
|
+
summary: version.changeSummary ?? ""
|
|
9240
|
+
}))
|
|
9241
|
+
);
|
|
9044
9242
|
out();
|
|
9045
|
-
|
|
9046
|
-
|
|
9243
|
+
return 0;
|
|
9244
|
+
}
|
|
9245
|
+
async function testPolicies(args, flags) {
|
|
9246
|
+
const context = await resolveContext(flags);
|
|
9247
|
+
const explicit = positional(args);
|
|
9248
|
+
const targets = explicit ? [await context.api.get(`/v1/policies/${explicit}`)] : await context.api.get("/v1/policies", {
|
|
9249
|
+
workspace_id: context.config.workspaceId
|
|
9250
|
+
});
|
|
9251
|
+
if (targets.length === 0) {
|
|
9252
|
+
out(`${symbol.warn} No policies to test.`);
|
|
9253
|
+
return 0;
|
|
9254
|
+
}
|
|
9255
|
+
const reports = [];
|
|
9256
|
+
for (const policy of targets) {
|
|
9257
|
+
try {
|
|
9258
|
+
const result = await context.api.post(
|
|
9259
|
+
`/v1/policies/${policy.id}/tests/run`,
|
|
9260
|
+
{}
|
|
9261
|
+
);
|
|
9262
|
+
reports.push({ policy, result });
|
|
9263
|
+
} catch (error) {
|
|
9264
|
+
reports.push({
|
|
9265
|
+
policy,
|
|
9266
|
+
result: null,
|
|
9267
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9268
|
+
});
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
if (flags.json) {
|
|
9272
|
+
printJson(reports);
|
|
9273
|
+
} else {
|
|
9274
|
+
renderTestReports(reports);
|
|
9275
|
+
}
|
|
9276
|
+
const failed = reports.filter(
|
|
9277
|
+
(report) => report.error !== void 0 || (report.result?.failed ?? 0) > 0
|
|
9278
|
+
);
|
|
9279
|
+
return failed.length > 0 ? 1 : 0;
|
|
9280
|
+
}
|
|
9281
|
+
function renderTestReports(reports) {
|
|
9282
|
+
heading("Policy tests");
|
|
9283
|
+
let totalCases = 0;
|
|
9284
|
+
let totalFailed = 0;
|
|
9285
|
+
let withoutTests = 0;
|
|
9286
|
+
for (const report of reports) {
|
|
9287
|
+
if (report.error) {
|
|
9288
|
+
out(`${symbol.fail} ${style.bold(report.policy.name)} ${style.grey(report.error)}`);
|
|
9289
|
+
totalFailed += 1;
|
|
9290
|
+
continue;
|
|
9291
|
+
}
|
|
9292
|
+
const result = report.result;
|
|
9293
|
+
if (!result || caseCount(result) === 0) {
|
|
9294
|
+
withoutTests += 1;
|
|
9295
|
+
out(
|
|
9296
|
+
`${symbol.warn} ${style.bold(report.policy.name)} ${style.amber("no test cases recorded")}`
|
|
9297
|
+
);
|
|
9298
|
+
continue;
|
|
9299
|
+
}
|
|
9300
|
+
totalCases += caseCount(result);
|
|
9301
|
+
totalFailed += result.failed;
|
|
9302
|
+
const icon = result.failed === 0 ? symbol.pass : symbol.fail;
|
|
9303
|
+
out(
|
|
9304
|
+
`${icon} ${style.bold(report.policy.name)} ${style.grey(`${result.passed}/${caseCount(result)} passing`)}`
|
|
9305
|
+
);
|
|
9306
|
+
for (const testCase of result.results.filter((entry) => !entry.passed)) {
|
|
9307
|
+
out(
|
|
9308
|
+
` ${symbol.fail} ${testCase.name}: expected ${decisionStyle(testCase.expected)}, got ${decisionStyle(testCase.actual)}`
|
|
9309
|
+
);
|
|
9310
|
+
if (testCase.reason) out(` ${style.grey(testCase.reason)}`);
|
|
9311
|
+
}
|
|
9312
|
+
}
|
|
9047
9313
|
out();
|
|
9048
|
-
out(style.bold("GETTING STARTED"));
|
|
9049
|
-
out(
|
|
9050
|
-
` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
|
|
9051
|
-
);
|
|
9052
9314
|
out(
|
|
9053
|
-
` ${style.
|
|
9315
|
+
totalFailed === 0 ? ` ${symbol.pass} ${style.green(`All ${totalCases} test case(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${totalFailed} failure(s) across ${totalCases} test case(s).`)}`
|
|
9054
9316
|
);
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9317
|
+
if (withoutTests > 0) {
|
|
9318
|
+
out(
|
|
9319
|
+
` ${symbol.warn} ${style.amber(`${withoutTests} policy/policies have no test cases.`)} Add cases so activation can be proven safe.`
|
|
9320
|
+
);
|
|
9321
|
+
}
|
|
9059
9322
|
out();
|
|
9060
9323
|
}
|
|
9061
|
-
function
|
|
9062
|
-
const
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
out();
|
|
9070
|
-
out(style.bold("DESCRIPTION"));
|
|
9071
|
-
for (const line of wrap2(command.description, 76)) out(` ${line}`);
|
|
9072
|
-
if (command.options && command.options.length > 0) {
|
|
9073
|
-
out();
|
|
9074
|
-
out(style.bold("OPTIONS"));
|
|
9075
|
-
renderOptions(command.options);
|
|
9324
|
+
async function deployPolicy(args, flags) {
|
|
9325
|
+
const context = await resolveContext(flags);
|
|
9326
|
+
const policyId = positional(args);
|
|
9327
|
+
if (!policyId) {
|
|
9328
|
+
throw new CliError(
|
|
9329
|
+
"A policy id is required.",
|
|
9330
|
+
'Usage: veefore policies deploy <policyId> [--version <n>] [--reason "\u2026"]'
|
|
9331
|
+
);
|
|
9076
9332
|
}
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9333
|
+
const policy = await context.api.get(`/v1/policies/${policyId}`);
|
|
9334
|
+
const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
|
|
9335
|
+
const requested = valueFor(args, "--version");
|
|
9336
|
+
const target = requested ? versions.find((version) => String(version.versionNumber) === requested) : versions.find((version) => version.status === "DRAFT") ?? versions.find((version) => version.status === "TESTED");
|
|
9337
|
+
if (!target) {
|
|
9338
|
+
throw new CliError(
|
|
9339
|
+
requested ? `Version ${requested} was not found for this policy.` : "There is no draft version to deploy.",
|
|
9340
|
+
"Edit the policy in the console to create a new draft, then deploy it."
|
|
9341
|
+
);
|
|
9081
9342
|
}
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
function renderOptions(options) {
|
|
9086
|
-
const width = Math.max(...options.map(([flag]) => flag.length));
|
|
9087
|
-
for (const [flag, description] of options) {
|
|
9088
|
-
out(` ${style.cyan(flag.padEnd(width))} ${description}`);
|
|
9343
|
+
if (target.versionNumber === policy.activeVersion?.versionNumber) {
|
|
9344
|
+
out(`${symbol.info} Version ${target.versionNumber} is already active. Nothing to do.`);
|
|
9345
|
+
return 0;
|
|
9089
9346
|
}
|
|
9090
|
-
}
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9347
|
+
heading(`Deploy ${policy.name} v${target.versionNumber}`);
|
|
9348
|
+
details([
|
|
9349
|
+
["Environment", `${context.config.environmentName} (${context.config.environmentType})`],
|
|
9350
|
+
["Policy", `${policy.name} (${policy.id})`],
|
|
9351
|
+
[
|
|
9352
|
+
"Currently active",
|
|
9353
|
+
policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
|
|
9354
|
+
],
|
|
9355
|
+
["Deploying", `v${target.versionNumber}`],
|
|
9356
|
+
["Bindings", String(policy.bindings.length)]
|
|
9357
|
+
]);
|
|
9358
|
+
if (policy.bindings.length === 0) {
|
|
9359
|
+
out();
|
|
9360
|
+
out(
|
|
9361
|
+
` ${symbol.warn} ${style.amber("This policy is not bound to any scope.")} Activating it will not change any decision until you bind it.`
|
|
9362
|
+
);
|
|
9363
|
+
}
|
|
9364
|
+
if (policy.activeVersion !== null) {
|
|
9365
|
+
const diff = await context.api.get(`/v1/policies/${policyId}/diff`, {
|
|
9366
|
+
from: policy.activeVersion.versionNumber,
|
|
9367
|
+
to: target.versionNumber
|
|
9368
|
+
}).catch(() => null);
|
|
9369
|
+
if (diff) {
|
|
9370
|
+
heading("Changes");
|
|
9371
|
+
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
9372
|
+
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
9373
|
+
for (const rule of diff.changed) {
|
|
9374
|
+
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
9375
|
+
}
|
|
9376
|
+
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
9377
|
+
out(style.grey(" No rule changes."));
|
|
9378
|
+
}
|
|
9379
|
+
if (diff.riskAssessment) {
|
|
9380
|
+
out();
|
|
9381
|
+
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
9382
|
+
}
|
|
9101
9383
|
}
|
|
9102
9384
|
}
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
out(style.bold("Let's get started."));
|
|
9385
|
+
heading("Tests");
|
|
9386
|
+
const testSpinner = spinner("Running recorded test cases");
|
|
9387
|
+
let testResult = null;
|
|
9388
|
+
try {
|
|
9389
|
+
testResult = await context.api.post(`/v1/policies/${policyId}/tests/run`, {
|
|
9390
|
+
version_id: target.id
|
|
9391
|
+
});
|
|
9392
|
+
testSpinner.succeed(`${testResult.passed}/${caseCount(testResult)} test case(s) passing`);
|
|
9393
|
+
} catch (error) {
|
|
9394
|
+
testSpinner.fail("Could not run the test cases");
|
|
9395
|
+
throw error;
|
|
9396
|
+
}
|
|
9397
|
+
if (testResult.failed > 0) {
|
|
9117
9398
|
out();
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
{
|
|
9123
|
-
label: "Preview what setup would find",
|
|
9124
|
-
hint: "veefore scan \xB7 changes nothing",
|
|
9125
|
-
value: "scan"
|
|
9126
|
-
},
|
|
9127
|
-
{ label: "Sign in", hint: "veefore login", value: "login" },
|
|
9128
|
-
{ label: "Show every command", hint: "veefore help", value: "help" },
|
|
9129
|
-
{ label: "Exit", hint: "esc", value: "exit" }
|
|
9130
|
-
]);
|
|
9131
|
-
} catch (error) {
|
|
9132
|
-
if (error instanceof MenuCancelled) return 0;
|
|
9133
|
-
throw error;
|
|
9399
|
+
for (const failure of testResult.results.filter((entry) => !entry.passed)) {
|
|
9400
|
+
out(
|
|
9401
|
+
` ${symbol.fail} ${failure.name}: expected ${decisionStyle(failure.expected)}, got ${decisionStyle(failure.actual)}`
|
|
9402
|
+
);
|
|
9134
9403
|
}
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9404
|
+
out();
|
|
9405
|
+
throw new CliError(
|
|
9406
|
+
`${testResult.failed} test case(s) fail. This version will not be deployed.`,
|
|
9407
|
+
"Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
|
|
9408
|
+
);
|
|
9409
|
+
}
|
|
9410
|
+
if (caseCount(testResult) === 0) {
|
|
9411
|
+
out();
|
|
9412
|
+
out(
|
|
9413
|
+
` ${symbol.warn} ${style.amber("This policy has no test cases.")} There is no evidence the new rules behave as intended.`
|
|
9414
|
+
);
|
|
9415
|
+
const proceed = await confirm("Deploy anyway?", { yes: flags.yes });
|
|
9416
|
+
if (!proceed) {
|
|
9417
|
+
out(style.grey("Cancelled. Add test cases first."));
|
|
9418
|
+
return 1;
|
|
9144
9419
|
}
|
|
9145
|
-
const code = await runOnScreen(source, flags, async () => {
|
|
9146
|
-
switch (action) {
|
|
9147
|
-
case "scan":
|
|
9148
|
-
return scanCommand([], flags);
|
|
9149
|
-
case "login":
|
|
9150
|
-
await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
|
|
9151
|
-
return 0;
|
|
9152
|
-
case "help":
|
|
9153
|
-
printRootHelp(source().version);
|
|
9154
|
-
return 0;
|
|
9155
|
-
}
|
|
9156
|
-
return 0;
|
|
9157
|
-
});
|
|
9158
|
-
if (!isInteractive()) return code;
|
|
9159
9420
|
}
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9421
|
+
const reason = valueFor(args, "--reason") ?? await ask("Reason for this change (recorded in the audit trail, min 10 characters):");
|
|
9422
|
+
if (reason.trim().length < 10) {
|
|
9423
|
+
throw new CliError("A reason of at least 10 characters is required.");
|
|
9424
|
+
}
|
|
9425
|
+
if (context.config.environmentType === "PRODUCTION") {
|
|
9426
|
+
out();
|
|
9427
|
+
out(` ${symbol.warn} ${style.amber("This affects a PRODUCTION environment.")}`);
|
|
9428
|
+
}
|
|
9429
|
+
const confirmed = await confirm(`Activate v${target.versionNumber}?`, { yes: flags.yes });
|
|
9430
|
+
if (!confirmed) {
|
|
9431
|
+
out(style.grey("Cancelled. No change was made."));
|
|
9432
|
+
return 1;
|
|
9433
|
+
}
|
|
9434
|
+
const deploySpinner = spinner(`Activating v${target.versionNumber}`);
|
|
9168
9435
|
try {
|
|
9169
|
-
|
|
9436
|
+
await withStepUp(
|
|
9437
|
+
context.api,
|
|
9438
|
+
() => context.api.post(`/v1/policies/${policyId}/activate`, {
|
|
9439
|
+
version_id: target.id,
|
|
9440
|
+
reason
|
|
9441
|
+
})
|
|
9442
|
+
);
|
|
9443
|
+
deploySpinner.succeed(`${policy.name} v${target.versionNumber} is ACTIVE`);
|
|
9170
9444
|
} catch (error) {
|
|
9171
|
-
|
|
9445
|
+
deploySpinner.fail("Activation failed");
|
|
9446
|
+
throw error;
|
|
9172
9447
|
}
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9448
|
+
out();
|
|
9449
|
+
out(` ${symbol.pass} The previous version is marked SUPERSEDED and can be rolled back.`);
|
|
9450
|
+
out(` ${symbol.pass} The change and its diff are recorded in the audit trail.`);
|
|
9451
|
+
out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
|
|
9452
|
+
out();
|
|
9453
|
+
return 0;
|
|
9454
|
+
}
|
|
9455
|
+
async function diffPolicy(args, flags) {
|
|
9456
|
+
const context = await resolveContext(flags);
|
|
9457
|
+
const policyId = positional(args);
|
|
9458
|
+
if (!policyId) {
|
|
9459
|
+
throw new CliError(
|
|
9460
|
+
"A policy id is required.",
|
|
9461
|
+
"Usage: veefore policies diff <policyId> --from <n> --to <n>"
|
|
9462
|
+
);
|
|
9463
|
+
}
|
|
9464
|
+
const from = valueFor(args, "--from");
|
|
9465
|
+
const to = valueFor(args, "--to");
|
|
9466
|
+
const diff = await context.api.get(`/v1/policies/${policyId}/diff`, { from, to });
|
|
9467
|
+
if (flags.json) {
|
|
9468
|
+
printJson(diff);
|
|
9469
|
+
return 0;
|
|
9470
|
+
}
|
|
9471
|
+
heading(`Policy diff v${diff.from} \u2192 v${diff.to}`);
|
|
9472
|
+
for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
|
|
9473
|
+
for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
|
|
9474
|
+
for (const rule of diff.changed) {
|
|
9475
|
+
out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
|
|
9476
|
+
}
|
|
9477
|
+
if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
|
|
9478
|
+
out(style.grey(" No differences."));
|
|
9479
|
+
}
|
|
9480
|
+
if (diff.riskAssessment) {
|
|
9481
|
+
out();
|
|
9482
|
+
out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
|
|
9483
|
+
}
|
|
9484
|
+
out();
|
|
9485
|
+
return 0;
|
|
9176
9486
|
}
|
|
9177
9487
|
|
|
9178
9488
|
// src/index.ts
|