create-ailk 0.6.1 → 0.8.0
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/README.md +87 -14
- package/component-catalog.json +735 -95
- package/component-catalog.md +13 -7
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +89 -29
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +56 -1
- package/dist/lib/extract-module-bundle.js +170 -14
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +34 -3
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +85 -10
- package/dist/parse-args.js +227 -15
- package/dist/programmatic.d.ts +81 -5
- package/dist/programmatic.js +291 -32
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +499 -0
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +204 -24
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +94 -19
- package/dist/template-manifest.js +170 -289
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +167 -0
- package/site-templates/marketing-site/DESIGN.md +59 -40
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +59 -40
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/DESIGN.md +54 -40
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/saas-product/DESIGN.md +59 -40
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +10 -9
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +115 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +8 -2
- package/templates/DESIGN.md +56 -44
- package/templates/README.md +116 -46
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
- package/templates/apps/api/src/config/index.ts +46 -14
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +13 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +146 -52
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +82 -18
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +1 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +148 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +78 -37
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/content/_site.mdx +25 -0
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +126 -0
- package/templates/content/en/ContactPage/contact.mdx +21 -92
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +268 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/site-templates/simple-service/DESIGN.md +0 -148
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
- package/site-templates/simple-service/ailk-template.json +0 -10
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* upgrade-plan — the helper behind the `/upgrade-plan` skill.
|
|
3
|
+
*
|
|
4
|
+
* Reads the paid options' prices from ONE public surface — the schema.org
|
|
5
|
+
* `Offer` JSON-LD on https://ailaunchkit.ai/pricing — at run time, reads this
|
|
6
|
+
* site's own modules / template / shape / design from files the scaffold
|
|
7
|
+
* already carries, and recommends exactly one option from a price-free rule
|
|
8
|
+
* table (levels per the AI Launch Kit product vocabulary). It holds no price of
|
|
9
|
+
* its own, caches nothing, and writes nothing.
|
|
10
|
+
*
|
|
11
|
+
* Run from the project root:
|
|
12
|
+
*
|
|
13
|
+
* pnpm exec tsx .claude/skills/upgrade-plan/scripts/upgrade-plan.ts [--need X] [--url U | --html-file F] [--root R]
|
|
14
|
+
*
|
|
15
|
+
* Prints one JSON object. Exit 0 whether the page was priced, unpriced or
|
|
16
|
+
* unreachable (each is a reported state); exit 2 on a usage error.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
export const PRICING_URL = "https://ailaunchkit.ai/pricing";
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
23
|
+
|
|
24
|
+
export type OptionId = "free" | "pro" | "pro-plus" | "agency" | "commerce" | "saas" | "saas-usage";
|
|
25
|
+
|
|
26
|
+
export interface OptionInfo {
|
|
27
|
+
id: OptionId;
|
|
28
|
+
name: string;
|
|
29
|
+
adds: string;
|
|
30
|
+
requires: string;
|
|
31
|
+
/** Normalized names (see `normalizeName`) an `Offer` may carry for this option. */
|
|
32
|
+
aliases: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The seven options, always listed, in this order. Names and structure only — no figure. */
|
|
36
|
+
export const OPTIONS: readonly OptionInfo[] = [
|
|
37
|
+
{
|
|
38
|
+
id: "free",
|
|
39
|
+
name: "Free",
|
|
40
|
+
adds: "The open-source base: the website, its API and MCP server, and the marketing module.",
|
|
41
|
+
requires: "Nothing — no licence key.",
|
|
42
|
+
aliases: ["free", "oss"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "pro",
|
|
46
|
+
name: "Pro",
|
|
47
|
+
adds: "The paid modules for one site — docs and blog, scheduling, signal collectors and the Admin app — kept current.",
|
|
48
|
+
requires: "A licence key for the site.",
|
|
49
|
+
aliases: ["pro"],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "pro-plus",
|
|
53
|
+
name: "Pro Plus",
|
|
54
|
+
adds: "Everything in Pro, plus the multi-tenant module (organizations, workspaces, members).",
|
|
55
|
+
requires: "A licence key for the site.",
|
|
56
|
+
aliases: ["pro plus"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "agency",
|
|
60
|
+
name: "Agency",
|
|
61
|
+
adds: "A reseller agreement for building sites for clients — not a bigger level.",
|
|
62
|
+
requires: "Each client site is then licensed at Pro or Pro Plus.",
|
|
63
|
+
aliases: ["agency"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "commerce",
|
|
67
|
+
name: "Commerce",
|
|
68
|
+
adds: "The commerce add-on: checkout, payment links, hosted invoices, subscriptions and the customer portal, through your own Stripe account.",
|
|
69
|
+
requires: "Any paid level (Pro or Pro Plus).",
|
|
70
|
+
aliases: ["commerce", "commerce add-on"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "saas",
|
|
74
|
+
name: "SaaS",
|
|
75
|
+
adds: "Commerce plus plans, trials, coupons and entitlements mapped to feature flags — for a product that bills its tenants.",
|
|
76
|
+
requires: "Pro Plus.",
|
|
77
|
+
aliases: ["saas"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "saas-usage",
|
|
81
|
+
name: "SaaS with Usage",
|
|
82
|
+
adds: "SaaS plus usage metering, prepaid credits, overage and spend limits.",
|
|
83
|
+
requires: "Pro Plus.",
|
|
84
|
+
aliases: ["saas with usage", "saas-usage"],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export const NEEDS = ["agency", "sell", "saas", "usage", "docs", "scheduling", "multi-tenant"] as const;
|
|
89
|
+
export type Need = (typeof NEEDS)[number];
|
|
90
|
+
|
|
91
|
+
export interface Offer {
|
|
92
|
+
name: string;
|
|
93
|
+
price?: string;
|
|
94
|
+
priceCurrency?: string;
|
|
95
|
+
unitText?: string;
|
|
96
|
+
url?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface Price {
|
|
100
|
+
price: string;
|
|
101
|
+
priceCurrency?: string;
|
|
102
|
+
unitText?: string;
|
|
103
|
+
url?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Site {
|
|
107
|
+
modules: string[];
|
|
108
|
+
template: string | null;
|
|
109
|
+
shape: string | null;
|
|
110
|
+
design: string | null;
|
|
111
|
+
sellsSomething: boolean;
|
|
112
|
+
commerceInstalled: boolean;
|
|
113
|
+
licenseKeySet: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface Recommendation {
|
|
117
|
+
rule: "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7";
|
|
118
|
+
option: string;
|
|
119
|
+
parts: OptionId[];
|
|
120
|
+
reason: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PlanOption {
|
|
124
|
+
id: OptionId;
|
|
125
|
+
name: string;
|
|
126
|
+
adds: string;
|
|
127
|
+
requires: string;
|
|
128
|
+
prices: Price[];
|
|
129
|
+
status: "published" | "not published on the pricing page";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface Plan {
|
|
133
|
+
pricing: {
|
|
134
|
+
status: "ok" | "unreachable" | "unpublished";
|
|
135
|
+
/** The link shown to the human — always the public pricing page. */
|
|
136
|
+
url: string;
|
|
137
|
+
/** The URL actually fetched (differs only under `--url`). */
|
|
138
|
+
fetchedUrl: string;
|
|
139
|
+
reason?: string;
|
|
140
|
+
};
|
|
141
|
+
options: PlanOption[];
|
|
142
|
+
alsoPublished: Offer[];
|
|
143
|
+
site: Site;
|
|
144
|
+
recommendation: Recommendation & { link: string };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Offers ────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
const LD_JSON_BLOCK = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
150
|
+
|
|
151
|
+
function str(v: unknown): string | undefined {
|
|
152
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
153
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isOffer(node: Record<string, unknown>): boolean {
|
|
158
|
+
const t = node["@type"];
|
|
159
|
+
return t === "Offer" || (Array.isArray(t) && t.includes("Offer"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toOffer(node: Record<string, unknown>, parentName: string | undefined): Offer | null {
|
|
163
|
+
const name = str(node.name) ?? parentName;
|
|
164
|
+
if (name === undefined) return null;
|
|
165
|
+
const specs = Array.isArray(node.priceSpecification) ? node.priceSpecification : [node.priceSpecification];
|
|
166
|
+
const spec = specs.find((s): s is Record<string, unknown> => typeof s === "object" && s !== null);
|
|
167
|
+
const offer: Offer = { name };
|
|
168
|
+
const price = str(node.price) ?? str(spec?.price);
|
|
169
|
+
const priceCurrency = str(node.priceCurrency) ?? str(spec?.priceCurrency);
|
|
170
|
+
const unitText = str(spec?.unitText);
|
|
171
|
+
const url = str(node.url);
|
|
172
|
+
if (price !== undefined) offer.price = price;
|
|
173
|
+
if (priceCurrency !== undefined) offer.priceCurrency = priceCurrency;
|
|
174
|
+
if (unitText !== undefined) offer.unitText = unitText;
|
|
175
|
+
if (url !== undefined) offer.url = url;
|
|
176
|
+
return offer;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectOffers(node: unknown, parentName: string | undefined, out: Offer[]): void {
|
|
180
|
+
if (Array.isArray(node)) {
|
|
181
|
+
for (const item of node) collectOffers(item, parentName, out);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (typeof node !== "object" || node === null) return;
|
|
185
|
+
const obj = node as Record<string, unknown>;
|
|
186
|
+
if (isOffer(obj)) {
|
|
187
|
+
const offer = toOffer(obj, parentName);
|
|
188
|
+
if (offer !== null) out.push(offer);
|
|
189
|
+
// An offer may carry others (an `addOn`, an `itemOffered` chain) — walk them too.
|
|
190
|
+
for (const value of Object.values(obj)) collectOffers(value, offer?.name ?? parentName, out);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const ownName = str(obj.name) ?? parentName;
|
|
194
|
+
for (const value of Object.values(obj)) collectOffers(value, ownName, out);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Every `Offer` node in the page's `application/ld+json` blocks, in document order. */
|
|
198
|
+
export function parseOffers(html: string): Offer[] {
|
|
199
|
+
const out: Offer[] = [];
|
|
200
|
+
for (const match of html.matchAll(LD_JSON_BLOCK)) {
|
|
201
|
+
let data: unknown;
|
|
202
|
+
try {
|
|
203
|
+
data = JSON.parse(match[1] ?? "");
|
|
204
|
+
} catch {
|
|
205
|
+
continue; // a block that fails to parse is skipped, not fatal
|
|
206
|
+
}
|
|
207
|
+
collectOffers(data, undefined, out);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Lower-case, drop a parenthetical and a product prefix, collapse whitespace. */
|
|
213
|
+
export function normalizeName(name: string): string {
|
|
214
|
+
return name
|
|
215
|
+
.toLowerCase()
|
|
216
|
+
.replace(/\([^)]*\)/g, " ")
|
|
217
|
+
.replace(/\s+/g, " ")
|
|
218
|
+
.trim()
|
|
219
|
+
.replace(/^(ailk|ai launch kit) /, "");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function matchOption(offer: Offer): OptionInfo | undefined {
|
|
223
|
+
const n = normalizeName(offer.name);
|
|
224
|
+
return OPTIONS.find((o) => o.aliases.includes(n));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── The site ──────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
function readText(path: string): string | null {
|
|
230
|
+
try {
|
|
231
|
+
return readFileSync(path, "utf8");
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readJson(path: string): Record<string, unknown> | null {
|
|
238
|
+
const text = readText(path);
|
|
239
|
+
if (text === null) return null;
|
|
240
|
+
try {
|
|
241
|
+
const data: unknown = JSON.parse(text);
|
|
242
|
+
return typeof data === "object" && data !== null ? (data as Record<string, unknown>) : null;
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function listMdx(dir: string): string[] {
|
|
249
|
+
if (!existsSync(dir)) return [];
|
|
250
|
+
const out: string[] = [];
|
|
251
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
252
|
+
const full = join(dir, entry.name);
|
|
253
|
+
if (entry.isDirectory()) out.push(...listMdx(full));
|
|
254
|
+
else if (entry.isFile() && entry.name.endsWith(".mdx")) out.push(full);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** True when the file's frontmatter carries a TOP-LEVEL `price:` that is a real figure. */
|
|
260
|
+
function hasTopLevelPrice(mdx: string): boolean {
|
|
261
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdx);
|
|
262
|
+
if (fm === null) return false;
|
|
263
|
+
const line = /^price:[ \t]*(.*)$/m.exec(fm[1] ?? "");
|
|
264
|
+
if (line === null) return false;
|
|
265
|
+
const value = (line[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
266
|
+
return !value.includes("{") && /^\d+(\.\d+)?$/.test(value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** What this scaffold records about itself. Each field from its own source; nothing inferred. */
|
|
270
|
+
export function readSite(root: string, env: Record<string, string | undefined> = process.env): Site {
|
|
271
|
+
const arch = readText(join(root, "architecture.yaml"));
|
|
272
|
+
const modulesLine = arch === null ? null : /^# Modules:[ \t]*(.*)$/m.exec(arch);
|
|
273
|
+
const modules =
|
|
274
|
+
modulesLine === null
|
|
275
|
+
? []
|
|
276
|
+
: (modulesLine[1] ?? "")
|
|
277
|
+
.split(",")
|
|
278
|
+
.map((m) => m.trim())
|
|
279
|
+
.filter((m) => m.length > 0);
|
|
280
|
+
|
|
281
|
+
let template: string | null = null;
|
|
282
|
+
const provenance = readJson(join(root, ".ailk-template.json"));
|
|
283
|
+
if (provenance !== null) {
|
|
284
|
+
template = str(provenance.template) ?? null;
|
|
285
|
+
} else {
|
|
286
|
+
const design = readText(join(root, "DESIGN.md"));
|
|
287
|
+
const source = design === null ? null : /Source template:[ \t]*(\S+)/.exec(design);
|
|
288
|
+
const value = source?.[1];
|
|
289
|
+
if (value !== undefined && !value.startsWith("_base")) template = value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The scaffold's roster of record (#6658 D7); a scaffold from before it
|
|
293
|
+
// carries the same JSON at its root under the template's own file name.
|
|
294
|
+
const manifest =
|
|
295
|
+
readJson(join(root, ".ailk-site-manifest.json")) ?? readJson(join(root, "TEMPLATE-MANIFEST.json"));
|
|
296
|
+
const shape = str(manifest?.shape) ?? null;
|
|
297
|
+
const designName = str(manifest?.design) ?? null;
|
|
298
|
+
|
|
299
|
+
const sellsSomething = listMdx(join(root, "content")).some((f) => hasTopLevelPrice(readText(f) ?? ""));
|
|
300
|
+
const commerceInstalled = existsSync(join(root, ".claude", "skills", "scaffold-commerce"));
|
|
301
|
+
// Presence only: the value is reduced to a boolean here and never kept.
|
|
302
|
+
const key = env.AILK_LICENSE_KEY;
|
|
303
|
+
const licenseKeySet = typeof key === "string" && key.length > 0;
|
|
304
|
+
|
|
305
|
+
return { modules, template, shape, design: designName, sellsSomething, commerceInstalled, licenseKeySet };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── The recommendation ────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
const PRO_MODULES = ["docs-blog", "scheduling", "signal-collectors", "workspace"];
|
|
311
|
+
|
|
312
|
+
/** One recommendation from a price-free rule table; the first matching rule wins. */
|
|
313
|
+
export function recommend(site: Site, need?: Need): Recommendation {
|
|
314
|
+
const has = (m: string): boolean => site.modules.includes(m);
|
|
315
|
+
const multiTenant = has("multi-tenant") || need === "multi-tenant";
|
|
316
|
+
|
|
317
|
+
if (need === "agency") {
|
|
318
|
+
return {
|
|
319
|
+
rule: "R1",
|
|
320
|
+
option: "Agency",
|
|
321
|
+
parts: ["agency"],
|
|
322
|
+
reason: "You build sites for clients; each client site is then Pro or Pro Plus.",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (need === "usage") {
|
|
326
|
+
return {
|
|
327
|
+
rule: "R2",
|
|
328
|
+
option: "Pro Plus + SaaS with Usage",
|
|
329
|
+
parts: ["pro-plus", "saas-usage"],
|
|
330
|
+
reason: "You want to meter your own product's usage; SaaS with Usage requires Pro Plus.",
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (need === "saas") {
|
|
334
|
+
return {
|
|
335
|
+
rule: "R3",
|
|
336
|
+
option: "Pro Plus + SaaS",
|
|
337
|
+
parts: ["pro-plus", "saas"],
|
|
338
|
+
reason: "You want to bill your own tenants on plans; SaaS requires Pro Plus.",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (multiTenant && !sells(site, need)) {
|
|
342
|
+
return {
|
|
343
|
+
rule: "R4",
|
|
344
|
+
option: "Pro Plus",
|
|
345
|
+
parts: ["pro-plus"],
|
|
346
|
+
reason: has("multi-tenant")
|
|
347
|
+
? "This site uses the multi-tenant module, which needs Pro Plus."
|
|
348
|
+
: "You need multi-tenant (organizations and workspaces), which needs Pro Plus.",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (sells(site, need)) {
|
|
352
|
+
const why = has("commerce")
|
|
353
|
+
? "This site uses the commerce module"
|
|
354
|
+
: site.commerceInstalled
|
|
355
|
+
? "This site has the commerce skill installed"
|
|
356
|
+
: site.sellsSomething
|
|
357
|
+
? "This site already has priced pages but no commerce module"
|
|
358
|
+
: "You want to take payments on this site";
|
|
359
|
+
return multiTenant
|
|
360
|
+
? {
|
|
361
|
+
rule: "R5",
|
|
362
|
+
option: "Pro Plus + Commerce",
|
|
363
|
+
parts: ["pro-plus", "commerce"],
|
|
364
|
+
reason: `${why}; the Commerce add-on sits on a paid level, and multi-tenant needs Pro Plus.`,
|
|
365
|
+
}
|
|
366
|
+
: {
|
|
367
|
+
rule: "R5",
|
|
368
|
+
option: "Pro + Commerce",
|
|
369
|
+
parts: ["pro", "commerce"],
|
|
370
|
+
reason: `${why}; the Commerce add-on sits on a paid level, and Pro is the first one.`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const proModules = site.modules.filter((m) => PRO_MODULES.includes(m));
|
|
374
|
+
if (proModules.length > 0 || need === "docs" || need === "scheduling") {
|
|
375
|
+
const named = proModules.length > 0 ? proModules.join(", ") : need === "docs" ? "docs-blog" : "scheduling";
|
|
376
|
+
return {
|
|
377
|
+
rule: "R6",
|
|
378
|
+
option: "Pro",
|
|
379
|
+
parts: ["pro"],
|
|
380
|
+
reason: `${proModules.length > 0 ? "This site uses" : "You want"} ${named}, which Pro covers.`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
rule: "R7",
|
|
385
|
+
option: "Free",
|
|
386
|
+
parts: ["free"],
|
|
387
|
+
reason:
|
|
388
|
+
"This site is marketing only with nothing priced, so what it uses today is covered by Free; Pro adds a kept-current foundation and the paid modules.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sells(site: Site, need?: Need): boolean {
|
|
393
|
+
return site.modules.includes("commerce") || site.commerceInstalled || need === "sell" || site.sellsSomething;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── The plan ──────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
async function fetchPage(
|
|
399
|
+
url: string,
|
|
400
|
+
fetchImpl: typeof fetch,
|
|
401
|
+
timeoutMs: number,
|
|
402
|
+
): Promise<{ html: string } | { error: string }> {
|
|
403
|
+
const controller = new AbortController();
|
|
404
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
405
|
+
const timeout = new Promise<{ error: string }>((resolve) => {
|
|
406
|
+
timer = setTimeout(() => {
|
|
407
|
+
controller.abort();
|
|
408
|
+
resolve({ error: `timed out after ${timeoutMs} ms` });
|
|
409
|
+
}, timeoutMs);
|
|
410
|
+
});
|
|
411
|
+
const attempt = (async (): Promise<{ html: string } | { error: string }> => {
|
|
412
|
+
try {
|
|
413
|
+
const res = await fetchImpl(url, { signal: controller.signal, redirect: "follow" });
|
|
414
|
+
if (!res.ok) return { error: `HTTP status ${res.status}` };
|
|
415
|
+
return { html: await res.text() };
|
|
416
|
+
} catch (err) {
|
|
417
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
418
|
+
}
|
|
419
|
+
})();
|
|
420
|
+
try {
|
|
421
|
+
return await Promise.race([attempt, timeout]);
|
|
422
|
+
} finally {
|
|
423
|
+
clearTimeout(timer);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function buildPlan(opts: {
|
|
428
|
+
root: string;
|
|
429
|
+
need?: Need;
|
|
430
|
+
url?: string;
|
|
431
|
+
html?: string;
|
|
432
|
+
fetch?: typeof fetch;
|
|
433
|
+
timeoutMs?: number;
|
|
434
|
+
}): Promise<Plan> {
|
|
435
|
+
const fetchedUrl = opts.url ?? PRICING_URL;
|
|
436
|
+
let html: string | undefined = opts.html;
|
|
437
|
+
let reason: string | undefined;
|
|
438
|
+
if (html === undefined) {
|
|
439
|
+
const page = await fetchPage(fetchedUrl, opts.fetch ?? fetch, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
440
|
+
if ("html" in page) html = page.html;
|
|
441
|
+
else reason = page.error;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const offers = html === undefined ? [] : parseOffers(html);
|
|
445
|
+
const prices = new Map<OptionId, Price[]>(OPTIONS.map((o) => [o.id, []]));
|
|
446
|
+
const alsoPublished: Offer[] = [];
|
|
447
|
+
for (const offer of offers) {
|
|
448
|
+
const option = matchOption(offer);
|
|
449
|
+
if (option === undefined || offer.price === undefined) {
|
|
450
|
+
if (option === undefined) alsoPublished.push(offer);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const { name: _name, ...price } = offer;
|
|
454
|
+
prices.get(option.id)?.push({ ...price, price: offer.price });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const options: PlanOption[] = OPTIONS.map((o) => {
|
|
458
|
+
const p = prices.get(o.id) ?? [];
|
|
459
|
+
return {
|
|
460
|
+
id: o.id,
|
|
461
|
+
name: o.name,
|
|
462
|
+
adds: o.adds,
|
|
463
|
+
requires: o.requires,
|
|
464
|
+
prices: p,
|
|
465
|
+
status: p.length > 0 ? "published" : "not published on the pricing page",
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const site = readSite(opts.root);
|
|
470
|
+
const rec = recommend(site, opts.need);
|
|
471
|
+
const link = prices.get(rec.parts[0] ?? "free")?.find((p) => p.url !== undefined)?.url ?? PRICING_URL;
|
|
472
|
+
|
|
473
|
+
const priced = options.some((o) => o.prices.length > 0) || alsoPublished.some((o) => o.price !== undefined);
|
|
474
|
+
const status = html === undefined ? "unreachable" : priced ? "ok" : "unpublished";
|
|
475
|
+
return {
|
|
476
|
+
pricing: { status, url: PRICING_URL, fetchedUrl, ...(reason !== undefined ? { reason } : {}) },
|
|
477
|
+
options,
|
|
478
|
+
alsoPublished,
|
|
479
|
+
site,
|
|
480
|
+
recommendation: { ...rec, link },
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── CLI ───────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
const USAGE =
|
|
487
|
+
"usage: upgrade-plan.ts [--need agency|sell|saas|usage|docs|scheduling|multi-tenant] [--url U | --html-file F] [--root R]";
|
|
488
|
+
|
|
489
|
+
function usageError(message: string): never {
|
|
490
|
+
process.stderr.write(`${message}\n${USAGE}\n`);
|
|
491
|
+
process.exit(2);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function main(argv: string[]): Promise<void> {
|
|
495
|
+
const opts: { root: string; need?: Need; url?: string; html?: string } = { root: process.cwd() };
|
|
496
|
+
for (let i = 0; i < argv.length; i++) {
|
|
497
|
+
const flag = argv[i];
|
|
498
|
+
const value = argv[i + 1];
|
|
499
|
+
if (flag !== "--need" && flag !== "--url" && flag !== "--html-file" && flag !== "--root") {
|
|
500
|
+
usageError(`unknown argument: ${flag}`);
|
|
501
|
+
}
|
|
502
|
+
if (value === undefined || value.startsWith("--")) usageError(`${flag} needs a value`);
|
|
503
|
+
i++;
|
|
504
|
+
if (flag === "--need") {
|
|
505
|
+
if (!(NEEDS as readonly string[]).includes(value)) usageError(`unknown need: ${value}`);
|
|
506
|
+
opts.need = value as Need;
|
|
507
|
+
} else if (flag === "--url") {
|
|
508
|
+
opts.url = value;
|
|
509
|
+
} else if (flag === "--html-file") {
|
|
510
|
+
const text = readText(value);
|
|
511
|
+
if (text === null) usageError(`cannot read ${value}`);
|
|
512
|
+
opts.html = text;
|
|
513
|
+
} else {
|
|
514
|
+
opts.root = value;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (opts.url !== undefined && opts.html !== undefined) usageError("--url and --html-file are exclusive");
|
|
518
|
+
const plan = await buildPlan(opts);
|
|
519
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (/upgrade-plan\.ts$/.test(process.argv[1] ?? "")) {
|
|
523
|
+
main(process.argv.slice(2)).catch((err: unknown) => {
|
|
524
|
+
process.stderr.write(`upgrade-plan: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
@@ -43,6 +43,11 @@ jobs:
|
|
|
43
43
|
|
|
44
44
|
- run: pnpm lint
|
|
45
45
|
|
|
46
|
+
# The boundary lint architecture.yaml declares — the same check the
|
|
47
|
+
# PostToolUse hook in .claude/settings.json and `pnpm preflight` run
|
|
48
|
+
# locally.
|
|
49
|
+
- run: pnpm architecture
|
|
50
|
+
|
|
46
51
|
- run: pnpm test
|
|
47
52
|
|
|
48
53
|
- run: pnpm build
|
package/templates/CLAUDE.md
CHANGED
|
@@ -24,7 +24,8 @@ The full rule lives at `.claude/rules/architecture.md`. Summary:
|
|
|
24
24
|
|
|
25
25
|
- `apps/*` are peer apps. They do not import from each other.
|
|
26
26
|
- `apps/web` does not read the database — it goes through `apps/api` via
|
|
27
|
-
the published HTTP client.
|
|
27
|
+
the published HTTP client. The one exception is the Better Auth adapter
|
|
28
|
+
(`apps/web/lib/prisma-for-auth.ts`, typed in `apps/web/auth.ts`).
|
|
28
29
|
- Vendor SDKs (Stripe, Resend, NextAuth, Prisma) are wrapped in their
|
|
29
30
|
respective `@working-theory/*` packages. Don't import them directly in your code.
|
|
30
31
|
- New page types live under `content/<TypeName>/` and consume the schema
|
|
@@ -34,6 +35,11 @@ Crossing one of these is an architecture decision, not a workaround. If
|
|
|
34
35
|
you think you need to, stop and write a short note in `docs/` explaining
|
|
35
36
|
why before you change the boundary.
|
|
36
37
|
|
|
38
|
+
`architecture.yaml` records these boundaries for your module set, and
|
|
39
|
+
`pnpm architecture` (`tools/lint-architecture.ts`) enforces it. A
|
|
40
|
+
PostToolUse hook in `.claude/settings.json` runs it after every Edit or
|
|
41
|
+
Write and reports a violation in-session; `pnpm preflight` and CI run it too.
|
|
42
|
+
|
|
37
43
|
## Resource map
|
|
38
44
|
|
|
39
45
|
Where to find the rule, surface, or fact you need.
|
|
@@ -49,7 +55,7 @@ Where to find the rule, surface, or fact you need.
|
|
|
49
55
|
| Environment variables and what they unlock | `.env.example` |
|
|
50
56
|
| Per-page-type content shape | `content/<TypeName>/*.mdx` frontmatter |
|
|
51
57
|
| Deploy targets | `apps/web/vercel.json`, `apps/api/vercel.json` |
|
|
52
|
-
| Verification before push | `pnpm preflight` (architecture + types + lint +
|
|
58
|
+
| Verification before push | `pnpm preflight` (architecture + types + lint + build) |
|
|
53
59
|
|
|
54
60
|
If a question doesn't fit any row above, add the answer to this table
|
|
55
61
|
when you find it. Future-you will thank present-you.
|