@kitsy/cnos-cli 1.8.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +532 -178
  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",
@@ -281,7 +284,7 @@ function printJson(value) {
281
284
 
282
285
  // src/services/projections.ts
283
286
  import { mkdir, writeFile } from "fs/promises";
284
- import path3 from "path";
287
+ import path4 from "path";
285
288
  import { resolveBrowserData, resolveFrameworkEnv, resolveServerProjection } from "@kitsy/cnos/build";
286
289
  import { stringifyYaml } from "@kitsy/cnos/internal";
287
290
 
@@ -313,6 +316,88 @@ function resolveFilesystemBasePath(root, cwd = process.cwd()) {
313
316
  return path2.resolve(root);
314
317
  }
315
318
 
319
+ // src/services/secretEnvBuild.ts
320
+ import { readFile } from "fs/promises";
321
+ import path3 from "path";
322
+ import readline from "readline/promises";
323
+ import { spawnSync } from "child_process";
324
+ function isInteractiveSession() {
325
+ return process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
326
+ }
327
+ function getSecretEnvMappings(runtime) {
328
+ return Object.entries(runtime.manifest.envMapping.explicit).filter(([, logicalKey]) => runtime.graph.entries.get(logicalKey)?.namespace === "secret").map(([envVar, logicalKey]) => ({
329
+ envVar,
330
+ logicalKey
331
+ })).sort((left, right) => left.envVar.localeCompare(right.envVar));
332
+ }
333
+ function resolveGitRoot(cwd) {
334
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
335
+ cwd,
336
+ encoding: "utf8",
337
+ shell: process.platform === "win32"
338
+ });
339
+ if (result.status !== 0) {
340
+ return void 0;
341
+ }
342
+ const value = result.stdout.trim();
343
+ return value ? path3.resolve(value) : void 0;
344
+ }
345
+ function isGitIgnored(repoRoot, targetPath) {
346
+ const relativeTarget = path3.relative(repoRoot, targetPath);
347
+ if (!relativeTarget || relativeTarget.startsWith("..") || path3.isAbsolute(relativeTarget)) {
348
+ return false;
349
+ }
350
+ const result = spawnSync("git", ["check-ignore", "--quiet", "--no-index", relativeTarget], {
351
+ cwd: repoRoot,
352
+ encoding: "utf8",
353
+ shell: process.platform === "win32"
354
+ });
355
+ return result.status === 0;
356
+ }
357
+ async function assertSecretEnvTargetIsGitIgnored(targetPath, cwd) {
358
+ const repoRoot = resolveGitRoot(cwd);
359
+ if (!repoRoot) {
360
+ throw new Error(
361
+ `Cannot write revealed secrets to ${targetPath} because CNOS could not verify gitignore protection. Run inside a git repo or omit --reveal.`
362
+ );
363
+ }
364
+ const gitignorePath = path3.join(repoRoot, ".gitignore");
365
+ try {
366
+ await readFile(gitignorePath, "utf8");
367
+ } catch {
368
+ throw new Error(
369
+ `Cannot write revealed secrets to ${targetPath} because ${gitignorePath} is missing. Add a gitignored env target or omit --reveal.`
370
+ );
371
+ }
372
+ if (!isGitIgnored(repoRoot, targetPath)) {
373
+ const relativeTarget = path3.relative(repoRoot, targetPath).replace(/\\/g, "/");
374
+ throw new Error(
375
+ `Cannot write revealed secrets to ${targetPath} because ${relativeTarget} is not gitignored. Add an ignore rule first, then re-run cnos build env --reveal.`
376
+ );
377
+ }
378
+ }
379
+ async function confirmSecretEnvBuild(targetPath, mappings) {
380
+ if (!isInteractiveSession()) {
381
+ return;
382
+ }
383
+ console.error(`!WARN CNOS is about to write resolved secret values into ${targetPath}.`);
384
+ console.error(
385
+ `!WARN Secret env vars: ${mappings.map((mapping) => mapping.envVar).join(", ")}`
386
+ );
387
+ const rl = readline.createInterface({
388
+ input: process.stdin,
389
+ output: process.stdout
390
+ });
391
+ try {
392
+ const answer = (await rl.question("Continue writing secrets to this env artifact? [y/N] ")).trim().toLowerCase();
393
+ if (answer !== "y" && answer !== "yes") {
394
+ throw new Error("Aborted secret env build.");
395
+ }
396
+ } finally {
397
+ rl.close();
398
+ }
399
+ }
400
+
316
401
  // src/services/projections.ts
317
402
  function stringifyScalar(value) {
318
403
  if (value === void 0 || value === null) {
@@ -351,8 +436,8 @@ function formatKeyValueMap(values, format) {
351
436
  }
352
437
  }
353
438
  async function writeProjectionFile(to, output, root = process.cwd()) {
354
- const targetPath = path3.resolve(root, to);
355
- await mkdir(path3.dirname(targetPath), { recursive: true });
439
+ const targetPath = path4.resolve(root, to);
440
+ await mkdir(path4.dirname(targetPath), { recursive: true });
356
441
  await writeFile(targetPath, output, "utf8");
357
442
  return targetPath;
358
443
  }
@@ -404,25 +489,32 @@ async function buildPublicProjectionArtifact(to, options = {}, format = "dotenv"
404
489
  return { targetPath, output, env };
405
490
  }
406
491
  async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
492
+ const cliArgs = [...options.cliArgs ?? []];
493
+ const revealSecrets = cliArgs.includes("--reveal");
494
+ const basePath = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
495
+ const targetPath = path4.resolve(basePath, to);
407
496
  const runtime = await createRuntimeService({
408
497
  ...options,
409
498
  cacheMode: "build",
410
- cliArgs: [...options.cliArgs ?? []]
499
+ cliArgs,
500
+ secretResolution: "lazy"
411
501
  });
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)) {
502
+ const secretMappings = getSecretEnvMappings(runtime);
503
+ if (revealSecrets && secretMappings.length > 0) {
504
+ await assertSecretEnvTargetIsGitIgnored(targetPath, basePath);
505
+ await confirmSecretEnvBuild(targetPath, secretMappings);
506
+ }
507
+ const env = runtime.toEnv({
508
+ includeSecrets: revealSecrets
509
+ });
510
+ for (const { envVar } of secretMappings) {
511
+ if (!revealSecrets && !(envVar in env)) {
416
512
  env[envVar] = "****";
417
513
  }
418
514
  }
419
515
  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 };
516
+ const writtenTargetPath = await writeProjectionFile(to, output, basePath);
517
+ return { targetPath: writtenTargetPath, output, env };
426
518
  }
427
519
 
428
520
  // src/commands/build.ts
@@ -490,7 +582,7 @@ async function runBuild(subcommand, options = {}) {
490
582
 
491
583
  // src/services/cache.ts
492
584
  import { readdir, rm, stat } from "fs/promises";
493
- import path4 from "path";
585
+ import path5 from "path";
494
586
  import {
495
587
  loadManifest,
496
588
  parseGitUri,
@@ -507,7 +599,7 @@ async function computeDirectorySize(targetPath) {
507
599
  }
508
600
  const entries = await readdir(targetPath, { withFileTypes: true });
509
601
  const sizes = await Promise.all(
510
- entries.map((entry) => computeDirectorySize(path4.join(targetPath, entry.name)))
602
+ entries.map((entry) => computeDirectorySize(path5.join(targetPath, entry.name)))
511
603
  );
512
604
  return sizes.reduce((sum, value) => sum + value, 0);
513
605
  } catch {
@@ -515,13 +607,13 @@ async function computeDirectorySize(targetPath) {
515
607
  }
516
608
  }
517
609
  async function listCachedRoots(processEnv = process.env) {
518
- const rootsDir = path4.join(resolveCnosCacheRoot(processEnv), "roots");
610
+ const rootsDir = path5.join(resolveCnosCacheRoot(processEnv), "roots");
519
611
  try {
520
612
  const entries = await readdir(rootsDir, { withFileTypes: true });
521
613
  const records = await Promise.all(
522
614
  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"));
615
+ const cacheDir = path5.join(rootsDir, entry.name);
616
+ const metadata = await readRemoteRootCacheMetadata(path5.join(cacheDir, ".cnos-cache-meta.json"));
525
617
  if (!metadata) {
526
618
  return void 0;
527
619
  }
@@ -646,11 +738,11 @@ async function runCache(args = [], options = {}) {
646
738
  }
647
739
 
648
740
  // src/commands/define.ts
649
- import path6 from "path";
741
+ import path7 from "path";
650
742
 
651
743
  // src/services/writes.ts
652
- import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
653
- import path5 from "path";
744
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
745
+ import path6 from "path";
654
746
  import {
655
747
  getNamespaceDefinition,
656
748
  normalizeDerivedValue,
@@ -730,7 +822,7 @@ function isSecretReference(value) {
730
822
  }
731
823
  async function readYamlDocument(filePath) {
732
824
  try {
733
- return parseYaml(await readFile(filePath, "utf8")) ?? {};
825
+ return parseYaml(await readFile2(filePath, "utf8")) ?? {};
734
826
  } catch {
735
827
  return {};
736
828
  }
@@ -794,7 +886,7 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
794
886
  parsedValue = parseScalarValue(rawValue);
795
887
  }
796
888
  setNestedValue(document, configPath.split("."), parsedValue);
797
- await mkdir2(path5.dirname(filePath), { recursive: true });
889
+ await mkdir2(path6.dirname(filePath), { recursive: true });
798
890
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
799
891
  return {
800
892
  filePath,
@@ -835,7 +927,7 @@ async function setSecret(configPath, rawValue, options = {}) {
835
927
  };
836
928
  }
837
929
  setNestedValue(document, configPath.split("."), reference);
838
- await mkdir2(path5.dirname(filePath), { recursive: true });
930
+ await mkdir2(path6.dirname(filePath), { recursive: true });
839
931
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
840
932
  return {
841
933
  filePath,
@@ -918,7 +1010,7 @@ async function deleteValue(namespace, configPath, options = {}) {
918
1010
  // src/commands/define.ts
919
1011
  async function runDefine(namespace, configPath, rawValue, options = {}) {
920
1012
  const cliArgs = [...options.cliArgs ?? []];
921
- const root = path6.resolve(options.root ?? process.cwd());
1013
+ const root = path7.resolve(options.root ?? process.cwd());
922
1014
  const target = consumeOption(cliArgs, "--target") ?? "local";
923
1015
  const local = consumeFlag(cliArgs, "--local");
924
1016
  const remote = consumeFlag(cliArgs, "--remote");
@@ -949,7 +1041,7 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
949
1041
 
950
1042
  // src/services/envMaterialization.ts
951
1043
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
952
- import path7 from "path";
1044
+ import path8 from "path";
953
1045
  function resolveEnvFromRuntime(runtime, cliArgs = []) {
954
1046
  const args = [...cliArgs];
955
1047
  const isPublic = consumeFlag(args, "--public");
@@ -976,11 +1068,11 @@ async function resolveMaterializedEnv(options = {}) {
976
1068
  };
977
1069
  }
978
1070
  function resolveMaterializedEnvTarget(to, root = process.cwd()) {
979
- return path7.resolve(root, to);
1071
+ return path8.resolve(root, to);
980
1072
  }
981
1073
  async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
982
1074
  const targetPath = resolveMaterializedEnvTarget(to, root);
983
- await mkdir3(path7.dirname(targetPath), { recursive: true });
1075
+ await mkdir3(path8.dirname(targetPath), { recursive: true });
984
1076
  await writeFile3(targetPath, output, "utf8");
985
1077
  return targetPath;
986
1078
  }
@@ -999,22 +1091,25 @@ async function materializeEnvToFile(to, options = {}) {
999
1091
 
1000
1092
  // src/services/spawn.ts
1001
1093
  import { spawn } from "child_process";
1002
- function shouldUseShellForCommand(command) {
1003
- if (process.platform !== "win32") {
1004
- return false;
1005
- }
1006
- return !/[\\/]/.test(command);
1094
+ function shouldUseWindowsCommandShim(command) {
1095
+ return process.platform === "win32" && !/[\\/]/.test(command);
1007
1096
  }
1008
1097
  function spawnCommand(command, options) {
1009
1098
  const executable = command[0];
1010
1099
  if (!executable) {
1011
1100
  throw new Error("A command is required.");
1012
1101
  }
1102
+ if (shouldUseWindowsCommandShim(executable)) {
1103
+ return spawn(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", ...command], {
1104
+ cwd: options.cwd,
1105
+ env: options.env,
1106
+ stdio: options.stdio ?? "inherit"
1107
+ });
1108
+ }
1013
1109
  return spawn(executable, command.slice(1), {
1014
1110
  cwd: options.cwd,
1015
1111
  env: options.env,
1016
- stdio: options.stdio ?? "inherit",
1017
- shell: shouldUseShellForCommand(executable)
1112
+ stdio: options.stdio ?? "inherit"
1018
1113
  });
1019
1114
  }
1020
1115
 
@@ -1238,8 +1333,8 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
1238
1333
  }
1239
1334
 
1240
1335
  // src/services/doctor.ts
1241
- import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
1242
- import path8 from "path";
1336
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
1337
+ import path9 from "path";
1243
1338
  import {
1244
1339
  detectLegacyVaultFormat,
1245
1340
  isSecretReference as isSecretReference2,
@@ -1262,7 +1357,7 @@ async function createValidationSummary(options = {}) {
1262
1357
 
1263
1358
  // src/services/doctor.ts
1264
1359
  async function checkGitignore(root) {
1265
- const gitignorePath = path8.join(root, ".gitignore");
1360
+ const gitignorePath = path9.join(root, ".gitignore");
1266
1361
  const expected = [
1267
1362
  ".cnos/env/.env",
1268
1363
  ".cnos/env/.env.*",
@@ -1274,7 +1369,7 @@ async function checkGitignore(root) {
1274
1369
  "!.cnos/workspaces/*/env/.env.*.example"
1275
1370
  ];
1276
1371
  try {
1277
- const content = await readFile2(gitignorePath, "utf8");
1372
+ const content = await readFile3(gitignorePath, "utf8");
1278
1373
  const missing = expected.filter((entry) => !content.includes(entry));
1279
1374
  return {
1280
1375
  name: "gitignore",
@@ -1297,12 +1392,12 @@ async function collectYamlFiles(root) {
1297
1392
  const entries = await readdir2(root, { withFileTypes: true });
1298
1393
  const results = [];
1299
1394
  for (const entry of entries) {
1300
- const target = path8.join(root, entry.name);
1395
+ const target = path9.join(root, entry.name);
1301
1396
  if (entry.isDirectory()) {
1302
1397
  results.push(...await collectYamlFiles(target));
1303
1398
  continue;
1304
1399
  }
1305
- if (entry.isFile() && [".yml", ".yaml"].includes(path8.extname(entry.name).toLowerCase())) {
1400
+ if (entry.isFile() && [".yml", ".yaml"].includes(path9.extname(entry.name).toLowerCase())) {
1306
1401
  results.push(target);
1307
1402
  }
1308
1403
  }
@@ -1327,12 +1422,12 @@ async function checkSecretSecurity(options, runtime) {
1327
1422
  );
1328
1423
  const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
1329
1424
  const secretFiles = await Promise.all(
1330
- runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path8.join(root.path, "secrets")))
1425
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path9.join(root.path, "secrets")))
1331
1426
  );
1332
1427
  const plaintextFiles = [];
1333
1428
  for (const file of secretFiles.flat()) {
1334
1429
  try {
1335
- const parsed = parseYaml2(await readFile2(file, "utf8"));
1430
+ const parsed = parseYaml2(await readFile3(file, "utf8"));
1336
1431
  if (hasPlaintextSecret(parsed)) {
1337
1432
  plaintextFiles.push(file);
1338
1433
  }
@@ -1851,20 +1946,20 @@ var COMMANDS = [
1851
1946
  },
1852
1947
  {
1853
1948
  id: "vault auth",
1854
- summary: "Authenticate a vault for the current shell session.",
1949
+ summary: "Authenticate a vault and cache reusable local auth state.",
1855
1950
  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.",
1951
+ 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
1952
  examples: ["cnos vault auth local-dev", "cnos vault auth local-dev --store-keychain"]
1858
1953
  },
1859
1954
  {
1860
1955
  id: "vault logout",
1861
- summary: "Clear vault auth state for the current shell session.",
1956
+ summary: "Clear cached vault auth state.",
1862
1957
  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.",
1958
+ 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
1959
  options: [
1865
1960
  {
1866
1961
  flag: "--all",
1867
- description: "Clear all active vault auth sessions for the current shell context."
1962
+ description: "Clear all cached vault auth sessions from ~/.cnos/secrets/sessions."
1868
1963
  }
1869
1964
  ],
1870
1965
  examples: ["cnos vault logout local-dev", "cnos vault logout --all"]
@@ -2124,8 +2219,8 @@ var COMMANDS = [
2124
2219
  {
2125
2220
  id: "build env",
2126
2221
  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.",
2222
+ usage: "cnos build env --to <path> [--format <dotenv|docker-env|json|shell|toml|yaml>] [--reveal] [global-options]",
2223
+ 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.",
2129
2224
  options: [
2130
2225
  {
2131
2226
  flag: "--to <path>",
@@ -2134,11 +2229,16 @@ var COMMANDS = [
2134
2229
  {
2135
2230
  flag: "--format <dotenv|docker-env|json|shell|toml|yaml>",
2136
2231
  description: "Select the output format. Defaults to dotenv."
2232
+ },
2233
+ {
2234
+ flag: "--reveal",
2235
+ description: "Write concrete values for secret env mappings after gitignore verification and an interactive warning prompt."
2137
2236
  }
2138
2237
  ],
2139
2238
  examples: [
2140
2239
  "cnos build env --profile local --to .env.local",
2141
2240
  "cnos build env --profile stage --to .env.stage",
2241
+ "cnos build env --profile prod --reveal --to .env.production.local",
2142
2242
  "cnos build env --profile prod --format yaml --to env.yaml"
2143
2243
  ]
2144
2244
  },
@@ -2252,13 +2352,17 @@ var COMMANDS = [
2252
2352
  {
2253
2353
  id: "run",
2254
2354
  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 --.",
2355
+ usage: "cnos run [--public] [--auth] [--framework <name>] [--set <logical-key=value>] [global-options] -- <command...>",
2356
+ 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
2357
  options: [
2258
2358
  {
2259
2359
  flag: "--set <logical-key=value>",
2260
2360
  description: "Apply inline logical-key overrides for this run without touching repo config files."
2261
2361
  },
2362
+ {
2363
+ flag: "--auth",
2364
+ description: "Resolve secrets eagerly and pass an encrypted secret payload to bootstrapped CNOS runtimes in the child process."
2365
+ },
2262
2366
  {
2263
2367
  flag: "--public",
2264
2368
  description: "Inject only promoted public env variables into the child process."
@@ -2275,6 +2379,7 @@ var COMMANDS = [
2275
2379
  examples: [
2276
2380
  "cnos run -- node server.js",
2277
2381
  "cnos run --profile stage -- node server.js",
2382
+ "cnos run --auth -- node server.js",
2278
2383
  "cnos run --set value.server.port=9999 -- node server.js",
2279
2384
  "cnos run --public --framework vite -- pnpm build"
2280
2385
  ]
@@ -2457,6 +2562,27 @@ var COMMANDS = [
2457
2562
  ],
2458
2563
  examples: ["cnos help-ai --format json", "cnos help-ai export env --format json"]
2459
2564
  },
2565
+ {
2566
+ id: "ui",
2567
+ summary: "Launch the CNOS local UI.",
2568
+ usage: "cnos ui [--host <host>] [--port <port>] [--api-port <port>] [global-options]",
2569
+ description: "Starts a local CNOS API server plus the Vite-powered React UI for browsing values, env mappings, public config, and inspect data.",
2570
+ options: [
2571
+ {
2572
+ flag: "--host <host>",
2573
+ description: "Host for the UI dev server. Defaults to 127.0.0.1."
2574
+ },
2575
+ {
2576
+ flag: "--port <port>",
2577
+ description: "Port for the UI dev server. Defaults to 4310."
2578
+ },
2579
+ {
2580
+ flag: "--api-port <port>",
2581
+ description: "Port for the backing CNOS API server. Defaults to 4311."
2582
+ }
2583
+ ],
2584
+ examples: ["cnos ui", "cnos ui --port 4400 --api-port 4401"]
2585
+ },
2460
2586
  {
2461
2587
  id: "version",
2462
2588
  summary: "Print the installed CNOS CLI version.",
@@ -2618,11 +2744,11 @@ function runHelpAi(topic, cliArgs = []) {
2618
2744
  }
2619
2745
 
2620
2746
  // src/commands/init.ts
2621
- import path10 from "path";
2747
+ import path11 from "path";
2622
2748
 
2623
2749
  // src/services/scaffold.ts
2624
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2625
- import path9 from "path";
2750
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2751
+ import path10 from "path";
2626
2752
  function scaffoldManifest(projectName, options = {}) {
2627
2753
  const mode = options.mode ?? "regular";
2628
2754
  const baseWorkspace = options.workspace ?? "base";
@@ -2662,7 +2788,7 @@ function scaffoldManifest(projectName, options = {}) {
2662
2788
  }
2663
2789
  async function ensureFile(filePath, content) {
2664
2790
  try {
2665
- await readFile3(filePath, "utf8");
2791
+ await readFile4(filePath, "utf8");
2666
2792
  return false;
2667
2793
  } catch {
2668
2794
  await writeFile4(filePath, content, "utf8");
@@ -2670,7 +2796,7 @@ async function ensureFile(filePath, content) {
2670
2796
  }
2671
2797
  }
2672
2798
  async function ensureGitignore(root) {
2673
- const gitignorePath = path9.join(root, ".gitignore");
2799
+ const gitignorePath = path10.join(root, ".gitignore");
2674
2800
  const requiredEntries = [
2675
2801
  ".cnos/env/.env",
2676
2802
  ".cnos/env/.env.*",
@@ -2683,7 +2809,7 @@ async function ensureGitignore(root) {
2683
2809
  ];
2684
2810
  let current = "";
2685
2811
  try {
2686
- current = await readFile3(gitignorePath, "utf8");
2812
+ current = await readFile4(gitignorePath, "utf8");
2687
2813
  } catch {
2688
2814
  current = "";
2689
2815
  }
@@ -2698,12 +2824,12 @@ async function ensureGitignore(root) {
2698
2824
  return true;
2699
2825
  }
2700
2826
  async function ensureWorkspaceLayout(cnosRoot, workspace) {
2701
- const workspaceRoot = workspace ? path9.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2827
+ const workspaceRoot = workspace ? path10.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2702
2828
  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 });
2829
+ await mkdir4(path10.join(workspaceRoot, "profiles"), { recursive: true });
2830
+ await mkdir4(path10.join(workspaceRoot, "values"), { recursive: true });
2831
+ await mkdir4(path10.join(workspaceRoot, "secrets"), { recursive: true });
2832
+ await mkdir4(path10.join(workspaceRoot, "env"), { recursive: true });
2707
2833
  const relativePaths = workspace ? [
2708
2834
  ["workspaces", workspace, "profiles", ".gitkeep"],
2709
2835
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -2716,16 +2842,16 @@ async function ensureWorkspaceLayout(cnosRoot, workspace) {
2716
2842
  ["env", ".gitkeep"]
2717
2843
  ];
2718
2844
  for (const relativePath of relativePaths) {
2719
- const filePath = path9.join(cnosRoot, ...relativePath);
2845
+ const filePath = path10.join(cnosRoot, ...relativePath);
2720
2846
  if (await ensureFile(filePath, "")) {
2721
- createdPaths.push(path9.relative(path9.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
2847
+ createdPaths.push(path10.relative(path10.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
2722
2848
  }
2723
2849
  }
2724
2850
  return createdPaths;
2725
2851
  }
2726
2852
  async function ensureCnosrc(root, workspace) {
2727
2853
  return ensureFile(
2728
- path9.join(root, ".cnosrc.yml"),
2854
+ path10.join(root, ".cnosrc.yml"),
2729
2855
  workspace ? `root: ./.cnos
2730
2856
  workspace: ${workspace}
2731
2857
  ` : "root: ./.cnos\n"
@@ -2735,7 +2861,7 @@ async function scaffoldProject(root, options = {}) {
2735
2861
  const mode = options.mode ?? "regular";
2736
2862
  const baseWorkspace = options.workspace ?? "base";
2737
2863
  const childWorkspaces = mode === "workspace" ? (options.workspaces ?? []).filter((workspaceId) => workspaceId !== baseWorkspace) : [];
2738
- const cnosRoot = path9.join(root, ".cnos");
2864
+ const cnosRoot = path10.join(root, ".cnos");
2739
2865
  const createdPaths = [];
2740
2866
  if (mode === "workspace") {
2741
2867
  createdPaths.push(
@@ -2749,13 +2875,13 @@ async function scaffoldProject(root, options = {}) {
2749
2875
  } else {
2750
2876
  createdPaths.push(...(await ensureWorkspaceLayout(cnosRoot)).map((entry) => entry.replace(/^\.cnos\//, ".cnos/")));
2751
2877
  }
2752
- if (await ensureFile(path9.join(cnosRoot, "cnos.yml"), scaffoldManifest(path9.basename(root), options))) {
2878
+ if (await ensureFile(path10.join(cnosRoot, "cnos.yml"), scaffoldManifest(path10.basename(root), options))) {
2753
2879
  createdPaths.push(".cnos/cnos.yml");
2754
2880
  }
2755
2881
  if (await ensureCnosrc(root, mode === "workspace" ? baseWorkspace : void 0)) {
2756
2882
  createdPaths.push(".cnosrc.yml");
2757
2883
  }
2758
- if (mode === "workspace" && await ensureFile(path9.join(root, ".cnos-workspace.yml"), `workspace: ${baseWorkspace}
2884
+ if (mode === "workspace" && await ensureFile(path10.join(root, ".cnos-workspace.yml"), `workspace: ${baseWorkspace}
2759
2885
  globalRoot: ~/.cnos
2760
2886
  `)) {
2761
2887
  createdPaths.push(".cnos-workspace.yml");
@@ -2779,7 +2905,7 @@ function parseWorkspaceList(value) {
2779
2905
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
2780
2906
  }
2781
2907
  async function runInit(options = {}) {
2782
- const root = path10.resolve(options.root ?? process.cwd());
2908
+ const root = path11.resolve(options.root ?? process.cwd());
2783
2909
  const cliArgs = [...options.cliArgs ?? []];
2784
2910
  const modeOption = consumeOption(cliArgs, "--mode");
2785
2911
  const workspacesOption = consumeOption(cliArgs, "--workspaces");
@@ -3019,7 +3145,7 @@ async function runList(args = [], options = {}) {
3019
3145
  }
3020
3146
 
3021
3147
  // src/commands/migrate.ts
3022
- import path11 from "path";
3148
+ import path12 from "path";
3023
3149
  import {
3024
3150
  applyManifestMappings,
3025
3151
  loadManifest as loadManifest4,
@@ -3047,7 +3173,7 @@ async function runMigrate(options = {}) {
3047
3173
  ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3048
3174
  ...options.forceRefresh ? { forceRefresh: true } : {}
3049
3175
  });
3050
- const scanRoot = path11.resolve(manifest.consumerRoot, scan ?? "src");
3176
+ const scanRoot = path12.resolve(manifest.consumerRoot, scan ?? "src");
3051
3177
  const usages = await scanEnvUsage(scanRoot);
3052
3178
  const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
3053
3179
  const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
@@ -3109,7 +3235,7 @@ async function runMigrate(options = {}) {
3109
3235
  }
3110
3236
 
3111
3237
  // src/commands/namespace.ts
3112
- import path12 from "path";
3238
+ import path13 from "path";
3113
3239
  function normalizeCommand2(args) {
3114
3240
  const [actionOrPath, ...tail] = args;
3115
3241
  if (!actionOrPath) {
@@ -3132,7 +3258,7 @@ function normalizeCommand2(args) {
3132
3258
  async function runNamespace(namespace, args = [], options = {}) {
3133
3259
  const { action, tail } = normalizeCommand2(args);
3134
3260
  const cliArgs = [...options.cliArgs ?? []];
3135
- const root = path12.resolve(options.root ?? process.cwd());
3261
+ const root = path13.resolve(options.root ?? process.cwd());
3136
3262
  if (action === "list") {
3137
3263
  const prefix = consumeOption(cliArgs, "--prefix");
3138
3264
  const entries = await listConfigEntries(namespace, {
@@ -3202,9 +3328,9 @@ async function runNamespace(namespace, args = [], options = {}) {
3202
3328
  }
3203
3329
 
3204
3330
  // 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";
3331
+ import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile5 } from "fs/promises";
3332
+ import path14 from "path";
3333
+ import readline2 from "readline/promises";
3208
3334
  import { loadManifest as loadManifest5, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
3209
3335
  import { parse as parseToml } from "smol-toml";
3210
3336
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
@@ -3273,7 +3399,7 @@ function flattenStructured(value, prefixSegments, currentKey = []) {
3273
3399
  );
3274
3400
  }
3275
3401
  async function parseSource(input, prefixSegments) {
3276
- const content = await readFile4(input.filePath, "utf8");
3402
+ const content = await readFile5(input.filePath, "utf8");
3277
3403
  switch (input.kind) {
3278
3404
  case "env":
3279
3405
  return Object.entries(parseEnv(content)).map(([sourceKey, value]) => {
@@ -3289,7 +3415,7 @@ async function parseSource(input, prefixSegments) {
3289
3415
  }
3290
3416
  }
3291
3417
  function detectKindFromPath(filePath) {
3292
- const ext = path13.extname(filePath).toLowerCase();
3418
+ const ext = path14.extname(filePath).toLowerCase();
3293
3419
  switch (ext) {
3294
3420
  case ".env":
3295
3421
  return "env";
@@ -3320,7 +3446,7 @@ function formatProposals(proposed) {
3320
3446
  });
3321
3447
  }
3322
3448
  async function promptForMaterialize() {
3323
- const rl = readline.createInterface({
3449
+ const rl = readline2.createInterface({
3324
3450
  input: process.stdin,
3325
3451
  output: process.stdout
3326
3452
  });
@@ -3356,19 +3482,19 @@ function resolveSourceInputs(root, cliArgs) {
3356
3482
  if (!source) {
3357
3483
  return [];
3358
3484
  }
3359
- const resolvedPath = path13.resolve(root, source.filePath);
3485
+ const resolvedPath = path14.resolve(root, source.filePath);
3360
3486
  return [
3361
3487
  {
3362
3488
  kind: source.kind,
3363
3489
  filePath: resolvedPath,
3364
- displayName: path13.basename(resolvedPath)
3490
+ displayName: path14.basename(resolvedPath)
3365
3491
  }
3366
3492
  ];
3367
3493
  }
3368
3494
  return [];
3369
3495
  }
3370
3496
  async function runOnboard(options = {}) {
3371
- const root = path13.resolve(options.root ?? process.cwd());
3497
+ const root = path14.resolve(options.root ?? process.cwd());
3372
3498
  const cliArgs = [...options.cliArgs ?? []];
3373
3499
  const move = consumeFlag(cliArgs, "--move");
3374
3500
  const materialize = consumeFlag(cliArgs, "--materialize");
@@ -3382,7 +3508,7 @@ async function runOnboard(options = {}) {
3382
3508
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
3383
3509
  }
3384
3510
  let scaffolded = [];
3385
- const manifestPath = path13.join(root, ".cnos", "cnos.yml");
3511
+ const manifestPath = path14.join(root, ".cnos", "cnos.yml");
3386
3512
  if (!await exists(manifestPath)) {
3387
3513
  const scaffold = await scaffoldProject(root, {
3388
3514
  mode: options.workspace && options.workspace !== "base" ? "workspace" : "regular",
@@ -3402,11 +3528,11 @@ async function runOnboard(options = {}) {
3402
3528
  if (!isWorkspaceMode && options.workspace && options.workspace !== "base") {
3403
3529
  throw new Error("This repo is still in regular mode. Run `cnos workspace enable` before onboarding into a child workspace.");
3404
3530
  }
3405
- const envRoot = isWorkspaceMode ? path13.join(root, ".cnos", "workspaces", selectedWorkspace, "env") : path13.join(root, ".cnos", "env");
3531
+ const envRoot = isWorkspaceMode ? path14.join(root, ".cnos", "workspaces", selectedWorkspace, "env") : path14.join(root, ".cnos", "env");
3406
3532
  await mkdir5(envRoot, { recursive: true });
3407
3533
  const rootFiles = explicitSources.length > 0 ? explicitSources : (await listRootEnvFiles(root)).map((fileName) => ({
3408
3534
  kind: "env",
3409
- filePath: path13.join(root, fileName),
3535
+ filePath: path14.join(root, fileName),
3410
3536
  displayName: fileName
3411
3537
  }));
3412
3538
  const imported = [];
@@ -3414,10 +3540,10 @@ async function runOnboard(options = {}) {
3414
3540
  const prefixSegments = buildPrefixSegments(prefix);
3415
3541
  const proposed = [];
3416
3542
  for (const source of rootFiles) {
3417
- const targetPath = path13.join(envRoot, source.displayName);
3543
+ const targetPath = path14.join(envRoot, source.displayName);
3418
3544
  try {
3419
3545
  await copyFile(source.filePath, targetPath);
3420
- imported.push(path13.relative(root, targetPath).replace(/\\/g, "/"));
3546
+ imported.push(path14.relative(root, targetPath).replace(/\\/g, "/"));
3421
3547
  if (move) {
3422
3548
  await rm2(source.filePath);
3423
3549
  }
@@ -3455,7 +3581,7 @@ async function runOnboard(options = {}) {
3455
3581
  }
3456
3582
  const lines = [
3457
3583
  `onboarded ${selectedWorkspace} at ${root}`,
3458
- `Imported ${imported.length} source file(s) into ${path13.relative(root, envRoot).replace(/\\/g, "/") || ".cnos/env"} using ${result.mode}.`,
3584
+ `Imported ${imported.length} source file(s) into ${path14.relative(root, envRoot).replace(/\\/g, "/") || ".cnos/env"} using ${result.mode}.`,
3459
3585
  "",
3460
3586
  `Discovered ${proposed.length} proposed value mapping(s):`,
3461
3587
  ...formatProposals(proposed)
@@ -3474,16 +3600,16 @@ async function runOnboard(options = {}) {
3474
3600
  }
3475
3601
 
3476
3602
  // src/commands/profile.ts
3477
- import path16 from "path";
3603
+ import path17 from "path";
3478
3604
 
3479
3605
  // src/services/context.ts
3480
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
3481
- import path14 from "path";
3606
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
3607
+ import path15 from "path";
3482
3608
  import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
3483
3609
  async function loadCliContext(root = process.cwd()) {
3484
- const filePath = path14.join(path14.resolve(root), ".cnos-workspace.yml");
3610
+ const filePath = path15.join(path15.resolve(root), ".cnos-workspace.yml");
3485
3611
  try {
3486
- const source = await readFile5(filePath, "utf8");
3612
+ const source = await readFile6(filePath, "utf8");
3487
3613
  const parsed = parseYaml4(source);
3488
3614
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3489
3615
  return {};
@@ -3494,8 +3620,8 @@ async function loadCliContext(root = process.cwd()) {
3494
3620
  }
3495
3621
  }
3496
3622
  async function saveCliContext(options = {}) {
3497
- const root = path14.resolve(options.root ?? process.cwd());
3498
- const filePath = path14.join(root, ".cnos-workspace.yml");
3623
+ const root = path15.resolve(options.root ?? process.cwd());
3624
+ const filePath = path15.join(root, ".cnos-workspace.yml");
3499
3625
  const current = await loadCliContext(root);
3500
3626
  const next = {
3501
3627
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -3513,21 +3639,21 @@ async function saveCliContext(options = {}) {
3513
3639
  }
3514
3640
 
3515
3641
  // 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";
3642
+ import { mkdir as mkdir6, readdir as readdir4, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "fs/promises";
3643
+ import path16 from "path";
3518
3644
  import { loadManifest as loadManifest6, parseYaml as parseYaml5, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
3519
3645
  async function resolveProfilesRoot(root = process.cwd()) {
3520
3646
  try {
3521
3647
  const loadedManifest = await loadManifest6({ root });
3522
- return path15.join(loadedManifest.manifestRoot, "profiles");
3648
+ return path16.join(loadedManifest.manifestRoot, "profiles");
3523
3649
  } catch {
3524
3650
  const loadedManifest = await loadManifest6({ cwd: root });
3525
- return path15.join(loadedManifest.manifestRoot, "profiles");
3651
+ return path16.join(loadedManifest.manifestRoot, "profiles");
3526
3652
  }
3527
3653
  }
3528
3654
  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 });
3655
+ const filePath = path16.join(await resolveProfilesRoot(root), `${profile}.yml`);
3656
+ await mkdir6(path16.dirname(filePath), { recursive: true });
3531
3657
  const document = options.noInherit ? {
3532
3658
  name: profile,
3533
3659
  activate: {
@@ -3565,7 +3691,7 @@ async function listProfiles(root = process.cwd()) {
3565
3691
  }
3566
3692
  }
3567
3693
  async function deleteProfileDefinition(root = process.cwd(), profile) {
3568
- const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3694
+ const filePath = path16.join(await resolveProfilesRoot(root), `${profile}.yml`);
3569
3695
  try {
3570
3696
  await rm3(filePath);
3571
3697
  return {
@@ -3585,9 +3711,9 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
3585
3711
  name: "base"
3586
3712
  };
3587
3713
  }
3588
- const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3714
+ const filePath = path16.join(await resolveProfilesRoot(root), `${profile}.yml`);
3589
3715
  try {
3590
- return parseYaml5(await readFile6(filePath, "utf8")) ?? void 0;
3716
+ return parseYaml5(await readFile7(filePath, "utf8")) ?? void 0;
3591
3717
  } catch {
3592
3718
  return void 0;
3593
3719
  }
@@ -3632,7 +3758,7 @@ async function runProfile(args, options = {}) {
3632
3758
  if (action === "use") {
3633
3759
  const profile = tail[0] ?? "base";
3634
3760
  const result = await saveCliContext({
3635
- root: path16.resolve(root),
3761
+ root: path17.resolve(root),
3636
3762
  profile
3637
3763
  });
3638
3764
  if (options.json) {
@@ -3663,7 +3789,7 @@ async function runProfile(args, options = {}) {
3663
3789
  }
3664
3790
 
3665
3791
  // src/commands/promote.ts
3666
- import path17 from "path";
3792
+ import path18 from "path";
3667
3793
  import { writeFile as writeFile7 } from "fs/promises";
3668
3794
  import {
3669
3795
  ensureProjectionAllowed,
@@ -3680,7 +3806,7 @@ function sortRecord(record) {
3680
3806
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
3681
3807
  }
3682
3808
  async function runPromote(args = [], options = {}) {
3683
- const root = path17.resolve(options.root ?? process.cwd());
3809
+ const root = path18.resolve(options.root ?? process.cwd());
3684
3810
  const cliArgs = [...options.cliArgs ?? []];
3685
3811
  const target = normalizeTarget(consumeOption(cliArgs, "--to"));
3686
3812
  const alias = consumeOption(cliArgs, "--as");
@@ -3760,8 +3886,11 @@ async function runRead(key, options = {}) {
3760
3886
  import {
3761
3887
  CNOS_GRAPH_ENV_VAR,
3762
3888
  CNOS_PROJECTION_ENV_VAR,
3889
+ CNOS_SECRET_PAYLOAD_ENV_VAR,
3890
+ CNOS_SESSION_KEY_ENV_VAR,
3763
3891
  serializeServerProjection,
3764
- serializeRuntimeGraph
3892
+ serializeRuntimeGraph,
3893
+ serializeSecretPayload
3765
3894
  } from "@kitsy/cnos/internal";
3766
3895
  function consumeOptions(args, flag) {
3767
3896
  const values = [];
@@ -3799,7 +3928,7 @@ async function runCommand(command, options = {}) {
3799
3928
  }
3800
3929
  const cliArgs = [...options.cliArgs ?? []];
3801
3930
  const isPublic = consumeFlag(cliArgs, "--public");
3802
- consumeFlag(cliArgs, "--auth");
3931
+ const isAuthenticated = consumeFlag(cliArgs, "--auth");
3803
3932
  const framework = consumeOption(cliArgs, "--framework");
3804
3933
  const prefix = consumeOption(cliArgs, "--prefix");
3805
3934
  const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
@@ -3807,14 +3936,22 @@ async function runCommand(command, options = {}) {
3807
3936
  ...options,
3808
3937
  cliArgs: [...cliArgs, ...setOverrides]
3809
3938
  });
3939
+ const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
3940
+ 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)
3941
+ ) : void 0;
3942
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
3810
3943
  const env = {
3811
3944
  ...process.env,
3812
3945
  ...isPublic ? runtime.toPublicEnv({
3813
3946
  ...framework ? { framework } : {},
3814
3947
  ...prefix ? { prefix } : {}
3815
- }) : runtime.toEnv(),
3948
+ }) : runtime.toEnv({ includeSecrets: true }),
3816
3949
  [CNOS_PROJECTION_ENV_VAR]: serializeServerProjection(runtime.toServerProjection()),
3817
- [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
3950
+ [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
3951
+ ...secretPayload ? {
3952
+ [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
3953
+ [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
3954
+ } : {}
3818
3955
  };
3819
3956
  return new Promise((resolve, reject) => {
3820
3957
  const executable = command[0];
@@ -3849,14 +3986,14 @@ async function runCommand(command, options = {}) {
3849
3986
  }
3850
3987
 
3851
3988
  // src/commands/secret.ts
3852
- import path20 from "path";
3989
+ import path21 from "path";
3853
3990
 
3854
3991
  // src/commands/vault.ts
3855
- import path19 from "path";
3992
+ import path20 from "path";
3856
3993
 
3857
3994
  // src/services/vaults.ts
3858
3995
  import { rm as rm4, writeFile as writeFile8 } from "fs/promises";
3859
- import path18 from "path";
3996
+ import path19 from "path";
3860
3997
  import {
3861
3998
  clearAllVaultSessionKeys,
3862
3999
  clearVaultSessionKey,
@@ -3985,7 +4122,7 @@ async function removeVaultDefinition(name, options = {}) {
3985
4122
  delete rawManifest.vaults;
3986
4123
  }
3987
4124
  await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
3988
- const vaultRoot = path18.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
4125
+ const vaultRoot = path19.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3989
4126
  let removedStore;
3990
4127
  try {
3991
4128
  await rm4(vaultRoot, { recursive: true, force: true });
@@ -4086,7 +4223,7 @@ function normalizeVaultAction(args) {
4086
4223
  async function runVault(args = [], options = {}) {
4087
4224
  const { action, tail } = normalizeVaultAction(args);
4088
4225
  const cliArgs = [...options.cliArgs ?? []];
4089
- const root = path19.resolve(options.root ?? process.cwd());
4226
+ const root = path20.resolve(options.root ?? process.cwd());
4090
4227
  if (consumeOption(cliArgs, "--passphrase")) {
4091
4228
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
4092
4229
  }
@@ -4192,7 +4329,7 @@ async function runSecret(argsOrPath, options = {}) {
4192
4329
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
4193
4330
  const { action, tail } = normalizeSecretCommand(args);
4194
4331
  const cliArgs = [...options.cliArgs ?? []];
4195
- const root = path20.resolve(options.root ?? process.cwd());
4332
+ const root = path21.resolve(options.root ?? process.cwd());
4196
4333
  if (consumeOption(cliArgs, "--passphrase")) {
4197
4334
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
4198
4335
  }
@@ -4295,9 +4432,9 @@ async function runSecret(argsOrPath, options = {}) {
4295
4432
  }
4296
4433
 
4297
4434
  // src/commands/use.ts
4298
- import path21 from "path";
4435
+ import path22 from "path";
4299
4436
  async function runUse(args = [], options = {}) {
4300
- const root = path21.resolve(options.root ?? process.cwd());
4437
+ const root = path22.resolve(options.root ?? process.cwd());
4301
4438
  const action = args[0];
4302
4439
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
4303
4440
  if (action === "show" || !action && !hasUpdates) {
@@ -4319,6 +4456,219 @@ async function runUse(args = [], options = {}) {
4319
4456
  return `updated CLI context in ${displayPath(result.filePath, root)}`;
4320
4457
  }
4321
4458
 
4459
+ // src/commands/ui.ts
4460
+ import { createServer } from "http";
4461
+ import path23 from "path";
4462
+ import { createRequire } from "module";
4463
+ import { loadManifest as loadManifest9 } from "@kitsy/cnos/internal";
4464
+ function parsePort(value, fallback, flag) {
4465
+ if (!value) {
4466
+ return fallback;
4467
+ }
4468
+ const parsed = Number(value);
4469
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
4470
+ throw new Error(`Invalid value for ${flag}: ${value}`);
4471
+ }
4472
+ return parsed;
4473
+ }
4474
+ function resolveUiPackageRoot() {
4475
+ const require2 = createRequire(import.meta.url);
4476
+ try {
4477
+ const packageJsonPath = require2.resolve("@kitsy/cnos-ui/package.json");
4478
+ return path23.dirname(packageJsonPath);
4479
+ } catch {
4480
+ throw new Error("Unable to resolve @kitsy/cnos-ui. Install workspace dependencies before running `cnos ui`.");
4481
+ }
4482
+ }
4483
+ function writeJson(response, statusCode, payload) {
4484
+ response.statusCode = statusCode;
4485
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
4486
+ response.end(`${printJson(payload)}
4487
+ `);
4488
+ }
4489
+ function maskInspectResult(key, value) {
4490
+ if (!key.startsWith("secret.")) {
4491
+ return value;
4492
+ }
4493
+ return {
4494
+ ...value,
4495
+ value: maskSecretValue(value.value),
4496
+ overridden: value.overridden.map((entry) => ({
4497
+ ...entry,
4498
+ value: maskSecretValue(entry.value)
4499
+ }))
4500
+ };
4501
+ }
4502
+ function maskListEntry(entry) {
4503
+ if (!entry.key.startsWith("secret.")) {
4504
+ return entry;
4505
+ }
4506
+ return {
4507
+ ...entry,
4508
+ value: maskSecretValue(entry.value)
4509
+ };
4510
+ }
4511
+ function toRuntimeOptionsFromQuery(baseOptions, searchParams) {
4512
+ const workspace = searchParams.get("workspace")?.trim();
4513
+ const profile = searchParams.get("profile")?.trim();
4514
+ return {
4515
+ ...baseOptions,
4516
+ ...workspace ? { workspace } : {},
4517
+ ...profile ? { profile } : {}
4518
+ };
4519
+ }
4520
+ async function handleSummary(options, searchParams) {
4521
+ const runtimeOptions = toRuntimeOptionsFromQuery(options, searchParams);
4522
+ const runtime = await createRuntimeService({
4523
+ ...runtimeOptions,
4524
+ secretResolution: "lazy"
4525
+ });
4526
+ const loadedManifest = await loadManifest9({
4527
+ ...options.root ? { root: options.root } : {},
4528
+ ...options.cwd ? { cwd: options.cwd } : {},
4529
+ ...options.processEnv ? { processEnv: options.processEnv } : {}
4530
+ });
4531
+ const declaredWorkspaces = Object.keys(loadedManifest.manifest.workspaces.items);
4532
+ const workspaces = declaredWorkspaces.length > 0 ? declaredWorkspaces.sort((left, right) => left.localeCompare(right)) : ["base"];
4533
+ const profiles = await listProfiles(loadedManifest.consumerRoot);
4534
+ const envEntries = runtime.toEnv();
4535
+ const publicEntries = runtime.toPublicEnv();
4536
+ const counts = Array.from(runtime.graph.entries.values()).reduce((acc, entry) => {
4537
+ acc.all += 1;
4538
+ acc[entry.namespace] = (acc[entry.namespace] ?? 0) + 1;
4539
+ return acc;
4540
+ }, { all: 0 });
4541
+ return {
4542
+ project: runtime.manifest.project.name,
4543
+ workspace: runtime.graph.workspace.workspaceId,
4544
+ workspaceSource: runtime.graph.workspace.workspaceSource,
4545
+ workspaceChain: runtime.graph.workspace.workspaceChain,
4546
+ profile: runtime.graph.profile,
4547
+ profileSource: runtime.graph.profileSource,
4548
+ counts: {
4549
+ ...counts,
4550
+ env: Object.keys(envEntries).length,
4551
+ public: Object.keys(publicEntries).length
4552
+ },
4553
+ envMapping: Object.entries(runtime.manifest.envMapping.explicit).map(([envVar, logicalKey]) => ({
4554
+ envVar,
4555
+ logicalKey,
4556
+ secret: runtime.graph.entries.get(logicalKey)?.namespace === "secret"
4557
+ })),
4558
+ promoted: runtime.manifest.public.promote,
4559
+ workspaces,
4560
+ profiles,
4561
+ runtimeNamespaces: Object.keys(runtime.manifest.runtimeNamespaces),
4562
+ vaults: Object.keys(runtime.manifest.vaults)
4563
+ };
4564
+ }
4565
+ async function handleRequest(request, response, options) {
4566
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
4567
+ if (request.method !== "GET") {
4568
+ writeJson(response, 405, { error: "Method not allowed" });
4569
+ return;
4570
+ }
4571
+ if (url.pathname === "/api/health") {
4572
+ writeJson(response, 200, { ok: true });
4573
+ return;
4574
+ }
4575
+ if (url.pathname === "/api/summary") {
4576
+ writeJson(response, 200, await handleSummary(options, url.searchParams));
4577
+ return;
4578
+ }
4579
+ if (url.pathname === "/api/list") {
4580
+ const namespace = url.searchParams.get("namespace") ?? "value";
4581
+ const prefix = url.searchParams.get("prefix") ?? void 0;
4582
+ const runtimeOptions = toRuntimeOptionsFromQuery(options, url.searchParams);
4583
+ const entries = await listConfigEntries(namespace, {
4584
+ ...runtimeOptions,
4585
+ ...prefix ? { prefix } : {},
4586
+ ...namespace === "secret" ? { secretResolution: "lazy" } : {}
4587
+ });
4588
+ writeJson(response, 200, {
4589
+ namespace,
4590
+ entries: entries.map((entry) => maskListEntry(entry))
4591
+ });
4592
+ return;
4593
+ }
4594
+ if (url.pathname === "/api/inspect") {
4595
+ const key = url.searchParams.get("key");
4596
+ if (!key) {
4597
+ writeJson(response, 400, { error: "Missing key query parameter" });
4598
+ return;
4599
+ }
4600
+ const runtimeOptions = toRuntimeOptionsFromQuery(options, url.searchParams);
4601
+ const runtime = await createRuntimeService({
4602
+ ...runtimeOptions,
4603
+ ...key.startsWith("secret.") ? { secretResolution: "lazy" } : {}
4604
+ });
4605
+ writeJson(response, 200, maskInspectResult(key, runtime.inspect(key)));
4606
+ return;
4607
+ }
4608
+ writeJson(response, 404, { error: "Not found" });
4609
+ }
4610
+ function resolveUiUrl(host, port) {
4611
+ if (host === "0.0.0.0" || host === "::") {
4612
+ return `http://127.0.0.1:${port}`;
4613
+ }
4614
+ return `http://${host}:${port}`;
4615
+ }
4616
+ async function runUi(options = {}) {
4617
+ const cliArgs = [...options.cliArgs ?? []];
4618
+ const host = consumeOption(cliArgs, "--host") ?? "127.0.0.1";
4619
+ const port = parsePort(consumeOption(cliArgs, "--port"), 4310, "--port");
4620
+ const apiPort = parsePort(consumeOption(cliArgs, "--api-port"), 4311, "--api-port");
4621
+ if (cliArgs.length > 0) {
4622
+ throw new Error(`Unsupported ui arguments: ${cliArgs.join(" ")}`);
4623
+ }
4624
+ const uiRoot = resolveUiPackageRoot();
4625
+ const apiServer = createServer((request, response) => {
4626
+ void handleRequest(request, response, options).catch((error) => {
4627
+ writeJson(response, 500, {
4628
+ error: error instanceof Error ? error.message : String(error)
4629
+ });
4630
+ });
4631
+ });
4632
+ await new Promise((resolve, reject) => {
4633
+ apiServer.once("error", reject);
4634
+ apiServer.listen(apiPort, "127.0.0.1", () => resolve());
4635
+ });
4636
+ const uiProcess = spawnCommand(
4637
+ ["pnpm", "exec", "vite", "--host", host, "--port", String(port)],
4638
+ {
4639
+ cwd: uiRoot,
4640
+ env: {
4641
+ ...process.env,
4642
+ ...options.processEnv,
4643
+ CNOS_UI_API_TARGET: `http://127.0.0.1:${apiPort}`
4644
+ },
4645
+ stdio: "inherit"
4646
+ }
4647
+ );
4648
+ const uiUrl = resolveUiUrl(host, port);
4649
+ const apiAddress = apiServer.address();
4650
+ console.log(`CNOS UI running at ${uiUrl}`);
4651
+ console.log(`CNOS UI API running at http://127.0.0.1:${apiAddress?.port ?? apiPort}`);
4652
+ const shutdown = () => {
4653
+ apiServer.close();
4654
+ if (!uiProcess.killed) {
4655
+ uiProcess.kill("SIGTERM");
4656
+ }
4657
+ };
4658
+ process.once("SIGINT", shutdown);
4659
+ process.once("SIGTERM", shutdown);
4660
+ try {
4661
+ await new Promise((resolve, reject) => {
4662
+ uiProcess.once("error", reject);
4663
+ uiProcess.once("exit", () => resolve());
4664
+ });
4665
+ } finally {
4666
+ process.removeListener("SIGINT", shutdown);
4667
+ process.removeListener("SIGTERM", shutdown);
4668
+ apiServer.close();
4669
+ }
4670
+ }
4671
+
4322
4672
  // src/commands/validate.ts
4323
4673
  async function runValidate(options = {}) {
4324
4674
  const { summary } = await createValidationSummary(options);
@@ -4334,7 +4684,7 @@ async function runValidate(options = {}) {
4334
4684
  // package.json
4335
4685
  var package_default = {
4336
4686
  name: "@kitsy/cnos-cli",
4337
- version: "1.8.3",
4687
+ version: "1.9.0",
4338
4688
  description: "CLI entry point and developer tooling for CNOS.",
4339
4689
  type: "module",
4340
4690
  main: "./dist/index.js",
@@ -4371,6 +4721,7 @@ var package_default = {
4371
4721
  },
4372
4722
  dependencies: {
4373
4723
  "@kitsy/cnos": "workspace:*",
4724
+ "@kitsy/cnos-ui": "workspace:*",
4374
4725
  "smol-toml": "^1.4.2"
4375
4726
  },
4376
4727
  scripts: {
@@ -4390,7 +4741,7 @@ function runVersion() {
4390
4741
  }
4391
4742
 
4392
4743
  // src/commands/value.ts
4393
- import path22 from "path";
4744
+ import path24 from "path";
4394
4745
  function normalizeValueCommand(args) {
4395
4746
  const [actionOrPath, ...tail] = args;
4396
4747
  if (!actionOrPath) {
@@ -4414,7 +4765,7 @@ async function runValue(argsOrPath, options = {}) {
4414
4765
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
4415
4766
  const { action, tail } = normalizeValueCommand(args);
4416
4767
  const cliArgs = [...options.cliArgs ?? []];
4417
- const root = path22.resolve(options.root ?? process.cwd());
4768
+ const root = path24.resolve(options.root ?? process.cwd());
4418
4769
  if (action === "list") {
4419
4770
  const prefix = consumeOption(cliArgs, "--prefix");
4420
4771
  const entries = await listConfigEntries("value", {
@@ -4485,10 +4836,10 @@ async function runValue(argsOrPath, options = {}) {
4485
4836
  // src/commands/watch.ts
4486
4837
  import {
4487
4838
  CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
4488
- CNOS_SECRET_PAYLOAD_ENV_VAR,
4489
- CNOS_SESSION_KEY_ENV_VAR,
4839
+ CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
4840
+ CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
4490
4841
  serializeRuntimeGraph as serializeRuntimeGraph2,
4491
- serializeSecretPayload
4842
+ serializeSecretPayload as serializeSecretPayload2
4492
4843
  } from "@kitsy/cnos/internal";
4493
4844
  async function buildRunEnvironment(options) {
4494
4845
  const cliArgs = [...options.cliArgs ?? []];
@@ -4504,7 +4855,7 @@ async function buildRunEnvironment(options) {
4504
4855
  const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
4505
4856
  Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
4506
4857
  ) : void 0;
4507
- const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
4858
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
4508
4859
  return {
4509
4860
  runtime,
4510
4861
  env: {
@@ -4512,11 +4863,11 @@ async function buildRunEnvironment(options) {
4512
4863
  ...isPublic ? runtime.toPublicEnv({
4513
4864
  ...framework ? { framework } : {},
4514
4865
  ...prefix ? { prefix } : {}
4515
- }) : runtime.toEnv(),
4866
+ }) : runtime.toEnv({ includeSecrets: true }),
4516
4867
  [CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
4517
4868
  ...secretPayload ? {
4518
- [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
4519
- [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
4869
+ [CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
4870
+ [CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
4520
4871
  } : {}
4521
4872
  }
4522
4873
  };
@@ -4610,9 +4961,9 @@ async function runWatch(command, options = {}) {
4610
4961
  }
4611
4962
 
4612
4963
  // 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";
4964
+ import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile8, rename, rm as rm5, stat as stat3, writeFile as writeFile9 } from "fs/promises";
4965
+ import path25 from "path";
4966
+ import { loadManifest as loadManifest10, parseYaml as parseYaml6, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
4616
4967
  async function exists2(targetPath) {
4617
4968
  try {
4618
4969
  await stat3(targetPath);
@@ -4625,7 +4976,7 @@ async function copyIfExists(source, target) {
4625
4976
  if (!await exists2(source)) {
4626
4977
  return;
4627
4978
  }
4628
- await mkdir7(path23.dirname(target), { recursive: true });
4979
+ await mkdir7(path25.dirname(target), { recursive: true });
4629
4980
  await cp(source, target, { recursive: true, force: true });
4630
4981
  }
4631
4982
  async function moveIfExists(source, target, force = false) {
@@ -4637,22 +4988,22 @@ async function moveIfExists(source, target, force = false) {
4637
4988
  } else if (await exists2(target)) {
4638
4989
  throw new Error(`Refusing to overwrite existing path ${target}. Use --force to replace it.`);
4639
4990
  }
4640
- await mkdir7(path23.dirname(target), { recursive: true });
4991
+ await mkdir7(path25.dirname(target), { recursive: true });
4641
4992
  await rename(source, target);
4642
4993
  return true;
4643
4994
  }
4644
4995
  async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
4645
4996
  for (const sourceRoot of sourceRoots) {
4646
4997
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4647
- await copyIfExists(path23.join(sourceRoot, folderName), path23.join(targetCnosRoot, folderName));
4998
+ await copyIfExists(path25.join(sourceRoot, folderName), path25.join(targetCnosRoot, folderName));
4648
4999
  }
4649
5000
  }
4650
5001
  }
4651
5002
  async function writeAnchor(packageRoot, manifestRoot, workspace) {
4652
- const relativeRoot = path23.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
5003
+ const relativeRoot = path25.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
4653
5004
  const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
4654
5005
  await writeFile9(
4655
- path23.join(packageRoot, ".cnosrc.yml"),
5006
+ path25.join(packageRoot, ".cnosrc.yml"),
4656
5007
  stringifyYaml7({
4657
5008
  root: rootValue,
4658
5009
  ...workspace ? { workspace } : {}
@@ -4689,7 +5040,7 @@ function splitExtends(value) {
4689
5040
  }
4690
5041
  async function hasDirectConfigData(cnosRoot) {
4691
5042
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4692
- const folder = path23.join(cnosRoot, folderName);
5043
+ const folder = path25.join(cnosRoot, folderName);
4693
5044
  if (!await exists2(folder)) {
4694
5045
  continue;
4695
5046
  }
@@ -4701,8 +5052,8 @@ async function hasDirectConfigData(cnosRoot) {
4701
5052
  return false;
4702
5053
  }
4703
5054
  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;
5055
+ const anchorPath = path25.join(packageRoot, ".cnosrc.yml");
5056
+ const current = await exists2(anchorPath) ? parseYaml6(await readFile8(anchorPath, "utf8")) : void 0;
4706
5057
  await writeFile9(
4707
5058
  anchorPath,
4708
5059
  stringifyYaml7({
@@ -4713,8 +5064,8 @@ async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
4713
5064
  );
4714
5065
  }
4715
5066
  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;
5067
+ const workspacePath = path25.join(packageRoot, ".cnos-workspace.yml");
5068
+ const current = await exists2(workspacePath) ? parseYaml6(await readFile8(workspacePath, "utf8")) : void 0;
4718
5069
  await writeFile9(
4719
5070
  workspacePath,
4720
5071
  stringifyYaml7({
@@ -4726,11 +5077,11 @@ async function updateWorkspaceContext(packageRoot, workspaceId) {
4726
5077
  );
4727
5078
  }
4728
5079
  async function runDetach(packageRoot, options = {}) {
4729
- const loaded = await loadManifest9({ cwd: packageRoot });
5080
+ const loaded = await loadManifest10({ cwd: packageRoot });
4730
5081
  if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
4731
5082
  throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
4732
5083
  }
4733
- const targetCnosRoot = path23.join(packageRoot, ".cnos");
5084
+ const targetCnosRoot = path25.join(packageRoot, ".cnos");
4734
5085
  const force = consumeFlag([...options.cliArgs ?? []], "--force");
4735
5086
  if (await exists2(targetCnosRoot) && !force) {
4736
5087
  throw new Error(`Refusing to detach because ${displayPath(targetCnosRoot, packageRoot)} already exists. Use --force to overwrite.`);
@@ -4747,11 +5098,11 @@ async function runDetach(packageRoot, options = {}) {
4747
5098
  await mkdir7(targetCnosRoot, { recursive: true });
4748
5099
  await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
4749
5100
  await writeFile9(
4750
- path23.join(targetCnosRoot, "cnos.yml"),
5101
+ path25.join(targetCnosRoot, "cnos.yml"),
4751
5102
  stringifyYaml7(createDetachedManifest(loaded.rawManifest)),
4752
5103
  "utf8"
4753
5104
  );
4754
- const relativeRoot = path23.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
5105
+ const relativeRoot = path25.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
4755
5106
  const marker = {
4756
5107
  detachedFrom: relativeRoot || ".",
4757
5108
  detachedWorkspace: loaded.anchoredWorkspace,
@@ -4761,8 +5112,8 @@ async function runDetach(packageRoot, options = {}) {
4761
5112
  workspace: loaded.anchoredWorkspace
4762
5113
  }
4763
5114
  };
4764
- await writeFile9(path23.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
4765
- await writeFile9(path23.join(packageRoot, ".cnosrc.yml"), stringifyYaml7({ root: "./.cnos" }), "utf8");
5115
+ await writeFile9(path25.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
5116
+ await writeFile9(path25.join(packageRoot, ".cnosrc.yml"), stringifyYaml7({ root: "./.cnos" }), "utf8");
4766
5117
  if (options.json) {
4767
5118
  return printJson({
4768
5119
  packageRoot,
@@ -4775,24 +5126,24 @@ async function runDetach(packageRoot, options = {}) {
4775
5126
  async function runAttach(packageRoot, options = {}) {
4776
5127
  const cliArgs = [...options.cliArgs ?? []];
4777
5128
  const force = consumeFlag(cliArgs, "--force");
4778
- const childCnosRoot = path23.join(packageRoot, ".cnos");
4779
- const markerPath = path23.join(childCnosRoot, ".detached");
5129
+ const childCnosRoot = path25.join(packageRoot, ".cnos");
5130
+ const markerPath = path25.join(childCnosRoot, ".detached");
4780
5131
  if (!await exists2(markerPath)) {
4781
5132
  throw new Error("workspace attach requires a detached package with .cnos/.detached");
4782
5133
  }
4783
- const marker = parseYaml6(await readFile7(markerPath, "utf8"));
5134
+ const marker = parseYaml6(await readFile8(markerPath, "utf8"));
4784
5135
  if (!marker?.originalCnosrc?.root || !marker.detachedWorkspace) {
4785
5136
  throw new Error("Invalid .detached marker");
4786
5137
  }
4787
- const parentManifestRoot = path23.resolve(packageRoot, marker.originalCnosrc.root);
4788
- const parentLoaded = await loadManifest9({ root: parentManifestRoot });
5138
+ const parentManifestRoot = path25.resolve(packageRoot, marker.originalCnosrc.root);
5139
+ const parentLoaded = await loadManifest10({ root: parentManifestRoot });
4789
5140
  if (parentLoaded.rootResolution.readOnly) {
4790
5141
  throw new Error(
4791
5142
  `Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
4792
5143
  );
4793
5144
  }
4794
5145
  const workspaceId = marker.originalCnosrc.workspace ?? marker.detachedWorkspace;
4795
- const parentWorkspaceRoot = path23.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
5146
+ const parentWorkspaceRoot = path25.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
4796
5147
  if (await exists2(parentWorkspaceRoot) && !force) {
4797
5148
  throw new Error(`workspace "${workspaceId}" already exists in parent root. Use --force to overwrite.`);
4798
5149
  }
@@ -4801,7 +5152,7 @@ async function runAttach(packageRoot, options = {}) {
4801
5152
  }
4802
5153
  await mkdir7(parentWorkspaceRoot, { recursive: true });
4803
5154
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4804
- await copyIfExists(path23.join(childCnosRoot, folderName), path23.join(parentWorkspaceRoot, folderName));
5155
+ await copyIfExists(path25.join(childCnosRoot, folderName), path25.join(parentWorkspaceRoot, folderName));
4805
5156
  }
4806
5157
  const rawManifest = structuredClone(parentLoaded.rawManifest);
4807
5158
  const workspaces = rawManifest.workspaces ?? {};
@@ -4809,8 +5160,8 @@ async function runAttach(packageRoot, options = {}) {
4809
5160
  items[workspaceId] = items[workspaceId] ?? {};
4810
5161
  workspaces.items = items;
4811
5162
  rawManifest.workspaces = workspaces;
4812
- await writeFile9(path23.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4813
- const archivePath = path23.join(packageRoot, ".cnos.detached.bak");
5163
+ await writeFile9(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
5164
+ const archivePath = path25.join(packageRoot, ".cnos.detached.bak");
4814
5165
  await rm5(archivePath, { recursive: true, force: true });
4815
5166
  await rename(childCnosRoot, archivePath);
4816
5167
  await writeAnchor(packageRoot, parentLoaded.manifestRoot, workspaceId);
@@ -4825,7 +5176,7 @@ async function runAttach(packageRoot, options = {}) {
4825
5176
  return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
4826
5177
  }
4827
5178
  async function runList2(manifestCwd, options = {}) {
4828
- const loaded = await loadManifest9({
5179
+ const loaded = await loadManifest10({
4829
5180
  ...options.root ? { root: options.root } : {},
4830
5181
  cwd: manifestCwd,
4831
5182
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4834,7 +5185,7 @@ async function runList2(manifestCwd, options = {}) {
4834
5185
  id,
4835
5186
  extends: config.extends,
4836
5187
  default: loaded.manifest.workspaces.default === id,
4837
- path: path23.join(loaded.manifestRoot, "workspaces", id)
5188
+ path: path25.join(loaded.manifestRoot, "workspaces", id)
4838
5189
  })).sort((left, right) => left.id.localeCompare(right.id));
4839
5190
  if (options.json) {
4840
5191
  return printJson({
@@ -4858,7 +5209,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
4858
5209
  if (cliArgs.length > 0) {
4859
5210
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4860
5211
  }
4861
- const loaded = await loadManifest9({
5212
+ const loaded = await loadManifest10({
4862
5213
  ...options.root ? { root: options.root } : {},
4863
5214
  cwd: manifestCwd,
4864
5215
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4875,13 +5226,13 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
4875
5226
  throw new Error("This CNOS root is already in workspace mode.");
4876
5227
  }
4877
5228
  const cnosRoot = loaded.manifestRoot;
4878
- const baseWorkspaceRoot = path23.join(cnosRoot, "workspaces", "base");
5229
+ const baseWorkspaceRoot = path25.join(cnosRoot, "workspaces", "base");
4879
5230
  if (await exists2(baseWorkspaceRoot)) {
4880
5231
  throw new Error("Cannot enable workspace mode because .cnos/workspaces/base already exists.");
4881
5232
  }
4882
5233
  const moved = [];
4883
5234
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
4884
- if (await moveIfExists(path23.join(cnosRoot, folderName), path23.join(baseWorkspaceRoot, folderName))) {
5235
+ if (await moveIfExists(path25.join(cnosRoot, folderName), path25.join(baseWorkspaceRoot, folderName))) {
4885
5236
  moved.push(folderName);
4886
5237
  }
4887
5238
  }
@@ -4891,19 +5242,19 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
4891
5242
  base: {}
4892
5243
  };
4893
5244
  rawManifest.workspaces = rawWorkspaces;
4894
- await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
5245
+ await writeFile9(path25.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4895
5246
  await updateRootAnchorToWorkspace(packageRoot, "base");
4896
5247
  await updateWorkspaceContext(packageRoot, "base");
4897
- await ensureGitignore(path23.dirname(cnosRoot));
5248
+ await ensureGitignore(path25.dirname(cnosRoot));
4898
5249
  if (options.json) {
4899
5250
  return printJson({
4900
- root: path23.dirname(cnosRoot),
5251
+ root: path25.dirname(cnosRoot),
4901
5252
  workspace: "base",
4902
5253
  moved
4903
5254
  });
4904
5255
  }
4905
5256
  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}`;
5257
+ return `enabled workspace mode at ${displayPath(path25.dirname(cnosRoot), packageRoot)} with base workspace${movedSummary}`;
4907
5258
  }
4908
5259
  async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, options = {}) {
4909
5260
  const cliArgs = [...options.cliArgs ?? []];
@@ -4912,7 +5263,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
4912
5263
  if (cliArgs.length > 0) {
4913
5264
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4914
5265
  }
4915
- const loaded = await loadManifest9({
5266
+ const loaded = await loadManifest10({
4916
5267
  ...options.root ? { root: options.root } : {},
4917
5268
  cwd: manifestCwd,
4918
5269
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4942,15 +5293,15 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
4942
5293
  rawWorkspaces.items = rawItems;
4943
5294
  rawWorkspaces.default = rawWorkspaces.default ?? workspaceId;
4944
5295
  rawManifest.workspaces = rawWorkspaces;
4945
- const workspaceRoot = path23.join(cnosRoot, "workspaces", workspaceId);
5296
+ const workspaceRoot = path25.join(cnosRoot, "workspaces", workspaceId);
4946
5297
  const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
4947
- await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4948
- await ensureGitignore(path23.dirname(cnosRoot));
5298
+ await writeFile9(path25.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
5299
+ await ensureGitignore(path25.dirname(cnosRoot));
4949
5300
  await writeAnchor(packageRoot, cnosRoot, workspaceId);
4950
5301
  await updateWorkspaceContext(packageRoot, workspaceId);
4951
5302
  const result = {
4952
5303
  workspace: workspaceId,
4953
- root: path23.dirname(cnosRoot),
5304
+ root: path25.dirname(cnosRoot),
4954
5305
  packageRoot,
4955
5306
  created
4956
5307
  };
@@ -4966,7 +5317,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
4966
5317
  if (cliArgs.length > 0) {
4967
5318
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4968
5319
  }
4969
- const loaded = await loadManifest9({
5320
+ const loaded = await loadManifest10({
4970
5321
  ...options.root ? { root: options.root } : {},
4971
5322
  cwd: manifestCwd,
4972
5323
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4988,8 +5339,8 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
4988
5339
  delete rawItems[workspaceId];
4989
5340
  rawWorkspaces.items = rawItems;
4990
5341
  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 });
5342
+ await writeFile9(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
5343
+ await rm5(path25.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
4993
5344
  if (options.json) {
4994
5345
  return printJson({
4995
5346
  workspace: workspaceId,
@@ -5001,8 +5352,8 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
5001
5352
  async function runWorkspace(args = [], options = {}) {
5002
5353
  const [action, workspaceArg] = args;
5003
5354
  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());
5355
+ const manifestCwd = path25.resolve(options.root ?? process.cwd());
5356
+ const packageRoot = path25.resolve(consumeOption(baseCliArgs, "--package-root") ?? options.root ?? process.cwd());
5006
5357
  switch (action) {
5007
5358
  case "attach":
5008
5359
  return runAttach(packageRoot, { ...options, cliArgs: baseCliArgs });
@@ -5125,6 +5476,9 @@ async function main(argv) {
5125
5476
  process.stdout.write(`${runVersion()}
5126
5477
  `);
5127
5478
  return;
5479
+ case "ui":
5480
+ await runUi(runtimeOptions);
5481
+ return;
5128
5482
  case "init":
5129
5483
  process.stdout.write(`${await runInit(runtimeOptions)}
5130
5484
  `);