@invisibleloop/pulse 0.1.21

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 (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pulse CLI
4
+ *
5
+ * Usage:
6
+ * pulse detect project or scaffold, then start AI session + dev server
7
+ * pulse dev dev server only (no AI)
8
+ * pulse build production build → public/dist/
9
+ * pulse start production server (requires prior build)
10
+ * pulse update re-copy pulse-ui.css/js from installed package → public/
11
+ */
12
+
13
+ import path from 'path'
14
+ import fs from 'fs'
15
+ import { scaffold } from './scaffold.js'
16
+
17
+ const args = process.argv.slice(2)
18
+ const command = args[0]
19
+ const CWD = process.cwd()
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function isPulseProject(dir) {
26
+ return (
27
+ fs.existsSync(path.join(dir, 'pulse.config.js')) ||
28
+ fs.existsSync(path.join(dir, 'src', 'pages'))
29
+ )
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // pulse dev
34
+ // ---------------------------------------------------------------------------
35
+
36
+ async function runDev(root) {
37
+ const devScript = new URL('./dev.js', import.meta.url).pathname
38
+ const { spawn } = await import('child_process')
39
+ const proc = spawn(
40
+ process.execPath,
41
+ [devScript, '--root', root],
42
+ { stdio: 'inherit' }
43
+ )
44
+ proc.on('exit', code => process.exit(code ?? 0))
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // pulse build
49
+ // ---------------------------------------------------------------------------
50
+
51
+ async function runBuild(root) {
52
+ const buildScript = new URL('../../scripts/build.js', import.meta.url).pathname
53
+ const { spawn } = await import('child_process')
54
+ const proc = spawn(
55
+ process.execPath,
56
+ [buildScript, '--root', root],
57
+ { stdio: 'inherit' }
58
+ )
59
+ proc.on('exit', code => process.exit(code ?? 0))
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // pulse (no subcommand) — scaffold or start AI session
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function prompt(question) {
67
+ process.stdout.write(question)
68
+ return new Promise(resolve => {
69
+ process.stdin.setEncoding('utf8')
70
+ process.stdin.once('data', d => resolve(d.trim()))
71
+ })
72
+ }
73
+
74
+ function isDirEmpty(dir) {
75
+ if (!fs.existsSync(dir)) return true
76
+ return fs.readdirSync(dir).length === 0
77
+ }
78
+
79
+ async function runDefault(root) {
80
+ if (!isPulseProject(root)) {
81
+ console.log(`\n⚡ No Pulse project found here.\n`)
82
+
83
+ let targetDir = root
84
+ let name = path.basename(root)
85
+
86
+ if (!isDirEmpty(root)) {
87
+ // Non-empty directory — ask for a project name and create a subdirectory
88
+ const raw = await prompt(` Project name: `)
89
+ if (!raw) {
90
+ console.log('\n Aborted.\n')
91
+ process.exit(0)
92
+ }
93
+ // Sanitise: lowercase, hyphens, no leading/trailing punctuation
94
+ name = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
95
+ targetDir = path.join(root, name)
96
+
97
+ if (fs.existsSync(targetDir)) {
98
+ console.error(`\n Directory already exists: ${targetDir}\n`)
99
+ process.exit(1)
100
+ }
101
+ } else {
102
+ // Empty directory — confirm scaffold here
103
+ const answer = await prompt(` Scaffold a new Pulse app here? (${name}) [Y/n] `)
104
+ if (answer.toLowerCase() === 'n') {
105
+ console.log('\n Aborted.\n')
106
+ process.exit(0)
107
+ }
108
+ }
109
+
110
+ console.log()
111
+ await scaffold(targetDir, { name })
112
+
113
+ if (targetDir !== root) {
114
+ console.log(`\n✓ Project created at ./${name}/\n`)
115
+ console.log(` Next steps:\n`)
116
+ console.log(` cd ${name}`)
117
+ console.log(` pulse\n`)
118
+ } else {
119
+ console.log('\n✓ Project ready. Run `pulse` again to start your AI session.\n')
120
+ }
121
+ process.exit(0)
122
+ }
123
+
124
+ // Start dev server + AI session
125
+ await launchSession(root)
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Launch AI session (Claude by default)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ async function launchSession(root) {
133
+ const { spawn } = await import('child_process')
134
+ const os = await import('os')
135
+
136
+ // Load project config for agent preference
137
+ const configPath = path.join(root, 'pulse.config.js')
138
+ let agent = 'claude'
139
+ if (fs.existsSync(configPath)) {
140
+ try {
141
+ const mod = await import(configPath)
142
+ agent = mod.default?.agent || 'claude'
143
+ } catch { /* use default */ }
144
+ }
145
+
146
+ // Write MCP config so the agent has access to Pulse tools
147
+ const mcpServerPath = new URL('../mcp/server.js', import.meta.url).pathname
148
+ const mcpConfig = {
149
+ mcpServers: {
150
+ pulse: {
151
+ command: process.execPath,
152
+ args: [mcpServerPath, '--root', root],
153
+ }
154
+ }
155
+ }
156
+ const mcpConfigPath = path.join(os.default.tmpdir(), `pulse-mcp-${Date.now()}.json`)
157
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2))
158
+
159
+ console.log(`\n⚡ Pulse project: ${root}`)
160
+ console.log(` Use /pulse-dev to start the dev server, /pulse-stop to stop it, /pulse-build to build, /pulse-start to run production.`)
161
+ console.log(` Tell me what you'd like to build — a new page, a component, a form, or anything else.\n`)
162
+
163
+ // Launch the agent with MCP config — don't spawn a dev server here,
164
+ // Claude Code cannot be launched as a child process from within a Claude session.
165
+ // The dev server is started via the /dev slash command instead.
166
+ const agentCmd = agentCommand(agent, mcpConfigPath)
167
+ const agentProc = spawn(agentCmd.cmd, agentCmd.args, {
168
+ stdio: 'inherit',
169
+ cwd: root,
170
+ })
171
+
172
+ agentProc.on('exit', () => {
173
+ try { fs.unlinkSync(mcpConfigPath) } catch { /* ignore */ }
174
+ process.exit(0)
175
+ })
176
+ }
177
+
178
+ function agentCommand(agent, mcpConfigPath) {
179
+ if (agent === 'claude') {
180
+ return { cmd: 'claude', args: ['--mcp-config', mcpConfigPath] }
181
+ }
182
+ // Future: copilot, etc.
183
+ console.warn(`Unknown agent "${agent}", falling back to claude`)
184
+ return { cmd: 'claude', args: ['--mcp-config', mcpConfigPath] }
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // pulse stop
189
+ // ---------------------------------------------------------------------------
190
+
191
+ async function runStop(root) {
192
+ const { execSync } = await import('child_process')
193
+ let port = 3000
194
+ const configPath = path.join(root, 'pulse.config.js')
195
+ if (fs.existsSync(configPath)) {
196
+ try {
197
+ const mod = await import(configPath)
198
+ if (mod.default?.port) port = mod.default.port
199
+ } catch { /* use default */ }
200
+ }
201
+ try {
202
+ execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null; true`, { stdio: 'inherit' })
203
+ console.log(`\n⚡ Dev server on port ${port} stopped.\n`)
204
+ } catch { /* nothing was running */ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // pulse report-server
209
+ // ---------------------------------------------------------------------------
210
+
211
+ async function runReportServer(root) {
212
+ const configPath = path.join(root, 'pulse.config.js')
213
+ let devPort = 3000
214
+ let reportPort = null
215
+ if (fs.existsSync(configPath)) {
216
+ try {
217
+ const mod = await import(configPath)
218
+ if (mod.default?.port) devPort = mod.default.port
219
+ if (mod.default?.reportPort) reportPort = mod.default.reportPort
220
+ } catch { /* use defaults */ }
221
+ }
222
+ if (!reportPort) reportPort = devPort + 1
223
+
224
+ const script = new URL('./report-server.js', import.meta.url).pathname
225
+ const { spawn } = await import('child_process')
226
+ const proc = spawn(
227
+ process.execPath,
228
+ [script, '--root', root, '--port', String(reportPort)],
229
+ { stdio: 'inherit' }
230
+ )
231
+ proc.on('exit', code => process.exit(code ?? 0))
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // pulse save-report
236
+ // ---------------------------------------------------------------------------
237
+
238
+ async function runSaveReport(root) {
239
+ const script = new URL('./report.js', import.meta.url).pathname
240
+ const { spawn } = await import('child_process')
241
+ const proc = spawn(process.execPath, [script, '--root', root, ...process.argv.slice(3)], { stdio: 'inherit' })
242
+ proc.on('exit', code => process.exit(code ?? 0))
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // pulse load-test
247
+ // ---------------------------------------------------------------------------
248
+
249
+ async function runLoadTest(_root) {
250
+ const script = new URL('./load-runner.js', import.meta.url).pathname
251
+ const { spawn } = await import('child_process')
252
+ const proc = spawn(process.execPath, [script, ...process.argv.slice(3)], { stdio: 'inherit' })
253
+ proc.on('exit', code => process.exit(code ?? 0))
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // pulse save-load-report
258
+ // ---------------------------------------------------------------------------
259
+
260
+ async function runSaveLoadReport(root) {
261
+ const script = new URL('./load-report.js', import.meta.url).pathname
262
+ const { spawn } = await import('child_process')
263
+ const proc = spawn(process.execPath, [script, '--root', root, ...process.argv.slice(3)], { stdio: 'inherit' })
264
+ proc.on('exit', code => process.exit(code ?? 0))
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // pulse update
269
+ // ---------------------------------------------------------------------------
270
+
271
+ async function runUpdate(root) {
272
+ if (!isPulseProject(root)) {
273
+ console.error('\n Not a Pulse project. Run from your project root.\n')
274
+ process.exit(1)
275
+ }
276
+
277
+ const pkgPublic = new URL('../../public', import.meta.url).pathname
278
+ const assets = ['pulse-ui.css', 'pulse-ui.js', '.pulse-ui-version']
279
+ const publicDir = path.join(root, 'public')
280
+ const updated = []
281
+ const missing = []
282
+
283
+ for (const asset of assets) {
284
+ const src = path.join(pkgPublic, asset)
285
+ const dst = path.join(publicDir, asset)
286
+ if (!fs.existsSync(src)) { missing.push(asset); continue }
287
+ fs.copyFileSync(src, dst)
288
+ updated.push(asset)
289
+ }
290
+
291
+ // Sync agent checklist into .claude/
292
+ const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
293
+ const checklistDst = path.join(root, '.claude', 'pulse-checklist.md')
294
+ if (fs.existsSync(checklistSrc)) {
295
+ fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
296
+ fs.copyFileSync(checklistSrc, checklistDst)
297
+ updated.push('.claude/pulse-checklist.md')
298
+ }
299
+
300
+ // Read the new version for the success message
301
+ const versionFile = path.join(publicDir, '.pulse-ui-version')
302
+ const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : '?'
303
+
304
+ console.log(`\n⚡ Pulse UI updated to ${version}\n`)
305
+ for (const f of updated) console.log(` ✓ public/${f}`)
306
+ for (const f of missing) console.log(` ✗ ${f} not found in package`)
307
+ console.log()
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // pulse start
312
+ // ---------------------------------------------------------------------------
313
+
314
+ async function runStart(root) {
315
+ const startScript = new URL('./start.js', import.meta.url).pathname
316
+ const { spawn } = await import('child_process')
317
+ // Forward any extra flags (e.g. --port 3002) to the start script
318
+ const extraArgs = args.slice(1)
319
+ const proc = spawn(
320
+ process.execPath,
321
+ [startScript, '--root', root, ...extraArgs],
322
+ { stdio: 'inherit' }
323
+ )
324
+ proc.on('exit', code => process.exit(code ?? 0))
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Route command
329
+ // ---------------------------------------------------------------------------
330
+
331
+ switch (command) {
332
+ case 'dev':
333
+ await runDev(CWD)
334
+ break
335
+ case 'stop':
336
+ await runStop(CWD)
337
+ break
338
+ case 'build':
339
+ await runBuild(CWD)
340
+ break
341
+ case 'start':
342
+ await runStart(CWD)
343
+ break
344
+ case 'report-server':
345
+ await runReportServer(CWD)
346
+ break
347
+ case 'save-report':
348
+ await runSaveReport(CWD)
349
+ break
350
+ case 'load-test':
351
+ await runLoadTest(CWD)
352
+ break
353
+ case 'save-load-report':
354
+ await runSaveLoadReport(CWD)
355
+ break
356
+ case 'update':
357
+ await runUpdate(CWD)
358
+ break
359
+ default:
360
+ await runDefault(CWD)
361
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Pulse — Load test report persistence
3
+ *
4
+ * Saves load test results to .pulse/load-reports/[slug]/[timestamp].json
5
+ * Prunes entries older than 30 days on each write.
6
+ *
7
+ * Usage (CLI):
8
+ * node src/cli/load-report.js --root /path/to/project --url http://localhost:3000/about --data '{...}'
9
+ *
10
+ * Usage (import):
11
+ * import { saveLoadReport } from './load-report.js'
12
+ */
13
+
14
+ import fs from 'fs'
15
+ import path from 'path'
16
+ import { urlToSlug } from './report.js'
17
+
18
+ const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public API
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Save a load test result to disk.
26
+ *
27
+ * @param {string} root - Project root directory
28
+ * @param {string} url - URL that was tested
29
+ * @param {Object} data - { config, rps, latency, requests, duration } from load-runner
30
+ */
31
+ export function saveLoadReport(root, url, data) {
32
+ const slug = urlToSlug(url)
33
+ const dir = path.join(root, '.pulse', 'load-reports', slug)
34
+ fs.mkdirSync(dir, { recursive: true })
35
+
36
+ const record = {
37
+ timestamp: new Date().toISOString(),
38
+ url,
39
+ ...data,
40
+ }
41
+
42
+ const filename = path.join(dir, `${Date.now()}.json`)
43
+ fs.writeFileSync(filename, JSON.stringify(record, null, 2))
44
+ pruneOld(dir)
45
+ return { slug, file: filename }
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Internal
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function pruneOld(dir) {
53
+ const cutoff = Date.now() - THIRTY_DAYS
54
+ try {
55
+ fs.readdirSync(dir)
56
+ .filter(f => f.endsWith('.json'))
57
+ .forEach(f => {
58
+ const ts = parseInt(path.basename(f, '.json'), 10)
59
+ if (!isNaN(ts) && ts < cutoff) fs.unlinkSync(path.join(dir, f))
60
+ })
61
+ } catch { /* non-fatal */ }
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // CLI entry point
66
+ // ---------------------------------------------------------------------------
67
+
68
+ if (process.argv[1] === new URL(import.meta.url).pathname) {
69
+ const args = process.argv.slice(2)
70
+ const rootArg = args.indexOf('--root')
71
+ const urlArg = args.indexOf('--url')
72
+ const dataArg = args.indexOf('--data')
73
+
74
+ const root = rootArg !== -1 ? path.resolve(args[rootArg + 1]) : process.cwd()
75
+ const url = urlArg !== -1 ? args[urlArg + 1] : null
76
+ const data = dataArg !== -1 ? args[dataArg + 1] : null
77
+
78
+ if (!url || !data) {
79
+ console.error('Usage: pulse save-load-report --url <url> --data \'{"rps":...,"latency":{...},...}\'')
80
+ process.exit(1)
81
+ }
82
+
83
+ try {
84
+ const parsed = JSON.parse(data)
85
+ const { slug } = saveLoadReport(root, url, parsed)
86
+ console.log(`✓ Load report saved for ${url} (slug: ${slug})`)
87
+ } catch (e) {
88
+ console.error('Failed to save load report:', e.message)
89
+ process.exit(1)
90
+ }
91
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Pulse — Built-in load test runner
3
+ *
4
+ * Sends N concurrent HTTP request chains for D seconds, collects latencies,
5
+ * calculates percentiles, and prints a JSON result.
6
+ *
7
+ * Usage:
8
+ * node src/cli/load-runner.js --url http://localhost:3000/about [--duration 10] [--connections 10]
9
+ */
10
+
11
+ import http from 'http'
12
+ import https from 'https'
13
+
14
+ const args = process.argv.slice(2)
15
+ const urlArg = args.indexOf('--url')
16
+ const durArg = args.indexOf('--duration')
17
+ const connArg = args.indexOf('--connections')
18
+
19
+ const url = urlArg !== -1 ? args[urlArg + 1] : null
20
+ const duration = durArg !== -1 ? parseInt(args[durArg + 1], 10) : 10
21
+ const connections = connArg !== -1 ? parseInt(args[connArg + 1], 10) : 10
22
+
23
+ // --header "Key: Value" (repeatable)
24
+ const headers = {}
25
+ for (let i = 0; i < args.length; i++) {
26
+ if (args[i] === '--header' && args[i + 1]) {
27
+ const colon = args[i + 1].indexOf(':')
28
+ if (colon > 0) {
29
+ headers[args[i + 1].slice(0, colon).trim()] = args[i + 1].slice(colon + 1).trim()
30
+ }
31
+ i++
32
+ }
33
+ }
34
+
35
+ if (!url) {
36
+ console.error('Usage: node load-runner.js --url <url> [--duration 10] [--connections 10] [--header "Key: Value"]')
37
+ process.exit(1)
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Runner
42
+ // ---------------------------------------------------------------------------
43
+
44
+ async function run(url, { duration, connections, headers = {} }) {
45
+ const parsed = new URL(url)
46
+ const client = parsed.protocol === 'https:' ? https : http
47
+ const opts = { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname + parsed.search, headers }
48
+ const latencies = []
49
+ let errors = 0
50
+ let inflight = 0
51
+ const deadline = Date.now() + duration * 1000
52
+ const start = Date.now()
53
+
54
+ function request() {
55
+ if (Date.now() >= deadline) return
56
+ inflight++
57
+ const t0 = Date.now()
58
+ const req = client.get(opts, res => {
59
+ res.resume()
60
+ res.on('end', () => {
61
+ if (res.statusCode < 500) {
62
+ latencies.push(Date.now() - t0)
63
+ } else {
64
+ errors++
65
+ }
66
+ inflight--
67
+ request()
68
+ })
69
+ })
70
+ req.on('error', () => {
71
+ errors++
72
+ inflight--
73
+ request()
74
+ })
75
+ req.setTimeout(10000, () => {
76
+ req.destroy()
77
+ errors++
78
+ inflight--
79
+ request()
80
+ })
81
+ }
82
+
83
+ // Start N concurrent request chains
84
+ for (let i = 0; i < connections; i++) request()
85
+
86
+ // Wait for deadline then drain in-flight requests
87
+ await new Promise(resolve => setTimeout(resolve, duration * 1000 + 500))
88
+ await new Promise(resolve => {
89
+ const drain = () => inflight === 0 ? resolve() : setTimeout(drain, 20)
90
+ drain()
91
+ })
92
+
93
+ const elapsed = (Date.now() - start) / 1000
94
+ latencies.sort((a, b) => a - b)
95
+ const n = latencies.length
96
+
97
+ function pct(p) {
98
+ if (!n) return 0
99
+ return latencies[Math.min(n - 1, Math.ceil((p / 100) * n) - 1)]
100
+ }
101
+
102
+ const mean = n ? +(latencies.reduce((s, v) => s + v, 0) / n).toFixed(1) : 0
103
+
104
+ return {
105
+ url,
106
+ config: { duration, connections },
107
+ rps: +(n / elapsed).toFixed(1),
108
+ latency: { mean, p50: pct(50), p95: pct(95), p99: pct(99), max: latencies[n - 1] ?? 0 },
109
+ requests: { total: n + errors, success: n, errors },
110
+ duration: +elapsed.toFixed(1),
111
+ }
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Entry point
116
+ // ---------------------------------------------------------------------------
117
+
118
+ process.stderr.write(`⚡ Load testing ${url} — ${connections} connections × ${duration}s\n`)
119
+
120
+ const result = await run(url, { duration, connections, headers })
121
+ console.log(JSON.stringify(result, null, 2))