@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.
- package/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- package/types/ui.d.ts +704 -0
package/src/cli/index.js
ADDED
|
@@ -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))
|