@webjsdev/cli 0.10.37 → 0.10.39

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/lib/doctor.js CHANGED
@@ -4,9 +4,10 @@
4
4
  * WebJs has unusually many fragile preconditions, each an independent failure
5
5
  * mode a contributor onboarding to an existing repo only hits at runtime: the
6
6
  * Node 24+ strip-types floor, the `erasableSyntaxOnly` TS flag, importmap pin
7
- * freshness, env drift vs `.env.example`, `@webjsdev/*` version coherence, and
8
- * the git pre-commit hook activation. `webjs doctor` verifies each one up front
9
- * and prints pass/warn/fail with an actionable fix line.
7
+ * freshness, env drift vs `.env.example`, `@webjsdev/*` version coherence,
8
+ * whether the framework even resolves from the app dir (the fresh-git-worktree
9
+ * trap, #954), and the git pre-commit hook activation. `webjs doctor` verifies
10
+ * each one up front and prints pass/warn/fail with an actionable fix line.
10
11
  *
11
12
  * This module is PURE: `runDoctorChecks(appDir, opts?)` reads files (and, for
12
13
  * the pin check, optionally the network), but NEVER calls `process.exit` and
@@ -25,7 +26,8 @@
25
26
  * app's own runtime concern, never a doctor hard-fail: a missing tsconfig
26
27
  * (a JS-only app legitimately has none), env drift, an outdated or
27
28
  * unverifiable vendor pin, a `@webjsdev/*` version drift or missing install,
28
- * and a missing/non-executable git hook.
29
+ * an unresolvable framework (a worktree with no node_modules, #954), and a
30
+ * missing/non-executable git hook.
29
31
  * - 'pass' is the green path.
30
32
  *
31
33
  * Every NETWORK touch (only the vendor-pin freshness check) is BEST-EFFORT: a
@@ -37,6 +39,7 @@
37
39
  import { existsSync, statSync } from 'node:fs';
38
40
  import { readFile } from 'node:fs/promises';
39
41
  import { join, relative } from 'node:path';
42
+ import { createRequire } from 'node:module';
40
43
  import { checkNodeInline } from './node-preflight.js';
41
44
 
42
45
  /**
@@ -842,6 +845,119 @@ async function checkElisionCarriers(appDir) {
842
845
  };
843
846
  }
844
847
 
848
+ /**
849
+ * ADVISORY: the delivered app still rides the scaffold shell. AGENTS.md /
850
+ * CONVENTIONS.md item 6 asks a UI app to own its design (layout, palette,
851
+ * typography, chrome); the scaffold is a teaching artifact, not a starting
852
+ * design. WARN-level and never a hard fail: a reading column or a theme toggle
853
+ * CAN be a legitimate choice, so this nudges, it does not gate. The signal is
854
+ * objective (distinctive scaffold-authored chrome strings still present in the
855
+ * root layout), not a judgment of taste. Two or more tells is the threshold.
856
+ * @param {string} appDir
857
+ * @returns {Promise<DoctorResult>}
858
+ */
859
+ async function checkScaffoldDesign(appDir) {
860
+ const name = 'App design (own design, not the scaffold shell)';
861
+ let layoutSrc = '';
862
+ for (const ext of ['ts', 'js', 'mts', 'mjs']) {
863
+ const p = join(appDir, 'app', `layout.${ext}`);
864
+ if (existsSync(p)) { layoutSrc = await readFile(p, 'utf8').catch(() => ''); break; }
865
+ }
866
+ if (!layoutSrc) {
867
+ return { name, status: 'pass', message: 'no app/layout to analyse' };
868
+ }
869
+ const { scaffoldShellTells } = await import('./design-bar.js');
870
+ const tells = scaffoldShellTells(layoutSrc);
871
+ if (tells.length < 2) {
872
+ return { name, status: 'pass', message: 'app/layout does not look like the unmodified scaffold shell' };
873
+ }
874
+ return {
875
+ name,
876
+ status: 'warn',
877
+ message:
878
+ `app/layout still carries ${tells.length} scaffold design signal(s): ${tells.join(', ')}. ` +
879
+ 'A delivered UI app should own its design (layout AND palette), not adapt the scaffold.',
880
+ fix: 'Design the app\'s own layout, palette, typography, and chrome from what the app IS (a centered board, a full-bleed dashboard, ...), not the scaffold\'s exact 760px reading column, its "Built with webjs" attribution footer, or the unmodified starter palette values (the theme-toggle and --header-h are keep-infrastructure). Recoloring the scaffold is not a redesign. Render the app and look at it. See AGENTS.md / CONVENTIONS.md item 6.',
881
+ };
882
+ }
883
+
884
+ /**
885
+ * Probe whether `@webjsdev/core` resolves from `appDir`. Node resolution is
886
+ * directory-relative, so this must probe FROM the app (not the CLI's own
887
+ * location, which resolves the framework fine from a global install even when
888
+ * the app cannot). A no-op-cheap resolve, no I/O beyond what Node's resolver
889
+ * does, no network. Returns true when the framework resolves, false otherwise.
890
+ * @param {string} appDir
891
+ * @returns {boolean}
892
+ */
893
+ export function frameworkResolves(appDir) {
894
+ try {
895
+ // The base file need not exist; createRequire only uses it to anchor the
896
+ // node_modules lookup at appDir.
897
+ const require = createRequire(join(appDir, '__webjs_resolve_probe__.js'));
898
+ require.resolve('@webjsdev/core');
899
+ return true;
900
+ } catch {
901
+ return false;
902
+ }
903
+ }
904
+
905
+ /**
906
+ * CHECK 8, framework resolvability (#954). WARN when `@webjsdev/core` cannot be
907
+ * resolved FROM the app directory, which is the fresh-git-worktree trap: a
908
+ * worktree does not copy `node_modules`, so a plain `webjs dev` there dies at
909
+ * SSR with a raw `ERR_MODULE_NOT_FOUND: Cannot find package '@webjsdev/core'`
910
+ * whose remedy is not obvious. Silent PASS when the framework resolves (the
911
+ * common case), so this never slows a healthy app. WARN (not a hard fail): it
912
+ * is a setup/environment concern, the same tier as the version-coherence check.
913
+ * @param {string} appDir
914
+ * @returns {DoctorResult}
915
+ */
916
+ export function checkFrameworkResolves(appDir) {
917
+ const name = 'framework-resolve';
918
+ if (frameworkResolves(appDir)) {
919
+ return { name, status: 'pass', message: '@webjsdev/core resolves from the app directory.' };
920
+ }
921
+ const hasNodeModules = existsSync(join(appDir, 'node_modules'));
922
+ // A git worktree checks out `.git` as a FILE (a gitdir pointer), not a
923
+ // directory. That, plus a missing node_modules, is the exact #954 cause.
924
+ let isWorktree = false;
925
+ try {
926
+ isWorktree = statSync(join(appDir, '.git')).isFile();
927
+ } catch {
928
+ isWorktree = false;
929
+ }
930
+ if (isWorktree && !hasNodeModules) {
931
+ return {
932
+ name,
933
+ status: 'warn',
934
+ message:
935
+ '@webjsdev/core cannot be resolved from this directory, and this is a git worktree with no ' +
936
+ 'node_modules. Git worktrees do not copy node_modules, so the framework is unresolvable here ' +
937
+ 'and `webjs dev` / `webjs start` would fail at SSR with a raw ERR_MODULE_NOT_FOUND.',
938
+ fix:
939
+ 'Install dependencies in this worktree (`npm install`), or symlink node_modules from the ' +
940
+ 'primary checkout (`ln -s ../<primary-checkout>/node_modules node_modules`).',
941
+ };
942
+ }
943
+ if (!hasNodeModules) {
944
+ return {
945
+ name,
946
+ status: 'warn',
947
+ message: '@webjsdev/core cannot be resolved from this directory (no node_modules present).',
948
+ fix: 'Run `npm install` in the app directory so the framework resolves.',
949
+ };
950
+ }
951
+ return {
952
+ name,
953
+ status: 'warn',
954
+ message:
955
+ '@webjsdev/core cannot be resolved from this directory even though node_modules exists ' +
956
+ '(a partial or corrupted install).',
957
+ fix: 'Reinstall dependencies (`npm install`, or remove node_modules and reinstall).',
958
+ };
959
+ }
960
+
845
961
  export async function runDoctorChecks(appDir, opts = {}) {
846
962
  const cliDir = opts.cliDir || new URL('.', import.meta.url).pathname;
847
963
  const results = await Promise.all([
@@ -851,9 +967,11 @@ export async function runDoctorChecks(appDir, opts = {}) {
851
967
  checkVendorPin(appDir, opts),
852
968
  checkVendorGitignore(appDir),
853
969
  checkWebjsVersions(appDir),
970
+ Promise.resolve(checkFrameworkResolves(appDir)),
854
971
  checkImportmapCoherence(appDir, opts),
855
972
  Promise.resolve(checkGitHook(appDir)),
856
973
  checkElisionCarriers(appDir),
974
+ checkScaffoldDesign(appDir),
857
975
  ]);
858
976
  return results;
859
977
  }
@@ -142,9 +142,10 @@ export function bunifyDockerfile(s) {
142
142
  .replace(
143
143
  /# `npm start` is a thin alias[\s\S]*?the migrate no longer depends on an npm `prestart` hook\.\nCMD \["npm", "start"\]/,
144
144
  '# `bun --bun run start` runs the `start` script on Bun (the server serves via\n' +
145
- '# Bun.serve). `webjs start` runs the `webjs.start.before` step (`webjs db migrate`,\n' +
146
- '# which resolves drizzle-kit and runs it under Bun, no npx, #570), idempotent / a\n' +
147
- '# no-op with no pending migrations, then serves on $PORT.\n' +
145
+ '# Bun.serve). `webjs start` runs the `webjs.start.before` steps: `webjs db migrate`\n' +
146
+ '# (resolves drizzle-kit and runs it under Bun, no npx, #570) and, for a UI app,\n' +
147
+ '# the Tailwind compile under `bun --bun` (no Node / npm in the image, #947), both\n' +
148
+ '# idempotent, then serves on $PORT.\n' +
148
149
  'CMD ["bun", "--bun", "run", "start"]',
149
150
  );
150
151
  }
@@ -118,6 +118,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
118
118
  " ...(process.env.AUTH_GOOGLE_ID ? [Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET })] : []),",
119
119
  " ],",
120
120
  " secret: authSecret,",
121
+ " // A failed credentials sign-in 302s to `${pages.error}?error=CredentialsSignin`.",
122
+ " // Point it at /login so app/login/page.ts reads searchParams.error and shows a",
123
+ " // message, instead of the createAuth default (the home page) swallowing the error.",
124
+ " pages: { error: '/login' },",
121
125
  "});",
122
126
  "",
123
127
  ].join('\n'));
@@ -339,7 +343,17 @@ export async function writeSaasFiles(appDir, opts = {}) {
339
343
  "",
340
344
  "export const metadata = { title: 'Login' };",
341
345
  "",
342
- "export default function LoginPage() {",
346
+ "// A failed sign-in 302s back here with ?error=... (createAuth is configured",
347
+ "// with pages.error: '/login' in lib/auth.server.ts). Map the code to a plain",
348
+ "// message so a bad password gets visible feedback instead of a silent bounce.",
349
+ "function errorMessage(code: string | undefined): string | null {",
350
+ " if (!code) return null;",
351
+ " if (code === 'CredentialsSignin') return 'Invalid email or password.';",
352
+ " return 'Could not sign you in. Please try again.';",
353
+ "}",
354
+ "",
355
+ "export default function LoginPage({ searchParams }: { searchParams: { error?: string } }) {",
356
+ " const error = errorMessage(searchParams.error);",
343
357
  " return html`",
344
358
  " <div class=\"max-w-sm mx-auto mt-12\">",
345
359
  " <div class=${cardClass()}>",
@@ -348,6 +362,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
348
362
  " <p class=${cardDescriptionClass()}>Welcome back: log in to continue.</p>",
349
363
  " </div>",
350
364
  " <div class=${cardContentClass()}>",
365
+ " ${error ? html`<p role=\"alert\" class=\"mb-4 text-sm text-destructive\">${error}</p>` : ''}",
351
366
  " <form method=\"POST\" action=\"/api/auth/signin/credentials\" class=\"flex flex-col gap-4\">",
352
367
  " <!-- createAuth reads redirectTo from the posted form and 302s there after a successful signin. -->",
353
368
  " <input type=\"hidden\" name=\"redirectTo\" value=\"/dashboard\">",
@@ -461,12 +476,39 @@ export async function writeSaasFiles(appDir, opts = {}) {
461
476
  "",
462
477
  ].join('\n'));
463
478
 
479
+ // app/dashboard/layout.ts: a thin sub-nav shared by every /dashboard page
480
+ // (page.ts and settings/page.ts). It carries the logout control so a signed-in
481
+ // user can end the session from anywhere under /dashboard.
482
+ await writeFile(join(appDir, 'app', 'dashboard', 'layout.ts'), [
483
+ "import { html } from '@webjsdev/core';",
484
+ "import { buttonClass } from '#components/ui/button.ts';",
485
+ "",
486
+ "// Nested layout for the protected /dashboard subtree. Logout is a plain",
487
+ "// <form method=\"POST\"> posting to the createAuth signout route: it clears the",
488
+ "// session cookie and 302s home, and works with JS off (progressive-enhancement",
489
+ "// default). signOut is server-only (lib/auth.server.ts), so we POST to its route",
490
+ "// rather than import it into a browser-shipping page. After signout the dashboard",
491
+ "// middleware bounces any later /dashboard visit to /login.",
492
+ "export default function DashboardLayout({ children }: { children: unknown }) {",
493
+ " return html`",
494
+ " <nav class=\"flex items-center gap-4 mb-6 pb-4 border-b border-border\">",
495
+ " <a href=\"/dashboard\" class=\"text-sm font-medium hover:underline\">Dashboard</a>",
496
+ " <a href=\"/dashboard/settings\" class=\"text-sm font-medium hover:underline\">Settings</a>",
497
+ " <form method=\"POST\" action=\"/api/auth/signout\" class=\"ml-auto\">",
498
+ " <button class=${buttonClass({ variant: 'outline', size: 'sm' })} type=\"submit\">Log out</button>",
499
+ " </form>",
500
+ " </nav>",
501
+ " ${children}",
502
+ " `;",
503
+ "}",
504
+ "",
505
+ ].join('\n'));
506
+
464
507
  // app/dashboard/page.ts
465
508
  await writeFile(join(appDir, 'app', 'dashboard', 'page.ts'), [
466
509
  "import { html } from '@webjsdev/core';",
467
510
  "import { currentUser } from '#modules/auth/queries/current-user.server.ts';",
468
- "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '#components/ui/card.ts';",
469
- "import { buttonClass } from '#components/ui/button.ts';",
511
+ "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass } from '#components/ui/card.ts';",
470
512
  "import { badgeClass } from '#components/ui/badge.ts';",
471
513
  "",
472
514
  "export const metadata = { title: 'Dashboard' };",
@@ -483,9 +525,6 @@ export async function writeSaasFiles(appDir, opts = {}) {
483
525
  " <h2 class=${cardTitleClass()}>Welcome, ${user?.name || user?.email}!</h2>",
484
526
  " <p class=${cardDescriptionClass()}>You're authenticated. Replace this scaffold with your real app.</p>",
485
527
  " </div>",
486
- " <div class=${cardContentClass()}>",
487
- " <a class=${buttonClass({ variant: 'outline' })} href=\"/dashboard/settings\">Settings</a>",
488
- " </div>",
489
528
  " </div>",
490
529
  " `;",
491
530
  "}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.37",
3
+ "version": "0.10.39",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -48,21 +48,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
48
48
  logic in `modules/`.
49
49
  - **Use a unique design, and redesign means more than recolor (UI apps).** Give
50
50
  the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
51
- from what the app IS. Recoloring the scaffold and swapping the logo while
52
- keeping its skeleton (a fixed top header with a Home link and a theme toggle,
53
- the centered ~760px reading column, the "Built with webjs" footer) is NOT a
54
- unique design. Decide from scratch whether this app even needs a header or
55
- footer, what nav (if any), and what layout fits (a centered board, a full-bleed
56
- dashboard, a split, a single card). The scaffold ships a
57
- `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until
58
- you remove or replace the "Built with webjs" branding. Self-audit before
59
- finishing: nothing should read as the scaffold example (no "Built with webjs"
60
- footer, no leftover example nav, no default reading column unless it truly
61
- fits). Keep only the design TOKENS and theme wiring in `app/layout.ts`
62
- (infrastructure the ui kit reads) and restyle on top. Style with Tailwind
63
- utilities wherever they reach, and use custom CSS only for what utilities
64
- cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or
65
- gradients). The `api` template has no UI, so this does not apply there.
51
+ from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design
52
+ tokens, and Tailwind infra, then `${children}` in a bare padded container) with
53
+ NO header, nav, footer, or reading column: design the app's own chrome from
54
+ scratch. Decide whether it needs a header at all, a nav (or none), a footer, a
55
+ sidebar, a centered reading column, or a full-bleed canvas, from what fits the
56
+ app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to
57
+ learn the patterns from, then build your own. Two `webjs-scaffold-placeholder`
58
+ markers gate `webjs check`: the minimal shell ("design your layout from
59
+ scratch") and the palette block ("own the colors"), so check fails until each
60
+ is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts`
61
+ (infrastructure the ui kit reads) and set the token VALUES to your own palette;
62
+ run `webjs check --clear-placeholders` to keep the starter palette
63
+ deliberately. Style with Tailwind utilities wherever they reach, and use custom
64
+ CSS only for what utilities cannot express (@theme tokens, @keyframes,
65
+ scrollbar, complex color-mix or gradients). The `api` template has no UI, so
66
+ this does not apply there.
67
+ - **Render the app and LOOK before you call UI work done (every agent, not just one harness).**
68
+ You write CSS blind, so a layout or design defect ships silently: `webjs check`
69
+ and `webjs typecheck` pass even when a component collapses, grid cells are
70
+ uneven, the layout resizes as it fills, or the app just kept the scaffold's
71
+ colors. Static tools give no failure signal for this. The only thing that
72
+ catches it is rendering the app and looking at the pixels. So for ANY page,
73
+ layout, or component work: run it (`webjs dev`), open every route you changed in
74
+ a real browser (drive it with your harness's browser tool or MCP if it has one,
75
+ otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty,
76
+ filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing
77
+ collapses or reflows, that cells stay equal, that the design is the app's OWN,
78
+ and that both themes read. Ship a real-browser test (`webjs test --browser`) for
79
+ the mechanical floor (measure `getBoundingClientRect()` and assert cells stay
80
+ equal across a move). Fix and re-render until it holds, then state in your final
81
+ message what you rendered and confirmed. Claude Code additionally ENFORCES this
82
+ via the `webjs-design-review` skill plus a Stop hook, but the discipline is
83
+ harness-agnostic and this rule is the source of truth for every agent.
66
84
  - **Only three templates exist:** `webjs create <name>` (default full-stack),
67
85
  `--template api`, `--template saas`. The CLI rejects any other `--template`
68
86
  value. Pick:
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Claude Code Stop hook: render-and-look before finishing UI work.
4
+ #
5
+ # An AI agent writes CSS blind (it never renders), and layout / design defects
6
+ # have NO failure signal: `webjs check` and `typecheck` pass, the app runs. So a
7
+ # collapsed board, uneven cells, a layout that resizes as it fills, or an app
8
+ # that just kept the scaffold's design all ship silently. The one thing that
9
+ # catches them is looking at the rendered pixels. This backstop fires at the END
10
+ # of a turn that touched UI files and reminds you to render the app and inspect
11
+ # every state (see the webjs-design-review skill + CONVENTIONS item 6) before you
12
+ # stop. Loop-safe (fires at most once per stop) and skipped when no UI changed.
13
+ #
14
+ # Disable with WEBJS_NO_DESIGN_STOP=1.
15
+
16
+ set -uo pipefail
17
+ payload=$(cat 2>/dev/null || true)
18
+
19
+ if [ "${WEBJS_NO_DESIGN_STOP:-}" = "1" ]; then exit 0; fi
20
+ active=$(printf '%s' "$payload" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false)
21
+ if [ "$active" = "true" ]; then exit 0; fi
22
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi
23
+
24
+ # Did this turn touch UI surface? A component, a page/layout, or app styling.
25
+ ui_changed=$(git status --porcelain --untracked-files=all 2>/dev/null \
26
+ | grep -vE '(^|/)(node_modules|\.webjs)(/|$)' \
27
+ | grep -cE '(app/.*(page|layout)\.(t|j)sx?$)|(components/.*\.(t|j)sx?$)|(modules/.*components/.*\.(t|j)sx?$)|(\.css$)' || true)
28
+
29
+ if [ -z "$ui_changed" ] || [ "$ui_changed" -lt 1 ]; then exit 0; fi
30
+
31
+ reason="You changed UI in this turn but a design/layout defect has no failing test: check and typecheck pass even when a component collapses, cells are uneven, the layout shifts as it fills, or the app just resembles the scaffold. Before you stop, RENDER the app and LOOK at it: start it (webjs dev / start), open the routes you changed in a browser, and PLAY THROUGH every state (fill the board, win, draw, reload). Confirm (1) nothing collapses or resizes, cells stay equal; (2) the design is the app's OWN (layout, palette, typography, chrome), not the scaffold shell or its default colors; (3) it looks correct in light AND dark. See the webjs-design-review skill and CONVENTIONS item 6. If you already rendered and verified it this turn, say so in your final message. Disable this backstop with WEBJS_NO_DESIGN_STOP=1."
32
+
33
+ jq -n --arg r "$reason" '{decision: "block", reason: $r}' 2>/dev/null \
34
+ || printf '{"decision":"block","reason":%s}\n' "$(printf '%s' "$reason" | jq -Rs . 2>/dev/null || echo '""')"
35
+
36
+ exit 0
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # UserPromptSubmit hook: route a UI-building prompt to the design-review
4
+ # skill, so it is never silently skipped.
5
+ #
6
+ # Why this exists: a Skill is model-invoked, so it fires only when the model
7
+ # judges the prompt to match, and that judgement is exactly what fails for
8
+ # design work ("build a tic-tac-toe app" reads as backend/logic work and the
9
+ # render-and-look step gets skipped, shipping a collapsed or scaffold-looking
10
+ # UI). A hook is deterministic: it runs on every prompt, decides from the
11
+ # prompt TEXT, and injects a directive the model reads before acting. It
12
+ # cannot invoke the Skill itself (the harness forbids that); the strongest
13
+ # lever is UserPromptSubmit additionalContext.
14
+ #
15
+ # Output contract: print one JSON object with
16
+ # hookSpecificOutput.additionalContext and exit 0. Never block (exit 2 would
17
+ # erase the prompt); routing informs, it does not gate.
18
+
19
+ set -euo pipefail
20
+ payload=$(cat)
21
+ prompt=$(printf '%s' "$payload" | jq -r '.prompt // empty' 2>/dev/null || true)
22
+ [ -z "$prompt" ] && exit 0
23
+ lc=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')
24
+ has() { printf '%s' "$lc" | grep -Eq "$1"; }
25
+
26
+ # UI / app-building intent: any request to build/create/change something the
27
+ # user will SEE. Broad on purpose (a false positive just reminds you to look).
28
+ if has '(build|create|make|add|design|redesign|style|implement|scaffold).{0,40}(app|page|layout|component|screen|view|board|form|dashboard|ui|site|game|list|table|card|nav|header|footer|modal|button|theme)' \
29
+ || has '(make|help me|let'\''s).{0,20}(look|prettier|beautiful|nicer|design)' \
30
+ || has '(tic.?tac.?toe|todo|blog|dashboard|landing|storefront|kanban|chat)'; then
31
+ ctx="ROUTING: this prompt involves UI work. Invoke the webjs-design-review skill (Skill tool) as part of this task: after building/changing any page, layout, or component and BEFORE reporting the work done, render the app in a real browser and LOOK at every state, confirming the app owns its design (layout + palette + type, not the scaffold), nothing collapses or resizes, cells stay even, and light + dark both read. A design/layout defect has NO failing test, so the render-and-look is the only check that catches it."
32
+ jq -n --arg c "$ctx" '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $c}}'
33
+ fi
34
+
35
+ exit 0
@@ -1,5 +1,15 @@
1
1
  {
2
2
  "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": ".claude/hooks/route-skills.sh"
9
+ }
10
+ ]
11
+ }
12
+ ],
3
13
  "PreToolUse": [
4
14
  {
5
15
  "matcher": "Write|Edit|MultiEdit",
@@ -73,6 +83,10 @@
73
83
  {
74
84
  "type": "command",
75
85
  "command": ".claude/hooks/commit-before-stop.sh"
86
+ },
87
+ {
88
+ "type": "command",
89
+ "command": ".claude/hooks/design-review-before-stop.sh"
76
90
  }
77
91
  ]
78
92
  }
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: webjs-design-review
3
+ description: >-
4
+ Render-and-look review for ANY UI work in a WebJs app. Invoke after building
5
+ or changing a page, layout, or component, and before you report the work done.
6
+ Triggers: "build", "create", "add a page", "component", "layout", "style",
7
+ "design", "UI", "screen", "board", "form", "dashboard", "make it look".
8
+ ---
9
+
10
+ # Render the app and LOOK before you call UI work done
11
+
12
+ You write CSS blind. You never see the pixels, so a whole class of defects ships
13
+ silently: `webjs check` passes, `webjs typecheck` passes, the server boots, and
14
+ the app still looks broken. A collapsed component, cells of unequal size, a
15
+ layout that resizes as it fills with content, text that overflows its box, an
16
+ app that just kept the scaffold's colors and chrome, none of these fail a test.
17
+ The only thing that catches them is rendering the app and looking at it.
18
+
19
+ So for ANY work that touches a page, layout, or component, this is the loop:
20
+
21
+ ## 1. Run the app and open what you changed
22
+
23
+ ```sh
24
+ webjs dev # or: webjs start, for the production render
25
+ ```
26
+
27
+ Open every route you touched in a real browser. Use the browser MCP
28
+ (`mcp__playwright__*` or `mcp__chrome-devtools__*`) if available so you can drive
29
+ and screenshot it; otherwise open it yourself and take screenshots.
30
+
31
+ ## 2. Drive EVERY state, not just the first paint
32
+
33
+ The first paint is the easy case. Bugs hide in the states you reach by
34
+ interacting. Play the app the way a user will:
35
+
36
+ - A game board: play a full game. Fill it. Win. Draw. Reset. Watch whether the
37
+ board or its cells change size as marks appear (they must NOT).
38
+ - A list: empty, one item, many items, an item long enough to wrap.
39
+ - A form: empty, invalid, submitted, error returned, success.
40
+ - Anything async: loading, loaded, error, refetch.
41
+
42
+ Reload each state. Resize the window narrow (mobile) and wide.
43
+
44
+ ## 3. Confirm the things a test can't
45
+
46
+ Look at each state and confirm, with your eyes:
47
+
48
+ 1. **Nothing collapses, overflows, or resizes.** A container is the size it
49
+ should be (not 0-height, not collapsed to its content when it should fill).
50
+ Grid/flex children that should be equal ARE equal, and STAY equal as content
51
+ changes. Text stays inside its box.
52
+ 2. **The design is this app's OWN.** Not the scaffold shell, not its default
53
+ color tokens. The palette (real `oklch`/hex values, not just shadcn token
54
+ NAMES), the typography, the layout, and the chrome are chosen for THIS app.
55
+ "It still looks like the starter" is a defect to fix, not ship.
56
+ 3. **Light AND dark both look right.** Toggle the theme. Check contrast, that
57
+ nothing disappears against its background, that borders and shadows read.
58
+ 4. **It still renders with JavaScript OFF.** WebJs is SSR + progressive
59
+ enhancement, so the page must read and look right with no JS (disable it in
60
+ devtools, or load in a JS-off context). Content shows, `<a>` navigates,
61
+ forms submit, and CRUCIALLY the CSS is fully applied (the app links a static
62
+ compiled `public/tailwind.css`, so utilities resolve with no JS). An app that
63
+ goes unstyled or blank with JS off is a broken first paint, not a design to
64
+ ship.
65
+
66
+ ## 4. Iterate until it holds, then say what you saw
67
+
68
+ If any of the above is wrong, fix it and re-render. Do not stop on the first
69
+ render. When it holds, state in your final message WHAT you rendered and WHAT
70
+ you confirmed (which states, light + dark), so the review is on the record.
71
+
72
+ ---
73
+
74
+ **Why this is a skill and not just a test:** a real-browser test
75
+ (`webjs test --browser`) catches the mechanical failures (collapse, uneven
76
+ cells, reflow) and you SHOULD ship one. There is no framework helper for this; a
77
+ layout-stability check is a few lines you write against your own component (in a
78
+ `*/test/**/browser/*.test.js`): measure `getBoundingClientRect()` on the grid
79
+ children and assert they stay equal-sized before AND after a move, so a collapse
80
+ or reflow FAILS the test. But "looks like the scaffold", "the
81
+ palette is bland", "the spacing is off", "it's ugly in dark mode" are judgment
82
+ calls no assertion makes for you. That is what this human-in-the-loop look is
83
+ for. Do both: the test for the mechanical floor, the look for everything above
84
+ it. See CONVENTIONS item 6 and `agent-docs/styling.md`.
@@ -48,21 +48,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
48
48
  `lib/utils/`, feature logic in `modules/`.
49
49
  - **Use a unique design, and redesign means more than recolor (UI apps).** Give
50
50
  the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
51
- from what the app IS. Recoloring the scaffold and swapping the logo while
52
- keeping its skeleton (a fixed top header with a Home link and a theme toggle,
53
- the centered ~760px reading column, the "Built with webjs" footer) is NOT a
54
- unique design. Decide from scratch whether this app even needs a header or
55
- footer, what nav (if any), and what layout fits (a centered board, a
56
- full-bleed dashboard, a split, a single card). The scaffold ships a
57
- `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until
58
- you remove or replace the "Built with webjs" branding. Self-audit before
59
- finishing: nothing should read as the scaffold example (no "Built with webjs"
60
- footer, no leftover example nav, no default reading column unless it truly
61
- fits). Keep only the design TOKENS and theme wiring in `app/layout.ts`
62
- (infrastructure the ui kit reads) and restyle on top. Style with Tailwind
63
- utilities wherever they reach, and use custom CSS only for what utilities
64
- cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or
65
- gradients). The `api` template has no UI, so this does not apply there.
51
+ from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design
52
+ tokens, and Tailwind infra, then `${children}` in a bare padded container) with
53
+ NO header, nav, footer, or reading column: design the app's own chrome from
54
+ scratch. Decide whether it needs a header at all, a nav (or none), a footer, a
55
+ sidebar, a centered reading column, or a full-bleed canvas, from what fits the
56
+ app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to
57
+ learn the patterns from, then build your own. Two `webjs-scaffold-placeholder`
58
+ markers gate `webjs check`: the minimal shell ("design your layout from
59
+ scratch") and the palette block ("own the colors"), so check fails until each
60
+ is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts`
61
+ (infrastructure the ui kit reads) and set the token VALUES to your own palette;
62
+ run `webjs check --clear-placeholders` to keep the starter palette
63
+ deliberately. Style with Tailwind utilities wherever they reach, and use custom
64
+ CSS only for what utilities cannot express (@theme tokens, @keyframes,
65
+ scrollbar, complex color-mix or gradients). The `api` template has no UI, so
66
+ this does not apply there.
67
+ - **Render the app and LOOK before you call UI work done (every agent, not just one harness).**
68
+ You write CSS blind, so a layout or design defect ships silently: `webjs check`
69
+ and `webjs typecheck` pass even when a component collapses, grid cells are
70
+ uneven, the layout resizes as it fills, or the app just kept the scaffold's
71
+ colors. Static tools give no failure signal for this. The only thing that
72
+ catches it is rendering the app and looking at the pixels. So for ANY page,
73
+ layout, or component work: run it (`webjs dev`), open every route you changed in
74
+ a real browser (drive it with your harness's browser tool or MCP if it has one,
75
+ otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty,
76
+ filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing
77
+ collapses or reflows, that cells stay equal, that the design is the app's OWN,
78
+ and that both themes read. Ship a real-browser test (`webjs test --browser`) for
79
+ the mechanical floor (measure `getBoundingClientRect()` and assert cells stay
80
+ equal across a move). Fix and re-render until it holds, then state in your final
81
+ message what you rendered and confirmed. Claude Code additionally ENFORCES this
82
+ via the `webjs-design-review` skill plus a Stop hook, but the discipline is
83
+ harness-agnostic and this rule is the source of truth for every agent.
66
84
  - **Only three templates exist:** `webjs create <name>` (default
67
85
  full-stack), `--template api`, `--template saas`. The CLI rejects any
68
86
  other `--template` value. Pick: