@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.cjs CHANGED
@@ -97317,11 +97317,23 @@ function sym(key) {
97317
97317
  }
97318
97318
 
97319
97319
  // src/output/formatter.ts
97320
+ var _jsonMode = false;
97321
+ var _yesMode = false;
97322
+ function setJsonMode(json) {
97323
+ _jsonMode = json;
97324
+ }
97325
+ function isJsonMode() {
97326
+ return _jsonMode;
97327
+ }
97328
+ function setYesMode(yes) {
97329
+ _yesMode = yes;
97330
+ }
97320
97331
  function color(fn, str2) {
97321
97332
  return isPlainMode() ? str2 : fn(str2);
97322
97333
  }
97323
97334
  var formatter = {
97324
97335
  success(message) {
97336
+ if (_jsonMode) return;
97325
97337
  const icon = sym("success");
97326
97338
  process.stdout.write(color(import_picocolors.default.green, `${icon} ${message}`) + "\n");
97327
97339
  },
@@ -97338,15 +97350,18 @@ var formatter = {
97338
97350
  process.stderr.write(color(import_picocolors.default.yellow, `${icon} ${message}`) + "\n");
97339
97351
  },
97340
97352
  info(message) {
97353
+ if (_jsonMode) return;
97341
97354
  const icon = sym("info");
97342
97355
  process.stdout.write(color(import_picocolors.default.blue, `${icon} ${message}`) + "\n");
97343
97356
  },
97344
97357
  hint(message) {
97358
+ if (_jsonMode) return;
97345
97359
  const icon = sym("arrow");
97346
97360
  process.stdout.write(`${icon} ${message}
97347
97361
  `);
97348
97362
  },
97349
97363
  keyValue(key, value) {
97364
+ if (_jsonMode) return;
97350
97365
  const icon = sym("key");
97351
97366
  const arrow = sym("arrow");
97352
97367
  const prefix = icon ? `${icon} ` : "";
@@ -97354,30 +97369,38 @@ var formatter = {
97354
97369
  `);
97355
97370
  },
97356
97371
  pendingItem(key, days) {
97372
+ if (_jsonMode) return;
97357
97373
  const icon = sym("pending");
97358
97374
  const prefix = icon ? `${icon} ` : "[pending] ";
97359
97375
  process.stdout.write(`${prefix}${key} \u2014 ${days} day${days !== 1 ? "s" : ""} pending
97360
97376
  `);
97361
97377
  },
97362
97378
  recipientItem(label, keyPreview2) {
97379
+ if (_jsonMode) return;
97363
97380
  const icon = sym("recipient");
97364
97381
  const prefix = icon ? `${icon} ` : "";
97365
97382
  process.stdout.write(`${prefix}${label.padEnd(15)}${keyPreview2}
97366
97383
  `);
97367
97384
  },
97368
97385
  section(label) {
97386
+ if (_jsonMode) return;
97369
97387
  process.stdout.write(`
97370
97388
  ${label}
97371
97389
 
97372
97390
  `);
97373
97391
  },
97374
97392
  print(message) {
97393
+ if (_jsonMode) return;
97375
97394
  process.stdout.write(message + "\n");
97376
97395
  },
97377
97396
  raw(message) {
97378
97397
  process.stdout.write(message);
97379
97398
  },
97399
+ json(data) {
97400
+ process.stdout.write(JSON.stringify(data) + "\n");
97401
+ },
97380
97402
  table(rows, columns) {
97403
+ if (_jsonMode) return;
97381
97404
  const widths = columns.map((col, i) => {
97382
97405
  const maxDataWidth = rows.reduce(
97383
97406
  (max, row) => Math.max(max, stripAnsi(row[i] ?? "").length),
@@ -97394,6 +97417,10 @@ ${label}
97394
97417
  }
97395
97418
  },
97396
97419
  async confirm(prompt) {
97420
+ if (_yesMode) return true;
97421
+ if (_jsonMode) {
97422
+ throw new Error("--json requires --yes for destructive operations");
97423
+ }
97397
97424
  const rl = readline.createInterface({
97398
97425
  input: process.stdin,
97399
97426
  output: process.stderr
@@ -97428,6 +97455,9 @@ ${label}
97428
97455
  `);
97429
97456
  },
97430
97457
  async secretPrompt(prompt) {
97458
+ if (_jsonMode) {
97459
+ throw new Error("--json mode requires value on the command line (no interactive prompt)");
97460
+ }
97431
97461
  return new Promise((resolve7, reject) => {
97432
97462
  process.stderr.write(color(import_picocolors.default.cyan, `${prompt}: `));
97433
97463
  if (process.stdin.isTTY) {
@@ -97470,7 +97500,14 @@ function pad(str2, width) {
97470
97500
  }
97471
97501
 
97472
97502
  // src/handle-error.ts
97503
+ function exitJsonError(message) {
97504
+ formatter.json({ error: true, message });
97505
+ process.exit(1);
97506
+ }
97473
97507
  function handleCommandError(err) {
97508
+ if (isJsonMode()) {
97509
+ exitJsonError(err.message);
97510
+ }
97474
97511
  if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
97475
97512
  formatter.formatDependencyError(err);
97476
97513
  } else {
@@ -97970,7 +98007,7 @@ async function handleSecondDevOnboarding(repoRoot, clefConfigPath, deps2, option
97970
98007
  "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."
97971
98008
  );
97972
98009
  let keyPath;
97973
- if (options.nonInteractive || !process.stdin.isTTY) {
98010
+ if (options.nonInteractive || isJsonMode() || !process.stdin.isTTY) {
97974
98011
  keyPath = process.env.CLEF_AGE_KEY_FILE || defaultAgeKeyPath(label);
97975
98012
  keyPath = path23.resolve(keyPath);
97976
98013
  } else {
@@ -98003,6 +98040,10 @@ async function handleSecondDevOnboarding(repoRoot, clefConfigPath, deps2, option
98003
98040
  formatter.success("Created .clef/.gitignore");
98004
98041
  }
98005
98042
  formatter.success(`Key label: ${label}`);
98043
+ if (isJsonMode()) {
98044
+ formatter.json({ action: "onboarded", manifest: "clef.yaml", config: ".clef/config.yaml" });
98045
+ return;
98046
+ }
98006
98047
  formatter.section("Next steps:");
98007
98048
  formatter.hint("clef recipients request \u2014 request access to encrypted secrets");
98008
98049
  formatter.hint("clef update \u2014 scaffold new environments");
@@ -98013,7 +98054,7 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98013
98054
  let namespaces = options.namespaces ? options.namespaces.split(",").map((s) => s.trim()) : [];
98014
98055
  const backend = options.backend ?? "age";
98015
98056
  let secretsDir = options.secretsDir ?? "secrets";
98016
- if (!options.nonInteractive && process.stdin.isTTY) {
98057
+ if (!options.nonInteractive && !isJsonMode() && process.stdin.isTTY) {
98017
98058
  const envAnswer = await promptWithDefault(
98018
98059
  "Environments (comma-separated)",
98019
98060
  environments.join(",")
@@ -98068,7 +98109,7 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98068
98109
  formatter.warn(
98069
98110
  "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."
98070
98111
  );
98071
- if (!options.nonInteractive && process.stdin.isTTY) {
98112
+ if (!options.nonInteractive && !isJsonMode() && process.stdin.isTTY) {
98072
98113
  const confirmed = await formatter.confirm("Write the private key to the filesystem?");
98073
98114
  if (!confirmed) {
98074
98115
  formatter.error(
@@ -98079,7 +98120,7 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98079
98120
  }
98080
98121
  }
98081
98122
  let keyPath;
98082
- if (options.nonInteractive || !process.stdin.isTTY) {
98123
+ if (options.nonInteractive || isJsonMode() || !process.stdin.isTTY) {
98083
98124
  keyPath = defaultAgeKeyPath(label);
98084
98125
  if (await isInsideAnyGitRepo(path23.resolve(keyPath))) {
98085
98126
  throw new Error(
@@ -98199,6 +98240,17 @@ async function handleFullSetup(repoRoot, manifestPath, clefConfigPath, deps2, op
98199
98240
  formatter.print(" clef config set analytics false (permanent)\n");
98200
98241
  } catch {
98201
98242
  }
98243
+ if (isJsonMode()) {
98244
+ formatter.json({
98245
+ action: "initialized",
98246
+ manifest: "clef.yaml",
98247
+ environments: manifest.environments.map((e) => e.name),
98248
+ namespaces: manifest.namespaces.map((n) => n.name),
98249
+ backend,
98250
+ scaffolded: scaffoldedCount
98251
+ });
98252
+ return;
98253
+ }
98202
98254
  formatter.section("Next steps:");
98203
98255
  formatter.hint("clef set <namespace>/<env> <KEY> <value> \u2014 add a secret");
98204
98256
  formatter.hint("clef scan \u2014 check for existing plaintext secrets");
@@ -98459,7 +98511,9 @@ function registerGetCommand(program3, deps2) {
98459
98511
  return;
98460
98512
  }
98461
98513
  const val = decrypted.values[key];
98462
- if (opts.raw) {
98514
+ if (isJsonMode()) {
98515
+ formatter.json({ key, value: val, namespace, environment });
98516
+ } else if (opts.raw) {
98463
98517
  formatter.raw(val);
98464
98518
  } else {
98465
98519
  const copied = copyToClipboard(val);
@@ -98564,6 +98618,16 @@ function registerSetCommand(program3, deps2) {
98564
98618
  pendingErrors.push(env.name);
98565
98619
  }
98566
98620
  }
98621
+ if (isJsonMode()) {
98622
+ formatter.json({
98623
+ key,
98624
+ namespace: namespace2,
98625
+ environments: manifest.environments.map((e) => e.name),
98626
+ action: "created",
98627
+ pending: true
98628
+ });
98629
+ return;
98630
+ }
98567
98631
  formatter.success(
98568
98632
  `'${key}' set in ${namespace2} across all environments ${sym("locked")}`
98569
98633
  );
@@ -98587,6 +98651,16 @@ function registerSetCommand(program3, deps2) {
98587
98651
  } catch {
98588
98652
  }
98589
98653
  }
98654
+ if (isJsonMode()) {
98655
+ formatter.json({
98656
+ key,
98657
+ namespace: namespace2,
98658
+ environments: manifest.environments.map((e) => e.name),
98659
+ action: "created",
98660
+ pending: false
98661
+ });
98662
+ return;
98663
+ }
98590
98664
  formatter.success(`'${key}' set in ${namespace2} across all environments`);
98591
98665
  formatter.hint(`git add ${namespace2}/ # stage all updated files`);
98592
98666
  }
@@ -98650,6 +98724,10 @@ function registerSetCommand(program3, deps2) {
98650
98724
  process.exit(1);
98651
98725
  return;
98652
98726
  }
98727
+ if (isJsonMode()) {
98728
+ formatter.json({ key, namespace, environment, action: "created", pending: true });
98729
+ return;
98730
+ }
98653
98731
  formatter.success(`${key} set in ${namespace}/${environment} ${sym("locked")}`);
98654
98732
  formatter.print(
98655
98733
  ` ${sym("pending")} Marked as pending \u2014 replace with a real value before deploying`
@@ -98664,6 +98742,10 @@ function registerSetCommand(program3, deps2) {
98664
98742
  The value is saved. Run clef lint to check for stale pending markers.`
98665
98743
  );
98666
98744
  }
98745
+ if (isJsonMode()) {
98746
+ formatter.json({ key, namespace, environment, action: "created", pending: false });
98747
+ return;
98748
+ }
98667
98749
  formatter.success(`${key} set in ${namespace}/${environment}`);
98668
98750
  formatter.hint(
98669
98751
  `Commit: git add ${manifest.file_pattern.replace("{namespace}", namespace).replace("{environment}", environment)}`
@@ -98732,7 +98814,10 @@ function registerCompareCommand(program3, deps2) {
98732
98814
  compareBuf.copy(paddedCompare);
98733
98815
  const timingEqual = crypto5.timingSafeEqual(paddedStored, paddedCompare);
98734
98816
  const match = storedBuf.length === compareBuf.length && timingEqual;
98735
- if (match) {
98817
+ if (isJsonMode()) {
98818
+ formatter.json({ match, key, namespace, environment });
98819
+ if (!match) process.exit(1);
98820
+ } else if (match) {
98736
98821
  formatter.success(`${key} ${sym("arrow")} values match`);
98737
98822
  } else {
98738
98823
  formatter.failure(`${key} ${sym("arrow")} values do not match`);
@@ -98780,6 +98865,15 @@ Type the key name to confirm:`
98780
98865
  }
98781
98866
  const bulkOps = new BulkOps();
98782
98867
  await bulkOps.deleteAcrossEnvironments(namespace, key, manifest, sopsClient, repoRoot);
98868
+ if (isJsonMode()) {
98869
+ formatter.json({
98870
+ key,
98871
+ namespace,
98872
+ environments: manifest.environments.map((e) => e.name),
98873
+ action: "deleted"
98874
+ });
98875
+ return;
98876
+ }
98783
98877
  formatter.success(`Deleted '${key}' from ${namespace} in all environments`);
98784
98878
  } else {
98785
98879
  const [namespace, environment] = parseTarget(target);
@@ -98818,6 +98912,10 @@ Type the key name to confirm:`
98818
98912
  `Key deleted but pending metadata could not be cleaned up. Run clef lint to verify.`
98819
98913
  );
98820
98914
  }
98915
+ if (isJsonMode()) {
98916
+ formatter.json({ key, namespace, environment, action: "deleted" });
98917
+ return;
98918
+ }
98821
98919
  formatter.success(`Deleted '${key}' from ${namespace}/${environment}`);
98822
98920
  formatter.hint(
98823
98921
  `Commit: git add ${manifest.file_pattern.replace("{namespace}", namespace).replace("{environment}", environment)}`
@@ -98836,10 +98934,11 @@ Type the key name to confirm:`
98836
98934
  var path33 = __toESM(require("path"));
98837
98935
  var import_picocolors2 = __toESM(require_picocolors());
98838
98936
  init_src();
98937
+ var MASKED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
98839
98938
  function registerDiffCommand(program3, deps2) {
98840
98939
  program3.command("diff <namespace> <env-a> <env-b>").description(
98841
98940
  "Compare secrets between two environments for a namespace.\n\nExit codes:\n 0 No differences\n 1 Differences found"
98842
- ).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(
98941
+ ).option("--show-identical", "Include identical keys in the output").option("--show-values", "Show plaintext values instead of masking them").action(
98843
98942
  async (namespace, envA, envB, options) => {
98844
98943
  try {
98845
98944
  const repoRoot = program3.opts().dir || process.cwd();
@@ -98866,19 +98965,20 @@ function registerDiffCommand(program3, deps2) {
98866
98965
  formatter.warn("Warning: printing plaintext values for protected environment.");
98867
98966
  }
98868
98967
  }
98869
- if (options.json) {
98870
- const jsonOutput = options.showValues ? result : {
98968
+ if (isJsonMode()) {
98969
+ const jsonData = options.showValues ? result : {
98871
98970
  ...result,
98872
98971
  rows: result.rows.map((r) => ({
98873
98972
  ...r,
98874
- valueA: r.valueA !== null ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null,
98875
- valueB: r.valueB !== null ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null,
98973
+ valueA: r.valueA !== null ? MASKED : null,
98974
+ valueB: r.valueB !== null ? MASKED : null,
98876
98975
  masked: true
98877
98976
  }))
98878
98977
  };
98879
- formatter.raw(JSON.stringify(jsonOutput, null, 2) + "\n");
98978
+ formatter.json(jsonData);
98880
98979
  const hasDiffs2 = result.rows.some((r) => r.status !== "identical");
98881
98980
  process.exit(hasDiffs2 ? 1 : 0);
98981
+ return;
98882
98982
  }
98883
98983
  formatDiffOutput(
98884
98984
  result,
@@ -98898,7 +98998,6 @@ function registerDiffCommand(program3, deps2) {
98898
98998
  }
98899
98999
  );
98900
99000
  }
98901
- var MASKED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
98902
99001
  function formatDiffOutput(result, envA, envB, showIdentical, showValues) {
98903
99002
  const filteredRows = showIdentical ? result.rows : result.rows.filter((r) => r.status !== "identical");
98904
99003
  if (filteredRows.length === 0) {
@@ -99180,7 +99279,7 @@ async function fetchCheckpoint(config) {
99180
99279
  }
99181
99280
 
99182
99281
  // package.json
99183
- var version2 = "0.1.12";
99282
+ var version2 = "0.1.13-beta.88";
99184
99283
  var package_default = {
99185
99284
  name: "@clef-sh/cli",
99186
99285
  version: version2,
@@ -99251,7 +99350,7 @@ var package_default = {
99251
99350
  function registerLintCommand(program3, deps2) {
99252
99351
  program3.command("lint").description(
99253
99352
  "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"
99254
- ).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) => {
99353
+ ).option("--fix", "Auto-fix safe issues (scaffold missing files)").option("--push", "Push results as OTLP to CLEF_TELEMETRY_URL").action(async (options) => {
99255
99354
  try {
99256
99355
  const repoRoot = program3.opts().dir || process.cwd();
99257
99356
  const parser = new ManifestParser();
@@ -99286,8 +99385,8 @@ function registerLintCommand(program3, deps2) {
99286
99385
  await pushOtlp(payload, config);
99287
99386
  formatter.success("Lint results pushed to telemetry endpoint.");
99288
99387
  }
99289
- if (options.json) {
99290
- formatter.raw(JSON.stringify(result, null, 2) + "\n");
99388
+ if (isJsonMode()) {
99389
+ formatter.json(result);
99291
99390
  const hasErrors2 = result.issues.some((i) => i.severity === "error");
99292
99391
  process.exit(hasErrors2 ? 1 : 0);
99293
99392
  return;
@@ -99396,6 +99495,10 @@ function registerRotateCommand(program3, deps2) {
99396
99495
  const relativeFile = manifest.file_pattern.replace("{namespace}", namespace).replace("{environment}", environment);
99397
99496
  formatter.print(`${sym("working")} Rotating ${namespace}/${environment}...`);
99398
99497
  await sopsClient.reEncrypt(filePath, options.newKey);
99498
+ if (isJsonMode()) {
99499
+ formatter.json({ namespace, environment, file: relativeFile, action: "rotated" });
99500
+ return;
99501
+ }
99399
99502
  formatter.success(`Rotated. New values encrypted. ${sym("locked")}`);
99400
99503
  formatter.hint(
99401
99504
  `git add ${relativeFile} && git commit -m "rotate: ${namespace}/${environment}"`
@@ -99441,15 +99544,28 @@ function registerHooksCommand(program3, deps2) {
99441
99544
  }
99442
99545
  const git = new GitIntegration(deps2.runner);
99443
99546
  await git.installPreCommitHook(repoRoot);
99547
+ let mergeDriverOk = false;
99548
+ try {
99549
+ await git.installMergeDriver(repoRoot);
99550
+ mergeDriverOk = true;
99551
+ } catch {
99552
+ }
99553
+ if (isJsonMode()) {
99554
+ formatter.json({
99555
+ preCommitHook: true,
99556
+ mergeDriver: mergeDriverOk,
99557
+ hookPath
99558
+ });
99559
+ return;
99560
+ }
99444
99561
  formatter.success("Pre-commit hook installed");
99445
99562
  formatter.print(` ${sym("pending")} ${hookPath}`);
99446
99563
  formatter.hint(
99447
99564
  "Hook checks SOPS metadata on staged .enc files and runs: clef scan --staged"
99448
99565
  );
99449
- try {
99450
- await git.installMergeDriver(repoRoot);
99566
+ if (mergeDriverOk) {
99451
99567
  formatter.success("SOPS merge driver configured");
99452
- } catch {
99568
+ } else {
99453
99569
  formatter.warn("Could not configure SOPS merge driver. Run inside a git repository.");
99454
99570
  }
99455
99571
  } catch (err) {
@@ -99748,6 +99864,14 @@ Usage: clef export payments/production --format env`
99748
99864
  );
99749
99865
  try {
99750
99866
  const decrypted = await sopsClient.decrypt(filePath);
99867
+ if (isJsonMode()) {
99868
+ const pairs = Object.entries(decrypted.values).map(([k, v]) => ({
99869
+ key: k,
99870
+ value: v
99871
+ }));
99872
+ formatter.json({ pairs, namespace, environment });
99873
+ return;
99874
+ }
99751
99875
  const consumption = new ConsumptionClient();
99752
99876
  const output = consumption.formatExport(decrypted, "env", !options.export);
99753
99877
  if (options.raw) {
@@ -99788,7 +99912,7 @@ init_src();
99788
99912
  function registerDoctorCommand(program3, deps2) {
99789
99913
  program3.command("doctor").description(
99790
99914
  "Check your environment for required dependencies and configuration.\n\nExit codes:\n 0 All checks pass\n 1 One or more checks failed"
99791
- ).option("--json", "Output the full status as JSON").option("--fix", "Attempt to auto-fix issues").action(async (options) => {
99915
+ ).option("--fix", "Attempt to auto-fix issues").action(async (options) => {
99792
99916
  const repoRoot = program3.opts().dir || process.cwd();
99793
99917
  const clefVersion = program3.version() ?? "unknown";
99794
99918
  const checks = [];
@@ -99875,7 +99999,7 @@ function registerDoctorCommand(program3, deps2) {
99875
99999
  formatter.warn("--fix cannot resolve these issues automatically.");
99876
100000
  }
99877
100001
  }
99878
- if (options.json) {
100002
+ if (isJsonMode()) {
99879
100003
  const json = {
99880
100004
  clef: { version: clefVersion, ok: true },
99881
100005
  sops: {
@@ -99905,7 +100029,7 @@ function registerDoctorCommand(program3, deps2) {
99905
100029
  ok: mergeDriverOk
99906
100030
  }
99907
100031
  };
99908
- formatter.raw(JSON.stringify(json, null, 2) + "\n");
100032
+ formatter.json(json);
99909
100033
  const hasFailures = checks.some((c) => !c.ok);
99910
100034
  process.exit(hasFailures ? 1 : 0);
99911
100035
  return;
@@ -100039,6 +100163,11 @@ function registerUpdateCommand(program3, deps2) {
100039
100163
  );
100040
100164
  }
100041
100165
  }
100166
+ if (isJsonMode()) {
100167
+ formatter.json({ scaffolded: scaffoldedCount, failed: failedCount });
100168
+ process.exit(failedCount > 0 ? 1 : 0);
100169
+ return;
100170
+ }
100042
100171
  if (scaffoldedCount > 0) {
100043
100172
  formatter.success(`Scaffolded ${scaffoldedCount} encrypted file(s)`);
100044
100173
  }
@@ -100064,69 +100193,57 @@ function registerScanCommand(program3, deps2) {
100064
100193
  "--severity <level>",
100065
100194
  "Detection level: all (patterns+entropy) or high (patterns only)",
100066
100195
  "all"
100067
- ).option("--json", "Output machine-readable JSON").action(
100068
- async (paths, options) => {
100069
- const repoRoot = program3.opts().dir || process.cwd();
100070
- let manifest;
100071
- try {
100072
- const parser = new ManifestParser();
100073
- manifest = parser.parse(path43.join(repoRoot, "clef.yaml"));
100074
- } catch (err) {
100075
- if (err instanceof ManifestValidationError || err.message?.includes("clef.yaml")) {
100076
- formatter.error("No clef.yaml found. Run 'clef init' to set up this repository.");
100077
- } else {
100078
- formatter.error(err.message);
100079
- }
100080
- process.exit(2);
100081
- return;
100082
- }
100083
- if (options.severity && options.severity !== "all" && options.severity !== "high") {
100084
- formatter.error(`Invalid severity '${options.severity}'. Must be 'all' or 'high'.`);
100085
- process.exit(2);
100086
- return;
100087
- }
100088
- const severity = options.severity === "high" ? "high" : "all";
100089
- const scanRunner = new ScanRunner(deps2.runner);
100090
- if (!options.json) {
100091
- formatter.print(import_picocolors4.default.dim("Scanning repository for unencrypted secrets..."));
100092
- }
100093
- let result;
100094
- try {
100095
- result = await scanRunner.scan(repoRoot, manifest, {
100096
- stagedOnly: options.staged,
100097
- paths: paths.length > 0 ? paths : void 0,
100098
- severity
100099
- });
100100
- } catch (err) {
100101
- formatter.error(`Scan failed: ${err.message}`);
100102
- process.exit(2);
100103
- return;
100104
- }
100105
- if (options.json) {
100106
- const totalIssues = result.matches.length + result.unencryptedMatrixFiles.length;
100107
- formatter.raw(
100108
- JSON.stringify(
100109
- {
100110
- matches: result.matches,
100111
- unencryptedMatrixFiles: result.unencryptedMatrixFiles,
100112
- filesScanned: result.filesScanned,
100113
- filesSkipped: result.filesSkipped,
100114
- durationMs: result.durationMs,
100115
- summary: `${totalIssues} issue${totalIssues !== 1 ? "s" : ""} found`
100116
- },
100117
- null,
100118
- 2
100119
- ) + "\n"
100120
- );
100121
- const hasIssues2 = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100122
- process.exit(hasIssues2 ? 1 : 0);
100123
- return;
100196
+ ).action(async (paths, options) => {
100197
+ const repoRoot = program3.opts().dir || process.cwd();
100198
+ let manifest;
100199
+ try {
100200
+ const parser = new ManifestParser();
100201
+ manifest = parser.parse(path43.join(repoRoot, "clef.yaml"));
100202
+ } catch (err) {
100203
+ if (err instanceof ManifestValidationError || err.message?.includes("clef.yaml")) {
100204
+ formatter.error("No clef.yaml found. Run 'clef init' to set up this repository.");
100205
+ } else {
100206
+ formatter.error(err.message);
100124
100207
  }
100125
- formatScanOutput(result);
100126
- const hasIssues = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100127
- process.exit(hasIssues ? 1 : 0);
100208
+ process.exit(2);
100209
+ return;
100128
100210
  }
100129
- );
100211
+ if (options.severity && options.severity !== "all" && options.severity !== "high") {
100212
+ formatter.error(`Invalid severity '${options.severity}'. Must be 'all' or 'high'.`);
100213
+ process.exit(2);
100214
+ return;
100215
+ }
100216
+ const severity = options.severity === "high" ? "high" : "all";
100217
+ const scanRunner = new ScanRunner(deps2.runner);
100218
+ formatter.print(import_picocolors4.default.dim("Scanning repository for unencrypted secrets..."));
100219
+ let result;
100220
+ try {
100221
+ result = await scanRunner.scan(repoRoot, manifest, {
100222
+ stagedOnly: options.staged,
100223
+ paths: paths.length > 0 ? paths : void 0,
100224
+ severity
100225
+ });
100226
+ } catch (err) {
100227
+ formatter.error(`Scan failed: ${err.message}`);
100228
+ process.exit(2);
100229
+ return;
100230
+ }
100231
+ if (isJsonMode()) {
100232
+ formatter.json({
100233
+ matches: result.matches,
100234
+ unencryptedMatrixFiles: result.unencryptedMatrixFiles,
100235
+ filesScanned: result.filesScanned,
100236
+ filesSkipped: result.filesSkipped,
100237
+ durationMs: result.durationMs
100238
+ });
100239
+ const hasIssues2 = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100240
+ process.exit(hasIssues2 ? 1 : 0);
100241
+ return;
100242
+ }
100243
+ formatScanOutput(result);
100244
+ const hasIssues = result.matches.length > 0 || result.unencryptedMatrixFiles.length > 0;
100245
+ process.exit(hasIssues ? 1 : 0);
100246
+ });
100130
100247
  }
100131
100248
  function formatScanOutput(result) {
100132
100249
  const totalIssues = result.matches.length + result.unencryptedMatrixFiles.length;
@@ -100291,6 +100408,17 @@ function registerImportCommand(program3, deps2) {
100291
100408
  process.exit(2);
100292
100409
  return;
100293
100410
  }
100411
+ if (isJsonMode()) {
100412
+ formatter.json({
100413
+ imported: result.imported,
100414
+ skipped: result.skipped,
100415
+ failed: result.failed,
100416
+ warnings: result.warnings,
100417
+ dryRun: opts.dryRun
100418
+ });
100419
+ process.exit(result.failed.length > 0 ? 1 : 0);
100420
+ return;
100421
+ }
100294
100422
  for (const warning of result.warnings) {
100295
100423
  formatter.print(` ${sym("warning")} ${warning}`);
100296
100424
  }
@@ -100350,6 +100478,7 @@ var path45 = __toESM(require("path"));
100350
100478
  var readline4 = __toESM(require("readline"));
100351
100479
  init_src();
100352
100480
  function waitForEnter(message) {
100481
+ if (isJsonMode()) return Promise.resolve();
100353
100482
  return new Promise((resolve7) => {
100354
100483
  const rl = readline4.createInterface({
100355
100484
  input: process.stdin,
@@ -100383,6 +100512,10 @@ function registerRecipientsCommand(program3, deps2) {
100383
100512
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
100384
100513
  const recipientManager = new RecipientManager(sopsClient, matrixManager);
100385
100514
  const recipients = await recipientManager.list(manifest, repoRoot, opts.environment);
100515
+ if (isJsonMode()) {
100516
+ formatter.json(recipients);
100517
+ return;
100518
+ }
100386
100519
  if (recipients.length === 0) {
100387
100520
  const scope2 = opts.environment ? ` for environment '${opts.environment}'` : "";
100388
100521
  formatter.info(`No recipients configured${scope2}.`);
@@ -100413,7 +100546,7 @@ function registerRecipientsCommand(program3, deps2) {
100413
100546
  return;
100414
100547
  }
100415
100548
  const normalizedKey = validation.key;
100416
- const success = await executeRecipientAdd(
100549
+ const result = await executeRecipientAdd(
100417
100550
  repoRoot,
100418
100551
  program3,
100419
100552
  deps2,
@@ -100421,11 +100554,21 @@ function registerRecipientsCommand(program3, deps2) {
100421
100554
  opts.label,
100422
100555
  opts.environment
100423
100556
  );
100424
- if (success) {
100425
- const label = opts.label || keyPreview(normalizedKey);
100426
- formatter.hint(
100427
- `git add clef.yaml && git add -A && git commit -m "add recipient: ${label} [${opts.environment}]"`
100428
- );
100557
+ if (result) {
100558
+ if (isJsonMode()) {
100559
+ formatter.json({
100560
+ action: "added",
100561
+ key: normalizedKey,
100562
+ label: opts.label || keyPreview(normalizedKey),
100563
+ environment: opts.environment,
100564
+ reEncryptedFiles: result.reEncryptedFiles.length
100565
+ });
100566
+ } else {
100567
+ const label = opts.label || keyPreview(normalizedKey);
100568
+ formatter.hint(
100569
+ `git add clef.yaml && git add -A && git commit -m "add recipient: ${label} [${opts.environment}]"`
100570
+ );
100571
+ }
100429
100572
  }
100430
100573
  } catch (err) {
100431
100574
  handleCommandError(err);
@@ -100521,6 +100664,16 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100521
100664
  const relative7 = path45.relative(repoRoot, file);
100522
100665
  formatter.print(` ${sym("success")} ${relative7}`);
100523
100666
  }
100667
+ if (isJsonMode()) {
100668
+ formatter.json({
100669
+ action: "removed",
100670
+ key: trimmedKey,
100671
+ label,
100672
+ environment: opts.environment ?? null,
100673
+ reEncryptedFiles: result.reEncryptedFiles.length
100674
+ });
100675
+ return;
100676
+ }
100524
100677
  formatter.success(
100525
100678
  `${label} removed. ${result.reEncryptedFiles.length} files re-encrypted. ${sym("locked")}`
100526
100679
  );
@@ -100585,6 +100738,15 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100585
100738
  return;
100586
100739
  }
100587
100740
  upsertRequest(repoRoot, publicKey, label, opts.environment);
100741
+ if (isJsonMode()) {
100742
+ formatter.json({
100743
+ action: "requested",
100744
+ label,
100745
+ key: publicKey,
100746
+ environment: opts.environment ?? null
100747
+ });
100748
+ return;
100749
+ }
100588
100750
  const scope = opts.environment ? ` for environment '${opts.environment}'` : "";
100589
100751
  formatter.success(`Access requested as '${label}'${scope}`);
100590
100752
  formatter.print(` Key: ${keyPreview(publicKey)}`);
@@ -100600,6 +100762,15 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100600
100762
  try {
100601
100763
  const repoRoot = program3.opts().dir || process.cwd();
100602
100764
  const requests = loadRequests(repoRoot);
100765
+ if (isJsonMode()) {
100766
+ formatter.json(
100767
+ requests.map((r) => ({
100768
+ ...r,
100769
+ requestedAt: r.requestedAt.toISOString()
100770
+ }))
100771
+ );
100772
+ return;
100773
+ }
100603
100774
  if (requests.length === 0) {
100604
100775
  formatter.info("No pending access requests.");
100605
100776
  return;
@@ -100639,7 +100810,7 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100639
100810
  process.exit(2);
100640
100811
  return;
100641
100812
  }
100642
- const success = await executeRecipientAdd(
100813
+ const result = await executeRecipientAdd(
100643
100814
  repoRoot,
100644
100815
  program3,
100645
100816
  deps2,
@@ -100647,11 +100818,21 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`
100647
100818
  request.label,
100648
100819
  environment
100649
100820
  );
100650
- if (success) {
100821
+ if (result) {
100651
100822
  removeRequest(repoRoot, identifier);
100652
- formatter.hint(
100653
- `git add clef.yaml ${REQUESTS_FILENAME} && git add -A && git commit -m "approve recipient: ${request.label} [${environment}]"`
100654
- );
100823
+ if (isJsonMode()) {
100824
+ formatter.json({
100825
+ action: "approved",
100826
+ identifier,
100827
+ label: request.label,
100828
+ environment,
100829
+ reEncryptedFiles: result.reEncryptedFiles.length
100830
+ });
100831
+ } else {
100832
+ formatter.hint(
100833
+ `git add clef.yaml ${REQUESTS_FILENAME} && git add -A && git commit -m "approve recipient: ${request.label} [${environment}]"`
100834
+ );
100835
+ }
100655
100836
  }
100656
100837
  } catch (err) {
100657
100838
  handleCommandError(err);
@@ -100667,7 +100848,7 @@ async function executeRecipientAdd(repoRoot, _program, deps2, key, label, enviro
100667
100848
  `Environment '${environment}' not found. Available: ${manifest.environments.map((e) => e.name).join(", ")}`
100668
100849
  );
100669
100850
  process.exit(2);
100670
- return false;
100851
+ return null;
100671
100852
  }
100672
100853
  const matrixManager = new MatrixManager();
100673
100854
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
@@ -100676,7 +100857,7 @@ async function executeRecipientAdd(repoRoot, _program, deps2, key, label, enviro
100676
100857
  if (existing.some((r) => r.key === key)) {
100677
100858
  formatter.error(`Recipient '${keyPreview(key)}' is already present.`);
100678
100859
  process.exit(2);
100679
- return false;
100860
+ return null;
100680
100861
  }
100681
100862
  const allCells = matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists && c.environment === environment);
100682
100863
  const fileCount = allCells.length;
@@ -100693,7 +100874,7 @@ This will re-encrypt ${fileCount} files in the matrix.`);
100693
100874
  const confirmed = await formatter.confirm("Proceed?");
100694
100875
  if (!confirmed) {
100695
100876
  formatter.info("Aborted.");
100696
- return false;
100877
+ return null;
100697
100878
  }
100698
100879
  formatter.print(`
100699
100880
  ${sym("working")} Re-encrypting matrix...`);
@@ -100710,7 +100891,7 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`);
100710
100891
  );
100711
100892
  formatter.print("\nNo changes were applied. Investigate the error above and retry.");
100712
100893
  process.exit(1);
100713
- return false;
100894
+ return null;
100714
100895
  }
100715
100896
  for (const file of result.reEncryptedFiles) {
100716
100897
  const relative7 = path45.relative(repoRoot, file);
@@ -100720,7 +100901,7 @@ ${sym("failure")} Re-encryption failed on ${path45.basename(failedFile)}`);
100720
100901
  formatter.success(
100721
100902
  `${displayLabel} added. ${result.reEncryptedFiles.length} files re-encrypted. ${sym("locked")}`
100722
100903
  );
100723
- return true;
100904
+ return { reEncryptedFiles: result.reEncryptedFiles };
100724
100905
  }
100725
100906
 
100726
100907
  // src/commands/merge-driver.ts
@@ -100883,6 +101064,16 @@ function registerServiceCommand(program3, deps2) {
100883
101064
  repoRoot,
100884
101065
  kmsEnvConfigs
100885
101066
  );
101067
+ if (isJsonMode()) {
101068
+ formatter.json({
101069
+ action: "created",
101070
+ identity: result.identity.name,
101071
+ namespaces: result.identity.namespaces,
101072
+ environments: Object.keys(result.identity.environments),
101073
+ privateKeys: result.privateKeys
101074
+ });
101075
+ return;
101076
+ }
100886
101077
  formatter.success(`Service identity '${name}' created.`);
100887
101078
  formatter.print(`
100888
101079
  Namespaces: ${result.identity.namespaces.join(", ")}`);
@@ -100936,6 +101127,10 @@ function registerServiceCommand(program3, deps2) {
100936
101127
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
100937
101128
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
100938
101129
  const identities = manager.list(manifest);
101130
+ if (isJsonMode()) {
101131
+ formatter.json(identities);
101132
+ return;
101133
+ }
100939
101134
  if (identities.length === 0) {
100940
101135
  formatter.info("No service identities configured.");
100941
101136
  return;
@@ -100965,6 +101160,10 @@ function registerServiceCommand(program3, deps2) {
100965
101160
  process.exit(1);
100966
101161
  return;
100967
101162
  }
101163
+ if (isJsonMode()) {
101164
+ formatter.json(identity);
101165
+ return;
101166
+ }
100968
101167
  formatter.print(`
100969
101168
  Service Identity: ${identity.name}`);
100970
101169
  formatter.print(`Description: ${identity.description}`);
@@ -100993,6 +101192,11 @@ Service Identity: ${identity.name}`);
100993
101192
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
100994
101193
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
100995
101194
  const issues = await manager.validate(manifest, repoRoot);
101195
+ if (isJsonMode()) {
101196
+ formatter.json({ issues });
101197
+ process.exit(issues.length > 0 ? 1 : 0);
101198
+ return;
101199
+ }
100996
101200
  if (issues.length === 0) {
100997
101201
  formatter.success("All service identities are valid.");
100998
101202
  return;
@@ -101043,6 +101247,17 @@ Service Identity: ${identity.name}`);
101043
101247
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
101044
101248
  formatter.print(`${sym("working")} Updating service identity '${name}'...`);
101045
101249
  await manager.updateEnvironments(name, kmsEnvConfigs, manifest, repoRoot);
101250
+ if (isJsonMode()) {
101251
+ formatter.json({
101252
+ action: "updated",
101253
+ identity: name,
101254
+ changed: Object.entries(kmsEnvConfigs).map(([env, cfg]) => ({
101255
+ environment: env,
101256
+ provider: cfg.provider
101257
+ }))
101258
+ });
101259
+ return;
101260
+ }
101046
101261
  formatter.success(`Service identity '${name}' updated.`);
101047
101262
  for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
101048
101263
  formatter.print(` ${envName}: switched to KMS envelope (${kmsConfig.provider})`);
@@ -101078,6 +101293,10 @@ Service Identity: ${identity.name}`);
101078
101293
  const manager = new ServiceIdentityManager(sopsClient, matrixManager);
101079
101294
  formatter.print(`${sym("working")} Deleting service identity '${name}'...`);
101080
101295
  await manager.delete(name, manifest, repoRoot);
101296
+ if (isJsonMode()) {
101297
+ formatter.json({ action: "deleted", identity: name });
101298
+ return;
101299
+ }
101081
101300
  formatter.success(`Service identity '${name}' deleted.`);
101082
101301
  formatter.hint(
101083
101302
  `git add clef.yaml && git commit -m "chore: delete service identity '${name}'"`
@@ -101114,6 +101333,15 @@ Service Identity: ${identity.name}`);
101114
101333
  }
101115
101334
  formatter.print(`${sym("working")} Rotating key for '${name}'...`);
101116
101335
  const newKeys = await manager.rotateKey(name, manifest, repoRoot, opts.environment);
101336
+ if (isJsonMode()) {
101337
+ formatter.json({
101338
+ action: "rotated",
101339
+ identity: name,
101340
+ environments: Object.keys(newKeys),
101341
+ privateKeys: newKeys
101342
+ });
101343
+ return;
101344
+ }
101117
101345
  formatter.success(`Key rotated for '${name}'.`);
101118
101346
  const entries = Object.entries(newKeys);
101119
101347
  const block = entries.map(([env, key]) => `${env}: ${key}`).join("\n");
@@ -101289,6 +101517,18 @@ function registerPackCommand(program3, deps2) {
101289
101517
  const fileOut = new FilePackOutput(outputPath);
101290
101518
  await fileOut.write(memOutput.artifact, memOutput.json);
101291
101519
  }
101520
+ if (isJsonMode()) {
101521
+ formatter.json({
101522
+ identity,
101523
+ environment,
101524
+ keyCount: result.keyCount,
101525
+ namespaceCount: result.namespaceCount,
101526
+ artifactSize: result.artifactSize,
101527
+ revision: result.revision,
101528
+ output: outputPath ?? null
101529
+ });
101530
+ return;
101531
+ }
101292
101532
  formatter.success(
101293
101533
  `Artifact packed: ${result.keyCount} keys from ${result.namespaceCount} namespace(s).`
101294
101534
  );
@@ -101366,6 +101606,15 @@ function registerRevokeCommand(program3, _deps) {
101366
101606
  fs32.mkdirSync(artifactDir, { recursive: true });
101367
101607
  fs32.writeFileSync(artifactPath, JSON.stringify(revoked, null, 2) + "\n", "utf-8");
101368
101608
  const relPath = path49.relative(repoRoot, artifactPath);
101609
+ if (isJsonMode()) {
101610
+ formatter.json({
101611
+ identity,
101612
+ environment,
101613
+ revokedAt: revoked.revokedAt,
101614
+ markerPath: relPath
101615
+ });
101616
+ return;
101617
+ }
101369
101618
  formatter.success(`Artifact revoked: ${relPath}`);
101370
101619
  formatter.print("");
101371
101620
  formatter.print(`${sym("arrow")} If your agent fetches artifacts from git (VCS source):`);
@@ -101394,37 +101643,35 @@ init_src();
101394
101643
  function registerDriftCommand(program3, _deps) {
101395
101644
  program3.command("drift <path>").description(
101396
101645
  "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"
101397
- ).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(
101398
- async (remotePath, options) => {
101399
- try {
101400
- const localRoot = program3.opts().dir || process.cwd();
101401
- const remoteRoot = path50.resolve(localRoot, remotePath);
101402
- const detector = new DriftDetector();
101403
- const result = detector.detect(localRoot, remoteRoot, options.namespace);
101404
- if (options.push) {
101405
- const config = resolveTelemetryConfig();
101406
- if (!config) {
101407
- formatter.error("--push requires CLEF_TELEMETRY_URL to be set.");
101408
- process.exit(1);
101409
- return;
101410
- }
101411
- const payload = driftResultToOtlp(result, version2);
101412
- await pushOtlp(payload, config);
101413
- formatter.success("Drift results pushed to telemetry endpoint.");
101414
- }
101415
- if (options.json) {
101416
- formatter.raw(JSON.stringify(result, null, 2) + "\n");
101417
- process.exit(result.issues.length > 0 ? 1 : 0);
101646
+ ).option("--push", "Push results as OTLP to CLEF_TELEMETRY_URL").option("--namespace <name...>", "Scope to specific namespace(s)").action(async (remotePath, options) => {
101647
+ try {
101648
+ const localRoot = program3.opts().dir || process.cwd();
101649
+ const remoteRoot = path50.resolve(localRoot, remotePath);
101650
+ const detector = new DriftDetector();
101651
+ const result = detector.detect(localRoot, remoteRoot, options.namespace);
101652
+ if (options.push) {
101653
+ const config = resolveTelemetryConfig();
101654
+ if (!config) {
101655
+ formatter.error("--push requires CLEF_TELEMETRY_URL to be set.");
101656
+ process.exit(1);
101418
101657
  return;
101419
101658
  }
101420
- formatDriftOutput(result);
101659
+ const payload = driftResultToOtlp(result, version2);
101660
+ await pushOtlp(payload, config);
101661
+ formatter.success("Drift results pushed to telemetry endpoint.");
101662
+ }
101663
+ if (isJsonMode()) {
101664
+ formatter.json(result);
101421
101665
  process.exit(result.issues.length > 0 ? 1 : 0);
101422
- } catch (err) {
101423
- formatter.error(err.message);
101424
- process.exit(1);
101666
+ return;
101425
101667
  }
101668
+ formatDriftOutput(result);
101669
+ process.exit(result.issues.length > 0 ? 1 : 0);
101670
+ } catch (err) {
101671
+ formatter.error(err.message);
101672
+ process.exit(1);
101426
101673
  }
101427
- );
101674
+ });
101428
101675
  }
101429
101676
  function formatDriftOutput(result) {
101430
101677
  if (result.namespacesCompared === 0) {
@@ -101517,7 +101764,7 @@ async function getHeadSha(repoRoot, runner2) {
101517
101764
  function registerReportCommand(program3, deps2) {
101518
101765
  program3.command("report").description(
101519
101766
  "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"
101520
- ).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(
101767
+ ).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(
101521
101768
  async (options) => {
101522
101769
  try {
101523
101770
  const repoRoot = program3.opts().dir || process.cwd();
@@ -101529,7 +101776,7 @@ function registerReportCommand(program3, deps2) {
101529
101776
  deps2.runner
101530
101777
  );
101531
101778
  await maybePush(report, options.push);
101532
- outputReport(report, options);
101779
+ outputReport(report);
101533
101780
  return;
101534
101781
  }
101535
101782
  const sopsClient = await createSopsClient(repoRoot, deps2.runner);
@@ -101547,7 +101794,7 @@ function registerReportCommand(program3, deps2) {
101547
101794
  });
101548
101795
  if (options.push) {
101549
101796
  await pushWithGapFill(repoRoot, headReport, deps2.runner);
101550
- outputReport(headReport, options);
101797
+ outputReport(headReport);
101551
101798
  return;
101552
101799
  }
101553
101800
  if (options.since) {
@@ -101561,8 +101808,8 @@ function registerReportCommand(program3, deps2) {
101561
101808
  reports.push(await generateReportAtCommit(repoRoot, sha, version2, deps2.runner));
101562
101809
  }
101563
101810
  }
101564
- if (options.json) {
101565
- formatter.raw(JSON.stringify(reports, null, 2) + "\n");
101811
+ if (isJsonMode()) {
101812
+ formatter.json(reports);
101566
101813
  } else {
101567
101814
  formatter.print(
101568
101815
  `Generated ${reports.length} report(s) for commits since ${options.since.slice(0, 8)}`
@@ -101577,7 +101824,7 @@ function registerReportCommand(program3, deps2) {
101577
101824
  process.exit(reports.some((r) => r.policy.issueCount.error > 0) ? 1 : 0);
101578
101825
  return;
101579
101826
  }
101580
- outputReport(headReport, options);
101827
+ outputReport(headReport);
101581
101828
  } catch (err) {
101582
101829
  handleCommandError(err);
101583
101830
  }
@@ -101628,9 +101875,9 @@ async function maybePush(report, push) {
101628
101875
  await pushOtlp(payload, config);
101629
101876
  formatter.success("Report pushed to telemetry endpoint.");
101630
101877
  }
101631
- function outputReport(report, opts) {
101632
- if (opts.json) {
101633
- formatter.raw(JSON.stringify(report, null, 2) + "\n");
101878
+ function outputReport(report) {
101879
+ if (isJsonMode()) {
101880
+ formatter.json(report);
101634
101881
  process.exit(report.policy.issueCount.error > 0 ? 1 : 0);
101635
101882
  return;
101636
101883
  }
@@ -101799,6 +102046,16 @@ function registerInstallCommand(program3, _deps) {
101799
102046
  }
101800
102047
  const manifestFile = files.find((f2) => f2.name === "broker.yaml");
101801
102048
  const manifest = manifestFile ? (0, import_yaml.parse)(manifestFile.content) : {};
102049
+ if (isJsonMode()) {
102050
+ formatter.json({
102051
+ broker: entry.name,
102052
+ provider: entry.provider,
102053
+ tier: entry.tier,
102054
+ files: files.map((f2) => `brokers/${entry.name}/${f2.name}`)
102055
+ });
102056
+ process.exit(0);
102057
+ return;
102058
+ }
101802
102059
  formatter.print("");
101803
102060
  formatter.print(` ${sym("success")} ${entry.name}`);
101804
102061
  formatter.print("");
@@ -101862,6 +102119,11 @@ function registerSearchCommand(program3, _deps) {
101862
102119
  if (options.tier) {
101863
102120
  results = results.filter((b) => b.tier === Number(options.tier));
101864
102121
  }
102122
+ if (isJsonMode()) {
102123
+ formatter.json(results);
102124
+ process.exit(0);
102125
+ return;
102126
+ }
101865
102127
  if (results.length === 0) {
101866
102128
  formatter.info("No brokers found matching your query.");
101867
102129
  process.exit(0);
@@ -101992,6 +102254,20 @@ ${sym("working")} Backend migration summary:`);
101992
102254
  }
101993
102255
  }
101994
102256
  );
102257
+ if (isJsonMode()) {
102258
+ formatter.json({
102259
+ backend: target.backend,
102260
+ migratedFiles: result.migratedFiles,
102261
+ skippedFiles: result.skippedFiles,
102262
+ verifiedFiles: result.verifiedFiles,
102263
+ warnings: result.warnings,
102264
+ rolledBack: result.rolledBack,
102265
+ error: result.error ?? null,
102266
+ dryRun: opts.dryRun ?? false
102267
+ });
102268
+ process.exit(result.rolledBack ? 1 : 0);
102269
+ return;
102270
+ }
101995
102271
  if (result.rolledBack) {
101996
102272
  formatter.error(`Migration failed: ${result.error}`);
101997
102273
  formatter.info("All changes have been rolled back.");
@@ -102142,12 +102418,18 @@ var VERSION = package_default.version;
102142
102418
  var program2 = new Command();
102143
102419
  var runner = new NodeSubprocessRunner();
102144
102420
  var deps = { runner };
102145
- program2.name("clef").option("--dir <path>", "Path to a local Clef repository root (default: current directory)").option("--plain", "Plain output, no emoji or colour");
102421
+ 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)");
102146
102422
  program2.hook("preAction", async () => {
102147
102423
  const opts = program2.opts();
102148
102424
  if (opts.plain) {
102149
102425
  setPlainMode(true);
102150
102426
  }
102427
+ if (opts.json) {
102428
+ setJsonMode(true);
102429
+ }
102430
+ if (opts.yes) {
102431
+ setYesMode(true);
102432
+ }
102151
102433
  });
102152
102434
  program2.addHelpText("beforeAll", () => {
102153
102435
  const clef = isPlainMode() ? "clef" : symbols.clef;
@@ -102246,6 +102528,9 @@ async function main() {
102246
102528
  }
102247
102529
  }
102248
102530
  main().catch((err) => {
102531
+ if (isJsonMode()) {
102532
+ exitJsonError(err.message);
102533
+ }
102249
102534
  formatter.error(err.message);
102250
102535
  process.exit(1);
102251
102536
  });