@kitsy/cnos-cli 1.8.4 → 1.9.1

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 +822 -247
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -27,6 +27,9 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
27
27
  "--expr",
28
28
  "--extends",
29
29
  "--workspaces",
30
+ "--host",
31
+ "--port",
32
+ "--api-port",
30
33
  "--env",
31
34
  "--yaml",
32
35
  "--toml",
@@ -51,7 +54,9 @@ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
51
54
  "--signal",
52
55
  "--derive",
53
56
  "--materialize",
54
- "--source-only"
57
+ "--source-only",
58
+ "--allow-secret",
59
+ "--fix-secret-env-mappings"
55
60
  ]);
56
61
  function normalizeCommand(argv) {
57
62
  const [command = "doctor", ...rest] = argv;
@@ -281,7 +286,7 @@ function printJson(value) {
281
286
 
282
287
  // src/services/projections.ts
283
288
  import { mkdir, writeFile } from "fs/promises";
284
- import path3 from "path";
289
+ import path4 from "path";
285
290
  import { resolveBrowserData, resolveFrameworkEnv, resolveServerProjection } from "@kitsy/cnos/build";
286
291
  import { stringifyYaml } from "@kitsy/cnos/internal";
287
292
 
@@ -313,6 +318,104 @@ function resolveFilesystemBasePath(root, cwd = process.cwd()) {
313
318
  return path2.resolve(root);
314
319
  }
315
320
 
321
+ // src/services/secretEnvBuild.ts
322
+ import { readFile } from "fs/promises";
323
+ import path3 from "path";
324
+ import readline from "readline/promises";
325
+ import { spawnSync } from "child_process";
326
+ function isInteractiveSession() {
327
+ return process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
328
+ }
329
+ function printSecretEnvBuildWarnings(targetPath, mappings) {
330
+ console.error(`!WARN CNOS detected explicit secret env mappings for ${targetPath}.`);
331
+ console.error(`!WARN Writing revealed env artifacts is a security risk and may leak plaintext secrets outside CNOS.`);
332
+ console.error(`!WARN Secret env vars: ${mappings.map((mapping) => mapping.envVar).join(", ")}`);
333
+ }
334
+ function getSecretEnvMappings(runtime) {
335
+ return Object.entries(runtime.manifest.envMapping.explicit).filter(([, logicalKey]) => runtime.graph.entries.get(logicalKey)?.namespace === "secret").map(([envVar, logicalKey]) => ({
336
+ envVar,
337
+ logicalKey
338
+ })).sort((left, right) => left.envVar.localeCompare(right.envVar));
339
+ }
340
+ async function hydrateSecretEnvMappings(runtime, mappings) {
341
+ for (const mapping of mappings) {
342
+ await runtime.refreshSecret(mapping.logicalKey);
343
+ }
344
+ }
345
+ function applyMaskedSecretEnvMappings(env, mappings) {
346
+ const nextEnv = { ...env };
347
+ for (const { envVar } of mappings) {
348
+ if (!(envVar in nextEnv)) {
349
+ nextEnv[envVar] = "****";
350
+ }
351
+ }
352
+ return nextEnv;
353
+ }
354
+ function resolveGitRoot(cwd) {
355
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
356
+ cwd,
357
+ encoding: "utf8",
358
+ shell: process.platform === "win32"
359
+ });
360
+ if (result.status !== 0) {
361
+ return void 0;
362
+ }
363
+ const value = result.stdout.trim();
364
+ return value ? path3.resolve(value) : void 0;
365
+ }
366
+ function isGitIgnored(repoRoot, targetPath) {
367
+ const relativeTarget = path3.relative(repoRoot, targetPath);
368
+ if (!relativeTarget || relativeTarget.startsWith("..") || path3.isAbsolute(relativeTarget)) {
369
+ return false;
370
+ }
371
+ const result = spawnSync("git", ["check-ignore", "--quiet", "--no-index", relativeTarget], {
372
+ cwd: repoRoot,
373
+ encoding: "utf8",
374
+ shell: process.platform === "win32"
375
+ });
376
+ return result.status === 0;
377
+ }
378
+ async function assertSecretEnvTargetIsGitIgnored(targetPath, cwd) {
379
+ const repoRoot = resolveGitRoot(cwd);
380
+ if (!repoRoot) {
381
+ throw new Error(
382
+ `Cannot write revealed secrets to ${targetPath} because CNOS could not verify gitignore protection. Run inside a git repo or omit --reveal.`
383
+ );
384
+ }
385
+ const gitignorePath = path3.join(repoRoot, ".gitignore");
386
+ try {
387
+ await readFile(gitignorePath, "utf8");
388
+ } catch {
389
+ throw new Error(
390
+ `Cannot write revealed secrets to ${targetPath} because ${gitignorePath} is missing. Add a gitignored env target or omit --reveal.`
391
+ );
392
+ }
393
+ if (!isGitIgnored(repoRoot, targetPath)) {
394
+ const relativeTarget = path3.relative(repoRoot, targetPath).replace(/\\/g, "/");
395
+ throw new Error(
396
+ `Cannot write revealed secrets to ${targetPath} because ${relativeTarget} is not gitignored. Add an ignore rule first, then re-run cnos build env --reveal.`
397
+ );
398
+ }
399
+ }
400
+ async function confirmSecretEnvBuild(targetPath, mappings) {
401
+ printSecretEnvBuildWarnings(targetPath, mappings);
402
+ if (!isInteractiveSession()) {
403
+ return;
404
+ }
405
+ const rl = readline.createInterface({
406
+ input: process.stdin,
407
+ output: process.stdout
408
+ });
409
+ try {
410
+ const answer = (await rl.question("Do you want to continue? [y/N] ")).trim().toLowerCase();
411
+ if (answer !== "y" && answer !== "yes") {
412
+ throw new Error("Aborted secret env build.");
413
+ }
414
+ } finally {
415
+ rl.close();
416
+ }
417
+ }
418
+
316
419
  // src/services/projections.ts
317
420
  function stringifyScalar(value) {
318
421
  if (value === void 0 || value === null) {
@@ -351,8 +454,8 @@ function formatKeyValueMap(values, format) {
351
454
  }
352
455
  }
353
456
  async function writeProjectionFile(to, output, root = process.cwd()) {
354
- const targetPath = path3.resolve(root, to);
355
- await mkdir(path3.dirname(targetPath), { recursive: true });
457
+ const targetPath = path4.resolve(root, to);
458
+ await mkdir(path4.dirname(targetPath), { recursive: true });
356
459
  await writeFile(targetPath, output, "utf8");
357
460
  return targetPath;
358
461
  }
@@ -404,25 +507,31 @@ async function buildPublicProjectionArtifact(to, options = {}, format = "dotenv"
404
507
  return { targetPath, output, env };
405
508
  }
406
509
  async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
510
+ const cliArgs = [...options.cliArgs ?? []];
511
+ const revealSecrets = cliArgs.includes("--reveal");
512
+ const basePath = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
513
+ const targetPath = path4.resolve(basePath, to);
407
514
  const runtime = await createRuntimeService({
408
515
  ...options,
409
516
  cacheMode: "build",
410
- cliArgs: [...options.cliArgs ?? []]
517
+ cliArgs,
518
+ secretResolution: "lazy"
411
519
  });
412
- const env = runtime.toEnv();
413
- for (const [envVar, logicalKey] of Object.entries(runtime.manifest.envMapping.explicit)) {
414
- const entry = runtime.graph.entries.get(logicalKey);
415
- if (entry?.namespace === "secret" && !(envVar in env)) {
416
- env[envVar] = "****";
417
- }
520
+ const secretMappings = getSecretEnvMappings(runtime);
521
+ if (revealSecrets && secretMappings.length > 0) {
522
+ await assertSecretEnvTargetIsGitIgnored(targetPath, basePath);
523
+ await confirmSecretEnvBuild(targetPath, secretMappings);
524
+ await hydrateSecretEnvMappings(runtime, secretMappings);
525
+ }
526
+ let env = runtime.toEnv({
527
+ includeSecrets: revealSecrets
528
+ });
529
+ if (!revealSecrets) {
530
+ env = applyMaskedSecretEnvMappings(env, secretMappings);
418
531
  }
419
532
  const output = formatKeyValueMap(env, format);
420
- const targetPath = await writeProjectionFile(
421
- to,
422
- output,
423
- resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
424
- );
425
- return { targetPath, output, env };
533
+ const writtenTargetPath = await writeProjectionFile(to, output, basePath);
534
+ return { targetPath: writtenTargetPath, output, env };
426
535
  }
427
536
 
428
537
  // src/commands/build.ts
@@ -490,7 +599,7 @@ async function runBuild(subcommand, options = {}) {
490
599
 
491
600
  // src/services/cache.ts
492
601
  import { readdir, rm, stat } from "fs/promises";
493
- import path4 from "path";
602
+ import path5 from "path";
494
603
  import {
495
604
  loadManifest,
496
605
  parseGitUri,
@@ -507,7 +616,7 @@ async function computeDirectorySize(targetPath) {
507
616
  }
508
617
  const entries = await readdir(targetPath, { withFileTypes: true });
509
618
  const sizes = await Promise.all(
510
- entries.map((entry) => computeDirectorySize(path4.join(targetPath, entry.name)))
619
+ entries.map((entry) => computeDirectorySize(path5.join(targetPath, entry.name)))
511
620
  );
512
621
  return sizes.reduce((sum, value) => sum + value, 0);
513
622
  } catch {
@@ -515,13 +624,13 @@ async function computeDirectorySize(targetPath) {
515
624
  }
516
625
  }
517
626
  async function listCachedRoots(processEnv = process.env) {
518
- const rootsDir = path4.join(resolveCnosCacheRoot(processEnv), "roots");
627
+ const rootsDir = path5.join(resolveCnosCacheRoot(processEnv), "roots");
519
628
  try {
520
629
  const entries = await readdir(rootsDir, { withFileTypes: true });
521
630
  const records = await Promise.all(
522
631
  entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
523
- const cacheDir = path4.join(rootsDir, entry.name);
524
- const metadata = await readRemoteRootCacheMetadata(path4.join(cacheDir, ".cnos-cache-meta.json"));
632
+ const cacheDir = path5.join(rootsDir, entry.name);
633
+ const metadata = await readRemoteRootCacheMetadata(path5.join(cacheDir, ".cnos-cache-meta.json"));
525
634
  if (!metadata) {
526
635
  return void 0;
527
636
  }
@@ -646,11 +755,11 @@ async function runCache(args = [], options = {}) {
646
755
  }
647
756
 
648
757
  // src/commands/define.ts
649
- import path6 from "path";
758
+ import path7 from "path";
650
759
 
651
760
  // src/services/writes.ts
652
- import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
653
- import path5 from "path";
761
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
762
+ import path6 from "path";
654
763
  import {
655
764
  getNamespaceDefinition,
656
765
  normalizeDerivedValue,
@@ -730,7 +839,7 @@ function isSecretReference(value) {
730
839
  }
731
840
  async function readYamlDocument(filePath) {
732
841
  try {
733
- return parseYaml(await readFile(filePath, "utf8")) ?? {};
842
+ return parseYaml(await readFile2(filePath, "utf8")) ?? {};
734
843
  } catch {
735
844
  return {};
736
845
  }
@@ -794,7 +903,7 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
794
903
  parsedValue = parseScalarValue(rawValue);
795
904
  }
796
905
  setNestedValue(document, configPath.split("."), parsedValue);
797
- await mkdir2(path5.dirname(filePath), { recursive: true });
906
+ await mkdir2(path6.dirname(filePath), { recursive: true });
798
907
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
799
908
  return {
800
909
  filePath,
@@ -835,7 +944,7 @@ async function setSecret(configPath, rawValue, options = {}) {
835
944
  };
836
945
  }
837
946
  setNestedValue(document, configPath.split("."), reference);
838
- await mkdir2(path5.dirname(filePath), { recursive: true });
947
+ await mkdir2(path6.dirname(filePath), { recursive: true });
839
948
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
840
949
  return {
841
950
  filePath,
@@ -918,7 +1027,7 @@ async function deleteValue(namespace, configPath, options = {}) {
918
1027
  // src/commands/define.ts
919
1028
  async function runDefine(namespace, configPath, rawValue, options = {}) {
920
1029
  const cliArgs = [...options.cliArgs ?? []];
921
- const root = path6.resolve(options.root ?? process.cwd());
1030
+ const root = path7.resolve(options.root ?? process.cwd());
922
1031
  const target = consumeOption(cliArgs, "--target") ?? "local";
923
1032
  const local = consumeFlag(cliArgs, "--local");
924
1033
  const remote = consumeFlag(cliArgs, "--remote");
@@ -949,7 +1058,7 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
949
1058
 
950
1059
  // src/services/envMaterialization.ts
951
1060
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
952
- import path7 from "path";
1061
+ import path8 from "path";
953
1062
  function resolveEnvFromRuntime(runtime, cliArgs = []) {
954
1063
  const args = [...cliArgs];
955
1064
  const isPublic = consumeFlag(args, "--public");
@@ -976,11 +1085,11 @@ async function resolveMaterializedEnv(options = {}) {
976
1085
  };
977
1086
  }
978
1087
  function resolveMaterializedEnvTarget(to, root = process.cwd()) {
979
- return path7.resolve(root, to);
1088
+ return path8.resolve(root, to);
980
1089
  }
981
1090
  async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
982
1091
  const targetPath = resolveMaterializedEnvTarget(to, root);
983
- await mkdir3(path7.dirname(targetPath), { recursive: true });
1092
+ await mkdir3(path8.dirname(targetPath), { recursive: true });
984
1093
  await writeFile3(targetPath, output, "utf8");
985
1094
  return targetPath;
986
1095
  }
@@ -999,22 +1108,25 @@ async function materializeEnvToFile(to, options = {}) {
999
1108
 
1000
1109
  // src/services/spawn.ts
1001
1110
  import { spawn } from "child_process";
1002
- function shouldUseShellForCommand(command) {
1003
- if (process.platform !== "win32") {
1004
- return false;
1005
- }
1006
- return !/[\\/]/.test(command);
1111
+ function shouldUseWindowsCommandShim(command) {
1112
+ return process.platform === "win32" && !/[\\/]/.test(command);
1007
1113
  }
1008
1114
  function spawnCommand(command, options) {
1009
1115
  const executable = command[0];
1010
1116
  if (!executable) {
1011
1117
  throw new Error("A command is required.");
1012
1118
  }
1119
+ if (shouldUseWindowsCommandShim(executable)) {
1120
+ return spawn(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", ...command], {
1121
+ cwd: options.cwd,
1122
+ env: options.env,
1123
+ stdio: options.stdio ?? "inherit"
1124
+ });
1125
+ }
1013
1126
  return spawn(executable, command.slice(1), {
1014
1127
  cwd: options.cwd,
1015
1128
  env: options.env,
1016
- stdio: options.stdio ?? "inherit",
1017
- shell: shouldUseShellForCommand(executable)
1129
+ stdio: options.stdio ?? "inherit"
1018
1130
  });
1019
1131
  }
1020
1132
 
@@ -1238,15 +1350,16 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
1238
1350
  }
1239
1351
 
1240
1352
  // src/services/doctor.ts
1241
- import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
1242
- import path8 from "path";
1353
+ import { readdir as readdir2, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
1354
+ import path9 from "path";
1243
1355
  import {
1244
1356
  detectLegacyVaultFormat,
1245
1357
  isSecretReference as isSecretReference2,
1246
1358
  loadManifest as loadManifest3,
1247
1359
  parseYaml as parseYaml2,
1248
1360
  readKeychain,
1249
- resolveSecretStoreRoot
1361
+ resolveSecretStoreRoot,
1362
+ stringifyYaml as stringifyYaml3
1250
1363
  } from "@kitsy/cnos/internal";
1251
1364
 
1252
1365
  // src/services/validation.ts
@@ -1262,7 +1375,7 @@ async function createValidationSummary(options = {}) {
1262
1375
 
1263
1376
  // src/services/doctor.ts
1264
1377
  async function checkGitignore(root) {
1265
- const gitignorePath = path8.join(root, ".gitignore");
1378
+ const gitignorePath = path9.join(root, ".gitignore");
1266
1379
  const expected = [
1267
1380
  ".cnos/env/.env",
1268
1381
  ".cnos/env/.env.*",
@@ -1274,7 +1387,7 @@ async function checkGitignore(root) {
1274
1387
  "!.cnos/workspaces/*/env/.env.*.example"
1275
1388
  ];
1276
1389
  try {
1277
- const content = await readFile2(gitignorePath, "utf8");
1390
+ const content = await readFile3(gitignorePath, "utf8");
1278
1391
  const missing = expected.filter((entry) => !content.includes(entry));
1279
1392
  return {
1280
1393
  name: "gitignore",
@@ -1297,12 +1410,12 @@ async function collectYamlFiles(root) {
1297
1410
  const entries = await readdir2(root, { withFileTypes: true });
1298
1411
  const results = [];
1299
1412
  for (const entry of entries) {
1300
- const target = path8.join(root, entry.name);
1413
+ const target = path9.join(root, entry.name);
1301
1414
  if (entry.isDirectory()) {
1302
1415
  results.push(...await collectYamlFiles(target));
1303
1416
  continue;
1304
1417
  }
1305
- if (entry.isFile() && [".yml", ".yaml"].includes(path8.extname(entry.name).toLowerCase())) {
1418
+ if (entry.isFile() && [".yml", ".yaml"].includes(path9.extname(entry.name).toLowerCase())) {
1306
1419
  results.push(target);
1307
1420
  }
1308
1421
  }
@@ -1327,12 +1440,12 @@ async function checkSecretSecurity(options, runtime) {
1327
1440
  );
1328
1441
  const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
1329
1442
  const secretFiles = await Promise.all(
1330
- runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path8.join(root.path, "secrets")))
1443
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path9.join(root.path, "secrets")))
1331
1444
  );
1332
1445
  const plaintextFiles = [];
1333
1446
  for (const file of secretFiles.flat()) {
1334
1447
  try {
1335
- const parsed = parseYaml2(await readFile2(file, "utf8"));
1448
+ const parsed = parseYaml2(await readFile3(file, "utf8"));
1336
1449
  if (hasPlaintextSecret(parsed)) {
1337
1450
  plaintextFiles.push(file);
1338
1451
  }
@@ -1348,6 +1461,7 @@ async function checkSecretSecurity(options, runtime) {
1348
1461
  const warnings = [
1349
1462
  ...legacyDetected.map((entry) => `legacy vault ${entry.vault}: ${entry.path}`),
1350
1463
  ...plaintextFiles.map((file) => `plaintext secret file: ${file}`),
1464
+ ...Object.entries(runtime.manifest.envMapping.explicit).filter(([, logicalKey]) => logicalKey.startsWith("secret.")).map(([envVar, logicalKey]) => `secret env mapping: ${envVar} -> ${logicalKey}`),
1351
1465
  ...keychainWarnings.filter((entry) => !entry.value).map((entry) => `no keychain entry for vault ${entry.vault} (${entry.source})`)
1352
1466
  ];
1353
1467
  return {
@@ -1356,6 +1470,39 @@ async function checkSecretSecurity(options, runtime) {
1356
1470
  details: warnings.length === 0 ? "no legacy vaults, plaintext secret files, or missing keychain entries" : warnings.join("; ")
1357
1471
  };
1358
1472
  }
1473
+ async function repairSecretEnvMappings(options = {}) {
1474
+ const loadedManifest = await loadManifest3({
1475
+ ...options.root ? { root: options.root } : {},
1476
+ ...options.cwd ? { cwd: options.cwd } : {},
1477
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
1478
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
1479
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
1480
+ ...options.forceRefresh ? { forceRefresh: true } : {}
1481
+ });
1482
+ const explicit = loadedManifest.rawManifest.envMapping?.explicit ?? {};
1483
+ const removed = Object.entries(explicit).filter(([, logicalKey]) => logicalKey.startsWith("secret.")).map(([envVar, logicalKey]) => ({ envVar, logicalKey }));
1484
+ if (removed.length === 0) {
1485
+ return {
1486
+ manifestPath: loadedManifest.manifestPath,
1487
+ removed
1488
+ };
1489
+ }
1490
+ const nextExplicit = Object.fromEntries(
1491
+ Object.entries(explicit).filter(([, logicalKey]) => !logicalKey.startsWith("secret."))
1492
+ );
1493
+ const nextRawManifest = {
1494
+ ...loadedManifest.rawManifest,
1495
+ envMapping: {
1496
+ ...loadedManifest.rawManifest.envMapping ?? {},
1497
+ explicit: nextExplicit
1498
+ }
1499
+ };
1500
+ await writeFile4(loadedManifest.manifestPath, stringifyYaml3(nextRawManifest), "utf8");
1501
+ return {
1502
+ manifestPath: loadedManifest.manifestPath,
1503
+ removed
1504
+ };
1505
+ }
1359
1506
  async function evaluateDoctor(options = {}) {
1360
1507
  const root = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
1361
1508
  const loadedManifest = await loadManifest3({
@@ -1413,15 +1560,22 @@ async function evaluateDoctor(options = {}) {
1413
1560
 
1414
1561
  // src/commands/doctor.ts
1415
1562
  async function runDoctor(options = {}) {
1563
+ const cliArgs = [...options.cliArgs ?? []];
1564
+ const shouldFixSecretEnvMappings = cliArgs.includes("--fix-secret-env-mappings");
1565
+ const repairResult = shouldFixSecretEnvMappings ? await repairSecretEnvMappings(options) : void 0;
1416
1566
  const checks = await evaluateDoctor(options);
1417
1567
  const hasFailures = checks.some((check) => !check.ok);
1418
1568
  if (hasFailures) {
1419
1569
  process.exitCode = 1;
1420
1570
  }
1421
1571
  if (options.json) {
1422
- return printJson(checks);
1572
+ return printJson({
1573
+ ...repairResult ? { repair: repairResult } : {},
1574
+ checks
1575
+ });
1423
1576
  }
1424
- return checks.map((check) => `${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.details}`).join("\n");
1577
+ const repairLine = repairResult ? repairResult.removed.length > 0 ? `REPAIRED secret-env-mappings: removed ${repairResult.removed.map((entry) => `${entry.envVar} -> ${entry.logicalKey}`).join(", ")}` : "REPAIRED secret-env-mappings: no secret env mappings found" : void 0;
1578
+ return [repairLine, ...checks.map((check) => `${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.details}`)].filter((value) => Boolean(value)).join("\n");
1425
1579
  }
1426
1580
 
1427
1581
  // src/commands/dump.ts
@@ -1851,20 +2005,20 @@ var COMMANDS = [
1851
2005
  },
1852
2006
  {
1853
2007
  id: "vault auth",
1854
- summary: "Authenticate a vault for the current shell session.",
2008
+ summary: "Authenticate a vault and cache reusable local auth state.",
1855
2009
  usage: "cnos vault auth <name> [--store-keychain] [global-options]",
1856
- 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.",
2010
+ description: "Authenticates an existing local vault using env, keychain, or prompt-based auth and stores a derived session key under ~/.cnos/secrets/sessions for later CNOS commands until logout. With --store-keychain, CNOS also writes the derived key to the OS keychain.",
1857
2011
  examples: ["cnos vault auth local-dev", "cnos vault auth local-dev --store-keychain"]
1858
2012
  },
1859
2013
  {
1860
2014
  id: "vault logout",
1861
- summary: "Clear vault auth state for the current shell session.",
2015
+ summary: "Clear cached vault auth state.",
1862
2016
  usage: "cnos vault logout <name> [global-options]",
1863
- description: "Removes active vault session auth for the selected vault or all vaults when used with --all.",
2017
+ description: "Removes cached vault session auth for the selected vault or all vaults when used with --all. This does not remove any stored OS keychain entry.",
1864
2018
  options: [
1865
2019
  {
1866
2020
  flag: "--all",
1867
- description: "Clear all active vault auth sessions for the current shell context."
2021
+ description: "Clear all cached vault auth sessions from ~/.cnos/secrets/sessions."
1868
2022
  }
1869
2023
  ],
1870
2024
  examples: ["cnos vault logout local-dev", "cnos vault logout --all"]
@@ -1997,8 +2151,8 @@ var COMMANDS = [
1997
2151
  {
1998
2152
  id: "promote",
1999
2153
  summary: "Promote shareable config into public or env projection surfaces.",
2000
- usage: "cnos promote <key...> --to <public|env> [--as <ENV_VAR>] [global-options]",
2001
- 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.",
2154
+ usage: "cnos promote <key...> --to <public|env> [--as <ENV_VAR>] [--allow-secret] [global-options]",
2155
+ description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected by default, but secret.* may be mapped to env explicitly when you pass --allow-secret. public never allows secret promotion.",
2002
2156
  options: [
2003
2157
  {
2004
2158
  flag: "--to <public|env>",
@@ -2007,12 +2161,17 @@ var COMMANDS = [
2007
2161
  {
2008
2162
  flag: "--as <ENV_VAR>",
2009
2163
  description: "Required for --to env. Sets the exported env var name for the promoted key."
2164
+ },
2165
+ {
2166
+ flag: "--allow-secret",
2167
+ description: "Allow secret.* only for --to env. This does not permit secret promotion to public."
2010
2168
  }
2011
2169
  ],
2012
2170
  examples: [
2013
2171
  "cnos promote value.flag.auth.upi_enabled --to public",
2014
2172
  "cnos promote flags.upi_enabled --to public",
2015
- "cnos promote value.server.port --to env --as PORT"
2173
+ "cnos promote value.server.port --to env --as PORT",
2174
+ "cnos promote secret.db.password --to env --as POSTGRES_PASSWORD --allow-secret"
2016
2175
  ]
2017
2176
  },
2018
2177
  {
@@ -2038,9 +2197,13 @@ var COMMANDS = [
2038
2197
  {
2039
2198
  id: "secret list",
2040
2199
  summary: "List resolved secrets.",
2041
- usage: "cnos secret list [--vault <name>] [--provider <name>] [global-options]",
2042
- description: "Lists stored secret entries for the selected workspace and profile, optionally filtered by vault or provider.",
2043
- examples: ["cnos secret list --workspace api", "cnos secret list --vault github-ci"]
2200
+ usage: "cnos secret list [--vault <name>] [--provider <name>] [--reveal] [global-options]",
2201
+ description: "Lists secret keys for the selected workspace and profile as masked values by default, or as resolved values when --reveal is supplied. Supports optional vault and provider filtering.",
2202
+ examples: [
2203
+ "cnos secret list --workspace api",
2204
+ "cnos secret list --vault github-ci",
2205
+ "cnos secret list --workspace api --reveal"
2206
+ ]
2044
2207
  },
2045
2208
  {
2046
2209
  id: "secret delete",
@@ -2124,8 +2287,8 @@ var COMMANDS = [
2124
2287
  {
2125
2288
  id: "build env",
2126
2289
  summary: "Build a flat env-file artifact from CNOS.",
2127
- usage: "cnos build env --to <path> [--format <dotenv|docker-env|json|shell|toml|yaml>] [global-options]",
2128
- description: "Builds a deterministic KEY=VALUE artifact for legacy build and runtime workflows. The target file is derived output, not the CNOS source of truth.",
2290
+ usage: "cnos build env --to <path> [--format <dotenv|docker-env|json|shell|toml|yaml>] [--reveal] [global-options]",
2291
+ description: "Builds a deterministic KEY=VALUE artifact for legacy build and runtime workflows. Secret env mappings stay masked by default; use --reveal only when the target env file is gitignored and you intentionally want concrete secret values. CNOS prints explicit risk warnings before revealed secret writes.",
2129
2292
  options: [
2130
2293
  {
2131
2294
  flag: "--to <path>",
@@ -2134,11 +2297,16 @@ var COMMANDS = [
2134
2297
  {
2135
2298
  flag: "--format <dotenv|docker-env|json|shell|toml|yaml>",
2136
2299
  description: "Select the output format. Defaults to dotenv."
2300
+ },
2301
+ {
2302
+ flag: "--reveal",
2303
+ description: "Write concrete values for secret env mappings after gitignore verification and an interactive warning prompt."
2137
2304
  }
2138
2305
  ],
2139
2306
  examples: [
2140
2307
  "cnos build env --profile local --to .env.local",
2141
2308
  "cnos build env --profile stage --to .env.stage",
2309
+ "cnos build env --profile prod --reveal --to .env.production.local",
2142
2310
  "cnos build env --profile prod --format yaml --to env.yaml"
2143
2311
  ]
2144
2312
  },
@@ -2252,13 +2420,17 @@ var COMMANDS = [
2252
2420
  {
2253
2421
  id: "run",
2254
2422
  summary: "Run a child process with CNOS env injected.",
2255
- usage: "cnos run [--public] [--framework <name>] [--set <logical-key=value>] [global-options] -- <command...>",
2256
- description: "Resolves the active workspace and profile, injects runtime env variables, bootstraps __CNOS_GRAPH__ for singleton runtime reads, and executes the command after --.",
2423
+ usage: "cnos run [--public] [--auth] [--framework <name>] [--set <logical-key=value>] [global-options] -- <command...>",
2424
+ description: "Resolves the active workspace and profile, injects runtime env variables, includes explicit secret env mappings for private runs, bootstraps __CNOS_GRAPH__ for singleton runtime reads, and executes the command after --.",
2257
2425
  options: [
2258
2426
  {
2259
2427
  flag: "--set <logical-key=value>",
2260
2428
  description: "Apply inline logical-key overrides for this run without touching repo config files."
2261
2429
  },
2430
+ {
2431
+ flag: "--auth",
2432
+ description: "Resolve secrets eagerly and pass an encrypted secret payload to bootstrapped CNOS runtimes in the child process."
2433
+ },
2262
2434
  {
2263
2435
  flag: "--public",
2264
2436
  description: "Inject only promoted public env variables into the child process."
@@ -2275,6 +2447,7 @@ var COMMANDS = [
2275
2447
  examples: [
2276
2448
  "cnos run -- node server.js",
2277
2449
  "cnos run --profile stage -- node server.js",
2450
+ "cnos run --auth -- node server.js",
2278
2451
  "cnos run --set value.server.port=9999 -- node server.js",
2279
2452
  "cnos run --public --framework vite -- pnpm build"
2280
2453
  ]
@@ -2367,9 +2540,9 @@ var COMMANDS = [
2367
2540
  {
2368
2541
  id: "doctor",
2369
2542
  summary: "Run repository and workspace diagnostics.",
2370
- usage: "cnos doctor [global-options]",
2371
- description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
2372
- examples: ["cnos doctor", "cnos doctor --workspace api --json"]
2543
+ usage: "cnos doctor [--fix-secret-env-mappings] [global-options]",
2544
+ description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace. Secret env mappings are reported as a security risk; use --fix-secret-env-mappings to remove them from envMapping.explicit in one shot.",
2545
+ examples: ["cnos doctor", "cnos doctor --workspace api --json", "cnos doctor --fix-secret-env-mappings"]
2373
2546
  },
2374
2547
  {
2375
2548
  id: "drift",
@@ -2457,6 +2630,27 @@ var COMMANDS = [
2457
2630
  ],
2458
2631
  examples: ["cnos help-ai --format json", "cnos help-ai export env --format json"]
2459
2632
  },
2633
+ {
2634
+ id: "ui",
2635
+ summary: "Launch the CNOS local UI.",
2636
+ usage: "cnos ui [--host <host>] [--port <port>] [--api-port <port>] [global-options]",
2637
+ description: "Starts a local CNOS API server plus the Vite-powered React UI for browsing values, env mappings, public config, and inspect data.",
2638
+ options: [
2639
+ {
2640
+ flag: "--host <host>",
2641
+ description: "Host for the UI dev server. Defaults to 127.0.0.1."
2642
+ },
2643
+ {
2644
+ flag: "--port <port>",
2645
+ description: "Port for the UI dev server. Defaults to 4310."
2646
+ },
2647
+ {
2648
+ flag: "--api-port <port>",
2649
+ description: "Port for the backing CNOS API server. Defaults to 4311."
2650
+ }
2651
+ ],
2652
+ examples: ["cnos ui", "cnos ui --port 4400 --api-port 4401"]
2653
+ },
2460
2654
  {
2461
2655
  id: "version",
2462
2656
  summary: "Print the installed CNOS CLI version.",
@@ -2618,11 +2812,11 @@ function runHelpAi(topic, cliArgs = []) {
2618
2812
  }
2619
2813
 
2620
2814
  // src/commands/init.ts
2621
- import path10 from "path";
2815
+ import path11 from "path";
2622
2816
 
2623
2817
  // src/services/scaffold.ts
2624
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2625
- import path9 from "path";
2818
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
2819
+ import path10 from "path";
2626
2820
  function scaffoldManifest(projectName, options = {}) {
2627
2821
  const mode = options.mode ?? "regular";
2628
2822
  const baseWorkspace = options.workspace ?? "base";
@@ -2662,15 +2856,15 @@ function scaffoldManifest(projectName, options = {}) {
2662
2856
  }
2663
2857
  async function ensureFile(filePath, content) {
2664
2858
  try {
2665
- await readFile3(filePath, "utf8");
2859
+ await readFile4(filePath, "utf8");
2666
2860
  return false;
2667
2861
  } catch {
2668
- await writeFile4(filePath, content, "utf8");
2862
+ await writeFile5(filePath, content, "utf8");
2669
2863
  return true;
2670
2864
  }
2671
2865
  }
2672
2866
  async function ensureGitignore(root) {
2673
- const gitignorePath = path9.join(root, ".gitignore");
2867
+ const gitignorePath = path10.join(root, ".gitignore");
2674
2868
  const requiredEntries = [
2675
2869
  ".cnos/env/.env",
2676
2870
  ".cnos/env/.env.*",
@@ -2683,7 +2877,7 @@ async function ensureGitignore(root) {
2683
2877
  ];
2684
2878
  let current = "";
2685
2879
  try {
2686
- current = await readFile3(gitignorePath, "utf8");
2880
+ current = await readFile4(gitignorePath, "utf8");
2687
2881
  } catch {
2688
2882
  current = "";
2689
2883
  }
@@ -2693,17 +2887,17 @@ async function ensureGitignore(root) {
2693
2887
  }
2694
2888
  const prefix = current.trim().length > 0 ? `${current.trimEnd()}
2695
2889
  ` : "";
2696
- await writeFile4(gitignorePath, `${prefix}${missingEntries.join("\n")}
2890
+ await writeFile5(gitignorePath, `${prefix}${missingEntries.join("\n")}
2697
2891
  `, "utf8");
2698
2892
  return true;
2699
2893
  }
2700
2894
  async function ensureWorkspaceLayout(cnosRoot, workspace) {
2701
- const workspaceRoot = workspace ? path9.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2895
+ const workspaceRoot = workspace ? path10.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2702
2896
  const createdPaths = [];
2703
- await mkdir4(path9.join(workspaceRoot, "profiles"), { recursive: true });
2704
- await mkdir4(path9.join(workspaceRoot, "values"), { recursive: true });
2705
- await mkdir4(path9.join(workspaceRoot, "secrets"), { recursive: true });
2706
- await mkdir4(path9.join(workspaceRoot, "env"), { recursive: true });
2897
+ await mkdir4(path10.join(workspaceRoot, "profiles"), { recursive: true });
2898
+ await mkdir4(path10.join(workspaceRoot, "values"), { recursive: true });
2899
+ await mkdir4(path10.join(workspaceRoot, "secrets"), { recursive: true });
2900
+ await mkdir4(path10.join(workspaceRoot, "env"), { recursive: true });
2707
2901
  const relativePaths = workspace ? [
2708
2902
  ["workspaces", workspace, "profiles", ".gitkeep"],
2709
2903
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -2716,16 +2910,16 @@ async function ensureWorkspaceLayout(cnosRoot, workspace) {
2716
2910
  ["env", ".gitkeep"]
2717
2911
  ];
2718
2912
  for (const relativePath of relativePaths) {
2719
- const filePath = path9.join(cnosRoot, ...relativePath);
2913
+ const filePath = path10.join(cnosRoot, ...relativePath);
2720
2914
  if (await ensureFile(filePath, "")) {
2721
- createdPaths.push(path9.relative(path9.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
2915
+ createdPaths.push(path10.relative(path10.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
2722
2916
  }
2723
2917
  }
2724
2918
  return createdPaths;
2725
2919
  }
2726
2920
  async function ensureCnosrc(root, workspace) {
2727
2921
  return ensureFile(
2728
- path9.join(root, ".cnosrc.yml"),
2922
+ path10.join(root, ".cnosrc.yml"),
2729
2923
  workspace ? `root: ./.cnos
2730
2924
  workspace: ${workspace}
2731
2925
  ` : "root: ./.cnos\n"
@@ -2735,7 +2929,7 @@ async function scaffoldProject(root, options = {}) {
2735
2929
  const mode = options.mode ?? "regular";
2736
2930
  const baseWorkspace = options.workspace ?? "base";
2737
2931
  const childWorkspaces = mode === "workspace" ? (options.workspaces ?? []).filter((workspaceId) => workspaceId !== baseWorkspace) : [];
2738
- const cnosRoot = path9.join(root, ".cnos");
2932
+ const cnosRoot = path10.join(root, ".cnos");
2739
2933
  const createdPaths = [];
2740
2934
  if (mode === "workspace") {
2741
2935
  createdPaths.push(
@@ -2749,13 +2943,13 @@ async function scaffoldProject(root, options = {}) {
2749
2943
  } else {
2750
2944
  createdPaths.push(...(await ensureWorkspaceLayout(cnosRoot)).map((entry) => entry.replace(/^\.cnos\//, ".cnos/")));
2751
2945
  }
2752
- if (await ensureFile(path9.join(cnosRoot, "cnos.yml"), scaffoldManifest(path9.basename(root), options))) {
2946
+ if (await ensureFile(path10.join(cnosRoot, "cnos.yml"), scaffoldManifest(path10.basename(root), options))) {
2753
2947
  createdPaths.push(".cnos/cnos.yml");
2754
2948
  }
2755
2949
  if (await ensureCnosrc(root, mode === "workspace" ? baseWorkspace : void 0)) {
2756
2950
  createdPaths.push(".cnosrc.yml");
2757
2951
  }
2758
- if (mode === "workspace" && await ensureFile(path9.join(root, ".cnos-workspace.yml"), `workspace: ${baseWorkspace}
2952
+ if (mode === "workspace" && await ensureFile(path10.join(root, ".cnos-workspace.yml"), `workspace: ${baseWorkspace}
2759
2953
  globalRoot: ~/.cnos
2760
2954
  `)) {
2761
2955
  createdPaths.push(".cnos-workspace.yml");
@@ -2779,7 +2973,7 @@ function parseWorkspaceList(value) {
2779
2973
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
2780
2974
  }
2781
2975
  async function runInit(options = {}) {
2782
- const root = path10.resolve(options.root ?? process.cwd());
2976
+ const root = path11.resolve(options.root ?? process.cwd());
2783
2977
  const cliArgs = [...options.cliArgs ?? []];
2784
2978
  const modeOption = consumeOption(cliArgs, "--mode");
2785
2979
  const workspacesOption = consumeOption(cliArgs, "--workspaces");
@@ -2869,6 +3063,45 @@ async function runInspect(key, options = {}) {
2869
3063
  return printInspect(printable);
2870
3064
  }
2871
3065
 
3066
+ // src/format/printTable.ts
3067
+ function stringifyCell(value) {
3068
+ if (value === void 0 || value === null) {
3069
+ return "";
3070
+ }
3071
+ if (typeof value === "string") {
3072
+ return value;
3073
+ }
3074
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
3075
+ return String(value);
3076
+ }
3077
+ return JSON.stringify(value);
3078
+ }
3079
+ function printTable(rows) {
3080
+ if (rows.length === 0) {
3081
+ return "";
3082
+ }
3083
+ const columns = Array.from(
3084
+ rows.reduce((set, row) => {
3085
+ for (const key of Object.keys(row)) {
3086
+ set.add(key);
3087
+ }
3088
+ return set;
3089
+ }, /* @__PURE__ */ new Set())
3090
+ );
3091
+ const widths = columns.map(
3092
+ (column) => Math.max(
3093
+ column.length,
3094
+ ...rows.map((row) => stringifyCell(row[column]).length)
3095
+ )
3096
+ );
3097
+ const renderRow = (row) => columns.map((column, index) => stringifyCell(row[column]).padEnd(widths[index], " ")).join(" ").trimEnd();
3098
+ return [
3099
+ columns.map((column, index) => column.padEnd(widths[index], " ")).join(" ").trimEnd(),
3100
+ widths.map((width) => "-".repeat(width)).join(" "),
3101
+ ...rows.map(renderRow)
3102
+ ].join("\n");
3103
+ }
3104
+
2872
3105
  // src/format/printValue.ts
2873
3106
  function printValue(value, json = false) {
2874
3107
  if (json) {
@@ -2914,6 +3147,10 @@ function toStoredEntry(namespace, entry, filter = {}) {
2914
3147
  return {
2915
3148
  key: entry.key,
2916
3149
  value: selectedCandidate.value,
3150
+ ...namespace === "secret" ? {
3151
+ vault: selectedCandidate.metadata?.secretRef?.vault ?? "default",
3152
+ provider: selectedCandidate.metadata?.secretRef?.provider ?? "local"
3153
+ } : {},
2917
3154
  ...typeof selectedCandidate.value === "object" && selectedCandidate.value !== null && !Array.isArray(selectedCandidate.value) && "$derive" in selectedCandidate.value ? {
2918
3155
  derived: true
2919
3156
  } : {}
@@ -2924,20 +3161,35 @@ async function listStoredNamespace(namespace, options) {
2924
3161
  ...options,
2925
3162
  ...namespace === "secret" ? { secretResolution: "lazy" } : {}
2926
3163
  });
2927
- return Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === namespace).map((entry) => {
3164
+ const revealSecrets = namespace === "secret" && (options.cliArgs?.includes("--reveal") ?? false);
3165
+ const results = [];
3166
+ for (const entry of Array.from(runtime.graph.entries.values()).filter((candidate) => candidate.namespace === namespace)) {
2928
3167
  const stored = toStoredEntry(namespace, entry, options);
2929
3168
  if (!stored) {
2930
- return void 0;
3169
+ continue;
2931
3170
  }
2932
- return {
3171
+ const value = namespace === "secret" ? revealSecrets ? (await runtime.refreshSecret(entry.key), runtime.secret(entry.key.slice("secret.".length))) : maskSecretValue(stored.value) : stored.derived ? runtime.read(entry.key) : stored.value;
3172
+ if (value === void 0 || !matchesPrefix(stored.key, options.prefix)) {
3173
+ continue;
3174
+ }
3175
+ results.push({
2933
3176
  ...stored,
2934
- value: stored.derived ? runtime.read(entry.key) : stored.value
2935
- };
2936
- }).filter((entry) => Boolean(entry)).filter((entry) => entry.value !== void 0).filter((entry) => matchesPrefix(entry.key, options.prefix)).sort((left, right) => left.key.localeCompare(right.key));
3177
+ value
3178
+ });
3179
+ }
3180
+ return results.sort((left, right) => left.key.localeCompare(right.key));
2937
3181
  }
2938
3182
  function listProjectedNamespace(namespace, options) {
2939
- return createRuntimeService(options).then((runtime) => {
2940
- const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : namespace === "public" ? runtime.toPublicEnv({
3183
+ return createRuntimeService({
3184
+ ...options,
3185
+ ...namespace === "env" ? { secretResolution: "lazy" } : {}
3186
+ }).then(async (runtime) => {
3187
+ const revealSecrets = options.cliArgs?.includes("--reveal") ?? false;
3188
+ const secretMappings = namespace === "env" ? getSecretEnvMappings(runtime) : [];
3189
+ if (namespace === "env" && revealSecrets && secretMappings.length > 0) {
3190
+ await hydrateSecretEnvMappings(runtime, secretMappings);
3191
+ }
3192
+ const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? revealSecrets ? runtime.toEnv({ includeSecrets: true }) : applyMaskedSecretEnvMappings(runtime.toEnv(), secretMappings) : namespace === "public" ? runtime.toPublicEnv({
2941
3193
  ...options.framework ? {
2942
3194
  framework: options.framework
2943
3195
  } : {}
@@ -3003,11 +3255,15 @@ async function runList(args = [], options = {}) {
3003
3255
  const namespace = normalizeNamespace(args[0] ?? consumeOption(cliArgs, "--namespace"));
3004
3256
  const prefix = consumeOption(cliArgs, "--prefix");
3005
3257
  const framework = consumeOption(cliArgs, "--framework");
3258
+ const vault = consumeOption(cliArgs, "--vault");
3259
+ const provider = consumeOption(cliArgs, "--provider");
3006
3260
  const entries = await listConfigEntries(namespace, {
3007
3261
  ...options,
3008
3262
  cliArgs,
3009
3263
  ...prefix ? { prefix } : {},
3010
- ...framework ? { framework } : {}
3264
+ ...framework ? { framework } : {},
3265
+ ...vault ? { vault } : {},
3266
+ ...provider ? { provider } : {}
3011
3267
  });
3012
3268
  if (options.json) {
3013
3269
  return printJson(entries);
@@ -3015,11 +3271,21 @@ async function runList(args = [], options = {}) {
3015
3271
  if (entries.length === 0) {
3016
3272
  return "";
3017
3273
  }
3274
+ if (namespace === "secret") {
3275
+ return printTable(
3276
+ entries.map((entry) => ({
3277
+ key: entry.key,
3278
+ value: printValue(entry.value),
3279
+ vault: entry.vault ?? "default",
3280
+ provider: entry.provider ?? "local"
3281
+ }))
3282
+ );
3283
+ }
3018
3284
  return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
3019
3285
  }
3020
3286
 
3021
3287
  // src/commands/migrate.ts
3022
- import path11 from "path";
3288
+ import path12 from "path";
3023
3289
  import {
3024
3290
  applyManifestMappings,
3025
3291
  loadManifest as loadManifest4,
@@ -3047,7 +3313,7 @@ async function runMigrate(options = {}) {
3047
3313
  ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3048
3314
  ...options.forceRefresh ? { forceRefresh: true } : {}
3049
3315
  });
3050
- const scanRoot = path11.resolve(manifest.consumerRoot, scan ?? "src");
3316
+ const scanRoot = path12.resolve(manifest.consumerRoot, scan ?? "src");
3051
3317
  const usages = await scanEnvUsage(scanRoot);
3052
3318
  const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
3053
3319
  const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
@@ -3109,7 +3375,7 @@ async function runMigrate(options = {}) {
3109
3375
  }
3110
3376
 
3111
3377
  // src/commands/namespace.ts
3112
- import path12 from "path";
3378
+ import path13 from "path";
3113
3379
  function normalizeCommand2(args) {
3114
3380
  const [actionOrPath, ...tail] = args;
3115
3381
  if (!actionOrPath) {
@@ -3132,7 +3398,7 @@ function normalizeCommand2(args) {
3132
3398
  async function runNamespace(namespace, args = [], options = {}) {
3133
3399
  const { action, tail } = normalizeCommand2(args);
3134
3400
  const cliArgs = [...options.cliArgs ?? []];
3135
- const root = path12.resolve(options.root ?? process.cwd());
3401
+ const root = path13.resolve(options.root ?? process.cwd());
3136
3402
  if (action === "list") {
3137
3403
  const prefix = consumeOption(cliArgs, "--prefix");
3138
3404
  const entries = await listConfigEntries(namespace, {
@@ -3202,9 +3468,9 @@ async function runNamespace(namespace, args = [], options = {}) {
3202
3468
  }
3203
3469
 
3204
3470
  // src/commands/onboard.ts
3205
- import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile4 } from "fs/promises";
3206
- import path13 from "path";
3207
- import readline from "readline/promises";
3471
+ import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile5 } from "fs/promises";
3472
+ import path14 from "path";
3473
+ import readline2 from "readline/promises";
3208
3474
  import { loadManifest as loadManifest5, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
3209
3475
  import { parse as parseToml } from "smol-toml";
3210
3476
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
@@ -3273,7 +3539,7 @@ function flattenStructured(value, prefixSegments, currentKey = []) {
3273
3539
  );
3274
3540
  }
3275
3541
  async function parseSource(input, prefixSegments) {
3276
- const content = await readFile4(input.filePath, "utf8");
3542
+ const content = await readFile5(input.filePath, "utf8");
3277
3543
  switch (input.kind) {
3278
3544
  case "env":
3279
3545
  return Object.entries(parseEnv(content)).map(([sourceKey, value]) => {
@@ -3289,7 +3555,7 @@ async function parseSource(input, prefixSegments) {
3289
3555
  }
3290
3556
  }
3291
3557
  function detectKindFromPath(filePath) {
3292
- const ext = path13.extname(filePath).toLowerCase();
3558
+ const ext = path14.extname(filePath).toLowerCase();
3293
3559
  switch (ext) {
3294
3560
  case ".env":
3295
3561
  return "env";
@@ -3320,7 +3586,7 @@ function formatProposals(proposed) {
3320
3586
  });
3321
3587
  }
3322
3588
  async function promptForMaterialize() {
3323
- const rl = readline.createInterface({
3589
+ const rl = readline2.createInterface({
3324
3590
  input: process.stdin,
3325
3591
  output: process.stdout
3326
3592
  });
@@ -3356,19 +3622,19 @@ function resolveSourceInputs(root, cliArgs) {
3356
3622
  if (!source) {
3357
3623
  return [];
3358
3624
  }
3359
- const resolvedPath = path13.resolve(root, source.filePath);
3625
+ const resolvedPath = path14.resolve(root, source.filePath);
3360
3626
  return [
3361
3627
  {
3362
3628
  kind: source.kind,
3363
3629
  filePath: resolvedPath,
3364
- displayName: path13.basename(resolvedPath)
3630
+ displayName: path14.basename(resolvedPath)
3365
3631
  }
3366
3632
  ];
3367
3633
  }
3368
3634
  return [];
3369
3635
  }
3370
3636
  async function runOnboard(options = {}) {
3371
- const root = path13.resolve(options.root ?? process.cwd());
3637
+ const root = path14.resolve(options.root ?? process.cwd());
3372
3638
  const cliArgs = [...options.cliArgs ?? []];
3373
3639
  const move = consumeFlag(cliArgs, "--move");
3374
3640
  const materialize = consumeFlag(cliArgs, "--materialize");
@@ -3382,7 +3648,7 @@ async function runOnboard(options = {}) {
3382
3648
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
3383
3649
  }
3384
3650
  let scaffolded = [];
3385
- const manifestPath = path13.join(root, ".cnos", "cnos.yml");
3651
+ const manifestPath = path14.join(root, ".cnos", "cnos.yml");
3386
3652
  if (!await exists(manifestPath)) {
3387
3653
  const scaffold = await scaffoldProject(root, {
3388
3654
  mode: options.workspace && options.workspace !== "base" ? "workspace" : "regular",
@@ -3402,11 +3668,11 @@ async function runOnboard(options = {}) {
3402
3668
  if (!isWorkspaceMode && options.workspace && options.workspace !== "base") {
3403
3669
  throw new Error("This repo is still in regular mode. Run `cnos workspace enable` before onboarding into a child workspace.");
3404
3670
  }
3405
- const envRoot = isWorkspaceMode ? path13.join(root, ".cnos", "workspaces", selectedWorkspace, "env") : path13.join(root, ".cnos", "env");
3671
+ const envRoot = isWorkspaceMode ? path14.join(root, ".cnos", "workspaces", selectedWorkspace, "env") : path14.join(root, ".cnos", "env");
3406
3672
  await mkdir5(envRoot, { recursive: true });
3407
3673
  const rootFiles = explicitSources.length > 0 ? explicitSources : (await listRootEnvFiles(root)).map((fileName) => ({
3408
3674
  kind: "env",
3409
- filePath: path13.join(root, fileName),
3675
+ filePath: path14.join(root, fileName),
3410
3676
  displayName: fileName
3411
3677
  }));
3412
3678
  const imported = [];
@@ -3414,10 +3680,10 @@ async function runOnboard(options = {}) {
3414
3680
  const prefixSegments = buildPrefixSegments(prefix);
3415
3681
  const proposed = [];
3416
3682
  for (const source of rootFiles) {
3417
- const targetPath = path13.join(envRoot, source.displayName);
3683
+ const targetPath = path14.join(envRoot, source.displayName);
3418
3684
  try {
3419
3685
  await copyFile(source.filePath, targetPath);
3420
- imported.push(path13.relative(root, targetPath).replace(/\\/g, "/"));
3686
+ imported.push(path14.relative(root, targetPath).replace(/\\/g, "/"));
3421
3687
  if (move) {
3422
3688
  await rm2(source.filePath);
3423
3689
  }
@@ -3455,7 +3721,7 @@ async function runOnboard(options = {}) {
3455
3721
  }
3456
3722
  const lines = [
3457
3723
  `onboarded ${selectedWorkspace} at ${root}`,
3458
- `Imported ${imported.length} source file(s) into ${path13.relative(root, envRoot).replace(/\\/g, "/") || ".cnos/env"} using ${result.mode}.`,
3724
+ `Imported ${imported.length} source file(s) into ${path14.relative(root, envRoot).replace(/\\/g, "/") || ".cnos/env"} using ${result.mode}.`,
3459
3725
  "",
3460
3726
  `Discovered ${proposed.length} proposed value mapping(s):`,
3461
3727
  ...formatProposals(proposed)
@@ -3474,16 +3740,16 @@ async function runOnboard(options = {}) {
3474
3740
  }
3475
3741
 
3476
3742
  // src/commands/profile.ts
3477
- import path16 from "path";
3743
+ import path17 from "path";
3478
3744
 
3479
3745
  // src/services/context.ts
3480
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
3481
- import path14 from "path";
3482
- import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
3746
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
3747
+ import path15 from "path";
3748
+ import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
3483
3749
  async function loadCliContext(root = process.cwd()) {
3484
- const filePath = path14.join(path14.resolve(root), ".cnos-workspace.yml");
3750
+ const filePath = path15.join(path15.resolve(root), ".cnos-workspace.yml");
3485
3751
  try {
3486
- const source = await readFile5(filePath, "utf8");
3752
+ const source = await readFile6(filePath, "utf8");
3487
3753
  const parsed = parseYaml4(source);
3488
3754
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3489
3755
  return {};
@@ -3494,8 +3760,8 @@ async function loadCliContext(root = process.cwd()) {
3494
3760
  }
3495
3761
  }
3496
3762
  async function saveCliContext(options = {}) {
3497
- const root = path14.resolve(options.root ?? process.cwd());
3498
- const filePath = path14.join(root, ".cnos-workspace.yml");
3763
+ const root = path15.resolve(options.root ?? process.cwd());
3764
+ const filePath = path15.join(root, ".cnos-workspace.yml");
3499
3765
  const current = await loadCliContext(root);
3500
3766
  const next = {
3501
3767
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -3505,7 +3771,7 @@ async function saveCliContext(options = {}) {
3505
3771
  ...options.profile ? { profile: options.profile } : {},
3506
3772
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
3507
3773
  };
3508
- await writeFile5(filePath, stringifyYaml3(next), "utf8");
3774
+ await writeFile6(filePath, stringifyYaml4(next), "utf8");
3509
3775
  return {
3510
3776
  filePath,
3511
3777
  context: next
@@ -3513,21 +3779,21 @@ async function saveCliContext(options = {}) {
3513
3779
  }
3514
3780
 
3515
3781
  // src/services/profiles.ts
3516
- import { mkdir as mkdir6, readdir as readdir4, readFile as readFile6, rm as rm3, writeFile as writeFile6 } from "fs/promises";
3517
- import path15 from "path";
3518
- import { loadManifest as loadManifest6, parseYaml as parseYaml5, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
3782
+ import { mkdir as mkdir6, readdir as readdir4, readFile as readFile7, rm as rm3, writeFile as writeFile7 } from "fs/promises";
3783
+ import path16 from "path";
3784
+ import { loadManifest as loadManifest6, parseYaml as parseYaml5, stringifyYaml as stringifyYaml5 } from "@kitsy/cnos/internal";
3519
3785
  async function resolveProfilesRoot(root = process.cwd()) {
3520
3786
  try {
3521
3787
  const loadedManifest = await loadManifest6({ root });
3522
- return path15.join(loadedManifest.manifestRoot, "profiles");
3788
+ return path16.join(loadedManifest.manifestRoot, "profiles");
3523
3789
  } catch {
3524
3790
  const loadedManifest = await loadManifest6({ cwd: root });
3525
- return path15.join(loadedManifest.manifestRoot, "profiles");
3791
+ return path16.join(loadedManifest.manifestRoot, "profiles");
3526
3792
  }
3527
3793
  }
3528
3794
  async function createProfileDefinition(root = process.cwd(), profile, inherit, options = {}) {
3529
- const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3530
- await mkdir6(path15.dirname(filePath), { recursive: true });
3795
+ const filePath = path16.join(await resolveProfilesRoot(root), `${profile}.yml`);
3796
+ await mkdir6(path16.dirname(filePath), { recursive: true });
3531
3797
  const document = options.noInherit ? {
3532
3798
  name: profile,
3533
3799
  activate: {
@@ -3541,7 +3807,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit, o
3541
3807
  } : {
3542
3808
  name: profile
3543
3809
  };
3544
- await writeFile6(filePath, stringifyYaml4(document), "utf8");
3810
+ await writeFile7(filePath, stringifyYaml5(document), "utf8");
3545
3811
  return {
3546
3812
  filePath,
3547
3813
  profile,
@@ -3565,7 +3831,7 @@ async function listProfiles(root = process.cwd()) {
3565
3831
  }
3566
3832
  }
3567
3833
  async function deleteProfileDefinition(root = process.cwd(), profile) {
3568
- const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3834
+ const filePath = path16.join(await resolveProfilesRoot(root), `${profile}.yml`);
3569
3835
  try {
3570
3836
  await rm3(filePath);
3571
3837
  return {
@@ -3585,9 +3851,9 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
3585
3851
  name: "base"
3586
3852
  };
3587
3853
  }
3588
- const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3854
+ const filePath = path16.join(await resolveProfilesRoot(root), `${profile}.yml`);
3589
3855
  try {
3590
- return parseYaml5(await readFile6(filePath, "utf8")) ?? void 0;
3856
+ return parseYaml5(await readFile7(filePath, "utf8")) ?? void 0;
3591
3857
  } catch {
3592
3858
  return void 0;
3593
3859
  }
@@ -3632,7 +3898,7 @@ async function runProfile(args, options = {}) {
3632
3898
  if (action === "use") {
3633
3899
  const profile = tail[0] ?? "base";
3634
3900
  const result = await saveCliContext({
3635
- root: path16.resolve(root),
3901
+ root: path17.resolve(root),
3636
3902
  profile
3637
3903
  });
3638
3904
  if (options.json) {
@@ -3663,12 +3929,12 @@ async function runProfile(args, options = {}) {
3663
3929
  }
3664
3930
 
3665
3931
  // src/commands/promote.ts
3666
- import path17 from "path";
3667
- import { writeFile as writeFile7 } from "fs/promises";
3932
+ import path18 from "path";
3933
+ import { writeFile as writeFile8 } from "fs/promises";
3668
3934
  import {
3669
3935
  ensureProjectionAllowed,
3670
3936
  loadManifest as loadManifest7,
3671
- stringifyYaml as stringifyYaml5
3937
+ stringifyYaml as stringifyYaml6
3672
3938
  } from "@kitsy/cnos/internal";
3673
3939
  function normalizeTarget(value) {
3674
3940
  if (value === "public" || value === "env") {
@@ -3680,10 +3946,11 @@ function sortRecord(record) {
3680
3946
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
3681
3947
  }
3682
3948
  async function runPromote(args = [], options = {}) {
3683
- const root = path17.resolve(options.root ?? process.cwd());
3949
+ const root = path18.resolve(options.root ?? process.cwd());
3684
3950
  const cliArgs = [...options.cliArgs ?? []];
3685
3951
  const target = normalizeTarget(consumeOption(cliArgs, "--to"));
3686
3952
  const alias = consumeOption(cliArgs, "--as");
3953
+ const allowSecret = cliArgs.includes("--allow-secret");
3687
3954
  const keys = args.filter(Boolean);
3688
3955
  if (keys.length === 0) {
3689
3956
  throw new Error("promote requires at least one logical key");
@@ -3695,6 +3962,8 @@ async function runPromote(args = [], options = {}) {
3695
3962
  if (!alias) {
3696
3963
  throw new Error("promote --to env requires --as <ENV_VAR>");
3697
3964
  }
3965
+ } else if (allowSecret) {
3966
+ throw new Error("--allow-secret is only supported with promote --to env");
3698
3967
  }
3699
3968
  const loadedManifest = await loadManifest7({
3700
3969
  ...options.root ? { root: options.root } : {},
@@ -3706,7 +3975,9 @@ async function runPromote(args = [], options = {}) {
3706
3975
  });
3707
3976
  await assertWritableConfigRoot(`promote ${keys.join(", ")}`, options);
3708
3977
  for (const key of keys) {
3709
- ensureProjectionAllowed(loadedManifest.manifest, key, target);
3978
+ ensureProjectionAllowed(loadedManifest.manifest, key, target, {
3979
+ allowSecretForEnv: allowSecret && target === "env"
3980
+ });
3710
3981
  }
3711
3982
  const rawManifest = {
3712
3983
  ...loadedManifest.rawManifest
@@ -3727,7 +3998,7 @@ async function runPromote(args = [], options = {}) {
3727
3998
  })
3728
3999
  };
3729
4000
  }
3730
- await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
4001
+ await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
3731
4002
  if (options.json) {
3732
4003
  return printJson({
3733
4004
  target,
@@ -3736,7 +4007,7 @@ async function runPromote(args = [], options = {}) {
3736
4007
  manifestPath: loadedManifest.manifestPath
3737
4008
  });
3738
4009
  }
3739
- 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)}`;
4010
+ return target === "public" ? `promoted ${keys.join(", ")} to public in ${displayPath(loadedManifest.manifestPath, root)}` : `promoted ${keys[0]} to env as ${alias}${allowSecret ? " with secret override" : ""} in ${displayPath(loadedManifest.manifestPath, root)}`;
3740
4011
  }
3741
4012
 
3742
4013
  // src/commands/read.ts
@@ -3760,8 +4031,11 @@ async function runRead(key, options = {}) {
3760
4031
  import {
3761
4032
  CNOS_GRAPH_ENV_VAR,
3762
4033
  CNOS_PROJECTION_ENV_VAR,
4034
+ CNOS_SECRET_PAYLOAD_ENV_VAR,
4035
+ CNOS_SESSION_KEY_ENV_VAR,
3763
4036
  serializeServerProjection,
3764
- serializeRuntimeGraph
4037
+ serializeRuntimeGraph,
4038
+ serializeSecretPayload
3765
4039
  } from "@kitsy/cnos/internal";
3766
4040
  function consumeOptions(args, flag) {
3767
4041
  const values = [];
@@ -3799,7 +4073,7 @@ async function runCommand(command, options = {}) {
3799
4073
  }
3800
4074
  const cliArgs = [...options.cliArgs ?? []];
3801
4075
  const isPublic = consumeFlag(cliArgs, "--public");
3802
- consumeFlag(cliArgs, "--auth");
4076
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
3803
4077
  const framework = consumeOption(cliArgs, "--framework");
3804
4078
  const prefix = consumeOption(cliArgs, "--prefix");
3805
4079
  const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
@@ -3807,14 +4081,22 @@ async function runCommand(command, options = {}) {
3807
4081
  ...options,
3808
4082
  cliArgs: [...cliArgs, ...setOverrides]
3809
4083
  });
4084
+ const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
4085
+ Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)]).filter((entry) => entry[1] !== void 0)
4086
+ ) : void 0;
4087
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
3810
4088
  const env = {
3811
4089
  ...process.env,
3812
4090
  ...isPublic ? runtime.toPublicEnv({
3813
4091
  ...framework ? { framework } : {},
3814
4092
  ...prefix ? { prefix } : {}
3815
- }) : runtime.toEnv(),
4093
+ }) : runtime.toEnv({ includeSecrets: true }),
3816
4094
  [CNOS_PROJECTION_ENV_VAR]: serializeServerProjection(runtime.toServerProjection()),
3817
- [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
4095
+ [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
4096
+ ...secretPayload ? {
4097
+ [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
4098
+ [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
4099
+ } : {}
3818
4100
  };
3819
4101
  return new Promise((resolve, reject) => {
3820
4102
  const executable = command[0];
@@ -3849,14 +4131,14 @@ async function runCommand(command, options = {}) {
3849
4131
  }
3850
4132
 
3851
4133
  // src/commands/secret.ts
3852
- import path20 from "path";
4134
+ import path21 from "path";
3853
4135
 
3854
4136
  // src/commands/vault.ts
3855
- import path19 from "path";
4137
+ import path20 from "path";
3856
4138
 
3857
4139
  // src/services/vaults.ts
3858
- import { rm as rm4, writeFile as writeFile8 } from "fs/promises";
3859
- import path18 from "path";
4140
+ import { rm as rm4, writeFile as writeFile9 } from "fs/promises";
4141
+ import path19 from "path";
3860
4142
  import {
3861
4143
  clearAllVaultSessionKeys,
3862
4144
  clearVaultSessionKey,
@@ -3869,7 +4151,7 @@ import {
3869
4151
  resolveSecretStoreRoot as resolveSecretStoreRoot2,
3870
4152
  resolveVaultAuth as resolveVaultAuth2,
3871
4153
  resolveVaultDefinition,
3872
- stringifyYaml as stringifyYaml6,
4154
+ stringifyYaml as stringifyYaml7,
3873
4155
  writeKeychain,
3874
4156
  writeVaultSessionKey
3875
4157
  } from "@kitsy/cnos/internal";
@@ -3918,7 +4200,7 @@ async function createVaultDefinition(name, options = {}) {
3918
4200
  [vault]: vaultDefinition
3919
4201
  }
3920
4202
  };
3921
- await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
4203
+ await writeFile9(loadedManifest.manifestPath, stringifyYaml7(rawManifest), "utf8");
3922
4204
  const definition = resolveVaultDefinition({ [vault]: vaultDefinition }, vault);
3923
4205
  if (provider === "local") {
3924
4206
  const auth = await resolveVaultAuth2(vault, vaultDefinition, options.processEnv ?? process.env);
@@ -3984,8 +4266,8 @@ async function removeVaultDefinition(name, options = {}) {
3984
4266
  if (Object.keys(nextVaults).length === 0) {
3985
4267
  delete rawManifest.vaults;
3986
4268
  }
3987
- await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
3988
- const vaultRoot = path18.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
4269
+ await writeFile9(loadedManifest.manifestPath, stringifyYaml7(rawManifest), "utf8");
4270
+ const vaultRoot = path19.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3989
4271
  let removedStore;
3990
4272
  try {
3991
4273
  await rm4(vaultRoot, { recursive: true, force: true });
@@ -4086,7 +4368,7 @@ function normalizeVaultAction(args) {
4086
4368
  async function runVault(args = [], options = {}) {
4087
4369
  const { action, tail } = normalizeVaultAction(args);
4088
4370
  const cliArgs = [...options.cliArgs ?? []];
4089
- const root = path19.resolve(options.root ?? process.cwd());
4371
+ const root = path20.resolve(options.root ?? process.cwd());
4090
4372
  if (consumeOption(cliArgs, "--passphrase")) {
4091
4373
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
4092
4374
  }
@@ -4192,7 +4474,7 @@ async function runSecret(argsOrPath, options = {}) {
4192
4474
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
4193
4475
  const { action, tail } = normalizeSecretCommand(args);
4194
4476
  const cliArgs = [...options.cliArgs ?? []];
4195
- const root = path20.resolve(options.root ?? process.cwd());
4477
+ const root = path21.resolve(options.root ?? process.cwd());
4196
4478
  if (consumeOption(cliArgs, "--passphrase")) {
4197
4479
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
4198
4480
  }
@@ -4200,34 +4482,27 @@ async function runSecret(argsOrPath, options = {}) {
4200
4482
  return runVault(["create", tail[0] ?? "default"], options);
4201
4483
  }
4202
4484
  if (action === "list") {
4203
- const runtime2 = await createRuntimeService({
4204
- ...options,
4205
- secretResolution: "lazy"
4206
- });
4207
4485
  const prefix = consumeOption(cliArgs, "--prefix");
4208
4486
  const vault = consumeOption(cliArgs, "--vault");
4209
4487
  const provider = consumeOption(cliArgs, "--provider");
4210
- 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) => {
4211
- const secretRef2 = entry2.winner.metadata?.secretRef;
4212
- if (vault && secretRef2?.vault !== vault) {
4213
- return false;
4214
- }
4215
- if (provider && secretRef2?.provider !== provider) {
4216
- return false;
4217
- }
4218
- return true;
4219
- }).map((entry2) => {
4220
- const secretRef2 = entry2.winner.metadata?.secretRef;
4221
- return {
4222
- key: entry2.key,
4223
- vault: secretRef2?.vault ?? "default",
4224
- provider: secretRef2?.provider ?? "local"
4225
- };
4226
- }).sort((left, right) => left.key.localeCompare(right.key));
4488
+ const entries = await listConfigEntries("secret", {
4489
+ ...options,
4490
+ cliArgs,
4491
+ ...prefix ? { prefix } : {},
4492
+ ...vault ? { vault } : {},
4493
+ ...provider ? { provider } : {}
4494
+ });
4227
4495
  if (options.json) {
4228
4496
  return printJson(entries);
4229
4497
  }
4230
- return entries.map((entry2) => `${entry2.key} (vault: ${entry2.vault}, provider: ${entry2.provider})`).join("\n");
4498
+ return printTable(
4499
+ entries.map((entry2) => ({
4500
+ key: entry2.key,
4501
+ value: printValue(entry2.value),
4502
+ vault: entry2.vault ?? "default",
4503
+ provider: entry2.provider ?? "local"
4504
+ }))
4505
+ );
4231
4506
  }
4232
4507
  if (action === "set") {
4233
4508
  const secretPath2 = tail[0];
@@ -4295,9 +4570,9 @@ async function runSecret(argsOrPath, options = {}) {
4295
4570
  }
4296
4571
 
4297
4572
  // src/commands/use.ts
4298
- import path21 from "path";
4573
+ import path22 from "path";
4299
4574
  async function runUse(args = [], options = {}) {
4300
- const root = path21.resolve(options.root ?? process.cwd());
4575
+ const root = path22.resolve(options.root ?? process.cwd());
4301
4576
  const action = args[0];
4302
4577
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
4303
4578
  if (action === "show" || !action && !hasUpdates) {
@@ -4319,6 +4594,302 @@ async function runUse(args = [], options = {}) {
4319
4594
  return `updated CLI context in ${displayPath(result.filePath, root)}`;
4320
4595
  }
4321
4596
 
4597
+ // src/commands/ui.ts
4598
+ import { createServer } from "http";
4599
+ import path23 from "path";
4600
+ import { createRequire } from "module";
4601
+ import { loadManifest as loadManifest9 } from "@kitsy/cnos/internal";
4602
+ function parsePort(value, fallback, flag) {
4603
+ if (!value) {
4604
+ return fallback;
4605
+ }
4606
+ const parsed = Number(value);
4607
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
4608
+ throw new Error(`Invalid value for ${flag}: ${value}`);
4609
+ }
4610
+ return parsed;
4611
+ }
4612
+ function resolveUiPackageRoot() {
4613
+ const require2 = createRequire(import.meta.url);
4614
+ try {
4615
+ const packageJsonPath = require2.resolve("@kitsy/cnos-ui/package.json");
4616
+ return path23.dirname(packageJsonPath);
4617
+ } catch {
4618
+ throw new Error("Unable to resolve @kitsy/cnos-ui. Install workspace dependencies before running `cnos ui`.");
4619
+ }
4620
+ }
4621
+ function writeJson(response, statusCode, payload) {
4622
+ response.statusCode = statusCode;
4623
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
4624
+ response.end(`${printJson(payload)}
4625
+ `);
4626
+ }
4627
+ async function readJsonBody(request) {
4628
+ const chunks = [];
4629
+ for await (const chunk of request) {
4630
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4631
+ }
4632
+ if (chunks.length === 0) {
4633
+ return {};
4634
+ }
4635
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
4636
+ }
4637
+ function maskInspectResult(key, value) {
4638
+ if (!key.startsWith("secret.")) {
4639
+ return value;
4640
+ }
4641
+ return {
4642
+ ...value,
4643
+ value: maskSecretValue(value.value),
4644
+ overridden: value.overridden.map((entry) => ({
4645
+ ...entry,
4646
+ value: maskSecretValue(entry.value)
4647
+ }))
4648
+ };
4649
+ }
4650
+ function maskListEntry(entry) {
4651
+ if (!entry.key.startsWith("secret.")) {
4652
+ return entry;
4653
+ }
4654
+ return {
4655
+ ...entry,
4656
+ value: maskSecretValue(entry.value)
4657
+ };
4658
+ }
4659
+ function toRuntimeOptionsFromQuery(baseOptions, searchParams) {
4660
+ const workspace = searchParams.get("workspace")?.trim();
4661
+ const profile = searchParams.get("profile")?.trim();
4662
+ return {
4663
+ ...baseOptions,
4664
+ ...workspace ? { workspace } : {},
4665
+ ...profile ? { profile } : {}
4666
+ };
4667
+ }
4668
+ function toRuntimeOptionsFromBody(baseOptions, body) {
4669
+ const workspace = typeof body.workspace === "string" ? body.workspace.trim() : "";
4670
+ const profile = typeof body.profile === "string" ? body.profile.trim() : "";
4671
+ return {
4672
+ ...baseOptions,
4673
+ ...workspace ? { workspace } : {},
4674
+ ...profile ? { profile } : {}
4675
+ };
4676
+ }
4677
+ function withUiPassphrase(processEnv, passphrase) {
4678
+ if (!passphrase?.trim()) {
4679
+ return processEnv;
4680
+ }
4681
+ return {
4682
+ ...processEnv ?? process.env,
4683
+ CNOS_SECRET_PASSPHRASE: passphrase.trim()
4684
+ };
4685
+ }
4686
+ async function handleSummary(options, searchParams) {
4687
+ const runtimeOptions = toRuntimeOptionsFromQuery(options, searchParams);
4688
+ const runtime = await createRuntimeService({
4689
+ ...runtimeOptions,
4690
+ secretResolution: "lazy"
4691
+ });
4692
+ const loadedManifest = await loadManifest9({
4693
+ ...options.root ? { root: options.root } : {},
4694
+ ...options.cwd ? { cwd: options.cwd } : {},
4695
+ ...options.processEnv ? { processEnv: options.processEnv } : {}
4696
+ });
4697
+ const declaredWorkspaces = Object.keys(loadedManifest.manifest.workspaces.items);
4698
+ const workspaces = declaredWorkspaces.length > 0 ? declaredWorkspaces.sort((left, right) => left.localeCompare(right)) : ["base"];
4699
+ const profiles = await listProfiles(loadedManifest.consumerRoot);
4700
+ const envEntries = runtime.toEnv();
4701
+ const publicEntries = runtime.toPublicEnv();
4702
+ const counts = Array.from(runtime.graph.entries.values()).reduce((acc, entry) => {
4703
+ acc.all += 1;
4704
+ acc[entry.namespace] = (acc[entry.namespace] ?? 0) + 1;
4705
+ return acc;
4706
+ }, { all: 0 });
4707
+ return {
4708
+ project: runtime.manifest.project.name,
4709
+ workspace: runtime.graph.workspace.workspaceId,
4710
+ workspaceSource: runtime.graph.workspace.workspaceSource,
4711
+ workspaceChain: runtime.graph.workspace.workspaceChain,
4712
+ profile: runtime.graph.profile,
4713
+ profileSource: runtime.graph.profileSource,
4714
+ counts: {
4715
+ ...counts,
4716
+ env: Object.keys(envEntries).length,
4717
+ public: Object.keys(publicEntries).length
4718
+ },
4719
+ envMapping: Object.entries(runtime.manifest.envMapping.explicit).map(([envVar, logicalKey]) => ({
4720
+ envVar,
4721
+ logicalKey,
4722
+ secret: runtime.graph.entries.get(logicalKey)?.namespace === "secret"
4723
+ })),
4724
+ promoted: runtime.manifest.public.promote,
4725
+ workspaces,
4726
+ profiles,
4727
+ runtimeNamespaces: Object.keys(runtime.manifest.runtimeNamespaces),
4728
+ vaults: Object.keys(runtime.manifest.vaults)
4729
+ };
4730
+ }
4731
+ async function handleRevealList(body, options) {
4732
+ const prefix = typeof body.prefix === "string" ? body.prefix.trim() : "";
4733
+ const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
4734
+ const runtimeOptions = toRuntimeOptionsFromBody(options, body);
4735
+ const runtime = await createRuntimeService({
4736
+ ...runtimeOptions,
4737
+ processEnv: withUiPassphrase(options.processEnv, passphrase),
4738
+ secretResolution: "lazy"
4739
+ });
4740
+ const entries = [];
4741
+ for (const entry of Array.from(runtime.graph.entries.values()).filter((candidate) => candidate.namespace === "secret").filter((candidate) => {
4742
+ if (!prefix) {
4743
+ return true;
4744
+ }
4745
+ return candidate.key.startsWith(prefix) || candidate.key.split(".").slice(1).join(".").startsWith(prefix);
4746
+ }).sort((left, right) => left.key.localeCompare(right.key))) {
4747
+ await runtime.refreshSecret(entry.key);
4748
+ entries.push({
4749
+ key: entry.key,
4750
+ value: runtime.read(entry.key),
4751
+ ...typeof entry.winner.value === "object" && entry.winner.value !== null && !Array.isArray(entry.winner.value) && "$derive" in entry.winner.value ? { derived: true } : {}
4752
+ });
4753
+ }
4754
+ return {
4755
+ namespace: "secret",
4756
+ entries
4757
+ };
4758
+ }
4759
+ async function handleRevealInspect(body, options) {
4760
+ const key = typeof body.key === "string" ? body.key.trim() : "";
4761
+ if (!key) {
4762
+ throw new Error("Missing key");
4763
+ }
4764
+ const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
4765
+ const runtimeOptions = toRuntimeOptionsFromBody(options, body);
4766
+ const runtime = await createRuntimeService({
4767
+ ...runtimeOptions,
4768
+ processEnv: withUiPassphrase(options.processEnv, passphrase),
4769
+ ...key.startsWith("secret.") ? { secretResolution: "lazy" } : {}
4770
+ });
4771
+ if (key.startsWith("secret.")) {
4772
+ await runtime.refreshSecret(key);
4773
+ }
4774
+ return runtime.inspect(key);
4775
+ }
4776
+ async function handleRequest(request, response, options) {
4777
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
4778
+ if (request.method === "GET" && url.pathname === "/api/health") {
4779
+ writeJson(response, 200, { ok: true });
4780
+ return;
4781
+ }
4782
+ if (request.method === "GET" && url.pathname === "/api/summary") {
4783
+ writeJson(response, 200, await handleSummary(options, url.searchParams));
4784
+ return;
4785
+ }
4786
+ if (request.method === "GET" && url.pathname === "/api/list") {
4787
+ const namespace = url.searchParams.get("namespace") ?? "value";
4788
+ const prefix = url.searchParams.get("prefix") ?? void 0;
4789
+ const runtimeOptions = toRuntimeOptionsFromQuery(options, url.searchParams);
4790
+ const entries = await listConfigEntries(namespace, {
4791
+ ...runtimeOptions,
4792
+ ...prefix ? { prefix } : {},
4793
+ ...namespace === "secret" ? { secretResolution: "lazy" } : {}
4794
+ });
4795
+ writeJson(response, 200, {
4796
+ namespace,
4797
+ entries: entries.map((entry) => maskListEntry(entry))
4798
+ });
4799
+ return;
4800
+ }
4801
+ if (request.method === "GET" && url.pathname === "/api/inspect") {
4802
+ const key = url.searchParams.get("key");
4803
+ if (!key) {
4804
+ writeJson(response, 400, { error: "Missing key query parameter" });
4805
+ return;
4806
+ }
4807
+ const runtimeOptions = toRuntimeOptionsFromQuery(options, url.searchParams);
4808
+ const runtime = await createRuntimeService({
4809
+ ...runtimeOptions,
4810
+ ...key.startsWith("secret.") ? { secretResolution: "lazy" } : {}
4811
+ });
4812
+ writeJson(response, 200, maskInspectResult(key, runtime.inspect(key)));
4813
+ return;
4814
+ }
4815
+ if (request.method === "POST" && url.pathname === "/api/reveal/list") {
4816
+ const body = await readJsonBody(request);
4817
+ writeJson(response, 200, await handleRevealList(body, options));
4818
+ return;
4819
+ }
4820
+ if (request.method === "POST" && url.pathname === "/api/reveal/inspect") {
4821
+ const body = await readJsonBody(request);
4822
+ writeJson(response, 200, await handleRevealInspect(body, options));
4823
+ return;
4824
+ }
4825
+ if (request.method !== "GET" && request.method !== "POST") {
4826
+ writeJson(response, 405, { error: "Method not allowed" });
4827
+ return;
4828
+ }
4829
+ writeJson(response, 404, { error: "Not found" });
4830
+ }
4831
+ function resolveUiUrl(host, port) {
4832
+ if (host === "0.0.0.0" || host === "::") {
4833
+ return `http://127.0.0.1:${port}`;
4834
+ }
4835
+ return `http://${host}:${port}`;
4836
+ }
4837
+ async function runUi(options = {}) {
4838
+ const cliArgs = [...options.cliArgs ?? []];
4839
+ const host = consumeOption(cliArgs, "--host") ?? "127.0.0.1";
4840
+ const port = parsePort(consumeOption(cliArgs, "--port"), 4310, "--port");
4841
+ const apiPort = parsePort(consumeOption(cliArgs, "--api-port"), 4311, "--api-port");
4842
+ if (cliArgs.length > 0) {
4843
+ throw new Error(`Unsupported ui arguments: ${cliArgs.join(" ")}`);
4844
+ }
4845
+ const uiRoot = resolveUiPackageRoot();
4846
+ const apiServer = createServer((request, response) => {
4847
+ void handleRequest(request, response, options).catch((error) => {
4848
+ writeJson(response, 500, {
4849
+ error: error instanceof Error ? error.message : String(error)
4850
+ });
4851
+ });
4852
+ });
4853
+ await new Promise((resolve, reject) => {
4854
+ apiServer.once("error", reject);
4855
+ apiServer.listen(apiPort, "127.0.0.1", () => resolve());
4856
+ });
4857
+ const uiProcess = spawnCommand(
4858
+ ["pnpm", "exec", "vite", "--host", host, "--port", String(port)],
4859
+ {
4860
+ cwd: uiRoot,
4861
+ env: {
4862
+ ...process.env,
4863
+ ...options.processEnv,
4864
+ CNOS_UI_API_TARGET: `http://127.0.0.1:${apiPort}`
4865
+ },
4866
+ stdio: "inherit"
4867
+ }
4868
+ );
4869
+ const uiUrl = resolveUiUrl(host, port);
4870
+ const apiAddress = apiServer.address();
4871
+ console.log(`CNOS UI running at ${uiUrl}`);
4872
+ console.log(`CNOS UI API running at http://127.0.0.1:${apiAddress?.port ?? apiPort}`);
4873
+ const shutdown = () => {
4874
+ apiServer.close();
4875
+ if (!uiProcess.killed) {
4876
+ uiProcess.kill("SIGTERM");
4877
+ }
4878
+ };
4879
+ process.once("SIGINT", shutdown);
4880
+ process.once("SIGTERM", shutdown);
4881
+ try {
4882
+ await new Promise((resolve, reject) => {
4883
+ uiProcess.once("error", reject);
4884
+ uiProcess.once("exit", () => resolve());
4885
+ });
4886
+ } finally {
4887
+ process.removeListener("SIGINT", shutdown);
4888
+ process.removeListener("SIGTERM", shutdown);
4889
+ apiServer.close();
4890
+ }
4891
+ }
4892
+
4322
4893
  // src/commands/validate.ts
4323
4894
  async function runValidate(options = {}) {
4324
4895
  const { summary } = await createValidationSummary(options);
@@ -4334,7 +4905,7 @@ async function runValidate(options = {}) {
4334
4905
  // package.json
4335
4906
  var package_default = {
4336
4907
  name: "@kitsy/cnos-cli",
4337
- version: "1.8.4",
4908
+ version: "1.9.1",
4338
4909
  description: "CLI entry point and developer tooling for CNOS.",
4339
4910
  type: "module",
4340
4911
  main: "./dist/index.js",
@@ -4371,6 +4942,7 @@ var package_default = {
4371
4942
  },
4372
4943
  dependencies: {
4373
4944
  "@kitsy/cnos": "workspace:*",
4945
+ "@kitsy/cnos-ui": "workspace:*",
4374
4946
  "smol-toml": "^1.4.2"
4375
4947
  },
4376
4948
  scripts: {
@@ -4390,7 +4962,7 @@ function runVersion() {
4390
4962
  }
4391
4963
 
4392
4964
  // src/commands/value.ts
4393
- import path22 from "path";
4965
+ import path24 from "path";
4394
4966
  function normalizeValueCommand(args) {
4395
4967
  const [actionOrPath, ...tail] = args;
4396
4968
  if (!actionOrPath) {
@@ -4414,7 +4986,7 @@ async function runValue(argsOrPath, options = {}) {
4414
4986
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
4415
4987
  const { action, tail } = normalizeValueCommand(args);
4416
4988
  const cliArgs = [...options.cliArgs ?? []];
4417
- const root = path22.resolve(options.root ?? process.cwd());
4989
+ const root = path24.resolve(options.root ?? process.cwd());
4418
4990
  if (action === "list") {
4419
4991
  const prefix = consumeOption(cliArgs, "--prefix");
4420
4992
  const entries = await listConfigEntries("value", {
@@ -4485,10 +5057,10 @@ async function runValue(argsOrPath, options = {}) {
4485
5057
  // src/commands/watch.ts
4486
5058
  import {
4487
5059
  CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
4488
- CNOS_SECRET_PAYLOAD_ENV_VAR,
4489
- CNOS_SESSION_KEY_ENV_VAR,
5060
+ CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
5061
+ CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
4490
5062
  serializeRuntimeGraph as serializeRuntimeGraph2,
4491
- serializeSecretPayload
5063
+ serializeSecretPayload as serializeSecretPayload2
4492
5064
  } from "@kitsy/cnos/internal";
4493
5065
  async function buildRunEnvironment(options) {
4494
5066
  const cliArgs = [...options.cliArgs ?? []];
@@ -4504,7 +5076,7 @@ async function buildRunEnvironment(options) {
4504
5076
  const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
4505
5077
  Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
4506
5078
  ) : void 0;
4507
- const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
5079
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
4508
5080
  return {
4509
5081
  runtime,
4510
5082
  env: {
@@ -4512,11 +5084,11 @@ async function buildRunEnvironment(options) {
4512
5084
  ...isPublic ? runtime.toPublicEnv({
4513
5085
  ...framework ? { framework } : {},
4514
5086
  ...prefix ? { prefix } : {}
4515
- }) : runtime.toEnv(),
5087
+ }) : runtime.toEnv({ includeSecrets: true }),
4516
5088
  [CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
4517
5089
  ...secretPayload ? {
4518
- [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
4519
- [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
5090
+ [CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
5091
+ [CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
4520
5092
  } : {}
4521
5093
  }
4522
5094
  };
@@ -4610,9 +5182,9 @@ async function runWatch(command, options = {}) {
4610
5182
  }
4611
5183
 
4612
5184
  // src/commands/workspace.ts
4613
- import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile7, rename, rm as rm5, stat as stat3, writeFile as writeFile9 } from "fs/promises";
4614
- import path23 from "path";
4615
- import { loadManifest as loadManifest9, parseYaml as parseYaml6, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
5185
+ import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile8, rename, rm as rm5, stat as stat3, writeFile as writeFile10 } from "fs/promises";
5186
+ import path25 from "path";
5187
+ import { loadManifest as loadManifest10, parseYaml as parseYaml6, stringifyYaml as stringifyYaml8 } from "@kitsy/cnos/internal";
4616
5188
  async function exists2(targetPath) {
4617
5189
  try {
4618
5190
  await stat3(targetPath);
@@ -4625,7 +5197,7 @@ async function copyIfExists(source, target) {
4625
5197
  if (!await exists2(source)) {
4626
5198
  return;
4627
5199
  }
4628
- await mkdir7(path23.dirname(target), { recursive: true });
5200
+ await mkdir7(path25.dirname(target), { recursive: true });
4629
5201
  await cp(source, target, { recursive: true, force: true });
4630
5202
  }
4631
5203
  async function moveIfExists(source, target, force = false) {
@@ -4637,23 +5209,23 @@ async function moveIfExists(source, target, force = false) {
4637
5209
  } else if (await exists2(target)) {
4638
5210
  throw new Error(`Refusing to overwrite existing path ${target}. Use --force to replace it.`);
4639
5211
  }
4640
- await mkdir7(path23.dirname(target), { recursive: true });
5212
+ await mkdir7(path25.dirname(target), { recursive: true });
4641
5213
  await rename(source, target);
4642
5214
  return true;
4643
5215
  }
4644
5216
  async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
4645
5217
  for (const sourceRoot of sourceRoots) {
4646
5218
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4647
- await copyIfExists(path23.join(sourceRoot, folderName), path23.join(targetCnosRoot, folderName));
5219
+ await copyIfExists(path25.join(sourceRoot, folderName), path25.join(targetCnosRoot, folderName));
4648
5220
  }
4649
5221
  }
4650
5222
  }
4651
5223
  async function writeAnchor(packageRoot, manifestRoot, workspace) {
4652
- const relativeRoot = path23.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
5224
+ const relativeRoot = path25.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
4653
5225
  const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
4654
- await writeFile9(
4655
- path23.join(packageRoot, ".cnosrc.yml"),
4656
- stringifyYaml7({
5226
+ await writeFile10(
5227
+ path25.join(packageRoot, ".cnosrc.yml"),
5228
+ stringifyYaml8({
4657
5229
  root: rootValue,
4658
5230
  ...workspace ? { workspace } : {}
4659
5231
  }),
@@ -4689,7 +5261,7 @@ function splitExtends(value) {
4689
5261
  }
4690
5262
  async function hasDirectConfigData(cnosRoot) {
4691
5263
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4692
- const folder = path23.join(cnosRoot, folderName);
5264
+ const folder = path25.join(cnosRoot, folderName);
4693
5265
  if (!await exists2(folder)) {
4694
5266
  continue;
4695
5267
  }
@@ -4701,11 +5273,11 @@ async function hasDirectConfigData(cnosRoot) {
4701
5273
  return false;
4702
5274
  }
4703
5275
  async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
4704
- const anchorPath = path23.join(packageRoot, ".cnosrc.yml");
4705
- const current = await exists2(anchorPath) ? parseYaml6(await readFile7(anchorPath, "utf8")) : void 0;
4706
- await writeFile9(
5276
+ const anchorPath = path25.join(packageRoot, ".cnosrc.yml");
5277
+ const current = await exists2(anchorPath) ? parseYaml6(await readFile8(anchorPath, "utf8")) : void 0;
5278
+ await writeFile10(
4707
5279
  anchorPath,
4708
- stringifyYaml7({
5280
+ stringifyYaml8({
4709
5281
  root: typeof current?.root === "string" ? current.root : "./.cnos",
4710
5282
  workspace: workspaceId
4711
5283
  }),
@@ -4713,11 +5285,11 @@ async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
4713
5285
  );
4714
5286
  }
4715
5287
  async function updateWorkspaceContext(packageRoot, workspaceId) {
4716
- const workspacePath = path23.join(packageRoot, ".cnos-workspace.yml");
4717
- const current = await exists2(workspacePath) ? parseYaml6(await readFile7(workspacePath, "utf8")) : void 0;
4718
- await writeFile9(
5288
+ const workspacePath = path25.join(packageRoot, ".cnos-workspace.yml");
5289
+ const current = await exists2(workspacePath) ? parseYaml6(await readFile8(workspacePath, "utf8")) : void 0;
5290
+ await writeFile10(
4719
5291
  workspacePath,
4720
- stringifyYaml7({
5292
+ stringifyYaml8({
4721
5293
  workspace: workspaceId,
4722
5294
  ...typeof current?.profile === "string" ? { profile: current.profile } : {},
4723
5295
  ...typeof current?.globalRoot === "string" ? { globalRoot: current.globalRoot } : { globalRoot: "~/.cnos" }
@@ -4726,11 +5298,11 @@ async function updateWorkspaceContext(packageRoot, workspaceId) {
4726
5298
  );
4727
5299
  }
4728
5300
  async function runDetach(packageRoot, options = {}) {
4729
- const loaded = await loadManifest9({ cwd: packageRoot });
5301
+ const loaded = await loadManifest10({ cwd: packageRoot });
4730
5302
  if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
4731
5303
  throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
4732
5304
  }
4733
- const targetCnosRoot = path23.join(packageRoot, ".cnos");
5305
+ const targetCnosRoot = path25.join(packageRoot, ".cnos");
4734
5306
  const force = consumeFlag([...options.cliArgs ?? []], "--force");
4735
5307
  if (await exists2(targetCnosRoot) && !force) {
4736
5308
  throw new Error(`Refusing to detach because ${displayPath(targetCnosRoot, packageRoot)} already exists. Use --force to overwrite.`);
@@ -4746,12 +5318,12 @@ async function runDetach(packageRoot, options = {}) {
4746
5318
  const localRoots = runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => root.path);
4747
5319
  await mkdir7(targetCnosRoot, { recursive: true });
4748
5320
  await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
4749
- await writeFile9(
4750
- path23.join(targetCnosRoot, "cnos.yml"),
4751
- stringifyYaml7(createDetachedManifest(loaded.rawManifest)),
5321
+ await writeFile10(
5322
+ path25.join(targetCnosRoot, "cnos.yml"),
5323
+ stringifyYaml8(createDetachedManifest(loaded.rawManifest)),
4752
5324
  "utf8"
4753
5325
  );
4754
- const relativeRoot = path23.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
5326
+ const relativeRoot = path25.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
4755
5327
  const marker = {
4756
5328
  detachedFrom: relativeRoot || ".",
4757
5329
  detachedWorkspace: loaded.anchoredWorkspace,
@@ -4761,8 +5333,8 @@ async function runDetach(packageRoot, options = {}) {
4761
5333
  workspace: loaded.anchoredWorkspace
4762
5334
  }
4763
5335
  };
4764
- await writeFile9(path23.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
4765
- await writeFile9(path23.join(packageRoot, ".cnosrc.yml"), stringifyYaml7({ root: "./.cnos" }), "utf8");
5336
+ await writeFile10(path25.join(targetCnosRoot, ".detached"), stringifyYaml8(marker), "utf8");
5337
+ await writeFile10(path25.join(packageRoot, ".cnosrc.yml"), stringifyYaml8({ root: "./.cnos" }), "utf8");
4766
5338
  if (options.json) {
4767
5339
  return printJson({
4768
5340
  packageRoot,
@@ -4775,24 +5347,24 @@ async function runDetach(packageRoot, options = {}) {
4775
5347
  async function runAttach(packageRoot, options = {}) {
4776
5348
  const cliArgs = [...options.cliArgs ?? []];
4777
5349
  const force = consumeFlag(cliArgs, "--force");
4778
- const childCnosRoot = path23.join(packageRoot, ".cnos");
4779
- const markerPath = path23.join(childCnosRoot, ".detached");
5350
+ const childCnosRoot = path25.join(packageRoot, ".cnos");
5351
+ const markerPath = path25.join(childCnosRoot, ".detached");
4780
5352
  if (!await exists2(markerPath)) {
4781
5353
  throw new Error("workspace attach requires a detached package with .cnos/.detached");
4782
5354
  }
4783
- const marker = parseYaml6(await readFile7(markerPath, "utf8"));
5355
+ const marker = parseYaml6(await readFile8(markerPath, "utf8"));
4784
5356
  if (!marker?.originalCnosrc?.root || !marker.detachedWorkspace) {
4785
5357
  throw new Error("Invalid .detached marker");
4786
5358
  }
4787
- const parentManifestRoot = path23.resolve(packageRoot, marker.originalCnosrc.root);
4788
- const parentLoaded = await loadManifest9({ root: parentManifestRoot });
5359
+ const parentManifestRoot = path25.resolve(packageRoot, marker.originalCnosrc.root);
5360
+ const parentLoaded = await loadManifest10({ root: parentManifestRoot });
4789
5361
  if (parentLoaded.rootResolution.readOnly) {
4790
5362
  throw new Error(
4791
5363
  `Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
4792
5364
  );
4793
5365
  }
4794
5366
  const workspaceId = marker.originalCnosrc.workspace ?? marker.detachedWorkspace;
4795
- const parentWorkspaceRoot = path23.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
5367
+ const parentWorkspaceRoot = path25.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
4796
5368
  if (await exists2(parentWorkspaceRoot) && !force) {
4797
5369
  throw new Error(`workspace "${workspaceId}" already exists in parent root. Use --force to overwrite.`);
4798
5370
  }
@@ -4801,7 +5373,7 @@ async function runAttach(packageRoot, options = {}) {
4801
5373
  }
4802
5374
  await mkdir7(parentWorkspaceRoot, { recursive: true });
4803
5375
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4804
- await copyIfExists(path23.join(childCnosRoot, folderName), path23.join(parentWorkspaceRoot, folderName));
5376
+ await copyIfExists(path25.join(childCnosRoot, folderName), path25.join(parentWorkspaceRoot, folderName));
4805
5377
  }
4806
5378
  const rawManifest = structuredClone(parentLoaded.rawManifest);
4807
5379
  const workspaces = rawManifest.workspaces ?? {};
@@ -4809,8 +5381,8 @@ async function runAttach(packageRoot, options = {}) {
4809
5381
  items[workspaceId] = items[workspaceId] ?? {};
4810
5382
  workspaces.items = items;
4811
5383
  rawManifest.workspaces = workspaces;
4812
- await writeFile9(path23.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4813
- const archivePath = path23.join(packageRoot, ".cnos.detached.bak");
5384
+ await writeFile10(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
5385
+ const archivePath = path25.join(packageRoot, ".cnos.detached.bak");
4814
5386
  await rm5(archivePath, { recursive: true, force: true });
4815
5387
  await rename(childCnosRoot, archivePath);
4816
5388
  await writeAnchor(packageRoot, parentLoaded.manifestRoot, workspaceId);
@@ -4825,7 +5397,7 @@ async function runAttach(packageRoot, options = {}) {
4825
5397
  return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
4826
5398
  }
4827
5399
  async function runList2(manifestCwd, options = {}) {
4828
- const loaded = await loadManifest9({
5400
+ const loaded = await loadManifest10({
4829
5401
  ...options.root ? { root: options.root } : {},
4830
5402
  cwd: manifestCwd,
4831
5403
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4834,7 +5406,7 @@ async function runList2(manifestCwd, options = {}) {
4834
5406
  id,
4835
5407
  extends: config.extends,
4836
5408
  default: loaded.manifest.workspaces.default === id,
4837
- path: path23.join(loaded.manifestRoot, "workspaces", id)
5409
+ path: path25.join(loaded.manifestRoot, "workspaces", id)
4838
5410
  })).sort((left, right) => left.id.localeCompare(right.id));
4839
5411
  if (options.json) {
4840
5412
  return printJson({
@@ -4858,7 +5430,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
4858
5430
  if (cliArgs.length > 0) {
4859
5431
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4860
5432
  }
4861
- const loaded = await loadManifest9({
5433
+ const loaded = await loadManifest10({
4862
5434
  ...options.root ? { root: options.root } : {},
4863
5435
  cwd: manifestCwd,
4864
5436
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4875,13 +5447,13 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
4875
5447
  throw new Error("This CNOS root is already in workspace mode.");
4876
5448
  }
4877
5449
  const cnosRoot = loaded.manifestRoot;
4878
- const baseWorkspaceRoot = path23.join(cnosRoot, "workspaces", "base");
5450
+ const baseWorkspaceRoot = path25.join(cnosRoot, "workspaces", "base");
4879
5451
  if (await exists2(baseWorkspaceRoot)) {
4880
5452
  throw new Error("Cannot enable workspace mode because .cnos/workspaces/base already exists.");
4881
5453
  }
4882
5454
  const moved = [];
4883
5455
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4884
- if (await moveIfExists(path23.join(cnosRoot, folderName), path23.join(baseWorkspaceRoot, folderName))) {
5456
+ if (await moveIfExists(path25.join(cnosRoot, folderName), path25.join(baseWorkspaceRoot, folderName))) {
4885
5457
  moved.push(folderName);
4886
5458
  }
4887
5459
  }
@@ -4891,19 +5463,19 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
4891
5463
  base: {}
4892
5464
  };
4893
5465
  rawManifest.workspaces = rawWorkspaces;
4894
- await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
5466
+ await writeFile10(path25.join(cnosRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
4895
5467
  await updateRootAnchorToWorkspace(packageRoot, "base");
4896
5468
  await updateWorkspaceContext(packageRoot, "base");
4897
- await ensureGitignore(path23.dirname(cnosRoot));
5469
+ await ensureGitignore(path25.dirname(cnosRoot));
4898
5470
  if (options.json) {
4899
5471
  return printJson({
4900
- root: path23.dirname(cnosRoot),
5472
+ root: path25.dirname(cnosRoot),
4901
5473
  workspace: "base",
4902
5474
  moved
4903
5475
  });
4904
5476
  }
4905
5477
  const movedSummary = moved.length > 0 ? `; moved ${moved.join(", ")} into .cnos/workspaces/base` : "";
4906
- return `enabled workspace mode at ${displayPath(path23.dirname(cnosRoot), packageRoot)} with base workspace${movedSummary}`;
5478
+ return `enabled workspace mode at ${displayPath(path25.dirname(cnosRoot), packageRoot)} with base workspace${movedSummary}`;
4907
5479
  }
4908
5480
  async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, options = {}) {
4909
5481
  const cliArgs = [...options.cliArgs ?? []];
@@ -4912,7 +5484,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
4912
5484
  if (cliArgs.length > 0) {
4913
5485
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4914
5486
  }
4915
- const loaded = await loadManifest9({
5487
+ const loaded = await loadManifest10({
4916
5488
  ...options.root ? { root: options.root } : {},
4917
5489
  cwd: manifestCwd,
4918
5490
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4942,15 +5514,15 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
4942
5514
  rawWorkspaces.items = rawItems;
4943
5515
  rawWorkspaces.default = rawWorkspaces.default ?? workspaceId;
4944
5516
  rawManifest.workspaces = rawWorkspaces;
4945
- const workspaceRoot = path23.join(cnosRoot, "workspaces", workspaceId);
5517
+ const workspaceRoot = path25.join(cnosRoot, "workspaces", workspaceId);
4946
5518
  const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
4947
- await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4948
- await ensureGitignore(path23.dirname(cnosRoot));
5519
+ await writeFile10(path25.join(cnosRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
5520
+ await ensureGitignore(path25.dirname(cnosRoot));
4949
5521
  await writeAnchor(packageRoot, cnosRoot, workspaceId);
4950
5522
  await updateWorkspaceContext(packageRoot, workspaceId);
4951
5523
  const result = {
4952
5524
  workspace: workspaceId,
4953
- root: path23.dirname(cnosRoot),
5525
+ root: path25.dirname(cnosRoot),
4954
5526
  packageRoot,
4955
5527
  created
4956
5528
  };
@@ -4966,7 +5538,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
4966
5538
  if (cliArgs.length > 0) {
4967
5539
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4968
5540
  }
4969
- const loaded = await loadManifest9({
5541
+ const loaded = await loadManifest10({
4970
5542
  ...options.root ? { root: options.root } : {},
4971
5543
  cwd: manifestCwd,
4972
5544
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4988,8 +5560,8 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
4988
5560
  delete rawItems[workspaceId];
4989
5561
  rawWorkspaces.items = rawItems;
4990
5562
  rawManifest.workspaces = rawWorkspaces;
4991
- await writeFile9(path23.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4992
- await rm5(path23.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
5563
+ await writeFile10(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
5564
+ await rm5(path25.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
4993
5565
  if (options.json) {
4994
5566
  return printJson({
4995
5567
  workspace: workspaceId,
@@ -5001,8 +5573,8 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
5001
5573
  async function runWorkspace(args = [], options = {}) {
5002
5574
  const [action, workspaceArg] = args;
5003
5575
  const baseCliArgs = [...options.cliArgs ?? []];
5004
- const manifestCwd = path23.resolve(options.root ?? process.cwd());
5005
- const packageRoot = path23.resolve(consumeOption(baseCliArgs, "--package-root") ?? options.root ?? process.cwd());
5576
+ const manifestCwd = path25.resolve(options.root ?? process.cwd());
5577
+ const packageRoot = path25.resolve(consumeOption(baseCliArgs, "--package-root") ?? options.root ?? process.cwd());
5006
5578
  switch (action) {
5007
5579
  case "attach":
5008
5580
  return runAttach(packageRoot, { ...options, cliArgs: baseCliArgs });
@@ -5125,6 +5697,9 @@ async function main(argv) {
5125
5697
  process.stdout.write(`${runVersion()}
5126
5698
  `);
5127
5699
  return;
5700
+ case "ui":
5701
+ await runUi(runtimeOptions);
5702
+ return;
5128
5703
  case "init":
5129
5704
  process.stdout.write(`${await runInit(runtimeOptions)}
5130
5705
  `);