@webjsdev/cli 0.10.40 → 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.
Files changed (69) hide show
  1. package/bin/webjs.js +349 -66
  2. package/lib/create.js +282 -479
  3. package/lib/doctor.js +44 -39
  4. package/package.json +5 -1
  5. package/templates/.agents/rules/workflow.md +61 -271
  6. package/templates/.agents/skills/webjs/SKILL.md +226 -0
  7. package/templates/.agents/skills/webjs/references/auth-and-sessions.md +220 -0
  8. package/templates/.agents/skills/webjs/references/built-ins.md +200 -0
  9. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +204 -0
  10. package/templates/.agents/skills/webjs/references/components.md +167 -0
  11. package/templates/.agents/skills/webjs/references/data-and-actions.md +187 -0
  12. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +170 -0
  13. package/templates/.agents/skills/webjs/references/optimistic-ui.md +128 -0
  14. package/templates/.agents/skills/webjs/references/routing-and-pages.md +158 -0
  15. package/templates/.agents/skills/webjs/references/runtime.md +80 -0
  16. package/templates/.agents/skills/webjs/references/service-worker.md +78 -0
  17. package/templates/.agents/skills/webjs/references/styling.md +123 -0
  18. package/templates/.agents/skills/webjs/references/testing.md +125 -0
  19. package/templates/.agents/skills/webjs/references/typescript.md +148 -0
  20. package/templates/.claude/hooks/check-server-imports.mjs +1 -1
  21. package/templates/.claude/hooks/require-tests-with-src.sh +1 -1
  22. package/templates/.claude/settings.json +0 -14
  23. package/templates/.cursorrules +21 -189
  24. package/templates/.github/copilot-instructions.md +7 -185
  25. package/templates/.github/pull_request_template.md +1 -1
  26. package/templates/AGENTS.md +59 -1494
  27. package/templates/CLAUDE.md +0 -1
  28. package/templates/CONVENTIONS.md +32 -1383
  29. package/templates/GEMINI.md +11 -0
  30. package/templates/gallery/app/apple-icon.ts +0 -1
  31. package/templates/gallery/app/examples/todo/page.ts +0 -1
  32. package/templates/gallery/app/features/async-render/page.ts +0 -1
  33. package/templates/gallery/app/features/boundaries/page.ts +0 -1
  34. package/templates/gallery/app/features/broadcast/page.ts +0 -1
  35. package/templates/gallery/app/features/caching/page.ts +0 -1
  36. package/templates/gallery/app/features/client-router/page.ts +0 -1
  37. package/templates/gallery/app/features/client-router/second/page.ts +0 -1
  38. package/templates/gallery/app/features/components/page.ts +0 -1
  39. package/templates/gallery/app/features/directives/page.ts +0 -1
  40. package/templates/gallery/app/features/env/page.ts +0 -1
  41. package/templates/gallery/app/features/file-storage/page.ts +0 -1
  42. package/templates/gallery/app/features/forms/page.ts +0 -1
  43. package/templates/gallery/app/features/metadata/page.ts +0 -1
  44. package/templates/gallery/app/features/optimistic-ui/page.ts +0 -1
  45. package/templates/gallery/app/features/rate-limit/page.ts +0 -1
  46. package/templates/gallery/app/features/route-handler/page.ts +0 -1
  47. package/templates/gallery/app/features/routing/page.ts +0 -1
  48. package/templates/gallery/app/features/server-actions/page.ts +0 -1
  49. package/templates/gallery/app/features/service-worker/page.ts +0 -1
  50. package/templates/gallery/app/features/sessions/page.ts +0 -1
  51. package/templates/gallery/app/features/websockets/page.ts +0 -1
  52. package/templates/gallery/app/global-error.ts +0 -1
  53. package/templates/gallery/app/global-not-found.ts +0 -1
  54. package/templates/gallery/app/icon.ts +0 -1
  55. package/templates/gallery/app/manifest.ts +0 -1
  56. package/templates/gallery/app/opengraph-image.ts +0 -1
  57. package/templates/gallery/app/robots.ts +0 -1
  58. package/templates/gallery/app/sitemap.ts +0 -1
  59. package/templates/gallery/app/twitter-image.ts +0 -1
  60. package/templates/public/favicon.svg +5 -0
  61. package/templates/public/sw.js +1 -1
  62. package/templates/scripts/clear-gallery.mjs +95 -0
  63. package/lib/clear-placeholders.js +0 -98
  64. package/lib/design-bar.js +0 -67
  65. package/templates/.claude/hooks/design-review-before-stop.sh +0 -36
  66. package/templates/.claude/hooks/route-skills.sh +0 -35
  67. package/templates/.claude/skills/webjs-design-review/SKILL.md +0 -84
  68. package/templates/LAYOUT-REFERENCE.md +0 -96
  69. package/templates/lib/utils/ui.ts +0 -83
package/bin/webjs.js CHANGED
@@ -1,10 +1,10 @@
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';
7
- import { DESIGN_REMINDER, hasUiLayout } from '../lib/design-bar.js';
8
8
  import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
9
9
  import { loadAppEnv, resolvePort } from '../lib/port.js';
10
10
  import { planDevSupervisor } from '../lib/dev-supervisor.js';
@@ -12,6 +12,11 @@ import { planDevSupervisor } from '../lib/dev-supervisor.js';
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  const [cmd, ...rest] = process.argv.slice(2);
14
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
+
15
20
  // Node-version preflight (issue #238), INLINE and dependency-free.
16
21
  // This MUST run before any `import @webjsdev/server`: importing the server
17
22
  // package links `src/dev.js`, which references Node 24+ builtins, so on an old
@@ -20,8 +25,9 @@ const [cmd, ...rest] = process.argv.slice(2);
20
25
  // `../lib/node-preflight.js`, which imports nothing), depending only on
21
26
  // `process.versions.node`. The richer `assertNodeVersion` import inside main()
22
27
  // stays as belt-and-suspenders for the link-ok (>= 22.13) cases.
23
- // `help` / no-arg is exempt so a user on an old Node can still read usage.
24
- if (cmd !== 'help' && cmd !== undefined) {
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) {
25
31
  let engines = '>=24.0.0';
26
32
  try {
27
33
  const { readFileSync } = await import('node:fs');
@@ -47,9 +53,11 @@ const USAGE = `webjs commands:
47
53
  (--no-hot: run in-process, no hot-reload supervisor)
48
54
  webjs start [--port 8080] Start production server (serves source directly, no build step)
49
55
  webjs test [--server|--browser] Run server + browser tests
50
- webjs check [--json] [--clear-placeholders] Run correctness checks (--json emits structured violations; --clear-placeholders strips scaffold markers)
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
51
58
  webjs mcp Start the read-only MCP server (routes / actions / components / check)
52
- webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision)
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
53
61
  webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
54
62
  webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
55
63
  webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
@@ -71,7 +79,189 @@ const USAGE = `webjs commands:
71
79
  --download: also downloads bundles for offline production
72
80
  webjs vendor unpin <pkg> Remove a specific package from the pin file
73
81
  webjs vendor list Show pinned packages with versions and URLs
74
- webjs help Show this help`;
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
+ }
75
265
 
76
266
  /** @param {string[]} args */
77
267
  function flag(args, name, def) {
@@ -117,12 +307,34 @@ async function startDevParallelTasks(commands, cwd) {
117
307
  }
118
308
 
119
309
  async function main() {
120
- // Preflight: webjs needs Node 24+ (built-in TS strip + recursive fs.watch).
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).
121
333
  // Run before any subcommand so an older Node fails fast with a clear,
122
334
  // actionable message naming the found + required version, exiting non-zero
123
- // instead of crashing cryptically later. `help` is exempt so a user on an
124
- // old Node can still read usage.
125
- 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) {
126
338
  const { assertNodeVersion } = await import('@webjsdev/server');
127
339
  assertNodeVersion({ onFail: 'exit' });
128
340
  }
@@ -412,37 +624,12 @@ async function main() {
412
624
  case 'check': {
413
625
  const { checkConventions, RULES } = await import('@webjsdev/server/check');
414
626
 
415
- // --clear-placeholders: acknowledge the whole scaffold gallery in one
416
- // command (strip the marker comment lines, keep the demo code), instead of
417
- // one hand-edit per file. The gate then reflects only real violations.
418
- if (rest.includes('--clear-placeholders')) {
419
- const { clearPlaceholders } = await import('../lib/clear-placeholders.js');
420
- const report = clearPlaceholders(process.cwd());
421
- const total = report.reduce((n, r) => n + r.markers, 0);
422
- if (report.length === 0) {
423
- console.log('webjs check: no scaffold-placeholder markers found (nothing to clear).');
424
- } else {
425
- console.log(`webjs check: cleared ${total} scaffold-placeholder marker(s) across ${report.length} file(s):`);
426
- for (const r of report) console.log(` ${r.file}`);
427
- console.log('\nThe demo code is kept. Delete any gallery route/module you do not want, then re-run `webjs check`.');
428
- // Re-surface the design bar the cleared markers carried. The
429
- // layout/home marker was the just-in-time "adapt this chrome" reminder;
430
- // stripping it silently is how an app ends up shipping the scaffold
431
- // shell. Print the bar so clearing the markers cannot quietly drop it,
432
- // but only for a UI app (the api template has no layout / no chrome).
433
- if (hasUiLayout(process.cwd())) console.log(DESIGN_REMINDER);
434
- }
435
- break;
436
- }
437
-
438
627
  if (rest.includes('--rules')) {
439
628
  console.log('webjs check, correctness rules:');
440
629
  console.log(' Every rule catches code that is wrong to ship: a crash, a');
441
- console.log(' security leak, a build/type-strip failure, or (the one');
442
- console.log(' sentinel-based rule, no-scaffold-placeholder) unreplaced');
443
- console.log(' scaffold example content. They always run. Project');
444
- console.log(' conventions (layout, style, process) are guidance in');
445
- console.log(' CONVENTIONS.md, not rules here.\n');
630
+ console.log(' security leak, or a build/type-strip failure. They always');
631
+ console.log(' run. Project conventions (layout, style, process) are');
632
+ console.log(' guidance in CONVENTIONS.md, not rules here.\n');
446
633
  for (const r of RULES) {
447
634
  console.log(` ${r.name.padEnd(30)} ${r.description}`);
448
635
  }
@@ -469,28 +656,12 @@ async function main() {
469
656
  console.log('webjs check: all checks pass ✓');
470
657
  } else {
471
658
  console.log(`webjs check: ${violations.length} violation(s) found\n`);
472
- // A fresh scaffold trips no-scaffold-placeholder on every unadapted demo
473
- // file at once. Printing an identical block per file drowns the real
474
- // feature violations, so collapse the sentinel to ONE grouped summary
475
- // (with a one-command clear) and print every other rule per-violation.
476
- // The returned violations and --json are unchanged; only this human
477
- // printout groups, so agents/tools still see one entry per file.
478
- const PLACEHOLDER = 'no-scaffold-placeholder';
479
- const placeholders = violations.filter((v) => v.rule === PLACEHOLDER);
480
659
  for (const v of violations) {
481
- if (v.rule === PLACEHOLDER) continue;
482
660
  console.log(` ✗ [${v.rule}] ${v.file}`);
483
661
  console.log(` ${v.message}`);
484
662
  if (v.fix) console.log(` Fix: ${v.fix}`);
485
663
  console.log();
486
664
  }
487
- if (placeholders.length > 0) {
488
- console.log(` ✗ [${PLACEHOLDER}] ${placeholders.length} file(s) still carry scaffold example content:`);
489
- for (const v of placeholders) console.log(` ${v.file}`);
490
- console.log(' Fix: adapt or delete each file, then remove its marker comment line.');
491
- console.log(' Keeping the gallery? Run `webjs check --clear-placeholders` to clear all markers at once.');
492
- console.log();
493
- }
494
665
  process.exit(1);
495
666
  }
496
667
  break;
@@ -503,14 +674,6 @@ async function main() {
503
674
  // app's concern, not a broken toolchain).
504
675
  const { runDoctorChecks } = await import('../lib/doctor.js');
505
676
  const results = await runDoctorChecks(process.cwd());
506
- const marker = { pass: '[pass]', warn: '[warn]', fail: '[fail]' };
507
- console.log('webjs doctor: project-health checklist\n');
508
- for (const r of results) {
509
- console.log(` ${marker[r.status]} ${r.name}`);
510
- console.log(` ${r.message}`);
511
- if (r.fix && r.status !== 'pass') console.log(` Fix: ${r.fix}`);
512
- console.log();
513
- }
514
677
  const counts = results.reduce((acc, r) => {
515
678
  acc[r.status] = (acc[r.status] || 0) + 1;
516
679
  return acc;
@@ -518,15 +681,121 @@ async function main() {
518
681
  const pass = counts.pass || 0;
519
682
  const warn = counts.warn || 0;
520
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
+ }
521
712
  console.log(` ${pass} passed, ${warn} warning(s), ${fail} failed.`);
522
- if (fail > 0) {
523
- console.error(
524
- `\nwebjs doctor: ${fail} hard check(s) failed. Fix the toolchain issue(s) above.`,
525
- );
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}`);
526
718
  process.exit(1);
527
719
  }
528
720
  break;
529
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
+ }
530
799
  case 'types': {
531
800
  // Generate `.webjs/routes.d.ts` from the app's `app/` routes (#258),
532
801
  // narrowing the @webjsdev/core `Route` href union + per-route `params`.
@@ -941,7 +1210,21 @@ Full docs: https://docs.webjs.dev`);
941
1210
  });
942
1211
  break;
943
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;
944
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;
945
1228
  case undefined:
946
1229
  console.log(USAGE);
947
1230
  break;