@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/README.md +2 -2
- package/dist/{chunk-UHZ7XSAN.js → chunk-5UGRNXHC.js} +61 -21
- package/dist/chunk-5UGRNXHC.js.map +1 -0
- package/dist/cli.js +380 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/vite/index.js +1 -1
- package/docs/en/changelog.md +71 -0
- package/docs/en/cli-reference.md +59 -5
- package/docs/en/components.md +2 -1
- package/docs/en/getting-started.md +6 -3
- package/docs/en/routing.md +27 -1
- package/docs/en/ui-and-theming.md +4 -2
- package/docs/ja/changelog.md +65 -0
- package/docs/ja/cli-reference.md +59 -5
- package/docs/ja/components.md +3 -2
- package/docs/ja/getting-started.md +6 -3
- package/docs/ja/routing.md +26 -1
- package/docs/ja/ui-and-theming.md +4 -2
- package/package.json +1 -1
- package/skills/vantage-add-feature/SKILL.md +26 -7
- package/skills/vantage-app/SKILL.md +9 -5
- package/templates/AGENTS.md +54 -8
- package/dist/chunk-UHZ7XSAN.js.map +0 -1
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-
|
|
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([
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
968
|
-
if (model.error) console.log(` ${pc3.cyan("error".padEnd(
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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)
|