@kitsy/cnos-cli 1.6.1 → 1.8.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 +881 -177
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ var OPTION_KEYS = {
5
5
  "--root": "root",
6
6
  "--workspace": "workspace",
7
7
  "--profile": "profile",
8
- "--global-root": "globalRoot"
8
+ "--global-root": "globalRoot",
9
+ "--cache-ttl": "cacheTtlSeconds"
9
10
  };
10
11
  var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
11
12
  "--format",
@@ -21,7 +22,9 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
21
22
  "--inherit",
22
23
  "--as",
23
24
  "--set",
24
- "--debounce"
25
+ "--debounce",
26
+ "--expr",
27
+ "--extends"
25
28
  ]);
26
29
  var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
27
30
  "--flatten",
@@ -39,7 +42,9 @@ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
39
42
  "--dry-run",
40
43
  "--apply",
41
44
  "--rewrite",
42
- "--signal"
45
+ "--signal",
46
+ "--derive",
47
+ "--onboard-current"
43
48
  ]);
44
49
  function normalizeCommand(argv) {
45
50
  const [command = "doctor", ...rest] = argv;
@@ -104,6 +109,14 @@ function normalizeCommand(argv) {
104
109
  return [command, ...rest];
105
110
  }
106
111
  function setOption(options, key, value) {
112
+ if (key === "cacheTtlSeconds") {
113
+ const parsed = Number(value);
114
+ if (!Number.isFinite(parsed) || parsed < 0) {
115
+ throw new Error(`Invalid value for --cache-ttl: ${value}`);
116
+ }
117
+ options.cacheTtlSeconds = parsed;
118
+ return;
119
+ }
107
120
  options[key] = value;
108
121
  }
109
122
  function parseArgs(argv) {
@@ -253,7 +266,7 @@ function printJson(value) {
253
266
 
254
267
  // src/services/projections.ts
255
268
  import { mkdir, writeFile } from "fs/promises";
256
- import path2 from "path";
269
+ import path3 from "path";
257
270
  import { resolveBrowserData, resolveFrameworkEnv, resolveServerProjection } from "@kitsy/cnos/build";
258
271
  import { stringifyYaml } from "@kitsy/cnos/internal";
259
272
 
@@ -266,6 +279,9 @@ async function createRuntimeService(options = {}) {
266
279
  ...options.workspace ? { workspace: options.workspace } : {},
267
280
  ...options.profile ? { profile: options.profile } : {},
268
281
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {},
282
+ cacheMode: options.cacheMode ?? "runtime",
283
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
284
+ ...options.forceRefresh ? { forceRefresh: true } : {},
269
285
  ...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
270
286
  ...options.secretResolution ? { secretResolution: options.secretResolution } : {},
271
287
  ...typeof options.secretRefreshTtl === "number" ? { secretRefreshTtl: options.secretRefreshTtl } : {},
@@ -273,6 +289,15 @@ async function createRuntimeService(options = {}) {
273
289
  });
274
290
  }
275
291
 
292
+ // src/services/paths.ts
293
+ import path2 from "path";
294
+ function resolveFilesystemBasePath(root, cwd = process.cwd()) {
295
+ if (!root || root.startsWith("git+") || root.startsWith("cnos://")) {
296
+ return path2.resolve(cwd);
297
+ }
298
+ return path2.resolve(root);
299
+ }
300
+
276
301
  // src/services/projections.ts
277
302
  function stringifyScalar(value) {
278
303
  if (value === void 0 || value === null) {
@@ -311,37 +336,62 @@ function formatKeyValueMap(values, format) {
311
336
  }
312
337
  }
313
338
  async function writeProjectionFile(to, output, root = process.cwd()) {
314
- const targetPath = path2.resolve(root, to);
315
- await mkdir(path2.dirname(targetPath), { recursive: true });
339
+ const targetPath = path3.resolve(root, to);
340
+ await mkdir(path3.dirname(targetPath), { recursive: true });
316
341
  await writeFile(targetPath, output, "utf8");
317
342
  return targetPath;
318
343
  }
319
344
  async function buildServerProjectionArtifact(to, options = {}, format = "json") {
320
- const projection = await resolveServerProjection(options);
345
+ const projection = await resolveServerProjection({
346
+ ...options,
347
+ cacheMode: "build"
348
+ });
321
349
  const output = format === "yaml" ? stringifyYaml(projection) : `${JSON.stringify(projection, null, 2)}
322
350
  `;
323
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
351
+ const targetPath = await writeProjectionFile(
352
+ to,
353
+ output,
354
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
355
+ );
324
356
  return { targetPath, output };
325
357
  }
326
358
  async function buildBrowserProjectionArtifact(to, options = {}, format = "json") {
327
- const projection = await resolveBrowserData(options);
359
+ const projection = await resolveBrowserData({
360
+ ...options,
361
+ cacheMode: "build"
362
+ });
328
363
  const output = format === "yaml" ? stringifyYaml(projection) : `${JSON.stringify(projection, null, 2)}
329
364
  `;
330
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
365
+ const targetPath = await writeProjectionFile(
366
+ to,
367
+ output,
368
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
369
+ );
331
370
  return { targetPath, output };
332
371
  }
333
372
  async function buildPublicProjectionArtifact(to, options = {}, format = "dotenv") {
334
373
  const cliArgs = [...options.cliArgs ?? []];
335
374
  const frameworkIndex = cliArgs.indexOf("--framework");
336
375
  const framework = frameworkIndex >= 0 && cliArgs[frameworkIndex + 1] ? cliArgs[frameworkIndex + 1] : "generic";
337
- const env = await resolveFrameworkEnv(options, framework);
376
+ const env = await resolveFrameworkEnv(
377
+ {
378
+ ...options,
379
+ cacheMode: "build"
380
+ },
381
+ framework
382
+ );
338
383
  const output = formatKeyValueMap(env, format);
339
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
384
+ const targetPath = await writeProjectionFile(
385
+ to,
386
+ output,
387
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
388
+ );
340
389
  return { targetPath, output, env };
341
390
  }
342
391
  async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
343
392
  const runtime = await createRuntimeService({
344
393
  ...options,
394
+ cacheMode: "build",
345
395
  cliArgs: [...options.cliArgs ?? []]
346
396
  });
347
397
  const env = runtime.toEnv();
@@ -352,7 +402,11 @@ async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
352
402
  }
353
403
  }
354
404
  const output = formatKeyValueMap(env, format);
355
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
405
+ const targetPath = await writeProjectionFile(
406
+ to,
407
+ output,
408
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
409
+ );
356
410
  return { targetPath, output, env };
357
411
  }
358
412
 
@@ -413,23 +467,208 @@ async function runBuild(subcommand, options = {}) {
413
467
  ...provenanceTarget ? { provenance: provenanceTarget } : {}
414
468
  });
415
469
  }
416
- return `built ${subcommand} artifact at ${displayPath(targetPath, options.root ?? process.cwd())}`;
470
+ return `built ${subcommand} artifact at ${displayPath(
471
+ targetPath,
472
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
473
+ )}`;
417
474
  }
418
475
 
419
- // src/commands/define.ts
476
+ // src/services/cache.ts
477
+ import { readdir, rm, stat } from "fs/promises";
420
478
  import path4 from "path";
479
+ import {
480
+ loadManifest,
481
+ parseGitUri,
482
+ readRemoteRootCacheMetadata,
483
+ resolveCnosCacheRoot,
484
+ resolveRemoteRootCachePaths,
485
+ resolveRootUri
486
+ } from "@kitsy/cnos/internal";
487
+ async function computeDirectorySize(targetPath) {
488
+ try {
489
+ const info = await stat(targetPath);
490
+ if (!info.isDirectory()) {
491
+ return info.size;
492
+ }
493
+ const entries = await readdir(targetPath, { withFileTypes: true });
494
+ const sizes = await Promise.all(
495
+ entries.map((entry) => computeDirectorySize(path4.join(targetPath, entry.name)))
496
+ );
497
+ return sizes.reduce((sum, value) => sum + value, 0);
498
+ } catch {
499
+ return 0;
500
+ }
501
+ }
502
+ async function listCachedRoots(processEnv = process.env) {
503
+ const rootsDir = path4.join(resolveCnosCacheRoot(processEnv), "roots");
504
+ try {
505
+ const entries = await readdir(rootsDir, { withFileTypes: true });
506
+ const records = await Promise.all(
507
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
508
+ const cacheDir = path4.join(rootsDir, entry.name);
509
+ const metadata = await readRemoteRootCacheMetadata(path4.join(cacheDir, ".cnos-cache-meta.json"));
510
+ if (!metadata) {
511
+ return void 0;
512
+ }
513
+ return {
514
+ uri: metadata.uri,
515
+ cacheDir,
516
+ cachedAt: metadata.cachedAt,
517
+ resolvedCommit: metadata.resolvedCommit,
518
+ immutable: metadata.isImmutable,
519
+ ref: metadata.ref,
520
+ subpath: metadata.subpath,
521
+ sizeBytes: await computeDirectorySize(cacheDir)
522
+ };
523
+ })
524
+ );
525
+ return records.filter((record) => Boolean(record)).sort((left, right) => left.uri.localeCompare(right.uri));
526
+ } catch {
527
+ return [];
528
+ }
529
+ }
530
+ async function clearCachedRoots(uri, processEnv = process.env) {
531
+ if (uri) {
532
+ const paths = resolveRemoteRootCachePaths(uri, processEnv);
533
+ await rm(paths.cacheDir, { recursive: true, force: true });
534
+ return { cleared: [uri] };
535
+ }
536
+ const records = await listCachedRoots(processEnv);
537
+ await Promise.all(records.map((record) => rm(record.cacheDir, { recursive: true, force: true })));
538
+ return {
539
+ cleared: records.map((record) => record.uri)
540
+ };
541
+ }
542
+ async function refreshCachedRoots(uri, options = {}) {
543
+ const processEnv = options.processEnv ?? process.env;
544
+ if (uri) {
545
+ const parsed = parseGitUri(uri);
546
+ await resolveRootUri(uri, process.cwd(), {
547
+ processEnv,
548
+ cacheMode: "build",
549
+ forceRefresh: true
550
+ });
551
+ return { refreshed: [parsed.uri] };
552
+ }
553
+ const loadedManifest = await loadManifest({
554
+ ...options.root ? { root: options.root } : {},
555
+ ...options.cwd ? { cwd: options.cwd } : {},
556
+ processEnv
557
+ }).catch(() => void 0);
558
+ if (loadedManifest?.rootResolution.remote && loadedManifest.rootResolution.protocol === "git") {
559
+ await resolveRootUri(loadedManifest.rootResolution.rootUri, loadedManifest.consumerRoot, {
560
+ processEnv,
561
+ cacheMode: "build",
562
+ forceRefresh: true
563
+ });
564
+ return {
565
+ refreshed: [loadedManifest.rootResolution.rootUri]
566
+ };
567
+ }
568
+ const records = await listCachedRoots(processEnv);
569
+ const mutable = records.filter((record) => !record.immutable);
570
+ for (const record of mutable) {
571
+ await resolveRootUri(record.uri, process.cwd(), {
572
+ processEnv,
573
+ cacheMode: "build",
574
+ forceRefresh: true
575
+ });
576
+ }
577
+ return {
578
+ refreshed: mutable.map((record) => record.uri)
579
+ };
580
+ }
581
+
582
+ // src/commands/cache.ts
583
+ function normalizeAction(args) {
584
+ const [action = "list", target] = args;
585
+ if (action === "list" || action === "clear" || action === "refresh") {
586
+ return {
587
+ action,
588
+ ...typeof target === "string" ? { target } : {}
589
+ };
590
+ }
591
+ return {
592
+ action: "list",
593
+ ...typeof args[0] === "string" ? { target: args[0] } : {}
594
+ };
595
+ }
596
+ function formatBytes(sizeBytes) {
597
+ if (sizeBytes < 1024) {
598
+ return `${sizeBytes} B`;
599
+ }
600
+ if (sizeBytes < 1024 * 1024) {
601
+ return `${Math.round(sizeBytes / 1024 * 10) / 10} KB`;
602
+ }
603
+ return `${Math.round(sizeBytes / (1024 * 1024) * 10) / 10} MB`;
604
+ }
605
+ async function runCache(args = [], options = {}) {
606
+ const { action, target } = normalizeAction(args);
607
+ if (action === "clear") {
608
+ const result = await clearCachedRoots(target, options.processEnv ?? process.env);
609
+ return options.json ? printJson(result) : `cleared ${result.cleared.length} cached root(s)`;
610
+ }
611
+ if (action === "refresh") {
612
+ const result = await refreshCachedRoots(target, options);
613
+ return options.json ? printJson(result) : `refreshed ${result.refreshed.length} cached root(s)`;
614
+ }
615
+ const records = await listCachedRoots(options.processEnv ?? process.env);
616
+ if (options.json) {
617
+ return printJson(records);
618
+ }
619
+ if (records.length === 0) {
620
+ return "no cached remote roots";
621
+ }
622
+ return records.map(
623
+ (record) => [
624
+ record.uri,
625
+ ` cached: ${record.cachedAt}`,
626
+ ` commit: ${record.resolvedCommit}`,
627
+ ` immutable: ${record.immutable ? "yes" : "no"}`,
628
+ ` size: ${formatBytes(record.sizeBytes)}`
629
+ ].join("\n")
630
+ ).join("\n\n");
631
+ }
632
+
633
+ // src/commands/define.ts
634
+ import path6 from "path";
421
635
 
422
636
  // src/services/writes.ts
423
637
  import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
424
- import path3 from "path";
638
+ import path5 from "path";
425
639
  import {
426
640
  getNamespaceDefinition,
641
+ normalizeDerivedValue,
642
+ parseDerivation,
427
643
  createSecretVaultProvider,
644
+ validateDerivedTargetNamespace,
645
+ validateParsedDerivation,
428
646
  parseYaml,
429
647
  resolveConfigDocumentPath,
430
648
  resolveVaultAuth,
431
649
  stringifyYaml as stringifyYaml2
432
650
  } from "@kitsy/cnos/internal";
651
+
652
+ // src/services/rootAccess.ts
653
+ import { loadManifest as loadManifest2 } from "@kitsy/cnos/internal";
654
+ async function assertWritableConfigRoot(action, options = {}) {
655
+ const loadedManifest = await loadManifest2({
656
+ ...options.root ? { root: options.root } : {},
657
+ ...options.cwd ? { cwd: options.cwd } : {},
658
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
659
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
660
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
661
+ ...options.forceRefresh ? { forceRefresh: true } : {}
662
+ });
663
+ if (!loadedManifest.rootResolution.readOnly) {
664
+ return;
665
+ }
666
+ throw new Error(
667
+ `Cannot ${action} because the active CNOS root is remote and read-only (${loadedManifest.rootResolution.rootUri}). Clone the config repo and edit it directly.`
668
+ );
669
+ }
670
+
671
+ // src/services/writes.ts
433
672
  function setNestedValue(target, pathSegments, value) {
434
673
  const [head, ...tail] = pathSegments;
435
674
  if (!head) {
@@ -495,6 +734,7 @@ function getSelectedWorkspaceRoot(options, runtime) {
495
734
  return workspaceRoot.path;
496
735
  }
497
736
  async function defineValue(namespace, configPath, rawValue, options = {}) {
737
+ await assertWritableConfigRoot(`write ${namespace}.${configPath}`, options);
498
738
  if (namespace === "secret") {
499
739
  const secret = await setSecret(configPath, rawValue, {
500
740
  ...options,
@@ -527,9 +767,17 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
527
767
  const profile = options.profile ?? runtime.graph.profile;
528
768
  const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
529
769
  const document = await readYamlDocument(filePath);
530
- const parsedValue = parseScalarValue(rawValue);
770
+ let parsedValue;
771
+ if (options.deriveExpression !== void 0) {
772
+ validateDerivedTargetNamespace(runtime.manifest, namespace);
773
+ const derivedValue = normalizeDerivedValue(options.deriveExpression, options.deriveExprMode ?? false);
774
+ validateParsedDerivation(runtime.manifest, parseDerivation(derivedValue));
775
+ parsedValue = derivedValue;
776
+ } else {
777
+ parsedValue = parseScalarValue(rawValue);
778
+ }
531
779
  setNestedValue(document, configPath.split("."), parsedValue);
532
- await mkdir2(path3.dirname(filePath), { recursive: true });
780
+ await mkdir2(path5.dirname(filePath), { recursive: true });
533
781
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
534
782
  return {
535
783
  filePath,
@@ -567,7 +815,7 @@ async function setSecret(configPath, rawValue, options = {}) {
567
815
  };
568
816
  }
569
817
  setNestedValue(document, configPath.split("."), reference);
570
- await mkdir2(path3.dirname(filePath), { recursive: true });
818
+ await mkdir2(path5.dirname(filePath), { recursive: true });
571
819
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
572
820
  return {
573
821
  filePath,
@@ -577,6 +825,7 @@ async function setSecret(configPath, rawValue, options = {}) {
577
825
  };
578
826
  }
579
827
  async function deleteSecret(configPath, options = {}) {
828
+ await assertWritableConfigRoot(`delete secret.${configPath}`, options);
580
829
  const runtime = await createRuntimeService(options);
581
830
  const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
582
831
  const profile = options.profile ?? runtime.graph.profile;
@@ -607,6 +856,7 @@ async function deleteSecret(configPath, options = {}) {
607
856
  };
608
857
  }
609
858
  async function deleteValue(namespace, configPath, options = {}) {
859
+ await assertWritableConfigRoot(`delete ${namespace}.${configPath}`, options);
610
860
  if (namespace === "secret") {
611
861
  return deleteSecret(configPath, options);
612
862
  }
@@ -645,7 +895,7 @@ async function deleteValue(namespace, configPath, options = {}) {
645
895
  // src/commands/define.ts
646
896
  async function runDefine(namespace, configPath, rawValue, options = {}) {
647
897
  const cliArgs = [...options.cliArgs ?? []];
648
- const root = path4.resolve(options.root ?? process.cwd());
898
+ const root = path6.resolve(options.root ?? process.cwd());
649
899
  const target = consumeOption(cliArgs, "--target") ?? "local";
650
900
  const local = consumeFlag(cliArgs, "--local");
651
901
  const remote = consumeFlag(cliArgs, "--remote");
@@ -676,7 +926,7 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
676
926
 
677
927
  // src/services/envMaterialization.ts
678
928
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
679
- import path5 from "path";
929
+ import path7 from "path";
680
930
  function resolveEnvFromRuntime(runtime, cliArgs = []) {
681
931
  const args = [...cliArgs];
682
932
  const isPublic = consumeFlag(args, "--public");
@@ -703,17 +953,21 @@ async function resolveMaterializedEnv(options = {}) {
703
953
  };
704
954
  }
705
955
  function resolveMaterializedEnvTarget(to, root = process.cwd()) {
706
- return path5.resolve(root, to);
956
+ return path7.resolve(root, to);
707
957
  }
708
958
  async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
709
959
  const targetPath = resolveMaterializedEnvTarget(to, root);
710
- await mkdir3(path5.dirname(targetPath), { recursive: true });
960
+ await mkdir3(path7.dirname(targetPath), { recursive: true });
711
961
  await writeFile3(targetPath, output, "utf8");
712
962
  return targetPath;
713
963
  }
714
964
  async function materializeEnvToFile(to, options = {}) {
715
965
  const result = await resolveMaterializedEnv(options);
716
- const targetPath = await writeMaterializedEnvFile(to, result.output, options.root ?? process.cwd());
966
+ const targetPath = await writeMaterializedEnvFile(
967
+ to,
968
+ result.output,
969
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
970
+ );
717
971
  return {
718
972
  ...result,
719
973
  targetPath
@@ -834,6 +1088,7 @@ async function startDevEnvLoop(command, options = {}) {
834
1088
  const writeCurrent = async () => {
835
1089
  await materializeEnvToFile(to, {
836
1090
  ...options,
1091
+ cacheMode: "dev",
837
1092
  cliArgs: [...cliArgs]
838
1093
  });
839
1094
  };
@@ -847,6 +1102,7 @@ async function startDevEnvLoop(command, options = {}) {
847
1102
  }
848
1103
  const watcher = await startGraphWatchLoop({
849
1104
  ...options,
1105
+ cacheMode: "dev",
850
1106
  cliArgs,
851
1107
  debounceMs,
852
1108
  async onChange(payload) {
@@ -903,7 +1159,10 @@ async function runDev(subcommand, command, options = {}) {
903
1159
  };
904
1160
  process.once("SIGINT", closeLoop);
905
1161
  process.once("SIGTERM", closeLoop);
906
- const targetPath = displayPath(to, options.root ?? process.cwd());
1162
+ const targetPath = displayPath(
1163
+ to,
1164
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
1165
+ );
907
1166
  return isSignal ? `watching config changes and rewriting ${targetPath} in signal mode` : `watching config changes, rewriting ${targetPath}, and restarting the child process`;
908
1167
  }
909
1168
 
@@ -956,11 +1215,12 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
956
1215
  }
957
1216
 
958
1217
  // src/services/doctor.ts
959
- import { readdir, readFile as readFile2 } from "fs/promises";
960
- import path6 from "path";
1218
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
1219
+ import path8 from "path";
961
1220
  import {
962
1221
  detectLegacyVaultFormat,
963
1222
  isSecretReference as isSecretReference2,
1223
+ loadManifest as loadManifest3,
964
1224
  parseYaml as parseYaml2,
965
1225
  readKeychain,
966
1226
  resolveSecretStoreRoot
@@ -979,7 +1239,7 @@ async function createValidationSummary(options = {}) {
979
1239
 
980
1240
  // src/services/doctor.ts
981
1241
  async function checkGitignore(root) {
982
- const gitignorePath = path6.join(root, ".gitignore");
1242
+ const gitignorePath = path8.join(root, ".gitignore");
983
1243
  const expected = [
984
1244
  ".cnos/env/.env",
985
1245
  ".cnos/env/.env.*",
@@ -1011,15 +1271,15 @@ function issueSummary(issues) {
1011
1271
  }
1012
1272
  async function collectYamlFiles(root) {
1013
1273
  try {
1014
- const entries = await readdir(root, { withFileTypes: true });
1274
+ const entries = await readdir2(root, { withFileTypes: true });
1015
1275
  const results = [];
1016
1276
  for (const entry of entries) {
1017
- const target = path6.join(root, entry.name);
1277
+ const target = path8.join(root, entry.name);
1018
1278
  if (entry.isDirectory()) {
1019
1279
  results.push(...await collectYamlFiles(target));
1020
1280
  continue;
1021
1281
  }
1022
- if (entry.isFile() && [".yml", ".yaml"].includes(path6.extname(entry.name).toLowerCase())) {
1282
+ if (entry.isFile() && [".yml", ".yaml"].includes(path8.extname(entry.name).toLowerCase())) {
1023
1283
  results.push(target);
1024
1284
  }
1025
1285
  }
@@ -1044,7 +1304,7 @@ async function checkSecretSecurity(options, runtime) {
1044
1304
  );
1045
1305
  const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
1046
1306
  const secretFiles = await Promise.all(
1047
- runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path6.join(root.path, "secrets")))
1307
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path8.join(root.path, "secrets")))
1048
1308
  );
1049
1309
  const plaintextFiles = [];
1050
1310
  for (const file of secretFiles.flat()) {
@@ -1074,7 +1334,15 @@ async function checkSecretSecurity(options, runtime) {
1074
1334
  };
1075
1335
  }
1076
1336
  async function evaluateDoctor(options = {}) {
1077
- const root = path6.resolve(options.root ?? process.cwd());
1337
+ const root = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
1338
+ const loadedManifest = await loadManifest3({
1339
+ ...options.root ? { root: options.root } : {},
1340
+ ...options.cwd ? { cwd: options.cwd } : {},
1341
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
1342
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
1343
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
1344
+ ...options.forceRefresh ? { forceRefresh: true } : {}
1345
+ });
1078
1346
  const { runtime, summary } = await createValidationSummary(options);
1079
1347
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
1080
1348
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
@@ -1090,6 +1358,11 @@ async function evaluateDoctor(options = {}) {
1090
1358
  ok: true,
1091
1359
  details: `${runtime.graph.workspace.workspaceId} via ${runtime.graph.workspace.workspaceSource}`
1092
1360
  },
1361
+ {
1362
+ name: "root",
1363
+ ok: true,
1364
+ details: loadedManifest.rootResolution.remote ? `${loadedManifest.rootResolution.rootUri} -> ${loadedManifest.manifestRoot}${loadedManifest.rootResolution.immutable ? " | immutable" : " | mutable ref"}${loadedManifest.rootResolution.resolvedCommit ? ` | commit ${loadedManifest.rootResolution.resolvedCommit}` : ""}` : loadedManifest.manifestRoot
1365
+ },
1093
1366
  {
1094
1367
  name: "namespaces",
1095
1368
  ok: true,
@@ -1214,7 +1487,10 @@ async function runExportEnv(options = {}) {
1214
1487
  ...framework ? { framework } : {}
1215
1488
  });
1216
1489
  }
1217
- return `Wrote ${Object.keys(result2.env).length} env vars to ${displayPath(result2.targetPath, options.root ?? process.cwd())}`;
1490
+ return `Wrote ${Object.keys(result2.env).length} env vars to ${displayPath(
1491
+ result2.targetPath,
1492
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
1493
+ )}`;
1218
1494
  }
1219
1495
  const result = await resolveMaterializedEnv(baseOptions);
1220
1496
  if (options.json) {
@@ -1235,7 +1511,7 @@ async function runExport(subcommand, options = {}) {
1235
1511
  var GLOBAL_OPTIONS = [
1236
1512
  {
1237
1513
  flag: "--root <path>",
1238
- description: "Resolve the CNOS project from a specific filesystem root."
1514
+ description: "Resolve the CNOS project from a specific filesystem root or remote root URI."
1239
1515
  },
1240
1516
  {
1241
1517
  flag: "--workspace <id>",
@@ -1249,6 +1525,10 @@ var GLOBAL_OPTIONS = [
1249
1525
  flag: "--global-root <path>",
1250
1526
  description: "Override the configured global CNOS root used for workspace layering."
1251
1527
  },
1528
+ {
1529
+ flag: "--cache-ttl <seconds>",
1530
+ description: "Override the remote-root cache TTL for mutable refs during this invocation."
1531
+ },
1252
1532
  {
1253
1533
  flag: "--json",
1254
1534
  description: "Emit JSON output for commands that support structured responses."
@@ -1263,6 +1543,39 @@ var GLOBAL_OPTIONS = [
1263
1543
  }
1264
1544
  ];
1265
1545
  var COMMANDS = [
1546
+ {
1547
+ id: "cache",
1548
+ summary: "Inspect and manage cached remote roots.",
1549
+ usage: "cnos cache [list|clear|refresh] [root-uri] [global-options]",
1550
+ description: "Lists cached git-backed remote roots, clears cache entries, or forces a refresh for mutable refs.",
1551
+ examples: [
1552
+ "cnos cache list",
1553
+ "cnos cache clear",
1554
+ "cnos cache clear git+https://github.com/org/config.git#v2.1.0",
1555
+ "cnos cache refresh"
1556
+ ]
1557
+ },
1558
+ {
1559
+ id: "cache list",
1560
+ summary: "List cached remote roots.",
1561
+ usage: "cnos cache list [global-options]",
1562
+ description: "Lists git-backed remote roots cached under ~/.cnos/cache together with cache time, resolved commit, immutability, and size.",
1563
+ examples: ["cnos cache list"]
1564
+ },
1565
+ {
1566
+ id: "cache clear",
1567
+ summary: "Clear cached remote roots.",
1568
+ usage: "cnos cache clear [root-uri] [global-options]",
1569
+ description: "Removes all cached remote roots by default, or clears one specific cached root when a full remote URI is provided.",
1570
+ examples: ["cnos cache clear", "cnos cache clear git+https://github.com/org/config.git#main"]
1571
+ },
1572
+ {
1573
+ id: "cache refresh",
1574
+ summary: "Force refresh mutable cached remote roots.",
1575
+ usage: "cnos cache refresh [root-uri] [global-options]",
1576
+ description: "Re-fetches a specific git-backed remote root, or refreshes the active remote root / all mutable cached roots when no URI is provided.",
1577
+ examples: ["cnos cache refresh", "cnos cache refresh git+ssh://git@github.com/org/config.git#main"]
1578
+ },
1266
1579
  {
1267
1580
  id: "init",
1268
1581
  summary: "Scaffold a workspace-aware CNOS tree in the current project.",
@@ -1330,6 +1643,14 @@ var COMMANDS = [
1330
1643
  flag: "--target <local|global>",
1331
1644
  description: "Choose whether writes land in the local project workspace or the configured global root."
1332
1645
  },
1646
+ {
1647
+ flag: "--derive",
1648
+ description: "Write a derived value instead of a literal. Use the second positional value as a template, or combine with --expr."
1649
+ },
1650
+ {
1651
+ flag: "--expr <expression>",
1652
+ description: "With --derive, write an expression-form derived value instead of template shorthand."
1653
+ },
1333
1654
  {
1334
1655
  flag: "--prefix <path>",
1335
1656
  description: "Filter value list output to keys that begin with this logical path or key prefix."
@@ -1338,6 +1659,8 @@ var COMMANDS = [
1338
1659
  examples: [
1339
1660
  "cnos value app.name",
1340
1661
  "cnos value set server.port 3000",
1662
+ "cnos value set app.origin --derive '${value.app.protocol}://${value.app.host}'",
1663
+ `cnos value set app.display_name --derive --expr "coalesce(value.app.custom_name, value.app.name, 'Unnamed')"`,
1341
1664
  "cnos add value app.name demo",
1342
1665
  "cnos value list --prefix app."
1343
1666
  ]
@@ -1345,9 +1668,28 @@ var COMMANDS = [
1345
1668
  {
1346
1669
  id: "value set",
1347
1670
  summary: "Write a value.",
1348
- usage: "cnos value set <path> <value> [--target <local|global>] [global-options]",
1349
- description: "Writes a deterministic value document into the selected workspace or explicit global target.",
1350
- examples: ["cnos value set app.name demo", "cnos add value server.port 3000 --target global"]
1671
+ usage: "cnos value set <path> <value> [--target <local|global>] [--derive] [--expr <expression>] [global-options]",
1672
+ description: "Writes either a literal value or a first-class derived value into the selected workspace or explicit global target.",
1673
+ options: [
1674
+ {
1675
+ flag: "--target <local|global>",
1676
+ description: "Choose whether writes land in the local project workspace or the configured global root."
1677
+ },
1678
+ {
1679
+ flag: "--derive",
1680
+ description: "Interpret the provided value as a derived template, or combine with --expr for expression syntax."
1681
+ },
1682
+ {
1683
+ flag: "--expr <expression>",
1684
+ description: "With --derive, store the value as an expression-form derivation instead of template shorthand."
1685
+ }
1686
+ ],
1687
+ examples: [
1688
+ "cnos value set app.name demo",
1689
+ "cnos value set app.origin --derive '${value.app.protocol}://${value.app.host}'",
1690
+ `cnos value set app.display_name --derive --expr "coalesce(value.app.custom_name, value.app.name, 'Unnamed')"`,
1691
+ "cnos add value server.port 3000 --target global"
1692
+ ]
1351
1693
  },
1352
1694
  {
1353
1695
  id: "value list",
@@ -1520,7 +1862,7 @@ var COMMANDS = [
1520
1862
  id: "list",
1521
1863
  summary: "List resolved config entries.",
1522
1864
  usage: "cnos list [<namespace>|all] [--prefix <path>] [--framework <name>] [global-options]",
1523
- description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering. Custom data namespaces such as flags are supported, and process exposes server-only ambient runtime state.",
1865
+ description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering. Derived values are annotated with `(derived)`. Custom data namespaces such as flags are supported, and process exposes server-only ambient runtime state.",
1524
1866
  options: [
1525
1867
  {
1526
1868
  flag: "--namespace <name>",
@@ -1646,7 +1988,7 @@ var COMMANDS = [
1646
1988
  id: "inspect",
1647
1989
  summary: "Inspect the winning value and provenance for a key.",
1648
1990
  usage: "cnos inspect <key> [global-options]",
1649
- description: "Shows the resolved value, namespace, active profile, workspace context, and the loader/origin that won precedence.",
1991
+ description: "Shows the resolved value, namespace, active profile, workspace context, loader/origin, and derived-expression metadata when the key is computed from other CNOS keys or runtime namespaces.",
1650
1992
  arguments: [
1651
1993
  {
1652
1994
  name: "key",
@@ -1874,10 +2216,48 @@ var COMMANDS = [
1874
2216
  },
1875
2217
  {
1876
2218
  id: "workspace",
1877
- summary: "Attach or detach package-local workspace config from a parent CNOS root.",
1878
- usage: "cnos workspace <attach|detach> [options] [global-options]",
1879
- description: "Detaches a child package into a standalone .cnos root or reattaches a detached package back into a parent workspace.",
1880
- examples: ["cnos workspace detach", "cnos workspace attach --package-root apps/travel"]
2219
+ summary: "Manage workspace creation, listing, migration, and attach/detach flows.",
2220
+ usage: "cnos workspace <add|list|remove|scaffold|attach|detach> [options] [global-options]",
2221
+ description: "Adds and removes manifest workspaces, scaffolds package anchors, migrates single-root projects into workspace mode, and handles detach/attach flows for independent child packages.",
2222
+ examples: [
2223
+ "cnos workspace list",
2224
+ "cnos workspace add travel --package-root apps/travel --extends base",
2225
+ "cnos workspace add main --onboard-current",
2226
+ "cnos workspace remove gallery",
2227
+ "cnos workspace detach --package-root apps/travel"
2228
+ ]
2229
+ },
2230
+ {
2231
+ id: "workspace add",
2232
+ summary: "Add a workspace to the manifest and scaffold its on-disk layout.",
2233
+ usage: "cnos workspace add <id> [--package-root <path>] [--extends <workspace>] [--onboard-current] [--force] [global-options]",
2234
+ description: "Creates .cnos/workspaces/<id>, updates cnos.yml, writes a .cnosrc.yml anchor at the selected package root, and optionally migrates an existing single-root .cnos tree into workspace mode with --onboard-current.",
2235
+ examples: [
2236
+ "cnos workspace add travel --package-root apps/travel --extends base",
2237
+ "cnos workspace add insights --package-root apps/insights",
2238
+ "cnos workspace add main --onboard-current"
2239
+ ]
2240
+ },
2241
+ {
2242
+ id: "workspace scaffold",
2243
+ summary: "Scaffold a workspace and anchor without changing other runtime flows.",
2244
+ usage: "cnos workspace scaffold <id> [--package-root <path>] [--extends <workspace>] [--force] [global-options]",
2245
+ description: "Creates the workspace manifest entry, workspace folders, and package anchor for a new app or package. This is an alias-oriented workflow for teams that prefer scaffold wording over add.",
2246
+ examples: ["cnos workspace scaffold gallery --package-root apps/gallery --extends base"]
2247
+ },
2248
+ {
2249
+ id: "workspace list",
2250
+ summary: "List declared workspaces and their inheritance.",
2251
+ usage: "cnos workspace list [global-options]",
2252
+ description: "Shows the declared workspace ids, default workspace, and extends relationships from cnos.yml.",
2253
+ examples: ["cnos workspace list", "cnos workspace list --json"]
2254
+ },
2255
+ {
2256
+ id: "workspace remove",
2257
+ summary: "Remove a workspace from the manifest and delete its local workspace tree.",
2258
+ usage: "cnos workspace remove <id> [global-options]",
2259
+ description: "Deletes .cnos/workspaces/<id> and removes the workspace entry from cnos.yml. CNOS refuses to remove the current default workspace until you change workspaces.default.",
2260
+ examples: ["cnos workspace remove gallery", "cnos workspace remove insights --json"]
1881
2261
  },
1882
2262
  {
1883
2263
  id: "workspace detach",
@@ -2050,6 +2430,7 @@ var HELP_DOCUMENT = {
2050
2430
  examples: [
2051
2431
  "cnos use --profile stage",
2052
2432
  "cnos doctor --workspace api",
2433
+ "cnos cache list",
2053
2434
  "cnos build env --profile stage --to .env.stage",
2054
2435
  "cnos dev env --profile local --to .env.local -- pnpm dev",
2055
2436
  "cnos export env --public --framework vite",
@@ -2165,11 +2546,11 @@ function runHelpAi(topic, cliArgs = []) {
2165
2546
  }
2166
2547
 
2167
2548
  // src/commands/init.ts
2168
- import path8 from "path";
2549
+ import path10 from "path";
2169
2550
 
2170
2551
  // src/services/scaffold.ts
2171
2552
  import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2172
- import path7 from "path";
2553
+ import path9 from "path";
2173
2554
  function scaffoldManifest(projectName, workspace) {
2174
2555
  const lines = [
2175
2556
  "version: 1",
@@ -2208,7 +2589,7 @@ async function ensureFile(filePath, content) {
2208
2589
  }
2209
2590
  }
2210
2591
  async function ensureGitignore(root) {
2211
- const gitignorePath = path7.join(root, ".gitignore");
2592
+ const gitignorePath = path9.join(root, ".gitignore");
2212
2593
  const requiredEntries = [
2213
2594
  ".cnos/env/.env",
2214
2595
  ".cnos/env/.env.*",
@@ -2235,14 +2616,13 @@ async function ensureGitignore(root) {
2235
2616
  `, "utf8");
2236
2617
  return true;
2237
2618
  }
2238
- async function scaffoldWorkspace(root, workspace) {
2239
- const cnosRoot = path7.join(root, ".cnos");
2240
- const workspaceRoot = workspace ? path7.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2619
+ async function ensureWorkspaceLayout(cnosRoot, workspace) {
2620
+ const workspaceRoot = workspace ? path9.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2241
2621
  const createdPaths = [];
2242
- await mkdir4(path7.join(workspaceRoot, "profiles"), { recursive: true });
2243
- await mkdir4(path7.join(workspaceRoot, "values"), { recursive: true });
2244
- await mkdir4(path7.join(workspaceRoot, "secrets"), { recursive: true });
2245
- await mkdir4(path7.join(workspaceRoot, "env"), { recursive: true });
2622
+ await mkdir4(path9.join(workspaceRoot, "profiles"), { recursive: true });
2623
+ await mkdir4(path9.join(workspaceRoot, "values"), { recursive: true });
2624
+ await mkdir4(path9.join(workspaceRoot, "secrets"), { recursive: true });
2625
+ await mkdir4(path9.join(workspaceRoot, "env"), { recursive: true });
2246
2626
  const relativePaths = workspace ? [
2247
2627
  ["workspaces", workspace, "profiles", ".gitkeep"],
2248
2628
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -2255,23 +2635,33 @@ async function scaffoldWorkspace(root, workspace) {
2255
2635
  ["env", ".gitkeep"]
2256
2636
  ];
2257
2637
  for (const relativePath of relativePaths) {
2258
- const filePath = path7.join(cnosRoot, ...relativePath);
2638
+ const filePath = path9.join(cnosRoot, ...relativePath);
2259
2639
  if (await ensureFile(filePath, "")) {
2260
- createdPaths.push(path7.relative(root, filePath).replace(/\\/g, "/"));
2640
+ createdPaths.push(path9.relative(path9.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
2261
2641
  }
2262
2642
  }
2263
- if (await ensureFile(path7.join(cnosRoot, "cnos.yml"), scaffoldManifest(path7.basename(root), workspace))) {
2264
- createdPaths.push(".cnos/cnos.yml");
2265
- }
2266
- if (await ensureFile(
2267
- path7.join(root, ".cnosrc.yml"),
2643
+ return createdPaths;
2644
+ }
2645
+ async function ensureCnosrc(root, workspace) {
2646
+ return ensureFile(
2647
+ path9.join(root, ".cnosrc.yml"),
2268
2648
  workspace ? `root: ./.cnos
2269
2649
  workspace: ${workspace}
2270
2650
  ` : "root: ./.cnos\n"
2271
- )) {
2651
+ );
2652
+ }
2653
+ async function scaffoldWorkspace(root, workspace) {
2654
+ const cnosRoot = path9.join(root, ".cnos");
2655
+ const createdPaths = (await ensureWorkspaceLayout(cnosRoot, workspace)).map(
2656
+ (entry) => entry.replace(/^\.cnos\//, ".cnos/")
2657
+ );
2658
+ if (await ensureFile(path9.join(cnosRoot, "cnos.yml"), scaffoldManifest(path9.basename(root), workspace))) {
2659
+ createdPaths.push(".cnos/cnos.yml");
2660
+ }
2661
+ if (await ensureCnosrc(root, workspace)) {
2272
2662
  createdPaths.push(".cnosrc.yml");
2273
2663
  }
2274
- if (workspace && await ensureFile(path7.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
2664
+ if (workspace && await ensureFile(path9.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
2275
2665
  globalRoot: ~/.cnos
2276
2666
  `)) {
2277
2667
  createdPaths.push(".cnos-workspace.yml");
@@ -2288,7 +2678,7 @@ globalRoot: ~/.cnos
2288
2678
 
2289
2679
  // src/commands/init.ts
2290
2680
  async function runInit(options = {}) {
2291
- const root = path8.resolve(options.root ?? process.cwd());
2681
+ const root = path10.resolve(options.root ?? process.cwd());
2292
2682
  const result = await scaffoldWorkspace(root, options.workspace);
2293
2683
  if (options.json) {
2294
2684
  return printJson(result);
@@ -2324,6 +2714,22 @@ function printInspect(record) {
2324
2714
  `overridden: ${record.overridden.map((entry) => `${entry.sourceId}@${entry.workspaceId}=${String(entry.value)}`).join(", ")}`
2325
2715
  );
2326
2716
  }
2717
+ if (record.derived) {
2718
+ lines.push(`derivedType: ${record.derived.type}`);
2719
+ lines.push(`derivedExpression: ${record.derived.expression}`);
2720
+ lines.push(`runtimeDependent: ${record.derived.runtimeDependent ? "yes" : "no"}`);
2721
+ if (record.derived.runtimeNamespaces.length > 0) {
2722
+ lines.push(`runtimeNamespaces: ${record.derived.runtimeNamespaces.join(", ")}`);
2723
+ }
2724
+ if (record.derived.dependencies.length > 0) {
2725
+ lines.push(
2726
+ `dependencies: ${record.derived.dependencies.map((entry) => `${entry.key}=${String(entry.value)}${entry.runtimeNamespace ? ` (${entry.runtimeNamespace})` : ""}`).join(", ")}`
2727
+ );
2728
+ }
2729
+ if (record.derived.promotionWarning) {
2730
+ lines.push(`warning: ${record.derived.promotionWarning}`);
2731
+ }
2732
+ }
2327
2733
  return lines.join("\n");
2328
2734
  }
2329
2735
 
@@ -2391,13 +2797,24 @@ function toStoredEntry(namespace, entry, filter = {}) {
2391
2797
  }
2392
2798
  return {
2393
2799
  key: entry.key,
2394
- value: selectedCandidate.value
2800
+ value: selectedCandidate.value,
2801
+ ...typeof selectedCandidate.value === "object" && selectedCandidate.value !== null && !Array.isArray(selectedCandidate.value) && "$derive" in selectedCandidate.value ? {
2802
+ derived: true
2803
+ } : {}
2395
2804
  };
2396
2805
  }
2397
- function listStoredNamespace(namespace, options) {
2398
- return createRuntimeService(options).then(
2399
- (runtime) => Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === namespace).map((entry) => toStoredEntry(namespace, entry, options)).filter((entry) => Boolean(entry)).filter((entry) => matchesPrefix(entry.key, options.prefix)).sort((left, right) => left.key.localeCompare(right.key))
2400
- );
2806
+ async function listStoredNamespace(namespace, options) {
2807
+ const runtime = await createRuntimeService(options);
2808
+ return Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === namespace).map((entry) => {
2809
+ const stored = toStoredEntry(namespace, entry, options);
2810
+ if (!stored) {
2811
+ return void 0;
2812
+ }
2813
+ return {
2814
+ ...stored,
2815
+ value: stored.derived ? runtime.read(entry.key) : stored.value
2816
+ };
2817
+ }).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));
2401
2818
  }
2402
2819
  function listProjectedNamespace(namespace, options) {
2403
2820
  return createRuntimeService(options).then((runtime) => {
@@ -2479,14 +2896,14 @@ async function runList(args = [], options = {}) {
2479
2896
  if (entries.length === 0) {
2480
2897
  return "";
2481
2898
  }
2482
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
2899
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
2483
2900
  }
2484
2901
 
2485
2902
  // src/commands/migrate.ts
2486
- import path9 from "path";
2903
+ import path11 from "path";
2487
2904
  import {
2488
2905
  applyManifestMappings,
2489
- loadManifest,
2906
+ loadManifest as loadManifest4,
2490
2907
  proposeMapping,
2491
2908
  rewriteSourceFiles,
2492
2909
  scanEnvUsage
@@ -2500,8 +2917,18 @@ async function runMigrate(options = {}) {
2500
2917
  if (cliArgs.length > 0) {
2501
2918
  throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
2502
2919
  }
2503
- const manifest = await loadManifest(options.root ? { root: options.root } : {});
2504
- const scanRoot = path9.resolve(manifest.repoRoot, scan ?? "src");
2920
+ if (apply) {
2921
+ await assertWritableConfigRoot("apply migration mappings", options);
2922
+ }
2923
+ const manifest = await loadManifest4({
2924
+ ...options.root ? { root: options.root } : {},
2925
+ ...options.cwd ? { cwd: options.cwd } : {},
2926
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
2927
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
2928
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
2929
+ ...options.forceRefresh ? { forceRefresh: true } : {}
2930
+ });
2931
+ const scanRoot = path11.resolve(manifest.consumerRoot, scan ?? "src");
2505
2932
  const usages = await scanEnvUsage(scanRoot);
2506
2933
  const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
2507
2934
  const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
@@ -2563,7 +2990,7 @@ async function runMigrate(options = {}) {
2563
2990
  }
2564
2991
 
2565
2992
  // src/commands/namespace.ts
2566
- import path10 from "path";
2993
+ import path12 from "path";
2567
2994
  function normalizeCommand2(args) {
2568
2995
  const [actionOrPath, ...tail] = args;
2569
2996
  if (!actionOrPath) {
@@ -2586,7 +3013,7 @@ function normalizeCommand2(args) {
2586
3013
  async function runNamespace(namespace, args = [], options = {}) {
2587
3014
  const { action, tail } = normalizeCommand2(args);
2588
3015
  const cliArgs = [...options.cliArgs ?? []];
2589
- const root = path10.resolve(options.root ?? process.cwd());
3016
+ const root = path12.resolve(options.root ?? process.cwd());
2590
3017
  if (action === "list") {
2591
3018
  const prefix = consumeOption(cliArgs, "--prefix");
2592
3019
  const entries = await listConfigEntries(namespace, {
@@ -2597,16 +3024,24 @@ async function runNamespace(namespace, args = [], options = {}) {
2597
3024
  if (options.json) {
2598
3025
  return printJson(entries);
2599
3026
  }
2600
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
3027
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
2601
3028
  }
2602
3029
  if (action === "set") {
2603
3030
  const configPath2 = tail[0] ?? "app.name";
2604
- const rawValue = tail[1] ?? "";
3031
+ const derive = consumeFlag(cliArgs, "--derive");
3032
+ const expr = consumeOption(cliArgs, "--expr");
3033
+ const deriveArg = derive && !expr && cliArgs[0] && !cliArgs[0].startsWith("--") ? cliArgs.shift() : void 0;
3034
+ const rawValue = derive ? "" : tail[1] ?? "";
3035
+ const deriveExpression = derive ? expr ?? tail[1] ?? deriveArg ?? "" : void 0;
2605
3036
  const target = consumeOption(cliArgs, "--target") ?? "local";
2606
3037
  const result = await defineValue(namespace, configPath2, rawValue, {
2607
3038
  ...options,
2608
3039
  cliArgs,
2609
- target
3040
+ target,
3041
+ ...deriveExpression !== void 0 ? {
3042
+ deriveExpression,
3043
+ deriveExprMode: Boolean(expr)
3044
+ } : {}
2610
3045
  });
2611
3046
  if (options.json) {
2612
3047
  return printJson({
@@ -2648,34 +3083,34 @@ async function runNamespace(namespace, args = [], options = {}) {
2648
3083
  }
2649
3084
 
2650
3085
  // src/commands/onboard.ts
2651
- import { copyFile, readdir as readdir2, rm } from "fs/promises";
2652
- import path11 from "path";
3086
+ import { copyFile, readdir as readdir3, rm as rm2 } from "fs/promises";
3087
+ import path13 from "path";
2653
3088
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
2654
3089
  async function listRootEnvFiles(root) {
2655
- const entries = await readdir2(root, { withFileTypes: true });
3090
+ const entries = await readdir3(root, { withFileTypes: true });
2656
3091
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
2657
3092
  }
2658
3093
  async function runOnboard(options = {}) {
2659
- const root = path11.resolve(options.root ?? process.cwd());
2660
- const workspace = options.workspace ?? path11.basename(root);
3094
+ const root = path13.resolve(options.root ?? process.cwd());
3095
+ const workspace = options.workspace ?? path13.basename(root);
2661
3096
  const cliArgs = [...options.cliArgs ?? []];
2662
3097
  const move = consumeFlag(cliArgs, "--move");
2663
3098
  if (cliArgs.length > 0) {
2664
3099
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
2665
3100
  }
2666
3101
  const scaffold = await scaffoldWorkspace(root, workspace);
2667
- const envRoot = path11.join(root, ".cnos", "workspaces", workspace, "env");
3102
+ const envRoot = path13.join(root, ".cnos", "workspaces", workspace, "env");
2668
3103
  const rootFiles = await listRootEnvFiles(root);
2669
3104
  const imported = [];
2670
3105
  const skipped = [];
2671
3106
  for (const fileName of rootFiles) {
2672
- const sourcePath = path11.join(root, fileName);
2673
- const targetPath = path11.join(envRoot, fileName);
3107
+ const sourcePath = path13.join(root, fileName);
3108
+ const targetPath = path13.join(envRoot, fileName);
2674
3109
  try {
2675
3110
  await copyFile(sourcePath, targetPath);
2676
- imported.push(path11.relative(root, targetPath).replace(/\\/g, "/"));
3111
+ imported.push(path13.relative(root, targetPath).replace(/\\/g, "/"));
2677
3112
  if (move) {
2678
- await rm(sourcePath);
3113
+ await rm2(sourcePath);
2679
3114
  }
2680
3115
  } catch {
2681
3116
  skipped.push(fileName);
@@ -2698,14 +3133,14 @@ async function runOnboard(options = {}) {
2698
3133
  }
2699
3134
 
2700
3135
  // src/commands/profile.ts
2701
- import path14 from "path";
3136
+ import path16 from "path";
2702
3137
 
2703
3138
  // src/services/context.ts
2704
3139
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
2705
- import path12 from "path";
3140
+ import path14 from "path";
2706
3141
  import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2707
3142
  async function loadCliContext(root = process.cwd()) {
2708
- const filePath = path12.join(path12.resolve(root), ".cnos-workspace.yml");
3143
+ const filePath = path14.join(path14.resolve(root), ".cnos-workspace.yml");
2709
3144
  try {
2710
3145
  const source = await readFile4(filePath, "utf8");
2711
3146
  const parsed = parseYaml3(source);
@@ -2718,8 +3153,8 @@ async function loadCliContext(root = process.cwd()) {
2718
3153
  }
2719
3154
  }
2720
3155
  async function saveCliContext(options = {}) {
2721
- const root = path12.resolve(options.root ?? process.cwd());
2722
- const filePath = path12.join(root, ".cnos-workspace.yml");
3156
+ const root = path14.resolve(options.root ?? process.cwd());
3157
+ const filePath = path14.join(root, ".cnos-workspace.yml");
2723
3158
  const current = await loadCliContext(root);
2724
3159
  const next = {
2725
3160
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -2737,12 +3172,21 @@ async function saveCliContext(options = {}) {
2737
3172
  }
2738
3173
 
2739
3174
  // src/services/profiles.ts
2740
- import { mkdir as mkdir5, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "fs/promises";
2741
- import path13 from "path";
2742
- import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
3175
+ import { mkdir as mkdir5, readdir as readdir4, readFile as readFile5, rm as rm3, writeFile as writeFile6 } from "fs/promises";
3176
+ import path15 from "path";
3177
+ import { loadManifest as loadManifest5, parseYaml as parseYaml4, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
3178
+ async function resolveProfilesRoot(root = process.cwd()) {
3179
+ try {
3180
+ const loadedManifest = await loadManifest5({ root });
3181
+ return path15.join(loadedManifest.manifestRoot, "profiles");
3182
+ } catch {
3183
+ const loadedManifest = await loadManifest5({ cwd: root });
3184
+ return path15.join(loadedManifest.manifestRoot, "profiles");
3185
+ }
3186
+ }
2743
3187
  async function createProfileDefinition(root = process.cwd(), profile, inherit, options = {}) {
2744
- const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2745
- await mkdir5(path13.dirname(filePath), { recursive: true });
3188
+ const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3189
+ await mkdir5(path15.dirname(filePath), { recursive: true });
2746
3190
  const document = options.noInherit ? {
2747
3191
  name: profile,
2748
3192
  activate: {
@@ -2765,9 +3209,9 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit, o
2765
3209
  };
2766
3210
  }
2767
3211
  async function listProfiles(root = process.cwd()) {
2768
- const profilesRoot = path13.join(path13.resolve(root), ".cnos", "profiles");
3212
+ const profilesRoot = await resolveProfilesRoot(root);
2769
3213
  try {
2770
- const entries = await readdir3(profilesRoot, { withFileTypes: true });
3214
+ const entries = await readdir4(profilesRoot, { withFileTypes: true });
2771
3215
  const discovered = /* @__PURE__ */ new Set(["base"]);
2772
3216
  for (const entry of entries) {
2773
3217
  if (entry.isFile() && entry.name.endsWith(".yml")) {
@@ -2780,9 +3224,9 @@ async function listProfiles(root = process.cwd()) {
2780
3224
  }
2781
3225
  }
2782
3226
  async function deleteProfileDefinition(root = process.cwd(), profile) {
2783
- const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
3227
+ const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
2784
3228
  try {
2785
- await rm2(filePath);
3229
+ await rm3(filePath);
2786
3230
  return {
2787
3231
  filePath,
2788
3232
  deleted: true
@@ -2800,7 +3244,7 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
2800
3244
  name: "base"
2801
3245
  };
2802
3246
  }
2803
- const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
3247
+ const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
2804
3248
  try {
2805
3249
  return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
2806
3250
  } catch {
@@ -2824,9 +3268,11 @@ function normalizeProfileAction(args) {
2824
3268
  }
2825
3269
  async function runProfile(args, options = {}) {
2826
3270
  const { action, tail } = normalizeProfileAction(args);
2827
- const root = path14.resolve(options.root ?? process.cwd());
3271
+ const root = options.root ?? process.cwd();
3272
+ const displayRoot = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
2828
3273
  const cliArgs = [...options.cliArgs ?? []];
2829
3274
  if (action === "create") {
3275
+ await assertWritableConfigRoot(`create profile ${tail[0] ?? "stage"}`, options);
2830
3276
  const profile = tail[0] ?? "stage";
2831
3277
  const inherit = consumeOption(cliArgs, "--inherit");
2832
3278
  const noInherit = consumeFlag(cliArgs, "--no-inherit");
@@ -2838,22 +3284,23 @@ async function runProfile(args, options = {}) {
2838
3284
  return printJson(result);
2839
3285
  }
2840
3286
  if (noInherit) {
2841
- return `created profile ${profile} at ${displayPath(result.filePath, root)} without inheriting base`;
3287
+ return `created profile ${profile} at ${displayPath(result.filePath, displayRoot)} without inheriting base`;
2842
3288
  }
2843
- return `created profile ${profile} at ${displayPath(result.filePath, root)}; inherits values from base by default`;
3289
+ return `created profile ${profile} at ${displayPath(result.filePath, displayRoot)}; inherits values from base by default`;
2844
3290
  }
2845
3291
  if (action === "use") {
2846
3292
  const profile = tail[0] ?? "base";
2847
3293
  const result = await saveCliContext({
2848
- root,
3294
+ root: path16.resolve(root),
2849
3295
  profile
2850
3296
  });
2851
3297
  if (options.json) {
2852
3298
  return printJson(result);
2853
3299
  }
2854
- return `active profile set to ${profile} in ${displayPath(result.filePath, root)}`;
3300
+ return `active profile set to ${profile} in ${displayPath(result.filePath, displayRoot)}`;
2855
3301
  }
2856
3302
  if (action === "delete") {
3303
+ await assertWritableConfigRoot(`delete profile ${tail[0] ?? "base"}`, options);
2857
3304
  const profile = tail[0] ?? "base";
2858
3305
  const result = await deleteProfileDefinition(root, profile);
2859
3306
  if (options.json) {
@@ -2875,11 +3322,11 @@ async function runProfile(args, options = {}) {
2875
3322
  }
2876
3323
 
2877
3324
  // src/commands/promote.ts
2878
- import path15 from "path";
3325
+ import path17 from "path";
2879
3326
  import { writeFile as writeFile7 } from "fs/promises";
2880
3327
  import {
2881
3328
  ensureProjectionAllowed,
2882
- loadManifest as loadManifest2,
3329
+ loadManifest as loadManifest6,
2883
3330
  stringifyYaml as stringifyYaml5
2884
3331
  } from "@kitsy/cnos/internal";
2885
3332
  function normalizeTarget(value) {
@@ -2892,7 +3339,7 @@ function sortRecord(record) {
2892
3339
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
2893
3340
  }
2894
3341
  async function runPromote(args = [], options = {}) {
2895
- const root = path15.resolve(options.root ?? process.cwd());
3342
+ const root = path17.resolve(options.root ?? process.cwd());
2896
3343
  const cliArgs = [...options.cliArgs ?? []];
2897
3344
  const target = normalizeTarget(consumeOption(cliArgs, "--to"));
2898
3345
  const alias = consumeOption(cliArgs, "--as");
@@ -2908,7 +3355,15 @@ async function runPromote(args = [], options = {}) {
2908
3355
  throw new Error("promote --to env requires --as <ENV_VAR>");
2909
3356
  }
2910
3357
  }
2911
- const loadedManifest = await loadManifest2(options.root ? { root: options.root } : {});
3358
+ const loadedManifest = await loadManifest6({
3359
+ ...options.root ? { root: options.root } : {},
3360
+ ...options.cwd ? { cwd: options.cwd } : {},
3361
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3362
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3363
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3364
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3365
+ });
3366
+ await assertWritableConfigRoot(`promote ${keys.join(", ")}`, options);
2912
3367
  for (const key of keys) {
2913
3368
  ensureProjectionAllowed(loadedManifest.manifest, key, target);
2914
3369
  }
@@ -3053,21 +3508,21 @@ async function runCommand(command, options = {}) {
3053
3508
  }
3054
3509
 
3055
3510
  // src/commands/secret.ts
3056
- import path18 from "path";
3511
+ import path20 from "path";
3057
3512
 
3058
3513
  // src/commands/vault.ts
3059
- import path17 from "path";
3514
+ import path19 from "path";
3060
3515
 
3061
3516
  // src/services/vaults.ts
3062
- import { rm as rm3, writeFile as writeFile8 } from "fs/promises";
3063
- import path16 from "path";
3517
+ import { rm as rm4, writeFile as writeFile8 } from "fs/promises";
3518
+ import path18 from "path";
3064
3519
  import {
3065
3520
  clearAllVaultSessionKeys,
3066
3521
  clearVaultSessionKey,
3067
3522
  createSecretVault,
3068
3523
  deriveVaultKey,
3069
3524
  listLocalSecrets,
3070
- loadManifest as loadManifest3,
3525
+ loadManifest as loadManifest7,
3071
3526
  listSecretVaults,
3072
3527
  readVaultMetadata,
3073
3528
  resolveSecretStoreRoot as resolveSecretStoreRoot2,
@@ -3100,12 +3555,20 @@ function defaultLocalAuthSources(vault) {
3100
3555
  return [`env:CNOS_SECRET_PASSPHRASE_${token}`, "env:CNOS_SECRET_PASSPHRASE", `keychain:cnos/${vault}`, "prompt"];
3101
3556
  }
3102
3557
  async function createVaultDefinition(name, options = {}) {
3558
+ await assertWritableConfigRoot(`create vault ${name}`, options);
3103
3559
  const vault = name.trim() || "default";
3104
3560
  const provider = options.provider?.trim() || "local";
3105
3561
  if (provider === "local" && (options.noPassphrase ?? false)) {
3106
3562
  throw new Error("Local vaults cannot be passwordless.");
3107
3563
  }
3108
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3564
+ const loadedManifest = await loadManifest7({
3565
+ ...options.root ? { root: options.root } : {},
3566
+ ...options.cwd ? { cwd: options.cwd } : {},
3567
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3568
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3569
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3570
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3571
+ });
3109
3572
  const vaultDefinition = buildVaultDefinition(vault, provider);
3110
3573
  const rawManifest = {
3111
3574
  ...loadedManifest.rawManifest,
@@ -3135,7 +3598,14 @@ async function createVaultDefinition(name, options = {}) {
3135
3598
  };
3136
3599
  }
3137
3600
  async function listVaultDefinitions(options = {}) {
3138
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3601
+ const loadedManifest = await loadManifest7({
3602
+ ...options.root ? { root: options.root } : {},
3603
+ ...options.cwd ? { cwd: options.cwd } : {},
3604
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3605
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3606
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3607
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3608
+ });
3139
3609
  const localStoreVaults = await listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
3140
3610
  return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
3141
3611
  const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
@@ -3147,8 +3617,16 @@ async function listVaultDefinitions(options = {}) {
3147
3617
  });
3148
3618
  }
3149
3619
  async function removeVaultDefinition(name, options = {}) {
3620
+ await assertWritableConfigRoot(`remove vault ${name}`, options);
3150
3621
  const vault = name.trim() || "default";
3151
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3622
+ const loadedManifest = await loadManifest7({
3623
+ ...options.root ? { root: options.root } : {},
3624
+ ...options.cwd ? { cwd: options.cwd } : {},
3625
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3626
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3627
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3628
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3629
+ });
3152
3630
  if (!loadedManifest.rawManifest.vaults?.[vault]) {
3153
3631
  return {
3154
3632
  name: vault,
@@ -3166,10 +3644,10 @@ async function removeVaultDefinition(name, options = {}) {
3166
3644
  delete rawManifest.vaults;
3167
3645
  }
3168
3646
  await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
3169
- const vaultRoot = path16.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3647
+ const vaultRoot = path18.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3170
3648
  let removedStore;
3171
3649
  try {
3172
- await rm3(vaultRoot, { recursive: true, force: true });
3650
+ await rm4(vaultRoot, { recursive: true, force: true });
3173
3651
  removedStore = vaultRoot;
3174
3652
  } catch {
3175
3653
  removedStore = void 0;
@@ -3187,7 +3665,14 @@ async function listLocalStoreVaults(options = {}) {
3187
3665
  }
3188
3666
  async function authenticateVault(name, options = {}) {
3189
3667
  const vault = name.trim() || "default";
3190
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3668
+ const loadedManifest = await loadManifest7({
3669
+ ...options.root ? { root: options.root } : {},
3670
+ ...options.cwd ? { cwd: options.cwd } : {},
3671
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3672
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3673
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3674
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3675
+ });
3191
3676
  const definition = loadedManifest.manifest.vaults[vault];
3192
3677
  if (!definition) {
3193
3678
  throw new Error(`Unknown vault "${vault}"`);
@@ -3260,7 +3745,7 @@ function normalizeVaultAction(args) {
3260
3745
  async function runVault(args = [], options = {}) {
3261
3746
  const { action, tail } = normalizeVaultAction(args);
3262
3747
  const cliArgs = [...options.cliArgs ?? []];
3263
- const root = path17.resolve(options.root ?? process.cwd());
3748
+ const root = path19.resolve(options.root ?? process.cwd());
3264
3749
  if (consumeOption(cliArgs, "--passphrase")) {
3265
3750
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
3266
3751
  }
@@ -3366,7 +3851,7 @@ async function runSecret(argsOrPath, options = {}) {
3366
3851
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
3367
3852
  const { action, tail } = normalizeSecretCommand(args);
3368
3853
  const cliArgs = [...options.cliArgs ?? []];
3369
- const root = path18.resolve(options.root ?? process.cwd());
3854
+ const root = path20.resolve(options.root ?? process.cwd());
3370
3855
  if (consumeOption(cliArgs, "--passphrase")) {
3371
3856
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
3372
3857
  }
@@ -3462,9 +3947,9 @@ async function runSecret(argsOrPath, options = {}) {
3462
3947
  }
3463
3948
 
3464
3949
  // src/commands/use.ts
3465
- import path19 from "path";
3950
+ import path21 from "path";
3466
3951
  async function runUse(args = [], options = {}) {
3467
- const root = path19.resolve(options.root ?? process.cwd());
3952
+ const root = path21.resolve(options.root ?? process.cwd());
3468
3953
  const action = args[0];
3469
3954
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
3470
3955
  if (action === "show" || !action && !hasUpdates) {
@@ -3501,7 +3986,7 @@ async function runValidate(options = {}) {
3501
3986
  // package.json
3502
3987
  var package_default = {
3503
3988
  name: "@kitsy/cnos-cli",
3504
- version: "1.6.1",
3989
+ version: "1.8.0",
3505
3990
  description: "CLI entry point and developer tooling for CNOS.",
3506
3991
  type: "module",
3507
3992
  main: "./dist/index.js",
@@ -3556,7 +4041,7 @@ function runVersion() {
3556
4041
  }
3557
4042
 
3558
4043
  // src/commands/value.ts
3559
- import path20 from "path";
4044
+ import path22 from "path";
3560
4045
  function normalizeValueCommand(args) {
3561
4046
  const [actionOrPath, ...tail] = args;
3562
4047
  if (!actionOrPath) {
@@ -3580,7 +4065,7 @@ async function runValue(argsOrPath, options = {}) {
3580
4065
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
3581
4066
  const { action, tail } = normalizeValueCommand(args);
3582
4067
  const cliArgs = [...options.cliArgs ?? []];
3583
- const root = path20.resolve(options.root ?? process.cwd());
4068
+ const root = path22.resolve(options.root ?? process.cwd());
3584
4069
  if (action === "list") {
3585
4070
  const prefix = consumeOption(cliArgs, "--prefix");
3586
4071
  const entries = await listConfigEntries("value", {
@@ -3591,16 +4076,24 @@ async function runValue(argsOrPath, options = {}) {
3591
4076
  if (options.json) {
3592
4077
  return printJson(entries);
3593
4078
  }
3594
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
4079
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
3595
4080
  }
3596
4081
  if (action === "set") {
3597
4082
  const valuePath = tail[0] ?? "app.name";
3598
- const rawValue = tail[1] ?? "";
4083
+ const derive = consumeFlag(cliArgs, "--derive");
4084
+ const expr = consumeOption(cliArgs, "--expr");
4085
+ const deriveArg = derive && !expr && cliArgs[0] && !cliArgs[0].startsWith("--") ? cliArgs.shift() : void 0;
4086
+ const rawValue = derive ? "" : tail[1] ?? "";
4087
+ const deriveExpression = derive ? expr ?? tail[1] ?? deriveArg ?? "" : void 0;
3599
4088
  const target = consumeOption(cliArgs, "--target") ?? "local";
3600
4089
  const result = await defineValue("value", valuePath, rawValue, {
3601
4090
  ...options,
3602
4091
  cliArgs,
3603
- target
4092
+ target,
4093
+ ...deriveExpression !== void 0 ? {
4094
+ deriveExpression,
4095
+ deriveExprMode: Boolean(expr)
4096
+ } : {}
3604
4097
  });
3605
4098
  if (options.json) {
3606
4099
  return printJson({
@@ -3656,6 +4149,7 @@ async function buildRunEnvironment(options) {
3656
4149
  const prefix = consumeOption(cliArgs, "--prefix");
3657
4150
  const runtime = await createRuntimeService({
3658
4151
  ...options,
4152
+ cacheMode: "dev",
3659
4153
  cliArgs
3660
4154
  });
3661
4155
  const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
@@ -3697,12 +4191,14 @@ async function startWatchLoop(options) {
3697
4191
  const root = options.root ?? process.cwd();
3698
4192
  let current = await buildRunEnvironment({
3699
4193
  ...options,
4194
+ cacheMode: "dev",
3700
4195
  cliArgs
3701
4196
  });
3702
4197
  let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
3703
4198
  let closed = false;
3704
4199
  const watcher = await startGraphWatchLoop({
3705
4200
  ...options,
4201
+ cacheMode: "dev",
3706
4202
  cliArgs,
3707
4203
  debounceMs,
3708
4204
  async onChange(payload) {
@@ -3711,6 +4207,7 @@ async function startWatchLoop(options) {
3711
4207
  }
3712
4208
  current = await buildRunEnvironment({
3713
4209
  ...options,
4210
+ cacheMode: "dev",
3714
4211
  cliArgs
3715
4212
  });
3716
4213
  if (isSignal) {
@@ -3764,12 +4261,12 @@ async function runWatch(command, options = {}) {
3764
4261
  }
3765
4262
 
3766
4263
  // src/commands/workspace.ts
3767
- import { cp, mkdir as mkdir6, rename, rm as rm4, stat, writeFile as writeFile9, readFile as readFile6 } from "fs/promises";
3768
- import path21 from "path";
3769
- import { loadManifest as loadManifest4, parseYaml as parseYaml5, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
4264
+ import { cp, mkdir as mkdir6, readdir as readdir5, readFile as readFile6, rename, rm as rm5, stat as stat2, writeFile as writeFile9 } from "fs/promises";
4265
+ import path23 from "path";
4266
+ import { loadManifest as loadManifest8, parseYaml as parseYaml5, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
3770
4267
  async function exists(targetPath) {
3771
4268
  try {
3772
- await stat(targetPath);
4269
+ await stat2(targetPath);
3773
4270
  return true;
3774
4271
  } catch {
3775
4272
  return false;
@@ -3779,25 +4276,37 @@ async function copyIfExists(source, target) {
3779
4276
  if (!await exists(source)) {
3780
4277
  return;
3781
4278
  }
3782
- await mkdir6(path21.dirname(target), { recursive: true });
4279
+ await mkdir6(path23.dirname(target), { recursive: true });
3783
4280
  await cp(source, target, { recursive: true, force: true });
3784
4281
  }
4282
+ async function moveIfExists(source, target, force = false) {
4283
+ if (!await exists(source)) {
4284
+ return false;
4285
+ }
4286
+ if (force) {
4287
+ await rm5(target, { recursive: true, force: true });
4288
+ } else if (await exists(target)) {
4289
+ throw new Error(`Refusing to overwrite existing path ${target}. Use --force to replace it.`);
4290
+ }
4291
+ await mkdir6(path23.dirname(target), { recursive: true });
4292
+ await rename(source, target);
4293
+ return true;
4294
+ }
3785
4295
  async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
3786
4296
  for (const sourceRoot of sourceRoots) {
3787
4297
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
3788
- await copyIfExists(
3789
- path21.join(sourceRoot, folderName),
3790
- path21.join(targetCnosRoot, folderName)
3791
- );
4298
+ await copyIfExists(path23.join(sourceRoot, folderName), path23.join(targetCnosRoot, folderName));
3792
4299
  }
3793
4300
  }
3794
4301
  }
3795
- async function writeCnosrc(packageRoot, config) {
4302
+ async function writeAnchor(packageRoot, manifestRoot, workspace) {
4303
+ const relativeRoot = path23.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
4304
+ const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
3796
4305
  await writeFile9(
3797
- path21.join(packageRoot, ".cnosrc.yml"),
4306
+ path23.join(packageRoot, ".cnosrc.yml"),
3798
4307
  stringifyYaml7({
3799
- root: config.root,
3800
- ...config.workspace ? { workspace: config.workspace } : {}
4308
+ root: rootValue,
4309
+ ...workspace ? { workspace } : {}
3801
4310
  }),
3802
4311
  "utf8"
3803
4312
  );
@@ -3809,18 +4318,48 @@ function createDetachedManifest(rawManifest) {
3809
4318
  }
3810
4319
  return next;
3811
4320
  }
4321
+ function normalizeWorkspaceId(value) {
4322
+ const workspaceId = value?.trim();
4323
+ if (!workspaceId) {
4324
+ throw new Error("Workspace id is required");
4325
+ }
4326
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(workspaceId)) {
4327
+ throw new Error(`Invalid workspace id "${workspaceId}". Use letters, numbers, dot, underscore, or dash.`);
4328
+ }
4329
+ return workspaceId;
4330
+ }
4331
+ function splitExtends(value) {
4332
+ if (!value) {
4333
+ return void 0;
4334
+ }
4335
+ const items = value.split(",").map((entry) => entry.trim()).filter(Boolean);
4336
+ return items.length > 0 ? items : void 0;
4337
+ }
4338
+ async function hasDirectConfigData(cnosRoot) {
4339
+ for (const folderName of ["values", "secrets", "env", "profiles"]) {
4340
+ const folder = path23.join(cnosRoot, folderName);
4341
+ if (!await exists(folder)) {
4342
+ continue;
4343
+ }
4344
+ const entries = await readdir5(folder, { withFileTypes: true });
4345
+ if (entries.some((entry) => entry.name !== ".gitkeep")) {
4346
+ return true;
4347
+ }
4348
+ }
4349
+ return false;
4350
+ }
3812
4351
  async function runDetach(packageRoot, options = {}) {
3813
- const loaded = await loadManifest4({ cwd: packageRoot });
4352
+ const loaded = await loadManifest8({ cwd: packageRoot });
3814
4353
  if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
3815
4354
  throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
3816
4355
  }
3817
- const targetCnosRoot = path21.join(packageRoot, ".cnos");
4356
+ const targetCnosRoot = path23.join(packageRoot, ".cnos");
3818
4357
  const force = consumeFlag([...options.cliArgs ?? []], "--force");
3819
4358
  if (await exists(targetCnosRoot) && !force) {
3820
4359
  throw new Error(`Refusing to detach because ${displayPath(targetCnosRoot, packageRoot)} already exists. Use --force to overwrite.`);
3821
4360
  }
3822
4361
  if (force) {
3823
- await rm4(targetCnosRoot, { recursive: true, force: true });
4362
+ await rm5(targetCnosRoot, { recursive: true, force: true });
3824
4363
  }
3825
4364
  const runtime = await createRuntimeService({
3826
4365
  ...options,
@@ -3831,11 +4370,11 @@ async function runDetach(packageRoot, options = {}) {
3831
4370
  await mkdir6(targetCnosRoot, { recursive: true });
3832
4371
  await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
3833
4372
  await writeFile9(
3834
- path21.join(targetCnosRoot, "cnos.yml"),
4373
+ path23.join(targetCnosRoot, "cnos.yml"),
3835
4374
  stringifyYaml7(createDetachedManifest(loaded.rawManifest)),
3836
4375
  "utf8"
3837
4376
  );
3838
- const relativeRoot = path21.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
4377
+ const relativeRoot = path23.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
3839
4378
  const marker = {
3840
4379
  detachedFrom: relativeRoot || ".",
3841
4380
  detachedWorkspace: loaded.anchoredWorkspace,
@@ -3845,8 +4384,8 @@ async function runDetach(packageRoot, options = {}) {
3845
4384
  workspace: loaded.anchoredWorkspace
3846
4385
  }
3847
4386
  };
3848
- await writeFile9(path21.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
3849
- await writeCnosrc(packageRoot, { root: "./.cnos" });
4387
+ await writeFile9(path23.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
4388
+ await writeFile9(path23.join(packageRoot, ".cnosrc.yml"), stringifyYaml7({ root: "./.cnos" }), "utf8");
3850
4389
  if (options.json) {
3851
4390
  return printJson({
3852
4391
  packageRoot,
@@ -3859,8 +4398,8 @@ async function runDetach(packageRoot, options = {}) {
3859
4398
  async function runAttach(packageRoot, options = {}) {
3860
4399
  const cliArgs = [...options.cliArgs ?? []];
3861
4400
  const force = consumeFlag(cliArgs, "--force");
3862
- const childCnosRoot = path21.join(packageRoot, ".cnos");
3863
- const markerPath = path21.join(childCnosRoot, ".detached");
4401
+ const childCnosRoot = path23.join(packageRoot, ".cnos");
4402
+ const markerPath = path23.join(childCnosRoot, ".detached");
3864
4403
  if (!await exists(markerPath)) {
3865
4404
  throw new Error("workspace attach requires a detached package with .cnos/.detached");
3866
4405
  }
@@ -3868,34 +4407,36 @@ async function runAttach(packageRoot, options = {}) {
3868
4407
  if (!marker?.originalCnosrc?.root || !marker.detachedWorkspace) {
3869
4408
  throw new Error("Invalid .detached marker");
3870
4409
  }
3871
- const parentManifestRoot = path21.resolve(packageRoot, marker.originalCnosrc.root);
3872
- const parentLoaded = await loadManifest4({ root: parentManifestRoot });
4410
+ const parentManifestRoot = path23.resolve(packageRoot, marker.originalCnosrc.root);
4411
+ const parentLoaded = await loadManifest8({ root: parentManifestRoot });
4412
+ if (parentLoaded.rootResolution.readOnly) {
4413
+ throw new Error(
4414
+ `Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
4415
+ );
4416
+ }
3873
4417
  const workspaceId = marker.originalCnosrc.workspace ?? marker.detachedWorkspace;
3874
- const parentWorkspaceRoot = path21.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
4418
+ const parentWorkspaceRoot = path23.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
3875
4419
  if (await exists(parentWorkspaceRoot) && !force) {
3876
4420
  throw new Error(`workspace "${workspaceId}" already exists in parent root. Use --force to overwrite.`);
3877
4421
  }
3878
4422
  if (force) {
3879
- await rm4(parentWorkspaceRoot, { recursive: true, force: true });
4423
+ await rm5(parentWorkspaceRoot, { recursive: true, force: true });
3880
4424
  }
3881
4425
  await mkdir6(parentWorkspaceRoot, { recursive: true });
3882
4426
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
3883
- await copyIfExists(path21.join(childCnosRoot, folderName), path21.join(parentWorkspaceRoot, folderName));
4427
+ await copyIfExists(path23.join(childCnosRoot, folderName), path23.join(parentWorkspaceRoot, folderName));
3884
4428
  }
3885
- const rawManifest = parentLoaded.rawManifest;
4429
+ const rawManifest = structuredClone(parentLoaded.rawManifest);
3886
4430
  const workspaces = rawManifest.workspaces ?? {};
3887
4431
  const items = workspaces.items ?? {};
3888
4432
  items[workspaceId] = items[workspaceId] ?? {};
3889
4433
  workspaces.items = items;
3890
4434
  rawManifest.workspaces = workspaces;
3891
- await writeFile9(path21.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
3892
- const archivePath = path21.join(packageRoot, ".cnos.detached.bak");
3893
- await rm4(archivePath, { recursive: true, force: true });
4435
+ await writeFile9(path23.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4436
+ const archivePath = path23.join(packageRoot, ".cnos.detached.bak");
4437
+ await rm5(archivePath, { recursive: true, force: true });
3894
4438
  await rename(childCnosRoot, archivePath);
3895
- await writeCnosrc(packageRoot, {
3896
- root: marker.originalCnosrc.root,
3897
- ...workspaceId ? { workspace: workspaceId } : {}
3898
- });
4439
+ await writeAnchor(packageRoot, parentLoaded.manifestRoot, workspaceId);
3899
4440
  if (options.json) {
3900
4441
  return printJson({
3901
4442
  packageRoot,
@@ -3906,15 +4447,168 @@ async function runAttach(packageRoot, options = {}) {
3906
4447
  }
3907
4448
  return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
3908
4449
  }
3909
- async function runWorkspace(args = [], options = {}) {
3910
- const [action] = args;
4450
+ async function runList2(manifestCwd, options = {}) {
4451
+ const loaded = await loadManifest8({
4452
+ ...options.root ? { root: options.root } : {},
4453
+ cwd: manifestCwd,
4454
+ ...options.processEnv ? { processEnv: options.processEnv } : {}
4455
+ });
4456
+ const entries = Object.entries(loaded.manifest.workspaces.items).map(([id, config]) => ({
4457
+ id,
4458
+ extends: config.extends,
4459
+ default: loaded.manifest.workspaces.default === id,
4460
+ path: path23.join(loaded.manifestRoot, "workspaces", id)
4461
+ })).sort((left, right) => left.id.localeCompare(right.id));
4462
+ if (options.json) {
4463
+ return printJson({
4464
+ default: loaded.manifest.workspaces.default,
4465
+ workspaces: entries
4466
+ });
4467
+ }
4468
+ if (entries.length === 0) {
4469
+ return "no workspaces declared";
4470
+ }
4471
+ return entries.map((entry) => {
4472
+ const tags = [
4473
+ entry.default ? "default" : void 0,
4474
+ entry.extends.length > 0 ? `extends=${entry.extends.join(",")}` : void 0
4475
+ ].filter(Boolean);
4476
+ return `${entry.id}${tags.length > 0 ? ` (${tags.join(", ")})` : ""}`;
4477
+ }).join("\n");
4478
+ }
4479
+ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, options = {}) {
3911
4480
  const cliArgs = [...options.cliArgs ?? []];
3912
- const packageRoot = path21.resolve(consumeOption(cliArgs, "--package-root") ?? options.root ?? process.cwd());
4481
+ const extendsOption = splitExtends(consumeOption(cliArgs, "--extends"));
4482
+ const onboardCurrent = consumeFlag(cliArgs, "--onboard-current");
4483
+ const force = consumeFlag(cliArgs, "--force");
4484
+ if (cliArgs.length > 0) {
4485
+ throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4486
+ }
4487
+ const loaded = await loadManifest8({
4488
+ ...options.root ? { root: options.root } : {},
4489
+ cwd: manifestCwd,
4490
+ ...options.processEnv ? { processEnv: options.processEnv } : {}
4491
+ });
4492
+ if (loaded.rootResolution.readOnly) {
4493
+ throw new Error(
4494
+ `Cannot ${action} workspace because the active CNOS root is remote and read-only (${loaded.rootResolution.rootUri}). Clone the config repo and edit it directly.`
4495
+ );
4496
+ }
4497
+ const manifestRoot = loaded.manifestRoot;
4498
+ const cnosRoot = manifestRoot;
4499
+ const rawManifest = structuredClone(loaded.rawManifest);
4500
+ const rawWorkspaces = rawManifest.workspaces ?? {};
4501
+ const rawItems = rawWorkspaces.items ?? {};
4502
+ const isWorkspaceMode = Object.keys(rawItems).length > 0;
4503
+ const directConfigPresent = await hasDirectConfigData(cnosRoot);
4504
+ if (!isWorkspaceMode && directConfigPresent && !onboardCurrent) {
4505
+ throw new Error(
4506
+ "This CNOS root is in single-root mode and already has direct values/secrets/env/profiles data. Re-run with --onboard-current to migrate it into workspace mode."
4507
+ );
4508
+ }
4509
+ if (rawItems[workspaceId] && !force) {
4510
+ throw new Error(`workspace "${workspaceId}" already exists. Use --force to update its manifest entry and anchor.`);
4511
+ }
4512
+ const defaultExtends = extendsOption ?? (!["base", "root"].includes(workspaceId) && rawItems.base ? ["base"] : void 0);
4513
+ rawItems[workspaceId] = defaultExtends && defaultExtends.length > 0 ? { extends: defaultExtends } : {};
4514
+ rawWorkspaces.items = rawItems;
4515
+ rawWorkspaces.default = rawWorkspaces.default ?? workspaceId;
4516
+ rawManifest.workspaces = rawWorkspaces;
4517
+ const workspaceRoot = path23.join(cnosRoot, "workspaces", workspaceId);
4518
+ if (onboardCurrent) {
4519
+ if (isWorkspaceMode) {
4520
+ throw new Error("--onboard-current can only be used when the manifest is not already in workspace mode.");
4521
+ }
4522
+ for (const folderName of ["values", "secrets", "env", "profiles"]) {
4523
+ await moveIfExists(path23.join(cnosRoot, folderName), path23.join(workspaceRoot, folderName), force);
4524
+ }
4525
+ }
4526
+ const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
4527
+ await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4528
+ await ensureGitignore(path23.dirname(cnosRoot));
4529
+ await writeAnchor(packageRoot, cnosRoot, workspaceId);
4530
+ await ensureFile(path23.join(packageRoot, ".cnos-workspace.yml"), `workspace: ${workspaceId}
4531
+ globalRoot: ~/.cnos
4532
+ `);
4533
+ const result = {
4534
+ workspace: workspaceId,
4535
+ root: path23.dirname(cnosRoot),
4536
+ packageRoot,
4537
+ onboarded: onboardCurrent,
4538
+ created
4539
+ };
4540
+ if (options.json) {
4541
+ return printJson(result);
4542
+ }
4543
+ const verb = action === "add" ? "added" : "scaffolded";
4544
+ return `${verb} workspace ${workspaceId} at ${displayPath(workspaceRoot, packageRoot)}`;
4545
+ }
4546
+ async function runRemove(workspaceId, manifestCwd, options = {}) {
4547
+ const cliArgs = [...options.cliArgs ?? []];
4548
+ consumeFlag(cliArgs, "--force");
4549
+ if (cliArgs.length > 0) {
4550
+ throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
4551
+ }
4552
+ const loaded = await loadManifest8({
4553
+ ...options.root ? { root: options.root } : {},
4554
+ cwd: manifestCwd,
4555
+ ...options.processEnv ? { processEnv: options.processEnv } : {}
4556
+ });
4557
+ if (loaded.rootResolution.readOnly) {
4558
+ throw new Error(
4559
+ `Cannot remove workspace because the active CNOS root is remote and read-only (${loaded.rootResolution.rootUri}). Clone the config repo and edit it directly.`
4560
+ );
4561
+ }
4562
+ const rawManifest = structuredClone(loaded.rawManifest);
4563
+ const rawWorkspaces = rawManifest.workspaces ?? {};
4564
+ const rawItems = rawWorkspaces.items ?? {};
4565
+ if (!rawItems[workspaceId]) {
4566
+ throw new Error(`workspace "${workspaceId}" does not exist`);
4567
+ }
4568
+ if (rawWorkspaces.default === workspaceId) {
4569
+ throw new Error(`Cannot remove workspace "${workspaceId}" because it is the default workspace. Change workspaces.default first.`);
4570
+ }
4571
+ delete rawItems[workspaceId];
4572
+ rawWorkspaces.items = rawItems;
4573
+ rawManifest.workspaces = rawWorkspaces;
4574
+ await writeFile9(path23.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4575
+ await rm5(path23.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
4576
+ if (options.json) {
4577
+ return printJson({
4578
+ workspace: workspaceId,
4579
+ removedFrom: loaded.manifestRoot
4580
+ });
4581
+ }
4582
+ return `removed workspace ${workspaceId}`;
4583
+ }
4584
+ async function runWorkspace(args = [], options = {}) {
4585
+ const [action, workspaceArg] = args;
4586
+ const baseCliArgs = [...options.cliArgs ?? []];
4587
+ const manifestCwd = path23.resolve(options.root ?? process.cwd());
4588
+ const packageRoot = path23.resolve(consumeOption(baseCliArgs, "--package-root") ?? options.root ?? process.cwd());
3913
4589
  switch (action) {
3914
- case "detach":
3915
- return runDetach(packageRoot, { ...options, cliArgs });
3916
4590
  case "attach":
3917
- return runAttach(packageRoot, { ...options, cliArgs });
4591
+ return runAttach(packageRoot, { ...options, cliArgs: baseCliArgs });
4592
+ case "detach":
4593
+ return runDetach(packageRoot, { ...options, cliArgs: baseCliArgs });
4594
+ case "list":
4595
+ return runList2(manifestCwd, options);
4596
+ case "add":
4597
+ return runAddOrScaffold("add", normalizeWorkspaceId(workspaceArg), manifestCwd, packageRoot, {
4598
+ ...options,
4599
+ cliArgs: baseCliArgs
4600
+ });
4601
+ case "scaffold":
4602
+ return runAddOrScaffold("scaffold", normalizeWorkspaceId(workspaceArg), manifestCwd, packageRoot, {
4603
+ ...options,
4604
+ cliArgs: baseCliArgs
4605
+ });
4606
+ case "remove":
4607
+ case "delete":
4608
+ return runRemove(normalizeWorkspaceId(workspaceArg), manifestCwd, {
4609
+ ...options,
4610
+ cliArgs: baseCliArgs
4611
+ });
3918
4612
  default:
3919
4613
  throw new Error(`Unsupported workspace action: ${action ?? "(missing)"}`);
3920
4614
  }
@@ -3934,12 +4628,15 @@ function resolveHelpTopic(command, args) {
3934
4628
  if (command === "build" && args[0] && ["env", "server", "browser", "public"].includes(args[0])) {
3935
4629
  return normalizeHelpTopic([command, args[0]]);
3936
4630
  }
3937
- if (command === "dev" && args[0] === "env") {
4631
+ if (command === "cache" && args[0] && ["list", "clear", "refresh"].includes(args[0])) {
3938
4632
  return normalizeHelpTopic([command, args[0]]);
3939
4633
  }
3940
- if (command === "workspace" && args[0] && ["attach", "detach"].includes(args[0])) {
4634
+ if (command === "dev" && args[0] === "env") {
3941
4635
  return normalizeHelpTopic([command, args[0]]);
3942
4636
  }
4637
+ if (command === "workspace" && args[0] && ["attach", "detach", "add", "list", "remove", "delete", "scaffold"].includes(args[0])) {
4638
+ return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0]]);
4639
+ }
3943
4640
  if (command === "vault" && args[0] && ["create", "add", "list", "delete", "remove"].includes(args[0])) {
3944
4641
  return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0] === "add" ? "create" : args[0]]);
3945
4642
  }
@@ -3983,6 +4680,9 @@ async function main(argv) {
3983
4680
  ...options.globalRoot ? {
3984
4681
  globalRoot: options.globalRoot
3985
4682
  } : {},
4683
+ ...typeof options.cacheTtlSeconds === "number" ? {
4684
+ cacheTtlSeconds: options.cacheTtlSeconds
4685
+ } : {},
3986
4686
  ...options.json ? {
3987
4687
  json: true
3988
4688
  } : {},
@@ -4074,6 +4774,10 @@ async function main(argv) {
4074
4774
  return;
4075
4775
  case "build":
4076
4776
  process.stdout.write(`${await runBuild(args[0], runtimeOptions)}
4777
+ `);
4778
+ return;
4779
+ case "cache":
4780
+ process.stdout.write(`${await runCache(args, runtimeOptions)}
4077
4781
  `);
4078
4782
  return;
4079
4783
  case "dev":