@kitsy/cnos-cli 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1326 -129
  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.",
@@ -2543,7 +2815,7 @@ var COMMANDS = [
2543
2815
  id: "doctor",
2544
2816
  summary: "Run repository and workspace diagnostics.",
2545
2817
  usage: "cnos doctor [--fix-secret-env-mappings] [global-options]",
2546
- description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace. Secret env mappings are reported as a security risk; use --fix-secret-env-mappings to remove them from envMapping.explicit in one shot.",
2818
+ description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace. Secret env mappings are reported as a security risk; use --fix-secret-env-mappings to remove them from envMapping.explicit in one shot. When schema entries exist, doctor points to cnos spec doctor for spec coverage and remediation.",
2547
2819
  examples: ["cnos doctor", "cnos doctor --workspace api --json", "cnos doctor --fix-secret-env-mappings"]
2548
2820
  },
2549
2821
  {
@@ -3096,9 +3368,15 @@ function printTable(rows) {
3096
3368
  ...rows.map((row) => stringifyCell(row[column]).length)
3097
3369
  )
3098
3370
  );
3099
- const renderRow = (row) => columns.map((column, index) => 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();
3100
3375
  return [
3101
- 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(),
3102
3380
  widths.map((width) => "-".repeat(width)).join(" "),
3103
3381
  ...rows.map(renderRow)
3104
3382
  ].join("\n");
@@ -3290,7 +3568,7 @@ async function runList(args = [], options = {}) {
3290
3568
  import path12 from "path";
3291
3569
  import {
3292
3570
  applyManifestMappings,
3293
- loadManifest as loadManifest4,
3571
+ loadManifest as loadManifest5,
3294
3572
  proposeMapping,
3295
3573
  rewriteSourceFiles,
3296
3574
  scanEnvUsage
@@ -3307,7 +3585,7 @@ async function runMigrate(options = {}) {
3307
3585
  if (apply) {
3308
3586
  await assertWritableConfigRoot("apply migration mappings", options);
3309
3587
  }
3310
- const manifest = await loadManifest4({
3588
+ const manifest = await loadManifest5({
3311
3589
  ...options.root ? { root: options.root } : {},
3312
3590
  ...options.cwd ? { cwd: options.cwd } : {},
3313
3591
  ...options.processEnv ? { processEnv: options.processEnv } : {},
@@ -3473,7 +3751,7 @@ async function runNamespace(namespace, args = [], options = {}) {
3473
3751
  import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile5 } from "fs/promises";
3474
3752
  import path14 from "path";
3475
3753
  import readline2 from "readline/promises";
3476
- import { loadManifest as loadManifest5, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
3754
+ import { loadManifest as loadManifest6, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
3477
3755
  import { parse as parseToml } from "smol-toml";
3478
3756
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
3479
3757
  var SECRET_LIKE_PATTERN = /(secret|token|password|passwd|private|api[_-]?key|client[_-]?secret|dsn)/i;
@@ -3658,7 +3936,7 @@ async function runOnboard(options = {}) {
3658
3936
  });
3659
3937
  scaffolded = scaffold.created;
3660
3938
  }
3661
- const loaded = await loadManifest5({
3939
+ const loaded = await loadManifest6({
3662
3940
  root,
3663
3941
  ...options.processEnv ? { processEnv: options.processEnv } : {}
3664
3942
  });
@@ -3783,13 +4061,13 @@ async function saveCliContext(options = {}) {
3783
4061
  // src/services/profiles.ts
3784
4062
  import { mkdir as mkdir6, readdir as readdir4, readFile as readFile7, rm as rm3, writeFile as writeFile7 } from "fs/promises";
3785
4063
  import path16 from "path";
3786
- import { loadManifest as 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";
3787
4065
  async function resolveProfilesRoot(root = process.cwd()) {
3788
4066
  try {
3789
- const loadedManifest = await loadManifest6({ root });
4067
+ const loadedManifest = await loadManifest7({ root });
3790
4068
  return path16.join(loadedManifest.manifestRoot, "profiles");
3791
4069
  } catch {
3792
- const loadedManifest = await loadManifest6({ cwd: root });
4070
+ const loadedManifest = await loadManifest7({ cwd: root });
3793
4071
  return path16.join(loadedManifest.manifestRoot, "profiles");
3794
4072
  }
3795
4073
  }
@@ -3935,7 +4213,7 @@ import path18 from "path";
3935
4213
  import { writeFile as writeFile8 } from "fs/promises";
3936
4214
  import {
3937
4215
  ensureProjectionAllowed,
3938
- loadManifest as loadManifest7,
4216
+ loadManifest as loadManifest8,
3939
4217
  stringifyYaml as stringifyYaml6
3940
4218
  } from "@kitsy/cnos/internal";
3941
4219
  function normalizeTarget(value) {
@@ -3967,7 +4245,7 @@ async function runPromote(args = [], options = {}) {
3967
4245
  } else if (allowSecret) {
3968
4246
  throw new Error("--allow-secret is only supported with promote --to env");
3969
4247
  }
3970
- const loadedManifest = await loadManifest7({
4248
+ const loadedManifest = await loadManifest8({
3971
4249
  ...options.root ? { root: options.root } : {},
3972
4250
  ...options.cwd ? { cwd: options.cwd } : {},
3973
4251
  ...options.processEnv ? { processEnv: options.processEnv } : {},
@@ -4039,7 +4317,7 @@ import {
4039
4317
  serializeRuntimeGraph,
4040
4318
  serializeSecretPayload
4041
4319
  } from "@kitsy/cnos/internal";
4042
- function consumeOptions(args, flag) {
4320
+ function consumeOptions2(args, flag) {
4043
4321
  const values = [];
4044
4322
  for (let index = 0; index < args.length; ) {
4045
4323
  const token = args[index];
@@ -4078,7 +4356,7 @@ async function runCommand(command, options = {}) {
4078
4356
  const isAuthenticated = consumeFlag(cliArgs, "--auth");
4079
4357
  const framework = consumeOption(cliArgs, "--framework");
4080
4358
  const prefix = consumeOption(cliArgs, "--prefix");
4081
- const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
4359
+ const setOverrides = normalizeSetOverrides(consumeOptions2(cliArgs, "--set"));
4082
4360
  const runtime = await createRuntimeService({
4083
4361
  ...options,
4084
4362
  cliArgs: [...cliArgs, ...setOverrides]
@@ -4149,7 +4427,7 @@ import {
4149
4427
  createSecretVault,
4150
4428
  deriveVaultKey,
4151
4429
  listLocalSecrets,
4152
- loadManifest as loadManifest8,
4430
+ loadManifest as loadManifest9,
4153
4431
  listSecretVaults,
4154
4432
  readVaultMetadata,
4155
4433
  resolveSecretStoreRoot as resolveSecretStoreRoot2,
@@ -4188,7 +4466,7 @@ async function createVaultDefinition(name, options = {}) {
4188
4466
  if (provider === "local" && (options.noPassphrase ?? false)) {
4189
4467
  throw new Error("Local vaults cannot be passwordless.");
4190
4468
  }
4191
- const loadedManifest = await loadManifest8({
4469
+ const loadedManifest = await loadManifest9({
4192
4470
  ...options.root ? { root: options.root } : {},
4193
4471
  ...options.cwd ? { cwd: options.cwd } : {},
4194
4472
  ...options.processEnv ? { processEnv: options.processEnv } : {},
@@ -4225,7 +4503,7 @@ async function createVaultDefinition(name, options = {}) {
4225
4503
  };
4226
4504
  }
4227
4505
  async function listVaultDefinitions(options = {}) {
4228
- const loadedManifest = await loadManifest8({
4506
+ const loadedManifest = await loadManifest9({
4229
4507
  ...options.root ? { root: options.root } : {},
4230
4508
  ...options.cwd ? { cwd: options.cwd } : {},
4231
4509
  ...options.processEnv ? { processEnv: options.processEnv } : {},
@@ -4246,7 +4524,7 @@ async function listVaultDefinitions(options = {}) {
4246
4524
  async function removeVaultDefinition(name, options = {}) {
4247
4525
  await assertWritableConfigRoot(`remove vault ${name}`, options);
4248
4526
  const vault = name.trim() || "default";
4249
- const loadedManifest = await loadManifest8({
4527
+ const loadedManifest = await loadManifest9({
4250
4528
  ...options.root ? { root: options.root } : {},
4251
4529
  ...options.cwd ? { cwd: options.cwd } : {},
4252
4530
  ...options.processEnv ? { processEnv: options.processEnv } : {},
@@ -4292,7 +4570,7 @@ async function listLocalStoreVaults(options = {}) {
4292
4570
  }
4293
4571
  async function authenticateVault(name, options = {}) {
4294
4572
  const vault = name.trim() || "default";
4295
- const loadedManifest = await loadManifest8({
4573
+ const loadedManifest = await loadManifest9({
4296
4574
  ...options.root ? { root: options.root } : {},
4297
4575
  ...options.cwd ? { cwd: options.cwd } : {},
4298
4576
  ...options.processEnv ? { processEnv: options.processEnv } : {},
@@ -4613,65 +4891,971 @@ async function runSecret(argsOrPath, options = {}) {
4613
4891
  return printValue(valueForOutput);
4614
4892
  }
4615
4893
 
4616
- // src/commands/use.ts
4617
- import path22 from "path";
4618
- async function runUse(args = [], options = {}) {
4619
- const root = path22.resolve(options.root ?? process.cwd());
4620
- const action = args[0];
4621
- const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
4622
- if (action === "show" || !action && !hasUpdates) {
4623
- const context = await loadCliContext(root);
4624
- if (options.json) {
4625
- 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);
4626
4905
  }
4627
- return Object.keys(context).length === 0 ? "no CLI context configured" : printJson(context);
4906
+ callback();
4628
4907
  }
4629
- const result = await saveCliContext({
4630
- root,
4631
- ...options.workspace ? { workspace: options.workspace } : {},
4632
- ...options.profile ? { profile: options.profile } : {},
4633
- ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
4634
- });
4635
- if (options.json) {
4636
- return printJson(result);
4908
+ };
4909
+ function assertInteractiveInput(mode) {
4910
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
4911
+ throw new Error(`Cannot prompt for ${mode} input in non-interactive mode.`);
4637
4912
  }
4638
- return `updated CLI context in ${displayPath(result.filePath, root)}`;
4639
4913
  }
4640
-
4641
- // src/commands/ui.ts
4642
- import { createServer } from "http";
4643
- import path23 from "path";
4644
- import { createRequire } from "module";
4645
- import { loadManifest as loadManifest9 } from "@kitsy/cnos/internal";
4646
- function parsePort(value, fallback, flag) {
4647
- if (!value) {
4648
- return fallback;
4649
- }
4650
- const parsed = Number(value);
4651
- if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
4652
- 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();
4653
4932
  }
4654
- return parsed;
4655
4933
  }
4656
- function resolveUiPackageRoot() {
4657
- 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
+ });
4658
4941
  try {
4659
- const packageJsonPath = require2.resolve("@kitsy/cnos-ui/package.json");
4660
- return path23.dirname(packageJsonPath);
4661
- } catch {
4662
- 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();
4663
4947
  }
4664
4948
  }
4665
- function writeJson(response, statusCode, payload) {
4666
- response.statusCode = statusCode;
4667
- response.setHeader("Content-Type", "application/json; charset=utf-8");
4668
- response.end(`${printJson(payload)}
4669
- `);
4949
+
4950
+ // src/services/spec/specDoctor.ts
4951
+ var BLOCKING_STATUSES = /* @__PURE__ */ new Set([
4952
+ "missing_required",
4953
+ "undeclared",
4954
+ "type_mismatch",
4955
+ "enum_mismatch",
4956
+ "pattern_mismatch"
4957
+ ]);
4958
+ function statusLabel(status) {
4959
+ return status.replace(/_/g, " ");
4670
4960
  }
4671
- async function readJsonBody(request) {
4672
- const chunks = [];
4673
- for await (const chunk of request) {
4674
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4961
+ function isBlockingStatus(status) {
4962
+ return BLOCKING_STATUSES.has(status);
4963
+ }
4964
+ function isBlockingIssue(issue) {
4965
+ return isBlockingStatus(issue.status);
4966
+ }
4967
+ function isInteractiveMode() {
4968
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
4969
+ }
4970
+ function toPath(key) {
4971
+ const separatorIndex = key.indexOf(".");
4972
+ if (separatorIndex <= 0 || separatorIndex >= key.length - 1) {
4973
+ throw new Error(`Spec key must be namespace-qualified: ${key}`);
4974
+ }
4975
+ return {
4976
+ namespace: key.slice(0, separatorIndex),
4977
+ configPath: key.slice(separatorIndex + 1)
4978
+ };
4979
+ }
4980
+ function toDoctorIssue(issue) {
4981
+ return {
4982
+ key: issue.key,
4983
+ status: issue.status,
4984
+ ...issue.expectedType ? {
4985
+ expectedType: issue.expectedType
4986
+ } : {},
4987
+ ...issue.actualType ? {
4988
+ actualType: issue.actualType
4989
+ } : {},
4990
+ ...issue.value !== void 0 ? {
4991
+ value: issue.value
4992
+ } : {},
4993
+ ...issue.sourceFile ? {
4994
+ sourceFile: issue.sourceFile
4995
+ } : {},
4996
+ ...issue.summary ? {
4997
+ summary: issue.summary
4998
+ } : {}
4999
+ };
5000
+ }
5001
+ function renderIssueLine(issue) {
5002
+ const context = [
5003
+ issue.expectedType ? `expected=${issue.expectedType}` : void 0,
5004
+ issue.actualType ? `actual=${issue.actualType}` : void 0,
5005
+ issue.sourceFile ? `source=${issue.sourceFile}` : void 0
5006
+ ].filter((value) => Boolean(value)).join(", ");
5007
+ return context.length > 0 ? `- ${issue.key} [${statusLabel(issue.status)}] (${context})` : `- ${issue.key} [${statusLabel(issue.status)}]`;
5008
+ }
5009
+ function makeReportResult(runtime, mode, actions) {
5010
+ const report = compareSpecToGraph(runtime);
5011
+ return {
5012
+ workspace: report.workspace,
5013
+ profile: report.profile,
5014
+ summary: report.summary,
5015
+ issues: report.issues.map((issue) => toDoctorIssue(issue)),
5016
+ mode,
5017
+ ...actions ? {
5018
+ actions
5019
+ } : {}
5020
+ };
5021
+ }
5022
+ function getRuleForKey(runtime, logicalKey) {
5023
+ return runtime.manifest.schema[logicalKey];
5024
+ }
5025
+ async function promptForKeyValue(key, rule) {
5026
+ const lines = [
5027
+ "",
5028
+ `Key: ${key}`,
5029
+ ...rule?.summary ? [`Summary: ${rule.summary}`] : [],
5030
+ ...rule?.description ? [`Description: ${rule.description}`] : [],
5031
+ ...rule?.enum ? [`Allowed values: ${rule.enum.map((entry) => JSON.stringify(entry)).join(", ")}`] : [],
5032
+ ...rule?.pattern ? [`Pattern: ${rule.pattern}`] : []
5033
+ ];
5034
+ process.stdout.write(`${lines.join("\n")}
5035
+ `);
5036
+ if (key.startsWith("secret.")) {
5037
+ return (await promptMaskedInput(`Enter value for ${key}: `)).trimEnd();
5038
+ }
5039
+ if (rule?.enum && rule.enum.length > 0) {
5040
+ process.stdout.write(
5041
+ `${rule.enum.map((entry, index2) => `${index2 + 1}. ${JSON.stringify(entry)}`).join("\n")}
5042
+ `
5043
+ );
5044
+ const choice = (await promptInput(`Choose [1-${rule.enum.length}] or enter a custom value: `)).trim();
5045
+ const index = Number(choice);
5046
+ if (Number.isFinite(index) && index >= 1 && index <= rule.enum.length) {
5047
+ return JSON.stringify(rule.enum[index - 1]);
5048
+ }
5049
+ return choice;
5050
+ }
5051
+ return (await promptInput(`Enter value for ${key}: `)).trim();
5052
+ }
5053
+ async function applyWriteForKey(key, rawValue, options) {
5054
+ const { namespace, configPath } = toPath(key);
5055
+ if (namespace === "secret") {
5056
+ await setSecret(configPath, rawValue, options);
5057
+ return;
5058
+ }
5059
+ await defineValue(namespace, configPath, rawValue, options);
5060
+ }
5061
+ function hasBlockingIssues(result) {
5062
+ return result.issues.some((issue) => isBlockingIssue(issue));
5063
+ }
5064
+ function hasFailedActions(actions) {
5065
+ return actions.some((action) => action.result === "failed");
5066
+ }
5067
+ async function runSpecDoctor(mode, options = {}) {
5068
+ const runtime = await createRuntimeService({
5069
+ ...options,
5070
+ secretResolution: "lazy"
5071
+ });
5072
+ if (mode === "report") {
5073
+ const result2 = makeReportResult(runtime, mode);
5074
+ return {
5075
+ result: result2,
5076
+ blocking: hasBlockingIssues(result2)
5077
+ };
5078
+ }
5079
+ if (!isInteractiveMode()) {
5080
+ throw new Error(`spec doctor --${mode} requires an interactive TTY.`);
5081
+ }
5082
+ const report = compareSpecToGraph(runtime);
5083
+ const actions = [];
5084
+ let unresolvedBlocking = false;
5085
+ if (mode === "fill-missing") {
5086
+ const missing = report.issues.filter((issue) => issue.status === "missing_required").sort((left, right) => left.key.localeCompare(right.key));
5087
+ for (const issue of missing) {
5088
+ const rule = getRuleForKey(runtime, issue.key);
5089
+ try {
5090
+ const value = await promptForKeyValue(issue.key, rule);
5091
+ await applyWriteForKey(issue.key, value, options);
5092
+ actions.push({
5093
+ key: issue.key,
5094
+ result: "applied"
5095
+ });
5096
+ } catch (error) {
5097
+ actions.push({
5098
+ key: issue.key,
5099
+ result: "failed",
5100
+ reason: error instanceof Error ? error.message : String(error)
5101
+ });
5102
+ }
5103
+ }
5104
+ } else {
5105
+ const schemaKeys = Object.keys(runtime.manifest.schema).sort((left, right) => left.localeCompare(right));
5106
+ const issuesByKey = /* @__PURE__ */ new Map();
5107
+ for (const issue of report.issues) {
5108
+ const existing = issuesByKey.get(issue.key) ?? [];
5109
+ existing.push(issue.status);
5110
+ issuesByKey.set(issue.key, existing);
5111
+ }
5112
+ for (const key of schemaKeys) {
5113
+ const keyStatuses = issuesByKey.get(key) ?? [];
5114
+ const rule = getRuleForKey(runtime, key);
5115
+ const statusText = keyStatuses.length > 0 ? keyStatuses.map(statusLabel).join(", ") : "ok";
5116
+ process.stdout.write(`
5117
+ Key: ${key}
5118
+ Current status: ${statusText}
5119
+ `);
5120
+ const actionChoice = (await promptInput("Action [k=keep, u=update, s=skip]: ")).trim().toLowerCase();
5121
+ if (actionChoice === "k" || actionChoice === "keep") {
5122
+ actions.push({
5123
+ key,
5124
+ result: "skipped",
5125
+ reason: "keep"
5126
+ });
5127
+ continue;
5128
+ }
5129
+ if (actionChoice === "s" || actionChoice === "skip") {
5130
+ if (keyStatuses.includes("missing_required")) {
5131
+ unresolvedBlocking = true;
5132
+ }
5133
+ actions.push({
5134
+ key,
5135
+ result: "skipped",
5136
+ reason: "skip"
5137
+ });
5138
+ continue;
5139
+ }
5140
+ try {
5141
+ const value = await promptForKeyValue(key, rule);
5142
+ await applyWriteForKey(key, value, options);
5143
+ actions.push({
5144
+ key,
5145
+ result: "applied"
5146
+ });
5147
+ } catch (error) {
5148
+ actions.push({
5149
+ key,
5150
+ result: "failed",
5151
+ reason: error instanceof Error ? error.message : String(error)
5152
+ });
5153
+ }
5154
+ }
5155
+ }
5156
+ const refreshed = await createRuntimeService({
5157
+ ...options,
5158
+ secretResolution: "lazy"
5159
+ });
5160
+ const result = makeReportResult(refreshed, mode, actions);
5161
+ const blocking = hasBlockingIssues(result) || hasFailedActions(actions) || unresolvedBlocking;
5162
+ return {
5163
+ result,
5164
+ blocking
5165
+ };
5166
+ }
5167
+ function formatSpecDoctorResult(result) {
5168
+ const lines = [
5169
+ `Spec doctor (${result.workspace} / ${result.profile}) [mode=${result.mode}]`,
5170
+ "",
5171
+ `missingRequired=${result.summary.missingRequired}`,
5172
+ `undeclared=${result.summary.undeclared}`,
5173
+ `typeMismatch=${result.summary.typeMismatch}`,
5174
+ `enumMismatch=${result.summary.enumMismatch}`,
5175
+ `patternMismatch=${result.summary.patternMismatch}`,
5176
+ `defaultApplied=${result.summary.defaultApplied}`,
5177
+ `deprecatedInUse=${result.summary.deprecatedInUse}`,
5178
+ ""
5179
+ ];
5180
+ if (result.issues.length === 0) {
5181
+ lines.push("No spec issues detected.");
5182
+ } else {
5183
+ lines.push("Issues:");
5184
+ lines.push(...result.issues.map(renderIssueLine));
5185
+ }
5186
+ if (result.actions && result.actions.length > 0) {
5187
+ lines.push("");
5188
+ lines.push("Actions:");
5189
+ lines.push(
5190
+ ...result.actions.map(
5191
+ (action) => action.reason ? `- ${action.key}: ${action.result} (${action.reason})` : `- ${action.key}: ${action.result}`
5192
+ )
5193
+ );
5194
+ }
5195
+ return lines.join("\n");
5196
+ }
5197
+
5198
+ // src/services/spec/manifestSpecStore.ts
5199
+ import { writeFile as writeFile10 } from "fs/promises";
5200
+ import { loadManifest as loadManifest10, stringifyYaml as stringifyYaml8 } from "@kitsy/cnos/internal";
5201
+ function hasOwn(target, key) {
5202
+ return Object.prototype.hasOwnProperty.call(target, key);
5203
+ }
5204
+ function sortSchema(schema) {
5205
+ return Object.fromEntries(Object.entries(schema).sort(([left], [right]) => left.localeCompare(right)));
5206
+ }
5207
+ async function loadSpecManifest(options = {}) {
5208
+ const loadedManifest = await loadManifest10({
5209
+ ...options.root ? { root: options.root } : {},
5210
+ ...options.cwd ? { cwd: options.cwd } : {},
5211
+ ...options.processEnv ? { processEnv: options.processEnv } : {},
5212
+ ...options.cacheMode ? { cacheMode: options.cacheMode } : {},
5213
+ ...typeof options.cacheTtlSeconds === "number" ? { cacheTtlSeconds: options.cacheTtlSeconds } : {},
5214
+ ...options.forceRefresh ? { forceRefresh: true } : {}
5215
+ });
5216
+ return {
5217
+ manifestPath: loadedManifest.manifestPath,
5218
+ rawManifest: loadedManifest.rawManifest,
5219
+ schema: loadedManifest.manifest.schema
5220
+ };
5221
+ }
5222
+ async function listSpecEntries(options = {}) {
5223
+ const loaded = await loadSpecManifest(options);
5224
+ const prefix = options.prefix?.trim();
5225
+ const entries = Object.entries(loaded.schema).filter(([key]) => prefix ? key.startsWith(prefix) : true).sort(([left], [right]) => left.localeCompare(right)).map(([key, rule]) => ({
5226
+ key,
5227
+ rule
5228
+ }));
5229
+ return {
5230
+ manifestPath: loaded.manifestPath,
5231
+ entries
5232
+ };
5233
+ }
5234
+ async function showSpecEntry(logicalKey, options = {}) {
5235
+ const loaded = await loadSpecManifest(options);
5236
+ const key = logicalKey.trim();
5237
+ return {
5238
+ manifestPath: loaded.manifestPath,
5239
+ key,
5240
+ ...loaded.schema[key] ? {
5241
+ rule: loaded.schema[key]
5242
+ } : {}
5243
+ };
5244
+ }
5245
+ async function setSpecEntry(logicalKey, options = {}) {
5246
+ const loaded = await loadSpecManifest(options);
5247
+ const key = logicalKey.trim();
5248
+ const hasSetFields = Object.keys(options.set ?? {}).length > 0;
5249
+ if (!loaded.schema[key] && !hasSetFields) {
5250
+ throw new Error(`Cannot clear fields for undeclared spec key ${key}.`);
5251
+ }
5252
+ const current = loaded.schema[key] ?? {};
5253
+ const next = {
5254
+ ...current,
5255
+ ...options.set ?? {}
5256
+ };
5257
+ for (const field of options.clear ?? []) {
5258
+ if (hasOwn(next, field)) {
5259
+ delete next[field];
5260
+ }
5261
+ }
5262
+ if (Object.keys(next).length === 0) {
5263
+ throw new Error(`Spec entry ${key} cannot be empty. Set at least one field or delete the entry.`);
5264
+ }
5265
+ const nextSchema = sortSchema({
5266
+ ...loaded.schema,
5267
+ [key]: next
5268
+ });
5269
+ const nextRawManifest = {
5270
+ ...loaded.rawManifest,
5271
+ schema: nextSchema
5272
+ };
5273
+ await writeFile10(loaded.manifestPath, stringifyYaml8(nextRawManifest), "utf8");
5274
+ return {
5275
+ manifestPath: loaded.manifestPath,
5276
+ key,
5277
+ action: loaded.schema[key] ? "updated" : "created",
5278
+ rule: next
5279
+ };
5280
+ }
5281
+ async function deleteSpecEntry(logicalKey, options = {}) {
5282
+ const loaded = await loadSpecManifest(options);
5283
+ const key = logicalKey.trim();
5284
+ if (!loaded.schema[key]) {
5285
+ return {
5286
+ manifestPath: loaded.manifestPath,
5287
+ key,
5288
+ deleted: false
5289
+ };
5290
+ }
5291
+ const nextSchema = { ...loaded.schema };
5292
+ delete nextSchema[key];
5293
+ const sorted = sortSchema(nextSchema);
5294
+ const nextRawManifest = {
5295
+ ...loaded.rawManifest,
5296
+ ...Object.keys(sorted).length > 0 ? { schema: sorted } : {}
5297
+ };
5298
+ if (Object.keys(sorted).length === 0) {
5299
+ delete nextRawManifest.schema;
5300
+ }
5301
+ await writeFile10(loaded.manifestPath, stringifyYaml8(nextRawManifest), "utf8");
5302
+ return {
5303
+ manifestPath: loaded.manifestPath,
5304
+ key,
5305
+ deleted: true
5306
+ };
5307
+ }
5308
+
5309
+ // src/services/spec/specPrompts.ts
5310
+ import readline5 from "readline";
5311
+
5312
+ // src/services/spec/patternValidation.ts
5313
+ function assertValidSpecPattern(pattern) {
5314
+ try {
5315
+ void new RegExp(pattern);
5316
+ } catch (error) {
5317
+ const reason = error instanceof Error ? error.message : String(error);
5318
+ throw new Error(`Invalid --pattern regex: ${reason}`);
5319
+ }
5320
+ }
5321
+
5322
+ // src/services/spec/specPrompts.ts
5323
+ var SPEC_TYPES = ["string", "number", "boolean", "object", "array"];
5324
+ function parseJsonOrString(rawValue) {
5325
+ try {
5326
+ return JSON.parse(rawValue);
5327
+ } catch {
5328
+ return rawValue;
5329
+ }
5330
+ }
5331
+ function createPrompt() {
5332
+ const rl = readline5.createInterface({
5333
+ input: process.stdin,
5334
+ output: process.stdout
5335
+ });
5336
+ return {
5337
+ ask(question) {
5338
+ return new Promise((resolve) => {
5339
+ rl.question(question, (answer) => resolve(answer.trim()));
5340
+ });
5341
+ },
5342
+ close() {
5343
+ rl.close();
5344
+ }
5345
+ };
5346
+ }
5347
+ function isInteractiveSpecPromptMode() {
5348
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
5349
+ }
5350
+ async function promptSpecSetInput(logicalKey) {
5351
+ const prompt = createPrompt();
5352
+ try {
5353
+ process.stdout.write(`Config spec for ${logicalKey}
5354
+ `);
5355
+ const set = {};
5356
+ const type = (await prompt.ask(`Type (${SPEC_TYPES.join("|")}) [skip]: `)).toLowerCase();
5357
+ if (type) {
5358
+ if (!SPEC_TYPES.includes(type)) {
5359
+ throw new Error(`Unsupported type: ${type}`);
5360
+ }
5361
+ set.type = type;
5362
+ }
5363
+ const required = (await prompt.ask("Required? (y/n) [skip]: ")).toLowerCase();
5364
+ if (required === "y" || required === "yes") {
5365
+ set.required = true;
5366
+ } else if (required === "n" || required === "no") {
5367
+ set.required = false;
5368
+ }
5369
+ const defaultValue = await prompt.ask("Default value (JSON or string) [skip]: ");
5370
+ if (defaultValue) {
5371
+ set.default = parseJsonOrString(defaultValue);
5372
+ }
5373
+ const enumValue = await prompt.ask("Enum values as JSON array [skip]: ");
5374
+ if (enumValue) {
5375
+ const parsed = parseJsonOrString(enumValue);
5376
+ if (!Array.isArray(parsed)) {
5377
+ throw new Error("Enum must be a JSON array");
5378
+ }
5379
+ if (parsed.length === 0) {
5380
+ throw new Error("Enum must not be empty");
5381
+ }
5382
+ set.enum = parsed;
5383
+ }
5384
+ const pattern = await prompt.ask("Pattern (regex string) [skip]: ");
5385
+ if (pattern) {
5386
+ assertValidSpecPattern(pattern);
5387
+ set.pattern = pattern;
5388
+ }
5389
+ const summary = await prompt.ask("Summary [skip]: ");
5390
+ if (summary) {
5391
+ set.summary = summary;
5392
+ }
5393
+ const description = await prompt.ask("Description [skip]: ");
5394
+ if (description) {
5395
+ set.description = description;
5396
+ }
5397
+ const examples = await prompt.ask("Examples as JSON array [skip]: ");
5398
+ if (examples) {
5399
+ const parsed = parseJsonOrString(examples);
5400
+ if (!Array.isArray(parsed)) {
5401
+ throw new Error("Examples must be a JSON array");
5402
+ }
5403
+ set.examples = parsed;
5404
+ }
5405
+ const usedBy = await prompt.ask("Used by (comma separated) [skip]: ");
5406
+ if (usedBy) {
5407
+ const values = usedBy.split(",").map((entry) => entry.trim()).filter(Boolean);
5408
+ if (values.length > 0) {
5409
+ set.usedBy = values;
5410
+ }
5411
+ }
5412
+ const deprecated = (await prompt.ask("Deprecated? (y/n) [skip]: ")).toLowerCase();
5413
+ if (deprecated === "y" || deprecated === "yes") {
5414
+ set.deprecated = true;
5415
+ } else if (deprecated === "n" || deprecated === "no") {
5416
+ set.deprecated = false;
5417
+ }
5418
+ const deprecationMessage = await prompt.ask("Deprecation message [skip]: ");
5419
+ if (deprecationMessage) {
5420
+ set.deprecationMessage = deprecationMessage;
5421
+ set.deprecated = true;
5422
+ }
5423
+ return {
5424
+ set,
5425
+ clear: [],
5426
+ hasFieldFlags: Object.keys(set).length > 0,
5427
+ cliArgs: []
5428
+ };
5429
+ } finally {
5430
+ prompt.close();
5431
+ }
5432
+ }
5433
+
5434
+ // src/services/spec/specSetInput.ts
5435
+ var SPEC_TYPES2 = /* @__PURE__ */ new Set(["string", "number", "boolean", "object", "array"]);
5436
+ var SECRET_FORBIDDEN_FIELDS = ["default", "enum", "examples"];
5437
+ function parseJsonOrString2(rawValue) {
5438
+ try {
5439
+ return JSON.parse(rawValue);
5440
+ } catch {
5441
+ return rawValue;
5442
+ }
5443
+ }
5444
+ function validateNamespaceQualifiedKey(logicalKey) {
5445
+ const trimmed = logicalKey.trim();
5446
+ if (!trimmed.includes(".")) {
5447
+ throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
5448
+ }
5449
+ const namespace = trimmed.slice(0, trimmed.indexOf("."));
5450
+ const keyPath = trimmed.slice(trimmed.indexOf(".") + 1);
5451
+ if (!namespace || !keyPath) {
5452
+ throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
5453
+ }
5454
+ }
5455
+ function parseSetOptionToField(option) {
5456
+ switch (option) {
5457
+ case "--clear-default":
5458
+ return "default";
5459
+ case "--clear-enum":
5460
+ return "enum";
5461
+ case "--clear-pattern":
5462
+ return "pattern";
5463
+ case "--clear-summary":
5464
+ return "summary";
5465
+ case "--clear-description":
5466
+ return "description";
5467
+ case "--clear-examples":
5468
+ return "examples";
5469
+ case "--clear-used-by":
5470
+ return "usedBy";
5471
+ case "--clear-deprecated":
5472
+ return "deprecated";
5473
+ case "--clear-deprecation-message":
5474
+ return "deprecationMessage";
5475
+ default:
5476
+ throw new Error(`Unknown clear option: ${option}`);
5477
+ }
5478
+ }
5479
+ function assertNoClearConflict(set, clear) {
5480
+ if (clear.has("default") && Object.prototype.hasOwnProperty.call(set, "default")) {
5481
+ throw new Error("Cannot combine --default with --clear-default");
5482
+ }
5483
+ if (clear.has("enum") && Object.prototype.hasOwnProperty.call(set, "enum")) {
5484
+ throw new Error("Cannot combine --enum with --clear-enum");
5485
+ }
5486
+ if (clear.has("pattern") && Object.prototype.hasOwnProperty.call(set, "pattern")) {
5487
+ throw new Error("Cannot combine --pattern with --clear-pattern");
5488
+ }
5489
+ if (clear.has("summary") && Object.prototype.hasOwnProperty.call(set, "summary")) {
5490
+ throw new Error("Cannot combine --summary with --clear-summary");
5491
+ }
5492
+ if (clear.has("description") && Object.prototype.hasOwnProperty.call(set, "description")) {
5493
+ throw new Error("Cannot combine --description with --clear-description");
5494
+ }
5495
+ if (clear.has("examples") && Object.prototype.hasOwnProperty.call(set, "examples")) {
5496
+ throw new Error("Cannot combine --example with --clear-examples");
5497
+ }
5498
+ if (clear.has("usedBy") && Object.prototype.hasOwnProperty.call(set, "usedBy")) {
5499
+ throw new Error("Cannot combine --used-by with --clear-used-by");
5500
+ }
5501
+ if (clear.has("deprecated") && Object.prototype.hasOwnProperty.call(set, "deprecated")) {
5502
+ throw new Error("Cannot combine --deprecated with --clear-deprecated");
5503
+ }
5504
+ if (clear.has("deprecated") && Object.prototype.hasOwnProperty.call(set, "deprecationMessage")) {
5505
+ throw new Error("Cannot combine --clear-deprecated with --deprecation-message");
5506
+ }
5507
+ }
5508
+ function assertSecretSafeSpecSet(logicalKey, set) {
5509
+ if (!logicalKey.startsWith("secret.")) {
5510
+ return;
5511
+ }
5512
+ const offending = SECRET_FORBIDDEN_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(set, field));
5513
+ if (offending.length > 0) {
5514
+ throw new Error(
5515
+ `Cannot set ${offending.join(", ")} for secret spec key ${logicalKey}. Store secret values in the vault, not schema metadata.`
5516
+ );
5517
+ }
5518
+ }
5519
+ function parseSpecSetInput(logicalKey, rawCliArgs = []) {
5520
+ validateNamespaceQualifiedKey(logicalKey);
5521
+ const cliArgs = [...rawCliArgs];
5522
+ const clearOptions = [
5523
+ "--clear-default",
5524
+ "--clear-enum",
5525
+ "--clear-pattern",
5526
+ "--clear-summary",
5527
+ "--clear-description",
5528
+ "--clear-examples",
5529
+ "--clear-used-by",
5530
+ "--clear-deprecated",
5531
+ "--clear-deprecation-message"
5532
+ ];
5533
+ const clear = /* @__PURE__ */ new Set();
5534
+ for (const option of clearOptions) {
5535
+ if (consumeFlag(cliArgs, option)) {
5536
+ clear.add(parseSetOptionToField(option));
5537
+ }
5538
+ }
5539
+ if (clear.has("deprecated")) {
5540
+ clear.add("deprecationMessage");
5541
+ }
5542
+ const set = {};
5543
+ const type = consumeOption(cliArgs, "--type");
5544
+ if (type) {
5545
+ if (!SPEC_TYPES2.has(type)) {
5546
+ throw new Error(`Unsupported --type value: ${type}`);
5547
+ }
5548
+ set.type = type;
5549
+ }
5550
+ const required = consumeFlag(cliArgs, "--required");
5551
+ const optional = consumeFlag(cliArgs, "--optional");
5552
+ if (required && optional) {
5553
+ throw new Error("Cannot combine --required and --optional");
5554
+ }
5555
+ if (required) {
5556
+ set.required = true;
5557
+ }
5558
+ if (optional) {
5559
+ set.required = false;
5560
+ }
5561
+ const defaultValue = consumeOption(cliArgs, "--default");
5562
+ if (defaultValue !== void 0) {
5563
+ set.default = parseJsonOrString2(defaultValue);
5564
+ }
5565
+ const enumValue = consumeOption(cliArgs, "--enum");
5566
+ if (enumValue !== void 0) {
5567
+ const parsed = parseJsonOrString2(enumValue);
5568
+ if (!Array.isArray(parsed)) {
5569
+ throw new Error("--enum must be a JSON array");
5570
+ }
5571
+ if (parsed.length === 0) {
5572
+ throw new Error("--enum must not be empty");
5573
+ }
5574
+ set.enum = parsed;
5575
+ }
5576
+ const pattern = consumeOption(cliArgs, "--pattern");
5577
+ if (pattern !== void 0) {
5578
+ assertValidSpecPattern(pattern);
5579
+ set.pattern = pattern;
5580
+ }
5581
+ const summary = consumeOption(cliArgs, "--summary");
5582
+ if (summary !== void 0) {
5583
+ set.summary = summary;
5584
+ }
5585
+ const description = consumeOption(cliArgs, "--description");
5586
+ if (description !== void 0) {
5587
+ set.description = description;
5588
+ }
5589
+ const examples = consumeOptions(cliArgs, "--example").map(parseJsonOrString2);
5590
+ if (examples.length > 0) {
5591
+ set.examples = examples;
5592
+ }
5593
+ const usedBy = consumeOptions(cliArgs, "--used-by");
5594
+ if (usedBy.length > 0) {
5595
+ set.usedBy = usedBy;
5596
+ }
5597
+ const deprecated = consumeFlag(cliArgs, "--deprecated");
5598
+ if (deprecated) {
5599
+ set.deprecated = true;
5600
+ }
5601
+ const deprecationMessage = consumeOption(cliArgs, "--deprecation-message");
5602
+ if (deprecationMessage !== void 0) {
5603
+ set.deprecationMessage = deprecationMessage;
5604
+ set.deprecated = true;
5605
+ }
5606
+ if (clear.has("deprecationMessage") && Object.prototype.hasOwnProperty.call(set, "deprecationMessage")) {
5607
+ throw new Error("Cannot combine --clear-deprecated with --deprecation-message");
5608
+ }
5609
+ if (clear.has("deprecationMessage") && Object.prototype.hasOwnProperty.call(set, "deprecated") && set.deprecated) {
5610
+ }
5611
+ assertNoClearConflict(set, clear);
5612
+ assertSecretSafeSpecSet(logicalKey, set);
5613
+ return {
5614
+ set,
5615
+ clear: Array.from(clear),
5616
+ hasFieldFlags: Object.keys(set).length > 0 || clear.size > 0,
5617
+ cliArgs
5618
+ };
5619
+ }
5620
+
5621
+ // src/commands/spec.ts
5622
+ function normalizeSpecAction(args) {
5623
+ const [action = "list", ...tail] = args;
5624
+ if (["list", "show", "set", "delete", "remove", "doctor"].includes(action)) {
5625
+ return {
5626
+ action: action === "remove" ? "delete" : action,
5627
+ tail
5628
+ };
5629
+ }
5630
+ return {
5631
+ action: "show",
5632
+ tail: args
5633
+ };
5634
+ }
5635
+ function validateNamespaceQualifiedKey2(logicalKey) {
5636
+ const key = logicalKey.trim();
5637
+ if (!key.includes(".")) {
5638
+ throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
5639
+ }
5640
+ const namespace = key.slice(0, key.indexOf("."));
5641
+ const keyPath = key.slice(key.indexOf(".") + 1);
5642
+ if (!namespace || !keyPath) {
5643
+ throw new Error(`Spec key must be namespace-qualified: ${logicalKey}`);
5644
+ }
5645
+ return key;
5646
+ }
5647
+ function hasFieldFlag(cliArgs) {
5648
+ const clearFlags = /* @__PURE__ */ new Set([
5649
+ "--clear-default",
5650
+ "--clear-enum",
5651
+ "--clear-pattern",
5652
+ "--clear-summary",
5653
+ "--clear-description",
5654
+ "--clear-examples",
5655
+ "--clear-used-by",
5656
+ "--clear-deprecated",
5657
+ "--clear-deprecation-message"
5658
+ ]);
5659
+ const optionFlags = /* @__PURE__ */ new Set([
5660
+ "--type",
5661
+ "--default",
5662
+ "--enum",
5663
+ "--pattern",
5664
+ "--summary",
5665
+ "--description",
5666
+ "--example",
5667
+ "--used-by",
5668
+ "--deprecation-message"
5669
+ ]);
5670
+ const toggleFlags = /* @__PURE__ */ new Set(["--required", "--optional", "--deprecated"]);
5671
+ return cliArgs.some((arg) => {
5672
+ const raw = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
5673
+ return clearFlags.has(raw) || optionFlags.has(raw) || toggleFlags.has(raw);
5674
+ });
5675
+ }
5676
+ function assertSecretSafeSpecSet2(logicalKey, set) {
5677
+ if (!logicalKey.startsWith("secret.")) {
5678
+ return;
5679
+ }
5680
+ const forbidden = ["default", "enum", "examples"].filter((field) => Object.prototype.hasOwnProperty.call(set, field));
5681
+ if (forbidden.length > 0) {
5682
+ throw new Error(
5683
+ `Cannot set ${forbidden.join(", ")} for secret spec key ${logicalKey}. Store secret values in the vault, not schema metadata.`
5684
+ );
5685
+ }
5686
+ }
5687
+ async function runSpec(args = [], options = {}) {
5688
+ const { action, tail } = normalizeSpecAction(args);
5689
+ const cliArgs = [...options.cliArgs ?? []];
5690
+ if (action === "list") {
5691
+ const prefix = consumeOption(cliArgs, "--prefix");
5692
+ if (cliArgs.length > 0) {
5693
+ throw new Error(`Unsupported spec list options: ${cliArgs.join(" ")}`);
5694
+ }
5695
+ const result2 = await listSpecEntries({
5696
+ ...options,
5697
+ ...prefix ? {
5698
+ prefix
5699
+ } : {}
5700
+ });
5701
+ if (options.json) {
5702
+ return printJson(result2);
5703
+ }
5704
+ return result2.entries.map((entry) => entry.key).join("\n");
5705
+ }
5706
+ if (action === "show") {
5707
+ const logicalKey2 = validateNamespaceQualifiedKey2(tail[0] ?? "");
5708
+ if (tail.length > 1) {
5709
+ throw new Error("spec show accepts exactly one <logicalKey>");
5710
+ }
5711
+ if (cliArgs.length > 0) {
5712
+ throw new Error(`Unsupported spec show options: ${cliArgs.join(" ")}`);
5713
+ }
5714
+ const result2 = await showSpecEntry(logicalKey2, options);
5715
+ if (!result2.rule) {
5716
+ throw new Error(`No spec entry found for ${logicalKey2}`);
5717
+ }
5718
+ if (options.json) {
5719
+ return printJson(result2);
5720
+ }
5721
+ return [
5722
+ `${result2.key}:`,
5723
+ ...Object.entries(result2.rule).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`)
5724
+ ].join("\n");
5725
+ }
5726
+ if (action === "set") {
5727
+ const logicalKey2 = validateNamespaceQualifiedKey2(tail[0] ?? "");
5728
+ if (tail.length > 1) {
5729
+ throw new Error("spec set accepts exactly one <logicalKey>");
5730
+ }
5731
+ const fieldFlagPresent = hasFieldFlag(cliArgs);
5732
+ if (!fieldFlagPresent && options.json) {
5733
+ throw new Error("spec set --json requires field flags; interactive JSON mode is not supported.");
5734
+ }
5735
+ let input = parseSpecSetInput(logicalKey2, cliArgs);
5736
+ if (!input.hasFieldFlags) {
5737
+ if (!isInteractiveSpecPromptMode()) {
5738
+ throw new Error("spec set without field flags requires an interactive TTY.");
5739
+ }
5740
+ input = await promptSpecSetInput(logicalKey2);
5741
+ }
5742
+ if (Object.keys(input.set).length === 0 && input.clear.length === 0) {
5743
+ throw new Error("spec set requires at least one field to set or clear.");
5744
+ }
5745
+ assertSecretSafeSpecSet2(logicalKey2, input.set);
5746
+ if (input.cliArgs.length > 0) {
5747
+ throw new Error(`Unsupported spec set options: ${input.cliArgs.join(" ")}`);
5748
+ }
5749
+ await assertWritableConfigRoot(`update spec ${logicalKey2}`, options);
5750
+ const result2 = await setSpecEntry(logicalKey2, {
5751
+ ...options,
5752
+ set: input.set,
5753
+ clear: input.clear
5754
+ });
5755
+ if (options.json) {
5756
+ return printJson(result2);
5757
+ }
5758
+ return `${result2.action} spec ${result2.key}`;
5759
+ }
5760
+ if (action === "doctor") {
5761
+ const fillMissing = cliArgs.includes("--fill-missing");
5762
+ const reviewAll = cliArgs.includes("--review-all");
5763
+ if (fillMissing && reviewAll) {
5764
+ throw new Error("spec doctor accepts only one write mode: --fill-missing or --review-all");
5765
+ }
5766
+ const mode = fillMissing ? "fill-missing" : reviewAll ? "review-all" : "report";
5767
+ if (mode !== "report" && options.json) {
5768
+ throw new Error(`spec doctor --${mode} does not support --json in Phases 1-3.`);
5769
+ }
5770
+ if (cliArgs.some((value) => !["--fill-missing", "--review-all"].includes(value))) {
5771
+ throw new Error(`Unsupported spec doctor options: ${cliArgs.join(" ")}`);
5772
+ }
5773
+ if (mode !== "report") {
5774
+ await assertWritableConfigRoot(`run spec doctor --${mode}`, options);
5775
+ }
5776
+ const outcome = await runSpecDoctor(mode, options);
5777
+ if (outcome.blocking) {
5778
+ process.exitCode = 1;
5779
+ }
5780
+ if (options.json) {
5781
+ return printJson(outcome.result);
5782
+ }
5783
+ return formatSpecDoctorResult(outcome.result);
5784
+ }
5785
+ const logicalKey = validateNamespaceQualifiedKey2(tail[0] ?? "");
5786
+ if (tail.length > 1) {
5787
+ throw new Error("spec delete accepts exactly one <logicalKey>");
5788
+ }
5789
+ if (cliArgs.length > 0) {
5790
+ throw new Error(`Unsupported spec delete options: ${cliArgs.join(" ")}`);
5791
+ }
5792
+ await assertWritableConfigRoot(`delete spec ${logicalKey}`, options);
5793
+ const result = await deleteSpecEntry(logicalKey, options);
5794
+ if (options.json) {
5795
+ return printJson(result);
5796
+ }
5797
+ return result.deleted ? `deleted spec ${result.key}` : `no spec entry found for ${result.key}`;
5798
+ }
5799
+
5800
+ // src/commands/use.ts
5801
+ import path22 from "path";
5802
+ async function runUse(args = [], options = {}) {
5803
+ const root = path22.resolve(options.root ?? process.cwd());
5804
+ const action = args[0];
5805
+ const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
5806
+ if (action === "show" || !action && !hasUpdates) {
5807
+ const context = await loadCliContext(root);
5808
+ if (options.json) {
5809
+ return printJson(context);
5810
+ }
5811
+ return Object.keys(context).length === 0 ? "no CLI context configured" : printJson(context);
5812
+ }
5813
+ const result = await saveCliContext({
5814
+ root,
5815
+ ...options.workspace ? { workspace: options.workspace } : {},
5816
+ ...options.profile ? { profile: options.profile } : {},
5817
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
5818
+ });
5819
+ if (options.json) {
5820
+ return printJson(result);
5821
+ }
5822
+ return `updated CLI context in ${displayPath(result.filePath, root)}`;
5823
+ }
5824
+
5825
+ // src/commands/ui.ts
5826
+ import { createServer } from "http";
5827
+ import path23 from "path";
5828
+ import { createRequire } from "module";
5829
+ import { loadManifest as loadManifest11 } from "@kitsy/cnos/internal";
5830
+ function parsePort(value, fallback, flag) {
5831
+ if (!value) {
5832
+ return fallback;
5833
+ }
5834
+ const parsed = Number(value);
5835
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
5836
+ throw new Error(`Invalid value for ${flag}: ${value}`);
5837
+ }
5838
+ return parsed;
5839
+ }
5840
+ function resolveUiPackageRoot() {
5841
+ const require2 = createRequire(import.meta.url);
5842
+ try {
5843
+ const packageJsonPath = require2.resolve("@kitsy/cnos-ui/package.json");
5844
+ return path23.dirname(packageJsonPath);
5845
+ } catch {
5846
+ throw new Error("Unable to resolve @kitsy/cnos-ui. Install workspace dependencies before running `cnos ui`.");
5847
+ }
5848
+ }
5849
+ function writeJson(response, statusCode, payload) {
5850
+ response.statusCode = statusCode;
5851
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
5852
+ response.end(`${printJson(payload)}
5853
+ `);
5854
+ }
5855
+ async function readJsonBody(request) {
5856
+ const chunks = [];
5857
+ for await (const chunk of request) {
5858
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4675
5859
  }
4676
5860
  if (chunks.length === 0) {
4677
5861
  return {};
@@ -4733,7 +5917,7 @@ async function handleSummary(options, searchParams) {
4733
5917
  ...runtimeOptions,
4734
5918
  secretResolution: "lazy"
4735
5919
  });
4736
- const loadedManifest = await loadManifest9({
5920
+ const loadedManifest = await loadManifest11({
4737
5921
  ...options.root ? { root: options.root } : {},
4738
5922
  ...options.cwd ? { cwd: options.cwd } : {},
4739
5923
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -4744,7 +5928,7 @@ async function handleSummary(options, searchParams) {
4744
5928
  const envEntries = runtime.toEnv();
4745
5929
  const publicEntries = runtime.toPublicEnv();
4746
5930
  const counts = Array.from(runtime.graph.entries.values()).reduce((acc, entry) => {
4747
- acc.all += 1;
5931
+ acc.all = (acc.all ?? 0) + 1;
4748
5932
  acc[entry.namespace] = (acc[entry.namespace] ?? 0) + 1;
4749
5933
  return acc;
4750
5934
  }, { all: 0 });
@@ -4776,9 +5960,12 @@ async function handleRevealList(body, options) {
4776
5960
  const prefix = typeof body.prefix === "string" ? body.prefix.trim() : "";
4777
5961
  const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
4778
5962
  const runtimeOptions = toRuntimeOptionsFromBody(options, body);
5963
+ const processEnv = withUiPassphrase(options.processEnv, passphrase);
4779
5964
  const runtime = await createRuntimeService({
4780
5965
  ...runtimeOptions,
4781
- processEnv: withUiPassphrase(options.processEnv, passphrase),
5966
+ ...processEnv ? {
5967
+ processEnv
5968
+ } : {},
4782
5969
  secretResolution: "lazy"
4783
5970
  });
4784
5971
  const entries = [];
@@ -4807,9 +5994,12 @@ async function handleRevealInspect(body, options) {
4807
5994
  }
4808
5995
  const passphrase = typeof body.passphrase === "string" ? body.passphrase : void 0;
4809
5996
  const runtimeOptions = toRuntimeOptionsFromBody(options, body);
5997
+ const processEnv = withUiPassphrase(options.processEnv, passphrase);
4810
5998
  const runtime = await createRuntimeService({
4811
5999
  ...runtimeOptions,
4812
- processEnv: withUiPassphrase(options.processEnv, passphrase),
6000
+ ...processEnv ? {
6001
+ processEnv
6002
+ } : {},
4813
6003
  ...key.startsWith("secret.") ? { secretResolution: "lazy" } : {}
4814
6004
  });
4815
6005
  if (key.startsWith("secret.")) {
@@ -4949,7 +6139,7 @@ async function runValidate(options = {}) {
4949
6139
  // package.json
4950
6140
  var package_default = {
4951
6141
  name: "@kitsy/cnos-cli",
4952
- version: "1.9.2",
6142
+ version: "1.10.0",
4953
6143
  description: "CLI entry point and developer tooling for CNOS.",
4954
6144
  type: "module",
4955
6145
  main: "./dist/index.js",
@@ -5226,9 +6416,9 @@ async function runWatch(command, options = {}) {
5226
6416
  }
5227
6417
 
5228
6418
  // src/commands/workspace.ts
5229
- import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile8, rename, rm as rm5, stat as stat3, writeFile as 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";
5230
6420
  import path25 from "path";
5231
- 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";
5232
6422
  async function exists2(targetPath) {
5233
6423
  try {
5234
6424
  await stat3(targetPath);
@@ -5267,9 +6457,9 @@ async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
5267
6457
  async function writeAnchor(packageRoot, manifestRoot, workspace) {
5268
6458
  const relativeRoot = path25.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
5269
6459
  const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
5270
- await writeFile10(
6460
+ await writeFile11(
5271
6461
  path25.join(packageRoot, ".cnosrc.yml"),
5272
- stringifyYaml8({
6462
+ stringifyYaml9({
5273
6463
  root: rootValue,
5274
6464
  ...workspace ? { workspace } : {}
5275
6465
  }),
@@ -5319,9 +6509,9 @@ async function hasDirectConfigData(cnosRoot) {
5319
6509
  async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
5320
6510
  const anchorPath = path25.join(packageRoot, ".cnosrc.yml");
5321
6511
  const current = await exists2(anchorPath) ? parseYaml6(await readFile8(anchorPath, "utf8")) : void 0;
5322
- await writeFile10(
6512
+ await writeFile11(
5323
6513
  anchorPath,
5324
- stringifyYaml8({
6514
+ stringifyYaml9({
5325
6515
  root: typeof current?.root === "string" ? current.root : "./.cnos",
5326
6516
  workspace: workspaceId
5327
6517
  }),
@@ -5331,9 +6521,9 @@ async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
5331
6521
  async function updateWorkspaceContext(packageRoot, workspaceId) {
5332
6522
  const workspacePath = path25.join(packageRoot, ".cnos-workspace.yml");
5333
6523
  const current = await exists2(workspacePath) ? parseYaml6(await readFile8(workspacePath, "utf8")) : void 0;
5334
- await writeFile10(
6524
+ await writeFile11(
5335
6525
  workspacePath,
5336
- stringifyYaml8({
6526
+ stringifyYaml9({
5337
6527
  workspace: workspaceId,
5338
6528
  ...typeof current?.profile === "string" ? { profile: current.profile } : {},
5339
6529
  ...typeof current?.globalRoot === "string" ? { globalRoot: current.globalRoot } : { globalRoot: "~/.cnos" }
@@ -5342,7 +6532,7 @@ async function updateWorkspaceContext(packageRoot, workspaceId) {
5342
6532
  );
5343
6533
  }
5344
6534
  async function runDetach(packageRoot, options = {}) {
5345
- const loaded = await loadManifest10({ cwd: packageRoot });
6535
+ const loaded = await loadManifest12({ cwd: packageRoot });
5346
6536
  if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
5347
6537
  throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
5348
6538
  }
@@ -5362,9 +6552,9 @@ async function runDetach(packageRoot, options = {}) {
5362
6552
  const localRoots = runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => root.path);
5363
6553
  await mkdir7(targetCnosRoot, { recursive: true });
5364
6554
  await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
5365
- await writeFile10(
6555
+ await writeFile11(
5366
6556
  path25.join(targetCnosRoot, "cnos.yml"),
5367
- stringifyYaml8(createDetachedManifest(loaded.rawManifest)),
6557
+ stringifyYaml9(createDetachedManifest(loaded.rawManifest)),
5368
6558
  "utf8"
5369
6559
  );
5370
6560
  const relativeRoot = path25.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
@@ -5377,8 +6567,8 @@ async function runDetach(packageRoot, options = {}) {
5377
6567
  workspace: loaded.anchoredWorkspace
5378
6568
  }
5379
6569
  };
5380
- await writeFile10(path25.join(targetCnosRoot, ".detached"), stringifyYaml8(marker), "utf8");
5381
- 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");
5382
6572
  if (options.json) {
5383
6573
  return printJson({
5384
6574
  packageRoot,
@@ -5401,7 +6591,7 @@ async function runAttach(packageRoot, options = {}) {
5401
6591
  throw new Error("Invalid .detached marker");
5402
6592
  }
5403
6593
  const parentManifestRoot = path25.resolve(packageRoot, marker.originalCnosrc.root);
5404
- const parentLoaded = await loadManifest10({ root: parentManifestRoot });
6594
+ const parentLoaded = await loadManifest12({ root: parentManifestRoot });
5405
6595
  if (parentLoaded.rootResolution.readOnly) {
5406
6596
  throw new Error(
5407
6597
  `Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
@@ -5425,7 +6615,7 @@ async function runAttach(packageRoot, options = {}) {
5425
6615
  items[workspaceId] = items[workspaceId] ?? {};
5426
6616
  workspaces.items = items;
5427
6617
  rawManifest.workspaces = workspaces;
5428
- await writeFile10(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
6618
+ await writeFile11(path25.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
5429
6619
  const archivePath = path25.join(packageRoot, ".cnos.detached.bak");
5430
6620
  await rm5(archivePath, { recursive: true, force: true });
5431
6621
  await rename(childCnosRoot, archivePath);
@@ -5441,7 +6631,7 @@ async function runAttach(packageRoot, options = {}) {
5441
6631
  return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
5442
6632
  }
5443
6633
  async function runList2(manifestCwd, options = {}) {
5444
- const loaded = await loadManifest10({
6634
+ const loaded = await loadManifest12({
5445
6635
  ...options.root ? { root: options.root } : {},
5446
6636
  cwd: manifestCwd,
5447
6637
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -5474,7 +6664,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
5474
6664
  if (cliArgs.length > 0) {
5475
6665
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
5476
6666
  }
5477
- const loaded = await loadManifest10({
6667
+ const loaded = await loadManifest12({
5478
6668
  ...options.root ? { root: options.root } : {},
5479
6669
  cwd: manifestCwd,
5480
6670
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -5507,7 +6697,7 @@ async function runEnable(manifestCwd, packageRoot, options = {}) {
5507
6697
  base: {}
5508
6698
  };
5509
6699
  rawManifest.workspaces = rawWorkspaces;
5510
- await writeFile10(path25.join(cnosRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
6700
+ await writeFile11(path25.join(cnosRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
5511
6701
  await updateRootAnchorToWorkspace(packageRoot, "base");
5512
6702
  await updateWorkspaceContext(packageRoot, "base");
5513
6703
  await ensureGitignore(path25.dirname(cnosRoot));
@@ -5528,7 +6718,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
5528
6718
  if (cliArgs.length > 0) {
5529
6719
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
5530
6720
  }
5531
- const loaded = await loadManifest10({
6721
+ const loaded = await loadManifest12({
5532
6722
  ...options.root ? { root: options.root } : {},
5533
6723
  cwd: manifestCwd,
5534
6724
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -5560,7 +6750,7 @@ async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, o
5560
6750
  rawManifest.workspaces = rawWorkspaces;
5561
6751
  const workspaceRoot = path25.join(cnosRoot, "workspaces", workspaceId);
5562
6752
  const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
5563
- await writeFile10(path25.join(cnosRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
6753
+ await writeFile11(path25.join(cnosRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
5564
6754
  await ensureGitignore(path25.dirname(cnosRoot));
5565
6755
  await writeAnchor(packageRoot, cnosRoot, workspaceId);
5566
6756
  await updateWorkspaceContext(packageRoot, workspaceId);
@@ -5582,7 +6772,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
5582
6772
  if (cliArgs.length > 0) {
5583
6773
  throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
5584
6774
  }
5585
- const loaded = await loadManifest10({
6775
+ const loaded = await loadManifest12({
5586
6776
  ...options.root ? { root: options.root } : {},
5587
6777
  cwd: manifestCwd,
5588
6778
  ...options.processEnv ? { processEnv: options.processEnv } : {}
@@ -5604,7 +6794,7 @@ async function runRemove(workspaceId, manifestCwd, options = {}) {
5604
6794
  delete rawItems[workspaceId];
5605
6795
  rawWorkspaces.items = rawItems;
5606
6796
  rawManifest.workspaces = rawWorkspaces;
5607
- await writeFile10(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml8(rawManifest), "utf8");
6797
+ await writeFile11(path25.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml9(rawManifest), "utf8");
5608
6798
  await rm5(path25.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
5609
6799
  if (options.json) {
5610
6800
  return printJson({
@@ -5693,6 +6883,9 @@ function resolveHelpTopic(command, args) {
5693
6883
  if (command === "profile" && args[0] && ["create", "list", "use", "delete", "remove"].includes(args[0])) {
5694
6884
  return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
5695
6885
  }
6886
+ if (command === "spec" && args[0] && ["list", "show", "set", "delete", "remove", "doctor"].includes(args[0])) {
6887
+ return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
6888
+ }
5696
6889
  return normalizeHelpTopic([command]);
5697
6890
  }
5698
6891
  async function main(argv) {
@@ -5770,6 +6963,10 @@ async function main(argv) {
5770
6963
  return;
5771
6964
  case "secret":
5772
6965
  process.stdout.write(`${await runSecret(args.length > 0 ? args : ["app.token"], runtimeOptions)}
6966
+ `);
6967
+ return;
6968
+ case "spec":
6969
+ process.stdout.write(`${await runSpec(args, runtimeOptions)}
5773
6970
  `);
5774
6971
  return;
5775
6972
  case "vault":