@kitsy/cnos-cli 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +646 -163
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -16,9 +16,11 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
16
16
  "--provider",
17
17
  "--passphrase",
18
18
  "--vault",
19
- "--inherit"
19
+ "--inherit",
20
+ "--as",
21
+ "--set"
20
22
  ]);
21
- var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public", "--local", "--remote", "--ref"]);
23
+ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public", "--local", "--remote", "--ref", "--no-passphrase"]);
22
24
  function normalizeCommand(argv) {
23
25
  const [command = "doctor", ...rest] = argv;
24
26
  const resource = rest[0];
@@ -26,12 +28,21 @@ function normalizeCommand(argv) {
26
28
  if ((command === "create" || command === "add") && resource === "profile") {
27
29
  return ["profile", "create", ...remaining];
28
30
  }
31
+ if ((command === "create" || command === "add") && resource === "vault") {
32
+ return ["vault", "create", ...remaining];
33
+ }
29
34
  if ((command === "delete" || command === "remove") && resource === "profile") {
30
35
  return ["profile", "delete", ...remaining];
31
36
  }
37
+ if ((command === "delete" || command === "remove") && resource === "vault") {
38
+ return ["vault", "remove", ...remaining];
39
+ }
32
40
  if (command === "list" && resource === "profile") {
33
41
  return ["profile", "list", ...remaining];
34
42
  }
43
+ if (command === "list" && resource === "vault") {
44
+ return ["vault", "list", ...remaining];
45
+ }
35
46
  if ((command === "create" || command === "add") && resource === "secret") {
36
47
  return ["secret", "set", ...remaining];
37
48
  }
@@ -186,16 +197,16 @@ function printJson(value) {
186
197
  }
187
198
 
188
199
  // src/services/writes.ts
189
- import { mkdir, readFile, rm, writeFile } from "fs/promises";
190
- import path from "path";
200
+ import { mkdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
201
+ import path2 from "path";
191
202
  import {
192
- createSecretVault,
203
+ getVaultPassphraseEnvVar,
193
204
  parseYaml,
194
205
  resolveConfigDocumentPath,
195
- resolveSecretPassphrase,
196
- resolveSecretStoreRoot,
197
- stringifyYaml,
198
- writeLocalSecret
206
+ stringifyYaml as stringifyYaml2,
207
+ writeLocalSecret,
208
+ resolveConfiguredVaultPassphrase as resolveConfiguredVaultPassphrase2,
209
+ resolveSecretStoreRoot as resolveSecretStoreRoot2
199
210
  } from "@kitsy/cnos/internal";
200
211
 
201
212
  // src/services/runtime.ts
@@ -228,6 +239,125 @@ async function createRuntimeService(options = {}) {
228
239
  });
229
240
  }
230
241
 
242
+ // src/services/vaults.ts
243
+ import { readFile, rm, writeFile } from "fs/promises";
244
+ import path from "path";
245
+ import {
246
+ createSecretVault,
247
+ loadManifest,
248
+ listSecretVaults,
249
+ resolveConfiguredVaultPassphrase,
250
+ resolveSecretStoreRoot,
251
+ resolveSecretVaultFile,
252
+ resolveVaultDefinition,
253
+ stringifyYaml
254
+ } from "@kitsy/cnos/internal";
255
+ function sortVaults(vaults) {
256
+ return Object.fromEntries(Object.entries(vaults).sort(([left], [right]) => left.localeCompare(right)));
257
+ }
258
+ async function createVaultDefinition(name, options = {}) {
259
+ const vault = name.trim() || "default";
260
+ const provider = options.provider?.trim() || "local";
261
+ const noPassphrase = options.noPassphrase ?? false;
262
+ if (provider === "local" && noPassphrase) {
263
+ throw new Error("Local vaults require a passphrase");
264
+ }
265
+ const loadedManifest = await loadManifest(options.root ? { root: options.root } : {});
266
+ const processEnv = options.processEnv ?? process.env;
267
+ const passphraseEnvVar = "CNOS_SECRET_PASSPHRASE";
268
+ const rawManifest = {
269
+ ...loadedManifest.rawManifest,
270
+ vaults: {
271
+ ...loadedManifest.rawManifest.vaults ?? {},
272
+ [vault]: provider === "local" ? {
273
+ provider: "local",
274
+ passphrase: `env:${passphraseEnvVar}`
275
+ } : {
276
+ provider
277
+ }
278
+ }
279
+ };
280
+ let storePath;
281
+ if (provider === "local") {
282
+ const passphrase = options.passphrase ?? resolveConfiguredVaultPassphrase(
283
+ {
284
+ provider: "local",
285
+ passphrase: `env:${passphraseEnvVar}`
286
+ },
287
+ vault,
288
+ processEnv
289
+ );
290
+ if (!passphrase) {
291
+ throw new Error(`Vault "${vault}" requires --passphrase or ${passphraseEnvVar}`);
292
+ }
293
+ storePath = await createSecretVault(resolveSecretStoreRoot(processEnv), vault, passphrase);
294
+ }
295
+ await writeFile(loadedManifest.manifestPath, stringifyYaml(rawManifest), "utf8");
296
+ return {
297
+ ...resolveVaultDefinition(
298
+ {
299
+ [vault]: rawManifest.vaults[vault]
300
+ },
301
+ vault
302
+ ),
303
+ passphrasePolicy: provider === "local" ? "required" : "none",
304
+ manifestPath: loadedManifest.manifestPath,
305
+ ...storePath ? { storePath } : {}
306
+ };
307
+ }
308
+ async function listVaultDefinitions(options = {}) {
309
+ const loadedManifest = await loadManifest(options.root ? { root: options.root } : {});
310
+ return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
311
+ const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
312
+ return {
313
+ ...definition,
314
+ passphrasePolicy: definition.requiresPassphrase ? "required" : "none"
315
+ };
316
+ });
317
+ }
318
+ async function removeVaultDefinition(name, options = {}) {
319
+ const vault = name.trim() || "default";
320
+ const loadedManifest = await loadManifest(options.root ? { root: options.root } : {});
321
+ if (!loadedManifest.rawManifest.vaults?.[vault]) {
322
+ return {
323
+ name: vault,
324
+ deleted: false,
325
+ manifestPath: loadedManifest.manifestPath
326
+ };
327
+ }
328
+ const nextVaults = { ...loadedManifest.rawManifest.vaults ?? {} };
329
+ delete nextVaults[vault];
330
+ const rawManifest = {
331
+ ...loadedManifest.rawManifest,
332
+ ...Object.keys(nextVaults).length > 0 ? { vaults: sortVaults(nextVaults) } : {}
333
+ };
334
+ if (Object.keys(nextVaults).length === 0) {
335
+ delete rawManifest.vaults;
336
+ }
337
+ await writeFile(loadedManifest.manifestPath, stringifyYaml(rawManifest), "utf8");
338
+ const storeRoot = resolveSecretStoreRoot(options.processEnv);
339
+ const vaultFile = resolveSecretVaultFile(storeRoot, vault);
340
+ const vaultStoreRoot = path.join(storeRoot, "vaults", vault);
341
+ let removedStore;
342
+ try {
343
+ await readFile(vaultFile, "utf8");
344
+ await rm(vaultFile, { force: true });
345
+ await rm(vaultStoreRoot, { recursive: true, force: true });
346
+ removedStore = vaultStoreRoot;
347
+ } catch {
348
+ removedStore = void 0;
349
+ }
350
+ return {
351
+ name: vault,
352
+ deleted: true,
353
+ manifestPath: loadedManifest.manifestPath,
354
+ ...removedStore ? { removedStore } : {}
355
+ };
356
+ }
357
+ async function listLocalStoreVaults(options = {}) {
358
+ return listSecretVaults(resolveSecretStoreRoot(options.processEnv));
359
+ }
360
+
231
361
  // src/services/writes.ts
232
362
  function setNestedValue(target, pathSegments, value) {
233
363
  const [head, ...tail] = pathSegments;
@@ -275,7 +405,7 @@ function isSecretReference(value) {
275
405
  }
276
406
  async function readYamlDocument(filePath) {
277
407
  try {
278
- return parseYaml(await readFile(filePath, "utf8")) ?? {};
408
+ return parseYaml(await readFile2(filePath, "utf8")) ?? {};
279
409
  } catch {
280
410
  return {};
281
411
  }
@@ -314,46 +444,45 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
314
444
  const document = await readYamlDocument(filePath);
315
445
  const parsedValue = parseScalarValue(rawValue);
316
446
  setNestedValue(document, configPath.split("."), parsedValue);
317
- await mkdir(path.dirname(filePath), { recursive: true });
318
- await writeFile(filePath, stringifyYaml(document), "utf8");
447
+ await mkdir(path2.dirname(filePath), { recursive: true });
448
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
319
449
  return {
320
450
  filePath,
321
451
  value: parsedValue
322
452
  };
323
453
  }
324
- async function createVault(vault, options = {}) {
325
- const passphrase = options.passphrase ?? resolveSecretPassphrase(vault, options.processEnv ?? process.env);
326
- if (!passphrase) {
327
- throw new Error("Vault creation requires --passphrase or CNOS_SECRET_PASSPHRASE");
328
- }
329
- const normalizedVault = vault.trim() || "default";
330
- const filePath = await createSecretVault(
331
- resolveSecretStoreRoot(options.processEnv),
332
- normalizedVault,
333
- passphrase
334
- );
335
- return {
336
- vault: normalizedVault,
337
- filePath
338
- };
339
- }
340
454
  async function setSecret(configPath, rawValue, options = {}) {
341
455
  const runtime = await createRuntimeService(options);
342
456
  const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
343
457
  const profile = options.profile ?? runtime.graph.profile;
344
458
  const filePath = resolveConfigDocumentPath(workspaceRoot, "secret", configPath, profile);
345
459
  const document = await readYamlDocument(filePath);
346
- const mode = options.mode ?? "local";
347
460
  const vault = options.vault?.trim() || "default";
461
+ const vaultDefinition = runtime.manifest.vaults[vault];
462
+ const inferredProvider = vaultDefinition?.provider ?? options.provider;
463
+ const mode = options.mode ?? (inferredProvider === "local" ? "local" : inferredProvider === "github-secrets" ? "ref" : "local");
348
464
  let reference;
349
465
  let storePath;
350
466
  if (mode === "local") {
351
- const passphrase = options.passphrase ?? resolveSecretPassphrase(vault, options.processEnv ?? process.env);
467
+ const provider = inferredProvider ?? "local";
468
+ if (provider !== "local") {
469
+ throw new Error(`Vault "${vault}" uses provider "${provider}" and cannot store local secret material`);
470
+ }
471
+ const passphrase = resolveConfiguredVaultPassphrase2(
472
+ vaultDefinition ? { provider: "local", ...vaultDefinition.passphrase ? { passphrase: vaultDefinition.passphrase } : {} } : { provider: "local" },
473
+ vault,
474
+ {
475
+ ...options.processEnv ?? process.env,
476
+ ...options.passphrase ? {
477
+ [getVaultPassphraseEnvVar(vault)]: options.passphrase
478
+ } : {}
479
+ }
480
+ ) ?? options.passphrase;
352
481
  if (!passphrase) {
353
- throw new Error(`Vault "${vault}" requires --passphrase or CNOS_SECRET_PASSPHRASE`);
482
+ throw new Error(`Vault "${vault}" requires --passphrase or its configured passphrase env var`);
354
483
  }
355
484
  const ref = configPath;
356
- storePath = await writeLocalSecret(resolveSecretStoreRoot(options.processEnv), ref, rawValue, passphrase, vault);
485
+ storePath = await writeLocalSecret(resolveSecretStoreRoot2(options.processEnv), ref, rawValue, passphrase, vault);
357
486
  reference = {
358
487
  provider: "local",
359
488
  ref,
@@ -361,13 +490,16 @@ async function setSecret(configPath, rawValue, options = {}) {
361
490
  };
362
491
  } else {
363
492
  reference = {
364
- provider: options.provider ?? (mode === "ref" ? "ref" : "remote"),
365
- ref: rawValue
493
+ provider: inferredProvider ?? (mode === "ref" ? "ref" : "remote"),
494
+ ref: rawValue || configPath.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(),
495
+ ...vaultDefinition || vault !== "default" ? {
496
+ vault
497
+ } : {}
366
498
  };
367
499
  }
368
500
  setNestedValue(document, configPath.split("."), reference);
369
- await mkdir(path.dirname(filePath), { recursive: true });
370
- await writeFile(filePath, stringifyYaml(document), "utf8");
501
+ await mkdir(path2.dirname(filePath), { recursive: true });
502
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
371
503
  return {
372
504
  filePath,
373
505
  provider: reference.provider,
@@ -390,18 +522,18 @@ async function deleteSecret(configPath, options = {}) {
390
522
  deleted: false
391
523
  };
392
524
  }
393
- await writeFile(filePath, stringifyYaml(document), "utf8");
525
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
394
526
  let removedStore;
395
527
  const secretRef = metadata?.secretRef;
396
528
  if (isSecretReference(secretRef) && secretRef.provider === "local") {
397
- const storePath = path.join(
398
- resolveSecretStoreRoot(options.processEnv),
529
+ const storePath = path2.join(
530
+ resolveSecretStoreRoot2(options.processEnv),
399
531
  "vaults",
400
532
  secretRef.vault ?? "default",
401
533
  "store",
402
534
  ...secretRef.ref.split("/")
403
535
  ).concat(".json");
404
- await rm(storePath, { force: true });
536
+ await rm2(storePath, { force: true });
405
537
  removedStore = storePath;
406
538
  }
407
539
  return {
@@ -426,7 +558,7 @@ async function deleteValue(namespace, configPath, options = {}) {
426
558
  deleted: false
427
559
  };
428
560
  }
429
- await writeFile(filePath, stringifyYaml(document), "utf8");
561
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
430
562
  return {
431
563
  filePath,
432
564
  deleted: true
@@ -502,8 +634,8 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
502
634
  }
503
635
 
504
636
  // src/services/doctor.ts
505
- import { readFile as readFile2 } from "fs/promises";
506
- import path2 from "path";
637
+ import { readFile as readFile3 } from "fs/promises";
638
+ import path3 from "path";
507
639
 
508
640
  // src/services/validation.ts
509
641
  import { validateRuntime } from "@kitsy/cnos/internal";
@@ -518,7 +650,7 @@ async function createValidationSummary(options = {}) {
518
650
 
519
651
  // src/services/doctor.ts
520
652
  async function checkGitignore(root) {
521
- const gitignorePath = path2.join(root, ".gitignore");
653
+ const gitignorePath = path3.join(root, ".gitignore");
522
654
  const expected = [
523
655
  ".cnos/env/.env",
524
656
  ".cnos/env/.env.*",
@@ -530,7 +662,7 @@ async function checkGitignore(root) {
530
662
  "!.cnos/workspaces/*/env/.env.*.example"
531
663
  ];
532
664
  try {
533
- const content = await readFile2(gitignorePath, "utf8");
665
+ const content = await readFile3(gitignorePath, "utf8");
534
666
  const missing = expected.filter((entry) => !content.includes(entry));
535
667
  return {
536
668
  name: "gitignore",
@@ -549,7 +681,7 @@ function issueSummary(issues) {
549
681
  return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
550
682
  }
551
683
  async function evaluateDoctor(options = {}) {
552
- const root = path2.resolve(options.root ?? process.cwd());
684
+ const root = path3.resolve(options.root ?? process.cwd());
553
685
  const { runtime, summary } = await createValidationSummary(options);
554
686
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
555
687
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
@@ -620,11 +752,17 @@ async function runDump(options = {}) {
620
752
  }
621
753
 
622
754
  // src/commands/exportEnv.ts
755
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
756
+ import path4 from "path";
757
+ function formatEnvOutput(env) {
758
+ return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
759
+ }
623
760
  async function runExportEnv(options = {}) {
624
761
  const cliArgs = [...options.cliArgs ?? []];
625
762
  const isPublic = consumeFlag(cliArgs, "--public");
626
763
  const framework = consumeOption(cliArgs, "--framework");
627
764
  const prefix = consumeOption(cliArgs, "--prefix");
765
+ const to = consumeOption(cliArgs, "--to");
628
766
  const runtime = await createRuntimeService({
629
767
  ...options,
630
768
  cliArgs
@@ -632,16 +770,26 @@ async function runExportEnv(options = {}) {
632
770
  const env = isPublic ? runtime.toPublicEnv({
633
771
  ...framework ? { framework } : {},
634
772
  ...prefix ? { prefix } : {}
635
- }) : Object.fromEntries(
636
- Object.entries(runtime.manifest.envMapping.explicit).map(([envVar, logicalKey]) => [envVar, runtime.read(logicalKey)]).filter((entry) => entry[1] !== void 0).map(([envVar, value]) => [
637
- envVar,
638
- typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? String(value) : JSON.stringify(value)
639
- ])
640
- );
773
+ }) : runtime.toEnv();
774
+ const output = formatEnvOutput(env);
775
+ if (to) {
776
+ const targetPath = path4.resolve(options.root ?? process.cwd(), to);
777
+ await mkdir2(path4.dirname(targetPath), { recursive: true });
778
+ await writeFile3(targetPath, output, "utf8");
779
+ if (options.json) {
780
+ return printJson({
781
+ to: targetPath,
782
+ count: Object.keys(env).length,
783
+ public: isPublic,
784
+ ...framework ? { framework } : {}
785
+ });
786
+ }
787
+ return `Wrote ${Object.keys(env).length} env vars to ${targetPath}`;
788
+ }
641
789
  if (options.json) {
642
790
  return printJson(env);
643
791
  }
644
- return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
792
+ return output;
645
793
  }
646
794
 
647
795
  // src/commands/export.ts
@@ -798,15 +946,70 @@ var COMMANDS = [
798
946
  {
799
947
  flag: "--passphrase <value>",
800
948
  description: "Passphrase used to encrypt local secret material when --local is selected."
949
+ },
950
+ {
951
+ flag: "--vault <name>",
952
+ description: "Use a manifest-defined vault. Provider behavior is inferred from the vault definition."
801
953
  }
802
954
  ],
803
955
  examples: [
804
956
  "cnos secret app.token",
805
- "cnos secret set app.token super-secret --local --passphrase dev-pass",
806
- "cnos add secret app.token APP_TOKEN --ref --provider env",
807
- "cnos secret list --workspace agents"
957
+ "cnos vault create local-dev --passphrase dev-pass",
958
+ "cnos secret set app.token super-secret --vault local-dev",
959
+ "cnos vault create github-ci --provider github-secrets --no-passphrase",
960
+ "cnos secret set app.token APP_TOKEN --vault github-ci"
808
961
  ]
809
962
  },
963
+ {
964
+ id: "vault",
965
+ summary: "Manage manifest-defined secret vaults.",
966
+ usage: "cnos vault [create <name> | list | remove <name>] [options] [global-options]",
967
+ description: "Creates, lists, and removes vault definitions in .cnos/cnos.yml. Local vaults use encrypted material under ~/.cnos/secrets, while github-secrets vaults resolve from process.env in CI.",
968
+ options: [
969
+ {
970
+ flag: "--provider <local|github-secrets>",
971
+ description: "Vault provider. Defaults to local."
972
+ },
973
+ {
974
+ flag: "--passphrase <value>",
975
+ description: "Required for local vault creation unless already available in the configured passphrase env var."
976
+ },
977
+ {
978
+ flag: "--no-passphrase",
979
+ description: "Allowed for passwordless providers such as github-secrets."
980
+ }
981
+ ],
982
+ examples: [
983
+ "cnos vault create local-dev --passphrase dev-pass",
984
+ "cnos vault create github-ci --provider github-secrets --no-passphrase",
985
+ "cnos vault list",
986
+ "cnos vault remove local-dev"
987
+ ]
988
+ },
989
+ {
990
+ id: "vault create",
991
+ summary: "Create a manifest-defined vault.",
992
+ usage: "cnos vault create <name> [--provider <local|github-secrets>] [--passphrase <value>] [--no-passphrase] [global-options]",
993
+ description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets.",
994
+ examples: [
995
+ "cnos vault create local-dev --passphrase dev-pass",
996
+ "cnos vault create github-ci --provider github-secrets --no-passphrase"
997
+ ]
998
+ },
999
+ {
1000
+ id: "vault list",
1001
+ summary: "List manifest-defined vaults.",
1002
+ usage: "cnos vault list [global-options]",
1003
+ description: "Lists vault definitions together with provider and passphrase policy.",
1004
+ examples: ["cnos vault list"]
1005
+ },
1006
+ {
1007
+ id: "vault remove",
1008
+ summary: "Remove a vault definition.",
1009
+ usage: "cnos vault remove <name> [global-options]",
1010
+ description: "Removes the vault from .cnos/cnos.yml and deletes local vault store metadata when present.",
1011
+ examples: ["cnos vault remove local-dev"]
1012
+ },
810
1013
  {
811
1014
  id: "define",
812
1015
  summary: "Write a value or secret into the selected workspace.",
@@ -850,7 +1053,7 @@ var COMMANDS = [
850
1053
  {
851
1054
  id: "list",
852
1055
  summary: "List resolved config entries.",
853
- usage: "cnos list [value|secret|meta|env|public|all] [--prefix <path>] [global-options]",
1056
+ usage: "cnos list [value|secret|meta|env|public|all] [--prefix <path>] [--framework <name>] [global-options]",
854
1057
  description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering.",
855
1058
  options: [
856
1059
  {
@@ -860,9 +1063,13 @@ var COMMANDS = [
860
1063
  {
861
1064
  flag: "--prefix <path>",
862
1065
  description: "Filter list output to entries whose logical keys begin with this prefix."
1066
+ },
1067
+ {
1068
+ flag: "--framework <name>",
1069
+ description: "When listing public output, apply framework-specific prefixes such as vite or next."
863
1070
  }
864
1071
  ],
865
- examples: ["cnos list", "cnos list value --prefix app.", "cnos list env", "cnos list --namespace secret"]
1072
+ examples: ["cnos list", "cnos list value --prefix app.", "cnos list env", "cnos list public --framework vite"]
866
1073
  },
867
1074
  {
868
1075
  id: "profile",
@@ -909,29 +1116,51 @@ var COMMANDS = [
909
1116
  description: "Deletes .cnos/profiles/<name>.yml.",
910
1117
  examples: ["cnos profile delete stage"]
911
1118
  },
1119
+ {
1120
+ id: "promote",
1121
+ summary: "Promote shareable config into public or env projection surfaces.",
1122
+ usage: "cnos promote <key...> --to <public|env> [--as <ENV_VAR>] [global-options]",
1123
+ description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected.",
1124
+ options: [
1125
+ {
1126
+ flag: "--to <public|env>",
1127
+ description: "Choose whether the keys are promoted to the public surface or env export surface."
1128
+ },
1129
+ {
1130
+ flag: "--as <ENV_VAR>",
1131
+ description: "Required for --to env. Sets the exported env var name for the promoted key."
1132
+ }
1133
+ ],
1134
+ examples: [
1135
+ "cnos promote value.flag.auth.upi_enabled --to public",
1136
+ "cnos promote value.server.port --to env --as PORT"
1137
+ ]
1138
+ },
912
1139
  {
913
1140
  id: "secret set",
914
1141
  summary: "Write a secret securely.",
915
1142
  usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [--passphrase <value>] [global-options]",
916
- description: "Writes a secret reference into the repo and, when --local is used, stores encrypted secret material outside the repo under ~/.cnos/secrets/vaults/<vault>.",
1143
+ description: "Writes a secret reference into the repo. When a local vault is selected, CNOS stores encrypted secret material outside the repo under ~/.cnos/secrets/vaults/<vault>; when a github-secrets vault is selected, CNOS writes a CI env-backed ref.",
917
1144
  examples: [
918
- "cnos secret create vault db --passphrase dev-pass",
919
- "cnos secret set app.token super-secret --local --vault db --passphrase dev-pass"
1145
+ "cnos vault create db --passphrase dev-pass",
1146
+ "cnos secret set app.token super-secret --vault db",
1147
+ "cnos vault create github-ci --provider github-secrets --no-passphrase",
1148
+ "cnos secret set app.token APP_TOKEN --vault github-ci"
920
1149
  ]
921
1150
  },
922
1151
  {
923
1152
  id: "secret create vault",
924
1153
  summary: "Create a local secret vault.",
925
1154
  usage: "cnos secret create vault <name> --passphrase <value> [global-options]",
926
- description: "Creates a named local secret vault under ~/.cnos/secrets/vaults.",
1155
+ description: "Alias for cnos vault create <name>.",
927
1156
  examples: ["cnos secret create vault db --passphrase dev-pass"]
928
1157
  },
929
1158
  {
930
1159
  id: "secret list",
931
1160
  summary: "List resolved secrets.",
932
- usage: "cnos secret list [global-options]",
933
- description: "Lists resolved secret keys for the selected workspace and profile.",
934
- examples: ["cnos secret list --workspace api"]
1161
+ usage: "cnos secret list [--vault <name>] [--provider <name>] [global-options]",
1162
+ description: "Lists stored secret entries for the selected workspace and profile, optionally filtered by vault or provider.",
1163
+ examples: ["cnos secret list --workspace api", "cnos secret list --vault github-ci"]
935
1164
  },
936
1165
  {
937
1166
  id: "secret delete",
@@ -982,7 +1211,7 @@ var COMMANDS = [
982
1211
  {
983
1212
  id: "export env",
984
1213
  summary: "Render environment variables for the selected workspace.",
985
- usage: "cnos export env [--public] [--framework <name>] [--prefix <prefix>] [global-options]",
1214
+ usage: "cnos export env [--public] [--framework <name>] [--prefix <prefix>] [--to <path>] [global-options]",
986
1215
  description: "Exports the effective environment as KEY=VALUE lines, or only promoted public values when --public is set.",
987
1216
  options: [
988
1217
  {
@@ -996,9 +1225,17 @@ var COMMANDS = [
996
1225
  {
997
1226
  flag: "--prefix <prefix>",
998
1227
  description: "Override the generated public env prefix."
1228
+ },
1229
+ {
1230
+ flag: "--to <path>",
1231
+ description: "Write the rendered KEY=VALUE output to a file instead of stdout."
999
1232
  }
1000
1233
  ],
1001
- examples: ["cnos export env", "cnos export env --public --framework vite --workspace api"]
1234
+ examples: [
1235
+ "cnos export env",
1236
+ "cnos export env --to .env.local",
1237
+ "cnos export env --public --framework vite --to .env.local --workspace api"
1238
+ ]
1002
1239
  },
1003
1240
  {
1004
1241
  id: "dump",
@@ -1020,9 +1257,32 @@ var COMMANDS = [
1020
1257
  {
1021
1258
  id: "run",
1022
1259
  summary: "Run a child process with CNOS env injected.",
1023
- usage: "cnos run [global-options] -- <command...>",
1024
- description: "Resolves the active workspace and profile, injects runtime env variables, and executes the command after --.",
1025
- examples: ["cnos run -- node server.js", "cnos run --workspace api -- pnpm dev"]
1260
+ usage: "cnos run [--public] [--framework <name>] [--set <logical-key=value>] [global-options] -- <command...>",
1261
+ description: "Resolves the active workspace and profile, injects runtime env variables, bootstraps __CNOS_GRAPH__ for singleton runtime reads, and executes the command after --.",
1262
+ options: [
1263
+ {
1264
+ flag: "--set <logical-key=value>",
1265
+ description: "Apply inline logical-key overrides for this run without touching repo config files."
1266
+ },
1267
+ {
1268
+ flag: "--public",
1269
+ description: "Inject only promoted public env variables into the child process."
1270
+ },
1271
+ {
1272
+ flag: "--framework <name>",
1273
+ description: "When used with --public, apply framework-specific prefixes such as vite or next."
1274
+ },
1275
+ {
1276
+ flag: "--prefix <prefix>",
1277
+ description: "Override the generated public env prefix for --public runs."
1278
+ }
1279
+ ],
1280
+ examples: [
1281
+ "cnos run -- node server.js",
1282
+ "cnos run --profile stage -- node server.js",
1283
+ "cnos run --set value.server.port=9999 -- node server.js",
1284
+ "cnos run --public --framework vite -- pnpm build"
1285
+ ]
1026
1286
  },
1027
1287
  {
1028
1288
  id: "diff",
@@ -1095,17 +1355,25 @@ var INTEGRATIONS = [
1095
1355
  id: "vite",
1096
1356
  packageName: "@kitsy/cnos-vite",
1097
1357
  entrypoint: "@kitsy/cnos-vite",
1098
- summary: "Inject CNOS public env into Vite define replacements and import.meta.env.",
1358
+ summary: "Inject CNOS public env into Vite and embed browser-readable CNOS public data.",
1099
1359
  usage: 'import { createCnosVitePlugin } from "@kitsy/cnos-vite"',
1100
- examples: ["cnos export env --public --framework vite", "vite.config.ts -> plugins: [createCnosVitePlugin()]"]
1360
+ examples: [
1361
+ "cnos export env --public --framework vite",
1362
+ "vite.config.ts -> plugins: [createCnosVitePlugin()]",
1363
+ 'browser code -> import cnos from "@kitsy/cnos/browser"'
1364
+ ]
1101
1365
  },
1102
1366
  {
1103
1367
  id: "next",
1104
1368
  packageName: "@kitsy/cnos-next",
1105
1369
  entrypoint: "@kitsy/cnos-next",
1106
- summary: "Merge CNOS public env into next.config.* using the NEXT_PUBLIC_ convention.",
1370
+ summary: "Merge CNOS public env into Next and embed browser-readable CNOS public data.",
1107
1371
  usage: 'import { withCnosNext } from "@kitsy/cnos-next"',
1108
- examples: ["cnos export env --public --framework next", "next.config.mjs -> export default withCnosNext({})"]
1372
+ examples: [
1373
+ "cnos export env --public --framework next",
1374
+ "next.config.mjs -> export default withCnosNext({})",
1375
+ 'browser code -> import cnos from "@kitsy/cnos/browser"'
1376
+ ]
1109
1377
  }
1110
1378
  ];
1111
1379
  var HELP_DOCUMENT = {
@@ -1232,11 +1500,11 @@ function runHelpAi(topic, cliArgs = []) {
1232
1500
  }
1233
1501
 
1234
1502
  // src/commands/init.ts
1235
- import path4 from "path";
1503
+ import path6 from "path";
1236
1504
 
1237
1505
  // src/services/scaffold.ts
1238
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1239
- import path3 from "path";
1506
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1507
+ import path5 from "path";
1240
1508
  function scaffoldManifest(projectName, workspace) {
1241
1509
  const lines = [
1242
1510
  "version: 1",
@@ -1267,15 +1535,15 @@ function scaffoldManifest(projectName, workspace) {
1267
1535
  }
1268
1536
  async function ensureFile(filePath, content) {
1269
1537
  try {
1270
- await readFile3(filePath, "utf8");
1538
+ await readFile4(filePath, "utf8");
1271
1539
  return false;
1272
1540
  } catch {
1273
- await writeFile2(filePath, content, "utf8");
1541
+ await writeFile4(filePath, content, "utf8");
1274
1542
  return true;
1275
1543
  }
1276
1544
  }
1277
1545
  async function ensureGitignore(root) {
1278
- const gitignorePath = path3.join(root, ".gitignore");
1546
+ const gitignorePath = path5.join(root, ".gitignore");
1279
1547
  const requiredEntries = [
1280
1548
  ".cnos/env/.env",
1281
1549
  ".cnos/env/.env.*",
@@ -1288,7 +1556,7 @@ async function ensureGitignore(root) {
1288
1556
  ];
1289
1557
  let current = "";
1290
1558
  try {
1291
- current = await readFile3(gitignorePath, "utf8");
1559
+ current = await readFile4(gitignorePath, "utf8");
1292
1560
  } catch {
1293
1561
  current = "";
1294
1562
  }
@@ -1298,18 +1566,18 @@ async function ensureGitignore(root) {
1298
1566
  }
1299
1567
  const prefix = current.trim().length > 0 ? `${current.trimEnd()}
1300
1568
  ` : "";
1301
- await writeFile2(gitignorePath, `${prefix}${missingEntries.join("\n")}
1569
+ await writeFile4(gitignorePath, `${prefix}${missingEntries.join("\n")}
1302
1570
  `, "utf8");
1303
1571
  return true;
1304
1572
  }
1305
1573
  async function scaffoldWorkspace(root, workspace) {
1306
- const cnosRoot = path3.join(root, ".cnos");
1307
- const workspaceRoot = workspace ? path3.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1574
+ const cnosRoot = path5.join(root, ".cnos");
1575
+ const workspaceRoot = workspace ? path5.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1308
1576
  const createdPaths = [];
1309
- await mkdir2(path3.join(workspaceRoot, "profiles"), { recursive: true });
1310
- await mkdir2(path3.join(workspaceRoot, "values"), { recursive: true });
1311
- await mkdir2(path3.join(workspaceRoot, "secrets"), { recursive: true });
1312
- await mkdir2(path3.join(workspaceRoot, "env"), { recursive: true });
1577
+ await mkdir3(path5.join(workspaceRoot, "profiles"), { recursive: true });
1578
+ await mkdir3(path5.join(workspaceRoot, "values"), { recursive: true });
1579
+ await mkdir3(path5.join(workspaceRoot, "secrets"), { recursive: true });
1580
+ await mkdir3(path5.join(workspaceRoot, "env"), { recursive: true });
1313
1581
  const relativePaths = workspace ? [
1314
1582
  ["workspaces", workspace, "profiles", ".gitkeep"],
1315
1583
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -1322,15 +1590,15 @@ async function scaffoldWorkspace(root, workspace) {
1322
1590
  ["env", ".gitkeep"]
1323
1591
  ];
1324
1592
  for (const relativePath of relativePaths) {
1325
- const filePath = path3.join(cnosRoot, ...relativePath);
1593
+ const filePath = path5.join(cnosRoot, ...relativePath);
1326
1594
  if (await ensureFile(filePath, "")) {
1327
- createdPaths.push(path3.relative(root, filePath).replace(/\\/g, "/"));
1595
+ createdPaths.push(path5.relative(root, filePath).replace(/\\/g, "/"));
1328
1596
  }
1329
1597
  }
1330
- if (await ensureFile(path3.join(cnosRoot, "cnos.yml"), scaffoldManifest(path3.basename(root), workspace))) {
1598
+ if (await ensureFile(path5.join(cnosRoot, "cnos.yml"), scaffoldManifest(path5.basename(root), workspace))) {
1331
1599
  createdPaths.push(".cnos/cnos.yml");
1332
1600
  }
1333
- if (workspace && await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1601
+ if (workspace && await ensureFile(path5.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1334
1602
  globalRoot: ~/.cnos
1335
1603
  `)) {
1336
1604
  createdPaths.push(".cnos-workspace.yml");
@@ -1347,7 +1615,7 @@ globalRoot: ~/.cnos
1347
1615
 
1348
1616
  // src/commands/init.ts
1349
1617
  async function runInit(options = {}) {
1350
- const root = path4.resolve(options.root ?? process.cwd());
1618
+ const root = path6.resolve(options.root ?? process.cwd());
1351
1619
  const result = await scaffoldWorkspace(root, options.workspace);
1352
1620
  if (options.json) {
1353
1621
  return printJson(result);
@@ -1406,34 +1674,52 @@ function printValue(value, json = false) {
1406
1674
 
1407
1675
  // src/services/listing.ts
1408
1676
  import { flattenObject as flattenObject2 } from "@kitsy/cnos/internal";
1677
+ function matchesSecretFilter(candidate, filter) {
1678
+ const secretRef = candidate.metadata?.secretRef;
1679
+ if (filter.vault && secretRef?.vault !== filter.vault) {
1680
+ return false;
1681
+ }
1682
+ if (filter.provider && secretRef?.provider !== filter.provider) {
1683
+ return false;
1684
+ }
1685
+ return true;
1686
+ }
1409
1687
  function matchesPrefix(key, prefix) {
1410
1688
  if (!prefix) {
1411
1689
  return true;
1412
1690
  }
1413
1691
  return key.startsWith(prefix) || key.split(".").slice(1).join(".").startsWith(prefix);
1414
1692
  }
1415
- function toStoredEntry(namespace, entry) {
1693
+ function toStoredEntry(namespace, entry, filter = {}) {
1416
1694
  const sourceId = namespace === "value" ? "filesystem-values" : "filesystem-secrets";
1417
1695
  const candidates = [entry.winner, ...entry.overridden].filter((candidate) => candidate.sourceId === sourceId);
1418
1696
  if (candidates.length === 0) {
1419
1697
  return void 0;
1420
1698
  }
1699
+ const selectedCandidate = namespace === "secret" ? candidates.find((candidate) => matchesSecretFilter(candidate, filter)) : candidates[0];
1700
+ if (!selectedCandidate) {
1701
+ return void 0;
1702
+ }
1421
1703
  return {
1422
1704
  key: entry.key,
1423
- value: candidates[0]?.value
1705
+ value: selectedCandidate.value
1424
1706
  };
1425
1707
  }
1426
1708
  function listStoredNamespace(namespace, options) {
1427
1709
  return createRuntimeService(options).then(
1428
- (runtime) => Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === namespace).map((entry) => toStoredEntry(namespace, entry)).filter((entry) => Boolean(entry)).filter((entry) => matchesPrefix(entry.key, options.prefix)).sort((left, right) => left.key.localeCompare(right.key))
1710
+ (runtime) => Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === namespace).map((entry) => toStoredEntry(namespace, entry, options)).filter((entry) => Boolean(entry)).filter((entry) => matchesPrefix(entry.key, options.prefix)).sort((left, right) => left.key.localeCompare(right.key))
1429
1711
  );
1430
1712
  }
1431
1713
  function listProjectedNamespace(namespace, options) {
1432
1714
  return createRuntimeService(options).then((runtime) => {
1433
- const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.manifest.envMapping.explicit : runtime.toPublicEnv();
1434
- const entries = namespace === "env" ? Object.entries(projected).map(([envVar, logicalKey]) => ({
1715
+ const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : runtime.toPublicEnv({
1716
+ ...options.framework ? {
1717
+ framework: options.framework
1718
+ } : {}
1719
+ });
1720
+ const entries = namespace === "env" ? Object.entries(projected).map(([envVar, value]) => ({
1435
1721
  key: envVar,
1436
- value: runtime.read(logicalKey)
1722
+ value
1437
1723
  })) : Object.entries(projected).map(([key, value]) => ({
1438
1724
  key: namespace === "meta" ? `meta.${key}` : key,
1439
1725
  value
@@ -1482,10 +1768,12 @@ async function runList(args = [], options = {}) {
1482
1768
  const cliArgs = [...options.cliArgs ?? []];
1483
1769
  const namespace = normalizeNamespace(args[0] ?? consumeOption(cliArgs, "--namespace"));
1484
1770
  const prefix = consumeOption(cliArgs, "--prefix");
1771
+ const framework = consumeOption(cliArgs, "--framework");
1485
1772
  const entries = await listConfigEntries(namespace, {
1486
1773
  ...options,
1487
1774
  cliArgs,
1488
- ...prefix ? { prefix } : {}
1775
+ ...prefix ? { prefix } : {},
1776
+ ...framework ? { framework } : {}
1489
1777
  });
1490
1778
  if (options.json) {
1491
1779
  return printJson(entries);
@@ -1497,34 +1785,34 @@ async function runList(args = [], options = {}) {
1497
1785
  }
1498
1786
 
1499
1787
  // src/commands/onboard.ts
1500
- import { copyFile, readdir, rm as rm2 } from "fs/promises";
1501
- import path5 from "path";
1788
+ import { copyFile, readdir, rm as rm3 } from "fs/promises";
1789
+ import path7 from "path";
1502
1790
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
1503
1791
  async function listRootEnvFiles(root) {
1504
1792
  const entries = await readdir(root, { withFileTypes: true });
1505
1793
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
1506
1794
  }
1507
1795
  async function runOnboard(options = {}) {
1508
- const root = path5.resolve(options.root ?? process.cwd());
1509
- const workspace = options.workspace ?? path5.basename(root);
1796
+ const root = path7.resolve(options.root ?? process.cwd());
1797
+ const workspace = options.workspace ?? path7.basename(root);
1510
1798
  const cliArgs = [...options.cliArgs ?? []];
1511
1799
  const move = consumeFlag(cliArgs, "--move");
1512
1800
  if (cliArgs.length > 0) {
1513
1801
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
1514
1802
  }
1515
1803
  const scaffold = await scaffoldWorkspace(root, workspace);
1516
- const envRoot = path5.join(root, ".cnos", "workspaces", workspace, "env");
1804
+ const envRoot = path7.join(root, ".cnos", "workspaces", workspace, "env");
1517
1805
  const rootFiles = await listRootEnvFiles(root);
1518
1806
  const imported = [];
1519
1807
  const skipped = [];
1520
1808
  for (const fileName of rootFiles) {
1521
- const sourcePath = path5.join(root, fileName);
1522
- const targetPath = path5.join(envRoot, fileName);
1809
+ const sourcePath = path7.join(root, fileName);
1810
+ const targetPath = path7.join(envRoot, fileName);
1523
1811
  try {
1524
1812
  await copyFile(sourcePath, targetPath);
1525
- imported.push(path5.relative(root, targetPath).replace(/\\/g, "/"));
1813
+ imported.push(path7.relative(root, targetPath).replace(/\\/g, "/"));
1526
1814
  if (move) {
1527
- await rm2(sourcePath);
1815
+ await rm3(sourcePath);
1528
1816
  }
1529
1817
  } catch {
1530
1818
  skipped.push(fileName);
@@ -1547,16 +1835,16 @@ async function runOnboard(options = {}) {
1547
1835
  }
1548
1836
 
1549
1837
  // src/commands/profile.ts
1550
- import path8 from "path";
1838
+ import path10 from "path";
1551
1839
 
1552
1840
  // src/services/context.ts
1553
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1554
- import path6 from "path";
1555
- import { parseYaml as parseYaml2, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
1841
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
1842
+ import path8 from "path";
1843
+ import { parseYaml as parseYaml2, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
1556
1844
  async function loadCliContext(root = process.cwd()) {
1557
- const filePath = path6.join(path6.resolve(root), ".cnos-workspace.yml");
1845
+ const filePath = path8.join(path8.resolve(root), ".cnos-workspace.yml");
1558
1846
  try {
1559
- const source = await readFile4(filePath, "utf8");
1847
+ const source = await readFile5(filePath, "utf8");
1560
1848
  const parsed = parseYaml2(source);
1561
1849
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1562
1850
  return {};
@@ -1567,8 +1855,8 @@ async function loadCliContext(root = process.cwd()) {
1567
1855
  }
1568
1856
  }
1569
1857
  async function saveCliContext(options = {}) {
1570
- const root = path6.resolve(options.root ?? process.cwd());
1571
- const filePath = path6.join(root, ".cnos-workspace.yml");
1858
+ const root = path8.resolve(options.root ?? process.cwd());
1859
+ const filePath = path8.join(root, ".cnos-workspace.yml");
1572
1860
  const current = await loadCliContext(root);
1573
1861
  const next = {
1574
1862
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -1578,7 +1866,7 @@ async function saveCliContext(options = {}) {
1578
1866
  ...options.profile ? { profile: options.profile } : {},
1579
1867
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1580
1868
  };
1581
- await writeFile3(filePath, stringifyYaml2(next), "utf8");
1869
+ await writeFile5(filePath, stringifyYaml3(next), "utf8");
1582
1870
  return {
1583
1871
  filePath,
1584
1872
  context: next
@@ -1586,19 +1874,19 @@ async function saveCliContext(options = {}) {
1586
1874
  }
1587
1875
 
1588
1876
  // src/services/profiles.ts
1589
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
1590
- import path7 from "path";
1591
- import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
1877
+ import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, rm as rm4, writeFile as writeFile6 } from "fs/promises";
1878
+ import path9 from "path";
1879
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
1592
1880
  async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1593
- const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1594
- await mkdir3(path7.dirname(filePath), { recursive: true });
1881
+ const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1882
+ await mkdir4(path9.dirname(filePath), { recursive: true });
1595
1883
  const document = inherit && inherit !== "base" ? {
1596
1884
  name: profile,
1597
1885
  extends: [inherit]
1598
1886
  } : {
1599
1887
  name: profile
1600
1888
  };
1601
- await writeFile4(filePath, stringifyYaml3(document), "utf8");
1889
+ await writeFile6(filePath, stringifyYaml4(document), "utf8");
1602
1890
  return {
1603
1891
  filePath,
1604
1892
  profile,
@@ -1606,7 +1894,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1606
1894
  };
1607
1895
  }
1608
1896
  async function listProfiles(root = process.cwd()) {
1609
- const profilesRoot = path7.join(path7.resolve(root), ".cnos", "profiles");
1897
+ const profilesRoot = path9.join(path9.resolve(root), ".cnos", "profiles");
1610
1898
  try {
1611
1899
  const entries = await readdir2(profilesRoot, { withFileTypes: true });
1612
1900
  const discovered = /* @__PURE__ */ new Set(["base"]);
@@ -1621,9 +1909,9 @@ async function listProfiles(root = process.cwd()) {
1621
1909
  }
1622
1910
  }
1623
1911
  async function deleteProfileDefinition(root = process.cwd(), profile) {
1624
- const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1912
+ const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1625
1913
  try {
1626
- await rm3(filePath);
1914
+ await rm4(filePath);
1627
1915
  return {
1628
1916
  filePath,
1629
1917
  deleted: true
@@ -1641,9 +1929,9 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
1641
1929
  name: "base"
1642
1930
  };
1643
1931
  }
1644
- const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1932
+ const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1645
1933
  try {
1646
- return parseYaml3(await readFile5(filePath, "utf8")) ?? void 0;
1934
+ return parseYaml3(await readFile6(filePath, "utf8")) ?? void 0;
1647
1935
  } catch {
1648
1936
  return void 0;
1649
1937
  }
@@ -1665,7 +1953,7 @@ function normalizeProfileAction(args) {
1665
1953
  }
1666
1954
  async function runProfile(args, options = {}) {
1667
1955
  const { action, tail } = normalizeProfileAction(args);
1668
- const root = path8.resolve(options.root ?? process.cwd());
1956
+ const root = path10.resolve(options.root ?? process.cwd());
1669
1957
  const cliArgs = [...options.cliArgs ?? []];
1670
1958
  if (action === "create") {
1671
1959
  const profile = tail[0] ?? "stage";
@@ -1708,6 +1996,73 @@ async function runProfile(args, options = {}) {
1708
1996
  return profiles.join("\n");
1709
1997
  }
1710
1998
 
1999
+ // src/commands/promote.ts
2000
+ import { writeFile as writeFile7 } from "fs/promises";
2001
+ import {
2002
+ ensureProjectionAllowed,
2003
+ loadManifest as loadManifest2,
2004
+ stringifyYaml as stringifyYaml5
2005
+ } from "@kitsy/cnos/internal";
2006
+ function normalizeTarget(value) {
2007
+ if (value === "public" || value === "env") {
2008
+ return value;
2009
+ }
2010
+ throw new Error("promote requires --to public|env");
2011
+ }
2012
+ function sortRecord(record) {
2013
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
2014
+ }
2015
+ async function runPromote(args = [], options = {}) {
2016
+ const cliArgs = [...options.cliArgs ?? []];
2017
+ const target = normalizeTarget(consumeOption(cliArgs, "--to"));
2018
+ const alias = consumeOption(cliArgs, "--as");
2019
+ const keys = args.filter(Boolean);
2020
+ if (keys.length === 0) {
2021
+ throw new Error("promote requires at least one logical key");
2022
+ }
2023
+ if (target === "env") {
2024
+ if (keys.length !== 1) {
2025
+ throw new Error("promote --to env requires exactly one logical key");
2026
+ }
2027
+ if (!alias) {
2028
+ throw new Error("promote --to env requires --as <ENV_VAR>");
2029
+ }
2030
+ }
2031
+ const loadedManifest = await loadManifest2(options.root ? { root: options.root } : {});
2032
+ for (const key of keys) {
2033
+ ensureProjectionAllowed(loadedManifest.manifest, key, target);
2034
+ }
2035
+ const rawManifest = {
2036
+ ...loadedManifest.rawManifest
2037
+ };
2038
+ if (target === "public") {
2039
+ rawManifest.public = {
2040
+ ...rawManifest.public ?? {},
2041
+ promote: Array.from(/* @__PURE__ */ new Set([...rawManifest.public?.promote ?? [], ...keys])).sort(
2042
+ (left, right) => left.localeCompare(right)
2043
+ )
2044
+ };
2045
+ } else {
2046
+ rawManifest.envMapping = {
2047
+ ...rawManifest.envMapping ?? {},
2048
+ explicit: sortRecord({
2049
+ ...rawManifest.envMapping?.explicit ?? {},
2050
+ [alias]: keys[0]
2051
+ })
2052
+ };
2053
+ }
2054
+ await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2055
+ if (options.json) {
2056
+ return printJson({
2057
+ target,
2058
+ keys,
2059
+ ...target === "env" ? { envVar: alias } : {},
2060
+ manifestPath: loadedManifest.manifestPath
2061
+ });
2062
+ }
2063
+ return target === "public" ? `promoted ${keys.join(", ")} to public in ${loadedManifest.manifestPath}` : `promoted ${keys[0]} to env as ${alias} in ${loadedManifest.manifestPath}`;
2064
+ }
2065
+
1711
2066
  // src/commands/read.ts
1712
2067
  async function runRead(key, options = {}) {
1713
2068
  const runtime = await createRuntimeService(options);
@@ -1723,14 +2078,60 @@ async function runRead(key, options = {}) {
1723
2078
 
1724
2079
  // src/commands/run.ts
1725
2080
  import { spawn } from "child_process";
2081
+ import {
2082
+ CNOS_GRAPH_ENV_VAR,
2083
+ serializeRuntimeGraph
2084
+ } from "@kitsy/cnos/internal";
2085
+ function consumeOptions(args, flag) {
2086
+ const values = [];
2087
+ for (let index = 0; index < args.length; ) {
2088
+ const token = args[index];
2089
+ if (token === flag) {
2090
+ const value = args[index + 1];
2091
+ if (!value) {
2092
+ throw new Error(`Missing value for ${flag}`);
2093
+ }
2094
+ values.push(value);
2095
+ args.splice(index, 2);
2096
+ continue;
2097
+ }
2098
+ if (token?.startsWith(`${flag}=`)) {
2099
+ values.push(token.slice(flag.length + 1));
2100
+ args.splice(index, 1);
2101
+ continue;
2102
+ }
2103
+ index += 1;
2104
+ }
2105
+ return values;
2106
+ }
2107
+ function normalizeSetOverrides(values) {
2108
+ return values.map((value) => {
2109
+ if (!value.includes("=")) {
2110
+ throw new Error("--set requires <logical-key>=<value>");
2111
+ }
2112
+ return value.startsWith("--") ? value : `--${value}`;
2113
+ });
2114
+ }
1726
2115
  async function runCommand(command, options = {}) {
1727
2116
  if (command.length === 0) {
1728
2117
  throw new Error("run requires a command after --");
1729
2118
  }
1730
- const runtime = await createRuntimeService(options);
2119
+ const cliArgs = [...options.cliArgs ?? []];
2120
+ const isPublic = consumeFlag(cliArgs, "--public");
2121
+ const framework = consumeOption(cliArgs, "--framework");
2122
+ const prefix = consumeOption(cliArgs, "--prefix");
2123
+ const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
2124
+ const runtime = await createRuntimeService({
2125
+ ...options,
2126
+ cliArgs: [...cliArgs, ...setOverrides]
2127
+ });
1731
2128
  const env = {
1732
2129
  ...process.env,
1733
- ...runtime.toEnv()
2130
+ ...isPublic ? runtime.toPublicEnv({
2131
+ ...framework ? { framework } : {},
2132
+ ...prefix ? { prefix } : {}
2133
+ }) : runtime.toEnv(),
2134
+ [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
1734
2135
  };
1735
2136
  return new Promise((resolve, reject) => {
1736
2137
  const executable = command[0];
@@ -1765,6 +2166,68 @@ async function runCommand(command, options = {}) {
1765
2166
  });
1766
2167
  }
1767
2168
 
2169
+ // src/commands/vault.ts
2170
+ function normalizeVaultAction(args) {
2171
+ const [action = "list", ...tail] = args;
2172
+ if (["create", "add", "list", "delete", "remove"].includes(action)) {
2173
+ return {
2174
+ action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : "list",
2175
+ tail
2176
+ };
2177
+ }
2178
+ return {
2179
+ action: "list",
2180
+ tail: args
2181
+ };
2182
+ }
2183
+ async function runVault(args = [], options = {}) {
2184
+ const { action, tail } = normalizeVaultAction(args);
2185
+ const cliArgs = [...options.cliArgs ?? []];
2186
+ if (action === "create") {
2187
+ const name = tail[0] ?? "default";
2188
+ const provider = consumeOption(cliArgs, "--provider") ?? "local";
2189
+ const passphrase = consumeOption(cliArgs, "--passphrase");
2190
+ const noPassphrase = consumeFlag(cliArgs, "--no-passphrase");
2191
+ const result = await createVaultDefinition(name, {
2192
+ ...options,
2193
+ cliArgs,
2194
+ provider,
2195
+ ...passphrase ? { passphrase } : {},
2196
+ ...noPassphrase ? { noPassphrase: true } : {}
2197
+ });
2198
+ if (options.json) {
2199
+ return printJson(result);
2200
+ }
2201
+ return `created vault "${result.name}" with provider "${result.provider}" in ${result.manifestPath}`;
2202
+ }
2203
+ if (action === "remove") {
2204
+ const name = tail[0] ?? "default";
2205
+ const result = await removeVaultDefinition(name, options);
2206
+ if (options.json) {
2207
+ return printJson(result);
2208
+ }
2209
+ return result.deleted ? `removed vault "${result.name}"` : `vault "${result.name}" was not found`;
2210
+ }
2211
+ const [manifestVaults, localStoreVaults] = await Promise.all([
2212
+ listVaultDefinitions(options),
2213
+ listLocalStoreVaults(options)
2214
+ ]);
2215
+ if (options.json) {
2216
+ return printJson(
2217
+ manifestVaults.map((vault) => ({
2218
+ ...vault,
2219
+ localStore: localStoreVaults.includes(vault.name)
2220
+ }))
2221
+ );
2222
+ }
2223
+ if (manifestVaults.length === 0) {
2224
+ return "";
2225
+ }
2226
+ return manifestVaults.map(
2227
+ (vault) => `${vault.name} provider=${vault.provider} passphrase=${vault.passphrasePolicy}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
2228
+ ).join("\n");
2229
+ }
2230
+
1768
2231
  // src/commands/secret.ts
1769
2232
  function isSecretRef(value) {
1770
2233
  return Boolean(
@@ -1801,32 +2264,26 @@ async function runSecret(argsOrPath, options = {}) {
1801
2264
  const { action, tail } = normalizeSecretCommand(args);
1802
2265
  const cliArgs = [...options.cliArgs ?? []];
1803
2266
  if (action === "create-vault") {
1804
- const vault = tail[0] ?? "default";
1805
- const passphrase = consumeOption(cliArgs, "--passphrase");
1806
- const result = await createVault(vault, {
1807
- ...options,
1808
- cliArgs,
1809
- ...passphrase ? { passphrase } : {}
1810
- });
1811
- if (options.json) {
1812
- return printJson(result);
1813
- }
1814
- return `created secret vault "${result.vault}" in ${result.filePath}`;
2267
+ return runVault(["create", tail[0] ?? "default"], options);
1815
2268
  }
1816
2269
  if (action === "list") {
1817
2270
  const prefix = consumeOption(cliArgs, "--prefix");
2271
+ const vault = consumeOption(cliArgs, "--vault");
2272
+ const provider = consumeOption(cliArgs, "--provider");
1818
2273
  const entries = await listConfigEntries("secret", {
1819
2274
  ...options,
1820
2275
  cliArgs,
1821
- ...prefix ? { prefix } : {}
2276
+ ...prefix ? { prefix } : {},
2277
+ ...vault ? { vault } : {},
2278
+ ...provider ? { provider } : {}
1822
2279
  });
1823
2280
  if (options.json) {
1824
2281
  return printJson(entries);
1825
2282
  }
1826
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
2283
+ return entries.map((entry2) => `${entry2.key}=${printValue(entry2.value)}`).join("\n");
1827
2284
  }
1828
2285
  if (action === "set") {
1829
- const secretPath = tail[0];
2286
+ const secretPath2 = tail[0];
1830
2287
  const rawValue = tail[1] ?? "";
1831
2288
  const local = consumeFlag(cliArgs, "--local");
1832
2289
  const remote = consumeFlag(cliArgs, "--remote");
@@ -1835,25 +2292,25 @@ async function runSecret(argsOrPath, options = {}) {
1835
2292
  const provider = consumeOption(cliArgs, "--provider");
1836
2293
  const passphrase = consumeOption(cliArgs, "--passphrase");
1837
2294
  const vault = consumeOption(cliArgs, "--vault") ?? "default";
1838
- const mode = local ? "local" : remote ? "remote" : ref ? "ref" : "local";
1839
- const result = await setSecret(secretPath ?? "app.token", rawValue, {
2295
+ const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
2296
+ const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
1840
2297
  ...options,
1841
2298
  cliArgs,
1842
2299
  target,
1843
- mode,
1844
2300
  vault,
2301
+ ...mode ? { mode } : {},
1845
2302
  ...provider ? { provider } : {},
1846
2303
  ...passphrase ? { passphrase } : {}
1847
2304
  });
1848
2305
  if (options.json) {
1849
2306
  return printJson(result);
1850
2307
  }
1851
- return result.provider === "local" ? `set secret.${secretPath} in vault "${result.vault ?? "default"}" with ref "${result.ref}" and repo pointer ${result.filePath}` : `set secret.${secretPath} via ${result.provider} in ${result.filePath}`;
2308
+ return result.provider === "local" ? `set secret.${secretPath2} in vault "${result.vault ?? "default"}" with ref "${result.ref}" and repo pointer ${result.filePath}` : `set secret.${secretPath2} via ${result.provider} in ${result.filePath}`;
1852
2309
  }
1853
2310
  if (action === "delete") {
1854
- const secretPath = tail[0];
2311
+ const secretPath2 = tail[0];
1855
2312
  const target = consumeOption(cliArgs, "--target") ?? "local";
1856
- const result = await deleteSecret(secretPath ?? "app.token", {
2313
+ const result = await deleteSecret(secretPath2 ?? "app.token", {
1857
2314
  ...options,
1858
2315
  cliArgs,
1859
2316
  target
@@ -1861,22 +2318,37 @@ async function runSecret(argsOrPath, options = {}) {
1861
2318
  if (options.json) {
1862
2319
  return printJson(result);
1863
2320
  }
1864
- return result.deleted ? `deleted secret.${secretPath} from ${result.filePath}` : `no secret.${secretPath} found in ${result.filePath}`;
2321
+ return result.deleted ? `deleted secret.${secretPath2} from ${result.filePath}` : `no secret.${secretPath2} found in ${result.filePath}`;
1865
2322
  }
1866
2323
  const runtime = await createRuntimeService(options);
1867
- const value = runtime.secret(tail[0] ?? "app.token");
2324
+ const secretPath = tail[0] ?? "app.token";
2325
+ const expectedVault = consumeOption(cliArgs, "--vault");
2326
+ const entry = runtime.graph.entries.get(`secret.${secretPath}`);
2327
+ const secretRef = entry?.winner.metadata?.secretRef;
2328
+ const value = runtime.secret(secretPath);
1868
2329
  if (value === void 0) {
1869
- throw new Error(`Missing CNOS secret path: ${tail[0] ?? "app.token"}`);
2330
+ throw new Error(`Missing CNOS secret path: ${secretPath}`);
2331
+ }
2332
+ if (expectedVault && secretRef?.vault && secretRef.vault !== expectedVault) {
2333
+ throw new Error(`Secret ${secretPath} belongs to vault "${secretRef.vault}", not "${expectedVault}"`);
1870
2334
  }
1871
2335
  if (isSecretRef(value)) {
1872
- const vault = value.vault ?? "default";
1873
- throw new Error(
1874
- `Secret ${tail[0] ?? "app.token"} is stored in vault "${vault}" as ref "${value.ref}". Provide the correct vault passphrase to resolve it.`
1875
- );
2336
+ if (value.provider === "local") {
2337
+ const vault = value.vault ?? "default";
2338
+ throw new Error(
2339
+ `Secret ${secretPath} is stored in vault "${vault}" as ref "${value.ref}". Provide the correct vault passphrase to resolve it.`
2340
+ );
2341
+ }
2342
+ if (value.provider === "github-secrets") {
2343
+ throw new Error(
2344
+ `Secret ${secretPath} is backed by GitHub secrets via ref "${value.ref}". Set that env var in the current process or CI job to resolve it.`
2345
+ );
2346
+ }
2347
+ throw new Error(`Secret ${secretPath} is stored as a ${value.provider} reference "${value.ref}" and is not resolved.`);
1876
2348
  }
1877
2349
  if (options.json) {
1878
2350
  return printJson({
1879
- key: `secret.${tail[0] ?? "app.token"}`,
2351
+ key: `secret.${secretPath}`,
1880
2352
  value
1881
2353
  });
1882
2354
  }
@@ -1884,9 +2356,9 @@ async function runSecret(argsOrPath, options = {}) {
1884
2356
  }
1885
2357
 
1886
2358
  // src/commands/use.ts
1887
- import path9 from "path";
2359
+ import path11 from "path";
1888
2360
  async function runUse(args = [], options = {}) {
1889
- const root = path9.resolve(options.root ?? process.cwd());
2361
+ const root = path11.resolve(options.root ?? process.cwd());
1890
2362
  const action = args[0] ?? "show";
1891
2363
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
1892
2364
  if (action === "show" || !hasUpdates) {
@@ -1923,7 +2395,7 @@ async function runValidate(options = {}) {
1923
2395
  // package.json
1924
2396
  var package_default = {
1925
2397
  name: "@kitsy/cnos-cli",
1926
- version: "1.1.1",
2398
+ version: "1.2.0",
1927
2399
  description: "CLI entry point and developer tooling for CNOS.",
1928
2400
  type: "module",
1929
2401
  main: "./dist/index.js",
@@ -2068,6 +2540,9 @@ function resolveHelpTopic(command, args) {
2068
2540
  if (command === "export" && args[0] === "env") {
2069
2541
  return normalizeHelpTopic([command, args[0]]);
2070
2542
  }
2543
+ if (command === "vault" && args[0] && ["create", "add", "list", "delete", "remove"].includes(args[0])) {
2544
+ return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0] === "add" ? "create" : args[0]]);
2545
+ }
2071
2546
  if (command === "secret" && args[0] && ["set", "create", "add", "list", "delete", "remove"].includes(args[0])) {
2072
2547
  if ((args[0] === "create" || args[0] === "add") && args[1] === "vault") {
2073
2548
  return normalizeHelpTopic(["secret", "create", "vault"]);
@@ -2149,6 +2624,10 @@ async function main(argv) {
2149
2624
  return;
2150
2625
  case "secret":
2151
2626
  process.stdout.write(`${await runSecret(args.length > 0 ? args : ["app.token"], runtimeOptions)}
2627
+ `);
2628
+ return;
2629
+ case "vault":
2630
+ process.stdout.write(`${await runVault(args, runtimeOptions)}
2152
2631
  `);
2153
2632
  return;
2154
2633
  case "use":
@@ -2157,6 +2636,10 @@ async function main(argv) {
2157
2636
  return;
2158
2637
  case "profile":
2159
2638
  process.stdout.write(`${await runProfile(args, runtimeOptions)}
2639
+ `);
2640
+ return;
2641
+ case "promote":
2642
+ process.stdout.write(`${await runPromote(args, runtimeOptions)}
2160
2643
  `);
2161
2644
  return;
2162
2645
  case "list":