@kitsy/cnos-cli 1.2.0 → 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 +885 -310
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ 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",
@@ -18,13 +19,34 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
18
19
  "--vault",
19
20
  "--inherit",
20
21
  "--as",
21
- "--set"
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"
22
42
  ]);
23
- var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public", "--local", "--remote", "--ref", "--no-passphrase"]);
24
43
  function normalizeCommand(argv) {
25
44
  const [command = "doctor", ...rest] = argv;
26
45
  const resource = rest[0];
27
46
  const remaining = rest.slice(1);
47
+ if ((command === "set" || command === "get") && (resource === "value" || resource === "secret")) {
48
+ return [resource, command, ...remaining];
49
+ }
28
50
  if ((command === "create" || command === "add") && resource === "profile") {
29
51
  return ["profile", "create", ...remaining];
30
52
  }
@@ -37,6 +59,12 @@ function normalizeCommand(argv) {
37
59
  if ((command === "delete" || command === "remove") && resource === "vault") {
38
60
  return ["vault", "remove", ...remaining];
39
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
+ }
40
68
  if (command === "list" && resource === "profile") {
41
69
  return ["profile", "list", ...remaining];
42
70
  }
@@ -197,166 +225,29 @@ function printJson(value) {
197
225
  }
198
226
 
199
227
  // src/services/writes.ts
200
- import { mkdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
201
- import path2 from "path";
228
+ import { mkdir, readFile, writeFile } from "fs/promises";
229
+ import path from "path";
202
230
  import {
203
- getVaultPassphraseEnvVar,
231
+ createSecretVaultProvider,
204
232
  parseYaml,
205
233
  resolveConfigDocumentPath,
206
- stringifyYaml as stringifyYaml2,
207
- writeLocalSecret,
208
- resolveConfiguredVaultPassphrase as resolveConfiguredVaultPassphrase2,
209
- resolveSecretStoreRoot as resolveSecretStoreRoot2
234
+ resolveVaultAuth,
235
+ stringifyYaml
210
236
  } from "@kitsy/cnos/internal";
211
237
 
212
238
  // src/services/runtime.ts
213
- import { createCnos } from "@kitsy/cnos";
239
+ import { createCnos } from "@kitsy/cnos/configure";
214
240
  async function createRuntimeService(options = {}) {
215
- const createOptions = {
216
- ...options.root ? {
217
- root: options.root
218
- } : {},
219
- ...options.workspace ? {
220
- workspace: options.workspace
221
- } : {},
222
- ...options.profile ? {
223
- profile: options.profile
224
- } : {},
225
- ...options.globalRoot ? {
226
- globalRoot: options.globalRoot
227
- } : {},
228
- ...options.cliArgs && options.cliArgs.length > 0 ? {
229
- cliArgs: options.cliArgs
230
- } : {},
231
- ...options.processEnv ? {
232
- processEnv: options.processEnv
233
- } : {
234
- processEnv: process.env
235
- }
236
- };
237
241
  return createCnos({
238
- ...createOptions
239
- });
240
- }
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
- };
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
316
249
  });
317
250
  }
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
251
 
361
252
  // src/services/writes.ts
362
253
  function setNestedValue(target, pathSegments, value) {
@@ -405,7 +296,7 @@ function isSecretReference(value) {
405
296
  }
406
297
  async function readYamlDocument(filePath) {
407
298
  try {
408
- return parseYaml(await readFile2(filePath, "utf8")) ?? {};
299
+ return parseYaml(await readFile(filePath, "utf8")) ?? {};
409
300
  } catch {
410
301
  return {};
411
302
  }
@@ -433,7 +324,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
433
324
  filePath: secret.filePath,
434
325
  value: {
435
326
  provider: secret.provider,
436
- ref: secret.ref
327
+ ref: secret.ref,
328
+ ...secret.vault ? { vault: secret.vault } : {}
437
329
  }
438
330
  };
439
331
  }
@@ -444,8 +336,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
444
336
  const document = await readYamlDocument(filePath);
445
337
  const parsedValue = parseScalarValue(rawValue);
446
338
  setNestedValue(document, configPath.split("."), parsedValue);
447
- await mkdir(path2.dirname(filePath), { recursive: true });
448
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
339
+ await mkdir(path.dirname(filePath), { recursive: true });
340
+ await writeFile(filePath, stringifyYaml(document), "utf8");
449
341
  return {
450
342
  filePath,
451
343
  value: parsedValue
@@ -459,53 +351,36 @@ async function setSecret(configPath, rawValue, options = {}) {
459
351
  const document = await readYamlDocument(filePath);
460
352
  const vault = options.vault?.trim() || "default";
461
353
  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");
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");
464
358
  let reference;
465
- let storePath;
466
359
  if (mode === "local") {
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;
481
- if (!passphrase) {
482
- throw new Error(`Vault "${vault}" requires --passphrase or its configured passphrase env var`);
483
- }
484
- const ref = configPath;
485
- storePath = await writeLocalSecret(resolveSecretStoreRoot2(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);
486
364
  reference = {
487
365
  provider: "local",
488
- ref,
366
+ ref: configPath,
489
367
  vault
490
368
  };
491
369
  } else {
492
370
  reference = {
493
- provider: inferredProvider ?? (mode === "ref" ? "ref" : "remote"),
371
+ provider: options.provider?.trim() || vaultDefinition.provider,
494
372
  ref: rawValue || configPath.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(),
495
- ...vaultDefinition || vault !== "default" ? {
496
- vault
497
- } : {}
373
+ vault
498
374
  };
499
375
  }
500
376
  setNestedValue(document, configPath.split("."), reference);
501
- await mkdir(path2.dirname(filePath), { recursive: true });
502
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
377
+ await mkdir(path.dirname(filePath), { recursive: true });
378
+ await writeFile(filePath, stringifyYaml(document), "utf8");
503
379
  return {
504
380
  filePath,
505
381
  provider: reference.provider,
506
382
  ref: reference.ref,
507
- ...reference.vault ? { vault: reference.vault } : {},
508
- ...storePath ? { storePath } : {}
383
+ ...reference.vault ? { vault: reference.vault } : {}
509
384
  };
510
385
  }
511
386
  async function deleteSecret(configPath, options = {}) {
@@ -522,24 +397,20 @@ async function deleteSecret(configPath, options = {}) {
522
397
  deleted: false
523
398
  };
524
399
  }
525
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
526
- let removedStore;
400
+ await writeFile(filePath, stringifyYaml(document), "utf8");
527
401
  const secretRef = metadata?.secretRef;
528
402
  if (isSecretReference(secretRef) && secretRef.provider === "local") {
529
- const storePath = path2.join(
530
- resolveSecretStoreRoot2(options.processEnv),
531
- "vaults",
532
- secretRef.vault ?? "default",
533
- "store",
534
- ...secretRef.ref.split("/")
535
- ).concat(".json");
536
- await rm2(storePath, { force: true });
537
- 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
+ }
538
410
  }
539
411
  return {
540
412
  filePath,
541
- deleted: true,
542
- ...removedStore ? { removedStore } : {}
413
+ deleted: true
543
414
  };
544
415
  }
545
416
  async function deleteValue(namespace, configPath, options = {}) {
@@ -558,7 +429,7 @@ async function deleteValue(namespace, configPath, options = {}) {
558
429
  deleted: false
559
430
  };
560
431
  }
561
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
432
+ await writeFile(filePath, stringifyYaml(document), "utf8");
562
433
  return {
563
434
  filePath,
564
435
  deleted: true
@@ -596,6 +467,17 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
596
467
  return `defined ${namespace}.${configPath} in ${result.filePath}`;
597
468
  }
598
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
+
599
481
  // src/commands/diff.ts
600
482
  import { flattenObject } from "@kitsy/cnos/internal";
601
483
  function flattenRuntime(runtime) {
@@ -634,8 +516,15 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
634
516
  }
635
517
 
636
518
  // src/services/doctor.ts
637
- import { readFile as readFile3 } from "fs/promises";
638
- import path3 from "path";
519
+ import { readdir, readFile as readFile2 } from "fs/promises";
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";
639
528
 
640
529
  // src/services/validation.ts
641
530
  import { validateRuntime } from "@kitsy/cnos/internal";
@@ -650,7 +539,7 @@ async function createValidationSummary(options = {}) {
650
539
 
651
540
  // src/services/doctor.ts
652
541
  async function checkGitignore(root) {
653
- const gitignorePath = path3.join(root, ".gitignore");
542
+ const gitignorePath = path2.join(root, ".gitignore");
654
543
  const expected = [
655
544
  ".cnos/env/.env",
656
545
  ".cnos/env/.env.*",
@@ -662,7 +551,7 @@ async function checkGitignore(root) {
662
551
  "!.cnos/workspaces/*/env/.env.*.example"
663
552
  ];
664
553
  try {
665
- const content = await readFile3(gitignorePath, "utf8");
554
+ const content = await readFile2(gitignorePath, "utf8");
666
555
  const missing = expected.filter((entry) => !content.includes(entry));
667
556
  return {
668
557
  name: "gitignore",
@@ -680,8 +569,72 @@ async function checkGitignore(root) {
680
569
  function issueSummary(issues) {
681
570
  return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
682
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
+ }
683
636
  async function evaluateDoctor(options = {}) {
684
- const root = path3.resolve(options.root ?? process.cwd());
637
+ const root = path2.resolve(options.root ?? process.cwd());
685
638
  const { runtime, summary } = await createValidationSummary(options);
686
639
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
687
640
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
@@ -711,6 +664,7 @@ async function evaluateDoctor(options = {}) {
711
664
  ok: !runtime.manifest.workspaces.global.enabled || Boolean(runtime.graph.workspace.globalRoot),
712
665
  details: runtime.manifest.workspaces.global.enabled ? runtime.graph.workspace.globalRoot ? `enabled at ${runtime.graph.workspace.globalRoot}` : "enabled but no global root resolved" : "disabled"
713
666
  },
667
+ await checkSecretSecurity(options, runtime),
714
668
  await checkGitignore(root)
715
669
  ];
716
670
  }
@@ -729,7 +683,7 @@ async function runDoctor(options = {}) {
729
683
  }
730
684
 
731
685
  // src/commands/dump.ts
732
- import { writeDump } from "@kitsy/cnos";
686
+ import { writeDump } from "@kitsy/cnos/configure";
733
687
  async function runDump(options = {}) {
734
688
  const cliArgs = [...options.cliArgs ?? []];
735
689
  const flatten = consumeFlag(cliArgs, "--flatten");
@@ -751,9 +705,51 @@ async function runDump(options = {}) {
751
705
  return `dumped ${result.files.length} files to ${result.root}`;
752
706
  }
753
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
+
754
750
  // src/commands/exportEnv.ts
755
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
756
- import path4 from "path";
751
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
752
+ import path3 from "path";
757
753
  function formatEnvOutput(env) {
758
754
  return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
759
755
  }
@@ -773,9 +769,9 @@ async function runExportEnv(options = {}) {
773
769
  }) : runtime.toEnv();
774
770
  const output = formatEnvOutput(env);
775
771
  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");
772
+ const targetPath = path3.resolve(options.root ?? process.cwd(), to);
773
+ await mkdir2(path3.dirname(targetPath), { recursive: true });
774
+ await writeFile2(targetPath, output, "utf8");
779
775
  if (options.json) {
780
776
  return printJson({
781
777
  to: targetPath,
@@ -852,6 +848,23 @@ var COMMANDS = [
852
848
  ],
853
849
  examples: ["cnos onboard", "cnos onboard --workspace webapp", "cnos onboard --root ../my-app --workspace app --move"]
854
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
+ },
855
868
  {
856
869
  id: "read",
857
870
  summary: "Read any fully-qualified CNOS key.",
@@ -943,18 +956,19 @@ var COMMANDS = [
943
956
  flag: "--provider <name>",
944
957
  description: "Provider name for --remote or --ref secret writes."
945
958
  },
946
- {
947
- flag: "--passphrase <value>",
948
- description: "Passphrase used to encrypt local secret material when --local is selected."
949
- },
950
959
  {
951
960
  flag: "--vault <name>",
952
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."
953
966
  }
954
967
  ],
955
968
  examples: [
956
969
  "cnos secret app.token",
957
- "cnos vault create local-dev --passphrase dev-pass",
970
+ "cnos vault create local-dev",
971
+ "cnos vault auth local-dev",
958
972
  "cnos secret set app.token super-secret --vault local-dev",
959
973
  "cnos vault create github-ci --provider github-secrets --no-passphrase",
960
974
  "cnos secret set app.token APP_TOKEN --vault github-ci"
@@ -970,17 +984,14 @@ var COMMANDS = [
970
984
  flag: "--provider <local|github-secrets>",
971
985
  description: "Vault provider. Defaults to local."
972
986
  },
973
- {
974
- flag: "--passphrase <value>",
975
- description: "Required for local vault creation unless already available in the configured passphrase env var."
976
- },
977
987
  {
978
988
  flag: "--no-passphrase",
979
989
  description: "Allowed for passwordless providers such as github-secrets."
980
990
  }
981
991
  ],
982
992
  examples: [
983
- "cnos vault create local-dev --passphrase dev-pass",
993
+ "cnos vault create local-dev",
994
+ "cnos vault auth local-dev",
984
995
  "cnos vault create github-ci --provider github-secrets --no-passphrase",
985
996
  "cnos vault list",
986
997
  "cnos vault remove local-dev"
@@ -989,13 +1000,33 @@ var COMMANDS = [
989
1000
  {
990
1001
  id: "vault create",
991
1002
  summary: "Create a manifest-defined vault.",
992
- usage: "cnos vault create <name> [--provider <local|github-secrets>] [--passphrase <value>] [--no-passphrase] [global-options]",
1003
+ usage: "cnos vault create <name> [--provider <local|github-secrets>] [--no-passphrase] [global-options]",
993
1004
  description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets.",
994
1005
  examples: [
995
- "cnos vault create local-dev --passphrase dev-pass",
1006
+ "cnos vault create local-dev",
996
1007
  "cnos vault create github-ci --provider github-secrets --no-passphrase"
997
1008
  ]
998
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
+ },
999
1030
  {
1000
1031
  id: "vault list",
1001
1032
  summary: "List manifest-defined vaults.",
@@ -1139,10 +1170,11 @@ var COMMANDS = [
1139
1170
  {
1140
1171
  id: "secret set",
1141
1172
  summary: "Write a secret securely.",
1142
- usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [--passphrase <value>] [global-options]",
1173
+ usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [global-options]",
1143
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.",
1144
1175
  examples: [
1145
- "cnos vault create db --passphrase dev-pass",
1176
+ "cnos vault create db",
1177
+ "cnos vault auth db",
1146
1178
  "cnos secret set app.token super-secret --vault db",
1147
1179
  "cnos vault create github-ci --provider github-secrets --no-passphrase",
1148
1180
  "cnos secret set app.token APP_TOKEN --vault github-ci"
@@ -1151,9 +1183,9 @@ var COMMANDS = [
1151
1183
  {
1152
1184
  id: "secret create vault",
1153
1185
  summary: "Create a local secret vault.",
1154
- usage: "cnos secret create vault <name> --passphrase <value> [global-options]",
1186
+ usage: "cnos secret create vault <name> [global-options]",
1155
1187
  description: "Alias for cnos vault create <name>.",
1156
- examples: ["cnos secret create vault db --passphrase dev-pass"]
1188
+ examples: ["cnos secret create vault db"]
1157
1189
  },
1158
1190
  {
1159
1191
  id: "secret list",
@@ -1310,6 +1342,60 @@ var COMMANDS = [
1310
1342
  description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
1311
1343
  examples: ["cnos doctor", "cnos doctor --workspace api --json"]
1312
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
+ },
1313
1399
  {
1314
1400
  id: "help",
1315
1401
  summary: "Show human-readable CLI help.",
@@ -1500,11 +1586,11 @@ function runHelpAi(topic, cliArgs = []) {
1500
1586
  }
1501
1587
 
1502
1588
  // src/commands/init.ts
1503
- import path6 from "path";
1589
+ import path5 from "path";
1504
1590
 
1505
1591
  // src/services/scaffold.ts
1506
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1507
- import path5 from "path";
1592
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1593
+ import path4 from "path";
1508
1594
  function scaffoldManifest(projectName, workspace) {
1509
1595
  const lines = [
1510
1596
  "version: 1",
@@ -1535,15 +1621,15 @@ function scaffoldManifest(projectName, workspace) {
1535
1621
  }
1536
1622
  async function ensureFile(filePath, content) {
1537
1623
  try {
1538
- await readFile4(filePath, "utf8");
1624
+ await readFile3(filePath, "utf8");
1539
1625
  return false;
1540
1626
  } catch {
1541
- await writeFile4(filePath, content, "utf8");
1627
+ await writeFile3(filePath, content, "utf8");
1542
1628
  return true;
1543
1629
  }
1544
1630
  }
1545
1631
  async function ensureGitignore(root) {
1546
- const gitignorePath = path5.join(root, ".gitignore");
1632
+ const gitignorePath = path4.join(root, ".gitignore");
1547
1633
  const requiredEntries = [
1548
1634
  ".cnos/env/.env",
1549
1635
  ".cnos/env/.env.*",
@@ -1556,7 +1642,7 @@ async function ensureGitignore(root) {
1556
1642
  ];
1557
1643
  let current = "";
1558
1644
  try {
1559
- current = await readFile4(gitignorePath, "utf8");
1645
+ current = await readFile3(gitignorePath, "utf8");
1560
1646
  } catch {
1561
1647
  current = "";
1562
1648
  }
@@ -1566,18 +1652,18 @@ async function ensureGitignore(root) {
1566
1652
  }
1567
1653
  const prefix = current.trim().length > 0 ? `${current.trimEnd()}
1568
1654
  ` : "";
1569
- await writeFile4(gitignorePath, `${prefix}${missingEntries.join("\n")}
1655
+ await writeFile3(gitignorePath, `${prefix}${missingEntries.join("\n")}
1570
1656
  `, "utf8");
1571
1657
  return true;
1572
1658
  }
1573
1659
  async function scaffoldWorkspace(root, workspace) {
1574
- const cnosRoot = path5.join(root, ".cnos");
1575
- const workspaceRoot = workspace ? path5.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1660
+ const cnosRoot = path4.join(root, ".cnos");
1661
+ const workspaceRoot = workspace ? path4.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1576
1662
  const createdPaths = [];
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 });
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 });
1581
1667
  const relativePaths = workspace ? [
1582
1668
  ["workspaces", workspace, "profiles", ".gitkeep"],
1583
1669
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -1590,15 +1676,15 @@ async function scaffoldWorkspace(root, workspace) {
1590
1676
  ["env", ".gitkeep"]
1591
1677
  ];
1592
1678
  for (const relativePath of relativePaths) {
1593
- const filePath = path5.join(cnosRoot, ...relativePath);
1679
+ const filePath = path4.join(cnosRoot, ...relativePath);
1594
1680
  if (await ensureFile(filePath, "")) {
1595
- createdPaths.push(path5.relative(root, filePath).replace(/\\/g, "/"));
1681
+ createdPaths.push(path4.relative(root, filePath).replace(/\\/g, "/"));
1596
1682
  }
1597
1683
  }
1598
- if (await ensureFile(path5.join(cnosRoot, "cnos.yml"), scaffoldManifest(path5.basename(root), workspace))) {
1684
+ if (await ensureFile(path4.join(cnosRoot, "cnos.yml"), scaffoldManifest(path4.basename(root), workspace))) {
1599
1685
  createdPaths.push(".cnos/cnos.yml");
1600
1686
  }
1601
- if (workspace && await ensureFile(path5.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1687
+ if (workspace && await ensureFile(path4.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1602
1688
  globalRoot: ~/.cnos
1603
1689
  `)) {
1604
1690
  createdPaths.push(".cnos-workspace.yml");
@@ -1615,7 +1701,7 @@ globalRoot: ~/.cnos
1615
1701
 
1616
1702
  // src/commands/init.ts
1617
1703
  async function runInit(options = {}) {
1618
- const root = path6.resolve(options.root ?? process.cwd());
1704
+ const root = path5.resolve(options.root ?? process.cwd());
1619
1705
  const result = await scaffoldWorkspace(root, options.workspace);
1620
1706
  if (options.json) {
1621
1707
  return printJson(result);
@@ -1626,6 +1712,12 @@ async function runInit(options = {}) {
1626
1712
  return `initialized CNOS project at ${root}`;
1627
1713
  }
1628
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
+
1629
1721
  // src/format/printInspect.ts
1630
1722
  function printInspect(record) {
1631
1723
  const lines = [
@@ -1650,12 +1742,22 @@ function printInspect(record) {
1650
1742
 
1651
1743
  // src/commands/inspect.ts
1652
1744
  async function runInspect(key, options = {}) {
1745
+ const reveal = options.cliArgs?.includes("--reveal") ?? false;
1653
1746
  const runtime = await createRuntimeService(options);
1654
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
+ };
1655
1757
  if (options.json) {
1656
- return printJson(inspectResult);
1758
+ return printJson(printable);
1657
1759
  }
1658
- return printInspect(inspectResult);
1760
+ return printInspect(printable);
1659
1761
  }
1660
1762
 
1661
1763
  // src/format/printValue.ts
@@ -1784,12 +1886,92 @@ async function runList(args = [], options = {}) {
1784
1886
  return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1785
1887
  }
1786
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
+
1787
1969
  // src/commands/onboard.ts
1788
- import { copyFile, readdir, rm as rm3 } from "fs/promises";
1970
+ import { copyFile, readdir as readdir2, rm } from "fs/promises";
1789
1971
  import path7 from "path";
1790
1972
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
1791
1973
  async function listRootEnvFiles(root) {
1792
- const entries = await readdir(root, { withFileTypes: true });
1974
+ const entries = await readdir2(root, { withFileTypes: true });
1793
1975
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
1794
1976
  }
1795
1977
  async function runOnboard(options = {}) {
@@ -1812,7 +1994,7 @@ async function runOnboard(options = {}) {
1812
1994
  await copyFile(sourcePath, targetPath);
1813
1995
  imported.push(path7.relative(root, targetPath).replace(/\\/g, "/"));
1814
1996
  if (move) {
1815
- await rm3(sourcePath);
1997
+ await rm(sourcePath);
1816
1998
  }
1817
1999
  } catch {
1818
2000
  skipped.push(fileName);
@@ -1838,14 +2020,14 @@ async function runOnboard(options = {}) {
1838
2020
  import path10 from "path";
1839
2021
 
1840
2022
  // src/services/context.ts
1841
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
2023
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1842
2024
  import path8 from "path";
1843
- import { parseYaml as parseYaml2, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2025
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
1844
2026
  async function loadCliContext(root = process.cwd()) {
1845
2027
  const filePath = path8.join(path8.resolve(root), ".cnos-workspace.yml");
1846
2028
  try {
1847
- const source = await readFile5(filePath, "utf8");
1848
- const parsed = parseYaml2(source);
2029
+ const source = await readFile4(filePath, "utf8");
2030
+ const parsed = parseYaml3(source);
1849
2031
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1850
2032
  return {};
1851
2033
  }
@@ -1866,7 +2048,7 @@ async function saveCliContext(options = {}) {
1866
2048
  ...options.profile ? { profile: options.profile } : {},
1867
2049
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1868
2050
  };
1869
- await writeFile5(filePath, stringifyYaml3(next), "utf8");
2051
+ await writeFile4(filePath, stringifyYaml2(next), "utf8");
1870
2052
  return {
1871
2053
  filePath,
1872
2054
  context: next
@@ -1874,9 +2056,9 @@ async function saveCliContext(options = {}) {
1874
2056
  }
1875
2057
 
1876
2058
  // src/services/profiles.ts
1877
- import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, rm as rm4, writeFile as writeFile6 } from "fs/promises";
2059
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
1878
2060
  import path9 from "path";
1879
- import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
2061
+ import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
1880
2062
  async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1881
2063
  const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1882
2064
  await mkdir4(path9.dirname(filePath), { recursive: true });
@@ -1886,7 +2068,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1886
2068
  } : {
1887
2069
  name: profile
1888
2070
  };
1889
- await writeFile6(filePath, stringifyYaml4(document), "utf8");
2071
+ await writeFile5(filePath, stringifyYaml3(document), "utf8");
1890
2072
  return {
1891
2073
  filePath,
1892
2074
  profile,
@@ -1896,7 +2078,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1896
2078
  async function listProfiles(root = process.cwd()) {
1897
2079
  const profilesRoot = path9.join(path9.resolve(root), ".cnos", "profiles");
1898
2080
  try {
1899
- const entries = await readdir2(profilesRoot, { withFileTypes: true });
2081
+ const entries = await readdir3(profilesRoot, { withFileTypes: true });
1900
2082
  const discovered = /* @__PURE__ */ new Set(["base"]);
1901
2083
  for (const entry of entries) {
1902
2084
  if (entry.isFile() && entry.name.endsWith(".yml")) {
@@ -1911,7 +2093,7 @@ async function listProfiles(root = process.cwd()) {
1911
2093
  async function deleteProfileDefinition(root = process.cwd(), profile) {
1912
2094
  const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1913
2095
  try {
1914
- await rm4(filePath);
2096
+ await rm2(filePath);
1915
2097
  return {
1916
2098
  filePath,
1917
2099
  deleted: true
@@ -1931,7 +2113,7 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
1931
2113
  }
1932
2114
  const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1933
2115
  try {
1934
- return parseYaml3(await readFile6(filePath, "utf8")) ?? void 0;
2116
+ return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
1935
2117
  } catch {
1936
2118
  return void 0;
1937
2119
  }
@@ -1997,11 +2179,11 @@ async function runProfile(args, options = {}) {
1997
2179
  }
1998
2180
 
1999
2181
  // src/commands/promote.ts
2000
- import { writeFile as writeFile7 } from "fs/promises";
2182
+ import { writeFile as writeFile6 } from "fs/promises";
2001
2183
  import {
2002
2184
  ensureProjectionAllowed,
2003
2185
  loadManifest as loadManifest2,
2004
- stringifyYaml as stringifyYaml5
2186
+ stringifyYaml as stringifyYaml4
2005
2187
  } from "@kitsy/cnos/internal";
2006
2188
  function normalizeTarget(value) {
2007
2189
  if (value === "public" || value === "env") {
@@ -2051,7 +2233,7 @@ async function runPromote(args = [], options = {}) {
2051
2233
  })
2052
2234
  };
2053
2235
  }
2054
- await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2236
+ await writeFile6(loadedManifest.manifestPath, stringifyYaml4(rawManifest), "utf8");
2055
2237
  if (options.json) {
2056
2238
  return printJson({
2057
2239
  target,
@@ -2070,16 +2252,21 @@ async function runRead(key, options = {}) {
2070
2252
  if (value === void 0) {
2071
2253
  throw new Error(`Missing CNOS config key: ${key}`);
2072
2254
  }
2255
+ const isSecret = key.startsWith("secret.");
2256
+ const valueForOutput = isSecret ? maskSecretValue(value) : value;
2073
2257
  if (options.json) {
2074
- return printJson({ key, value });
2258
+ return printJson({ key, value: valueForOutput });
2075
2259
  }
2076
- return printValue(value);
2260
+ return printValue(valueForOutput);
2077
2261
  }
2078
2262
 
2079
2263
  // src/commands/run.ts
2080
2264
  import { spawn } from "child_process";
2081
2265
  import {
2082
2266
  CNOS_GRAPH_ENV_VAR,
2267
+ CNOS_SECRET_PAYLOAD_ENV_VAR,
2268
+ CNOS_SESSION_KEY_ENV_VAR,
2269
+ serializeSecretPayload,
2083
2270
  serializeRuntimeGraph
2084
2271
  } from "@kitsy/cnos/internal";
2085
2272
  function consumeOptions(args, flag) {
@@ -2118,6 +2305,7 @@ async function runCommand(command, options = {}) {
2118
2305
  }
2119
2306
  const cliArgs = [...options.cliArgs ?? []];
2120
2307
  const isPublic = consumeFlag(cliArgs, "--public");
2308
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
2121
2309
  const framework = consumeOption(cliArgs, "--framework");
2122
2310
  const prefix = consumeOption(cliArgs, "--prefix");
2123
2311
  const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
@@ -2125,13 +2313,21 @@ async function runCommand(command, options = {}) {
2125
2313
  ...options,
2126
2314
  cliArgs: [...cliArgs, ...setOverrides]
2127
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;
2128
2320
  const env = {
2129
2321
  ...process.env,
2130
2322
  ...isPublic ? runtime.toPublicEnv({
2131
2323
  ...framework ? { framework } : {},
2132
2324
  ...prefix ? { prefix } : {}
2133
2325
  }) : runtime.toEnv(),
2134
- [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
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
+ } : {}
2135
2331
  };
2136
2332
  return new Promise((resolve, reject) => {
2137
2333
  const executable = command[0];
@@ -2166,12 +2362,174 @@ async function runCommand(command, options = {}) {
2166
2362
  });
2167
2363
  }
2168
2364
 
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
+
2169
2527
  // src/commands/vault.ts
2170
2528
  function normalizeVaultAction(args) {
2171
2529
  const [action = "list", ...tail] = args;
2172
- if (["create", "add", "list", "delete", "remove"].includes(action)) {
2530
+ if (["create", "add", "list", "delete", "remove", "auth", "logout"].includes(action)) {
2173
2531
  return {
2174
- action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : "list",
2532
+ action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : action === "auth" ? "auth" : action === "logout" ? "logout" : "list",
2175
2533
  tail
2176
2534
  };
2177
2535
  }
@@ -2183,16 +2541,17 @@ function normalizeVaultAction(args) {
2183
2541
  async function runVault(args = [], options = {}) {
2184
2542
  const { action, tail } = normalizeVaultAction(args);
2185
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
+ }
2186
2547
  if (action === "create") {
2187
2548
  const name = tail[0] ?? "default";
2188
2549
  const provider = consumeOption(cliArgs, "--provider") ?? "local";
2189
- const passphrase = consumeOption(cliArgs, "--passphrase");
2190
2550
  const noPassphrase = consumeFlag(cliArgs, "--no-passphrase");
2191
2551
  const result = await createVaultDefinition(name, {
2192
2552
  ...options,
2193
2553
  cliArgs,
2194
2554
  provider,
2195
- ...passphrase ? { passphrase } : {},
2196
2555
  ...noPassphrase ? { noPassphrase: true } : {}
2197
2556
  });
2198
2557
  if (options.json) {
@@ -2200,6 +2559,28 @@ async function runVault(args = [], options = {}) {
2200
2559
  }
2201
2560
  return `created vault "${result.name}" with provider "${result.provider}" in ${result.manifestPath}`;
2202
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
+ }
2203
2584
  if (action === "remove") {
2204
2585
  const name = tail[0] ?? "default";
2205
2586
  const result = await removeVaultDefinition(name, options);
@@ -2224,16 +2605,11 @@ async function runVault(args = [], options = {}) {
2224
2605
  return "";
2225
2606
  }
2226
2607
  return manifestVaults.map(
2227
- (vault) => `${vault.name} provider=${vault.provider} passphrase=${vault.passphrasePolicy}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
2608
+ (vault) => `${vault.name} provider=${vault.provider} auth=${vault.authMethod}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
2228
2609
  ).join("\n");
2229
2610
  }
2230
2611
 
2231
2612
  // src/commands/secret.ts
2232
- function isSecretRef(value) {
2233
- return Boolean(
2234
- value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
2235
- );
2236
- }
2237
2613
  function normalizeSecretCommand(args) {
2238
2614
  const [actionOrPath, next, ...tail] = args;
2239
2615
  if (!actionOrPath) {
@@ -2259,48 +2635,68 @@ function normalizeSecretCommand(args) {
2259
2635
  tail: args
2260
2636
  };
2261
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
+ }
2262
2645
  async function runSecret(argsOrPath, options = {}) {
2263
2646
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
2264
2647
  const { action, tail } = normalizeSecretCommand(args);
2265
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
+ }
2266
2652
  if (action === "create-vault") {
2267
2653
  return runVault(["create", tail[0] ?? "default"], options);
2268
2654
  }
2269
2655
  if (action === "list") {
2656
+ const runtime2 = await createRuntimeService(options);
2270
2657
  const prefix = consumeOption(cliArgs, "--prefix");
2271
2658
  const vault = consumeOption(cliArgs, "--vault");
2272
2659
  const provider = consumeOption(cliArgs, "--provider");
2273
- const entries = await listConfigEntries("secret", {
2274
- ...options,
2275
- cliArgs,
2276
- ...prefix ? { prefix } : {},
2277
- ...vault ? { vault } : {},
2278
- ...provider ? { provider } : {}
2279
- });
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));
2280
2677
  if (options.json) {
2281
2678
  return printJson(entries);
2282
2679
  }
2283
- return entries.map((entry2) => `${entry2.key}=${printValue(entry2.value)}`).join("\n");
2680
+ return entries.map((entry2) => `${entry2.key} (vault: ${entry2.vault}, provider: ${entry2.provider})`).join("\n");
2284
2681
  }
2285
2682
  if (action === "set") {
2286
2683
  const secretPath2 = tail[0];
2287
- const rawValue = tail[1] ?? "";
2288
2684
  const local = consumeFlag(cliArgs, "--local");
2289
2685
  const remote = consumeFlag(cliArgs, "--remote");
2290
2686
  const ref = consumeFlag(cliArgs, "--ref");
2687
+ const stdin = consumeFlag(cliArgs, "--stdin");
2291
2688
  const target = consumeOption(cliArgs, "--target") ?? "local";
2292
2689
  const provider = consumeOption(cliArgs, "--provider");
2293
- const passphrase = consumeOption(cliArgs, "--passphrase");
2294
2690
  const vault = consumeOption(cliArgs, "--vault") ?? "default";
2295
2691
  const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
2692
+ const rawValue = stdin ? await readStdinValue() : tail[1] ?? "";
2296
2693
  const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
2297
2694
  ...options,
2298
2695
  cliArgs,
2299
2696
  target,
2300
2697
  vault,
2301
2698
  ...mode ? { mode } : {},
2302
- ...provider ? { provider } : {},
2303
- ...passphrase ? { passphrase } : {}
2699
+ ...provider ? { provider } : {}
2304
2700
  });
2305
2701
  if (options.json) {
2306
2702
  return printJson(result);
@@ -2323,6 +2719,7 @@ async function runSecret(argsOrPath, options = {}) {
2323
2719
  const runtime = await createRuntimeService(options);
2324
2720
  const secretPath = tail[0] ?? "app.token";
2325
2721
  const expectedVault = consumeOption(cliArgs, "--vault");
2722
+ const reveal = consumeFlag(cliArgs, "--reveal");
2326
2723
  const entry = runtime.graph.entries.get(`secret.${secretPath}`);
2327
2724
  const secretRef = entry?.winner.metadata?.secretRef;
2328
2725
  const value = runtime.secret(secretPath);
@@ -2332,33 +2729,21 @@ async function runSecret(argsOrPath, options = {}) {
2332
2729
  if (expectedVault && secretRef?.vault && secretRef.vault !== expectedVault) {
2333
2730
  throw new Error(`Secret ${secretPath} belongs to vault "${secretRef.vault}", not "${expectedVault}"`);
2334
2731
  }
2335
- if (isSecretRef(value)) {
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.`);
2348
- }
2732
+ const valueForOutput = reveal ? value : maskSecretValue(value);
2349
2733
  if (options.json) {
2350
2734
  return printJson({
2351
2735
  key: `secret.${secretPath}`,
2352
- value
2736
+ value: valueForOutput,
2737
+ vault: secretRef?.vault ?? "default"
2353
2738
  });
2354
2739
  }
2355
- return printValue(value);
2740
+ return printValue(valueForOutput);
2356
2741
  }
2357
2742
 
2358
2743
  // src/commands/use.ts
2359
- import path11 from "path";
2744
+ import path12 from "path";
2360
2745
  async function runUse(args = [], options = {}) {
2361
- const root = path11.resolve(options.root ?? process.cwd());
2746
+ const root = path12.resolve(options.root ?? process.cwd());
2362
2747
  const action = args[0] ?? "show";
2363
2748
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
2364
2749
  if (action === "show" || !hasUpdates) {
@@ -2395,7 +2780,7 @@ async function runValidate(options = {}) {
2395
2780
  // package.json
2396
2781
  var package_default = {
2397
2782
  name: "@kitsy/cnos-cli",
2398
- version: "1.2.0",
2783
+ version: "1.3.0",
2399
2784
  description: "CLI entry point and developer tooling for CNOS.",
2400
2785
  type: "module",
2401
2786
  main: "./dist/index.js",
@@ -2532,6 +2917,180 @@ async function runValue(argsOrPath, options = {}) {
2532
2917
  return printValue(value);
2533
2918
  }
2534
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
+
2535
3094
  // src/index.ts
2536
3095
  function resolveHelpTopic(command, args) {
2537
3096
  if (command === "help" || command === "help-ai") {
@@ -2612,6 +3171,14 @@ async function main(argv) {
2612
3171
  return;
2613
3172
  case "onboard":
2614
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)}
2615
3182
  `);
2616
3183
  return;
2617
3184
  case "read":
@@ -2676,8 +3243,16 @@ async function main(argv) {
2676
3243
  process.exitCode = result.exitCode;
2677
3244
  return;
2678
3245
  }
3246
+ case "watch":
3247
+ process.stdout.write(`${await runWatch(passthrough.length > 0 ? passthrough : args, runtimeOptions)}
3248
+ `);
3249
+ return;
2679
3250
  case "diff":
2680
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)}
2681
3256
  `);
2682
3257
  return;
2683
3258
  case "doctor":