@kitsy/cnos-cli 1.1.2 → 1.3.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 +1276 -218
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -11,27 +11,66 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
11
11
  "--format",
12
12
  "--framework",
13
13
  "--prefix",
14
+ "--scan",
14
15
  "--target",
15
16
  "--to",
16
17
  "--provider",
17
18
  "--passphrase",
18
19
  "--vault",
19
- "--inherit"
20
+ "--inherit",
21
+ "--as",
22
+ "--set",
23
+ "--debounce"
24
+ ]);
25
+ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
26
+ "--flatten",
27
+ "--public",
28
+ "--local",
29
+ "--remote",
30
+ "--ref",
31
+ "--no-passphrase",
32
+ "--stdin",
33
+ "--reveal",
34
+ "--auth",
35
+ "--all",
36
+ "--store-keychain",
37
+ "--watch",
38
+ "--dry-run",
39
+ "--apply",
40
+ "--rewrite",
41
+ "--signal"
20
42
  ]);
21
- var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public", "--local", "--remote", "--ref"]);
22
43
  function normalizeCommand(argv) {
23
44
  const [command = "doctor", ...rest] = argv;
24
45
  const resource = rest[0];
25
46
  const remaining = rest.slice(1);
47
+ if ((command === "set" || command === "get") && (resource === "value" || resource === "secret")) {
48
+ return [resource, command, ...remaining];
49
+ }
26
50
  if ((command === "create" || command === "add") && resource === "profile") {
27
51
  return ["profile", "create", ...remaining];
28
52
  }
53
+ if ((command === "create" || command === "add") && resource === "vault") {
54
+ return ["vault", "create", ...remaining];
55
+ }
29
56
  if ((command === "delete" || command === "remove") && resource === "profile") {
30
57
  return ["profile", "delete", ...remaining];
31
58
  }
59
+ if ((command === "delete" || command === "remove") && resource === "vault") {
60
+ return ["vault", "remove", ...remaining];
61
+ }
62
+ if (command === "auth" && resource === "vault") {
63
+ return ["vault", "auth", ...remaining];
64
+ }
65
+ if (command === "logout" && resource === "vault") {
66
+ return ["vault", "logout", ...remaining];
67
+ }
32
68
  if (command === "list" && resource === "profile") {
33
69
  return ["profile", "list", ...remaining];
34
70
  }
71
+ if (command === "list" && resource === "vault") {
72
+ return ["vault", "list", ...remaining];
73
+ }
35
74
  if ((command === "create" || command === "add") && resource === "secret") {
36
75
  return ["secret", "set", ...remaining];
37
76
  }
@@ -186,45 +225,27 @@ function printJson(value) {
186
225
  }
187
226
 
188
227
  // src/services/writes.ts
189
- import { mkdir, readFile, rm, writeFile } from "fs/promises";
228
+ import { mkdir, readFile, writeFile } from "fs/promises";
190
229
  import path from "path";
191
230
  import {
192
- createSecretVault,
231
+ createSecretVaultProvider,
193
232
  parseYaml,
194
233
  resolveConfigDocumentPath,
195
- resolveSecretPassphrase,
196
- resolveSecretStoreRoot,
197
- stringifyYaml,
198
- writeLocalSecret
234
+ resolveVaultAuth,
235
+ stringifyYaml
199
236
  } from "@kitsy/cnos/internal";
200
237
 
201
238
  // src/services/runtime.ts
202
- import { createCnos } from "@kitsy/cnos";
239
+ import { createCnos } from "@kitsy/cnos/configure";
203
240
  async function createRuntimeService(options = {}) {
204
- const createOptions = {
205
- ...options.root ? {
206
- root: options.root
207
- } : {},
208
- ...options.workspace ? {
209
- workspace: options.workspace
210
- } : {},
211
- ...options.profile ? {
212
- profile: options.profile
213
- } : {},
214
- ...options.globalRoot ? {
215
- globalRoot: options.globalRoot
216
- } : {},
217
- ...options.cliArgs && options.cliArgs.length > 0 ? {
218
- cliArgs: options.cliArgs
219
- } : {},
220
- ...options.processEnv ? {
221
- processEnv: options.processEnv
222
- } : {
223
- processEnv: process.env
224
- }
225
- };
226
241
  return createCnos({
227
- ...createOptions
242
+ ...options.root ? { root: options.root } : {},
243
+ ...options.workspace ? { workspace: options.workspace } : {},
244
+ ...options.profile ? { profile: options.profile } : {},
245
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {},
246
+ ...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
247
+ ...options.secretResolution ? { secretResolution: options.secretResolution } : {},
248
+ processEnv: options.processEnv ?? process.env
228
249
  });
229
250
  }
230
251
 
@@ -303,7 +324,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
303
324
  filePath: secret.filePath,
304
325
  value: {
305
326
  provider: secret.provider,
306
- ref: secret.ref
327
+ ref: secret.ref,
328
+ ...secret.vault ? { vault: secret.vault } : {}
307
329
  }
308
330
  };
309
331
  }
@@ -321,48 +343,34 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
321
343
  value: parsedValue
322
344
  };
323
345
  }
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
346
  async function setSecret(configPath, rawValue, options = {}) {
341
347
  const runtime = await createRuntimeService(options);
342
348
  const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
343
349
  const profile = options.profile ?? runtime.graph.profile;
344
350
  const filePath = resolveConfigDocumentPath(workspaceRoot, "secret", configPath, profile);
345
351
  const document = await readYamlDocument(filePath);
346
- const mode = options.mode ?? "local";
347
352
  const vault = options.vault?.trim() || "default";
353
+ const vaultDefinition = runtime.manifest.vaults[vault];
354
+ if (!vaultDefinition) {
355
+ throw new Error(`Unknown vault "${vault}". Create it first with cnos vault create ${vault}.`);
356
+ }
357
+ const mode = options.mode ?? (vaultDefinition.provider === "local" ? "local" : vaultDefinition.provider === "github-secrets" ? "ref" : "remote");
348
358
  let reference;
349
- let storePath;
350
359
  if (mode === "local") {
351
- const passphrase = options.passphrase ?? resolveSecretPassphrase(vault, options.processEnv ?? process.env);
352
- if (!passphrase) {
353
- throw new Error(`Vault "${vault}" requires --passphrase or CNOS_SECRET_PASSPHRASE`);
354
- }
355
- const ref = configPath;
356
- storePath = await writeLocalSecret(resolveSecretStoreRoot(options.processEnv), ref, rawValue, passphrase, vault);
360
+ const auth = await resolveVaultAuth(vault, vaultDefinition, options.processEnv ?? process.env);
361
+ const provider = createSecretVaultProvider(vault, vaultDefinition, options.processEnv ?? process.env);
362
+ await provider.authenticate(auth);
363
+ await provider.set(configPath, rawValue);
357
364
  reference = {
358
365
  provider: "local",
359
- ref,
366
+ ref: configPath,
360
367
  vault
361
368
  };
362
369
  } else {
363
370
  reference = {
364
- provider: options.provider ?? (mode === "ref" ? "ref" : "remote"),
365
- ref: rawValue
371
+ provider: options.provider?.trim() || vaultDefinition.provider,
372
+ ref: rawValue || configPath.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(),
373
+ vault
366
374
  };
367
375
  }
368
376
  setNestedValue(document, configPath.split("."), reference);
@@ -372,8 +380,7 @@ async function setSecret(configPath, rawValue, options = {}) {
372
380
  filePath,
373
381
  provider: reference.provider,
374
382
  ref: reference.ref,
375
- ...reference.vault ? { vault: reference.vault } : {},
376
- ...storePath ? { storePath } : {}
383
+ ...reference.vault ? { vault: reference.vault } : {}
377
384
  };
378
385
  }
379
386
  async function deleteSecret(configPath, options = {}) {
@@ -391,23 +398,19 @@ async function deleteSecret(configPath, options = {}) {
391
398
  };
392
399
  }
393
400
  await writeFile(filePath, stringifyYaml(document), "utf8");
394
- let removedStore;
395
401
  const secretRef = metadata?.secretRef;
396
402
  if (isSecretReference(secretRef) && secretRef.provider === "local") {
397
- const storePath = path.join(
398
- resolveSecretStoreRoot(options.processEnv),
399
- "vaults",
400
- secretRef.vault ?? "default",
401
- "store",
402
- ...secretRef.ref.split("/")
403
- ).concat(".json");
404
- await rm(storePath, { force: true });
405
- removedStore = storePath;
403
+ const definition = runtime.manifest.vaults[secretRef.vault ?? "default"];
404
+ if (definition) {
405
+ const auth = await resolveVaultAuth(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
406
+ const provider = createSecretVaultProvider(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
407
+ await provider.authenticate(auth);
408
+ await provider.delete(secretRef.ref);
409
+ }
406
410
  }
407
411
  return {
408
412
  filePath,
409
- deleted: true,
410
- ...removedStore ? { removedStore } : {}
413
+ deleted: true
411
414
  };
412
415
  }
413
416
  async function deleteValue(namespace, configPath, options = {}) {
@@ -464,6 +467,17 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
464
467
  return `defined ${namespace}.${configPath} in ${result.filePath}`;
465
468
  }
466
469
 
470
+ // src/commands/drift.ts
471
+ import { compareSchemaToGraph, formatDriftReport } from "@kitsy/cnos/internal";
472
+ async function runDrift(options = {}) {
473
+ const runtime = await createRuntimeService(options);
474
+ const report = compareSchemaToGraph(runtime);
475
+ if (options.json) {
476
+ return printJson(report);
477
+ }
478
+ return formatDriftReport(report);
479
+ }
480
+
467
481
  // src/commands/diff.ts
468
482
  import { flattenObject } from "@kitsy/cnos/internal";
469
483
  function flattenRuntime(runtime) {
@@ -502,8 +516,15 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
502
516
  }
503
517
 
504
518
  // src/services/doctor.ts
505
- import { readFile as readFile2 } from "fs/promises";
519
+ import { readdir, readFile as readFile2 } from "fs/promises";
506
520
  import path2 from "path";
521
+ import {
522
+ detectLegacyVaultFormat,
523
+ isSecretReference as isSecretReference2,
524
+ parseYaml as parseYaml2,
525
+ readKeychain,
526
+ resolveSecretStoreRoot
527
+ } from "@kitsy/cnos/internal";
507
528
 
508
529
  // src/services/validation.ts
509
530
  import { validateRuntime } from "@kitsy/cnos/internal";
@@ -548,6 +569,70 @@ async function checkGitignore(root) {
548
569
  function issueSummary(issues) {
549
570
  return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
550
571
  }
572
+ async function collectYamlFiles(root) {
573
+ try {
574
+ const entries = await readdir(root, { withFileTypes: true });
575
+ const results = [];
576
+ for (const entry of entries) {
577
+ const target = path2.join(root, entry.name);
578
+ if (entry.isDirectory()) {
579
+ results.push(...await collectYamlFiles(target));
580
+ continue;
581
+ }
582
+ if (entry.isFile() && [".yml", ".yaml"].includes(path2.extname(entry.name).toLowerCase())) {
583
+ results.push(target);
584
+ }
585
+ }
586
+ return results;
587
+ } catch {
588
+ return [];
589
+ }
590
+ }
591
+ function hasPlaintextSecret(value) {
592
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
593
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
594
+ }
595
+ if (isSecretReference2(value)) {
596
+ return false;
597
+ }
598
+ return Object.values(value).some((entry) => hasPlaintextSecret(entry));
599
+ }
600
+ async function checkSecretSecurity(options, runtime) {
601
+ const storeRoot = resolveSecretStoreRoot(options.processEnv);
602
+ const legacyPaths = await Promise.all(
603
+ Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").map(async ([vault]) => ({ vault, path: await detectLegacyVaultFormat(storeRoot, vault) }))
604
+ );
605
+ const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
606
+ const secretFiles = await Promise.all(
607
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path2.join(root.path, "secrets")))
608
+ );
609
+ const plaintextFiles = [];
610
+ for (const file of secretFiles.flat()) {
611
+ try {
612
+ const parsed = parseYaml2(await readFile2(file, "utf8"));
613
+ if (hasPlaintextSecret(parsed)) {
614
+ plaintextFiles.push(file);
615
+ }
616
+ } catch {
617
+ plaintextFiles.push(file);
618
+ }
619
+ }
620
+ const keychainWarnings = await Promise.all(
621
+ Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").flatMap(
622
+ ([vault, definition]) => (definition.auth?.passphrase?.from ?? []).filter((source) => source.startsWith("keychain:")).map(async (source) => ({ vault, source, value: await readKeychain(source.slice("keychain:".length)) }))
623
+ )
624
+ );
625
+ const warnings = [
626
+ ...legacyDetected.map((entry) => `legacy vault ${entry.vault}: ${entry.path}`),
627
+ ...plaintextFiles.map((file) => `plaintext secret file: ${file}`),
628
+ ...keychainWarnings.filter((entry) => !entry.value).map((entry) => `no keychain entry for vault ${entry.vault} (${entry.source})`)
629
+ ];
630
+ return {
631
+ name: "security",
632
+ ok: warnings.length === 0,
633
+ details: warnings.length === 0 ? "no legacy vaults, plaintext secret files, or missing keychain entries" : warnings.join("; ")
634
+ };
635
+ }
551
636
  async function evaluateDoctor(options = {}) {
552
637
  const root = path2.resolve(options.root ?? process.cwd());
553
638
  const { runtime, summary } = await createValidationSummary(options);
@@ -579,6 +664,7 @@ async function evaluateDoctor(options = {}) {
579
664
  ok: !runtime.manifest.workspaces.global.enabled || Boolean(runtime.graph.workspace.globalRoot),
580
665
  details: runtime.manifest.workspaces.global.enabled ? runtime.graph.workspace.globalRoot ? `enabled at ${runtime.graph.workspace.globalRoot}` : "enabled but no global root resolved" : "disabled"
581
666
  },
667
+ await checkSecretSecurity(options, runtime),
582
668
  await checkGitignore(root)
583
669
  ];
584
670
  }
@@ -597,7 +683,7 @@ async function runDoctor(options = {}) {
597
683
  }
598
684
 
599
685
  // src/commands/dump.ts
600
- import { writeDump } from "@kitsy/cnos";
686
+ import { writeDump } from "@kitsy/cnos/configure";
601
687
  async function runDump(options = {}) {
602
688
  const cliArgs = [...options.cliArgs ?? []];
603
689
  const flatten = consumeFlag(cliArgs, "--flatten");
@@ -619,12 +705,60 @@ async function runDump(options = {}) {
619
705
  return `dumped ${result.files.length} files to ${result.root}`;
620
706
  }
621
707
 
708
+ // src/commands/codegen.ts
709
+ import { watchSchema, writeCodegenOutput } from "@kitsy/cnos/internal";
710
+ async function runCodegen(options = {}) {
711
+ const cliArgs = [...options.cliArgs ?? []];
712
+ const out = consumeOption(cliArgs, "--out");
713
+ const watch2 = consumeFlag(cliArgs, "--watch");
714
+ if (cliArgs.length > 0) {
715
+ throw new Error(`Unknown codegen options: ${cliArgs.join(" ")}`);
716
+ }
717
+ if (watch2) {
718
+ const watcher = await watchSchema({
719
+ ...options.root ? {
720
+ root: options.root
721
+ } : {},
722
+ ...out ? {
723
+ out
724
+ } : {},
725
+ onError(error) {
726
+ const message = error instanceof Error ? error.message : String(error);
727
+ process.stderr.write(`${message}
728
+ `);
729
+ }
730
+ });
731
+ const closeWatcher = () => {
732
+ watcher.close();
733
+ };
734
+ process.once("SIGINT", closeWatcher);
735
+ process.once("SIGTERM", closeWatcher);
736
+ return `watching schema changes -> ${out ?? ".cnos/types/cnos.d.ts"}`;
737
+ }
738
+ const result = await writeCodegenOutput({
739
+ ...options.root ? {
740
+ root: options.root
741
+ } : {},
742
+ ...out ? {
743
+ out
744
+ } : {}
745
+ });
746
+ const summary = result.hasSchema ? `generated types from ${result.schemaEntryCount} schema entr${result.schemaEntryCount === 1 ? "y" : "ies"}` : "generated empty types (no schema section found)";
747
+ return `${summary} -> ${result.typesPath} and ${result.runtimePath}`;
748
+ }
749
+
622
750
  // src/commands/exportEnv.ts
751
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
752
+ import path3 from "path";
753
+ function formatEnvOutput(env) {
754
+ return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
755
+ }
623
756
  async function runExportEnv(options = {}) {
624
757
  const cliArgs = [...options.cliArgs ?? []];
625
758
  const isPublic = consumeFlag(cliArgs, "--public");
626
759
  const framework = consumeOption(cliArgs, "--framework");
627
760
  const prefix = consumeOption(cliArgs, "--prefix");
761
+ const to = consumeOption(cliArgs, "--to");
628
762
  const runtime = await createRuntimeService({
629
763
  ...options,
630
764
  cliArgs
@@ -632,16 +766,26 @@ async function runExportEnv(options = {}) {
632
766
  const env = isPublic ? runtime.toPublicEnv({
633
767
  ...framework ? { framework } : {},
634
768
  ...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
- );
769
+ }) : runtime.toEnv();
770
+ const output = formatEnvOutput(env);
771
+ if (to) {
772
+ const targetPath = path3.resolve(options.root ?? process.cwd(), to);
773
+ await mkdir2(path3.dirname(targetPath), { recursive: true });
774
+ await writeFile2(targetPath, output, "utf8");
775
+ if (options.json) {
776
+ return printJson({
777
+ to: targetPath,
778
+ count: Object.keys(env).length,
779
+ public: isPublic,
780
+ ...framework ? { framework } : {}
781
+ });
782
+ }
783
+ return `Wrote ${Object.keys(env).length} env vars to ${targetPath}`;
784
+ }
641
785
  if (options.json) {
642
786
  return printJson(env);
643
787
  }
644
- return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
788
+ return output;
645
789
  }
646
790
 
647
791
  // src/commands/export.ts
@@ -704,6 +848,23 @@ var COMMANDS = [
704
848
  ],
705
849
  examples: ["cnos onboard", "cnos onboard --workspace webapp", "cnos onboard --root ../my-app --workspace app --move"]
706
850
  },
851
+ {
852
+ id: "codegen",
853
+ summary: "Generate typed CNOS access wrappers from schema.",
854
+ usage: "cnos codegen [--out <path>] [--watch] [--root <path>]",
855
+ description: "Reads schema from .cnos/cnos.yml and generates typed CNOS declaration output plus a typed createCnos wrapper.",
856
+ options: [
857
+ {
858
+ flag: "--out <path>",
859
+ description: "Custom path for the generated type declaration file. runtime.ts is emitted beside it."
860
+ },
861
+ {
862
+ flag: "--watch",
863
+ description: "Watch the manifest schema and regenerate output when it changes."
864
+ }
865
+ ],
866
+ examples: ["cnos codegen", "cnos codegen --out src/cnos-config.d.ts", "cnos codegen --watch"]
867
+ },
707
868
  {
708
869
  id: "read",
709
870
  summary: "Read any fully-qualified CNOS key.",
@@ -796,17 +957,90 @@ var COMMANDS = [
796
957
  description: "Provider name for --remote or --ref secret writes."
797
958
  },
798
959
  {
799
- flag: "--passphrase <value>",
800
- description: "Passphrase used to encrypt local secret material when --local is selected."
960
+ flag: "--vault <name>",
961
+ description: "Use a manifest-defined vault. Provider behavior is inferred from the vault definition."
962
+ },
963
+ {
964
+ flag: "--reveal",
965
+ description: "Reveal the resolved secret value for get-style reads. Output is masked by default."
801
966
  }
802
967
  ],
803
968
  examples: [
804
969
  "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"
970
+ "cnos vault create local-dev",
971
+ "cnos vault auth local-dev",
972
+ "cnos secret set app.token super-secret --vault local-dev",
973
+ "cnos vault create github-ci --provider github-secrets --no-passphrase",
974
+ "cnos secret set app.token APP_TOKEN --vault github-ci"
808
975
  ]
809
976
  },
977
+ {
978
+ id: "vault",
979
+ summary: "Manage manifest-defined secret vaults.",
980
+ usage: "cnos vault [create <name> | list | remove <name>] [options] [global-options]",
981
+ 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.",
982
+ options: [
983
+ {
984
+ flag: "--provider <local|github-secrets>",
985
+ description: "Vault provider. Defaults to local."
986
+ },
987
+ {
988
+ flag: "--no-passphrase",
989
+ description: "Allowed for passwordless providers such as github-secrets."
990
+ }
991
+ ],
992
+ examples: [
993
+ "cnos vault create local-dev",
994
+ "cnos vault auth local-dev",
995
+ "cnos vault create github-ci --provider github-secrets --no-passphrase",
996
+ "cnos vault list",
997
+ "cnos vault remove local-dev"
998
+ ]
999
+ },
1000
+ {
1001
+ id: "vault create",
1002
+ summary: "Create a manifest-defined vault.",
1003
+ usage: "cnos vault create <name> [--provider <local|github-secrets>] [--no-passphrase] [global-options]",
1004
+ description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets.",
1005
+ examples: [
1006
+ "cnos vault create local-dev",
1007
+ "cnos vault create github-ci --provider github-secrets --no-passphrase"
1008
+ ]
1009
+ },
1010
+ {
1011
+ id: "vault auth",
1012
+ summary: "Authenticate a vault for the current shell session.",
1013
+ usage: "cnos vault auth <name> [--store-keychain] [global-options]",
1014
+ description: "Authenticates a local vault using env, keychain, or prompt-based auth and stores a derived session key for later CNOS commands in the same shell.",
1015
+ examples: ["cnos vault auth local-dev", "cnos vault auth local-dev --store-keychain"]
1016
+ },
1017
+ {
1018
+ id: "vault logout",
1019
+ summary: "Clear vault auth state for the current shell session.",
1020
+ usage: "cnos vault logout <name> [global-options]",
1021
+ description: "Removes active vault session auth for the selected vault or all vaults when used with --all.",
1022
+ options: [
1023
+ {
1024
+ flag: "--all",
1025
+ description: "Clear all active vault auth sessions for the current shell context."
1026
+ }
1027
+ ],
1028
+ examples: ["cnos vault logout local-dev", "cnos vault logout --all"]
1029
+ },
1030
+ {
1031
+ id: "vault list",
1032
+ summary: "List manifest-defined vaults.",
1033
+ usage: "cnos vault list [global-options]",
1034
+ description: "Lists vault definitions together with provider and passphrase policy.",
1035
+ examples: ["cnos vault list"]
1036
+ },
1037
+ {
1038
+ id: "vault remove",
1039
+ summary: "Remove a vault definition.",
1040
+ usage: "cnos vault remove <name> [global-options]",
1041
+ description: "Removes the vault from .cnos/cnos.yml and deletes local vault store metadata when present.",
1042
+ examples: ["cnos vault remove local-dev"]
1043
+ },
810
1044
  {
811
1045
  id: "define",
812
1046
  summary: "Write a value or secret into the selected workspace.",
@@ -850,7 +1084,7 @@ var COMMANDS = [
850
1084
  {
851
1085
  id: "list",
852
1086
  summary: "List resolved config entries.",
853
- usage: "cnos list [value|secret|meta|env|public|all] [--prefix <path>] [global-options]",
1087
+ usage: "cnos list [value|secret|meta|env|public|all] [--prefix <path>] [--framework <name>] [global-options]",
854
1088
  description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering.",
855
1089
  options: [
856
1090
  {
@@ -860,9 +1094,13 @@ var COMMANDS = [
860
1094
  {
861
1095
  flag: "--prefix <path>",
862
1096
  description: "Filter list output to entries whose logical keys begin with this prefix."
1097
+ },
1098
+ {
1099
+ flag: "--framework <name>",
1100
+ description: "When listing public output, apply framework-specific prefixes such as vite or next."
863
1101
  }
864
1102
  ],
865
- examples: ["cnos list", "cnos list value --prefix app.", "cnos list env", "cnos list --namespace secret"]
1103
+ examples: ["cnos list", "cnos list value --prefix app.", "cnos list env", "cnos list public --framework vite"]
866
1104
  },
867
1105
  {
868
1106
  id: "profile",
@@ -909,29 +1147,52 @@ var COMMANDS = [
909
1147
  description: "Deletes .cnos/profiles/<name>.yml.",
910
1148
  examples: ["cnos profile delete stage"]
911
1149
  },
1150
+ {
1151
+ id: "promote",
1152
+ summary: "Promote shareable config into public or env projection surfaces.",
1153
+ usage: "cnos promote <key...> --to <public|env> [--as <ENV_VAR>] [global-options]",
1154
+ description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected.",
1155
+ options: [
1156
+ {
1157
+ flag: "--to <public|env>",
1158
+ description: "Choose whether the keys are promoted to the public surface or env export surface."
1159
+ },
1160
+ {
1161
+ flag: "--as <ENV_VAR>",
1162
+ description: "Required for --to env. Sets the exported env var name for the promoted key."
1163
+ }
1164
+ ],
1165
+ examples: [
1166
+ "cnos promote value.flag.auth.upi_enabled --to public",
1167
+ "cnos promote value.server.port --to env --as PORT"
1168
+ ]
1169
+ },
912
1170
  {
913
1171
  id: "secret set",
914
1172
  summary: "Write a secret securely.",
915
- 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>.",
1173
+ usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [global-options]",
1174
+ 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
1175
  examples: [
918
- "cnos secret create vault db --passphrase dev-pass",
919
- "cnos secret set app.token super-secret --local --vault db --passphrase dev-pass"
1176
+ "cnos vault create db",
1177
+ "cnos vault auth db",
1178
+ "cnos secret set app.token super-secret --vault db",
1179
+ "cnos vault create github-ci --provider github-secrets --no-passphrase",
1180
+ "cnos secret set app.token APP_TOKEN --vault github-ci"
920
1181
  ]
921
1182
  },
922
1183
  {
923
1184
  id: "secret create vault",
924
1185
  summary: "Create a local secret vault.",
925
- usage: "cnos secret create vault <name> --passphrase <value> [global-options]",
926
- description: "Creates a named local secret vault under ~/.cnos/secrets/vaults.",
927
- examples: ["cnos secret create vault db --passphrase dev-pass"]
1186
+ usage: "cnos secret create vault <name> [global-options]",
1187
+ description: "Alias for cnos vault create <name>.",
1188
+ examples: ["cnos secret create vault db"]
928
1189
  },
929
1190
  {
930
1191
  id: "secret list",
931
1192
  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"]
1193
+ usage: "cnos secret list [--vault <name>] [--provider <name>] [global-options]",
1194
+ description: "Lists stored secret entries for the selected workspace and profile, optionally filtered by vault or provider.",
1195
+ examples: ["cnos secret list --workspace api", "cnos secret list --vault github-ci"]
935
1196
  },
936
1197
  {
937
1198
  id: "secret delete",
@@ -982,7 +1243,7 @@ var COMMANDS = [
982
1243
  {
983
1244
  id: "export env",
984
1245
  summary: "Render environment variables for the selected workspace.",
985
- usage: "cnos export env [--public] [--framework <name>] [--prefix <prefix>] [global-options]",
1246
+ usage: "cnos export env [--public] [--framework <name>] [--prefix <prefix>] [--to <path>] [global-options]",
986
1247
  description: "Exports the effective environment as KEY=VALUE lines, or only promoted public values when --public is set.",
987
1248
  options: [
988
1249
  {
@@ -996,9 +1257,17 @@ var COMMANDS = [
996
1257
  {
997
1258
  flag: "--prefix <prefix>",
998
1259
  description: "Override the generated public env prefix."
1260
+ },
1261
+ {
1262
+ flag: "--to <path>",
1263
+ description: "Write the rendered KEY=VALUE output to a file instead of stdout."
999
1264
  }
1000
1265
  ],
1001
- examples: ["cnos export env", "cnos export env --public --framework vite --workspace api"]
1266
+ examples: [
1267
+ "cnos export env",
1268
+ "cnos export env --to .env.local",
1269
+ "cnos export env --public --framework vite --to .env.local --workspace api"
1270
+ ]
1002
1271
  },
1003
1272
  {
1004
1273
  id: "dump",
@@ -1020,9 +1289,32 @@ var COMMANDS = [
1020
1289
  {
1021
1290
  id: "run",
1022
1291
  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"]
1292
+ usage: "cnos run [--public] [--framework <name>] [--set <logical-key=value>] [global-options] -- <command...>",
1293
+ description: "Resolves the active workspace and profile, injects runtime env variables, bootstraps __CNOS_GRAPH__ for singleton runtime reads, and executes the command after --.",
1294
+ options: [
1295
+ {
1296
+ flag: "--set <logical-key=value>",
1297
+ description: "Apply inline logical-key overrides for this run without touching repo config files."
1298
+ },
1299
+ {
1300
+ flag: "--public",
1301
+ description: "Inject only promoted public env variables into the child process."
1302
+ },
1303
+ {
1304
+ flag: "--framework <name>",
1305
+ description: "When used with --public, apply framework-specific prefixes such as vite or next."
1306
+ },
1307
+ {
1308
+ flag: "--prefix <prefix>",
1309
+ description: "Override the generated public env prefix for --public runs."
1310
+ }
1311
+ ],
1312
+ examples: [
1313
+ "cnos run -- node server.js",
1314
+ "cnos run --profile stage -- node server.js",
1315
+ "cnos run --set value.server.port=9999 -- node server.js",
1316
+ "cnos run --public --framework vite -- pnpm build"
1317
+ ]
1026
1318
  },
1027
1319
  {
1028
1320
  id: "diff",
@@ -1050,6 +1342,60 @@ var COMMANDS = [
1050
1342
  description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
1051
1343
  examples: ["cnos doctor", "cnos doctor --workspace api --json"]
1052
1344
  },
1345
+ {
1346
+ id: "drift",
1347
+ summary: "Compare resolved config against schema and report drift.",
1348
+ usage: "cnos drift [--workspace <id>] [--profile <name>] [--json]",
1349
+ description: "Reports missing required keys, undeclared keys, type mismatches, and defaults applied for the selected workspace/profile.",
1350
+ examples: ["cnos drift", "cnos drift --workspace api --profile stage", "cnos drift --json"]
1351
+ },
1352
+ {
1353
+ id: "watch",
1354
+ summary: "Watch CNOS inputs and either restart a process or emit changed keys.",
1355
+ usage: "cnos watch [--signal] [--debounce <ms>] [global-options] -- <command...>",
1356
+ description: "Watches the active manifest, workspace roots, env files, and config documents. In restart mode it respawns the child command after changes; in signal mode it prints changed keys as JSON.",
1357
+ options: [
1358
+ {
1359
+ flag: "--signal",
1360
+ description: "Emit changed keys as JSON instead of restarting a child process."
1361
+ },
1362
+ {
1363
+ flag: "--debounce <ms>",
1364
+ description: "Debounce change handling before re-resolving the graph. Defaults to 300ms."
1365
+ }
1366
+ ],
1367
+ examples: ["cnos watch -- node server.js", "cnos watch --signal", "cnos watch --debounce 100 -- node server.js"]
1368
+ },
1369
+ {
1370
+ id: "migrate",
1371
+ summary: "Scan env usage and propose CNOS manifest mappings.",
1372
+ usage: "cnos migrate [--scan <path>] [--dry-run] [--apply] [--rewrite] [global-options]",
1373
+ description: "Scans JS/TS source for process.env and import.meta.env usage, proposes logical CNOS mappings, updates envMapping/public promote entries, and can rewrite supported source files with backups.",
1374
+ options: [
1375
+ {
1376
+ flag: "--scan <path>",
1377
+ description: "Directory to scan. Defaults to ./src relative to the repo root."
1378
+ },
1379
+ {
1380
+ flag: "--dry-run",
1381
+ description: "Preview the proposed mappings without changing the manifest."
1382
+ },
1383
+ {
1384
+ flag: "--apply",
1385
+ description: "Write proposed env mappings and public promotions into .cnos/cnos.yml."
1386
+ },
1387
+ {
1388
+ flag: "--rewrite",
1389
+ description: "With --apply, rewrite supported process.env usages in source files and create .bak backups."
1390
+ }
1391
+ ],
1392
+ examples: [
1393
+ "cnos migrate",
1394
+ "cnos migrate --scan src --dry-run",
1395
+ "cnos migrate --scan apps/api/src --apply",
1396
+ "cnos migrate --apply --rewrite"
1397
+ ]
1398
+ },
1053
1399
  {
1054
1400
  id: "help",
1055
1401
  summary: "Show human-readable CLI help.",
@@ -1095,17 +1441,25 @@ var INTEGRATIONS = [
1095
1441
  id: "vite",
1096
1442
  packageName: "@kitsy/cnos-vite",
1097
1443
  entrypoint: "@kitsy/cnos-vite",
1098
- summary: "Inject CNOS public env into Vite define replacements and import.meta.env.",
1444
+ summary: "Inject CNOS public env into Vite and embed browser-readable CNOS public data.",
1099
1445
  usage: 'import { createCnosVitePlugin } from "@kitsy/cnos-vite"',
1100
- examples: ["cnos export env --public --framework vite", "vite.config.ts -> plugins: [createCnosVitePlugin()]"]
1446
+ examples: [
1447
+ "cnos export env --public --framework vite",
1448
+ "vite.config.ts -> plugins: [createCnosVitePlugin()]",
1449
+ 'browser code -> import cnos from "@kitsy/cnos/browser"'
1450
+ ]
1101
1451
  },
1102
1452
  {
1103
1453
  id: "next",
1104
1454
  packageName: "@kitsy/cnos-next",
1105
1455
  entrypoint: "@kitsy/cnos-next",
1106
- summary: "Merge CNOS public env into next.config.* using the NEXT_PUBLIC_ convention.",
1456
+ summary: "Merge CNOS public env into Next and embed browser-readable CNOS public data.",
1107
1457
  usage: 'import { withCnosNext } from "@kitsy/cnos-next"',
1108
- examples: ["cnos export env --public --framework next", "next.config.mjs -> export default withCnosNext({})"]
1458
+ examples: [
1459
+ "cnos export env --public --framework next",
1460
+ "next.config.mjs -> export default withCnosNext({})",
1461
+ 'browser code -> import cnos from "@kitsy/cnos/browser"'
1462
+ ]
1109
1463
  }
1110
1464
  ];
1111
1465
  var HELP_DOCUMENT = {
@@ -1232,11 +1586,11 @@ function runHelpAi(topic, cliArgs = []) {
1232
1586
  }
1233
1587
 
1234
1588
  // src/commands/init.ts
1235
- import path4 from "path";
1589
+ import path5 from "path";
1236
1590
 
1237
1591
  // src/services/scaffold.ts
1238
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1239
- import path3 from "path";
1592
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1593
+ import path4 from "path";
1240
1594
  function scaffoldManifest(projectName, workspace) {
1241
1595
  const lines = [
1242
1596
  "version: 1",
@@ -1270,12 +1624,12 @@ async function ensureFile(filePath, content) {
1270
1624
  await readFile3(filePath, "utf8");
1271
1625
  return false;
1272
1626
  } catch {
1273
- await writeFile2(filePath, content, "utf8");
1627
+ await writeFile3(filePath, content, "utf8");
1274
1628
  return true;
1275
1629
  }
1276
1630
  }
1277
1631
  async function ensureGitignore(root) {
1278
- const gitignorePath = path3.join(root, ".gitignore");
1632
+ const gitignorePath = path4.join(root, ".gitignore");
1279
1633
  const requiredEntries = [
1280
1634
  ".cnos/env/.env",
1281
1635
  ".cnos/env/.env.*",
@@ -1298,18 +1652,18 @@ async function ensureGitignore(root) {
1298
1652
  }
1299
1653
  const prefix = current.trim().length > 0 ? `${current.trimEnd()}
1300
1654
  ` : "";
1301
- await writeFile2(gitignorePath, `${prefix}${missingEntries.join("\n")}
1655
+ await writeFile3(gitignorePath, `${prefix}${missingEntries.join("\n")}
1302
1656
  `, "utf8");
1303
1657
  return true;
1304
1658
  }
1305
1659
  async function scaffoldWorkspace(root, workspace) {
1306
- const cnosRoot = path3.join(root, ".cnos");
1307
- const workspaceRoot = workspace ? path3.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1660
+ const cnosRoot = path4.join(root, ".cnos");
1661
+ const workspaceRoot = workspace ? path4.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1308
1662
  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 });
1663
+ await mkdir3(path4.join(workspaceRoot, "profiles"), { recursive: true });
1664
+ await mkdir3(path4.join(workspaceRoot, "values"), { recursive: true });
1665
+ await mkdir3(path4.join(workspaceRoot, "secrets"), { recursive: true });
1666
+ await mkdir3(path4.join(workspaceRoot, "env"), { recursive: true });
1313
1667
  const relativePaths = workspace ? [
1314
1668
  ["workspaces", workspace, "profiles", ".gitkeep"],
1315
1669
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -1322,15 +1676,15 @@ async function scaffoldWorkspace(root, workspace) {
1322
1676
  ["env", ".gitkeep"]
1323
1677
  ];
1324
1678
  for (const relativePath of relativePaths) {
1325
- const filePath = path3.join(cnosRoot, ...relativePath);
1679
+ const filePath = path4.join(cnosRoot, ...relativePath);
1326
1680
  if (await ensureFile(filePath, "")) {
1327
- createdPaths.push(path3.relative(root, filePath).replace(/\\/g, "/"));
1681
+ createdPaths.push(path4.relative(root, filePath).replace(/\\/g, "/"));
1328
1682
  }
1329
1683
  }
1330
- if (await ensureFile(path3.join(cnosRoot, "cnos.yml"), scaffoldManifest(path3.basename(root), workspace))) {
1684
+ if (await ensureFile(path4.join(cnosRoot, "cnos.yml"), scaffoldManifest(path4.basename(root), workspace))) {
1331
1685
  createdPaths.push(".cnos/cnos.yml");
1332
1686
  }
1333
- if (workspace && await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1687
+ if (workspace && await ensureFile(path4.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1334
1688
  globalRoot: ~/.cnos
1335
1689
  `)) {
1336
1690
  createdPaths.push(".cnos-workspace.yml");
@@ -1347,7 +1701,7 @@ globalRoot: ~/.cnos
1347
1701
 
1348
1702
  // src/commands/init.ts
1349
1703
  async function runInit(options = {}) {
1350
- const root = path4.resolve(options.root ?? process.cwd());
1704
+ const root = path5.resolve(options.root ?? process.cwd());
1351
1705
  const result = await scaffoldWorkspace(root, options.workspace);
1352
1706
  if (options.json) {
1353
1707
  return printJson(result);
@@ -1358,6 +1712,12 @@ async function runInit(options = {}) {
1358
1712
  return `initialized CNOS project at ${root}`;
1359
1713
  }
1360
1714
 
1715
+ // src/format/maskSecret.ts
1716
+ var MASKED_SECRET_VALUE = "****";
1717
+ function maskSecretValue(value) {
1718
+ return value === void 0 ? "" : MASKED_SECRET_VALUE;
1719
+ }
1720
+
1361
1721
  // src/format/printInspect.ts
1362
1722
  function printInspect(record) {
1363
1723
  const lines = [
@@ -1382,12 +1742,22 @@ function printInspect(record) {
1382
1742
 
1383
1743
  // src/commands/inspect.ts
1384
1744
  async function runInspect(key, options = {}) {
1745
+ const reveal = options.cliArgs?.includes("--reveal") ?? false;
1385
1746
  const runtime = await createRuntimeService(options);
1386
1747
  const inspectResult = runtime.inspect(key);
1748
+ const value = key.startsWith("secret.") && !reveal ? maskSecretValue(inspectResult.value) : inspectResult.value;
1749
+ const printable = {
1750
+ ...inspectResult,
1751
+ value,
1752
+ overridden: inspectResult.overridden.map((entry) => ({
1753
+ ...entry,
1754
+ value: key.startsWith("secret.") && !reveal ? maskSecretValue(entry.value) : entry.value
1755
+ }))
1756
+ };
1387
1757
  if (options.json) {
1388
- return printJson(inspectResult);
1758
+ return printJson(printable);
1389
1759
  }
1390
- return printInspect(inspectResult);
1760
+ return printInspect(printable);
1391
1761
  }
1392
1762
 
1393
1763
  // src/format/printValue.ts
@@ -1406,34 +1776,52 @@ function printValue(value, json = false) {
1406
1776
 
1407
1777
  // src/services/listing.ts
1408
1778
  import { flattenObject as flattenObject2 } from "@kitsy/cnos/internal";
1779
+ function matchesSecretFilter(candidate, filter) {
1780
+ const secretRef = candidate.metadata?.secretRef;
1781
+ if (filter.vault && secretRef?.vault !== filter.vault) {
1782
+ return false;
1783
+ }
1784
+ if (filter.provider && secretRef?.provider !== filter.provider) {
1785
+ return false;
1786
+ }
1787
+ return true;
1788
+ }
1409
1789
  function matchesPrefix(key, prefix) {
1410
1790
  if (!prefix) {
1411
1791
  return true;
1412
1792
  }
1413
1793
  return key.startsWith(prefix) || key.split(".").slice(1).join(".").startsWith(prefix);
1414
1794
  }
1415
- function toStoredEntry(namespace, entry) {
1795
+ function toStoredEntry(namespace, entry, filter = {}) {
1416
1796
  const sourceId = namespace === "value" ? "filesystem-values" : "filesystem-secrets";
1417
1797
  const candidates = [entry.winner, ...entry.overridden].filter((candidate) => candidate.sourceId === sourceId);
1418
1798
  if (candidates.length === 0) {
1419
1799
  return void 0;
1420
1800
  }
1801
+ const selectedCandidate = namespace === "secret" ? candidates.find((candidate) => matchesSecretFilter(candidate, filter)) : candidates[0];
1802
+ if (!selectedCandidate) {
1803
+ return void 0;
1804
+ }
1421
1805
  return {
1422
1806
  key: entry.key,
1423
- value: candidates[0]?.value
1807
+ value: selectedCandidate.value
1424
1808
  };
1425
1809
  }
1426
1810
  function listStoredNamespace(namespace, options) {
1427
1811
  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))
1812
+ (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
1813
  );
1430
1814
  }
1431
1815
  function listProjectedNamespace(namespace, options) {
1432
1816
  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]) => ({
1817
+ const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : runtime.toPublicEnv({
1818
+ ...options.framework ? {
1819
+ framework: options.framework
1820
+ } : {}
1821
+ });
1822
+ const entries = namespace === "env" ? Object.entries(projected).map(([envVar, value]) => ({
1435
1823
  key: envVar,
1436
- value: runtime.read(logicalKey)
1824
+ value
1437
1825
  })) : Object.entries(projected).map(([key, value]) => ({
1438
1826
  key: namespace === "meta" ? `meta.${key}` : key,
1439
1827
  value
@@ -1482,10 +1870,12 @@ async function runList(args = [], options = {}) {
1482
1870
  const cliArgs = [...options.cliArgs ?? []];
1483
1871
  const namespace = normalizeNamespace(args[0] ?? consumeOption(cliArgs, "--namespace"));
1484
1872
  const prefix = consumeOption(cliArgs, "--prefix");
1873
+ const framework = consumeOption(cliArgs, "--framework");
1485
1874
  const entries = await listConfigEntries(namespace, {
1486
1875
  ...options,
1487
1876
  cliArgs,
1488
- ...prefix ? { prefix } : {}
1877
+ ...prefix ? { prefix } : {},
1878
+ ...framework ? { framework } : {}
1489
1879
  });
1490
1880
  if (options.json) {
1491
1881
  return printJson(entries);
@@ -1496,35 +1886,115 @@ async function runList(args = [], options = {}) {
1496
1886
  return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1497
1887
  }
1498
1888
 
1889
+ // src/commands/migrate.ts
1890
+ import path6 from "path";
1891
+ import {
1892
+ applyManifestMappings,
1893
+ loadManifest,
1894
+ proposeMapping,
1895
+ rewriteSourceFiles,
1896
+ scanEnvUsage
1897
+ } from "@kitsy/cnos/internal";
1898
+ async function runMigrate(options = {}) {
1899
+ const cliArgs = [...options.cliArgs ?? []];
1900
+ const scan = consumeOption(cliArgs, "--scan");
1901
+ const apply = consumeFlag(cliArgs, "--apply");
1902
+ const dryRun = consumeFlag(cliArgs, "--dry-run") || !apply;
1903
+ const rewrite = consumeFlag(cliArgs, "--rewrite");
1904
+ if (cliArgs.length > 0) {
1905
+ throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
1906
+ }
1907
+ const manifest = await loadManifest(options.root ? { root: options.root } : {});
1908
+ const scanRoot = path6.resolve(manifest.repoRoot, scan ?? "src");
1909
+ const usages = await scanEnvUsage(scanRoot);
1910
+ const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
1911
+ const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
1912
+ let manifestResult;
1913
+ let rewriteResult;
1914
+ if (apply) {
1915
+ manifestResult = await applyManifestMappings(proposals, options.root);
1916
+ if (rewrite) {
1917
+ rewriteResult = await rewriteSourceFiles(usages.filter((usage) => usage.kind === "process-env"), uniqueProposals);
1918
+ }
1919
+ }
1920
+ if (options.json) {
1921
+ return printJson({
1922
+ scanRoot,
1923
+ dryRun,
1924
+ apply,
1925
+ rewrite,
1926
+ usages,
1927
+ proposals,
1928
+ ...manifestResult ? { manifest: manifestResult } : {},
1929
+ ...rewriteResult ? { rewriteResult } : {}
1930
+ });
1931
+ }
1932
+ const lines = [
1933
+ `Scanned ${usages.length} env usage${usages.length === 1 ? "" : "s"} in ${scanRoot}`,
1934
+ "",
1935
+ "Proposed mappings:",
1936
+ ...proposals.map(
1937
+ (proposal) => ` ${proposal.envVar} -> ${proposal.logicalKey}${proposal.public ? " (promote to public)" : ""}`
1938
+ )
1939
+ ];
1940
+ if (proposals.length === 0) {
1941
+ lines.push(" none");
1942
+ }
1943
+ if (dryRun) {
1944
+ lines.push("", "Dry run only. Re-run with --apply to update the manifest.");
1945
+ }
1946
+ if (manifestResult) {
1947
+ lines.push(
1948
+ "",
1949
+ `Updated ${manifestResult.manifestPath} with ${manifestResult.appliedMappings} env mapping${manifestResult.appliedMappings === 1 ? "" : "s"} and ${manifestResult.appliedPromotions} public promotion${manifestResult.appliedPromotions === 1 ? "" : "s"}.`
1950
+ );
1951
+ }
1952
+ if (rewrite) {
1953
+ if (!apply) {
1954
+ lines.push("", "Source rewrite requested but skipped because --apply was not set.");
1955
+ } else if (rewriteResult) {
1956
+ lines.push(
1957
+ "",
1958
+ `Rewrote ${rewriteResult.rewrittenFiles.length} file${rewriteResult.rewrittenFiles.length === 1 ? "" : "s"} and created ${rewriteResult.backupFiles.length} backup${rewriteResult.backupFiles.length === 1 ? "" : "s"}.`
1959
+ );
1960
+ if (rewriteResult.skippedUsages.length > 0) {
1961
+ lines.push("Skipped usages:");
1962
+ lines.push(...rewriteResult.skippedUsages.map((entry) => ` ${entry}`));
1963
+ }
1964
+ }
1965
+ }
1966
+ return lines.join("\n");
1967
+ }
1968
+
1499
1969
  // src/commands/onboard.ts
1500
- import { copyFile, readdir, rm as rm2 } from "fs/promises";
1501
- import path5 from "path";
1970
+ import { copyFile, readdir as readdir2, rm } from "fs/promises";
1971
+ import path7 from "path";
1502
1972
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
1503
1973
  async function listRootEnvFiles(root) {
1504
- const entries = await readdir(root, { withFileTypes: true });
1974
+ const entries = await readdir2(root, { withFileTypes: true });
1505
1975
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
1506
1976
  }
1507
1977
  async function runOnboard(options = {}) {
1508
- const root = path5.resolve(options.root ?? process.cwd());
1509
- const workspace = options.workspace ?? path5.basename(root);
1978
+ const root = path7.resolve(options.root ?? process.cwd());
1979
+ const workspace = options.workspace ?? path7.basename(root);
1510
1980
  const cliArgs = [...options.cliArgs ?? []];
1511
1981
  const move = consumeFlag(cliArgs, "--move");
1512
1982
  if (cliArgs.length > 0) {
1513
1983
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
1514
1984
  }
1515
1985
  const scaffold = await scaffoldWorkspace(root, workspace);
1516
- const envRoot = path5.join(root, ".cnos", "workspaces", workspace, "env");
1986
+ const envRoot = path7.join(root, ".cnos", "workspaces", workspace, "env");
1517
1987
  const rootFiles = await listRootEnvFiles(root);
1518
1988
  const imported = [];
1519
1989
  const skipped = [];
1520
1990
  for (const fileName of rootFiles) {
1521
- const sourcePath = path5.join(root, fileName);
1522
- const targetPath = path5.join(envRoot, fileName);
1991
+ const sourcePath = path7.join(root, fileName);
1992
+ const targetPath = path7.join(envRoot, fileName);
1523
1993
  try {
1524
1994
  await copyFile(sourcePath, targetPath);
1525
- imported.push(path5.relative(root, targetPath).replace(/\\/g, "/"));
1995
+ imported.push(path7.relative(root, targetPath).replace(/\\/g, "/"));
1526
1996
  if (move) {
1527
- await rm2(sourcePath);
1997
+ await rm(sourcePath);
1528
1998
  }
1529
1999
  } catch {
1530
2000
  skipped.push(fileName);
@@ -1547,17 +2017,17 @@ async function runOnboard(options = {}) {
1547
2017
  }
1548
2018
 
1549
2019
  // src/commands/profile.ts
1550
- import path8 from "path";
2020
+ import path10 from "path";
1551
2021
 
1552
2022
  // 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";
2023
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2024
+ import path8 from "path";
2025
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
1556
2026
  async function loadCliContext(root = process.cwd()) {
1557
- const filePath = path6.join(path6.resolve(root), ".cnos-workspace.yml");
2027
+ const filePath = path8.join(path8.resolve(root), ".cnos-workspace.yml");
1558
2028
  try {
1559
2029
  const source = await readFile4(filePath, "utf8");
1560
- const parsed = parseYaml2(source);
2030
+ const parsed = parseYaml3(source);
1561
2031
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1562
2032
  return {};
1563
2033
  }
@@ -1567,8 +2037,8 @@ async function loadCliContext(root = process.cwd()) {
1567
2037
  }
1568
2038
  }
1569
2039
  async function saveCliContext(options = {}) {
1570
- const root = path6.resolve(options.root ?? process.cwd());
1571
- const filePath = path6.join(root, ".cnos-workspace.yml");
2040
+ const root = path8.resolve(options.root ?? process.cwd());
2041
+ const filePath = path8.join(root, ".cnos-workspace.yml");
1572
2042
  const current = await loadCliContext(root);
1573
2043
  const next = {
1574
2044
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -1578,7 +2048,7 @@ async function saveCliContext(options = {}) {
1578
2048
  ...options.profile ? { profile: options.profile } : {},
1579
2049
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1580
2050
  };
1581
- await writeFile3(filePath, stringifyYaml2(next), "utf8");
2051
+ await writeFile4(filePath, stringifyYaml2(next), "utf8");
1582
2052
  return {
1583
2053
  filePath,
1584
2054
  context: next
@@ -1586,19 +2056,19 @@ async function saveCliContext(options = {}) {
1586
2056
  }
1587
2057
 
1588
2058
  // 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";
2059
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
2060
+ import path9 from "path";
2061
+ import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
1592
2062
  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 });
2063
+ const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2064
+ await mkdir4(path9.dirname(filePath), { recursive: true });
1595
2065
  const document = inherit && inherit !== "base" ? {
1596
2066
  name: profile,
1597
2067
  extends: [inherit]
1598
2068
  } : {
1599
2069
  name: profile
1600
2070
  };
1601
- await writeFile4(filePath, stringifyYaml3(document), "utf8");
2071
+ await writeFile5(filePath, stringifyYaml3(document), "utf8");
1602
2072
  return {
1603
2073
  filePath,
1604
2074
  profile,
@@ -1606,9 +2076,9 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1606
2076
  };
1607
2077
  }
1608
2078
  async function listProfiles(root = process.cwd()) {
1609
- const profilesRoot = path7.join(path7.resolve(root), ".cnos", "profiles");
2079
+ const profilesRoot = path9.join(path9.resolve(root), ".cnos", "profiles");
1610
2080
  try {
1611
- const entries = await readdir2(profilesRoot, { withFileTypes: true });
2081
+ const entries = await readdir3(profilesRoot, { withFileTypes: true });
1612
2082
  const discovered = /* @__PURE__ */ new Set(["base"]);
1613
2083
  for (const entry of entries) {
1614
2084
  if (entry.isFile() && entry.name.endsWith(".yml")) {
@@ -1621,9 +2091,9 @@ async function listProfiles(root = process.cwd()) {
1621
2091
  }
1622
2092
  }
1623
2093
  async function deleteProfileDefinition(root = process.cwd(), profile) {
1624
- const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2094
+ const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1625
2095
  try {
1626
- await rm3(filePath);
2096
+ await rm2(filePath);
1627
2097
  return {
1628
2098
  filePath,
1629
2099
  deleted: true
@@ -1641,9 +2111,9 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
1641
2111
  name: "base"
1642
2112
  };
1643
2113
  }
1644
- const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2114
+ const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1645
2115
  try {
1646
- return parseYaml3(await readFile5(filePath, "utf8")) ?? void 0;
2116
+ return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
1647
2117
  } catch {
1648
2118
  return void 0;
1649
2119
  }
@@ -1665,7 +2135,7 @@ function normalizeProfileAction(args) {
1665
2135
  }
1666
2136
  async function runProfile(args, options = {}) {
1667
2137
  const { action, tail } = normalizeProfileAction(args);
1668
- const root = path8.resolve(options.root ?? process.cwd());
2138
+ const root = path10.resolve(options.root ?? process.cwd());
1669
2139
  const cliArgs = [...options.cliArgs ?? []];
1670
2140
  if (action === "create") {
1671
2141
  const profile = tail[0] ?? "stage";
@@ -1708,6 +2178,73 @@ async function runProfile(args, options = {}) {
1708
2178
  return profiles.join("\n");
1709
2179
  }
1710
2180
 
2181
+ // src/commands/promote.ts
2182
+ import { writeFile as writeFile6 } from "fs/promises";
2183
+ import {
2184
+ ensureProjectionAllowed,
2185
+ loadManifest as loadManifest2,
2186
+ stringifyYaml as stringifyYaml4
2187
+ } from "@kitsy/cnos/internal";
2188
+ function normalizeTarget(value) {
2189
+ if (value === "public" || value === "env") {
2190
+ return value;
2191
+ }
2192
+ throw new Error("promote requires --to public|env");
2193
+ }
2194
+ function sortRecord(record) {
2195
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
2196
+ }
2197
+ async function runPromote(args = [], options = {}) {
2198
+ const cliArgs = [...options.cliArgs ?? []];
2199
+ const target = normalizeTarget(consumeOption(cliArgs, "--to"));
2200
+ const alias = consumeOption(cliArgs, "--as");
2201
+ const keys = args.filter(Boolean);
2202
+ if (keys.length === 0) {
2203
+ throw new Error("promote requires at least one logical key");
2204
+ }
2205
+ if (target === "env") {
2206
+ if (keys.length !== 1) {
2207
+ throw new Error("promote --to env requires exactly one logical key");
2208
+ }
2209
+ if (!alias) {
2210
+ throw new Error("promote --to env requires --as <ENV_VAR>");
2211
+ }
2212
+ }
2213
+ const loadedManifest = await loadManifest2(options.root ? { root: options.root } : {});
2214
+ for (const key of keys) {
2215
+ ensureProjectionAllowed(loadedManifest.manifest, key, target);
2216
+ }
2217
+ const rawManifest = {
2218
+ ...loadedManifest.rawManifest
2219
+ };
2220
+ if (target === "public") {
2221
+ rawManifest.public = {
2222
+ ...rawManifest.public ?? {},
2223
+ promote: Array.from(/* @__PURE__ */ new Set([...rawManifest.public?.promote ?? [], ...keys])).sort(
2224
+ (left, right) => left.localeCompare(right)
2225
+ )
2226
+ };
2227
+ } else {
2228
+ rawManifest.envMapping = {
2229
+ ...rawManifest.envMapping ?? {},
2230
+ explicit: sortRecord({
2231
+ ...rawManifest.envMapping?.explicit ?? {},
2232
+ [alias]: keys[0]
2233
+ })
2234
+ };
2235
+ }
2236
+ await writeFile6(loadedManifest.manifestPath, stringifyYaml4(rawManifest), "utf8");
2237
+ if (options.json) {
2238
+ return printJson({
2239
+ target,
2240
+ keys,
2241
+ ...target === "env" ? { envVar: alias } : {},
2242
+ manifestPath: loadedManifest.manifestPath
2243
+ });
2244
+ }
2245
+ return target === "public" ? `promoted ${keys.join(", ")} to public in ${loadedManifest.manifestPath}` : `promoted ${keys[0]} to env as ${alias} in ${loadedManifest.manifestPath}`;
2246
+ }
2247
+
1711
2248
  // src/commands/read.ts
1712
2249
  async function runRead(key, options = {}) {
1713
2250
  const runtime = await createRuntimeService(options);
@@ -1715,22 +2252,82 @@ async function runRead(key, options = {}) {
1715
2252
  if (value === void 0) {
1716
2253
  throw new Error(`Missing CNOS config key: ${key}`);
1717
2254
  }
2255
+ const isSecret = key.startsWith("secret.");
2256
+ const valueForOutput = isSecret ? maskSecretValue(value) : value;
1718
2257
  if (options.json) {
1719
- return printJson({ key, value });
2258
+ return printJson({ key, value: valueForOutput });
1720
2259
  }
1721
- return printValue(value);
2260
+ return printValue(valueForOutput);
1722
2261
  }
1723
2262
 
1724
2263
  // src/commands/run.ts
1725
2264
  import { spawn } from "child_process";
2265
+ import {
2266
+ CNOS_GRAPH_ENV_VAR,
2267
+ CNOS_SECRET_PAYLOAD_ENV_VAR,
2268
+ CNOS_SESSION_KEY_ENV_VAR,
2269
+ serializeSecretPayload,
2270
+ serializeRuntimeGraph
2271
+ } from "@kitsy/cnos/internal";
2272
+ function consumeOptions(args, flag) {
2273
+ const values = [];
2274
+ for (let index = 0; index < args.length; ) {
2275
+ const token = args[index];
2276
+ if (token === flag) {
2277
+ const value = args[index + 1];
2278
+ if (!value) {
2279
+ throw new Error(`Missing value for ${flag}`);
2280
+ }
2281
+ values.push(value);
2282
+ args.splice(index, 2);
2283
+ continue;
2284
+ }
2285
+ if (token?.startsWith(`${flag}=`)) {
2286
+ values.push(token.slice(flag.length + 1));
2287
+ args.splice(index, 1);
2288
+ continue;
2289
+ }
2290
+ index += 1;
2291
+ }
2292
+ return values;
2293
+ }
2294
+ function normalizeSetOverrides(values) {
2295
+ return values.map((value) => {
2296
+ if (!value.includes("=")) {
2297
+ throw new Error("--set requires <logical-key>=<value>");
2298
+ }
2299
+ return value.startsWith("--") ? value : `--${value}`;
2300
+ });
2301
+ }
1726
2302
  async function runCommand(command, options = {}) {
1727
2303
  if (command.length === 0) {
1728
2304
  throw new Error("run requires a command after --");
1729
2305
  }
1730
- const runtime = await createRuntimeService(options);
2306
+ const cliArgs = [...options.cliArgs ?? []];
2307
+ const isPublic = consumeFlag(cliArgs, "--public");
2308
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
2309
+ const framework = consumeOption(cliArgs, "--framework");
2310
+ const prefix = consumeOption(cliArgs, "--prefix");
2311
+ const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
2312
+ const runtime = await createRuntimeService({
2313
+ ...options,
2314
+ cliArgs: [...cliArgs, ...setOverrides]
2315
+ });
2316
+ const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
2317
+ Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
2318
+ ) : void 0;
2319
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
1731
2320
  const env = {
1732
2321
  ...process.env,
1733
- ...runtime.toEnv()
2322
+ ...isPublic ? runtime.toPublicEnv({
2323
+ ...framework ? { framework } : {},
2324
+ ...prefix ? { prefix } : {}
2325
+ }) : runtime.toEnv(),
2326
+ [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
2327
+ ...secretPayload ? {
2328
+ [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
2329
+ [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
2330
+ } : {}
1734
2331
  };
1735
2332
  return new Promise((resolve, reject) => {
1736
2333
  const executable = command[0];
@@ -1765,12 +2362,254 @@ async function runCommand(command, options = {}) {
1765
2362
  });
1766
2363
  }
1767
2364
 
1768
- // src/commands/secret.ts
1769
- function isSecretRef(value) {
1770
- return Boolean(
1771
- value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
1772
- );
2365
+ // src/services/vaults.ts
2366
+ import { rm as rm3, writeFile as writeFile7 } from "fs/promises";
2367
+ import path11 from "path";
2368
+ import {
2369
+ clearAllVaultSessionKeys,
2370
+ clearVaultSessionKey,
2371
+ createSecretVault,
2372
+ deriveVaultKey,
2373
+ loadManifest as loadManifest3,
2374
+ listSecretVaults,
2375
+ readVaultMetadata,
2376
+ resolveSecretStoreRoot as resolveSecretStoreRoot2,
2377
+ resolveVaultAuth as resolveVaultAuth2,
2378
+ resolveVaultDefinition,
2379
+ stringifyYaml as stringifyYaml5,
2380
+ writeKeychain,
2381
+ writeVaultSessionKey
2382
+ } from "@kitsy/cnos/internal";
2383
+ function sortVaults(vaults) {
2384
+ return Object.fromEntries(Object.entries(vaults).sort(([left], [right]) => left.localeCompare(right)));
2385
+ }
2386
+ function defaultLocalAuthSources(vault) {
2387
+ const token = vault.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2388
+ return [`env:CNOS_SECRET_PASSPHRASE_${token}`, "env:CNOS_SECRET_PASSPHRASE", `keychain:cnos/${vault}`, "prompt"];
2389
+ }
2390
+ async function createVaultDefinition(name, options = {}) {
2391
+ const vault = name.trim() || "default";
2392
+ const provider = options.provider?.trim() || "local";
2393
+ if (provider === "local" && (options.noPassphrase ?? false)) {
2394
+ throw new Error("Local vaults cannot be passwordless.");
2395
+ }
2396
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2397
+ const rawManifest = {
2398
+ ...loadedManifest.rawManifest,
2399
+ vaults: {
2400
+ ...loadedManifest.rawManifest.vaults ?? {},
2401
+ [vault]: provider === "local" ? {
2402
+ provider: "local",
2403
+ auth: {
2404
+ passphrase: {
2405
+ from: defaultLocalAuthSources(vault)
2406
+ }
2407
+ }
2408
+ } : {
2409
+ provider,
2410
+ auth: {
2411
+ method: "environment"
2412
+ }
2413
+ }
2414
+ }
2415
+ };
2416
+ await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2417
+ const definition = resolveVaultDefinition({ [vault]: rawManifest.vaults[vault] }, vault);
2418
+ return {
2419
+ ...definition,
2420
+ authMethod: definition.auth?.method ?? (provider === "local" ? "passphrase" : "environment"),
2421
+ localStore: provider === "local",
2422
+ manifestPath: loadedManifest.manifestPath
2423
+ };
2424
+ }
2425
+ async function listVaultDefinitions(options = {}) {
2426
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2427
+ const localStoreVaults = await listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
2428
+ return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
2429
+ const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
2430
+ return {
2431
+ ...definition,
2432
+ authMethod: definition.auth?.method ?? (definition.provider === "local" ? "passphrase" : "environment"),
2433
+ localStore: localStoreVaults.includes(name)
2434
+ };
2435
+ });
2436
+ }
2437
+ async function removeVaultDefinition(name, options = {}) {
2438
+ const vault = name.trim() || "default";
2439
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2440
+ if (!loadedManifest.rawManifest.vaults?.[vault]) {
2441
+ return {
2442
+ name: vault,
2443
+ deleted: false,
2444
+ manifestPath: loadedManifest.manifestPath
2445
+ };
2446
+ }
2447
+ const nextVaults = { ...loadedManifest.rawManifest.vaults ?? {} };
2448
+ delete nextVaults[vault];
2449
+ const rawManifest = {
2450
+ ...loadedManifest.rawManifest,
2451
+ ...Object.keys(nextVaults).length > 0 ? { vaults: sortVaults(nextVaults) } : {}
2452
+ };
2453
+ if (Object.keys(nextVaults).length === 0) {
2454
+ delete rawManifest.vaults;
2455
+ }
2456
+ await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2457
+ const vaultRoot = path11.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
2458
+ let removedStore;
2459
+ try {
2460
+ await rm3(vaultRoot, { recursive: true, force: true });
2461
+ removedStore = vaultRoot;
2462
+ } catch {
2463
+ removedStore = void 0;
2464
+ }
2465
+ await clearVaultSessionKey(vault, options.processEnv);
2466
+ return {
2467
+ name: vault,
2468
+ deleted: true,
2469
+ manifestPath: loadedManifest.manifestPath,
2470
+ ...removedStore ? { removedStore } : {}
2471
+ };
2472
+ }
2473
+ async function listLocalStoreVaults(options = {}) {
2474
+ return listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
2475
+ }
2476
+ async function authenticateVault(name, options = {}) {
2477
+ const vault = name.trim() || "default";
2478
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2479
+ const definition = loadedManifest.manifest.vaults[vault];
2480
+ if (!definition) {
2481
+ throw new Error(`Unknown vault "${vault}"`);
2482
+ }
2483
+ const auth = await resolveVaultAuth2(vault, definition, options.processEnv ?? process.env);
2484
+ const storeRoot = resolveSecretStoreRoot2(options.processEnv);
2485
+ if (definition.provider === "local") {
2486
+ if (!auth.passphrase) {
2487
+ throw new Error(`Vault "${vault}" requires passphrase-based authentication.`);
2488
+ }
2489
+ const existing = await readVaultMetadata(storeRoot, vault);
2490
+ if (!existing) {
2491
+ await createSecretVault(storeRoot, vault, auth.passphrase);
2492
+ }
2493
+ const metadata = await readVaultMetadata(storeRoot, vault);
2494
+ if (!metadata) {
2495
+ throw new Error(`Failed to initialize vault "${vault}"`);
2496
+ }
2497
+ const derivedKey = deriveVaultKey(auth.passphrase, Buffer.from(metadata.salt, "base64"), metadata.iterations);
2498
+ const sessionPath2 = await writeVaultSessionKey(vault, derivedKey, options.processEnv);
2499
+ if (options.storeKeychain) {
2500
+ await writeKeychain(`cnos/${vault}`, derivedKey.toString("hex"));
2501
+ }
2502
+ return {
2503
+ name: vault,
2504
+ method: auth.method,
2505
+ storedInKeychain: options.storeKeychain ?? false,
2506
+ sessionPath: sessionPath2
2507
+ };
2508
+ }
2509
+ const sessionPath = await writeVaultSessionKey(vault, Buffer.from(vault, "utf8"), options.processEnv);
2510
+ return {
2511
+ name: vault,
2512
+ method: auth.method,
2513
+ storedInKeychain: false,
2514
+ sessionPath
2515
+ };
2516
+ }
2517
+ async function logoutVault(name, options = {}) {
2518
+ if (options.all) {
2519
+ await clearAllVaultSessionKeys(options.processEnv);
2520
+ return { scope: "all" };
2521
+ }
2522
+ const vault = name?.trim() || "default";
2523
+ await clearVaultSessionKey(vault, options.processEnv);
2524
+ return { scope: vault };
2525
+ }
2526
+
2527
+ // src/commands/vault.ts
2528
+ function normalizeVaultAction(args) {
2529
+ const [action = "list", ...tail] = args;
2530
+ if (["create", "add", "list", "delete", "remove", "auth", "logout"].includes(action)) {
2531
+ return {
2532
+ action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : action === "auth" ? "auth" : action === "logout" ? "logout" : "list",
2533
+ tail
2534
+ };
2535
+ }
2536
+ return {
2537
+ action: "list",
2538
+ tail: args
2539
+ };
2540
+ }
2541
+ async function runVault(args = [], options = {}) {
2542
+ const { action, tail } = normalizeVaultAction(args);
2543
+ const cliArgs = [...options.cliArgs ?? []];
2544
+ if (consumeOption(cliArgs, "--passphrase")) {
2545
+ throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
2546
+ }
2547
+ if (action === "create") {
2548
+ const name = tail[0] ?? "default";
2549
+ const provider = consumeOption(cliArgs, "--provider") ?? "local";
2550
+ const noPassphrase = consumeFlag(cliArgs, "--no-passphrase");
2551
+ const result = await createVaultDefinition(name, {
2552
+ ...options,
2553
+ cliArgs,
2554
+ provider,
2555
+ ...noPassphrase ? { noPassphrase: true } : {}
2556
+ });
2557
+ if (options.json) {
2558
+ return printJson(result);
2559
+ }
2560
+ return `created vault "${result.name}" with provider "${result.provider}" in ${result.manifestPath}`;
2561
+ }
2562
+ if (action === "auth") {
2563
+ const result = await authenticateVault(tail[0] ?? "default", {
2564
+ ...options,
2565
+ cliArgs,
2566
+ storeKeychain: consumeFlag(cliArgs, "--store-keychain")
2567
+ });
2568
+ if (options.json) {
2569
+ return printJson(result);
2570
+ }
2571
+ return `authenticated vault "${result.name}" via ${result.method}`;
2572
+ }
2573
+ if (action === "logout") {
2574
+ const result = await logoutVault(tail[0], {
2575
+ ...options,
2576
+ cliArgs,
2577
+ all: consumeFlag(cliArgs, "--all")
2578
+ });
2579
+ if (options.json) {
2580
+ return printJson(result);
2581
+ }
2582
+ return result.scope === "all" ? "logged out all vault sessions" : `logged out vault "${result.scope}"`;
2583
+ }
2584
+ if (action === "remove") {
2585
+ const name = tail[0] ?? "default";
2586
+ const result = await removeVaultDefinition(name, options);
2587
+ if (options.json) {
2588
+ return printJson(result);
2589
+ }
2590
+ return result.deleted ? `removed vault "${result.name}"` : `vault "${result.name}" was not found`;
2591
+ }
2592
+ const [manifestVaults, localStoreVaults] = await Promise.all([
2593
+ listVaultDefinitions(options),
2594
+ listLocalStoreVaults(options)
2595
+ ]);
2596
+ if (options.json) {
2597
+ return printJson(
2598
+ manifestVaults.map((vault) => ({
2599
+ ...vault,
2600
+ localStore: localStoreVaults.includes(vault.name)
2601
+ }))
2602
+ );
2603
+ }
2604
+ if (manifestVaults.length === 0) {
2605
+ return "";
2606
+ }
2607
+ return manifestVaults.map(
2608
+ (vault) => `${vault.name} provider=${vault.provider} auth=${vault.authMethod}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
2609
+ ).join("\n");
1773
2610
  }
2611
+
2612
+ // src/commands/secret.ts
1774
2613
  function normalizeSecretCommand(args) {
1775
2614
  const [actionOrPath, next, ...tail] = args;
1776
2615
  if (!actionOrPath) {
@@ -1796,64 +2635,78 @@ function normalizeSecretCommand(args) {
1796
2635
  tail: args
1797
2636
  };
1798
2637
  }
2638
+ async function readStdinValue() {
2639
+ const chunks = [];
2640
+ for await (const chunk of process.stdin) {
2641
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2642
+ }
2643
+ return Buffer.concat(chunks).toString("utf8").trimEnd();
2644
+ }
1799
2645
  async function runSecret(argsOrPath, options = {}) {
1800
2646
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
1801
2647
  const { action, tail } = normalizeSecretCommand(args);
1802
2648
  const cliArgs = [...options.cliArgs ?? []];
2649
+ if (consumeOption(cliArgs, "--passphrase")) {
2650
+ throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
2651
+ }
1803
2652
  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}`;
2653
+ return runVault(["create", tail[0] ?? "default"], options);
1815
2654
  }
1816
2655
  if (action === "list") {
2656
+ const runtime2 = await createRuntimeService(options);
1817
2657
  const prefix = consumeOption(cliArgs, "--prefix");
1818
- const entries = await listConfigEntries("secret", {
1819
- ...options,
1820
- cliArgs,
1821
- ...prefix ? { prefix } : {}
1822
- });
2658
+ const vault = consumeOption(cliArgs, "--vault");
2659
+ const provider = consumeOption(cliArgs, "--provider");
2660
+ const entries = Array.from(runtime2.graph.entries.values()).filter((entry2) => entry2.namespace === "secret").filter((entry2) => !prefix || entry2.key.startsWith(`secret.${prefix}`) || entry2.key.startsWith(prefix)).filter((entry2) => {
2661
+ const secretRef2 = entry2.winner.metadata?.secretRef;
2662
+ if (vault && secretRef2?.vault !== vault) {
2663
+ return false;
2664
+ }
2665
+ if (provider && secretRef2?.provider !== provider) {
2666
+ return false;
2667
+ }
2668
+ return true;
2669
+ }).map((entry2) => {
2670
+ const secretRef2 = entry2.winner.metadata?.secretRef;
2671
+ return {
2672
+ key: entry2.key,
2673
+ vault: secretRef2?.vault ?? "default",
2674
+ provider: secretRef2?.provider ?? "local"
2675
+ };
2676
+ }).sort((left, right) => left.key.localeCompare(right.key));
1823
2677
  if (options.json) {
1824
2678
  return printJson(entries);
1825
2679
  }
1826
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
2680
+ return entries.map((entry2) => `${entry2.key} (vault: ${entry2.vault}, provider: ${entry2.provider})`).join("\n");
1827
2681
  }
1828
2682
  if (action === "set") {
1829
- const secretPath = tail[0];
1830
- const rawValue = tail[1] ?? "";
2683
+ const secretPath2 = tail[0];
1831
2684
  const local = consumeFlag(cliArgs, "--local");
1832
2685
  const remote = consumeFlag(cliArgs, "--remote");
1833
2686
  const ref = consumeFlag(cliArgs, "--ref");
2687
+ const stdin = consumeFlag(cliArgs, "--stdin");
1834
2688
  const target = consumeOption(cliArgs, "--target") ?? "local";
1835
2689
  const provider = consumeOption(cliArgs, "--provider");
1836
- const passphrase = consumeOption(cliArgs, "--passphrase");
1837
2690
  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, {
2691
+ const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
2692
+ const rawValue = stdin ? await readStdinValue() : tail[1] ?? "";
2693
+ const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
1840
2694
  ...options,
1841
2695
  cliArgs,
1842
2696
  target,
1843
- mode,
1844
2697
  vault,
1845
- ...provider ? { provider } : {},
1846
- ...passphrase ? { passphrase } : {}
2698
+ ...mode ? { mode } : {},
2699
+ ...provider ? { provider } : {}
1847
2700
  });
1848
2701
  if (options.json) {
1849
2702
  return printJson(result);
1850
2703
  }
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}`;
2704
+ 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
2705
  }
1853
2706
  if (action === "delete") {
1854
- const secretPath = tail[0];
2707
+ const secretPath2 = tail[0];
1855
2708
  const target = consumeOption(cliArgs, "--target") ?? "local";
1856
- const result = await deleteSecret(secretPath ?? "app.token", {
2709
+ const result = await deleteSecret(secretPath2 ?? "app.token", {
1857
2710
  ...options,
1858
2711
  cliArgs,
1859
2712
  target
@@ -1861,32 +2714,36 @@ async function runSecret(argsOrPath, options = {}) {
1861
2714
  if (options.json) {
1862
2715
  return printJson(result);
1863
2716
  }
1864
- return result.deleted ? `deleted secret.${secretPath} from ${result.filePath}` : `no secret.${secretPath} found in ${result.filePath}`;
2717
+ return result.deleted ? `deleted secret.${secretPath2} from ${result.filePath}` : `no secret.${secretPath2} found in ${result.filePath}`;
1865
2718
  }
1866
2719
  const runtime = await createRuntimeService(options);
1867
- const value = runtime.secret(tail[0] ?? "app.token");
2720
+ const secretPath = tail[0] ?? "app.token";
2721
+ const expectedVault = consumeOption(cliArgs, "--vault");
2722
+ const reveal = consumeFlag(cliArgs, "--reveal");
2723
+ const entry = runtime.graph.entries.get(`secret.${secretPath}`);
2724
+ const secretRef = entry?.winner.metadata?.secretRef;
2725
+ const value = runtime.secret(secretPath);
1868
2726
  if (value === void 0) {
1869
- throw new Error(`Missing CNOS secret path: ${tail[0] ?? "app.token"}`);
2727
+ throw new Error(`Missing CNOS secret path: ${secretPath}`);
1870
2728
  }
1871
- 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
- );
2729
+ if (expectedVault && secretRef?.vault && secretRef.vault !== expectedVault) {
2730
+ throw new Error(`Secret ${secretPath} belongs to vault "${secretRef.vault}", not "${expectedVault}"`);
1876
2731
  }
2732
+ const valueForOutput = reveal ? value : maskSecretValue(value);
1877
2733
  if (options.json) {
1878
2734
  return printJson({
1879
- key: `secret.${tail[0] ?? "app.token"}`,
1880
- value
2735
+ key: `secret.${secretPath}`,
2736
+ value: valueForOutput,
2737
+ vault: secretRef?.vault ?? "default"
1881
2738
  });
1882
2739
  }
1883
- return printValue(value);
2740
+ return printValue(valueForOutput);
1884
2741
  }
1885
2742
 
1886
2743
  // src/commands/use.ts
1887
- import path9 from "path";
2744
+ import path12 from "path";
1888
2745
  async function runUse(args = [], options = {}) {
1889
- const root = path9.resolve(options.root ?? process.cwd());
2746
+ const root = path12.resolve(options.root ?? process.cwd());
1890
2747
  const action = args[0] ?? "show";
1891
2748
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
1892
2749
  if (action === "show" || !hasUpdates) {
@@ -1923,7 +2780,7 @@ async function runValidate(options = {}) {
1923
2780
  // package.json
1924
2781
  var package_default = {
1925
2782
  name: "@kitsy/cnos-cli",
1926
- version: "1.1.2",
2783
+ version: "1.3.0",
1927
2784
  description: "CLI entry point and developer tooling for CNOS.",
1928
2785
  type: "module",
1929
2786
  main: "./dist/index.js",
@@ -2060,6 +2917,180 @@ async function runValue(argsOrPath, options = {}) {
2060
2917
  return printValue(value);
2061
2918
  }
2062
2919
 
2920
+ // src/commands/watch.ts
2921
+ import { watch } from "fs";
2922
+ import { spawn as spawn2 } from "child_process";
2923
+ import {
2924
+ CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
2925
+ CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
2926
+ CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
2927
+ diffGraphs,
2928
+ serializeRuntimeGraph as serializeRuntimeGraph2,
2929
+ serializeSecretPayload as serializeSecretPayload2,
2930
+ watchFiles
2931
+ } from "@kitsy/cnos/internal";
2932
+ async function buildRunEnvironment(options) {
2933
+ const cliArgs = [...options.cliArgs ?? []];
2934
+ const isPublic = consumeFlag(cliArgs, "--public");
2935
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
2936
+ const framework = consumeOption(cliArgs, "--framework");
2937
+ const prefix = consumeOption(cliArgs, "--prefix");
2938
+ const runtime = await createRuntimeService({
2939
+ ...options,
2940
+ cliArgs
2941
+ });
2942
+ const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
2943
+ Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
2944
+ ) : void 0;
2945
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
2946
+ return {
2947
+ runtime,
2948
+ env: {
2949
+ ...process.env,
2950
+ ...isPublic ? runtime.toPublicEnv({
2951
+ ...framework ? { framework } : {},
2952
+ ...prefix ? { prefix } : {}
2953
+ }) : runtime.toEnv(),
2954
+ [CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
2955
+ ...secretPayload ? {
2956
+ [CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
2957
+ [CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
2958
+ } : {}
2959
+ }
2960
+ };
2961
+ }
2962
+ function spawnWatchedChild(command, cwd, env) {
2963
+ const executable = command[0];
2964
+ if (!executable) {
2965
+ throw new Error("watch requires a command after -- unless --signal is used");
2966
+ }
2967
+ return spawn2(executable, command.slice(1), {
2968
+ cwd,
2969
+ env,
2970
+ stdio: "inherit",
2971
+ shell: false
2972
+ });
2973
+ }
2974
+ async function startWatchLoop(options) {
2975
+ const cliArgs = [...options.cliArgs ?? []];
2976
+ const isSignal = consumeFlag(cliArgs, "--signal");
2977
+ const debounceMs = Number(consumeOption(cliArgs, "--debounce") ?? "300");
2978
+ const command = options.command ?? [];
2979
+ const root = options.root ?? process.cwd();
2980
+ let current = await buildRunEnvironment({
2981
+ ...options,
2982
+ cliArgs
2983
+ });
2984
+ let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
2985
+ const watcherMap = /* @__PURE__ */ new Map();
2986
+ let timer;
2987
+ let closed = false;
2988
+ const attachWatcher = (targetPath, recursive = false) => {
2989
+ if (watcherMap.has(targetPath)) {
2990
+ return;
2991
+ }
2992
+ try {
2993
+ const watcher = watch(
2994
+ targetPath,
2995
+ recursive ? {
2996
+ recursive: true
2997
+ } : void 0,
2998
+ () => {
2999
+ if (timer) {
3000
+ clearTimeout(timer);
3001
+ }
3002
+ timer = setTimeout(() => {
3003
+ void handleChange();
3004
+ }, debounceMs);
3005
+ }
3006
+ );
3007
+ watcherMap.set(targetPath, watcher);
3008
+ } catch {
3009
+ if (recursive) {
3010
+ attachWatcher(targetPath, false);
3011
+ }
3012
+ }
3013
+ };
3014
+ const refreshWatchers = async () => {
3015
+ const nextTargets = await watchFiles(current.runtime, options.root);
3016
+ attachWatcher(nextTargets.manifestPath, false);
3017
+ for (const workspaceRoot of nextTargets.roots) {
3018
+ attachWatcher(workspaceRoot, true);
3019
+ }
3020
+ for (const filePath of nextTargets.files) {
3021
+ attachWatcher(filePath, false);
3022
+ }
3023
+ };
3024
+ const handleChange = async () => {
3025
+ if (closed) {
3026
+ return;
3027
+ }
3028
+ const next = await buildRunEnvironment({
3029
+ ...options,
3030
+ cliArgs
3031
+ });
3032
+ const changedKeys = diffGraphs(current.runtime.graph, next.runtime.graph);
3033
+ current = next;
3034
+ await refreshWatchers();
3035
+ if (changedKeys.length === 0) {
3036
+ return;
3037
+ }
3038
+ if (isSignal) {
3039
+ await options.onSignal?.({ changedKeys });
3040
+ process.stdout.write(`${printJson({ changedKeys })}
3041
+ `);
3042
+ return;
3043
+ }
3044
+ if (child && !child.killed) {
3045
+ await new Promise((resolve) => {
3046
+ child?.once("close", () => resolve());
3047
+ child?.kill();
3048
+ });
3049
+ }
3050
+ child = spawnWatchedChild(command, root, current.env);
3051
+ await options.onRestart?.({ changedKeys });
3052
+ };
3053
+ await refreshWatchers();
3054
+ return {
3055
+ async close() {
3056
+ closed = true;
3057
+ if (timer) {
3058
+ clearTimeout(timer);
3059
+ }
3060
+ for (const watcher of watcherMap.values()) {
3061
+ watcher.close();
3062
+ }
3063
+ watcherMap.clear();
3064
+ if (child && !child.killed) {
3065
+ await new Promise((resolve) => {
3066
+ child?.once("close", () => resolve());
3067
+ child?.kill();
3068
+ });
3069
+ }
3070
+ }
3071
+ };
3072
+ }
3073
+ async function runWatch(command, options = {}) {
3074
+ const cliArgs = [...options.cliArgs ?? []];
3075
+ const isSignal = consumeFlag(cliArgs, "--signal");
3076
+ const debounce = consumeOption(cliArgs, "--debounce");
3077
+ const handle = await startWatchLoop({
3078
+ ...options,
3079
+ cliArgs: [
3080
+ ...cliArgs,
3081
+ ...isSignal ? ["--signal"] : [],
3082
+ ...debounce ? ["--debounce", debounce] : []
3083
+ ],
3084
+ command
3085
+ });
3086
+ const closeWatcher = () => {
3087
+ void handle.close();
3088
+ };
3089
+ process.once("SIGINT", closeWatcher);
3090
+ process.once("SIGTERM", closeWatcher);
3091
+ return isSignal ? "watching config changes in signal mode" : "watching config changes in restart mode";
3092
+ }
3093
+
2063
3094
  // src/index.ts
2064
3095
  function resolveHelpTopic(command, args) {
2065
3096
  if (command === "help" || command === "help-ai") {
@@ -2068,6 +3099,9 @@ function resolveHelpTopic(command, args) {
2068
3099
  if (command === "export" && args[0] === "env") {
2069
3100
  return normalizeHelpTopic([command, args[0]]);
2070
3101
  }
3102
+ if (command === "vault" && args[0] && ["create", "add", "list", "delete", "remove"].includes(args[0])) {
3103
+ return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0] === "add" ? "create" : args[0]]);
3104
+ }
2071
3105
  if (command === "secret" && args[0] && ["set", "create", "add", "list", "delete", "remove"].includes(args[0])) {
2072
3106
  if ((args[0] === "create" || args[0] === "add") && args[1] === "vault") {
2073
3107
  return normalizeHelpTopic(["secret", "create", "vault"]);
@@ -2137,6 +3171,14 @@ async function main(argv) {
2137
3171
  return;
2138
3172
  case "onboard":
2139
3173
  process.stdout.write(`${await runOnboard(runtimeOptions)}
3174
+ `);
3175
+ return;
3176
+ case "migrate":
3177
+ process.stdout.write(`${await runMigrate(runtimeOptions)}
3178
+ `);
3179
+ return;
3180
+ case "codegen":
3181
+ process.stdout.write(`${await runCodegen(runtimeOptions)}
2140
3182
  `);
2141
3183
  return;
2142
3184
  case "read":
@@ -2149,6 +3191,10 @@ async function main(argv) {
2149
3191
  return;
2150
3192
  case "secret":
2151
3193
  process.stdout.write(`${await runSecret(args.length > 0 ? args : ["app.token"], runtimeOptions)}
3194
+ `);
3195
+ return;
3196
+ case "vault":
3197
+ process.stdout.write(`${await runVault(args, runtimeOptions)}
2152
3198
  `);
2153
3199
  return;
2154
3200
  case "use":
@@ -2157,6 +3203,10 @@ async function main(argv) {
2157
3203
  return;
2158
3204
  case "profile":
2159
3205
  process.stdout.write(`${await runProfile(args, runtimeOptions)}
3206
+ `);
3207
+ return;
3208
+ case "promote":
3209
+ process.stdout.write(`${await runPromote(args, runtimeOptions)}
2160
3210
  `);
2161
3211
  return;
2162
3212
  case "list":
@@ -2193,8 +3243,16 @@ async function main(argv) {
2193
3243
  process.exitCode = result.exitCode;
2194
3244
  return;
2195
3245
  }
3246
+ case "watch":
3247
+ process.stdout.write(`${await runWatch(passthrough.length > 0 ? passthrough : args, runtimeOptions)}
3248
+ `);
3249
+ return;
2196
3250
  case "diff":
2197
3251
  process.stdout.write(`${await runDiff(args[0] ?? "local", args[1] ?? "stage", runtimeOptions)}
3252
+ `);
3253
+ return;
3254
+ case "drift":
3255
+ process.stdout.write(`${await runDrift(runtimeOptions)}
2198
3256
  `);
2199
3257
  return;
2200
3258
  case "doctor":