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
package/component-catalog.md
CHANGED
|
@@ -15,13 +15,13 @@ The component inventory of `@working-theory/ui` and the shells of `@working-theo
|
|
|
15
15
|
3. Check the Primitives, Panels and Shells tables too — a low-level control, a collapsible layout container, or a route-level frame lives there, not under Sections or Blocks.
|
|
16
16
|
4. Only then build. If you do build, you are declaring the capability is genuinely absent.
|
|
17
17
|
|
|
18
|
-
Inventory: **
|
|
18
|
+
Inventory: **303 component files** across 16 tiers — 55 sections, 99 blocks, 18 chrome, 79 primitives, 6 panels, 5 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 5 scheduling-blocks, 3 signal-collectors-sections, 8 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 300 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
|
|
19
19
|
|
|
20
|
-
Description coverage: **
|
|
20
|
+
Description coverage: **299/303** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 224 from the component's own doc-comment header, 31 from its tier barrel's roster line. **4** have none; they are listed under [Undescribed components](#undescribed-components) rather than given a sentence invented from their name.
|
|
21
21
|
|
|
22
|
-
Of those, **
|
|
22
|
+
Of those, **268** are *strong* — a capability sentence you could match a requirement against — and **31** are *weak*: either one short clause, or a long one that spends itself on wiring (client boundaries, class names, which primitives it composes) instead of on what the thing is for. Weak rows are marked `(weak)` and listed in full under [Weak descriptions](#weak-descriptions). Coverage is 99%, but **findable-without-luck coverage is 88%**.
|
|
23
23
|
|
|
24
|
-
Per tier, strong out of total: **Sections**
|
|
24
|
+
Per tier, strong out of total: **Sections** 54/55 · **Blocks** 93/99 · **Chrome** 10/18 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 4/5 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 5/5 · **Signal-Collectors Sections** 3/3 · **Signal-Collectors Blocks** 7/8 · **Commerce Sections** 2/2 · **Commerce Blocks** 12/12 · **Commerce Panels** 1/1. A tier low on this line is a tier where a capability search is most likely to come up empty on something that exists.
|
|
25
25
|
|
|
26
26
|
That grade is **mechanical**. It counts what it can measure — how much a description adds beyond the component's own name, how much of it is spent on wiring, whether the extraction came out damaged — and it cannot tell a fluent paragraph about the wrong subject from a useful one. A person reading these tables will find more rows unusable than the weak count admits. Treat it as a floor on the problem, not a measurement of it, and read the source before concluding a capability is absent.
|
|
27
27
|
|
|
@@ -53,7 +53,7 @@ The left column is search vocabulary — how a person phrases the need. It is ha
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
-
## Sections (
|
|
56
|
+
## Sections (55)
|
|
57
57
|
|
|
58
58
|
Page-level content bands. Most are MDX-driven (authored by `type` in page frontmatter and dispatched by `SectionRenderer`); a few are composed directly in a route.
|
|
59
59
|
|
|
@@ -66,6 +66,7 @@ Import: `import { X } from "@working-theory/ui/sections";` (or from the `@workin
|
|
|
66
66
|
| `AskAi` | Use above the footer — hands the visitor to a third-party AI seeded with the entity question. Bounded to five measured providers (ChatGPT/Claude/Google AI/Grok/Perplexity), each deep-linked with an authored prompt. The `ticker` variant renders a compact full-width band instead of the default headline/description/provider-row layout. | MDX `type: ask-ai` |
|
|
67
67
|
| `AuthPanel` | A sign-in/sign-up panel with an optional bounded OAuth provider row (google, github) — UI-contract only, no auth backend; submit and provider clicks are callback props the composing page wires up. | MDX `type: auth-panel` · `import { AuthPanel } from "@working-theory/ui/sections"` |
|
|
68
68
|
| `Awards` | A two-column evidence band — headline/CTA left, up to 5 award or press badges right — use for third-party recognition (badges may render as labeled placeholders until real marks are supplied). | MDX `type: awards` |
|
|
69
|
+
| `BeforeAfter` | A before/after comparison — use on a case-study or portfolio page to show what a client had and what they have now: two labeled panels behind a Side-by-side / Before / After selector, each body an image or rows of content, with an optional catalyst column between them and a key-takeaway box below. | MDX `type: before-after` · `import { BeforeAfter } from "@working-theory/ui/sections"` |
|
|
69
70
|
| `BentoGrid` | A mixed-span mosaic of feature cards — use for a visually varied capability overview. | MDX `type: bento-grid` · `import { BentoGrid } from "@working-theory/ui/sections"` |
|
|
70
71
|
| `BlogList` | A blog post list with pagination — use for the blog index page only. | MDX `type: blog-list` · `import { BlogList } from "@working-theory/ui/sections"` |
|
|
71
72
|
| `BlogPost` | A single blog post body with TOC and related posts — use only on the blog-post chassis. | MDX `type: blog-post` · `import { BlogPost } from "@working-theory/ui/sections"` |
|
|
@@ -118,7 +119,7 @@ Import: `import { X } from "@working-theory/ui/sections";` (or from the `@workin
|
|
|
118
119
|
|
|
119
120
|
---
|
|
120
121
|
|
|
121
|
-
## Blocks (
|
|
122
|
+
## Blocks (99)
|
|
122
123
|
|
|
123
124
|
Intermediate composition tiles — the pieces sections are built from. Compose primitives and panels; never import a section.
|
|
124
125
|
|
|
@@ -147,8 +148,10 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
|
|
|
147
148
|
| `ComparisonMatrix` | Full feature × tier comparison matrix for the Pricing 'with-comparison-table' variant. Renders N collapsible ComparisonGroups, each containing M feature rows with per-tier values. | `import { ComparisonMatrix } from "@working-theory/ui/blocks"` |
|
|
148
149
|
| `Composer` | The chat input, per the ploy.ai capture contract. | `import { Composer } from "@working-theory/ui/blocks"` |
|
|
149
150
|
| `ConsentGroup` | The composite control for the `consent` field kind: one real checkbox per consent purpose, each with its statement and inline policy links, inside one fieldset. | `import { ConsentGroup } from "@working-theory/ui/blocks"` |
|
|
151
|
+
| `ConsentSettingsButton` | Footer "Cookie settings" control. Lets a visitor who already accepted/declined change their mind later, from the footer, without reloading the page. Lives at the BLOCKS tier (not chrome) so `FooterBottomBar` (a block) can render it without an upward import; `BannerConsentIsland` (chrome) imports the exported event constant DOWNWARD, per the component tier hierarchy. | internal — compose from `packages/ui/src/blocks/ConsentSettingsButton.tsx` |
|
|
150
152
|
| `ConsentStatement` | The sentence a person agrees to, with its policy links inline. | `import { ConsentStatement } from "@working-theory/ui/blocks"` |
|
|
151
153
|
| `ContactChannelItem` | Renders one support channel entry: colored icon square + headline + description paragraph + CTA link (with trailing arrow icon). | `import { ContactChannelItem } from "@working-theory/ui/blocks"` |
|
|
154
|
+
| `ContactFallback` | The contact email in place of a contact form. Shown when a site cannot take messages through its form: in production with neither a database nor an email key, the page renders this instead, and `LeadCaptureForm`'s `unavailable` state renders this same markup when a submit is refused — so the two fallbacks are one component. | `import { ContactFallback } from "@working-theory/ui/blocks"` |
|
|
152
155
|
| `ContactFormFields` | Renders the form field grid (Input / Textarea / Select primitives) + submit Button + optional consent text footer. | `import { ContactFormFields } from "@working-theory/ui/blocks"` |
|
|
153
156
|
| `ContactInfoAside` | For ContactForm 'split-with-pattern' variant. Renders address / phone / email contact info rows. Each row: sr-only <dt> label + visible <dd> with Icon + Text. Phone and email render as tel:/mailto: links (click-to-call / click-to-email). | `import { ContactInfoAside } from "@working-theory/ui/blocks"` |
|
|
154
157
|
| `ConversationalIntake` | The UI half of the conversational-intake feature: renders the transcript, closed-question answer pills, a free-text form, a thinking indicator, and Skip — driven entirely through the `NextTurn` interface exported by `./engine` (merged). | `import { ConversationalIntake } from "@working-theory/ui/blocks"` |
|
|
@@ -166,6 +169,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
|
|
|
166
169
|
| `FormCard` | Stateless, centered max-w-md form-card chassis: a Card wrapping an optional title + description header, a field-stack body (children), and a footer slot for the primary action + a secondary line below it. The SHARED look composed by both the contact-form section's 'card' variant and the auth-panel section — this block owns none of their field/state machinery, only the surrounding chrome. | `import { FormCard } from "@working-theory/ui/blocks"` |
|
|
167
170
|
| `InviteMemberDialog` | The invite modal — reimplements the ploy.ai "Invite Team Member" capture (Email textbox + Role select, default Member, options Member/Admin, helper "Admins can invite and remove members." + Send Invitation). | `import { InviteMemberDialog } from "@working-theory/ui/blocks"` |
|
|
168
171
|
| `KeyPointsPanel` | ONE shared block composed by two consumers: Hero's `panel` variant (text-left / panel-right) and Pricing's `band-hero` arrangement (header-left / panel-right, tiers below). | internal — compose from `packages/ui/src/blocks/KeyPointsPanel.tsx` |
|
|
172
|
+
| `LeadCaptureForm` | The one contact form every template renders. Submits ONLY through the `action` prop — a Server Action the page binds — so there is no fetch, no URL and no browser-side JSON here. | `import { LeadCaptureForm } from "@working-theory/ui/blocks"` |
|
|
169
173
|
| `LinkCard` | A whole-tile link card: the entire tile IS the anchor (Claude-docs link-card anatomy). Optional leading icon, a title heading, and an optional description paragraph. Hover fill + chrome are intrinsic to the block — never props. | `import { LinkCard } from "@working-theory/ui/blocks"` |
|
|
170
174
|
| `LinkListItem` | Hub-page spoke list row: prose Link + optional description, enlarged to the 44px touch-target floor. Fix: the enlarged-hit-area pattern (same idiom as ShortcutTrigger, SegmentedControl's touchTargetFloor) — `min-h-(control-height-big)` clamps the anchor's clickable box to 44px while `inline-flex items-center` keeps the visible text at its normal prose size. | `import { LinkListItem } from "@working-theory/ui/blocks"` |
|
|
171
175
|
| `ListRow` | The PHONE form of one admin list item. The reference is `apps-paid/admin/design/demos/first-steps/demo.html:475` (`rowDoc`), which lays the row out as: a leading type glyph, a two-line text column with the MUTED META LINE FIRST and the item's name second (every phone screen in the demo agrees), an optional trailing control, and a chevron. | `import { ListRow } from "@working-theory/ui/blocks"` |
|
|
@@ -188,6 +192,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
|
|
|
188
192
|
| `PageHeaderSwitcher` | Header-owned context-switcher island (DropdownMenu-backed). Header-owned segment switcher rendered inside PageHeader's `switcher` slot. Composes DropdownMenu (Radix-backed — positioning, click-outside, Escape, focus management, and roving keyboard nav all come for free). | `import { PageHeaderSwitcher } from "@working-theory/ui/blocks"` |
|
|
189
193
|
| `PairingCard` | Quiet card naming a matched WaitlistRecommendation pairing: both resolved offer names + the resolved reason, joined against a caller-supplied offers[] by key. | `import { PairingCard } from "@working-theory/ui/blocks"` |
|
|
190
194
|
| `PhoneNumberField` | Single-box combo phone input for the "tel" field kind (supersedes the two-segment country-Select + number-Input layout). | `import { PhoneNumberField } from "@working-theory/ui/blocks"` |
|
|
195
|
+
| `PostCard` | Same props as PostRow, a vertical arrangement. A single linked blog-index card (cover · date · reading time · title · excerpt · "Read more"). Tier 3 block; composes MediaFigure + primitives. RSC default. *(weak)* | `import { PostCard } from "@working-theory/ui/blocks"` |
|
|
191
196
|
| `PostRow` | A single horizontal row in the two-column blog index: a landscape cover thumbnail (left) followed by the post's title · excerpt · author+date meta · tag list. The WHOLE row is one link (aria-labelled by the title, so screen readers announce the post title rather than the concatenated row content). | `import { PostRow } from "@working-theory/ui/blocks"` |
|
|
192
197
|
| `PricingExtraTierRow` | Compact horizontal extra-tier row for the Pricing 'two-tiers-with-extra-tier' variant. Renders plan name + description on the left and a CTA on the right. Structurally different from PricingTierCard (no price display in this variant). | `import { PricingExtraTierRow } from "@working-theory/ui/blocks"` |
|
|
193
198
|
| `PricingTierCard` | Single pricing tier card (all 4 Pricing variants). Renders a single pricing tier: plan name + optional badge, description, price display, CTA button, and feature checklist. | `import { PricingTierCard } from "@working-theory/ui/blocks"` |
|
|
@@ -529,13 +534,14 @@ Import: `import { X } from "@working-theory/ui-commerce";` (or from the `@workin
|
|
|
529
534
|
|
|
530
535
|
## Weak descriptions
|
|
531
536
|
|
|
532
|
-
These
|
|
537
|
+
These 31 components have a description that is accurate but does not do the job well: too short to match against, or long but spent on implementation. A capability search reaches them only if the searcher's word happens to be in it, so they are the most likely place for this catalog to repeat the failure it was built to fix. Read the source before concluding the capability is absent — and if you own one of these components, its header is the fix.
|
|
533
538
|
|
|
534
539
|
| Component | Tier | What the source gives us |
|
|
535
540
|
| --- | --- | --- |
|
|
536
541
|
| `DocumentViewerClient` | sections | The interactive shell for DocumentViewer. Split out of DocumentViewer.tsx (which stays RSC) for the same reason AlertSection composes AlertSectionDismissIsland: interactivity is contained to one client island. |
|
|
537
542
|
| `AgentContextCard` | blocks | Capability-bearing block that renders agent provenance. RSC note: no hooks or browser APIs — server-renderable by default. |
|
|
538
543
|
| `PageHeader` | blocks | RSC. Page-level header composing the Breadcrumb primitive for the left-side wayfinding trail, a generic right-side `actions` slot, and an optional `switcher` slot for header-owned segment switching. Fills AppShell's `header` slot (AdminShell and similar). No accent bar. No tab API. No named action props — the caller assembles whatever button/menu nodes it needs and passes them as `actions`. |
|
|
544
|
+
| `PostCard` | blocks | Same props as PostRow, a vertical arrangement. A single linked blog-index card (cover · date · reading time · title · excerpt · "Read more"). Tier 3 block; composes MediaFigure + primitives. RSC default. |
|
|
539
545
|
| `PricingTierCardSeatControl` | blocks | Seat-count range picker for the Team tier card. Renders a Select primitive that lets the user choose an "Up to N seats" option from a caller-supplied list of numbers. The parent PricingTierCard stays a pure RSC; only this tiny island is hydrated. Pattern mirrors PageHeaderSwitcher.tsx: small "use client" island imported by an otherwise-server-rendered parent, keeping the client bundle minimal. |
|
|
540
546
|
| `SinglePriceCard` | blocks | 2-column split card for the Pricing 'single-price-with-details' variant. |
|
|
541
547
|
| `SocialIcons` | blocks | Horizontal row of social icon links. Renders brand icons (via Icon primitive + simple-icons paths) inside accessible Link primitives. Each platform link gets an aria-label. Shared by company-mission, simple-centered, and newsletter-below footer variants, and composed by TeamMemberCard for member social links. |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleName } from "./parse-args.js";
|
|
2
|
+
/**
|
|
3
|
+
* A refusal the CLI exits 2 on — a request this command will not carry out
|
|
4
|
+
* as asked (a bundle patching outside the allowlist, an `architecture.yaml`
|
|
5
|
+
* it cannot read the module set from, a free module), as distinct from a
|
|
6
|
+
* failure. Thrown before anything is written.
|
|
7
|
+
*/
|
|
8
|
+
export declare class AddModuleRefusedError extends Error {
|
|
9
|
+
readonly exitCode = 2;
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The site's current module set and declared surfaces, read back off the
|
|
14
|
+
* generated `architecture.yaml` header (`deriveArchitecture`'s `# Modules:` /
|
|
15
|
+
* `# Surfaces:` lines). Refuses rather than guesses when either line is
|
|
16
|
+
* absent or names an unknown module — regenerating from the added module
|
|
17
|
+
* alone would silently drop every other module's boundaries.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readArchitectureHeader(archYaml: string): {
|
|
20
|
+
modules: ModuleName[];
|
|
21
|
+
surfaces: string[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two `add-module` symbols `site-roster.ts` needs as well (#6682 D1):
|
|
3
|
+
* the refusal type and the `architecture.yaml` header reader. They live
|
|
4
|
+
* here rather than in `add-module.ts` because `add-module.ts` imports
|
|
5
|
+
* `site-roster.ts` — reading them from `add-module.ts` would close an import
|
|
6
|
+
* cycle (`import/no-cycle`). `add-module.ts` re-exports both, so every
|
|
7
|
+
* existing import of them from `add-module.js` is unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import { normalizeModules } from "./module-architecture.js";
|
|
10
|
+
/**
|
|
11
|
+
* A refusal the CLI exits 2 on — a request this command will not carry out
|
|
12
|
+
* as asked (a bundle patching outside the allowlist, an `architecture.yaml`
|
|
13
|
+
* it cannot read the module set from, a free module), as distinct from a
|
|
14
|
+
* failure. Thrown before anything is written.
|
|
15
|
+
*/
|
|
16
|
+
export class AddModuleRefusedError extends Error {
|
|
17
|
+
exitCode = 2;
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "AddModuleRefusedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The site's current module set and declared surfaces, read back off the
|
|
25
|
+
* generated `architecture.yaml` header (`deriveArchitecture`'s `# Modules:` /
|
|
26
|
+
* `# Surfaces:` lines). Refuses rather than guesses when either line is
|
|
27
|
+
* absent or names an unknown module — regenerating from the added module
|
|
28
|
+
* alone would silently drop every other module's boundaries.
|
|
29
|
+
*/
|
|
30
|
+
export function readArchitectureHeader(archYaml) {
|
|
31
|
+
const modulesLine = /^# Modules: (.+)$/m.exec(archYaml)?.[1];
|
|
32
|
+
const surfacesLine = /^# Surfaces: (.+)$/m.exec(archYaml)?.[1];
|
|
33
|
+
if (modulesLine === undefined || surfacesLine === undefined) {
|
|
34
|
+
throw new AddModuleRefusedError("architecture.yaml has no `# Modules:` / `# Surfaces:` header, so the site's current module set is unknown. Regenerate it with `create-ailk --sync-architecture <modules>` first.");
|
|
35
|
+
}
|
|
36
|
+
let modules;
|
|
37
|
+
try {
|
|
38
|
+
modules = normalizeModules(modulesLine.split(","));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new AddModuleRefusedError(`architecture.yaml's \`# Modules:\` header is not a module list: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
const surfaces = surfacesLine.trim() === "core only"
|
|
44
|
+
? []
|
|
45
|
+
: surfacesLine
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((s) => s.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
return { modules, surfaces };
|
|
50
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AddModuleRefusedError, readArchitectureHeader } from "./add-module-core.js";
|
|
2
|
+
import { type InstallReportRow } from "./lib/extract-module-bundle.js";
|
|
3
|
+
import { type ModulePatch } from "./module-architecture.js";
|
|
4
|
+
import { type ModuleName } from "./parse-args.js";
|
|
5
|
+
import type { MarkerMap, WorkspaceVersionMap } from "./substitute.js";
|
|
6
|
+
import { type SyncRoutesResult } from "./sync-routes.js";
|
|
7
|
+
export { AddModuleRefusedError, readArchitectureHeader };
|
|
8
|
+
/**
|
|
9
|
+
* One report row: a bundle file written, patched or skipped, or a seed the
|
|
10
|
+
* roster append removed (a superseded template row's untouched seed, #6682 D4).
|
|
11
|
+
*/
|
|
12
|
+
export type AddModuleReportRow = InstallReportRow | {
|
|
13
|
+
path: string;
|
|
14
|
+
action: "removed";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Refuse a bundle whose `patches` reach past the plus-config allowlist
|
|
18
|
+
* (#6653 D3), matched on the target's basename so any depth qualifies:
|
|
19
|
+
* `package.json` (only `merge-keys`/`append-unique` on a dependency or script
|
|
20
|
+
* key), `next.config.*`, `jest.config.*`. Runs inside `installBundleTree`
|
|
21
|
+
* after the patches validate and before any is computed or written.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkPlusConfigPatches(patches: readonly ModulePatch[]): void;
|
|
24
|
+
export interface ApplyModuleOptions {
|
|
25
|
+
/** The already-scaffolded site (validated by the caller). */
|
|
26
|
+
targetDir: string;
|
|
27
|
+
moduleName: ModuleName;
|
|
28
|
+
/** The module's fetched bundle — this function never fetches. */
|
|
29
|
+
bundleBytes: Uint8Array;
|
|
30
|
+
/** Workspace version map, for `workspace:*` in patch targets and route copies. */
|
|
31
|
+
versions: WorkspaceVersionMap;
|
|
32
|
+
/** The scaffold's marker map (`buildScaffoldMarkers`). */
|
|
33
|
+
markers: MarkerMap;
|
|
34
|
+
/** The package's `templates/` dir, for the route sync and the roster check. */
|
|
35
|
+
templatesDir: string;
|
|
36
|
+
/** The package's `site-templates/` dir — the shipped manifests the roster rows are read from. */
|
|
37
|
+
siteTemplatesDir: string;
|
|
38
|
+
}
|
|
39
|
+
export interface AddModuleResult {
|
|
40
|
+
/** Resolved absolute path to the site. */
|
|
41
|
+
targetDir: string;
|
|
42
|
+
/** The module set `architecture.yaml` now declares. */
|
|
43
|
+
modules: ModuleName[];
|
|
44
|
+
/** One row per file written, patched, skipped or removed — empty when nothing changed. */
|
|
45
|
+
report: AddModuleReportRow[];
|
|
46
|
+
/** The route sync run over the module's surfaces. */
|
|
47
|
+
routes: SyncRoutesResult;
|
|
48
|
+
/** The page-roster append (#6682). `none` = the site has no `.ailk-site-manifest.json` (nor a pre-#6658 root `TEMPLATE-MANIFEST.json`). */
|
|
49
|
+
roster: {
|
|
50
|
+
state: "appended" | "unchanged" | "none";
|
|
51
|
+
/** Routes appended to the roster. */
|
|
52
|
+
appended: string[];
|
|
53
|
+
/** Superseded seeds removed. */
|
|
54
|
+
removed: string[];
|
|
55
|
+
/** `siteRosterProblems` after the append — empty means clean. */
|
|
56
|
+
problems: string[];
|
|
57
|
+
/**
|
|
58
|
+
* The site-relative roster file read and written — `SITE_ROSTER_PATH`, or
|
|
59
|
+
* a pre-#6658 site's root `TEMPLATE-MANIFEST.json`.
|
|
60
|
+
*/
|
|
61
|
+
path: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Merge one module's bundle onto an existing site, sync its routes,
|
|
66
|
+
* regenerate `architecture.yaml` and the nav, then append the module's pages
|
|
67
|
+
* to the site's roster. Every refusal — an unreadable header or roster, a
|
|
68
|
+
* module row no shipped manifest carries, a patch outside the allowlist, a
|
|
69
|
+
* patch conflict, an unsafe bundle — throws before the first write (the header
|
|
70
|
+
* and roster are read first; `extractModuleBundle` validates and computes
|
|
71
|
+
* everything before it copies).
|
|
72
|
+
*/
|
|
73
|
+
export declare function applyModuleToScaffold(opts: ApplyModuleOptions): Promise<AddModuleResult>;
|
|
74
|
+
/** The report `add-module` prints. */
|
|
75
|
+
export declare function formatAddModuleReport(result: AddModuleResult, moduleName: string): string;
|
|
76
|
+
export interface AddModuleArgs {
|
|
77
|
+
/** The module to add; undefined only with `help`. */
|
|
78
|
+
module: ModuleName | undefined;
|
|
79
|
+
/** Positional target directory; undefined = the caller's cwd. */
|
|
80
|
+
target: string | undefined;
|
|
81
|
+
help: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare const ADD_MODULE_HELP = "create-ailk add-module \u2014 add a purchased module to an existing site.\n\nUsage:\n create-ailk add-module <module> [<target-dir>]\n create-ailk upgrade <module> [<target-dir>]\n\nArguments:\n <module> One paid module, e.g. docs-blog. Needs\n AILK_LICENSE_KEY covering it.\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nWrites the module's files the site lacks and never overwrites one it has: a\nfile whose content differs is left as-is and listed for manual review with the\nchange the module wanted. Only package.json dependency/script keys,\nnext.config.* and jest.config.* are patched. Then copies the module's routes\nand regenerates architecture.yaml. Running it again changes nothing.\n";
|
|
84
|
+
/**
|
|
85
|
+
* Parse the argv AFTER the `add-module` / `upgrade` word. Strict
|
|
86
|
+
* `node:util` parseArgs, like `parseSyncRoutesArgs`; the module is validated
|
|
87
|
+
* the same way one `--modules` entry is.
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseAddModuleArgs(argv: string[]): AddModuleArgs;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create-ailk add-module <module>` (alias `upgrade`) — apply a purchased
|
|
3
|
+
* module to a site that was scaffolded without it, in place (#6653).
|
|
4
|
+
*
|
|
5
|
+
* A Free customer who buys Pro already has a site. `scaffold()` merges a
|
|
6
|
+
* module bundle only onto a fresh tree; this merges one onto a tree someone
|
|
7
|
+
* has lived in, under the plus-config policy (operator-disposed 2026-09-24):
|
|
8
|
+
*
|
|
9
|
+
* - a file the tree lacks is written;
|
|
10
|
+
* - a file the tree has is never overwritten — when its bytes differ from
|
|
11
|
+
* the bundle's it is reported `skipped` with the exact change the bundle
|
|
12
|
+
* wanted (a manual-review item), and when they match it is not reported;
|
|
13
|
+
* - the bundle's `patches` are the only edits to an existing file, and only
|
|
14
|
+
* to the config allowlist: `package.json` dependency/script keys,
|
|
15
|
+
* `next.config.*`, `jest.config.*`.
|
|
16
|
+
*
|
|
17
|
+
* Then the module's route surfaces are synced in (`syncRoutes` with the
|
|
18
|
+
* module's surfaces forced carried), `architecture.yaml` is regenerated from
|
|
19
|
+
* the site's current module set plus this one, and the generated nav follows
|
|
20
|
+
* the new surfaces unless the site has edited it. Run twice, the second run
|
|
21
|
+
* changes nothing and reports nothing.
|
|
22
|
+
*
|
|
23
|
+
* Pure module: no `import.meta.url`, so jest can import it directly — the
|
|
24
|
+
* package-rooted inputs (`templatesDir`, `versions`) and the fetched bundle
|
|
25
|
+
* bytes are supplied by `programmatic.ts`'s `addModule`, the same split as
|
|
26
|
+
* `sync-routes.ts` / `syncRoutesSync`.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync, } from "node:fs";
|
|
29
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
30
|
+
import { parseArgs } from "node:util";
|
|
31
|
+
import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-core.js";
|
|
32
|
+
import { deriveArchitecture } from "./derive-architecture.js";
|
|
33
|
+
import { diffTwoStrings, extractModuleBundle, } from "./lib/extract-module-bundle.js";
|
|
34
|
+
import { normalizeModules } from "./module-architecture.js";
|
|
35
|
+
import { ParseError, parseModules } from "./parse-args.js";
|
|
36
|
+
import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
37
|
+
import { MODULE_SURFACES, NAV_CONFIG_PATH, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
38
|
+
import { syncRoutes } from "./sync-routes.js";
|
|
39
|
+
export { AddModuleRefusedError, readArchitectureHeader };
|
|
40
|
+
/** `package.json` paths a `json` patch may write under the plus-config policy. */
|
|
41
|
+
const PACKAGE_JSON_PATCH_KEYS = new Set([
|
|
42
|
+
"dependencies",
|
|
43
|
+
"devDependencies",
|
|
44
|
+
"scripts",
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Refuse a bundle whose `patches` reach past the plus-config allowlist
|
|
48
|
+
* (#6653 D3), matched on the target's basename so any depth qualifies:
|
|
49
|
+
* `package.json` (only `merge-keys`/`append-unique` on a dependency or script
|
|
50
|
+
* key), `next.config.*`, `jest.config.*`. Runs inside `installBundleTree`
|
|
51
|
+
* after the patches validate and before any is computed or written.
|
|
52
|
+
*/
|
|
53
|
+
export function checkPlusConfigPatches(patches) {
|
|
54
|
+
for (const patch of patches) {
|
|
55
|
+
const name = basename(patch.target);
|
|
56
|
+
if (name === "package.json") {
|
|
57
|
+
for (const op of patch.ops) {
|
|
58
|
+
const allowed = (op.op === "merge-keys" || op.op === "append-unique") &&
|
|
59
|
+
op.path.length > 0 &&
|
|
60
|
+
PACKAGE_JSON_PATCH_KEYS.has(op.path[0]);
|
|
61
|
+
if (!allowed) {
|
|
62
|
+
throw new AddModuleRefusedError(`the module bundle patches ${patch.target} outside its dependency and script keys — add-module only patches dependencies, devDependencies and scripts there`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (/^next\.config\.[^/]+$/.test(name) ||
|
|
68
|
+
/^jest\.config\.[^/]+$/.test(name)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
throw new AddModuleRefusedError(`the module bundle patches ${patch.target}, which is outside the config files add-module may edit (package.json, next.config.*, jest.config.*)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Merge one module's bundle onto an existing site, sync its routes,
|
|
76
|
+
* regenerate `architecture.yaml` and the nav, then append the module's pages
|
|
77
|
+
* to the site's roster. Every refusal — an unreadable header or roster, a
|
|
78
|
+
* module row no shipped manifest carries, a patch outside the allowlist, a
|
|
79
|
+
* patch conflict, an unsafe bundle — throws before the first write (the header
|
|
80
|
+
* and roster are read first; `extractModuleBundle` validates and computes
|
|
81
|
+
* everything before it copies).
|
|
82
|
+
*/
|
|
83
|
+
export async function applyModuleToScaffold(opts) {
|
|
84
|
+
const targetAbs = resolve(opts.targetDir);
|
|
85
|
+
const archPath = join(targetAbs, "architecture.yaml");
|
|
86
|
+
const current = readArchitectureHeader(readFileSync(archPath, "utf8"));
|
|
87
|
+
const modules = normalizeModules([...current.modules, opts.moduleName]);
|
|
88
|
+
// Resolved before the first write: a header surface that is unknown, or
|
|
89
|
+
// belongs to a module the site does not carry, is a refusal with the tree
|
|
90
|
+
// untouched — not a failure after the bundle has landed.
|
|
91
|
+
let surfaces;
|
|
92
|
+
try {
|
|
93
|
+
surfaces = resolveSurfaces(modules, current.surfaces);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
throw new AddModuleRefusedError(`architecture.yaml's \`# Surfaces:\` header cannot be carried forward: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
// The roster and every module row it may need, likewise before the first
|
|
99
|
+
// write. A template-less site has no roster and needs none.
|
|
100
|
+
const siteRoster = readSiteRoster(targetAbs);
|
|
101
|
+
const references = siteRoster === undefined
|
|
102
|
+
? []
|
|
103
|
+
: modules.flatMap((m) => moduleRosterRows(m, opts.siteTemplatesDir));
|
|
104
|
+
const { report } = await extractModuleBundle({
|
|
105
|
+
bytes: opts.bundleBytes,
|
|
106
|
+
targetDir: targetAbs,
|
|
107
|
+
moduleName: opts.moduleName,
|
|
108
|
+
versions: opts.versions,
|
|
109
|
+
markers: opts.markers,
|
|
110
|
+
existingFilePolicy: "skip-existing",
|
|
111
|
+
checkPatches: checkPlusConfigPatches,
|
|
112
|
+
});
|
|
113
|
+
const routes = syncRoutes({
|
|
114
|
+
targetDir: targetAbs,
|
|
115
|
+
templatesDir: opts.templatesDir,
|
|
116
|
+
versions: opts.versions,
|
|
117
|
+
apply: true,
|
|
118
|
+
forceSurfaces: MODULE_SURFACES[opts.moduleName] ?? [],
|
|
119
|
+
});
|
|
120
|
+
// The same composition `syncArchitectureSync` writes — a pure function of
|
|
121
|
+
// the module set, so an unchanged set rewrites identical bytes.
|
|
122
|
+
writeFileSync(archPath, deriveArchitecture(modules, surfaces));
|
|
123
|
+
const rows = [...report];
|
|
124
|
+
const navRow = syncNavConfig(targetAbs, current.surfaces, surfaces);
|
|
125
|
+
if (navRow !== undefined)
|
|
126
|
+
rows.push(navRow);
|
|
127
|
+
const roster = {
|
|
128
|
+
state: "none",
|
|
129
|
+
appended: [],
|
|
130
|
+
removed: [],
|
|
131
|
+
problems: [],
|
|
132
|
+
path: siteRoster?.path ?? SITE_ROSTER_PATH,
|
|
133
|
+
};
|
|
134
|
+
if (siteRoster !== undefined) {
|
|
135
|
+
// After the bundle has landed: a `contentDir` row's form follows the
|
|
136
|
+
// seeds now on disk.
|
|
137
|
+
const plan = planRosterAppend({
|
|
138
|
+
targetDir: targetAbs,
|
|
139
|
+
roster: siteRoster,
|
|
140
|
+
modules,
|
|
141
|
+
references,
|
|
142
|
+
siteTemplatesDir: opts.siteTemplatesDir,
|
|
143
|
+
templatesDir: opts.templatesDir,
|
|
144
|
+
markers: opts.markers,
|
|
145
|
+
});
|
|
146
|
+
for (const seed of plan.seeds) {
|
|
147
|
+
if (seed.remove) {
|
|
148
|
+
const abs = join(targetAbs, seed.path);
|
|
149
|
+
rmSync(abs);
|
|
150
|
+
if (readdirSync(dirname(abs)).length === 0)
|
|
151
|
+
rmdirSync(dirname(abs));
|
|
152
|
+
rows.push({ path: seed.path, action: "removed" });
|
|
153
|
+
roster.removed.push(seed.path);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
rows.push({ path: seed.path, action: "skipped", wanted: seed.wanted });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (plan.next !== undefined) {
|
|
160
|
+
writeFileSync(join(targetAbs, siteRoster.path), plan.next);
|
|
161
|
+
rows.push({
|
|
162
|
+
path: siteRoster.path,
|
|
163
|
+
action: "patched",
|
|
164
|
+
change: diffTwoStrings(siteRoster.path, siteRoster.text, plan.next, true),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
roster.state = plan.next === undefined ? "unchanged" : "appended";
|
|
168
|
+
roster.appended = plan.appended;
|
|
169
|
+
roster.problems = siteRosterProblems({
|
|
170
|
+
targetDir: targetAbs,
|
|
171
|
+
siteTemplatesDir: opts.siteTemplatesDir,
|
|
172
|
+
templatesDir: opts.templatesDir,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return { targetDir: targetAbs, modules, report: rows, routes, roster };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Regenerate the site nav (`scaffoldSync` writes it from the surface set) for
|
|
179
|
+
* the new surfaces — but only when the site's copy is still exactly what the
|
|
180
|
+
* OLD surface set generates. An edited nav is the customer's file: left as-is
|
|
181
|
+
* and reported `skipped` with the change, like any other.
|
|
182
|
+
*/
|
|
183
|
+
function syncNavConfig(targetAbs, before, after) {
|
|
184
|
+
const navPath = join(targetAbs, NAV_CONFIG_PATH);
|
|
185
|
+
const wanted = renderNavConfig(after);
|
|
186
|
+
const existing = existsSync(navPath)
|
|
187
|
+
? readFileSync(navPath, "utf8")
|
|
188
|
+
: undefined;
|
|
189
|
+
if (existing === wanted)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (existing === undefined || existing === renderNavConfig(before)) {
|
|
192
|
+
writeFileSync(navPath, wanted);
|
|
193
|
+
return { path: NAV_CONFIG_PATH, action: "written" };
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
path: NAV_CONFIG_PATH,
|
|
197
|
+
action: "skipped",
|
|
198
|
+
wanted: diffTwoStrings(NAV_CONFIG_PATH, existing, wanted, true),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Indent a multi-line diff under its report row. */
|
|
202
|
+
function indent(text) {
|
|
203
|
+
return text.split("\n").map((line) => ` ${line}`);
|
|
204
|
+
}
|
|
205
|
+
/** The report `add-module` prints. */
|
|
206
|
+
export function formatAddModuleReport(result, moduleName) {
|
|
207
|
+
const written = result.report.filter((row) => row.action === "written");
|
|
208
|
+
const patched = result.report.filter((row) => row.action === "patched");
|
|
209
|
+
const skipped = result.report.filter((row) => row.action === "skipped");
|
|
210
|
+
const removed = result.report.filter((row) => row.action === "removed");
|
|
211
|
+
const lines = [
|
|
212
|
+
`[create-ailk] add-module ${moduleName} → ${result.targetDir}`,
|
|
213
|
+
];
|
|
214
|
+
if (result.report.length === 0 && result.routes.applied.length === 0) {
|
|
215
|
+
lines.push(`Nothing to add — the site already carries everything ${moduleName} brings.`);
|
|
216
|
+
}
|
|
217
|
+
if (written.length > 0) {
|
|
218
|
+
lines.push(`Written (${written.length}):`);
|
|
219
|
+
lines.push(...written.map((row) => ` ${row.path}`));
|
|
220
|
+
}
|
|
221
|
+
if (patched.length > 0) {
|
|
222
|
+
lines.push(`Patched (${patched.length}):`);
|
|
223
|
+
for (const row of patched) {
|
|
224
|
+
lines.push(` ${row.path}`);
|
|
225
|
+
if (row.action === "patched")
|
|
226
|
+
lines.push(...indent(row.change));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (removed.length > 0) {
|
|
230
|
+
lines.push(`Removed (${removed.length}):`);
|
|
231
|
+
lines.push(...removed.map((row) => ` ${row.path}`));
|
|
232
|
+
}
|
|
233
|
+
if (skipped.length > 0) {
|
|
234
|
+
lines.push(`Needs manual review (${skipped.length}) — the site's copy differs from the module's and was left untouched. Apply the change by hand if you want it:`);
|
|
235
|
+
for (const row of skipped) {
|
|
236
|
+
lines.push(` ${row.path}`);
|
|
237
|
+
if (row.action === "skipped")
|
|
238
|
+
lines.push(...indent(row.wanted));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const { routes } = result;
|
|
242
|
+
if (routes.applied.length > 0) {
|
|
243
|
+
lines.push(`Routes copied (${routes.applied.length}):`);
|
|
244
|
+
lines.push(...routes.applied.map((route) => ` ${route}`));
|
|
245
|
+
}
|
|
246
|
+
if (routes.appliedDependencies.length > 0) {
|
|
247
|
+
lines.push(`Also copied ${routes.appliedDependencies.length} file(s) those routes import:`);
|
|
248
|
+
lines.push(...routes.appliedDependencies.map((dep) => ` ${dep}`));
|
|
249
|
+
}
|
|
250
|
+
if (routes.missingMessageNamespaces.length > 0) {
|
|
251
|
+
lines.push("Also required — message namespace(s) not found in apps/web/messages/en.json (add manually, not auto-merged):");
|
|
252
|
+
lines.push(...routes.missingMessageNamespaces.map((ns) => ` ${ns}`));
|
|
253
|
+
}
|
|
254
|
+
lines.push(`architecture.yaml modules: ${result.modules.join(", ")}`);
|
|
255
|
+
lines.push(...formatRoster(result.roster, moduleName));
|
|
256
|
+
if (written.length > 0 || patched.length > 0 || routes.applied.length > 0) {
|
|
257
|
+
lines.push("Next: pnpm install, then pnpm build.");
|
|
258
|
+
}
|
|
259
|
+
return lines.join("\n") + "\n";
|
|
260
|
+
}
|
|
261
|
+
/** The page-roster lines of the report (#6682 D7). */
|
|
262
|
+
function formatRoster(roster, moduleName) {
|
|
263
|
+
if (roster.state === "none") {
|
|
264
|
+
return [
|
|
265
|
+
`Page roster: none. This site was scaffolded without a template, so it has no ${SITE_ROSTER_PATH} to append to.`,
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
const lines = [
|
|
269
|
+
roster.state === "appended" && roster.appended.length === 0
|
|
270
|
+
? `Page roster: reordered ${roster.path}'s rows into template-then-module order`
|
|
271
|
+
: roster.state === "appended"
|
|
272
|
+
? `Page roster: appended ${roster.appended.length} row(s) to ${roster.path}: ${roster.appended.join(", ")}`
|
|
273
|
+
: `Page roster: ${roster.path} already carries ${moduleName}'s rows`,
|
|
274
|
+
];
|
|
275
|
+
if (roster.problems.length === 0) {
|
|
276
|
+
lines.push("Page roster check: clean");
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
lines.push(`Page roster check: ${roster.problems.length} problem(s)`);
|
|
280
|
+
lines.push(...roster.problems.map((p) => ` ${p}`));
|
|
281
|
+
}
|
|
282
|
+
return lines;
|
|
283
|
+
}
|
|
284
|
+
export const ADD_MODULE_HELP = `create-ailk add-module — add a purchased module to an existing site.
|
|
285
|
+
|
|
286
|
+
Usage:
|
|
287
|
+
create-ailk add-module <module> [<target-dir>]
|
|
288
|
+
create-ailk upgrade <module> [<target-dir>]
|
|
289
|
+
|
|
290
|
+
Arguments:
|
|
291
|
+
<module> One paid module, e.g. docs-blog. Needs
|
|
292
|
+
AILK_LICENSE_KEY covering it.
|
|
293
|
+
<target-dir> An already-scaffolded repo (contains
|
|
294
|
+
architecture.yaml). Defaults to the current dir.
|
|
295
|
+
|
|
296
|
+
Options:
|
|
297
|
+
--help, -h Print this message.
|
|
298
|
+
|
|
299
|
+
Writes the module's files the site lacks and never overwrites one it has: a
|
|
300
|
+
file whose content differs is left as-is and listed for manual review with the
|
|
301
|
+
change the module wanted. Only package.json dependency/script keys,
|
|
302
|
+
next.config.* and jest.config.* are patched. Then copies the module's routes
|
|
303
|
+
and regenerates architecture.yaml. Running it again changes nothing.
|
|
304
|
+
`;
|
|
305
|
+
/**
|
|
306
|
+
* Parse the argv AFTER the `add-module` / `upgrade` word. Strict
|
|
307
|
+
* `node:util` parseArgs, like `parseSyncRoutesArgs`; the module is validated
|
|
308
|
+
* the same way one `--modules` entry is.
|
|
309
|
+
*/
|
|
310
|
+
export function parseAddModuleArgs(argv) {
|
|
311
|
+
const parsed = parseArgs({
|
|
312
|
+
args: argv,
|
|
313
|
+
strict: true,
|
|
314
|
+
allowPositionals: true,
|
|
315
|
+
options: {
|
|
316
|
+
help: { type: "boolean", default: false, short: "h" },
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
const help = parsed.values.help === true;
|
|
320
|
+
const [raw, target, ...rest] = parsed.positionals;
|
|
321
|
+
if (rest.length > 0) {
|
|
322
|
+
throw new ParseError(`expected a module and at most one target directory, got ${parsed.positionals.length} arguments`);
|
|
323
|
+
}
|
|
324
|
+
if (raw === undefined) {
|
|
325
|
+
if (help)
|
|
326
|
+
return { module: undefined, target: undefined, help };
|
|
327
|
+
throw new ParseError("add-module needs a module name, e.g. `create-ailk add-module docs-blog`");
|
|
328
|
+
}
|
|
329
|
+
const modules = parseModules(raw);
|
|
330
|
+
if (modules.length !== 1) {
|
|
331
|
+
throw new ParseError(`add-module takes exactly one module, got "${raw}" — run it once per module`);
|
|
332
|
+
}
|
|
333
|
+
return { module: modules[0], target, help };
|
|
334
|
+
}
|