@clef-sh/cli 0.1.6-beta.32 → 0.1.7-beta.43

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.
@@ -39,7 +39,7 @@
39
39
  color: #3d4455;
40
40
  }
41
41
  </style>
42
- <script type="module" crossorigin src="/assets/index-DvB73bYZ.js"></script>
42
+ <script type="module" crossorigin src="/assets/index-DDSYn57I.js"></script>
43
43
  </head>
44
44
  <body>
45
45
  <div id="root"></div>
package/dist/index.cjs CHANGED
@@ -19907,7 +19907,7 @@ var init_runner = __esm({
19907
19907
  /**
19908
19908
  * Lint service identity configurations for drift issues.
19909
19909
  */
19910
- async lintServiceIdentities(identities, manifest, _repoRoot, existingCells) {
19910
+ async lintServiceIdentities(identities, manifest, repoRoot, existingCells) {
19911
19911
  const issues = [];
19912
19912
  const declaredEnvNames = new Set(manifest.environments.map((e) => e.name));
19913
19913
  const declaredNsNames = new Set(manifest.namespaces.map((ns) => ns.name));
@@ -21541,6 +21541,55 @@ var init_manager2 = __esm({
21541
21541
  get(manifest, name) {
21542
21542
  return manifest.service_identities?.find((si) => si.name === name);
21543
21543
  }
21544
+ /**
21545
+ * Update environment backends on an existing service identity.
21546
+ * Switches age → KMS (removes old recipient) or updates KMS config.
21547
+ * Returns new private keys for any environments switched from KMS → age.
21548
+ */
21549
+ async updateEnvironments(name, kmsEnvConfigs, manifest, repoRoot) {
21550
+ const identity = this.get(manifest, name);
21551
+ if (!identity) {
21552
+ throw new Error(`Service identity '${name}' not found.`);
21553
+ }
21554
+ const manifestPath = path17.join(repoRoot, CLEF_MANIFEST_FILENAME);
21555
+ const raw = fs15.readFileSync(manifestPath, "utf-8");
21556
+ const doc = YAML10.parse(raw);
21557
+ const identities = doc.service_identities;
21558
+ const siDoc = identities.find((si) => si.name === name);
21559
+ const envs = siDoc.environments;
21560
+ const cells = this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists);
21561
+ const privateKeys = {};
21562
+ for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
21563
+ const oldConfig = identity.environments[envName];
21564
+ if (!oldConfig) {
21565
+ throw new Error(`Environment '${envName}' not found on identity '${name}'.`);
21566
+ }
21567
+ if (oldConfig.recipient) {
21568
+ const scopedCells = cells.filter(
21569
+ (c) => identity.namespaces.includes(c.namespace) && c.environment === envName
21570
+ );
21571
+ for (const cell of scopedCells) {
21572
+ try {
21573
+ await this.encryption.removeRecipient(cell.filePath, oldConfig.recipient);
21574
+ } catch {
21575
+ }
21576
+ }
21577
+ }
21578
+ envs[envName] = { kms: kmsConfig };
21579
+ identity.environments[envName] = { kms: kmsConfig };
21580
+ }
21581
+ const tmp = path17.join(os.tmpdir(), `clef-manifest-${process.pid}-${Date.now()}.tmp`);
21582
+ try {
21583
+ fs15.writeFileSync(tmp, YAML10.stringify(doc), "utf-8");
21584
+ fs15.renameSync(tmp, manifestPath);
21585
+ } finally {
21586
+ try {
21587
+ fs15.unlinkSync(tmp);
21588
+ } catch {
21589
+ }
21590
+ }
21591
+ return { privateKeys };
21592
+ }
21544
21593
  /**
21545
21594
  * Register a service identity's public keys as SOPS recipients on scoped matrix files.
21546
21595
  */
@@ -21801,7 +21850,8 @@ var init_packer = __esm({
21801
21850
  try {
21802
21851
  const e = new Encrypter();
21803
21852
  e.addRecipient(ephemeralPublicKey);
21804
- ciphertext = await e.encrypt(plaintext);
21853
+ const encrypted = await e.encrypt(plaintext);
21854
+ ciphertext = typeof encrypted === "string" ? encrypted : Buffer.from(encrypted).toString("base64");
21805
21855
  } catch {
21806
21856
  throw new Error("Failed to age-encrypt artifact with ephemeral key.");
21807
21857
  }
@@ -21830,7 +21880,8 @@ var init_packer = __esm({
21830
21880
  const { Encrypter } = await Promise.resolve().then(() => __toESM(require_age_encryption()));
21831
21881
  const e = new Encrypter();
21832
21882
  e.addRecipient(resolved.recipient);
21833
- ciphertext = await e.encrypt(plaintext);
21883
+ const encrypted = await e.encrypt(plaintext);
21884
+ ciphertext = typeof encrypted === "string" ? encrypted : Buffer.from(encrypted).toString("base64");
21834
21885
  } catch {
21835
21886
  throw new Error("Failed to age-encrypt artifact. Check recipient key.");
21836
21887
  }
@@ -75633,6 +75684,42 @@ var require_api = __commonJS({
75633
75684
  res.status(500).json({ error: message, code: "RECIPIENTS_REMOVE_ERROR" });
75634
75685
  }
75635
75686
  });
75687
+ router.get("/service-identities", (_req, res) => {
75688
+ try {
75689
+ setNoCacheHeaders(res);
75690
+ const manifest = loadManifest();
75691
+ const identities = manifest.service_identities ?? [];
75692
+ const result = identities.map((si) => {
75693
+ const environments = {};
75694
+ for (const [envName, envConfig] of Object.entries(si.environments)) {
75695
+ const env = manifest.environments.find((e) => e.name === envName);
75696
+ if (envConfig.kms) {
75697
+ environments[envName] = {
75698
+ type: "kms",
75699
+ kms: envConfig.kms,
75700
+ protected: env?.protected ?? false
75701
+ };
75702
+ } else {
75703
+ environments[envName] = {
75704
+ type: "age",
75705
+ publicKey: envConfig.recipient,
75706
+ protected: env?.protected ?? false
75707
+ };
75708
+ }
75709
+ }
75710
+ return {
75711
+ name: si.name,
75712
+ description: si.description,
75713
+ namespaces: si.namespaces,
75714
+ environments
75715
+ };
75716
+ });
75717
+ res.json({ identities: result });
75718
+ } catch (err) {
75719
+ const message = err instanceof Error ? err.message : "Failed to load service identities";
75720
+ res.status(500).json({ error: message, code: "SERVICE_IDENTITY_ERROR" });
75721
+ }
75722
+ });
75636
75723
  function dispose() {
75637
75724
  lastScanResult = null;
75638
75725
  lastScanAt = null;
@@ -76052,7 +76139,9 @@ var require_decrypt = __commonJS({
76052
76139
  const { Decrypter } = await Promise.resolve(`${"age-encryption"}`).then((s) => __importStar(require(s)));
76053
76140
  const d = new Decrypter();
76054
76141
  d.addIdentity(privateKey);
76055
- return d.decrypt(ciphertext, "text");
76142
+ const isAgePem = ciphertext.startsWith("age-encryption.org/v1\n");
76143
+ const input = isAgePem ? ciphertext : Buffer.from(ciphertext, "base64");
76144
+ return d.decrypt(input, "text");
76056
76145
  }
76057
76146
  /**
76058
76147
  * Resolve the age private key from either an inline value or a file path.
@@ -77511,6 +77600,7 @@ ${label}
77511
77600
  return new Promise((resolve6) => {
77512
77601
  rl.question(color(import_picocolors.default.yellow, `${prompt} [y/N] `), (answer) => {
77513
77602
  rl.close();
77603
+ process.stdin.pause();
77514
77604
  resolve6(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
77515
77605
  });
77516
77606
  });
@@ -77550,6 +77640,7 @@ ${label}
77550
77640
  process.stdin.setRawMode(false);
77551
77641
  }
77552
77642
  process.stdin.removeListener("data", onData);
77643
+ process.stdin.pause();
77553
77644
  process.stderr.write("\n");
77554
77645
  resolve6(value);
77555
77646
  } else if (char === "") {
@@ -77682,25 +77773,53 @@ async function getDarwin(runner2, account) {
77682
77773
  if (result.exitCode === 0) {
77683
77774
  const key = result.stdout.trim();
77684
77775
  if (key.startsWith("AGE-SECRET-KEY-")) return key;
77776
+ if (key) {
77777
+ formatter.warn(
77778
+ "OS keychain entry exists but contains invalid key data\n (expected AGE-SECRET-KEY-... format). The entry may be corrupted.\n Delete the 'clef' entry in Keychain Access and re-run clef init."
77779
+ );
77780
+ }
77685
77781
  }
77686
77782
  return null;
77687
77783
  } catch {
77688
77784
  return null;
77689
77785
  }
77690
77786
  }
77787
+ async function readDarwinRaw(runner2, account) {
77788
+ try {
77789
+ const result = await runner2.run("security", [
77790
+ "find-generic-password",
77791
+ "-a",
77792
+ account,
77793
+ "-s",
77794
+ SERVICE,
77795
+ "-w"
77796
+ ]);
77797
+ return result.exitCode === 0 ? result.stdout.trim() : "";
77798
+ } catch {
77799
+ return "";
77800
+ }
77801
+ }
77691
77802
  async function setDarwin(runner2, privateKey, account) {
77692
77803
  await runner2.run("security", ["delete-generic-password", "-a", account, "-s", SERVICE]).catch(() => {
77693
77804
  });
77694
- const result = await runner2.run("security", [
77695
- "add-generic-password",
77696
- "-a",
77697
- account,
77698
- "-s",
77699
- SERVICE,
77700
- "-w",
77701
- privateKey
77702
- ]);
77703
- return result.exitCode === 0;
77805
+ try {
77806
+ const result = await runner2.run(
77807
+ "security",
77808
+ ["add-generic-password", "-a", account, "-s", SERVICE, "-w"],
77809
+ { stdin: privateKey }
77810
+ );
77811
+ if (result.exitCode !== 0) return false;
77812
+ const stored = await readDarwinRaw(runner2, account);
77813
+ if (stored === privateKey) return true;
77814
+ formatter.warn(
77815
+ "Keychain write succeeded but read-back verification failed \u2014\n the stored value may be truncated or corrupted.\n Falling back to file-based key storage."
77816
+ );
77817
+ await runner2.run("security", ["delete-generic-password", "-a", account, "-s", SERVICE]).catch(() => {
77818
+ });
77819
+ return false;
77820
+ } catch {
77821
+ return false;
77822
+ }
77704
77823
  }
77705
77824
  async function getLinux(runner2, account) {
77706
77825
  try {
@@ -77714,6 +77833,11 @@ async function getLinux(runner2, account) {
77714
77833
  if (result.exitCode === 0) {
77715
77834
  const key = result.stdout.trim();
77716
77835
  if (key.startsWith("AGE-SECRET-KEY-")) return key;
77836
+ if (key) {
77837
+ formatter.warn(
77838
+ "OS keychain entry exists but contains invalid key data\n (expected AGE-SECRET-KEY-... format). The entry may be corrupted."
77839
+ );
77840
+ }
77717
77841
  }
77718
77842
  return null;
77719
77843
  } catch {
@@ -77757,6 +77881,11 @@ ${CRED_HELPER_CS}
77757
77881
  if (result.exitCode === 0) {
77758
77882
  const key = result.stdout.trim();
77759
77883
  if (key.startsWith("AGE-SECRET-KEY-")) return key;
77884
+ if (key) {
77885
+ formatter.warn(
77886
+ "Windows Credential Manager entry exists but contains invalid key data\n (expected AGE-SECRET-KEY-... format). The entry may be corrupted."
77887
+ );
77888
+ }
77760
77889
  }
77761
77890
  return null;
77762
77891
  } catch {
@@ -78023,6 +78152,9 @@ async function handleSecondDevOnboarding(repoRoot, clefConfigPath, deps2, option
78023
78152
  formatter.success("Stored age key in OS keychain");
78024
78153
  config = { age_key_storage: "keychain", age_keychain_label: label };
78025
78154
  } else {
78155
+ formatter.warn(
78156
+ "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."
78157
+ );
78026
78158
  let keyPath;
78027
78159
  if (options.nonInteractive || !process.stdin.isTTY) {
78028
78160
  keyPath = process.env.CLEF_AGE_KEY_FILE || defaultAgeKeyPath(label);
@@ -78398,6 +78530,7 @@ function promptWithDefault(message, defaultValue) {
78398
78530
  return new Promise((resolve6) => {
78399
78531
  rl.question(prompt, (answer) => {
78400
78532
  rl.close();
78533
+ process.stdin.pause();
78401
78534
  resolve6(answer.trim() || defaultValue);
78402
78535
  });
78403
78536
  });
@@ -78420,6 +78553,11 @@ async function resolveAgeCredential(repoRoot, runner2) {
78420
78553
  if (label) {
78421
78554
  const keychainKey = await getKeychainKey(runner2, label);
78422
78555
  if (keychainKey) return { source: "keychain", privateKey: keychainKey };
78556
+ if (config?.age_key_storage !== "file") {
78557
+ formatter.warn(
78558
+ "OS keychain is configured but the age key could not be retrieved.\n Falling back to environment variables / key file.\n Run clef doctor for diagnostics."
78559
+ );
78560
+ }
78423
78561
  }
78424
78562
  if (process.env.CLEF_AGE_KEY) return { source: "env-key" };
78425
78563
  if (process.env.CLEF_AGE_KEY_FILE) return { source: "env-file" };
@@ -78473,7 +78611,10 @@ async function resolveAgePrivateKey(repoRoot, runner2) {
78473
78611
  const content = fs18.readFileSync(filePath, "utf-8");
78474
78612
  const match = content.match(AGE_SECRET_KEY_RE);
78475
78613
  return match ? match[1] : null;
78476
- } catch {
78614
+ } catch (err) {
78615
+ formatter.warn(
78616
+ `Could not read age key file (CLEF_AGE_KEY_FILE=${filePath}): ${err instanceof Error ? err.message : String(err)}`
78617
+ );
78477
78618
  return null;
78478
78619
  }
78479
78620
  }
@@ -78482,7 +78623,10 @@ async function resolveAgePrivateKey(repoRoot, runner2) {
78482
78623
  const content = fs18.readFileSync(credential.path, "utf-8");
78483
78624
  const match = content.match(AGE_SECRET_KEY_RE);
78484
78625
  return match ? match[1] : null;
78485
- } catch {
78626
+ } catch (err) {
78627
+ formatter.warn(
78628
+ `Could not read age key file (${credential.path}): ${err instanceof Error ? err.message : String(err)}`
78629
+ );
78486
78630
  return null;
78487
78631
  }
78488
78632
  }
@@ -78493,16 +78637,52 @@ function readLocalConfig(repoRoot) {
78493
78637
  try {
78494
78638
  if (!fs18.existsSync(clefConfigPath)) return null;
78495
78639
  return YAML12.parse(fs18.readFileSync(clefConfigPath, "utf-8"));
78496
- } catch {
78640
+ } catch (err) {
78641
+ formatter.warn(
78642
+ `Failed to parse ${clefConfigPath}: ${err instanceof Error ? err.message : String(err)}
78643
+ Credential resolution will proceed without local config.`
78644
+ );
78497
78645
  return null;
78498
78646
  }
78499
78647
  }
78500
78648
 
78649
+ // src/clipboard.ts
78650
+ var import_child_process2 = require("child_process");
78651
+ function copyToClipboard(text) {
78652
+ try {
78653
+ switch (process.platform) {
78654
+ case "darwin":
78655
+ (0, import_child_process2.execFileSync)("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
78656
+ return true;
78657
+ case "win32":
78658
+ (0, import_child_process2.execFileSync)("clip", { input: text, stdio: ["pipe", "ignore", "ignore"], shell: true });
78659
+ return true;
78660
+ default: {
78661
+ for (const cmd of ["xclip", "xsel"]) {
78662
+ try {
78663
+ const args = cmd === "xclip" ? ["-selection", "clipboard"] : ["--clipboard", "--input"];
78664
+ (0, import_child_process2.execFileSync)(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
78665
+ return true;
78666
+ } catch {
78667
+ continue;
78668
+ }
78669
+ }
78670
+ return false;
78671
+ }
78672
+ }
78673
+ } catch {
78674
+ return false;
78675
+ }
78676
+ }
78677
+ function maskedPlaceholder() {
78678
+ return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
78679
+ }
78680
+
78501
78681
  // src/commands/get.ts
78502
78682
  function registerGetCommand(program3, deps2) {
78503
78683
  program3.command("get <target> <key>").description(
78504
- "Get a single decrypted value.\n\n target: namespace/environment (e.g. payments/production)\n key: the key name to retrieve\n\nExit codes:\n 0 Value found and printed\n 1 Key not found or decryption error"
78505
- ).action(async (target, key) => {
78684
+ "Get a single decrypted value.\n\n target: namespace/environment (e.g. payments/production)\n key: the key name to retrieve\n\nBy default, the value is copied to clipboard and obfuscated on screen.\nUse --raw to print the plaintext value to stdout.\n\nExit codes:\n 0 Value found\n 1 Key not found or decryption error"
78685
+ ).option("--raw", "Print the plaintext value to stdout (for piping/scripting)").action(async (target, key, opts) => {
78506
78686
  try {
78507
78687
  const [namespace, environment] = parseTarget(target);
78508
78688
  const repoRoot = program3.opts().dir || process.cwd();
@@ -78521,7 +78701,17 @@ function registerGetCommand(program3, deps2) {
78521
78701
  process.exit(1);
78522
78702
  return;
78523
78703
  }
78524
- formatter.keyValue(key, decrypted.values[key]);
78704
+ const val = decrypted.values[key];
78705
+ if (opts.raw) {
78706
+ formatter.raw(val);
78707
+ } else {
78708
+ const copied = copyToClipboard(val);
78709
+ if (copied) {
78710
+ formatter.print(` ${key}: ${maskedPlaceholder()} (copied to clipboard)`);
78711
+ } else {
78712
+ formatter.keyValue(key, val);
78713
+ }
78714
+ }
78525
78715
  } catch (err) {
78526
78716
  if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
78527
78717
  formatter.formatDependencyError(err);
@@ -79233,7 +79423,7 @@ async function fetchCheckpoint(config) {
79233
79423
  }
79234
79424
 
79235
79425
  // package.json
79236
- var version = "0.1.6-beta.32";
79426
+ var version = "0.1.7-beta.43";
79237
79427
  var package_default = {
79238
79428
  name: "@clef-sh/cli",
79239
79429
  version,
@@ -79596,7 +79786,7 @@ async function openBrowser(url, runner2) {
79596
79786
 
79597
79787
  // src/commands/exec.ts
79598
79788
  var path30 = __toESM(require("path"));
79599
- var import_child_process2 = require("child_process");
79789
+ var import_child_process3 = require("child_process");
79600
79790
  init_src();
79601
79791
  function collect(value, previous) {
79602
79792
  return previous.concat([value]);
@@ -79701,7 +79891,7 @@ function spawnChild(command, args, env) {
79701
79891
  return new Promise((resolve6) => {
79702
79892
  let child;
79703
79893
  try {
79704
- child = (0, import_child_process2.spawn)(command, args, {
79894
+ child = (0, import_child_process3.spawn)(command, args, {
79705
79895
  env,
79706
79896
  stdio: "inherit"
79707
79897
  });
@@ -79754,8 +79944,8 @@ var path31 = __toESM(require("path"));
79754
79944
  init_src();
79755
79945
  function registerExportCommand(program3, deps2) {
79756
79946
  program3.command("export <target>").description(
79757
- "Print decrypted secrets as shell export statements to stdout.\n\n target: namespace/environment (e.g. payments/production)\n\nUsage:\n eval $(clef export payments/production --format env)\n\nExit codes:\n 0 Values printed successfully\n 1 Decryption error or invalid arguments"
79758
- ).option("--format <format>", "Output format (only 'env' is supported)", "env").option("--no-export", "Omit the 'export' keyword \u2014 output bare KEY=value pairs").action(async (target, options) => {
79947
+ "Export decrypted secrets as shell export statements.\n\n target: namespace/environment (e.g. payments/production)\n\nBy default, exports are copied to clipboard. Use --raw to print to stdout.\n\nUsage:\n clef export payments/production (copies to clipboard)\n eval $(clef export payments/production --raw) (injects into shell)\n\nExit codes:\n 0 Values exported successfully\n 1 Decryption error or invalid arguments"
79948
+ ).option("--format <format>", "Output format (only 'env' is supported)", "env").option("--no-export", "Omit the 'export' keyword \u2014 output bare KEY=value pairs").option("--raw", "Print to stdout instead of clipboard (for eval/piping)").action(async (target, options) => {
79759
79949
  try {
79760
79950
  if (options.format !== "env") {
79761
79951
  if (options.format === "dotenv" || options.format === "json" || options.format === "yaml") {
@@ -79788,12 +79978,28 @@ Usage: clef export payments/production --format env`
79788
79978
  const decrypted = await sopsClient.decrypt(filePath);
79789
79979
  const consumption = new ConsumptionClient();
79790
79980
  const output = consumption.formatExport(decrypted, "env", !options.export);
79791
- if (process.platform === "linux") {
79792
- formatter.warn(
79793
- "Exported values will be visible in /proc/<pid>/environ to processes with ptrace access. Use clef exec when possible."
79794
- );
79981
+ if (options.raw) {
79982
+ if (process.platform === "linux") {
79983
+ formatter.warn(
79984
+ "Exported values will be visible in /proc/<pid>/environ to processes with ptrace access. Use clef exec when possible."
79985
+ );
79986
+ }
79987
+ formatter.raw(output);
79988
+ } else {
79989
+ const keyCount = Object.keys(decrypted.values).length;
79990
+ const copied = copyToClipboard(output);
79991
+ if (copied) {
79992
+ formatter.success(`${keyCount} secret(s) copied to clipboard as env exports.`);
79993
+ formatter.hint("eval $(clef export " + target + " --raw) to inject into shell");
79994
+ } else {
79995
+ if (process.platform === "linux") {
79996
+ formatter.warn(
79997
+ "Exported values will be visible in /proc/<pid>/environ to processes with ptrace access. Use clef exec when possible."
79998
+ );
79999
+ }
80000
+ formatter.raw(output);
80001
+ }
79795
80002
  }
79796
- formatter.raw(output);
79797
80003
  } catch (err) {
79798
80004
  if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
79799
80005
  formatter.formatDependencyError(err);
@@ -80453,6 +80659,7 @@ function waitForEnter(message) {
80453
80659
  });
80454
80660
  rl.question(message, () => {
80455
80661
  rl.close();
80662
+ process.stdin.pause();
80456
80663
  resolve6();
80457
80664
  });
80458
80665
  });
@@ -81002,6 +81209,9 @@ function registerServiceCommand(program3, deps2) {
81002
81209
  `Invalid KMS provider '${provider}'. Must be one of: aws, gcp, azure.`
81003
81210
  );
81004
81211
  }
81212
+ if (kmsEnvConfigs[envName]) {
81213
+ throw new Error(`Duplicate --kms-env for environment '${envName}'.`);
81214
+ }
81005
81215
  kmsEnvConfigs[envName] = {
81006
81216
  provider,
81007
81217
  keyId
@@ -81040,13 +81250,24 @@ function registerServiceCommand(program3, deps2) {
81040
81250
  `
81041
81251
  );
81042
81252
  if (Object.keys(result.privateKeys).length > 0) {
81043
- formatter.warn(
81044
- "Private keys are shown ONCE. Store them securely (e.g. AWS Secrets Manager, Vault).\n"
81045
- );
81046
- for (const [envName, privateKey] of Object.entries(result.privateKeys)) {
81047
- formatter.print(` ${envName}:`);
81048
- formatter.print(` ${privateKey}
81253
+ const entries = Object.entries(result.privateKeys);
81254
+ const block = entries.map(([env, key]) => `${env}: ${key}`).join("\n");
81255
+ const copied = copyToClipboard(block);
81256
+ if (copied) {
81257
+ formatter.warn("Private keys copied to clipboard. Store them securely.\n");
81258
+ for (const [envName] of entries) {
81259
+ formatter.print(` ${envName}: ${maskedPlaceholder()}`);
81260
+ }
81261
+ formatter.print("");
81262
+ } else {
81263
+ formatter.warn(
81264
+ "Private keys are shown ONCE. Store them securely (e.g. AWS Secrets Manager, Vault).\n"
81265
+ );
81266
+ for (const [envName, privateKey] of entries) {
81267
+ formatter.print(` ${envName}:`);
81268
+ formatter.print(` ${privateKey}
81049
81269
  `);
81270
+ }
81050
81271
  }
81051
81272
  for (const k of Object.keys(result.privateKeys)) result.privateKeys[k] = "";
81052
81273
  }
@@ -81181,6 +81402,71 @@ Service Identity: ${identity.name}`);
81181
81402
  process.exit(1);
81182
81403
  }
81183
81404
  });
81405
+ serviceCmd.command("update <name>").description("Update an existing service identity's environment backends.").option(
81406
+ "--kms-env <mapping>",
81407
+ "Switch an environment to KMS envelope encryption: env=provider:keyId (repeatable)",
81408
+ (val, acc) => {
81409
+ acc.push(val);
81410
+ return acc;
81411
+ },
81412
+ []
81413
+ ).action(async (name, opts) => {
81414
+ try {
81415
+ if (opts.kmsEnv.length === 0) {
81416
+ formatter.error("Nothing to update. Provide --kms-env to change environment backends.");
81417
+ process.exit(1);
81418
+ return;
81419
+ }
81420
+ const repoRoot = program3.opts().dir || process.cwd();
81421
+ const parser = new ManifestParser();
81422
+ const manifest = parser.parse(path38.join(repoRoot, "clef.yaml"));
81423
+ const kmsEnvConfigs = {};
81424
+ for (const mapping of opts.kmsEnv) {
81425
+ const eqIdx = mapping.indexOf("=");
81426
+ if (eqIdx === -1) {
81427
+ throw new Error(`Invalid --kms-env format: '${mapping}'. Expected: env=provider:keyId`);
81428
+ }
81429
+ const envName = mapping.slice(0, eqIdx);
81430
+ const rest = mapping.slice(eqIdx + 1);
81431
+ const colonIdx = rest.indexOf(":");
81432
+ if (colonIdx === -1) {
81433
+ throw new Error(`Invalid --kms-env format: '${mapping}'. Expected: env=provider:keyId`);
81434
+ }
81435
+ const provider = rest.slice(0, colonIdx);
81436
+ const keyId = rest.slice(colonIdx + 1);
81437
+ if (!["aws", "gcp", "azure"].includes(provider)) {
81438
+ throw new Error(`Invalid KMS provider '${provider}'. Must be one of: aws, gcp, azure.`);
81439
+ }
81440
+ if (kmsEnvConfigs[envName]) {
81441
+ throw new Error(`Duplicate --kms-env for environment '${envName}'.`);
81442
+ }
81443
+ kmsEnvConfigs[envName] = {
81444
+ provider,
81445
+ keyId
81446
+ };
81447
+ }
81448
+ const matrixManager = new MatrixManager();
81449
+ const sopsClient = await createSopsClient(repoRoot, deps2.runner);
81450
+ const manager = new ServiceIdentityManager(sopsClient, matrixManager);
81451
+ formatter.print(`${sym("working")} Updating service identity '${name}'...`);
81452
+ await manager.updateEnvironments(name, kmsEnvConfigs, manifest, repoRoot);
81453
+ formatter.success(`Service identity '${name}' updated.`);
81454
+ for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
81455
+ formatter.print(` ${envName}: switched to KMS envelope (${kmsConfig.provider})`);
81456
+ }
81457
+ formatter.hint(
81458
+ `git add clef.yaml && git commit -m "chore: update service identity '${name}'"`
81459
+ );
81460
+ } catch (err) {
81461
+ if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
81462
+ formatter.formatDependencyError(err);
81463
+ process.exit(1);
81464
+ return;
81465
+ }
81466
+ formatter.error(err.message);
81467
+ process.exit(1);
81468
+ }
81469
+ });
81184
81470
  serviceCmd.command("rotate <name>").description("Rotate the age key for a service identity.").option("-e, --environment <env>", "Rotate only a specific environment").action(async (name, opts) => {
81185
81471
  try {
81186
81472
  const repoRoot = program3.opts().dir || process.cwd();
@@ -81210,11 +81496,22 @@ Service Identity: ${identity.name}`);
81210
81496
  formatter.print(`${sym("working")} Rotating key for '${name}'...`);
81211
81497
  const newKeys = await manager.rotateKey(name, manifest, repoRoot, opts.environment);
81212
81498
  formatter.success(`Key rotated for '${name}'.`);
81213
- formatter.warn("New private keys are shown ONCE. Store them securely.\n");
81214
- for (const [envName, privateKey] of Object.entries(newKeys)) {
81215
- formatter.print(` ${envName}:`);
81216
- formatter.print(` ${privateKey}
81499
+ const entries = Object.entries(newKeys);
81500
+ const block = entries.map(([env, key]) => `${env}: ${key}`).join("\n");
81501
+ const copied = copyToClipboard(block);
81502
+ if (copied) {
81503
+ formatter.warn("New private keys copied to clipboard. Store them securely.\n");
81504
+ for (const [envName] of entries) {
81505
+ formatter.print(` ${envName}: ${maskedPlaceholder()}`);
81506
+ }
81507
+ formatter.print("");
81508
+ } else {
81509
+ formatter.warn("New private keys are shown ONCE. Store them securely.\n");
81510
+ for (const [envName, privateKey] of entries) {
81511
+ formatter.print(` ${envName}:`);
81512
+ formatter.print(` ${privateKey}
81217
81513
  `);
81514
+ }
81218
81515
  }
81219
81516
  for (const k of Object.keys(newKeys)) newKeys[k] = "";
81220
81517
  formatter.hint(
@@ -81228,11 +81525,25 @@ Service Identity: ${identity.name}`);
81228
81525
  }
81229
81526
  if (err instanceof PartialRotationError) {
81230
81527
  formatter.error(err.message);
81231
- formatter.warn("Partial rotation succeeded. New private keys below \u2014 store them NOW.\n");
81232
- for (const [envName, privateKey] of Object.entries(err.rotatedKeys)) {
81233
- formatter.print(` ${envName}:`);
81234
- formatter.print(` ${privateKey}
81528
+ const partialEntries = Object.entries(err.rotatedKeys);
81529
+ const partialBlock = partialEntries.map(([env, key]) => `${env}: ${key}`).join("\n");
81530
+ const partialCopied = copyToClipboard(partialBlock);
81531
+ if (partialCopied) {
81532
+ formatter.warn(
81533
+ "Partial rotation succeeded. Rotated keys copied to clipboard \u2014 store them NOW.\n"
81534
+ );
81535
+ for (const [envName] of partialEntries) {
81536
+ formatter.print(` ${envName}: ${maskedPlaceholder()}`);
81537
+ }
81538
+ } else {
81539
+ formatter.warn(
81540
+ "Partial rotation succeeded. New private keys below \u2014 store them NOW.\n"
81541
+ );
81542
+ for (const [envName, privateKey] of partialEntries) {
81543
+ formatter.print(` ${envName}:`);
81544
+ formatter.print(` ${privateKey}
81235
81545
  `);
81546
+ }
81236
81547
  }
81237
81548
  for (const k of Object.keys(err.rotatedKeys)) {
81238
81549
  err.rotatedKeys[k] = "";