@invisibleloop/pulse 0.12.3 → 0.14.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/commands/build-page.md +5 -1
- package/.claude/commands/verify.md +31 -17
- package/.claude/pulse-checklist.md +21 -1
- package/.github/workflows/publish.yml +1 -0
- package/.pulse-verified +1 -0
- package/CLAUDE.md +56 -15
- package/docs/.claude/commands/verify.md +4 -0
- package/docs/.claude/pulse-checklist.md +20 -2
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/andypixel.css +237 -0
- package/docs/public/bookworms.css +422 -0
- package/docs/public/coutumes.css +584 -0
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/journey.css +528 -0
- package/docs/public/pulse-ui.css +1036 -22
- package/docs/public/retrowheels.css +402 -0
- package/docs/public/shop.css +235 -0
- package/docs/public/sparkys.css +349 -0
- package/docs/public/suzukibento.css +517 -0
- package/docs/public/swish.css +394 -0
- package/docs/server.js +19 -4
- package/docs/src/lib/nav.js +14 -3
- package/docs/src/pages/components/decorate.js +74 -0
- package/docs/src/pages/components/gallery.js +102 -0
- package/docs/src/pages/components/hero.js +1 -0
- package/docs/src/pages/components/marquee.js +76 -0
- package/docs/src/pages/components/photo-card.js +74 -0
- package/docs/src/pages/components/section.js +40 -1
- package/docs/src/pages/meta.js +26 -0
- package/docs/src/pages/templates/andypixel.js +260 -0
- package/docs/src/pages/templates/bookworms.js +227 -0
- package/docs/src/pages/templates/coutumes.js +226 -0
- package/docs/src/pages/templates/journey.js +186 -0
- package/docs/src/pages/templates/retrowheels.js +342 -0
- package/docs/src/pages/templates/shop.js +257 -0
- package/docs/src/pages/templates/sparkys.js +249 -0
- package/docs/src/pages/templates/suzukibento.js +200 -0
- package/docs/src/pages/templates/swish.js +313 -0
- package/docs/src/pages/templates.js +313 -2
- package/docs/src/pages/testing.js +2 -1
- 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/andypixel.css +237 -0
- package/examples/andypixel.js +258 -0
- package/examples/bookworms.css +422 -0
- package/examples/bookworms.js +225 -0
- package/examples/contact.js +1 -0
- package/examples/counter.js +1 -0
- package/examples/dev.server.js +17 -0
- package/examples/journey.css +528 -0
- package/examples/journey.js +186 -0
- package/examples/pricing.js +1 -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/examples/quiz.js +1 -0
- package/examples/retrowheels.css +402 -0
- package/examples/retrowheels.js +340 -0
- package/examples/shared.js +6 -0
- package/examples/suzukibento.css +513 -0
- package/examples/suzukibento.js +200 -0
- package/examples/swish.css +394 -0
- package/examples/swish.js +311 -0
- package/examples/todos.js +1 -0
- package/package.json +2 -3
- package/public/.pulse-ui-version +1 -1
- package/public/images/bookworms/hero-books.jpg +0 -0
- package/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/public/images/journey/178.jpg +0 -0
- package/public/images/journey/29.jpg +0 -0
- package/public/images/retrowheels/hero.jpg +0 -0
- package/public/images/shop/devbook-pro.jpg +0 -0
- package/public/images/suzukibento/1015.jpg +0 -0
- package/public/images/suzukibento/1025.jpg +0 -0
- package/public/images/suzukibento/1043.jpg +0 -0
- package/public/images/suzukibento/1059.jpg +0 -0
- package/public/images/suzukibento/1062.jpg +0 -0
- package/public/images/suzukibento/1084.jpg +0 -0
- package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/public/pulse-ui.css +1080 -22
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +22 -2
- package/src/agent/commands/verify.md +4 -0
- package/src/agent/guide-components.md +146 -8
- package/src/agent/guide-design-gallery.md +288 -0
- package/src/agent/guide-design-references.md +172 -0
- package/src/agent/guide-explore.md +313 -0
- package/src/agent/guide-styles.md +212 -11
- package/src/agent/guide-templates.md +223 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/identity.md +2 -1
- package/src/agent/skills/verify/SKILL.md +64 -28
- package/src/agent/workflow.md +247 -61
- package/src/cli/dev.js +39 -14
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +60 -33
- package/src/cli/logger.js +145 -0
- package/src/cli/scaffold.js +57 -3
- package/src/html.js +6 -1
- package/src/mcp/server.js +2561 -99
- package/src/mcp/validate-worker.js +49 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +29 -5
- package/src/spec/schema.js +21 -0
- package/src/spec/schema.test.js +28 -0
- package/src/testing/html.js +130 -4
- package/src/testing/testing.test.js +58 -0
- package/src/ui/banner.js +24 -9
- package/src/ui/brand-statement.js +58 -0
- package/src/ui/button.js +13 -2
- package/src/ui/card.js +20 -1
- package/src/ui/colophon.js +29 -0
- package/src/ui/contact-row.js +41 -0
- package/src/ui/cta.js +18 -4
- package/src/ui/decorate.js +55 -0
- package/src/ui/display-heading.js +44 -0
- package/src/ui/drop-cap.js +29 -0
- package/src/ui/editorial-hero.js +63 -0
- package/src/ui/entry-list.js +36 -0
- package/src/ui/feature.js +20 -3
- package/src/ui/footer.js +78 -9
- package/src/ui/gallery.js +56 -0
- package/src/ui/hero.js +68 -18
- package/src/ui/image-pair.js +72 -0
- package/src/ui/index.js +20 -0
- package/src/ui/input.js +1 -0
- package/src/ui/marquee.js +48 -0
- package/src/ui/nav.js +19 -9
- package/src/ui/photo-card.js +44 -0
- package/src/ui/product-card.js +65 -0
- package/src/ui/pullquote.js +14 -3
- package/src/ui/search.js +1 -0
- package/src/ui/section-label.js +38 -0
- package/src/ui/section.js +1 -1
- package/src/ui/service-card.js +47 -0
- package/src/ui/stat-grid.js +40 -0
- package/src/ui/stat.js +16 -2
- package/src/ui/testimonial.js +17 -6
- package/src/ui/textarea.js +1 -0
- package/src/ui/top-strip.js +47 -0
- package/src/ui/trust-list.js +46 -0
- package/src/ui/ui.test.js +35 -0
package/src/cli/index.js
CHANGED
|
@@ -91,7 +91,7 @@ function isDirEmpty(dir) {
|
|
|
91
91
|
return fs.readdirSync(dir).length === 0
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
async function runDefault(root) {
|
|
94
|
+
async function runDefault(root, agentFlag = null) {
|
|
95
95
|
if (!isPulseProject(root)) {
|
|
96
96
|
console.log(`\n⚡ No Pulse project found here.\n`)
|
|
97
97
|
|
|
@@ -169,13 +169,14 @@ async function launchSession(root, agentOverride = null) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
console.log(`\n⚡ Pulse
|
|
173
|
-
console.log(`
|
|
174
|
-
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`)
|
|
175
174
|
|
|
176
175
|
if (agent === 'copilot') {
|
|
176
|
+
process.env.PULSE_AGENT_MODE = '1'
|
|
177
177
|
await launchCopilotSession(root, mcpServerPath, spawn, os.default)
|
|
178
178
|
} else {
|
|
179
|
+
process.env.PULSE_AGENT_MODE = '1'
|
|
179
180
|
await launchClaudeSession(root, mcpConfig, spawn, os.default)
|
|
180
181
|
}
|
|
181
182
|
}
|
|
@@ -343,7 +344,11 @@ async function runUpdate(root) {
|
|
|
343
344
|
process.exit(1)
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
// Copy assets from the package this CLI binary lives in (import.meta.url).
|
|
348
|
+
// When globally npm-linked to the dev repo, this resolves to dev source.
|
|
349
|
+
// When installed normally, this resolves to the installed version.
|
|
346
350
|
const pkgPublic = new URL('../../public', import.meta.url).pathname
|
|
351
|
+
const pkgSrc = new URL('..', import.meta.url).pathname
|
|
347
352
|
const assets = ['pulse-ui.css', 'pulse-ui.js', '.pulse-ui-version']
|
|
348
353
|
const publicDir = path.join(root, 'public')
|
|
349
354
|
const updated = []
|
|
@@ -359,11 +364,10 @@ async function runUpdate(root) {
|
|
|
359
364
|
|
|
360
365
|
// Sync agent files into .claude/
|
|
361
366
|
const agentFiles = [
|
|
362
|
-
['
|
|
363
|
-
['
|
|
367
|
+
[path.join(pkgSrc, 'agent', 'checklist.md'), 'pulse-checklist.md'],
|
|
368
|
+
[path.join(pkgSrc, 'agent', 'coverage-check.js'), 'coverage-check.js'],
|
|
364
369
|
]
|
|
365
|
-
for (const [
|
|
366
|
-
const src = new URL(rel, import.meta.url).pathname
|
|
370
|
+
for (const [src, dst] of agentFiles) {
|
|
367
371
|
if (fs.existsSync(src)) {
|
|
368
372
|
const dstPath = path.join(root, '.claude', dst)
|
|
369
373
|
fs.mkdirSync(path.dirname(dstPath), { recursive: true })
|
|
@@ -373,7 +377,7 @@ async function runUpdate(root) {
|
|
|
373
377
|
}
|
|
374
378
|
|
|
375
379
|
// Sync slash commands into .claude/commands/
|
|
376
|
-
const commandsSrc =
|
|
380
|
+
const commandsSrc = path.join(pkgSrc, 'agent', 'commands')
|
|
377
381
|
const commandsDst = path.join(root, '.claude', 'commands')
|
|
378
382
|
if (fs.existsSync(commandsSrc)) {
|
|
379
383
|
fs.mkdirSync(commandsDst, { recursive: true })
|
|
@@ -410,9 +414,16 @@ async function runUpdate(root) {
|
|
|
410
414
|
const versionFile = path.join(publicDir, '.pulse-ui-version')
|
|
411
415
|
const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : '?'
|
|
412
416
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
417
|
+
const { c, ok: fmtOk, fail: fmtFail, icon } = await import('./fmt.js').catch(() => ({
|
|
418
|
+
c: { dim: s => s, bold: s => s },
|
|
419
|
+
ok: s => ` ✓ ${s}`,
|
|
420
|
+
fail: s => ` ✗ ${s}`,
|
|
421
|
+
icon: { bolt: () => '⚡' },
|
|
422
|
+
}))
|
|
423
|
+
|
|
424
|
+
console.log(`\n ${icon.bolt()} ${c.bold('Pulse')} updated to ${c.bold(version)}\n`)
|
|
425
|
+
for (const f of updated) console.log(fmtOk(f))
|
|
426
|
+
for (const f of missing) console.log(fmtFail(`${f} not found in package`))
|
|
416
427
|
console.log()
|
|
417
428
|
}
|
|
418
429
|
|
|
@@ -446,29 +457,37 @@ switch (command) {
|
|
|
446
457
|
process.exit(0)
|
|
447
458
|
}
|
|
448
459
|
case '--help':
|
|
449
|
-
case '-h':
|
|
460
|
+
case '-h': {
|
|
461
|
+
const { c, icon } = await import('./fmt.js').catch(() => ({
|
|
462
|
+
c: { bold: s => s, dim: s => s, cyan: s => s, purple: s => s, white: s => s },
|
|
463
|
+
icon: { bolt: () => '⚡' },
|
|
464
|
+
}))
|
|
450
465
|
console.log(`
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
Usage:
|
|
454
|
-
|
|
455
|
-
Commands:
|
|
456
|
-
(none)
|
|
457
|
-
dev
|
|
458
|
-
build
|
|
459
|
-
start
|
|
460
|
-
update
|
|
461
|
-
|
|
462
|
-
Options:
|
|
463
|
-
--agent <name>
|
|
464
|
-
-v, --version
|
|
465
|
-
-h, --help
|
|
466
|
-
|
|
467
|
-
Examples:
|
|
468
|
-
pulse
|
|
469
|
-
pulse --agent copilot
|
|
466
|
+
${icon.bolt()} ${c.bold('Pulse')} ${c.dim('— spec-first frontend framework')}
|
|
467
|
+
|
|
468
|
+
${c.bold('Usage:')} pulse ${c.dim('[command] [options]')}
|
|
469
|
+
|
|
470
|
+
${c.bold('Commands:')}
|
|
471
|
+
${c.cyan('(none)')} detect project, or launch the scaffold wizard
|
|
472
|
+
${c.cyan('dev')} start the dev server
|
|
473
|
+
${c.cyan('build')} production build ${c.dim('→ public/dist/')}
|
|
474
|
+
${c.cyan('start')} production server ${c.dim('(requires prior build)')}
|
|
475
|
+
${c.cyan('update')} re-copy pulse-ui assets from installed package
|
|
476
|
+
|
|
477
|
+
${c.bold('Options:')}
|
|
478
|
+
${c.cyan('--agent')} ${c.dim('<name>')} use specific agent ${c.dim('(claude | copilot)')}
|
|
479
|
+
${c.cyan('-v')}, ${c.cyan('--version')} print version and exit
|
|
480
|
+
${c.cyan('-h')}, ${c.cyan('--help')} show this help
|
|
481
|
+
|
|
482
|
+
${c.bold('Examples:')}
|
|
483
|
+
${c.dim('pulse # new project wizard')}
|
|
484
|
+
${c.dim('pulse --agent copilot # launch with GitHub Copilot CLI')}
|
|
485
|
+
${c.dim('pulse dev # dev server')}
|
|
486
|
+
${c.dim('pulse build # bundle for production')}
|
|
487
|
+
${c.dim('pulse start # serve production build')}
|
|
470
488
|
`)
|
|
471
489
|
process.exit(0)
|
|
490
|
+
}
|
|
472
491
|
case 'dev':
|
|
473
492
|
await runDev(CWD)
|
|
474
493
|
break
|
|
@@ -496,6 +515,14 @@ switch (command) {
|
|
|
496
515
|
case 'update':
|
|
497
516
|
await runUpdate(CWD)
|
|
498
517
|
break
|
|
518
|
+
case 'link': {
|
|
519
|
+
// DEV-ONLY: npm link the local dev repo into this project, then pulse update.
|
|
520
|
+
const { execSync } = await import('child_process')
|
|
521
|
+
console.log('\n Linking @invisibleloop/pulse from dev repo…\n')
|
|
522
|
+
execSync('npm link @invisibleloop/pulse', { cwd: CWD, stdio: 'inherit' })
|
|
523
|
+
await runUpdate(CWD)
|
|
524
|
+
break
|
|
525
|
+
}
|
|
499
526
|
default:
|
|
500
|
-
await runDefault(CWD)
|
|
527
|
+
await runDefault(CWD, agentFlag)
|
|
501
528
|
}
|
|
@@ -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
|
|
|
@@ -451,8 +505,8 @@ const fallback = spec.onViewError(new Error('boom'), {}, {})
|
|
|
451
505
|
assert(fallback.includes('main-content'))
|
|
452
506
|
\`\`\`
|
|
453
507
|
|
|
454
|
-
**Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`,
|
|
455
|
-
Descendant selectors
|
|
508
|
+
**Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`, combinations (\`button.primary[disabled]\`), and **descendant combinators** (\`r.count('tbody tr')\`, \`r.find('.card h2')\`).
|
|
509
|
+
Descendant selectors match the rightmost token within the ancestor context — \`r.count('.foo span')\` counts spans that are descendants of \`.foo\` elements.
|
|
456
510
|
|
|
457
511
|
@.claude/pulse-checklist.md
|
|
458
512
|
`
|
package/src/html.js
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Escape HTML special characters. Use this in view functions whenever
|
|
8
8
|
* rendering untrusted user data into HTML attributes or text content.
|
|
9
|
+
*
|
|
10
|
+
* Preserves HTML entities (·, , ©, etc) so component
|
|
11
|
+
* labels can use them without double-escaping.
|
|
9
12
|
*
|
|
10
13
|
* @param {unknown} str
|
|
11
14
|
* @returns {string}
|
|
12
15
|
*/
|
|
13
16
|
export function escHtml(str) {
|
|
14
17
|
return String(str)
|
|
15
|
-
|
|
18
|
+
// Replace & only if not already part of an HTML entity
|
|
19
|
+
// Matches &<word>; or &#<digits>; or &#x<hex>;
|
|
20
|
+
.replace(/&(?![a-zA-Z]+;|#\d+;|#x[0-9a-fA-F]+;)/g, '&')
|
|
16
21
|
.replace(/</g, '<')
|
|
17
22
|
.replace(/>/g, '>')
|
|
18
23
|
.replace(/"/g, '"')
|