@clef-sh/cli 0.1.12 → 0.1.13-beta.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -97715,11 +97715,23 @@ function sym(key) {
97715
97715
  }
97716
97716
 
97717
97717
  // src/output/formatter.ts
97718
+ var _jsonMode = false;
97719
+ var _yesMode = false;
97720
+ function setJsonMode(json) {
97721
+ _jsonMode = json;
97722
+ }
97723
+ function isJsonMode() {
97724
+ return _jsonMode;
97725
+ }
97726
+ function setYesMode(yes) {
97727
+ _yesMode = yes;
97728
+ }
97718
97729
  function color(fn, str2) {
97719
97730
  return isPlainMode() ? str2 : fn(str2);
97720
97731
  }
97721
97732
  var formatter = {
97722
97733
  success(message) {
97734
+ if (_jsonMode) return;
97723
97735
  const icon = sym("success");
97724
97736
  process.stdout.write(color(import_picocolors.default.green, `${icon} ${message}`) + "\n");
97725
97737
  },
@@ -97736,15 +97748,18 @@ var formatter = {
97736
97748
  process.stderr.write(color(import_picocolors.default.yellow, `${icon} ${message}`) + "\n");
97737
97749
  },
97738
97750
  info(message) {
97751
+ if (_jsonMode) return;
97739
97752
  const icon = sym("info");
97740
97753
  process.stdout.write(color(import_picocolors.default.blue, `${icon} ${message}`) + "\n");
97741
97754
  },
97742
97755
  hint(message) {
97756
+ if (_jsonMode) return;
97743
97757
  const icon = sym("arrow");
97744
97758
  process.stdout.write(`${icon} ${message}
97745
97759
  `);
97746
97760
  },
97747
97761
  keyValue(key, value) {
97762
+ if (_jsonMode) return;
97748
97763
  const icon = sym("key");
97749
97764
  const arrow = sym("arrow");
97750
97765
  const prefix2 = icon ? `${icon} ` : "";
@@ -97752,30 +97767,38 @@ var formatter = {
97752
97767
  `);
97753
97768
  },
97754
97769
  pendingItem(key, days) {
97770
+ if (_jsonMode) return;
97755
97771
  const icon = sym("pending");
97756
97772
  const prefix2 = icon ? `${icon} ` : "[pending] ";
97757
97773
  process.stdout.write(`${prefix2}${key} \u2014 ${days} day${days !== 1 ? "s" : ""} pending
97758
97774
  `);
97759
97775
  },
97760
97776
  recipientItem(label2, keyPreview2) {
97777
+ if (_jsonMode) return;
97761
97778
  const icon = sym("recipient");
97762
97779
  const prefix2 = icon ? `${icon} ` : "";
97763
97780
  process.stdout.write(`${prefix2}${label2.padEnd(15)}${keyPreview2}
97764
97781
  `);
97765
97782
  },
97766
97783
  section(label2) {
97784
+ if (_jsonMode) return;
97767
97785
  process.stdout.write(`
97768
97786
  ${label2}
97769
97787
 
97770
97788
  `);
97771
97789
  },
97772
97790
  print(message) {
97791
+ if (_jsonMode) return;
97773
97792
  process.stdout.write(message + "\n");
97774
97793
  },
97775
97794
  raw(message) {
97776
97795
  process.stdout.write(message);
97777
97796
  },
97797
+ json(data) {
97798
+ process.stdout.write(JSON.stringify(data) + "\n");
97799
+ },
97778
97800
  table(rows, columns) {
97801
+ if (_jsonMode) return;
97779
97802
  const widths = columns.map((col, i) => {
97780
97803
  const maxDataWidth = rows.reduce(
97781
97804
  (max, row) => Math.max(max, stripAnsi(row[i] ?? "").length),
@@ -97792,6 +97815,10 @@ ${label2}
97792
97815
  }
97793
97816
  },
97794
97817
  async confirm(prompt) {
97818
+ if (_yesMode) return true;
97819
+ if (_jsonMode) {
97820
+ throw new Error("--json requires --yes for destructive operations");
97821
+ }
97795
97822
  const rl = readline.createInterface({
97796
97823
  input: process.stdin,
97797
97824
  output: process.stderr
@@ -97826,6 +97853,9 @@ ${label2}
97826
97853
  `);
97827
97854
  },
97828
97855
  async secretPrompt(prompt) {
97856
+ if (_jsonMode) {
97857
+ throw new Error("--json mode requires value on the command line (no interactive prompt)");
97858
+ }
97829
97859
  return new Promise((resolve7, reject) => {
97830
97860
  process.stderr.write(color(import_picocolors.default.cyan, `${prompt}: `));
97831
97861
  if (process.stdin.isTTY) {
@@ -97868,7 +97898,14 @@ function pad(str2, width) {
97868
97898
  }
97869
97899
 
97870
97900
  // src/handle-error.ts
97901
+ function exitJsonError(message) {
97902
+ formatter.json({ error: true, message });
97903
+ process.exit(1);
97904
+ }
97871
97905
  function handleCommandError(err) {
97906
+ if (isJsonMode()) {
97907
+ exitJsonError(err.message);
97908
+ }
97872
97909
  if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
97873
97910
  formatter.formatDependencyError(err);
97874
97911
  } else {
@@ -98368,7 +98405,7 @@ async function handleSecondDevOnboarding(repoRoot, clefConfigPath, deps2, option
98368
98405
  "OS keychain is not available on this system.\n The private key will be written to the filesystem instead.\n See https://docs.clef.sh/guide/key-storage for security implications."
98369
98406
  );
98370
98407
  let keyPath;
98371
- if (options.nonInteractive || !process.stdin.isTTY) {
98408
+ if (options.nonInteractive || isJsonMode() || !process.stdin.isTTY) {
98372
98409
  keyPath = process.env.CLEF_AGE_KEY_FILE || defaultAgeKeyPath(label2);
98373
98410
  keyPath = path23.resolve(keyPath);
98374
98411
  } else {
@@ -98401,6 +98438,10 @@ async function handleSecondDevOnboarding(repoRoot, clefConfigPath, deps2, option
98401
98438
  formatter.success("Created .clef/.gitignore");
98402
98439
  }
98403
98440
  formatter.success(`Key label: ${label2}`);
98441
+ if (isJsonMode()) {
98442
+ formatter.json({ action: "onboarded", manifest: "clef.yaml", config: ".clef/config.yaml" });
98443
+ return;
98444
+ }
98404
98445
  formatter.section("Next steps:");
98405
98446
  formatter.hint("clef recipients request \u2014 request access to encrypted secrets");
98406
98447
  formatter.hint("clef update \u2014 scaffold new environments");
@@ -98411,7 +98452,7 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98411
98452
  let namespaces = options.namespaces ? options.namespaces.split(",").map((s) => s.trim()) : [];
98412
98453
  const backend = options.backend ?? "age";
98413
98454
  let secretsDir = options.secretsDir ?? "secrets";
98414
- if (!options.nonInteractive && process.stdin.isTTY) {
98455
+ if (!options.nonInteractive && !isJsonMode() && process.stdin.isTTY) {
98415
98456
  const envAnswer = await promptWithDefault(
98416
98457
  "Environments (comma-separated)",
98417
98458
  environments.join(",")
@@ -98466,7 +98507,7 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98466
98507
  formatter.warn(
98467
98508
  "OS keychain is not available on this system.\n The private key must be written to the filesystem instead.\n See https://docs.clef.sh/guide/key-storage for security implications."
98468
98509
  );
98469
- if (!options.nonInteractive && process.stdin.isTTY) {
98510
+ if (!options.nonInteractive && !isJsonMode() && process.stdin.isTTY) {
98470
98511
  const confirmed = await formatter.confirm("Write the private key to the filesystem?");
98471
98512
  if (!confirmed) {
98472
98513
  formatter.error(
@@ -98477,7 +98518,7 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98477
98518
  }
98478
98519
  }
98479
98520
  let keyPath;
98480
- if (options.nonInteractive || !process.stdin.isTTY) {
98521
+ if (options.nonInteractive || isJsonMode() || !process.stdin.isTTY) {
98481
98522
  keyPath = defaultAgeKeyPath(label2);
98482
98523
  if (await isInsideAnyGitRepo(path23.resolve(keyPath))) {
98483
98524
  throw new Error(
@@ -98597,6 +98638,17 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98597
98638
  formatter.print(" clef config set analytics false (permanent)\n");
98598
98639
  } catch {
98599
98640
  }
98641
+ if (isJsonMode()) {
98642
+ formatter.json({
98643
+ action: "initialized",
98644
+ manifest: "clef.yaml",
98645
+ environments: manifest.environments.map((e) => e.name),
98646
+ namespaces: manifest.namespaces.map((n) => n.name),
98647
+ backend,
98648
+ scaffolded: scaffoldedCount
98649
+ });
98650
+ return;
98651
+ }
98600
98652
  formatter.section("Next steps:");
98601
98653
  formatter.hint("clef set <namespace>/<env> <KEY> <value> \u2014 add a secret");
98602
98654
  formatter.hint("clef scan \u2014 check for existing plaintext secrets");
@@ -98857,7 +98909,9 @@ function registerGetCommand(program3, deps2) {
98857
98909
  return;
98858
98910
  }
98859
98911
  const val = decrypted.values[key];
98860
- if (opts2.raw) {
98912
+ if (isJsonMode()) {
98913
+ formatter.json({ key, value: val, namespace, environment });
98914
+ } else if (opts2.raw) {
98861
98915
  formatter.raw(val);
98862
98916
  } else {
98863
98917
  const copied = copyToClipboard(val);
@@ -98962,6 +99016,16 @@ function registerSetCommand(program3, deps2) {
98962
99016
  pendingErrors.push(env.name);
98963
99017
  }
98964
99018
  }
99019
+ if (isJsonMode()) {
99020
+ formatter.json({
99021
+ key,
99022
+ namespace: namespace2,
99023
+ environments: manifest.environments.map((e) => e.name),
99024
+ action: "created",
99025
+ pending: true
99026
+ });
99027
+ return;
99028
+ }
98965
99029
  formatter.success(
98966
99030
  `'${key}' set in ${namespace2} across all environments ${sym("locked")}`
98967
99031
  );
@@ -98985,6 +99049,16 @@ function registerSetCommand(program3, deps2) {
98985
99049
  } catch {
98986
99050
  }
98987
99051
  }
99052
+ if (isJsonMode()) {
99053
+ formatter.json({
99054
+ key,
99055
+ namespace: namespace2,
99056
+ environments: manifest.environments.map((e) => e.name),
99057
+ action: "created",
99058
+ pending: false
99059
+ });
99060
+ return;
99061
+ }
98988
99062
  formatter.success(`'${key}' set in ${namespace2} across all environments`);
98989
99063
  formatter.hint(`git add ${namespace2}/ # stage all updated files`);
98990
99064
  }
@@ -99048,6 +99122,10 @@ function registerSetCommand(program3, deps2) {
99048
99122
  process.exit(1);
99049
99123
  return;
99050
99124
  }
99125
+ if (isJsonMode()) {
99126
+ formatter.json({ key, namespace, environment, action: "created", pending: true });
99127
+ return;
99128
+ }
99051
99129
  formatter.success(`${key} set in ${namespace}/${environment} ${sym("locked")}`);
99052
99130
  formatter.print(
99053
99131
  ` ${sym("pending")} Marked as pending \u2014 replace with a real value before deploying`
@@ -99062,6 +99140,10 @@ function registerSetCommand(program3, deps2) {
99062
99140
  The value is saved. Run clef lint to check for stale pending markers.`
99063
99141
  );
99064
99142
  }
99143
+ if (isJsonMode()) {
99144
+ formatter.json({ key, namespace, environment, action: "created", pending: false });
99145
+ return;
99146
+ }
99065
99147
  formatter.success(`${key} set in ${namespace}/${environment}`);
99066
99148
  formatter.hint(
99067
99149
  `Commit: git add ${manifest.file_pattern.replace("{namespace}", namespace).replace("{environment}", environment)}`
@@ -99130,7 +99212,10 @@ function registerCompareCommand(program3, deps2) {
99130
99212
  compareBuf.copy(paddedCompare);
99131
99213
  const timingEqual = crypto5.timingSafeEqual(paddedStored, paddedCompare);
99132
99214
  const match = storedBuf.length === compareBuf.length && timingEqual;
99133
- if (match) {
99215
+ if (isJsonMode()) {
99216
+ formatter.json({ match, key, namespace, environment });
99217
+ if (!match) process.exit(1);
99218
+ } else if (match) {
99134
99219
  formatter.success(`${key} ${sym("arrow")} values match`);
99135
99220
  } else {
99136
99221
  formatter.failure(`${key} ${sym("arrow")} values do not match`);
@@ -99178,6 +99263,15 @@ Type the key name to confirm:`
99178
99263
  }
99179
99264
  const bulkOps = new BulkOps();
99180
99265
  await bulkOps.deleteAcrossEnvironments(namespace, key, manifest, sopsClient, repoRoot);
99266
+ if (isJsonMode()) {
99267
+ formatter.json({
99268
+ key,
99269
+ namespace,
99270
+ environments: manifest.environments.map((e) => e.name),
99271
+ action: "deleted"
99272
+ });
99273
+ return;
99274
+ }
99181
99275
  formatter.success(`Deleted '${key}' from ${namespace} in all environments`);
99182
99276
  } else {
99183
99277
  const [namespace, environment] = parseTarget(target);
@@ -99216,6 +99310,10 @@ Type the key name to confirm:`
99216
99310
  `Key deleted but pending metadata could not be cleaned up. Run clef lint to verify.`
99217
99311
  );
99218
99312
  }
99313
+ if (isJsonMode()) {
99314
+ formatter.json({ key, namespace, environment, action: "deleted" });
99315
+ return;
99316
+ }
99219
99317
  formatter.success(`Deleted '${key}' from ${namespace}/${environment}`);
99220
99318
  formatter.hint(
99221
99319
  `Commit: git add ${manifest.file_pattern.replace("{namespace}", namespace).replace("{environment}", environment)}`
@@ -99234,10 +99332,11 @@ Type the key name to confirm:`
99234
99332
  var import_picocolors2 = __toESM(require_picocolors());
99235
99333
  init_src();
99236
99334
  import * as path33 from "path";
99335
+ var MASKED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
99237
99336
  function registerDiffCommand(program3, deps2) {
99238
99337
  program3.command("diff <namespace> <env-a> <env-b>").description(
99239
99338
  "Compare secrets between two environments for a namespace.\n\nExit codes:\n 0 No differences\n 1 Differences found"
99240
- ).option("--show-identical", "Include identical keys in the output").option("--show-values", "Show plaintext values instead of masking them").option("--json", "Output raw DiffResult JSON").action(
99339
+ ).option("--show-identical", "Include identical keys in the output").option("--show-values", "Show plaintext values instead of masking them").action(
99241
99340
  async (namespace, envA, envB, options) => {
99242
99341
  try {
99243
99342
  const repoRoot = program3.opts().dir || process.cwd();
@@ -99264,19 +99363,20 @@ function registerDiffCommand(program3, deps2) {
99264
99363
  formatter.warn("Warning: printing plaintext values for protected environment.");
99265
99364
  }
99266
99365
  }
99267
- if (options.json) {
99268
- const jsonOutput = options.showValues ? result : {
99366
+ if (isJsonMode()) {
99367
+ const jsonData = options.showValues ? result : {
99269
99368
  ...result,
99270
99369
  rows: result.rows.map((r) => ({
99271
99370
  ...r,
99272
- valueA: r.valueA !== null ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null,
99273
- valueB: r.valueB !== null ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null,
99371
+ valueA: r.valueA !== null ? MASKED : null,
99372
+ valueB: r.valueB !== null ? MASKED : null,
99274
99373
  masked: true
99275
99374
  }))
99276
99375
  };
99277
- formatter.raw(JSON.stringify(jsonOutput, null, 2) + "\n");
99376
+ formatter.json(jsonData);
99278
99377
  const hasDiffs2 = result.rows.some((r) => r.status !== "identical");
99279
99378
  process.exit(hasDiffs2 ? 1 : 0);
99379
+ return;
99280
99380
  }
99281
99381
  formatDiffOutput(
99282
99382
  result,
@@ -99296,7 +99396,6 @@ function registerDiffCommand(program3, deps2) {
99296
99396
  }
99297
99397
  );
99298
99398
  }
99299
- var MASKED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
99300
99399
  function formatDiffOutput(result, envA, envB, showIdentical, showValues) {
99301
99400
  const filteredRows = showIdentical ? result.rows : result.rows.filter((r) => r.status !== "identical");
99302
99401
  if (filteredRows.length === 0) {
@@ -99578,7 +99677,7 @@ async function fetchCheckpoint(config) {
99578
99677
  }
99579
99678
 
99580
99679
  // package.json
99581
- var version2 = "0.1.12";
99680
+ var version2 = "0.1.13-beta.88";
99582
99681
  var package_default = {
99583
99682
  name: "@clef-sh/cli",
99584
99683
  version: version2,
@@ -99649,7 +99748,7 @@ var package_default = {
99649
99748
  function registerLintCommand(program3, deps2) {
99650
99749
  program3.command("lint").description(
99651
99750
  "Full repo health check \u2014 matrix completeness, schema validation, SOPS integrity.\n\nExit codes:\n 0 No errors (warnings are allowed)\n 1 Errors found"
99652
- ).option("--fix", "Auto-fix safe issues (scaffold missing files)").option("--json", "Output raw LintResult JSON").option("--push", "Push results as OTLP to CLEF_TELEMETRY_URL").action(async (options) => {
99751
+ ).option("--fix", "Auto-fix safe issues (scaffold missing files)").option("--push", "Push results as OTLP to CLEF_TELEMETRY_URL").action(async (options) => {
99653
99752
  try {
99654
99753
  const repoRoot = program3.opts().dir || process.cwd();
99655
99754
  const parser = new ManifestParser();
@@ -99684,8 +99783,8 @@ function registerLintCommand(program3, deps2) {
99684
99783
  await pushOtlp(payload, config);
99685
99784
  formatter.success("Lint results pushed to telemetry endpoint.");
99686
99785
  }
99687
- if (options.json) {
99688
- formatter.raw(JSON.stringify(result, null, 2) + "\n");
99786
+ if (isJsonMode()) {
99787
+ formatter.json(result);
99689
99788
  const hasErrors2 = result.issues.some((i) => i.severity === "error");
99690
99789
  process.exit(hasErrors2 ? 1 : 0);
99691
99790
  return;
@@ -99794,6 +99893,10 @@ function registerRotateCommand(program3, deps2) {
99794
99893
  const relativeFile = manifest.file_pattern.replace("{namespace}", namespace).replace("{environment}", environment);
99795
99894
  formatter.print(`${sym("working")} Rotating ${namespace}/${environment}...`);
99796
99895
  await sopsClient.reEncrypt(filePath, options.newKey);
99896
+ if (isJsonMode()) {
99897
+ formatter.json({ namespace, environment, file: relativeFile, action: "rotated" });
99898
+ return;
99899
+ }
99797
99900
  formatter.success(`Rotated. New values encrypted. ${sym("locked")}`);
99798
99901
  formatter.hint(
99799
99902
  `git add ${relativeFile} && git commit -m "rotate: ${namespace}/${environment}"`
@@ -99839,15 +99942,28 @@ function registerHooksCommand(program3, deps2) {
99839
99942
  }
99840
99943
  const git = new GitIntegration(deps2.runner);
99841
99944
  await git.installPreCommitHook(repoRoot);
99945
+ let mergeDriverOk = false;
99946
+ try {
99947
+ await git.installMergeDriver(repoRoot);
99948
+ mergeDriverOk = true;
99949
+ } catch {
99950
+ }
99951
+ if (isJsonMode()) {
99952
+ formatter.json({
99953
+ preCommitHook: true,
99954
+ mergeDriver: mergeDriverOk,
99955
+ hookPath
99956
+ });
99957
+ return;
99958
+ }
99842
99959
  formatter.success("Pre-commit hook installed");
99843
99960
  formatter.print(` ${sym("pending")} ${hookPath}`);
99844
99961
  formatter.hint(
99845
99962
  "Hook checks SOPS metadata on staged .enc files and runs: clef scan --staged"
99846
99963
  );
99847
- try {
99848
- await git.installMergeDriver(repoRoot);
99964
+ if (mergeDriverOk) {
99849
99965
  formatter.success("SOPS merge driver configured");
99850
- } catch {
99966
+ } else {
99851
99967
  formatter.warn("Could not configure SOPS merge driver. Run inside a git repository.");
99852
99968
  }
99853
99969
  } catch (err) {
@@ -100146,6 +100262,14 @@ Usage: clef export payments/production --format env`
100146
100262
  );
100147
100263
  try {
100148
100264
  const decrypted = await sopsClient.decrypt(filePath);
100265
+ if (isJsonMode()) {
100266
+ const pairs = Object.entries(decrypted.values).map(([k, v]) => ({
100267
+ key: k,
100268
+ value: v
100269
+ }));
100270
+ formatter.json({ pairs, namespace, environment });
100271
+ return;
100272
+ }
100149
100273
  const consumption = new ConsumptionClient();
100150
100274
  const output = consumption.formatExport(decrypted, "env", !options.export);
100151
100275
  if (options.raw) {
@@ -100186,7 +100310,7 @@ import * as path41 from "path";
100186
100310
  function registerDoctorCommand(program3, deps2) {
100187
100311
  program3.command("doctor").description(
100188
100312
  "Check your environment for required dependencies and configuration.\n\nExit codes:\n 0 All checks pass\n 1 One or more checks failed"
100189
- ).option("--json", "Output the full status as JSON").option("--fix", "Attempt to auto-fix issues").action(async (options) => {
100313
+ ).option("--fix", "Attempt to auto-fix issues").action(async (options) => {
100190
100314
  const repoRoot = program3.opts().dir || process.cwd();
100191
100315
  const clefVersion = program3.version() ?? "unknown";
100192
100316
  const checks = [];
@@ -100273,7 +100397,7 @@ function registerDoctorCommand(program3, deps2) {
100273
100397
  formatter.warn("--fix cannot resolve these issues automatically.");
100274
100398
  }
100275
100399
  }
100276
- if (options.json) {
100400
+ if (isJsonMode()) {
100277
100401
  const json = {
100278
100402
  clef: { version: clefVersion, ok: true },
100279
100403
  sops: {
@@ -100303,7 +100427,7 @@ function registerDoctorCommand(program3, deps2) {
100303
100427
  ok: mergeDriverOk
100304
100428
  }
100305
100429
  };
100306
- formatter.raw(JSON.stringify(json, null, 2) + "\n");
100430
+ formatter.json(json);
100307
100431
  const hasFailures = checks.some((c) => !c.ok);
100308
100432
  process.exit(hasFailures ? 1 : 0);
100309
100433
  return;
@@ -100437,6 +100561,11 @@ function registerUpdateCommand(program3, deps2) {
100437
100561
  );
100438
100562
  }
100439
100563
  }
100564
+ if (isJsonMode()) {
100565
+ formatter.json({ scaffolded: scaffoldedCount, failed: failedCount });
100566
+ process.exit(failedCount > 0 ? 1 : 0);
100567
+ return;
100568
+ }
100440
100569
  if (scaffoldedCount > 0) {
100441
100570
  formatter.success(`Scaffolded ${scaffoldedCount} encrypted file(s)`);
100442
100571
  }
@@ -100462,69 +100591,57 @@ function registerScanCommand(program3, deps2) {
100462
100591
  "--severity <level>",
100463
100592
  "Detection level: all (patterns+entropy) or high (patterns only)",
100464
100593
  "all"
100465
- ).option("--json", "Output machine-readable JSON").action(
100466
- async (paths, options) => {
100467
- const repoRoot = program3.opts().dir || process.cwd();
100468
- let manifest;
100469
- try {
100470
- const parser = new ManifestParser();
100471
- manifest = parser.parse(path43.join(repoRoot, "clef.yaml"));
100472
- } catch (err) {
100473
- if (err instanceof ManifestValidationError || err.message?.includes("clef.yaml")) {
100474
- formatter.error("No clef.yaml found. Run 'clef init' to set up this repository.");
100475
- } else {
100476
- formatter.error(err.message);
100477
- }
100478
- process.exit(2);
100479
- return;
100480
- }
100481
- if (options.severity && options.severity !== "all" && options.severity !== "high") {
100482
- formatter.error(`Invalid severity '${options.severity}'. Must be 'all' or 'high'.`);
100483
- process.exit(2);
100484
- return;
100485
- }
100486
- const severity = options.severity === "high" ? "high" : "all";
100487
- const scanRunner = new ScanRunner(deps2.runner);
100488
- if (!options.json) {
100489
- formatter.print(import_picocolors4.default.dim("Scanning repository for unencrypted secrets..."));
100490
- }
100491
- let result;
100492
- try {
100493
- result = await scanRunner.scan(repoRoot, manifest, {
100494
- stagedOnly: options.staged,
100495
- paths: paths.length > 0 ? paths : void 0,
100496
- severity
100497
- });
100498
- } catch (err) {
100499
- formatter.error(`Scan failed: ${err.message}`);
100500
- process.exit(2);
100501
- return;
100502
- }
100503
- if (options.json) {
100504
- const totalIssues = result.matches.length + result.unencryptedMatrixFiles.length;
100505
- formatter.raw(
100506
- JSON.stringify(
100507
- {
100508
- matches: result.matches,
100509
- unencryptedMatrixFiles: result.unencryptedMatrixFiles,
100510
- filesScanned: result.filesScanned,
100511
- filesSkipped: result.filesSkipped,
100512
- durationMs: result.durationMs,
100513
- summary: `${totalIssues} issue${totalIssues !== 1 ? "s" : ""} found`
100514
- },
100515
- null,
100516
- 2
100517
- ) + "\n"
100518
- );
100519
- const hasIssues2 = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100520
- process.exit(hasIssues2 ? 1 : 0);
100521
- return;
100594
+ ).action(async (paths, options) => {
100595
+ const repoRoot = program3.opts().dir || process.cwd();
100596
+ let manifest;
100597
+ try {
100598
+ const parser = new ManifestParser();
100599
+ manifest = parser.parse(path43.join(repoRoot, "clef.yaml"));
100600
+ } catch (err) {
100601
+ if (err instanceof ManifestValidationError || err.message?.includes("clef.yaml")) {
100602
+ formatter.error("No clef.yaml found. Run 'clef init' to set up this repository.");
100603
+ } else {
100604
+ formatter.error(err.message);
100522
100605
  }
100523
- formatScanOutput(result);
100524
- const hasIssues = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100525
- process.exit(hasIssues ? 1 : 0);
100606
+ process.exit(2);
100607
+ return;
100526
100608
  }
100527
- );
100609
+ if (options.severity && options.severity !== "all" && options.severity !== "high") {
100610
+ formatter.error(`Invalid severity '${options.severity}'. Must be 'all' or 'high'.`);
100611
+ process.exit(2);
100612
+ return;
100613
+ }
100614
+ const severity = options.severity === "high" ? "high" : "all";
100615
+ const scanRunner = new ScanRunner(deps2.runner);
100616
+ formatter.print(import_picocolors4.default.dim("Scanning repository for unencrypted secrets..."));
100617
+ let result;
100618
+ try {
100619
+ result = await scanRunner.scan(repoRoot, manifest, {
100620
+ stagedOnly: options.staged,
100621
+ paths: paths.length > 0 ? paths : void 0,
100622
+ severity
100623
+ });
100624
+ } catch (err) {
100625
+ formatter.error(`Scan failed: ${err.message}`);
100626
+ process.exit(2);
100627
+ return;
100628
+ }
100629
+ if (isJsonMode()) {
100630
+ formatter.json({
100631
+ matches: result.matches,
100632
+ unencryptedMatrixFiles: result.unencryptedMatrixFiles,
100633
+ filesScanned: result.filesScanned,
100634
+ filesSkipped: result.filesSkipped,
100635
+ durationMs: result.durationMs
100636
+ });
100637
+ const hasIssues2 = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100638
+ process.exit(hasIssues2 ? 1 : 0);
100639
+ return;
100640
+ }
100641
+ formatScanOutput(result);
100642
+ const hasIssues = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100643
+ process.exit(hasIssues ? 1 : 0);
100644
+ });
100528
100645
  }
100529
100646
  function formatScanOutput(result) {
100530
100647
  const totalIssues = result.matches.length + result.unencryptedMatrixFiles.length;
@@ -100689,6 +100806,17 @@ function registerImportCommand(program3, deps2) {
100689
100806
  process.exit(2);
100690
100807
  return;
100691
100808
  }
100809
+ if (isJsonMode()) {
100810
+ formatter.json({
100811
+ imported: result.imported,
100812
+ skipped: result.skipped,
100813
+ failed: result.failed,
100814
+ warnings: result.warnings,
100815
+ dryRun: opts2.dryRun
100816
+ });
100817
+ process.exit(result.failed.length > 0 ? 1 : 0);
100818
+ return;
100819
+ }
100692
100820
  for (const warning of result.warnings) {
100693
100821
  formatter.print(` ${sym("warning")} ${warning}`);
100694
100822
  }
@@ -100748,6 +100876,7 @@ init_src();
100748
100876
  import * as path45 from "path";
100749
100877
  import * as readline4 from "readline";
100750
100878
  function waitForEnter(message) {
100879
+ if (isJsonMode()) return Promise.resolve();
100751
100880
  return new Promise((resolve7) => {
100752
100881
  const rl = readline4.createInterface({
100753
100882
  input: process.stdin,
@@ -100781,6 +100910,10 @@ function registerRecipientsCommand(program3, deps2) {
100781
100910
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
100782
100911
  const recipientManager = new RecipientManager(sopsClient, matrixManager);
100783
100912
  const recipients = await recipientManager.list(manifest, repoRoot, opts2.environment);
100913
+ if (isJsonMode()) {
100914
+ formatter.json(recipients);
100915
+ return;
100916
+ }
100784
100917
  if (recipients.length === 0) {
100785
100918
  const scope2 = opts2.environment ? ` for environment '${opts2.environment}'` : "";
100786
100919
  formatter.info(`No recipients configured${scope2}.`);
@@ -100811,7 +100944,7 @@ function registerRecipientsCommand(program3, deps2) {
100811
100944
  return;
100812
100945
  }
100813
100946
  const normalizedKey = validation.key;
100814
- const success = await executeRecipientAdd(
100947
+ const result = await executeRecipientAdd(
100815
100948
  repoRoot,
100816
100949
  program3,
100817
100950
  deps2,
@@ -100819,11 +100952,21 @@ function registerRecipientsCommand(program3, deps2) {
100819
100952
  opts2.label,
100820
100953
  opts2.environment
100821
100954
  );
100822
- if (success) {
100823
- const label2 = opts2.label || keyPreview(normalizedKey);
100824
- formatter.hint(
100825
- `git add clef.yaml && git add -A && git commit -m "add recipient: ${label2} [${opts2.environment}]"`
100826
- );
100955
+ if (result) {
100956
+ if (isJsonMode()) {
100957
+ formatter.json({
100958
+ action: "added",
100959
+ key: normalizedKey,
100960
+ label: opts2.label || keyPreview(normalizedKey),
100961
+ environment: opts2.environment,
100962
+ reEncryptedFiles: result.reEncryptedFiles.length
100963
+ });
100964
+ } else {
100965
+ const label2 = opts2.label || keyPreview(normalizedKey);
100966
+ formatter.hint(
100967
+ `git add clef.yaml && git add -A && git commit -m "add recipient: ${label2} [${opts2.environment}]"`
100968
+ );
100969
+ }
100827
100970
  }
100828
100971
  } catch (err) {
100829
100972
  handleCommandError(err);
@@ -100919,6 +101062,16 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100919
101062
  const relative7 = path45.relative(repoRoot, file);
100920
101063
  formatter.print(` ${sym("success")} ${relative7}`);
100921
101064
  }
101065
+ if (isJsonMode()) {
101066
+ formatter.json({
101067
+ action: "removed",
101068
+ key: trimmedKey,
101069
+ label: label2,
101070
+ environment: opts2.environment ?? null,
101071
+ reEncryptedFiles: result.reEncryptedFiles.length
101072
+ });
101073
+ return;
101074
+ }
100922
101075
  formatter.success(
100923
101076
  `${label2} removed. ${result.reEncryptedFiles.length} files re-encrypted. ${sym("locked")}`
100924
101077
  );
@@ -100983,6 +101136,15 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100983
101136
  return;
100984
101137
  }
100985
101138
  upsertRequest(repoRoot, publicKey, label2, opts2.environment);
101139
+ if (isJsonMode()) {
101140
+ formatter.json({
101141
+ action: "requested",
101142
+ label: label2,
101143
+ key: publicKey,
101144
+ environment: opts2.environment ?? null
101145
+ });
101146
+ return;
101147
+ }
100986
101148
  const scope = opts2.environment ? ` for environment '${opts2.environment}'` : "";
100987
101149
  formatter.success(`Access requested as '${label2}'${scope}`);
100988
101150
  formatter.print(` Key: ${keyPreview(publicKey)}`);
@@ -100998,6 +101160,15 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100998
101160
  try {
100999
101161
  const repoRoot = program3.opts().dir || process.cwd();
101000
101162
  const requests = loadRequests(repoRoot);
101163
+ if (isJsonMode()) {
101164
+ formatter.json(
101165
+ requests.map((r) => ({
101166
+ ...r,
101167
+ requestedAt: r.requestedAt.toISOString()
101168
+ }))
101169
+ );
101170
+ return;
101171
+ }
101001
101172
  if (requests.length === 0) {
101002
101173
  formatter.info("No pending access requests.");
101003
101174
  return;
@@ -101037,7 +101208,7 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
101037
101208
  process.exit(2);
101038
101209
  return;
101039
101210
  }
101040
- const success = await executeRecipientAdd(
101211
+ const result = await executeRecipientAdd(
101041
101212
  repoRoot,
101042
101213
  program3,
101043
101214
  deps2,
@@ -101045,11 +101216,21 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
101045
101216
  request.label,
101046
101217
  environment
101047
101218
  );
101048
- if (success) {
101219
+ if (result) {
101049
101220
  removeRequest(repoRoot, identifier);
101050
- formatter.hint(
101051
- `git add clef.yaml ${REQUESTS_FILENAME} && git add -A && git commit -m "approve recipient: ${request.label} [${environment}]"`
101052
- );
101221
+ if (isJsonMode()) {
101222
+ formatter.json({
101223
+ action: "approved",
101224
+ identifier,
101225
+ label: request.label,
101226
+ environment,
101227
+ reEncryptedFiles: result.reEncryptedFiles.length
101228
+ });
101229
+ } else {
101230
+ formatter.hint(
101231
+ `git add clef.yaml ${REQUESTS_FILENAME} && git add -A && git commit -m "approve recipient: ${request.label} [${environment}]"`
101232
+ );
101233
+ }
101053
101234
  }
101054
101235
  } catch (err) {
101055
101236
  handleCommandError(err);
@@ -101065,7 +101246,7 @@ async function executeRecipientAdd(repoRoot, _program, deps2, key, label2, envir
101065
101246
  `Environment '${environment}' not found. Available: ${manifest.environments.map((e) => e.name).join(", ")}`
101066
101247
  );
101067
101248
  process.exit(2);
101068
- return false;
101249
+ return null;
101069
101250
  }
101070
101251
  const matrixManager = new MatrixManager();
101071
101252
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
@@ -101074,7 +101255,7 @@ async function executeRecipientAdd(repoRoot, _program, deps2, key, label2, envir
101074
101255
  if (existing.some((r) => r.key === key)) {
101075
101256
  formatter.error(`Recipient '${keyPreview(key)}' is already present.`);
101076
101257
  process.exit(2);
101077
- return false;
101258
+ return null;
101078
101259
  }
101079
101260
  const allCells = matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists && c.environment === environment);
101080
101261
  const fileCount = allCells.length;
@@ -101091,7 +101272,7 @@ This will re-encrypt ${fileCount} files in the matrix.`);
101091
101272
  const confirmed = await formatter.confirm("Proceed?");
101092
101273
  if (!confirmed) {
101093
101274
  formatter.info("Aborted.");
101094
- return false;
101275
+ return null;
101095
101276
  }
101096
101277
  formatter.print(`
101097
101278
  ${sym("working")} Re-encrypting matrix...`);
@@ -101108,7 +101289,7 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`);
101108
101289
  );
101109
101290
  formatter.print("\nNo changes were applied. Investigate the error above and retry.");
101110
101291
  process.exit(1);
101111
- return false;
101292
+ return null;
101112
101293
  }
101113
101294
  for (const file of result.reEncryptedFiles) {
101114
101295
  const relative7 = path45.relative(repoRoot, file);
@@ -101118,7 +101299,7 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`);
101118
101299
  formatter.success(
101119
101300
  `${displayLabel} added. ${result.reEncryptedFiles.length} files re-encrypted. ${sym("locked")}`
101120
101301
  );
101121
- return true;
101302
+ return { reEncryptedFiles: result.reEncryptedFiles };
101122
101303
  }
101123
101304
 
101124
101305
  // src/commands/merge-driver.ts
@@ -101281,6 +101462,16 @@ function registerServiceCommand(program3, deps2) {
101281
101462
  repoRoot,
101282
101463
  kmsEnvConfigs
101283
101464
  );
101465
+ if (isJsonMode()) {
101466
+ formatter.json({
101467
+ action: "created",
101468
+ identity: result.identity.name,
101469
+ namespaces: result.identity.namespaces,
101470
+ environments: Object.keys(result.identity.environments),
101471
+ privateKeys: result.privateKeys
101472
+ });
101473
+ return;
101474
+ }
101284
101475
  formatter.success(`Service identity '${name}' created.`);
101285
101476
  formatter.print(`
101286
101477
  Namespaces: ${result.identity.namespaces.join(", ")}`);
@@ -101334,6 +101525,10 @@ function registerServiceCommand(program3, deps2) {
101334
101525
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
101335
101526
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
101336
101527
  const identities = manager.list(manifest);
101528
+ if (isJsonMode()) {
101529
+ formatter.json(identities);
101530
+ return;
101531
+ }
101337
101532
  if (identities.length === 0) {
101338
101533
  formatter.info("No service identities configured.");
101339
101534
  return;
@@ -101363,6 +101558,10 @@ function registerServiceCommand(program3, deps2) {
101363
101558
  process.exit(1);
101364
101559
  return;
101365
101560
  }
101561
+ if (isJsonMode()) {
101562
+ formatter.json(identity);
101563
+ return;
101564
+ }
101366
101565
  formatter.print(`
101367
101566
  Service Identity: ${identity.name}`);
101368
101567
  formatter.print(`Description: ${identity.description}`);
@@ -101391,6 +101590,11 @@ Service Identity: ${identity.name}`);
101391
101590
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
101392
101591
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
101393
101592
  const issues = await manager.validate(manifest, repoRoot);
101593
+ if (isJsonMode()) {
101594
+ formatter.json({ issues });
101595
+ process.exit(issues.length > 0 ? 1 : 0);
101596
+ return;
101597
+ }
101394
101598
  if (issues.length === 0) {
101395
101599
  formatter.success("All service identities are valid.");
101396
101600
  return;
@@ -101441,6 +101645,17 @@ Service Identity: ${identity.name}`);
101441
101645
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
101442
101646
  formatter.print(`${sym("working")} Updating service identity '${name}'...`);
101443
101647
  await manager.updateEnvironments(name, kmsEnvConfigs, manifest, repoRoot);
101648
+ if (isJsonMode()) {
101649
+ formatter.json({
101650
+ action: "updated",
101651
+ identity: name,
101652
+ changed: Object.entries(kmsEnvConfigs).map(([env, cfg]) => ({
101653
+ environment: env,
101654
+ provider: cfg.provider
101655
+ }))
101656
+ });
101657
+ return;
101658
+ }
101444
101659
  formatter.success(`Service identity '${name}' updated.`);
101445
101660
  for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
101446
101661
  formatter.print(` ${envName}: switched to KMS envelope (${kmsConfig.provider})`);
@@ -101476,6 +101691,10 @@ Service Identity: ${identity.name}`);
101476
101691
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
101477
101692
  formatter.print(`${sym("working")} Deleting service identity '${name}'...`);
101478
101693
  await manager.delete(name, manifest, repoRoot);
101694
+ if (isJsonMode()) {
101695
+ formatter.json({ action: "deleted", identity: name });
101696
+ return;
101697
+ }
101479
101698
  formatter.success(`Service identity '${name}' deleted.`);
101480
101699
  formatter.hint(
101481
101700
  `git add clef.yaml && git commit -m "chore: delete service identity '${name}'"`
@@ -101512,6 +101731,15 @@ Service Identity: ${identity.name}`);
101512
101731
  }
101513
101732
  formatter.print(`${sym("working")} Rotating key for '${name}'...`);
101514
101733
  const newKeys = await manager.rotateKey(name, manifest, repoRoot, opts2.environment);
101734
+ if (isJsonMode()) {
101735
+ formatter.json({
101736
+ action: "rotated",
101737
+ identity: name,
101738
+ environments: Object.keys(newKeys),
101739
+ privateKeys: newKeys
101740
+ });
101741
+ return;
101742
+ }
101515
101743
  formatter.success(`Key rotated for '${name}'.`);
101516
101744
  const entries = Object.entries(newKeys);
101517
101745
  const block = entries.map(([env, key]) => `${env}: ${key}`).join("\n");
@@ -101687,6 +101915,18 @@ function registerPackCommand(program3, deps2) {
101687
101915
  const fileOut = new FilePackOutput(outputPath);
101688
101916
  await fileOut.write(memOutput.artifact, memOutput.json);
101689
101917
  }
101918
+ if (isJsonMode()) {
101919
+ formatter.json({
101920
+ identity,
101921
+ environment,
101922
+ keyCount: result.keyCount,
101923
+ namespaceCount: result.namespaceCount,
101924
+ artifactSize: result.artifactSize,
101925
+ revision: result.revision,
101926
+ output: outputPath ?? null
101927
+ });
101928
+ return;
101929
+ }
101690
101930
  formatter.success(
101691
101931
  `Artifact packed: ${result.keyCount} keys from ${result.namespaceCount} namespace(s).`
101692
101932
  );
@@ -101764,6 +102004,15 @@ function registerRevokeCommand(program3, _deps) {
101764
102004
  fs32.mkdirSync(artifactDir, { recursive: true });
101765
102005
  fs32.writeFileSync(artifactPath, JSON.stringify(revoked, null, 2) + "\n", "utf-8");
101766
102006
  const relPath = path49.relative(repoRoot, artifactPath);
102007
+ if (isJsonMode()) {
102008
+ formatter.json({
102009
+ identity,
102010
+ environment,
102011
+ revokedAt: revoked.revokedAt,
102012
+ markerPath: relPath
102013
+ });
102014
+ return;
102015
+ }
101767
102016
  formatter.success(`Artifact revoked: ${relPath}`);
101768
102017
  formatter.print("");
101769
102018
  formatter.print(`${sym("arrow")} If your agent fetches artifacts from git (VCS source):`);
@@ -101792,37 +102041,35 @@ import * as path50 from "path";
101792
102041
  function registerDriftCommand(program3, _deps) {
101793
102042
  program3.command("drift <path>").description(
101794
102043
  "Compare key sets across two local Clef repos without decryption.\n\nReads encrypted YAML files as plaintext (key names are not encrypted)\nand reports keys that exist in some environments but not others.\n\nDoes not require sops to be installed.\n\nExit codes:\n 0 No drift\n 1 Drift found"
101795
- ).option("--json", "Output raw DriftResult JSON for CI parsing").option("--push", "Push results as OTLP to CLEF_TELEMETRY_URL").option("--namespace <name...>", "Scope to specific namespace(s)").action(
101796
- async (remotePath, options) => {
101797
- try {
101798
- const localRoot = program3.opts().dir || process.cwd();
101799
- const remoteRoot = path50.resolve(localRoot, remotePath);
101800
- const detector = new DriftDetector();
101801
- const result = detector.detect(localRoot, remoteRoot, options.namespace);
101802
- if (options.push) {
101803
- const config = resolveTelemetryConfig();
101804
- if (!config) {
101805
- formatter.error("--push requires CLEF_TELEMETRY_URL to be set.");
101806
- process.exit(1);
101807
- return;
101808
- }
101809
- const payload = driftResultToOtlp(result, version2);
101810
- await pushOtlp(payload, config);
101811
- formatter.success("Drift results pushed to telemetry endpoint.");
101812
- }
101813
- if (options.json) {
101814
- formatter.raw(JSON.stringify(result, null, 2) + "\n");
101815
- process.exit(result.issues.length > 0 ? 1 : 0);
102044
+ ).option("--push", "Push results as OTLP to CLEF_TELEMETRY_URL").option("--namespace <name...>", "Scope to specific namespace(s)").action(async (remotePath, options) => {
102045
+ try {
102046
+ const localRoot = program3.opts().dir || process.cwd();
102047
+ const remoteRoot = path50.resolve(localRoot, remotePath);
102048
+ const detector = new DriftDetector();
102049
+ const result = detector.detect(localRoot, remoteRoot, options.namespace);
102050
+ if (options.push) {
102051
+ const config = resolveTelemetryConfig();
102052
+ if (!config) {
102053
+ formatter.error("--push requires CLEF_TELEMETRY_URL to be set.");
102054
+ process.exit(1);
101816
102055
  return;
101817
102056
  }
101818
- formatDriftOutput(result);
102057
+ const payload = driftResultToOtlp(result, version2);
102058
+ await pushOtlp(payload, config);
102059
+ formatter.success("Drift results pushed to telemetry endpoint.");
102060
+ }
102061
+ if (isJsonMode()) {
102062
+ formatter.json(result);
101819
102063
  process.exit(result.issues.length > 0 ? 1 : 0);
101820
- } catch (err) {
101821
- formatter.error(err.message);
101822
- process.exit(1);
102064
+ return;
101823
102065
  }
102066
+ formatDriftOutput(result);
102067
+ process.exit(result.issues.length > 0 ? 1 : 0);
102068
+ } catch (err) {
102069
+ formatter.error(err.message);
102070
+ process.exit(1);
101824
102071
  }
101825
- );
102072
+ });
101826
102073
  }
101827
102074
  function formatDriftOutput(result) {
101828
102075
  if (result.namespacesCompared === 0) {
@@ -101915,7 +102162,7 @@ async function getHeadSha(repoRoot, runner2) {
101915
102162
  function registerReportCommand(program3, deps2) {
101916
102163
  program3.command("report").description(
101917
102164
  "Generate a metadata report for this Clef repository.\n\nIncludes repo identity, matrix status, policy issues, and recipient\nsummaries. Never exposes ciphertext, key names, or decrypted values.\n\nExit codes:\n 0 No errors\n 1 Errors found"
101918
- ).option("--json", "Output full report as JSON").option("--push", "Push report as OTLP to CLEF_TELEMETRY_URL (with automatic gap-fill)").option("--at <sha>", "Generate report at a specific commit").option("--since <sha>", "Generate reports for all commits since <sha>").option("--namespace <name...>", "Filter to namespace(s)").option("--environment <name...>", "Filter to environment(s)").action(
102165
+ ).option("--push", "Push report as OTLP to CLEF_TELEMETRY_URL (with automatic gap-fill)").option("--at <sha>", "Generate report at a specific commit").option("--since <sha>", "Generate reports for all commits since <sha>").option("--namespace <name...>", "Filter to namespace(s)").option("--environment <name...>", "Filter to environment(s)").action(
101919
102166
  async (options) => {
101920
102167
  try {
101921
102168
  const repoRoot = program3.opts().dir || process.cwd();
@@ -101927,7 +102174,7 @@ function registerReportCommand(program3, deps2) {
101927
102174
  deps2.runner
101928
102175
  );
101929
102176
  await maybePush(report, options.push);
101930
- outputReport(report, options);
102177
+ outputReport(report);
101931
102178
  return;
101932
102179
  }
101933
102180
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
@@ -101945,7 +102192,7 @@ function registerReportCommand(program3, deps2) {
101945
102192
  });
101946
102193
  if (options.push) {
101947
102194
  await pushWithGapFill(repoRoot, headReport, deps2.runner);
101948
- outputReport(headReport, options);
102195
+ outputReport(headReport);
101949
102196
  return;
101950
102197
  }
101951
102198
  if (options.since) {
@@ -101959,8 +102206,8 @@ function registerReportCommand(program3, deps2) {
101959
102206
  reports.push(await generateReportAtCommit(repoRoot, sha, version2, deps2.runner));
101960
102207
  }
101961
102208
  }
101962
- if (options.json) {
101963
- formatter.raw(JSON.stringify(reports, null, 2) + "\n");
102209
+ if (isJsonMode()) {
102210
+ formatter.json(reports);
101964
102211
  } else {
101965
102212
  formatter.print(
101966
102213
  `Generated ${reports.length} report(s) for commits since ${options.since.slice(0, 8)}`
@@ -101975,7 +102222,7 @@ function registerReportCommand(program3, deps2) {
101975
102222
  process.exit(reports.some((r) => r.policy.issueCount.error > 0) ? 1 : 0);
101976
102223
  return;
101977
102224
  }
101978
- outputReport(headReport, options);
102225
+ outputReport(headReport);
101979
102226
  } catch (err) {
101980
102227
  handleCommandError(err);
101981
102228
  }
@@ -102026,9 +102273,9 @@ async function maybePush(report, push) {
102026
102273
  await pushOtlp(payload, config);
102027
102274
  formatter.success("Report pushed to telemetry endpoint.");
102028
102275
  }
102029
- function outputReport(report, opts2) {
102030
- if (opts2.json) {
102031
- formatter.raw(JSON.stringify(report, null, 2) + "\n");
102276
+ function outputReport(report) {
102277
+ if (isJsonMode()) {
102278
+ formatter.json(report);
102032
102279
  process.exit(report.policy.issueCount.error > 0 ? 1 : 0);
102033
102280
  return;
102034
102281
  }
@@ -102197,6 +102444,16 @@ function registerInstallCommand(program3, _deps) {
102197
102444
  }
102198
102445
  const manifestFile = files.find((f2) => f2.name === "broker.yaml");
102199
102446
  const manifest = manifestFile ? (0, import_yaml.parse)(manifestFile.content) : {};
102447
+ if (isJsonMode()) {
102448
+ formatter.json({
102449
+ broker: entry.name,
102450
+ provider: entry.provider,
102451
+ tier: entry.tier,
102452
+ files: files.map((f2) => `brokers/${entry.name}/${f2.name}`)
102453
+ });
102454
+ process.exit(0);
102455
+ return;
102456
+ }
102200
102457
  formatter.print("");
102201
102458
  formatter.print(` ${sym("success")} ${entry.name}`);
102202
102459
  formatter.print("");
@@ -102260,6 +102517,11 @@ function registerSearchCommand(program3, _deps) {
102260
102517
  if (options.tier) {
102261
102518
  results = results.filter((b) => b.tier === Number(options.tier));
102262
102519
  }
102520
+ if (isJsonMode()) {
102521
+ formatter.json(results);
102522
+ process.exit(0);
102523
+ return;
102524
+ }
102263
102525
  if (results.length === 0) {
102264
102526
  formatter.info("No brokers found matching your query.");
102265
102527
  process.exit(0);
@@ -102390,6 +102652,20 @@ ${sym("working")} Backend migration summary:`);
102390
102652
  }
102391
102653
  }
102392
102654
  );
102655
+ if (isJsonMode()) {
102656
+ formatter.json({
102657
+ backend: target.backend,
102658
+ migratedFiles: result.migratedFiles,
102659
+ skippedFiles: result.skippedFiles,
102660
+ verifiedFiles: result.verifiedFiles,
102661
+ warnings: result.warnings,
102662
+ rolledBack: result.rolledBack,
102663
+ error: result.error ?? null,
102664
+ dryRun: opts2.dryRun ?? false
102665
+ });
102666
+ process.exit(result.rolledBack ? 1 : 0);
102667
+ return;
102668
+ }
102393
102669
  if (result.rolledBack) {
102394
102670
  formatter.error(`Migration failed: ${result.error}`);
102395
102671
  formatter.info("All changes have been rolled back.");
@@ -102540,12 +102816,18 @@ var VERSION = package_default.version;
102540
102816
  var program2 = new Command();
102541
102817
  var runner = new NodeSubprocessRunner();
102542
102818
  var deps = { runner };
102543
- program2.name("clef").option("--dir <path>", "Path to a local Clef repository root (default: current directory)").option("--plain", "Plain output, no emoji or colour");
102819
+ program2.name("clef").option("--dir <path>", "Path to a local Clef repository root (default: current directory)").option("--plain", "Plain output, no emoji or colour").option("--json", "Output machine-readable JSON (suppresses human output)").option("--yes", "Auto-confirm destructive operations (required with --json for writes)");
102544
102820
  program2.hook("preAction", async () => {
102545
102821
  const opts2 = program2.opts();
102546
102822
  if (opts2.plain) {
102547
102823
  setPlainMode(true);
102548
102824
  }
102825
+ if (opts2.json) {
102826
+ setJsonMode(true);
102827
+ }
102828
+ if (opts2.yes) {
102829
+ setYesMode(true);
102830
+ }
102549
102831
  });
102550
102832
  program2.addHelpText("beforeAll", () => {
102551
102833
  const clef = isPlainMode() ? "clef" : symbols.clef;
@@ -102644,6 +102926,9 @@ async function main() {
102644
102926
  }
102645
102927
  }
102646
102928
  main().catch((err) => {
102929
+ if (isJsonMode()) {
102930
+ exitJsonError(err.message);
102931
+ }
102647
102932
  formatter.error(err.message);
102648
102933
  process.exit(1);
102649
102934
  });