@invisibleloop/pulse 0.14.1 → 0.15.0
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/commands/build-page.md +5 -1
- package/.claude/commands/verify.md +23 -13
- package/docs/.claude/pulse-checklist.md +5 -3
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/docs.css +236 -0
- package/docs/public/home-brut.css +998 -0
- package/docs/public/menu.js +237 -6
- package/docs/public/pulse-ui.css +44 -0
- package/docs/public/theme.css +8 -0
- package/docs/server.js +24 -0
- package/docs/src/lib/layout.js +51 -3
- package/docs/src/pages/getting-started.js +1 -1
- package/docs/src/pages/home.js +231 -312
- package/docs/src/pages/how-it-works.js +67 -12
- package/examples/.claude/commands/pulse-build.md +7 -0
- package/examples/.claude/commands/pulse-dev.md +7 -0
- package/examples/.claude/commands/pulse-start.md +9 -0
- package/examples/.claude/commands/pulse-stop.md +5 -0
- package/examples/.claude/commands/verify.md +84 -0
- package/examples/.claude/coverage-check.js +127 -0
- package/examples/.claude/pulse-checklist.md +177 -0
- package/examples/public/.pulse-ui-version +1 -0
- package/examples/public/pulse-ui.css +4031 -0
- package/examples/public/pulse-ui.js +257 -0
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +44 -0
- package/scripts/build.js +127 -21
- package/src/agent/checklist.md +5 -3
- package/src/agent/guide-components.md +2 -2
- package/src/agent/guide-examples.md +310 -32
- package/src/agent/guide-styles.md +15 -0
- package/src/agent/guide-templates.md +3 -0
- package/src/agent/identity.md +2 -1
- package/src/agent/skills/verify/SKILL.md +64 -28
- package/src/agent/workflow.md +53 -17
- package/src/cli/dev.js +49 -13
- package/src/cli/index.js +4 -10
- package/src/cli/logger.js +145 -0
- package/src/cli/scaffold.js +55 -1
- package/src/mcp/server.js +174 -26
- package/src/mcp/validate-worker.js +21 -6
- package/src/server/index.js +20 -16
- package/src/spec/schema.js +19 -3
- package/src/spec/schema.test.js +28 -0
- package/src/ui/button.js +1 -1
package/src/agent/workflow.md
CHANGED
|
@@ -21,13 +21,17 @@
|
|
|
21
21
|
For any **new page, landing page, or branded site**, run the intake sequence first:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
0.
|
|
24
|
+
0. ASK: "Do you have any design inspiration — a site you love, a screenshot, or a mood board
|
|
25
|
+
image? Drop images into public/intake/ or paste a URL and I'll extract the design
|
|
26
|
+
intent before we start." ← MANDATORY. Ask this before doing anything else.
|
|
27
|
+
0b. pulse_extract_inspiration(url/image) → structured design brief [if user supplies reference]
|
|
25
28
|
1. pulse_intake(name, pitch, features, ...) → product brief + contrast check
|
|
26
29
|
2. pulse_sketch(brief, vibe?, antiStyle?) → 3 layout directions to choose from
|
|
27
30
|
3. pulse_intent(description) → archetype + scaffold + guide list
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
-
|
|
33
|
+
- **Step 0 — Ask for inspiration first (mandatory):** Before calling any tool or presenting any plan, ask the user: *"Do you have any design inspiration — a site you love, a screenshot, or a mood board image? Drop images into `public/intake/` or paste a URL and I'll extract the design intent before we start."* Wait for the answer. Do not skip this question, even if the user's brief already seems detailed. Check `public/intake/` for any images already dropped there.
|
|
34
|
+
- **`pulse_extract_inspiration`** — call this after step 0 if the user supplies a URL, site name, or image they like. Also call it for any images already present in `public/intake/`. Gives you a structured extraction template: visit the URL or analyse the image with your vision tools, extract colours, layout, typography, and feel, then feed those findings into pulse_intake. If the user explicitly says they have no reference, skip this step.
|
|
31
35
|
- **`pulse_intake`** — captures the real content (name, pitch, features, palette, vibe, what it should NOT look like). Returns a brief with copy-ready content and early contrast warnings. Always ask the user one question at a time — never multi-choice for open-ended questions.
|
|
32
36
|
- **`pulse_sketch`** — generates 3 structurally distinct layout directions (full-bleed, asymmetric split, typography-only, editorial, dense grid, story scroll, content-first). **Call this before writing any code.** Prevents defaulting to centred hero + three-column features every time.
|
|
33
37
|
- **Skip `pulse_sketch` only** when the vibe is `corporate` or `minimal` AND the user hasn't expressed any structural preference. For `playful`, `bold`, `brutalist`, `retro`, or `neon` vibes, `pulse_sketch` is mandatory — templates have default section orders that actively fight these vibes.
|
|
@@ -86,7 +90,8 @@ Not every task needs all 8 phases. Match the tier to the task:
|
|
|
86
90
|
| 1 | Intent + Understand | — |
|
|
87
91
|
| 2 | Build | announce brief → write files |
|
|
88
92
|
| 3 | Validate | `pulse_validate` clean |
|
|
89
|
-
|
|
|
93
|
+
| 4a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
|
|
94
|
+
| 4b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) |
|
|
90
95
|
|
|
91
96
|
No tests required for pure view specs with no logic. Add tests if the view has non-trivial helper functions.
|
|
92
97
|
|
|
@@ -100,15 +105,16 @@ No tests required for pure view specs with no logic. Add tests if the view has n
|
|
|
100
105
|
| 2 | Plan | user confirmation (skip if unambiguous) |
|
|
101
106
|
| 3 | Build | announce brief → write files |
|
|
102
107
|
| 4 | Validate | `pulse_validate` clean |
|
|
103
|
-
|
|
|
108
|
+
| 5a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
|
|
109
|
+
| 5b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) + CLS 0.00 |
|
|
104
110
|
| 6 | Tests | mutations, view landmarks, any utility functions |
|
|
105
|
-
| 7 | Code review | `pulse_review` — only after
|
|
111
|
+
| 7 | Code review | `pulse_review` — only after 5–6 pass |
|
|
106
112
|
|
|
107
113
|
### Tier 3 — Complex (multi-step, shared state, or security-sensitive)
|
|
108
114
|
|
|
109
115
|
*Applies to:* multi-step wizards, pages with guards and auth, store-connected pages, anything touching sensitive data or complex branching state.
|
|
110
116
|
|
|
111
|
-
All phases as originally defined — full
|
|
117
|
+
All phases as originally defined — full flow with every gate.
|
|
112
118
|
|
|
113
119
|
| Phase | Action | Gate |
|
|
114
120
|
|---|---|---|
|
|
@@ -116,7 +122,8 @@ All phases as originally defined — full 8-phase flow with every gate.
|
|
|
116
122
|
| 2 | Plan | **user confirmation required** |
|
|
117
123
|
| 3 | Build | announce brief → write files |
|
|
118
124
|
| 4 | Validate | `pulse_validate` clean |
|
|
119
|
-
|
|
|
125
|
+
| 5a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
|
|
126
|
+
| 5b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse 100/100/100 + CLS 0.00, desktop + mobile |
|
|
120
127
|
| 6 | Tests | full coverage: mutations, actions, guards, view |
|
|
121
128
|
| 7 | Code review | `pulse_review` |
|
|
122
129
|
| 8 | Fix + re-verify | re-run all gates |
|
|
@@ -209,7 +216,7 @@ Run `pulse_validate` on the spec file.
|
|
|
209
216
|
|
|
210
217
|
---
|
|
211
218
|
|
|
212
|
-
## Phase 5 — Browser check
|
|
219
|
+
## Phase 5 — Browser check
|
|
213
220
|
|
|
214
221
|
**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:
|
|
215
222
|
1. Call `pulse_restart_server` — waits for the server to be ready
|
|
@@ -217,15 +224,40 @@ Run `pulse_validate` on the spec file.
|
|
|
217
224
|
|
|
218
225
|
Do this before every screenshot, console check, or visual inspection. Never debug a problem that might just be a stale tab.
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Phase 5a — Design approval *(new builds from `pulse_intake` only)*
|
|
230
|
+
|
|
231
|
+
**This phase applies when `pulse_intake` ran** — i.e. you are building a new page or site from scratch, not editing an existing one. For edits, bug fixes, or "add X to existing Y", skip to Phase 5b immediately.
|
|
232
|
+
|
|
233
|
+
1. Take a screenshot of the built page
|
|
234
|
+
2. Describe what you see in 2–3 plain sentences — layout structure, visual tone, key sections. Do NOT list features or code details.
|
|
235
|
+
3. **Ask the user directly:** *"Happy with the layout and direction, or any changes before I run tests and Lighthouse?"*
|
|
236
|
+
4. **Wait for their response before doing anything else.** Do not run design_review, layout_review, or Lighthouse until you have a clear go-ahead.
|
|
237
|
+
|
|
238
|
+
**If the user requests changes:**
|
|
239
|
+
- Make the edits
|
|
240
|
+
- Take a new screenshot
|
|
241
|
+
- Describe what changed
|
|
242
|
+
- Ask again
|
|
243
|
+
- Repeat until approved — each iteration is design-only, no Lighthouse between rounds
|
|
244
|
+
|
|
245
|
+
**If the user approves (or says "looks good", "go ahead", "yes", etc.):**
|
|
246
|
+
- Continue to Phase 5b
|
|
247
|
+
|
|
248
|
+
> The point of this gate is to let the user redirect the design before expensive, slow checks run. A Lighthouse build takes ~90 seconds — do not start it until the user has seen and approved the visual result.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Phase 5b — Full verification (pass gate)
|
|
253
|
+
|
|
254
|
+
**Run `/verify` now.** Do not execute these steps manually.
|
|
221
255
|
|
|
222
256
|
`/verify` validates, screenshots, runs Lighthouse (desktop + mobile), runs the performance trace (LCP + CLS), checks console errors, runs `pulse_review`, and writes the `.pulse-verified` stamp. Running steps manually does not write the stamp.
|
|
223
257
|
|
|
224
258
|
> **Lighthouse pre-flight:** `/verify` calls `pulse_build` automatically before running Lighthouse, and audits against the production server on port 3001. If you ever call `lighthouse_audit` manually outside `/verify`, you must first run `pulse_build` and navigate to `http://localhost:3001/your-route`. Auditing the dev server (port 3000) will produce misleading results.
|
|
225
259
|
|
|
226
|
-
### ⛔ After
|
|
227
|
-
|
|
228
|
-
Taking a screenshot is NOT the end of phase 5. After every screenshot, you **must** run:
|
|
260
|
+
### ⛔ After design approval — mandatory steps before continuing
|
|
229
261
|
|
|
230
262
|
1. `pulse_design_review` (if `pulse_intake` ran) — work through all 7 signals, fix any Fail
|
|
231
263
|
2. `pulse_layout_review <url>` — 390/768/1280px overflow, broken images, collapsed sections
|
|
@@ -234,7 +266,7 @@ Taking a screenshot is NOT the end of phase 5. After every screenshot, you **mus
|
|
|
234
266
|
**Do not report the page as done until `/verify` writes the `.pulse-verified` stamp.** Skipping Lighthouse means broken contrast, missing landmarks, and Best Practices failures ship to the user.
|
|
235
267
|
|
|
236
268
|
Pass gates:
|
|
237
|
-
-
|
|
269
|
+
- Design approval: user has explicitly said they're happy with the layout
|
|
238
270
|
- Design review: all 7 signals pass (or intake didn't run)
|
|
239
271
|
- Layout review: no overflow, broken images, or collapsed sections at any viewport
|
|
240
272
|
- Lighthouse desktop: Accessibility, Best Practices, SEO all 100
|
|
@@ -282,7 +314,9 @@ Only declare the task done after all gates still pass.
|
|
|
282
314
|
|
|
283
315
|
```
|
|
284
316
|
New project / new page:
|
|
285
|
-
|
|
317
|
+
ASK first → "Do you have any design inspiration — a site, screenshot, or
|
|
318
|
+
mood board? Drop into public/intake/ or paste a URL."
|
|
319
|
+
pulse_extract_inspiration → (if user supplies a URL or image, or images found in public/intake/)
|
|
286
320
|
pulse_intake → capture real content, palette, vibe, anti-style
|
|
287
321
|
pulse_sketch → choose 1 of 3 structural layout directions
|
|
288
322
|
pulse_intent → get scaffold + guide list
|
|
@@ -293,8 +327,10 @@ Targeted edit / bug fix:
|
|
|
293
327
|
Step 1 Understand pulse_list_structure + relevant guides
|
|
294
328
|
Step 2 Plan confirm with user if non-trivial
|
|
295
329
|
|
|
296
|
-
Tier 1 Simple → phases 3 → 4 →
|
|
297
|
-
Tier 2 Standard → phases 3 → 4 →
|
|
298
|
-
Tier 3 Complex → phases 3 → 4 →
|
|
330
|
+
Tier 1 Simple → phases 3 → 4 → 5a (design approval) → 5b (verify)
|
|
331
|
+
Tier 2 Standard → phases 3 → 4 → 5a (design approval) → 5b (verify) → 6 → 7
|
|
332
|
+
Tier 3 Complex → phases 3 → 4 → 5a (design approval) → 5b (verify) → 6 → 7 → 8
|
|
333
|
+
|
|
334
|
+
Phase 5a is skipped for edits, bug fixes, and "add X to existing Y".
|
|
299
335
|
```
|
|
300
336
|
|
package/src/cli/dev.js
CHANGED
|
@@ -12,6 +12,7 @@ import path from 'path'
|
|
|
12
12
|
import fs from 'fs'
|
|
13
13
|
import { createServer } from '../server/index.js'
|
|
14
14
|
import { loadPages } from './discover.js'
|
|
15
|
+
import * as log from './logger.js'
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Args
|
|
@@ -106,19 +107,13 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
|
106
107
|
// Start
|
|
107
108
|
// ---------------------------------------------------------------------------
|
|
108
109
|
|
|
109
|
-
console.log(`⚡ Pulse dev server starting...\n`)
|
|
110
|
-
|
|
111
110
|
const specs = await loadPages(ROOT)
|
|
112
111
|
|
|
113
112
|
if (specs.length === 0) {
|
|
114
|
-
|
|
113
|
+
log.error('No pages found in src/pages/. Create a page to get started.')
|
|
115
114
|
process.exit(1)
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
console.log('Pages:\n')
|
|
119
|
-
specs.forEach(spec => console.log(` ${spec.route}`))
|
|
120
|
-
console.log()
|
|
121
|
-
|
|
122
117
|
// ---------------------------------------------------------------------------
|
|
123
118
|
// Hot reload — SSE clients + file watcher
|
|
124
119
|
// ---------------------------------------------------------------------------
|
|
@@ -145,22 +140,62 @@ process.on('SIGINT', closeReloadClients)
|
|
|
145
140
|
let reloadTimer = null
|
|
146
141
|
let lastReload = 0
|
|
147
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Check rendered HTML class names against CSS files in public/ and warn about
|
|
145
|
+
* any classes that are used in templates but not defined in any stylesheet.
|
|
146
|
+
* Only checks project-owned CSS (not pulse-ui.css) to avoid noise.
|
|
147
|
+
*/
|
|
148
|
+
async function checkUndefinedClasses(specs) {
|
|
149
|
+
try {
|
|
150
|
+
// Collect all class names defined in project CSS files
|
|
151
|
+
const cssDir = path.join(ROOT, 'public')
|
|
152
|
+
const definedClasses = new Set()
|
|
153
|
+
if (fs.existsSync(cssDir)) {
|
|
154
|
+
for (const file of fs.readdirSync(cssDir).filter(f => f.endsWith('.css') && f !== 'pulse-ui.css')) {
|
|
155
|
+
const css = fs.readFileSync(path.join(cssDir, file), 'utf8')
|
|
156
|
+
for (const [, cls] of css.matchAll(/\.([a-zA-Z][a-zA-Z0-9_-]*)\s*[{,:\[]/g)) {
|
|
157
|
+
definedClasses.add(cls)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (definedClasses.size === 0) return
|
|
162
|
+
|
|
163
|
+
// Render each spec and collect used class names from the HTML output
|
|
164
|
+
const { renderToString } = await import('../runtime/ssr.js')
|
|
165
|
+
for (const spec of specs) {
|
|
166
|
+
if (!spec.view) continue
|
|
167
|
+
try {
|
|
168
|
+
const { html } = await renderToString(spec, {})
|
|
169
|
+
const usedInSpec = new Set()
|
|
170
|
+
for (const [, classes] of html.matchAll(/class="([^"]+)"/g)) {
|
|
171
|
+
for (const cls of classes.trim().split(/\s+/)) {
|
|
172
|
+
if (cls && !cls.startsWith('ui-')) usedInSpec.add(cls)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const undefined_ = [...usedInSpec].filter(cls => !definedClasses.has(cls))
|
|
176
|
+
if (undefined_.length > 0) {
|
|
177
|
+
const route = spec.route ?? spec.filePath ?? '(unknown)'
|
|
178
|
+
log.warn(`${route}: class${undefined_.length === 1 ? '' : 'es'} used but not defined in any CSS: ${undefined_.slice(0, 5).map(c => `.${c}`).join(', ')}${undefined_.length > 5 ? ` … +${undefined_.length - 5} more` : ''}`)
|
|
179
|
+
}
|
|
180
|
+
} catch { /* render failures are not our concern here */ }
|
|
181
|
+
}
|
|
182
|
+
} catch { /* never crash the dev server over a lint warning */ }
|
|
183
|
+
}
|
|
184
|
+
|
|
148
185
|
async function triggerReload(label = 'File changed') {
|
|
149
186
|
clearTimeout(reloadTimer)
|
|
150
187
|
reloadTimer = setTimeout(async () => {
|
|
151
|
-
// Debounce rapid file changes (e.g. multiple Edit tool calls)
|
|
152
188
|
const now = Date.now()
|
|
153
189
|
if (now - lastReload < 100) return
|
|
154
190
|
lastReload = now
|
|
155
|
-
|
|
156
|
-
console.log(` ⟳ ${label}, reloading specs...`)
|
|
191
|
+
|
|
157
192
|
try {
|
|
158
193
|
const fresh = await loadPages(ROOT, now)
|
|
159
194
|
updateSpecs(fresh)
|
|
160
|
-
|
|
195
|
+
log.info(`${label} — specs reloaded`)
|
|
196
|
+
checkUndefinedClasses(fresh)
|
|
161
197
|
} catch (err) {
|
|
162
|
-
|
|
163
|
-
/* spec error — browser will show the old page, not crash */
|
|
198
|
+
log.error(`Spec reload failed: ${err.message}`)
|
|
164
199
|
}
|
|
165
200
|
notifyReload()
|
|
166
201
|
}, 200)
|
|
@@ -199,6 +234,7 @@ const { updateSpecs } = await createServer(specs, {
|
|
|
199
234
|
manifest: {}, // never use a build manifest in dev — always serve source files
|
|
200
235
|
extraBody: reloadScript,
|
|
201
236
|
dev: true,
|
|
237
|
+
agentMode: !!process.env.PULSE_AGENT_MODE,
|
|
202
238
|
...(_config.csp ? { csp: _config.csp } : {}),
|
|
203
239
|
|
|
204
240
|
onRequest(req, res) {
|
package/src/cli/index.js
CHANGED
|
@@ -33,13 +33,6 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
33
33
|
const command = args[0]
|
|
34
34
|
const CWD = process.cwd()
|
|
35
35
|
|
|
36
|
-
// Parse --agent flag before routing to commands
|
|
37
|
-
let agentFlag = null
|
|
38
|
-
const agentIdx = args.indexOf('--agent')
|
|
39
|
-
if (agentIdx !== -1 && args[agentIdx + 1]) {
|
|
40
|
-
agentFlag = args[agentIdx + 1]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
36
|
// ---------------------------------------------------------------------------
|
|
44
37
|
// Helpers
|
|
45
38
|
// ---------------------------------------------------------------------------
|
|
@@ -176,13 +169,14 @@ async function launchSession(root, agentOverride = null) {
|
|
|
176
169
|
}
|
|
177
170
|
}
|
|
178
171
|
|
|
179
|
-
console.log(`\n⚡ Pulse
|
|
180
|
-
console.log(`
|
|
181
|
-
console.log(` Tell me what you'd like to build — a new page, a component, a form, or anything else.\n`)
|
|
172
|
+
console.log(`\n ⚡ Pulse → ${root}`)
|
|
173
|
+
console.log(` ${agent === 'copilot' ? '● Copilot' : '● Claude'} agent starting — tell me what you'd like to build.\n`)
|
|
182
174
|
|
|
183
175
|
if (agent === 'copilot') {
|
|
176
|
+
process.env.PULSE_AGENT_MODE = '1'
|
|
184
177
|
await launchCopilotSession(root, mcpServerPath, spawn, os.default)
|
|
185
178
|
} else {
|
|
179
|
+
process.env.PULSE_AGENT_MODE = '1'
|
|
186
180
|
await launchClaudeSession(root, mcpConfig, spawn, os.default)
|
|
187
181
|
}
|
|
188
182
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI — structured terminal logger
|
|
3
|
+
*
|
|
4
|
+
* Single module that owns all terminal output. Produces clean, scannable
|
|
5
|
+
* output: one-time startup banner, then a timestamped scrolling log.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Static asset requests (JS, CSS, images, fonts) are dimmed and collapsed
|
|
9
|
+
* - Errors always stand out (red ✗ prefix)
|
|
10
|
+
* - Agent activity gets a distinct indicator
|
|
11
|
+
* - Respects NO_COLOR and CI env vars
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { c, icon, elapsed, visLen } from './fmt.js'
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const STATIC_EXT = /\.(js|css|map|ico|png|jpg|jpeg|gif|svg|woff2?|ttf|eot|webp|avif|json)$/i
|
|
21
|
+
const STATIC_PATH = /^\/(dist|fonts|images|public)\//
|
|
22
|
+
|
|
23
|
+
function isStatic(pathname) {
|
|
24
|
+
return STATIC_EXT.test(pathname) || STATIC_PATH.test(pathname)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function timestamp() {
|
|
28
|
+
return c.dim(new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function statusColor(code) {
|
|
32
|
+
if (code >= 500) return c.red(String(code))
|
|
33
|
+
if (code >= 400) return c.yellow(String(code))
|
|
34
|
+
if (code >= 300) return c.cyan(String(code))
|
|
35
|
+
return c.dim(String(code))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function methodColor(method) {
|
|
39
|
+
if (method === 'POST' || method === 'PUT' || method === 'PATCH') return c.yellow(method.padEnd(4))
|
|
40
|
+
if (method === 'DELETE') return c.red(method.padEnd(4))
|
|
41
|
+
return c.dim(method.padEnd(4))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Banner — printed once at startup
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Print the startup banner.
|
|
50
|
+
* @param {{ url: string, version: string, specs: Array<{route:string, method?:string, hydrate?:string}>, agentMode?: boolean }} opts
|
|
51
|
+
*/
|
|
52
|
+
export function banner({ url, version, specs = [], agentMode = false }) {
|
|
53
|
+
const agentLabel = agentMode ? ` ${c.cyan('●')} ${c.dim('Agent active')}` : ''
|
|
54
|
+
|
|
55
|
+
process.stdout.write('\n')
|
|
56
|
+
process.stdout.write(` ${icon.bolt()} ${c.bold('Pulse')} ${c.dim(`v${version}`)} ${c.dim('→')} ${c.cyan(url)}${agentLabel}\n`)
|
|
57
|
+
process.stdout.write('\n')
|
|
58
|
+
|
|
59
|
+
if (specs.length > 0) {
|
|
60
|
+
// Column widths
|
|
61
|
+
const routeW = Math.max(5, ...specs.map(s => (s.route || '?').length))
|
|
62
|
+
const methodW = 6
|
|
63
|
+
|
|
64
|
+
const header = ` ${c.dim('Route'.padEnd(routeW))} ${c.dim('Method'.padEnd(methodW))} ${c.dim('JS')}`
|
|
65
|
+
const divider = ` ${c.dim('─'.repeat(routeW + methodW + 8))}`
|
|
66
|
+
|
|
67
|
+
process.stdout.write(header + '\n')
|
|
68
|
+
process.stdout.write(divider + '\n')
|
|
69
|
+
|
|
70
|
+
for (const s of specs) {
|
|
71
|
+
const route = (s.route || '?').padEnd(routeW)
|
|
72
|
+
const method = (s.method?.toUpperCase() || 'GET').padEnd(methodW)
|
|
73
|
+
const hydrate = s.hydrate ? c.green('✓ hydrated') : c.dim('— static')
|
|
74
|
+
process.stdout.write(` ${c.cyan(route)} ${c.dim(method)} ${hydrate}\n`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
process.stdout.write('\n')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.stdout.write(c.dim(' Ready. Watching for changes…\n'))
|
|
81
|
+
process.stdout.write(c.dim(' ' + '─'.repeat(48) + '\n'))
|
|
82
|
+
process.stdout.write('\n')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Request log — one line per request
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Log a completed HTTP request.
|
|
91
|
+
* @param {{ method: string, pathname: string, status: number, ms: number }} opts
|
|
92
|
+
*/
|
|
93
|
+
export function request({ method, pathname, status, ms }) {
|
|
94
|
+
const isAsset = isStatic(pathname)
|
|
95
|
+
|
|
96
|
+
// Skip health check noise
|
|
97
|
+
if (pathname === '/healthz' || pathname === '/_pulse/health') return
|
|
98
|
+
|
|
99
|
+
const ts = timestamp()
|
|
100
|
+
const m = methodColor(method)
|
|
101
|
+
const p = isAsset ? c.dim(pathname) : pathname
|
|
102
|
+
const s = statusColor(status)
|
|
103
|
+
const t = c.dim(elapsed(ms))
|
|
104
|
+
|
|
105
|
+
if (isAsset) {
|
|
106
|
+
// Static assets — dim the whole line, no leading symbol
|
|
107
|
+
process.stdout.write(`${ts} ${c.dim(method.padEnd(4))} ${p} ${c.dim(String(status))} ${t}\n`)
|
|
108
|
+
} else if (status >= 500) {
|
|
109
|
+
process.stdout.write(`${ts} ${icon.fail()} ${m} ${c.red(pathname)} ${s} ${t}\n`)
|
|
110
|
+
} else if (status >= 400) {
|
|
111
|
+
process.stdout.write(`${ts} ${icon.warn()} ${m} ${c.yellow(pathname)} ${s} ${t}\n`)
|
|
112
|
+
} else {
|
|
113
|
+
process.stdout.write(`${ts} ${icon.info()} ${m} ${p} ${s} ${t}\n`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Named events — server lifecycle
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export function ready(url) {
|
|
122
|
+
process.stdout.write(`${timestamp()} ${icon.ok()} ${c.dim('Server ready')} ${c.dim(url)}\n`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function shutdown(forced = false) {
|
|
126
|
+
process.stdout.write('\n')
|
|
127
|
+
if (forced) {
|
|
128
|
+
process.stdout.write(` ${icon.fail()} ${c.red('Force-exiting after shutdown timeout')}\n`)
|
|
129
|
+
} else {
|
|
130
|
+
process.stdout.write(` ${icon.info()} ${c.dim('Shutting down gracefully…')}\n`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function error(msg, err) {
|
|
135
|
+
process.stdout.write(`${timestamp()} ${icon.fail()} ${c.red(msg)}\n`)
|
|
136
|
+
if (err?.stack) process.stdout.write(c.dim(err.stack.split('\n').slice(1, 4).map(l => ' ' + l).join('\n')) + '\n')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function info(msg) {
|
|
140
|
+
process.stdout.write(`${timestamp()} ${icon.info()} ${c.dim(msg)}\n`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function warn(msg) {
|
|
144
|
+
process.stdout.write(`${timestamp()} ${icon.warn()} ${c.yellow(msg)}\n`)
|
|
145
|
+
}
|
package/src/cli/scaffold.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function scaffold(targetDir, options = {}) {
|
|
|
27
27
|
fs.mkdirSync(path.join(targetDir, 'src', 'pages'), { recursive: true })
|
|
28
28
|
fs.mkdirSync(path.join(targetDir, 'src', 'components'), { recursive: true })
|
|
29
29
|
fs.mkdirSync(path.join(targetDir, 'public'), { recursive: true })
|
|
30
|
+
fs.mkdirSync(path.join(targetDir, 'public', 'intake'), { recursive: true })
|
|
30
31
|
|
|
31
32
|
// package.json
|
|
32
33
|
write(targetDir, 'package.json', JSON.stringify({
|
|
@@ -62,6 +63,9 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
|
|
|
62
63
|
write(targetDir, 'public/theme.css', themeCSS())
|
|
63
64
|
write(targetDir, 'public/app.css', baseCSS())
|
|
64
65
|
|
|
66
|
+
// Intake directory — drop inspiration images here before running pulse_intake
|
|
67
|
+
write(targetDir, 'public/intake/README.md', intakeReadme())
|
|
68
|
+
|
|
65
69
|
// Copy pulse-ui assets from the package into public/ so they're served immediately
|
|
66
70
|
const pkgPublic = new URL('../../public', import.meta.url).pathname
|
|
67
71
|
for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
|
|
@@ -124,13 +128,18 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
|
|
|
124
128
|
hooks: [
|
|
125
129
|
{
|
|
126
130
|
type: 'command',
|
|
127
|
-
command: `node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'START OF SESSION: Before doing anything else — (1) run pulse_list_structure to see the current project structure, (2) read pulse://guide from MCP for the complete Pulse reference. Both are mandatory. Do not skip them.'}}))"`,
|
|
131
|
+
command: `node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'START OF SESSION: Before doing anything else — (1) run pulse_list_structure to see the current project structure, (2) read pulse://guide from MCP for the complete Pulse reference. Both are mandatory. Do not skip them.\\n\\nBUILD RULE — ALWAYS ASK BEFORE PLANNING: If the user is requesting a new page or site, your FIRST action must be to ask: \\'Do you have any design inspiration — a site you love, a screenshot, a mood board image? Drop images into public/intake/ or paste a URL and I will extract the design intent before we start.\\' Do not present a plan or write any code until you have asked this question and received an answer. This question is not optional and cannot be skipped.'}}))"`,
|
|
128
132
|
statusMessage: 'Loading Pulse session...',
|
|
129
133
|
},
|
|
130
134
|
{
|
|
131
135
|
type: 'command',
|
|
132
136
|
command: `node -e "const{execSync}=require('child_process');let out='';let ok=true;try{out=execSync('npm test 2>&1',{encoding:'utf8',timeout:60000});}catch(e){ok=false;out=e.stdout||e.message;}const lines=out.trim().split('\\n');const summary=lines[lines.length-1]||'';const msg=ok?'TEST BASELINE — all tests passing: '+summary+'. Note this result. If tests fail after your changes, you introduced the regression.':'PRE-EXISTING TEST FAILURES detected before you wrote any code:\\n'+out.slice(-2000)+'\\nDo not treat these as regressions you caused. Fix them first if they are related to your task, otherwise note them and proceed.';process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:msg}}))"`,
|
|
133
137
|
statusMessage: 'Running test baseline...',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'command',
|
|
141
|
+
command: `node -e "const fs=require('fs');const path=require('path');const intakeDir='public/intake';let images=[];try{images=fs.readdirSync(intakeDir).filter(f=>/\\.(jpe?g|png|gif|webp|avif)$/i.test(f));}catch{}if(!images.length){process.exit(0);}const list=images.map(f=>' • '+f).join('\\n');process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'DESIGN REFERENCES FOUND in public/intake/:\\n'+list+'\\n\\nBefore running pulse_intake, call pulse_extract_inspiration on each image to extract palette, vibe, layout density, and type signals. This gives the design much more precision than text descriptions alone.\\n\\nWorkflow: pulse_extract_inspiration(image) → fill in the template using your vision → feed results into pulse_intake(inspiration: \\'...\\')'}}));"`,
|
|
142
|
+
statusMessage: 'Checking for design references...',
|
|
134
143
|
}
|
|
135
144
|
]
|
|
136
145
|
}
|
|
@@ -209,6 +218,13 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
|
|
|
209
218
|
'public/dist',
|
|
210
219
|
'.pulse-build',
|
|
211
220
|
'.DS_Store',
|
|
221
|
+
'# Intake images — local design references, not committed',
|
|
222
|
+
'public/intake/*.jpg',
|
|
223
|
+
'public/intake/*.jpeg',
|
|
224
|
+
'public/intake/*.png',
|
|
225
|
+
'public/intake/*.gif',
|
|
226
|
+
'public/intake/*.webp',
|
|
227
|
+
'public/intake/*.avif',
|
|
212
228
|
].join('\n') + '\n')
|
|
213
229
|
|
|
214
230
|
console.log(' ✓ Project files created')
|
|
@@ -349,6 +365,42 @@ function themeCSS() {
|
|
|
349
365
|
`
|
|
350
366
|
}
|
|
351
367
|
|
|
368
|
+
function intakeReadme() {
|
|
369
|
+
return `\
|
|
370
|
+
# Design References
|
|
371
|
+
|
|
372
|
+
Drop inspiration images here before starting a new build.
|
|
373
|
+
|
|
374
|
+
The session start hook will detect them and remind the agent to run
|
|
375
|
+
\`pulse_extract_inspiration\` on each one before \`pulse_intake\`.
|
|
376
|
+
|
|
377
|
+
## What to put here
|
|
378
|
+
|
|
379
|
+
- Screenshots of sites whose design you admire
|
|
380
|
+
- Your brand mood board or style guide
|
|
381
|
+
- Product photography that anchors the visual direction
|
|
382
|
+
- Competitor sites you want to differentiate from
|
|
383
|
+
- Any image that captures the feeling you're going for
|
|
384
|
+
|
|
385
|
+
## How the agent uses them
|
|
386
|
+
|
|
387
|
+
1. \`pulse_extract_inspiration(image)\` — reads the image and extracts palette,
|
|
388
|
+
type weight, spacing density, radius signals, and layout intent
|
|
389
|
+
2. The extracted values feed into \`pulse_intake\` as concrete constraints
|
|
390
|
+
3. \`pulse_sketch\` uses them to generate layout directions matched to the actual
|
|
391
|
+
visual reference, not just a text description of a vibe
|
|
392
|
+
|
|
393
|
+
## File formats
|
|
394
|
+
|
|
395
|
+
JPEG, PNG, WebP, AVIF, GIF — any image the model can read.
|
|
396
|
+
|
|
397
|
+
## Note
|
|
398
|
+
|
|
399
|
+
These files are listed in .gitignore and will not be committed.
|
|
400
|
+
Only this README is tracked.
|
|
401
|
+
`
|
|
402
|
+
}
|
|
403
|
+
|
|
352
404
|
function baseCSS() {
|
|
353
405
|
return `\
|
|
354
406
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -377,12 +429,14 @@ src/pages/ ← one .js file per page, auto-discovered
|
|
|
377
429
|
src/components/ ← shared view fragments (JS functions returning HTML strings)
|
|
378
430
|
public/theme.css ← token definitions (hex values, colour overrides) — hex allowed here only
|
|
379
431
|
public/app.css ← layout and component overrides — var() references only, no hex
|
|
432
|
+
public/intake/ ← drop design reference images here before starting (not committed to git)
|
|
380
433
|
\`\`\`
|
|
381
434
|
|
|
382
435
|
## Start of every session
|
|
383
436
|
|
|
384
437
|
1. Run \`pulse_list_structure\` to see what already exists
|
|
385
438
|
2. Read \`pulse://guide\` from MCP — the complete reference for spec format, components, verification workflow, CSS rules, and patterns
|
|
439
|
+
3. If images are present in \`public/intake/\`, call \`pulse_extract_inspiration\` on each before \`pulse_intake\` — the session hook will remind you if you forget
|
|
386
440
|
|
|
387
441
|
The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
|
|
388
442
|
|