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