@squadbase/vantage 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { PAGE_EXTENSIONS, scanProject, vantagePlugin, generateArtifacts, VANTAGE_DIR, MANIFEST_SCHEMA_VERSION, FORBIDDEN_FILES, API_METHODS, PUBLIC_ENV_PREFIX } from './chunk-UHZ7XSAN.js';
2
+ import { PAGE_EXTENSIONS, scanProject, vantagePlugin, generateArtifacts, resolvePageRoot, VANTAGE_DIR, MANIFEST_SCHEMA_VERSION, FORBIDDEN_FILES, SRC_DIR, SUSPICIOUS_ROUTE_DIRS, API_METHODS, PUBLIC_ENV_PREFIX, extractPageMeta } from './chunk-5UGRNXHC.js';
3
3
  import './chunk-46QI6GFC.js';
4
4
  import './chunk-B7LLBNLV.js';
5
5
  import path2 from 'path';
@@ -12,7 +12,21 @@ import { fileURLToPath } from 'url';
12
12
  import { spawn } from 'child_process';
13
13
 
14
14
  // @squadbase/vantage — generated build. Do not edit.
15
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "force", "verbose", "version", "all", "list", "regex", "h", "v"]);
15
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
16
+ "json",
17
+ "help",
18
+ "force",
19
+ "verbose",
20
+ "version",
21
+ "all",
22
+ "list",
23
+ "regex",
24
+ "pages",
25
+ "apis",
26
+ "detail",
27
+ "h",
28
+ "v"
29
+ ]);
16
30
  function parseArgs(argv) {
17
31
  const positionals = [];
18
32
  const flags = {};
@@ -61,12 +75,20 @@ function resolveRoot(flags) {
61
75
  const raw = typeof flags.root === "string" ? flags.root : process.cwd();
62
76
  return path2.resolve(raw);
63
77
  }
78
+ var QUICK_ROOT_MAX_DEPTH = 5;
64
79
  function resolveQuickRoot(target) {
65
80
  try {
66
81
  const abs = path2.resolve(target);
67
82
  const stat = fs2.statSync(abs);
68
83
  if (stat.isDirectory()) return abs;
69
84
  if (stat.isFile() && PAGE_EXTENSIONS.some((ext) => abs.endsWith(ext))) {
85
+ let dir = path2.dirname(abs);
86
+ for (let i = 0; i <= QUICK_ROOT_MAX_DEPTH; i++) {
87
+ if (fs2.existsSync(path2.join(dir, "package.json"))) return dir;
88
+ const parent = path2.dirname(dir);
89
+ if (parent === dir) break;
90
+ dir = parent;
91
+ }
70
92
  return path2.dirname(abs);
71
93
  }
72
94
  } catch {
@@ -305,7 +327,7 @@ function copyFromRegistry(root, kind, name, force) {
305
327
  log.error(`Unknown ${kind} "${name}". Available: ${available.join(", ") || "(none)"}`);
306
328
  return 1;
307
329
  }
308
- const destDir = path2.join(root, "components", kind === "ui" ? "ui" : "blocks");
330
+ const destDir = path2.join(resolvePageRoot(root).abs, "components", kind === "ui" ? "ui" : "blocks");
309
331
  const dest = path2.join(destDir, `${name}.tsx`);
310
332
  if (fs2.existsSync(dest) && !force) {
311
333
  log.error(`${path2.relative(root, dest)} already exists. Use --force to overwrite.`);
@@ -331,7 +353,8 @@ async function add(root, args) {
331
353
  return 1;
332
354
  }
333
355
  if (kind === "page") {
334
- const file = path2.join(root, name.endsWith(".tsx") ? name : `${name}.tsx`);
356
+ const pageRoot = resolvePageRoot(root).abs;
357
+ const file = path2.join(pageRoot, name.endsWith(".tsx") ? name : `${name}.tsx`);
335
358
  if (fs2.existsSync(file) && !force) {
336
359
  log.error(`${path2.relative(root, file)} already exists. Use --force to overwrite.`);
337
360
  return 1;
@@ -358,10 +381,202 @@ async function add(root, args) {
358
381
  log.error(`Unknown add target "${kind}". Use one of: page, api, ui, block, skill, agents.`);
359
382
  return 1;
360
383
  }
361
- var ALLOWED_ENV_KEYS = /* @__PURE__ */ new Set(["MODE", "DEV", "PROD", "SSR", "BASE_URL", "PUBLIC_"]);
384
+
385
+ // src/core/source.ts
362
386
  function stripComments(src) {
363
387
  return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`\\])\/\/[^\n]*/g, "$1");
364
388
  }
389
+ var OPENERS = { "(": ")", "[": "]", "{": "}" };
390
+ function readBracketBody(src, openIndex) {
391
+ const open = src[openIndex];
392
+ if (!open || !OPENERS[open]) return null;
393
+ let depth = 0;
394
+ let quote = null;
395
+ for (let i = openIndex; i < src.length; i++) {
396
+ const c = src[i];
397
+ if (quote) {
398
+ if (c === "\\") i++;
399
+ else if (c === quote) quote = null;
400
+ continue;
401
+ }
402
+ if (c === '"' || c === "'" || c === "`") {
403
+ quote = c;
404
+ continue;
405
+ }
406
+ if (OPENERS[c]) depth++;
407
+ else if (c === ")" || c === "]" || c === "}") {
408
+ if (--depth === 0) return src.slice(openIndex + 1, i);
409
+ }
410
+ }
411
+ return null;
412
+ }
413
+ function splitTopLevel(body) {
414
+ const parts = [];
415
+ let depth = 0;
416
+ let quote = null;
417
+ let start = 0;
418
+ for (let i = 0; i < body.length; i++) {
419
+ const c = body[i];
420
+ if (quote) {
421
+ if (c === "\\") i++;
422
+ else if (c === quote) quote = null;
423
+ continue;
424
+ }
425
+ if (c === '"' || c === "'" || c === "`") {
426
+ quote = c;
427
+ continue;
428
+ }
429
+ if (OPENERS[c]) depth++;
430
+ else if (c === ")" || c === "]" || c === "}") depth--;
431
+ else if (c === "," && depth === 0) {
432
+ parts.push(body.slice(start, i));
433
+ start = i + 1;
434
+ }
435
+ }
436
+ parts.push(body.slice(start));
437
+ return parts.map((p) => p.trim()).filter(Boolean);
438
+ }
439
+
440
+ // src/core/route-spec.ts
441
+ function detectApiMethods(src) {
442
+ return API_METHODS.filter(
443
+ (m) => new RegExp(`export\\s+(async\\s+)?(function|const|let|var)\\s+${m}\\b`).test(src) || new RegExp(`export\\s*\\{[^}]*\\b${m}\\b[^}]*\\}`).test(src)
444
+ );
445
+ }
446
+ function methodRegions(src) {
447
+ const re = new RegExp(
448
+ `export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+(${API_METHODS.join("|")})\\b`,
449
+ "g"
450
+ );
451
+ const decls = [];
452
+ let m;
453
+ while (m = re.exec(src)) decls.push({ method: m[1], index: m.index });
454
+ const regions = /* @__PURE__ */ new Map();
455
+ decls.forEach((d, i) => {
456
+ regions.set(d.method, src.slice(d.index, decls[i + 1]?.index ?? src.length));
457
+ });
458
+ for (const method of detectApiMethods(src)) {
459
+ if (!regions.has(method)) regions.set(method, src);
460
+ }
461
+ return regions;
462
+ }
463
+ function objectKeys(expr) {
464
+ const trimmed = expr.trim();
465
+ if (!trimmed.startsWith("{")) return void 0;
466
+ const body = readBracketBody(trimmed, 0);
467
+ if (body == null) return void 0;
468
+ const keys = [];
469
+ for (const part of splitTopLevel(body)) {
470
+ if (part.startsWith("...")) {
471
+ keys.push("\u2026");
472
+ continue;
473
+ }
474
+ const name = part.match(/^["'`]?([A-Za-z0-9_$]+)/);
475
+ if (name) keys.push(name[1]);
476
+ }
477
+ return keys.length ? keys : void 0;
478
+ }
479
+ function initStatus(init) {
480
+ const m = init?.match(/\bstatus\s*:\s*(\d{3})\b/);
481
+ return m ? Number(m[1]) : void 0;
482
+ }
483
+ function initContentType(init) {
484
+ const m = init?.match(/["']?content-type["']?\s*:\s*["']([^"';]+)/i);
485
+ return m ? m[1].trim() : void 0;
486
+ }
487
+ function extractResponses(region) {
488
+ const re = /\bnew\s+Response\s*\(|\bResponse\s*\.\s*json\s*\(|(?<![.\w$])json\s*\(/g;
489
+ const out = [];
490
+ const seen = /* @__PURE__ */ new Set();
491
+ let m;
492
+ while (m = re.exec(region)) {
493
+ const isJson = !m[0].startsWith("new");
494
+ const args = readBracketBody(region, m.index + m[0].length - 1);
495
+ if (args == null) continue;
496
+ const [payload, init] = splitTopLevel(args);
497
+ const contentType = isJson ? "application/json" : initContentType(init) ?? (payload?.startsWith("JSON.stringify") ? "application/json" : void 0);
498
+ const keys = isJson && payload ? objectKeys(payload) : void 0;
499
+ const spec = {
500
+ status: initStatus(init) ?? 200,
501
+ ...contentType ? { contentType } : {},
502
+ ...keys ? { keys } : {}
503
+ };
504
+ const key = JSON.stringify(spec);
505
+ if (seen.has(key)) continue;
506
+ seen.add(key);
507
+ out.push(spec);
508
+ }
509
+ return out;
510
+ }
511
+ function extractErrors(region) {
512
+ const re = /\bHttpError\s*\(\s*(\d{3})\s*(?:,\s*(?:(["'`])((?:\\.|(?!\2)[\s\S])*)\2))?/g;
513
+ const out = [];
514
+ const seen = /* @__PURE__ */ new Set();
515
+ let m;
516
+ while (m = re.exec(region)) {
517
+ const message = m[3]?.replace(/\\(.)/g, "$1");
518
+ const spec = { status: Number(m[1]), ...message ? { message } : {} };
519
+ const key = JSON.stringify(spec);
520
+ if (seen.has(key)) continue;
521
+ seen.add(key);
522
+ out.push(spec);
523
+ }
524
+ return out;
525
+ }
526
+ function extractQuery(region) {
527
+ const re = /searchParams\s*\.\s*(?:get|getAll|has)\s*\(\s*["'`]([^"'`]+)["'`]/g;
528
+ const out = /* @__PURE__ */ new Set();
529
+ let m;
530
+ while (m = re.exec(region)) out.add(m[1]);
531
+ return [...out];
532
+ }
533
+ function extractBody(region) {
534
+ const read = region.match(/\b(?:request|req)\s*\.\s*(json|formData|text|arrayBuffer|blob)\s*\(/);
535
+ if (!read) return void 0;
536
+ const method = read[1];
537
+ const format = method === "json" || method === "formData" || method === "text" ? method : "binary";
538
+ const destructured = region.match(
539
+ /(?:const|let|var)\s*(\{[\s\S]*?\})\s*=\s*(?:await\s+)?[\w.]*\.json\s*\(/
540
+ );
541
+ const keys = format === "json" && destructured ? objectKeys(destructured[1]) : void 0;
542
+ return { format, ...keys ? { keys } : {} };
543
+ }
544
+ function toParams(segments) {
545
+ const out = [];
546
+ for (const s of segments) {
547
+ if (s.kind === "static") continue;
548
+ out.push({ name: s.name, kind: s.kind });
549
+ }
550
+ return out;
551
+ }
552
+ function extractPageSpec(page) {
553
+ return { params: toParams(page.segments), ...extractPageMeta(page.absFile) };
554
+ }
555
+ function extractApiSpec(api) {
556
+ const params = toParams(api.segments);
557
+ let src;
558
+ try {
559
+ src = stripComments(fs2.readFileSync(api.absFile, "utf8"));
560
+ } catch {
561
+ return { params, methods: [] };
562
+ }
563
+ const regions = methodRegions(src);
564
+ const methods = API_METHODS.filter((m) => regions.has(m)).map((method) => {
565
+ const region = regions.get(method);
566
+ const body = extractBody(region);
567
+ return {
568
+ method,
569
+ query: extractQuery(region),
570
+ ...body ? { body } : {},
571
+ responses: extractResponses(region),
572
+ errors: extractErrors(region)
573
+ };
574
+ });
575
+ return { params, methods };
576
+ }
577
+
578
+ // src/core/diagnostics.ts
579
+ var ALLOWED_ENV_KEYS = /* @__PURE__ */ new Set(["MODE", "DEV", "PROD", "SSR", "BASE_URL", "PUBLIC_"]);
365
580
  function hasDefaultExport(src) {
366
581
  return /\bexport\s+default\b/.test(src) || /\bexport\s*\{[^}]*\bas\s+default\b[^}]*\}/.test(src) || /\bexport\s*\{[^}]*\bdefault\b[^}]*\}\s*from\b/.test(src);
367
582
  }
@@ -415,6 +630,67 @@ function checkForbiddenFiles(root) {
415
630
  }
416
631
  return diags;
417
632
  }
633
+ function checkSrcLayout(root, model) {
634
+ if (model.pageDir === "") return [];
635
+ const diags = [];
636
+ let entries = [];
637
+ try {
638
+ entries = fs2.readdirSync(root);
639
+ } catch {
640
+ return diags;
641
+ }
642
+ const strayPages = entries.filter((name) => PAGE_EXTENSIONS.some((ext) => name.endsWith(ext)));
643
+ if (strayPages.length > 0) {
644
+ diags.push({
645
+ code: "SRC_DIR_SPLIT",
646
+ severity: "error",
647
+ message: `${strayPages.length} page module(s) sit at the project root, but ${SRC_DIR}/ exists so pages are only scanned from ${SRC_DIR}/.`,
648
+ files: strayPages.sort(),
649
+ fix: `Move them into ${SRC_DIR}/, or delete ${SRC_DIR}/ to keep a root-level layout.`
650
+ });
651
+ }
652
+ if (entries.includes("styles.css")) {
653
+ diags.push({
654
+ code: "SRC_DIR_SPLIT",
655
+ severity: "error",
656
+ message: `styles.css sits at the project root, but ${SRC_DIR}/ exists so the global stylesheet is read from ${SRC_DIR}/styles.css.`,
657
+ files: ["styles.css"],
658
+ fix: `Move it to ${SRC_DIR}/styles.css.`
659
+ });
660
+ }
661
+ if (fs2.existsSync(path2.join(root, SRC_DIR, "server"))) {
662
+ diags.push({
663
+ code: "SRC_DIR_SPLIT",
664
+ severity: "error",
665
+ message: `${SRC_DIR}/server/ is never scanned: server code lives at the project root, outside the client source tree.`,
666
+ files: [`${SRC_DIR}/server`],
667
+ fix: `Move it to server/ at the project root.`
668
+ });
669
+ }
670
+ return diags;
671
+ }
672
+ function checkSuspiciousRouteDirs(model) {
673
+ const byDir = /* @__PURE__ */ new Map();
674
+ for (const page of model.pages) {
675
+ const top = page.dir.split("/")[0] ?? "";
676
+ if (top === "" || !SUSPICIOUS_ROUTE_DIRS.has(top)) continue;
677
+ const hit = byDir.get(top) ?? { files: [], routes: [] };
678
+ hit.files.push(page.file);
679
+ hit.routes.push(page.routePath);
680
+ byDir.set(top, hit);
681
+ }
682
+ const diags = [];
683
+ for (const [dir, hit] of byDir) {
684
+ diags.push({
685
+ code: "SUSPICIOUS_ROUTE_DIR",
686
+ severity: "warning",
687
+ message: `${dir}/ is part of the URL: ${hit.routes.slice(0, 3).join(", ")}${hit.routes.length > 3 ? ", \u2026" : ""}`,
688
+ files: hit.files,
689
+ fix: `Directory names become route segments. Non-page modules belong in components/, hooks/ or lib/ (never scanned); if ${dir}/ really is meant to be a URL segment, ignore this warning.`
690
+ });
691
+ }
692
+ return diags;
693
+ }
418
694
  function checkRouteConflicts(model) {
419
695
  const byPath = /* @__PURE__ */ new Map();
420
696
  for (const page of model.pages) {
@@ -472,9 +748,7 @@ function checkApiExports(root, model) {
472
748
  } catch {
473
749
  continue;
474
750
  }
475
- const exported = API_METHODS.filter(
476
- (m) => new RegExp(`export\\s+(async\\s+)?(function|const|let|var)\\s+${m}\\b`).test(src) || new RegExp(`export\\s*\\{[^}]*\\b${m}\\b[^}]*\\}`).test(src)
477
- );
751
+ const exported = detectApiMethods(src);
478
752
  if (exported.length === 0) {
479
753
  const lower = ["get", "post", "put", "patch", "delete", "options"].filter(
480
754
  (m) => new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`).test(src)
@@ -553,6 +827,8 @@ function checkPublicEnv(root) {
553
827
  function runDiagnostics(root, model) {
554
828
  return [
555
829
  ...checkForbiddenFiles(root),
830
+ ...checkSrcLayout(root, model),
831
+ ...checkSuspiciousRouteDirs(model),
556
832
  ...checkRouteConflicts(model),
557
833
  ...checkPageExports(root, model),
558
834
  ...checkApiExports(root, model),
@@ -941,39 +1217,110 @@ async function preview(root, args) {
941
1217
  return new Promise(() => {
942
1218
  });
943
1219
  }
944
- async function routes(root, args) {
945
- const model = scanProject(root);
946
- if (args.flags.json) {
947
- printJson({
948
- pages: model.pages.map((p) => ({ route: p.routePath, file: p.file, dynamic: p.dynamic })),
949
- layouts: model.layouts.map((l) => ({ dir: l.dir === "" ? "/" : l.dir, file: l.file })),
950
- notFound: model.notFound,
951
- error: model.error,
952
- apis: model.apis.map((a) => ({ route: a.routePath, file: a.file, params: a.params })),
953
- hasServer: model.hasServer
954
- });
955
- return 0;
1220
+ var ROUTE_COLUMN = 28;
1221
+ var LABEL_COLUMN = 9;
1222
+ function detailLine(indent, label, value) {
1223
+ console.log(`${indent}${pc3.dim(label.padEnd(LABEL_COLUMN))}${value}`);
1224
+ }
1225
+ function formatParams(params) {
1226
+ return params.map((p) => p.kind === "catchall" ? `*${p.name}` : `:${p.name}`).join(", ");
1227
+ }
1228
+ function formatKeys(keys) {
1229
+ return keys?.length ? ` { ${keys.join(", ")} }` : "";
1230
+ }
1231
+ function printPageSpec(spec) {
1232
+ const indent = " ";
1233
+ if (spec.title) detailLine(indent, "title", spec.title);
1234
+ if (spec.navLabel) detailLine(indent, "nav", spec.navLabel);
1235
+ if (spec.description) detailLine(indent, "desc", spec.description);
1236
+ if (spec.params.length) detailLine(indent, "params", formatParams(spec.params));
1237
+ }
1238
+ function printApiMethodSpec(method, params) {
1239
+ const indent = " ";
1240
+ console.log(` ${pc3.bold(method.method)}`);
1241
+ if (params.length) detailLine(indent, "path", formatParams(params));
1242
+ if (method.query.length) detailLine(indent, "query", method.query.join(", "));
1243
+ if (method.body) detailLine(indent, "body", method.body.format + formatKeys(method.body.keys));
1244
+ for (const res of method.responses) {
1245
+ const parts = [String(res.status), res.contentType].filter(Boolean).join(" ");
1246
+ detailLine(indent, "response", parts + formatKeys(res.keys));
956
1247
  }
1248
+ for (const err of method.errors) {
1249
+ detailLine(indent, "error", `${err.status}${err.message ? ` "${err.message}"` : ""}`);
1250
+ }
1251
+ }
1252
+ function printPages(model, detail) {
957
1253
  console.log(pc3.bold("\nPages"));
958
1254
  if (model.pages.length === 0) console.log(pc3.dim(" (none)"));
959
1255
  for (const p of model.pages) {
960
- console.log(` ${pc3.green(p.routePath.padEnd(28))} ${pc3.dim(p.file)}`);
1256
+ console.log(` ${pc3.green(p.routePath.padEnd(ROUTE_COLUMN))} ${pc3.dim(p.file)}`);
1257
+ if (detail) printPageSpec(extractPageSpec(p));
961
1258
  }
962
1259
  if (model.layouts.length || model.notFound || model.error) {
963
1260
  console.log(pc3.bold("\nSpecial"));
964
1261
  for (const l of model.layouts) {
965
- console.log(` ${pc3.cyan(("layout " + (l.dir === "" ? "/" : "/" + l.dir)).padEnd(28))} ${pc3.dim(l.file)}`);
1262
+ const label = "layout " + (l.dir === "" ? "/" : "/" + l.dir);
1263
+ console.log(` ${pc3.cyan(label.padEnd(ROUTE_COLUMN))} ${pc3.dim(l.file)}`);
966
1264
  }
967
- if (model.notFound) console.log(` ${pc3.cyan("404".padEnd(28))} ${pc3.dim(model.notFound)}`);
968
- if (model.error) console.log(` ${pc3.cyan("error".padEnd(28))} ${pc3.dim(model.error)}`);
1265
+ if (model.notFound) console.log(` ${pc3.cyan("404".padEnd(ROUTE_COLUMN))} ${pc3.dim(model.notFound)}`);
1266
+ if (model.error) console.log(` ${pc3.cyan("error".padEnd(ROUTE_COLUMN))} ${pc3.dim(model.error)}`);
969
1267
  }
970
- if (model.hasServer) {
971
- console.log(pc3.bold("\nAPI"));
972
- if (model.apis.length === 0) console.log(pc3.dim(" (none)"));
973
- for (const a of model.apis) {
974
- console.log(` ${pc3.yellow(a.routePath.padEnd(28))} ${pc3.dim(a.file)}`);
1268
+ }
1269
+ function printApis(model, detail) {
1270
+ console.log(pc3.bold("\nAPI"));
1271
+ if (!model.hasServer) {
1272
+ console.log(pc3.dim(" (none \u2014 this project has no server/ directory)"));
1273
+ return;
1274
+ }
1275
+ if (model.apis.length === 0) console.log(pc3.dim(" (none)"));
1276
+ for (const a of model.apis) {
1277
+ console.log(` ${pc3.yellow(a.routePath.padEnd(ROUTE_COLUMN))} ${pc3.dim(a.file)}`);
1278
+ if (!detail) continue;
1279
+ const spec = extractApiSpec(a);
1280
+ if (spec.methods.length === 0) console.log(` ${pc3.dim("(no HTTP method handler exported)")}`);
1281
+ for (const method of spec.methods) printApiMethodSpec(method, spec.params);
1282
+ }
1283
+ }
1284
+ function pageJson(p, detail) {
1285
+ return {
1286
+ route: p.routePath,
1287
+ file: p.file,
1288
+ dynamic: p.dynamic,
1289
+ ...detail ? { spec: extractPageSpec(p) } : {}
1290
+ };
1291
+ }
1292
+ function apiJson(a, detail) {
1293
+ return {
1294
+ route: a.routePath,
1295
+ file: a.file,
1296
+ params: a.params,
1297
+ ...detail ? { spec: extractApiSpec(a) } : {}
1298
+ };
1299
+ }
1300
+ async function routes(root, args) {
1301
+ const model = scanProject(root);
1302
+ const onlyPages = Boolean(args.flags.pages);
1303
+ const onlyApis = Boolean(args.flags.apis);
1304
+ const showPages = onlyPages || !onlyApis;
1305
+ const showApis = onlyApis || !onlyPages;
1306
+ const detail = Boolean(args.flags.detail);
1307
+ if (args.flags.json) {
1308
+ const payload = {};
1309
+ if (showPages) {
1310
+ payload.pages = model.pages.map((p) => pageJson(p, detail));
1311
+ payload.layouts = model.layouts.map((l) => ({ dir: l.dir === "" ? "/" : l.dir, file: l.file }));
1312
+ payload.notFound = model.notFound;
1313
+ payload.error = model.error;
1314
+ }
1315
+ if (showApis) {
1316
+ payload.apis = model.apis.map((a) => apiJson(a, detail));
1317
+ payload.hasServer = model.hasServer;
975
1318
  }
1319
+ printJson(payload);
1320
+ return 0;
976
1321
  }
1322
+ if (showPages) printPages(model, detail);
1323
+ if (showApis && (model.hasServer || onlyApis)) printApis(model, detail);
977
1324
  console.log("");
978
1325
  return 0;
979
1326
  }
@@ -1322,7 +1669,7 @@ ${pc3.bold("Commands")}
1322
1669
  build Build client and optional server into dist/
1323
1670
  preview Run the production build locally
1324
1671
  check Static verification (routes, boundaries, forbidden files)
1325
- routes Show page and API URL map
1672
+ routes Show page and API URL map (--pages | --apis | --detail)
1326
1673
  add <kind> <name> Scaffold a page | api | ui | block | skill | agents
1327
1674
  (add skill with no name lists the skills and where they sit)
1328
1675
  docs [name] Show the bundled guide and component reference
@@ -1335,6 +1682,9 @@ ${pc3.bold("Options")}
1335
1682
  --port <n> Port for dev/preview
1336
1683
  --root <dir> Project root (default: cwd)
1337
1684
  --force Overwrite when scaffolding
1685
+ --pages Show only page routes (routes)
1686
+ --apis Show only API routes (routes)
1687
+ --detail Add each route's static spec (routes)
1338
1688
  --list List pages without showing one (docs)
1339
1689
  --lang <ja|en> Documentation language (docs, search, default: ja)
1340
1690
  --regex Treat the query as a regular expression (search)