@kitsy/cnos-cli 1.6.1 → 1.7.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 +613 -150
  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,8 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
21
22
  "--inherit",
22
23
  "--as",
23
24
  "--set",
24
- "--debounce"
25
+ "--debounce",
26
+ "--expr"
25
27
  ]);
26
28
  var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
27
29
  "--flatten",
@@ -39,7 +41,8 @@ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
39
41
  "--dry-run",
40
42
  "--apply",
41
43
  "--rewrite",
42
- "--signal"
44
+ "--signal",
45
+ "--derive"
43
46
  ]);
44
47
  function normalizeCommand(argv) {
45
48
  const [command = "doctor", ...rest] = argv;
@@ -104,6 +107,14 @@ function normalizeCommand(argv) {
104
107
  return [command, ...rest];
105
108
  }
106
109
  function setOption(options, key, value) {
110
+ if (key === "cacheTtlSeconds") {
111
+ const parsed = Number(value);
112
+ if (!Number.isFinite(parsed) || parsed < 0) {
113
+ throw new Error(`Invalid value for --cache-ttl: ${value}`);
114
+ }
115
+ options.cacheTtlSeconds = parsed;
116
+ return;
117
+ }
107
118
  options[key] = value;
108
119
  }
109
120
  function parseArgs(argv) {
@@ -253,7 +264,7 @@ function printJson(value) {
253
264
 
254
265
  // src/services/projections.ts
255
266
  import { mkdir, writeFile } from "fs/promises";
256
- import path2 from "path";
267
+ import path3 from "path";
257
268
  import { resolveBrowserData, resolveFrameworkEnv, resolveServerProjection } from "@kitsy/cnos/build";
258
269
  import { stringifyYaml } from "@kitsy/cnos/internal";
259
270
 
@@ -266,6 +277,9 @@ async function createRuntimeService(options = {}) {
266
277
  ...options.workspace ? { workspace: options.workspace } : {},
267
278
  ...options.profile ? { profile: options.profile } : {},
268
279
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {},
280
+ cacheMode: options.cacheMode ?? "runtime",
281
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
282
+ ...options.forceRefresh ? { forceRefresh: true } : {},
269
283
  ...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
270
284
  ...options.secretResolution ? { secretResolution: options.secretResolution } : {},
271
285
  ...typeof options.secretRefreshTtl === "number" ? { secretRefreshTtl: options.secretRefreshTtl } : {},
@@ -273,6 +287,15 @@ async function createRuntimeService(options = {}) {
273
287
  });
274
288
  }
275
289
 
290
+ // src/services/paths.ts
291
+ import path2 from "path";
292
+ function resolveFilesystemBasePath(root, cwd = process.cwd()) {
293
+ if (!root || root.startsWith("git+") || root.startsWith("cnos://")) {
294
+ return path2.resolve(cwd);
295
+ }
296
+ return path2.resolve(root);
297
+ }
298
+
276
299
  // src/services/projections.ts
277
300
  function stringifyScalar(value) {
278
301
  if (value === void 0 || value === null) {
@@ -311,37 +334,62 @@ function formatKeyValueMap(values, format) {
311
334
  }
312
335
  }
313
336
  async function writeProjectionFile(to, output, root = process.cwd()) {
314
- const targetPath = path2.resolve(root, to);
315
- await mkdir(path2.dirname(targetPath), { recursive: true });
337
+ const targetPath = path3.resolve(root, to);
338
+ await mkdir(path3.dirname(targetPath), { recursive: true });
316
339
  await writeFile(targetPath, output, "utf8");
317
340
  return targetPath;
318
341
  }
319
342
  async function buildServerProjectionArtifact(to, options = {}, format = "json") {
320
- const projection = await resolveServerProjection(options);
343
+ const projection = await resolveServerProjection({
344
+ ...options,
345
+ cacheMode: "build"
346
+ });
321
347
  const output = format === "yaml" ? stringifyYaml(projection) : `${JSON.stringify(projection, null, 2)}
322
348
  `;
323
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
349
+ const targetPath = await writeProjectionFile(
350
+ to,
351
+ output,
352
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
353
+ );
324
354
  return { targetPath, output };
325
355
  }
326
356
  async function buildBrowserProjectionArtifact(to, options = {}, format = "json") {
327
- const projection = await resolveBrowserData(options);
357
+ const projection = await resolveBrowserData({
358
+ ...options,
359
+ cacheMode: "build"
360
+ });
328
361
  const output = format === "yaml" ? stringifyYaml(projection) : `${JSON.stringify(projection, null, 2)}
329
362
  `;
330
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
363
+ const targetPath = await writeProjectionFile(
364
+ to,
365
+ output,
366
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
367
+ );
331
368
  return { targetPath, output };
332
369
  }
333
370
  async function buildPublicProjectionArtifact(to, options = {}, format = "dotenv") {
334
371
  const cliArgs = [...options.cliArgs ?? []];
335
372
  const frameworkIndex = cliArgs.indexOf("--framework");
336
373
  const framework = frameworkIndex >= 0 && cliArgs[frameworkIndex + 1] ? cliArgs[frameworkIndex + 1] : "generic";
337
- const env = await resolveFrameworkEnv(options, framework);
374
+ const env = await resolveFrameworkEnv(
375
+ {
376
+ ...options,
377
+ cacheMode: "build"
378
+ },
379
+ framework
380
+ );
338
381
  const output = formatKeyValueMap(env, format);
339
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
382
+ const targetPath = await writeProjectionFile(
383
+ to,
384
+ output,
385
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
386
+ );
340
387
  return { targetPath, output, env };
341
388
  }
342
389
  async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
343
390
  const runtime = await createRuntimeService({
344
391
  ...options,
392
+ cacheMode: "build",
345
393
  cliArgs: [...options.cliArgs ?? []]
346
394
  });
347
395
  const env = runtime.toEnv();
@@ -352,7 +400,11 @@ async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
352
400
  }
353
401
  }
354
402
  const output = formatKeyValueMap(env, format);
355
- const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
403
+ const targetPath = await writeProjectionFile(
404
+ to,
405
+ output,
406
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
407
+ );
356
408
  return { targetPath, output, env };
357
409
  }
358
410
 
@@ -413,23 +465,208 @@ async function runBuild(subcommand, options = {}) {
413
465
  ...provenanceTarget ? { provenance: provenanceTarget } : {}
414
466
  });
415
467
  }
416
- return `built ${subcommand} artifact at ${displayPath(targetPath, options.root ?? process.cwd())}`;
468
+ return `built ${subcommand} artifact at ${displayPath(
469
+ targetPath,
470
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
471
+ )}`;
417
472
  }
418
473
 
419
- // src/commands/define.ts
474
+ // src/services/cache.ts
475
+ import { readdir, rm, stat } from "fs/promises";
420
476
  import path4 from "path";
477
+ import {
478
+ loadManifest,
479
+ parseGitUri,
480
+ readRemoteRootCacheMetadata,
481
+ resolveCnosCacheRoot,
482
+ resolveRemoteRootCachePaths,
483
+ resolveRootUri
484
+ } from "@kitsy/cnos/internal";
485
+ async function computeDirectorySize(targetPath) {
486
+ try {
487
+ const info = await stat(targetPath);
488
+ if (!info.isDirectory()) {
489
+ return info.size;
490
+ }
491
+ const entries = await readdir(targetPath, { withFileTypes: true });
492
+ const sizes = await Promise.all(
493
+ entries.map((entry) => computeDirectorySize(path4.join(targetPath, entry.name)))
494
+ );
495
+ return sizes.reduce((sum, value) => sum + value, 0);
496
+ } catch {
497
+ return 0;
498
+ }
499
+ }
500
+ async function listCachedRoots(processEnv = process.env) {
501
+ const rootsDir = path4.join(resolveCnosCacheRoot(processEnv), "roots");
502
+ try {
503
+ const entries = await readdir(rootsDir, { withFileTypes: true });
504
+ const records = await Promise.all(
505
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
506
+ const cacheDir = path4.join(rootsDir, entry.name);
507
+ const metadata = await readRemoteRootCacheMetadata(path4.join(cacheDir, ".cnos-cache-meta.json"));
508
+ if (!metadata) {
509
+ return void 0;
510
+ }
511
+ return {
512
+ uri: metadata.uri,
513
+ cacheDir,
514
+ cachedAt: metadata.cachedAt,
515
+ resolvedCommit: metadata.resolvedCommit,
516
+ immutable: metadata.isImmutable,
517
+ ref: metadata.ref,
518
+ subpath: metadata.subpath,
519
+ sizeBytes: await computeDirectorySize(cacheDir)
520
+ };
521
+ })
522
+ );
523
+ return records.filter((record) => Boolean(record)).sort((left, right) => left.uri.localeCompare(right.uri));
524
+ } catch {
525
+ return [];
526
+ }
527
+ }
528
+ async function clearCachedRoots(uri, processEnv = process.env) {
529
+ if (uri) {
530
+ const paths = resolveRemoteRootCachePaths(uri, processEnv);
531
+ await rm(paths.cacheDir, { recursive: true, force: true });
532
+ return { cleared: [uri] };
533
+ }
534
+ const records = await listCachedRoots(processEnv);
535
+ await Promise.all(records.map((record) => rm(record.cacheDir, { recursive: true, force: true })));
536
+ return {
537
+ cleared: records.map((record) => record.uri)
538
+ };
539
+ }
540
+ async function refreshCachedRoots(uri, options = {}) {
541
+ const processEnv = options.processEnv ?? process.env;
542
+ if (uri) {
543
+ const parsed = parseGitUri(uri);
544
+ await resolveRootUri(uri, process.cwd(), {
545
+ processEnv,
546
+ cacheMode: "build",
547
+ forceRefresh: true
548
+ });
549
+ return { refreshed: [parsed.uri] };
550
+ }
551
+ const loadedManifest = await loadManifest({
552
+ ...options.root ? { root: options.root } : {},
553
+ ...options.cwd ? { cwd: options.cwd } : {},
554
+ processEnv
555
+ }).catch(() => void 0);
556
+ if (loadedManifest?.rootResolution.remote && loadedManifest.rootResolution.protocol === "git") {
557
+ await resolveRootUri(loadedManifest.rootResolution.rootUri, loadedManifest.consumerRoot, {
558
+ processEnv,
559
+ cacheMode: "build",
560
+ forceRefresh: true
561
+ });
562
+ return {
563
+ refreshed: [loadedManifest.rootResolution.rootUri]
564
+ };
565
+ }
566
+ const records = await listCachedRoots(processEnv);
567
+ const mutable = records.filter((record) => !record.immutable);
568
+ for (const record of mutable) {
569
+ await resolveRootUri(record.uri, process.cwd(), {
570
+ processEnv,
571
+ cacheMode: "build",
572
+ forceRefresh: true
573
+ });
574
+ }
575
+ return {
576
+ refreshed: mutable.map((record) => record.uri)
577
+ };
578
+ }
579
+
580
+ // src/commands/cache.ts
581
+ function normalizeAction(args) {
582
+ const [action = "list", target] = args;
583
+ if (action === "list" || action === "clear" || action === "refresh") {
584
+ return {
585
+ action,
586
+ ...typeof target === "string" ? { target } : {}
587
+ };
588
+ }
589
+ return {
590
+ action: "list",
591
+ ...typeof args[0] === "string" ? { target: args[0] } : {}
592
+ };
593
+ }
594
+ function formatBytes(sizeBytes) {
595
+ if (sizeBytes < 1024) {
596
+ return `${sizeBytes} B`;
597
+ }
598
+ if (sizeBytes < 1024 * 1024) {
599
+ return `${Math.round(sizeBytes / 1024 * 10) / 10} KB`;
600
+ }
601
+ return `${Math.round(sizeBytes / (1024 * 1024) * 10) / 10} MB`;
602
+ }
603
+ async function runCache(args = [], options = {}) {
604
+ const { action, target } = normalizeAction(args);
605
+ if (action === "clear") {
606
+ const result = await clearCachedRoots(target, options.processEnv ?? process.env);
607
+ return options.json ? printJson(result) : `cleared ${result.cleared.length} cached root(s)`;
608
+ }
609
+ if (action === "refresh") {
610
+ const result = await refreshCachedRoots(target, options);
611
+ return options.json ? printJson(result) : `refreshed ${result.refreshed.length} cached root(s)`;
612
+ }
613
+ const records = await listCachedRoots(options.processEnv ?? process.env);
614
+ if (options.json) {
615
+ return printJson(records);
616
+ }
617
+ if (records.length === 0) {
618
+ return "no cached remote roots";
619
+ }
620
+ return records.map(
621
+ (record) => [
622
+ record.uri,
623
+ ` cached: ${record.cachedAt}`,
624
+ ` commit: ${record.resolvedCommit}`,
625
+ ` immutable: ${record.immutable ? "yes" : "no"}`,
626
+ ` size: ${formatBytes(record.sizeBytes)}`
627
+ ].join("\n")
628
+ ).join("\n\n");
629
+ }
630
+
631
+ // src/commands/define.ts
632
+ import path6 from "path";
421
633
 
422
634
  // src/services/writes.ts
423
635
  import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
424
- import path3 from "path";
636
+ import path5 from "path";
425
637
  import {
426
638
  getNamespaceDefinition,
639
+ normalizeDerivedValue,
640
+ parseDerivation,
427
641
  createSecretVaultProvider,
642
+ validateDerivedTargetNamespace,
643
+ validateParsedDerivation,
428
644
  parseYaml,
429
645
  resolveConfigDocumentPath,
430
646
  resolveVaultAuth,
431
647
  stringifyYaml as stringifyYaml2
432
648
  } from "@kitsy/cnos/internal";
649
+
650
+ // src/services/rootAccess.ts
651
+ import { loadManifest as loadManifest2 } from "@kitsy/cnos/internal";
652
+ async function assertWritableConfigRoot(action, options = {}) {
653
+ const loadedManifest = await loadManifest2({
654
+ ...options.root ? { root: options.root } : {},
655
+ ...options.cwd ? { cwd: options.cwd } : {},
656
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
657
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
658
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
659
+ ...options.forceRefresh ? { forceRefresh: true } : {}
660
+ });
661
+ if (!loadedManifest.rootResolution.readOnly) {
662
+ return;
663
+ }
664
+ throw new Error(
665
+ `Cannot ${action} because the active CNOS root is remote and read-only (${loadedManifest.rootResolution.rootUri}). Clone the config repo and edit it directly.`
666
+ );
667
+ }
668
+
669
+ // src/services/writes.ts
433
670
  function setNestedValue(target, pathSegments, value) {
434
671
  const [head, ...tail] = pathSegments;
435
672
  if (!head) {
@@ -495,6 +732,7 @@ function getSelectedWorkspaceRoot(options, runtime) {
495
732
  return workspaceRoot.path;
496
733
  }
497
734
  async function defineValue(namespace, configPath, rawValue, options = {}) {
735
+ await assertWritableConfigRoot(`write ${namespace}.${configPath}`, options);
498
736
  if (namespace === "secret") {
499
737
  const secret = await setSecret(configPath, rawValue, {
500
738
  ...options,
@@ -527,9 +765,17 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
527
765
  const profile = options.profile ?? runtime.graph.profile;
528
766
  const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
529
767
  const document = await readYamlDocument(filePath);
530
- const parsedValue = parseScalarValue(rawValue);
768
+ let parsedValue;
769
+ if (options.deriveExpression !== void 0) {
770
+ validateDerivedTargetNamespace(runtime.manifest, namespace);
771
+ const derivedValue = normalizeDerivedValue(options.deriveExpression, options.deriveExprMode ?? false);
772
+ validateParsedDerivation(runtime.manifest, parseDerivation(derivedValue));
773
+ parsedValue = derivedValue;
774
+ } else {
775
+ parsedValue = parseScalarValue(rawValue);
776
+ }
531
777
  setNestedValue(document, configPath.split("."), parsedValue);
532
- await mkdir2(path3.dirname(filePath), { recursive: true });
778
+ await mkdir2(path5.dirname(filePath), { recursive: true });
533
779
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
534
780
  return {
535
781
  filePath,
@@ -567,7 +813,7 @@ async function setSecret(configPath, rawValue, options = {}) {
567
813
  };
568
814
  }
569
815
  setNestedValue(document, configPath.split("."), reference);
570
- await mkdir2(path3.dirname(filePath), { recursive: true });
816
+ await mkdir2(path5.dirname(filePath), { recursive: true });
571
817
  await writeFile2(filePath, stringifyYaml2(document), "utf8");
572
818
  return {
573
819
  filePath,
@@ -577,6 +823,7 @@ async function setSecret(configPath, rawValue, options = {}) {
577
823
  };
578
824
  }
579
825
  async function deleteSecret(configPath, options = {}) {
826
+ await assertWritableConfigRoot(`delete secret.${configPath}`, options);
580
827
  const runtime = await createRuntimeService(options);
581
828
  const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
582
829
  const profile = options.profile ?? runtime.graph.profile;
@@ -607,6 +854,7 @@ async function deleteSecret(configPath, options = {}) {
607
854
  };
608
855
  }
609
856
  async function deleteValue(namespace, configPath, options = {}) {
857
+ await assertWritableConfigRoot(`delete ${namespace}.${configPath}`, options);
610
858
  if (namespace === "secret") {
611
859
  return deleteSecret(configPath, options);
612
860
  }
@@ -645,7 +893,7 @@ async function deleteValue(namespace, configPath, options = {}) {
645
893
  // src/commands/define.ts
646
894
  async function runDefine(namespace, configPath, rawValue, options = {}) {
647
895
  const cliArgs = [...options.cliArgs ?? []];
648
- const root = path4.resolve(options.root ?? process.cwd());
896
+ const root = path6.resolve(options.root ?? process.cwd());
649
897
  const target = consumeOption(cliArgs, "--target") ?? "local";
650
898
  const local = consumeFlag(cliArgs, "--local");
651
899
  const remote = consumeFlag(cliArgs, "--remote");
@@ -676,7 +924,7 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
676
924
 
677
925
  // src/services/envMaterialization.ts
678
926
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
679
- import path5 from "path";
927
+ import path7 from "path";
680
928
  function resolveEnvFromRuntime(runtime, cliArgs = []) {
681
929
  const args = [...cliArgs];
682
930
  const isPublic = consumeFlag(args, "--public");
@@ -703,17 +951,21 @@ async function resolveMaterializedEnv(options = {}) {
703
951
  };
704
952
  }
705
953
  function resolveMaterializedEnvTarget(to, root = process.cwd()) {
706
- return path5.resolve(root, to);
954
+ return path7.resolve(root, to);
707
955
  }
708
956
  async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
709
957
  const targetPath = resolveMaterializedEnvTarget(to, root);
710
- await mkdir3(path5.dirname(targetPath), { recursive: true });
958
+ await mkdir3(path7.dirname(targetPath), { recursive: true });
711
959
  await writeFile3(targetPath, output, "utf8");
712
960
  return targetPath;
713
961
  }
714
962
  async function materializeEnvToFile(to, options = {}) {
715
963
  const result = await resolveMaterializedEnv(options);
716
- const targetPath = await writeMaterializedEnvFile(to, result.output, options.root ?? process.cwd());
964
+ const targetPath = await writeMaterializedEnvFile(
965
+ to,
966
+ result.output,
967
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
968
+ );
717
969
  return {
718
970
  ...result,
719
971
  targetPath
@@ -834,6 +1086,7 @@ async function startDevEnvLoop(command, options = {}) {
834
1086
  const writeCurrent = async () => {
835
1087
  await materializeEnvToFile(to, {
836
1088
  ...options,
1089
+ cacheMode: "dev",
837
1090
  cliArgs: [...cliArgs]
838
1091
  });
839
1092
  };
@@ -847,6 +1100,7 @@ async function startDevEnvLoop(command, options = {}) {
847
1100
  }
848
1101
  const watcher = await startGraphWatchLoop({
849
1102
  ...options,
1103
+ cacheMode: "dev",
850
1104
  cliArgs,
851
1105
  debounceMs,
852
1106
  async onChange(payload) {
@@ -903,7 +1157,10 @@ async function runDev(subcommand, command, options = {}) {
903
1157
  };
904
1158
  process.once("SIGINT", closeLoop);
905
1159
  process.once("SIGTERM", closeLoop);
906
- const targetPath = displayPath(to, options.root ?? process.cwd());
1160
+ const targetPath = displayPath(
1161
+ to,
1162
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
1163
+ );
907
1164
  return isSignal ? `watching config changes and rewriting ${targetPath} in signal mode` : `watching config changes, rewriting ${targetPath}, and restarting the child process`;
908
1165
  }
909
1166
 
@@ -956,11 +1213,12 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
956
1213
  }
957
1214
 
958
1215
  // src/services/doctor.ts
959
- import { readdir, readFile as readFile2 } from "fs/promises";
960
- import path6 from "path";
1216
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
1217
+ import path8 from "path";
961
1218
  import {
962
1219
  detectLegacyVaultFormat,
963
1220
  isSecretReference as isSecretReference2,
1221
+ loadManifest as loadManifest3,
964
1222
  parseYaml as parseYaml2,
965
1223
  readKeychain,
966
1224
  resolveSecretStoreRoot
@@ -979,7 +1237,7 @@ async function createValidationSummary(options = {}) {
979
1237
 
980
1238
  // src/services/doctor.ts
981
1239
  async function checkGitignore(root) {
982
- const gitignorePath = path6.join(root, ".gitignore");
1240
+ const gitignorePath = path8.join(root, ".gitignore");
983
1241
  const expected = [
984
1242
  ".cnos/env/.env",
985
1243
  ".cnos/env/.env.*",
@@ -1011,15 +1269,15 @@ function issueSummary(issues) {
1011
1269
  }
1012
1270
  async function collectYamlFiles(root) {
1013
1271
  try {
1014
- const entries = await readdir(root, { withFileTypes: true });
1272
+ const entries = await readdir2(root, { withFileTypes: true });
1015
1273
  const results = [];
1016
1274
  for (const entry of entries) {
1017
- const target = path6.join(root, entry.name);
1275
+ const target = path8.join(root, entry.name);
1018
1276
  if (entry.isDirectory()) {
1019
1277
  results.push(...await collectYamlFiles(target));
1020
1278
  continue;
1021
1279
  }
1022
- if (entry.isFile() && [".yml", ".yaml"].includes(path6.extname(entry.name).toLowerCase())) {
1280
+ if (entry.isFile() && [".yml", ".yaml"].includes(path8.extname(entry.name).toLowerCase())) {
1023
1281
  results.push(target);
1024
1282
  }
1025
1283
  }
@@ -1044,7 +1302,7 @@ async function checkSecretSecurity(options, runtime) {
1044
1302
  );
1045
1303
  const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
1046
1304
  const secretFiles = await Promise.all(
1047
- runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path6.join(root.path, "secrets")))
1305
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path8.join(root.path, "secrets")))
1048
1306
  );
1049
1307
  const plaintextFiles = [];
1050
1308
  for (const file of secretFiles.flat()) {
@@ -1074,7 +1332,15 @@ async function checkSecretSecurity(options, runtime) {
1074
1332
  };
1075
1333
  }
1076
1334
  async function evaluateDoctor(options = {}) {
1077
- const root = path6.resolve(options.root ?? process.cwd());
1335
+ const root = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
1336
+ const loadedManifest = await loadManifest3({
1337
+ ...options.root ? { root: options.root } : {},
1338
+ ...options.cwd ? { cwd: options.cwd } : {},
1339
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
1340
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
1341
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
1342
+ ...options.forceRefresh ? { forceRefresh: true } : {}
1343
+ });
1078
1344
  const { runtime, summary } = await createValidationSummary(options);
1079
1345
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
1080
1346
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
@@ -1090,6 +1356,11 @@ async function evaluateDoctor(options = {}) {
1090
1356
  ok: true,
1091
1357
  details: `${runtime.graph.workspace.workspaceId} via ${runtime.graph.workspace.workspaceSource}`
1092
1358
  },
1359
+ {
1360
+ name: "root",
1361
+ ok: true,
1362
+ details: loadedManifest.rootResolution.remote ? `${loadedManifest.rootResolution.rootUri} -> ${loadedManifest.manifestRoot}${loadedManifest.rootResolution.immutable ? " | immutable" : " | mutable ref"}${loadedManifest.rootResolution.resolvedCommit ? ` | commit ${loadedManifest.rootResolution.resolvedCommit}` : ""}` : loadedManifest.manifestRoot
1363
+ },
1093
1364
  {
1094
1365
  name: "namespaces",
1095
1366
  ok: true,
@@ -1214,7 +1485,10 @@ async function runExportEnv(options = {}) {
1214
1485
  ...framework ? { framework } : {}
1215
1486
  });
1216
1487
  }
1217
- return `Wrote ${Object.keys(result2.env).length} env vars to ${displayPath(result2.targetPath, options.root ?? process.cwd())}`;
1488
+ return `Wrote ${Object.keys(result2.env).length} env vars to ${displayPath(
1489
+ result2.targetPath,
1490
+ resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
1491
+ )}`;
1218
1492
  }
1219
1493
  const result = await resolveMaterializedEnv(baseOptions);
1220
1494
  if (options.json) {
@@ -1235,7 +1509,7 @@ async function runExport(subcommand, options = {}) {
1235
1509
  var GLOBAL_OPTIONS = [
1236
1510
  {
1237
1511
  flag: "--root <path>",
1238
- description: "Resolve the CNOS project from a specific filesystem root."
1512
+ description: "Resolve the CNOS project from a specific filesystem root or remote root URI."
1239
1513
  },
1240
1514
  {
1241
1515
  flag: "--workspace <id>",
@@ -1249,6 +1523,10 @@ var GLOBAL_OPTIONS = [
1249
1523
  flag: "--global-root <path>",
1250
1524
  description: "Override the configured global CNOS root used for workspace layering."
1251
1525
  },
1526
+ {
1527
+ flag: "--cache-ttl <seconds>",
1528
+ description: "Override the remote-root cache TTL for mutable refs during this invocation."
1529
+ },
1252
1530
  {
1253
1531
  flag: "--json",
1254
1532
  description: "Emit JSON output for commands that support structured responses."
@@ -1263,6 +1541,39 @@ var GLOBAL_OPTIONS = [
1263
1541
  }
1264
1542
  ];
1265
1543
  var COMMANDS = [
1544
+ {
1545
+ id: "cache",
1546
+ summary: "Inspect and manage cached remote roots.",
1547
+ usage: "cnos cache [list|clear|refresh] [root-uri] [global-options]",
1548
+ description: "Lists cached git-backed remote roots, clears cache entries, or forces a refresh for mutable refs.",
1549
+ examples: [
1550
+ "cnos cache list",
1551
+ "cnos cache clear",
1552
+ "cnos cache clear git+https://github.com/org/config.git#v2.1.0",
1553
+ "cnos cache refresh"
1554
+ ]
1555
+ },
1556
+ {
1557
+ id: "cache list",
1558
+ summary: "List cached remote roots.",
1559
+ usage: "cnos cache list [global-options]",
1560
+ description: "Lists git-backed remote roots cached under ~/.cnos/cache together with cache time, resolved commit, immutability, and size.",
1561
+ examples: ["cnos cache list"]
1562
+ },
1563
+ {
1564
+ id: "cache clear",
1565
+ summary: "Clear cached remote roots.",
1566
+ usage: "cnos cache clear [root-uri] [global-options]",
1567
+ description: "Removes all cached remote roots by default, or clears one specific cached root when a full remote URI is provided.",
1568
+ examples: ["cnos cache clear", "cnos cache clear git+https://github.com/org/config.git#main"]
1569
+ },
1570
+ {
1571
+ id: "cache refresh",
1572
+ summary: "Force refresh mutable cached remote roots.",
1573
+ usage: "cnos cache refresh [root-uri] [global-options]",
1574
+ description: "Re-fetches a specific git-backed remote root, or refreshes the active remote root / all mutable cached roots when no URI is provided.",
1575
+ examples: ["cnos cache refresh", "cnos cache refresh git+ssh://git@github.com/org/config.git#main"]
1576
+ },
1266
1577
  {
1267
1578
  id: "init",
1268
1579
  summary: "Scaffold a workspace-aware CNOS tree in the current project.",
@@ -1330,6 +1641,14 @@ var COMMANDS = [
1330
1641
  flag: "--target <local|global>",
1331
1642
  description: "Choose whether writes land in the local project workspace or the configured global root."
1332
1643
  },
1644
+ {
1645
+ flag: "--derive",
1646
+ description: "Write a derived value instead of a literal. Use the second positional value as a template, or combine with --expr."
1647
+ },
1648
+ {
1649
+ flag: "--expr <expression>",
1650
+ description: "With --derive, write an expression-form derived value instead of template shorthand."
1651
+ },
1333
1652
  {
1334
1653
  flag: "--prefix <path>",
1335
1654
  description: "Filter value list output to keys that begin with this logical path or key prefix."
@@ -1338,6 +1657,8 @@ var COMMANDS = [
1338
1657
  examples: [
1339
1658
  "cnos value app.name",
1340
1659
  "cnos value set server.port 3000",
1660
+ "cnos value set app.origin --derive '${value.app.protocol}://${value.app.host}'",
1661
+ `cnos value set app.display_name --derive --expr "coalesce(value.app.custom_name, value.app.name, 'Unnamed')"`,
1341
1662
  "cnos add value app.name demo",
1342
1663
  "cnos value list --prefix app."
1343
1664
  ]
@@ -1345,9 +1666,28 @@ var COMMANDS = [
1345
1666
  {
1346
1667
  id: "value set",
1347
1668
  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"]
1669
+ usage: "cnos value set <path> <value> [--target <local|global>] [--derive] [--expr <expression>] [global-options]",
1670
+ description: "Writes either a literal value or a first-class derived value into the selected workspace or explicit global target.",
1671
+ options: [
1672
+ {
1673
+ flag: "--target <local|global>",
1674
+ description: "Choose whether writes land in the local project workspace or the configured global root."
1675
+ },
1676
+ {
1677
+ flag: "--derive",
1678
+ description: "Interpret the provided value as a derived template, or combine with --expr for expression syntax."
1679
+ },
1680
+ {
1681
+ flag: "--expr <expression>",
1682
+ description: "With --derive, store the value as an expression-form derivation instead of template shorthand."
1683
+ }
1684
+ ],
1685
+ examples: [
1686
+ "cnos value set app.name demo",
1687
+ "cnos value set app.origin --derive '${value.app.protocol}://${value.app.host}'",
1688
+ `cnos value set app.display_name --derive --expr "coalesce(value.app.custom_name, value.app.name, 'Unnamed')"`,
1689
+ "cnos add value server.port 3000 --target global"
1690
+ ]
1351
1691
  },
1352
1692
  {
1353
1693
  id: "value list",
@@ -1520,7 +1860,7 @@ var COMMANDS = [
1520
1860
  id: "list",
1521
1861
  summary: "List resolved config entries.",
1522
1862
  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.",
1863
+ 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
1864
  options: [
1525
1865
  {
1526
1866
  flag: "--namespace <name>",
@@ -1646,7 +1986,7 @@ var COMMANDS = [
1646
1986
  id: "inspect",
1647
1987
  summary: "Inspect the winning value and provenance for a key.",
1648
1988
  usage: "cnos inspect <key> [global-options]",
1649
- description: "Shows the resolved value, namespace, active profile, workspace context, and the loader/origin that won precedence.",
1989
+ 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
1990
  arguments: [
1651
1991
  {
1652
1992
  name: "key",
@@ -2050,6 +2390,7 @@ var HELP_DOCUMENT = {
2050
2390
  examples: [
2051
2391
  "cnos use --profile stage",
2052
2392
  "cnos doctor --workspace api",
2393
+ "cnos cache list",
2053
2394
  "cnos build env --profile stage --to .env.stage",
2054
2395
  "cnos dev env --profile local --to .env.local -- pnpm dev",
2055
2396
  "cnos export env --public --framework vite",
@@ -2165,11 +2506,11 @@ function runHelpAi(topic, cliArgs = []) {
2165
2506
  }
2166
2507
 
2167
2508
  // src/commands/init.ts
2168
- import path8 from "path";
2509
+ import path10 from "path";
2169
2510
 
2170
2511
  // src/services/scaffold.ts
2171
2512
  import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2172
- import path7 from "path";
2513
+ import path9 from "path";
2173
2514
  function scaffoldManifest(projectName, workspace) {
2174
2515
  const lines = [
2175
2516
  "version: 1",
@@ -2208,7 +2549,7 @@ async function ensureFile(filePath, content) {
2208
2549
  }
2209
2550
  }
2210
2551
  async function ensureGitignore(root) {
2211
- const gitignorePath = path7.join(root, ".gitignore");
2552
+ const gitignorePath = path9.join(root, ".gitignore");
2212
2553
  const requiredEntries = [
2213
2554
  ".cnos/env/.env",
2214
2555
  ".cnos/env/.env.*",
@@ -2236,13 +2577,13 @@ async function ensureGitignore(root) {
2236
2577
  return true;
2237
2578
  }
2238
2579
  async function scaffoldWorkspace(root, workspace) {
2239
- const cnosRoot = path7.join(root, ".cnos");
2240
- const workspaceRoot = workspace ? path7.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2580
+ const cnosRoot = path9.join(root, ".cnos");
2581
+ const workspaceRoot = workspace ? path9.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2241
2582
  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 });
2583
+ await mkdir4(path9.join(workspaceRoot, "profiles"), { recursive: true });
2584
+ await mkdir4(path9.join(workspaceRoot, "values"), { recursive: true });
2585
+ await mkdir4(path9.join(workspaceRoot, "secrets"), { recursive: true });
2586
+ await mkdir4(path9.join(workspaceRoot, "env"), { recursive: true });
2246
2587
  const relativePaths = workspace ? [
2247
2588
  ["workspaces", workspace, "profiles", ".gitkeep"],
2248
2589
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -2255,23 +2596,23 @@ async function scaffoldWorkspace(root, workspace) {
2255
2596
  ["env", ".gitkeep"]
2256
2597
  ];
2257
2598
  for (const relativePath of relativePaths) {
2258
- const filePath = path7.join(cnosRoot, ...relativePath);
2599
+ const filePath = path9.join(cnosRoot, ...relativePath);
2259
2600
  if (await ensureFile(filePath, "")) {
2260
- createdPaths.push(path7.relative(root, filePath).replace(/\\/g, "/"));
2601
+ createdPaths.push(path9.relative(root, filePath).replace(/\\/g, "/"));
2261
2602
  }
2262
2603
  }
2263
- if (await ensureFile(path7.join(cnosRoot, "cnos.yml"), scaffoldManifest(path7.basename(root), workspace))) {
2604
+ if (await ensureFile(path9.join(cnosRoot, "cnos.yml"), scaffoldManifest(path9.basename(root), workspace))) {
2264
2605
  createdPaths.push(".cnos/cnos.yml");
2265
2606
  }
2266
2607
  if (await ensureFile(
2267
- path7.join(root, ".cnosrc.yml"),
2608
+ path9.join(root, ".cnosrc.yml"),
2268
2609
  workspace ? `root: ./.cnos
2269
2610
  workspace: ${workspace}
2270
2611
  ` : "root: ./.cnos\n"
2271
2612
  )) {
2272
2613
  createdPaths.push(".cnosrc.yml");
2273
2614
  }
2274
- if (workspace && await ensureFile(path7.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
2615
+ if (workspace && await ensureFile(path9.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
2275
2616
  globalRoot: ~/.cnos
2276
2617
  `)) {
2277
2618
  createdPaths.push(".cnos-workspace.yml");
@@ -2288,7 +2629,7 @@ globalRoot: ~/.cnos
2288
2629
 
2289
2630
  // src/commands/init.ts
2290
2631
  async function runInit(options = {}) {
2291
- const root = path8.resolve(options.root ?? process.cwd());
2632
+ const root = path10.resolve(options.root ?? process.cwd());
2292
2633
  const result = await scaffoldWorkspace(root, options.workspace);
2293
2634
  if (options.json) {
2294
2635
  return printJson(result);
@@ -2324,6 +2665,22 @@ function printInspect(record) {
2324
2665
  `overridden: ${record.overridden.map((entry) => `${entry.sourceId}@${entry.workspaceId}=${String(entry.value)}`).join(", ")}`
2325
2666
  );
2326
2667
  }
2668
+ if (record.derived) {
2669
+ lines.push(`derivedType: ${record.derived.type}`);
2670
+ lines.push(`derivedExpression: ${record.derived.expression}`);
2671
+ lines.push(`runtimeDependent: ${record.derived.runtimeDependent ? "yes" : "no"}`);
2672
+ if (record.derived.runtimeNamespaces.length > 0) {
2673
+ lines.push(`runtimeNamespaces: ${record.derived.runtimeNamespaces.join(", ")}`);
2674
+ }
2675
+ if (record.derived.dependencies.length > 0) {
2676
+ lines.push(
2677
+ `dependencies: ${record.derived.dependencies.map((entry) => `${entry.key}=${String(entry.value)}${entry.runtimeNamespace ? ` (${entry.runtimeNamespace})` : ""}`).join(", ")}`
2678
+ );
2679
+ }
2680
+ if (record.derived.promotionWarning) {
2681
+ lines.push(`warning: ${record.derived.promotionWarning}`);
2682
+ }
2683
+ }
2327
2684
  return lines.join("\n");
2328
2685
  }
2329
2686
 
@@ -2391,13 +2748,24 @@ function toStoredEntry(namespace, entry, filter = {}) {
2391
2748
  }
2392
2749
  return {
2393
2750
  key: entry.key,
2394
- value: selectedCandidate.value
2751
+ value: selectedCandidate.value,
2752
+ ...typeof selectedCandidate.value === "object" && selectedCandidate.value !== null && !Array.isArray(selectedCandidate.value) && "$derive" in selectedCandidate.value ? {
2753
+ derived: true
2754
+ } : {}
2395
2755
  };
2396
2756
  }
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
- );
2757
+ async function listStoredNamespace(namespace, options) {
2758
+ const runtime = await createRuntimeService(options);
2759
+ return Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === namespace).map((entry) => {
2760
+ const stored = toStoredEntry(namespace, entry, options);
2761
+ if (!stored) {
2762
+ return void 0;
2763
+ }
2764
+ return {
2765
+ ...stored,
2766
+ value: stored.derived ? runtime.read(entry.key) : stored.value
2767
+ };
2768
+ }).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
2769
  }
2402
2770
  function listProjectedNamespace(namespace, options) {
2403
2771
  return createRuntimeService(options).then((runtime) => {
@@ -2479,14 +2847,14 @@ async function runList(args = [], options = {}) {
2479
2847
  if (entries.length === 0) {
2480
2848
  return "";
2481
2849
  }
2482
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
2850
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
2483
2851
  }
2484
2852
 
2485
2853
  // src/commands/migrate.ts
2486
- import path9 from "path";
2854
+ import path11 from "path";
2487
2855
  import {
2488
2856
  applyManifestMappings,
2489
- loadManifest,
2857
+ loadManifest as loadManifest4,
2490
2858
  proposeMapping,
2491
2859
  rewriteSourceFiles,
2492
2860
  scanEnvUsage
@@ -2500,8 +2868,18 @@ async function runMigrate(options = {}) {
2500
2868
  if (cliArgs.length > 0) {
2501
2869
  throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
2502
2870
  }
2503
- const manifest = await loadManifest(options.root ? { root: options.root } : {});
2504
- const scanRoot = path9.resolve(manifest.repoRoot, scan ?? "src");
2871
+ if (apply) {
2872
+ await assertWritableConfigRoot("apply migration mappings", options);
2873
+ }
2874
+ const manifest = await loadManifest4({
2875
+ ...options.root ? { root: options.root } : {},
2876
+ ...options.cwd ? { cwd: options.cwd } : {},
2877
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
2878
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
2879
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
2880
+ ...options.forceRefresh ? { forceRefresh: true } : {}
2881
+ });
2882
+ const scanRoot = path11.resolve(manifest.consumerRoot, scan ?? "src");
2505
2883
  const usages = await scanEnvUsage(scanRoot);
2506
2884
  const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
2507
2885
  const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
@@ -2563,7 +2941,7 @@ async function runMigrate(options = {}) {
2563
2941
  }
2564
2942
 
2565
2943
  // src/commands/namespace.ts
2566
- import path10 from "path";
2944
+ import path12 from "path";
2567
2945
  function normalizeCommand2(args) {
2568
2946
  const [actionOrPath, ...tail] = args;
2569
2947
  if (!actionOrPath) {
@@ -2586,7 +2964,7 @@ function normalizeCommand2(args) {
2586
2964
  async function runNamespace(namespace, args = [], options = {}) {
2587
2965
  const { action, tail } = normalizeCommand2(args);
2588
2966
  const cliArgs = [...options.cliArgs ?? []];
2589
- const root = path10.resolve(options.root ?? process.cwd());
2967
+ const root = path12.resolve(options.root ?? process.cwd());
2590
2968
  if (action === "list") {
2591
2969
  const prefix = consumeOption(cliArgs, "--prefix");
2592
2970
  const entries = await listConfigEntries(namespace, {
@@ -2597,16 +2975,24 @@ async function runNamespace(namespace, args = [], options = {}) {
2597
2975
  if (options.json) {
2598
2976
  return printJson(entries);
2599
2977
  }
2600
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
2978
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
2601
2979
  }
2602
2980
  if (action === "set") {
2603
2981
  const configPath2 = tail[0] ?? "app.name";
2604
- const rawValue = tail[1] ?? "";
2982
+ const derive = consumeFlag(cliArgs, "--derive");
2983
+ const expr = consumeOption(cliArgs, "--expr");
2984
+ const deriveArg = derive && !expr && cliArgs[0] && !cliArgs[0].startsWith("--") ? cliArgs.shift() : void 0;
2985
+ const rawValue = derive ? "" : tail[1] ?? "";
2986
+ const deriveExpression = derive ? expr ?? tail[1] ?? deriveArg ?? "" : void 0;
2605
2987
  const target = consumeOption(cliArgs, "--target") ?? "local";
2606
2988
  const result = await defineValue(namespace, configPath2, rawValue, {
2607
2989
  ...options,
2608
2990
  cliArgs,
2609
- target
2991
+ target,
2992
+ ...deriveExpression !== void 0 ? {
2993
+ deriveExpression,
2994
+ deriveExprMode: Boolean(expr)
2995
+ } : {}
2610
2996
  });
2611
2997
  if (options.json) {
2612
2998
  return printJson({
@@ -2648,34 +3034,34 @@ async function runNamespace(namespace, args = [], options = {}) {
2648
3034
  }
2649
3035
 
2650
3036
  // src/commands/onboard.ts
2651
- import { copyFile, readdir as readdir2, rm } from "fs/promises";
2652
- import path11 from "path";
3037
+ import { copyFile, readdir as readdir3, rm as rm2 } from "fs/promises";
3038
+ import path13 from "path";
2653
3039
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
2654
3040
  async function listRootEnvFiles(root) {
2655
- const entries = await readdir2(root, { withFileTypes: true });
3041
+ const entries = await readdir3(root, { withFileTypes: true });
2656
3042
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
2657
3043
  }
2658
3044
  async function runOnboard(options = {}) {
2659
- const root = path11.resolve(options.root ?? process.cwd());
2660
- const workspace = options.workspace ?? path11.basename(root);
3045
+ const root = path13.resolve(options.root ?? process.cwd());
3046
+ const workspace = options.workspace ?? path13.basename(root);
2661
3047
  const cliArgs = [...options.cliArgs ?? []];
2662
3048
  const move = consumeFlag(cliArgs, "--move");
2663
3049
  if (cliArgs.length > 0) {
2664
3050
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
2665
3051
  }
2666
3052
  const scaffold = await scaffoldWorkspace(root, workspace);
2667
- const envRoot = path11.join(root, ".cnos", "workspaces", workspace, "env");
3053
+ const envRoot = path13.join(root, ".cnos", "workspaces", workspace, "env");
2668
3054
  const rootFiles = await listRootEnvFiles(root);
2669
3055
  const imported = [];
2670
3056
  const skipped = [];
2671
3057
  for (const fileName of rootFiles) {
2672
- const sourcePath = path11.join(root, fileName);
2673
- const targetPath = path11.join(envRoot, fileName);
3058
+ const sourcePath = path13.join(root, fileName);
3059
+ const targetPath = path13.join(envRoot, fileName);
2674
3060
  try {
2675
3061
  await copyFile(sourcePath, targetPath);
2676
- imported.push(path11.relative(root, targetPath).replace(/\\/g, "/"));
3062
+ imported.push(path13.relative(root, targetPath).replace(/\\/g, "/"));
2677
3063
  if (move) {
2678
- await rm(sourcePath);
3064
+ await rm2(sourcePath);
2679
3065
  }
2680
3066
  } catch {
2681
3067
  skipped.push(fileName);
@@ -2698,14 +3084,14 @@ async function runOnboard(options = {}) {
2698
3084
  }
2699
3085
 
2700
3086
  // src/commands/profile.ts
2701
- import path14 from "path";
3087
+ import path16 from "path";
2702
3088
 
2703
3089
  // src/services/context.ts
2704
3090
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
2705
- import path12 from "path";
3091
+ import path14 from "path";
2706
3092
  import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2707
3093
  async function loadCliContext(root = process.cwd()) {
2708
- const filePath = path12.join(path12.resolve(root), ".cnos-workspace.yml");
3094
+ const filePath = path14.join(path14.resolve(root), ".cnos-workspace.yml");
2709
3095
  try {
2710
3096
  const source = await readFile4(filePath, "utf8");
2711
3097
  const parsed = parseYaml3(source);
@@ -2718,8 +3104,8 @@ async function loadCliContext(root = process.cwd()) {
2718
3104
  }
2719
3105
  }
2720
3106
  async function saveCliContext(options = {}) {
2721
- const root = path12.resolve(options.root ?? process.cwd());
2722
- const filePath = path12.join(root, ".cnos-workspace.yml");
3107
+ const root = path14.resolve(options.root ?? process.cwd());
3108
+ const filePath = path14.join(root, ".cnos-workspace.yml");
2723
3109
  const current = await loadCliContext(root);
2724
3110
  const next = {
2725
3111
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -2737,12 +3123,21 @@ async function saveCliContext(options = {}) {
2737
3123
  }
2738
3124
 
2739
3125
  // 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";
3126
+ import { mkdir as mkdir5, readdir as readdir4, readFile as readFile5, rm as rm3, writeFile as writeFile6 } from "fs/promises";
3127
+ import path15 from "path";
3128
+ import { loadManifest as loadManifest5, parseYaml as parseYaml4, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
3129
+ async function resolveProfilesRoot(root = process.cwd()) {
3130
+ try {
3131
+ const loadedManifest = await loadManifest5({ root });
3132
+ return path15.join(loadedManifest.manifestRoot, "profiles");
3133
+ } catch {
3134
+ const loadedManifest = await loadManifest5({ cwd: root });
3135
+ return path15.join(loadedManifest.manifestRoot, "profiles");
3136
+ }
3137
+ }
2743
3138
  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 });
3139
+ const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
3140
+ await mkdir5(path15.dirname(filePath), { recursive: true });
2746
3141
  const document = options.noInherit ? {
2747
3142
  name: profile,
2748
3143
  activate: {
@@ -2765,9 +3160,9 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit, o
2765
3160
  };
2766
3161
  }
2767
3162
  async function listProfiles(root = process.cwd()) {
2768
- const profilesRoot = path13.join(path13.resolve(root), ".cnos", "profiles");
3163
+ const profilesRoot = await resolveProfilesRoot(root);
2769
3164
  try {
2770
- const entries = await readdir3(profilesRoot, { withFileTypes: true });
3165
+ const entries = await readdir4(profilesRoot, { withFileTypes: true });
2771
3166
  const discovered = /* @__PURE__ */ new Set(["base"]);
2772
3167
  for (const entry of entries) {
2773
3168
  if (entry.isFile() && entry.name.endsWith(".yml")) {
@@ -2780,9 +3175,9 @@ async function listProfiles(root = process.cwd()) {
2780
3175
  }
2781
3176
  }
2782
3177
  async function deleteProfileDefinition(root = process.cwd(), profile) {
2783
- const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
3178
+ const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
2784
3179
  try {
2785
- await rm2(filePath);
3180
+ await rm3(filePath);
2786
3181
  return {
2787
3182
  filePath,
2788
3183
  deleted: true
@@ -2800,7 +3195,7 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
2800
3195
  name: "base"
2801
3196
  };
2802
3197
  }
2803
- const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
3198
+ const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
2804
3199
  try {
2805
3200
  return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
2806
3201
  } catch {
@@ -2824,9 +3219,11 @@ function normalizeProfileAction(args) {
2824
3219
  }
2825
3220
  async function runProfile(args, options = {}) {
2826
3221
  const { action, tail } = normalizeProfileAction(args);
2827
- const root = path14.resolve(options.root ?? process.cwd());
3222
+ const root = options.root ?? process.cwd();
3223
+ const displayRoot = resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd());
2828
3224
  const cliArgs = [...options.cliArgs ?? []];
2829
3225
  if (action === "create") {
3226
+ await assertWritableConfigRoot(`create profile ${tail[0] ?? "stage"}`, options);
2830
3227
  const profile = tail[0] ?? "stage";
2831
3228
  const inherit = consumeOption(cliArgs, "--inherit");
2832
3229
  const noInherit = consumeFlag(cliArgs, "--no-inherit");
@@ -2838,22 +3235,23 @@ async function runProfile(args, options = {}) {
2838
3235
  return printJson(result);
2839
3236
  }
2840
3237
  if (noInherit) {
2841
- return `created profile ${profile} at ${displayPath(result.filePath, root)} without inheriting base`;
3238
+ return `created profile ${profile} at ${displayPath(result.filePath, displayRoot)} without inheriting base`;
2842
3239
  }
2843
- return `created profile ${profile} at ${displayPath(result.filePath, root)}; inherits values from base by default`;
3240
+ return `created profile ${profile} at ${displayPath(result.filePath, displayRoot)}; inherits values from base by default`;
2844
3241
  }
2845
3242
  if (action === "use") {
2846
3243
  const profile = tail[0] ?? "base";
2847
3244
  const result = await saveCliContext({
2848
- root,
3245
+ root: path16.resolve(root),
2849
3246
  profile
2850
3247
  });
2851
3248
  if (options.json) {
2852
3249
  return printJson(result);
2853
3250
  }
2854
- return `active profile set to ${profile} in ${displayPath(result.filePath, root)}`;
3251
+ return `active profile set to ${profile} in ${displayPath(result.filePath, displayRoot)}`;
2855
3252
  }
2856
3253
  if (action === "delete") {
3254
+ await assertWritableConfigRoot(`delete profile ${tail[0] ?? "base"}`, options);
2857
3255
  const profile = tail[0] ?? "base";
2858
3256
  const result = await deleteProfileDefinition(root, profile);
2859
3257
  if (options.json) {
@@ -2875,11 +3273,11 @@ async function runProfile(args, options = {}) {
2875
3273
  }
2876
3274
 
2877
3275
  // src/commands/promote.ts
2878
- import path15 from "path";
3276
+ import path17 from "path";
2879
3277
  import { writeFile as writeFile7 } from "fs/promises";
2880
3278
  import {
2881
3279
  ensureProjectionAllowed,
2882
- loadManifest as loadManifest2,
3280
+ loadManifest as loadManifest6,
2883
3281
  stringifyYaml as stringifyYaml5
2884
3282
  } from "@kitsy/cnos/internal";
2885
3283
  function normalizeTarget(value) {
@@ -2892,7 +3290,7 @@ function sortRecord(record) {
2892
3290
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
2893
3291
  }
2894
3292
  async function runPromote(args = [], options = {}) {
2895
- const root = path15.resolve(options.root ?? process.cwd());
3293
+ const root = path17.resolve(options.root ?? process.cwd());
2896
3294
  const cliArgs = [...options.cliArgs ?? []];
2897
3295
  const target = normalizeTarget(consumeOption(cliArgs, "--to"));
2898
3296
  const alias = consumeOption(cliArgs, "--as");
@@ -2908,7 +3306,15 @@ async function runPromote(args = [], options = {}) {
2908
3306
  throw new Error("promote --to env requires --as <ENV_VAR>");
2909
3307
  }
2910
3308
  }
2911
- const loadedManifest = await loadManifest2(options.root ? { root: options.root } : {});
3309
+ const loadedManifest = await loadManifest6({
3310
+ ...options.root ? { root: options.root } : {},
3311
+ ...options.cwd ? { cwd: options.cwd } : {},
3312
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3313
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3314
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3315
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3316
+ });
3317
+ await assertWritableConfigRoot(`promote ${keys.join(", ")}`, options);
2912
3318
  for (const key of keys) {
2913
3319
  ensureProjectionAllowed(loadedManifest.manifest, key, target);
2914
3320
  }
@@ -3053,21 +3459,21 @@ async function runCommand(command, options = {}) {
3053
3459
  }
3054
3460
 
3055
3461
  // src/commands/secret.ts
3056
- import path18 from "path";
3462
+ import path20 from "path";
3057
3463
 
3058
3464
  // src/commands/vault.ts
3059
- import path17 from "path";
3465
+ import path19 from "path";
3060
3466
 
3061
3467
  // src/services/vaults.ts
3062
- import { rm as rm3, writeFile as writeFile8 } from "fs/promises";
3063
- import path16 from "path";
3468
+ import { rm as rm4, writeFile as writeFile8 } from "fs/promises";
3469
+ import path18 from "path";
3064
3470
  import {
3065
3471
  clearAllVaultSessionKeys,
3066
3472
  clearVaultSessionKey,
3067
3473
  createSecretVault,
3068
3474
  deriveVaultKey,
3069
3475
  listLocalSecrets,
3070
- loadManifest as loadManifest3,
3476
+ loadManifest as loadManifest7,
3071
3477
  listSecretVaults,
3072
3478
  readVaultMetadata,
3073
3479
  resolveSecretStoreRoot as resolveSecretStoreRoot2,
@@ -3100,12 +3506,20 @@ function defaultLocalAuthSources(vault) {
3100
3506
  return [`env:CNOS_SECRET_PASSPHRASE_${token}`, "env:CNOS_SECRET_PASSPHRASE", `keychain:cnos/${vault}`, "prompt"];
3101
3507
  }
3102
3508
  async function createVaultDefinition(name, options = {}) {
3509
+ await assertWritableConfigRoot(`create vault ${name}`, options);
3103
3510
  const vault = name.trim() || "default";
3104
3511
  const provider = options.provider?.trim() || "local";
3105
3512
  if (provider === "local" && (options.noPassphrase ?? false)) {
3106
3513
  throw new Error("Local vaults cannot be passwordless.");
3107
3514
  }
3108
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3515
+ const loadedManifest = await loadManifest7({
3516
+ ...options.root ? { root: options.root } : {},
3517
+ ...options.cwd ? { cwd: options.cwd } : {},
3518
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3519
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3520
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3521
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3522
+ });
3109
3523
  const vaultDefinition = buildVaultDefinition(vault, provider);
3110
3524
  const rawManifest = {
3111
3525
  ...loadedManifest.rawManifest,
@@ -3135,7 +3549,14 @@ async function createVaultDefinition(name, options = {}) {
3135
3549
  };
3136
3550
  }
3137
3551
  async function listVaultDefinitions(options = {}) {
3138
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3552
+ const loadedManifest = await loadManifest7({
3553
+ ...options.root ? { root: options.root } : {},
3554
+ ...options.cwd ? { cwd: options.cwd } : {},
3555
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3556
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3557
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3558
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3559
+ });
3139
3560
  const localStoreVaults = await listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
3140
3561
  return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
3141
3562
  const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
@@ -3147,8 +3568,16 @@ async function listVaultDefinitions(options = {}) {
3147
3568
  });
3148
3569
  }
3149
3570
  async function removeVaultDefinition(name, options = {}) {
3571
+ await assertWritableConfigRoot(`remove vault ${name}`, options);
3150
3572
  const vault = name.trim() || "default";
3151
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3573
+ const loadedManifest = await loadManifest7({
3574
+ ...options.root ? { root: options.root } : {},
3575
+ ...options.cwd ? { cwd: options.cwd } : {},
3576
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3577
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3578
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3579
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3580
+ });
3152
3581
  if (!loadedManifest.rawManifest.vaults?.[vault]) {
3153
3582
  return {
3154
3583
  name: vault,
@@ -3166,10 +3595,10 @@ async function removeVaultDefinition(name, options = {}) {
3166
3595
  delete rawManifest.vaults;
3167
3596
  }
3168
3597
  await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
3169
- const vaultRoot = path16.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3598
+ const vaultRoot = path18.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3170
3599
  let removedStore;
3171
3600
  try {
3172
- await rm3(vaultRoot, { recursive: true, force: true });
3601
+ await rm4(vaultRoot, { recursive: true, force: true });
3173
3602
  removedStore = vaultRoot;
3174
3603
  } catch {
3175
3604
  removedStore = void 0;
@@ -3187,7 +3616,14 @@ async function listLocalStoreVaults(options = {}) {
3187
3616
  }
3188
3617
  async function authenticateVault(name, options = {}) {
3189
3618
  const vault = name.trim() || "default";
3190
- const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
3619
+ const loadedManifest = await loadManifest7({
3620
+ ...options.root ? { root: options.root } : {},
3621
+ ...options.cwd ? { cwd: options.cwd } : {},
3622
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
3623
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
3624
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
3625
+ ...options.forceRefresh ? { forceRefresh: true } : {}
3626
+ });
3191
3627
  const definition = loadedManifest.manifest.vaults[vault];
3192
3628
  if (!definition) {
3193
3629
  throw new Error(`Unknown vault "${vault}"`);
@@ -3260,7 +3696,7 @@ function normalizeVaultAction(args) {
3260
3696
  async function runVault(args = [], options = {}) {
3261
3697
  const { action, tail } = normalizeVaultAction(args);
3262
3698
  const cliArgs = [...options.cliArgs ?? []];
3263
- const root = path17.resolve(options.root ?? process.cwd());
3699
+ const root = path19.resolve(options.root ?? process.cwd());
3264
3700
  if (consumeOption(cliArgs, "--passphrase")) {
3265
3701
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
3266
3702
  }
@@ -3366,7 +3802,7 @@ async function runSecret(argsOrPath, options = {}) {
3366
3802
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
3367
3803
  const { action, tail } = normalizeSecretCommand(args);
3368
3804
  const cliArgs = [...options.cliArgs ?? []];
3369
- const root = path18.resolve(options.root ?? process.cwd());
3805
+ const root = path20.resolve(options.root ?? process.cwd());
3370
3806
  if (consumeOption(cliArgs, "--passphrase")) {
3371
3807
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
3372
3808
  }
@@ -3462,9 +3898,9 @@ async function runSecret(argsOrPath, options = {}) {
3462
3898
  }
3463
3899
 
3464
3900
  // src/commands/use.ts
3465
- import path19 from "path";
3901
+ import path21 from "path";
3466
3902
  async function runUse(args = [], options = {}) {
3467
- const root = path19.resolve(options.root ?? process.cwd());
3903
+ const root = path21.resolve(options.root ?? process.cwd());
3468
3904
  const action = args[0];
3469
3905
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
3470
3906
  if (action === "show" || !action && !hasUpdates) {
@@ -3501,7 +3937,7 @@ async function runValidate(options = {}) {
3501
3937
  // package.json
3502
3938
  var package_default = {
3503
3939
  name: "@kitsy/cnos-cli",
3504
- version: "1.6.1",
3940
+ version: "1.7.0",
3505
3941
  description: "CLI entry point and developer tooling for CNOS.",
3506
3942
  type: "module",
3507
3943
  main: "./dist/index.js",
@@ -3556,7 +3992,7 @@ function runVersion() {
3556
3992
  }
3557
3993
 
3558
3994
  // src/commands/value.ts
3559
- import path20 from "path";
3995
+ import path22 from "path";
3560
3996
  function normalizeValueCommand(args) {
3561
3997
  const [actionOrPath, ...tail] = args;
3562
3998
  if (!actionOrPath) {
@@ -3580,7 +4016,7 @@ async function runValue(argsOrPath, options = {}) {
3580
4016
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
3581
4017
  const { action, tail } = normalizeValueCommand(args);
3582
4018
  const cliArgs = [...options.cliArgs ?? []];
3583
- const root = path20.resolve(options.root ?? process.cwd());
4019
+ const root = path22.resolve(options.root ?? process.cwd());
3584
4020
  if (action === "list") {
3585
4021
  const prefix = consumeOption(cliArgs, "--prefix");
3586
4022
  const entries = await listConfigEntries("value", {
@@ -3591,16 +4027,24 @@ async function runValue(argsOrPath, options = {}) {
3591
4027
  if (options.json) {
3592
4028
  return printJson(entries);
3593
4029
  }
3594
- return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
4030
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}${entry.derived ? " (derived)" : ""}`).join("\n");
3595
4031
  }
3596
4032
  if (action === "set") {
3597
4033
  const valuePath = tail[0] ?? "app.name";
3598
- const rawValue = tail[1] ?? "";
4034
+ const derive = consumeFlag(cliArgs, "--derive");
4035
+ const expr = consumeOption(cliArgs, "--expr");
4036
+ const deriveArg = derive && !expr && cliArgs[0] && !cliArgs[0].startsWith("--") ? cliArgs.shift() : void 0;
4037
+ const rawValue = derive ? "" : tail[1] ?? "";
4038
+ const deriveExpression = derive ? expr ?? tail[1] ?? deriveArg ?? "" : void 0;
3599
4039
  const target = consumeOption(cliArgs, "--target") ?? "local";
3600
4040
  const result = await defineValue("value", valuePath, rawValue, {
3601
4041
  ...options,
3602
4042
  cliArgs,
3603
- target
4043
+ target,
4044
+ ...deriveExpression !== void 0 ? {
4045
+ deriveExpression,
4046
+ deriveExprMode: Boolean(expr)
4047
+ } : {}
3604
4048
  });
3605
4049
  if (options.json) {
3606
4050
  return printJson({
@@ -3656,6 +4100,7 @@ async function buildRunEnvironment(options) {
3656
4100
  const prefix = consumeOption(cliArgs, "--prefix");
3657
4101
  const runtime = await createRuntimeService({
3658
4102
  ...options,
4103
+ cacheMode: "dev",
3659
4104
  cliArgs
3660
4105
  });
3661
4106
  const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
@@ -3697,12 +4142,14 @@ async function startWatchLoop(options) {
3697
4142
  const root = options.root ?? process.cwd();
3698
4143
  let current = await buildRunEnvironment({
3699
4144
  ...options,
4145
+ cacheMode: "dev",
3700
4146
  cliArgs
3701
4147
  });
3702
4148
  let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
3703
4149
  let closed = false;
3704
4150
  const watcher = await startGraphWatchLoop({
3705
4151
  ...options,
4152
+ cacheMode: "dev",
3706
4153
  cliArgs,
3707
4154
  debounceMs,
3708
4155
  async onChange(payload) {
@@ -3711,6 +4158,7 @@ async function startWatchLoop(options) {
3711
4158
  }
3712
4159
  current = await buildRunEnvironment({
3713
4160
  ...options,
4161
+ cacheMode: "dev",
3714
4162
  cliArgs
3715
4163
  });
3716
4164
  if (isSignal) {
@@ -3764,12 +4212,12 @@ async function runWatch(command, options = {}) {
3764
4212
  }
3765
4213
 
3766
4214
  // 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";
4215
+ import { cp, mkdir as mkdir6, rename, rm as rm5, stat as stat2, writeFile as writeFile9, readFile as readFile6 } from "fs/promises";
4216
+ import path23 from "path";
4217
+ import { loadManifest as loadManifest8, parseYaml as parseYaml5, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
3770
4218
  async function exists(targetPath) {
3771
4219
  try {
3772
- await stat(targetPath);
4220
+ await stat2(targetPath);
3773
4221
  return true;
3774
4222
  } catch {
3775
4223
  return false;
@@ -3779,22 +4227,22 @@ async function copyIfExists(source, target) {
3779
4227
  if (!await exists(source)) {
3780
4228
  return;
3781
4229
  }
3782
- await mkdir6(path21.dirname(target), { recursive: true });
4230
+ await mkdir6(path23.dirname(target), { recursive: true });
3783
4231
  await cp(source, target, { recursive: true, force: true });
3784
4232
  }
3785
4233
  async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
3786
4234
  for (const sourceRoot of sourceRoots) {
3787
4235
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
3788
4236
  await copyIfExists(
3789
- path21.join(sourceRoot, folderName),
3790
- path21.join(targetCnosRoot, folderName)
4237
+ path23.join(sourceRoot, folderName),
4238
+ path23.join(targetCnosRoot, folderName)
3791
4239
  );
3792
4240
  }
3793
4241
  }
3794
4242
  }
3795
4243
  async function writeCnosrc(packageRoot, config) {
3796
4244
  await writeFile9(
3797
- path21.join(packageRoot, ".cnosrc.yml"),
4245
+ path23.join(packageRoot, ".cnosrc.yml"),
3798
4246
  stringifyYaml7({
3799
4247
  root: config.root,
3800
4248
  ...config.workspace ? { workspace: config.workspace } : {}
@@ -3810,17 +4258,17 @@ function createDetachedManifest(rawManifest) {
3810
4258
  return next;
3811
4259
  }
3812
4260
  async function runDetach(packageRoot, options = {}) {
3813
- const loaded = await loadManifest4({ cwd: packageRoot });
4261
+ const loaded = await loadManifest8({ cwd: packageRoot });
3814
4262
  if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
3815
4263
  throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
3816
4264
  }
3817
- const targetCnosRoot = path21.join(packageRoot, ".cnos");
4265
+ const targetCnosRoot = path23.join(packageRoot, ".cnos");
3818
4266
  const force = consumeFlag([...options.cliArgs ?? []], "--force");
3819
4267
  if (await exists(targetCnosRoot) && !force) {
3820
4268
  throw new Error(`Refusing to detach because ${displayPath(targetCnosRoot, packageRoot)} already exists. Use --force to overwrite.`);
3821
4269
  }
3822
4270
  if (force) {
3823
- await rm4(targetCnosRoot, { recursive: true, force: true });
4271
+ await rm5(targetCnosRoot, { recursive: true, force: true });
3824
4272
  }
3825
4273
  const runtime = await createRuntimeService({
3826
4274
  ...options,
@@ -3831,11 +4279,11 @@ async function runDetach(packageRoot, options = {}) {
3831
4279
  await mkdir6(targetCnosRoot, { recursive: true });
3832
4280
  await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
3833
4281
  await writeFile9(
3834
- path21.join(targetCnosRoot, "cnos.yml"),
4282
+ path23.join(targetCnosRoot, "cnos.yml"),
3835
4283
  stringifyYaml7(createDetachedManifest(loaded.rawManifest)),
3836
4284
  "utf8"
3837
4285
  );
3838
- const relativeRoot = path21.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
4286
+ const relativeRoot = path23.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
3839
4287
  const marker = {
3840
4288
  detachedFrom: relativeRoot || ".",
3841
4289
  detachedWorkspace: loaded.anchoredWorkspace,
@@ -3845,7 +4293,7 @@ async function runDetach(packageRoot, options = {}) {
3845
4293
  workspace: loaded.anchoredWorkspace
3846
4294
  }
3847
4295
  };
3848
- await writeFile9(path21.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
4296
+ await writeFile9(path23.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
3849
4297
  await writeCnosrc(packageRoot, { root: "./.cnos" });
3850
4298
  if (options.json) {
3851
4299
  return printJson({
@@ -3859,8 +4307,8 @@ async function runDetach(packageRoot, options = {}) {
3859
4307
  async function runAttach(packageRoot, options = {}) {
3860
4308
  const cliArgs = [...options.cliArgs ?? []];
3861
4309
  const force = consumeFlag(cliArgs, "--force");
3862
- const childCnosRoot = path21.join(packageRoot, ".cnos");
3863
- const markerPath = path21.join(childCnosRoot, ".detached");
4310
+ const childCnosRoot = path23.join(packageRoot, ".cnos");
4311
+ const markerPath = path23.join(childCnosRoot, ".detached");
3864
4312
  if (!await exists(markerPath)) {
3865
4313
  throw new Error("workspace attach requires a detached package with .cnos/.detached");
3866
4314
  }
@@ -3868,19 +4316,24 @@ async function runAttach(packageRoot, options = {}) {
3868
4316
  if (!marker?.originalCnosrc?.root || !marker.detachedWorkspace) {
3869
4317
  throw new Error("Invalid .detached marker");
3870
4318
  }
3871
- const parentManifestRoot = path21.resolve(packageRoot, marker.originalCnosrc.root);
3872
- const parentLoaded = await loadManifest4({ root: parentManifestRoot });
4319
+ const parentManifestRoot = path23.resolve(packageRoot, marker.originalCnosrc.root);
4320
+ const parentLoaded = await loadManifest8({ root: parentManifestRoot });
4321
+ if (parentLoaded.rootResolution.readOnly) {
4322
+ throw new Error(
4323
+ `Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
4324
+ );
4325
+ }
3873
4326
  const workspaceId = marker.originalCnosrc.workspace ?? marker.detachedWorkspace;
3874
- const parentWorkspaceRoot = path21.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
4327
+ const parentWorkspaceRoot = path23.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
3875
4328
  if (await exists(parentWorkspaceRoot) && !force) {
3876
4329
  throw new Error(`workspace "${workspaceId}" already exists in parent root. Use --force to overwrite.`);
3877
4330
  }
3878
4331
  if (force) {
3879
- await rm4(parentWorkspaceRoot, { recursive: true, force: true });
4332
+ await rm5(parentWorkspaceRoot, { recursive: true, force: true });
3880
4333
  }
3881
4334
  await mkdir6(parentWorkspaceRoot, { recursive: true });
3882
4335
  for (const folderName of ["values", "secrets", "env", "profiles"]) {
3883
- await copyIfExists(path21.join(childCnosRoot, folderName), path21.join(parentWorkspaceRoot, folderName));
4336
+ await copyIfExists(path23.join(childCnosRoot, folderName), path23.join(parentWorkspaceRoot, folderName));
3884
4337
  }
3885
4338
  const rawManifest = parentLoaded.rawManifest;
3886
4339
  const workspaces = rawManifest.workspaces ?? {};
@@ -3888,9 +4341,9 @@ async function runAttach(packageRoot, options = {}) {
3888
4341
  items[workspaceId] = items[workspaceId] ?? {};
3889
4342
  workspaces.items = items;
3890
4343
  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 });
4344
+ await writeFile9(path23.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
4345
+ const archivePath = path23.join(packageRoot, ".cnos.detached.bak");
4346
+ await rm5(archivePath, { recursive: true, force: true });
3894
4347
  await rename(childCnosRoot, archivePath);
3895
4348
  await writeCnosrc(packageRoot, {
3896
4349
  root: marker.originalCnosrc.root,
@@ -3909,7 +4362,7 @@ async function runAttach(packageRoot, options = {}) {
3909
4362
  async function runWorkspace(args = [], options = {}) {
3910
4363
  const [action] = args;
3911
4364
  const cliArgs = [...options.cliArgs ?? []];
3912
- const packageRoot = path21.resolve(consumeOption(cliArgs, "--package-root") ?? options.root ?? process.cwd());
4365
+ const packageRoot = path23.resolve(consumeOption(cliArgs, "--package-root") ?? options.root ?? process.cwd());
3913
4366
  switch (action) {
3914
4367
  case "detach":
3915
4368
  return runDetach(packageRoot, { ...options, cliArgs });
@@ -3934,6 +4387,9 @@ function resolveHelpTopic(command, args) {
3934
4387
  if (command === "build" && args[0] && ["env", "server", "browser", "public"].includes(args[0])) {
3935
4388
  return normalizeHelpTopic([command, args[0]]);
3936
4389
  }
4390
+ if (command === "cache" && args[0] && ["list", "clear", "refresh"].includes(args[0])) {
4391
+ return normalizeHelpTopic([command, args[0]]);
4392
+ }
3937
4393
  if (command === "dev" && args[0] === "env") {
3938
4394
  return normalizeHelpTopic([command, args[0]]);
3939
4395
  }
@@ -3983,6 +4439,9 @@ async function main(argv) {
3983
4439
  ...options.globalRoot ? {
3984
4440
  globalRoot: options.globalRoot
3985
4441
  } : {},
4442
+ ...typeof options.cacheTtlSeconds === "number" ? {
4443
+ cacheTtlSeconds: options.cacheTtlSeconds
4444
+ } : {},
3986
4445
  ...options.json ? {
3987
4446
  json: true
3988
4447
  } : {},
@@ -4074,6 +4533,10 @@ async function main(argv) {
4074
4533
  return;
4075
4534
  case "build":
4076
4535
  process.stdout.write(`${await runBuild(args[0], runtimeOptions)}
4536
+ `);
4537
+ return;
4538
+ case "cache":
4539
+ process.stdout.write(`${await runCache(args, runtimeOptions)}
4077
4540
  `);
4078
4541
  return;
4079
4542
  case "dev":