@kitsy/cnos-cli 1.9.1 → 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 +1373 -132
- 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.",
|
|
@@ -2177,12 +2449,14 @@ var COMMANDS = [
|
|
|
2177
2449
|
{
|
|
2178
2450
|
id: "secret set",
|
|
2179
2451
|
summary: "Write a secret securely.",
|
|
2180
|
-
usage: "cnos secret set <path>
|
|
2181
|
-
description: "Writes a secret reference into the repo. When a local vault is selected, CNOS stores encrypted secret material outside the repo under ~/.cnos/secrets/vaults/<vault>; when an environment-backed vault is selected, CNOS writes an env-backed ref for CI or cloud runtimes.",
|
|
2452
|
+
usage: "cnos secret set <path> [value] [--local|--remote|--ref] [--vault <name>] [--provider <name>] [--stdin] [global-options]",
|
|
2453
|
+
description: "Writes a secret reference into the repo. When a local vault is selected, CNOS stores encrypted secret material outside the repo under ~/.cnos/secrets/vaults/<vault>; when an environment-backed vault is selected, CNOS writes an env-backed ref for CI or cloud runtimes. If [value] is omitted, CNOS prompts for a masked value interactively; use --stdin for pipelines.",
|
|
2182
2454
|
examples: [
|
|
2183
2455
|
"cnos vault create db",
|
|
2184
2456
|
"cnos vault auth db",
|
|
2185
2457
|
"cnos secret set app.token super-secret --vault db",
|
|
2458
|
+
"cnos secret set app.token --vault db",
|
|
2459
|
+
'printf "super-secret" | cnos secret set app.token --vault db --stdin',
|
|
2186
2460
|
"cnos vault create github-ci --provider environment --no-passphrase",
|
|
2187
2461
|
"cnos secret set app.token APP_TOKEN --vault github-ci"
|
|
2188
2462
|
]
|
|
@@ -2541,7 +2815,7 @@ var COMMANDS = [
|
|
|
2541
2815
|
id: "doctor",
|
|
2542
2816
|
summary: "Run repository and workspace diagnostics.",
|
|
2543
2817
|
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.",
|
|
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.",
|
|
2545
2819
|
examples: ["cnos doctor", "cnos doctor --workspace api --json", "cnos doctor --fix-secret-env-mappings"]
|
|
2546
2820
|
},
|
|
2547
2821
|
{
|
|
@@ -3094,9 +3368,15 @@ function printTable(rows) {
|
|
|
3094
3368
|
...rows.map((row) => stringifyCell(row[column]).length)
|
|
3095
3369
|
)
|
|
3096
3370
|
);
|
|
3097
|
-
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();
|
|
3098
3375
|
return [
|
|
3099
|
-
columns.map((column, index) =>
|
|
3376
|
+
columns.map((column, index) => {
|
|
3377
|
+
const width = widths[index] ?? column.length;
|
|
3378
|
+
return column.padEnd(width, " ");
|
|
3379
|
+
}).join(" ").trimEnd(),
|
|
3100
3380
|
widths.map((width) => "-".repeat(width)).join(" "),
|
|
3101
3381
|
...rows.map(renderRow)
|
|
3102
3382
|
].join("\n");
|
|
@@ -3288,7 +3568,7 @@ async function runList(args = [], options = {}) {
|
|
|
3288
3568
|
import path12 from "path";
|
|
3289
3569
|
import {
|
|
3290
3570
|
applyManifestMappings,
|
|
3291
|
-
loadManifest as
|
|
3571
|
+
loadManifest as loadManifest5,
|
|
3292
3572
|
proposeMapping,
|
|
3293
3573
|
rewriteSourceFiles,
|
|
3294
3574
|
scanEnvUsage
|
|
@@ -3305,7 +3585,7 @@ async function runMigrate(options = {}) {
|
|
|
3305
3585
|
if (apply) {
|
|
3306
3586
|
await assertWritableConfigRoot("apply migration mappings", options);
|
|
3307
3587
|
}
|
|
3308
|
-
const manifest = await
|
|
3588
|
+
const manifest = await loadManifest5({
|
|
3309
3589
|
...options.root ? { root: options.root } : {},
|
|
3310
3590
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3311
3591
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3471,7 +3751,7 @@ async function runNamespace(namespace, args = [], options = {}) {
|
|
|
3471
3751
|
import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile5 } from "fs/promises";
|
|
3472
3752
|
import path14 from "path";
|
|
3473
3753
|
import readline2 from "readline/promises";
|
|
3474
|
-
import { loadManifest as
|
|
3754
|
+
import { loadManifest as loadManifest6, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
|
|
3475
3755
|
import { parse as parseToml } from "smol-toml";
|
|
3476
3756
|
var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
|
|
3477
3757
|
var SECRET_LIKE_PATTERN = /(secret|token|password|passwd|private|api[_-]?key|client[_-]?secret|dsn)/i;
|
|
@@ -3656,7 +3936,7 @@ async function runOnboard(options = {}) {
|
|
|
3656
3936
|
});
|
|
3657
3937
|
scaffolded = scaffold.created;
|
|
3658
3938
|
}
|
|
3659
|
-
const loaded = await
|
|
3939
|
+
const loaded = await loadManifest6({
|
|
3660
3940
|
root,
|
|
3661
3941
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
3662
3942
|
});
|
|
@@ -3781,13 +4061,13 @@ async function saveCliContext(options = {}) {
|
|
|
3781
4061
|
// src/services/profiles.ts
|
|
3782
4062
|
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile7, rm as rm3, writeFile as writeFile7 } from "fs/promises";
|
|
3783
4063
|
import path16 from "path";
|
|
3784
|
-
import { loadManifest as
|
|
4064
|
+
import { loadManifest as loadManifest7, parseYaml as parseYaml5, stringifyYaml as stringifyYaml5 } from "@kitsy/cnos/internal";
|
|
3785
4065
|
async function resolveProfilesRoot(root = process.cwd()) {
|
|
3786
4066
|
try {
|
|
3787
|
-
const loadedManifest = await
|
|
4067
|
+
const loadedManifest = await loadManifest7({ root });
|
|
3788
4068
|
return path16.join(loadedManifest.manifestRoot, "profiles");
|
|
3789
4069
|
} catch {
|
|
3790
|
-
const loadedManifest = await
|
|
4070
|
+
const loadedManifest = await loadManifest7({ cwd: root });
|
|
3791
4071
|
return path16.join(loadedManifest.manifestRoot, "profiles");
|
|
3792
4072
|
}
|
|
3793
4073
|
}
|
|
@@ -3933,7 +4213,7 @@ import path18 from "path";
|
|
|
3933
4213
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
3934
4214
|
import {
|
|
3935
4215
|
ensureProjectionAllowed,
|
|
3936
|
-
loadManifest as
|
|
4216
|
+
loadManifest as loadManifest8,
|
|
3937
4217
|
stringifyYaml as stringifyYaml6
|
|
3938
4218
|
} from "@kitsy/cnos/internal";
|
|
3939
4219
|
function normalizeTarget(value) {
|
|
@@ -3965,7 +4245,7 @@ async function runPromote(args = [], options = {}) {
|
|
|
3965
4245
|
} else if (allowSecret) {
|
|
3966
4246
|
throw new Error("--allow-secret is only supported with promote --to env");
|
|
3967
4247
|
}
|
|
3968
|
-
const loadedManifest = await
|
|
4248
|
+
const loadedManifest = await loadManifest8({
|
|
3969
4249
|
...options.root ? { root: options.root } : {},
|
|
3970
4250
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3971
4251
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4037,7 +4317,7 @@ import {
|
|
|
4037
4317
|
serializeRuntimeGraph,
|
|
4038
4318
|
serializeSecretPayload
|
|
4039
4319
|
} from "@kitsy/cnos/internal";
|
|
4040
|
-
function
|
|
4320
|
+
function consumeOptions2(args, flag) {
|
|
4041
4321
|
const values = [];
|
|
4042
4322
|
for (let index = 0; index < args.length; ) {
|
|
4043
4323
|
const token = args[index];
|
|
@@ -4076,7 +4356,7 @@ async function runCommand(command, options = {}) {
|
|
|
4076
4356
|
const isAuthenticated = consumeFlag(cliArgs, "--auth");
|
|
4077
4357
|
const framework = consumeOption(cliArgs, "--framework");
|
|
4078
4358
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
4079
|
-
const setOverrides = normalizeSetOverrides(
|
|
4359
|
+
const setOverrides = normalizeSetOverrides(consumeOptions2(cliArgs, "--set"));
|
|
4080
4360
|
const runtime = await createRuntimeService({
|
|
4081
4361
|
...options,
|
|
4082
4362
|
cliArgs: [...cliArgs, ...setOverrides]
|
|
@@ -4132,6 +4412,8 @@ async function runCommand(command, options = {}) {
|
|
|
4132
4412
|
|
|
4133
4413
|
// src/commands/secret.ts
|
|
4134
4414
|
import path21 from "path";
|
|
4415
|
+
import readline3 from "readline";
|
|
4416
|
+
import { Writable } from "stream";
|
|
4135
4417
|
|
|
4136
4418
|
// src/commands/vault.ts
|
|
4137
4419
|
import path20 from "path";
|
|
@@ -4145,7 +4427,7 @@ import {
|
|
|
4145
4427
|
createSecretVault,
|
|
4146
4428
|
deriveVaultKey,
|
|
4147
4429
|
listLocalSecrets,
|
|
4148
|
-
loadManifest as
|
|
4430
|
+
loadManifest as loadManifest9,
|
|
4149
4431
|
listSecretVaults,
|
|
4150
4432
|
readVaultMetadata,
|
|
4151
4433
|
resolveSecretStoreRoot as resolveSecretStoreRoot2,
|
|
@@ -4184,7 +4466,7 @@ async function createVaultDefinition(name, options = {}) {
|
|
|
4184
4466
|
if (provider === "local" && (options.noPassphrase ?? false)) {
|
|
4185
4467
|
throw new Error("Local vaults cannot be passwordless.");
|
|
4186
4468
|
}
|
|
4187
|
-
const loadedManifest = await
|
|
4469
|
+
const loadedManifest = await loadManifest9({
|
|
4188
4470
|
...options.root ? { root: options.root } : {},
|
|
4189
4471
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4190
4472
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4221,7 +4503,7 @@ async function createVaultDefinition(name, options = {}) {
|
|
|
4221
4503
|
};
|
|
4222
4504
|
}
|
|
4223
4505
|
async function listVaultDefinitions(options = {}) {
|
|
4224
|
-
const loadedManifest = await
|
|
4506
|
+
const loadedManifest = await loadManifest9({
|
|
4225
4507
|
...options.root ? { root: options.root } : {},
|
|
4226
4508
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4227
4509
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4242,7 +4524,7 @@ async function listVaultDefinitions(options = {}) {
|
|
|
4242
4524
|
async function removeVaultDefinition(name, options = {}) {
|
|
4243
4525
|
await assertWritableConfigRoot(`remove vault ${name}`, options);
|
|
4244
4526
|
const vault = name.trim() || "default";
|
|
4245
|
-
const loadedManifest = await
|
|
4527
|
+
const loadedManifest = await loadManifest9({
|
|
4246
4528
|
...options.root ? { root: options.root } : {},
|
|
4247
4529
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4248
4530
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4288,7 +4570,7 @@ async function listLocalStoreVaults(options = {}) {
|
|
|
4288
4570
|
}
|
|
4289
4571
|
async function authenticateVault(name, options = {}) {
|
|
4290
4572
|
const vault = name.trim() || "default";
|
|
4291
|
-
const loadedManifest = await
|
|
4573
|
+
const loadedManifest = await loadManifest9({
|
|
4292
4574
|
...options.root ? { root: options.root } : {},
|
|
4293
4575
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4294
4576
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -4470,6 +4752,45 @@ async function readStdinValue() {
|
|
|
4470
4752
|
}
|
|
4471
4753
|
return Buffer.concat(chunks).toString("utf8").trimEnd();
|
|
4472
4754
|
}
|
|
4755
|
+
async function promptHiddenValue(message) {
|
|
4756
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4757
|
+
throw new Error("Cannot prompt for a secret value in non-interactive mode. Pass <value> explicitly or use --stdin.");
|
|
4758
|
+
}
|
|
4759
|
+
const mutableStdout = new WritableMask();
|
|
4760
|
+
const rl = readline3.createInterface({
|
|
4761
|
+
input: process.stdin,
|
|
4762
|
+
output: mutableStdout,
|
|
4763
|
+
terminal: true
|
|
4764
|
+
});
|
|
4765
|
+
try {
|
|
4766
|
+
mutableStdout.muted = true;
|
|
4767
|
+
const value = await new Promise((resolve) => {
|
|
4768
|
+
rl.question(message, resolve);
|
|
4769
|
+
});
|
|
4770
|
+
process.stdout.write("\n");
|
|
4771
|
+
return value;
|
|
4772
|
+
} finally {
|
|
4773
|
+
rl.close();
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
async function resolveSecretSetValue(secretPath, providedValue, stdin) {
|
|
4777
|
+
if (stdin) {
|
|
4778
|
+
return readStdinValue();
|
|
4779
|
+
}
|
|
4780
|
+
if (providedValue !== void 0) {
|
|
4781
|
+
return providedValue;
|
|
4782
|
+
}
|
|
4783
|
+
return promptHiddenValue(`Enter value for secret "${secretPath}": `);
|
|
4784
|
+
}
|
|
4785
|
+
var WritableMask = class extends Writable {
|
|
4786
|
+
muted = false;
|
|
4787
|
+
_write(chunk, _encoding, callback) {
|
|
4788
|
+
if (!this.muted) {
|
|
4789
|
+
process.stdout.write(chunk);
|
|
4790
|
+
}
|
|
4791
|
+
callback();
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4473
4794
|
async function runSecret(argsOrPath, options = {}) {
|
|
4474
4795
|
const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
|
|
4475
4796
|
const { action, tail } = normalizeSecretCommand(args);
|
|
@@ -4514,8 +4835,9 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
4514
4835
|
const provider = consumeOption(cliArgs, "--provider");
|
|
4515
4836
|
const vault = consumeOption(cliArgs, "--vault") ?? "default";
|
|
4516
4837
|
const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
|
|
4517
|
-
const
|
|
4518
|
-
const
|
|
4838
|
+
const resolvedSecretPath = secretPath2 ?? "app.token";
|
|
4839
|
+
const rawValue = await resolveSecretSetValue(resolvedSecretPath, tail[1], stdin);
|
|
4840
|
+
const result = await setSecret(resolvedSecretPath, rawValue, {
|
|
4519
4841
|
...options,
|
|
4520
4842
|
cliArgs,
|
|
4521
4843
|
target,
|
|
@@ -4569,64 +4891,970 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
4569
4891
|
return printValue(valueForOutput);
|
|
4570
4892
|
}
|
|
4571
4893
|
|
|
4572
|
-
// src/
|
|
4573
|
-
import
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
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);
|
|
4582
4905
|
}
|
|
4583
|
-
|
|
4906
|
+
callback();
|
|
4584
4907
|
}
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
...options.globalRoot ? { globalRoot: options.globalRoot } : {}
|
|
4590
|
-
});
|
|
4591
|
-
if (options.json) {
|
|
4592
|
-
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.`);
|
|
4593
4912
|
}
|
|
4594
|
-
return `updated CLI context in ${displayPath(result.filePath, root)}`;
|
|
4595
4913
|
}
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
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();
|
|
4609
4932
|
}
|
|
4610
|
-
return parsed;
|
|
4611
4933
|
}
|
|
4612
|
-
function
|
|
4613
|
-
|
|
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
|
+
});
|
|
4614
4941
|
try {
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4942
|
+
return await new Promise((resolve) => {
|
|
4943
|
+
rl.question(message, (answer) => resolve(answer));
|
|
4944
|
+
});
|
|
4945
|
+
} finally {
|
|
4946
|
+
rl.close();
|
|
4619
4947
|
}
|
|
4620
4948
|
}
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
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, " ");
|
|
4626
4960
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
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) {
|
|
4630
5858
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4631
5859
|
}
|
|
4632
5860
|
if (chunks.length === 0) {
|
|
@@ -4689,7 +5917,7 @@ async function handleSummary(options, searchParams) {
|
|
|
4689
5917
|
...runtimeOptions,
|
|
4690
5918
|
secretResolution: "lazy"
|
|
4691
5919
|
});
|
|
4692
|
-
const loadedManifest = await
|
|
5920
|
+
const loadedManifest = await loadManifest11({
|
|
4693
5921
|
...options.root ? { root: options.root } : {},
|
|
4694
5922
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
4695
5923
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -4700,7 +5928,7 @@ async function handleSummary(options, searchParams) {
|
|
|
4700
5928
|
const envEntries = runtime.toEnv();
|
|
4701
5929
|
const publicEntries = runtime.toPublicEnv();
|
|
4702
5930
|
const counts = Array.from(runtime.graph.entries.values()).reduce((acc, entry) => {
|
|
4703
|
-
acc.all
|
|
5931
|
+
acc.all = (acc.all ?? 0) + 1;
|
|
4704
5932
|
acc[entry.namespace] = (acc[entry.namespace] ?? 0) + 1;
|
|
4705
5933
|
return acc;
|
|
4706
5934
|
}, { all: 0 });
|
|
@@ -4732,9 +5960,12 @@ async function handleRevealList(body, options) {
|
|
|
4732
5960
|
const prefix = typeof body.prefix === "string" ? body.prefix.trim() : "";
|
|
4733
5961
|
const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
|
|
4734
5962
|
const runtimeOptions = toRuntimeOptionsFromBody(options, body);
|
|
5963
|
+
const processEnv = withUiPassphrase(options.processEnv, passphrase);
|
|
4735
5964
|
const runtime = await createRuntimeService({
|
|
4736
5965
|
...runtimeOptions,
|
|
4737
|
-
processEnv
|
|
5966
|
+
...processEnv ? {
|
|
5967
|
+
processEnv
|
|
5968
|
+
} : {},
|
|
4738
5969
|
secretResolution: "lazy"
|
|
4739
5970
|
});
|
|
4740
5971
|
const entries = [];
|
|
@@ -4763,9 +5994,12 @@ async function handleRevealInspect(body, options) {
|
|
|
4763
5994
|
}
|
|
4764
5995
|
const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
|
|
4765
5996
|
const runtimeOptions = toRuntimeOptionsFromBody(options, body);
|
|
5997
|
+
const processEnv = withUiPassphrase(options.processEnv, passphrase);
|
|
4766
5998
|
const runtime = await createRuntimeService({
|
|
4767
5999
|
...runtimeOptions,
|
|
4768
|
-
processEnv
|
|
6000
|
+
...processEnv ? {
|
|
6001
|
+
processEnv
|
|
6002
|
+
} : {},
|
|
4769
6003
|
...key.startsWith("secret.") ? { secretResolution: "lazy" } : {}
|
|
4770
6004
|
});
|
|
4771
6005
|
if (key.startsWith("secret.")) {
|
|
@@ -4905,7 +6139,7 @@ async function runValidate(options = {}) {
|
|
|
4905
6139
|
// package.json
|
|
4906
6140
|
var package_default = {
|
|
4907
6141
|
name: "@kitsy/cnos-cli",
|
|
4908
|
-
version: "1.
|
|
6142
|
+
version: "1.10.0",
|
|
4909
6143
|
description: "CLI entry point and developer tooling for CNOS.",
|
|
4910
6144
|
type: "module",
|
|
4911
6145
|
main: "./dist/index.js",
|
|
@@ -5182,9 +6416,9 @@ async function runWatch(command, options = {}) {
|
|
|
5182
6416
|
}
|
|
5183
6417
|
|
|
5184
6418
|
// src/commands/workspace.ts
|
|
5185
|
-
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";
|
|
5186
6420
|
import path25 from "path";
|
|
5187
|
-
import { loadManifest as
|
|
6421
|
+
import { loadManifest as loadManifest12, parseYaml as parseYaml6, stringifyYaml as stringifyYaml9 } from "@kitsy/cnos/internal";
|
|
5188
6422
|
async function exists2(targetPath) {
|
|
5189
6423
|
try {
|
|
5190
6424
|
await stat3(targetPath);
|
|
@@ -5223,9 +6457,9 @@ async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
|
|
|
5223
6457
|
async function writeAnchor(packageRoot, manifestRoot, workspace) {
|
|
5224
6458
|
const relativeRoot = path25.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
|
|
5225
6459
|
const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
|
|
5226
|
-
await
|
|
6460
|
+
await writeFile11(
|
|
5227
6461
|
path25.join(packageRoot, ".cnosrc.yml"),
|
|
5228
|
-
|
|
6462
|
+
stringifyYaml9({
|
|
5229
6463
|
root: rootValue,
|
|
5230
6464
|
...workspace ? { workspace } : {}
|
|
5231
6465
|
}),
|
|
@@ -5275,9 +6509,9 @@ async function hasDirectConfigData(cnosRoot) {
|
|
|
5275
6509
|
async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
|
|
5276
6510
|
const anchorPath = path25.join(packageRoot, ".cnosrc.yml");
|
|
5277
6511
|
const current = await exists2(anchorPath) ? parseYaml6(await readFile8(anchorPath, "utf8")) : void 0;
|
|
5278
|
-
await
|
|
6512
|
+
await writeFile11(
|
|
5279
6513
|
anchorPath,
|
|
5280
|
-
|
|
6514
|
+
stringifyYaml9({
|
|
5281
6515
|
root: typeof current?.root === "string" ? current.root : "./.cnos",
|
|
5282
6516
|
workspace: workspaceId
|
|
5283
6517
|
}),
|
|
@@ -5287,9 +6521,9 @@ async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
|
|
|
5287
6521
|
async function updateWorkspaceContext(packageRoot, workspaceId) {
|
|
5288
6522
|
const workspacePath = path25.join(packageRoot, ".cnos-workspace.yml");
|
|
5289
6523
|
const current = await exists2(workspacePath) ? parseYaml6(await readFile8(workspacePath, "utf8")) : void 0;
|
|
5290
|
-
await
|
|
6524
|
+
await writeFile11(
|
|
5291
6525
|
workspacePath,
|
|
5292
|
-
|
|
6526
|
+
stringifyYaml9({
|
|
5293
6527
|
workspace: workspaceId,
|
|
5294
6528
|
...typeof current?.profile === "string" ? { profile: current.profile } : {},
|
|
5295
6529
|
...typeof current?.globalRoot === "string" ? { globalRoot: current.globalRoot } : { globalRoot: "~/.cnos" }
|
|
@@ -5298,7 +6532,7 @@ async function updateWorkspaceContext(packageRoot, workspaceId) {
|
|
|
5298
6532
|
);
|
|
5299
6533
|
}
|
|
5300
6534
|
async function runDetach(packageRoot, options = {}) {
|
|
5301
|
-
const loaded = await
|
|
6535
|
+
const loaded = await loadManifest12({ cwd: packageRoot });
|
|
5302
6536
|
if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
|
|
5303
6537
|
throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
|
|
5304
6538
|
}
|
|
@@ -5318,9 +6552,9 @@ async function runDetach(packageRoot, options = {}) {
|
|
|
5318
6552
|
const localRoots = runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => root.path);
|
|
5319
6553
|
await mkdir7(targetCnosRoot, { recursive: true });
|
|
5320
6554
|
await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
|
|
5321
|
-
await
|
|
6555
|
+
await writeFile11(
|
|
5322
6556
|
path25.join(targetCnosRoot, "cnos.yml"),
|
|
5323
|
-
|
|
6557
|
+
stringifyYaml9(createDetachedManifest(loaded.rawManifest)),
|
|
5324
6558
|
"utf8"
|
|
5325
6559
|
);
|
|
5326
6560
|
const relativeRoot = path25.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
|
|
@@ -5333,8 +6567,8 @@ async function runDetach(packageRoot, options = {}) {
|
|
|
5333
6567
|
workspace: loaded.anchoredWorkspace
|
|
5334
6568
|
}
|
|
5335
6569
|
};
|
|
5336
|
-
await
|
|
5337
|
-
await
|
|
6570
|
+
await writeFile11(path25.join(targetCnosRoot, ".detached"), stringifyYaml9(marker), "utf8");
|
|
6571
|
+
await writeFile11(path25.join(packageRoot, ".cnosrc.yml"), stringifyYaml9({ root: "./.cnos" }), "utf8");
|
|
5338
6572
|
if (options.json) {
|
|
5339
6573
|
return printJson({
|
|
5340
6574
|
packageRoot,
|
|
@@ -5357,7 +6591,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
5357
6591
|
throw new Error("Invalid .detached marker");
|
|
5358
6592
|
}
|
|
5359
6593
|
const parentManifestRoot = path25.resolve(packageRoot, marker.originalCnosrc.root);
|
|
5360
|
-
const parentLoaded = await
|
|
6594
|
+
const parentLoaded = await loadManifest12({ root: parentManifestRoot });
|
|
5361
6595
|
if (parentLoaded.rootResolution.readOnly) {
|
|
5362
6596
|
throw new Error(
|
|
5363
6597
|
`Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
|
|
@@ -5381,7 +6615,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
5381
6615
|
items[workspaceId] = items[workspaceId] ?? {};
|
|
5382
6616
|
workspaces.items = items;
|
|
5383
6617
|
rawManifest.workspaces = workspaces;
|
|
5384
|
-
await
|
|
6618
|
+
await writeFile11(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5385
6619
|
const archivePath = path25.join(packageRoot, ".cnos.detached.bak");
|
|
5386
6620
|
await rm5(archivePath, { recursive: true, force: true });
|
|
5387
6621
|
await rename(childCnosRoot, archivePath);
|
|
@@ -5397,7 +6631,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
5397
6631
|
return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
|
|
5398
6632
|
}
|
|
5399
6633
|
async function runList2(manifestCwd, options = {}) {
|
|
5400
|
-
const loaded = await
|
|
6634
|
+
const loaded = await loadManifest12({
|
|
5401
6635
|
...options.root ? { root: options.root } : {},
|
|
5402
6636
|
cwd: manifestCwd,
|
|
5403
6637
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5430,7 +6664,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
|
|
|
5430
6664
|
if (cliArgs.length > 0) {
|
|
5431
6665
|
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
5432
6666
|
}
|
|
5433
|
-
const loaded = await
|
|
6667
|
+
const loaded = await loadManifest12({
|
|
5434
6668
|
...options.root ? { root: options.root } : {},
|
|
5435
6669
|
cwd: manifestCwd,
|
|
5436
6670
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5463,7 +6697,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
|
|
|
5463
6697
|
base: {}
|
|
5464
6698
|
};
|
|
5465
6699
|
rawManifest.workspaces = rawWorkspaces;
|
|
5466
|
-
await
|
|
6700
|
+
await writeFile11(path25.join(cnosRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5467
6701
|
await updateRootAnchorToWorkspace(packageRoot, "base");
|
|
5468
6702
|
await updateWorkspaceContext(packageRoot, "base");
|
|
5469
6703
|
await ensureGitignore(path25.dirname(cnosRoot));
|
|
@@ -5484,7 +6718,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
|
|
|
5484
6718
|
if (cliArgs.length > 0) {
|
|
5485
6719
|
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
5486
6720
|
}
|
|
5487
|
-
const loaded = await
|
|
6721
|
+
const loaded = await loadManifest12({
|
|
5488
6722
|
...options.root ? { root: options.root } : {},
|
|
5489
6723
|
cwd: manifestCwd,
|
|
5490
6724
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5516,7 +6750,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
|
|
|
5516
6750
|
rawManifest.workspaces = rawWorkspaces;
|
|
5517
6751
|
const workspaceRoot = path25.join(cnosRoot, "workspaces", workspaceId);
|
|
5518
6752
|
const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
|
|
5519
|
-
await
|
|
6753
|
+
await writeFile11(path25.join(cnosRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5520
6754
|
await ensureGitignore(path25.dirname(cnosRoot));
|
|
5521
6755
|
await writeAnchor(packageRoot, cnosRoot, workspaceId);
|
|
5522
6756
|
await updateWorkspaceContext(packageRoot, workspaceId);
|
|
@@ -5538,7 +6772,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
|
|
|
5538
6772
|
if (cliArgs.length > 0) {
|
|
5539
6773
|
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
5540
6774
|
}
|
|
5541
|
-
const loaded = await
|
|
6775
|
+
const loaded = await loadManifest12({
|
|
5542
6776
|
...options.root ? { root: options.root } : {},
|
|
5543
6777
|
cwd: manifestCwd,
|
|
5544
6778
|
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
@@ -5560,7 +6794,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
|
|
|
5560
6794
|
delete rawItems[workspaceId];
|
|
5561
6795
|
rawWorkspaces.items = rawItems;
|
|
5562
6796
|
rawManifest.workspaces = rawWorkspaces;
|
|
5563
|
-
await
|
|
6797
|
+
await writeFile11(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
|
|
5564
6798
|
await rm5(path25.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
|
|
5565
6799
|
if (options.json) {
|
|
5566
6800
|
return printJson({
|
|
@@ -5649,6 +6883,9 @@ function resolveHelpTopic(command, args) {
|
|
|
5649
6883
|
if (command === "profile" && args[0] && ["create", "list", "use", "delete", "remove"].includes(args[0])) {
|
|
5650
6884
|
return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
|
|
5651
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
|
+
}
|
|
5652
6889
|
return normalizeHelpTopic([command]);
|
|
5653
6890
|
}
|
|
5654
6891
|
async function main(argv) {
|
|
@@ -5726,6 +6963,10 @@ async function main(argv) {
|
|
|
5726
6963
|
return;
|
|
5727
6964
|
case "secret":
|
|
5728
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)}
|
|
5729
6970
|
`);
|
|
5730
6971
|
return;
|
|
5731
6972
|
case "vault":
|