@kitsy/cnos-cli 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1326 -129
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -33,7 +33,16 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
33
33
|
"--env",
|
|
34
34
|
"--yaml",
|
|
35
35
|
"--toml",
|
|
36
|
-
"--config"
|
|
36
|
+
"--config",
|
|
37
|
+
"--type",
|
|
38
|
+
"--default",
|
|
39
|
+
"--enum",
|
|
40
|
+
"--pattern",
|
|
41
|
+
"--summary",
|
|
42
|
+
"--description",
|
|
43
|
+
"--example",
|
|
44
|
+
"--used-by",
|
|
45
|
+
"--deprecation-message"
|
|
37
46
|
]);
|
|
38
47
|
var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
39
48
|
"--flatten",
|
|
@@ -56,7 +65,21 @@ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
|
56
65
|
"--materialize",
|
|
57
66
|
"--source-only",
|
|
58
67
|
"--allow-secret",
|
|
59
|
-
"--fix-secret-env-mappings"
|
|
68
|
+
"--fix-secret-env-mappings",
|
|
69
|
+
"--required",
|
|
70
|
+
"--optional",
|
|
71
|
+
"--deprecated",
|
|
72
|
+
"--clear-default",
|
|
73
|
+
"--clear-enum",
|
|
74
|
+
"--clear-pattern",
|
|
75
|
+
"--clear-summary",
|
|
76
|
+
"--clear-description",
|
|
77
|
+
"--clear-examples",
|
|
78
|
+
"--clear-used-by",
|
|
79
|
+
"--clear-deprecated",
|
|
80
|
+
"--clear-deprecation-message",
|
|
81
|
+
"--fill-missing",
|
|
82
|
+
"--review-all"
|
|
60
83
|
]);
|
|
61
84
|
function normalizeCommand(argv) {
|
|
62
85
|
const [command = "doctor", ...rest] = argv;
|
|
@@ -263,9 +286,37 @@ function consumeOption(args, flag) {
|
|
|
263
286
|
}
|
|
264
287
|
return void 0;
|
|
265
288
|
}
|
|
289
|
+
function consumeOptions(args, flag) {
|
|
290
|
+
const values = [];
|
|
291
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
292
|
+
const token = args[index];
|
|
293
|
+
if (!token) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (token.startsWith(`${flag}=`)) {
|
|
297
|
+
values.push(token.slice(flag.length + 1));
|
|
298
|
+
args.splice(index, 1);
|
|
299
|
+
index -= 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (token === flag) {
|
|
303
|
+
const value = args[index + 1];
|
|
304
|
+
if (!value) {
|
|
305
|
+
throw new Error(`Missing value for ${flag}`);
|
|
306
|
+
}
|
|
307
|
+
values.push(value);
|
|
308
|
+
args.splice(index, 2);
|
|
309
|
+
index -= 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return values;
|
|
313
|
+
}
|
|
266
314
|
|
|
267
315
|
// src/format/displayPath.ts
|
|
268
316
|
import path from "path";
|
|
317
|
+
function toPortablePath(filePath) {
|
|
318
|
+
return filePath.replace(/\\/g, "/");
|
|
319
|
+
}
|
|
269
320
|
function displayPath(filePath, root = process.cwd()) {
|
|
270
321
|
const absoluteRoot = path.resolve(root);
|
|
271
322
|
const absoluteFile = path.resolve(filePath);
|
|
@@ -274,9 +325,9 @@ function displayPath(filePath, root = process.cwd()) {
|
|
|
274
325
|
return ".";
|
|
275
326
|
}
|
|
276
327
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
277
|
-
return absoluteFile;
|
|
328
|
+
return toPortablePath(absoluteFile);
|
|
278
329
|
}
|
|
279
|
-
return relative;
|
|
330
|
+
return toPortablePath(relative);
|
|
280
331
|
}
|
|
281
332
|
|
|
282
333
|
// src/format/printJson.ts
|
|
@@ -290,6 +341,52 @@ import path4 from "path";
|
|
|
290
341
|
import { resolveBrowserData, resolveFrameworkEnv, resolveServerProjection } from "@kitsy/cnos/build";
|
|
291
342
|
import { stringifyYaml } from "@kitsy/cnos/internal";
|
|
292
343
|
|
|
344
|
+
// src/services/envSerialization.ts
|
|
345
|
+
var DOTENV_SAFE_UNQUOTED = /^[A-Za-z0-9_./:@%+*-]+$/;
|
|
346
|
+
function escapeDoubleQuoted(value) {
|
|
347
|
+
return value.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/"/g, '\\"');
|
|
348
|
+
}
|
|
349
|
+
function requiresDotenvQuoting(value) {
|
|
350
|
+
return value.length === 0 || !DOTENV_SAFE_UNQUOTED.test(value) || value.includes(" ") || value.includes("#") || value.includes("$");
|
|
351
|
+
}
|
|
352
|
+
function quoteDotenv(value) {
|
|
353
|
+
if (!requiresDotenvQuoting(value)) {
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
return `"${escapeDoubleQuoted(value)}"`;
|
|
357
|
+
}
|
|
358
|
+
function quoteToml(value) {
|
|
359
|
+
return `"${escapeDoubleQuoted(value)}"`;
|
|
360
|
+
}
|
|
361
|
+
function quoteShell(value) {
|
|
362
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
363
|
+
}
|
|
364
|
+
function stringifyScalar(value) {
|
|
365
|
+
if (value === void 0 || value === null) {
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
if (typeof value === "string") {
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
372
|
+
return String(value);
|
|
373
|
+
}
|
|
374
|
+
return JSON.stringify(value);
|
|
375
|
+
}
|
|
376
|
+
function formatEnvEntries(values, format = "dotenv") {
|
|
377
|
+
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right));
|
|
378
|
+
switch (format) {
|
|
379
|
+
case "shell":
|
|
380
|
+
return entries.map(([key, value]) => `export ${key}=${quoteShell(stringifyScalar(value))}`).join("\n");
|
|
381
|
+
case "toml":
|
|
382
|
+
return entries.map(([key, value]) => `${key} = ${quoteToml(stringifyScalar(value))}`).join("\n");
|
|
383
|
+
case "docker-env":
|
|
384
|
+
case "dotenv":
|
|
385
|
+
default:
|
|
386
|
+
return entries.map(([key, value]) => `${key}=${quoteDotenv(stringifyScalar(value))}`).join("\n");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
293
390
|
// src/services/runtime.ts
|
|
294
391
|
import { createCnos } from "@kitsy/cnos/configure";
|
|
295
392
|
async function createRuntimeService(options = {}) {
|
|
@@ -319,7 +416,7 @@ function resolveFilesystemBasePath(root, cwd = process.cwd()) {
|
|
|
319
416
|
}
|
|
320
417
|
|
|
321
418
|
// src/services/secretEnvBuild.ts
|
|
322
|
-
import { readFile } from "fs/promises";
|
|
419
|
+
import { readFile, realpath } from "fs/promises";
|
|
323
420
|
import path3 from "path";
|
|
324
421
|
import readline from "readline/promises";
|
|
325
422
|
import { spawnSync } from "child_process";
|
|
@@ -375,8 +472,28 @@ function isGitIgnored(repoRoot, targetPath) {
|
|
|
375
472
|
});
|
|
376
473
|
return result.status === 0;
|
|
377
474
|
}
|
|
378
|
-
async function
|
|
475
|
+
async function resolveCanonicalRepoRoot(cwd) {
|
|
379
476
|
const repoRoot = resolveGitRoot(cwd);
|
|
477
|
+
if (!repoRoot) {
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
return await realpath(repoRoot);
|
|
482
|
+
} catch {
|
|
483
|
+
return path3.resolve(repoRoot);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function resolveCanonicalTargetPath(targetPath) {
|
|
487
|
+
const resolvedPath = path3.resolve(targetPath);
|
|
488
|
+
try {
|
|
489
|
+
const resolvedDir = await realpath(path3.dirname(resolvedPath));
|
|
490
|
+
return path3.join(resolvedDir, path3.basename(resolvedPath));
|
|
491
|
+
} catch {
|
|
492
|
+
return resolvedPath;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function assertSecretEnvTargetIsGitIgnored(targetPath, cwd) {
|
|
496
|
+
const repoRoot = await resolveCanonicalRepoRoot(cwd);
|
|
380
497
|
if (!repoRoot) {
|
|
381
498
|
throw new Error(
|
|
382
499
|
`Cannot write revealed secrets to ${targetPath} because CNOS could not verify gitignore protection. Run inside a git repo or omit --reveal.`
|
|
@@ -390,8 +507,9 @@ async function assertSecretEnvTargetIsGitIgnored(targetPath, cwd) {
|
|
|
390
507
|
`Cannot write revealed secrets to ${targetPath} because ${gitignorePath} is missing. Add a gitignored env target or omit --reveal.`
|
|
391
508
|
);
|
|
392
509
|
}
|
|
393
|
-
|
|
394
|
-
|
|
510
|
+
const canonicalTargetPath = await resolveCanonicalTargetPath(targetPath);
|
|
511
|
+
if (!isGitIgnored(repoRoot, canonicalTargetPath)) {
|
|
512
|
+
const relativeTarget = path3.relative(repoRoot, canonicalTargetPath).replace(/\\/g, "/");
|
|
395
513
|
throw new Error(
|
|
396
514
|
`Cannot write revealed secrets to ${targetPath} because ${relativeTarget} is not gitignored. Add an ignore rule first, then re-run cnos build env --reveal.`
|
|
397
515
|
);
|
|
@@ -417,24 +535,6 @@ async function confirmSecretEnvBuild(targetPath, mappings) {
|
|
|
417
535
|
}
|
|
418
536
|
|
|
419
537
|
// src/services/projections.ts
|
|
420
|
-
function stringifyScalar(value) {
|
|
421
|
-
if (value === void 0 || value === null) {
|
|
422
|
-
return "";
|
|
423
|
-
}
|
|
424
|
-
if (typeof value === "string") {
|
|
425
|
-
return value;
|
|
426
|
-
}
|
|
427
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
428
|
-
return String(value);
|
|
429
|
-
}
|
|
430
|
-
return JSON.stringify(value);
|
|
431
|
-
}
|
|
432
|
-
function escapeShell(value) {
|
|
433
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
434
|
-
}
|
|
435
|
-
function quoteToml(value) {
|
|
436
|
-
return `"${escapeShell(value)}"`;
|
|
437
|
-
}
|
|
438
538
|
function formatKeyValueMap(values, format) {
|
|
439
539
|
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right));
|
|
440
540
|
switch (format) {
|
|
@@ -444,13 +544,13 @@ function formatKeyValueMap(values, format) {
|
|
|
444
544
|
case "yaml":
|
|
445
545
|
return stringifyYaml(Object.fromEntries(entries));
|
|
446
546
|
case "shell":
|
|
447
|
-
return
|
|
547
|
+
return formatEnvEntries(Object.fromEntries(entries), "shell");
|
|
448
548
|
case "toml":
|
|
449
|
-
return
|
|
549
|
+
return formatEnvEntries(Object.fromEntries(entries), "toml");
|
|
450
550
|
case "docker-env":
|
|
451
551
|
case "dotenv":
|
|
452
552
|
default:
|
|
453
|
-
return
|
|
553
|
+
return formatEnvEntries(Object.fromEntries(entries), format);
|
|
454
554
|
}
|
|
455
555
|
}
|
|
456
556
|
async function writeProjectionFile(to, output, root = process.cwd()) {
|
|
@@ -1070,7 +1170,7 @@ function resolveEnvFromRuntime(runtime, cliArgs = []) {
|
|
|
1070
1170
|
}) : runtime.toEnv();
|
|
1071
1171
|
}
|
|
1072
1172
|
function formatEnvOutput(env) {
|
|
1073
|
-
return
|
|
1173
|
+
return formatEnvEntries(env, "dotenv");
|
|
1074
1174
|
}
|
|
1075
1175
|
async function resolveMaterializedEnv(options = {}) {
|
|
1076
1176
|
const runtime = await createRuntimeService({
|
|
@@ -1158,6 +1258,17 @@ async function startGraphWatchLoop(options) {
|
|
|
1158
1258
|
}, debounceMs);
|
|
1159
1259
|
}
|
|
1160
1260
|
);
|
|
1261
|
+
watcher.on("error", (error) => {
|
|
1262
|
+
if (closed) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (recursive && (error.code === "EMFILE" || error.code === "ENOSPC")) {
|
|
1266
|
+
watcherMap.delete(targetPath);
|
|
1267
|
+
watcher.close();
|
|
1268
|
+
attachWatcher(targetPath, false);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1161
1272
|
watcherMap.set(targetPath, watcher);
|
|
1162
1273
|
} catch {
|
|
1163
1274
|
if (recursive) {
|
|
@@ -1349,6 +1460,9 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
|
|
|
1349
1460
|
return rows.map((row) => `${row.key}: ${JSON.stringify(row.left)} -> ${JSON.stringify(row.right)}`).join("\n");
|
|
1350
1461
|
}
|
|
1351
1462
|
|
|
1463
|
+
// src/commands/doctor.ts
|
|
1464
|
+
import { loadManifest as loadManifest4 } from "@kitsy/cnos/internal";
|
|
1465
|
+
|
|
1352
1466
|
// src/services/doctor.ts
|
|
1353
1467
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
1354
1468
|
import path9 from "path";
|
|
@@ -1563,6 +1677,16 @@ async function runDoctor(options = {}) {
|
|
|
1563
1677
|
const cliArgs = [...options.cliArgs ?? []];
|
|
1564
1678
|
const shouldFixSecretEnvMappings = cliArgs.includes("--fix-secret-env-mappings");
|
|
1565
1679
|
const repairResult = shouldFixSecretEnvMappings ? await repairSecretEnvMappings(options) : void 0;
|
|
1680
|
+
const loadedManifest = await loadManifest4({
|
|
1681
|
+
...options.root ? { root: options.root } : {},
|
|
1682
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
1683
|
+
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
1684
|
+
...options.cacheMode ? { cacheMode: options.cacheMode } : {},
|
|
1685
|
+
...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
|
|
1686
|
+
...options.forceRefresh ? { forceRefresh: true } : {}
|
|
1687
|
+
});
|
|
1688
|
+
const hasSchema = Object.keys(loadedManifest.manifest.schema).length > 0;
|
|
1689
|
+
const specPointer = hasSchema ? "Run cnos spec doctor to review config spec coverage." : void 0;
|
|
1566
1690
|
const checks = await evaluateDoctor(options);
|
|
1567
1691
|
const hasFailures = checks.some((check) => !check.ok);
|
|
1568
1692
|
if (hasFailures) {
|
|
@@ -1571,11 +1695,12 @@ async function runDoctor(options = {}) {
|
|
|
1571
1695
|
if (options.json) {
|
|
1572
1696
|
return printJson({
|
|
1573
1697
|
...repairResult ? { repair: repairResult } : {},
|
|
1574
|
-
checks
|
|
1698
|
+
checks,
|
|
1699
|
+
...specPointer ? { guidance: [specPointer] } : {}
|
|
1575
1700
|
});
|
|
1576
1701
|
}
|
|
1577
1702
|
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");
|
|
1703
|
+
return [repairLine, ...checks.map((check) => `${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.details}`), specPointer].filter((value) => Boolean(value)).join("\n");
|
|
1579
1704
|
}
|
|
1580
1705
|
|
|
1581
1706
|
// src/commands/dump.ts
|
|
@@ -1603,6 +1728,9 @@ async function runDump(options = {}) {
|
|
|
1603
1728
|
|
|
1604
1729
|
// src/commands/codegen.ts
|
|
1605
1730
|
import { watchSchema, writeCodegenOutput } from "@kitsy/cnos/internal";
|
|
1731
|
+
function formatPortablePath(filePath) {
|
|
1732
|
+
return filePath.replace(/\\/g, "/");
|
|
1733
|
+
}
|
|
1606
1734
|
async function runCodegen(options = {}) {
|
|
1607
1735
|
const cliArgs = [...options.cliArgs ?? []];
|
|
1608
1736
|
const out = consumeOption(cliArgs, "--out");
|
|
@@ -1629,7 +1757,7 @@ async function runCodegen(options = {}) {
|
|
|
1629
1757
|
};
|
|
1630
1758
|
process.once("SIGINT", closeWatcher);
|
|
1631
1759
|
process.once("SIGTERM", closeWatcher);
|
|
1632
|
-
return `watching schema changes -> ${out ?? ".cnos/types/cnos.d.ts"}`;
|
|
1760
|
+
return `watching schema changes -> ${formatPortablePath(out ?? ".cnos/types/cnos.d.ts")}`;
|
|
1633
1761
|
}
|
|
1634
1762
|
const result = await writeCodegenOutput({
|
|
1635
1763
|
...options.root ? {
|
|
@@ -1640,7 +1768,7 @@ async function runCodegen(options = {}) {
|
|
|
1640
1768
|
} : {}
|
|
1641
1769
|
});
|
|
1642
1770
|
const summary = result.hasSchema ? `generated types from ${result.schemaEntryCount} schema entr${result.schemaEntryCount === 1 ? "y" : "ies"}` : "generated empty types (no schema section found)";
|
|
1643
|
-
return `${summary} -> ${result.typesPath} and ${result.runtimePath}`;
|
|
1771
|
+
return `${summary} -> ${formatPortablePath(result.typesPath)} and ${formatPortablePath(result.runtimePath)}`;
|
|
1644
1772
|
}
|
|
1645
1773
|
|
|
1646
1774
|
// src/commands/exportEnv.ts
|
|
@@ -2070,6 +2198,150 @@ var COMMANDS = [
|
|
|
2070
2198
|
"cnos define secret app.token super-secret --workspace api"
|
|
2071
2199
|
]
|
|
2072
2200
|
},
|
|
2201
|
+
{
|
|
2202
|
+
id: "spec",
|
|
2203
|
+
summary: "Author and inspect manifest-global config specs stored under schema.",
|
|
2204
|
+
usage: "cnos spec [list | show <logicalKey> | set <logicalKey> | delete <logicalKey> | doctor] [options] [global-options]",
|
|
2205
|
+
description: 'Manages CNOS config specs (user-facing "spec") stored in the manifest schema: block. Use cnos define/value/secret for concrete value authoring.',
|
|
2206
|
+
examples: [
|
|
2207
|
+
"cnos spec list",
|
|
2208
|
+
"cnos spec show value.server.port",
|
|
2209
|
+
'cnos spec set value.server.port --type number --required --summary "HTTP server port"',
|
|
2210
|
+
"cnos spec delete value.legacy.flag",
|
|
2211
|
+
"cnos spec doctor"
|
|
2212
|
+
]
|
|
2213
|
+
},
|
|
2214
|
+
{
|
|
2215
|
+
id: "spec list",
|
|
2216
|
+
summary: "List declared spec entries.",
|
|
2217
|
+
usage: "cnos spec list [--prefix <path>] [global-options]",
|
|
2218
|
+
description: "Lists manifest schema entries. In v1, spec entries are manifest-global rather than workspace-scoped.",
|
|
2219
|
+
options: [
|
|
2220
|
+
{
|
|
2221
|
+
flag: "--prefix <path>",
|
|
2222
|
+
description: "Filter listed spec keys by logical-key prefix."
|
|
2223
|
+
}
|
|
2224
|
+
],
|
|
2225
|
+
examples: ["cnos spec list", "cnos spec list --prefix value.server."]
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
id: "spec show",
|
|
2229
|
+
summary: "Show one spec entry.",
|
|
2230
|
+
usage: "cnos spec show <logicalKey> [global-options]",
|
|
2231
|
+
description: "Shows one manifest schema entry by namespace-qualified logical key.",
|
|
2232
|
+
arguments: [
|
|
2233
|
+
{
|
|
2234
|
+
name: "logicalKey",
|
|
2235
|
+
description: "Namespace-qualified key such as value.server.port.",
|
|
2236
|
+
required: true
|
|
2237
|
+
}
|
|
2238
|
+
],
|
|
2239
|
+
examples: ["cnos spec show value.server.port", "cnos spec show secret.db.password --json"]
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
id: "spec set",
|
|
2243
|
+
summary: "Create or update one spec entry.",
|
|
2244
|
+
usage: "cnos spec set <logicalKey> [field-flags] [global-options]",
|
|
2245
|
+
description: "Writes one manifest schema entry. With no field flags in a TTY, CNOS enters interactive authoring mode. With field flags, CNOS uses non-interactive mode.",
|
|
2246
|
+
arguments: [
|
|
2247
|
+
{
|
|
2248
|
+
name: "logicalKey",
|
|
2249
|
+
description: "Namespace-qualified key such as value.server.port.",
|
|
2250
|
+
required: true
|
|
2251
|
+
}
|
|
2252
|
+
],
|
|
2253
|
+
options: [
|
|
2254
|
+
{
|
|
2255
|
+
flag: "--type <string|number|boolean|object|array>",
|
|
2256
|
+
description: "Set expected value type."
|
|
2257
|
+
},
|
|
2258
|
+
{
|
|
2259
|
+
flag: "--required | --optional",
|
|
2260
|
+
description: "Mark key required or optional."
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
flag: "--default <jsonOrScalar>",
|
|
2264
|
+
description: "Set default using JSON-first parsing; fallback is literal string."
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
flag: "--enum <jsonArray>",
|
|
2268
|
+
description: "Set allowed values from a non-empty JSON array."
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
flag: "--pattern <regex>",
|
|
2272
|
+
description: "Set regex pattern for string values."
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
flag: "--summary <text>",
|
|
2276
|
+
description: "Set short description."
|
|
2277
|
+
},
|
|
2278
|
+
{
|
|
2279
|
+
flag: "--description <text>",
|
|
2280
|
+
description: "Set long description."
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
flag: "--example <value>",
|
|
2284
|
+
description: "Add example value. Repeatable. JSON-first parsing."
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
flag: "--used-by <text>",
|
|
2288
|
+
description: "Add usage context text. Repeatable."
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
flag: "--deprecated",
|
|
2292
|
+
description: "Mark as deprecated."
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
flag: "--deprecation-message <text>",
|
|
2296
|
+
description: "Set deprecation message and auto-mark deprecated."
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
flag: "--clear-default | --clear-enum | --clear-pattern | --clear-summary | --clear-description | --clear-examples | --clear-used-by | --clear-deprecated | --clear-deprecation-message",
|
|
2300
|
+
description: "Explicitly clear stored fields."
|
|
2301
|
+
}
|
|
2302
|
+
],
|
|
2303
|
+
examples: [
|
|
2304
|
+
'cnos spec set value.server.port --type number --required --summary "HTTP server port"',
|
|
2305
|
+
`cnos spec set value.app.stage --enum '["local","stage","prod"]'`,
|
|
2306
|
+
"cnos spec set value.legacy.flag --clear-deprecated"
|
|
2307
|
+
]
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
id: "spec delete",
|
|
2311
|
+
summary: "Delete one spec entry.",
|
|
2312
|
+
usage: "cnos spec delete <logicalKey> [global-options]",
|
|
2313
|
+
description: "Removes one manifest schema entry by namespace-qualified logical key.",
|
|
2314
|
+
arguments: [
|
|
2315
|
+
{
|
|
2316
|
+
name: "logicalKey",
|
|
2317
|
+
description: "Namespace-qualified key such as value.server.port.",
|
|
2318
|
+
required: true
|
|
2319
|
+
}
|
|
2320
|
+
],
|
|
2321
|
+
examples: ["cnos spec delete value.legacy.flag", "cnos spec remove value.legacy.flag --json"]
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
id: "spec doctor",
|
|
2325
|
+
summary: "Compare declared spec against current config and guide remediation.",
|
|
2326
|
+
usage: "cnos spec doctor [--fill-missing|--review-all] [global-options]",
|
|
2327
|
+
description: "Report mode shows missing required keys, undeclared keys, type/enum/pattern mismatches, defaults in use, and deprecated keys in use. Write modes run interactive remediation flows.",
|
|
2328
|
+
options: [
|
|
2329
|
+
{
|
|
2330
|
+
flag: "--fill-missing",
|
|
2331
|
+
description: "Interactively fill only missing required keys. Requires TTY and writable root."
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
flag: "--review-all",
|
|
2335
|
+
description: "Interactively review all declared spec keys one by one. Requires TTY and writable root."
|
|
2336
|
+
}
|
|
2337
|
+
],
|
|
2338
|
+
examples: [
|
|
2339
|
+
"cnos spec doctor",
|
|
2340
|
+
"cnos spec doctor --json",
|
|
2341
|
+
"cnos spec doctor --fill-missing",
|
|
2342
|
+
"cnos spec doctor --review-all --workspace api --profile stage"
|
|
2343
|
+
]
|
|
2344
|
+
},
|
|
2073
2345
|
{
|
|
2074
2346
|
id: "use",
|
|
2075
2347
|
summary: "Persist repo-local CLI defaults such as workspace and profile.",
|
|
@@ -2543,7 +2815,7 @@ var COMMANDS = [
|
|
|
2543
2815
|
id: "doctor",
|
|
2544
2816
|
summary: "Run repository and workspace diagnostics.",
|
|
2545
2817
|
usage: "cnos doctor [--fix-secret-env-mappings] [global-options]",
|
|
2546
|
-
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.",
|
|
2818
|
+
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. When schema entries exist, doctor points to cnos spec doctor for spec coverage and remediation.",
|
|
2547
2819
|
examples: ["cnos doctor", "cnos doctor --workspace api --json", "cnos doctor --fix-secret-env-mappings"]
|
|
2548
2820
|
},
|
|
2549
2821
|
{
|
|
@@ -3096,9 +3368,15 @@ function printTable(rows) {
|
|
|
3096
3368
|
...rows.map((row) => stringifyCell(row[column]).length)
|
|
3097
3369
|
)
|
|
3098
3370
|
);
|
|
3099
|
-
const renderRow = (row) => columns.map((column, index) =>
|
|
3371
|
+
const renderRow = (row) => columns.map((column, index) => {
|
|
3372
|
+
const width = widths[index] ?? column.length;
|
|
3373
|
+
return stringifyCell(row[column]).padEnd(width, " ");
|
|
3374
|
+
}).join(" ").trimEnd();
|
|
3100
3375
|
return [
|
|
3101
|
-
columns.map((column, index) =>
|
|
3376
|
+
columns.map((column, index) => {
|
|
3377
|
+
const width = widths[index] ?? column.length;
|
|
3378
|
+
return column.padEnd(width, " ");
|
|
3379
|
+
}).join(" ").trimEnd(),
|
|
3102
3380
|
widths.map((width) => "-".repeat(width)).join(" "),
|
|
3103
3381
|
...rows.map(renderRow)
|
|
3104
3382
|
].join("\n");
|
|
@@ -3290,7 +3568,7 @@ async function runList(args = [], options = {}) {
|
|
|
3290
3568
|
import path12 from "path";
|
|
3291
3569
|
import {
|
|
3292
3570
|
applyManifestMappings,
|
|
3293
|
-
loadManifest as
|
|
3571
|
+
loadManifest as loadManifest5,
|
|
3294
3572
|
proposeMapping,
|
|
3295
3573
|
rewriteSourceFiles,
|
|
3296
3574
|
scanEnvUsage
|
|
@@ -3307,7 +3585,7 @@ async function runMigrate(options = {}) {
|
|
|
3307
3585
|
if (apply) {
|
|
3308
3586
|
await assertWritableConfigRoot("apply migration mappings", options);
|
|
3309
3587
|
}
|
|
3310
|
-
const manifest = await
|
|
3588
|
+
const manifest = await loadManifest5({
|
|
3311
3589
|
...options.root ? { root: options.root } : {},
|
|
3312
3590
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3313
3591
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3473,7 +3751,7 @@ async function runNamespace(namespace, args = [], options = {}) {
|
|
|
3473
3751
|
import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile5 } from "fs/promises";
|
|
3474
3752
|
import path14 from "path";
|
|
3475
3753
|
import readline2 from "readline/promises";
|
|
3476
|
-
import { loadManifest as
|
|
3754
|
+
import { loadManifest as loadManifest6, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
|
|
3477
3755
|
import { parse as parseToml } from "smol-toml";
|
|
3478
3756
|
var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
|
|
3479
3757
|
var SECRET_LIKE_PATTERN = /(secret|token|password|passwd|private|api[_-]?key|client[_-]?secret|dsn)/i;
|
|
@@ -3658,7 +3936,7 @@ async function runOnboard(options = {}) {
|
|
|
3658
3936
|
});
|
|
3659
3937
|
scaffolded = scaffold.created;
|
|
3660
3938
|
}
|
|
3661
|
-
const loaded = await
|
|
3939
|
+
const loaded = await loadManifest6({
|
|
3662
3940
|
root,
|
|
3663
3941
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
3664
3942
|
});
|
|
@@ -3783,13 +4061,13 @@ async function saveCliContext(options = {}) {
|
|
|
3783
4061
|
// src/services/profiles.ts
|
|
3784
4062
|
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile7, rm as rm3, writeFile as writeFile7 } from "fs/promises";
|
|
3785
4063
|
import path16 from "path";
|
|
3786
|
-
import { loadManifest as
|
|
4064
|
+
import { loadManifest as loadManifest7, parseYaml as parseYaml5, stringifyYaml as stringifyYaml5 } from "@kitsy/cnos/internal";
|
|
3787
4065
|
async function resolveProfilesRoot(root = process.cwd()) {
|
|
3788
4066
|
try {
|
|
3789
|
-
const loadedManifest = await
|
|
4067
|
+
const loadedManifest = await loadManifest7({ root });
|
|
3790
4068
|
return path16.join(loadedManifest.manifestRoot, "profiles");
|
|
3791
4069
|
} catch {
|
|
3792
|
-
const loadedManifest = await
|
|
4070
|
+
const loadedManifest = await loadManifest7({ cwd: root });
|
|
3793
4071
|
return path16.join(loadedManifest.manifestRoot, "profiles");
|
|
3794
4072
|
}
|
|
3795
4073
|
}
|
|
@@ -3935,7 +4213,7 @@ import path18 from "path";
|
|
|
3935
4213
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
3936
4214
|
import {
|
|
3937
4215
|
ensureProjectionAllowed,
|
|
3938
|
-
loadManifest as
|
|
4216
|
+
loadManifest as loadManifest8,
|
|
3939
4217
|
stringifyYaml as stringifyYaml6
|
|
3940
4218
|
} from "@kitsy/cnos/internal";
|
|
3941
4219
|
function normalizeTarget(value) {
|
|
@@ -3967,7 +4245,7 @@ async function runPromote(args = [], options = {}) {
|
|
|
3967
4245
|
} else if (allowSecret) {
|
|
3968
4246
|
throw new Error("--allow-secret is only supported with promote --to env");
|
|
3969
4247
|
}
|
|
3970
|
-
const loadedManifest = await
|
|
4248
|
+
const loadedManifest = await loadManifest8({
|
|
3971
4249
|
...options.root ? { root: options.root } : {},
|
|
3972
4250
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3973
4251
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4039,7 +4317,7 @@ import {
|
|
|
4039
4317
|
serializeRuntimeGraph,
|
|
4040
4318
|
serializeSecretPayload
|
|
4041
4319
|
} from "@kitsy/cnos/internal";
|
|
4042
|
-
function
|
|
4320
|
+
function consumeOptions2(args, flag) {
|
|
4043
4321
|
const values = [];
|
|
4044
4322
|
for (let index = 0; index < args.length; ) {
|
|
4045
4323
|
const token = args[index];
|
|
@@ -4078,7 +4356,7 @@ async function runCommand(command, options = {}) {
|
|
|
4078
4356
|
const isAuthenticated = consumeFlag(cliArgs, "--auth");
|
|
4079
4357
|
const framework = consumeOption(cliArgs, "--framework");
|
|
4080
4358
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
4081
|
-
const setOverrides = normalizeSetOverrides(
|
|
4359
|
+
const setOverrides = normalizeSetOverrides(consumeOptions2(cliArgs, "--set"));
|
|
4082
4360
|
const runtime = await createRuntimeService({
|
|
4083
4361
|
...options,
|
|
4084
4362
|
cliArgs: [...cliArgs, ...setOverrides]
|
|
@@ -4149,7 +4427,7 @@ import {
|
|
|
4149
4427
|
createSecretVault,
|
|
4150
4428
|
deriveVaultKey,
|
|
4151
4429
|
listLocalSecrets,
|
|
4152
|
-
loadManifest as
|
|
4430
|
+
loadManifest as loadManifest9,
|
|
4153
4431
|
listSecretVaults,
|
|
4154
4432
|
readVaultMetadata,
|
|
4155
4433
|
resolveSecretStoreRoot as resolveSecretStoreRoot2,
|
|
@@ -4188,7 +4466,7 @@ async function createVaultDefinition(name, options = {}) {
|
|
|
4188
4466
|
if (provider === "local" && (options.noPassphrase ?? false)) {
|
|
4189
4467
|
throw new Error("Local vaults cannot be passwordless.");
|
|
4190
4468
|
}
|
|
4191
|
-
const loadedManifest = await
|
|
4469
|
+
const loadedManifest = await loadManifest9({
|
|
4192
4470
|
...options.root ? { root: options.root } : {},
|
|
4193
4471
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4194
4472
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4225,7 +4503,7 @@ async function createVaultDefinition(name, options = {}) {
|
|
|
4225
4503
|
};
|
|
4226
4504
|
}
|
|
4227
4505
|
async function listVaultDefinitions(options = {}) {
|
|
4228
|
-
const loadedManifest = await
|
|
4506
|
+
const loadedManifest = await loadManifest9({
|
|
4229
4507
|
...options.root ? { root: options.root } : {},
|
|
4230
4508
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4231
4509
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4246,7 +4524,7 @@ async function listVaultDefinitions(options = {}) {
|
|
|
4246
4524
|
async function removeVaultDefinition(name, options = {}) {
|
|
4247
4525
|
await assertWritableConfigRoot(`remove vault ${name}`, options);
|
|
4248
4526
|
const vault = name.trim() || "default";
|
|
4249
|
-
const loadedManifest = await
|
|
4527
|
+
const loadedManifest = await loadManifest9({
|
|
4250
4528
|
...options.root ? { root: options.root } : {},
|
|
4251
4529
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4252
4530
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4292,7 +4570,7 @@ async function listLocalStoreVaults(options = {}) {
|
|
|
4292
4570
|
}
|
|
4293
4571
|
async function authenticateVault(name, options = {}) {
|
|
4294
4572
|
const vault = name.trim() || "default";
|
|
4295
|
-
const loadedManifest = await
|
|
4573
|
+
const loadedManifest = await loadManifest9({
|
|
4296
4574
|
...options.root ? { root: options.root } : {},
|
|
4297
4575
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4298
4576
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4613,65 +4891,971 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
4613
4891
|
return printValue(valueForOutput);
|
|
4614
4892
|
}
|
|
4615
4893
|
|
|
4616
|
-
// src/
|
|
4617
|
-
import
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4894
|
+
// src/services/spec/specDoctor.ts
|
|
4895
|
+
import { compareSpecToGraph } from "@kitsy/cnos/internal";
|
|
4896
|
+
|
|
4897
|
+
// src/services/maskedPrompt.ts
|
|
4898
|
+
import readline4 from "readline";
|
|
4899
|
+
import { Writable as Writable2 } from "stream";
|
|
4900
|
+
var WritableMask2 = class extends Writable2 {
|
|
4901
|
+
muted = false;
|
|
4902
|
+
_write(chunk, _encoding, callback) {
|
|
4903
|
+
if (!this.muted) {
|
|
4904
|
+
process.stdout.write(chunk);
|
|
4626
4905
|
}
|
|
4627
|
-
|
|
4906
|
+
callback();
|
|
4628
4907
|
}
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
...options.globalRoot ? { globalRoot: options.globalRoot } : {}
|
|
4634
|
-
});
|
|
4635
|
-
if (options.json) {
|
|
4636
|
-
return printJson(result);
|
|
4908
|
+
};
|
|
4909
|
+
function assertInteractiveInput(mode) {
|
|
4910
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4911
|
+
throw new Error(`Cannot prompt for ${mode} input in non-interactive mode.`);
|
|
4637
4912
|
}
|
|
4638
|
-
return `updated CLI context in ${displayPath(result.filePath, root)}`;
|
|
4639
4913
|
}
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4914
|
+
async function promptMaskedInput(message) {
|
|
4915
|
+
assertInteractiveInput("masked");
|
|
4916
|
+
const mutableStdout = new WritableMask2();
|
|
4917
|
+
const rl = readline4.createInterface({
|
|
4918
|
+
input: process.stdin,
|
|
4919
|
+
output: mutableStdout,
|
|
4920
|
+
terminal: true
|
|
4921
|
+
});
|
|
4922
|
+
try {
|
|
4923
|
+
process.stdout.write(message);
|
|
4924
|
+
mutableStdout.muted = true;
|
|
4925
|
+
const value = await new Promise((resolve) => {
|
|
4926
|
+
rl.question("", resolve);
|
|
4927
|
+
});
|
|
4928
|
+
process.stdout.write("\n");
|
|
4929
|
+
return value;
|
|
4930
|
+
} finally {
|
|
4931
|
+
rl.close();
|
|
4653
4932
|
}
|
|
4654
|
-
return parsed;
|
|
4655
4933
|
}
|
|
4656
|
-
function
|
|
4657
|
-
|
|
4934
|
+
async function promptInput(message) {
|
|
4935
|
+
assertInteractiveInput("plain");
|
|
4936
|
+
const rl = readline4.createInterface({
|
|
4937
|
+
input: process.stdin,
|
|
4938
|
+
output: process.stdout,
|
|
4939
|
+
terminal: true
|
|
4940
|
+
});
|
|
4658
4941
|
try {
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4942
|
+
return await new Promise((resolve) => {
|
|
4943
|
+
rl.question(message, (answer) => resolve(answer));
|
|
4944
|
+
});
|
|
4945
|
+
} finally {
|
|
4946
|
+
rl.close();
|
|
4663
4947
|
}
|
|
4664
4948
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4949
|
+
|
|
4950
|
+
// src/services/spec/specDoctor.ts
|
|
4951
|
+
var BLOCKING_STATUSES = /* @__PURE__ */ new Set([
|
|
4952
|
+
"missing_required",
|
|
4953
|
+
"undeclared",
|
|
4954
|
+
"type_mismatch",
|
|
4955
|
+
"enum_mismatch",
|
|
4956
|
+
"pattern_mismatch"
|
|
4957
|
+
]);
|
|
4958
|
+
function statusLabel(status) {
|
|
4959
|
+
return status.replace(/_/g, " ");
|
|
4670
4960
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4961
|
+
function isBlockingStatus(status) {
|
|
4962
|
+
return BLOCKING_STATUSES.has(status);
|
|
4963
|
+
}
|
|
4964
|
+
function isBlockingIssue(issue) {
|
|
4965
|
+
return isBlockingStatus(issue.status);
|
|
4966
|
+
}
|
|
4967
|
+
function isInteractiveMode() {
|
|
4968
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
4969
|
+
}
|
|
4970
|
+
function toPath(key) {
|
|
4971
|
+
const separatorIndex = key.indexOf(".");
|
|
4972
|
+
if (separatorIndex <= 0 || separatorIndex >= key.length - 1) {
|
|
4973
|
+
throw new Error(`Spec key must be namespace-qualified: ${key}`);
|
|
4974
|
+
}
|
|
4975
|
+
return {
|
|
4976
|
+
namespace: key.slice(0, separatorIndex),
|
|
4977
|
+
configPath: key.slice(separatorIndex + 1)
|
|
4978
|
+
};
|
|
4979
|
+
}
|
|
4980
|
+
function toDoctorIssue(issue) {
|
|
4981
|
+
return {
|
|
4982
|
+
key: issue.key,
|
|
4983
|
+
status: issue.status,
|
|
4984
|
+
...issue.expectedType ? {
|
|
4985
|
+
expectedType: issue.expectedType
|
|
4986
|
+
} : {},
|
|
4987
|
+
...issue.actualType ? {
|
|
4988
|
+
actualType: issue.actualType
|
|
4989
|
+
} : {},
|
|
4990
|
+
...issue.value !== void 0 ? {
|
|
4991
|
+
value: issue.value
|
|
4992
|
+
} : {},
|
|
4993
|
+
...issue.sourceFile ? {
|
|
4994
|
+
sourceFile: issue.sourceFile
|
|
4995
|
+
} : {},
|
|
4996
|
+
...issue.summary ? {
|
|
4997
|
+
summary: issue.summary
|
|
4998
|
+
} : {}
|
|
4999
|
+
};
|
|
5000
|
+
}
|
|
5001
|
+
function renderIssueLine(issue) {
|
|
5002
|
+
const context = [
|
|
5003
|
+
issue.expectedType ? `expected=${issue.expectedType}` : void 0,
|
|
5004
|
+
issue.actualType ? `actual=${issue.actualType}` : void 0,
|
|
5005
|
+
issue.sourceFile ? `source=${issue.sourceFile}` : void 0
|
|
5006
|
+
].filter((value) => Boolean(value)).join(", ");
|
|
5007
|
+
return context.length > 0 ? `- ${issue.key} [${statusLabel(issue.status)}] (${context})` : `- ${issue.key} [${statusLabel(issue.status)}]`;
|
|
5008
|
+
}
|
|
5009
|
+
function makeReportResult(runtime, mode, actions) {
|
|
5010
|
+
const report = compareSpecToGraph(runtime);
|
|
5011
|
+
return {
|
|
5012
|
+
workspace: report.workspace,
|
|
5013
|
+
profile: report.profile,
|
|
5014
|
+
summary: report.summary,
|
|
5015
|
+
issues: report.issues.map((issue) => toDoctorIssue(issue)),
|
|
5016
|
+
mode,
|
|
5017
|
+
...actions ? {
|
|
5018
|
+
actions
|
|
5019
|
+
} : {}
|
|
5020
|
+
};
|
|
5021
|
+
}
|
|
5022
|
+
function getRuleForKey(runtime, logicalKey) {
|
|
5023
|
+
return runtime.manifest.schema[logicalKey];
|
|
5024
|
+
}
|
|
5025
|
+
async function promptForKeyValue(key, rule) {
|
|
5026
|
+
const lines = [
|
|
5027
|
+
"",
|
|
5028
|
+
`Key: ${key}`,
|
|
5029
|
+
...rule?.summary ? [`Summary: ${rule.summary}`] : [],
|
|
5030
|
+
...rule?.description ? [`Description: ${rule.description}`] : [],
|
|
5031
|
+
...rule?.enum ? [`Allowed values: ${rule.enum.map((entry) => JSON.stringify(entry)).join(", ")}`] : [],
|
|
5032
|
+
...rule?.pattern ? [`Pattern: ${rule.pattern}`] : []
|
|
5033
|
+
];
|
|
5034
|
+
process.stdout.write(`${lines.join("\n")}
|
|
5035
|
+
`);
|
|
5036
|
+
if (key.startsWith("secret.")) {
|
|
5037
|
+
return (await promptMaskedInput(`Enter value for ${key}: `)).trimEnd();
|
|
5038
|
+
}
|
|
5039
|
+
if (rule?.enum && rule.enum.length > 0) {
|
|
5040
|
+
process.stdout.write(
|
|
5041
|
+
`${rule.enum.map((entry, index2) => `${index2 + 1}. ${JSON.stringify(entry)}`).join("\n")}
|
|
5042
|
+
`
|
|
5043
|
+
);
|
|
5044
|
+
const choice = (await promptInput(`Choose [1-${rule.enum.length}] or enter a custom value: `)).trim();
|
|
5045
|
+
const index = Number(choice);
|
|
5046
|
+
if (Number.isFinite(index) && index >= 1 && index <= rule.enum.length) {
|
|
5047
|
+
return JSON.stringify(rule.enum[index - 1]);
|
|
5048
|
+
}
|
|
5049
|
+
return choice;
|
|
5050
|
+
}
|
|
5051
|
+
return (await promptInput(`Enter value for ${key}: `)).trim();
|
|
5052
|
+
}
|
|
5053
|
+
async function applyWriteForKey(key, rawValue, options) {
|
|
5054
|
+
const { namespace, configPath } = toPath(key);
|
|
5055
|
+
if (namespace === "secret") {
|
|
5056
|
+
await setSecret(configPath, rawValue, options);
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
await defineValue(namespace, configPath, rawValue, options);
|
|
5060
|
+
}
|
|
5061
|
+
function hasBlockingIssues(result) {
|
|
5062
|
+
return result.issues.some((issue) => isBlockingIssue(issue));
|
|
5063
|
+
}
|
|
5064
|
+
function hasFailedActions(actions) {
|
|
5065
|
+
return actions.some((action) => action.result === "failed");
|
|
5066
|
+
}
|
|
5067
|
+
async function runSpecDoctor(mode, options = {}) {
|
|
5068
|
+
const runtime = await createRuntimeService({
|
|
5069
|
+
...options,
|
|
5070
|
+
secretResolution: "lazy"
|
|
5071
|
+
});
|
|
5072
|
+
if (mode === "report") {
|
|
5073
|
+
const result2 = makeReportResult(runtime, mode);
|
|
5074
|
+
return {
|
|
5075
|
+
result: result2,
|
|
5076
|
+
blocking: hasBlockingIssues(result2)
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
5079
|
+
if (!isInteractiveMode()) {
|
|
5080
|
+
throw new Error(`spec doctor --${mode} requires an interactive TTY.`);
|
|
5081
|
+
}
|
|
5082
|
+
const report = compareSpecToGraph(runtime);
|
|
5083
|
+
const actions = [];
|
|
5084
|
+
let unresolvedBlocking = false;
|
|
5085
|
+
if (mode === "fill-missing") {
|
|
5086
|
+
const missing = report.issues.filter((issue) => issue.status === "missing_required").sort((left, right) => left.key.localeCompare(right.key));
|
|
5087
|
+
for (const issue of missing) {
|
|
5088
|
+
const rule = getRuleForKey(runtime, issue.key);
|
|
5089
|
+
try {
|
|
5090
|
+
const value = await promptForKeyValue(issue.key, rule);
|
|
5091
|
+
await applyWriteForKey(issue.key, value, options);
|
|
5092
|
+
actions.push({
|
|
5093
|
+
key: issue.key,
|
|
5094
|
+
result: "applied"
|
|
5095
|
+
});
|
|
5096
|
+
} catch (error) {
|
|
5097
|
+
actions.push({
|
|
5098
|
+
key: issue.key,
|
|
5099
|
+
result: "failed",
|
|
5100
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
} else {
|
|
5105
|
+
const schemaKeys = Object.keys(runtime.manifest.schema).sort((left, right) => left.localeCompare(right));
|
|
5106
|
+
const issuesByKey = /* @__PURE__ */ new Map();
|
|
5107
|
+
for (const issue of report.issues) {
|
|
5108
|
+
const existing = issuesByKey.get(issue.key) ?? [];
|
|
5109
|
+
existing.push(issue.status);
|
|
5110
|
+
issuesByKey.set(issue.key, existing);
|
|
5111
|
+
}
|
|
5112
|
+
for (const key of schemaKeys) {
|
|
5113
|
+
const keyStatuses = issuesByKey.get(key) ?? [];
|
|
5114
|
+
const rule = getRuleForKey(runtime, key);
|
|
5115
|
+
const statusText = keyStatuses.length > 0 ? keyStatuses.map(statusLabel).join(", ") : "ok";
|
|
5116
|
+
process.stdout.write(`
|
|
5117
|
+
Key: ${key}
|
|
5118
|
+
Current status: ${statusText}
|
|
5119
|
+
`);
|
|
5120
|
+
const actionChoice = (await promptInput("Action [k=keep, u=update, s=skip]: ")).trim().toLowerCase();
|
|
5121
|
+
if (actionChoice === "k" || actionChoice === "keep") {
|
|
5122
|
+
actions.push({
|
|
5123
|
+
key,
|
|
5124
|
+
result: "skipped",
|
|
5125
|
+
reason: "keep"
|
|
5126
|
+
});
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
if (actionChoice === "s" || actionChoice === "skip") {
|
|
5130
|
+
if (keyStatuses.includes("missing_required")) {
|
|
5131
|
+
unresolvedBlocking = true;
|
|
5132
|
+
}
|
|
5133
|
+
actions.push({
|
|
5134
|
+
key,
|
|
5135
|
+
result: "skipped",
|
|
5136
|
+
reason: "skip"
|
|
5137
|
+
});
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
try {
|
|
5141
|
+
const value = await promptForKeyValue(key, rule);
|
|
5142
|
+
await applyWriteForKey(key, value, options);
|
|
5143
|
+
actions.push({
|
|
5144
|
+
key,
|
|
5145
|
+
result: "applied"
|
|
5146
|
+
});
|
|
5147
|
+
} catch (error) {
|
|
5148
|
+
actions.push({
|
|
5149
|
+
key,
|
|
5150
|
+
result: "failed",
|
|
5151
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const refreshed = await createRuntimeService({
|
|
5157
|
+
...options,
|
|
5158
|
+
secretResolution: "lazy"
|
|
5159
|
+
});
|
|
5160
|
+
const result = makeReportResult(refreshed, mode, actions);
|
|
5161
|
+
const blocking = hasBlockingIssues(result) || hasFailedActions(actions) || unresolvedBlocking;
|
|
5162
|
+
return {
|
|
5163
|
+
result,
|
|
5164
|
+
blocking
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
function formatSpecDoctorResult(result) {
|
|
5168
|
+
const lines = [
|
|
5169
|
+
`Spec doctor (${result.workspace} / ${result.profile}) [mode=${result.mode}]`,
|
|
5170
|
+
"",
|
|
5171
|
+
`missingRequired=${result.summary.missingRequired}`,
|
|
5172
|
+
`undeclared=${result.summary.undeclared}`,
|
|
5173
|
+
`typeMismatch=${result.summary.typeMismatch}`,
|
|
5174
|
+
`enumMismatch=${result.summary.enumMismatch}`,
|
|
5175
|
+
`patternMismatch=${result.summary.patternMismatch}`,
|
|
5176
|
+
`defaultApplied=${result.summary.defaultApplied}`,
|
|
5177
|
+
`deprecatedInUse=${result.summary.deprecatedInUse}`,
|
|
5178
|
+
""
|
|
5179
|
+
];
|
|
5180
|
+
if (result.issues.length === 0) {
|
|
5181
|
+
lines.push("No spec issues detected.");
|
|
5182
|
+
} else {
|
|
5183
|
+
lines.push("Issues:");
|
|
5184
|
+
lines.push(...result.issues.map(renderIssueLine));
|
|
5185
|
+
}
|
|
5186
|
+
if (result.actions && result.actions.length > 0) {
|
|
5187
|
+
lines.push("");
|
|
5188
|
+
lines.push("Actions:");
|
|
5189
|
+
lines.push(
|
|
5190
|
+
...result.actions.map(
|
|
5191
|
+
(action) => action.reason ? `- ${action.key}: ${action.result} (${action.reason})` : `- ${action.key}: ${action.result}`
|
|
5192
|
+
)
|
|
5193
|
+
);
|
|
5194
|
+
}
|
|
5195
|
+
return lines.join("\n");
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
// src/services/spec/manifestSpecStore.ts
|
|
5199
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
5200
|
+
import { loadManifest as loadManifest10, stringifyYaml as stringifyYaml8 } from "@kitsy/cnos/internal";
|
|
5201
|
+
function hasOwn(target, key) {
|
|
5202
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
5203
|
+
}
|
|
5204
|
+
function sortSchema(schema) {
|
|
5205
|
+
return Object.fromEntries(Object.entries(schema).sort(([left], [right]) => left.localeCompare(right)));
|
|
5206
|
+
}
|
|
5207
|
+
async function loadSpecManifest(options = {}) {
|
|
5208
|
+
const loadedManifest = await loadManifest10({
|
|
5209
|
+
...options.root ? { root: options.root } : {},
|
|
5210
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
5211
|
+
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
5212
|
+
...options.cacheMode ? { cacheMode: options.cacheMode } : {},
|
|
5213
|
+
...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
|
|
5214
|
+
...options.forceRefresh ? { forceRefresh: true } : {}
|
|
5215
|
+
});
|
|
5216
|
+
return {
|
|
5217
|
+
manifestPath: loadedManifest.manifestPath,
|
|
5218
|
+
rawManifest: loadedManifest.rawManifest,
|
|
5219
|
+
schema: loadedManifest.manifest.schema
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
async function listSpecEntries(options = {}) {
|
|
5223
|
+
const loaded = await loadSpecManifest(options);
|
|
5224
|
+
const prefix = options.prefix?.trim();
|
|
5225
|
+
const entries = Object.entries(loaded.schema).filter(([key]) => prefix ? key.startsWith(prefix) : true).sort(([left], [right]) => left.localeCompare(right)).map(([key, rule]) => ({
|
|
5226
|
+
key,
|
|
5227
|
+
rule
|
|
5228
|
+
}));
|
|
5229
|
+
return {
|
|
5230
|
+
manifestPath: loaded.manifestPath,
|
|
5231
|
+
entries
|
|
5232
|
+
};
|
|
5233
|
+
}
|
|
5234
|
+
async function showSpecEntry(logicalKey, options = {}) {
|
|
5235
|
+
const loaded = await loadSpecManifest(options);
|
|
5236
|
+
const key = logicalKey.trim();
|
|
5237
|
+
return {
|
|
5238
|
+
manifestPath: loaded.manifestPath,
|
|
5239
|
+
key,
|
|
5240
|
+
...loaded.schema[key] ? {
|
|
5241
|
+
rule: loaded.schema[key]
|
|
5242
|
+
} : {}
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
async function setSpecEntry(logicalKey, options = {}) {
|
|
5246
|
+
const loaded = await loadSpecManifest(options);
|
|
5247
|
+
const key = logicalKey.trim();
|
|
5248
|
+
const hasSetFields = Object.keys(options.set ?? {}).length > 0;
|
|
5249
|
+
if (!loaded.schema[key] && !hasSetFields) {
|
|
5250
|
+
throw new Error(`Cannot clear fields for undeclared spec key ${key}.`);
|
|
5251
|
+
}
|
|
5252
|
+
const current = loaded.schema[key] ?? {};
|
|
5253
|
+
const next = {
|
|
5254
|
+
...current,
|
|
5255
|
+
...options.set ?? {}
|
|
5256
|
+
};
|
|
5257
|
+
for (const field of options.clear ?? []) {
|
|
5258
|
+
if (hasOwn(next, field)) {
|
|
5259
|
+
delete next[field];
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
if (Object.keys(next).length === 0) {
|
|
5263
|
+
throw new Error(`Spec entry ${key} cannot be empty. Set at least one field or delete the entry.`);
|
|
5264
|
+
}
|
|
5265
|
+
const nextSchema = sortSchema({
|
|
5266
|
+
...loaded.schema,
|
|
5267
|
+
[key]: next
|
|
5268
|
+
});
|
|
5269
|
+
const nextRawManifest = {
|
|
5270
|
+
...loaded.rawManifest,
|
|
5271
|
+
schema: nextSchema
|
|
5272
|
+
};
|
|
5273
|
+
await writeFile10(loaded.manifestPath, stringifyYaml8(nextRawManifest), "utf8");
|
|
5274
|
+
return {
|
|
5275
|
+
manifestPath: loaded.manifestPath,
|
|
5276
|
+
key,
|
|
5277
|
+
action: loaded.schema[key] ? "updated" : "created",
|
|
5278
|
+
rule: next
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
async function deleteSpecEntry(logicalKey, options = {}) {
|
|
5282
|
+
const loaded = await loadSpecManifest(options);
|
|
5283
|
+
const key = logicalKey.trim();
|
|
5284
|
+
if (!loaded.schema[key]) {
|
|
5285
|
+
return {
|
|
5286
|
+
manifestPath: loaded.manifestPath,
|
|
5287
|
+
key,
|
|
5288
|
+
deleted: false
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
const nextSchema = { ...loaded.schema };
|
|
5292
|
+
delete nextSchema[key];
|
|
5293
|
+
const sorted = sortSchema(nextSchema);
|
|
5294
|
+
const nextRawManifest = {
|
|
5295
|
+
...loaded.rawManifest,
|
|
5296
|
+
...Object.keys(sorted).length > 0 ? { schema: sorted } : {}
|
|
5297
|
+
};
|
|
5298
|
+
if (Object.keys(sorted).length === 0) {
|
|
5299
|
+
delete nextRawManifest.schema;
|
|
5300
|
+
}
|
|
5301
|
+
await writeFile10(loaded.manifestPath, stringifyYaml8(nextRawManifest), "utf8");
|
|
5302
|
+
return {
|
|
5303
|
+
manifestPath: loaded.manifestPath,
|
|
5304
|
+
key,
|
|
5305
|
+
deleted: true
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
// src/services/spec/specPrompts.ts
|
|
5310
|
+
import readline5 from "readline";
|
|
5311
|
+
|
|
5312
|
+
// src/services/spec/patternValidation.ts
|
|
5313
|
+
function assertValidSpecPattern(pattern) {
|
|
5314
|
+
try {
|
|
5315
|
+
void new RegExp(pattern);
|
|
5316
|
+
} catch (error) {
|
|
5317
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5318
|
+
throw new Error(`Invalid --pattern regex: ${reason}`);
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
// src/services/spec/specPrompts.ts
|
|
5323
|
+
var SPEC_TYPES = ["string", "number", "boolean", "object", "array"];
|
|
5324
|
+
function parseJsonOrString(rawValue) {
|
|
5325
|
+
try {
|
|
5326
|
+
return JSON.parse(rawValue);
|
|
5327
|
+
} catch {
|
|
5328
|
+
return rawValue;
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
function createPrompt() {
|
|
5332
|
+
const rl = readline5.createInterface({
|
|
5333
|
+
input: process.stdin,
|
|
5334
|
+
output: process.stdout
|
|
5335
|
+
});
|
|
5336
|
+
return {
|
|
5337
|
+
ask(question) {
|
|
5338
|
+
return new Promise((resolve) => {
|
|
5339
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
5340
|
+
});
|
|
5341
|
+
},
|
|
5342
|
+
close() {
|
|
5343
|
+
rl.close();
|
|
5344
|
+
}
|
|
5345
|
+
};
|
|
5346
|
+
}
|
|
5347
|
+
function isInteractiveSpecPromptMode() {
|
|
5348
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
5349
|
+
}
|
|
5350
|
+
async function promptSpecSetInput(logicalKey) {
|
|
5351
|
+
const prompt = createPrompt();
|
|
5352
|
+
try {
|
|
5353
|
+
process.stdout.write(`Config spec for ${logicalKey}
|
|
5354
|
+
`);
|
|
5355
|
+
const set = {};
|
|
5356
|
+
const type = (await prompt.ask(`Type (${SPEC_TYPES.join("|")}) [skip]: `)).toLowerCase();
|
|
5357
|
+
if (type) {
|
|
5358
|
+
if (!SPEC_TYPES.includes(type)) {
|
|
5359
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
5360
|
+
}
|
|
5361
|
+
set.type = type;
|
|
5362
|
+
}
|
|
5363
|
+
const required = (await prompt.ask("Required? (y/n) [skip]: ")).toLowerCase();
|
|
5364
|
+
if (required === "y" || required === "yes") {
|
|
5365
|
+
set.required = true;
|
|
5366
|
+
} else if (required === "n" || required === "no") {
|
|
5367
|
+
set.required = false;
|
|
5368
|
+
}
|
|
5369
|
+
const defaultValue = await prompt.ask("Default value (JSON or string) [skip]: ");
|
|
5370
|
+
if (defaultValue) {
|
|
5371
|
+
set.default = parseJsonOrString(defaultValue);
|
|
5372
|
+
}
|
|
5373
|
+
const enumValue = await prompt.ask("Enum values as JSON array [skip]: ");
|
|
5374
|
+
if (enumValue) {
|
|
5375
|
+
const parsed = parseJsonOrString(enumValue);
|
|
5376
|
+
if (!Array.isArray(parsed)) {
|
|
5377
|
+
throw new Error("Enum must be a JSON array");
|
|
5378
|
+
}
|
|
5379
|
+
if (parsed.length === 0) {
|
|
5380
|
+
throw new Error("Enum must not be empty");
|
|
5381
|
+
}
|
|
5382
|
+
set.enum = parsed;
|
|
5383
|
+
}
|
|
5384
|
+
const pattern = await prompt.ask("Pattern (regex string) [skip]: ");
|
|
5385
|
+
if (pattern) {
|
|
5386
|
+
assertValidSpecPattern(pattern);
|
|
5387
|
+
set.pattern = pattern;
|
|
5388
|
+
}
|
|
5389
|
+
const summary = await prompt.ask("Summary [skip]: ");
|
|
5390
|
+
if (summary) {
|
|
5391
|
+
set.summary = summary;
|
|
5392
|
+
}
|
|
5393
|
+
const description = await prompt.ask("Description [skip]: ");
|
|
5394
|
+
if (description) {
|
|
5395
|
+
set.description = description;
|
|
5396
|
+
}
|
|
5397
|
+
const examples = await prompt.ask("Examples as JSON array [skip]: ");
|
|
5398
|
+
if (examples) {
|
|
5399
|
+
const parsed = parseJsonOrString(examples);
|
|
5400
|
+
if (!Array.isArray(parsed)) {
|
|
5401
|
+
throw new Error("Examples must be a JSON array");
|
|
5402
|
+
}
|
|
5403
|
+
set.examples = parsed;
|
|
5404
|
+
}
|
|
5405
|
+
const usedBy = await prompt.ask("Used by (comma separated) [skip]: ");
|
|
5406
|
+
if (usedBy) {
|
|
5407
|
+
const values = usedBy.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
5408
|
+
if (values.length > 0) {
|
|
5409
|
+
set.usedBy = values;
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
const deprecated = (await prompt.ask("Deprecated? (y/n) [skip]: ")).toLowerCase();
|
|
5413
|
+
if (deprecated === "y" || deprecated === "yes") {
|
|
5414
|
+
set.deprecated = true;
|
|
5415
|
+
} else if (deprecated === "n" || deprecated === "no") {
|
|
5416
|
+
set.deprecated = false;
|
|
5417
|
+
}
|
|
5418
|
+
const deprecationMessage = await prompt.ask("Deprecation message [skip]: ");
|
|
5419
|
+
if (deprecationMessage) {
|
|
5420
|
+
set.deprecationMessage = deprecationMessage;
|
|
5421
|
+
set.deprecated = true;
|
|
5422
|
+
}
|
|
5423
|
+
return {
|
|
5424
|
+
set,
|
|
5425
|
+
clear: [],
|
|
5426
|
+
hasFieldFlags: Object.keys(set).length > 0,
|
|
5427
|
+
cliArgs: []
|
|
5428
|
+
};
|
|
5429
|
+
} finally {
|
|
5430
|
+
prompt.close();
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5434
|
+
// src/services/spec/specSetInput.ts
|
|
5435
|
+
var SPEC_TYPES2 = /* @__PURE__ */ new Set(["string", "number", "boolean", "object", "array"]);
|
|
5436
|
+
var SECRET_FORBIDDEN_FIELDS = ["default", "enum", "examples"];
|
|
5437
|
+
function parseJsonOrString2(rawValue) {
|
|
5438
|
+
try {
|
|
5439
|
+
return JSON.parse(rawValue);
|
|
5440
|
+
} catch {
|
|
5441
|
+
return rawValue;
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
function validateNamespaceQualifiedKey(logicalKey) {
|
|
5445
|
+
const trimmed = logicalKey.trim();
|
|
5446
|
+
if (!trimmed.includes(".")) {
|
|
5447
|
+
throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
|
|
5448
|
+
}
|
|
5449
|
+
const namespace = trimmed.slice(0, trimmed.indexOf("."));
|
|
5450
|
+
const keyPath = trimmed.slice(trimmed.indexOf(".") + 1);
|
|
5451
|
+
if (!namespace || !keyPath) {
|
|
5452
|
+
throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
function parseSetOptionToField(option) {
|
|
5456
|
+
switch (option) {
|
|
5457
|
+
case "--clear-default":
|
|
5458
|
+
return "default";
|
|
5459
|
+
case "--clear-enum":
|
|
5460
|
+
return "enum";
|
|
5461
|
+
case "--clear-pattern":
|
|
5462
|
+
return "pattern";
|
|
5463
|
+
case "--clear-summary":
|
|
5464
|
+
return "summary";
|
|
5465
|
+
case "--clear-description":
|
|
5466
|
+
return "description";
|
|
5467
|
+
case "--clear-examples":
|
|
5468
|
+
return "examples";
|
|
5469
|
+
case "--clear-used-by":
|
|
5470
|
+
return "usedBy";
|
|
5471
|
+
case "--clear-deprecated":
|
|
5472
|
+
return "deprecated";
|
|
5473
|
+
case "--clear-deprecation-message":
|
|
5474
|
+
return "deprecationMessage";
|
|
5475
|
+
default:
|
|
5476
|
+
throw new Error(`Unknown clear option: ${option}`);
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
function assertNoClearConflict(set, clear) {
|
|
5480
|
+
if (clear.has("default") && Object.prototype.hasOwnProperty.call(set, "default")) {
|
|
5481
|
+
throw new Error("Cannot combine --default with --clear-default");
|
|
5482
|
+
}
|
|
5483
|
+
if (clear.has("enum") && Object.prototype.hasOwnProperty.call(set, "enum")) {
|
|
5484
|
+
throw new Error("Cannot combine --enum with --clear-enum");
|
|
5485
|
+
}
|
|
5486
|
+
if (clear.has("pattern") && Object.prototype.hasOwnProperty.call(set, "pattern")) {
|
|
5487
|
+
throw new Error("Cannot combine --pattern with --clear-pattern");
|
|
5488
|
+
}
|
|
5489
|
+
if (clear.has("summary") && Object.prototype.hasOwnProperty.call(set, "summary")) {
|
|
5490
|
+
throw new Error("Cannot combine --summary with --clear-summary");
|
|
5491
|
+
}
|
|
5492
|
+
if (clear.has("description") && Object.prototype.hasOwnProperty.call(set, "description")) {
|
|
5493
|
+
throw new Error("Cannot combine --description with --clear-description");
|
|
5494
|
+
}
|
|
5495
|
+
if (clear.has("examples") && Object.prototype.hasOwnProperty.call(set, "examples")) {
|
|
5496
|
+
throw new Error("Cannot combine --example with --clear-examples");
|
|
5497
|
+
}
|
|
5498
|
+
if (clear.has("usedBy") && Object.prototype.hasOwnProperty.call(set, "usedBy")) {
|
|
5499
|
+
throw new Error("Cannot combine --used-by with --clear-used-by");
|
|
5500
|
+
}
|
|
5501
|
+
if (clear.has("deprecated") && Object.prototype.hasOwnProperty.call(set, "deprecated")) {
|
|
5502
|
+
throw new Error("Cannot combine --deprecated with --clear-deprecated");
|
|
5503
|
+
}
|
|
5504
|
+
if (clear.has("deprecated") && Object.prototype.hasOwnProperty.call(set, "deprecationMessage")) {
|
|
5505
|
+
throw new Error("Cannot combine --clear-deprecated with --deprecation-message");
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
function assertSecretSafeSpecSet(logicalKey, set) {
|
|
5509
|
+
if (!logicalKey.startsWith("secret.")) {
|
|
5510
|
+
return;
|
|
5511
|
+
}
|
|
5512
|
+
const offending = SECRET_FORBIDDEN_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(set, field));
|
|
5513
|
+
if (offending.length > 0) {
|
|
5514
|
+
throw new Error(
|
|
5515
|
+
`Cannot set ${offending.join(", ")} for secret spec key ${logicalKey}. Store secret values in the vault, not schema metadata.`
|
|
5516
|
+
);
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
function parseSpecSetInput(logicalKey, rawCliArgs = []) {
|
|
5520
|
+
validateNamespaceQualifiedKey(logicalKey);
|
|
5521
|
+
const cliArgs = [...rawCliArgs];
|
|
5522
|
+
const clearOptions = [
|
|
5523
|
+
"--clear-default",
|
|
5524
|
+
"--clear-enum",
|
|
5525
|
+
"--clear-pattern",
|
|
5526
|
+
"--clear-summary",
|
|
5527
|
+
"--clear-description",
|
|
5528
|
+
"--clear-examples",
|
|
5529
|
+
"--clear-used-by",
|
|
5530
|
+
"--clear-deprecated",
|
|
5531
|
+
"--clear-deprecation-message"
|
|
5532
|
+
];
|
|
5533
|
+
const clear = /* @__PURE__ */ new Set();
|
|
5534
|
+
for (const option of clearOptions) {
|
|
5535
|
+
if (consumeFlag(cliArgs, option)) {
|
|
5536
|
+
clear.add(parseSetOptionToField(option));
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
if (clear.has("deprecated")) {
|
|
5540
|
+
clear.add("deprecationMessage");
|
|
5541
|
+
}
|
|
5542
|
+
const set = {};
|
|
5543
|
+
const type = consumeOption(cliArgs, "--type");
|
|
5544
|
+
if (type) {
|
|
5545
|
+
if (!SPEC_TYPES2.has(type)) {
|
|
5546
|
+
throw new Error(`Unsupported --type value: ${type}`);
|
|
5547
|
+
}
|
|
5548
|
+
set.type = type;
|
|
5549
|
+
}
|
|
5550
|
+
const required = consumeFlag(cliArgs, "--required");
|
|
5551
|
+
const optional = consumeFlag(cliArgs, "--optional");
|
|
5552
|
+
if (required && optional) {
|
|
5553
|
+
throw new Error("Cannot combine --required and --optional");
|
|
5554
|
+
}
|
|
5555
|
+
if (required) {
|
|
5556
|
+
set.required = true;
|
|
5557
|
+
}
|
|
5558
|
+
if (optional) {
|
|
5559
|
+
set.required = false;
|
|
5560
|
+
}
|
|
5561
|
+
const defaultValue = consumeOption(cliArgs, "--default");
|
|
5562
|
+
if (defaultValue !== void 0) {
|
|
5563
|
+
set.default = parseJsonOrString2(defaultValue);
|
|
5564
|
+
}
|
|
5565
|
+
const enumValue = consumeOption(cliArgs, "--enum");
|
|
5566
|
+
if (enumValue !== void 0) {
|
|
5567
|
+
const parsed = parseJsonOrString2(enumValue);
|
|
5568
|
+
if (!Array.isArray(parsed)) {
|
|
5569
|
+
throw new Error("--enum must be a JSON array");
|
|
5570
|
+
}
|
|
5571
|
+
if (parsed.length === 0) {
|
|
5572
|
+
throw new Error("--enum must not be empty");
|
|
5573
|
+
}
|
|
5574
|
+
set.enum = parsed;
|
|
5575
|
+
}
|
|
5576
|
+
const pattern = consumeOption(cliArgs, "--pattern");
|
|
5577
|
+
if (pattern !== void 0) {
|
|
5578
|
+
assertValidSpecPattern(pattern);
|
|
5579
|
+
set.pattern = pattern;
|
|
5580
|
+
}
|
|
5581
|
+
const summary = consumeOption(cliArgs, "--summary");
|
|
5582
|
+
if (summary !== void 0) {
|
|
5583
|
+
set.summary = summary;
|
|
5584
|
+
}
|
|
5585
|
+
const description = consumeOption(cliArgs, "--description");
|
|
5586
|
+
if (description !== void 0) {
|
|
5587
|
+
set.description = description;
|
|
5588
|
+
}
|
|
5589
|
+
const examples = consumeOptions(cliArgs, "--example").map(parseJsonOrString2);
|
|
5590
|
+
if (examples.length > 0) {
|
|
5591
|
+
set.examples = examples;
|
|
5592
|
+
}
|
|
5593
|
+
const usedBy = consumeOptions(cliArgs, "--used-by");
|
|
5594
|
+
if (usedBy.length > 0) {
|
|
5595
|
+
set.usedBy = usedBy;
|
|
5596
|
+
}
|
|
5597
|
+
const deprecated = consumeFlag(cliArgs, "--deprecated");
|
|
5598
|
+
if (deprecated) {
|
|
5599
|
+
set.deprecated = true;
|
|
5600
|
+
}
|
|
5601
|
+
const deprecationMessage = consumeOption(cliArgs, "--deprecation-message");
|
|
5602
|
+
if (deprecationMessage !== void 0) {
|
|
5603
|
+
set.deprecationMessage = deprecationMessage;
|
|
5604
|
+
set.deprecated = true;
|
|
5605
|
+
}
|
|
5606
|
+
if (clear.has("deprecationMessage") && Object.prototype.hasOwnProperty.call(set, "deprecationMessage")) {
|
|
5607
|
+
throw new Error("Cannot combine --clear-deprecated with --deprecation-message");
|
|
5608
|
+
}
|
|
5609
|
+
if (clear.has("deprecationMessage") && Object.prototype.hasOwnProperty.call(set, "deprecated") && set.deprecated) {
|
|
5610
|
+
}
|
|
5611
|
+
assertNoClearConflict(set, clear);
|
|
5612
|
+
assertSecretSafeSpecSet(logicalKey, set);
|
|
5613
|
+
return {
|
|
5614
|
+
set,
|
|
5615
|
+
clear: Array.from(clear),
|
|
5616
|
+
hasFieldFlags: Object.keys(set).length > 0 || clear.size > 0,
|
|
5617
|
+
cliArgs
|
|
5618
|
+
};
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
// src/commands/spec.ts
|
|
5622
|
+
function normalizeSpecAction(args) {
|
|
5623
|
+
const [action = "list", ...tail] = args;
|
|
5624
|
+
if (["list", "show", "set", "delete", "remove", "doctor"].includes(action)) {
|
|
5625
|
+
return {
|
|
5626
|
+
action: action === "remove" ? "delete" : action,
|
|
5627
|
+
tail
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5630
|
+
return {
|
|
5631
|
+
action: "show",
|
|
5632
|
+
tail: args
|
|
5633
|
+
};
|
|
5634
|
+
}
|
|
5635
|
+
function validateNamespaceQualifiedKey2(logicalKey) {
|
|
5636
|
+
const key = logicalKey.trim();
|
|
5637
|
+
if (!key.includes(".")) {
|
|
5638
|
+
throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
|
|
5639
|
+
}
|
|
5640
|
+
const namespace = key.slice(0, key.indexOf("."));
|
|
5641
|
+
const keyPath = key.slice(key.indexOf(".") + 1);
|
|
5642
|
+
if (!namespace || !keyPath) {
|
|
5643
|
+
throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
|
|
5644
|
+
}
|
|
5645
|
+
return key;
|
|
5646
|
+
}
|
|
5647
|
+
function hasFieldFlag(cliArgs) {
|
|
5648
|
+
const clearFlags = /* @__PURE__ */ new Set([
|
|
5649
|
+
"--clear-default",
|
|
5650
|
+
"--clear-enum",
|
|
5651
|
+
"--clear-pattern",
|
|
5652
|
+
"--clear-summary",
|
|
5653
|
+
"--clear-description",
|
|
5654
|
+
"--clear-examples",
|
|
5655
|
+
"--clear-used-by",
|
|
5656
|
+
"--clear-deprecated",
|
|
5657
|
+
"--clear-deprecation-message"
|
|
5658
|
+
]);
|
|
5659
|
+
const optionFlags = /* @__PURE__ */ new Set([
|
|
5660
|
+
"--type",
|
|
5661
|
+
"--default",
|
|
5662
|
+
"--enum",
|
|
5663
|
+
"--pattern",
|
|
5664
|
+
"--summary",
|
|
5665
|
+
"--description",
|
|
5666
|
+
"--example",
|
|
5667
|
+
"--used-by",
|
|
5668
|
+
"--deprecation-message"
|
|
5669
|
+
]);
|
|
5670
|
+
const toggleFlags = /* @__PURE__ */ new Set(["--required", "--optional", "--deprecated"]);
|
|
5671
|
+
return cliArgs.some((arg) => {
|
|
5672
|
+
const raw = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
5673
|
+
return clearFlags.has(raw) || optionFlags.has(raw) || toggleFlags.has(raw);
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
function assertSecretSafeSpecSet2(logicalKey, set) {
|
|
5677
|
+
if (!logicalKey.startsWith("secret.")) {
|
|
5678
|
+
return;
|
|
5679
|
+
}
|
|
5680
|
+
const forbidden = ["default", "enum", "examples"].filter((field) => Object.prototype.hasOwnProperty.call(set, field));
|
|
5681
|
+
if (forbidden.length > 0) {
|
|
5682
|
+
throw new Error(
|
|
5683
|
+
`Cannot set ${forbidden.join(", ")} for secret spec key ${logicalKey}. Store secret values in the vault, not schema metadata.`
|
|
5684
|
+
);
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
async function runSpec(args = [], options = {}) {
|
|
5688
|
+
const { action, tail } = normalizeSpecAction(args);
|
|
5689
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
5690
|
+
if (action === "list") {
|
|
5691
|
+
const prefix = consumeOption(cliArgs, "--prefix");
|
|
5692
|
+
if (cliArgs.length > 0) {
|
|
5693
|
+
throw new Error(`Unsupported spec list options: ${cliArgs.join(" ")}`);
|
|
5694
|
+
}
|
|
5695
|
+
const result2 = await listSpecEntries({
|
|
5696
|
+
...options,
|
|
5697
|
+
...prefix ? {
|
|
5698
|
+
prefix
|
|
5699
|
+
} : {}
|
|
5700
|
+
});
|
|
5701
|
+
if (options.json) {
|
|
5702
|
+
return printJson(result2);
|
|
5703
|
+
}
|
|
5704
|
+
return result2.entries.map((entry) => entry.key).join("\n");
|
|
5705
|
+
}
|
|
5706
|
+
if (action === "show") {
|
|
5707
|
+
const logicalKey2 = validateNamespaceQualifiedKey2(tail[0] ?? "");
|
|
5708
|
+
if (tail.length > 1) {
|
|
5709
|
+
throw new Error("spec show accepts exactly one <logicalKey>");
|
|
5710
|
+
}
|
|
5711
|
+
if (cliArgs.length > 0) {
|
|
5712
|
+
throw new Error(`Unsupported spec show options: ${cliArgs.join(" ")}`);
|
|
5713
|
+
}
|
|
5714
|
+
const result2 = await showSpecEntry(logicalKey2, options);
|
|
5715
|
+
if (!result2.rule) {
|
|
5716
|
+
throw new Error(`No spec entry found for ${logicalKey2}`);
|
|
5717
|
+
}
|
|
5718
|
+
if (options.json) {
|
|
5719
|
+
return printJson(result2);
|
|
5720
|
+
}
|
|
5721
|
+
return [
|
|
5722
|
+
`${result2.key}:`,
|
|
5723
|
+
...Object.entries(result2.rule).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`)
|
|
5724
|
+
].join("\n");
|
|
5725
|
+
}
|
|
5726
|
+
if (action === "set") {
|
|
5727
|
+
const logicalKey2 = validateNamespaceQualifiedKey2(tail[0] ?? "");
|
|
5728
|
+
if (tail.length > 1) {
|
|
5729
|
+
throw new Error("spec set accepts exactly one <logicalKey>");
|
|
5730
|
+
}
|
|
5731
|
+
const fieldFlagPresent = hasFieldFlag(cliArgs);
|
|
5732
|
+
if (!fieldFlagPresent && options.json) {
|
|
5733
|
+
throw new Error("spec set --json requires field flags; interactive JSON mode is not supported.");
|
|
5734
|
+
}
|
|
5735
|
+
let input = parseSpecSetInput(logicalKey2, cliArgs);
|
|
5736
|
+
if (!input.hasFieldFlags) {
|
|
5737
|
+
if (!isInteractiveSpecPromptMode()) {
|
|
5738
|
+
throw new Error("spec set without field flags requires an interactive TTY.");
|
|
5739
|
+
}
|
|
5740
|
+
input = await promptSpecSetInput(logicalKey2);
|
|
5741
|
+
}
|
|
5742
|
+
if (Object.keys(input.set).length === 0 && input.clear.length === 0) {
|
|
5743
|
+
throw new Error("spec set requires at least one field to set or clear.");
|
|
5744
|
+
}
|
|
5745
|
+
assertSecretSafeSpecSet2(logicalKey2, input.set);
|
|
5746
|
+
if (input.cliArgs.length > 0) {
|
|
5747
|
+
throw new Error(`Unsupported spec set options: ${input.cliArgs.join(" ")}`);
|
|
5748
|
+
}
|
|
5749
|
+
await assertWritableConfigRoot(`update spec ${logicalKey2}`, options);
|
|
5750
|
+
const result2 = await setSpecEntry(logicalKey2, {
|
|
5751
|
+
...options,
|
|
5752
|
+
set: input.set,
|
|
5753
|
+
clear: input.clear
|
|
5754
|
+
});
|
|
5755
|
+
if (options.json) {
|
|
5756
|
+
return printJson(result2);
|
|
5757
|
+
}
|
|
5758
|
+
return `${result2.action} spec ${result2.key}`;
|
|
5759
|
+
}
|
|
5760
|
+
if (action === "doctor") {
|
|
5761
|
+
const fillMissing = cliArgs.includes("--fill-missing");
|
|
5762
|
+
const reviewAll = cliArgs.includes("--review-all");
|
|
5763
|
+
if (fillMissing && reviewAll) {
|
|
5764
|
+
throw new Error("spec doctor accepts only one write mode: --fill-missing or --review-all");
|
|
5765
|
+
}
|
|
5766
|
+
const mode = fillMissing ? "fill-missing" : reviewAll ? "review-all" : "report";
|
|
5767
|
+
if (mode !== "report" && options.json) {
|
|
5768
|
+
throw new Error(`spec doctor --${mode} does not support --json in Phases 1-3.`);
|
|
5769
|
+
}
|
|
5770
|
+
if (cliArgs.some((value) => !["--fill-missing", "--review-all"].includes(value))) {
|
|
5771
|
+
throw new Error(`Unsupported spec doctor options: ${cliArgs.join(" ")}`);
|
|
5772
|
+
}
|
|
5773
|
+
if (mode !== "report") {
|
|
5774
|
+
await assertWritableConfigRoot(`run spec doctor --${mode}`, options);
|
|
5775
|
+
}
|
|
5776
|
+
const outcome = await runSpecDoctor(mode, options);
|
|
5777
|
+
if (outcome.blocking) {
|
|
5778
|
+
process.exitCode = 1;
|
|
5779
|
+
}
|
|
5780
|
+
if (options.json) {
|
|
5781
|
+
return printJson(outcome.result);
|
|
5782
|
+
}
|
|
5783
|
+
return formatSpecDoctorResult(outcome.result);
|
|
5784
|
+
}
|
|
5785
|
+
const logicalKey = validateNamespaceQualifiedKey2(tail[0] ?? "");
|
|
5786
|
+
if (tail.length > 1) {
|
|
5787
|
+
throw new Error("spec delete accepts exactly one <logicalKey>");
|
|
5788
|
+
}
|
|
5789
|
+
if (cliArgs.length > 0) {
|
|
5790
|
+
throw new Error(`Unsupported spec delete options: ${cliArgs.join(" ")}`);
|
|
5791
|
+
}
|
|
5792
|
+
await assertWritableConfigRoot(`delete spec ${logicalKey}`, options);
|
|
5793
|
+
const result = await deleteSpecEntry(logicalKey, options);
|
|
5794
|
+
if (options.json) {
|
|
5795
|
+
return printJson(result);
|
|
5796
|
+
}
|
|
5797
|
+
return result.deleted ? `deleted spec ${result.key}` : `no spec entry found for ${result.key}`;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
// src/commands/use.ts
|
|
5801
|
+
import path22 from "path";
|
|
5802
|
+
async function runUse(args = [], options = {}) {
|
|
5803
|
+
const root = path22.resolve(options.root ?? process.cwd());
|
|
5804
|
+
const action = args[0];
|
|
5805
|
+
const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
|
|
5806
|
+
if (action === "show" || !action && !hasUpdates) {
|
|
5807
|
+
const context = await loadCliContext(root);
|
|
5808
|
+
if (options.json) {
|
|
5809
|
+
return printJson(context);
|
|
5810
|
+
}
|
|
5811
|
+
return Object.keys(context).length === 0 ? "no CLI context configured" : printJson(context);
|
|
5812
|
+
}
|
|
5813
|
+
const result = await saveCliContext({
|
|
5814
|
+
root,
|
|
5815
|
+
...options.workspace ? { workspace: options.workspace } : {},
|
|
5816
|
+
...options.profile ? { profile: options.profile } : {},
|
|
5817
|
+
...options.globalRoot ? { globalRoot: options.globalRoot } : {}
|
|
5818
|
+
});
|
|
5819
|
+
if (options.json) {
|
|
5820
|
+
return printJson(result);
|
|
5821
|
+
}
|
|
5822
|
+
return `updated CLI context in ${displayPath(result.filePath, root)}`;
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
// src/commands/ui.ts
|
|
5826
|
+
import { createServer } from "http";
|
|
5827
|
+
import path23 from "path";
|
|
5828
|
+
import { createRequire } from "module";
|
|
5829
|
+
import { loadManifest as loadManifest11 } from "@kitsy/cnos/internal";
|
|
5830
|
+
function parsePort(value, fallback, flag) {
|
|
5831
|
+
if (!value) {
|
|
5832
|
+
return fallback;
|
|
5833
|
+
}
|
|
5834
|
+
const parsed = Number(value);
|
|
5835
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
5836
|
+
throw new Error(`Invalid value for ${flag}: ${value}`);
|
|
5837
|
+
}
|
|
5838
|
+
return parsed;
|
|
5839
|
+
}
|
|
5840
|
+
function resolveUiPackageRoot() {
|
|
5841
|
+
const require2 = createRequire(import.meta.url);
|
|
5842
|
+
try {
|
|
5843
|
+
const packageJsonPath = require2.resolve("@kitsy/cnos-ui/package.json");
|
|
5844
|
+
return path23.dirname(packageJsonPath);
|
|
5845
|
+
} catch {
|
|
5846
|
+
throw new Error("Unable to resolve @kitsy/cnos-ui. Install workspace dependencies before running `cnos ui`.");
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
function writeJson(response, statusCode, payload) {
|
|
5850
|
+
response.statusCode = statusCode;
|
|
5851
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
5852
|
+
response.end(`${printJson(payload)}
|
|
5853
|
+
`);
|
|
5854
|
+
}
|
|
5855
|
+
async function readJsonBody(request) {
|
|
5856
|
+
const chunks = [];
|
|
5857
|
+
for await (const chunk of request) {
|
|
5858
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4675
5859
|
}
|
|
4676
5860
|
if (chunks.length === 0) {
|
|
4677
5861
|
return {};
|
|
@@ -4733,7 +5917,7 @@ async function handleSummary(options, searchParams) {
|
|
|
4733
5917
|
...runtimeOptions,
|
|
4734
5918
|
secretResolution: "lazy"
|
|
4735
5919
|
});
|
|
4736
|
-
const loadedManifest = await
|
|
5920
|
+
const loadedManifest = await loadManifest11({
|
|
4737
5921
|
...options.root ? { root: options.root } : {},
|
|
4738
5922
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4739
5923
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -4744,7 +5928,7 @@ async function handleSummary(options, searchParams) {
|
|
|
4744
5928
|
const envEntries = runtime.toEnv();
|
|
4745
5929
|
const publicEntries = runtime.toPublicEnv();
|
|
4746
5930
|
const counts = Array.from(runtime.graph.entries.values()).reduce((acc, entry) => {
|
|
4747
|
-
acc.all
|
|
5931
|
+
acc.all = (acc.all ?? 0) + 1;
|
|
4748
5932
|
acc[entry.namespace] = (acc[entry.namespace] ?? 0) + 1;
|
|
4749
5933
|
return acc;
|
|
4750
5934
|
}, { all: 0 });
|
|
@@ -4776,9 +5960,12 @@ async function handleRevealList(body, options) {
|
|
|
4776
5960
|
const prefix = typeof body.prefix === "string" ? body.prefix.trim() : "";
|
|
4777
5961
|
const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
|
|
4778
5962
|
const runtimeOptions = toRuntimeOptionsFromBody(options, body);
|
|
5963
|
+
const processEnv = withUiPassphrase(options.processEnv, passphrase);
|
|
4779
5964
|
const runtime = await createRuntimeService({
|
|
4780
5965
|
...runtimeOptions,
|
|
4781
|
-
processEnv
|
|
5966
|
+
...processEnv ? {
|
|
5967
|
+
processEnv
|
|
5968
|
+
} : {},
|
|
4782
5969
|
secretResolution: "lazy"
|
|
4783
5970
|
});
|
|
4784
5971
|
const entries = [];
|
|
@@ -4807,9 +5994,12 @@ async function handleRevealInspect(body, options) {
|
|
|
4807
5994
|
}
|
|
4808
5995
|
const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
|
|
4809
5996
|
const runtimeOptions = toRuntimeOptionsFromBody(options, body);
|
|
5997
|
+
const processEnv = withUiPassphrase(options.processEnv, passphrase);
|
|
4810
5998
|
const runtime = await createRuntimeService({
|
|
4811
5999
|
...runtimeOptions,
|
|
4812
|
-
processEnv
|
|
6000
|
+
...processEnv ? {
|
|
6001
|
+
processEnv
|
|
6002
|
+
} : {},
|
|
4813
6003
|
...key.startsWith("secret.") ? { secretResolution: "lazy" } : {}
|
|
4814
6004
|
});
|
|
4815
6005
|
if (key.startsWith("secret.")) {
|
|
@@ -4949,7 +6139,7 @@ async function runValidate(options = {}) {
|
|
|
4949
6139
|
// package.json
|
|
4950
6140
|
var package_default = {
|
|
4951
6141
|
name: "@kitsy/cnos-cli",
|
|
4952
|
-
version: "1.
|
|
6142
|
+
version: "1.10.0",
|
|
4953
6143
|
description: "CLI entry point and developer tooling for CNOS.",
|
|
4954
6144
|
type: "module",
|
|
4955
6145
|
main: "./dist/index.js",
|
|
@@ -5226,9 +6416,9 @@ async function runWatch(command, options = {}) {
|
|
|
5226
6416
|
}
|
|
5227
6417
|
|
|
5228
6418
|
// src/commands/workspace.ts
|
|
5229
|
-
import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile8, rename, rm as rm5, stat as stat3, writeFile as
|
|
6419
|
+
import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile8, rename, rm as rm5, stat as stat3, writeFile as writeFile11 } from "fs/promises";
|
|
5230
6420
|
import path25 from "path";
|
|
5231
|
-
import { loadManifest as
|
|
6421
|
+
import { loadManifest as loadManifest12, parseYaml as parseYaml6, stringifyYaml as stringifyYaml9 } from "@kitsy/cnos/internal";
|
|
5232
6422
|
async function exists2(targetPath) {
|
|
5233
6423
|
try {
|
|
5234
6424
|
await stat3(targetPath);
|
|
@@ -5267,9 +6457,9 @@ async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
|
|
|
5267
6457
|
async function writeAnchor(packageRoot, manifestRoot, workspace) {
|
|
5268
6458
|
const relativeRoot = path25.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
|
|
5269
6459
|
const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
|
|
5270
|
-
await
|
|
6460
|
+
await writeFile11(
|
|
5271
6461
|
path25.join(packageRoot, ".cnosrc.yml"),
|
|
5272
|
-
|
|
6462
|
+
stringifyYaml9({
|
|
5273
6463
|
root: rootValue,
|
|
5274
6464
|
...workspace ? { workspace } : {}
|
|
5275
6465
|
}),
|
|
@@ -5319,9 +6509,9 @@ async function hasDirectConfigData(cnosRoot) {
|
|
|
5319
6509
|
async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
|
|
5320
6510
|
const anchorPath = path25.join(packageRoot, ".cnosrc.yml");
|
|
5321
6511
|
const current = await exists2(anchorPath) ? parseYaml6(await readFile8(anchorPath, "utf8")) : void 0;
|
|
5322
|
-
await
|
|
6512
|
+
await writeFile11(
|
|
5323
6513
|
anchorPath,
|
|
5324
|
-
|
|
6514
|
+
stringifyYaml9({
|
|
5325
6515
|
root: typeof current?.root === "string" ? current.root : "./.cnos",
|
|
5326
6516
|
workspace: workspaceId
|
|
5327
6517
|
}),
|
|
@@ -5331,9 +6521,9 @@ async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
|
|
|
5331
6521
|
async function updateWorkspaceContext(packageRoot, workspaceId) {
|
|
5332
6522
|
const workspacePath = path25.join(packageRoot, ".cnos-workspace.yml");
|
|
5333
6523
|
const current = await exists2(workspacePath) ? parseYaml6(await readFile8(workspacePath, "utf8")) : void 0;
|
|
5334
|
-
await
|
|
6524
|
+
await writeFile11(
|
|
5335
6525
|
workspacePath,
|
|
5336
|
-
|
|
6526
|
+
stringifyYaml9({
|
|
5337
6527
|
workspace: workspaceId,
|
|
5338
6528
|
...typeof current?.profile === "string" ? { profile: current.profile } : {},
|
|
5339
6529
|
...typeof current?.globalRoot === "string" ? { globalRoot: current.globalRoot } : { globalRoot: "~/.cnos" }
|
|
@@ -5342,7 +6532,7 @@ async function updateWorkspaceContext(packageRoot, workspaceId) {
|
|
|
5342
6532
|
);
|
|
5343
6533
|
}
|
|
5344
6534
|
async function runDetach(packageRoot, options = {}) {
|
|
5345
|
-
const loaded = await
|
|
6535
|
+
const loaded = await loadManifest12({ cwd: packageRoot });
|
|
5346
6536
|
if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
|
|
5347
6537
|
throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
|
|
5348
6538
|
}
|
|
@@ -5362,9 +6552,9 @@ async function runDetach(packageRoot, options = {}) {
|
|
|
5362
6552
|
const localRoots = runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => root.path);
|
|
5363
6553
|
await mkdir7(targetCnosRoot, { recursive: true });
|
|
5364
6554
|
await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
|
|
5365
|
-
await
|
|
6555
|
+
await writeFile11(
|
|
5366
6556
|
path25.join(targetCnosRoot, "cnos.yml"),
|
|
5367
|
-
|
|
6557
|
+
stringifyYaml9(createDetachedManifest(loaded.rawManifest)),
|
|
5368
6558
|
"utf8"
|
|
5369
6559
|
);
|
|
5370
6560
|
const relativeRoot = path25.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
|
|
@@ -5377,8 +6567,8 @@ async function runDetach(packageRoot, options = {}) {
|
|
|
5377
6567
|
workspace: loaded.anchoredWorkspace
|
|
5378
6568
|
}
|
|
5379
6569
|
};
|
|
5380
|
-
await
|
|
5381
|
-
await
|
|
6570
|
+
await writeFile11(path25.join(targetCnosRoot, ".detached"), stringifyYaml9(marker), "utf8");
|
|
6571
|
+
await writeFile11(path25.join(packageRoot, ".cnosrc.yml"), stringifyYaml9({ root: "./.cnos" }), "utf8");
|
|
5382
6572
|
if (options.json) {
|
|
5383
6573
|
return printJson({
|
|
5384
6574
|
packageRoot,
|
|
@@ -5401,7 +6591,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
5401
6591
|
throw new Error("Invalid .detached marker");
|
|
5402
6592
|
}
|
|
5403
6593
|
const parentManifestRoot = path25.resolve(packageRoot, marker.originalCnosrc.root);
|
|
5404
|
-
const parentLoaded = await
|
|
6594
|
+
const parentLoaded = await loadManifest12({ root: parentManifestRoot });
|
|
5405
6595
|
if (parentLoaded.rootResolution.readOnly) {
|
|
5406
6596
|
throw new Error(
|
|
5407
6597
|
`Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
|
|
@@ -5425,7 +6615,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
5425
6615
|
items[workspaceId] = items[workspaceId] ?? {};
|
|
5426
6616
|
workspaces.items = items;
|
|
5427
6617
|
rawManifest.workspaces = workspaces;
|
|
5428
|
-
await
|
|
6618
|
+
await writeFile11(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5429
6619
|
const archivePath = path25.join(packageRoot, ".cnos.detached.bak");
|
|
5430
6620
|
await rm5(archivePath, { recursive: true, force: true });
|
|
5431
6621
|
await rename(childCnosRoot, archivePath);
|
|
@@ -5441,7 +6631,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
5441
6631
|
return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
|
|
5442
6632
|
}
|
|
5443
6633
|
async function runList2(manifestCwd, options = {}) {
|
|
5444
|
-
const loaded = await
|
|
6634
|
+
const loaded = await loadManifest12({
|
|
5445
6635
|
...options.root ? { root: options.root } : {},
|
|
5446
6636
|
cwd: manifestCwd,
|
|
5447
6637
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5474,7 +6664,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
|
|
|
5474
6664
|
if (cliArgs.length > 0) {
|
|
5475
6665
|
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
5476
6666
|
}
|
|
5477
|
-
const loaded = await
|
|
6667
|
+
const loaded = await loadManifest12({
|
|
5478
6668
|
...options.root ? { root: options.root } : {},
|
|
5479
6669
|
cwd: manifestCwd,
|
|
5480
6670
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5507,7 +6697,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
|
|
|
5507
6697
|
base: {}
|
|
5508
6698
|
};
|
|
5509
6699
|
rawManifest.workspaces = rawWorkspaces;
|
|
5510
|
-
await
|
|
6700
|
+
await writeFile11(path25.join(cnosRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5511
6701
|
await updateRootAnchorToWorkspace(packageRoot, "base");
|
|
5512
6702
|
await updateWorkspaceContext(packageRoot, "base");
|
|
5513
6703
|
await ensureGitignore(path25.dirname(cnosRoot));
|
|
@@ -5528,7 +6718,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
|
|
|
5528
6718
|
if (cliArgs.length > 0) {
|
|
5529
6719
|
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
5530
6720
|
}
|
|
5531
|
-
const loaded = await
|
|
6721
|
+
const loaded = await loadManifest12({
|
|
5532
6722
|
...options.root ? { root: options.root } : {},
|
|
5533
6723
|
cwd: manifestCwd,
|
|
5534
6724
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5560,7 +6750,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
|
|
|
5560
6750
|
rawManifest.workspaces = rawWorkspaces;
|
|
5561
6751
|
const workspaceRoot = path25.join(cnosRoot, "workspaces", workspaceId);
|
|
5562
6752
|
const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
|
|
5563
|
-
await
|
|
6753
|
+
await writeFile11(path25.join(cnosRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5564
6754
|
await ensureGitignore(path25.dirname(cnosRoot));
|
|
5565
6755
|
await writeAnchor(packageRoot, cnosRoot, workspaceId);
|
|
5566
6756
|
await updateWorkspaceContext(packageRoot, workspaceId);
|
|
@@ -5582,7 +6772,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
|
|
|
5582
6772
|
if (cliArgs.length > 0) {
|
|
5583
6773
|
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
5584
6774
|
}
|
|
5585
|
-
const loaded = await
|
|
6775
|
+
const loaded = await loadManifest12({
|
|
5586
6776
|
...options.root ? { root: options.root } : {},
|
|
5587
6777
|
cwd: manifestCwd,
|
|
5588
6778
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5604,7 +6794,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
|
|
|
5604
6794
|
delete rawItems[workspaceId];
|
|
5605
6795
|
rawWorkspaces.items = rawItems;
|
|
5606
6796
|
rawManifest.workspaces = rawWorkspaces;
|
|
5607
|
-
await
|
|
6797
|
+
await writeFile11(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5608
6798
|
await rm5(path25.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
|
|
5609
6799
|
if (options.json) {
|
|
5610
6800
|
return printJson({
|
|
@@ -5693,6 +6883,9 @@ function resolveHelpTopic(command, args) {
|
|
|
5693
6883
|
if (command === "profile" && args[0] && ["create", "list", "use", "delete", "remove"].includes(args[0])) {
|
|
5694
6884
|
return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
|
|
5695
6885
|
}
|
|
6886
|
+
if (command === "spec" && args[0] && ["list", "show", "set", "delete", "remove", "doctor"].includes(args[0])) {
|
|
6887
|
+
return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
|
|
6888
|
+
}
|
|
5696
6889
|
return normalizeHelpTopic([command]);
|
|
5697
6890
|
}
|
|
5698
6891
|
async function main(argv) {
|
|
@@ -5770,6 +6963,10 @@ async function main(argv) {
|
|
|
5770
6963
|
return;
|
|
5771
6964
|
case "secret":
|
|
5772
6965
|
process.stdout.write(`${await runSecret(args.length > 0 ? args : ["app.token"], runtimeOptions)}
|
|
6966
|
+
`);
|
|
6967
|
+
return;
|
|
6968
|
+
case "spec":
|
|
6969
|
+
process.stdout.write(`${await runSpec(args, runtimeOptions)}
|
|
5773
6970
|
`);
|
|
5774
6971
|
return;
|
|
5775
6972
|
case "vault":
|