@webjsdev/cli 0.10.52 → 0.10.54

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 (33) hide show
  1. package/README.md +2 -2
  2. package/bin/webjs.js +17 -0
  3. package/lib/app-name.js +73 -0
  4. package/lib/check-target.js +145 -0
  5. package/lib/create.js +42 -26
  6. package/lib/doctor.js +91 -16
  7. package/lib/gallery-shell-files.js +36 -0
  8. package/package.json +3 -3
  9. package/templates/.agents/skills/webjs/SKILL.md +6 -2
  10. package/templates/.agents/skills/webjs/references/built-ins.md +1 -1
  11. package/templates/.agents/skills/webjs/references/components.md +95 -3
  12. package/templates/.agents/skills/webjs/references/data-and-actions.md +29 -2
  13. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +25 -1
  14. package/templates/.agents/skills/webjs/references/routing-and-pages.md +24 -1
  15. package/templates/.agents/skills/webjs/references/styling.md +14 -1
  16. package/templates/.agents/skills/webjs/references/testing.md +8 -0
  17. package/templates/.agents/skills/webjs/references/typescript.md +17 -3
  18. package/templates/.agents/skills/webjs/references/ui-kit.md +15 -0
  19. package/templates/.claude/hooks/block-prose-punctuation.sh +66 -24
  20. package/templates/gallery/app/icon.ts +10 -5
  21. package/templates/gallery/modules/directives/components/directive-demo.ts +4 -1
  22. package/templates/gallery/modules/gallery/components/gallery-nav.ts +5 -0
  23. package/templates/gallery/modules/gallery/nav.ts +12 -3
  24. package/templates/gallery/modules/todo/actions/submit-todo.server.ts +7 -0
  25. package/templates/test/hello/e2e/hello.test.ts +26 -1
  26. package/templates/.cursor/hooks/nudge-uncommitted.sh +0 -38
  27. package/templates/.cursor/hooks.json +0 -8
  28. package/templates/.cursorrules +0 -21
  29. package/templates/.gemini/hooks/nudge-uncommitted.sh +0 -42
  30. package/templates/.gemini/settings.json +0 -15
  31. package/templates/.github/copilot-instructions.md +0 -9
  32. package/templates/.opencode/plugins/nudge-uncommitted.ts +0 -62
  33. package/templates/GEMINI.md +0 -11
@@ -6,16 +6,18 @@
6
6
  #
7
7
  # 1. U+2014 em-dash, anywhere.
8
8
  # 2. Space-hyphen-space " - " in PROSE contexts (comment lines, markdown
9
- # lines, headings, blockquotes). Math expressions in code like
9
+ # lines, headings, blockquotes, a JSON "description" / "title" /
10
+ # "displayName" string value, and a column-0 YAML front-matter
11
+ # description: / title: / displayName: line). Math expressions in code like
10
12
  # `Math.abs(a - b)` or `arr.length - 1` are NOT flagged.
11
- # 3. Space-semicolon-space " ; " in PROSE contexts. JS / CSS statement
12
- # terminators (`;\n`) are NOT flagged.
13
+ # 3. Space-semicolon-space " ; " in the same PROSE contexts as rule 2.
14
+ # JS / CSS statement terminators (`;\n`) are NOT flagged.
13
15
  # 4. Code-shaped left-hand side immediately followed by a colon and prose:
14
16
  # - `<code>foo()</code>:` (markdown code-LHS in docs)
15
17
  # - `<my-tag>:` (custom-element tag with hyphen)
16
18
  # - Inline comment `// foo(): description`
17
19
  #
18
- # Why this exists: see AGENTS.md "Invariants", item 10. These patterns
20
+ # Why this exists: see AGENTS.md "Invariants", item 11. These patterns
19
21
  # confuse AI agents that try to parse the prose as TypeScript / shorthand-
20
22
  # method / object-literal syntax, and trip humans reading API docs.
21
23
  #
@@ -46,8 +48,13 @@ if [ -z "$new_content" ]; then
46
48
  exit 0
47
49
  fi
48
50
 
51
+ # Every match below reads from a here-string, never a pipe. `grep -q` exits on
52
+ # the first match, which closes a pipe under `printf`, and with `set -o pipefail`
53
+ # that SIGPIPE became the pipeline status, so the rule silently skipped on any
54
+ # payload past the pipe buffer (measured: 0 of 8 blocks at 128 KB).
55
+
49
56
  # --- 1. U+2014 em-dash --------------------------------------------------
50
- if printf '%s' "$new_content" | grep -q $'\xe2\x80\x94'; then
57
+ if grep -q $'\xe2\x80\x94' <<< "$new_content"; then
51
58
  cat >&2 <<'EOF'
52
59
  BLOCKED: em-dash (U+2014) detected in this tool call.
53
60
 
@@ -57,7 +64,7 @@ restructured sentence. Do NOT replace it with " - " or " ; " or a
57
64
  trailing colon on code: those are also banned. See rule 2 / 3 / 4
58
65
  below for the alternatives.
59
66
 
60
- Rule: AGENTS.md, Invariants section, item 10.
67
+ Rule: AGENTS.md, Invariants section, item 11.
61
68
  Hook: .claude/hooks/block-prose-punctuation.sh.
62
69
  EOF
63
70
  exit 2
@@ -81,25 +88,42 @@ block_pause_hyphen=0
81
88
  # `*` (markdown bold-start would have a letter after, distinguishable),
82
89
  # followed by prose with `\w+ - \w+` pattern. Specifically: catch lines
83
90
  # like `// foo - bar`, ` * foo - bar`, `* foo - bar`.
84
- if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
91
+ if grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then
85
92
  block_pause_hyphen=1
86
93
  fi
87
94
 
88
95
  # Markdown heading " - " pause: line starts with `#` followed by prose
89
96
  # and ` - ` pattern.
90
- if printf '%s\n' "$new_content" | grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
97
+ if grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then
91
98
  block_pause_hyphen=1
92
99
  fi
93
100
 
94
101
  # Markdown blockquote " - " pause: line starts with `>` followed by prose
95
102
  # and ` - ` pattern. (Single `>` blockquote, not table.)
96
- if printf '%s\n' "$new_content" | grep -qE '^>[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
103
+ if grep -qE '^>[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then
97
104
  block_pause_hyphen=1
98
105
  fi
99
106
 
100
107
  # HTML / markdown <p>, <li>, <td> body " - " pause: line contains a
101
108
  # closing HTML tag from a prose context, then prose-style ` - `.
102
- if printf '%s\n' "$new_content" | grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
109
+ if grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then
110
+ block_pause_hyphen=1
111
+ fi
112
+
113
+ # JSON prose-value " - " pause: a string assignment whose KEY is one of the
114
+ # three prose-bearing keys this project's JSON uses. Scoping to the key is what
115
+ # keeps this off semver ranges, script commands, urls, paths and globs, every
116
+ # one of which lives under a different key. Shape, not file path: the Bash
117
+ # payload carries no file_path, so a heredoc writing a manifest is covered too.
118
+ if grep -qE '^[[:space:]]*"(description|title|displayName)"[[:space:]]*:[[:space:]]*".*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then
119
+ block_pause_hyphen=1
120
+ fi
121
+
122
+ # YAML front-matter " - " pause, same three keys. Anchored at column 0 with no
123
+ # leading whitespace, which is what confines it to document front matter: every
124
+ # nested YAML mapping is indented, including the workflow-input `description:`
125
+ # values in .github/workflows/release.yml.
126
+ if grep -qE '^(description|title|displayName):[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then
103
127
  block_pause_hyphen=1
104
128
  fi
105
129
 
@@ -118,13 +142,17 @@ restructured phrasing.
118
142
  Bad: <li>Foo - bar.</li>
119
143
  Good: <li>Foo, with bar.</li>
120
144
 
145
+ Bad: "description": "A library - for things"
146
+ Good: "description": "A library for things"
147
+
121
148
  Plain hyphens are still fine in compound words (`AI-first`), CLI
122
149
  flags (`--http2`), filenames, ranges, and math expressions in code
123
150
  (`arr.length - 1`, `Math.abs(a - b)`). The hook only flags the
124
151
  ` < word > - < word > ` pause-pattern in prose contexts (comments,
125
- markdown headings, blockquotes, HTML prose tags).
152
+ markdown headings, blockquotes, HTML prose tags, and a JSON or
153
+ front-matter description / title / displayName value).
126
154
 
127
- Rule: AGENTS.md, Invariants section, item 10.
155
+ Rule: AGENTS.md, Invariants section, item 11.
128
156
  Hook: .claude/hooks/block-prose-punctuation.sh.
129
157
  EOF
130
158
  exit 2
@@ -134,19 +162,29 @@ fi
134
162
  # Same prose-context guard as #2.
135
163
  block_pause_semicolon=0
136
164
 
137
- if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
165
+ if grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then
138
166
  block_pause_semicolon=1
139
167
  fi
140
168
 
141
- if printf '%s\n' "$new_content" | grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
169
+ if grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then
142
170
  block_pause_semicolon=1
143
171
  fi
144
172
 
145
- if printf '%s\n' "$new_content" | grep -qE '^>[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
173
+ if grep -qE '^>[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then
146
174
  block_pause_semicolon=1
147
175
  fi
148
176
 
149
- if printf '%s\n' "$new_content" | grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
177
+ if grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then
178
+ block_pause_semicolon=1
179
+ fi
180
+
181
+ # JSON prose-value " ; " pause, same three keys as rule 2.
182
+ if grep -qE '^[[:space:]]*"(description|title|displayName)"[[:space:]]*:[[:space:]]*".*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then
183
+ block_pause_semicolon=1
184
+ fi
185
+
186
+ # YAML front-matter " ; " pause, column-0 anchored like rule 2.
187
+ if grep -qE '^(description|title|displayName):[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then
150
188
  block_pause_semicolon=1
151
189
  fi
152
190
 
@@ -161,10 +199,14 @@ two sentences (period) or with a conjunction (", and", ", but", ", so").
161
199
  Good: // Forms work. Links work too.
162
200
  Good: // Forms work, and links work too.
163
201
 
202
+ Bad: "description": "Forms work ; links work too."
203
+ Good: "description": "Forms work. Links work too."
204
+
164
205
  Semicolons stay fine inside code (JS statement terminators, CSS
165
- declarations) since those are not flagged.
206
+ declarations) since those are not flagged. Only the space-surrounded
207
+ form is banned, so an ordinary English semicolon is untouched.
166
208
 
167
- Rule: AGENTS.md, Invariants section, item 10.
209
+ Rule: AGENTS.md, Invariants section, item 11.
168
210
  Hook: .claude/hooks/block-prose-punctuation.sh.
169
211
  EOF
170
212
  exit 2
@@ -175,7 +217,7 @@ fi
175
217
  # lowercase prose. The `)</code>:` shape is unambiguous: this is markdown,
176
218
  # not code, AND the inner code ends in `()` so the colon visually parses
177
219
  # as a return-type annotation.
178
- if printf '%s' "$new_content" | grep -qE '\)</code>:[[:space:]][a-z]'; then
220
+ if grep -qE '\)</code>:[[:space:]][a-z]' <<< "$new_content"; then
179
221
  cat >&2 <<'EOF'
180
222
  BLOCKED: code-LHS colon-then-prose detected ("<code>foo()</code>: ...").
181
223
 
@@ -186,7 +228,7 @@ parses as a TypeScript return-type annotation. Rewrite verb-led.
186
228
  Good: <code>repeat()</code> is the keyed list directive
187
229
  Good: <code>startServer()</code> creates an HTTP(S) server
188
230
 
189
- Rule: AGENTS.md, Invariants section, item 10.
231
+ Rule: AGENTS.md, Invariants section, item 11.
190
232
  Hook: .claude/hooks/block-prose-punctuation.sh.
191
233
  EOF
192
234
  exit 2
@@ -195,7 +237,7 @@ fi
195
237
  # --- 4b. Custom-element-tag <my-tag>: prose ------------------------------
196
238
  # HTML reserves hyphenated tag names for custom elements (W3C spec), so
197
239
  # `<x-y>:` is unambiguous prose, never JSX / TS / CSS.
198
- if printf '%s' "$new_content" | grep -qE '<[a-z][a-z0-9]*(-[a-z0-9]+)+([[:space:]][^>]*)?>:[[:space:]][a-z]'; then
240
+ if grep -qE '<[a-z][a-z0-9]*(-[a-z0-9]+)+([[:space:]][^>]*)?>:[[:space:]][a-z]' <<< "$new_content"; then
199
241
  cat >&2 <<'EOF'
200
242
  BLOCKED: custom-element-tag colon-then-prose detected ("<my-tag>: ...").
201
243
 
@@ -206,7 +248,7 @@ webjs bans `<my-tag>: <prose>` in comments and docs. Rewrite verb-led.
206
248
  Bad: // <ui-dialog-content>: the centered panel.
207
249
  Good: // <ui-dialog-content> is the centered panel.
208
250
 
209
- Rule: AGENTS.md, Invariants section, item 10.
251
+ Rule: AGENTS.md, Invariants section, item 11.
210
252
  Hook: .claude/hooks/block-prose-punctuation.sh.
211
253
  EOF
212
254
  exit 2
@@ -216,7 +258,7 @@ fi
216
258
  # Match comment-line prefix (`//` or leading `*`) before `\w+(...): ` and
217
259
  # lowercase prose. Avoids TS return-type annotations because those never
218
260
  # appear inside comment lines.
219
- if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]][^(]*[A-Za-z_][A-Za-z0-9_]*\([^)]*\):[[:space:]][a-z]'; then
261
+ if grep -qE '^[[:space:]]*(//|\*)[[:space:]][^(]*[A-Za-z_][A-Za-z0-9_]*\([^)]*\):[[:space:]][a-z]' <<< "$new_content"; then
220
262
  cat >&2 <<'EOF'
221
263
  BLOCKED: comment-line code-LHS colon-then-prose detected ("// foo(): ...").
222
264
 
@@ -227,7 +269,7 @@ webjs bans `xyz(): <prose>` inside comments and JSDoc. Rewrite verb-led.
227
269
  Bad: // closest(): null if the click wasn't inside a frame
228
270
  Good: // closest() returns null when the click wasn't inside a frame
229
271
 
230
- Rule: AGENTS.md, Invariants section, item 10.
272
+ Rule: AGENTS.md, Invariants section, item 11.
231
273
  Hook: .claude/hooks/block-prose-punctuation.sh.
232
274
  EOF
233
275
  exit 2
@@ -1,9 +1,14 @@
1
- // app/icon.ts serves /icon (the dynamic favicon). The default export is a
1
+ // app/icon.ts serves /icon (a dynamic favicon). The default export is a
2
2
  // (possibly async) server function; returning a Response lets you set the exact
3
- // content type, so an inline SVG needs no asset file. For a favicon that never
4
- // changes, put a static file in public/ instead (e.g. public/favicon.ico) and
5
- // delete this route. Generate it dynamically (per-theme, per-tenant) when the
6
- // mark must be computed at request time.
3
+ // content type, so an inline SVG needs no asset file. Generate it dynamically
4
+ // (per-theme, per-tenant) when the mark must be computed at request time.
5
+ //
6
+ // This is the DEMO of that surface, not the gallery's own favicon. A metadata
7
+ // route is not auto-linked: the framework emits `<link rel="icon">` only from
8
+ // metadata.icons, so the gallery declares the static WebJs brand mark from
9
+ // public/ there (see app/layout.ts) and this route stays browsable at /icon.
10
+ // For a favicon that never changes, that static path is the one to copy; drop
11
+ // this route when your app has no request-time mark to compute.
7
12
  export default function Icon() {
8
13
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
9
14
  <rect width="32" height="32" rx="7" fill="#1e2226"/>
@@ -31,7 +31,10 @@ export class DirectiveDemo extends WebComponent {
31
31
  // A controlled value (for `live`) and a key (for `keyed`).
32
32
  private text = signal('type here');
33
33
  private variant = signal(0);
34
- // A handle to the input node, attached by `ref` in the browser.
34
+ // A handle to the input node, attached by `ref` in the browser. A ref rather
35
+ // than a `querySelector` because the template already owns the node, so the
36
+ // handle flows out of `render()` instead of being re-found by a selector that
37
+ // could match someone else's markup (or nothing at all after a rename).
35
38
  private inputRef = createRef<HTMLInputElement>();
36
39
  // Created ONCE (not per render), so `until` keeps the resolved value across
37
40
  // re-renders instead of flashing back to the fallback each time.
@@ -6,6 +6,11 @@
6
6
  // active item from location.pathname, so the highlight follows soft-nav. SSR is
7
7
  // still correct: `render()` reads the `current` prop (the pathname the layout
8
8
  // passes) for the first paint, and the client takes over from location after.
9
+ // The document LISTENER below is the legitimate case, not a reach across the
10
+ // app: the router has no element to dispatch from, and the handler reads only
11
+ // location.pathname and writes only this module's own signal, so it queries
12
+ // nothing outside itself. Querying the document for another file's markup is
13
+ // the shape to avoid.
9
14
  import { WebComponent, prop, html, signal } from '@webjsdev/core';
10
15
  import { FEATURE_GROUPS } from '#modules/gallery/nav.ts';
11
16
 
@@ -63,7 +63,7 @@ export const FEATURE_GROUPS: NavGroup[] = [
63
63
  items: [
64
64
  { href: '/features/caching', title: 'Caching', blurb: 'export const revalidate caches the page HTML per URL, with the safety rule for when a shared cache is allowed.' },
65
65
  { href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
66
- { href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
66
+ { href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After once the interval resets.' },
67
67
  { href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
68
68
  { href: '/features/service-worker', title: 'Service worker', blurb: 'The opt-in offline enhancement, registered from a browser-only lifecycle hook (never a page or layout).' },
69
69
  ],
@@ -75,5 +75,14 @@ export const EXAMPLES: NavItem[] = [
75
75
  { href: '/examples/todo', title: 'Optimistic todo', blurb: 'A whole app composing several features: the declarative optimistic() list API, progressive-enhancement forms, accessible labels, the modules split, and SQLite.' },
76
76
  ];
77
77
 
78
- /** Flattened single-feature list (for the home card grid). */
79
- export const FEATURES: NavItem[] = FEATURE_GROUPS.flatMap((g) => g.items);
78
+ /**
79
+ * Flattened single-feature list (for the home card grid).
80
+ *
81
+ * This is a function, not a `const` initialised by a top-level `.flatMap()`
82
+ * call. A top-level call is a module side effect, so the const form pinned
83
+ * every page importing this module into the browser bundle, even a home page
84
+ * with no client behaviour at all. Call it inside the render function.
85
+ */
86
+ export function featureList(): NavItem[] {
87
+ return FEATURE_GROUPS.flatMap((g) => g.items);
88
+ }
@@ -21,6 +21,13 @@ import { deleteTodo } from './delete-todo.server.ts';
21
21
  // control's visible label), and a bound submitter cannot carry its own
22
22
  // `name`/`value`, which is exactly the channel `name="intent"` uses below.
23
23
  //
24
+ // Third thing to know: no `formaction` url is emitted, because the identity
25
+ // travels in the body instead. So the submission targets whatever the FORM
26
+ // targets, and a form declaring `action="/x"` sends its buttons there. The
27
+ // action still runs when `/x` is a PAGE route; against a `route.ts` or another
28
+ // origin the identity is ignored and nothing runs, which the dev-time client
29
+ // guard reports at submit time.
30
+ //
24
31
  // With JS the component intercepts the submit and calls the underlying action
25
32
  // directly for the optimistic path, so this runs only with JS off.
26
33
  export async function submitTodo(formData: FormData) {
@@ -20,6 +20,13 @@ import { createServer } from 'node:net';
20
20
  // minimal structural types keep the file typed in the meantime; swap them for
21
21
  // the real imports once puppeteer-core is in package.json. Reaching for `any`
22
22
  // here would silently un-type every call below.
23
+ //
24
+ // They also stay in force when the package IS present, which is the case a
25
+ // generated app usually hits, since @web/test-runner pulls puppeteer-core in
26
+ // transitively. The real Page / Browser are far richer than these, so letting
27
+ // them flow in would fail against the narrow shapes here for the goto return
28
+ // type and the event-handler signature. The single import below is the one
29
+ // boundary where that is resolved, and it is the only suppressed line.
23
30
  type Page = {
24
31
  // goto resolves an HTTPResponse this file never reads, and modelling that
25
32
  // type would mean re-declaring puppeteer's. Returning void is the honest
@@ -30,6 +37,10 @@ type Page = {
30
37
  removeAllListeners(event: string): void;
31
38
  };
32
39
  type Browser = { newPage(): Promise<Page>; close(): Promise<void> };
40
+ // The module's own default export, narrowed to the one call this file makes.
41
+ type Puppeteer = {
42
+ launch(opts: { executablePath?: string; headless?: boolean; args?: string[] }): Promise<Browser>;
43
+ };
33
44
 
34
45
  let browser: Browser, page: Page, serverProcess: ChildProcess, baseUrl: string;
35
46
 
@@ -46,9 +57,23 @@ function freePort(): Promise<number> {
46
57
  }
47
58
 
48
59
  before(async () => {
49
- let puppeteer;
60
+ let puppeteer: Puppeteer | undefined;
61
+ // The next line is where the optional dependency enters, and what it reports
62
+ // depends on whether puppeteer-core is installed: an unresolved specifier
63
+ // when it is absent, a type mismatch against the structural shapes above
64
+ // when it is present. Suppressing it keeps the rest of the file checked
65
+ // against those shapes either way.
66
+ //
67
+ // It is deliberately ts-ignore rather than the expect-error directive, whose
68
+ // name is spelled out here rather than written, because a comment line
69
+ // starting with that token IS a live directive to tsc even inside prose. The
70
+ // expect-error form is wrong on its own merits too: it errors when there is
71
+ // nothing to suppress, so it would break whenever the package resolves
72
+ // cleanly.
73
+ // @ts-ignore
50
74
  try { puppeteer = (await import('puppeteer-core')).default; }
51
75
  catch { console.log('# Skipping: puppeteer-core not installed'); return; }
76
+ if (!puppeteer) return;
52
77
 
53
78
  const port = await freePort();
54
79
  baseUrl = `http://localhost:${port}`;
@@ -1,38 +0,0 @@
1
- #!/bin/bash
2
- #
3
- # Cursor afterFileEdit hook.
4
- #
5
- # Counterpart of .claude/hooks/nudge-uncommitted.sh. After each
6
- # file edit, counts uncommitted changes in the working tree.
7
- # When the count crosses a threshold (default 4, override with
8
- # WEBJS_COMMIT_NUDGE_THRESHOLD), injects a reminder via the
9
- # top-level additional_context field (snake_case, unlike Claude
10
- # Code's nested hookSpecificOutput.additionalContext).
11
- #
12
- # Soft nudge. Exit 0 always. Skipped on main/master and outside
13
- # a git work tree.
14
-
15
- set -e
16
-
17
- THRESHOLD="${WEBJS_COMMIT_NUDGE_THRESHOLD:-4}"
18
-
19
- if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
20
- exit 0
21
- fi
22
-
23
- BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
24
- if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
25
- exit 0
26
- fi
27
-
28
- cat /dev/stdin >/dev/null 2>&1 || true
29
-
30
- CHANGED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
31
-
32
- if [ -z "$CHANGED" ] || [ "$CHANGED" -lt "$THRESHOLD" ]; then
33
- exit 0
34
- fi
35
-
36
- REASON="You have ${CHANGED} uncommitted changes on '${BRANCH}'. The webjs convention is small, focused commits per logical unit (one feature, one fix, one rename, one doc rewrite). Before continuing with more edits, group the current changes into a meaningful commit. See AGENTS.md \"Git workflow\" for the rule and the rationale. To raise the threshold for this hook in long-running tasks, set WEBJS_COMMIT_NUDGE_THRESHOLD."
37
-
38
- jq -n --arg ctx "$REASON" '{ additional_context: $ctx }'
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "hooks": {
4
- "afterFileEdit": [
5
- { "command": ".cursor/hooks/nudge-uncommitted.sh" }
6
- ]
7
- }
8
- }
@@ -1,21 +0,0 @@
1
- # WebJs app rules (Cursor)
2
-
3
- Cursor reads `AGENTS.md` natively. This file points you at it and the agent
4
- skill, and carries the commit rule.
5
-
6
- - **Read `AGENTS.md` first, then `.agents/skills/webjs/SKILL.md`** (the guide to
7
- building a WebJs app; it routes to focused references under
8
- `.agents/skills/webjs/references/`). These are required context, not optional
9
- reading: WebJs is not React, Next, or Lit, so gather this context before you
10
- write code.
11
- - **Study the shipped examples, then clear them and build.** The scaffold ships
12
- a browsable showcase to learn the real idioms from (a full-stack app ships a
13
- UI feature gallery, the api template ships a backend-features showcase). Read
14
- the parts that match your task, run `npm run gallery:clear` to shed the
15
- showcase and reset to a clean base, then grow the app in place: add routes
16
- under `app/`, features under `modules/<feature>/`, and keep server-only code
17
- behind `.server.ts`. `AGENTS.md` carries the full template-specific playbook.
18
- - **Use the wired-up database (Drizzle)** for persistence. Never a JSON file, an
19
- in-memory array, or localStorage.
20
- - **Commit per logical unit** as soon as it is complete, and never commit to
21
- `main`.
@@ -1,42 +0,0 @@
1
- #!/bin/bash
2
- #
3
- # Gemini CLI AfterTool hook.
4
- #
5
- # Counterpart of .claude/hooks/nudge-uncommitted.sh. After each
6
- # write_file or replace, counts uncommitted changes in the working
7
- # tree. When the count crosses a threshold (default 4, override
8
- # with the WEBJS_COMMIT_NUDGE_THRESHOLD env var), injects a
9
- # reminder via hookSpecificOutput.additionalContext (same shape
10
- # as Claude Code).
11
- #
12
- # Soft nudge. Does NOT block the edit (exit 0). Skipped on
13
- # main/master and outside a git work tree.
14
-
15
- set -e
16
-
17
- THRESHOLD="${WEBJS_COMMIT_NUDGE_THRESHOLD:-4}"
18
-
19
- if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
20
- exit 0
21
- fi
22
-
23
- BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
24
- if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
25
- exit 0
26
- fi
27
-
28
- cat /dev/stdin >/dev/null 2>&1 || true
29
-
30
- CHANGED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
31
-
32
- if [ -z "$CHANGED" ] || [ "$CHANGED" -lt "$THRESHOLD" ]; then
33
- exit 0
34
- fi
35
-
36
- REASON="You have ${CHANGED} uncommitted changes on '${BRANCH}'. The webjs convention is small, focused commits per logical unit (one feature, one fix, one rename, one doc rewrite). Before continuing with more edits, group the current changes into a meaningful commit. See AGENTS.md \"Git workflow\" for the rule and the rationale. To raise the threshold for this hook in long-running tasks, set WEBJS_COMMIT_NUDGE_THRESHOLD."
37
-
38
- jq -n --arg ctx "$REASON" '{
39
- hookSpecificOutput: {
40
- additionalContext: $ctx
41
- }
42
- }'
@@ -1,15 +0,0 @@
1
- {
2
- "hooks": {
3
- "AfterTool": [
4
- {
5
- "matcher": "write_file|replace",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": ".gemini/hooks/nudge-uncommitted.sh"
10
- }
11
- ]
12
- }
13
- ]
14
- }
15
- }
@@ -1,9 +0,0 @@
1
- # Copilot instructions
2
-
3
- This is a thin bridge to the single source. GitHub Copilot always reads this
4
- file; in VS Code it reads `AGENTS.md` directly only when `chat.useAgentsMdFile`
5
- is enabled, so this bridge keeps Copilot pointed at the instructions regardless.
6
-
7
- The instructions for this app live in `AGENTS.md` (the cross-agent source) and
8
- the skill at `.agents/skills/webjs/SKILL.md`. Read `AGENTS.md` first, then the
9
- skill (it routes to focused references on demand).
@@ -1,62 +0,0 @@
1
- /**
2
- * OpenCode commit-frequency nudge plugin.
3
- *
4
- * Counterpart of the Claude Code, Gemini CLI, and Cursor hooks in
5
- * `.claude/hooks/`, `.gemini/hooks/`, and `.cursor/hooks/`. After
6
- * each edit/write tool call, counts uncommitted changes in the
7
- * working tree. When the count crosses a threshold (default 4,
8
- * override with the WEBJS_COMMIT_NUDGE_THRESHOLD env var), appends
9
- * a reminder to the tool result so the agent sees it on the next
10
- * turn.
11
- *
12
- * Soft nudge by design. Does NOT block the edit. The goal is to
13
- * keep the agent honest about the "commit per logical unit" rule,
14
- * not to interrupt valid work.
15
- *
16
- * Skipped on main/master (different guard rules cover that) and
17
- * outside a git work tree.
18
- *
19
- * Auto-discovered by OpenCode at startup. No opencode.json entry
20
- * needed. Lives in .opencode/plugins/ at the project root.
21
- *
22
- * Docs: https://opencode.ai/docs/plugins/
23
- */
24
- import type { Plugin } from "@opencode-ai/plugin";
25
-
26
- export const NudgeUncommitted: Plugin = async ({ $ }) => {
27
- const THRESHOLD = Number(process.env.WEBJS_COMMIT_NUDGE_THRESHOLD ?? 4);
28
-
29
- return {
30
- "tool.execute.after": async (input, output) => {
31
- if (input.tool !== "edit" && input.tool !== "write") return;
32
-
33
- let branch = "";
34
- try {
35
- branch = (await $`git symbolic-ref --short HEAD`.text()).trim();
36
- } catch {
37
- return; // not in a git work tree
38
- }
39
- if (branch === "main" || branch === "master") return;
40
-
41
- let changed = 0;
42
- try {
43
- const out = (await $`git status --porcelain`.text()).trim();
44
- changed = out === "" ? 0 : out.split("\n").length;
45
- } catch {
46
- return;
47
- }
48
- if (changed < THRESHOLD) return;
49
-
50
- const reason =
51
- `[webjs] You have ${changed} uncommitted changes on '${branch}'. ` +
52
- `The webjs convention is small, focused commits per logical unit ` +
53
- `(one feature, one fix, one rename, one doc rewrite). Before ` +
54
- `continuing with more edits, group the current changes into a ` +
55
- `meaningful commit. See AGENTS.md "Git workflow" for the rule ` +
56
- `and the rationale. To raise the threshold for this hook in ` +
57
- `long-running tasks, set WEBJS_COMMIT_NUDGE_THRESHOLD.`;
58
-
59
- output.output = output.output ? `${output.output}\n\n${reason}` : reason;
60
- },
61
- };
62
- };
@@ -1,11 +0,0 @@
1
- # GEMINI.md
2
-
3
- Gemini CLI reads `GEMINI.md`, not `AGENTS.md`, by default, so this file is a
4
- thin bridge to the single source.
5
-
6
- The instructions for this app live in `AGENTS.md` (the cross-agent source) and
7
- the skill at `.agents/skills/webjs/SKILL.md`. Read `AGENTS.md` first, then the
8
- skill (it routes to focused references on demand).
9
-
10
- To have Gemini read `AGENTS.md` directly instead of this bridge, add it to
11
- `context.fileName` in `.gemini/settings.json`.