@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.
package/dist/index.mjs CHANGED
@@ -20307,7 +20307,7 @@ var init_runner = __esm({
20307
20307
  /**
20308
20308
  * Lint service identity configurations for drift issues.
20309
20309
  */
20310
- async lintServiceIdentities(identities, manifest, _repoRoot, existingCells) {
20310
+ async lintServiceIdentities(identities, manifest, repoRoot, existingCells) {
20311
20311
  const issues = [];
20312
20312
  const declaredEnvNames = new Set(manifest.environments.map((e) => e.name));
20313
20313
  const declaredNsNames = new Set(manifest.namespaces.map((ns) => ns.name));
@@ -21941,6 +21941,55 @@ var init_manager2 = __esm({
21941
21941
  get(manifest, name) {
21942
21942
  return manifest.service_identities?.find((si) => si.name === name);
21943
21943
  }
21944
+ /**
21945
+ * Update environment backends on an existing service identity.
21946
+ * Switches age → KMS (removes old recipient) or updates KMS config.
21947
+ * Returns new private keys for any environments switched from KMS → age.
21948
+ */
21949
+ async updateEnvironments(name, kmsEnvConfigs, manifest, repoRoot) {
21950
+ const identity = this.get(manifest, name);
21951
+ if (!identity) {
21952
+ throw new Error(`Service identity '${name}' not found.`);
21953
+ }
21954
+ const manifestPath = path17.join(repoRoot, CLEF_MANIFEST_FILENAME);
21955
+ const raw = fs15.readFileSync(manifestPath, "utf-8");
21956
+ const doc = YAML10.parse(raw);
21957
+ const identities = doc.service_identities;
21958
+ const siDoc = identities.find((si) => si.name === name);
21959
+ const envs = siDoc.environments;
21960
+ const cells = this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists);
21961
+ const privateKeys = {};
21962
+ for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
21963
+ const oldConfig = identity.environments[envName];
21964
+ if (!oldConfig) {
21965
+ throw new Error(`Environment '${envName}' not found on identity '${name}'.`);
21966
+ }
21967
+ if (oldConfig.recipient) {
21968
+ const scopedCells = cells.filter(
21969
+ (c) => identity.namespaces.includes(c.namespace) && c.environment === envName
21970
+ );
21971
+ for (const cell of scopedCells) {
21972
+ try {
21973
+ await this.encryption.removeRecipient(cell.filePath, oldConfig.recipient);
21974
+ } catch {
21975
+ }
21976
+ }
21977
+ }
21978
+ envs[envName] = { kms: kmsConfig };
21979
+ identity.environments[envName] = { kms: kmsConfig };
21980
+ }
21981
+ const tmp = path17.join(os.tmpdir(), `clef-manifest-${process.pid}-${Date.now()}.tmp`);
21982
+ try {
21983
+ fs15.writeFileSync(tmp, YAML10.stringify(doc), "utf-8");
21984
+ fs15.renameSync(tmp, manifestPath);
21985
+ } finally {
21986
+ try {
21987
+ fs15.unlinkSync(tmp);
21988
+ } catch {
21989
+ }
21990
+ }
21991
+ return { privateKeys };
21992
+ }
21944
21993
  /**
21945
21994
  * Register a service identity's public keys as SOPS recipients on scoped matrix files.
21946
21995
  */
@@ -22201,7 +22250,8 @@ var init_packer = __esm({
22201
22250
  try {
22202
22251
  const e = new Encrypter2();
22203
22252
  e.addRecipient(ephemeralPublicKey);
22204
- ciphertext = await e.encrypt(plaintext);
22253
+ const encrypted = await e.encrypt(plaintext);
22254
+ ciphertext = typeof encrypted === "string" ? encrypted : Buffer.from(encrypted).toString("base64");
22205
22255
  } catch {
22206
22256
  throw new Error("Failed to age-encrypt artifact with ephemeral key.");
22207
22257
  }
@@ -22230,7 +22280,8 @@ var init_packer = __esm({
22230
22280
  const { Encrypter: Encrypter2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
22231
22281
  const e = new Encrypter2();
22232
22282
  e.addRecipient(resolved.recipient);
22233
- ciphertext = await e.encrypt(plaintext);
22283
+ const encrypted = await e.encrypt(plaintext);
22284
+ ciphertext = typeof encrypted === "string" ? encrypted : Buffer.from(encrypted).toString("base64");
22234
22285
  } catch {
22235
22286
  throw new Error("Failed to age-encrypt artifact. Check recipient key.");
22236
22287
  }
@@ -76033,6 +76084,42 @@ var require_api = __commonJS({
76033
76084
  res.status(500).json({ error: message, code: "RECIPIENTS_REMOVE_ERROR" });
76034
76085
  }
76035
76086
  });
76087
+ router.get("/service-identities", (_req, res) => {
76088
+ try {
76089
+ setNoCacheHeaders(res);
76090
+ const manifest = loadManifest();
76091
+ const identities = manifest.service_identities ?? [];
76092
+ const result = identities.map((si) => {
76093
+ const environments = {};
76094
+ for (const [envName, envConfig] of Object.entries(si.environments)) {
76095
+ const env = manifest.environments.find((e) => e.name === envName);
76096
+ if (envConfig.kms) {
76097
+ environments[envName] = {
76098
+ type: "kms",
76099
+ kms: envConfig.kms,
76100
+ protected: env?.protected ?? false
76101
+ };
76102
+ } else {
76103
+ environments[envName] = {
76104
+ type: "age",
76105
+ publicKey: envConfig.recipient,
76106
+ protected: env?.protected ?? false
76107
+ };
76108
+ }
76109
+ }
76110
+ return {
76111
+ name: si.name,
76112
+ description: si.description,
76113
+ namespaces: si.namespaces,
76114
+ environments
76115
+ };
76116
+ });
76117
+ res.json({ identities: result });
76118
+ } catch (err) {
76119
+ const message = err instanceof Error ? err.message : "Failed to load service identities";
76120
+ res.status(500).json({ error: message, code: "SERVICE_IDENTITY_ERROR" });
76121
+ }
76122
+ });
76036
76123
  function dispose() {
76037
76124
  lastScanResult = null;
76038
76125
  lastScanAt = null;
@@ -76452,7 +76539,9 @@ var require_decrypt = __commonJS({
76452
76539
  const { Decrypter: Decrypter2 } = await Promise.resolve(`${"age-encryption"}`).then((s) => __importStar(__require(s)));
76453
76540
  const d = new Decrypter2();
76454
76541
  d.addIdentity(privateKey);
76455
- return d.decrypt(ciphertext, "text");
76542
+ const isAgePem = ciphertext.startsWith("age-encryption.org/v1\n");
76543
+ const input = isAgePem ? ciphertext : Buffer.from(ciphertext, "base64");
76544
+ return d.decrypt(input, "text");
76456
76545
  }
76457
76546
  /**
76458
76547
  * Resolve the age private key from either an inline value or a file path.
@@ -77911,6 +78000,7 @@ ${label2}
77911
78000
  return new Promise((resolve6) => {
77912
78001
  rl.question(color(import_picocolors.default.yellow, `${prompt} [y/N] `), (answer) => {
77913
78002
  rl.close();
78003
+ process.stdin.pause();
77914
78004
  resolve6(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
77915
78005
  });
77916
78006
  });
@@ -77950,6 +78040,7 @@ ${label2}
77950
78040
  process.stdin.setRawMode(false);
77951
78041
  }
77952
78042
  process.stdin.removeListener("data", onData);
78043
+ process.stdin.pause();
77953
78044
  process.stderr.write("\n");
77954
78045
  resolve6(value);
77955
78046
  } else if (char === "") {
@@ -78082,25 +78173,53 @@ async function getDarwin(runner2, account) {
78082
78173
  if (result.exitCode === 0) {
78083
78174
  const key = result.stdout.trim();
78084
78175
  if (key.startsWith("AGE-SECRET-KEY-")) return key;
78176
+ if (key) {
78177
+ formatter.warn(
78178
+ "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."
78179
+ );
78180
+ }
78085
78181
  }
78086
78182
  return null;
78087
78183
  } catch {
78088
78184
  return null;
78089
78185
  }
78090
78186
  }
78187
+ async function readDarwinRaw(runner2, account) {
78188
+ try {
78189
+ const result = await runner2.run("security", [
78190
+ "find-generic-password",
78191
+ "-a",
78192
+ account,
78193
+ "-s",
78194
+ SERVICE,
78195
+ "-w"
78196
+ ]);
78197
+ return result.exitCode === 0 ? result.stdout.trim() : "";
78198
+ } catch {
78199
+ return "";
78200
+ }
78201
+ }
78091
78202
  async function setDarwin(runner2, privateKey, account) {
78092
78203
  await runner2.run("security", ["delete-generic-password", "-a", account, "-s", SERVICE]).catch(() => {
78093
78204
  });
78094
- const result = await runner2.run("security", [
78095
- "add-generic-password",
78096
- "-a",
78097
- account,
78098
- "-s",
78099
- SERVICE,
78100
- "-w",
78101
- privateKey
78102
- ]);
78103
- return result.exitCode === 0;
78205
+ try {
78206
+ const result = await runner2.run(
78207
+ "security",
78208
+ ["add-generic-password", "-a", account, "-s", SERVICE, "-w"],
78209
+ { stdin: privateKey }
78210
+ );
78211
+ if (result.exitCode !== 0) return false;
78212
+ const stored = await readDarwinRaw(runner2, account);
78213
+ if (stored === privateKey) return true;
78214
+ formatter.warn(
78215
+ "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."
78216
+ );
78217
+ await runner2.run("security", ["delete-generic-password", "-a", account, "-s", SERVICE]).catch(() => {
78218
+ });
78219
+ return false;
78220
+ } catch {
78221
+ return false;
78222
+ }
78104
78223
  }
78105
78224
  async function getLinux(runner2, account) {
78106
78225
  try {
@@ -78114,6 +78233,11 @@ async function getLinux(runner2, account) {
78114
78233
  if (result.exitCode === 0) {
78115
78234
  const key = result.stdout.trim();
78116
78235
  if (key.startsWith("AGE-SECRET-KEY-")) return key;
78236
+ if (key) {
78237
+ formatter.warn(
78238
+ "OS keychain entry exists but contains invalid key data\n (expected AGE-SECRET-KEY-... format). The entry may be corrupted."
78239
+ );
78240
+ }
78117
78241
  }
78118
78242
  return null;
78119
78243
  } catch {
@@ -78157,6 +78281,11 @@ ${CRED_HELPER_CS}
78157
78281
  if (result.exitCode === 0) {
78158
78282
  const key = result.stdout.trim();
78159
78283
  if (key.startsWith("AGE-SECRET-KEY-")) return key;
78284
+ if (key) {
78285
+ formatter.warn(
78286
+ "Windows Credential Manager entry exists but contains invalid key data\n (expected AGE-SECRET-KEY-... format). The entry may be corrupted."
78287
+ );
78288
+ }
78160
78289
  }
78161
78290
  return null;
78162
78291
  } catch {
@@ -78423,6 +78552,9 @@ async function handleSecondDevOnboarding(repoRoot, clefConfigPath, deps2, option
78423
78552
  formatter.success("Stored age key in OS keychain");
78424
78553
  config = { age_key_storage: "keychain", age_keychain_label: label2 };
78425
78554
  } else {
78555
+ formatter.warn(
78556
+ "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."
78557
+ );
78426
78558
  let keyPath;
78427
78559
  if (options.nonInteractive || !process.stdin.isTTY) {
78428
78560
  keyPath = process.env.CLEF_AGE_KEY_FILE || defaultAgeKeyPath(label2);
@@ -78798,6 +78930,7 @@ function promptWithDefault(message, defaultValue) {
78798
78930
  return new Promise((resolve6) => {
78799
78931
  rl.question(prompt, (answer) => {
78800
78932
  rl.close();
78933
+ process.stdin.pause();
78801
78934
  resolve6(answer.trim() || defaultValue);
78802
78935
  });
78803
78936
  });
@@ -78820,6 +78953,11 @@ async function resolveAgeCredential(repoRoot, runner2) {
78820
78953
  if (label2) {
78821
78954
  const keychainKey = await getKeychainKey(runner2, label2);
78822
78955
  if (keychainKey) return { source: "keychain", privateKey: keychainKey };
78956
+ if (config?.age_key_storage !== "file") {
78957
+ formatter.warn(
78958
+ "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."
78959
+ );
78960
+ }
78823
78961
  }
78824
78962
  if (process.env.CLEF_AGE_KEY) return { source: "env-key" };
78825
78963
  if (process.env.CLEF_AGE_KEY_FILE) return { source: "env-file" };
@@ -78873,7 +79011,10 @@ async function resolveAgePrivateKey(repoRoot, runner2) {
78873
79011
  const content = fs18.readFileSync(filePath, "utf-8");
78874
79012
  const match = content.match(AGE_SECRET_KEY_RE);
78875
79013
  return match ? match[1] : null;
78876
- } catch {
79014
+ } catch (err) {
79015
+ formatter.warn(
79016
+ `Could not read age key file (CLEF_AGE_KEY_FILE=${filePath}): ${err instanceof Error ? err.message : String(err)}`
79017
+ );
78877
79018
  return null;
78878
79019
  }
78879
79020
  }
@@ -78882,7 +79023,10 @@ async function resolveAgePrivateKey(repoRoot, runner2) {
78882
79023
  const content = fs18.readFileSync(credential.path, "utf-8");
78883
79024
  const match = content.match(AGE_SECRET_KEY_RE);
78884
79025
  return match ? match[1] : null;
78885
- } catch {
79026
+ } catch (err) {
79027
+ formatter.warn(
79028
+ `Could not read age key file (${credential.path}): ${err instanceof Error ? err.message : String(err)}`
79029
+ );
78886
79030
  return null;
78887
79031
  }
78888
79032
  }
@@ -78893,16 +79037,52 @@ function readLocalConfig(repoRoot) {
78893
79037
  try {
78894
79038
  if (!fs18.existsSync(clefConfigPath)) return null;
78895
79039
  return YAML12.parse(fs18.readFileSync(clefConfigPath, "utf-8"));
78896
- } catch {
79040
+ } catch (err) {
79041
+ formatter.warn(
79042
+ `Failed to parse ${clefConfigPath}: ${err instanceof Error ? err.message : String(err)}
79043
+ Credential resolution will proceed without local config.`
79044
+ );
78897
79045
  return null;
78898
79046
  }
78899
79047
  }
78900
79048
 
79049
+ // src/clipboard.ts
79050
+ import { execFileSync } from "child_process";
79051
+ function copyToClipboard(text) {
79052
+ try {
79053
+ switch (process.platform) {
79054
+ case "darwin":
79055
+ execFileSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
79056
+ return true;
79057
+ case "win32":
79058
+ execFileSync("clip", { input: text, stdio: ["pipe", "ignore", "ignore"], shell: true });
79059
+ return true;
79060
+ default: {
79061
+ for (const cmd of ["xclip", "xsel"]) {
79062
+ try {
79063
+ const args = cmd === "xclip" ? ["-selection", "clipboard"] : ["--clipboard", "--input"];
79064
+ execFileSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
79065
+ return true;
79066
+ } catch {
79067
+ continue;
79068
+ }
79069
+ }
79070
+ return false;
79071
+ }
79072
+ }
79073
+ } catch {
79074
+ return false;
79075
+ }
79076
+ }
79077
+ function maskedPlaceholder() {
79078
+ return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
79079
+ }
79080
+
78901
79081
  // src/commands/get.ts
78902
79082
  function registerGetCommand(program3, deps2) {
78903
79083
  program3.command("get <target> <key>").description(
78904
- "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"
78905
- ).action(async (target, key) => {
79084
+ "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"
79085
+ ).option("--raw", "Print the plaintext value to stdout (for piping/scripting)").action(async (target, key, opts2) => {
78906
79086
  try {
78907
79087
  const [namespace, environment] = parseTarget(target);
78908
79088
  const repoRoot = program3.opts().dir || process.cwd();
@@ -78921,7 +79101,17 @@ function registerGetCommand(program3, deps2) {
78921
79101
  process.exit(1);
78922
79102
  return;
78923
79103
  }
78924
- formatter.keyValue(key, decrypted.values[key]);
79104
+ const val = decrypted.values[key];
79105
+ if (opts2.raw) {
79106
+ formatter.raw(val);
79107
+ } else {
79108
+ const copied = copyToClipboard(val);
79109
+ if (copied) {
79110
+ formatter.print(` ${key}: ${maskedPlaceholder()} (copied to clipboard)`);
79111
+ } else {
79112
+ formatter.keyValue(key, val);
79113
+ }
79114
+ }
78925
79115
  } catch (err) {
78926
79116
  if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
78927
79117
  formatter.formatDependencyError(err);
@@ -79633,7 +79823,7 @@ async function fetchCheckpoint(config) {
79633
79823
  }
79634
79824
 
79635
79825
  // package.json
79636
- var version = "0.1.6-beta.32";
79826
+ var version = "0.1.7-beta.43";
79637
79827
  var package_default = {
79638
79828
  name: "@clef-sh/cli",
79639
79829
  version,
@@ -80154,8 +80344,8 @@ init_src();
80154
80344
  import * as path31 from "path";
80155
80345
  function registerExportCommand(program3, deps2) {
80156
80346
  program3.command("export <target>").description(
80157
- "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"
80158
- ).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) => {
80347
+ "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"
80348
+ ).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) => {
80159
80349
  try {
80160
80350
  if (options.format !== "env") {
80161
80351
  if (options.format === "dotenv" || options.format === "json" || options.format === "yaml") {
@@ -80188,12 +80378,28 @@ Usage: clef export payments/production --format env`
80188
80378
  const decrypted = await sopsClient.decrypt(filePath);
80189
80379
  const consumption = new ConsumptionClient();
80190
80380
  const output = consumption.formatExport(decrypted, "env", !options.export);
80191
- if (process.platform === "linux") {
80192
- formatter.warn(
80193
- "Exported values will be visible in /proc/<pid>/environ to processes with ptrace access. Use clef exec when possible."
80194
- );
80381
+ if (options.raw) {
80382
+ if (process.platform === "linux") {
80383
+ formatter.warn(
80384
+ "Exported values will be visible in /proc/<pid>/environ to processes with ptrace access. Use clef exec when possible."
80385
+ );
80386
+ }
80387
+ formatter.raw(output);
80388
+ } else {
80389
+ const keyCount = Object.keys(decrypted.values).length;
80390
+ const copied = copyToClipboard(output);
80391
+ if (copied) {
80392
+ formatter.success(`${keyCount} secret(s) copied to clipboard as env exports.`);
80393
+ formatter.hint("eval $(clef export " + target + " --raw) to inject into shell");
80394
+ } else {
80395
+ if (process.platform === "linux") {
80396
+ formatter.warn(
80397
+ "Exported values will be visible in /proc/<pid>/environ to processes with ptrace access. Use clef exec when possible."
80398
+ );
80399
+ }
80400
+ formatter.raw(output);
80401
+ }
80195
80402
  }
80196
- formatter.raw(output);
80197
80403
  } catch (err) {
80198
80404
  if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
80199
80405
  formatter.formatDependencyError(err);
@@ -80853,6 +81059,7 @@ function waitForEnter(message) {
80853
81059
  });
80854
81060
  rl.question(message, () => {
80855
81061
  rl.close();
81062
+ process.stdin.pause();
80856
81063
  resolve6();
80857
81064
  });
80858
81065
  });
@@ -81402,6 +81609,9 @@ function registerServiceCommand(program3, deps2) {
81402
81609
  `Invalid KMS provider '${provider}'. Must be one of: aws, gcp, azure.`
81403
81610
  );
81404
81611
  }
81612
+ if (kmsEnvConfigs[envName]) {
81613
+ throw new Error(`Duplicate --kms-env for environment '${envName}'.`);
81614
+ }
81405
81615
  kmsEnvConfigs[envName] = {
81406
81616
  provider,
81407
81617
  keyId
@@ -81440,13 +81650,24 @@ function registerServiceCommand(program3, deps2) {
81440
81650
  `
81441
81651
  );
81442
81652
  if (Object.keys(result.privateKeys).length > 0) {
81443
- formatter.warn(
81444
- "Private keys are shown ONCE. Store them securely (e.g. AWS Secrets Manager, Vault).\n"
81445
- );
81446
- for (const [envName, privateKey] of Object.entries(result.privateKeys)) {
81447
- formatter.print(` ${envName}:`);
81448
- formatter.print(` ${privateKey}
81653
+ const entries = Object.entries(result.privateKeys);
81654
+ const block = entries.map(([env, key]) => `${env}: ${key}`).join("\n");
81655
+ const copied = copyToClipboard(block);
81656
+ if (copied) {
81657
+ formatter.warn("Private keys copied to clipboard. Store them securely.\n");
81658
+ for (const [envName] of entries) {
81659
+ formatter.print(` ${envName}: ${maskedPlaceholder()}`);
81660
+ }
81661
+ formatter.print("");
81662
+ } else {
81663
+ formatter.warn(
81664
+ "Private keys are shown ONCE. Store them securely (e.g. AWS Secrets Manager, Vault).\n"
81665
+ );
81666
+ for (const [envName, privateKey] of entries) {
81667
+ formatter.print(` ${envName}:`);
81668
+ formatter.print(` ${privateKey}
81449
81669
  `);
81670
+ }
81450
81671
  }
81451
81672
  for (const k of Object.keys(result.privateKeys)) result.privateKeys[k] = "";
81452
81673
  }
@@ -81581,6 +81802,71 @@ Service Identity: ${identity.name}`);
81581
81802
  process.exit(1);
81582
81803
  }
81583
81804
  });
81805
+ serviceCmd.command("update <name>").description("Update an existing service identity's environment backends.").option(
81806
+ "--kms-env <mapping>",
81807
+ "Switch an environment to KMS envelope encryption: env=provider:keyId (repeatable)",
81808
+ (val, acc) => {
81809
+ acc.push(val);
81810
+ return acc;
81811
+ },
81812
+ []
81813
+ ).action(async (name, opts2) => {
81814
+ try {
81815
+ if (opts2.kmsEnv.length === 0) {
81816
+ formatter.error("Nothing to update. Provide --kms-env to change environment backends.");
81817
+ process.exit(1);
81818
+ return;
81819
+ }
81820
+ const repoRoot = program3.opts().dir || process.cwd();
81821
+ const parser = new ManifestParser();
81822
+ const manifest = parser.parse(path38.join(repoRoot, "clef.yaml"));
81823
+ const kmsEnvConfigs = {};
81824
+ for (const mapping of opts2.kmsEnv) {
81825
+ const eqIdx = mapping.indexOf("=");
81826
+ if (eqIdx === -1) {
81827
+ throw new Error(`Invalid --kms-env format: '${mapping}'. Expected: env=provider:keyId`);
81828
+ }
81829
+ const envName = mapping.slice(0, eqIdx);
81830
+ const rest = mapping.slice(eqIdx + 1);
81831
+ const colonIdx = rest.indexOf(":");
81832
+ if (colonIdx === -1) {
81833
+ throw new Error(`Invalid --kms-env format: '${mapping}'. Expected: env=provider:keyId`);
81834
+ }
81835
+ const provider = rest.slice(0, colonIdx);
81836
+ const keyId = rest.slice(colonIdx + 1);
81837
+ if (!["aws", "gcp", "azure"].includes(provider)) {
81838
+ throw new Error(`Invalid KMS provider '${provider}'. Must be one of: aws, gcp, azure.`);
81839
+ }
81840
+ if (kmsEnvConfigs[envName]) {
81841
+ throw new Error(`Duplicate --kms-env for environment '${envName}'.`);
81842
+ }
81843
+ kmsEnvConfigs[envName] = {
81844
+ provider,
81845
+ keyId
81846
+ };
81847
+ }
81848
+ const matrixManager = new MatrixManager();
81849
+ const sopsClient = await createSopsClient(repoRoot, deps2.runner);
81850
+ const manager = new ServiceIdentityManager(sopsClient, matrixManager);
81851
+ formatter.print(`${sym("working")} Updating service identity '${name}'...`);
81852
+ await manager.updateEnvironments(name, kmsEnvConfigs, manifest, repoRoot);
81853
+ formatter.success(`Service identity '${name}' updated.`);
81854
+ for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
81855
+ formatter.print(` ${envName}: switched to KMS envelope (${kmsConfig.provider})`);
81856
+ }
81857
+ formatter.hint(
81858
+ `git add clef.yaml && git commit -m "chore: update service identity '${name}'"`
81859
+ );
81860
+ } catch (err) {
81861
+ if (err instanceof SopsMissingError || err instanceof SopsVersionError) {
81862
+ formatter.formatDependencyError(err);
81863
+ process.exit(1);
81864
+ return;
81865
+ }
81866
+ formatter.error(err.message);
81867
+ process.exit(1);
81868
+ }
81869
+ });
81584
81870
  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, opts2) => {
81585
81871
  try {
81586
81872
  const repoRoot = program3.opts().dir || process.cwd();
@@ -81610,11 +81896,22 @@ Service Identity: ${identity.name}`);
81610
81896
  formatter.print(`${sym("working")} Rotating key for '${name}'...`);
81611
81897
  const newKeys = await manager.rotateKey(name, manifest, repoRoot, opts2.environment);
81612
81898
  formatter.success(`Key rotated for '${name}'.`);
81613
- formatter.warn("New private keys are shown ONCE. Store them securely.\n");
81614
- for (const [envName, privateKey] of Object.entries(newKeys)) {
81615
- formatter.print(` ${envName}:`);
81616
- formatter.print(` ${privateKey}
81899
+ const entries = Object.entries(newKeys);
81900
+ const block = entries.map(([env, key]) => `${env}: ${key}`).join("\n");
81901
+ const copied = copyToClipboard(block);
81902
+ if (copied) {
81903
+ formatter.warn("New private keys copied to clipboard. Store them securely.\n");
81904
+ for (const [envName] of entries) {
81905
+ formatter.print(` ${envName}: ${maskedPlaceholder()}`);
81906
+ }
81907
+ formatter.print("");
81908
+ } else {
81909
+ formatter.warn("New private keys are shown ONCE. Store them securely.\n");
81910
+ for (const [envName, privateKey] of entries) {
81911
+ formatter.print(` ${envName}:`);
81912
+ formatter.print(` ${privateKey}
81617
81913
  `);
81914
+ }
81618
81915
  }
81619
81916
  for (const k of Object.keys(newKeys)) newKeys[k] = "";
81620
81917
  formatter.hint(
@@ -81628,11 +81925,25 @@ Service Identity: ${identity.name}`);
81628
81925
  }
81629
81926
  if (err instanceof PartialRotationError) {
81630
81927
  formatter.error(err.message);
81631
- formatter.warn("Partial rotation succeeded. New private keys below \u2014 store them NOW.\n");
81632
- for (const [envName, privateKey] of Object.entries(err.rotatedKeys)) {
81633
- formatter.print(` ${envName}:`);
81634
- formatter.print(` ${privateKey}
81928
+ const partialEntries = Object.entries(err.rotatedKeys);
81929
+ const partialBlock = partialEntries.map(([env, key]) => `${env}: ${key}`).join("\n");
81930
+ const partialCopied = copyToClipboard(partialBlock);
81931
+ if (partialCopied) {
81932
+ formatter.warn(
81933
+ "Partial rotation succeeded. Rotated keys copied to clipboard \u2014 store them NOW.\n"
81934
+ );
81935
+ for (const [envName] of partialEntries) {
81936
+ formatter.print(` ${envName}: ${maskedPlaceholder()}`);
81937
+ }
81938
+ } else {
81939
+ formatter.warn(
81940
+ "Partial rotation succeeded. New private keys below \u2014 store them NOW.\n"
81941
+ );
81942
+ for (const [envName, privateKey] of partialEntries) {
81943
+ formatter.print(` ${envName}:`);
81944
+ formatter.print(` ${privateKey}
81635
81945
  `);
81946
+ }
81636
81947
  }
81637
81948
  for (const k of Object.keys(err.rotatedKeys)) {
81638
81949
  err.rotatedKeys[k] = "";