@invisibleloop/pulse 0.12.2 → 0.14.1
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/verify.md +9 -5
- 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 +36 -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 +1051 -25
- package/docs/public/pulse-ui.js +6 -0
- 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/phone-frame.js +26 -7
- 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/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/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 +1 -1
- 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 +1051 -22
- package/public/pulse-ui.js +6 -0
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +20 -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 +197 -11
- package/src/agent/guide-templates.md +220 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/workflow.md +208 -58
- package/src/cli/dev.js +39 -7
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +63 -30
- package/src/cli/scaffold.js +2 -2
- package/src/html.js +6 -1
- package/src/mcp/server.js +2415 -101
- package/src/mcp/validate-worker.js +34 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +22 -2
- package/src/spec/schema.js +5 -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 +12 -1
- 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/phone-frame.js +6 -5
- 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/dev.js
CHANGED
|
@@ -57,7 +57,15 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
|
57
57
|
const stampPath = path.join(PUBLIC_DIR, '.pulse-ui-version')
|
|
58
58
|
const stamp = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// Compare CSS byte-length so any content change triggers a re-copy, even
|
|
61
|
+
// within the same version (e.g. when iterating locally with npm link).
|
|
62
|
+
const srcCss = path.join(pkgPublic, 'pulse-ui.css')
|
|
63
|
+
const dstCss = path.join(PUBLIC_DIR, 'pulse-ui.css')
|
|
64
|
+
const srcSize = fs.existsSync(srcCss) ? fs.statSync(srcCss).size : -1
|
|
65
|
+
const dstSize = fs.existsSync(dstCss) ? fs.statSync(dstCss).size : -2
|
|
66
|
+
const cssStale = srcSize !== dstSize
|
|
67
|
+
|
|
68
|
+
if (stamp === pkgVersion && !cssStale) return
|
|
61
69
|
|
|
62
70
|
fs.mkdirSync(PUBLIC_DIR, { recursive: true })
|
|
63
71
|
for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
|
|
@@ -135,16 +143,40 @@ process.on('SIGTERM', closeReloadClients)
|
|
|
135
143
|
process.on('SIGINT', closeReloadClients)
|
|
136
144
|
|
|
137
145
|
let reloadTimer = null
|
|
138
|
-
|
|
146
|
+
let lastReload = 0
|
|
147
|
+
|
|
148
|
+
async function triggerReload(label = 'File changed') {
|
|
139
149
|
clearTimeout(reloadTimer)
|
|
140
150
|
reloadTimer = setTimeout(async () => {
|
|
151
|
+
// Debounce rapid file changes (e.g. multiple Edit tool calls)
|
|
152
|
+
const now = Date.now()
|
|
153
|
+
if (now - lastReload < 100) return
|
|
154
|
+
lastReload = now
|
|
155
|
+
|
|
156
|
+
console.log(` ⟳ ${label}, reloading specs...`)
|
|
141
157
|
try {
|
|
142
|
-
const fresh = await loadPages(ROOT,
|
|
158
|
+
const fresh = await loadPages(ROOT, now)
|
|
143
159
|
updateSpecs(fresh)
|
|
144
|
-
|
|
160
|
+
console.log(' ✓ Specs reloaded')
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error(' ✗ Spec reload failed:', err.message)
|
|
163
|
+
/* spec error — browser will show the old page, not crash */
|
|
164
|
+
}
|
|
145
165
|
notifyReload()
|
|
146
|
-
},
|
|
147
|
-
}
|
|
166
|
+
}, 200)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Watch src/ for changes to existing files (spec edits, component edits)
|
|
170
|
+
fs.watch(path.join(ROOT, 'src'), { recursive: true }, () => triggerReload('File changed'))
|
|
171
|
+
|
|
172
|
+
// Also watch the pages directory explicitly so macOS reliably fires on new file creation.
|
|
173
|
+
// On macOS, fs.watch with recursive=true watches subdirectory contents but may not fire
|
|
174
|
+
// for the parent dir entry when a brand-new file is created. Watching the dir non-recursively
|
|
175
|
+
// catches the "new entry added to this directory" event.
|
|
176
|
+
const pagesDir = path.join(ROOT, 'src', 'pages')
|
|
177
|
+
if (fs.existsSync(pagesDir)) {
|
|
178
|
+
fs.watch(pagesDir, { recursive: false }, () => triggerReload('New page detected'))
|
|
179
|
+
}
|
|
148
180
|
|
|
149
181
|
// Tiny script injected into every page — connects to SSE and reloads on change
|
|
150
182
|
// Passed as a function so the server can inject the per-request CSP nonce
|
|
@@ -160,7 +192,7 @@ const reloadScript = (nonce) => `<script nonce="${nonce}">
|
|
|
160
192
|
})();
|
|
161
193
|
</script>`
|
|
162
194
|
|
|
163
|
-
const { updateSpecs } = createServer(specs, {
|
|
195
|
+
const { updateSpecs } = await createServer(specs, {
|
|
164
196
|
port: PORT,
|
|
165
197
|
stream: true,
|
|
166
198
|
staticDir: fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : null,
|
package/src/cli/fmt.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI — terminal formatting helpers
|
|
3
|
+
*
|
|
4
|
+
* Respects NO_COLOR and CI env vars — falls back to plain text automatically.
|
|
5
|
+
* Zero dependencies, Node built-ins only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const USE_COLOR = !process.env.NO_COLOR && !process.env.CI && process.stdout.isTTY
|
|
9
|
+
|
|
10
|
+
// ANSI escape helpers — no-ops when colour is disabled
|
|
11
|
+
const c = {
|
|
12
|
+
reset: s => USE_COLOR ? `\x1b[0m${s}\x1b[0m` : s,
|
|
13
|
+
bold: s => USE_COLOR ? `\x1b[1m${s}\x1b[0m` : s,
|
|
14
|
+
dim: s => USE_COLOR ? `\x1b[2m${s}\x1b[0m` : s,
|
|
15
|
+
green: s => USE_COLOR ? `\x1b[32m${s}\x1b[0m` : s,
|
|
16
|
+
yellow: s => USE_COLOR ? `\x1b[33m${s}\x1b[0m` : s,
|
|
17
|
+
blue: s => USE_COLOR ? `\x1b[34m${s}\x1b[0m` : s,
|
|
18
|
+
cyan: s => USE_COLOR ? `\x1b[36m${s}\x1b[0m` : s,
|
|
19
|
+
white: s => USE_COLOR ? `\x1b[97m${s}\x1b[0m` : s,
|
|
20
|
+
red: s => USE_COLOR ? `\x1b[31m${s}\x1b[0m` : s,
|
|
21
|
+
purple: s => USE_COLOR ? `\x1b[35m${s}\x1b[0m` : s,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Strip ANSI codes to measure true visual width
|
|
25
|
+
function visLen(s) {
|
|
26
|
+
// eslint-disable-next-line no-control-regex
|
|
27
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Pad a string to a visual width (accounting for ANSI codes)
|
|
31
|
+
function pad(s, width, align = 'left') {
|
|
32
|
+
const len = visLen(s)
|
|
33
|
+
const fill = Math.max(0, width - len)
|
|
34
|
+
return align === 'right' ? ' '.repeat(fill) + s : s + ' '.repeat(fill)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Box ─────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export function box(lines, opts = {}) {
|
|
40
|
+
const { title = '', style = 'single' } = opts
|
|
41
|
+
const chars = style === 'double'
|
|
42
|
+
? { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' }
|
|
43
|
+
: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }
|
|
44
|
+
|
|
45
|
+
const inner = Array.isArray(lines) ? lines : [lines]
|
|
46
|
+
const maxW = Math.max(...inner.map(visLen), title ? visLen(title) + 2 : 0)
|
|
47
|
+
const bar = chars.h.repeat(maxW + 2)
|
|
48
|
+
|
|
49
|
+
const top = title
|
|
50
|
+
? `${chars.tl}${chars.h} ${c.bold(title)} ${chars.h.repeat(maxW - visLen(title) - 1)}${chars.tr}`
|
|
51
|
+
: `${chars.tl}${bar}${chars.tr}`
|
|
52
|
+
|
|
53
|
+
const rows = inner.map(l => `${chars.v} ${pad(l, maxW)} ${chars.v}`)
|
|
54
|
+
return [top, ...rows, `${chars.bl}${bar}${chars.br}`].join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Header (branded section label) ──────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function header(text) {
|
|
60
|
+
const line = `${c.purple('⚡')} ${c.bold(c.white(text))}`
|
|
61
|
+
return `\n${line}\n${c.dim('─'.repeat(visLen('⚡ ' + text) - (USE_COLOR ? 0 : 0)))}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Table ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render an aligned table.
|
|
68
|
+
* @param {string[]} cols - Column headers
|
|
69
|
+
* @param {string[][]} rows - Data rows (array of arrays)
|
|
70
|
+
* @param {{align?: ('left'|'right')[]}} opts
|
|
71
|
+
*/
|
|
72
|
+
export function table(cols, rows, opts = {}) {
|
|
73
|
+
const aligns = opts.align || cols.map(() => 'left')
|
|
74
|
+
const allRows = [cols, ...rows]
|
|
75
|
+
const widths = cols.map((_, i) => Math.max(...allRows.map(r => visLen(r[i] ?? ''))))
|
|
76
|
+
|
|
77
|
+
const sep = ' '
|
|
78
|
+
|
|
79
|
+
const headerRow = cols.map((h, i) => pad(c.dim(h), widths[i], aligns[i])).join(sep)
|
|
80
|
+
const divider = c.dim('─'.repeat(widths.reduce((s, w, i) => s + w + (i < widths.length - 1 ? sep.length : 0), 0)))
|
|
81
|
+
const body = rows.map(row =>
|
|
82
|
+
row.map((cell, i) => pad(cell ?? '', widths[i], aligns[i])).join(sep)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return [headerRow, divider, ...body].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Status line helpers ──────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export const icon = {
|
|
91
|
+
ok: () => c.green('✓'),
|
|
92
|
+
fail: () => c.red('✗'),
|
|
93
|
+
warn: () => c.yellow('⚠'),
|
|
94
|
+
info: () => c.cyan('·'),
|
|
95
|
+
run: () => c.yellow('›'),
|
|
96
|
+
bolt: () => c.purple('⚡'),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function ok(msg) { return ` ${icon.ok()} ${msg}` }
|
|
100
|
+
export function fail(msg) { return ` ${icon.fail()} ${c.red(msg)}` }
|
|
101
|
+
export function warn(msg) { return ` ${icon.warn()} ${c.yellow(msg)}` }
|
|
102
|
+
export function info(msg) { return ` ${icon.info()} ${c.dim(msg)}` }
|
|
103
|
+
|
|
104
|
+
// ─── Timing ───────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function elapsed(ms) {
|
|
107
|
+
if (ms < 1000) return `${Math.round(ms)}ms`
|
|
108
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export { c, pad, visLen }
|
package/src/cli/index.js
CHANGED
|
@@ -33,6 +33,13 @@ 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
|
+
|
|
36
43
|
// ---------------------------------------------------------------------------
|
|
37
44
|
// Helpers
|
|
38
45
|
// ---------------------------------------------------------------------------
|
|
@@ -91,7 +98,7 @@ function isDirEmpty(dir) {
|
|
|
91
98
|
return fs.readdirSync(dir).length === 0
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
async function runDefault(root) {
|
|
101
|
+
async function runDefault(root, agentFlag = null) {
|
|
95
102
|
if (!isPulseProject(root)) {
|
|
96
103
|
console.log(`\n⚡ No Pulse project found here.\n`)
|
|
97
104
|
|
|
@@ -343,7 +350,11 @@ async function runUpdate(root) {
|
|
|
343
350
|
process.exit(1)
|
|
344
351
|
}
|
|
345
352
|
|
|
353
|
+
// Copy assets from the package this CLI binary lives in (import.meta.url).
|
|
354
|
+
// When globally npm-linked to the dev repo, this resolves to dev source.
|
|
355
|
+
// When installed normally, this resolves to the installed version.
|
|
346
356
|
const pkgPublic = new URL('../../public', import.meta.url).pathname
|
|
357
|
+
const pkgSrc = new URL('..', import.meta.url).pathname
|
|
347
358
|
const assets = ['pulse-ui.css', 'pulse-ui.js', '.pulse-ui-version']
|
|
348
359
|
const publicDir = path.join(root, 'public')
|
|
349
360
|
const updated = []
|
|
@@ -359,11 +370,10 @@ async function runUpdate(root) {
|
|
|
359
370
|
|
|
360
371
|
// Sync agent files into .claude/
|
|
361
372
|
const agentFiles = [
|
|
362
|
-
['
|
|
363
|
-
['
|
|
373
|
+
[path.join(pkgSrc, 'agent', 'checklist.md'), 'pulse-checklist.md'],
|
|
374
|
+
[path.join(pkgSrc, 'agent', 'coverage-check.js'), 'coverage-check.js'],
|
|
364
375
|
]
|
|
365
|
-
for (const [
|
|
366
|
-
const src = new URL(rel, import.meta.url).pathname
|
|
376
|
+
for (const [src, dst] of agentFiles) {
|
|
367
377
|
if (fs.existsSync(src)) {
|
|
368
378
|
const dstPath = path.join(root, '.claude', dst)
|
|
369
379
|
fs.mkdirSync(path.dirname(dstPath), { recursive: true })
|
|
@@ -373,7 +383,7 @@ async function runUpdate(root) {
|
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
// Sync slash commands into .claude/commands/
|
|
376
|
-
const commandsSrc =
|
|
386
|
+
const commandsSrc = path.join(pkgSrc, 'agent', 'commands')
|
|
377
387
|
const commandsDst = path.join(root, '.claude', 'commands')
|
|
378
388
|
if (fs.existsSync(commandsSrc)) {
|
|
379
389
|
fs.mkdirSync(commandsDst, { recursive: true })
|
|
@@ -410,9 +420,16 @@ async function runUpdate(root) {
|
|
|
410
420
|
const versionFile = path.join(publicDir, '.pulse-ui-version')
|
|
411
421
|
const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : '?'
|
|
412
422
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
423
|
+
const { c, ok: fmtOk, fail: fmtFail, icon } = await import('./fmt.js').catch(() => ({
|
|
424
|
+
c: { dim: s => s, bold: s => s },
|
|
425
|
+
ok: s => ` ✓ ${s}`,
|
|
426
|
+
fail: s => ` ✗ ${s}`,
|
|
427
|
+
icon: { bolt: () => '⚡' },
|
|
428
|
+
}))
|
|
429
|
+
|
|
430
|
+
console.log(`\n ${icon.bolt()} ${c.bold('Pulse')} updated to ${c.bold(version)}\n`)
|
|
431
|
+
for (const f of updated) console.log(fmtOk(f))
|
|
432
|
+
for (const f of missing) console.log(fmtFail(`${f} not found in package`))
|
|
416
433
|
console.log()
|
|
417
434
|
}
|
|
418
435
|
|
|
@@ -446,29 +463,37 @@ switch (command) {
|
|
|
446
463
|
process.exit(0)
|
|
447
464
|
}
|
|
448
465
|
case '--help':
|
|
449
|
-
case '-h':
|
|
466
|
+
case '-h': {
|
|
467
|
+
const { c, icon } = await import('./fmt.js').catch(() => ({
|
|
468
|
+
c: { bold: s => s, dim: s => s, cyan: s => s, purple: s => s, white: s => s },
|
|
469
|
+
icon: { bolt: () => '⚡' },
|
|
470
|
+
}))
|
|
450
471
|
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
|
|
472
|
+
${icon.bolt()} ${c.bold('Pulse')} ${c.dim('— spec-first frontend framework')}
|
|
473
|
+
|
|
474
|
+
${c.bold('Usage:')} pulse ${c.dim('[command] [options]')}
|
|
475
|
+
|
|
476
|
+
${c.bold('Commands:')}
|
|
477
|
+
${c.cyan('(none)')} detect project, or launch the scaffold wizard
|
|
478
|
+
${c.cyan('dev')} start the dev server
|
|
479
|
+
${c.cyan('build')} production build ${c.dim('→ public/dist/')}
|
|
480
|
+
${c.cyan('start')} production server ${c.dim('(requires prior build)')}
|
|
481
|
+
${c.cyan('update')} re-copy pulse-ui assets from installed package
|
|
482
|
+
|
|
483
|
+
${c.bold('Options:')}
|
|
484
|
+
${c.cyan('--agent')} ${c.dim('<name>')} use specific agent ${c.dim('(claude | copilot)')}
|
|
485
|
+
${c.cyan('-v')}, ${c.cyan('--version')} print version and exit
|
|
486
|
+
${c.cyan('-h')}, ${c.cyan('--help')} show this help
|
|
487
|
+
|
|
488
|
+
${c.bold('Examples:')}
|
|
489
|
+
${c.dim('pulse # new project wizard')}
|
|
490
|
+
${c.dim('pulse --agent copilot # launch with GitHub Copilot CLI')}
|
|
491
|
+
${c.dim('pulse dev # dev server')}
|
|
492
|
+
${c.dim('pulse build # bundle for production')}
|
|
493
|
+
${c.dim('pulse start # serve production build')}
|
|
470
494
|
`)
|
|
471
495
|
process.exit(0)
|
|
496
|
+
}
|
|
472
497
|
case 'dev':
|
|
473
498
|
await runDev(CWD)
|
|
474
499
|
break
|
|
@@ -496,6 +521,14 @@ switch (command) {
|
|
|
496
521
|
case 'update':
|
|
497
522
|
await runUpdate(CWD)
|
|
498
523
|
break
|
|
524
|
+
case 'link': {
|
|
525
|
+
// DEV-ONLY: npm link the local dev repo into this project, then pulse update.
|
|
526
|
+
const { execSync } = await import('child_process')
|
|
527
|
+
console.log('\n Linking @invisibleloop/pulse from dev repo…\n')
|
|
528
|
+
execSync('npm link @invisibleloop/pulse', { cwd: CWD, stdio: 'inherit' })
|
|
529
|
+
await runUpdate(CWD)
|
|
530
|
+
break
|
|
531
|
+
}
|
|
499
532
|
default:
|
|
500
|
-
await runDefault(CWD)
|
|
533
|
+
await runDefault(CWD, agentFlag)
|
|
501
534
|
}
|
package/src/cli/scaffold.js
CHANGED
|
@@ -451,8 +451,8 @@ const fallback = spec.onViewError(new Error('boom'), {}, {})
|
|
|
451
451
|
assert(fallback.includes('main-content'))
|
|
452
452
|
\`\`\`
|
|
453
453
|
|
|
454
|
-
**Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`,
|
|
455
|
-
Descendant selectors
|
|
454
|
+
**Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`, combinations (\`button.primary[disabled]\`), and **descendant combinators** (\`r.count('tbody tr')\`, \`r.find('.card h2')\`).
|
|
455
|
+
Descendant selectors match the rightmost token within the ancestor context — \`r.count('.foo span')\` counts spans that are descendants of \`.foo\` elements.
|
|
456
456
|
|
|
457
457
|
@.claude/pulse-checklist.md
|
|
458
458
|
`
|
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, '"')
|