@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.
- package/TODO.md +21 -0
- package/ai/AGENTS.md +79 -0
- package/ai/README.md +45 -0
- package/ai/claude/launch.json +12 -0
- package/ai/skills/stacks-actions/SKILL.md +128 -0
- package/ai/skills/stacks-ai/SKILL.md +276 -0
- package/ai/skills/stacks-alias/SKILL.md +94 -0
- package/ai/skills/stacks-analytics/SKILL.md +91 -0
- package/ai/skills/stacks-api/SKILL.md +738 -0
- package/ai/skills/stacks-arrays/SKILL.md +277 -0
- package/ai/skills/stacks-auth/SKILL.md +422 -0
- package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
- package/ai/skills/stacks-brandkit/SKILL.md +811 -0
- package/ai/skills/stacks-browse/SKILL.md +127 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
- package/ai/skills/stacks-browser/SKILL.md +145 -0
- package/ai/skills/stacks-buddy/SKILL.md +676 -0
- package/ai/skills/stacks-build/SKILL.md +163 -0
- package/ai/skills/stacks-cache/SKILL.md +125 -0
- package/ai/skills/stacks-calendar/SKILL.md +113 -0
- package/ai/skills/stacks-chat/SKILL.md +371 -0
- package/ai/skills/stacks-cli/SKILL.md +112 -0
- package/ai/skills/stacks-cloud/SKILL.md +192 -0
- package/ai/skills/stacks-cms/SKILL.md +217 -0
- package/ai/skills/stacks-collections/SKILL.md +82 -0
- package/ai/skills/stacks-commerce/SKILL.md +114 -0
- package/ai/skills/stacks-composables/SKILL.md +161 -0
- package/ai/skills/stacks-config/SKILL.md +144 -0
- package/ai/skills/stacks-configuration/SKILL.md +154 -0
- package/ai/skills/stacks-cron/SKILL.md +123 -0
- package/ai/skills/stacks-crosswind/SKILL.md +190 -0
- package/ai/skills/stacks-dashboard/SKILL.md +173 -0
- package/ai/skills/stacks-database/SKILL.md +286 -0
- package/ai/skills/stacks-datetime/SKILL.md +269 -0
- package/ai/skills/stacks-dependencies/SKILL.md +111 -0
- package/ai/skills/stacks-deploy/SKILL.md +102 -0
- package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
- package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
- package/ai/skills/stacks-design-output/SKILL.md +55 -0
- package/ai/skills/stacks-design-soft/SKILL.md +156 -0
- package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
- package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
- package/ai/skills/stacks-desktop/SKILL.md +78 -0
- package/ai/skills/stacks-development/SKILL.md +298 -0
- package/ai/skills/stacks-dns/SKILL.md +149 -0
- package/ai/skills/stacks-docs/SKILL.md +109 -0
- package/ai/skills/stacks-email/SKILL.md +236 -0
- package/ai/skills/stacks-enums/SKILL.md +100 -0
- package/ai/skills/stacks-env/SKILL.md +181 -0
- package/ai/skills/stacks-error-handling/SKILL.md +137 -0
- package/ai/skills/stacks-events/SKILL.md +228 -0
- package/ai/skills/stacks-faker/SKILL.md +158 -0
- package/ai/skills/stacks-git/SKILL.md +158 -0
- package/ai/skills/stacks-guard/SKILL.md +98 -0
- package/ai/skills/stacks-health/SKILL.md +75 -0
- package/ai/skills/stacks-http/SKILL.md +316 -0
- package/ai/skills/stacks-i18n/SKILL.md +198 -0
- package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
- package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
- package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
- package/ai/skills/stacks-investigate/SKILL.md +119 -0
- package/ai/skills/stacks-jobs/SKILL.md +136 -0
- package/ai/skills/stacks-lint/SKILL.md +184 -0
- package/ai/skills/stacks-listeners/SKILL.md +99 -0
- package/ai/skills/stacks-logging/SKILL.md +78 -0
- package/ai/skills/stacks-mail/SKILL.md +120 -0
- package/ai/skills/stacks-middleware/SKILL.md +249 -0
- package/ai/skills/stacks-migrations/SKILL.md +105 -0
- package/ai/skills/stacks-models/SKILL.md +267 -0
- package/ai/skills/stacks-new-feature/SKILL.md +206 -0
- package/ai/skills/stacks-notifications/SKILL.md +210 -0
- package/ai/skills/stacks-objects/SKILL.md +205 -0
- package/ai/skills/stacks-office-hours/SKILL.md +134 -0
- package/ai/skills/stacks-orm/SKILL.md +342 -0
- package/ai/skills/stacks-path/SKILL.md +118 -0
- package/ai/skills/stacks-payments/SKILL.md +373 -0
- package/ai/skills/stacks-plan-review/SKILL.md +152 -0
- package/ai/skills/stacks-plugins/SKILL.md +149 -0
- package/ai/skills/stacks-push/SKILL.md +376 -0
- package/ai/skills/stacks-query-builder/SKILL.md +128 -0
- package/ai/skills/stacks-queue/SKILL.md +559 -0
- package/ai/skills/stacks-realtime/SKILL.md +442 -0
- package/ai/skills/stacks-redesign/SKILL.md +182 -0
- package/ai/skills/stacks-registry/SKILL.md +58 -0
- package/ai/skills/stacks-repl/SKILL.md +80 -0
- package/ai/skills/stacks-retro/SKILL.md +122 -0
- package/ai/skills/stacks-review/SKILL.md +135 -0
- package/ai/skills/stacks-router/SKILL.md +231 -0
- package/ai/skills/stacks-routes/SKILL.md +105 -0
- package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
- package/ai/skills/stacks-scheduler/SKILL.md +363 -0
- package/ai/skills/stacks-search-engine/SKILL.md +112 -0
- package/ai/skills/stacks-security/SKILL.md +129 -0
- package/ai/skills/stacks-security-audit/SKILL.md +128 -0
- package/ai/skills/stacks-server/SKILL.md +458 -0
- package/ai/skills/stacks-shell/SKILL.md +62 -0
- package/ai/skills/stacks-slug/SKILL.md +87 -0
- package/ai/skills/stacks-sms/SKILL.md +385 -0
- package/ai/skills/stacks-socials/SKILL.md +303 -0
- package/ai/skills/stacks-storage/SKILL.md +576 -0
- package/ai/skills/stacks-strings/SKILL.md +416 -0
- package/ai/skills/stacks-stx/SKILL.md +176 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
- package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
- package/ai/skills/stacks-testing/SKILL.md +129 -0
- package/ai/skills/stacks-tunnel/SKILL.md +125 -0
- package/ai/skills/stacks-types/SKILL.md +178 -0
- package/ai/skills/stacks-ui/SKILL.md +246 -0
- package/ai/skills/stacks-utils/SKILL.md +444 -0
- package/ai/skills/stacks-validation/SKILL.md +371 -0
- package/ai/skills/stacks-whois/SKILL.md +315 -0
- package/assets/world-map-data.ts +121 -0
- package/bootstrap.ts +114 -0
- package/docs/api-examples.md +46 -0
- package/docs/index.md +24 -0
- package/docs/markdown-examples.md +87 -0
- package/functions/analytics.ts +97 -0
- package/functions/api.ts +143 -0
- package/functions/auth.ts +208 -0
- package/functions/billing/payments.ts +149 -0
- package/functions/blog.ts +120 -0
- package/functions/cms/categorizables.ts +107 -0
- package/functions/cms/pages.ts +108 -0
- package/functions/cms/posts.ts +100 -0
- package/functions/cms/taggables.ts +102 -0
- package/functions/commerce/coupons.ts +128 -0
- package/functions/commerce/customers.ts +128 -0
- package/functions/commerce/gift-cards.ts +119 -0
- package/functions/commerce/orders.ts +112 -0
- package/functions/commerce/payments.ts +119 -0
- package/functions/commerce/products/categories.ts +123 -0
- package/functions/commerce/products/manufacturers.ts +123 -0
- package/functions/commerce/products/products.ts +123 -0
- package/functions/commerce/products/reviews.ts +123 -0
- package/functions/commerce/products/units.ts +123 -0
- package/functions/commerce/products/variants.ts +123 -0
- package/functions/commerce/shippings/delivery-routes.ts +123 -0
- package/functions/commerce/shippings/digital-deliveries.ts +119 -0
- package/functions/commerce/shippings/drivers.ts +123 -0
- package/functions/commerce/shippings/license-keys.ts +123 -0
- package/functions/commerce/shippings/shipping-methods.ts +123 -0
- package/functions/commerce/shippings/shipping-rates.ts +119 -0
- package/functions/commerce/shippings/shipping-zones.ts +119 -0
- package/functions/commerce/taxes.ts +119 -0
- package/functions/commerce/waitlist/products.ts +123 -0
- package/functions/commerce/waitlist/restaurants.ts +123 -0
- package/functions/craft.ts +107 -0
- package/functions/dashboard.ts +153 -0
- package/functions/inbox.ts +95 -0
- package/functions/jobs.ts +86 -0
- package/functions/monitoring/errors.ts +295 -0
- package/functions/queue.ts +111 -0
- package/functions/realtime/websockets.ts +96 -0
- package/functions/realtime.ts +52 -0
- package/functions/releases.ts +49 -0
- package/functions/search.ts +126 -0
- package/functions/toasts.ts +95 -0
- package/functions/uploads.ts +216 -0
- package/ide/.gitignore +4 -0
- package/ide/cursor/rules/auto-imports.mdc +574 -0
- package/ide/cursor/rules/code-style.mdc +12 -0
- package/ide/cursor/rules/documentation.mdc +9 -0
- package/ide/cursor/rules/error-handling-validation.mdc +11 -0
- package/ide/cursor/rules/key-conventions.mdc +11 -0
- package/ide/cursor/rules/project-structure.mdc +169 -0
- package/ide/cursor/rules/readme.mdc +318 -0
- package/ide/cursor/rules/syntax-formatting.mdc +9 -0
- package/ide/cursor/rules/testing.mdc +10 -0
- package/ide/cursor/rules/typescript.mdc +9 -0
- package/ide/cursor/rules/ui-styling.mdc +0 -0
- package/ide/dictionary.txt +397 -0
- package/ide/jetbrains/.fleet/settings.json +4 -0
- package/ide/jetbrains/.idea/.gitignore +5 -0
- package/ide/jetbrains/.idea/.name +1 -0
- package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
- package/ide/jetbrains/.idea/modules.xml +8 -0
- package/ide/jetbrains/.idea/stacks.iml +22 -0
- package/ide/jetbrains/.idea/vcs.xml +6 -0
- package/ide/jetbrains/.idea/webResources.xml +14 -0
- package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
- package/ide/jetbrains/installed.txt +4 -0
- package/ide/jetbrains/options/github-copilot.xml +5 -0
- package/ide/jetbrains/options/nodejs.xml +7 -0
- package/ide/jetbrains/options/templates.xml +124 -0
- package/ide/jetbrains/templates/JavaScript.xml +6 -0
- package/ide/jetbrains/templates/Stacks.xml +8 -0
- package/ide/vscode/.vscode/README.md +23 -0
- package/ide/vscode/.vscode/extensions.json +5 -0
- package/ide/vscode/.vscode/settings.json +176 -0
- package/ide/vscode/.vscode/stacks.code-snippets +181 -0
- package/ide/vscode/LICENSE.md +21 -0
- package/ide/vscode/README.md +36 -0
- package/ide/vscode/logo.png +0 -0
- package/ide/vscode/package.json +79 -0
- package/ide/zed/.zed/settings.json +139 -0
- package/lang/README.md +28 -0
- package/lang/de.yml +14 -0
- package/lang/en.yml +15 -0
- package/package.json +19 -3
- package/project/bootstrap +161 -0
- package/project/buddy +175 -0
- package/project/storage/framework/tsconfig.app.json +50 -0
- package/project/storage/framework/tsconfig.base.json +58 -0
- package/routes/core.ts +78 -0
- package/routes/dashboard-api.ts +233 -0
- package/routes/dashboard.ts +739 -0
- package/routes/email.ts +129 -0
- package/stores/[name].stx +16 -0
- package/stores/git.ts +71 -0
- package/stores/payment.ts +474 -0
- package/stores/queue.ts +43 -0
- package/stores/user.ts +29 -0
- package/stx-components-plugin.ts +25 -0
- package/styles/design-tokens.css +597 -0
- package/types/dashboard.ts +70 -0
- package/types/defaults.ts +438 -0
- package/vcs/github/CODE_OF_CONDUCT.md +83 -0
- package/vcs/github/CONTRIBUTING.md +187 -0
- package/vcs/github/EXPLANATIONS.md +41 -0
- package/vcs/github/FUNDING.yml +1 -0
- package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
- package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
- package/vcs/github/SECURITY.md +70 -0
- package/vcs/github/renovate.json +5 -0
- package/vcs/github/stale.yml +14 -0
- package/vcs/github/workflows/README.md +16 -0
- package/vcs/github/workflows/ci.yml +114 -0
- package/vcs/github/workflows/export-size.yml +25 -0
- package/vcs/github/workflows/labeler.yml +13 -0
- package/vcs/github/workflows/release.yml +219 -0
- package/views/[...all].stx +8 -0
- package/views/about.md +3 -0
- package/views/auth/login.stx +1 -0
- package/views/auth/signup.stx +85 -0
- package/views/dashboard/.discovered-models.json +542 -0
- package/views/dashboard/AUDIT.md +904 -0
- package/views/dashboard/[...all].stx +89 -0
- package/views/dashboard/access-tokens/index.stx +177 -0
- package/views/dashboard/actions/index.stx +129 -0
- package/views/dashboard/analytics/blog/index.stx +356 -0
- package/views/dashboard/analytics/browsers/index.stx +263 -0
- package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
- package/views/dashboard/analytics/commerce/web/index.stx +379 -0
- package/views/dashboard/analytics/countries/index.stx +289 -0
- package/views/dashboard/analytics/devices/index.stx +305 -0
- package/views/dashboard/analytics/events/index.stx +536 -0
- package/views/dashboard/analytics/index.stx +88 -0
- package/views/dashboard/analytics/marketing/index.stx +315 -0
- package/views/dashboard/analytics/pages/index.stx +271 -0
- package/views/dashboard/analytics/referrers/index.stx +319 -0
- package/views/dashboard/analytics/web/index.stx +356 -0
- package/views/dashboard/buddy/index.stx +198 -0
- package/views/dashboard/ci/index.stx +462 -0
- package/views/dashboard/cloud/index.stx +452 -0
- package/views/dashboard/commands/index.stx +295 -0
- package/views/dashboard/commerce/categories/index.stx +406 -0
- package/views/dashboard/commerce/coupons/index.stx +485 -0
- package/views/dashboard/commerce/customers/index.stx +410 -0
- package/views/dashboard/commerce/dashboard/index.stx +287 -0
- package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
- package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
- package/views/dashboard/commerce/delivery/drivers.stx +397 -0
- package/views/dashboard/commerce/delivery/index.stx +18 -0
- package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
- package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
- package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
- package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
- package/views/dashboard/commerce/delivery/tabs.ts +25 -0
- package/views/dashboard/commerce/gift-cards/index.stx +561 -0
- package/views/dashboard/commerce/manufacturers/index.stx +308 -0
- package/views/dashboard/commerce/orders/index.stx +247 -0
- package/views/dashboard/commerce/payments/index.stx +535 -0
- package/views/dashboard/commerce/pos/index.stx +653 -0
- package/views/dashboard/commerce/printers/devices/index.stx +273 -0
- package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
- package/views/dashboard/commerce/products/detail.stx +563 -0
- package/views/dashboard/commerce/products/index.stx +370 -0
- package/views/dashboard/commerce/reviews/index.stx +656 -0
- package/views/dashboard/commerce/reviews/index.ts +0 -0
- package/views/dashboard/commerce/taxes/index.stx +377 -0
- package/views/dashboard/commerce/units/index.stx +317 -0
- package/views/dashboard/commerce/variants/index.stx +319 -0
- package/views/dashboard/commerce/waitlist/products.stx +942 -0
- package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
- package/views/dashboard/components/index.stx +330 -0
- package/views/dashboard/composables/index.ts +19 -0
- package/views/dashboard/composables/useChart.ts +77 -0
- package/views/dashboard/composables/useNavigation.ts +99 -0
- package/views/dashboard/composables/useRole.ts +58 -0
- package/views/dashboard/composables/useTheme.ts +74 -0
- package/views/dashboard/content/authors/index.stx +171 -0
- package/views/dashboard/content/blog/index.stx +493 -0
- package/views/dashboard/content/categories/index.stx +169 -0
- package/views/dashboard/content/comments/index.stx +189 -0
- package/views/dashboard/content/dashboard.stx +383 -0
- package/views/dashboard/content/files/index.stx +706 -0
- package/views/dashboard/content/pages/index.stx +174 -0
- package/views/dashboard/content/posts/index.stx +269 -0
- package/views/dashboard/content/seo/index.stx +240 -0
- package/views/dashboard/content/tags/index.stx +167 -0
- package/views/dashboard/data/activity/index.stx +261 -0
- package/views/dashboard/data/dashboard/index.stx +116 -0
- package/views/dashboard/data/subscribers.stx +270 -0
- package/views/dashboard/data/teams.stx +275 -0
- package/views/dashboard/data/users.stx +279 -0
- package/views/dashboard/dependencies/index.stx +178 -0
- package/views/dashboard/deployments/[id].stx +234 -0
- package/views/dashboard/deployments/index.stx +9 -0
- package/views/dashboard/dns/index.stx +163 -0
- package/views/dashboard/environment/index.stx +283 -0
- package/views/dashboard/errors/index.stx +274 -0
- package/views/dashboard/forgot-password.stx +57 -0
- package/views/dashboard/functions/index.stx +159 -0
- package/views/dashboard/health/index.stx +248 -0
- package/views/dashboard/inbox/activity.stx +350 -0
- package/views/dashboard/inbox/index.stx +452 -0
- package/views/dashboard/inbox/settings.stx +290 -0
- package/views/dashboard/index.stx +252 -0
- package/views/dashboard/insights/index.stx +529 -0
- package/views/dashboard/jobs/[id].stx +430 -0
- package/views/dashboard/jobs/history.stx +222 -0
- package/views/dashboard/jobs/index.stx +214 -0
- package/views/dashboard/kanban/[id].stx +899 -0
- package/views/dashboard/kanban/index.stx +260 -0
- package/views/dashboard/layouts/default.stx +565 -0
- package/views/dashboard/layouts/guest.stx +10 -0
- package/views/dashboard/login.stx +48 -0
- package/views/dashboard/logs/index.stx +540 -0
- package/views/dashboard/mailboxes/index.stx +149 -0
- package/views/dashboard/management/permissions/index.stx +490 -0
- package/views/dashboard/marketing/campaigns/index.stx +604 -0
- package/views/dashboard/marketing/lists/index.stx +514 -0
- package/views/dashboard/marketing/reviews/index.stx +475 -0
- package/views/dashboard/marketing/social-posts/index.stx +560 -0
- package/views/dashboard/models/[model].stx +195 -0
- package/views/dashboard/models/index.stx +159 -0
- package/views/dashboard/monitoring/errors/index.stx +415 -0
- package/views/dashboard/notifications/dashboard.stx +383 -0
- package/views/dashboard/notifications/email.stx +336 -0
- package/views/dashboard/notifications/history.stx +373 -0
- package/views/dashboard/notifications/sms.stx +332 -0
- package/views/dashboard/packages/index.stx +195 -0
- package/views/dashboard/queries/[id].stx +381 -0
- package/views/dashboard/queries/history.stx +227 -0
- package/views/dashboard/queries/index.stx +335 -0
- package/views/dashboard/queries/slow.stx +318 -0
- package/views/dashboard/queue/index.stx +427 -0
- package/views/dashboard/realtime/index.stx +429 -0
- package/views/dashboard/register.stx +46 -0
- package/views/dashboard/releases/index.stx +389 -0
- package/views/dashboard/requests/index.stx +462 -0
- package/views/dashboard/serverless/index.stx +475 -0
- package/views/dashboard/servers/[id].stx +299 -0
- package/views/dashboard/servers/index.stx +483 -0
- package/views/dashboard/settings/billing.stx +50 -0
- package/views/dashboard/settings/index.stx +256 -0
- package/views/dashboard/settings/mail.stx +252 -0
- package/views/dashboard/stores/auth.ts +123 -0
- package/views/dashboard/stores/ci.ts +342 -0
- package/views/dashboard/stores/index.ts +129 -0
- package/views/dashboard/stores/kanban.ts +723 -0
- package/views/dashboard/stores/rbac.ts +350 -0
- package/views/dashboard/stx.config.ts +34 -0
- package/views/dashboard/teams/[id].stx +170 -0
- package/views/forum/category.stx +92 -0
- package/views/forum/index.stx +209 -0
- package/views/forum/new-topic.stx +113 -0
- package/views/forum/profile.stx +1 -0
- package/views/forum/topic.stx +143 -0
- package/views/hello-world/[name].stx +47 -0
- package/views/index.stx +21 -0
- 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
|
+
})
|