@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.
- package/dist/index.js +881 -177
- 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
|
|
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 =
|
|
315
|
-
await mkdir(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
470
|
+
return `built ${subcommand} artifact at ${displayPath(
|
|
471
|
+
targetPath,
|
|
472
|
+
resolveFilesystemBasePath(options.root, options.cwd ?? process.cwd())
|
|
473
|
+
)}`;
|
|
417
474
|
}
|
|
418
475
|
|
|
419
|
-
// src/
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
1274
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
1015
1275
|
const results = [];
|
|
1016
1276
|
for (const entry of entries) {
|
|
1017
|
-
const target =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
1350
|
-
|
|
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
|
|
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: "
|
|
1878
|
-
usage: "cnos workspace <attach|detach> [options] [global-options]",
|
|
1879
|
-
description: "
|
|
1880
|
-
examples: [
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2239
|
-
const
|
|
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(
|
|
2243
|
-
await mkdir4(
|
|
2244
|
-
await mkdir4(
|
|
2245
|
-
await mkdir4(
|
|
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 =
|
|
2638
|
+
const filePath = path9.join(cnosRoot, ...relativePath);
|
|
2259
2639
|
if (await ensureFile(filePath, "")) {
|
|
2260
|
-
createdPaths.push(
|
|
2640
|
+
createdPaths.push(path9.relative(path9.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
|
|
2261
2641
|
}
|
|
2262
2642
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
2399
|
-
|
|
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
|
|
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
|
-
|
|
2504
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
2652
|
-
import
|
|
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
|
|
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 =
|
|
2660
|
-
const workspace = options.workspace ??
|
|
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 =
|
|
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 =
|
|
2673
|
-
const targetPath =
|
|
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(
|
|
3111
|
+
imported.push(path13.relative(root, targetPath).replace(/\\/g, "/"));
|
|
2677
3112
|
if (move) {
|
|
2678
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
2722
|
-
const filePath =
|
|
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
|
|
2741
|
-
import
|
|
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 =
|
|
2745
|
-
await mkdir5(
|
|
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 =
|
|
3212
|
+
const profilesRoot = await resolveProfilesRoot(root);
|
|
2769
3213
|
try {
|
|
2770
|
-
const entries = await
|
|
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 =
|
|
3227
|
+
const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
|
|
2784
3228
|
try {
|
|
2785
|
-
await
|
|
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 =
|
|
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 =
|
|
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,
|
|
3287
|
+
return `created profile ${profile} at ${displayPath(result.filePath, displayRoot)} without inheriting base`;
|
|
2842
3288
|
}
|
|
2843
|
-
return `created profile ${profile} at ${displayPath(result.filePath,
|
|
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,
|
|
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
|
|
3325
|
+
import path17 from "path";
|
|
2879
3326
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
2880
3327
|
import {
|
|
2881
3328
|
ensureProjectionAllowed,
|
|
2882
|
-
loadManifest as
|
|
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 =
|
|
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
|
|
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
|
|
3511
|
+
import path20 from "path";
|
|
3057
3512
|
|
|
3058
3513
|
// src/commands/vault.ts
|
|
3059
|
-
import
|
|
3514
|
+
import path19 from "path";
|
|
3060
3515
|
|
|
3061
3516
|
// src/services/vaults.ts
|
|
3062
|
-
import { rm as
|
|
3063
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3647
|
+
const vaultRoot = path18.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
|
|
3170
3648
|
let removedStore;
|
|
3171
3649
|
try {
|
|
3172
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
3950
|
+
import path21 from "path";
|
|
3466
3951
|
async function runUse(args = [], options = {}) {
|
|
3467
|
-
const root =
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
3768
|
-
import
|
|
3769
|
-
import { loadManifest as
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
4306
|
+
path23.join(packageRoot, ".cnosrc.yml"),
|
|
3798
4307
|
stringifyYaml7({
|
|
3799
|
-
root:
|
|
3800
|
-
...
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
4373
|
+
path23.join(targetCnosRoot, "cnos.yml"),
|
|
3835
4374
|
stringifyYaml7(createDetachedManifest(loaded.rawManifest)),
|
|
3836
4375
|
"utf8"
|
|
3837
4376
|
);
|
|
3838
|
-
const relativeRoot =
|
|
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(
|
|
3849
|
-
await
|
|
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 =
|
|
3863
|
-
const markerPath =
|
|
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 =
|
|
3872
|
-
const parentLoaded = await
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
3892
|
-
const archivePath =
|
|
3893
|
-
await
|
|
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
|
|
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
|
|
3910
|
-
const
|
|
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
|
|
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 === "
|
|
4631
|
+
if (command === "cache" && args[0] && ["list", "clear", "refresh"].includes(args[0])) {
|
|
3938
4632
|
return normalizeHelpTopic([command, args[0]]);
|
|
3939
4633
|
}
|
|
3940
|
-
if (command === "
|
|
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":
|