@invisibleloop/pulse 0.12.0 → 0.12.2
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/.claude/pulse-checklist.md +51 -3
- package/docs/public/docs.css +5 -1
- package/docs/public/themes/observatory.css +1 -0
- package/docs/src/pages/getting-started.js +37 -14
- package/docs/src/pages/templates.js +61 -0
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/agent/checklist.md +16 -0
- package/src/agent/guide-components.md +1 -1
- package/src/agent/identity.md +7 -0
- package/src/agent/skills/build-page/SKILL.md +75 -0
- package/src/agent/skills/new-doc-page/SKILL.md +50 -0
- package/src/agent/skills/verify/SKILL.md +67 -0
- package/src/agent/workflow.md +6 -0
- package/src/cli/cli.test.js +29 -0
- package/src/cli/index.js +111 -18
- package/src/cli/scaffold.js +118 -4
|
@@ -4,16 +4,42 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
4
4
|
|
|
5
5
|
### Critical
|
|
6
6
|
|
|
7
|
-
- **`
|
|
7
|
+
- **`meta` must be a plain object — never a function.** Individual fields (`title`, `description`, `styles`) can be `async (ctx) => value` functions, but `meta` itself is always `{}`.
|
|
8
|
+
|
|
8
9
|
```js
|
|
9
|
-
|
|
10
|
+
// WRONG — meta is not a function factory
|
|
11
|
+
meta: async (ctx) => ({ title: '...', styles: [...] })
|
|
12
|
+
|
|
13
|
+
// CORRECT — meta is a plain object; individual fields are functions
|
|
14
|
+
meta: {
|
|
15
|
+
title: async (ctx) => (await post(ctx)).frontmatter.title,
|
|
16
|
+
description: async (ctx) => (await post(ctx)).frontmatter.description,
|
|
17
|
+
styles: ['/pulse-ui.css', '/app.css'],
|
|
18
|
+
}
|
|
10
19
|
```
|
|
11
|
-
|
|
20
|
+
|
|
21
|
+
- **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
|
|
22
|
+
|
|
23
|
+
- **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
|
|
12
24
|
|
|
13
25
|
### Components first
|
|
14
26
|
|
|
15
27
|
- **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
|
|
16
28
|
|
|
29
|
+
- **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { appBadge } from '@invisibleloop/pulse/ui'
|
|
33
|
+
|
|
34
|
+
// In a hero() actions slot:
|
|
35
|
+
${hero({
|
|
36
|
+
actions: `
|
|
37
|
+
${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
|
|
38
|
+
${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
|
|
39
|
+
`,
|
|
40
|
+
})}
|
|
41
|
+
```
|
|
42
|
+
|
|
17
43
|
### Reuse (DRY)
|
|
18
44
|
|
|
19
45
|
- **Extract a view helper when the same HTML pattern appears 3 or more times in a single spec.** A plain JS function returning an HTML string is sufficient — no framework needed:
|
|
@@ -56,6 +82,11 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
56
82
|
- **Validate FormData fields before use.** `formData.get('email')` returns `null` if the field is missing. Check for null/empty before passing to an API or database.
|
|
57
83
|
- **Do not trust URL params.** `ctx.params.id` is a raw string from the URL. Validate it before use — check it exists, is the right type, and refers to a real resource. Return a 404 or redirect if it doesn't.
|
|
58
84
|
|
|
85
|
+
### CSS
|
|
86
|
+
|
|
87
|
+
- **`app.css` must contain no hex values or raw colour values.** A lint hook enforces this and will block the build. Hex values belong in `public/theme.css` as token definitions; `app.css` references them via `var()` only.
|
|
88
|
+
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
89
|
+
|
|
59
90
|
### Security
|
|
60
91
|
|
|
61
92
|
- Any value from user input (URL params, form fields, external APIs) interpolated into view HTML must be escaped.
|
|
@@ -81,6 +112,12 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
81
112
|
```
|
|
82
113
|
Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, and combinations (`button.primary[disabled]`).
|
|
83
114
|
|
|
115
|
+
### Markdown
|
|
116
|
+
|
|
117
|
+
- **Use `md()` from `@invisibleloop/pulse/md` for `.md` file content.** Never read and parse markdown manually. Pass the result's `html` to `prose()`.
|
|
118
|
+
- **Call the same `md()` fetcher in both `meta` and `server`** — it caches on `ctx._mdCache` so the file is only read once per request. Do not create two separate fetchers for the same file.
|
|
119
|
+
- **Always add `onViewError` on dynamic markdown routes** (routes with `:param` segments loading `.md` files). A missing file throws `{ status: 404 }` which must be caught gracefully.
|
|
120
|
+
|
|
84
121
|
### View error handling
|
|
85
122
|
|
|
86
123
|
- **Define `onViewError` on any page where the view could throw due to bad or missing data.** Without it, a runtime view error returns a 500 on the server and shows a generic inline message on the client. With it, the server returns 200 with your fallback HTML instead of a 500, and the client renders your fallback:
|
|
@@ -109,3 +146,14 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
109
146
|
- Interactive elements without visible text have an `aria-label`.
|
|
110
147
|
- Disabled state is reflected with the `disabled` attribute, not just CSS.
|
|
111
148
|
- The page has a `<main id="main-content">` landmark.
|
|
149
|
+
- **Every element with `data-event`, `data-store-event`, `data-dialog-open`, or `data-dialog-close` is natively interactive (`button`, `a`, `input`, `select`, `textarea`, `summary`) or has `tabindex="0"`.** Non-interactive tags (`div`, `span`, `li`, `td`, etc.) with these attributes are not keyboard reachable. Prefer `<button>` — it is focusable, fires on Enter/Space, and carries the correct ARIA role for free. Only use `tabindex="0"` on a non-interactive element when a `<button>` genuinely cannot be used.
|
|
150
|
+
- **Every interactive element communicates its purpose and state:**
|
|
151
|
+
- Buttons have visible text or `aria-label`. Links use descriptive text — "click here", "here", "read more" are failures.
|
|
152
|
+
- Form inputs have an associated `<label for="id">` or `aria-label`. `placeholder` alone is not a label.
|
|
153
|
+
- Toggle controls (open/close, show/hide, expand/collapse) carry `aria-expanded="true|false"` or `aria-pressed="true|false"`.
|
|
154
|
+
- While an action is in flight, the trigger button has `aria-busy="true"` or its label changes (e.g. "Saving…"). A spinner alone is not enough.
|
|
155
|
+
- Active nav items have `aria-current="page"`. Selected items in a list, tab set, or option group have `aria-selected="true"`.
|
|
156
|
+
- **The page has a logical tab order:**
|
|
157
|
+
- No `tabindex` value greater than 0. Positive tabindex values override the natural DOM order and create a broken tab sequence. Remove them and reorder the DOM instead.
|
|
158
|
+
- Visually hidden interactive content (off-canvas menus, collapsed panels, `opacity:0` elements) is removed from the tab order with `tabindex="-1"` or the `inert` attribute — CSS alone does not remove an element from tab order.
|
|
159
|
+
- DOM order matches the visual reading order. When flexbox `order` or CSS grid placement visually reposition elements, the tab sequence still follows the DOM — author the DOM in the order a user would read and interact with the page.
|
package/docs/public/docs.css
CHANGED
|
@@ -1375,7 +1375,7 @@ a:hover {
|
|
|
1375
1375
|
top: 0;
|
|
1376
1376
|
left: 0;
|
|
1377
1377
|
width: var(--sidebar-w);
|
|
1378
|
-
height:
|
|
1378
|
+
height: 100dvh;
|
|
1379
1379
|
overflow-y: auto;
|
|
1380
1380
|
background: var(--surface);
|
|
1381
1381
|
border-right: 1px solid var(--border);
|
|
@@ -1941,6 +1941,10 @@ hr.doc-hr {
|
|
|
1941
1941
|
padding: 2rem 1.25rem 4rem;
|
|
1942
1942
|
}
|
|
1943
1943
|
|
|
1944
|
+
.sidebar-nav {
|
|
1945
|
+
padding-bottom: 6rem;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1944
1948
|
.doc-h1 {
|
|
1945
1949
|
font-size: 1.75rem;
|
|
1946
1950
|
}
|
|
@@ -17,16 +17,19 @@ export default {
|
|
|
17
17
|
next,
|
|
18
18
|
content: `
|
|
19
19
|
${h1('Getting Started')}
|
|
20
|
-
${lead('Install Pulse, run one command, and have
|
|
20
|
+
${lead('Install Pulse, run one command, and have an AI agent building your first page — with streaming SSR, security headers, and a 100 Lighthouse score already in place.')}
|
|
21
21
|
|
|
22
22
|
${section('requirements', 'Requirements')}
|
|
23
23
|
<ul>
|
|
24
24
|
<li><strong>Node.js 22 or later</strong> — <a href="https://nodejs.org" target="_blank" rel="noopener" aria-label="nodejs.org (opens in new tab)">nodejs.org</a></li>
|
|
25
25
|
<li><strong>Google Chrome</strong> — used by the agent for screenshots and Lighthouse audits — <a href="https://www.google.com/chrome" target="_blank" rel="noopener" aria-label="Download Google Chrome (opens in new tab)">google.com/chrome</a></li>
|
|
26
|
-
<li><strong>
|
|
26
|
+
<li><strong>An AI agent</strong> — Claude Code or GitHub Copilot CLI (pick one):</li>
|
|
27
27
|
</ul>
|
|
28
|
-
<
|
|
29
|
-
|
|
28
|
+
<ul>
|
|
29
|
+
<li><strong>Claude Code</strong> — <a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" target="_blank" rel="noopener" aria-label="Claude Code installation guide (opens in new tab)">installation guide</a> — the default; no extra config needed</li>
|
|
30
|
+
<li><strong>GitHub Copilot CLI</strong> — <a href="https://docs.github.com/copilot/concepts/agents/about-copilot-cli" target="_blank" rel="noopener" aria-label="GitHub Copilot CLI documentation (opens in new tab)">documentation</a> — use with the <code>--agent copilot</code> flag</li>
|
|
31
|
+
</ul>
|
|
32
|
+
<p>Pulse launches your chosen agent automatically with the Pulse MCP server wired in — so the agent has instant access to the framework reference, your project structure, and all Pulse tools without any manual configuration.</p>
|
|
30
33
|
|
|
31
34
|
${section('install', 'Install Pulse')}
|
|
32
35
|
<p>Install the Pulse CLI globally:</p>
|
|
@@ -44,8 +47,17 @@ pulse`, 'bash'))}
|
|
|
44
47
|
${section('session', 'Start a session')}
|
|
45
48
|
<p>Run <code>pulse</code> again from inside your project directory:</p>
|
|
46
49
|
${codeBlock(highlight('pulse', 'bash'))}
|
|
47
|
-
<p>
|
|
48
|
-
${callout('note', 'Run <code>pulse</code> every time you open a working session. It handles starting
|
|
50
|
+
<p>Pulse detects the existing project and launches Claude Code with the Pulse MCP server already connected. Claude opens with the complete framework guide loaded, your project structure visible, and all Pulse tools available — ready to build immediately.</p>
|
|
51
|
+
${callout('note', 'Run <code>pulse</code> every time you open a working session. It handles starting the agent and wiring up the MCP server. Once the agent is open, use <code>/pulse-dev</code> (Claude) or the <code>build-page</code> skill (Copilot) to start the dev server.')}
|
|
52
|
+
|
|
53
|
+
${section('copilot', 'Using GitHub Copilot')}
|
|
54
|
+
<p>To use GitHub Copilot CLI instead of Claude, pass the <code>--agent</code> flag when scaffolding:</p>
|
|
55
|
+
${codeBlock(highlight('pulse --agent copilot', 'bash'))}
|
|
56
|
+
<p>This scaffolds the project and writes <code>agent: \'copilot\'</code> into <code>pulse.config.js</code> — so subsequent <code>pulse</code> runs pick up the preference automatically. You can also set it manually in <code>pulse.config.js</code> at any time:</p>
|
|
57
|
+
${codeBlock(highlight(`export default {
|
|
58
|
+
agent: 'copilot',
|
|
59
|
+
}`, 'js'))}
|
|
60
|
+
<p>When using Copilot, project skills (<code>build-page</code>, <code>verify</code>, <code>new-doc-page</code>) are available via the <code>/skills</code> command inside the Copilot CLI session. The Pulse MCP server is automatically injected into your Copilot MCP config for the duration of the session and removed cleanly on exit.</p>
|
|
49
61
|
|
|
50
62
|
${section('first-build', 'Build your first page')}
|
|
51
63
|
<p>Once Claude opens, start the dev server and ask for something:</p>
|
|
@@ -74,7 +86,7 @@ Validate the email format before submitting."`, 'bash'))}
|
|
|
74
86
|
│ ├── app.css ← global stylesheet
|
|
75
87
|
│ ├── pulse-ui.css ← Pulse component library styles
|
|
76
88
|
│ └── pulse-ui.js ← Pulse component library behaviour
|
|
77
|
-
├── .claude/
|
|
89
|
+
├── .claude/ ← Claude Code agent context
|
|
78
90
|
│ ├── CLAUDE.md ← session instructions Claude reads on startup
|
|
79
91
|
│ ├── settings.json ← hooks: syntax checks, colour guards, package blocklist
|
|
80
92
|
│ ├── pulse-checklist.md ← spec review checklist, kept in sync by Pulse
|
|
@@ -83,10 +95,19 @@ Validate the email format before submitting."`, 'bash'))}
|
|
|
83
95
|
│ ├── pulse-stop.md
|
|
84
96
|
│ ├── pulse-build.md
|
|
85
97
|
│ └── pulse-start.md
|
|
98
|
+
├── .copilot/ ← GitHub Copilot CLI agent context
|
|
99
|
+
│ └── skills/ ← project skills available inside Copilot
|
|
100
|
+
│ ├── build-page/
|
|
101
|
+
│ ├── verify/
|
|
102
|
+
│ └── new-doc-page/
|
|
103
|
+
├── .github/
|
|
104
|
+
│ ├── copilot-instructions.md ← session instructions for Copilot
|
|
105
|
+
│ └── instructions/
|
|
106
|
+
│ └── pulse-checklist.instructions.md
|
|
86
107
|
├── package.json
|
|
87
|
-
└── pulse.config.js ← port and project settings`, 'bash'))}
|
|
88
|
-
<p><code>src/pages/home.js</code> is a complete working spec — a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after
|
|
89
|
-
<p>The <code>.claude/</code> directory contains the agent's operating context. <code
|
|
108
|
+
└── pulse.config.js ← port, agent, and project settings`, 'bash'))}
|
|
109
|
+
<p><code>src/pages/home.js</code> is a complete working spec — a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after starting the dev server to see it. Every new page you create goes into <code>src/pages/</code> and is discovered automatically.</p>
|
|
110
|
+
<p>The <code>.claude/</code> directory contains the Claude agent's operating context. The <code>.copilot/</code> directory contains the equivalent context for GitHub Copilot — project skills and instructions. Both are scaffolded by default so you can switch agents at any time.</p>
|
|
90
111
|
|
|
91
112
|
${section('commands', 'Agent commands')}
|
|
92
113
|
<p>These slash commands are available once Claude is open:</p>
|
|
@@ -95,7 +116,8 @@ Validate the email format before submitting."`, 'bash'))}
|
|
|
95
116
|
/pulse-build # production build → public/dist/
|
|
96
117
|
/pulse-start # run the production server
|
|
97
118
|
/pulse-report # Lighthouse audit + performance report`, 'bash'))}
|
|
98
|
-
<p>
|
|
119
|
+
<p>When using GitHub Copilot CLI, the equivalent capabilities are available as project skills. Type <code>/skills</code> inside Copilot to browse them — <code>build-page</code>, <code>verify</code>, and <code>new-doc-page</code> are all pre-installed.</p>
|
|
120
|
+
<p>You can also skip commands and skills entirely — just describe what you want and the agent handles the rest.</p>
|
|
99
121
|
|
|
100
122
|
${section('update', 'Keeping up to date')}
|
|
101
123
|
<p>When a new version of Pulse is released, update the package and then run <code>pulse update</code> from your project root:</p>
|
|
@@ -104,10 +126,11 @@ pulse update`, 'bash'))}
|
|
|
104
126
|
<p><code>pulse update</code> copies the latest UI assets and agent files into your project:</p>
|
|
105
127
|
<ul>
|
|
106
128
|
<li><code>public/pulse-ui.css</code> and <code>public/pulse-ui.js</code> — component library styles and behaviour</li>
|
|
107
|
-
<li><code>.claude/pulse-checklist.md</code> —
|
|
108
|
-
<li><code>.
|
|
129
|
+
<li><code>.claude/pulse-checklist.md</code> and <code>.claude/commands/</code> — Claude agent files</li>
|
|
130
|
+
<li><code>.copilot/skills/</code> — Copilot project skills</li>
|
|
131
|
+
<li><code>.github/instructions/pulse-checklist.instructions.md</code> — Copilot checklist</li>
|
|
109
132
|
</ul>
|
|
110
|
-
<p>Your own source files, <code>CLAUDE.md</code>, and <code>pulse.config.js</code> are never touched.</p>
|
|
133
|
+
<p>Your own source files, <code>CLAUDE.md</code>, <code>.github/copilot-instructions.md</code>, and <code>pulse.config.js</code> are never touched.</p>
|
|
111
134
|
|
|
112
135
|
${section('next-steps', 'Next steps')}
|
|
113
136
|
<ul>
|
|
@@ -111,6 +111,53 @@ const mobileAppPreview = `
|
|
|
111
111
|
</div>
|
|
112
112
|
</div>`
|
|
113
113
|
|
|
114
|
+
const blogPostPreview = `
|
|
115
|
+
<div style="display:flex;flex-direction:column;gap:6px;font-size:0;background:#0a2239;border-radius:4px;padding:8px">
|
|
116
|
+
|
|
117
|
+
<!-- nav -->
|
|
118
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
119
|
+
${bar('20px', 6, '#53a2be')}
|
|
120
|
+
${bar('36px', 4, '#53a2be', 2)}
|
|
121
|
+
<div style="flex:1"></div>
|
|
122
|
+
${bar('28px', 4)} ${bar('28px', 4)} ${bar('28px', 4)}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- hero -->
|
|
126
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 0 6px;background:linear-gradient(160deg,#0a2239 0%,#132e32 100%);border-radius:3px;margin-top:2px">
|
|
127
|
+
${bar('40px', 3, '#53a2be')}
|
|
128
|
+
${bar('90%', 8, 'rgba(255,255,255,0.7)', 2)}
|
|
129
|
+
${bar('75%', 8, 'rgba(255,255,255,0.7)', 2)}
|
|
130
|
+
${bar('65%', 4, 'rgba(83,162,190,0.6)', 2)}
|
|
131
|
+
${bar('50%', 3, 'rgba(83,162,190,0.4)', 2)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- article + aside -->
|
|
135
|
+
<div style="display:flex;gap:6px;margin-top:2px">
|
|
136
|
+
<div style="flex:1;display:flex;flex-direction:column;gap:4px">
|
|
137
|
+
${bar('100%', 48, 'rgba(83,162,190,0.15)', 3)}
|
|
138
|
+
${bar('100%', 4, 'rgba(255,255,255,0.5)', 1)}
|
|
139
|
+
${bar('90%', 4, 'rgba(255,255,255,0.5)', 1)}
|
|
140
|
+
${bar('95%', 4, 'rgba(255,255,255,0.5)', 1)}
|
|
141
|
+
${bar('60%', 4, 'rgba(255,255,255,0.5)', 1)}
|
|
142
|
+
${bar('55%', 6, 'rgba(255,255,255,0.7)', 1)}
|
|
143
|
+
${bar('100%', 3, 'rgba(255,255,255,0.4)', 1)}
|
|
144
|
+
${bar('85%', 3, 'rgba(255,255,255,0.4)', 1)}
|
|
145
|
+
</div>
|
|
146
|
+
<div style="width:38px;display:flex;flex-direction:column;gap:3px">
|
|
147
|
+
${bar('100%', 4, 'rgba(255,255,255,0.5)', 1)}
|
|
148
|
+
${bar('100%', 3, 'rgba(83,162,190,0.6)', 1)}
|
|
149
|
+
${bar('100%', 3, 'rgba(83,162,190,0.6)', 1)}
|
|
150
|
+
${bar('100%', 3, 'rgba(83,162,190,0.6)', 1)}
|
|
151
|
+
<div style="margin-top:3px;background:#132e32;border-radius:3px;padding:3px;display:flex;flex-direction:column;gap:2px">
|
|
152
|
+
${bar('60%', 3, '#53a2be')}
|
|
153
|
+
${bar('100%', 5, 'rgba(255,255,255,0.6)', 1)}
|
|
154
|
+
${bar('100%', 5, 'rgba(255,255,255,0.6)', 1)}
|
|
155
|
+
${bar('100%', 10, 'rgba(83,162,190,0.5)', 2)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>`
|
|
160
|
+
|
|
114
161
|
const TEMPLATES = [
|
|
115
162
|
{
|
|
116
163
|
name: 'Mobile App Landing Page',
|
|
@@ -126,6 +173,20 @@ const TEMPLATES = [
|
|
|
126
173
|
'JS': '~4.2 kB',
|
|
127
174
|
},
|
|
128
175
|
},
|
|
176
|
+
{
|
|
177
|
+
name: 'Blog Post',
|
|
178
|
+
route: '/templates/blog-post',
|
|
179
|
+
description: 'A full editorial article page — sticky nav, article hero, prose with image, two-column layout with table-of-contents sidebar, newsletter signup, and footer. Dark astronomy theme.',
|
|
180
|
+
tags: ['Blog', 'Editorial'],
|
|
181
|
+
preview: blogPostPreview,
|
|
182
|
+
stats: {
|
|
183
|
+
'Accessibility': '100',
|
|
184
|
+
'Best Practices': '100',
|
|
185
|
+
'SEO': '100',
|
|
186
|
+
'CLS': '0.00',
|
|
187
|
+
'JS': '~4.2 kB',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
129
190
|
]
|
|
130
191
|
|
|
131
192
|
const content = `
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.12.
|
|
1
|
+
0.12.2
|
package/src/agent/checklist.md
CHANGED
|
@@ -20,10 +20,26 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
20
20
|
|
|
21
21
|
- **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
|
|
22
22
|
|
|
23
|
+
- **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
|
|
24
|
+
|
|
23
25
|
### Components first
|
|
24
26
|
|
|
25
27
|
- **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
|
|
26
28
|
|
|
29
|
+
- **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { appBadge } from '@invisibleloop/pulse/ui'
|
|
33
|
+
|
|
34
|
+
// In a hero() actions slot:
|
|
35
|
+
${hero({
|
|
36
|
+
actions: `
|
|
37
|
+
${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
|
|
38
|
+
${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
|
|
39
|
+
`,
|
|
40
|
+
})}
|
|
41
|
+
```
|
|
42
|
+
|
|
27
43
|
### Reuse (DRY)
|
|
28
44
|
|
|
29
45
|
- **Extract a view helper when the same HTML pattern appears 3 or more times in a single spec.** A plain JS function returning an HTML string is sufficient — no framework needed:
|
|
@@ -153,7 +153,7 @@ Use `prose()` for HTML from external sources (CMS, markdown). Use `heading()`/`l
|
|
|
153
153
|
| `testimonial` | `quote`, `name`, `role`, `src`, `rating` (1–5) |
|
|
154
154
|
| `pricing` | `name`, `level` (1–6, default 3), `price`, `period`, `features` ([strings]), `action` (HTML slot), `highlighted` |
|
|
155
155
|
| `accordion` | `items` ([{question,answer}]) — native `<details>`, no JS |
|
|
156
|
-
| `appBadge` | `store` (apple/google), `href` |
|
|
156
|
+
| `appBadge` | `store` (apple/google), `href` — **always use this for App Store / Google Play download buttons. Never write a raw `<a>` with an SVG badge image.** Designed to sit in a `hero()` `actions` slot or anywhere a download CTA is needed. |
|
|
157
157
|
| `cta` | `eyebrow`, `title`, `level` (1–6, default 2), `subtitle`, `actions` (HTML slot), `align` |
|
|
158
158
|
|
|
159
159
|
### Layout components
|
package/src/agent/identity.md
CHANGED
|
@@ -62,6 +62,13 @@ Every build task follows a fixed sequence of phases with explicit pass gates. Fe
|
|
|
62
62
|
|
|
63
63
|
You work exclusively inside the Pulse project directory. You do not read or modify files outside that directory.
|
|
64
64
|
|
|
65
|
+
**After writing any file, always refresh before checking the result.** The dev server restarts automatically on file changes (node --watch), but the browser does not. The sequence after every edit is:
|
|
66
|
+
1. Call `pulse_restart_server` — waits for the server to be ready
|
|
67
|
+
2. Call `navigate_page` with the page URL — reloads the browser tab
|
|
68
|
+
3. Only then take a screenshot or check for errors
|
|
69
|
+
|
|
70
|
+
Never attempt to diagnose a visual problem without first doing these two steps. A stale browser tab is not a bug — it is an unrefreshed tab.
|
|
71
|
+
|
|
65
72
|
When a shell command fails, you diagnose the root cause before retrying. You never retry the same failing command more than once. If a command fails due to permissions, auth, or token scope issues — these are unrecoverable without user action. Stop immediately, explain what failed and why, and tell the user exactly what they need to do to unblock it. Do not attempt workarounds that will also fail.
|
|
66
73
|
|
|
67
74
|
Before installing any npm package, check whether the task can be accomplished with Node.js built-ins or code already in the project. Only install a package if there is no reasonable built-in alternative.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-page
|
|
3
|
+
description: "Scaffold a new Pulse page spec, validate it, and visually confirm it renders correctly. $ARGUMENTS Read `src/ui/index.js` to see what UI components are available before writing any HTML. Note which components apply to this page."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build Page
|
|
7
|
+
|
|
8
|
+
Scaffold a new Pulse page spec, validate it, and visually confirm it renders correctly.
|
|
9
|
+
|
|
10
|
+
## What to build
|
|
11
|
+
|
|
12
|
+
$ARGUMENTS
|
|
13
|
+
|
|
14
|
+
## Steps — follow in order
|
|
15
|
+
|
|
16
|
+
### 1. Check existing components
|
|
17
|
+
|
|
18
|
+
Read `src/ui/index.js` to see what UI components are available before writing any HTML. Note which components apply to this page.
|
|
19
|
+
|
|
20
|
+
### 2. Plan
|
|
21
|
+
|
|
22
|
+
State before writing any files:
|
|
23
|
+
- The route (e.g. `/dashboard`)
|
|
24
|
+
- The file path (e.g. `src/pages/dashboard.js`)
|
|
25
|
+
- State shape
|
|
26
|
+
- Mutations and/or actions needed
|
|
27
|
+
- Which UI components will be used
|
|
28
|
+
- Whether `server` data fetchers are needed
|
|
29
|
+
|
|
30
|
+
### 3. Build the spec
|
|
31
|
+
|
|
32
|
+
Write the spec file. Follow these rules:
|
|
33
|
+
- Use `class` not `className`, template literals not JSX
|
|
34
|
+
- No React imports, hooks, or patterns
|
|
35
|
+
- Use components from `src/ui/` — do not hand-write HTML that a component already covers
|
|
36
|
+
- Use optional chaining (`?.`) on any server data that could be null
|
|
37
|
+
- Escape any user-supplied values interpolated into HTML
|
|
38
|
+
|
|
39
|
+
### 4. Validate
|
|
40
|
+
|
|
41
|
+
Use `pulse_validate` on the spec. Fix any errors before continuing.
|
|
42
|
+
|
|
43
|
+
### 5. Register with the server
|
|
44
|
+
|
|
45
|
+
Check the project's server entry file (e.g. `src/server.js` or `pulse.config.js`) and add the new spec if needed.
|
|
46
|
+
|
|
47
|
+
### 6. Screenshot
|
|
48
|
+
|
|
49
|
+
Use the chrome-devtools navigate_page tool to load the page, then take_screenshot.
|
|
50
|
+
|
|
51
|
+
Check for:
|
|
52
|
+
- Page renders without a blank screen or error
|
|
53
|
+
- Layout looks correct
|
|
54
|
+
- No console errors via list_console_messages
|
|
55
|
+
|
|
56
|
+
### 7. Lighthouse
|
|
57
|
+
|
|
58
|
+
Run lighthouse_audit (desktop, then mobile). All four scores — Accessibility, Best Practices, SEO, Performance — must be 100 before continuing.
|
|
59
|
+
|
|
60
|
+
### 8. Run the checklist
|
|
61
|
+
|
|
62
|
+
Go through `.github/instructions/pulse-checklist.instructions.md` and confirm every applicable point passes. Fix anything that fails.
|
|
63
|
+
|
|
64
|
+
### 9. Write tests
|
|
65
|
+
|
|
66
|
+
Write a `src/pages/<name>.test.js` covering:
|
|
67
|
+
- View renders expected HTML landmarks
|
|
68
|
+
- Each mutation as a pure function
|
|
69
|
+
- Any utility functions the page defines
|
|
70
|
+
|
|
71
|
+
Run them with `node src/pages/<name>.test.js` and fix every failure.
|
|
72
|
+
|
|
73
|
+
### 10. Report
|
|
74
|
+
|
|
75
|
+
Summarise what was built, what components were used, and confirm validation + visual check passed.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: new-doc-page
|
|
3
|
+
description: "Create a new documentation page for the Pulse docs site. $ARGUMENTS - Read `docs/src/lib/nav.js` to understand the full nav structure and find where the new page fits"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# New Docs Page
|
|
7
|
+
|
|
8
|
+
Create a new documentation page for the Pulse docs site.
|
|
9
|
+
|
|
10
|
+
## What to build
|
|
11
|
+
|
|
12
|
+
$ARGUMENTS
|
|
13
|
+
|
|
14
|
+
## Steps — follow in order
|
|
15
|
+
|
|
16
|
+
### 1. Research
|
|
17
|
+
- Read `docs/src/lib/nav.js` to understand the full nav structure and find where the new page fits
|
|
18
|
+
- Read 2–3 adjacent pages (the pages that will be `prev`/`next` neighbours) to match tone, section depth, and helper usage (`h1`, `lead`, `section`, `sub`, `codeBlock`, `callout`, `table`)
|
|
19
|
+
- Read `docs/src/lib/layout.js` exported functions to confirm available helpers
|
|
20
|
+
- If the feature being documented has a source file (e.g. in `src/`), read it to ensure accuracy
|
|
21
|
+
|
|
22
|
+
### 2. Plan
|
|
23
|
+
Before writing any files, state:
|
|
24
|
+
- The route (e.g. `/request-bodies`)
|
|
25
|
+
- Where it sits in the nav (section, between which two pages)
|
|
26
|
+
- Sections the page will cover (IDs + titles)
|
|
27
|
+
- Any doc pages that need updating to link to or mention this page
|
|
28
|
+
|
|
29
|
+
Wait for approval if anything is uncertain.
|
|
30
|
+
|
|
31
|
+
### 3. Implement
|
|
32
|
+
|
|
33
|
+
**a) Write the page file** at `docs/src/pages/<name>.js`:
|
|
34
|
+
- Import from `../lib/layout.js`, `../lib/nav.js`, `../lib/highlight.js`
|
|
35
|
+
- Use `prevNext('/your-route')` for prev/next links
|
|
36
|
+
- Export a valid Pulse spec as `export default { route, meta, state, view }`
|
|
37
|
+
- `meta.styles` must be `['/docs.css']`
|
|
38
|
+
- Use `renderLayout({ currentHref, prev, next, content })` for the page body
|
|
39
|
+
- All code examples must use `${codeBlock(highlight(\`...\`, 'js'))}` — never raw `<pre>`
|
|
40
|
+
- Tables use `${table(['Col1', 'Col2'], [['val', 'val']])}`
|
|
41
|
+
- Callouts use `${callout('note|tip|warning|info', 'text')}`
|
|
42
|
+
- Section anchors use `${section('anchor-id', 'Section Title')}`
|
|
43
|
+
|
|
44
|
+
**b) Register in nav** — add the entry to `docs/src/lib/nav.js` in the correct position
|
|
45
|
+
|
|
46
|
+
**c) Verify syntax** — run `node --check docs/src/pages/<name>.js`
|
|
47
|
+
|
|
48
|
+
**d) Check neighbours** — read the prev and next pages. If they reference topics this new page covers, add a cross-link.
|
|
49
|
+
|
|
50
|
+
**e) Final check** — confirm the new route appears in `prevNext` output for its neighbours (the nav order drives prev/next automatically, so adding to NAV is sufficient).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify
|
|
3
|
+
description: "Run the full verification loop on the current page or the page specified in $ARGUMENTS. If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify the most recently edited spec file from context. Use `pulse_validate` with the spec file content. If validation fails, report every error clearly and stop — do not proceed to screenshot until the spec is valid."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify
|
|
7
|
+
|
|
8
|
+
Run the full verification loop on the current page or the page specified in $ARGUMENTS.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
### 1. Identify the target
|
|
13
|
+
|
|
14
|
+
If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify the most recently edited spec file from context.
|
|
15
|
+
|
|
16
|
+
### 2. Validate the spec
|
|
17
|
+
|
|
18
|
+
Use `pulse_validate` with the spec file content. If validation fails, report every error clearly and stop — do not proceed to screenshot until the spec is valid.
|
|
19
|
+
|
|
20
|
+
### 3. Check the dev server
|
|
21
|
+
|
|
22
|
+
Use `pulse_fetch_page` to confirm the server is responding for the route. If it errors, use `pulse_restart_server` and retry once.
|
|
23
|
+
|
|
24
|
+
### 4. Screenshot
|
|
25
|
+
|
|
26
|
+
Use the chrome-devtools navigate_page tool to load the page route, then take_screenshot to capture the result. Describe what you see — layout, content, any obvious rendering issues.
|
|
27
|
+
|
|
28
|
+
### 5. Lighthouse — desktop
|
|
29
|
+
|
|
30
|
+
Run lighthouse_audit on the route in desktop mode.
|
|
31
|
+
|
|
32
|
+
**Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Report the actual scores. If any score is below 100, identify the failing audit(s), fix the issue, and restart from step 2.
|
|
33
|
+
|
|
34
|
+
### 6. Lighthouse — mobile
|
|
35
|
+
|
|
36
|
+
Run lighthouse_audit on the same route in mobile mode.
|
|
37
|
+
|
|
38
|
+
**Same pass bar: all three categories must be 100.** Report the actual scores. If any score is below 100, fix and restart from step 2.
|
|
39
|
+
|
|
40
|
+
### 7. Performance
|
|
41
|
+
|
|
42
|
+
Navigate to the page route, then run performance_start_trace with `reload: true` and `autoStop: true`. Report LCP and CLS. Flag any LCP insight that suggests a fixable problem (render-blocking resources, large image load delay, etc.).
|
|
43
|
+
|
|
44
|
+
### 8. Console errors
|
|
45
|
+
|
|
46
|
+
Use list_console_messages — report any errors or unexpected warnings.
|
|
47
|
+
|
|
48
|
+
### 9. Code review
|
|
49
|
+
|
|
50
|
+
Use `pulse_review` to switch into reviewer mode. Read the spec against the checklist in `.github/instructions/pulse-checklist.instructions.md`. Work through every item. Fix anything that fails before proceeding.
|
|
51
|
+
|
|
52
|
+
### 10. Close the browser
|
|
53
|
+
|
|
54
|
+
Use list_pages to get all open pages, then call close_page for every page ID returned.
|
|
55
|
+
|
|
56
|
+
### 11. Report
|
|
57
|
+
|
|
58
|
+
Summarise:
|
|
59
|
+
- Validation: pass or fail (with errors if any)
|
|
60
|
+
- Visual: what was visible in the screenshot
|
|
61
|
+
- Lighthouse desktop: scores for all three categories
|
|
62
|
+
- Lighthouse mobile: scores for all three categories
|
|
63
|
+
- Performance: LCP and CLS values, any flagged insights
|
|
64
|
+
- Console: any errors
|
|
65
|
+
- Review: pass or issues found and fixed
|
|
66
|
+
|
|
67
|
+
Only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100, CLS is 0.00, and there are no console errors. Otherwise, fix and run `/verify` again.
|
package/src/agent/workflow.md
CHANGED
|
@@ -80,6 +80,12 @@ Run `pulse_validate` on the spec file.
|
|
|
80
80
|
|
|
81
81
|
## Phase 5 — Browser check (pass gate)
|
|
82
82
|
|
|
83
|
+
**After every file edit, restart the server and reload the browser before checking anything.** The dev server auto-restarts on file changes but the browser tab stays stale. Always:
|
|
84
|
+
1. Call `pulse_restart_server` — waits for the server to be ready
|
|
85
|
+
2. Call `navigate_page` with the page URL — reloads the browser tab
|
|
86
|
+
|
|
87
|
+
Do this before every screenshot, console check, or visual inspection. Never debug a problem that might just be a stale tab.
|
|
88
|
+
|
|
83
89
|
**Run `/verify` now — as soon as the first build is working.** Do not defer it to the end. Do not execute these steps manually.
|
|
84
90
|
|
|
85
91
|
`/verify` is the canonical implementation of phases 4–7: it validates, screenshots, runs Lighthouse (desktop + mobile), runs the performance trace (LCP + CLS), checks console errors, runs `pulse_review`, and — critically — **writes the `.pulse-verified` stamp** at the end. Running the steps manually without writing the stamp will cause the stop hook to block.
|
package/src/cli/cli.test.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* run: node src/cli/cli.test.js
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { existsSync, readFileSync } from 'fs'
|
|
12
|
+
import path from 'path'
|
|
13
|
+
|
|
11
14
|
let passed = 0
|
|
12
15
|
let failed = 0
|
|
13
16
|
|
|
@@ -76,6 +79,32 @@ test('discover: nested pages get unique names (collision prevention)', () => {
|
|
|
76
79
|
}
|
|
77
80
|
})
|
|
78
81
|
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Skill source files
|
|
84
|
+
|
|
85
|
+
console.log('\nCopilot skill source files\n')
|
|
86
|
+
|
|
87
|
+
const skillsDir = new URL('../agent/skills', import.meta.url).pathname
|
|
88
|
+
const requiredSkills = ['build-page', 'verify', 'new-doc-page']
|
|
89
|
+
|
|
90
|
+
for (const skill of requiredSkills) {
|
|
91
|
+
const skillMd = path.join(skillsDir, skill, 'SKILL.md')
|
|
92
|
+
|
|
93
|
+
test(`${skill}/SKILL.md exists`, () => {
|
|
94
|
+
if (!existsSync(skillMd)) throw new Error(`Missing: ${skillMd}`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test(`${skill}/SKILL.md has valid frontmatter`, () => {
|
|
98
|
+
const content = readFileSync(skillMd, 'utf8')
|
|
99
|
+
if (!content.startsWith('---\n')) throw new Error('Missing opening YAML frontmatter ---')
|
|
100
|
+
const end = content.indexOf('\n---\n', 4)
|
|
101
|
+
if (end === -1) throw new Error('Missing closing YAML frontmatter ---')
|
|
102
|
+
const frontmatter = content.slice(4, end)
|
|
103
|
+
if (!frontmatter.includes('name:')) throw new Error('Frontmatter missing name:')
|
|
104
|
+
if (!frontmatter.includes('description:')) throw new Error('Frontmatter missing description:')
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
// ---------------------------------------------------------------------------
|
|
80
109
|
|
|
81
110
|
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
|
package/src/cli/index.js
CHANGED
|
@@ -12,13 +12,24 @@
|
|
|
12
12
|
* pulse -v alias for --version
|
|
13
13
|
* pulse --help show usage and exit
|
|
14
14
|
* pulse -h alias for --help
|
|
15
|
+
* pulse --agent copilot use GitHub Copilot CLI instead of Claude
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import path from 'path'
|
|
18
19
|
import fs from 'fs'
|
|
19
20
|
import { scaffold } from './scaffold.js'
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// Extract --agent flag before routing, so it works alongside any subcommand
|
|
23
|
+
const rawArgs = process.argv.slice(2)
|
|
24
|
+
let agentFlag = null
|
|
25
|
+
const args = []
|
|
26
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
27
|
+
if (rawArgs[i] === '--agent' && rawArgs[i + 1] && !rawArgs[i + 1].startsWith('-')) {
|
|
28
|
+
agentFlag = rawArgs[++i]
|
|
29
|
+
} else {
|
|
30
|
+
args.push(rawArgs[i])
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
const command = args[0]
|
|
23
34
|
const CWD = process.cwd()
|
|
24
35
|
|
|
@@ -112,7 +123,7 @@ async function runDefault(root) {
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
console.log()
|
|
115
|
-
await scaffold(targetDir, { name })
|
|
126
|
+
await scaffold(targetDir, { name, agent: agentFlag })
|
|
116
127
|
|
|
117
128
|
if (targetDir !== root) {
|
|
118
129
|
console.log(`\n✓ Project created at ./${name}/\n`)
|
|
@@ -126,21 +137,21 @@ async function runDefault(root) {
|
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
// Start dev server + AI session
|
|
129
|
-
await launchSession(root)
|
|
140
|
+
await launchSession(root, agentFlag)
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
// ---------------------------------------------------------------------------
|
|
133
144
|
// Launch AI session (Claude by default)
|
|
134
145
|
// ---------------------------------------------------------------------------
|
|
135
146
|
|
|
136
|
-
async function launchSession(root) {
|
|
147
|
+
async function launchSession(root, agentOverride = null) {
|
|
137
148
|
const { spawn } = await import('child_process')
|
|
138
149
|
const os = await import('os')
|
|
139
150
|
|
|
140
|
-
// Load project config for agent preference
|
|
151
|
+
// Load project config for agent preference — CLI flag takes priority
|
|
141
152
|
const configPath = path.join(root, 'pulse.config.js')
|
|
142
|
-
let agent = 'claude'
|
|
143
|
-
if (fs.existsSync(configPath)) {
|
|
153
|
+
let agent = agentOverride || 'claude'
|
|
154
|
+
if (!agentOverride && fs.existsSync(configPath)) {
|
|
144
155
|
try {
|
|
145
156
|
const mod = await import(configPath)
|
|
146
157
|
agent = mod.default?.agent || 'claude'
|
|
@@ -157,18 +168,30 @@ async function launchSession(root) {
|
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
|
-
const mcpConfigPath = path.join(os.default.tmpdir(), `pulse-mcp-${Date.now()}.json`)
|
|
161
|
-
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2))
|
|
162
171
|
|
|
163
172
|
console.log(`\n⚡ Pulse project: ${root}`)
|
|
164
173
|
console.log(` Use /pulse-dev to start the dev server, /pulse-stop to stop it, /pulse-build to build, /pulse-start to run production.`)
|
|
165
174
|
console.log(` Tell me what you'd like to build — a new page, a component, a form, or anything else.\n`)
|
|
166
175
|
|
|
176
|
+
if (agent === 'copilot') {
|
|
177
|
+
await launchCopilotSession(root, mcpServerPath, spawn, os.default)
|
|
178
|
+
} else {
|
|
179
|
+
await launchClaudeSession(root, mcpConfig, spawn, os.default)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Claude session — temp MCP config file passed via --mcp-config flag
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
async function launchClaudeSession(root, mcpConfig, spawn, os) {
|
|
188
|
+
const mcpConfigPath = path.join(os.tmpdir(), `pulse-mcp-${Date.now()}.json`)
|
|
189
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2))
|
|
190
|
+
|
|
167
191
|
// Launch the agent with MCP config — don't spawn a dev server here,
|
|
168
192
|
// Claude Code cannot be launched as a child process from within a Claude session.
|
|
169
193
|
// The dev server is started via the /dev slash command instead.
|
|
170
|
-
const
|
|
171
|
-
const agentProc = spawn(agentCmd.cmd, agentCmd.args, {
|
|
194
|
+
const agentProc = spawn('claude', ['--mcp-config', mcpConfigPath], {
|
|
172
195
|
stdio: 'inherit',
|
|
173
196
|
cwd: root,
|
|
174
197
|
})
|
|
@@ -179,13 +202,55 @@ async function launchSession(root) {
|
|
|
179
202
|
})
|
|
180
203
|
}
|
|
181
204
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Copilot session — pulse entry injected into ~/.copilot/mcp-config.json
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
async function launchCopilotSession(root, mcpServerPath, spawn, os) {
|
|
210
|
+
const mcpConfigPath = path.join(os.homedir(), '.copilot', 'mcp-config.json')
|
|
211
|
+
|
|
212
|
+
// Read existing Copilot MCP config (or start fresh)
|
|
213
|
+
let config = { mcpServers: {} }
|
|
214
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
215
|
+
try { config = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8')) } catch { /* use default */ }
|
|
185
216
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
config.mcpServers ??= {}
|
|
218
|
+
|
|
219
|
+
const hadPulse = 'pulse' in config.mcpServers
|
|
220
|
+
const originalPulse = hadPulse ? config.mcpServers.pulse : undefined
|
|
221
|
+
|
|
222
|
+
// Inject pulse MCP server
|
|
223
|
+
config.mcpServers.pulse = {
|
|
224
|
+
type: 'local',
|
|
225
|
+
command: process.execPath,
|
|
226
|
+
args: [mcpServerPath, '--root', root],
|
|
227
|
+
tools: ['*'],
|
|
228
|
+
}
|
|
229
|
+
fs.mkdirSync(path.dirname(mcpConfigPath), { recursive: true })
|
|
230
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2))
|
|
231
|
+
|
|
232
|
+
// Restore config on exit — removes pulse entry (or restores original if it existed)
|
|
233
|
+
let cleaned = false
|
|
234
|
+
const cleanup = () => {
|
|
235
|
+
if (cleaned) return
|
|
236
|
+
cleaned = true
|
|
237
|
+
try {
|
|
238
|
+
const current = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'))
|
|
239
|
+
if (hadPulse) {
|
|
240
|
+
current.mcpServers.pulse = originalPulse
|
|
241
|
+
} else {
|
|
242
|
+
delete current.mcpServers.pulse
|
|
243
|
+
}
|
|
244
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(current, null, 2))
|
|
245
|
+
} catch { /* ignore cleanup errors */ }
|
|
246
|
+
process.exit(0)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.on('SIGINT', cleanup)
|
|
250
|
+
process.on('SIGTERM', cleanup)
|
|
251
|
+
|
|
252
|
+
const agentProc = spawn('copilot', [], { stdio: 'inherit', cwd: root })
|
|
253
|
+
agentProc.on('exit', cleanup)
|
|
189
254
|
}
|
|
190
255
|
|
|
191
256
|
// ---------------------------------------------------------------------------
|
|
@@ -318,6 +383,29 @@ async function runUpdate(root) {
|
|
|
318
383
|
}
|
|
319
384
|
}
|
|
320
385
|
|
|
386
|
+
// Sync Copilot skills into .copilot/skills/
|
|
387
|
+
const skillsSrc = new URL('../agent/skills', import.meta.url).pathname
|
|
388
|
+
if (fs.existsSync(skillsSrc)) {
|
|
389
|
+
for (const skillDir of fs.readdirSync(skillsSrc)) {
|
|
390
|
+
const skillMd = path.join(skillsSrc, skillDir, 'SKILL.md')
|
|
391
|
+
if (fs.existsSync(skillMd)) {
|
|
392
|
+
const dstDir = path.join(root, '.copilot', 'skills', skillDir)
|
|
393
|
+
fs.mkdirSync(dstDir, { recursive: true })
|
|
394
|
+
fs.copyFileSync(skillMd, path.join(dstDir, 'SKILL.md'))
|
|
395
|
+
updated.push(`.copilot/skills/${skillDir}/SKILL.md`)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Sync checklist to .github/instructions/ for Copilot
|
|
401
|
+
const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
|
|
402
|
+
if (fs.existsSync(checklistSrc)) {
|
|
403
|
+
const checklistDst = path.join(root, '.github', 'instructions', 'pulse-checklist.instructions.md')
|
|
404
|
+
fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
|
|
405
|
+
fs.copyFileSync(checklistSrc, checklistDst)
|
|
406
|
+
updated.push('.github/instructions/pulse-checklist.instructions.md')
|
|
407
|
+
}
|
|
408
|
+
|
|
321
409
|
// Read the new version for the success message
|
|
322
410
|
const versionFile = path.join(publicDir, '.pulse-ui-version')
|
|
323
411
|
const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : '?'
|
|
@@ -362,7 +450,7 @@ switch (command) {
|
|
|
362
450
|
console.log(`
|
|
363
451
|
⚡ Pulse — spec-first frontend framework
|
|
364
452
|
|
|
365
|
-
Usage: pulse [command]
|
|
453
|
+
Usage: pulse [command] [options]
|
|
366
454
|
|
|
367
455
|
Commands:
|
|
368
456
|
(none) detect project or start scaffold wizard
|
|
@@ -372,8 +460,13 @@ switch (command) {
|
|
|
372
460
|
update re-copy pulse-ui assets from installed package → public/
|
|
373
461
|
|
|
374
462
|
Options:
|
|
463
|
+
--agent <name> AI agent to use: claude (default) or copilot
|
|
375
464
|
-v, --version print version and exit
|
|
376
465
|
-h, --help show this help
|
|
466
|
+
|
|
467
|
+
Examples:
|
|
468
|
+
pulse start with Claude (default)
|
|
469
|
+
pulse --agent copilot start with GitHub Copilot CLI
|
|
377
470
|
`)
|
|
378
471
|
process.exit(0)
|
|
379
472
|
case 'dev':
|
package/src/cli/scaffold.js
CHANGED
|
@@ -20,8 +20,9 @@ const PULSE_PKG = '@invisibleloop/pulse'
|
|
|
20
20
|
* @param {number} [options.port] - Dev server port (defaults to 3000)
|
|
21
21
|
*/
|
|
22
22
|
export async function scaffold(targetDir, options = {}) {
|
|
23
|
-
const name
|
|
24
|
-
const port
|
|
23
|
+
const name = options.name || path.basename(targetDir)
|
|
24
|
+
const port = options.port || 3000
|
|
25
|
+
const agent = options.agent || null
|
|
25
26
|
|
|
26
27
|
fs.mkdirSync(path.join(targetDir, 'src', 'pages'), { recursive: true })
|
|
27
28
|
fs.mkdirSync(path.join(targetDir, 'src', 'components'), { recursive: true })
|
|
@@ -47,10 +48,10 @@ export async function scaffold(targetDir, options = {}) {
|
|
|
47
48
|
}
|
|
48
49
|
}, null, 2))
|
|
49
50
|
|
|
50
|
-
// pulse.config.js
|
|
51
|
+
// pulse.config.js — write agent if specified, otherwise leave as commented hint
|
|
51
52
|
write(targetDir, 'pulse.config.js',
|
|
52
53
|
`export default {
|
|
53
|
-
${port !== 3000 ? ` port: ${port},\n` : ''}}
|
|
54
|
+
${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` agent: '${agent}',\n` : ` // agent: 'copilot', // uncomment to use GitHub Copilot CLI instead of Claude\n`}}
|
|
54
55
|
`)
|
|
55
56
|
|
|
56
57
|
// Home page + tests — working counter proves the app runs
|
|
@@ -94,6 +95,27 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
// Copilot skills — .copilot/skills/ so GitHub Copilot CLI picks them up as project skills
|
|
99
|
+
const skillsSrc = new URL('../agent/skills', import.meta.url).pathname
|
|
100
|
+
if (fs.existsSync(skillsSrc)) {
|
|
101
|
+
for (const skillDir of fs.readdirSync(skillsSrc)) {
|
|
102
|
+
const skillMd = path.join(skillsSrc, skillDir, 'SKILL.md')
|
|
103
|
+
if (fs.existsSync(skillMd)) {
|
|
104
|
+
write(targetDir, `.copilot/skills/${skillDir}/SKILL.md`, fs.readFileSync(skillMd, 'utf8'))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Copilot project instructions — .github/copilot-instructions.md is the canonical Copilot location
|
|
110
|
+
write(targetDir, '.github/copilot-instructions.md', copilotInstructionsMd(name))
|
|
111
|
+
|
|
112
|
+
// Copilot checklist as a separate instructions file — auto-loaded from .github/instructions/
|
|
113
|
+
const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
|
|
114
|
+
if (fs.existsSync(checklistSrc)) {
|
|
115
|
+
write(targetDir, '.github/instructions/pulse-checklist.instructions.md',
|
|
116
|
+
fs.readFileSync(checklistSrc, 'utf8'))
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
// settings.json — hooks that enforce correct agent behaviour
|
|
98
120
|
write(targetDir, '.claude/settings.json', JSON.stringify({
|
|
99
121
|
hooks: {
|
|
@@ -436,6 +458,98 @@ Descendant selectors (\`tbody tr\`, \`ul li\`) are NOT supported — use \`r.cou
|
|
|
436
458
|
`
|
|
437
459
|
}
|
|
438
460
|
|
|
461
|
+
function copilotInstructionsMd(appName) {
|
|
462
|
+
return `\
|
|
463
|
+
# ${appName} — Pulse App
|
|
464
|
+
|
|
465
|
+
## Project
|
|
466
|
+
|
|
467
|
+
\`\`\`
|
|
468
|
+
src/pages/ ← one .js file per page, auto-discovered
|
|
469
|
+
src/components/ ← shared view fragments (JS functions returning HTML strings)
|
|
470
|
+
public/theme.css ← token definitions (hex values, colour overrides) — hex allowed here only
|
|
471
|
+
public/app.css ← layout and component overrides — var() references only, no hex
|
|
472
|
+
\`\`\`
|
|
473
|
+
|
|
474
|
+
## Start of every session
|
|
475
|
+
|
|
476
|
+
1. Run \`pulse_list_structure\` to see what already exists
|
|
477
|
+
2. Read \`pulse://guide\` from MCP — the complete reference for spec format, components, verification workflow, CSS rules, and patterns
|
|
478
|
+
|
|
479
|
+
The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
|
|
480
|
+
|
|
481
|
+
## CSS and theming
|
|
482
|
+
|
|
483
|
+
- Hex values and colour token definitions go in \`public/theme.css\` — that is the only file where hex is allowed.
|
|
484
|
+
- \`public/app.css\` must not contain hex colour values. Use \`var(--ui-space-N)\` spacing tokens for padding, margin, and gap. Use \`var(--ui-text-*)\` tokens for font sizes.
|
|
485
|
+
- Load order in \`meta.styles\`: \`/pulse-ui.css\` → \`/theme.css\` → \`/app.css\`.
|
|
486
|
+
|
|
487
|
+
## Before writing any code
|
|
488
|
+
|
|
489
|
+
**Always present a plan and wait for the user to confirm before writing a single line of code.**
|
|
490
|
+
|
|
491
|
+
The plan must include: route, page sections, components used, state shape, and whether hydration is needed. End the plan with an explicit question — "Shall I go ahead?" — and stop. Do not proceed until the user says yes (or equivalent). This applies to every new page or significant change, no matter how clear the task seems.
|
|
492
|
+
|
|
493
|
+
## After completing any feature
|
|
494
|
+
|
|
495
|
+
Run these steps in order — do not declare work done without them:
|
|
496
|
+
|
|
497
|
+
1. \`pulse_validate\` — fix all errors and warnings
|
|
498
|
+
2. \`pulse_review\` — switch into reviewer mode, read the source and rendered HTML critically, fix every issue before continuing
|
|
499
|
+
3. Write tests for every page you created or changed, run \`npm test\` (all pass), then \`npm run test:coverage\` — fix any untested branches
|
|
500
|
+
4. Navigate to the page in the browser and take a screenshot
|
|
501
|
+
5. Run Lighthouse (desktop then mobile) — Accessibility, Best Practices, and SEO must all be 100
|
|
502
|
+
|
|
503
|
+
## Writing tests
|
|
504
|
+
|
|
505
|
+
Test files live next to the page they test: \`src/pages/foo.test.js\` for \`src/pages/foo.js\`.
|
|
506
|
+
|
|
507
|
+
\`\`\`
|
|
508
|
+
npm test # run all tests
|
|
509
|
+
npm run test:coverage # run tests + show branch/line coverage for src/pages/
|
|
510
|
+
\`\`\`
|
|
511
|
+
|
|
512
|
+
Coverage target: **100% branch coverage on every view function**. The coverage report shows uncovered lines — add tests until every branch is exercised. Server fetcher functions (which hit real APIs) are exempt.
|
|
513
|
+
|
|
514
|
+
What to test for every page:
|
|
515
|
+
- View with real/populated server data (success path)
|
|
516
|
+
- View with \`null\` server data per fetcher (each fetcher can fail independently)
|
|
517
|
+
- View with empty arrays/collections (empty state vs populated state)
|
|
518
|
+
- \`onViewError\` fallback — call it directly: \`spec.onViewError(new Error('x'), {}, {})\`
|
|
519
|
+
- Every mutation — pure functions, test directly
|
|
520
|
+
- Every exported pure function (formatters, validators, etc.)
|
|
521
|
+
- XSS: pass \`'<script>alert(1)</script>'\` as user-controlled strings, assert \`!r.has('script')\`
|
|
522
|
+
|
|
523
|
+
\`\`\`js
|
|
524
|
+
import assert from 'node:assert/strict'
|
|
525
|
+
import { renderSync } from '@invisibleloop/pulse/testing'
|
|
526
|
+
import spec from './my-page.js'
|
|
527
|
+
|
|
528
|
+
// View — pass mock state and server data
|
|
529
|
+
const r = renderSync(spec, { state: { count: 5 }, server: { items: [] } })
|
|
530
|
+
assert(r.has('main#main-content')) // element exists
|
|
531
|
+
assert.equal(r.get('h1').text, 'Title') // text content (throws if not found)
|
|
532
|
+
assert(r.has('button[disabled]')) // attribute present
|
|
533
|
+
assert(!r.has('.ui-badge')) // element absent
|
|
534
|
+
assert.equal(r.count('li'), 3) // count elements
|
|
535
|
+
|
|
536
|
+
// Mutations — pure functions, test directly
|
|
537
|
+
assert.deepEqual(spec.mutations.increment({ count: 0 }), { count: 1 })
|
|
538
|
+
|
|
539
|
+
// onViewError
|
|
540
|
+
const fallback = spec.onViewError(new Error('boom'), {}, {})
|
|
541
|
+
assert(fallback.includes('main-content'))
|
|
542
|
+
\`\`\`
|
|
543
|
+
|
|
544
|
+
**Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`, and combinations (\`button.primary[disabled]\`).
|
|
545
|
+
Descendant selectors (\`tbody tr\`, \`ul li\`) are NOT supported — use \`r.count('tr')\` not \`r.count('tbody tr')\`.
|
|
546
|
+
|
|
547
|
+
## Spec review checklist
|
|
548
|
+
|
|
549
|
+
See \`.github/instructions/pulse-checklist.instructions.md\` — loaded automatically by Copilot.
|
|
550
|
+
`
|
|
551
|
+
}
|
|
552
|
+
|
|
439
553
|
// ---------------------------------------------------------------------------
|
|
440
554
|
// Helpers
|
|
441
555
|
// ---------------------------------------------------------------------------
|