create-ailk 0.21.0 → 0.23.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/component-catalog.json +186 -8
- package/component-catalog.md +9 -7
- package/dist/add-module.js +20 -14
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +12 -3
- package/dist/derive-architecture.js +20 -8
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +5 -5
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +99 -9
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/programmatic.js +19 -5
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +65 -41
- package/dist/spoke-checkout.js +208 -430
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +52 -26
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +187 -13
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +8 -4
- package/dist/topology.js +25 -1
- package/dist/update.js +15 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +87 -17
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +33 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +268 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +164 -7
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
- package/templates/apps/api/src/server.ts +163 -46
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +431 -47
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +164 -17
- package/templates/apps/api/src/services/plan-offerings.ts +125 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
- package/templates/apps/mcp/CLAUDE.md +22 -14
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +24 -31
- package/templates/apps/web/CLAUDE.md +393 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +16 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +7 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +71 -20
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +69 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +64 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +62 -3
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
package/dist/add-route.js
CHANGED
|
@@ -24,11 +24,16 @@
|
|
|
24
24
|
* this. A route the site already declares is a no-op: a second run changes
|
|
25
25
|
* nothing, even where the site has since deleted a seed.
|
|
26
26
|
*
|
|
27
|
+
* `create-ailk add-route magnet --path <path> --flow <slug>` (#8840 D12) is
|
|
28
|
+
* the second mode: it writes a site's own magnet page pair at `<path>` and
|
|
29
|
+
* `<path>/start`, bound to one flow slug, over the standard renderers in
|
|
30
|
+
* `flows/[slug]/magnet-page.tsx` — see `addMagnetToScaffold`.
|
|
31
|
+
*
|
|
27
32
|
* Pure module: no `import.meta.url`, so jest can import it directly — the
|
|
28
33
|
* package-rooted inputs are supplied by the CLI, the `add-module.ts` split.
|
|
29
34
|
*/
|
|
30
35
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
31
|
-
import { dirname, join, resolve } from "node:path";
|
|
36
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
32
37
|
import { parseArgs } from "node:util";
|
|
33
38
|
import { AddModuleRefusedError, readArchitectureHeader, syncNavConfig, } from "./add-module.js";
|
|
34
39
|
import { isTextFile } from "./copy.js";
|
|
@@ -57,16 +62,7 @@ export class AddRouteRefusedError extends Error {
|
|
|
57
62
|
*/
|
|
58
63
|
export function addRouteToScaffold(opts) {
|
|
59
64
|
const targetAbs = resolve(opts.targetDir);
|
|
60
|
-
|
|
61
|
-
throw new Error(`add-route target does not exist: ${targetAbs}. Point add-route at an already-scaffolded repo.`);
|
|
62
|
-
}
|
|
63
|
-
if (!statSync(targetAbs).isDirectory()) {
|
|
64
|
-
throw new Error(`add-route target is not a directory: ${targetAbs}`);
|
|
65
|
-
}
|
|
66
|
-
const archPath = join(targetAbs, "architecture.yaml");
|
|
67
|
-
if (!existsSync(archPath)) {
|
|
68
|
-
throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
|
|
69
|
-
}
|
|
65
|
+
const archPath = scaffoldArchitecturePath(targetAbs);
|
|
70
66
|
// #7426 D13 — a spoke stays a spoke. Read before the first write: an
|
|
71
67
|
// unreadable manifest is a refusal this command names (exit 2).
|
|
72
68
|
let spoke;
|
|
@@ -134,6 +130,20 @@ export function addRouteToScaffold(opts) {
|
|
|
134
130
|
}
|
|
135
131
|
return { targetDir: targetAbs, route, surfaces, report, routes };
|
|
136
132
|
}
|
|
133
|
+
/** The site's `architecture.yaml`, after checking `targetAbs` is a scaffold. */
|
|
134
|
+
function scaffoldArchitecturePath(targetAbs) {
|
|
135
|
+
if (!existsSync(targetAbs)) {
|
|
136
|
+
throw new Error(`add-route target does not exist: ${targetAbs}. Point add-route at an already-scaffolded repo.`);
|
|
137
|
+
}
|
|
138
|
+
if (!statSync(targetAbs).isDirectory()) {
|
|
139
|
+
throw new Error(`add-route target is not a directory: ${targetAbs}`);
|
|
140
|
+
}
|
|
141
|
+
const archPath = join(targetAbs, "architecture.yaml");
|
|
142
|
+
if (!existsSync(archPath)) {
|
|
143
|
+
throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
|
|
144
|
+
}
|
|
145
|
+
return archPath;
|
|
146
|
+
}
|
|
137
147
|
/** The canonical id for one route argument, refusing an unknown one. */
|
|
138
148
|
function canonicalRoute(raw) {
|
|
139
149
|
let parsed;
|
|
@@ -255,10 +265,251 @@ export function formatAddRouteReport(result) {
|
|
|
255
265
|
}
|
|
256
266
|
return lines.join("\n") + "\n";
|
|
257
267
|
}
|
|
268
|
+
// ── add-route magnet (#8840 D12) ───────────────────────────────────────────
|
|
269
|
+
/** The route word that selects the magnet mode. */
|
|
270
|
+
export const MAGNET_ROUTE = "magnet";
|
|
271
|
+
/**
|
|
272
|
+
* The header-comment marker every standard magnet page carries — the same
|
|
273
|
+
* string `flows/[slug]/magnet-page.tsx` exports, which the site's
|
|
274
|
+
* `structured-data.test.tsx` reads to exempt the pair (#8840 D13).
|
|
275
|
+
*/
|
|
276
|
+
export const MAGNET_PAGE_MARKER = "Standard magnet page (#8840)";
|
|
277
|
+
/** The module whose renderers and flow routes a magnet pair mounts. */
|
|
278
|
+
const MAGNET_MODULE = "signal-collectors";
|
|
279
|
+
/**
|
|
280
|
+
* `flowSlugSchema`'s rule (`@working-theory/validation`), restated because
|
|
281
|
+
* create-ailk has no runtime dependency; `add-route-magnet.test.ts` pins the
|
|
282
|
+
* two equal over a fixed table.
|
|
283
|
+
*/
|
|
284
|
+
const FLOW_SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
285
|
+
const FLOW_SLUG_MAX = 59;
|
|
286
|
+
/** One to three kebab-case segments, nothing else (SEC-8840-J). */
|
|
287
|
+
const MAGNET_PATH_RE = /^\/[a-z0-9]+(-[a-z0-9]+)*(\/[a-z0-9]+(-[a-z0-9]+)*){0,2}$/;
|
|
288
|
+
/** A walk's own last segments: `<path>/start` is the pair's, `apply` the careers walks'. */
|
|
289
|
+
const RESERVED_LAST_SEGMENTS = new Set(["start", "apply"]);
|
|
290
|
+
/**
|
|
291
|
+
* First segments a pair may not take: the API and relay prefixes, the flows
|
|
292
|
+
* routes, the account and Admin areas, and `apps/web/lib/auth-routes.ts`'s
|
|
293
|
+
* `AUTH_SEGMENTS` (restated — create-ailk cannot import `apps/web`).
|
|
294
|
+
*/
|
|
295
|
+
const RESERVED_FIRST_SEGMENTS = new Set([
|
|
296
|
+
"api",
|
|
297
|
+
"v1",
|
|
298
|
+
"flows",
|
|
299
|
+
"account",
|
|
300
|
+
"admin",
|
|
301
|
+
"sign-in",
|
|
302
|
+
"sign-up",
|
|
303
|
+
"forgot-password",
|
|
304
|
+
"reset-password",
|
|
305
|
+
"verify-email",
|
|
306
|
+
]);
|
|
307
|
+
/** A first segment next-intl would read as a locale prefix. */
|
|
308
|
+
const LOCALE_SHAPED_RE = /^[a-z]{2}(-[a-z]{2})?$/;
|
|
309
|
+
/** Template-relative root every page directory sits under. */
|
|
310
|
+
const WEB_ROUTE_ROOT = "apps/web/app/[locale]";
|
|
311
|
+
/** Whether `slug` passes `flowSlugSchema`'s rule. */
|
|
312
|
+
export function isMagnetFlowSlug(slug) {
|
|
313
|
+
return slug.length <= FLOW_SLUG_MAX && FLOW_SLUG_RE.test(slug);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Write a magnet page pair — `apps/web/app/[locale]/<path>/page.tsx` (the
|
|
317
|
+
* landing) and `<path>/start/page.tsx` (the walk) — bound to one flow slug.
|
|
318
|
+
* Each page is a few lines over the factories in `flows/[slug]/magnet-page`;
|
|
319
|
+
* the copy, consent and ending come from the flow's record.
|
|
320
|
+
*
|
|
321
|
+
* Every refusal throws `AddRouteRefusedError` before the first write. A page
|
|
322
|
+
* that already exists is never overwritten: a second run reports both
|
|
323
|
+
* `skipped`. No `architecture.yaml`, nav or seed changes — the pair is the
|
|
324
|
+
* site's own, with no `ROUTE_SINCE` key or routes-map row (D13).
|
|
325
|
+
*/
|
|
326
|
+
export function addMagnetToScaffold(opts) {
|
|
327
|
+
const targetAbs = resolve(opts.targetDir);
|
|
328
|
+
const archPath = scaffoldArchitecturePath(targetAbs);
|
|
329
|
+
let modules;
|
|
330
|
+
try {
|
|
331
|
+
modules = readArchitectureHeader(readFileSync(archPath, "utf8")).modules;
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (err instanceof AddModuleRefusedError) {
|
|
335
|
+
throw new AddRouteRefusedError(err.message);
|
|
336
|
+
}
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
if (!modules.includes(MAGNET_MODULE)) {
|
|
340
|
+
throw new AddRouteRefusedError(`a magnet page mounts the "${MAGNET_MODULE}" module's flow stepper, which this site does not carry — add it with \`create-ailk add-module ${MAGNET_MODULE}\``);
|
|
341
|
+
}
|
|
342
|
+
if (!isMagnetFlowSlug(opts.flow)) {
|
|
343
|
+
throw new AddRouteRefusedError(`--flow "${opts.flow}" is not a flow slug: kebab-case (lowercase letters, digits and single hyphens), at most ${FLOW_SLUG_MAX} characters`);
|
|
344
|
+
}
|
|
345
|
+
const segments = magnetPathSegments(opts.path);
|
|
346
|
+
if (segments.length === 1) {
|
|
347
|
+
const shadowed = shadowedLandingPage(targetAbs, segments[0]);
|
|
348
|
+
if (shadowed !== undefined) {
|
|
349
|
+
throw new AddRouteRefusedError(`--path ${opts.path} would shadow the site's own page ${shadowed} — pick another path`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const routeDir = segments.join("/");
|
|
353
|
+
const landing = `${WEB_ROUTE_ROOT}/${routeDir}/page.tsx`;
|
|
354
|
+
const walk = `${WEB_ROUTE_ROOT}/${routeDir}/start/page.tsx`;
|
|
355
|
+
const served = staticPageServing(targetAbs, [routeDir, `${routeDir}/start`], {
|
|
356
|
+
ownPages: [landing, walk],
|
|
357
|
+
});
|
|
358
|
+
if (served !== undefined) {
|
|
359
|
+
throw new AddRouteRefusedError(`--path ${opts.path} is already served by ${served} — pick another path`);
|
|
360
|
+
}
|
|
361
|
+
const pages = [
|
|
362
|
+
[
|
|
363
|
+
landing,
|
|
364
|
+
renderMagnetPage("landing", opts.path, opts.flow, segments.length),
|
|
365
|
+
],
|
|
366
|
+
[walk, renderMagnetPage("walk", opts.path, opts.flow, segments.length + 1)],
|
|
367
|
+
];
|
|
368
|
+
const report = [];
|
|
369
|
+
for (const [path, text] of pages) {
|
|
370
|
+
const dest = join(targetAbs, path);
|
|
371
|
+
if (existsSync(dest)) {
|
|
372
|
+
report.push({ path, action: "skipped" });
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
376
|
+
writeFileSync(dest, text);
|
|
377
|
+
report.push({ path, action: "written" });
|
|
378
|
+
}
|
|
379
|
+
return { targetDir: targetAbs, path: opts.path, flow: opts.flow, report };
|
|
380
|
+
}
|
|
381
|
+
/** `--path`'s segments, refusing a path a pair may not take (SEC-8840-J). */
|
|
382
|
+
function magnetPathSegments(path) {
|
|
383
|
+
if (!MAGNET_PATH_RE.test(path)) {
|
|
384
|
+
throw new AddRouteRefusedError(`--path "${path}" is not a magnet path: one to three kebab-case segments after a leading slash, e.g. /diagnostic or /careers/writer`);
|
|
385
|
+
}
|
|
386
|
+
const segments = path.slice(1).split("/");
|
|
387
|
+
const first = segments[0];
|
|
388
|
+
const last = segments[segments.length - 1];
|
|
389
|
+
if (RESERVED_LAST_SEGMENTS.has(last)) {
|
|
390
|
+
throw new AddRouteRefusedError(`--path ${path} ends in "${last}", which a walk route uses — the pair's walk is written at <path>/start`);
|
|
391
|
+
}
|
|
392
|
+
if (RESERVED_FIRST_SEGMENTS.has(first) || LOCALE_SHAPED_RE.test(first)) {
|
|
393
|
+
throw new AddRouteRefusedError(`--path ${path} starts with "${first}", a segment the site reserves (the API, flows, account, Admin and sign-in routes, and locale prefixes)`);
|
|
394
|
+
}
|
|
395
|
+
return segments;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The site's `content/<locale>/LandingPage/<segment>.mdx`, in any locale,
|
|
399
|
+
* that a one-segment pair would shadow (the generic `[slug]` route serves it).
|
|
400
|
+
*/
|
|
401
|
+
function shadowedLandingPage(targetAbs, segment) {
|
|
402
|
+
const contentRoot = join(targetAbs, "content");
|
|
403
|
+
if (!existsSync(contentRoot))
|
|
404
|
+
return undefined;
|
|
405
|
+
for (const entry of readdirSync(contentRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
406
|
+
if (!entry.isDirectory())
|
|
407
|
+
continue;
|
|
408
|
+
const rel = `content/${entry.name}/LandingPage/${segment}.mdx`;
|
|
409
|
+
if (existsSync(join(targetAbs, rel)))
|
|
410
|
+
return rel;
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The first `page.tsx` under the site's `[locale]` tree whose directory, with
|
|
416
|
+
* route-group segments removed, is one of `dirs` — a path the site already
|
|
417
|
+
* serves statically. A dynamic segment never matches (`careers/[slug]` does
|
|
418
|
+
* not serve `careers/writer` once a static `careers/writer` exists), and the
|
|
419
|
+
* pair's own two files are skipped, so a rerun reaches the no-overwrite rule.
|
|
420
|
+
*/
|
|
421
|
+
function staticPageServing(targetAbs, dirs, { ownPages }) {
|
|
422
|
+
const root = join(targetAbs, WEB_ROUTE_ROOT);
|
|
423
|
+
const wanted = new Set(dirs);
|
|
424
|
+
const own = new Set(ownPages);
|
|
425
|
+
const walk = (abs) => {
|
|
426
|
+
if (!existsSync(abs))
|
|
427
|
+
return undefined;
|
|
428
|
+
const entries = readdirSync(abs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
const full = join(abs, entry.name);
|
|
431
|
+
if (entry.isDirectory()) {
|
|
432
|
+
if (entry.name === "__tests__" || entry.name === "node_modules")
|
|
433
|
+
continue;
|
|
434
|
+
const hit = walk(full);
|
|
435
|
+
if (hit !== undefined)
|
|
436
|
+
return hit;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!entry.isFile() || entry.name !== "page.tsx")
|
|
440
|
+
continue;
|
|
441
|
+
const rel = relative(targetAbs, full).split(sep).join("/");
|
|
442
|
+
if (own.has(rel) &&
|
|
443
|
+
readFileSync(full, "utf8").includes(MAGNET_PAGE_MARKER)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const dir = relative(root, abs)
|
|
447
|
+
.split(sep)
|
|
448
|
+
.filter((segment) => segment !== "" && !/^\(.*\)$/.test(segment))
|
|
449
|
+
.join("/");
|
|
450
|
+
if (wanted.has(dir))
|
|
451
|
+
return rel;
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
};
|
|
455
|
+
return walk(root);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* One page of the pair. `depth` is the page directory's segment count under
|
|
459
|
+
* `[locale]`, so the import climbs back to `flows/[slug]/magnet-page`. Every
|
|
460
|
+
* interpolated value has passed `MAGNET_PATH_RE` or `FLOW_SLUG_RE`, so none
|
|
461
|
+
* can leave its string literal (SEC-8840-V5).
|
|
462
|
+
*/
|
|
463
|
+
function renderMagnetPage(kind, path, flow, depth) {
|
|
464
|
+
const [page, metadata, role] = kind === "landing"
|
|
465
|
+
? [
|
|
466
|
+
"magnetLanding",
|
|
467
|
+
"magnetLandingMetadata",
|
|
468
|
+
`the landing of the ${path} magnet pair, its copy from the ${flow} flow's record`,
|
|
469
|
+
]
|
|
470
|
+
: [
|
|
471
|
+
"magnetStart",
|
|
472
|
+
"magnetStartMetadata",
|
|
473
|
+
`the walk of the ${path} magnet pair, its back arrow to ${path}`,
|
|
474
|
+
];
|
|
475
|
+
return [
|
|
476
|
+
`// Generated by \`create-ailk add-route magnet --path ${path} --flow ${flow}\`.`,
|
|
477
|
+
`// ${MAGNET_PAGE_MARKER} — ${role}.`,
|
|
478
|
+
`import { ${page}, ${metadata} } from "${"../".repeat(depth)}flows/[slug]/magnet-page";`,
|
|
479
|
+
"",
|
|
480
|
+
`const MAGNET = { slug: "${flow}", path: "${path}" } as const;`,
|
|
481
|
+
"",
|
|
482
|
+
'export const dynamic = "force-dynamic";',
|
|
483
|
+
`export const generateMetadata = ${metadata}(MAGNET);`,
|
|
484
|
+
`export default ${page}(MAGNET);`,
|
|
485
|
+
"",
|
|
486
|
+
].join("\n");
|
|
487
|
+
}
|
|
488
|
+
/** The report `add-route magnet` prints. */
|
|
489
|
+
export function formatAddMagnetReport(result) {
|
|
490
|
+
const written = result.report.filter((row) => row.action === "written");
|
|
491
|
+
const skipped = result.report.filter((row) => row.action === "skipped");
|
|
492
|
+
const lines = [
|
|
493
|
+
`[create-ailk] add-route magnet ${result.path} (flow ${result.flow}) → ${result.targetDir}`,
|
|
494
|
+
];
|
|
495
|
+
if (written.length > 0) {
|
|
496
|
+
lines.push(`Written (${written.length}):`);
|
|
497
|
+
lines.push(...written.map((row) => ` ${row.path}`));
|
|
498
|
+
}
|
|
499
|
+
if (skipped.length > 0) {
|
|
500
|
+
lines.push(`Skipped (${skipped.length}) — the site already has this page, left untouched:`);
|
|
501
|
+
lines.push(...skipped.map((row) => ` ${row.path}`));
|
|
502
|
+
}
|
|
503
|
+
if (written.length > 0) {
|
|
504
|
+
lines.push(`Next: pnpm build, then open ${result.path}. The page's copy comes from the ${result.flow} flow's record.`);
|
|
505
|
+
}
|
|
506
|
+
return lines.join("\n") + "\n";
|
|
507
|
+
}
|
|
258
508
|
export const ADD_ROUTE_HELP = `create-ailk add-route — add a route family to an existing site.
|
|
259
509
|
|
|
260
510
|
Usage:
|
|
261
511
|
create-ailk add-route <route> [<target-dir>]
|
|
512
|
+
create-ailk add-route magnet --path <path> --flow <slug> [<target-dir>]
|
|
262
513
|
|
|
263
514
|
Arguments:
|
|
264
515
|
<route> One route family, named as --surfaces names it,
|
|
@@ -267,6 +518,9 @@ Arguments:
|
|
|
267
518
|
architecture.yaml). Defaults to the current dir.
|
|
268
519
|
|
|
269
520
|
Options:
|
|
521
|
+
--path <path> magnet only: the site path the landing serves,
|
|
522
|
+
one to three kebab-case segments, e.g. /diagnostic.
|
|
523
|
+
--flow <slug> magnet only: the flow slug the pages are bound to.
|
|
270
524
|
--help, -h Print this message.
|
|
271
525
|
|
|
272
526
|
Copies the route's pages, writes its seed content, declares it in
|
|
@@ -278,11 +532,25 @@ Like add-module, it also restores any other page route the site carries but
|
|
|
278
532
|
lacks. A route a module delivers (blog and docs come with docs-blog) is
|
|
279
533
|
refused; add that module with create-ailk add-module. Running it again changes
|
|
280
534
|
nothing.
|
|
535
|
+
|
|
536
|
+
add-route magnet writes a magnet page pair bound to one flow: a landing at
|
|
537
|
+
apps/web/app/[locale]/<path>/page.tsx and a full-screen walk at
|
|
538
|
+
<path>/start/page.tsx, each a few lines over the standard magnet pages, so the
|
|
539
|
+
copy, consent and ending come from the flow's record. It needs the
|
|
540
|
+
signal-collectors module, and refuses (nothing written) a path the site already
|
|
541
|
+
serves, a one-segment path a LandingPage seed of the same slug owns, a path
|
|
542
|
+
ending in start or apply, a reserved first segment (api, v1, flows, account,
|
|
543
|
+
admin, the sign-in routes, a locale) and a slug the flow API would refuse. A
|
|
544
|
+
page that exists is never overwritten: running it again skips both. It changes
|
|
545
|
+
no architecture.yaml, nav or seed.
|
|
281
546
|
`;
|
|
282
547
|
/**
|
|
283
548
|
* Parse the argv AFTER the `add-route` word. Strict `node:util` parseArgs,
|
|
284
549
|
* like `parseAddModuleArgs`; the route itself is validated by
|
|
285
550
|
* `addRouteToScaffold`, which refuses an unknown one naming the recognized.
|
|
551
|
+
* `--path` and `--flow` belong to the `magnet` route word alone (#8840 D12):
|
|
552
|
+
* with any other route they are refused (`AddRouteRefusedError`, exit 2),
|
|
553
|
+
* and `magnet` needs both. Their values are checked by `addMagnetToScaffold`.
|
|
286
554
|
*/
|
|
287
555
|
export function parseAddRouteArgs(argv) {
|
|
288
556
|
const parsed = parseArgs({
|
|
@@ -291,9 +559,12 @@ export function parseAddRouteArgs(argv) {
|
|
|
291
559
|
allowPositionals: true,
|
|
292
560
|
options: {
|
|
293
561
|
help: { type: "boolean", default: false, short: "h" },
|
|
562
|
+
path: { type: "string" },
|
|
563
|
+
flow: { type: "string" },
|
|
294
564
|
},
|
|
295
565
|
});
|
|
296
566
|
const help = parsed.values.help === true;
|
|
567
|
+
const { path, flow } = parsed.values;
|
|
297
568
|
const [route, target, ...rest] = parsed.positionals;
|
|
298
569
|
if (rest.length > 0) {
|
|
299
570
|
throw new ParseError(`expected a route and at most one target directory, got ${parsed.positionals.length} arguments`);
|
|
@@ -306,5 +577,16 @@ export function parseAddRouteArgs(argv) {
|
|
|
306
577
|
if (route.includes(",")) {
|
|
307
578
|
throw new ParseError(`add-route takes exactly one route, got "${route}" — run it once per route`);
|
|
308
579
|
}
|
|
309
|
-
|
|
580
|
+
if (route !== MAGNET_ROUTE) {
|
|
581
|
+
if (path !== undefined || flow !== undefined) {
|
|
582
|
+
throw new AddRouteRefusedError(`--path and --flow belong to \`create-ailk add-route magnet\`, not to the "${route}" route`);
|
|
583
|
+
}
|
|
584
|
+
return { route, target, help };
|
|
585
|
+
}
|
|
586
|
+
if (help)
|
|
587
|
+
return { route, target, help };
|
|
588
|
+
if (path === undefined || flow === undefined) {
|
|
589
|
+
throw new ParseError("add-route magnet needs --path and --flow, e.g. `create-ailk add-route magnet --path /diagnostic --flow growth`");
|
|
590
|
+
}
|
|
591
|
+
return { route, target, help, path, flow };
|
|
310
592
|
}
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 0. on an `add-module` / `upgrade` subcommand: parse its own argv, run
|
|
7
7
|
* addModule(), print the report, exit (2 on a refusal it names)
|
|
8
8
|
* on an `add-route` subcommand: add one route surface to an existing
|
|
9
|
-
* site, print the report, exit (2 on a refusal it names) (#6947)
|
|
9
|
+
* site, print the report, exit (2 on a refusal it names) (#6947); with
|
|
10
|
+
* the `magnet` route word, write a magnet page pair instead (#8840)
|
|
10
11
|
* on an `adopt` subcommand: record an existing site's topology and
|
|
11
12
|
* pattern version in its manifest, print the report, exit (2 on a
|
|
12
13
|
* refusal it names) (#7875)
|
|
@@ -45,15 +46,17 @@
|
|
|
45
46
|
*/
|
|
46
47
|
import { spawnSync } from "node:child_process";
|
|
47
48
|
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
+
import { homedir } from "node:os";
|
|
48
50
|
import { basename, dirname, resolve } from "node:path";
|
|
49
51
|
import { fileURLToPath } from "node:url";
|
|
50
52
|
import { ADD_MODULE_HELP, AddModuleRefusedError, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
|
|
51
|
-
import { ADD_ROUTE_HELP, AddRouteRefusedError, addRouteToScaffold, formatAddRouteReport, parseAddRouteArgs, } from "./add-route.js";
|
|
53
|
+
import { ADD_ROUTE_HELP, AddRouteRefusedError, MAGNET_ROUTE, addMagnetToScaffold, addRouteToScaffold, formatAddMagnetReport, formatAddRouteReport, parseAddRouteArgs, } from "./add-route.js";
|
|
52
54
|
import { ADOPT_HELP, AdoptRefusedError, adoptSite, formatAdoptReport, parseAdoptArgs, } from "./adopt.js";
|
|
53
55
|
import { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
|
|
54
56
|
import { CLAUDE_MD_CHECK_PATH } from "./claude-md.js";
|
|
55
57
|
import { buildScaffoldMarkers } from "./copy.js";
|
|
56
58
|
import { assertReleased } from "./lib/module-license-gate.js";
|
|
59
|
+
import { withNpmTokenUserConfig } from "./lib/npm-auth.js";
|
|
57
60
|
import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
|
|
58
61
|
import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
|
|
59
62
|
import { PLUGIN_INSTALL_LINES, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
|
|
@@ -147,14 +150,26 @@ async function main() {
|
|
|
147
150
|
// scaffolded without — add-module's route, architecture and nav steps with
|
|
148
151
|
// no licence and no bundle. A refusal it names (an unknown route, one a
|
|
149
152
|
// module delivers, an unreadable architecture.yaml) exits 2.
|
|
153
|
+
// `add-route magnet --path <path> --flow <slug> [dir]` (#8840 D12) writes a
|
|
154
|
+
// magnet page pair instead; its refusals (no signal-collectors, a path the
|
|
155
|
+
// site serves or reserves, a bad slug, --path/--flow on another route) exit 2.
|
|
150
156
|
if (argv[0] === "add-route") {
|
|
151
|
-
const args = parseAddRouteArgs(argv.slice(1));
|
|
152
|
-
if (args.help || args.route === undefined) {
|
|
153
|
-
process.stdout.write(ADD_ROUTE_HELP);
|
|
154
|
-
process.exit(0);
|
|
155
|
-
}
|
|
156
|
-
const targetDir = resolve(args.target ?? process.cwd());
|
|
157
157
|
try {
|
|
158
|
+
const args = parseAddRouteArgs(argv.slice(1));
|
|
159
|
+
if (args.help || args.route === undefined) {
|
|
160
|
+
process.stdout.write(ADD_ROUTE_HELP);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
const targetDir = resolve(args.target ?? process.cwd());
|
|
164
|
+
if (args.route === MAGNET_ROUTE) {
|
|
165
|
+
const result = addMagnetToScaffold({
|
|
166
|
+
targetDir,
|
|
167
|
+
path: args.path ?? "",
|
|
168
|
+
flow: args.flow ?? "",
|
|
169
|
+
});
|
|
170
|
+
process.stdout.write(formatAddMagnetReport(result));
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
158
173
|
const result = addRouteToScaffold({
|
|
159
174
|
targetDir,
|
|
160
175
|
route: args.route,
|
|
@@ -428,11 +443,13 @@ async function main() {
|
|
|
428
443
|
const templateName = starter?.template ?? opts.template;
|
|
429
444
|
if (opts.install) {
|
|
430
445
|
// Hard-coded argv. User input only flows through `cwd`, never the command.
|
|
431
|
-
|
|
446
|
+
// NPM_TOKEN reaches pnpm at user level, never a project .npmrc (#9092).
|
|
447
|
+
const result = withNpmTokenUserConfig(process.env, homedir(), (env) => spawnSync("pnpm", ["install"], {
|
|
432
448
|
cwd: targetAbs,
|
|
449
|
+
env,
|
|
433
450
|
stdio: "inherit",
|
|
434
451
|
shell: false,
|
|
435
|
-
});
|
|
452
|
+
}));
|
|
436
453
|
if (result.status !== 0) {
|
|
437
454
|
throw new Error(`pnpm install failed in ${targetAbs} (exit ${result.status}). Run it manually after fixing the issue.`);
|
|
438
455
|
}
|
|
@@ -22,13 +22,22 @@ export interface DeriveArchitectureOptions {
|
|
|
22
22
|
/** Spoke only: whether the spoke keeps `apps/mcp` (default true). */
|
|
23
23
|
mcp?: boolean;
|
|
24
24
|
}
|
|
25
|
+
/** The billing action a spoke that signs in through its hub renders (#8217 D6). */
|
|
26
|
+
export declare const SPOKE_BILLING_ACTION_PATH = "apps/web/app/[locale]/(authed)/account/billing/spoke-billing.action.ts";
|
|
25
27
|
/**
|
|
26
28
|
* The only spoke files that may import @working-theory/spoke-checkout (#7873):
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
29
|
+
* the billing action, on a spoke that signs in through its hub (#8217 D9).
|
|
30
|
+
* The buy route and the Stripe webhook route that were also here are retired
|
|
31
|
+
* (#9142): a spoke sells through its hub's Offerings buy route.
|
|
29
32
|
*/
|
|
30
33
|
export declare const SPOKE_CHECKOUT_ROUTE_FILES: readonly string[];
|
|
31
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The spoke files that may import @working-theory/spoke-checkout to READ a
|
|
36
|
+
* paid session, and nothing else: the thank-you page's `readPaidCheckoutEmail`
|
|
37
|
+
* (#8757 D13).
|
|
38
|
+
*/
|
|
39
|
+
export declare const SPOKE_CHECKOUT_READER_FILES: readonly string[];
|
|
40
|
+
/** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2; #8748). The fulfilment pattern that guarded `lib/checkout.ts` retired with it (#9142). */
|
|
32
41
|
export declare const SPOKE_WEB_FORBIDDEN_PATTERNS: readonly ForbiddenPattern[];
|
|
33
42
|
/** The same three for a spoke's kept apps/mcp — no MCP file may import the package (#7873). */
|
|
34
43
|
export declare const SPOKE_MCP_FORBIDDEN_PATTERNS: readonly ForbiddenPattern[];
|
|
@@ -27,15 +27,24 @@ const SPOKE_TIERS = {
|
|
|
27
27
|
// import only to a workspace on disk, so the one way to ban an npm package is
|
|
28
28
|
// a line-level forbidden pattern. These go under the spoke's own `apps.web`
|
|
29
29
|
// and `apps.mcp` only — never top level, and never on a hub derivation.
|
|
30
|
+
/** The billing action a spoke that signs in through its hub renders (#8217 D6). */
|
|
31
|
+
export const SPOKE_BILLING_ACTION_PATH = "apps/web/app/[locale]/(authed)/account/billing/spoke-billing.action.ts";
|
|
30
32
|
/**
|
|
31
33
|
* The only spoke files that may import @working-theory/spoke-checkout (#7873):
|
|
32
|
-
* the
|
|
33
|
-
*
|
|
34
|
+
* the billing action, on a spoke that signs in through its hub (#8217 D9).
|
|
35
|
+
* The buy route and the Stripe webhook route that were also here are retired
|
|
36
|
+
* (#9142): a spoke sells through its hub's Offerings buy route.
|
|
34
37
|
*/
|
|
35
38
|
export const SPOKE_CHECKOUT_ROUTE_FILES = Object.freeze([
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
SPOKE_BILLING_ACTION_PATH,
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* The spoke files that may import @working-theory/spoke-checkout to READ a
|
|
43
|
+
* paid session, and nothing else: the thank-you page's `readPaidCheckoutEmail`
|
|
44
|
+
* (#8757 D13).
|
|
45
|
+
*/
|
|
46
|
+
export const SPOKE_CHECKOUT_READER_FILES = Object.freeze([
|
|
47
|
+
"apps/web/app/[locale]/checkout/thank-you/page.tsx",
|
|
39
48
|
]);
|
|
40
49
|
/**
|
|
41
50
|
* A line-level regex for an import of `specifier` or one of its subpaths, in
|
|
@@ -53,7 +62,7 @@ function specifierForm(specifier) {
|
|
|
53
62
|
/** P1 with no exclusions — a kept apps/mcp's form; apps/web adds the route files. */
|
|
54
63
|
const SPOKE_CHECKOUT_PATTERN = Object.freeze({
|
|
55
64
|
pattern: specifierForm("@working-theory/spoke-checkout"),
|
|
56
|
-
reason: "Spoke checkout: only the
|
|
65
|
+
reason: "Spoke checkout: only the billing action and the thank-you page may import @working-theory/spoke-checkout (brief Part 2 §2, #7873, #8217, #8757, #9142)",
|
|
57
66
|
});
|
|
58
67
|
const STRIPE_CLIENT_PATTERN = Object.freeze({
|
|
59
68
|
pattern: specifierForm("@working-theory/stripe-client"),
|
|
@@ -63,9 +72,12 @@ const STRIPE_SDK_PATTERN = Object.freeze({
|
|
|
63
72
|
pattern: specifierForm("stripe"),
|
|
64
73
|
reason: "A spoke never imports the stripe SDK directly — it reaches Stripe only through @working-theory/spoke-checkout (#7873)",
|
|
65
74
|
});
|
|
66
|
-
/** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2). */
|
|
75
|
+
/** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2; #8748). The fulfilment pattern that guarded `lib/checkout.ts` retired with it (#9142). */
|
|
67
76
|
export const SPOKE_WEB_FORBIDDEN_PATTERNS = Object.freeze([
|
|
68
|
-
Object.freeze({
|
|
77
|
+
Object.freeze({
|
|
78
|
+
...SPOKE_CHECKOUT_PATTERN,
|
|
79
|
+
paths_excluded: [...SPOKE_CHECKOUT_ROUTE_FILES, ...SPOKE_CHECKOUT_READER_FILES],
|
|
80
|
+
}),
|
|
69
81
|
STRIPE_CLIENT_PATTERN,
|
|
70
82
|
STRIPE_SDK_PATTERN,
|
|
71
83
|
]);
|
package/dist/design-package.js
CHANGED
|
@@ -486,13 +486,14 @@ function validateStringMap(raw, file, field) {
|
|
|
486
486
|
}
|
|
487
487
|
const LAYOUT_KEYS = ["nav", "footer", "announcementBanner", "home"];
|
|
488
488
|
/** Every key a package's `layout` may carry — #7719 D3 adds the optional `deckGrounds` (a deck home only),
|
|
489
|
-
* #7721 D1 the optional `innerPages`, #7740 D1 the optional `deckPresentation` (a deck home only),
|
|
489
|
+
* #7721 D1 the optional `innerPages`, #7740 D1 the optional `deckPresentation` (a deck home only), #8773 the optional `deckAnchors` (a presenting deck only),
|
|
490
490
|
* #7742 D1 the optional `colorMode` and `chromeStyle`, #7797 D1 the optional `closingFooterPaths`. */
|
|
491
491
|
const ALLOWED_LAYOUT_KEYS = new Set([
|
|
492
492
|
...LAYOUT_KEYS,
|
|
493
493
|
"deckGrounds",
|
|
494
494
|
"innerPages",
|
|
495
495
|
"deckPresentation",
|
|
496
|
+
"deckAnchors",
|
|
496
497
|
"colorMode",
|
|
497
498
|
"chromeStyle",
|
|
498
499
|
"closingFooterPaths",
|
|
@@ -524,11 +525,12 @@ function validateLayout(raw, file) {
|
|
|
524
525
|
// `ResolvedSiteChrome` shape; an explicit "default" is the absent key.
|
|
525
526
|
// #7740 D1 — and `deckPresentation` only as `true`.
|
|
526
527
|
// #7742 D1 — and `colorMode` only as "light", `chromeStyle` only as "minimal".
|
|
527
|
-
const { innerPages, deckPresentation, colorMode, chromeStyle, ...rest } = validated;
|
|
528
|
+
const { innerPages, deckPresentation, deckAnchors, colorMode, chromeStyle, ...rest } = validated;
|
|
528
529
|
const layout = {
|
|
529
530
|
...rest,
|
|
530
531
|
...(innerPages === "bare" ? { innerPages } : {}),
|
|
531
532
|
...(deckPresentation === true ? { deckPresentation } : {}),
|
|
533
|
+
...(deckAnchors === "bare" ? { deckAnchors } : {}),
|
|
532
534
|
...(colorMode === "light" ? { colorMode } : {}),
|
|
533
535
|
...(chromeStyle === "minimal" ? { chromeStyle } : {}),
|
|
534
536
|
};
|
|
@@ -538,6 +540,9 @@ function validateLayout(raw, file) {
|
|
|
538
540
|
if (deckPresentation === true && layout.home !== "deck") {
|
|
539
541
|
refuse(file, "layout.deckPresentation", "applies only to home: deck");
|
|
540
542
|
}
|
|
543
|
+
if (deckAnchors === "bare" && deckPresentation !== true) {
|
|
544
|
+
refuse(file, "layout.deckAnchors", "applies only with deckPresentation: true");
|
|
545
|
+
}
|
|
541
546
|
return layout;
|
|
542
547
|
}
|
|
543
548
|
const TREATMENTS_ENUMS = {
|
|
@@ -134,11 +134,13 @@ export function admitSeededPeopleSlide(before, after) {
|
|
|
134
134
|
if (!Array.isArray(graph))
|
|
135
135
|
return after;
|
|
136
136
|
const [page, ...people] = graph;
|
|
137
|
+
// A seeded `{placeholder}` name or title never reaches the graph (#8797 D8.4).
|
|
138
|
+
const slot = (value) => /^\{[^{}]+\}$/.test(value.trim());
|
|
137
139
|
const expected = TEAM_SEED_MEMBERS.map((m) => ({
|
|
138
140
|
"@context": "https://schema.org",
|
|
139
141
|
"@type": "Person",
|
|
140
|
-
name: m.name,
|
|
141
|
-
jobTitle: m.role,
|
|
142
|
+
...(!slot(m.name) && { name: m.name }),
|
|
143
|
+
...(!slot(m.role) && { jobTitle: m.role }),
|
|
142
144
|
}));
|
|
143
145
|
const isPeopleSlide = isDeepStrictEqual(page, JSON.parse(standard.jsonld)) && isDeepStrictEqual(people, expected);
|
|
144
146
|
return isPeopleSlide ? { ...after, [HOME_PAGE]: standard } : after;
|
package/dist/lib/env-catalog.js
CHANGED
|
@@ -52,7 +52,7 @@ export const ENV_CATALOG = {
|
|
|
52
52
|
secret: false,
|
|
53
53
|
},
|
|
54
54
|
AILK_LEAD_SITE: {
|
|
55
|
-
description: "
|
|
55
|
+
description: "This site's slug, written as payload.site on every lead its /contact form and MCP server capture, so a hub can tell its sites apart; unset leaves the lead unmarked.",
|
|
56
56
|
secret: false,
|
|
57
57
|
},
|
|
58
58
|
AILK_LICENSE_STORAGE_PATH: {
|
|
@@ -71,6 +71,10 @@ export const ENV_CATALOG = {
|
|
|
71
71
|
description: "The scope captured leads are stamped with; on a single-tenant install, the default workspace id.",
|
|
72
72
|
secret: false,
|
|
73
73
|
},
|
|
74
|
+
AILK_SPOKE_KEY: {
|
|
75
|
+
description: "The hub-issued key this site sends as x-ailk-spoke-key; the checkout fulfilment refuses to run without it. Never committed.",
|
|
76
|
+
secret: true,
|
|
77
|
+
},
|
|
74
78
|
AILK_VISITOR_FORWARD_KEY: {
|
|
75
79
|
description: "This spoke's key for forwarding each visitor's IP to the hub, so the hub rate-limits per visitor. Server-only.",
|
|
76
80
|
secret: true,
|
|
@@ -203,10 +207,6 @@ export const ENV_CATALOG = {
|
|
|
203
207
|
description: "Shared secret the /dev/purchase action sends to the API's checkout route; must equal the API's value. Server-only.",
|
|
204
208
|
secret: true,
|
|
205
209
|
},
|
|
206
|
-
SCHEDULING_CAL_LINK: {
|
|
207
|
-
description: 'Which event /meet books, as "<owner>/<event-slug>"; unset makes /meet 404.',
|
|
208
|
-
secret: false,
|
|
209
|
-
},
|
|
210
210
|
SENTRY_AUTH_TOKEN: {
|
|
211
211
|
description: "Build-time Sentry token that uploads source maps; unset skips the upload.",
|
|
212
212
|
secret: true,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The one user-level line: the registry mapped to the env var, no value. */
|
|
2
|
+
export declare const NPM_TOKEN_AUTH_LINE = "//registry.npmjs.org/:_authToken=${NPM_TOKEN}";
|
|
3
|
+
/**
|
|
4
|
+
* Calls `run` with the env the install should use: `env` itself, or — when
|
|
5
|
+
* `NPM_TOKEN` is set and no user npmrc exists — `env` plus
|
|
6
|
+
* `NPM_CONFIG_USERCONFIG` naming a temporary reference-only npmrc, which is
|
|
7
|
+
* removed once `run` returns or throws.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withNpmTokenUserConfig<T>(env: NodeJS.ProcessEnv, home: string, run: (env: NodeJS.ProcessEnv) => T): T;
|