@kitsy/cnos-cli 1.2.0 → 1.4.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 +1161 -366
  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,42 @@ 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
+ const dottedNamespace = resource?.includes(".") ? {
48
+ namespace: resource.slice(0, resource.indexOf(".")),
49
+ path: resource.slice(resource.indexOf(".") + 1)
50
+ } : void 0;
51
+ const normalizedVerb = command === "remove" || command === "delete" ? "delete" : command === "create" || command === "add" ? "set" : command === "set" || command === "get" ? command : void 0;
52
+ if ((command === "set" || command === "get") && (resource === "value" || resource === "secret")) {
53
+ return [resource, command, ...remaining];
54
+ }
55
+ if (normalizedVerb && dottedNamespace && dottedNamespace.namespace && dottedNamespace.path) {
56
+ return [dottedNamespace.namespace, normalizedVerb, dottedNamespace.path, ...remaining];
57
+ }
28
58
  if ((command === "create" || command === "add") && resource === "profile") {
29
59
  return ["profile", "create", ...remaining];
30
60
  }
@@ -37,6 +67,12 @@ function normalizeCommand(argv) {
37
67
  if ((command === "delete" || command === "remove") && resource === "vault") {
38
68
  return ["vault", "remove", ...remaining];
39
69
  }
70
+ if (command === "auth" && resource === "vault") {
71
+ return ["vault", "auth", ...remaining];
72
+ }
73
+ if (command === "logout" && resource === "vault") {
74
+ return ["vault", "logout", ...remaining];
75
+ }
40
76
  if (command === "list" && resource === "profile") {
41
77
  return ["profile", "list", ...remaining];
42
78
  }
@@ -58,6 +94,9 @@ function normalizeCommand(argv) {
58
94
  if ((command === "delete" || command === "remove") && resource === "value") {
59
95
  return ["value", "delete", ...remaining];
60
96
  }
97
+ if (normalizedVerb && resource && !["profile", "vault", "secret", "value"].includes(resource)) {
98
+ return [resource, normalizedVerb, ...remaining];
99
+ }
61
100
  if (command === "list" && resource === "value") {
62
101
  return ["value", "list", ...remaining];
63
102
  }
@@ -164,6 +203,9 @@ function parseArgs(argv) {
164
203
  };
165
204
  }
166
205
 
206
+ // src/commands/define.ts
207
+ import path3 from "path";
208
+
167
209
  // src/cli/commandOptions.ts
168
210
  function consumeFlag(args, flag) {
169
211
  const index = args.indexOf(flag);
@@ -191,172 +233,51 @@ function consumeOption(args, flag) {
191
233
  return void 0;
192
234
  }
193
235
 
236
+ // src/format/displayPath.ts
237
+ import path from "path";
238
+ function displayPath(filePath, root = process.cwd()) {
239
+ const absoluteRoot = path.resolve(root);
240
+ const absoluteFile = path.resolve(filePath);
241
+ const relative = path.relative(absoluteRoot, absoluteFile);
242
+ if (!relative || relative === "") {
243
+ return ".";
244
+ }
245
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
246
+ return absoluteFile;
247
+ }
248
+ return relative;
249
+ }
250
+
194
251
  // src/format/printJson.ts
195
252
  function printJson(value) {
196
253
  return JSON.stringify(value, null, 2);
197
254
  }
198
255
 
199
256
  // src/services/writes.ts
200
- import { mkdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
257
+ import { mkdir, readFile, writeFile } from "fs/promises";
201
258
  import path2 from "path";
202
259
  import {
203
- getVaultPassphraseEnvVar,
260
+ getNamespaceDefinition,
261
+ createSecretVaultProvider,
204
262
  parseYaml,
205
263
  resolveConfigDocumentPath,
206
- stringifyYaml as stringifyYaml2,
207
- writeLocalSecret,
208
- resolveConfiguredVaultPassphrase as resolveConfiguredVaultPassphrase2,
209
- resolveSecretStoreRoot as resolveSecretStoreRoot2
264
+ resolveVaultAuth,
265
+ stringifyYaml
210
266
  } from "@kitsy/cnos/internal";
211
267
 
212
268
  // src/services/runtime.ts
213
- import { createCnos } from "@kitsy/cnos";
269
+ import { createCnos } from "@kitsy/cnos/configure";
214
270
  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
271
  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
- };
272
+ ...options.root ? { root: options.root } : {},
273
+ ...options.workspace ? { workspace: options.workspace } : {},
274
+ ...options.profile ? { profile: options.profile } : {},
275
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {},
276
+ ...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
277
+ ...options.secretResolution ? { secretResolution: options.secretResolution } : {},
278
+ processEnv: options.processEnv ?? process.env
316
279
  });
317
280
  }
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
281
 
361
282
  // src/services/writes.ts
362
283
  function setNestedValue(target, pathSegments, value) {
@@ -405,7 +326,7 @@ function isSecretReference(value) {
405
326
  }
406
327
  async function readYamlDocument(filePath) {
407
328
  try {
408
- return parseYaml(await readFile2(filePath, "utf8")) ?? {};
329
+ return parseYaml(await readFile(filePath, "utf8")) ?? {};
409
330
  } catch {
410
331
  return {};
411
332
  }
@@ -433,11 +354,25 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
433
354
  filePath: secret.filePath,
434
355
  value: {
435
356
  provider: secret.provider,
436
- ref: secret.ref
357
+ ref: secret.ref,
358
+ ...secret.vault ? { vault: secret.vault } : {}
437
359
  }
438
360
  };
439
361
  }
440
362
  const runtime = await createRuntimeService(options);
363
+ if (namespace !== "value" && namespace !== "secret" && !runtime.manifest.namespaces[namespace]) {
364
+ throw new Error(`Cannot write ${namespace}.${configPath} because namespace "${namespace}" is not declared in .cnos/cnos.yml.`);
365
+ }
366
+ const namespaceDefinition = getNamespaceDefinition(runtime.manifest, namespace);
367
+ if (namespaceDefinition.kind !== "data") {
368
+ throw new Error(`Cannot write ${namespace}.${configPath} because namespace "${namespace}" is not a data namespace.`);
369
+ }
370
+ if (namespaceDefinition.readonly) {
371
+ throw new Error(`Cannot write ${namespace}.${configPath} because namespace "${namespace}" is readonly.`);
372
+ }
373
+ if (namespaceDefinition.sensitive) {
374
+ throw new Error(`Cannot write ${namespace}.${configPath} with the generic data writer because namespace "${namespace}" is sensitive.`);
375
+ }
441
376
  const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
442
377
  const profile = options.profile ?? runtime.graph.profile;
443
378
  const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
@@ -445,7 +380,7 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
445
380
  const parsedValue = parseScalarValue(rawValue);
446
381
  setNestedValue(document, configPath.split("."), parsedValue);
447
382
  await mkdir(path2.dirname(filePath), { recursive: true });
448
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
383
+ await writeFile(filePath, stringifyYaml(document), "utf8");
449
384
  return {
450
385
  filePath,
451
386
  value: parsedValue
@@ -459,53 +394,36 @@ async function setSecret(configPath, rawValue, options = {}) {
459
394
  const document = await readYamlDocument(filePath);
460
395
  const vault = options.vault?.trim() || "default";
461
396
  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");
397
+ if (!vaultDefinition) {
398
+ throw new Error(`Unknown vault "${vault}". Create it first with cnos vault create ${vault}.`);
399
+ }
400
+ const mode = options.mode ?? (vaultDefinition.provider === "local" ? "local" : vaultDefinition.provider === "github-secrets" ? "ref" : "remote");
464
401
  let reference;
465
- let storePath;
466
402
  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);
403
+ const auth = await resolveVaultAuth(vault, vaultDefinition, options.processEnv ?? process.env);
404
+ const provider = createSecretVaultProvider(vault, vaultDefinition, options.processEnv ?? process.env);
405
+ await provider.authenticate(auth);
406
+ await provider.set(configPath, rawValue);
486
407
  reference = {
487
408
  provider: "local",
488
- ref,
409
+ ref: configPath,
489
410
  vault
490
411
  };
491
412
  } else {
492
413
  reference = {
493
- provider: inferredProvider ?? (mode === "ref" ? "ref" : "remote"),
414
+ provider: options.provider?.trim() || vaultDefinition.provider,
494
415
  ref: rawValue || configPath.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(),
495
- ...vaultDefinition || vault !== "default" ? {
496
- vault
497
- } : {}
416
+ vault
498
417
  };
499
418
  }
500
419
  setNestedValue(document, configPath.split("."), reference);
501
420
  await mkdir(path2.dirname(filePath), { recursive: true });
502
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
421
+ await writeFile(filePath, stringifyYaml(document), "utf8");
503
422
  return {
504
423
  filePath,
505
424
  provider: reference.provider,
506
425
  ref: reference.ref,
507
- ...reference.vault ? { vault: reference.vault } : {},
508
- ...storePath ? { storePath } : {}
426
+ ...reference.vault ? { vault: reference.vault } : {}
509
427
  };
510
428
  }
511
429
  async function deleteSecret(configPath, options = {}) {
@@ -522,24 +440,20 @@ async function deleteSecret(configPath, options = {}) {
522
440
  deleted: false
523
441
  };
524
442
  }
525
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
526
- let removedStore;
443
+ await writeFile(filePath, stringifyYaml(document), "utf8");
527
444
  const secretRef = metadata?.secretRef;
528
445
  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;
446
+ const definition = runtime.manifest.vaults[secretRef.vault ?? "default"];
447
+ if (definition) {
448
+ const auth = await resolveVaultAuth(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
449
+ const provider = createSecretVaultProvider(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
450
+ await provider.authenticate(auth);
451
+ await provider.delete(secretRef.ref);
452
+ }
538
453
  }
539
454
  return {
540
455
  filePath,
541
- deleted: true,
542
- ...removedStore ? { removedStore } : {}
456
+ deleted: true
543
457
  };
544
458
  }
545
459
  async function deleteValue(namespace, configPath, options = {}) {
@@ -547,6 +461,19 @@ async function deleteValue(namespace, configPath, options = {}) {
547
461
  return deleteSecret(configPath, options);
548
462
  }
549
463
  const runtime = await createRuntimeService(options);
464
+ if (namespace !== "value" && namespace !== "secret" && !runtime.manifest.namespaces[namespace]) {
465
+ throw new Error(`Cannot delete ${namespace}.${configPath} because namespace "${namespace}" is not declared in .cnos/cnos.yml.`);
466
+ }
467
+ const namespaceDefinition = getNamespaceDefinition(runtime.manifest, namespace);
468
+ if (namespaceDefinition.kind !== "data") {
469
+ throw new Error(`Cannot delete ${namespace}.${configPath} because namespace "${namespace}" is not a data namespace.`);
470
+ }
471
+ if (namespaceDefinition.readonly) {
472
+ throw new Error(`Cannot delete ${namespace}.${configPath} because namespace "${namespace}" is readonly.`);
473
+ }
474
+ if (namespaceDefinition.sensitive) {
475
+ throw new Error(`Cannot delete ${namespace}.${configPath} with the generic data writer because namespace "${namespace}" is sensitive.`);
476
+ }
550
477
  const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
551
478
  const profile = options.profile ?? runtime.graph.profile;
552
479
  const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
@@ -558,7 +485,7 @@ async function deleteValue(namespace, configPath, options = {}) {
558
485
  deleted: false
559
486
  };
560
487
  }
561
- await writeFile2(filePath, stringifyYaml2(document), "utf8");
488
+ await writeFile(filePath, stringifyYaml(document), "utf8");
562
489
  return {
563
490
  filePath,
564
491
  deleted: true
@@ -568,6 +495,7 @@ async function deleteValue(namespace, configPath, options = {}) {
568
495
  // src/commands/define.ts
569
496
  async function runDefine(namespace, configPath, rawValue, options = {}) {
570
497
  const cliArgs = [...options.cliArgs ?? []];
498
+ const root = path3.resolve(options.root ?? process.cwd());
571
499
  const target = consumeOption(cliArgs, "--target") ?? "local";
572
500
  const local = consumeFlag(cliArgs, "--local");
573
501
  const remote = consumeFlag(cliArgs, "--remote");
@@ -593,7 +521,18 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
593
521
  value: result.value
594
522
  });
595
523
  }
596
- return `defined ${namespace}.${configPath} in ${result.filePath}`;
524
+ return `defined ${namespace}.${configPath} in ${displayPath(result.filePath, root)}`;
525
+ }
526
+
527
+ // src/commands/drift.ts
528
+ import { compareSchemaToGraph, formatDriftReport } from "@kitsy/cnos/internal";
529
+ async function runDrift(options = {}) {
530
+ const runtime = await createRuntimeService(options);
531
+ const report = compareSchemaToGraph(runtime);
532
+ if (options.json) {
533
+ return printJson(report);
534
+ }
535
+ return formatDriftReport(report);
597
536
  }
598
537
 
599
538
  // src/commands/diff.ts
@@ -634,8 +573,15 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
634
573
  }
635
574
 
636
575
  // src/services/doctor.ts
637
- import { readFile as readFile3 } from "fs/promises";
638
- import path3 from "path";
576
+ import { readdir, readFile as readFile2 } from "fs/promises";
577
+ import path4 from "path";
578
+ import {
579
+ detectLegacyVaultFormat,
580
+ isSecretReference as isSecretReference2,
581
+ parseYaml as parseYaml2,
582
+ readKeychain,
583
+ resolveSecretStoreRoot
584
+ } from "@kitsy/cnos/internal";
639
585
 
640
586
  // src/services/validation.ts
641
587
  import { validateRuntime } from "@kitsy/cnos/internal";
@@ -650,7 +596,7 @@ async function createValidationSummary(options = {}) {
650
596
 
651
597
  // src/services/doctor.ts
652
598
  async function checkGitignore(root) {
653
- const gitignorePath = path3.join(root, ".gitignore");
599
+ const gitignorePath = path4.join(root, ".gitignore");
654
600
  const expected = [
655
601
  ".cnos/env/.env",
656
602
  ".cnos/env/.env.*",
@@ -662,7 +608,7 @@ async function checkGitignore(root) {
662
608
  "!.cnos/workspaces/*/env/.env.*.example"
663
609
  ];
664
610
  try {
665
- const content = await readFile3(gitignorePath, "utf8");
611
+ const content = await readFile2(gitignorePath, "utf8");
666
612
  const missing = expected.filter((entry) => !content.includes(entry));
667
613
  return {
668
614
  name: "gitignore",
@@ -680,11 +626,76 @@ async function checkGitignore(root) {
680
626
  function issueSummary(issues) {
681
627
  return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
682
628
  }
629
+ async function collectYamlFiles(root) {
630
+ try {
631
+ const entries = await readdir(root, { withFileTypes: true });
632
+ const results = [];
633
+ for (const entry of entries) {
634
+ const target = path4.join(root, entry.name);
635
+ if (entry.isDirectory()) {
636
+ results.push(...await collectYamlFiles(target));
637
+ continue;
638
+ }
639
+ if (entry.isFile() && [".yml", ".yaml"].includes(path4.extname(entry.name).toLowerCase())) {
640
+ results.push(target);
641
+ }
642
+ }
643
+ return results;
644
+ } catch {
645
+ return [];
646
+ }
647
+ }
648
+ function hasPlaintextSecret(value) {
649
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
650
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
651
+ }
652
+ if (isSecretReference2(value)) {
653
+ return false;
654
+ }
655
+ return Object.values(value).some((entry) => hasPlaintextSecret(entry));
656
+ }
657
+ async function checkSecretSecurity(options, runtime) {
658
+ const storeRoot = resolveSecretStoreRoot(options.processEnv);
659
+ const legacyPaths = await Promise.all(
660
+ Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").map(async ([vault]) => ({ vault, path: await detectLegacyVaultFormat(storeRoot, vault) }))
661
+ );
662
+ const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
663
+ const secretFiles = await Promise.all(
664
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path4.join(root.path, "secrets")))
665
+ );
666
+ const plaintextFiles = [];
667
+ for (const file of secretFiles.flat()) {
668
+ try {
669
+ const parsed = parseYaml2(await readFile2(file, "utf8"));
670
+ if (hasPlaintextSecret(parsed)) {
671
+ plaintextFiles.push(file);
672
+ }
673
+ } catch {
674
+ plaintextFiles.push(file);
675
+ }
676
+ }
677
+ const keychainWarnings = await Promise.all(
678
+ Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").flatMap(
679
+ ([vault, definition]) => (definition.auth?.passphrase?.from ?? []).filter((source) => source.startsWith("keychain:")).map(async (source) => ({ vault, source, value: await readKeychain(source.slice("keychain:".length)) }))
680
+ )
681
+ );
682
+ const warnings = [
683
+ ...legacyDetected.map((entry) => `legacy vault ${entry.vault}: ${entry.path}`),
684
+ ...plaintextFiles.map((file) => `plaintext secret file: ${file}`),
685
+ ...keychainWarnings.filter((entry) => !entry.value).map((entry) => `no keychain entry for vault ${entry.vault} (${entry.source})`)
686
+ ];
687
+ return {
688
+ name: "security",
689
+ ok: warnings.length === 0,
690
+ details: warnings.length === 0 ? "no legacy vaults, plaintext secret files, or missing keychain entries" : warnings.join("; ")
691
+ };
692
+ }
683
693
  async function evaluateDoctor(options = {}) {
684
- const root = path3.resolve(options.root ?? process.cwd());
694
+ const root = path4.resolve(options.root ?? process.cwd());
685
695
  const { runtime, summary } = await createValidationSummary(options);
686
696
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
687
697
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
698
+ const declaredCustomNamespaces = Object.entries(runtime.manifest.namespaces).filter(([namespace]) => !["value", "secret", "meta", "process", "public", "env"].includes(namespace)).map(([namespace, definition]) => `${namespace}(${definition.kind}${definition.shareable ? ",shareable" : ""}${definition.readonly ? ",readonly" : ""})`).sort((left, right) => left.localeCompare(right));
688
699
  return [
689
700
  {
690
701
  name: "manifest",
@@ -696,6 +707,11 @@ async function evaluateDoctor(options = {}) {
696
707
  ok: true,
697
708
  details: `${runtime.graph.workspace.workspaceId} via ${runtime.graph.workspace.workspaceSource}`
698
709
  },
710
+ {
711
+ name: "namespaces",
712
+ ok: true,
713
+ details: declaredCustomNamespaces.length === 0 ? "built-ins: value, secret, meta, process, public, env" : `built-ins: value, secret, meta, process, public, env | custom: ${declaredCustomNamespaces.join(", ")}`
714
+ },
699
715
  {
700
716
  name: "source-roots",
701
717
  ok: Boolean(localRoot),
@@ -711,6 +727,7 @@ async function evaluateDoctor(options = {}) {
711
727
  ok: !runtime.manifest.workspaces.global.enabled || Boolean(runtime.graph.workspace.globalRoot),
712
728
  details: runtime.manifest.workspaces.global.enabled ? runtime.graph.workspace.globalRoot ? `enabled at ${runtime.graph.workspace.globalRoot}` : "enabled but no global root resolved" : "disabled"
713
729
  },
730
+ await checkSecretSecurity(options, runtime),
714
731
  await checkGitignore(root)
715
732
  ];
716
733
  }
@@ -729,7 +746,7 @@ async function runDoctor(options = {}) {
729
746
  }
730
747
 
731
748
  // src/commands/dump.ts
732
- import { writeDump } from "@kitsy/cnos";
749
+ import { writeDump } from "@kitsy/cnos/configure";
733
750
  async function runDump(options = {}) {
734
751
  const cliArgs = [...options.cliArgs ?? []];
735
752
  const flatten = consumeFlag(cliArgs, "--flatten");
@@ -751,9 +768,51 @@ async function runDump(options = {}) {
751
768
  return `dumped ${result.files.length} files to ${result.root}`;
752
769
  }
753
770
 
771
+ // src/commands/codegen.ts
772
+ import { watchSchema, writeCodegenOutput } from "@kitsy/cnos/internal";
773
+ async function runCodegen(options = {}) {
774
+ const cliArgs = [...options.cliArgs ?? []];
775
+ const out = consumeOption(cliArgs, "--out");
776
+ const watch2 = consumeFlag(cliArgs, "--watch");
777
+ if (cliArgs.length > 0) {
778
+ throw new Error(`Unknown codegen options: ${cliArgs.join(" ")}`);
779
+ }
780
+ if (watch2) {
781
+ const watcher = await watchSchema({
782
+ ...options.root ? {
783
+ root: options.root
784
+ } : {},
785
+ ...out ? {
786
+ out
787
+ } : {},
788
+ onError(error) {
789
+ const message = error instanceof Error ? error.message : String(error);
790
+ process.stderr.write(`${message}
791
+ `);
792
+ }
793
+ });
794
+ const closeWatcher = () => {
795
+ watcher.close();
796
+ };
797
+ process.once("SIGINT", closeWatcher);
798
+ process.once("SIGTERM", closeWatcher);
799
+ return `watching schema changes -> ${out ?? ".cnos/types/cnos.d.ts"}`;
800
+ }
801
+ const result = await writeCodegenOutput({
802
+ ...options.root ? {
803
+ root: options.root
804
+ } : {},
805
+ ...out ? {
806
+ out
807
+ } : {}
808
+ });
809
+ const summary = result.hasSchema ? `generated types from ${result.schemaEntryCount} schema entr${result.schemaEntryCount === 1 ? "y" : "ies"}` : "generated empty types (no schema section found)";
810
+ return `${summary} -> ${result.typesPath} and ${result.runtimePath}`;
811
+ }
812
+
754
813
  // src/commands/exportEnv.ts
755
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
756
- import path4 from "path";
814
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
815
+ import path5 from "path";
757
816
  function formatEnvOutput(env) {
758
817
  return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
759
818
  }
@@ -773,9 +832,9 @@ async function runExportEnv(options = {}) {
773
832
  }) : runtime.toEnv();
774
833
  const output = formatEnvOutput(env);
775
834
  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");
835
+ const targetPath = path5.resolve(options.root ?? process.cwd(), to);
836
+ await mkdir2(path5.dirname(targetPath), { recursive: true });
837
+ await writeFile2(targetPath, output, "utf8");
779
838
  if (options.json) {
780
839
  return printJson({
781
840
  to: targetPath,
@@ -852,6 +911,23 @@ var COMMANDS = [
852
911
  ],
853
912
  examples: ["cnos onboard", "cnos onboard --workspace webapp", "cnos onboard --root ../my-app --workspace app --move"]
854
913
  },
914
+ {
915
+ id: "codegen",
916
+ summary: "Generate typed CNOS access wrappers from schema.",
917
+ usage: "cnos codegen [--out <path>] [--watch] [--root <path>]",
918
+ description: "Reads schema from .cnos/cnos.yml and generates typed CNOS declaration output plus a typed createCnos wrapper.",
919
+ options: [
920
+ {
921
+ flag: "--out <path>",
922
+ description: "Custom path for the generated type declaration file. runtime.ts is emitted beside it."
923
+ },
924
+ {
925
+ flag: "--watch",
926
+ description: "Watch the manifest schema and regenerate output when it changes."
927
+ }
928
+ ],
929
+ examples: ["cnos codegen", "cnos codegen --out src/cnos-config.d.ts", "cnos codegen --watch"]
930
+ },
855
931
  {
856
932
  id: "read",
857
933
  summary: "Read any fully-qualified CNOS key.",
@@ -943,18 +1019,19 @@ var COMMANDS = [
943
1019
  flag: "--provider <name>",
944
1020
  description: "Provider name for --remote or --ref secret writes."
945
1021
  },
946
- {
947
- flag: "--passphrase <value>",
948
- description: "Passphrase used to encrypt local secret material when --local is selected."
949
- },
950
1022
  {
951
1023
  flag: "--vault <name>",
952
1024
  description: "Use a manifest-defined vault. Provider behavior is inferred from the vault definition."
1025
+ },
1026
+ {
1027
+ flag: "--reveal",
1028
+ description: "Reveal the resolved secret value for get-style reads. Output is masked by default."
953
1029
  }
954
1030
  ],
955
1031
  examples: [
956
1032
  "cnos secret app.token",
957
- "cnos vault create local-dev --passphrase dev-pass",
1033
+ "cnos vault create local-dev",
1034
+ "cnos vault auth local-dev",
958
1035
  "cnos secret set app.token super-secret --vault local-dev",
959
1036
  "cnos vault create github-ci --provider github-secrets --no-passphrase",
960
1037
  "cnos secret set app.token APP_TOKEN --vault github-ci"
@@ -970,17 +1047,14 @@ var COMMANDS = [
970
1047
  flag: "--provider <local|github-secrets>",
971
1048
  description: "Vault provider. Defaults to local."
972
1049
  },
973
- {
974
- flag: "--passphrase <value>",
975
- description: "Required for local vault creation unless already available in the configured passphrase env var."
976
- },
977
1050
  {
978
1051
  flag: "--no-passphrase",
979
1052
  description: "Allowed for passwordless providers such as github-secrets."
980
1053
  }
981
1054
  ],
982
1055
  examples: [
983
- "cnos vault create local-dev --passphrase dev-pass",
1056
+ "cnos vault create local-dev",
1057
+ "cnos vault auth local-dev",
984
1058
  "cnos vault create github-ci --provider github-secrets --no-passphrase",
985
1059
  "cnos vault list",
986
1060
  "cnos vault remove local-dev"
@@ -989,13 +1063,33 @@ var COMMANDS = [
989
1063
  {
990
1064
  id: "vault create",
991
1065
  summary: "Create a manifest-defined vault.",
992
- usage: "cnos vault create <name> [--provider <local|github-secrets>] [--passphrase <value>] [--no-passphrase] [global-options]",
993
- description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets.",
1066
+ usage: "cnos vault create <name> [--provider <local|github-secrets>] [--no-passphrase] [global-options]",
1067
+ description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets. CNOS prompts for a passphrase when one is not already available from env or keychain.",
994
1068
  examples: [
995
- "cnos vault create local-dev --passphrase dev-pass",
1069
+ "cnos vault create local-dev",
996
1070
  "cnos vault create github-ci --provider github-secrets --no-passphrase"
997
1071
  ]
998
1072
  },
1073
+ {
1074
+ id: "vault auth",
1075
+ summary: "Authenticate a vault for the current shell session.",
1076
+ usage: "cnos vault auth <name> [--store-keychain] [global-options]",
1077
+ description: "Authenticates an existing local vault using env, keychain, or prompt-based auth and stores a derived session key for later CNOS commands in the same shell. Wrong passphrases fail authentication.",
1078
+ examples: ["cnos vault auth local-dev", "cnos vault auth local-dev --store-keychain"]
1079
+ },
1080
+ {
1081
+ id: "vault logout",
1082
+ summary: "Clear vault auth state for the current shell session.",
1083
+ usage: "cnos vault logout <name> [global-options]",
1084
+ description: "Removes active vault session auth for the selected vault or all vaults when used with --all.",
1085
+ options: [
1086
+ {
1087
+ flag: "--all",
1088
+ description: "Clear all active vault auth sessions for the current shell context."
1089
+ }
1090
+ ],
1091
+ examples: ["cnos vault logout local-dev", "cnos vault logout --all"]
1092
+ },
999
1093
  {
1000
1094
  id: "vault list",
1001
1095
  summary: "List manifest-defined vaults.",
@@ -1053,11 +1147,11 @@ var COMMANDS = [
1053
1147
  {
1054
1148
  id: "list",
1055
1149
  summary: "List resolved config entries.",
1056
- usage: "cnos list [value|secret|meta|env|public|all] [--prefix <path>] [--framework <name>] [global-options]",
1057
- description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering.",
1150
+ usage: "cnos list [<namespace>|all] [--prefix <path>] [--framework <name>] [global-options]",
1151
+ description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering. Custom data namespaces such as flags are supported, and process exposes server-only ambient runtime state.",
1058
1152
  options: [
1059
1153
  {
1060
- flag: "--namespace <value|secret|meta|env|public|all>",
1154
+ flag: "--namespace <name>",
1061
1155
  description: "Explicit namespace selector when not using a positional namespace argument."
1062
1156
  },
1063
1157
  {
@@ -1069,7 +1163,7 @@ var COMMANDS = [
1069
1163
  description: "When listing public output, apply framework-specific prefixes such as vite or next."
1070
1164
  }
1071
1165
  ],
1072
- examples: ["cnos list", "cnos list value --prefix app.", "cnos list env", "cnos list public --framework vite"]
1166
+ examples: ["cnos list", "cnos list value --prefix app.", "cnos list flags", "cnos list process --prefix env.PATH", "cnos list env", "cnos list public --framework vite"]
1073
1167
  },
1074
1168
  {
1075
1169
  id: "profile",
@@ -1079,11 +1173,16 @@ var COMMANDS = [
1079
1173
  options: [
1080
1174
  {
1081
1175
  flag: "--inherit <name>",
1082
- description: "Parent profile to extend when creating a profile."
1176
+ description: "Parent profile to extend when creating a profile. Base inheritance is implicit by default."
1177
+ },
1178
+ {
1179
+ flag: "--no-inherit",
1180
+ description: "Create a clean profile that does not inherit base fallback layers."
1083
1181
  }
1084
1182
  ],
1085
1183
  examples: [
1086
- "cnos profile create stage --inherit base",
1184
+ "cnos profile create stage",
1185
+ "cnos profile create isolated --no-inherit",
1087
1186
  "cnos profile list",
1088
1187
  "cnos profile use stage"
1089
1188
  ]
@@ -1091,9 +1190,9 @@ var COMMANDS = [
1091
1190
  {
1092
1191
  id: "profile create",
1093
1192
  summary: "Create a profile definition.",
1094
- usage: "cnos profile create <name> [--inherit <name>] [--root <path>] [--json]",
1095
- description: "Creates .cnos/profiles/<name>.yml for an explicit profile overlay.",
1096
- examples: ["cnos profile create stage --inherit base"]
1193
+ usage: "cnos profile create <name> [--inherit <name> | --no-inherit] [--root <path>] [--json]",
1194
+ description: "Creates .cnos/profiles/<name>.yml for an explicit profile overlay. New profiles inherit base by default unless --no-inherit is set.",
1195
+ examples: ["cnos profile create stage", "cnos profile create isolated --no-inherit"]
1097
1196
  },
1098
1197
  {
1099
1198
  id: "profile list",
@@ -1120,7 +1219,7 @@ var COMMANDS = [
1120
1219
  id: "promote",
1121
1220
  summary: "Promote shareable config into public or env projection surfaces.",
1122
1221
  usage: "cnos promote <key...> --to <public|env> [--as <ENV_VAR>] [global-options]",
1123
- description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected.",
1222
+ description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected, but declared shareable data namespaces such as flags are allowed.",
1124
1223
  options: [
1125
1224
  {
1126
1225
  flag: "--to <public|env>",
@@ -1133,16 +1232,18 @@ var COMMANDS = [
1133
1232
  ],
1134
1233
  examples: [
1135
1234
  "cnos promote value.flag.auth.upi_enabled --to public",
1235
+ "cnos promote flags.upi_enabled --to public",
1136
1236
  "cnos promote value.server.port --to env --as PORT"
1137
1237
  ]
1138
1238
  },
1139
1239
  {
1140
1240
  id: "secret set",
1141
1241
  summary: "Write a secret securely.",
1142
- usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [--passphrase <value>] [global-options]",
1242
+ usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [global-options]",
1143
1243
  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
1244
  examples: [
1145
- "cnos vault create db --passphrase dev-pass",
1245
+ "cnos vault create db",
1246
+ "cnos vault auth db",
1146
1247
  "cnos secret set app.token super-secret --vault db",
1147
1248
  "cnos vault create github-ci --provider github-secrets --no-passphrase",
1148
1249
  "cnos secret set app.token APP_TOKEN --vault github-ci"
@@ -1151,9 +1252,9 @@ var COMMANDS = [
1151
1252
  {
1152
1253
  id: "secret create vault",
1153
1254
  summary: "Create a local secret vault.",
1154
- usage: "cnos secret create vault <name> --passphrase <value> [global-options]",
1255
+ usage: "cnos secret create vault <name> [global-options]",
1155
1256
  description: "Alias for cnos vault create <name>.",
1156
- examples: ["cnos secret create vault db --passphrase dev-pass"]
1257
+ examples: ["cnos secret create vault db"]
1157
1258
  },
1158
1259
  {
1159
1260
  id: "secret list",
@@ -1310,6 +1411,60 @@ var COMMANDS = [
1310
1411
  description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
1311
1412
  examples: ["cnos doctor", "cnos doctor --workspace api --json"]
1312
1413
  },
1414
+ {
1415
+ id: "drift",
1416
+ summary: "Compare resolved config against schema and report drift.",
1417
+ usage: "cnos drift [--workspace <id>] [--profile <name>] [--json]",
1418
+ description: "Reports missing required keys, undeclared keys, type mismatches, and defaults applied for the selected workspace/profile.",
1419
+ examples: ["cnos drift", "cnos drift --workspace api --profile stage", "cnos drift --json"]
1420
+ },
1421
+ {
1422
+ id: "watch",
1423
+ summary: "Watch CNOS inputs and either restart a process or emit changed keys.",
1424
+ usage: "cnos watch [--signal] [--debounce <ms>] [global-options] -- <command...>",
1425
+ 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.",
1426
+ options: [
1427
+ {
1428
+ flag: "--signal",
1429
+ description: "Emit changed keys as JSON instead of restarting a child process."
1430
+ },
1431
+ {
1432
+ flag: "--debounce <ms>",
1433
+ description: "Debounce change handling before re-resolving the graph. Defaults to 300ms."
1434
+ }
1435
+ ],
1436
+ examples: ["cnos watch -- node server.js", "cnos watch --signal", "cnos watch --debounce 100 -- node server.js"]
1437
+ },
1438
+ {
1439
+ id: "migrate",
1440
+ summary: "Scan env usage and propose CNOS manifest mappings.",
1441
+ usage: "cnos migrate [--scan <path>] [--dry-run] [--apply] [--rewrite] [global-options]",
1442
+ 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.",
1443
+ options: [
1444
+ {
1445
+ flag: "--scan <path>",
1446
+ description: "Directory to scan. Defaults to ./src relative to the repo root."
1447
+ },
1448
+ {
1449
+ flag: "--dry-run",
1450
+ description: "Preview the proposed mappings without changing the manifest."
1451
+ },
1452
+ {
1453
+ flag: "--apply",
1454
+ description: "Write proposed env mappings and public promotions into .cnos/cnos.yml."
1455
+ },
1456
+ {
1457
+ flag: "--rewrite",
1458
+ description: "With --apply, rewrite supported process.env usages in source files and create .bak backups."
1459
+ }
1460
+ ],
1461
+ examples: [
1462
+ "cnos migrate",
1463
+ "cnos migrate --scan src --dry-run",
1464
+ "cnos migrate --scan apps/api/src --apply",
1465
+ "cnos migrate --apply --rewrite"
1466
+ ]
1467
+ },
1313
1468
  {
1314
1469
  id: "help",
1315
1470
  summary: "Show human-readable CLI help.",
@@ -1500,11 +1655,11 @@ function runHelpAi(topic, cliArgs = []) {
1500
1655
  }
1501
1656
 
1502
1657
  // src/commands/init.ts
1503
- import path6 from "path";
1658
+ import path7 from "path";
1504
1659
 
1505
1660
  // src/services/scaffold.ts
1506
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1507
- import path5 from "path";
1661
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1662
+ import path6 from "path";
1508
1663
  function scaffoldManifest(projectName, workspace) {
1509
1664
  const lines = [
1510
1665
  "version: 1",
@@ -1535,15 +1690,15 @@ function scaffoldManifest(projectName, workspace) {
1535
1690
  }
1536
1691
  async function ensureFile(filePath, content) {
1537
1692
  try {
1538
- await readFile4(filePath, "utf8");
1693
+ await readFile3(filePath, "utf8");
1539
1694
  return false;
1540
1695
  } catch {
1541
- await writeFile4(filePath, content, "utf8");
1696
+ await writeFile3(filePath, content, "utf8");
1542
1697
  return true;
1543
1698
  }
1544
1699
  }
1545
1700
  async function ensureGitignore(root) {
1546
- const gitignorePath = path5.join(root, ".gitignore");
1701
+ const gitignorePath = path6.join(root, ".gitignore");
1547
1702
  const requiredEntries = [
1548
1703
  ".cnos/env/.env",
1549
1704
  ".cnos/env/.env.*",
@@ -1556,7 +1711,7 @@ async function ensureGitignore(root) {
1556
1711
  ];
1557
1712
  let current = "";
1558
1713
  try {
1559
- current = await readFile4(gitignorePath, "utf8");
1714
+ current = await readFile3(gitignorePath, "utf8");
1560
1715
  } catch {
1561
1716
  current = "";
1562
1717
  }
@@ -1566,18 +1721,18 @@ async function ensureGitignore(root) {
1566
1721
  }
1567
1722
  const prefix = current.trim().length > 0 ? `${current.trimEnd()}
1568
1723
  ` : "";
1569
- await writeFile4(gitignorePath, `${prefix}${missingEntries.join("\n")}
1724
+ await writeFile3(gitignorePath, `${prefix}${missingEntries.join("\n")}
1570
1725
  `, "utf8");
1571
1726
  return true;
1572
1727
  }
1573
1728
  async function scaffoldWorkspace(root, workspace) {
1574
- const cnosRoot = path5.join(root, ".cnos");
1575
- const workspaceRoot = workspace ? path5.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1729
+ const cnosRoot = path6.join(root, ".cnos");
1730
+ const workspaceRoot = workspace ? path6.join(cnosRoot, "workspaces", workspace) : cnosRoot;
1576
1731
  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 });
1732
+ await mkdir3(path6.join(workspaceRoot, "profiles"), { recursive: true });
1733
+ await mkdir3(path6.join(workspaceRoot, "values"), { recursive: true });
1734
+ await mkdir3(path6.join(workspaceRoot, "secrets"), { recursive: true });
1735
+ await mkdir3(path6.join(workspaceRoot, "env"), { recursive: true });
1581
1736
  const relativePaths = workspace ? [
1582
1737
  ["workspaces", workspace, "profiles", ".gitkeep"],
1583
1738
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -1590,15 +1745,15 @@ async function scaffoldWorkspace(root, workspace) {
1590
1745
  ["env", ".gitkeep"]
1591
1746
  ];
1592
1747
  for (const relativePath of relativePaths) {
1593
- const filePath = path5.join(cnosRoot, ...relativePath);
1748
+ const filePath = path6.join(cnosRoot, ...relativePath);
1594
1749
  if (await ensureFile(filePath, "")) {
1595
- createdPaths.push(path5.relative(root, filePath).replace(/\\/g, "/"));
1750
+ createdPaths.push(path6.relative(root, filePath).replace(/\\/g, "/"));
1596
1751
  }
1597
1752
  }
1598
- if (await ensureFile(path5.join(cnosRoot, "cnos.yml"), scaffoldManifest(path5.basename(root), workspace))) {
1753
+ if (await ensureFile(path6.join(cnosRoot, "cnos.yml"), scaffoldManifest(path6.basename(root), workspace))) {
1599
1754
  createdPaths.push(".cnos/cnos.yml");
1600
1755
  }
1601
- if (workspace && await ensureFile(path5.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1756
+ if (workspace && await ensureFile(path6.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1602
1757
  globalRoot: ~/.cnos
1603
1758
  `)) {
1604
1759
  createdPaths.push(".cnos-workspace.yml");
@@ -1615,7 +1770,7 @@ globalRoot: ~/.cnos
1615
1770
 
1616
1771
  // src/commands/init.ts
1617
1772
  async function runInit(options = {}) {
1618
- const root = path6.resolve(options.root ?? process.cwd());
1773
+ const root = path7.resolve(options.root ?? process.cwd());
1619
1774
  const result = await scaffoldWorkspace(root, options.workspace);
1620
1775
  if (options.json) {
1621
1776
  return printJson(result);
@@ -1626,6 +1781,12 @@ async function runInit(options = {}) {
1626
1781
  return `initialized CNOS project at ${root}`;
1627
1782
  }
1628
1783
 
1784
+ // src/format/maskSecret.ts
1785
+ var MASKED_SECRET_VALUE = "****";
1786
+ function maskSecretValue(value) {
1787
+ return value === void 0 ? "" : MASKED_SECRET_VALUE;
1788
+ }
1789
+
1629
1790
  // src/format/printInspect.ts
1630
1791
  function printInspect(record) {
1631
1792
  const lines = [
@@ -1650,12 +1811,22 @@ function printInspect(record) {
1650
1811
 
1651
1812
  // src/commands/inspect.ts
1652
1813
  async function runInspect(key, options = {}) {
1814
+ const reveal = options.cliArgs?.includes("--reveal") ?? false;
1653
1815
  const runtime = await createRuntimeService(options);
1654
1816
  const inspectResult = runtime.inspect(key);
1817
+ const value = key.startsWith("secret.") && !reveal ? maskSecretValue(inspectResult.value) : inspectResult.value;
1818
+ const printable = {
1819
+ ...inspectResult,
1820
+ value,
1821
+ overridden: inspectResult.overridden.map((entry) => ({
1822
+ ...entry,
1823
+ value: key.startsWith("secret.") && !reveal ? maskSecretValue(entry.value) : entry.value
1824
+ }))
1825
+ };
1655
1826
  if (options.json) {
1656
- return printJson(inspectResult);
1827
+ return printJson(printable);
1657
1828
  }
1658
- return printInspect(inspectResult);
1829
+ return printInspect(printable);
1659
1830
  }
1660
1831
 
1661
1832
  // src/format/printValue.ts
@@ -1691,7 +1862,7 @@ function matchesPrefix(key, prefix) {
1691
1862
  return key.startsWith(prefix) || key.split(".").slice(1).join(".").startsWith(prefix);
1692
1863
  }
1693
1864
  function toStoredEntry(namespace, entry, filter = {}) {
1694
- const sourceId = namespace === "value" ? "filesystem-values" : "filesystem-secrets";
1865
+ const sourceId = namespace === "secret" ? "filesystem-secrets" : "filesystem-values";
1695
1866
  const candidates = [entry.winner, ...entry.overridden].filter((candidate) => candidate.sourceId === sourceId);
1696
1867
  if (candidates.length === 0) {
1697
1868
  return void 0;
@@ -1712,16 +1883,16 @@ function listStoredNamespace(namespace, options) {
1712
1883
  }
1713
1884
  function listProjectedNamespace(namespace, options) {
1714
1885
  return createRuntimeService(options).then((runtime) => {
1715
- const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : runtime.toPublicEnv({
1886
+ const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : namespace === "public" ? runtime.toPublicEnv({
1716
1887
  ...options.framework ? {
1717
1888
  framework: options.framework
1718
1889
  } : {}
1719
- });
1890
+ }) : flattenObject2(runtime.toNamespace(namespace));
1720
1891
  const entries = namespace === "env" ? Object.entries(projected).map(([envVar, value]) => ({
1721
1892
  key: envVar,
1722
1893
  value
1723
1894
  })) : Object.entries(projected).map(([key, value]) => ({
1724
- key: namespace === "meta" ? `meta.${key}` : key,
1895
+ key: namespace === "meta" || namespace === "process" ? `${namespace}.${key}` : key,
1725
1896
  value
1726
1897
  }));
1727
1898
  return entries.filter((entry) => entry.value !== void 0).filter((entry) => matchesPrefix(entry.key, options.prefix)).sort((left, right) => left.key.localeCompare(right.key));
@@ -1731,15 +1902,21 @@ async function listConfigEntries(namespace, options = {}) {
1731
1902
  if (namespace === "value" || namespace === "secret") {
1732
1903
  return listStoredNamespace(namespace, options);
1733
1904
  }
1734
- if (namespace === "meta" || namespace === "env" || namespace === "public") {
1905
+ if (namespace === "meta" || namespace === "env" || namespace === "public" || namespace === "process") {
1735
1906
  return listProjectedNamespace(namespace, options);
1736
1907
  }
1737
- const [values, secrets, meta] = await Promise.all([
1738
- listStoredNamespace("value", options),
1739
- listStoredNamespace("secret", options),
1740
- listProjectedNamespace("meta", options)
1741
- ]);
1742
- return [...values, ...secrets, ...meta].sort((left, right) => left.key.localeCompare(right.key));
1908
+ if (namespace !== "all") {
1909
+ return listStoredNamespace(namespace, options);
1910
+ }
1911
+ const runtime = await createRuntimeService(options);
1912
+ const namespaces = Array.from(
1913
+ new Set(
1914
+ Array.from(runtime.graph.entries.values()).map((entry) => entry.namespace).filter((entry) => entry !== "meta" && entry !== "env" && entry !== "public")
1915
+ )
1916
+ ).sort((left, right) => left.localeCompare(right));
1917
+ const stored = await Promise.all(namespaces.map((entry) => listStoredNamespace(entry, options)));
1918
+ const meta = await listProjectedNamespace("meta", options);
1919
+ return [...stored.flat(), ...meta].sort((left, right) => left.key.localeCompare(right.key));
1743
1920
  }
1744
1921
 
1745
1922
  // src/commands/list.ts
@@ -1756,13 +1933,16 @@ function normalizeNamespace(value) {
1756
1933
  if (value === "meta") {
1757
1934
  return "meta";
1758
1935
  }
1936
+ if (value === "process") {
1937
+ return "process";
1938
+ }
1759
1939
  if (value === "env") {
1760
1940
  return "env";
1761
1941
  }
1762
1942
  if (value === "public") {
1763
1943
  return "public";
1764
1944
  }
1765
- throw new Error(`Unsupported list namespace: ${value}`);
1945
+ return value;
1766
1946
  }
1767
1947
  async function runList(args = [], options = {}) {
1768
1948
  const cliArgs = [...options.cliArgs ?? []];
@@ -1784,35 +1964,200 @@ async function runList(args = [], options = {}) {
1784
1964
  return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1785
1965
  }
1786
1966
 
1787
- // src/commands/onboard.ts
1788
- import { copyFile, readdir, rm as rm3 } from "fs/promises";
1789
- import path7 from "path";
1967
+ // src/commands/migrate.ts
1968
+ import path8 from "path";
1969
+ import {
1970
+ applyManifestMappings,
1971
+ loadManifest,
1972
+ proposeMapping,
1973
+ rewriteSourceFiles,
1974
+ scanEnvUsage
1975
+ } from "@kitsy/cnos/internal";
1976
+ async function runMigrate(options = {}) {
1977
+ const cliArgs = [...options.cliArgs ?? []];
1978
+ const scan = consumeOption(cliArgs, "--scan");
1979
+ const apply = consumeFlag(cliArgs, "--apply");
1980
+ const dryRun = consumeFlag(cliArgs, "--dry-run") || !apply;
1981
+ const rewrite = consumeFlag(cliArgs, "--rewrite");
1982
+ if (cliArgs.length > 0) {
1983
+ throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
1984
+ }
1985
+ const manifest = await loadManifest(options.root ? { root: options.root } : {});
1986
+ const scanRoot = path8.resolve(manifest.repoRoot, scan ?? "src");
1987
+ const usages = await scanEnvUsage(scanRoot);
1988
+ const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
1989
+ const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
1990
+ let manifestResult;
1991
+ let rewriteResult;
1992
+ if (apply) {
1993
+ manifestResult = await applyManifestMappings(proposals, options.root);
1994
+ if (rewrite) {
1995
+ rewriteResult = await rewriteSourceFiles(usages.filter((usage) => usage.kind === "process-env"), uniqueProposals);
1996
+ }
1997
+ }
1998
+ if (options.json) {
1999
+ return printJson({
2000
+ scanRoot,
2001
+ dryRun,
2002
+ apply,
2003
+ rewrite,
2004
+ usages,
2005
+ proposals,
2006
+ ...manifestResult ? { manifest: manifestResult } : {},
2007
+ ...rewriteResult ? { rewriteResult } : {}
2008
+ });
2009
+ }
2010
+ const lines = [
2011
+ `Scanned ${usages.length} env usage${usages.length === 1 ? "" : "s"} in ${scanRoot}`,
2012
+ "",
2013
+ "Proposed mappings:",
2014
+ ...proposals.map(
2015
+ (proposal) => ` ${proposal.envVar} -> ${proposal.logicalKey}${proposal.public ? " (promote to public)" : ""}`
2016
+ )
2017
+ ];
2018
+ if (proposals.length === 0) {
2019
+ lines.push(" none");
2020
+ }
2021
+ if (dryRun) {
2022
+ lines.push("", "Dry run only. Re-run with --apply to update the manifest.");
2023
+ }
2024
+ if (manifestResult) {
2025
+ lines.push(
2026
+ "",
2027
+ `Updated ${manifestResult.manifestPath} with ${manifestResult.appliedMappings} env mapping${manifestResult.appliedMappings === 1 ? "" : "s"} and ${manifestResult.appliedPromotions} public promotion${manifestResult.appliedPromotions === 1 ? "" : "s"}.`
2028
+ );
2029
+ }
2030
+ if (rewrite) {
2031
+ if (!apply) {
2032
+ lines.push("", "Source rewrite requested but skipped because --apply was not set.");
2033
+ } else if (rewriteResult) {
2034
+ lines.push(
2035
+ "",
2036
+ `Rewrote ${rewriteResult.rewrittenFiles.length} file${rewriteResult.rewrittenFiles.length === 1 ? "" : "s"} and created ${rewriteResult.backupFiles.length} backup${rewriteResult.backupFiles.length === 1 ? "" : "s"}.`
2037
+ );
2038
+ if (rewriteResult.skippedUsages.length > 0) {
2039
+ lines.push("Skipped usages:");
2040
+ lines.push(...rewriteResult.skippedUsages.map((entry) => ` ${entry}`));
2041
+ }
2042
+ }
2043
+ }
2044
+ return lines.join("\n");
2045
+ }
2046
+
2047
+ // src/commands/namespace.ts
2048
+ import path9 from "path";
2049
+ function normalizeCommand2(args) {
2050
+ const [actionOrPath, ...tail] = args;
2051
+ if (!actionOrPath) {
2052
+ return {
2053
+ action: "list",
2054
+ tail: []
2055
+ };
2056
+ }
2057
+ if (["get", "set", "create", "add", "list", "delete", "remove"].includes(actionOrPath)) {
2058
+ return {
2059
+ action: actionOrPath === "remove" ? "delete" : actionOrPath === "create" || actionOrPath === "add" ? "set" : actionOrPath,
2060
+ tail
2061
+ };
2062
+ }
2063
+ return {
2064
+ action: "get",
2065
+ tail: args
2066
+ };
2067
+ }
2068
+ async function runNamespace(namespace, args = [], options = {}) {
2069
+ const { action, tail } = normalizeCommand2(args);
2070
+ const cliArgs = [...options.cliArgs ?? []];
2071
+ const root = path9.resolve(options.root ?? process.cwd());
2072
+ if (action === "list") {
2073
+ const prefix = consumeOption(cliArgs, "--prefix");
2074
+ const entries = await listConfigEntries(namespace, {
2075
+ ...options,
2076
+ cliArgs,
2077
+ ...prefix ? { prefix } : {}
2078
+ });
2079
+ if (options.json) {
2080
+ return printJson(entries);
2081
+ }
2082
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
2083
+ }
2084
+ if (action === "set") {
2085
+ const configPath2 = tail[0] ?? "app.name";
2086
+ const rawValue = tail[1] ?? "";
2087
+ const target = consumeOption(cliArgs, "--target") ?? "local";
2088
+ const result = await defineValue(namespace, configPath2, rawValue, {
2089
+ ...options,
2090
+ cliArgs,
2091
+ target
2092
+ });
2093
+ if (options.json) {
2094
+ return printJson({
2095
+ namespace,
2096
+ path: configPath2,
2097
+ target,
2098
+ filePath: result.filePath,
2099
+ value: result.value
2100
+ });
2101
+ }
2102
+ return `set ${namespace}.${configPath2} in ${displayPath(result.filePath, root)}`;
2103
+ }
2104
+ if (action === "delete") {
2105
+ const configPath2 = tail[0] ?? "app.name";
2106
+ const target = consumeOption(cliArgs, "--target") ?? "local";
2107
+ const result = await deleteValue(namespace, configPath2, {
2108
+ ...options,
2109
+ cliArgs,
2110
+ target
2111
+ });
2112
+ if (options.json) {
2113
+ return printJson(result);
2114
+ }
2115
+ return result.deleted ? `deleted ${namespace}.${configPath2} from ${displayPath(result.filePath, root)}` : `no ${namespace}.${configPath2} found in ${displayPath(result.filePath, root)}`;
2116
+ }
2117
+ const runtime = await createRuntimeService(options);
2118
+ const configPath = tail[0] ?? "app.name";
2119
+ const value = runtime.read(`${namespace}.${configPath}`);
2120
+ if (value === void 0) {
2121
+ throw new Error(`Missing CNOS ${namespace} path: ${configPath}`);
2122
+ }
2123
+ if (options.json) {
2124
+ return printJson({
2125
+ key: `${namespace}.${configPath}`,
2126
+ value
2127
+ });
2128
+ }
2129
+ return printValue(value);
2130
+ }
2131
+
2132
+ // src/commands/onboard.ts
2133
+ import { copyFile, readdir as readdir2, rm } from "fs/promises";
2134
+ import path10 from "path";
1790
2135
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
1791
2136
  async function listRootEnvFiles(root) {
1792
- const entries = await readdir(root, { withFileTypes: true });
2137
+ const entries = await readdir2(root, { withFileTypes: true });
1793
2138
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
1794
2139
  }
1795
2140
  async function runOnboard(options = {}) {
1796
- const root = path7.resolve(options.root ?? process.cwd());
1797
- const workspace = options.workspace ?? path7.basename(root);
2141
+ const root = path10.resolve(options.root ?? process.cwd());
2142
+ const workspace = options.workspace ?? path10.basename(root);
1798
2143
  const cliArgs = [...options.cliArgs ?? []];
1799
2144
  const move = consumeFlag(cliArgs, "--move");
1800
2145
  if (cliArgs.length > 0) {
1801
2146
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
1802
2147
  }
1803
2148
  const scaffold = await scaffoldWorkspace(root, workspace);
1804
- const envRoot = path7.join(root, ".cnos", "workspaces", workspace, "env");
2149
+ const envRoot = path10.join(root, ".cnos", "workspaces", workspace, "env");
1805
2150
  const rootFiles = await listRootEnvFiles(root);
1806
2151
  const imported = [];
1807
2152
  const skipped = [];
1808
2153
  for (const fileName of rootFiles) {
1809
- const sourcePath = path7.join(root, fileName);
1810
- const targetPath = path7.join(envRoot, fileName);
2154
+ const sourcePath = path10.join(root, fileName);
2155
+ const targetPath = path10.join(envRoot, fileName);
1811
2156
  try {
1812
2157
  await copyFile(sourcePath, targetPath);
1813
- imported.push(path7.relative(root, targetPath).replace(/\\/g, "/"));
2158
+ imported.push(path10.relative(root, targetPath).replace(/\\/g, "/"));
1814
2159
  if (move) {
1815
- await rm3(sourcePath);
2160
+ await rm(sourcePath);
1816
2161
  }
1817
2162
  } catch {
1818
2163
  skipped.push(fileName);
@@ -1835,17 +2180,17 @@ async function runOnboard(options = {}) {
1835
2180
  }
1836
2181
 
1837
2182
  // src/commands/profile.ts
1838
- import path10 from "path";
2183
+ import path13 from "path";
1839
2184
 
1840
2185
  // src/services/context.ts
1841
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
1842
- import path8 from "path";
1843
- import { parseYaml as parseYaml2, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2186
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2187
+ import path11 from "path";
2188
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
1844
2189
  async function loadCliContext(root = process.cwd()) {
1845
- const filePath = path8.join(path8.resolve(root), ".cnos-workspace.yml");
2190
+ const filePath = path11.join(path11.resolve(root), ".cnos-workspace.yml");
1846
2191
  try {
1847
- const source = await readFile5(filePath, "utf8");
1848
- const parsed = parseYaml2(source);
2192
+ const source = await readFile4(filePath, "utf8");
2193
+ const parsed = parseYaml3(source);
1849
2194
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1850
2195
  return {};
1851
2196
  }
@@ -1855,8 +2200,8 @@ async function loadCliContext(root = process.cwd()) {
1855
2200
  }
1856
2201
  }
1857
2202
  async function saveCliContext(options = {}) {
1858
- const root = path8.resolve(options.root ?? process.cwd());
1859
- const filePath = path8.join(root, ".cnos-workspace.yml");
2203
+ const root = path11.resolve(options.root ?? process.cwd());
2204
+ const filePath = path11.join(root, ".cnos-workspace.yml");
1860
2205
  const current = await loadCliContext(root);
1861
2206
  const next = {
1862
2207
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -1866,7 +2211,7 @@ async function saveCliContext(options = {}) {
1866
2211
  ...options.profile ? { profile: options.profile } : {},
1867
2212
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1868
2213
  };
1869
- await writeFile5(filePath, stringifyYaml3(next), "utf8");
2214
+ await writeFile4(filePath, stringifyYaml2(next), "utf8");
1870
2215
  return {
1871
2216
  filePath,
1872
2217
  context: next
@@ -1874,29 +2219,37 @@ async function saveCliContext(options = {}) {
1874
2219
  }
1875
2220
 
1876
2221
  // src/services/profiles.ts
1877
- import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, rm as rm4, writeFile as writeFile6 } from "fs/promises";
1878
- import path9 from "path";
1879
- import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
1880
- async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1881
- const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1882
- await mkdir4(path9.dirname(filePath), { recursive: true });
1883
- const document = inherit && inherit !== "base" ? {
2222
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
2223
+ import path12 from "path";
2224
+ import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2225
+ async function createProfileDefinition(root = process.cwd(), profile, inherit, options = {}) {
2226
+ const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2227
+ await mkdir4(path12.dirname(filePath), { recursive: true });
2228
+ const document = options.noInherit ? {
2229
+ name: profile,
2230
+ activate: {
2231
+ values: [`profiles/${profile}/values`, `values/${profile}`],
2232
+ secrets: [`profiles/${profile}/secrets`, `secrets/${profile}`],
2233
+ envFiles: [`.env.${profile}`]
2234
+ }
2235
+ } : inherit && inherit !== "base" ? {
1884
2236
  name: profile,
1885
2237
  extends: [inherit]
1886
2238
  } : {
1887
2239
  name: profile
1888
2240
  };
1889
- await writeFile6(filePath, stringifyYaml4(document), "utf8");
2241
+ await writeFile5(filePath, stringifyYaml3(document), "utf8");
1890
2242
  return {
1891
2243
  filePath,
1892
2244
  profile,
1893
- ...inherit ? { inherit } : {}
2245
+ ...inherit ? { inherit } : {},
2246
+ ...options.noInherit ? { noInherit: true } : {}
1894
2247
  };
1895
2248
  }
1896
2249
  async function listProfiles(root = process.cwd()) {
1897
- const profilesRoot = path9.join(path9.resolve(root), ".cnos", "profiles");
2250
+ const profilesRoot = path12.join(path12.resolve(root), ".cnos", "profiles");
1898
2251
  try {
1899
- const entries = await readdir2(profilesRoot, { withFileTypes: true });
2252
+ const entries = await readdir3(profilesRoot, { withFileTypes: true });
1900
2253
  const discovered = /* @__PURE__ */ new Set(["base"]);
1901
2254
  for (const entry of entries) {
1902
2255
  if (entry.isFile() && entry.name.endsWith(".yml")) {
@@ -1909,9 +2262,9 @@ async function listProfiles(root = process.cwd()) {
1909
2262
  }
1910
2263
  }
1911
2264
  async function deleteProfileDefinition(root = process.cwd(), profile) {
1912
- const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2265
+ const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1913
2266
  try {
1914
- await rm4(filePath);
2267
+ await rm2(filePath);
1915
2268
  return {
1916
2269
  filePath,
1917
2270
  deleted: true
@@ -1929,9 +2282,9 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
1929
2282
  name: "base"
1930
2283
  };
1931
2284
  }
1932
- const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2285
+ const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1933
2286
  try {
1934
- return parseYaml3(await readFile6(filePath, "utf8")) ?? void 0;
2287
+ return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
1935
2288
  } catch {
1936
2289
  return void 0;
1937
2290
  }
@@ -1953,16 +2306,23 @@ function normalizeProfileAction(args) {
1953
2306
  }
1954
2307
  async function runProfile(args, options = {}) {
1955
2308
  const { action, tail } = normalizeProfileAction(args);
1956
- const root = path10.resolve(options.root ?? process.cwd());
2309
+ const root = path13.resolve(options.root ?? process.cwd());
1957
2310
  const cliArgs = [...options.cliArgs ?? []];
1958
2311
  if (action === "create") {
1959
2312
  const profile = tail[0] ?? "stage";
1960
2313
  const inherit = consumeOption(cliArgs, "--inherit");
1961
- const result = await createProfileDefinition(root, profile, inherit);
2314
+ const noInherit = consumeFlag(cliArgs, "--no-inherit");
2315
+ if (inherit && noInherit) {
2316
+ throw new Error("profile create accepts either --inherit <name> or --no-inherit, not both");
2317
+ }
2318
+ const result = await createProfileDefinition(root, profile, inherit, { noInherit });
1962
2319
  if (options.json) {
1963
2320
  return printJson(result);
1964
2321
  }
1965
- return `created profile ${profile} at ${result.filePath}`;
2322
+ if (noInherit) {
2323
+ return `created profile ${profile} at ${displayPath(result.filePath, root)} without inheriting base`;
2324
+ }
2325
+ return `created profile ${profile} at ${displayPath(result.filePath, root)}; inherits values from base by default`;
1966
2326
  }
1967
2327
  if (action === "use") {
1968
2328
  const profile = tail[0] ?? "base";
@@ -1973,7 +2333,7 @@ async function runProfile(args, options = {}) {
1973
2333
  if (options.json) {
1974
2334
  return printJson(result);
1975
2335
  }
1976
- return `active profile set to ${profile} in ${result.filePath}`;
2336
+ return `active profile set to ${profile} in ${displayPath(result.filePath, root)}`;
1977
2337
  }
1978
2338
  if (action === "delete") {
1979
2339
  const profile = tail[0] ?? "base";
@@ -1997,11 +2357,12 @@ async function runProfile(args, options = {}) {
1997
2357
  }
1998
2358
 
1999
2359
  // src/commands/promote.ts
2000
- import { writeFile as writeFile7 } from "fs/promises";
2360
+ import path14 from "path";
2361
+ import { writeFile as writeFile6 } from "fs/promises";
2001
2362
  import {
2002
2363
  ensureProjectionAllowed,
2003
2364
  loadManifest as loadManifest2,
2004
- stringifyYaml as stringifyYaml5
2365
+ stringifyYaml as stringifyYaml4
2005
2366
  } from "@kitsy/cnos/internal";
2006
2367
  function normalizeTarget(value) {
2007
2368
  if (value === "public" || value === "env") {
@@ -2013,6 +2374,7 @@ function sortRecord(record) {
2013
2374
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
2014
2375
  }
2015
2376
  async function runPromote(args = [], options = {}) {
2377
+ const root = path14.resolve(options.root ?? process.cwd());
2016
2378
  const cliArgs = [...options.cliArgs ?? []];
2017
2379
  const target = normalizeTarget(consumeOption(cliArgs, "--to"));
2018
2380
  const alias = consumeOption(cliArgs, "--as");
@@ -2051,7 +2413,7 @@ async function runPromote(args = [], options = {}) {
2051
2413
  })
2052
2414
  };
2053
2415
  }
2054
- await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2416
+ await writeFile6(loadedManifest.manifestPath, stringifyYaml4(rawManifest), "utf8");
2055
2417
  if (options.json) {
2056
2418
  return printJson({
2057
2419
  target,
@@ -2060,7 +2422,7 @@ async function runPromote(args = [], options = {}) {
2060
2422
  manifestPath: loadedManifest.manifestPath
2061
2423
  });
2062
2424
  }
2063
- return target === "public" ? `promoted ${keys.join(", ")} to public in ${loadedManifest.manifestPath}` : `promoted ${keys[0]} to env as ${alias} in ${loadedManifest.manifestPath}`;
2425
+ return target === "public" ? `promoted ${keys.join(", ")} to public in ${displayPath(loadedManifest.manifestPath, root)}` : `promoted ${keys[0]} to env as ${alias} in ${displayPath(loadedManifest.manifestPath, root)}`;
2064
2426
  }
2065
2427
 
2066
2428
  // src/commands/read.ts
@@ -2070,16 +2432,23 @@ async function runRead(key, options = {}) {
2070
2432
  if (value === void 0) {
2071
2433
  throw new Error(`Missing CNOS config key: ${key}`);
2072
2434
  }
2435
+ const isSecret = key.startsWith("secret.");
2436
+ const cliArgs = [...options.cliArgs ?? []];
2437
+ const reveal = consumeFlag(cliArgs, "--reveal");
2438
+ const valueForOutput = isSecret && !reveal ? maskSecretValue(value) : value;
2073
2439
  if (options.json) {
2074
- return printJson({ key, value });
2440
+ return printJson({ key, value: valueForOutput });
2075
2441
  }
2076
- return printValue(value);
2442
+ return printValue(valueForOutput);
2077
2443
  }
2078
2444
 
2079
2445
  // src/commands/run.ts
2080
2446
  import { spawn } from "child_process";
2081
2447
  import {
2082
2448
  CNOS_GRAPH_ENV_VAR,
2449
+ CNOS_SECRET_PAYLOAD_ENV_VAR,
2450
+ CNOS_SESSION_KEY_ENV_VAR,
2451
+ serializeSecretPayload,
2083
2452
  serializeRuntimeGraph
2084
2453
  } from "@kitsy/cnos/internal";
2085
2454
  function consumeOptions(args, flag) {
@@ -2118,6 +2487,7 @@ async function runCommand(command, options = {}) {
2118
2487
  }
2119
2488
  const cliArgs = [...options.cliArgs ?? []];
2120
2489
  const isPublic = consumeFlag(cliArgs, "--public");
2490
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
2121
2491
  const framework = consumeOption(cliArgs, "--framework");
2122
2492
  const prefix = consumeOption(cliArgs, "--prefix");
2123
2493
  const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
@@ -2125,13 +2495,21 @@ async function runCommand(command, options = {}) {
2125
2495
  ...options,
2126
2496
  cliArgs: [...cliArgs, ...setOverrides]
2127
2497
  });
2498
+ const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
2499
+ Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
2500
+ ) : void 0;
2501
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
2128
2502
  const env = {
2129
2503
  ...process.env,
2130
2504
  ...isPublic ? runtime.toPublicEnv({
2131
2505
  ...framework ? { framework } : {},
2132
2506
  ...prefix ? { prefix } : {}
2133
2507
  }) : runtime.toEnv(),
2134
- [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
2508
+ [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
2509
+ ...secretPayload ? {
2510
+ [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
2511
+ [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
2512
+ } : {}
2135
2513
  };
2136
2514
  return new Promise((resolve, reject) => {
2137
2515
  const executable = command[0];
@@ -2166,12 +2544,203 @@ async function runCommand(command, options = {}) {
2166
2544
  });
2167
2545
  }
2168
2546
 
2547
+ // src/commands/secret.ts
2548
+ import path17 from "path";
2549
+
2550
+ // src/commands/vault.ts
2551
+ import path16 from "path";
2552
+
2553
+ // src/services/vaults.ts
2554
+ import { rm as rm3, writeFile as writeFile7 } from "fs/promises";
2555
+ import path15 from "path";
2556
+ import {
2557
+ clearAllVaultSessionKeys,
2558
+ clearVaultSessionKey,
2559
+ createSecretVault,
2560
+ deriveVaultKey,
2561
+ listLocalSecrets,
2562
+ loadManifest as loadManifest3,
2563
+ listSecretVaults,
2564
+ readVaultMetadata,
2565
+ resolveSecretStoreRoot as resolveSecretStoreRoot2,
2566
+ resolveVaultAuth as resolveVaultAuth2,
2567
+ resolveVaultDefinition,
2568
+ stringifyYaml as stringifyYaml5,
2569
+ writeKeychain,
2570
+ writeVaultSessionKey
2571
+ } from "@kitsy/cnos/internal";
2572
+ function buildVaultDefinition(vault, provider) {
2573
+ return provider === "local" ? {
2574
+ provider: "local",
2575
+ auth: {
2576
+ passphrase: {
2577
+ from: defaultLocalAuthSources(vault)
2578
+ }
2579
+ }
2580
+ } : {
2581
+ provider,
2582
+ auth: {
2583
+ method: "environment"
2584
+ }
2585
+ };
2586
+ }
2587
+ function sortVaults(vaults) {
2588
+ return Object.fromEntries(Object.entries(vaults).sort(([left], [right]) => left.localeCompare(right)));
2589
+ }
2590
+ function defaultLocalAuthSources(vault) {
2591
+ const token = vault.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2592
+ return [`env:CNOS_SECRET_PASSPHRASE_${token}`, "env:CNOS_SECRET_PASSPHRASE", `keychain:cnos/${vault}`, "prompt"];
2593
+ }
2594
+ async function createVaultDefinition(name, options = {}) {
2595
+ const vault = name.trim() || "default";
2596
+ const provider = options.provider?.trim() || "local";
2597
+ if (provider === "local" && (options.noPassphrase ?? false)) {
2598
+ throw new Error("Local vaults cannot be passwordless.");
2599
+ }
2600
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2601
+ const vaultDefinition = buildVaultDefinition(vault, provider);
2602
+ const rawManifest = {
2603
+ ...loadedManifest.rawManifest,
2604
+ vaults: {
2605
+ ...loadedManifest.rawManifest.vaults ?? {},
2606
+ [vault]: vaultDefinition
2607
+ }
2608
+ };
2609
+ await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2610
+ const definition = resolveVaultDefinition({ [vault]: vaultDefinition }, vault);
2611
+ if (provider === "local") {
2612
+ const auth = await resolveVaultAuth2(vault, vaultDefinition, options.processEnv ?? process.env);
2613
+ if (!auth.passphrase) {
2614
+ throw new Error(`Vault "${vault}" requires passphrase-based authentication during creation.`);
2615
+ }
2616
+ const storeRoot = resolveSecretStoreRoot2(options.processEnv);
2617
+ const existing = await readVaultMetadata(storeRoot, vault);
2618
+ if (!existing) {
2619
+ await createSecretVault(storeRoot, vault, auth.passphrase);
2620
+ }
2621
+ }
2622
+ return {
2623
+ ...definition,
2624
+ authMethod: definition.auth?.method ?? (provider === "local" ? "passphrase" : "environment"),
2625
+ localStore: provider === "local",
2626
+ manifestPath: loadedManifest.manifestPath
2627
+ };
2628
+ }
2629
+ async function listVaultDefinitions(options = {}) {
2630
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2631
+ const localStoreVaults = await listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
2632
+ return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
2633
+ const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
2634
+ return {
2635
+ ...definition,
2636
+ authMethod: definition.auth?.method ?? (definition.provider === "local" ? "passphrase" : "environment"),
2637
+ localStore: localStoreVaults.includes(name)
2638
+ };
2639
+ });
2640
+ }
2641
+ async function removeVaultDefinition(name, options = {}) {
2642
+ const vault = name.trim() || "default";
2643
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2644
+ if (!loadedManifest.rawManifest.vaults?.[vault]) {
2645
+ return {
2646
+ name: vault,
2647
+ deleted: false,
2648
+ manifestPath: loadedManifest.manifestPath
2649
+ };
2650
+ }
2651
+ const nextVaults = { ...loadedManifest.rawManifest.vaults ?? {} };
2652
+ delete nextVaults[vault];
2653
+ const rawManifest = {
2654
+ ...loadedManifest.rawManifest,
2655
+ ...Object.keys(nextVaults).length > 0 ? { vaults: sortVaults(nextVaults) } : {}
2656
+ };
2657
+ if (Object.keys(nextVaults).length === 0) {
2658
+ delete rawManifest.vaults;
2659
+ }
2660
+ await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2661
+ const vaultRoot = path15.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
2662
+ let removedStore;
2663
+ try {
2664
+ await rm3(vaultRoot, { recursive: true, force: true });
2665
+ removedStore = vaultRoot;
2666
+ } catch {
2667
+ removedStore = void 0;
2668
+ }
2669
+ await clearVaultSessionKey(vault, options.processEnv);
2670
+ return {
2671
+ name: vault,
2672
+ deleted: true,
2673
+ manifestPath: loadedManifest.manifestPath,
2674
+ ...removedStore ? { removedStore } : {}
2675
+ };
2676
+ }
2677
+ async function listLocalStoreVaults(options = {}) {
2678
+ return listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
2679
+ }
2680
+ async function authenticateVault(name, options = {}) {
2681
+ const vault = name.trim() || "default";
2682
+ const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
2683
+ const definition = loadedManifest.manifest.vaults[vault];
2684
+ if (!definition) {
2685
+ throw new Error(`Unknown vault "${vault}"`);
2686
+ }
2687
+ const auth = await resolveVaultAuth2(vault, definition, options.processEnv ?? process.env);
2688
+ const storeRoot = resolveSecretStoreRoot2(options.processEnv);
2689
+ if (definition.provider === "local") {
2690
+ if (!auth.passphrase) {
2691
+ throw new Error(`Vault "${vault}" requires passphrase-based authentication.`);
2692
+ }
2693
+ const metadata = await readVaultMetadata(storeRoot, vault);
2694
+ if (!metadata) {
2695
+ throw new Error(
2696
+ `Vault "${vault}" has not been initialized yet. Run cnos vault create ${vault} first.`
2697
+ );
2698
+ }
2699
+ const derivedKey = deriveVaultKey(auth.passphrase, Buffer.from(metadata.salt, "base64"), metadata.iterations);
2700
+ await listLocalSecrets(
2701
+ storeRoot,
2702
+ {
2703
+ derivedKey,
2704
+ method: auth.method,
2705
+ ...definition.auth?.config ? { config: definition.auth.config } : {}
2706
+ },
2707
+ vault
2708
+ );
2709
+ const sessionPath2 = await writeVaultSessionKey(vault, derivedKey, options.processEnv);
2710
+ if (options.storeKeychain) {
2711
+ await writeKeychain(`cnos/${vault}`, derivedKey.toString("hex"));
2712
+ }
2713
+ return {
2714
+ name: vault,
2715
+ method: auth.method,
2716
+ storedInKeychain: options.storeKeychain ?? false,
2717
+ sessionPath: sessionPath2
2718
+ };
2719
+ }
2720
+ const sessionPath = await writeVaultSessionKey(vault, Buffer.from(vault, "utf8"), options.processEnv);
2721
+ return {
2722
+ name: vault,
2723
+ method: auth.method,
2724
+ storedInKeychain: false,
2725
+ sessionPath
2726
+ };
2727
+ }
2728
+ async function logoutVault(name, options = {}) {
2729
+ if (options.all) {
2730
+ await clearAllVaultSessionKeys(options.processEnv);
2731
+ return { scope: "all" };
2732
+ }
2733
+ const vault = name?.trim() || "default";
2734
+ await clearVaultSessionKey(vault, options.processEnv);
2735
+ return { scope: vault };
2736
+ }
2737
+
2169
2738
  // src/commands/vault.ts
2170
2739
  function normalizeVaultAction(args) {
2171
2740
  const [action = "list", ...tail] = args;
2172
- if (["create", "add", "list", "delete", "remove"].includes(action)) {
2741
+ if (["create", "add", "list", "delete", "remove", "auth", "logout"].includes(action)) {
2173
2742
  return {
2174
- action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : "list",
2743
+ action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : action === "auth" ? "auth" : action === "logout" ? "logout" : "list",
2175
2744
  tail
2176
2745
  };
2177
2746
  }
@@ -2183,22 +2752,46 @@ function normalizeVaultAction(args) {
2183
2752
  async function runVault(args = [], options = {}) {
2184
2753
  const { action, tail } = normalizeVaultAction(args);
2185
2754
  const cliArgs = [...options.cliArgs ?? []];
2755
+ const root = path16.resolve(options.root ?? process.cwd());
2756
+ if (consumeOption(cliArgs, "--passphrase")) {
2757
+ throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
2758
+ }
2186
2759
  if (action === "create") {
2187
2760
  const name = tail[0] ?? "default";
2188
2761
  const provider = consumeOption(cliArgs, "--provider") ?? "local";
2189
- const passphrase = consumeOption(cliArgs, "--passphrase");
2190
2762
  const noPassphrase = consumeFlag(cliArgs, "--no-passphrase");
2191
2763
  const result = await createVaultDefinition(name, {
2192
2764
  ...options,
2193
2765
  cliArgs,
2194
2766
  provider,
2195
- ...passphrase ? { passphrase } : {},
2196
2767
  ...noPassphrase ? { noPassphrase: true } : {}
2197
2768
  });
2198
2769
  if (options.json) {
2199
2770
  return printJson(result);
2200
2771
  }
2201
- return `created vault "${result.name}" with provider "${result.provider}" in ${result.manifestPath}`;
2772
+ return `created vault "${result.name}" with provider "${result.provider}" in ${displayPath(result.manifestPath, root)}`;
2773
+ }
2774
+ if (action === "auth") {
2775
+ const result = await authenticateVault(tail[0] ?? "default", {
2776
+ ...options,
2777
+ cliArgs,
2778
+ storeKeychain: consumeFlag(cliArgs, "--store-keychain")
2779
+ });
2780
+ if (options.json) {
2781
+ return printJson(result);
2782
+ }
2783
+ return `authenticated vault "${result.name}" via ${result.method}`;
2784
+ }
2785
+ if (action === "logout") {
2786
+ const result = await logoutVault(tail[0], {
2787
+ ...options,
2788
+ cliArgs,
2789
+ all: consumeFlag(cliArgs, "--all")
2790
+ });
2791
+ if (options.json) {
2792
+ return printJson(result);
2793
+ }
2794
+ return result.scope === "all" ? "logged out all vault sessions" : `logged out vault "${result.scope}"`;
2202
2795
  }
2203
2796
  if (action === "remove") {
2204
2797
  const name = tail[0] ?? "default";
@@ -2224,16 +2817,11 @@ async function runVault(args = [], options = {}) {
2224
2817
  return "";
2225
2818
  }
2226
2819
  return manifestVaults.map(
2227
- (vault) => `${vault.name} provider=${vault.provider} passphrase=${vault.passphrasePolicy}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
2820
+ (vault) => `${vault.name} provider=${vault.provider} auth=${vault.authMethod}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
2228
2821
  ).join("\n");
2229
2822
  }
2230
2823
 
2231
2824
  // 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
2825
  function normalizeSecretCommand(args) {
2238
2826
  const [actionOrPath, next, ...tail] = args;
2239
2827
  if (!actionOrPath) {
@@ -2259,53 +2847,74 @@ function normalizeSecretCommand(args) {
2259
2847
  tail: args
2260
2848
  };
2261
2849
  }
2850
+ async function readStdinValue() {
2851
+ const chunks = [];
2852
+ for await (const chunk of process.stdin) {
2853
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2854
+ }
2855
+ return Buffer.concat(chunks).toString("utf8").trimEnd();
2856
+ }
2262
2857
  async function runSecret(argsOrPath, options = {}) {
2263
2858
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
2264
2859
  const { action, tail } = normalizeSecretCommand(args);
2265
2860
  const cliArgs = [...options.cliArgs ?? []];
2861
+ const root = path17.resolve(options.root ?? process.cwd());
2862
+ if (consumeOption(cliArgs, "--passphrase")) {
2863
+ throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
2864
+ }
2266
2865
  if (action === "create-vault") {
2267
2866
  return runVault(["create", tail[0] ?? "default"], options);
2268
2867
  }
2269
2868
  if (action === "list") {
2869
+ const runtime2 = await createRuntimeService(options);
2270
2870
  const prefix = consumeOption(cliArgs, "--prefix");
2271
2871
  const vault = consumeOption(cliArgs, "--vault");
2272
2872
  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
- });
2873
+ 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) => {
2874
+ const secretRef2 = entry2.winner.metadata?.secretRef;
2875
+ if (vault && secretRef2?.vault !== vault) {
2876
+ return false;
2877
+ }
2878
+ if (provider && secretRef2?.provider !== provider) {
2879
+ return false;
2880
+ }
2881
+ return true;
2882
+ }).map((entry2) => {
2883
+ const secretRef2 = entry2.winner.metadata?.secretRef;
2884
+ return {
2885
+ key: entry2.key,
2886
+ vault: secretRef2?.vault ?? "default",
2887
+ provider: secretRef2?.provider ?? "local"
2888
+ };
2889
+ }).sort((left, right) => left.key.localeCompare(right.key));
2280
2890
  if (options.json) {
2281
2891
  return printJson(entries);
2282
2892
  }
2283
- return entries.map((entry2) => `${entry2.key}=${printValue(entry2.value)}`).join("\n");
2893
+ return entries.map((entry2) => `${entry2.key} (vault: ${entry2.vault}, provider: ${entry2.provider})`).join("\n");
2284
2894
  }
2285
2895
  if (action === "set") {
2286
2896
  const secretPath2 = tail[0];
2287
- const rawValue = tail[1] ?? "";
2288
2897
  const local = consumeFlag(cliArgs, "--local");
2289
2898
  const remote = consumeFlag(cliArgs, "--remote");
2290
2899
  const ref = consumeFlag(cliArgs, "--ref");
2900
+ const stdin = consumeFlag(cliArgs, "--stdin");
2291
2901
  const target = consumeOption(cliArgs, "--target") ?? "local";
2292
2902
  const provider = consumeOption(cliArgs, "--provider");
2293
- const passphrase = consumeOption(cliArgs, "--passphrase");
2294
2903
  const vault = consumeOption(cliArgs, "--vault") ?? "default";
2295
2904
  const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
2905
+ const rawValue = stdin ? await readStdinValue() : tail[1] ?? "";
2296
2906
  const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
2297
2907
  ...options,
2298
2908
  cliArgs,
2299
2909
  target,
2300
2910
  vault,
2301
2911
  ...mode ? { mode } : {},
2302
- ...provider ? { provider } : {},
2303
- ...passphrase ? { passphrase } : {}
2912
+ ...provider ? { provider } : {}
2304
2913
  });
2305
2914
  if (options.json) {
2306
2915
  return printJson(result);
2307
2916
  }
2308
- return result.provider === "local" ? `set secret.${secretPath2} in vault "${result.vault ?? "default"}" with ref "${result.ref}" and repo pointer ${result.filePath}` : `set secret.${secretPath2} via ${result.provider} in ${result.filePath}`;
2917
+ return result.provider === "local" ? `set secret.${secretPath2} in vault "${result.vault ?? "default"}" with ref "${result.ref}" and repo pointer ${displayPath(result.filePath, root)}` : `set secret.${secretPath2} via ${result.provider} in ${displayPath(result.filePath, root)}`;
2309
2918
  }
2310
2919
  if (action === "delete") {
2311
2920
  const secretPath2 = tail[0];
@@ -2318,11 +2927,12 @@ async function runSecret(argsOrPath, options = {}) {
2318
2927
  if (options.json) {
2319
2928
  return printJson(result);
2320
2929
  }
2321
- return result.deleted ? `deleted secret.${secretPath2} from ${result.filePath}` : `no secret.${secretPath2} found in ${result.filePath}`;
2930
+ return result.deleted ? `deleted secret.${secretPath2} from ${displayPath(result.filePath, root)}` : `no secret.${secretPath2} found in ${displayPath(result.filePath, root)}`;
2322
2931
  }
2323
2932
  const runtime = await createRuntimeService(options);
2324
2933
  const secretPath = tail[0] ?? "app.token";
2325
2934
  const expectedVault = consumeOption(cliArgs, "--vault");
2935
+ const reveal = consumeFlag(cliArgs, "--reveal");
2326
2936
  const entry = runtime.graph.entries.get(`secret.${secretPath}`);
2327
2937
  const secretRef = entry?.winner.metadata?.secretRef;
2328
2938
  const value = runtime.secret(secretPath);
@@ -2332,36 +2942,24 @@ async function runSecret(argsOrPath, options = {}) {
2332
2942
  if (expectedVault && secretRef?.vault && secretRef.vault !== expectedVault) {
2333
2943
  throw new Error(`Secret ${secretPath} belongs to vault "${secretRef.vault}", not "${expectedVault}"`);
2334
2944
  }
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
- }
2945
+ const valueForOutput = reveal ? value : maskSecretValue(value);
2349
2946
  if (options.json) {
2350
2947
  return printJson({
2351
2948
  key: `secret.${secretPath}`,
2352
- value
2949
+ value: valueForOutput,
2950
+ vault: secretRef?.vault ?? "default"
2353
2951
  });
2354
2952
  }
2355
- return printValue(value);
2953
+ return printValue(valueForOutput);
2356
2954
  }
2357
2955
 
2358
2956
  // src/commands/use.ts
2359
- import path11 from "path";
2957
+ import path18 from "path";
2360
2958
  async function runUse(args = [], options = {}) {
2361
- const root = path11.resolve(options.root ?? process.cwd());
2362
- const action = args[0] ?? "show";
2959
+ const root = path18.resolve(options.root ?? process.cwd());
2960
+ const action = args[0];
2363
2961
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
2364
- if (action === "show" || !hasUpdates) {
2962
+ if (action === "show" || !action && !hasUpdates) {
2365
2963
  const context = await loadCliContext(root);
2366
2964
  if (options.json) {
2367
2965
  return printJson(context);
@@ -2377,7 +2975,7 @@ async function runUse(args = [], options = {}) {
2377
2975
  if (options.json) {
2378
2976
  return printJson(result);
2379
2977
  }
2380
- return `updated CLI context in ${result.filePath}`;
2978
+ return `updated CLI context in ${displayPath(result.filePath, root)}`;
2381
2979
  }
2382
2980
 
2383
2981
  // src/commands/validate.ts
@@ -2395,7 +2993,7 @@ async function runValidate(options = {}) {
2395
2993
  // package.json
2396
2994
  var package_default = {
2397
2995
  name: "@kitsy/cnos-cli",
2398
- version: "1.2.0",
2996
+ version: "1.4.0",
2399
2997
  description: "CLI entry point and developer tooling for CNOS.",
2400
2998
  type: "module",
2401
2999
  main: "./dist/index.js",
@@ -2450,6 +3048,7 @@ function runVersion() {
2450
3048
  }
2451
3049
 
2452
3050
  // src/commands/value.ts
3051
+ import path19 from "path";
2453
3052
  function normalizeValueCommand(args) {
2454
3053
  const [actionOrPath, ...tail] = args;
2455
3054
  if (!actionOrPath) {
@@ -2473,6 +3072,7 @@ async function runValue(argsOrPath, options = {}) {
2473
3072
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
2474
3073
  const { action, tail } = normalizeValueCommand(args);
2475
3074
  const cliArgs = [...options.cliArgs ?? []];
3075
+ const root = path19.resolve(options.root ?? process.cwd());
2476
3076
  if (action === "list") {
2477
3077
  const prefix = consumeOption(cliArgs, "--prefix");
2478
3078
  const entries = await listConfigEntries("value", {
@@ -2503,7 +3103,7 @@ async function runValue(argsOrPath, options = {}) {
2503
3103
  value: result.value
2504
3104
  });
2505
3105
  }
2506
- return `set value.${valuePath} in ${result.filePath}`;
3106
+ return `set value.${valuePath} in ${displayPath(result.filePath, root)}`;
2507
3107
  }
2508
3108
  if (action === "delete") {
2509
3109
  const valuePath = tail[0] ?? "app.name";
@@ -2516,7 +3116,7 @@ async function runValue(argsOrPath, options = {}) {
2516
3116
  if (options.json) {
2517
3117
  return printJson(result);
2518
3118
  }
2519
- return result.deleted ? `deleted value.${valuePath} from ${result.filePath}` : `no value.${valuePath} found in ${result.filePath}`;
3119
+ return result.deleted ? `deleted value.${valuePath} from ${displayPath(result.filePath, root)}` : `no value.${valuePath} found in ${displayPath(result.filePath, root)}`;
2520
3120
  }
2521
3121
  const runtime = await createRuntimeService(options);
2522
3122
  const value = runtime.value(tail[0] ?? "app.name");
@@ -2532,6 +3132,180 @@ async function runValue(argsOrPath, options = {}) {
2532
3132
  return printValue(value);
2533
3133
  }
2534
3134
 
3135
+ // src/commands/watch.ts
3136
+ import { watch } from "fs";
3137
+ import { spawn as spawn2 } from "child_process";
3138
+ import {
3139
+ CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
3140
+ CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
3141
+ CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
3142
+ diffGraphs,
3143
+ serializeRuntimeGraph as serializeRuntimeGraph2,
3144
+ serializeSecretPayload as serializeSecretPayload2,
3145
+ watchFiles
3146
+ } from "@kitsy/cnos/internal";
3147
+ async function buildRunEnvironment(options) {
3148
+ const cliArgs = [...options.cliArgs ?? []];
3149
+ const isPublic = consumeFlag(cliArgs, "--public");
3150
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
3151
+ const framework = consumeOption(cliArgs, "--framework");
3152
+ const prefix = consumeOption(cliArgs, "--prefix");
3153
+ const runtime = await createRuntimeService({
3154
+ ...options,
3155
+ cliArgs
3156
+ });
3157
+ const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
3158
+ Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
3159
+ ) : void 0;
3160
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
3161
+ return {
3162
+ runtime,
3163
+ env: {
3164
+ ...process.env,
3165
+ ...isPublic ? runtime.toPublicEnv({
3166
+ ...framework ? { framework } : {},
3167
+ ...prefix ? { prefix } : {}
3168
+ }) : runtime.toEnv(),
3169
+ [CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
3170
+ ...secretPayload ? {
3171
+ [CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
3172
+ [CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
3173
+ } : {}
3174
+ }
3175
+ };
3176
+ }
3177
+ function spawnWatchedChild(command, cwd, env) {
3178
+ const executable = command[0];
3179
+ if (!executable) {
3180
+ throw new Error("watch requires a command after -- unless --signal is used");
3181
+ }
3182
+ return spawn2(executable, command.slice(1), {
3183
+ cwd,
3184
+ env,
3185
+ stdio: "inherit",
3186
+ shell: false
3187
+ });
3188
+ }
3189
+ async function startWatchLoop(options) {
3190
+ const cliArgs = [...options.cliArgs ?? []];
3191
+ const isSignal = consumeFlag(cliArgs, "--signal");
3192
+ const debounceMs = Number(consumeOption(cliArgs, "--debounce") ?? "300");
3193
+ const command = options.command ?? [];
3194
+ const root = options.root ?? process.cwd();
3195
+ let current = await buildRunEnvironment({
3196
+ ...options,
3197
+ cliArgs
3198
+ });
3199
+ let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
3200
+ const watcherMap = /* @__PURE__ */ new Map();
3201
+ let timer;
3202
+ let closed = false;
3203
+ const attachWatcher = (targetPath, recursive = false) => {
3204
+ if (watcherMap.has(targetPath)) {
3205
+ return;
3206
+ }
3207
+ try {
3208
+ const watcher = watch(
3209
+ targetPath,
3210
+ recursive ? {
3211
+ recursive: true
3212
+ } : void 0,
3213
+ () => {
3214
+ if (timer) {
3215
+ clearTimeout(timer);
3216
+ }
3217
+ timer = setTimeout(() => {
3218
+ void handleChange();
3219
+ }, debounceMs);
3220
+ }
3221
+ );
3222
+ watcherMap.set(targetPath, watcher);
3223
+ } catch {
3224
+ if (recursive) {
3225
+ attachWatcher(targetPath, false);
3226
+ }
3227
+ }
3228
+ };
3229
+ const refreshWatchers = async () => {
3230
+ const nextTargets = await watchFiles(current.runtime, options.root);
3231
+ attachWatcher(nextTargets.manifestPath, false);
3232
+ for (const workspaceRoot of nextTargets.roots) {
3233
+ attachWatcher(workspaceRoot, true);
3234
+ }
3235
+ for (const filePath of nextTargets.files) {
3236
+ attachWatcher(filePath, false);
3237
+ }
3238
+ };
3239
+ const handleChange = async () => {
3240
+ if (closed) {
3241
+ return;
3242
+ }
3243
+ const next = await buildRunEnvironment({
3244
+ ...options,
3245
+ cliArgs
3246
+ });
3247
+ const changedKeys = diffGraphs(current.runtime.graph, next.runtime.graph);
3248
+ current = next;
3249
+ await refreshWatchers();
3250
+ if (changedKeys.length === 0) {
3251
+ return;
3252
+ }
3253
+ if (isSignal) {
3254
+ await options.onSignal?.({ changedKeys });
3255
+ process.stdout.write(`${printJson({ changedKeys })}
3256
+ `);
3257
+ return;
3258
+ }
3259
+ if (child && !child.killed) {
3260
+ await new Promise((resolve) => {
3261
+ child?.once("close", () => resolve());
3262
+ child?.kill();
3263
+ });
3264
+ }
3265
+ child = spawnWatchedChild(command, root, current.env);
3266
+ await options.onRestart?.({ changedKeys });
3267
+ };
3268
+ await refreshWatchers();
3269
+ return {
3270
+ async close() {
3271
+ closed = true;
3272
+ if (timer) {
3273
+ clearTimeout(timer);
3274
+ }
3275
+ for (const watcher of watcherMap.values()) {
3276
+ watcher.close();
3277
+ }
3278
+ watcherMap.clear();
3279
+ if (child && !child.killed) {
3280
+ await new Promise((resolve) => {
3281
+ child?.once("close", () => resolve());
3282
+ child?.kill();
3283
+ });
3284
+ }
3285
+ }
3286
+ };
3287
+ }
3288
+ async function runWatch(command, options = {}) {
3289
+ const cliArgs = [...options.cliArgs ?? []];
3290
+ const isSignal = consumeFlag(cliArgs, "--signal");
3291
+ const debounce = consumeOption(cliArgs, "--debounce");
3292
+ const handle = await startWatchLoop({
3293
+ ...options,
3294
+ cliArgs: [
3295
+ ...cliArgs,
3296
+ ...isSignal ? ["--signal"] : [],
3297
+ ...debounce ? ["--debounce", debounce] : []
3298
+ ],
3299
+ command
3300
+ });
3301
+ const closeWatcher = () => {
3302
+ void handle.close();
3303
+ };
3304
+ process.once("SIGINT", closeWatcher);
3305
+ process.once("SIGTERM", closeWatcher);
3306
+ return isSignal ? "watching config changes in signal mode" : "watching config changes in restart mode";
3307
+ }
3308
+
2535
3309
  // src/index.ts
2536
3310
  function resolveHelpTopic(command, args) {
2537
3311
  if (command === "help" || command === "help-ai") {
@@ -2612,6 +3386,14 @@ async function main(argv) {
2612
3386
  return;
2613
3387
  case "onboard":
2614
3388
  process.stdout.write(`${await runOnboard(runtimeOptions)}
3389
+ `);
3390
+ return;
3391
+ case "migrate":
3392
+ process.stdout.write(`${await runMigrate(runtimeOptions)}
3393
+ `);
3394
+ return;
3395
+ case "codegen":
3396
+ process.stdout.write(`${await runCodegen(runtimeOptions)}
2615
3397
  `);
2616
3398
  return;
2617
3399
  case "read":
@@ -2676,8 +3458,16 @@ async function main(argv) {
2676
3458
  process.exitCode = result.exitCode;
2677
3459
  return;
2678
3460
  }
3461
+ case "watch":
3462
+ process.stdout.write(`${await runWatch(passthrough.length > 0 ? passthrough : args, runtimeOptions)}
3463
+ `);
3464
+ return;
2679
3465
  case "diff":
2680
3466
  process.stdout.write(`${await runDiff(args[0] ?? "local", args[1] ?? "stage", runtimeOptions)}
3467
+ `);
3468
+ return;
3469
+ case "drift":
3470
+ process.stdout.write(`${await runDrift(runtimeOptions)}
2681
3471
  `);
2682
3472
  return;
2683
3473
  case "doctor":
@@ -2685,6 +3475,11 @@ async function main(argv) {
2685
3475
  `);
2686
3476
  return;
2687
3477
  default:
3478
+ if (args.length > 0 && /^[a-z][a-z0-9-]*$/i.test(command)) {
3479
+ process.stdout.write(`${await runNamespace(command, args, runtimeOptions)}
3480
+ `);
3481
+ return;
3482
+ }
2688
3483
  process.stderr.write(`Unknown command: ${command}
2689
3484
  `);
2690
3485
  process.exitCode = 1;