veefore 1.2.0 → 1.4.0

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