@webjsdev/cli 0.10.0 → 0.10.1

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/create.js CHANGED
@@ -373,6 +373,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
373
373
  '.claude/hooks/block-prose-punctuation.sh',
374
374
  '.claude/hooks/guard-branch-context.sh',
375
375
  '.claude/hooks/nudge-uncommitted.sh',
376
+ '.claude/hooks/require-tests-with-src.sh',
376
377
  // Gemini CLI config + hooks
377
378
  '.gemini/settings.json',
378
379
  '.gemini/hooks/nudge-uncommitted.sh',
@@ -405,7 +406,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
405
406
 
406
407
  // Make hook scripts executable
407
408
  const { chmod } = await import('node:fs/promises');
408
- for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh']) {
409
+ for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'require-tests-with-src.sh']) {
409
410
  const hookPath = join(appDir, '.claude', 'hooks', hook);
410
411
  if (existsSync(hookPath)) await chmod(hookPath, 0o755);
411
412
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PreToolUse hook (scaffolded by `webjs create`): block a `git commit`
4
+ # that adds or changes application code without any accompanying test.
5
+ #
6
+ # webjs is AI-first: most apps are built with an AI agent, and the
7
+ # easiest corner to cut is shipping a feature with no test. This gate
8
+ # makes "every change ships with a test" a hard floor, not a suggestion.
9
+ #
10
+ # What a hook CANNOT do: judge WHICH test layer a change needs (a unit
11
+ # test vs a browser/e2e test is a judgement call). So it enforces the
12
+ # floor (some real test must accompany app code) and reminds you to add
13
+ # browser/e2e coverage for interactive surfaces. `webjs test` runs the
14
+ # actual suite in the commit hook.
15
+ #
16
+ # Scope: fires only on `git commit`. Inspects the STAGED diff.
17
+ #
18
+ # Blocks (exit 2) when the staged diff changes app code (app/, modules/,
19
+ # components/, lib/) but stages no test (test/** or *.test.* / *.spec.*).
20
+ # Allowed: commits with no app-code change, commits that stage a test
21
+ # alongside, and WEBJS_NO_TEST_GATE=1 for a genuine non-code commit.
22
+ #
23
+ # Bypass (humans, emergencies): git commit --no-verify.
24
+
25
+ set -euo pipefail
26
+
27
+ if [ "${WEBJS_NO_TEST_GATE:-}" = "1" ]; then
28
+ exit 0
29
+ fi
30
+
31
+ payload=$(cat)
32
+ cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
33
+ if [ -z "$cmd" ]; then exit 0; fi
34
+ # Match `git commit` as a whole word so sibling subcommands
35
+ # (git commit-graph, git commit-tree) and string mentions do not trip it.
36
+ if ! printf '%s' "$cmd" | grep -Eq '(^|[^[:alnum:]-])git commit([^[:alnum:]-]|$)'; then
37
+ exit 0
38
+ fi
39
+
40
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi
41
+
42
+ staged=$(git diff --cached --name-only 2>/dev/null || true)
43
+ if [ -z "$staged" ]; then exit 0; fi
44
+
45
+ # App code lives under app/, modules/, components/, lib/. A `.server.*`
46
+ # file is still app code. Match source extensions only (skip .css, .md).
47
+ app_code=$(printf '%s\n' "$staged" \
48
+ | grep -E '^(app|modules|components|lib)/.*\.([mc]?[jt]sx?)$' || true)
49
+ if [ -z "$app_code" ]; then exit 0; fi
50
+
51
+ test_staged=$(printf '%s\n' "$staged" \
52
+ | grep -E '(^|/)test/|\.test\.[mc]?[jt]sx?$|\.spec\.[mc]?[jt]sx?$' || true)
53
+
54
+ if [ -z "$test_staged" ]; then
55
+ cat >&2 <<'EOF'
56
+ BLOCKED: this commit changes app code but stages no test.
57
+
58
+ You staged application code (app/, modules/, components/, lib/) with no
59
+ accompanying test. Every change ships with a test. Add or update the test
60
+ that proves the new behaviour, then `git add` it.
61
+
62
+ Pick the layer the change needs (a unit test is not always enough):
63
+ - logic / actions / queries / utils -> a unit test
64
+ - a component, hydration, a server action called from the client, the
65
+ router, anything interactive -> a browser or e2e test that asserts the
66
+ real behaviour in a browser, not just the function in isolation.
67
+
68
+ See `webjs test` and the testing guide. Genuine non-code commit (docs,
69
+ config) that needs no test? Re-run with WEBJS_NO_TEST_GATE=1.
70
+
71
+ Hook: .claude/hooks/require-tests-with-src.sh
72
+ EOF
73
+ exit 2
74
+ fi
75
+
76
+ # Reminder for interactive surfaces: a unit test alone rarely covers them.
77
+ interactive=$(printf '%s\n' "$app_code" | grep -E '^components/|/components/' || true)
78
+ if [ -n "$interactive" ]; then
79
+ jq -n --arg ctx "Reminder: this commit changes component code. A unit test alone usually is not enough for an interactive component; add a browser test (webjs test --browser) that asserts the rendered/hydrated behaviour." '{
80
+ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: $ctx }
81
+ }'
82
+ fi
83
+ exit 0
@@ -18,6 +18,15 @@
18
18
  "command": ".claude/hooks/guard-branch-context.sh"
19
19
  }
20
20
  ]
21
+ },
22
+ {
23
+ "matcher": "Bash",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": ".claude/hooks/require-tests-with-src.sh"
28
+ }
29
+ ]
21
30
  }
22
31
  ],
23
32
  "PostToolUse": [
@@ -21,6 +21,31 @@ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
21
21
  exit 1
22
22
  fi
23
23
 
24
+ # Require a test to accompany app-code changes. Tool-agnostic floor: a
25
+ # commit that stages app code (app/, modules/, components/, lib/) with no
26
+ # test (test/** or *.test.* / *.spec.*) is blocked. The full suite below
27
+ # proves tests PASS; this proves a test was WRITTEN. Bypass for a genuine
28
+ # non-code commit with WEBJS_NO_TEST_GATE=1, or --no-verify in emergencies.
29
+ if [ "${WEBJS_NO_TEST_GATE:-}" != "1" ]; then
30
+ STAGED=$(git diff --cached --name-only 2>/dev/null)
31
+ APP_CODE=$(printf '%s\n' "$STAGED" | grep -E '^(app|modules|components|lib)/.*\.([mc]?[jt]sx?)$' || true)
32
+ if [ -n "$APP_CODE" ]; then
33
+ TESTS=$(printf '%s\n' "$STAGED" | grep -E '(^|/)test/|\.test\.[mc]?[jt]sx?$|\.spec\.[mc]?[jt]sx?$' || true)
34
+ if [ -z "$TESTS" ]; then
35
+ echo ""
36
+ echo "ERROR: app code changed but no test is staged."
37
+ echo "Every change ships with a test. Add or update the test that"
38
+ echo "proves the new behaviour (a browser/e2e test for interactive"
39
+ echo "or component code), then git add it."
40
+ echo ""
41
+ echo "Genuine non-code commit? WEBJS_NO_TEST_GATE=1 git commit ..."
42
+ echo "Emergency bypass: git commit --no-verify"
43
+ echo ""
44
+ exit 1
45
+ fi
46
+ fi
47
+ fi
48
+
24
49
  # webjs test + webjs check on every commit. Tool-agnostic enforcement:
25
50
  # fires regardless of which agent (Claude, Cursor, Antigravity, Copilot,
26
51
  # human) is making the commit. Skipped if the CLI is not yet installed
@@ -883,8 +883,15 @@ composition, so a nested shell ends up dropped by the HTML parser.
883
883
  ## Workflow expectations for AI agents
884
884
 
885
885
  1. Branch before editing. Never push to `main` directly.
886
- 2. Every code change comes with: unit test(s), AGENTS.md / docs updates if
887
- the feature surface changed, `webjs check` passing.
886
+ 2. Every code change comes with a test, AGENTS.md / docs updates if the
887
+ feature surface changed, `webjs check` passing. A unit test is not
888
+ always enough: a component, hydration, the client router, or a server
889
+ action called from the client needs a browser test
890
+ (`webjs test --browser`) asserting the behaviour in a real browser. A
891
+ commit that stages app code (`app/`, `modules/`, `components/`, `lib/`)
892
+ with no test is blocked by `.claude/hooks/require-tests-with-src.sh`
893
+ and the `.hooks/pre-commit` floor (bypass a genuine non-code commit
894
+ with `WEBJS_NO_TEST_GATE=1`).
888
895
  3. Commit and push **per logical unit**, not at the end. A logical unit is one
889
896
  feature, one fix, one rename, one doc rewrite. If you have 5+ unstaged files
890
897
  spanning different concerns, commit the current group before continuing.
@@ -451,6 +451,15 @@ test/
451
451
  - `webjs test --browser` (or `npx wtr`) runs the browser tests.
452
452
  - `WEBJS_E2E=1 webjs test` adds the e2e tests.
453
453
 
454
+ **Every change ships with a test, enforced at commit time.** A commit
455
+ that stages app code (`app/`, `modules/`, `components/`, `lib/`) without
456
+ staging a test is blocked by `.claude/hooks/require-tests-with-src.sh`
457
+ (Claude Code) and the universal `.hooks/pre-commit` (any agent or human).
458
+ A unit test alone is not enough for interactive or component code: add
459
+ the browser test that asserts the rendered/hydrated behaviour. Bypass a
460
+ genuine non-code commit with `WEBJS_NO_TEST_GATE=1`, or `--no-verify` in
461
+ an emergency.
462
+
454
463
  ### Choosing a feature folder
455
464
 
456
465
  Use the same name as the matching module folder when one exists: