veefore 1.1.0 → 1.4.0

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