@stacksjs/defaults 0.70.206 → 0.70.208

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 (408) hide show
  1. package/TODO.md +21 -0
  2. package/ai/AGENTS.md +79 -0
  3. package/ai/README.md +45 -0
  4. package/ai/claude/launch.json +12 -0
  5. package/ai/skills/stacks-actions/SKILL.md +128 -0
  6. package/ai/skills/stacks-ai/SKILL.md +276 -0
  7. package/ai/skills/stacks-alias/SKILL.md +94 -0
  8. package/ai/skills/stacks-analytics/SKILL.md +91 -0
  9. package/ai/skills/stacks-api/SKILL.md +738 -0
  10. package/ai/skills/stacks-arrays/SKILL.md +277 -0
  11. package/ai/skills/stacks-auth/SKILL.md +422 -0
  12. package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
  13. package/ai/skills/stacks-brandkit/SKILL.md +811 -0
  14. package/ai/skills/stacks-browse/SKILL.md +127 -0
  15. package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
  16. package/ai/skills/stacks-browser/SKILL.md +145 -0
  17. package/ai/skills/stacks-buddy/SKILL.md +676 -0
  18. package/ai/skills/stacks-build/SKILL.md +163 -0
  19. package/ai/skills/stacks-cache/SKILL.md +125 -0
  20. package/ai/skills/stacks-calendar/SKILL.md +113 -0
  21. package/ai/skills/stacks-chat/SKILL.md +371 -0
  22. package/ai/skills/stacks-cli/SKILL.md +112 -0
  23. package/ai/skills/stacks-cloud/SKILL.md +192 -0
  24. package/ai/skills/stacks-cms/SKILL.md +217 -0
  25. package/ai/skills/stacks-collections/SKILL.md +82 -0
  26. package/ai/skills/stacks-commerce/SKILL.md +114 -0
  27. package/ai/skills/stacks-composables/SKILL.md +161 -0
  28. package/ai/skills/stacks-config/SKILL.md +144 -0
  29. package/ai/skills/stacks-configuration/SKILL.md +154 -0
  30. package/ai/skills/stacks-cron/SKILL.md +123 -0
  31. package/ai/skills/stacks-crosswind/SKILL.md +190 -0
  32. package/ai/skills/stacks-dashboard/SKILL.md +173 -0
  33. package/ai/skills/stacks-database/SKILL.md +286 -0
  34. package/ai/skills/stacks-datetime/SKILL.md +269 -0
  35. package/ai/skills/stacks-dependencies/SKILL.md +111 -0
  36. package/ai/skills/stacks-deploy/SKILL.md +102 -0
  37. package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
  38. package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
  39. package/ai/skills/stacks-design-output/SKILL.md +55 -0
  40. package/ai/skills/stacks-design-soft/SKILL.md +156 -0
  41. package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
  42. package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
  43. package/ai/skills/stacks-desktop/SKILL.md +78 -0
  44. package/ai/skills/stacks-development/SKILL.md +298 -0
  45. package/ai/skills/stacks-dns/SKILL.md +149 -0
  46. package/ai/skills/stacks-docs/SKILL.md +109 -0
  47. package/ai/skills/stacks-email/SKILL.md +236 -0
  48. package/ai/skills/stacks-enums/SKILL.md +100 -0
  49. package/ai/skills/stacks-env/SKILL.md +181 -0
  50. package/ai/skills/stacks-error-handling/SKILL.md +137 -0
  51. package/ai/skills/stacks-events/SKILL.md +228 -0
  52. package/ai/skills/stacks-faker/SKILL.md +158 -0
  53. package/ai/skills/stacks-git/SKILL.md +158 -0
  54. package/ai/skills/stacks-guard/SKILL.md +98 -0
  55. package/ai/skills/stacks-health/SKILL.md +75 -0
  56. package/ai/skills/stacks-http/SKILL.md +316 -0
  57. package/ai/skills/stacks-i18n/SKILL.md +198 -0
  58. package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
  59. package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
  60. package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
  61. package/ai/skills/stacks-investigate/SKILL.md +119 -0
  62. package/ai/skills/stacks-jobs/SKILL.md +136 -0
  63. package/ai/skills/stacks-lint/SKILL.md +184 -0
  64. package/ai/skills/stacks-listeners/SKILL.md +99 -0
  65. package/ai/skills/stacks-logging/SKILL.md +78 -0
  66. package/ai/skills/stacks-mail/SKILL.md +120 -0
  67. package/ai/skills/stacks-middleware/SKILL.md +249 -0
  68. package/ai/skills/stacks-migrations/SKILL.md +105 -0
  69. package/ai/skills/stacks-models/SKILL.md +267 -0
  70. package/ai/skills/stacks-new-feature/SKILL.md +206 -0
  71. package/ai/skills/stacks-notifications/SKILL.md +210 -0
  72. package/ai/skills/stacks-objects/SKILL.md +205 -0
  73. package/ai/skills/stacks-office-hours/SKILL.md +134 -0
  74. package/ai/skills/stacks-orm/SKILL.md +342 -0
  75. package/ai/skills/stacks-path/SKILL.md +118 -0
  76. package/ai/skills/stacks-payments/SKILL.md +373 -0
  77. package/ai/skills/stacks-plan-review/SKILL.md +152 -0
  78. package/ai/skills/stacks-plugins/SKILL.md +149 -0
  79. package/ai/skills/stacks-push/SKILL.md +376 -0
  80. package/ai/skills/stacks-query-builder/SKILL.md +128 -0
  81. package/ai/skills/stacks-queue/SKILL.md +559 -0
  82. package/ai/skills/stacks-realtime/SKILL.md +442 -0
  83. package/ai/skills/stacks-redesign/SKILL.md +182 -0
  84. package/ai/skills/stacks-registry/SKILL.md +58 -0
  85. package/ai/skills/stacks-repl/SKILL.md +80 -0
  86. package/ai/skills/stacks-retro/SKILL.md +122 -0
  87. package/ai/skills/stacks-review/SKILL.md +135 -0
  88. package/ai/skills/stacks-router/SKILL.md +231 -0
  89. package/ai/skills/stacks-routes/SKILL.md +105 -0
  90. package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
  91. package/ai/skills/stacks-scheduler/SKILL.md +363 -0
  92. package/ai/skills/stacks-search-engine/SKILL.md +112 -0
  93. package/ai/skills/stacks-security/SKILL.md +129 -0
  94. package/ai/skills/stacks-security-audit/SKILL.md +128 -0
  95. package/ai/skills/stacks-server/SKILL.md +458 -0
  96. package/ai/skills/stacks-shell/SKILL.md +62 -0
  97. package/ai/skills/stacks-slug/SKILL.md +87 -0
  98. package/ai/skills/stacks-sms/SKILL.md +385 -0
  99. package/ai/skills/stacks-socials/SKILL.md +303 -0
  100. package/ai/skills/stacks-storage/SKILL.md +576 -0
  101. package/ai/skills/stacks-strings/SKILL.md +416 -0
  102. package/ai/skills/stacks-stx/SKILL.md +176 -0
  103. package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
  104. package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
  105. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
  106. package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
  107. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
  108. package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
  109. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
  110. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
  111. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
  112. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
  113. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
  114. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
  115. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
  116. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
  117. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
  118. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
  119. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
  120. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
  121. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
  122. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
  123. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
  124. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
  125. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
  126. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
  127. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
  128. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
  129. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
  130. package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
  131. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
  132. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
  133. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
  134. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
  135. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
  136. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
  137. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
  138. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
  139. package/ai/skills/stacks-testing/SKILL.md +129 -0
  140. package/ai/skills/stacks-tunnel/SKILL.md +125 -0
  141. package/ai/skills/stacks-types/SKILL.md +178 -0
  142. package/ai/skills/stacks-ui/SKILL.md +246 -0
  143. package/ai/skills/stacks-utils/SKILL.md +444 -0
  144. package/ai/skills/stacks-validation/SKILL.md +371 -0
  145. package/ai/skills/stacks-whois/SKILL.md +315 -0
  146. package/assets/world-map-data.ts +121 -0
  147. package/bootstrap.ts +114 -0
  148. package/docs/api-examples.md +46 -0
  149. package/docs/index.md +24 -0
  150. package/docs/markdown-examples.md +87 -0
  151. package/functions/analytics.ts +97 -0
  152. package/functions/api.ts +143 -0
  153. package/functions/auth.ts +208 -0
  154. package/functions/billing/payments.ts +149 -0
  155. package/functions/blog.ts +120 -0
  156. package/functions/cms/categorizables.ts +107 -0
  157. package/functions/cms/pages.ts +108 -0
  158. package/functions/cms/posts.ts +100 -0
  159. package/functions/cms/taggables.ts +102 -0
  160. package/functions/commerce/coupons.ts +128 -0
  161. package/functions/commerce/customers.ts +128 -0
  162. package/functions/commerce/gift-cards.ts +119 -0
  163. package/functions/commerce/orders.ts +112 -0
  164. package/functions/commerce/payments.ts +119 -0
  165. package/functions/commerce/products/categories.ts +123 -0
  166. package/functions/commerce/products/manufacturers.ts +123 -0
  167. package/functions/commerce/products/products.ts +123 -0
  168. package/functions/commerce/products/reviews.ts +123 -0
  169. package/functions/commerce/products/units.ts +123 -0
  170. package/functions/commerce/products/variants.ts +123 -0
  171. package/functions/commerce/shippings/delivery-routes.ts +123 -0
  172. package/functions/commerce/shippings/digital-deliveries.ts +119 -0
  173. package/functions/commerce/shippings/drivers.ts +123 -0
  174. package/functions/commerce/shippings/license-keys.ts +123 -0
  175. package/functions/commerce/shippings/shipping-methods.ts +123 -0
  176. package/functions/commerce/shippings/shipping-rates.ts +119 -0
  177. package/functions/commerce/shippings/shipping-zones.ts +119 -0
  178. package/functions/commerce/taxes.ts +119 -0
  179. package/functions/commerce/waitlist/products.ts +123 -0
  180. package/functions/commerce/waitlist/restaurants.ts +123 -0
  181. package/functions/craft.ts +107 -0
  182. package/functions/dashboard.ts +153 -0
  183. package/functions/inbox.ts +95 -0
  184. package/functions/jobs.ts +86 -0
  185. package/functions/monitoring/errors.ts +295 -0
  186. package/functions/queue.ts +111 -0
  187. package/functions/realtime/websockets.ts +96 -0
  188. package/functions/realtime.ts +52 -0
  189. package/functions/releases.ts +49 -0
  190. package/functions/search.ts +126 -0
  191. package/functions/toasts.ts +95 -0
  192. package/functions/uploads.ts +216 -0
  193. package/ide/.gitignore +4 -0
  194. package/ide/cursor/rules/auto-imports.mdc +574 -0
  195. package/ide/cursor/rules/code-style.mdc +12 -0
  196. package/ide/cursor/rules/documentation.mdc +9 -0
  197. package/ide/cursor/rules/error-handling-validation.mdc +11 -0
  198. package/ide/cursor/rules/key-conventions.mdc +11 -0
  199. package/ide/cursor/rules/project-structure.mdc +169 -0
  200. package/ide/cursor/rules/readme.mdc +318 -0
  201. package/ide/cursor/rules/syntax-formatting.mdc +9 -0
  202. package/ide/cursor/rules/testing.mdc +10 -0
  203. package/ide/cursor/rules/typescript.mdc +9 -0
  204. package/ide/cursor/rules/ui-styling.mdc +0 -0
  205. package/ide/dictionary.txt +397 -0
  206. package/ide/jetbrains/.fleet/settings.json +4 -0
  207. package/ide/jetbrains/.idea/.gitignore +5 -0
  208. package/ide/jetbrains/.idea/.name +1 -0
  209. package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
  210. package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
  211. package/ide/jetbrains/.idea/modules.xml +8 -0
  212. package/ide/jetbrains/.idea/stacks.iml +22 -0
  213. package/ide/jetbrains/.idea/vcs.xml +6 -0
  214. package/ide/jetbrains/.idea/webResources.xml +14 -0
  215. package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
  216. package/ide/jetbrains/installed.txt +4 -0
  217. package/ide/jetbrains/options/github-copilot.xml +5 -0
  218. package/ide/jetbrains/options/nodejs.xml +7 -0
  219. package/ide/jetbrains/options/templates.xml +124 -0
  220. package/ide/jetbrains/templates/JavaScript.xml +6 -0
  221. package/ide/jetbrains/templates/Stacks.xml +8 -0
  222. package/ide/vscode/.vscode/README.md +23 -0
  223. package/ide/vscode/.vscode/extensions.json +5 -0
  224. package/ide/vscode/.vscode/settings.json +176 -0
  225. package/ide/vscode/.vscode/stacks.code-snippets +181 -0
  226. package/ide/vscode/LICENSE.md +21 -0
  227. package/ide/vscode/README.md +36 -0
  228. package/ide/vscode/logo.png +0 -0
  229. package/ide/vscode/package.json +79 -0
  230. package/ide/zed/.zed/settings.json +139 -0
  231. package/lang/README.md +28 -0
  232. package/lang/de.yml +14 -0
  233. package/lang/en.yml +15 -0
  234. package/package.json +19 -3
  235. package/project/bootstrap +161 -0
  236. package/project/buddy +175 -0
  237. package/project/storage/framework/tsconfig.app.json +50 -0
  238. package/project/storage/framework/tsconfig.base.json +58 -0
  239. package/routes/core.ts +78 -0
  240. package/routes/dashboard-api.ts +233 -0
  241. package/routes/dashboard.ts +739 -0
  242. package/routes/email.ts +129 -0
  243. package/stores/[name].stx +16 -0
  244. package/stores/git.ts +71 -0
  245. package/stores/payment.ts +474 -0
  246. package/stores/queue.ts +43 -0
  247. package/stores/user.ts +29 -0
  248. package/stx-components-plugin.ts +25 -0
  249. package/styles/design-tokens.css +597 -0
  250. package/types/dashboard.ts +70 -0
  251. package/types/defaults.ts +438 -0
  252. package/vcs/github/CODE_OF_CONDUCT.md +83 -0
  253. package/vcs/github/CONTRIBUTING.md +187 -0
  254. package/vcs/github/EXPLANATIONS.md +41 -0
  255. package/vcs/github/FUNDING.yml +1 -0
  256. package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  257. package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
  258. package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
  259. package/vcs/github/SECURITY.md +70 -0
  260. package/vcs/github/renovate.json +5 -0
  261. package/vcs/github/stale.yml +14 -0
  262. package/vcs/github/workflows/README.md +16 -0
  263. package/vcs/github/workflows/ci.yml +114 -0
  264. package/vcs/github/workflows/export-size.yml +25 -0
  265. package/vcs/github/workflows/labeler.yml +13 -0
  266. package/vcs/github/workflows/release.yml +219 -0
  267. package/views/[...all].stx +8 -0
  268. package/views/about.md +3 -0
  269. package/views/auth/login.stx +1 -0
  270. package/views/auth/signup.stx +85 -0
  271. package/views/dashboard/.discovered-models.json +542 -0
  272. package/views/dashboard/AUDIT.md +904 -0
  273. package/views/dashboard/[...all].stx +89 -0
  274. package/views/dashboard/access-tokens/index.stx +177 -0
  275. package/views/dashboard/actions/index.stx +129 -0
  276. package/views/dashboard/analytics/blog/index.stx +356 -0
  277. package/views/dashboard/analytics/browsers/index.stx +263 -0
  278. package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
  279. package/views/dashboard/analytics/commerce/web/index.stx +379 -0
  280. package/views/dashboard/analytics/countries/index.stx +289 -0
  281. package/views/dashboard/analytics/devices/index.stx +305 -0
  282. package/views/dashboard/analytics/events/index.stx +536 -0
  283. package/views/dashboard/analytics/index.stx +88 -0
  284. package/views/dashboard/analytics/marketing/index.stx +315 -0
  285. package/views/dashboard/analytics/pages/index.stx +271 -0
  286. package/views/dashboard/analytics/referrers/index.stx +319 -0
  287. package/views/dashboard/analytics/web/index.stx +356 -0
  288. package/views/dashboard/buddy/index.stx +198 -0
  289. package/views/dashboard/ci/index.stx +462 -0
  290. package/views/dashboard/cloud/index.stx +452 -0
  291. package/views/dashboard/commands/index.stx +295 -0
  292. package/views/dashboard/commerce/categories/index.stx +406 -0
  293. package/views/dashboard/commerce/coupons/index.stx +485 -0
  294. package/views/dashboard/commerce/customers/index.stx +410 -0
  295. package/views/dashboard/commerce/dashboard/index.stx +287 -0
  296. package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
  297. package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
  298. package/views/dashboard/commerce/delivery/drivers.stx +397 -0
  299. package/views/dashboard/commerce/delivery/index.stx +18 -0
  300. package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
  301. package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
  302. package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
  303. package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
  304. package/views/dashboard/commerce/delivery/tabs.ts +25 -0
  305. package/views/dashboard/commerce/gift-cards/index.stx +561 -0
  306. package/views/dashboard/commerce/manufacturers/index.stx +308 -0
  307. package/views/dashboard/commerce/orders/index.stx +247 -0
  308. package/views/dashboard/commerce/payments/index.stx +535 -0
  309. package/views/dashboard/commerce/pos/index.stx +653 -0
  310. package/views/dashboard/commerce/printers/devices/index.stx +273 -0
  311. package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
  312. package/views/dashboard/commerce/products/detail.stx +563 -0
  313. package/views/dashboard/commerce/products/index.stx +370 -0
  314. package/views/dashboard/commerce/reviews/index.stx +656 -0
  315. package/views/dashboard/commerce/reviews/index.ts +0 -0
  316. package/views/dashboard/commerce/taxes/index.stx +377 -0
  317. package/views/dashboard/commerce/units/index.stx +317 -0
  318. package/views/dashboard/commerce/variants/index.stx +319 -0
  319. package/views/dashboard/commerce/waitlist/products.stx +942 -0
  320. package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
  321. package/views/dashboard/components/index.stx +330 -0
  322. package/views/dashboard/composables/index.ts +19 -0
  323. package/views/dashboard/composables/useChart.ts +77 -0
  324. package/views/dashboard/composables/useNavigation.ts +99 -0
  325. package/views/dashboard/composables/useRole.ts +58 -0
  326. package/views/dashboard/composables/useTheme.ts +74 -0
  327. package/views/dashboard/content/authors/index.stx +171 -0
  328. package/views/dashboard/content/blog/index.stx +493 -0
  329. package/views/dashboard/content/categories/index.stx +169 -0
  330. package/views/dashboard/content/comments/index.stx +189 -0
  331. package/views/dashboard/content/dashboard.stx +383 -0
  332. package/views/dashboard/content/files/index.stx +706 -0
  333. package/views/dashboard/content/pages/index.stx +174 -0
  334. package/views/dashboard/content/posts/index.stx +269 -0
  335. package/views/dashboard/content/seo/index.stx +240 -0
  336. package/views/dashboard/content/tags/index.stx +167 -0
  337. package/views/dashboard/data/activity/index.stx +261 -0
  338. package/views/dashboard/data/dashboard/index.stx +116 -0
  339. package/views/dashboard/data/subscribers.stx +270 -0
  340. package/views/dashboard/data/teams.stx +275 -0
  341. package/views/dashboard/data/users.stx +279 -0
  342. package/views/dashboard/dependencies/index.stx +178 -0
  343. package/views/dashboard/deployments/[id].stx +234 -0
  344. package/views/dashboard/deployments/index.stx +9 -0
  345. package/views/dashboard/dns/index.stx +163 -0
  346. package/views/dashboard/environment/index.stx +283 -0
  347. package/views/dashboard/errors/index.stx +274 -0
  348. package/views/dashboard/forgot-password.stx +57 -0
  349. package/views/dashboard/functions/index.stx +159 -0
  350. package/views/dashboard/health/index.stx +248 -0
  351. package/views/dashboard/inbox/activity.stx +350 -0
  352. package/views/dashboard/inbox/index.stx +452 -0
  353. package/views/dashboard/inbox/settings.stx +290 -0
  354. package/views/dashboard/index.stx +252 -0
  355. package/views/dashboard/insights/index.stx +529 -0
  356. package/views/dashboard/jobs/[id].stx +430 -0
  357. package/views/dashboard/jobs/history.stx +222 -0
  358. package/views/dashboard/jobs/index.stx +214 -0
  359. package/views/dashboard/kanban/[id].stx +899 -0
  360. package/views/dashboard/kanban/index.stx +260 -0
  361. package/views/dashboard/layouts/default.stx +565 -0
  362. package/views/dashboard/layouts/guest.stx +10 -0
  363. package/views/dashboard/login.stx +48 -0
  364. package/views/dashboard/logs/index.stx +540 -0
  365. package/views/dashboard/mailboxes/index.stx +149 -0
  366. package/views/dashboard/management/permissions/index.stx +490 -0
  367. package/views/dashboard/marketing/campaigns/index.stx +604 -0
  368. package/views/dashboard/marketing/lists/index.stx +514 -0
  369. package/views/dashboard/marketing/reviews/index.stx +475 -0
  370. package/views/dashboard/marketing/social-posts/index.stx +560 -0
  371. package/views/dashboard/models/[model].stx +195 -0
  372. package/views/dashboard/models/index.stx +159 -0
  373. package/views/dashboard/monitoring/errors/index.stx +415 -0
  374. package/views/dashboard/notifications/dashboard.stx +383 -0
  375. package/views/dashboard/notifications/email.stx +336 -0
  376. package/views/dashboard/notifications/history.stx +373 -0
  377. package/views/dashboard/notifications/sms.stx +332 -0
  378. package/views/dashboard/packages/index.stx +195 -0
  379. package/views/dashboard/queries/[id].stx +381 -0
  380. package/views/dashboard/queries/history.stx +227 -0
  381. package/views/dashboard/queries/index.stx +335 -0
  382. package/views/dashboard/queries/slow.stx +318 -0
  383. package/views/dashboard/queue/index.stx +427 -0
  384. package/views/dashboard/realtime/index.stx +429 -0
  385. package/views/dashboard/register.stx +46 -0
  386. package/views/dashboard/releases/index.stx +389 -0
  387. package/views/dashboard/requests/index.stx +462 -0
  388. package/views/dashboard/serverless/index.stx +475 -0
  389. package/views/dashboard/servers/[id].stx +299 -0
  390. package/views/dashboard/servers/index.stx +483 -0
  391. package/views/dashboard/settings/billing.stx +50 -0
  392. package/views/dashboard/settings/index.stx +256 -0
  393. package/views/dashboard/settings/mail.stx +252 -0
  394. package/views/dashboard/stores/auth.ts +123 -0
  395. package/views/dashboard/stores/ci.ts +342 -0
  396. package/views/dashboard/stores/index.ts +129 -0
  397. package/views/dashboard/stores/kanban.ts +723 -0
  398. package/views/dashboard/stores/rbac.ts +350 -0
  399. package/views/dashboard/stx.config.ts +34 -0
  400. package/views/dashboard/teams/[id].stx +170 -0
  401. package/views/forum/category.stx +92 -0
  402. package/views/forum/index.stx +209 -0
  403. package/views/forum/new-topic.stx +113 -0
  404. package/views/forum/profile.stx +1 -0
  405. package/views/forum/topic.stx +143 -0
  406. package/views/hello-world/[name].stx +47 -0
  407. package/views/index.stx +21 -0
  408. package/views/system-tray/index.stx +147 -0
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: stacks-browse
3
+ description: Use for headless browser QA on Stacks applications — navigation, screenshots, responsive testing, console/network monitoring, and accessibility snapshots. Dependency-free: drives a system browser over the Chrome DevTools Protocol using only Bun (no Playwright/Puppeteer). Invoke with /stacks-browse.
4
+ license: MIT
5
+ compatibility: Bun >= 1.3.0, TypeScript, a Chromium-family browser on the machine
6
+ allowed-tools: Read Edit Write Bash Grep Glob
7
+ ---
8
+
9
+ # /stacks-browse — Headless Browser QA (zero dependencies)
10
+
11
+ You are a QA engineer testing Stacks applications with a **dependency-free** headless
12
+ browser driver. It uses **no Playwright, no Puppeteer, no npm packages** — it launches a
13
+ Chromium-family browser already on the machine and drives it over the Chrome DevTools
14
+ Protocol (CDP) using only Bun's native `Bun.spawn`, `fetch`, and `WebSocket`.
15
+
16
+ The driver lives at `storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts`. Run it with `bun`:
17
+
18
+ ```bash
19
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts <command> <url> [flags]
20
+ ```
21
+
22
+ Every command prints a JSON result. Screenshots are written under `storage/framework/runtime/shots/` by
23
+ default; **Read the PNG** to view it.
24
+
25
+ ## Browser discovery (no install step)
26
+
27
+ The driver finds a browser at runtime in this order, and falls through to the next if one
28
+ won't launch:
29
+
30
+ 1. `$BROWSE_BROWSER` (explicit override — set to an absolute browser path)
31
+ 2. PATH: `chromium`, `google-chrome`, `brave-browser`, `microsoft-edge`, …
32
+ 3. macOS app bundles (Google Chrome, Chromium, Brave, Edge)
33
+ 4. A Chromium binary already cached on disk (e.g. under `~/Library/Caches/ms-playwright`) —
34
+ the **binary only** is borrowed; Playwright is never imported.
35
+
36
+ A candidate is validated with `--version` before use, so a dead wrapper (e.g. a Homebrew
37
+ shim pointing at an uninstalled `.app`) is skipped automatically. If none launch, it tells
38
+ you to `brew install --cask chromium` or set `BROWSE_BROWSER`.
39
+
40
+ ## Default Stacks dev URLs
41
+
42
+ - Frontend: `http://localhost:3000`
43
+ - Backend API: `http://localhost:3001`
44
+ - Admin dashboard: `http://localhost:3002`
45
+ - Docs: `http://localhost:3005`
46
+ - API: `http://localhost:3008`
47
+
48
+ > Pages with an open HMR/SSE connection never reach "network idle"; the driver waits for
49
+ > the load event (with a timeout) plus a short settle, so it won't hang on the dev server.
50
+
51
+ ## Commands
52
+
53
+ ### Navigate
54
+ ```bash
55
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts navigate <url>
56
+ ```
57
+ Reports: `title`, HTTP `status` of the main document, `loadMs`, `consoleErrors`, request count, and which browser was used.
58
+
59
+ ### Screenshot
60
+ ```bash
61
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts screenshot <url> [--viewport 1280x900] [--full] [--element "SELECTOR"] [--scale 2] [--out path.png] [--cookie "name=value"] [--settle 1500]
62
+ ```
63
+ - `--full` captures the entire scroll height (via `Page.getLayoutMetrics` + `captureBeyondViewport`).
64
+ - `--element` clips to a CSS selector's bounding box.
65
+ - `--scale 2` renders at 2× (retina).
66
+ - `--cookie` pre-seeds cookies before navigation (repeatable). Use it for gated pages, e.g. the coming-soon bypass: `--cookie "stacks_coming_soon_bypass=<secret>" --cookie "stacks_coming_soon_preview=<secret>"`.
67
+ - `--settle` waits N ms after the load event before capturing (default 700). Stretch it when the page has load-triggered entrance animations, or the shot can catch elements mid-fade.
68
+ - `--scheme light|dark` emulates `prefers-color-scheme`, so theme-aware pages (auto dark mode) can be captured in both schemes without changing the host OS setting.
69
+ - Default output: `storage/framework/runtime/shots/<path>.png`.
70
+
71
+ ### Responsive
72
+ ```bash
73
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts responsive <url> [--out-dir DIR]
74
+ ```
75
+ Full-page screenshot at each breakpoint and a horizontal-overflow check:
76
+
77
+ | Device | Width | Height |
78
+ |--------|-------|--------|
79
+ | Mobile S | 320 | 568 |
80
+ | Mobile L | 428 | 926 |
81
+ | Tablet | 768 | 1024 |
82
+ | Desktop | 1280 | 720 |
83
+ | Wide | 1920 | 1080 |
84
+
85
+ Reports `horizontalOverflowPx` per breakpoint (0 = no overflow). Shots in `storage/framework/runtime/shots/responsive/`.
86
+
87
+ ### Monitor (console + network)
88
+ ```bash
89
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts monitor <url> [--ms 5000]
90
+ ```
91
+ Watches for `--ms` after load. Reports console errors/messages, failed requests (≥400), slow requests (>3s), and total requests.
92
+
93
+ ### Snapshot (accessibility / structure)
94
+ ```bash
95
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts snapshot <url>
96
+ ```
97
+ Extracts headings, links (`text -> href`), buttons, forms (action + field count), and ARIA landmarks — useful for auditing structure and catching broken links.
98
+
99
+ ## Stacks-Specific QA
100
+
101
+ When testing a Stacks app, check:
102
+ - **Dashboard routes** — admin pages rendering? (`localhost:3002`)
103
+ - **API health** — `GET localhost:3008/health` returns ok?
104
+ - **Auth flow** — `/login`, `/register`
105
+ - **CMS/blog** — `/blog`, post detail pages, `/blog/feed.xml`, `/blog/sitemap.xml`
106
+ - **STX components** — do custom components render server-side?
107
+ - **Crosswind CSS** — utility classes generating styles?
108
+ - **Links** — run `snapshot` and verify hrefs resolve (no `/blog/index`, no `/api/blog/*` 404s).
109
+
110
+ ## Rules
111
+
112
+ - **No npm install.** Never add Playwright/Puppeteer. The driver is self-contained.
113
+ - **The browser is always killed** in a `finally` block; a fresh temp profile per run (no shared cookies/sessions).
114
+ - **Never submit forms with real data** unless explicitly asked.
115
+ - **Local URLs are fine** (`localhost`, `127.0.0.1`).
116
+ - **Report what you see, not what you expect.** Read the PNGs.
117
+
118
+ ## Extending
119
+
120
+ The CDP client (`Cdp` class in `browse.ts`) exposes `send(method, params)` and
121
+ `waitFor(event, predicate)`. New QA commands (click, fill, hover, PDF export, coverage)
122
+ are a few lines each on top of `Page.*`, `Input.*`, `Runtime.evaluate`, and `DOM.*` — no
123
+ new dependencies required.
124
+
125
+ ## Downstream
126
+
127
+ > **QA complete.** Run `/stacks-retro` to review this development session.
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Dependency-free headless browser driver for the /stacks-browse QA skill.
4
+ *
5
+ * No Playwright, no Puppeteer, no npm packages. It launches a Chromium-family
6
+ * browser already on the machine and drives it over the Chrome DevTools
7
+ * Protocol using only Bun's native `Bun.spawn`, `fetch`, and `WebSocket`.
8
+ *
9
+ * Commands:
10
+ * bun browse.ts navigate <url>
11
+ * bun browse.ts screenshot <url> [--viewport WxH] [--full] [--element SEL] [--scale N] [--out PATH]
12
+ * bun browse.ts responsive <url> [--out-dir DIR]
13
+ * bun browse.ts monitor <url> [--ms 5000]
14
+ * bun browse.ts snapshot <url>
15
+ *
16
+ * Browser discovery order: $BROWSE_BROWSER → PATH (chromium, google-chrome, …)
17
+ * → common macOS app bundles → a Playwright-cached chromium as last resort.
18
+ */
19
+
20
+ import { spawn } from 'bun'
21
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs'
22
+ import { tmpdir } from 'node:os'
23
+ import { join } from 'node:path'
24
+
25
+ // ── Browser discovery ──────────────────────────────────────────────────────
26
+
27
+ function which(bin: string): string | null {
28
+ try {
29
+ const r = Bun.spawnSync(['which', bin])
30
+ const out = r.stdout.toString().trim()
31
+ return out && existsSync(out) ? out : null
32
+ }
33
+ catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ /** Ordered list of candidate browser binaries — validated lazily at launch. */
39
+ function collectCandidates(): string[] {
40
+ const out: string[] = []
41
+ const add = (p: string | null) => { if (p && existsSync(p) && !out.includes(p)) out.push(p) }
42
+
43
+ if (process.env.BROWSE_BROWSER)
44
+ add(process.env.BROWSE_BROWSER)
45
+
46
+ for (const bin of ['chromium', 'chromium-browser', 'google-chrome-stable', 'google-chrome', 'brave-browser', 'microsoft-edge', 'chrome'])
47
+ add(which(bin))
48
+
49
+ for (const p of [
50
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
51
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
52
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
53
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
54
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
55
+ ]) add(p)
56
+
57
+ // Last resort: a Chromium that Playwright may have cached. We only borrow the
58
+ // binary — we never import or require Playwright itself.
59
+ const cacheRoot = join(process.env.HOME || '', 'Library/Caches/ms-playwright')
60
+ if (existsSync(cacheRoot)) {
61
+ try {
62
+ const hit = Bun.spawnSync(['find', cacheRoot, '-maxdepth', '3', '-name', 'chrome-headless-shell', '-o', '-maxdepth', '3', '-name', 'Chromium'])
63
+ for (const p of hit.stdout.toString().trim().split('\n').filter(Boolean)) add(p)
64
+ }
65
+ catch { /* ignore */ }
66
+ }
67
+
68
+ if (!out.length)
69
+ throw new Error('No Chromium-family browser found. Install one (e.g. `brew install --cask chromium`) or set BROWSE_BROWSER=/path/to/chrome.')
70
+ return out
71
+ }
72
+
73
+ /** A candidate is usable only if it actually runs — `--version` weeds out dead
74
+ * wrappers (e.g. a Homebrew shim pointing at an uninstalled .app). */
75
+ function runs(bin: string): boolean {
76
+ try {
77
+ const r = Bun.spawnSync([bin, '--version'], { stdout: 'pipe', stderr: 'pipe' })
78
+ return r.exitCode === 0 && r.stdout.toString().trim().length > 0
79
+ }
80
+ catch {
81
+ return false
82
+ }
83
+ }
84
+
85
+ // ── Minimal CDP client over Bun's native WebSocket ──────────────────────────
86
+
87
+ interface CdpEvent { method: string, params: any }
88
+
89
+ class Cdp {
90
+ private ws: WebSocket
91
+ private id = 0
92
+ // `_v` / `_e` are parameter names in a type position, never bound to anything.
93
+ private pending = new Map<number, { resolve: (_v: any) => void, reject: (_e: any) => void }>()
94
+ private listeners: ((_e: CdpEvent) => void)[] = []
95
+
96
+ private constructor(ws: WebSocket) {
97
+ this.ws = ws
98
+ ws.addEventListener('message', (ev: any) => {
99
+ const msg = JSON.parse(ev.data)
100
+ if (msg.id != null && this.pending.has(msg.id)) {
101
+ const { resolve, reject } = this.pending.get(msg.id)!
102
+ this.pending.delete(msg.id)
103
+ msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result)
104
+ }
105
+ else if (msg.method) {
106
+ for (const l of this.listeners) l({ method: msg.method, params: msg.params })
107
+ }
108
+ })
109
+ }
110
+
111
+ static connect(wsUrl: string, timeoutMs = 10_000): Promise<Cdp> {
112
+ return new Promise((resolve, reject) => {
113
+ const ws = new WebSocket(wsUrl)
114
+ const t = setTimeout(() => reject(new Error('CDP connect timeout')), timeoutMs)
115
+ ws.addEventListener('open', () => { clearTimeout(t); resolve(new Cdp(ws)) })
116
+ ws.addEventListener('error', e => { clearTimeout(t); reject(e) })
117
+ })
118
+ }
119
+
120
+ send(method: string, params: Record<string, any> = {}): Promise<any> {
121
+ const id = ++this.id
122
+ return new Promise((resolve, reject) => {
123
+ this.pending.set(id, { resolve, reject })
124
+ this.ws.send(JSON.stringify({ id, method, params }))
125
+ })
126
+ }
127
+
128
+ on(fn: (e: CdpEvent) => void): void {
129
+ this.listeners.push(fn)
130
+ }
131
+
132
+ waitFor(method: string, predicate: (p: any) => boolean = () => true, timeoutMs = 15_000): Promise<any> {
133
+ return new Promise((resolve, reject) => {
134
+ const t = setTimeout(() => reject(new Error(`Timed out waiting for ${method}`)), timeoutMs)
135
+ this.on(e => {
136
+ if (e.method === method && predicate(e.params)) {
137
+ clearTimeout(t)
138
+ resolve(e.params)
139
+ }
140
+ })
141
+ })
142
+ }
143
+
144
+ close(): void {
145
+ try { this.ws.close() }
146
+ catch { /* ignore */ }
147
+ }
148
+ }
149
+
150
+ // ── Browser lifecycle ───────────────────────────────────────────────────────
151
+
152
+ interface Session { proc: ReturnType<typeof spawn>, port: number, userDataDir: string, browser: string }
153
+
154
+ async function tryLaunch(browser: string): Promise<Session | null> {
155
+ const userDataDir = join(tmpdir(), `stacks-browse-${process.pid}-${Math.floor(Number(process.hrtime.bigint() % 1000000n))}`)
156
+ mkdirSync(userDataDir, { recursive: true })
157
+
158
+ const isHeadlessShell = /chrome-headless-shell|headless_shell/.test(browser)
159
+ const proc = spawn([
160
+ browser,
161
+ ...(isHeadlessShell ? [] : ['--headless=new']),
162
+ '--remote-debugging-port=0',
163
+ `--user-data-dir=${userDataDir}`,
164
+ '--no-first-run',
165
+ '--no-default-browser-check',
166
+ '--disable-gpu',
167
+ '--disable-dev-shm-usage',
168
+ '--hide-scrollbars',
169
+ '--mute-audio',
170
+ '--force-color-profile=srgb',
171
+ 'about:blank',
172
+ ], { stdout: 'ignore', stderr: 'ignore' })
173
+
174
+ // Chromium writes the chosen port to <user-data-dir>/DevToolsActivePort.
175
+ const portFile = join(userDataDir, 'DevToolsActivePort')
176
+ for (let i = 0; i < 80; i++) {
177
+ if (existsSync(portFile)) {
178
+ const line = readFileSync(portFile, 'utf8').split('\n')[0]?.trim()
179
+ if (line)
180
+ return { proc, port: Number(line), userDataDir, browser }
181
+ }
182
+ await Bun.sleep(50)
183
+ }
184
+ try { proc.kill() }
185
+ catch { /* ignore */ }
186
+ return null
187
+ }
188
+
189
+ async function launch(): Promise<Session> {
190
+ const tried: string[] = []
191
+ for (const browser of collectCandidates()) {
192
+ if (!runs(browser)) { tried.push(`${browser} (won't run)`); continue }
193
+ const s = await tryLaunch(browser)
194
+ if (s)
195
+ return s
196
+ tried.push(`${browser} (no DevTools port)`)
197
+ }
198
+ throw new Error(`Could not launch any browser. Tried:\n ${tried.join('\n ')}\nSet BROWSE_BROWSER=/path/to/chrome to override.`)
199
+ }
200
+
201
+ async function openPage(port: number): Promise<Cdp> {
202
+ // The initial about:blank tab already exists; grab its page-level WS URL.
203
+ for (let i = 0; i < 50; i++) {
204
+ try {
205
+ const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json() as any[]
206
+ const page = list.find(t => t.type === 'page')
207
+ if (page?.webSocketDebuggerUrl)
208
+ return Cdp.connect(page.webSocketDebuggerUrl)
209
+ }
210
+ catch { /* not ready yet */ }
211
+ await Bun.sleep(50)
212
+ }
213
+ throw new Error('Could not find a page target to attach to.')
214
+ }
215
+
216
+ function kill(s: Session): void {
217
+ try { s.proc.kill() }
218
+ catch { /* ignore */ }
219
+ }
220
+
221
+ // ── Page helpers ────────────────────────────────────────────────────────────
222
+
223
+ interface PageState { consoleErrors: string[], console: string[], responses: { url: string, status: number, ms: number, type: string }[], mainStatus: number | null }
224
+
225
+ async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w: number, h: number }, scale?: number, timeoutMs?: number, cookies?: string[], settleMs?: number, scheme?: string } = {}): Promise<PageState> {
226
+ const state: PageState = { consoleErrors: [], console: [], responses: [], mainStatus: null }
227
+ const startById = new Map<string, number>()
228
+
229
+ await cdp.send('Page.enable')
230
+ await cdp.send('Runtime.enable')
231
+ await cdp.send('Log.enable')
232
+ await cdp.send('Network.enable')
233
+
234
+ // Pre-seed cookies (e.g. maintenance/coming-soon bypass tokens) so gated
235
+ // pages can be QA'd headlessly. Each entry is a `name=value` pair scoped
236
+ // to the target origin.
237
+ if (opts.cookies?.length) {
238
+ const origin = new URL(url)
239
+ for (const entry of opts.cookies) {
240
+ const eq = entry.indexOf('=')
241
+ if (eq === -1) continue
242
+ await cdp.send('Network.setCookie', {
243
+ name: entry.slice(0, eq).trim(),
244
+ value: entry.slice(eq + 1).trim(),
245
+ domain: origin.hostname,
246
+ path: '/',
247
+ })
248
+ }
249
+ }
250
+
251
+ if (opts.viewport) {
252
+ await cdp.send('Emulation.setDeviceMetricsOverride', {
253
+ width: opts.viewport.w,
254
+ height: opts.viewport.h,
255
+ deviceScaleFactor: opts.scale ?? 1,
256
+ mobile: opts.viewport.w < 600,
257
+ })
258
+ }
259
+
260
+ // Emulate light/dark so prefers-color-scheme pages can be QA'd in both
261
+ // schemes without flipping the host OS setting.
262
+ if (opts.scheme === 'light' || opts.scheme === 'dark') {
263
+ await cdp.send('Emulation.setEmulatedMedia', {
264
+ features: [{ name: 'prefers-color-scheme', value: opts.scheme }],
265
+ })
266
+ }
267
+
268
+ cdp.on((e) => {
269
+ if (e.method === 'Runtime.consoleAPICalled') {
270
+ const text = (e.params.args || []).map((a: any) => a.value ?? a.description ?? a.type).join(' ')
271
+ state.console.push(`[${e.params.type}] ${text}`)
272
+ if (e.params.type === 'error') state.consoleErrors.push(text)
273
+ }
274
+ else if (e.method === 'Runtime.exceptionThrown') {
275
+ const d = e.params.exceptionDetails
276
+ state.consoleErrors.push(d?.exception?.description || d?.text || 'Uncaught exception')
277
+ }
278
+ else if (e.method === 'Log.entryAdded' && e.params.entry?.level === 'error') {
279
+ state.consoleErrors.push(e.params.entry.text)
280
+ }
281
+ else if (e.method === 'Network.requestWillBeSent') {
282
+ startById.set(e.params.requestId, e.params.timestamp * 1000)
283
+ }
284
+ else if (e.method === 'Network.responseReceived') {
285
+ const r = e.params.response
286
+ const started = startById.get(e.params.requestId) ?? r.timing?.requestTime * 1000 ?? 0
287
+ const ms = started ? Math.max(0, Math.round(e.params.timestamp * 1000 - started)) : 0
288
+ state.responses.push({ url: r.url, status: r.status, ms, type: e.params.type })
289
+ if (e.params.type === 'Document' && state.mainStatus == null)
290
+ state.mainStatus = r.status
291
+ }
292
+ })
293
+
294
+ await cdp.send('Page.navigate', { url })
295
+ try { await cdp.waitFor('Page.loadEventFired', () => true, opts.timeoutMs ?? 15_000) }
296
+ catch { /* SSE/long-poll pages may never fire load; proceed after a settle */ }
297
+ // Settle after load so load-triggered entrance animations finish before a
298
+ // screenshot; pages with longer motion pass --settle to stretch it.
299
+ await Bun.sleep(opts.settleMs ?? 700)
300
+ return state
301
+ }
302
+
303
+ async function title(cdp: Cdp): Promise<string> {
304
+ const r = await cdp.send('Runtime.evaluate', { expression: 'document.title', returnByValue: true })
305
+ return r.result?.value ?? ''
306
+ }
307
+
308
+ async function captureScreenshot(cdp: Cdp, opts: { full?: boolean, element?: string } = {}): Promise<Buffer> {
309
+ let clip: any
310
+ let captureBeyondViewport = false
311
+
312
+ if (opts.element) {
313
+ const r = await cdp.send('Runtime.evaluate', {
314
+ expression: `(() => { const el = document.querySelector(${JSON.stringify(opts.element)}); if (!el) return null; const b = el.getBoundingClientRect(); return { x: b.x + scrollX, y: b.y + scrollY, width: b.width, height: b.height }; })()`,
315
+ returnByValue: true,
316
+ })
317
+ if (!r.result?.value)
318
+ throw new Error(`Element not found: ${opts.element}`)
319
+ clip = { ...r.result.value, scale: 1 }
320
+ captureBeyondViewport = true
321
+ }
322
+ else if (opts.full) {
323
+ const m = await cdp.send('Page.getLayoutMetrics')
324
+ const size = m.cssContentSize || m.contentSize
325
+ clip = { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height), scale: 1 }
326
+ captureBeyondViewport = true
327
+ }
328
+
329
+ const r = await cdp.send('Page.captureScreenshot', { format: 'png', ...(clip ? { clip, captureBeyondViewport } : {}) })
330
+ return Buffer.from(r.data, 'base64')
331
+ }
332
+
333
+ // ── CLI ─────────────────────────────────────────────────────────────────────
334
+
335
+ function parseFlags(args: string[]): { positional: string[], flags: Record<string, string | boolean | Array<string | boolean>> } {
336
+ const positional: string[] = []
337
+ const flags: Record<string, string | boolean | Array<string | boolean>> = {}
338
+ for (let i = 0; i < args.length; i++) {
339
+ const a = args[i]
340
+ if (a.startsWith('--')) {
341
+ const key = a.slice(2)
342
+ const next = args[i + 1]
343
+ const value: string | boolean = (next && !next.startsWith('--')) ? next : true
344
+ if (typeof value === 'string') i++
345
+ // Repeated flags (--cookie a=1 --cookie b=2) collect into an array.
346
+ if (key in flags) {
347
+ const prev = flags[key]
348
+ flags[key] = Array.isArray(prev) ? [...prev, value] : [prev as string | boolean, value]
349
+ }
350
+ else {
351
+ flags[key] = value
352
+ }
353
+ }
354
+ else { positional.push(a) }
355
+ }
356
+ return { positional, flags }
357
+ }
358
+
359
+ /** Normalize a (possibly repeated) flag into a flat string list. */
360
+ function flagList(value: string | boolean | Array<string | boolean> | undefined): string[] {
361
+ if (value == null || value === false) return []
362
+ const arr = Array.isArray(value) ? value : [value]
363
+ return arr.filter((v): v is string => typeof v === 'string')
364
+ }
365
+
366
+ const BREAKPOINTS = [
367
+ { device: 'Mobile S', w: 320, h: 568 },
368
+ { device: 'Mobile L', w: 428, h: 926 },
369
+ { device: 'Tablet', w: 768, h: 1024 },
370
+ { device: 'Desktop', w: 1280, h: 720 },
371
+ { device: 'Wide', w: 1920, h: 1080 },
372
+ ]
373
+
374
+ async function main() {
375
+ const [command, ...rest] = process.argv.slice(2)
376
+ const { positional, flags } = parseFlags(rest)
377
+ const url = positional[0]
378
+
379
+ if (!command || command === 'help' || !url) {
380
+ console.log('Usage: bun browse.ts <navigate|screenshot|responsive|monitor|snapshot> <url> [flags]')
381
+ console.log(' --cookie "name=value" repeatable; pre-seeds cookies (e.g. coming-soon bypass)')
382
+ console.log(' --settle 1500 ms to wait after load before acting (default 700; stretch for entrance animations)')
383
+ console.log(' --scheme dark emulate prefers-color-scheme (light|dark) for QA of theme-aware pages')
384
+ process.exit(url ? 0 : 1)
385
+ }
386
+
387
+ const session = await launch()
388
+ const cookies = flagList(flags.cookie)
389
+ const settleMs = flags.settle ? Number(flags.settle) : undefined
390
+ const scheme = typeof flags.scheme === 'string' ? flags.scheme : undefined
391
+ try {
392
+ if (command === 'navigate' || command === 'go') {
393
+ const cdp = await openPage(session.port)
394
+ const t0 = performance.now()
395
+ const state = await gotoAndInstrument(cdp, url, { cookies, settleMs, scheme })
396
+ const loadMs = Math.round(performance.now() - t0)
397
+ console.log(JSON.stringify({
398
+ browser: session.browser,
399
+ url,
400
+ title: await title(cdp),
401
+ status: state.mainStatus,
402
+ loadMs,
403
+ consoleErrors: state.consoleErrors,
404
+ requests: state.responses.length,
405
+ }, null, 2))
406
+ cdp.close()
407
+ }
408
+
409
+ else if (command === 'screenshot') {
410
+ const cdp = await openPage(session.port)
411
+ const vp = typeof flags.viewport === 'string' ? flags.viewport.split('x').map(Number) : [1280, 900]
412
+ const scale = flags.scale ? Number(flags.scale) : 1
413
+ const out = (flags.out as string) || `storage/framework/runtime/shots/${new URL(url).pathname.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home'}.png`
414
+ mkdirSync(out.split('/').slice(0, -1).join('/') || '.', { recursive: true })
415
+ await gotoAndInstrument(cdp, url, { viewport: { w: vp[0], h: vp[1] }, scale, cookies, settleMs, scheme })
416
+ const png = await captureScreenshot(cdp, { full: !!flags.full, element: flags.element as string | undefined })
417
+ await Bun.write(out, png)
418
+ console.log(JSON.stringify({ url, out, viewport: `${vp[0]}x${vp[1]}`, scale, full: !!flags.full, element: flags.element ?? null, bytes: png.length }, null, 2))
419
+ cdp.close()
420
+ }
421
+
422
+ else if (command === 'responsive') {
423
+ const outDir = (flags['out-dir'] as string) || 'storage/framework/runtime/shots/responsive'
424
+ mkdirSync(outDir, { recursive: true })
425
+ const results: any[] = []
426
+ for (const bp of BREAKPOINTS) {
427
+ const cdp = await openPage(session.port)
428
+ await gotoAndInstrument(cdp, url, { viewport: { w: bp.w, h: bp.h }, cookies, settleMs, scheme })
429
+ const overflow = await cdp.send('Runtime.evaluate', {
430
+ expression: 'document.documentElement.scrollWidth > window.innerWidth ? document.documentElement.scrollWidth - window.innerWidth : 0',
431
+ returnByValue: true,
432
+ })
433
+ const out = join(outDir, `${bp.device.toLowerCase().replace(/\s+/g, '-')}.png`)
434
+ await Bun.write(out, await captureScreenshot(cdp, { full: true }))
435
+ results.push({ device: bp.device, viewport: `${bp.w}x${bp.h}`, out, horizontalOverflowPx: overflow.result?.value ?? 0 })
436
+ cdp.close()
437
+ }
438
+ console.log(JSON.stringify({ url, results }, null, 2))
439
+ }
440
+
441
+ else if (command === 'monitor') {
442
+ const cdp = await openPage(session.port)
443
+ const ms = flags.ms ? Number(flags.ms) : 5000
444
+ const state = await gotoAndInstrument(cdp, url, { cookies, settleMs, scheme })
445
+ await Bun.sleep(ms)
446
+ const failed = state.responses.filter(r => r.status >= 400)
447
+ const slow = state.responses.filter(r => r.ms > 3000)
448
+ console.log(JSON.stringify({
449
+ url,
450
+ consoleErrors: state.consoleErrors,
451
+ consoleMessages: state.console,
452
+ failedRequests: failed,
453
+ slowRequests: slow,
454
+ totalRequests: state.responses.length,
455
+ }, null, 2))
456
+ cdp.close()
457
+ }
458
+
459
+ else if (command === 'snapshot') {
460
+ const cdp = await openPage(session.port)
461
+ await gotoAndInstrument(cdp, url, { cookies, settleMs, scheme })
462
+ const expr = `(() => {
463
+ const sel = (q) => Array.from(document.querySelectorAll(q));
464
+ const txt = (e) => (e.innerText || e.textContent || '').trim().slice(0, 80);
465
+ return {
466
+ title: document.title,
467
+ headings: sel('h1,h2,h3').map(h => h.tagName + ': ' + txt(h)).slice(0, 40),
468
+ links: sel('a[href]').map(a => txt(a) + ' -> ' + a.getAttribute('href')).slice(0, 60),
469
+ buttons: sel('button, [role=button]').map(txt).filter(Boolean).slice(0, 40),
470
+ forms: sel('form').map(f => ({ action: f.getAttribute('action'), fields: sel.call(null, 'input,select,textarea').length })).slice(0, 10),
471
+ landmarks: sel('[role], nav, main, header, footer, aside').map(e => e.getAttribute('role') || e.tagName.toLowerCase()).slice(0, 20),
472
+ };
473
+ })()`
474
+ const r = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true })
475
+ console.log(JSON.stringify({ url, ...r.result?.value }, null, 2))
476
+ cdp.close()
477
+ }
478
+
479
+ else {
480
+ console.error(`Unknown command: ${command}`)
481
+ process.exit(1)
482
+ }
483
+ }
484
+ finally {
485
+ kill(session)
486
+ }
487
+ }
488
+
489
+ main().catch((e) => {
490
+ console.error('browse error:', e?.message || e)
491
+ process.exit(1)
492
+ })