@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,186 +0,0 @@
1
- import { renderLayout, h1, lead } from '../lib/layout.js'
2
- import { prevNext } from '../lib/nav.js'
3
-
4
- const { prev, next } = prevNext('/prompt-examples')
5
-
6
- function promptCard({ tag, prompt, produces }) {
7
- return `
8
- <div class="prompt-card">
9
- <div class="prompt-tag">${tag}</div>
10
- <blockquote class="prompt-text">${prompt}</blockquote>
11
- <p class="prompt-produces">${produces}</p>
12
- </div>`
13
- }
14
-
15
- function promptGroup(title, cards) {
16
- return `
17
- <div class="prompt-group">
18
- <h3 class="prompt-group-title">${title}</h3>
19
- <div class="prompt-grid">
20
- ${cards.map(promptCard).join('')}
21
- </div>
22
- </div>`
23
- }
24
-
25
- export default {
26
- route: '/prompt-examples',
27
- meta: {
28
- theme: 'light',
29
- title: 'Prompt Examples — Pulse Docs',
30
- description: 'Real prompts for building Pulse pages with your AI agent — from creating pages to authentication, performance audits, and integrations.',
31
- styles: ['/theme.css', '/docs.css'],
32
- },
33
- view: () => renderLayout({
34
- currentHref: '/prompt-examples',
35
- prev,
36
- next,
37
- content: `
38
- ${h1('Prompt Examples')}
39
- ${lead('These prompts produce correct Pulse specs because the framework constrains the output. The agent works within a defined structure — there is no ambiguity about where state lives, how data is fetched, or how validation is wired.')}
40
-
41
- ${promptGroup('Creating pages', [
42
- {
43
- tag: 'New page',
44
- prompt: 'Create a page at /about with a heading, a short paragraph about the company, and a link back to the home page.',
45
- produces: 'A static page spec with <code>route</code>, <code>meta</code>, and a <code>view</code> returning the HTML.',
46
- },
47
- {
48
- tag: 'Dynamic route',
49
- prompt: 'Create a blog post page at /posts/:slug. It should fetch the post from the database by slug and display the title, published date, and body content. If no post is found, return a 404.',
50
- produces: 'A spec with a parameterised <code>route</code>, a <code>server.data</code> fetcher using <code>ctx.params.slug</code>, and a 404 raw response if the post is missing.',
51
- },
52
- {
53
- tag: 'Listing page',
54
- prompt: 'Create a paginated list of articles at /articles. Read the ?page query parameter to fetch the right slice. Show 10 articles per page with previous and next links.',
55
- produces: 'A spec with <code>server.data</code> reading <code>ctx.query.page</code>, pagination state, and prev/next links rendered in the view.',
56
- },
57
- ])}
58
-
59
- ${promptGroup('State & interactions', [
60
- {
61
- tag: 'Live filter',
62
- prompt: 'Add a search input to the /products page that filters the visible product list as the user types. Keep the full product list in server data and filter it in the view.',
63
- produces: 'A mutation bound to <code>data-event="input:setQuery"</code> that updates a <code>query</code> state field, with the view filtering <code>server.data.products</code> against it.',
64
- },
65
- {
66
- tag: 'Toggle',
67
- prompt: 'Add a show/hide toggle to the FAQ page. Each question should expand its answer when clicked and collapse when clicked again.',
68
- produces: 'An <code>openId</code> state field and a <code>toggle</code> mutation. The view renders answers conditionally based on whether their id matches <code>openId</code>.',
69
- },
70
- {
71
- tag: 'Tabs',
72
- prompt: 'The /dashboard page has three tabs: Overview, Activity, and Settings. Clicking a tab should show its panel and hide the others without a page reload.',
73
- produces: 'An <code>activeTab</code> state field, a <code>setTab</code> mutation, and tab panels rendered conditionally in the view.',
74
- },
75
- ])}
76
-
77
- ${promptGroup('Forms & validation', [
78
- {
79
- tag: 'Contact form',
80
- prompt: 'Add a contact form at /contact with name, email, and message fields. All three are required. Email must be a valid format. Show inline errors on failure. Show a success message on submission.',
81
- produces: 'A spec with <code>validation</code> rules, an <code>actions.submit</code> with <code>validate: true</code>, and <code>onSuccess</code>/<code>onError</code> state transitions.',
82
- },
83
- {
84
- tag: 'Multi-step form',
85
- prompt: 'Create a multi-step signup form at /signup with three steps: account details, personal info, and confirmation. Show a step indicator at the top. Only advance when the current step is valid.',
86
- produces: 'A <code>step</code> state field, step-specific validation, a <code>next</code> action that validates before advancing, and a view that renders the active step panel.',
87
- },
88
- {
89
- tag: 'Constraints',
90
- prompt: 'The quantity input on the /cart page should never go below 1 or above 99. Enforce this on the server — not just in the UI.',
91
- produces: 'A <code>constraints</code> block with <code>quantity: { min: 1, max: 99 }</code> that the framework enforces after every mutation, regardless of what the client sends.',
92
- },
93
- ])}
94
-
95
- ${promptGroup('Server data & actions', [
96
- {
97
- tag: 'Dashboard data',
98
- prompt: 'The /dashboard page should show the logged-in user\'s name and their last 5 orders. Fetch both in parallel.',
99
- produces: 'A <code>server.data</code> fetcher that runs two queries concurrently with <code>Promise.all</code> and passes the results to the view as <code>server.data.user</code> and <code>server.data.orders</code>.',
100
- },
101
- {
102
- tag: 'Async action',
103
- prompt: 'Add a Delete button to each row in the /users table. It should send a DELETE request to the API, remove the row on success, and show an error message on failure.',
104
- produces: 'An action with <code>onStart</code> setting a loading flag, <code>run</code> making the API call, <code>onSuccess</code> removing the item from state, and <code>onError</code> setting an error message.',
105
- },
106
- {
107
- tag: 'File upload',
108
- prompt: 'Add an avatar upload form to /settings. The user picks a file, it uploads to storage, and the page shows the new avatar on success.',
109
- produces: 'An action whose <code>run</code> reads the file from <code>FormData</code>, converts it to an <code>ArrayBuffer</code>, and uploads it to storage. <code>onSuccess</code> updates the avatar URL in state.',
110
- },
111
- ])}
112
-
113
- ${promptGroup('Authentication', [
114
- {
115
- tag: 'Login page',
116
- prompt: 'Create a login page at /login with email and password fields. On success, set an httpOnly session cookie and redirect to /dashboard. Show an error message if credentials are wrong.',
117
- produces: 'A raw response action that verifies credentials, sets <code>Set-Cookie</code> headers with <code>httpOnly; SameSite=Strict</code>, and redirects. Failed logins return the form with an error state.',
118
- },
119
- {
120
- tag: 'Protected route',
121
- prompt: 'Protect the /dashboard page so only logged-in users can access it. Redirect anyone without a valid session to /login.',
122
- produces: 'A <code>guard</code> function that reads the session cookie, verifies it, and returns a redirect response if invalid.',
123
- },
124
- {
125
- tag: 'Logout',
126
- prompt: 'Add a logout button to the nav. It should clear the session and redirect to /login.',
127
- produces: 'A page spec at <code>/logout</code> using the raw response spec (<code>contentType</code> + <code>render</code>) that sets the session cookie to <code>maxAge: 0</code> and issues a 302 redirect.',
128
- },
129
- ])}
130
-
131
- ${promptGroup('Streaming SSR', [
132
- {
133
- tag: 'Deferred content',
134
- prompt: 'The /feed page is slow because it waits for the activity feed before painting anything. Make it stream — show the page chrome instantly and load the feed in the background.',
135
- produces: 'A <code>stream</code> block with <code>shell: [\'header\']</code> and <code>deferred: [\'feed\']</code>. The view becomes a keyed object of named segment functions.',
136
- },
137
- {
138
- tag: 'Multiple segments',
139
- prompt: 'The /home page has a hero, a trending section, and a recommendations section. The hero should appear instantly. The other two can load as data resolves.',
140
- produces: 'Three view segments — <code>hero</code> in the shell, <code>trending</code> and <code>recommendations</code> deferred. Each fetches its own data independently.',
141
- },
142
- ])}
143
-
144
- ${promptGroup('Performance & tooling', [
145
- {
146
- tag: 'Lighthouse audit',
147
- prompt: 'Run a Lighthouse audit across all pages and show me the results.',
148
- produces: 'The agent runs <code>/pulse-report</code>, which audits every registered route and opens the results dashboard.',
149
- },
150
- {
151
- tag: 'Score thresholds',
152
- prompt: 'Make the audit fail if any page scores below 90 for performance or below 95 for accessibility.',
153
- produces: 'A <code>lighthouse</code> block in <code>pulse.config.js</code> with <code>performance: 90</code> and <code>accessibility: 95</code>.',
154
- },
155
- {
156
- tag: 'Load test',
157
- prompt: 'Load test the /api/data endpoint with 50 concurrent connections for 30 seconds and tell me the p99 latency.',
158
- produces: 'The agent runs <code>/pulse-load</code> against that route with the specified parameters and reports the results.',
159
- },
160
- {
161
- tag: 'Environment config',
162
- prompt: 'Set up a staging environment so I can run audits against https://staging.myapp.com instead of localhost.',
163
- produces: 'An <code>environments.staging</code> block in <code>pulse.config.js</code> with the remote URL. The agent uses it when running <code>/pulse-report</code> or <code>/pulse-load</code>.',
164
- },
165
- ])}
166
-
167
- ${promptGroup('Integrations', [
168
- {
169
- tag: 'Supabase query',
170
- prompt: 'The /profile page should load the current user\'s row from the Supabase users table. It should only return data the logged-in user owns.',
171
- produces: 'A <code>server.data</code> fetcher using the Supabase public client initialised with the user\'s access token, so Row Level Security applies automatically.',
172
- },
173
- {
174
- tag: 'Supabase auth',
175
- prompt: 'Use Supabase for authentication. The login form should call Supabase\'s signInWithPassword and store the tokens in httpOnly cookies.',
176
- produces: 'A login action that calls <code>supabase.auth.signInWithPassword</code>, reads the session tokens from the response, and sets them as <code>httpOnly; SameSite=Strict</code> cookies.',
177
- },
178
- {
179
- tag: 'Stripe checkout',
180
- prompt: 'Add a checkout button to the /pricing page for the Pro plan. Clicking it should create a Stripe Checkout Session and redirect the user to the Stripe-hosted page.',
181
- produces: 'An action whose <code>run</code> calls the Stripe API to create a session and returns the checkout URL. <code>onSuccess</code> triggers a redirect via state.',
182
- },
183
- ])}
184
- `,
185
- }),
186
- }
@@ -1,122 +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('/raw-responses')
6
-
7
- export default {
8
- route: '/raw-responses',
9
- meta: {
10
- theme: 'light',
11
- title: 'Raw Responses — Pulse Docs',
12
- description: 'Return non-HTML responses from Pulse specs — RSS, XML, JSON, and other content types.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- view: () => renderLayout({
16
- currentHref: '/raw-responses',
17
- prev,
18
- next,
19
- content: `
20
- ${h1('Raw Responses')}
21
- ${lead('Setting <code>contentType</code> switches a spec from the HTML pipeline to a raw response mode. The view returns the response body directly — no doctype, no hydration script. Security headers and compression still apply.')}
22
-
23
- ${section('basics', 'The basics')}
24
- <p>Set <code>contentType</code> to any valid MIME type. When present, the normal HTML wrapper (doctype, head, body, hydration script) is skipped. The <code>render</code> function receives <code>(ctx, serverState)</code> and returns a string:</p>
25
- ${codeBlock(highlight(`export default {
26
- route: '/feed.xml',
27
- contentType: 'application/rss+xml',
28
- server: {
29
- data: async () => ({
30
- posts: await db.posts.latest(20),
31
- }),
32
- },
33
- view: (ctx, server) => \`<?xml version="1.0" encoding="UTF-8"?>
34
- <rss version="2.0">
35
- <channel>
36
- <title>My Blog</title>
37
- <link>https://example.com</link>
38
- <description>Recent posts</description>
39
- \${server.posts.map(post => \`
40
- <item>
41
- <title>\${esc(post.title)}</title>
42
- <link>https://example.com/blog/\${post.slug}</link>
43
- <pubDate>\${new Date(post.date).toUTCString()}</pubDate>
44
- <description>\${esc(post.excerpt)}</description>
45
- </item>
46
- \`).join('')}
47
- </channel>
48
- </rss>\`,
49
- }`, 'js'))}
50
-
51
- ${section('json', 'JSON API endpoints')}
52
- ${codeBlock(highlight(`export default {
53
- route: '/api/products',
54
- contentType: 'application/json',
55
- server: {
56
- data: async (ctx) => ({
57
- products: await db.products.list({
58
- page: parseInt(ctx.query.page ?? '1', 10),
59
- category: ctx.query.category,
60
- }),
61
- }),
62
- },
63
- view: (ctx, server) => JSON.stringify({
64
- products: server.products,
65
- page: parseInt(ctx.query.page ?? '1', 10),
66
- }),
67
- }`, 'js'))}
68
-
69
- ${section('sitemap', 'XML sitemap')}
70
- ${callout('tip', 'For most sites you don\'t need a raw spec for this — set <code>sitemap: true</code> in <code>createServer</code> and Pulse generates <code>/sitemap.xml</code> and <code>robots.txt</code> from the registered routes automatically. See <a href="/meta#sitemap">Metadata &amp; SEO</a>. Write a raw sitemap spec only when you need full control over the XML.')}
71
- ${codeBlock(highlight(`export default {
72
- route: '/sitemap.xml',
73
- contentType: 'application/xml',
74
- serverTtl: 3600,
75
- server: {
76
- data: async () => ({
77
- pages: await db.pages.allPublished(),
78
- }),
79
- },
80
- view: (ctx, server) => \`<?xml version="1.0" encoding="UTF-8"?>
81
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
82
- \${server.pages.map(p => \`
83
- <url>
84
- <loc>https://example.com\${p.path}</loc>
85
- <lastmod>\${p.updatedAt.toISOString().slice(0, 10)}</lastmod>
86
- </url>
87
- \`).join('')}
88
- </urlset>\`,
89
- }`, 'js'))}
90
-
91
- ${section('view-signature', 'View function signature')}
92
- <p>For raw responses, the <code>view</code> function signature is <code>(ctx, serverState)</code>, not <code>(state, serverState)</code>. The <code>ctx</code> argument is the request context object with <code>params</code>, <code>query</code>, <code>headers</code>, and <code>cookies</code>.</p>
93
- ${callout('note', 'There is no client state (<code>state</code>) for raw response specs — they are purely server-side. The <code>state: {}</code> field is still required for spec validation, but is not used.')}
94
-
95
- ${section('escaping', 'Escaping in XML and HTML')}
96
- <p>Pulse does not auto-escape raw response bodies. When returning XML or HTML, escaping all user-supplied and dynamic content is required — unescaped output is an injection vulnerability:</p>
97
- ${codeBlock(highlight(`// Simple escape helper for XML/HTML contexts
98
- function esc(str) {
99
- return String(str)
100
- .replace(/&/g, '&amp;')
101
- .replace(/</g, '&lt;')
102
- .replace(/>/g, '&gt;')
103
- .replace(/"/g, '&quot;')
104
- .replace(/'/g, '&#39;')
105
- }`, 'js'))}
106
- ${callout('warning', 'Never interpolate raw user data or database content into XML/HTML without escaping. This applies to raw response specs just as much as to HTML view functions.')}
107
-
108
- ${section('caching-raw', 'Caching raw responses')}
109
- <p>Raw response specs support the same <code>serverTtl</code> and <code>cache</code> options as HTML specs. For feeds and sitemaps that update infrequently, a generous TTL dramatically reduces server load:</p>
110
- ${codeBlock(highlight(`export default {
111
- route: '/feed.xml',
112
- contentType: 'application/rss+xml',
113
- serverTtl: 300, // cache data for 5 minutes
114
- cache: {
115
- public: true,
116
- maxAge: 300,
117
- },
118
- // ...
119
- }`, 'js'))}
120
- `,
121
- }),
122
- }
@@ -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('/routing')
6
-
7
- export default {
8
- route: '/routing',
9
- meta: {
10
- theme: 'light',
11
- title: 'Routing — Pulse Docs',
12
- description: 'How Pulse routes requests to page specs — static and dynamic routes, params, and conventions.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- view: () => renderLayout({
16
- currentHref: '/routing',
17
- prev,
18
- next,
19
- content: `
20
- ${h1('Routing')}
21
- ${lead('Every route in Pulse is explicitly declared in the spec. There is no file-based routing, no magic directory conventions, and no implicit mapping. Every page\'s URL is visible in its spec file and nowhere else.')}
22
-
23
- ${section('route-field', 'The route field')}
24
- <p>Every spec has a <code>route</code> field. This is the URL pattern the spec handles:</p>
25
- ${codeBlock(highlight(`export default {
26
- route: '/about',
27
- view: () => \`<h1>About</h1>\`,
28
- }`, 'js'))}
29
- <p>Pulse matches the exact path. By default, trailing slashes are removed — <code>/about/</code> redirects to <code>/about</code> with a 301. This is controlled by the <code>trailingSlash</code> option in <code>createServer</code>:</p>
30
- ${table(
31
- ['Value', 'Behaviour'],
32
- [
33
- ['<code>"remove"</code> (default)', 'Redirects <code>/about/</code> → <code>/about</code> (301)'],
34
- ['<code>"add"</code>', 'Redirects <code>/about</code> → <code>/about/</code> (301)'],
35
- ['<code>"allow"</code>', 'Serves both — no redirect'],
36
- ]
37
- )}
38
- ${codeBlock(highlight(`createServer(specs, {
39
- trailingSlash: 'add', // enforce trailing slashes
40
- })`, 'js'))}
41
-
42
- ${section('dynamic', 'Dynamic segments')}
43
- <p>Use a colon prefix for dynamic path segments. Named segments are captured and available in <code>ctx.params</code> in <a href="/server-data">server data</a>:</p>
44
- ${codeBlock(highlight(`export default {
45
- route: '/products/:id',
46
- state: { quantity: 1 },
47
- server: {
48
- data: async (ctx) => {
49
- // ctx.params.id is the captured segment
50
- const product = await db.products.find(ctx.params.id)
51
- return { product }
52
- },
53
- },
54
- view: (state, server) => \`<h1>\${server.product.name}</h1>\`,
55
- }`, 'js'))}
56
-
57
- ${section('multi-segment', 'Multiple dynamic segments')}
58
- <p>Any number of dynamic segments can appear in a route:</p>
59
- ${codeBlock(highlight(`route: '/blog/:year/:month/:slug'
60
- // Matches: /blog/2025/03/my-first-post
61
- // ctx.params = { year: '2025', month: '03', slug: 'my-first-post' }`, 'js'))}
62
-
63
- ${section('registering', 'Registering routes')}
64
- <p>Specs are registered explicitly by passing them to <code>createServer</code> as an array. Routes are matched in order — more specific routes must come before more general ones:</p>
65
- ${codeBlock(highlight(`import { createServer } from '@invisibleloop/pulse'
66
- import home from './src/pages/home.js'
67
- import products from './src/pages/products.js'
68
- import product from './src/pages/product.js' // more specific — comes first
69
- import blog from './src/pages/blog.js'
70
-
71
- createServer([home, product, products, blog], { port: 3000 })`, 'js'))}
72
-
73
- ${section('query', 'Query strings')}
74
- <p>Query string parameters are not part of the route pattern but are accessible via <code>ctx.query</code> in server data:</p>
75
- ${codeBlock(highlight(`// URL: /products?category=shoes&sort=price
76
- server: {
77
- data: async (ctx) => {
78
- const { category, sort } = ctx.query
79
- return { products: await db.products.list({ category, sort }) }
80
- },
81
- }`, 'js'))}
82
-
83
- ${section('redirects', 'Redirects')}
84
- <p>Migrating an existing site? Preserve the old URLs with the <code>redirects</code> map — legacy links and search rankings survive the move:</p>
85
- ${codeBlock(highlight(`await createServer(pages, {
86
- redirects: {
87
- '/old-blog/:slug': '/blog/:slug', // 301 — :params carry over
88
- '/pricing-2024': '/pricing', // 301 (default)
89
- '/promo': { to: '/sale', status: 302 }, // temporary redirect
90
- '/moved/:slug': 'https://other.com/:slug', // absolute targets for domain moves
91
- },
92
- })`, 'js'))}
93
- <p>Redirects respond <strong>301</strong> by default (permanent — search engines transfer ranking to the target). Use <code>{ to, status }</code> for <code>302</code>, <code>307</code>, or <code>308</code>. The query string is preserved, redirects apply to GET/HEAD only, and the map is validated at startup — a bad entry fails the boot rather than silently misrouting traffic.</p>
94
- ${callout('note', 'Redirects are checked before route matching, so a redirect source that equals a registered route shadows the page — the server logs a startup warning when that happens. Occasionally intentional (retiring a page), usually a mistake.')}
95
-
96
- ${section('not-found', '404 handling')}
97
- <p>If no spec matches the incoming request path, Pulse returns a 404. To customise it, create a spec with <code>route: '*'</code> — it renders through the normal pipeline (layout, styles, components) with status 404:</p>
98
- ${codeBlock(highlight(`export default {
99
- route: '*',
100
- meta: { title: 'Page not found', styles: ['/pulse-ui.css', '/theme.css', '/app.css'] },
101
- view: () => \`<main id="main-content"><h1>Page not found</h1><p><a href="/">Back home</a></p></main>\`,
102
- }`, 'js'), 'src/pages/not-found.js')}
103
- <p>See <a href="/error-pages">Error Pages</a> for the full behaviour, including 500 handling.</p>
104
-
105
- ${section('conventions', 'File naming conventions')}
106
- <p>While Pulse does not auto-discover files, the recommended convention maps file names to routes:</p>
107
- ${table(
108
- ['File', 'Route'],
109
- [
110
- ['<code>src/pages/home.js</code>', '<code>/</code>'],
111
- ['<code>src/pages/about.js</code>', '<code>/about</code>'],
112
- ['<code>src/pages/products.js</code>', '<code>/products</code>'],
113
- ['<code>src/pages/product.js</code>', '<code>/products/:id</code>'],
114
- ['<code>src/pages/blog-post.js</code>', '<code>/blog/:slug</code>'],
115
- ]
116
- )}
117
- ${callout('tip', 'The filename does not need to match the route exactly — it is just a helpful convention. A file named <code>product.js</code> can handle <code>/products/:id</code> without any issue.')}
118
- `,
119
- }),
120
- }