@webjsdev/cli 0.10.41 → 0.10.42
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/bin/webjs.js +345 -20
- package/lib/doctor.js +43 -1
- package/package.json +1 -1
package/bin/webjs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve, join, dirname } from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { resolveBin } from '../lib/resolve-bin.js';
|
|
6
7
|
import { dbGenerateTtyHint } from '../lib/db-hints.js';
|
|
@@ -11,6 +12,11 @@ import { planDevSupervisor } from '../lib/dev-supervisor.js';
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
13
14
|
|
|
15
|
+
// A `--help`/`-h` or `--version`/`-v` request (top-level or after a subcommand)
|
|
16
|
+
// must not be gated by the Node-version preflight, so it works on an old Node.
|
|
17
|
+
const wantsHelp = cmd === '--help' || cmd === '-h' || rest.includes('--help') || rest.includes('-h');
|
|
18
|
+
const wantsVersion = cmd === '--version' || cmd === '-v' || cmd === 'version';
|
|
19
|
+
|
|
14
20
|
// Node-version preflight (issue #238), INLINE and dependency-free.
|
|
15
21
|
// This MUST run before any `import @webjsdev/server`: importing the server
|
|
16
22
|
// package links `src/dev.js`, which references Node 24+ builtins, so on an old
|
|
@@ -19,8 +25,9 @@ const [cmd, ...rest] = process.argv.slice(2);
|
|
|
19
25
|
// `../lib/node-preflight.js`, which imports nothing), depending only on
|
|
20
26
|
// `process.versions.node`. The richer `assertNodeVersion` import inside main()
|
|
21
27
|
// stays as belt-and-suspenders for the link-ok (>= 22.13) cases.
|
|
22
|
-
// `help` / no-arg
|
|
23
|
-
|
|
28
|
+
// `help` / no-arg / any `--help`|`-h` / `version`|`--version`|`-v` request is
|
|
29
|
+
// exempt so a user on an old Node can still read usage and the version.
|
|
30
|
+
if (cmd !== 'help' && cmd !== undefined && !wantsHelp && !wantsVersion) {
|
|
24
31
|
let engines = '>=24.0.0';
|
|
25
32
|
try {
|
|
26
33
|
const { readFileSync } = await import('node:fs');
|
|
@@ -47,8 +54,10 @@ const USAGE = `webjs commands:
|
|
|
47
54
|
webjs start [--port 8080] Start production server (serves source directly, no build step)
|
|
48
55
|
webjs test [--server|--browser] Run server + browser tests
|
|
49
56
|
webjs check [--json] Run correctness checks (--json emits structured violations)
|
|
57
|
+
webjs routes [--json|--table] [--no-headers] Print the route table (path / owner file / methods). Default tree; --json matches the MCP list_routes shape; --no-headers drops the --table header
|
|
50
58
|
webjs mcp Start the read-only MCP server (routes / actions / components / check)
|
|
51
|
-
webjs doctor
|
|
59
|
+
webjs doctor [--json] [--strict] Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision).
|
|
60
|
+
--json emits the structured results (with stable codes). --strict also fails the exit on warnings
|
|
52
61
|
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
53
62
|
webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
|
|
54
63
|
webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
|
|
@@ -70,7 +79,189 @@ const USAGE = `webjs commands:
|
|
|
70
79
|
--download: also downloads bundles for offline production
|
|
71
80
|
webjs vendor unpin <pkg> Remove a specific package from the pin file
|
|
72
81
|
webjs vendor list Show pinned packages with versions and URLs
|
|
73
|
-
webjs
|
|
82
|
+
webjs version Print the installed @webjsdev/cli version (also: webjs --version / -v)
|
|
83
|
+
webjs help [command] Show this help, or per-command usage + examples (e.g. webjs help routes).
|
|
84
|
+
The flag forms work too: webjs --help / -h for this banner, webjs <command> --help / -h for one command`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Per-command help: usage line, one-line summary, an Options table, and an
|
|
88
|
+
* Examples block (#975). `webjs help <cmd>` (and `webjs <cmd> --help`) renders
|
|
89
|
+
* this so an agent sees the exact flags + worked examples instead of guessing
|
|
90
|
+
* from the one-line USAGE row. Keyed by the top-level command; `db` / `ui` /
|
|
91
|
+
* `vendor` document their subcommand shape. Every entry that takes flags lists
|
|
92
|
+
* them in `options` so the flag surface is machine-readable, matching the Remix
|
|
93
|
+
* CLI's per-command Options section.
|
|
94
|
+
* @type {Record<string, { usage: string, summary: string, options?: Array<{ flag: string, description: string }>, examples: string[] }>}
|
|
95
|
+
*/
|
|
96
|
+
const HELP = {
|
|
97
|
+
dev: {
|
|
98
|
+
usage: 'webjs dev [--port <n>] [--no-hot]',
|
|
99
|
+
summary: 'Start the dev server with live reload (source is the runtime, no build step).',
|
|
100
|
+
options: [
|
|
101
|
+
{ flag: '--port <n>', description: 'Port to listen on (else PORT, else 8080).' },
|
|
102
|
+
{ flag: '--no-hot', description: 'Run in-process, without the hot-reload supervisor.' },
|
|
103
|
+
],
|
|
104
|
+
examples: ['webjs dev', 'webjs dev --port 3000', 'webjs dev --no-hot'],
|
|
105
|
+
},
|
|
106
|
+
start: {
|
|
107
|
+
usage: 'webjs start [--port <n>]',
|
|
108
|
+
summary: 'Start the production server (serves source directly, plain HTTP/1.1).',
|
|
109
|
+
options: [
|
|
110
|
+
{ flag: '--port <n>', description: 'Port to listen on (else PORT, else 8080).' },
|
|
111
|
+
],
|
|
112
|
+
examples: ['webjs start', 'webjs start --port 8080', 'PORT=8080 webjs start'],
|
|
113
|
+
},
|
|
114
|
+
test: {
|
|
115
|
+
usage: 'webjs test [--server] [--browser] [--watch]',
|
|
116
|
+
summary: 'Run the app test suites (server-side node:test and/or browser via web-test-runner).',
|
|
117
|
+
options: [
|
|
118
|
+
{ flag: '--server', description: 'Run only the server-side tests.' },
|
|
119
|
+
{ flag: '--browser', description: 'Run only the browser tests.' },
|
|
120
|
+
{ flag: '--watch', description: 'Re-run on change.' },
|
|
121
|
+
],
|
|
122
|
+
examples: ['webjs test', 'webjs test --server', 'webjs test --browser --watch'],
|
|
123
|
+
},
|
|
124
|
+
check: {
|
|
125
|
+
usage: 'webjs check [--rules] [--json]',
|
|
126
|
+
summary: 'Run the correctness checks (report-only, no autofix). Exits non-zero on any violation.',
|
|
127
|
+
options: [
|
|
128
|
+
{ flag: '--json', description: 'Emit the structured violations + summary as JSON (agent-friendly).' },
|
|
129
|
+
{ flag: '--rules', description: 'List the correctness rules instead of running them.' },
|
|
130
|
+
],
|
|
131
|
+
examples: ['webjs check', 'webjs check --json', 'webjs check --rules'],
|
|
132
|
+
},
|
|
133
|
+
routes: {
|
|
134
|
+
usage: 'webjs routes [--json | --table] [--no-headers]',
|
|
135
|
+
summary: 'Print the route table: each page/route path, its owner file, and (for route handlers) its HTTP methods.',
|
|
136
|
+
options: [
|
|
137
|
+
{ flag: '--json', description: 'Emit { pages, apis } as JSON (byte-identical to the MCP list_routes tool).' },
|
|
138
|
+
{ flag: '--table', description: 'Flat aligned KIND / PATH / METHODS / FILE columns.' },
|
|
139
|
+
{ flag: '--no-headers', description: 'Omit the header row (only with --table); easier to pipe.' },
|
|
140
|
+
],
|
|
141
|
+
examples: ['webjs routes', 'webjs routes --table', 'webjs routes --table --no-headers', 'webjs routes --json'],
|
|
142
|
+
},
|
|
143
|
+
doctor: {
|
|
144
|
+
usage: 'webjs doctor [--json] [--strict]',
|
|
145
|
+
summary: 'Verify project health. Each result carries a stable code so an agent branches on the failure kind.',
|
|
146
|
+
options: [
|
|
147
|
+
{ flag: '--json', description: 'Emit the DoctorResult[] (with stable codes) + a summary as JSON.' },
|
|
148
|
+
{ flag: '--strict', description: 'Also fail the exit on warnings, not just hard failures.' },
|
|
149
|
+
],
|
|
150
|
+
examples: ['webjs doctor', 'webjs doctor --json', 'webjs doctor --strict', 'webjs doctor --json --strict'],
|
|
151
|
+
},
|
|
152
|
+
types: {
|
|
153
|
+
usage: 'webjs types',
|
|
154
|
+
summary: 'Generate .webjs/routes.d.ts (the typed Route href union + per-route params).',
|
|
155
|
+
examples: ['webjs types'],
|
|
156
|
+
},
|
|
157
|
+
typecheck: {
|
|
158
|
+
usage: 'webjs typecheck [tsc args...]',
|
|
159
|
+
summary: "Type-check the app with the project's own tsc --noEmit. Extra args pass through to tsc.",
|
|
160
|
+
examples: ['webjs typecheck', 'webjs typecheck --watch'],
|
|
161
|
+
},
|
|
162
|
+
create: {
|
|
163
|
+
usage: 'webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install]',
|
|
164
|
+
summary: 'Scaffold a new app. Defaults: full-stack template, Drizzle + SQLite, Node runtime.',
|
|
165
|
+
options: [
|
|
166
|
+
{ flag: '--template <t>', description: 'full-stack (default), api, or saas.' },
|
|
167
|
+
{ flag: '--db <d>', description: 'sqlite (default) or postgres.' },
|
|
168
|
+
{ flag: '--runtime <r>', description: 'node (default) or bun.' },
|
|
169
|
+
{ flag: '--no-install', description: 'Skip the package-manager install step.' },
|
|
170
|
+
],
|
|
171
|
+
examples: [
|
|
172
|
+
'webjs create my-app',
|
|
173
|
+
'webjs create my-api --template api',
|
|
174
|
+
'webjs create my-saas --template saas --db postgres',
|
|
175
|
+
'webjs create my-app --runtime bun',
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
db: {
|
|
179
|
+
usage: 'webjs db <generate|migrate|push|studio|seed>',
|
|
180
|
+
summary: 'Database tasks (wraps drizzle-kit); seed runs db/seed.server.ts.',
|
|
181
|
+
examples: ['webjs db generate', 'webjs db migrate', 'webjs db studio', 'webjs db seed'],
|
|
182
|
+
},
|
|
183
|
+
ui: {
|
|
184
|
+
usage: 'webjs ui <init|add|list|view|diff|info> [names...]',
|
|
185
|
+
summary: 'AI-first component library CLI. Requires @webjsdev/ui installed in the project.',
|
|
186
|
+
examples: ['webjs ui init', 'webjs ui add button card', 'webjs ui list'],
|
|
187
|
+
},
|
|
188
|
+
vendor: {
|
|
189
|
+
usage: 'webjs vendor <pin|unpin|list|audit|outdated|update> [--from <provider>] [--download]',
|
|
190
|
+
summary: 'Pin client-side npm packages into .webjs/vendor/importmap.json.',
|
|
191
|
+
options: [
|
|
192
|
+
{ flag: '--from <provider>', description: 'jspm (default), jsdelivr, unpkg, or skypack.' },
|
|
193
|
+
{ flag: '--download', description: 'Also download the bundles for offline production (with pin).' },
|
|
194
|
+
],
|
|
195
|
+
examples: ['webjs vendor pin', 'webjs vendor pin --download', 'webjs vendor list', 'webjs vendor outdated'],
|
|
196
|
+
},
|
|
197
|
+
mcp: {
|
|
198
|
+
usage: 'webjs mcp',
|
|
199
|
+
summary: 'Start the read-only MCP server (routes / actions / components / check + a docs/source knowledge layer).',
|
|
200
|
+
examples: ['webjs mcp'],
|
|
201
|
+
},
|
|
202
|
+
version: {
|
|
203
|
+
usage: 'webjs version',
|
|
204
|
+
summary: 'Print the installed @webjsdev/cli version. Also available as webjs --version / -v.',
|
|
205
|
+
examples: ['webjs version', 'webjs --version'],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Render `webjs help <cmd>` to stdout: usage, summary, an Options table (when
|
|
211
|
+
* the command takes flags), and Examples, in the Remix-CLI section shape. A
|
|
212
|
+
* universal `-h, --help` option row is appended so every command advertises it.
|
|
213
|
+
* Returns true if the command was found, false otherwise (so the caller can
|
|
214
|
+
* exit non-zero on an unknown help topic, matching the Remix CLI).
|
|
215
|
+
* @param {string} name
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
function printCommandHelp(name) {
|
|
219
|
+
const h = HELP[name];
|
|
220
|
+
if (!h) {
|
|
221
|
+
console.error(`Unknown help topic "${name}".\n`);
|
|
222
|
+
console.error(USAGE);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Usage: ${h.usage}\n`);
|
|
226
|
+
console.log(` ${h.summary}\n`);
|
|
227
|
+
// A command that forwards to an external tool does NOT show webjs's own help
|
|
228
|
+
// for `--help`; word its `-h, --help` row to say so, rather than the generic
|
|
229
|
+
// "Show this help." which would be false for those three commands.
|
|
230
|
+
const tool = HELP_FLAG_PASSTHROUGH_TOOL[name];
|
|
231
|
+
const helpRow = tool
|
|
232
|
+
? { flag: '-h, --help', description: `Forwarded to ${tool} (this command wraps it).` }
|
|
233
|
+
: { flag: '-h, --help', description: 'Show this help.' };
|
|
234
|
+
const options = [...(h.options || []), helpRow];
|
|
235
|
+
const width = Math.max(...options.map((o) => o.flag.length));
|
|
236
|
+
console.log('Options:');
|
|
237
|
+
for (const o of options) console.log(` ${o.flag.padEnd(width)} ${o.description}`);
|
|
238
|
+
console.log('\nExamples:');
|
|
239
|
+
for (const ex of h.examples) console.log(` ${ex}`);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Commands that forward their remaining args to an external CLI, so a trailing
|
|
245
|
+
* `--help` / `-h` should reach THAT tool's own help, not webjs's. Maps the
|
|
246
|
+
* command to the tool it wraps (used both to skip the help-flag intercept and
|
|
247
|
+
* to word the `-h, --help` row in that command's help accurately).
|
|
248
|
+
* @type {Record<string, string>}
|
|
249
|
+
*/
|
|
250
|
+
const HELP_FLAG_PASSTHROUGH_TOOL = { typecheck: 'tsc', db: 'drizzle-kit', ui: '@webjsdev/ui' };
|
|
251
|
+
const HELP_FLAG_PASSTHROUGH = new Set(Object.keys(HELP_FLAG_PASSTHROUGH_TOOL));
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The installed `@webjsdev/cli` version, read from this package's own
|
|
255
|
+
* package.json. Falls back to `0.0.0` if unreadable (never throws).
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
function readCliVersion() {
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version || '0.0.0';
|
|
261
|
+
} catch {
|
|
262
|
+
return '0.0.0';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
74
265
|
|
|
75
266
|
/** @param {string[]} args */
|
|
76
267
|
function flag(args, name, def) {
|
|
@@ -116,12 +307,34 @@ async function startDevParallelTasks(commands, cwd) {
|
|
|
116
307
|
}
|
|
117
308
|
|
|
118
309
|
async function main() {
|
|
119
|
-
//
|
|
310
|
+
// `--version` / `-v` (top level): print the installed CLI version and exit.
|
|
311
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
312
|
+
console.log(readCliVersion());
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// `--help` / `-h` (#975): a top-level flag (webjs --help / -h) prints the
|
|
316
|
+
// banner; the same flag AFTER a subcommand (webjs routes --help) prints that
|
|
317
|
+
// command's help and short-circuits it. Handled before the Node preflight so
|
|
318
|
+
// it works even on an old Node. A command that forwards its args to an
|
|
319
|
+
// external CLI (typecheck/db/ui, in HELP_FLAG_PASSTHROUGH) is skipped so the
|
|
320
|
+
// tool's own --help still reaches it; an UNRECOGNISED command is not
|
|
321
|
+
// intercepted either, so it falls through to the Unknown-command error
|
|
322
|
+
// (exit 1) rather than a silent 0.
|
|
323
|
+
if (cmd === '--help' || cmd === '-h') {
|
|
324
|
+
console.log(USAGE);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (cmd && HELP[cmd] && !HELP_FLAG_PASSTHROUGH.has(cmd) && (rest.includes('--help') || rest.includes('-h'))) {
|
|
328
|
+
printCommandHelp(cmd);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Node preflight: WebJs needs Node 24+ (built-in TS strip + recursive fs.watch).
|
|
120
333
|
// Run before any subcommand so an older Node fails fast with a clear,
|
|
121
334
|
// actionable message naming the found + required version, exiting non-zero
|
|
122
|
-
// instead of crashing cryptically later. `help`
|
|
123
|
-
// old Node can still read usage.
|
|
124
|
-
if (cmd !== 'help' && cmd !== undefined) {
|
|
335
|
+
// instead of crashing cryptically later. `help` / a help or version request
|
|
336
|
+
// is exempt so a user on an old Node can still read usage and the version.
|
|
337
|
+
if (cmd !== 'help' && cmd !== undefined && !wantsHelp && !wantsVersion) {
|
|
125
338
|
const { assertNodeVersion } = await import('@webjsdev/server');
|
|
126
339
|
assertNodeVersion({ onFail: 'exit' });
|
|
127
340
|
}
|
|
@@ -461,14 +674,6 @@ async function main() {
|
|
|
461
674
|
// app's concern, not a broken toolchain).
|
|
462
675
|
const { runDoctorChecks } = await import('../lib/doctor.js');
|
|
463
676
|
const results = await runDoctorChecks(process.cwd());
|
|
464
|
-
const marker = { pass: '[pass]', warn: '[warn]', fail: '[fail]' };
|
|
465
|
-
console.log('webjs doctor: project-health checklist\n');
|
|
466
|
-
for (const r of results) {
|
|
467
|
-
console.log(` ${marker[r.status]} ${r.name}`);
|
|
468
|
-
console.log(` ${r.message}`);
|
|
469
|
-
if (r.fix && r.status !== 'pass') console.log(` Fix: ${r.fix}`);
|
|
470
|
-
console.log();
|
|
471
|
-
}
|
|
472
677
|
const counts = results.reduce((acc, r) => {
|
|
473
678
|
acc[r.status] = (acc[r.status] || 0) + 1;
|
|
474
679
|
return acc;
|
|
@@ -476,15 +681,121 @@ async function main() {
|
|
|
476
681
|
const pass = counts.pass || 0;
|
|
477
682
|
const warn = counts.warn || 0;
|
|
478
683
|
const fail = counts.fail || 0;
|
|
684
|
+
// `--strict` also fails the exit on warnings, so an agent can gate on a
|
|
685
|
+
// fully-clean toolchain (drift / staleness / pin freshness) in a fix loop,
|
|
686
|
+
// not just on a hard toolchain break. Default keeps warnings non-fatal.
|
|
687
|
+
const strict = rest.includes('--strict');
|
|
688
|
+
const failing = fail > 0 || (strict && warn > 0);
|
|
689
|
+
|
|
690
|
+
// --json emits the raw DoctorResult[] (each carries a stable `code`) plus
|
|
691
|
+
// a summary, so an agent consumes structured data instead of scraping the
|
|
692
|
+
// text. Shape mirrors `check --json`: a top-level array-bearing object
|
|
693
|
+
// with a `summary` count. The non-zero exit is preserved (an agent gates
|
|
694
|
+
// on the exit code AND parses the report).
|
|
695
|
+
if (rest.includes('--json')) {
|
|
696
|
+
console.log(JSON.stringify({
|
|
697
|
+
results,
|
|
698
|
+
summary: { pass, warn, fail, strict, ok: !failing },
|
|
699
|
+
}));
|
|
700
|
+
if (failing) process.exit(1);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const marker = { pass: '[pass]', warn: '[warn]', fail: '[fail]' };
|
|
705
|
+
console.log('webjs doctor: project-health checklist\n');
|
|
706
|
+
for (const r of results) {
|
|
707
|
+
console.log(` ${marker[r.status]} ${r.name} (${r.code})`);
|
|
708
|
+
console.log(` ${r.message}`);
|
|
709
|
+
if (r.fix && r.status !== 'pass') console.log(` Fix: ${r.fix}`);
|
|
710
|
+
console.log();
|
|
711
|
+
}
|
|
479
712
|
console.log(` ${pass} passed, ${warn} warning(s), ${fail} failed.`);
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
713
|
+
if (failing) {
|
|
714
|
+
const reason = fail > 0
|
|
715
|
+
? `${fail} hard check(s) failed. Fix the toolchain issue(s) above.`
|
|
716
|
+
: `${warn} warning(s) found and --strict was set.`;
|
|
717
|
+
console.error(`\nwebjs doctor: ${reason}`);
|
|
484
718
|
process.exit(1);
|
|
485
719
|
}
|
|
486
720
|
break;
|
|
487
721
|
}
|
|
722
|
+
case 'routes': {
|
|
723
|
+
// Print the app route table to stdout (#975): every page (path, owner
|
|
724
|
+
// file, dynamic params) and every route.{js,ts} API handler (path, owner
|
|
725
|
+
// file, HTTP methods). Reuses the ONE route walker (`buildRouteTable`, the
|
|
726
|
+
// same walk that backs `webjs types` and the dev server) and the shared
|
|
727
|
+
// `projectRoutes` projector, so `--json` is byte-identical to the MCP
|
|
728
|
+
// `list_routes` tool. Read-only: no module load, no autofix.
|
|
729
|
+
const { buildRouteTable } = await import('@webjsdev/server');
|
|
730
|
+
const { projectRoutes } = await import('@webjsdev/mcp/routes-report');
|
|
731
|
+
const { extractRouteMethods } = await import('@webjsdev/mcp');
|
|
732
|
+
const { readFile } = await import('node:fs/promises');
|
|
733
|
+
const appDir = process.cwd();
|
|
734
|
+
const table = await buildRouteTable(appDir);
|
|
735
|
+
const report = await projectRoutes(table, { appDir, readFile, extractRouteMethods });
|
|
736
|
+
|
|
737
|
+
// --json: the machine contract, identical to the MCP `list_routes` shape.
|
|
738
|
+
if (rest.includes('--json')) {
|
|
739
|
+
console.log(JSON.stringify(report));
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const { pages, apis } = report;
|
|
744
|
+
// A page is reached via GET (its server render); a route.{js,ts} exposes
|
|
745
|
+
// exactly its exported verbs. Present both as one path -> methods -> file
|
|
746
|
+
// view. Dynamic pages append their param names.
|
|
747
|
+
const pageMethods = 'GET';
|
|
748
|
+
|
|
749
|
+
// --table: flat, aligned columns (KIND / PATH / METHODS / FILE), the
|
|
750
|
+
// easiest shape for an agent to scan or a human to grep. `--no-headers`
|
|
751
|
+
// drops the header row so the output pipes cleanly into awk/cut.
|
|
752
|
+
if (rest.includes('--table')) {
|
|
753
|
+
/** @type {Array<[string,string,string,string]>} */
|
|
754
|
+
const rows = [];
|
|
755
|
+
if (!rest.includes('--no-headers')) rows.push(['KIND', 'PATH', 'METHODS', 'FILE']);
|
|
756
|
+
for (const p of pages) {
|
|
757
|
+
rows.push(['page', p.path + (p.params ? ` [${p.params.join(', ')}]` : ''), pageMethods, p.file]);
|
|
758
|
+
}
|
|
759
|
+
for (const a of apis) {
|
|
760
|
+
rows.push(['api', a.path, a.methods.join(', ') || '(none)', a.file]);
|
|
761
|
+
}
|
|
762
|
+
// With --no-headers and no routes there are no rows; nothing to print.
|
|
763
|
+
if (rows.length === 0) break;
|
|
764
|
+
const widths = [0, 1, 2].map((c) => Math.max(...rows.map((r) => r[c].length)));
|
|
765
|
+
for (const r of rows) {
|
|
766
|
+
console.log(
|
|
767
|
+
`${r[0].padEnd(widths[0])} ${r[1].padEnd(widths[1])} ${r[2].padEnd(widths[2])} ${r[3]}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Default: a grouped tree.
|
|
774
|
+
console.log(`webjs routes: ${pages.length} page(s), ${apis.length} API route(s)\n`);
|
|
775
|
+
if (pages.length) {
|
|
776
|
+
console.log('Pages');
|
|
777
|
+
const pathW = Math.max(...pages.map((p) => p.path.length));
|
|
778
|
+
for (const p of pages) {
|
|
779
|
+
const params = p.params ? ` ${p.params.map((n) => `[${n}]`).join(' ')}` : '';
|
|
780
|
+
console.log(` ${p.path.padEnd(pathW)} ${p.file}${params}`);
|
|
781
|
+
}
|
|
782
|
+
console.log();
|
|
783
|
+
}
|
|
784
|
+
if (apis.length) {
|
|
785
|
+
console.log('API routes');
|
|
786
|
+
const pathW = Math.max(...apis.map((a) => a.path.length));
|
|
787
|
+
const methW = Math.max(...apis.map((a) => (a.methods.join(', ') || '(none)').length));
|
|
788
|
+
for (const a of apis) {
|
|
789
|
+
const methods = a.methods.join(', ') || '(none)';
|
|
790
|
+
console.log(` ${a.path.padEnd(pathW)} ${methods.padEnd(methW)} ${a.file}`);
|
|
791
|
+
}
|
|
792
|
+
console.log();
|
|
793
|
+
}
|
|
794
|
+
if (!pages.length && !apis.length) {
|
|
795
|
+
console.log(' No routes found. Add an app/page.ts or an app/**/route.ts.');
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
488
799
|
case 'types': {
|
|
489
800
|
// Generate `.webjs/routes.d.ts` from the app's `app/` routes (#258),
|
|
490
801
|
// narrowing the @webjsdev/core `Route` href union + per-route `params`.
|
|
@@ -899,7 +1210,21 @@ Full docs: https://docs.webjs.dev`);
|
|
|
899
1210
|
});
|
|
900
1211
|
break;
|
|
901
1212
|
}
|
|
1213
|
+
case 'version':
|
|
1214
|
+
// Print the installed CLI version (#975). Also reachable as the top-level
|
|
1215
|
+
// `webjs --version` / `-v` flag, handled at the top of main().
|
|
1216
|
+
console.log(readCliVersion());
|
|
1217
|
+
break;
|
|
902
1218
|
case 'help':
|
|
1219
|
+
// `webjs help <cmd>` prints that command's usage + Options + Examples
|
|
1220
|
+
// (#975); a bare `webjs help` prints the full banner. An unknown topic
|
|
1221
|
+
// exits non-zero (printCommandHelp returns false), matching the Remix CLI.
|
|
1222
|
+
if (rest[0]) {
|
|
1223
|
+
if (!printCommandHelp(rest[0])) process.exit(1);
|
|
1224
|
+
} else {
|
|
1225
|
+
console.log(USAGE);
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
903
1228
|
case undefined:
|
|
904
1229
|
console.log(USAGE);
|
|
905
1230
|
break;
|
package/lib/doctor.js
CHANGED
|
@@ -44,9 +44,48 @@ import { checkNodeInline } from './node-preflight.js';
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* @typedef {'pass' | 'warn' | 'fail'} DoctorStatus
|
|
47
|
-
* @typedef {{ name: string, status: DoctorStatus, message: string, fix?: string }} DoctorResult
|
|
47
|
+
* @typedef {{ name: string, code: string, status: DoctorStatus, message: string, fix?: string }} DoctorResult
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Stable machine-readable code per check (#975), so an agent consuming
|
|
52
|
+
* `webjs doctor --json` branches on the failure KIND, not the human message
|
|
53
|
+
* text (which is free to change). The `name` stays the display identity (some
|
|
54
|
+
* are kebab-case, two are prose); the `code` is the durable contract, a
|
|
55
|
+
* SCREAMING_SNAKE_CASE constant that never changes for a given check. Attached
|
|
56
|
+
* centrally in `runDoctorChecks` so every check function stays focused on its
|
|
57
|
+
* own logic. Mirrors Remix's `DoctorFindingCode` enum (its `doctor/types.ts`).
|
|
58
|
+
*
|
|
59
|
+
* Keyed by each check's `name`. A missing entry falls back to a name-derived
|
|
60
|
+
* code (see `codeForName`), but every shipped check is listed here explicitly
|
|
61
|
+
* and a drift test asserts each result carries one of these codes.
|
|
62
|
+
* @type {Record<string, string>}
|
|
63
|
+
*/
|
|
64
|
+
export const DOCTOR_CODES = {
|
|
65
|
+
'node-version': 'NODE_VERSION',
|
|
66
|
+
'tsconfig-erasable': 'TSCONFIG_ERASABLE',
|
|
67
|
+
'env-drift': 'ENV_DRIFT',
|
|
68
|
+
'vendor-pin': 'VENDOR_PIN',
|
|
69
|
+
'vendor-gitignore': 'VENDOR_GITIGNORE',
|
|
70
|
+
'webjs-versions': 'WEBJS_VERSIONS',
|
|
71
|
+
'framework-resolve': 'FRAMEWORK_RESOLVE',
|
|
72
|
+
'importmap-coherence': 'IMPORTMAP_COHERENCE',
|
|
73
|
+
'git-hook': 'GIT_HOOK',
|
|
74
|
+
'Page/layout elision (carrier hygiene)': 'ELISION_CARRIERS',
|
|
75
|
+
'Static build outputs (dev.regenerate freshness)': 'STATIC_ASSET_FRESHNESS',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The stable code for a check name: the explicit `DOCTOR_CODES` entry, else a
|
|
80
|
+
* best-effort derivation (uppercased, non-alphanumerics collapsed to `_`) so a
|
|
81
|
+
* newly-added check that forgets its map entry still gets a non-empty code.
|
|
82
|
+
* @param {string} name
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function codeForName(name) {
|
|
86
|
+
return DOCTOR_CODES[name] || name.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
/**
|
|
51
90
|
* Read the CLI package's own `engines.node` so the required Node major lives in
|
|
52
91
|
* one place (mirrors how `bin/webjs.js` sources it). Falls back to `>=24.0.0`.
|
|
@@ -1020,5 +1059,8 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
1020
1059
|
checkElisionCarriers(appDir),
|
|
1021
1060
|
checkStaticAssetFreshness(appDir),
|
|
1022
1061
|
]);
|
|
1062
|
+
// Attach the stable machine code to every result (#975). Centralized here so
|
|
1063
|
+
// each check function stays free of the code-contract concern.
|
|
1064
|
+
for (const r of results) r.code = codeForName(r.name);
|
|
1023
1065
|
return results;
|
|
1024
1066
|
}
|