@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.
- package/README.md +2 -2
- package/bin/webjs.js +17 -0
- package/lib/app-name.js +73 -0
- package/lib/check-target.js +145 -0
- package/lib/create.js +42 -26
- package/lib/doctor.js +91 -16
- package/lib/gallery-shell-files.js +36 -0
- package/package.json +3 -3
- package/templates/.agents/skills/webjs/SKILL.md +6 -2
- package/templates/.agents/skills/webjs/references/built-ins.md +1 -1
- package/templates/.agents/skills/webjs/references/components.md +95 -3
- package/templates/.agents/skills/webjs/references/data-and-actions.md +29 -2
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +25 -1
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +24 -1
- package/templates/.agents/skills/webjs/references/styling.md +14 -1
- package/templates/.agents/skills/webjs/references/testing.md +8 -0
- package/templates/.agents/skills/webjs/references/typescript.md +17 -3
- package/templates/.agents/skills/webjs/references/ui-kit.md +15 -0
- package/templates/.claude/hooks/block-prose-punctuation.sh +66 -24
- package/templates/gallery/app/icon.ts +10 -5
- package/templates/gallery/modules/directives/components/directive-demo.ts +4 -1
- package/templates/gallery/modules/gallery/components/gallery-nav.ts +5 -0
- package/templates/gallery/modules/gallery/nav.ts +12 -3
- package/templates/gallery/modules/todo/actions/submit-todo.server.ts +7 -0
- package/templates/test/hello/e2e/hello.test.ts +26 -1
- package/templates/.cursor/hooks/nudge-uncommitted.sh +0 -38
- package/templates/.cursor/hooks.json +0 -8
- package/templates/.cursorrules +0 -21
- package/templates/.gemini/hooks/nudge-uncommitted.sh +0 -42
- package/templates/.gemini/settings.json +0 -15
- package/templates/.github/copilot-instructions.md +0 -9
- package/templates/.opencode/plugins/nudge-uncommitted.ts +0 -62
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
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
|
-
/**
|
|
79
|
-
|
|
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 }'
|
package/templates/.cursorrules
DELETED
|
@@ -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,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
|
-
};
|
package/templates/GEMINI.md
DELETED
|
@@ -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`.
|