@invisibleloop/pulse 0.20.11 → 0.20.20

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.
Files changed (266) hide show
  1. package/package.json +6 -5
  2. package/public/.pulse-ui-version +1 -1
  3. package/scripts/build.js +26 -0
  4. package/scripts/release-version.js +17 -8
  5. package/scripts/strip-server.test.js +60 -0
  6. package/src/agent/checklist.md +8 -12
  7. package/src/agent/commands/pulse-start.md +2 -0
  8. package/src/agent/commands/verify.md +30 -8
  9. package/src/agent/guide-components.md +7 -25
  10. package/src/agent/guide-routing.md +0 -12
  11. package/src/agent/guide-server.md +102 -1
  12. package/src/agent/guide-spec.md +8 -1
  13. package/src/agent/guide-styles.md +23 -0
  14. package/src/agent/identity.md +9 -0
  15. package/src/agent/workflow.md +4 -23
  16. package/src/cli/check-bundles.js +113 -0
  17. package/src/cli/check-bundles.test.js +137 -0
  18. package/src/cli/cli.test.js +105 -0
  19. package/src/cli/dev.js +48 -3
  20. package/src/cli/diagnose.js +105 -0
  21. package/src/cli/diagnose.test.js +124 -0
  22. package/src/cli/discover.js +9 -2
  23. package/src/cli/index.js +171 -9
  24. package/src/cli/process-utils.js +57 -0
  25. package/src/cli/process-utils.test.js +114 -0
  26. package/src/cli/review.js +391 -0
  27. package/src/cli/review.test.js +143 -0
  28. package/src/cli/scaffold.js +9 -4
  29. package/src/cli/stamp.js +102 -0
  30. package/src/cli/stamp.test.js +149 -0
  31. package/src/cli/start.js +15 -2
  32. package/src/cli/validate.js +184 -0
  33. package/src/cli/validate.test.js +153 -0
  34. package/src/mcp/mcp.test.js +261 -30
  35. package/src/mcp/server.js +207 -515
  36. package/src/mcp/validate-worker.js +18 -1
  37. package/src/runtime/index.js +144 -11
  38. package/src/runtime/runtime.test.js +199 -0
  39. package/src/runtime/ssr.js +13 -6
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +162 -9
  42. package/src/server/server.test.js +225 -2
  43. package/src/spec/schema.js +1 -1
  44. package/src/ui/index.js +0 -5
  45. package/src/ui/ui.test.js +0 -75
  46. package/types/ui.d.ts +0 -21
  47. package/.claude/commands/build-page.md +0 -63
  48. package/.claude/commands/new-doc-page.md +0 -45
  49. package/.claude/commands/pulse-start.md +0 -11
  50. package/.claude/commands/pulse-stop.md +0 -5
  51. package/.claude/commands/verify.md +0 -118
  52. package/.claude/pulse-checklist.md +0 -189
  53. package/.github/workflows/ci.yml +0 -25
  54. package/.github/workflows/publish.yml +0 -45
  55. package/.pulse/load-reports/home/1773432711417.json +0 -22
  56. package/.pulse-verified +0 -1
  57. package/AGENTS.md +0 -697
  58. package/CLAUDE.md +0 -697
  59. package/README.md +0 -95
  60. package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
  61. package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
  62. package/benchmark/public/dist/manifest.json +0 -5
  63. package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
  64. package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
  65. package/benchmark/results/2026-03-27-09-20.json +0 -188
  66. package/benchmark/results/2026-03-27-09-21.json +0 -188
  67. package/benchmark/results/2026-03-27-09-29.json +0 -188
  68. package/benchmark/results/2026-03-27-10-14.json +0 -188
  69. package/benchmark/results/2026-03-27-10-18.json +0 -232
  70. package/benchmark/src/pages/counter.js +0 -37
  71. package/benchmark/src/pages/home.js +0 -21
  72. package/docs/.claude/commands/pulse-build.md +0 -7
  73. package/docs/.claude/commands/pulse-dev.md +0 -7
  74. package/docs/.claude/commands/pulse-start.md +0 -9
  75. package/docs/.claude/commands/pulse-stop.md +0 -5
  76. package/docs/.claude/commands/verify.md +0 -84
  77. package/docs/.claude/coverage-check.js +0 -196
  78. package/docs/.claude/pulse-checklist.md +0 -189
  79. package/docs/public/.pulse-ui-version +0 -1
  80. package/docs/public/andypixel.css +0 -237
  81. package/docs/public/bookworms.css +0 -422
  82. package/docs/public/coutumes.css +0 -561
  83. package/docs/public/docs.css +0 -2388
  84. package/docs/public/favicon.svg +0 -4
  85. package/docs/public/home-brut.css +0 -982
  86. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  87. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/docs/public/images/journey/178.jpg +0 -0
  91. package/docs/public/images/journey/29.jpg +0 -0
  92. package/docs/public/images/retrowheels/hero.jpg +0 -0
  93. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  94. package/docs/public/images/suzukibento/1015.jpg +0 -0
  95. package/docs/public/images/suzukibento/1025.jpg +0 -0
  96. package/docs/public/images/suzukibento/1043.jpg +0 -0
  97. package/docs/public/images/suzukibento/1059.jpg +0 -0
  98. package/docs/public/images/suzukibento/1062.jpg +0 -0
  99. package/docs/public/images/suzukibento/1084.jpg +0 -0
  100. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/docs/public/img/heic1501a.jpg +0 -0
  111. package/docs/public/img/placeholder-landscape.svg +0 -18
  112. package/docs/public/img/placeholder-square.svg +0 -14
  113. package/docs/public/img/placeholder-tall.svg +0 -22
  114. package/docs/public/journey.css +0 -528
  115. package/docs/public/menu.js +0 -327
  116. package/docs/public/pulse-ui.css +0 -3028
  117. package/docs/public/pulse-ui.js +0 -257
  118. package/docs/public/retrowheels.css +0 -425
  119. package/docs/public/shop.css +0 -269
  120. package/docs/public/sparkys.css +0 -382
  121. package/docs/public/suzukibento.css +0 -517
  122. package/docs/public/swish.css +0 -394
  123. package/docs/public/theme.css +0 -52
  124. package/docs/public/themes/lumio.css +0 -50
  125. package/docs/public/themes/observatory.css +0 -104
  126. package/docs/server.js +0 -161
  127. package/docs/src/lib/component-page.js +0 -49
  128. package/docs/src/lib/highlight.js +0 -2
  129. package/docs/src/lib/layout.js +0 -190
  130. package/docs/src/lib/metrics-store.js +0 -6
  131. package/docs/src/lib/nav.js +0 -171
  132. package/docs/src/lib/stats.js +0 -81
  133. package/docs/src/pages/accessibility.js +0 -157
  134. package/docs/src/pages/actions.js +0 -194
  135. package/docs/src/pages/auth.js +0 -176
  136. package/docs/src/pages/caching.js +0 -95
  137. package/docs/src/pages/components/accordion.js +0 -48
  138. package/docs/src/pages/components/alert.js +0 -35
  139. package/docs/src/pages/components/app-badge.js +0 -41
  140. package/docs/src/pages/components/avatar.js +0 -35
  141. package/docs/src/pages/components/badge.js +0 -36
  142. package/docs/src/pages/components/breadcrumbs.js +0 -94
  143. package/docs/src/pages/components/button.js +0 -84
  144. package/docs/src/pages/components/card.js +0 -225
  145. package/docs/src/pages/components/charts.js +0 -283
  146. package/docs/src/pages/components/checkbox.js +0 -129
  147. package/docs/src/pages/components/cluster.js +0 -47
  148. package/docs/src/pages/components/container.js +0 -40
  149. package/docs/src/pages/components/cta.js +0 -54
  150. package/docs/src/pages/components/divider.js +0 -37
  151. package/docs/src/pages/components/empty.js +0 -36
  152. package/docs/src/pages/components/feature.js +0 -60
  153. package/docs/src/pages/components/fieldset.js +0 -65
  154. package/docs/src/pages/components/fileupload.js +0 -128
  155. package/docs/src/pages/components/footer.js +0 -58
  156. package/docs/src/pages/components/grid.js +0 -169
  157. package/docs/src/pages/components/heading.js +0 -107
  158. package/docs/src/pages/components/hero.js +0 -123
  159. package/docs/src/pages/components/icons.js +0 -410
  160. package/docs/src/pages/components/image.js +0 -71
  161. package/docs/src/pages/components/input.js +0 -51
  162. package/docs/src/pages/components/media.js +0 -51
  163. package/docs/src/pages/components/modal.js +0 -111
  164. package/docs/src/pages/components/nav.js +0 -136
  165. package/docs/src/pages/components/phone-frame.js +0 -74
  166. package/docs/src/pages/components/progress.js +0 -102
  167. package/docs/src/pages/components/prose.js +0 -111
  168. package/docs/src/pages/components/pullquote.js +0 -71
  169. package/docs/src/pages/components/radio.js +0 -194
  170. package/docs/src/pages/components/rating.js +0 -106
  171. package/docs/src/pages/components/search.js +0 -61
  172. package/docs/src/pages/components/section.js +0 -98
  173. package/docs/src/pages/components/segmented.js +0 -121
  174. package/docs/src/pages/components/select.js +0 -45
  175. package/docs/src/pages/components/slider.js +0 -115
  176. package/docs/src/pages/components/spinner.js +0 -73
  177. package/docs/src/pages/components/stack.js +0 -48
  178. package/docs/src/pages/components/stat.js +0 -55
  179. package/docs/src/pages/components/stepper.js +0 -66
  180. package/docs/src/pages/components/table.js +0 -45
  181. package/docs/src/pages/components/textarea.js +0 -31
  182. package/docs/src/pages/components/toggle.js +0 -84
  183. package/docs/src/pages/components/tooltip.js +0 -48
  184. package/docs/src/pages/components.js +0 -200
  185. package/docs/src/pages/config.js +0 -193
  186. package/docs/src/pages/constraints.js +0 -100
  187. package/docs/src/pages/deploy.js +0 -233
  188. package/docs/src/pages/error-pages.js +0 -99
  189. package/docs/src/pages/extending.js +0 -198
  190. package/docs/src/pages/faq.js +0 -99
  191. package/docs/src/pages/forms.js +0 -129
  192. package/docs/src/pages/getting-started.js +0 -147
  193. package/docs/src/pages/guard.js +0 -120
  194. package/docs/src/pages/home.js +0 -350
  195. package/docs/src/pages/how-it-works.js +0 -244
  196. package/docs/src/pages/hydration.js +0 -108
  197. package/docs/src/pages/images.js +0 -120
  198. package/docs/src/pages/markdown.js +0 -156
  199. package/docs/src/pages/meta.js +0 -193
  200. package/docs/src/pages/mutations.js +0 -107
  201. package/docs/src/pages/navigation.js +0 -85
  202. package/docs/src/pages/performance.js +0 -157
  203. package/docs/src/pages/persist.js +0 -89
  204. package/docs/src/pages/project-structure.js +0 -91
  205. package/docs/src/pages/prompt-examples.js +0 -186
  206. package/docs/src/pages/raw-responses.js +0 -122
  207. package/docs/src/pages/routing.js +0 -120
  208. package/docs/src/pages/server-api.js +0 -291
  209. package/docs/src/pages/server-data.js +0 -203
  210. package/docs/src/pages/slash-commands.js +0 -55
  211. package/docs/src/pages/spec.js +0 -207
  212. package/docs/src/pages/state.js +0 -102
  213. package/docs/src/pages/store.js +0 -203
  214. package/docs/src/pages/streaming.js +0 -107
  215. package/docs/src/pages/stripe.js +0 -194
  216. package/docs/src/pages/supabase.js +0 -324
  217. package/docs/src/pages/templates/andypixel.js +0 -260
  218. package/docs/src/pages/templates/blog-post.js +0 -226
  219. package/docs/src/pages/templates/bookworms.js +0 -227
  220. package/docs/src/pages/templates/coutumes.js +0 -237
  221. package/docs/src/pages/templates/journey.js +0 -186
  222. package/docs/src/pages/templates/mobile-app.js +0 -371
  223. package/docs/src/pages/templates/retrowheels.js +0 -355
  224. package/docs/src/pages/templates/shop.js +0 -268
  225. package/docs/src/pages/templates/sparkys.js +0 -264
  226. package/docs/src/pages/templates/suzukibento.js +0 -200
  227. package/docs/src/pages/templates/swish.js +0 -313
  228. package/docs/src/pages/templates.js +0 -534
  229. package/docs/src/pages/testing.js +0 -199
  230. package/docs/src/pages/validation.js +0 -138
  231. package/examples/.claude/commands/pulse-build.md +0 -7
  232. package/examples/.claude/commands/pulse-dev.md +0 -7
  233. package/examples/.claude/commands/pulse-start.md +0 -9
  234. package/examples/.claude/commands/pulse-stop.md +0 -5
  235. package/examples/.claude/commands/verify.md +0 -84
  236. package/examples/.claude/coverage-check.js +0 -196
  237. package/examples/.claude/pulse-checklist.md +0 -189
  238. package/examples/andypixel.css +0 -237
  239. package/examples/andypixel.js +0 -258
  240. package/examples/bookworms.css +0 -422
  241. package/examples/bookworms.js +0 -225
  242. package/examples/contact.js +0 -166
  243. package/examples/counter.js +0 -94
  244. package/examples/dev.server.js +0 -110
  245. package/examples/examples.test.js +0 -394
  246. package/examples/journey.css +0 -528
  247. package/examples/journey.js +0 -186
  248. package/examples/pricing.js +0 -262
  249. package/examples/products.js +0 -190
  250. package/examples/public/.pulse-ui-version +0 -1
  251. package/examples/public/pulse-ui.css +0 -3028
  252. package/examples/public/pulse-ui.js +0 -257
  253. package/examples/quiz.js +0 -208
  254. package/examples/retrowheels.css +0 -425
  255. package/examples/retrowheels.js +0 -353
  256. package/examples/shared.js +0 -84
  257. package/examples/suzukibento.css +0 -513
  258. package/examples/suzukibento.js +0 -200
  259. package/examples/swish.css +0 -394
  260. package/examples/swish.js +0 -311
  261. package/examples/todos.js +0 -162
  262. package/src/ui/fieldset.js +0 -37
  263. package/src/ui/phone-frame.js +0 -40
  264. package/src/ui/search.js +0 -78
  265. package/src/ui/segmented.js +0 -50
  266. package/src/ui/tooltip.js +0 -28
@@ -1,129 +0,0 @@
1
- import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
2
- import { prevNext } from '../lib/nav.js'
3
- import { highlight } from '../lib/highlight.js'
4
-
5
- const { prev, next } = prevNext('/forms')
6
-
7
- export default {
8
- route: '/forms',
9
- meta: {
10
- theme: 'light',
11
- title: 'Forms — Pulse Docs',
12
- description: 'Server-side form handling in Pulse. spec.submit processes POST requests without client JavaScript, with automatic CSRF protection and POST-redirect-GET.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- view: () => renderLayout({
16
- currentHref: '/forms',
17
- prev,
18
- next,
19
- content: `
20
- ${h1('Forms')}
21
- ${lead('A spec with a <code>submit</code> handler accepts POST requests on its own route — the form works with JavaScript disabled. CSRF protection is automatic, and successful submissions follow POST-redirect-GET. Client-side <code>actions</code> become an enhancement, not a requirement.')}
22
-
23
- ${section('basics', 'Basic usage')}
24
- <p>Add a <code>submit</code> function to the spec and render a plain <code>&lt;form method="POST"&gt;</code>. The route accepts POST automatically — no <code>methods</code> declaration needed.</p>
25
- ${codeBlock(highlight(`export default {
26
- route: '/contact',
27
-
28
- submit: async (ctx) => {
29
- const data = await ctx.formData()
30
- if (!data?.email) return { errors: { email: 'Email is required' }, values: data ?? {} }
31
- await sendMessage(data)
32
- return { redirect: '/contact?sent=1' } // 303 POST-redirect-GET
33
- },
34
-
35
- view: (state, server) => \`
36
- <main id="main-content">
37
- \${server.form?.errors?.email ? \`<p class="error">\${escHtml(server.form.errors.email)}</p>\` : ''}
38
- <form method="POST">
39
- \${server.csrf}
40
- \${input({ label: 'Email', name: 'email', type: 'email', required: true,
41
- value: server.form?.values?.email ?? '' })}
42
- \${button({ label: 'Send', type: 'submit' })}
43
- </form>
44
- </main>
45
- \`,
46
- }`, 'js'))}
47
- <p>The flow: the browser POSTs the form, <code>submit(ctx)</code> runs server-side, and either redirects (success) or re-renders the page with the returned value available to the view as <code>server.form</code> (validation failure).</p>
48
-
49
- ${callout('warning', '<strong><code>\${server.csrf}</code> must be inside every POST form.</strong> It renders the hidden CSRF token input — submissions without a valid token are rejected with <strong>403</strong> before <code>submit</code> runs.')}
50
-
51
- ${section('return-values', 'Return values')}
52
- ${table(
53
- ['Return', 'Response', 'Use for'],
54
- [
55
- ['<code>{ redirect: \'/path\' }</code>', '<strong>303 See Other</strong>', 'Successful submissions — POST-redirect-GET means refresh never resubmits'],
56
- ['<code>{ errors, values, … }</code>', 'Page re-renders (200), value exposed as <code>server.form</code>', 'Validation failures — show errors, echo values back into inputs'],
57
- ['<code>{ status: 422, errors, … }</code>', 'Same re-render with status <strong>422</strong>', 'Validation failures where the status code matters (API clients, tests)'],
58
- ['<code>undefined</code>', 'Page re-renders (200), <code>server.form</code> is <code>{}</code>', 'Fire-and-forget side effects'],
59
- ]
60
- )}
61
- <p><strong>Always redirect after a successful mutation.</strong> Rendering a success message directly from the POST means a browser refresh resubmits the form. Redirect to a URL that shows the confirmation instead (e.g. <code>/contact?sent=1</code>).</p>
62
-
63
- ${section('server-form', 'server.form — errors and echoed values')}
64
- <p>On a re-render, whatever <code>submit</code> returned is available to the view as <code>server.form</code>. On a plain GET it is <code>undefined</code> — always use optional chaining.</p>
65
- ${codeBlock(highlight(`// In submit — return errors AND the submitted values:
66
- return { errors: { email: 'Invalid email' }, values: data }
67
-
68
- // In the view — show the error, echo the value back so the form isn't wiped:
69
- \${server.form?.errors?.email ? \`<p class="error">\${escHtml(server.form.errors.email)}</p>\` : ''}
70
- \${input({ name: 'email', label: 'Email', value: server.form?.values?.email ?? '' })}`, 'js'))}
71
- ${callout('warning', 'Escape user-submitted values before interpolating them into HTML — <code>server.form.values</code> is raw user input. Component props like <code>input({ value })</code> are auto-escaped; manual interpolation needs <code>escHtml()</code>.')}
72
-
73
- ${section('csrf', 'CSRF protection')}
74
- <p>Every <code>submit</code> spec gets CSRF protection automatically — there is nothing to configure. Under the hood Pulse uses an HMAC double-submit pattern:</p>
75
- ${table(
76
- ['Piece', 'Where it lives', 'What it does'],
77
- [
78
- ['Random id', '<code>pulse_csrf</code> cookie (HttpOnly, SameSite=Lax)', 'Set on the first GET of the form page'],
79
- ['Token', 'Hidden <code>_csrf</code> input via <code>\${server.csrf}</code>', '<code>HMAC-SHA256(secret, id)</code> — embedded into the form server-side'],
80
- ['Verification', 'On every POST, before <code>submit</code> runs', 'Recomputes the HMAC from the cookie and compares in constant time — mismatch is 403'],
81
- ]
82
- )}
83
- <p>An attacker on another origin can cause the cookie to be sent, but cannot read it or compute the HMAC — so they cannot forge the pair.</p>
84
-
85
- ${sub('multi-instance', 'Multiple server instances')}
86
- <p>The HMAC secret defaults to a random value per server boot. Behind a load balancer, pin it so tokens issued by one instance validate on another:</p>
87
- ${codeBlock(highlight(`await createServer(pages, {
88
- secret: process.env.PULSE_SECRET, // stable across instances and restarts
89
- })`, 'js'))}
90
-
91
- ${sub('opt-out', 'Opting out')}
92
- <p>Set <code>csrf: false</code> on the spec to disable protection — only for endpoints with their own authentication, such as signed webhooks:</p>
93
- ${codeBlock(highlight(`export default {
94
- route: '/webhooks/stripe',
95
- csrf: false, // Stripe signs its requests — verify the signature instead
96
- submit: async (ctx) => {
97
- verifyStripeSignature(ctx.headers['stripe-signature'], await ctx.text())
98
- // ...
99
- },
100
- view: () => '<main id="main-content">ok</main>',
101
- }`, 'js'))}
102
- ${callout('warning', 'Never set <code>csrf: false</code> to "fix" a 403. The 403 means the form is missing <code>\${server.csrf}</code> — add the field, don\'t remove the protection.')}
103
-
104
- ${section('progressive-enhancement', 'Progressive enhancement')}
105
- <p>The same form can serve both worlds: <code>method="POST"</code> is the no-JS fallback, <code>data-action</code> is the hydrated path. With JavaScript enabled the runtime intercepts the submit and runs the async action (no page reload); without it the browser POSTs to <code>submit</code>.</p>
106
- ${codeBlock(highlight(`<form method="POST" data-action="send">
107
- \${server.csrf}
108
- \${input({ label: 'Email', name: 'email', type: 'email', required: true })}
109
- \${button({ label: 'Send', type: 'submit' })}
110
- </form>`, 'html'))}
111
- <p>Keep the two paths consistent: <code>submit</code> and the action's <code>run</code> should call the same underlying function so validation and side effects cannot drift apart.</p>
112
-
113
- ${section('caching', 'Interaction with caching')}
114
- <p>Pages with <code>submit</code> are automatically excluded from the in-process page cache — the CSRF token is per-visitor, and a cached page would serve one visitor's token to everyone. POST responses are never cached.</p>
115
-
116
- ${section('reference', 'Reference')}
117
- ${table(
118
- ['Property', 'Type', 'Notes'],
119
- [
120
- ['<code>submit</code>', '<code>async (ctx) =&gt; { redirect } | object | void</code>', 'Handles POST on the route. Guard runs first.'],
121
- ['<code>csrf</code>', '<code>boolean</code>', 'Default <code>true</code>. Set <code>false</code> only for self-authenticated endpoints.'],
122
- ['<code>server.csrf</code>', 'string (view)', 'Hidden <code>_csrf</code> input — required inside every POST form.'],
123
- ['<code>server.form</code>', 'object | undefined (view)', 'Return value of <code>submit</code> on a re-render; <code>undefined</code> on GET.'],
124
- ['<code>secret</code>', 'string (createServer option)', 'Stable HMAC secret for multi-instance deployments.'],
125
- ]
126
- )}
127
- `,
128
- }),
129
- }
@@ -1,147 +0,0 @@
1
- import { renderLayout, h1, lead, section, codeBlock, callout } from '../lib/layout.js'
2
- import { prevNext } from '../lib/nav.js'
3
- import { highlight } from '../lib/highlight.js'
4
-
5
- const { prev, next } = prevNext('/getting-started')
6
-
7
- export default {
8
- route: '/getting-started',
9
- meta: {
10
- theme: 'light',
11
- title: 'Getting Started — Pulse Docs',
12
- description: 'Install Pulse and build your first page with an AI agent in minutes.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- view: () => renderLayout({
16
- currentHref: '/getting-started',
17
- prev,
18
- next,
19
- content: `
20
- ${h1('Getting Started')}
21
- ${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.')}
22
-
23
- ${section('requirements', 'Requirements')}
24
- <ul>
25
- <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>
26
- <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="google.com/chrome (opens in new tab)">google.com/chrome</a></li>
27
- <li><strong>An AI agent</strong> — Claude Code or GitHub Copilot CLI (pick one):</li>
28
- </ul>
29
- <ul>
30
- <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>
31
- <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>
32
- </ul>
33
- <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>
34
-
35
- ${section('install', 'Install Pulse')}
36
- <p>Install the Pulse CLI globally:</p>
37
- ${codeBlock(highlight('npm install -g @invisibleloop/pulse', 'bash'))}
38
-
39
- ${section('create', 'Create your project')}
40
- <p>Run <code>pulse</code> in any empty directory:</p>
41
- ${codeBlock(highlight(`mkdir my-app
42
- cd my-app
43
- pulse`, 'bash'))}
44
- <p>Pulse detects the directory is empty and scaffolds a project there. It creates the project files, installs dependencies, and exits with:</p>
45
- ${codeBlock(highlight('✓ Project ready. Run `pulse` again to start your AI session.', 'bash'))}
46
- ${callout('tip', 'Running <code>pulse</code> in a non-empty directory prompts for a project name, then creates and scaffolds a subdirectory with that name — so you can run it from anywhere.')}
47
-
48
- ${section('session', 'Start a session')}
49
- <p>Run <code>pulse</code> again from inside your project directory:</p>
50
- ${codeBlock(highlight('pulse', 'bash'))}
51
- <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>
52
- ${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.')}
53
-
54
- ${section('copilot', 'Using GitHub Copilot')}
55
- <p>To use GitHub Copilot CLI instead of Claude, pass the <code>--agent</code> flag when scaffolding:</p>
56
- ${codeBlock(highlight('pulse --agent copilot', 'bash'))}
57
- <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>
58
- ${codeBlock(highlight(`export default {
59
- agent: 'copilot',
60
- }`, 'js'))}
61
- <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>
62
-
63
- ${section('first-build', 'Build your first page')}
64
- <p>Once Claude opens, start the dev server and ask for something:</p>
65
- ${codeBlock(highlight(`/pulse-dev
66
-
67
- "Create a contact form with name, email, and message fields.
68
- Validate the email format before submitting."`, 'bash'))}
69
- <p>The agent will:</p>
70
- <ol>
71
- <li>Fetch the Pulse guide from the MCP server — spec format, component library, quality rules</li>
72
- <li>Check what pages already exist in your project</li>
73
- <li>Write the spec — route, state, validation, action lifecycle, view</li>
74
- <li>Validate it against the schema and fix every error and warning</li>
75
- <li>Open the page in the browser and confirm it looks right</li>
76
- </ol>
77
- <p>You do not need to explain Pulse to the agent. The MCP server supplies the reference. Just describe what you want.</p>
78
-
79
- ${section('what-was-created', 'What got created')}
80
- <p>When you ran <code>pulse</code> in step 2, these files were written to your directory:</p>
81
- ${codeBlock(highlight(`my-app/
82
- ├── src/
83
- │ ├── pages/
84
- │ │ └── home.js ← your first page spec (a working counter)
85
- │ └── components/ ← shared view components go here
86
- ├── public/
87
- │ ├── app.css ← global stylesheet
88
- │ ├── pulse-ui.css ← Pulse component library styles
89
- │ └── pulse-ui.js ← Pulse component library behaviour
90
- ├── .claude/ ← Claude Code agent context
91
- │ ├── CLAUDE.md ← session instructions Claude reads on startup
92
- │ ├── settings.json ← hooks: syntax checks, colour guards, package blocklist
93
- │ ├── pulse-checklist.md ← spec review checklist, kept in sync by Pulse
94
- │ └── commands/ ← slash commands available inside Claude
95
- │ ├── pulse-dev.md
96
- │ ├── pulse-stop.md
97
- │ ├── pulse-build.md
98
- │ └── pulse-start.md
99
- ├── .copilot/ ← GitHub Copilot CLI agent context
100
- │ └── skills/ ← project skills available inside Copilot
101
- │ ├── build-page/
102
- │ ├── verify/
103
- │ └── new-doc-page/
104
- ├── .github/
105
- │ ├── copilot-instructions.md ← session instructions for Copilot
106
- │ └── instructions/
107
- │ └── pulse-checklist.instructions.md
108
- ├── package.json
109
- └── pulse.config.js ← port, agent, and project settings`, 'bash'))}
110
- <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>
111
- <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>
112
-
113
- ${section('commands', 'Agent commands')}
114
- <p>These slash commands are available once Claude is open:</p>
115
- ${codeBlock(highlight(`/pulse-dev # start (or restart) the dev server
116
- /pulse-stop # stop the dev server
117
- /pulse-build # production build → public/dist/
118
- /pulse-start # run the production server
119
- /pulse-report # Lighthouse audit + performance report`, 'bash'))}
120
- <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>
121
- <p>You can also skip commands and skills entirely — just describe what you want and the agent handles the rest.</p>
122
-
123
- ${section('update', 'Keeping up to date')}
124
- <p>When a new version of Pulse is released, update the package and then run <code>pulse update</code> from your project root:</p>
125
- ${codeBlock(highlight(`npm update @invisibleloop/pulse
126
- pulse update`, 'bash'))}
127
- <p><code>pulse update</code> copies the latest UI assets and agent files into your project:</p>
128
- <ul>
129
- <li><code>public/pulse-ui.css</code> and <code>public/pulse-ui.js</code> — component library styles and behaviour</li>
130
- <li><code>.claude/pulse-checklist.md</code> and <code>.claude/commands/</code> — Claude agent files</li>
131
- <li><code>.copilot/skills/</code> — Copilot project skills</li>
132
- <li><code>.github/instructions/pulse-checklist.instructions.md</code> — Copilot checklist</li>
133
- </ul>
134
- <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>
135
-
136
- ${section('next-steps', 'Next steps')}
137
- <ul>
138
- <li><a href="/how-it-works">How It Works</a> — the MCP server, what the agent knows, and the full build cycle</li>
139
- <li><a href="/project-structure">Project Structure</a> — where files live and how pages are discovered</li>
140
- <li><a href="/spec">Spec Reference</a> — every field the spec supports</li>
141
- <li><a href="/state">State</a> — client state and mutations</li>
142
- <li><a href="/server-data">Server Data</a> — fetch data on the server before the page renders</li>
143
- <li><a href="/prompt-examples">Prompt Examples</a> — real prompts with the output they produce</li>
144
- </ul>
145
- `,
146
- }),
147
- }
@@ -1,120 +0,0 @@
1
- import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
2
- import { prevNext } from '../lib/nav.js'
3
- import { highlight } from '../lib/highlight.js'
4
-
5
- const { prev, next } = prevNext('/guard')
6
-
7
- export default {
8
- route: '/guard',
9
- meta: {
10
- theme: 'light',
11
- title: 'Guard — Pulse Docs',
12
- description: 'Per-route authorization in Pulse. Guard functions run before server data fetchers and redirect unauthorized requests.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- view: () => renderLayout({
16
- currentHref: '/guard',
17
- prev,
18
- next,
19
- content: `
20
- ${h1('Guard')}
21
- ${lead('A <code>guard</code> function runs on every request to a route, before any server data is fetched. It is the enforced access control point — unauthorized requests are redirected before any database queries or data fetchers execute.')}
22
-
23
- ${section('basics', 'Basic usage')}
24
- <p>A <code>guard</code> function on any spec receives the same <code>ctx</code> object as server data fetchers — params, query, headers, and cookies.</p>
25
- ${codeBlock(highlight(`export default {
26
- route: '/dashboard',
27
-
28
- guard: async (ctx) => {
29
- if (!ctx.cookies.session) return { redirect: '/login' }
30
- },
31
-
32
- server: {
33
- user: async (ctx) => getCurrentUser(ctx.cookies.session),
34
- },
35
-
36
- view: (state, server) => \`
37
- <main id="main-content">
38
- <h1>Welcome, \${server.user.name}</h1>
39
- </main>
40
- \`,
41
- }`, 'js'))}
42
-
43
- <p>When the guard returns <code>{ redirect }</code>, the server responds with a <strong>302</strong> and all server data fetchers are skipped — no data is fetched for unauthorized requests. When the guard returns nothing, the request proceeds normally.</p>
44
-
45
- ${section('ctx', 'What ctx contains')}
46
- ${table(
47
- ['Property / Method', 'Type', 'Description'],
48
- [
49
- ['<code>ctx.cookies</code>', 'object', 'Parsed cookies from the <code>Cookie</code> header'],
50
- ['<code>ctx.headers</code>', 'object', 'Raw request headers'],
51
- ['<code>ctx.params</code>', 'object', 'Route params e.g. <code>{ id: "42" }</code>'],
52
- ['<code>ctx.query</code>', 'object', 'Parsed query string'],
53
- ['<code>ctx.pathname</code>','string', 'URL path e.g. <code>/dashboard</code>'],
54
- ['<code>ctx.method</code>', 'string', 'HTTP method e.g. <code>GET</code>, <code>POST</code>'],
55
- ['<code>ctx.store</code>', 'object', 'Resolved global store state (if a store is registered)'],
56
- ['<code>ctx.nonce</code>', 'string', 'CSP nonce for the current request'],
57
- ['<code>await ctx.json()</code>', 'object | null', 'Parse a JSON request body'],
58
- ['<code>await ctx.text()</code>', 'string', 'Read the body as a plain string'],
59
- ['<code>await ctx.formData()</code>', 'object | null', 'Parse a URL-encoded body into a plain object'],
60
- ['<code>await ctx.buffer()</code>', 'Buffer', 'Read the raw body as a Node.js Buffer'],
61
- ]
62
- )}
63
-
64
- ${section('examples', 'Common patterns')}
65
-
66
- ${sub('Session check')}
67
- <p>Redirect to login when no session cookie is present.</p>
68
- ${codeBlock(highlight(`guard: async (ctx) => {
69
- if (!ctx.cookies.session) return { redirect: '/login' }
70
- }`, 'js'))}
71
-
72
- ${sub('Role-based access')}
73
- <p>Fetch the user from the session and check their role. Keep the lookup fast — guard runs on every request to the route.</p>
74
- ${codeBlock(highlight(`guard: async (ctx) => {
75
- const user = await getUserFromSession(ctx.cookies.session)
76
- if (!user) return { redirect: '/login' }
77
- if (!user.isAdmin) return { redirect: '/403' }
78
- }`, 'js'))}
79
-
80
- ${sub('Redirect authenticated users away from login')}
81
- <p>Useful for login and signup pages — send already-authenticated users somewhere useful.</p>
82
- ${codeBlock(highlight(`export default {
83
- route: '/login',
84
-
85
- guard: async (ctx) => {
86
- if (ctx.cookies.session) return { redirect: '/dashboard' }
87
- },
88
-
89
- view: () => \`<main id="main-content">...</main>\`,
90
- }`, 'js'))}
91
-
92
- ${callout('info', 'Guard runs server-side on every request, including client-side navigation requests — those go through the same server pipeline. There is no way to bypass guard from the browser.')}
93
-
94
- ${section('custom-responses', 'Custom status responses')}
95
- <p>Guard can return a custom HTTP response instead of (or alongside) a redirect. Return <code>{ status, json?, body?, headers? }</code> to send any status code with an optional JSON or text body. This is useful for POST handlers that need to signal validation errors or API-style rejections:</p>
96
- ${codeBlock(highlight(`guard: async (ctx) => {
97
- const token = ctx.headers.authorization
98
- if (!token) return { status: 401, json: { error: 'Unauthorized' } }
99
-
100
- if (ctx.method === 'POST') {
101
- const data = await ctx.formData()
102
- if (!data.email) return { status: 422, json: { error: 'Email required' } }
103
- await db.leads.create(data)
104
- return { redirect: '/contact?sent=1' }
105
- }
106
- // return nothing to let a GET request proceed to the view
107
- }`, 'js'))}
108
- ${callout('note', 'To use <code>guard</code> as a POST handler, the spec must declare <code>methods: [\'GET\', \'POST\']</code>. Without it, POST requests are rejected with 405 before guard runs.')}
109
- ${callout('tip', 'For HTML form submissions, prefer <a href="/forms"><code>spec.submit</code></a> — it accepts POST automatically, gets CSRF protection for free, and re-renders the page with validation errors. Guard-based POST handling is best for API-style endpoints.')}
110
-
111
- ${section('reference', 'Reference')}
112
- ${table(
113
- ['Property', 'Type', 'Required'],
114
- [
115
- ['<code>guard</code>', '<code>async (ctx) =&gt; { redirect?: string } | { status, json?, body?, headers? } | void</code>', 'No'],
116
- ]
117
- )}
118
- `,
119
- }),
120
- }