@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.
Files changed (2) hide show
  1. package/dist/index.js +1373 -132
  2. 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 assertSecretEnvTargetIsGitIgnored(targetPath, cwd) {
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
- if (!isGitIgnored(repoRoot, targetPath)) {
394
- const relativeTarget = path3.relative(repoRoot, targetPath).replace(/\\/g, "/");
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 entries.map(([key, value]) => `export ${key}="${escapeShell(stringifyScalar(value))}"`).join("\n");
547
+ return formatEnvEntries(Object.fromEntries(entries), "shell");
448
548
  case "toml":
449
- return entries.map(([key, value]) => `${key} = ${quoteToml(stringifyScalar(value))}`).join("\n");
549
+ return formatEnvEntries(Object.fromEntries(entries), "toml");
450
550
  case "docker-env":
451
551
  case "dotenv":
452
552
  default:
453
- return entries.map(([key, value]) => `${key}=${stringifyScalar(value)}`).join("\n");
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 Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
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> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [global-options]",
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) => stringifyCell(row[column]).padEnd(widths[index], " ")).join(" ").trimEnd();
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) => column.padEnd(widths[index], " ")).join(" ").trimEnd(),
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 loadManifest4,
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 loadManifest4({
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 loadManifest5, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
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 loadManifest5({
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 loadManifest6, parseYaml as parseYaml5, stringifyYaml as stringifyYaml5 } from "@kitsy/cnos/internal";
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 loadManifest6({ root });
4067
+ const loadedManifest = await loadManifest7({ root });
3788
4068
  return path16.join(loadedManifest.manifestRoot, "profiles");
3789
4069
  } catch {
3790
- const loadedManifest = await loadManifest6({ cwd: root });
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 loadManifest7,
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 loadManifest7({
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 consumeOptions(args, flag) {
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(consumeOptions(cliArgs, "--set"));
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 loadManifest8,
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 loadManifest8({
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 loadManifest8({
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 loadManifest8({
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 loadManifest8({
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 rawValue = stdin ? await readStdinValue() : tail[1] ?? "";
4518
- const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
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/commands/use.ts
4573
- import path22 from "path";
4574
- async function runUse(args = [], options = {}) {
4575
- const root = path22.resolve(options.root ?? process.cwd());
4576
- const action = args[0];
4577
- const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
4578
- if (action === "show" || !action && !hasUpdates) {
4579
- const context = await loadCliContext(root);
4580
- if (options.json) {
4581
- return printJson(context);
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
- return Object.keys(context).length === 0 ? "no CLI context configured" : printJson(context);
4906
+ callback();
4584
4907
  }
4585
- const result = await saveCliContext({
4586
- root,
4587
- ...options.workspace ? { workspace: options.workspace } : {},
4588
- ...options.profile ? { profile: options.profile } : {},
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
- // src/commands/ui.ts
4598
- import { createServer } from "http";
4599
- import path23 from "path";
4600
- import { createRequire } from "module";
4601
- import { loadManifest as loadManifest9 } from "@kitsy/cnos/internal";
4602
- function parsePort(value, fallback, flag) {
4603
- if (!value) {
4604
- return fallback;
4605
- }
4606
- const parsed = Number(value);
4607
- if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
4608
- throw new Error(`Invalid value for ${flag}: ${value}`);
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 resolveUiPackageRoot() {
4613
- const require2 = createRequire(import.meta.url);
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
- const packageJsonPath = require2.resolve("@kitsy/cnos-ui/package.json");
4616
- return path23.dirname(packageJsonPath);
4617
- } catch {
4618
- throw new Error("Unable to resolve @kitsy/cnos-ui. Install workspace dependencies before running `cnos ui`.");
4942
+ return await new Promise((resolve) => {
4943
+ rl.question(message, (answer) => resolve(answer));
4944
+ });
4945
+ } finally {
4946
+ rl.close();
4619
4947
  }
4620
4948
  }
4621
- function writeJson(response, statusCode, payload) {
4622
- response.statusCode = statusCode;
4623
- response.setHeader("Content-Type", "application/json; charset=utf-8");
4624
- response.end(`${printJson(payload)}
4625
- `);
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
- async function readJsonBody(request) {
4628
- const chunks = [];
4629
- for await (const chunk of request) {
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 loadManifest9({
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 += 1;
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: withUiPassphrase(options.processEnv, passphrase),
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: withUiPassphrase(options.processEnv, passphrase),
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.9.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 writeFile10 } from "fs/promises";
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 loadManifest10, parseYaml as parseYaml6, stringifyYaml as stringifyYaml8 } from "@kitsy/cnos/internal";
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 writeFile10(
6460
+ await writeFile11(
5227
6461
  path25.join(packageRoot, ".cnosrc.yml"),
5228
- stringifyYaml8({
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 writeFile10(
6512
+ await writeFile11(
5279
6513
  anchorPath,
5280
- stringifyYaml8({
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 writeFile10(
6524
+ await writeFile11(
5291
6525
  workspacePath,
5292
- stringifyYaml8({
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 loadManifest10({ cwd: packageRoot });
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 writeFile10(
6555
+ await writeFile11(
5322
6556
  path25.join(targetCnosRoot, "cnos.yml"),
5323
- stringifyYaml8(createDetachedManifest(loaded.rawManifest)),
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 writeFile10(path25.join(targetCnosRoot, ".detached"), stringifyYaml8(marker), "utf8");
5337
- await writeFile10(path25.join(packageRoot, ".cnosrc.yml"), stringifyYaml8({ root: "./.cnos" }), "utf8");
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 loadManifest10({ root: parentManifestRoot });
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 writeFile10(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
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 loadManifest10({
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 loadManifest10({
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 writeFile10(path25.join(cnosRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
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 loadManifest10({
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 writeFile10(path25.join(cnosRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
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 loadManifest10({
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 writeFile10(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
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":