create-ailk 0.7.0 → 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.
Files changed (169) hide show
  1. package/README.md +44 -10
  2. package/component-catalog.json +520 -5
  3. package/component-catalog.md +9 -6
  4. package/dist/cli.js +22 -24
  5. package/dist/lib/extract-module-bundle.js +5 -1
  6. package/dist/lib/module-license-gate.d.ts +79 -0
  7. package/dist/lib/module-license-gate.js +160 -12
  8. package/dist/lib/module-tier.d.ts +45 -16
  9. package/dist/lib/module-tier.js +76 -0
  10. package/dist/lib/pro-callout.js +10 -2
  11. package/dist/parse-args.d.ts +78 -10
  12. package/dist/parse-args.js +204 -14
  13. package/dist/programmatic.d.ts +39 -9
  14. package/dist/programmatic.js +163 -25
  15. package/dist/site-roster.js +11 -6
  16. package/dist/starter-grid.d.ts +216 -0
  17. package/dist/starter-grid.js +449 -0
  18. package/dist/surfaces.d.ts +4 -4
  19. package/dist/surfaces.js +102 -43
  20. package/dist/template-manifest.d.ts +36 -17
  21. package/dist/template-manifest.js +121 -367
  22. package/dist/template-names.d.ts +14 -12
  23. package/dist/template-names.js +13 -17
  24. package/package.json +3 -3
  25. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
  26. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  27. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  28. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  29. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  30. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  32. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  33. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  34. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  35. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  36. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  37. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  38. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  39. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  40. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  41. package/site-templates/designs/standard/DESIGN.md +5 -0
  42. package/site-templates/marketing-site/DESIGN.md +5 -0
  43. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
  44. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
  45. package/site-templates/marketing-site-free/DESIGN.md +5 -0
  46. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
  48. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  49. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  50. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
  51. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
  52. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  53. package/site-templates/saas-product/DESIGN.md +5 -0
  54. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  55. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
  56. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
  57. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  58. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  59. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  60. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  61. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  62. package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  63. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  64. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  65. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  66. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  67. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  68. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
  69. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
  70. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  71. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  72. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  73. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  74. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  75. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  76. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  77. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  78. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  79. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  80. package/site-templates/shapes/organization-service/provenance.json +4 -0
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  82. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  83. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  84. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  85. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  86. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  87. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  88. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  89. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  90. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  91. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  92. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  93. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  94. package/site-templates/shapes/person-service/provenance.json +4 -0
  95. package/templates/.claude/skills/README.md +3 -2
  96. package/templates/.claude/skills/add-page/references/page-types.md +4 -0
  97. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  98. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  99. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  100. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
  101. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  102. package/templates/DESIGN.md +5 -0
  103. package/templates/README.md +46 -38
  104. package/templates/apps/api/CLAUDE.md +3 -2
  105. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  106. package/templates/apps/api/src/config/index.ts +35 -16
  107. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  108. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  109. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  110. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  111. package/templates/apps/api/src/openapi/spec.ts +63 -12
  112. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  113. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  114. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  115. package/templates/apps/api/src/server.ts +7 -0
  116. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  117. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  118. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  119. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  120. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  121. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  122. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  123. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  124. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  125. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  126. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  127. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  128. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  129. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  130. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  131. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  132. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  133. package/templates/apps/web/app/[locale]/layout.tsx +37 -5
  134. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  135. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  136. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  137. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  138. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  139. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  140. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
  141. package/templates/apps/web/lib/nav-config.ts +48 -36
  142. package/templates/apps/web/lib/site-identity.ts +14 -0
  143. package/templates/apps/web/messages/en.json +28 -2
  144. package/templates/content/_site.mdx +4 -2
  145. package/templates/content/en/BlogList/blog.mdx +37 -16
  146. package/templates/content/en/ContactPage/contact.mdx +14 -44
  147. package/templates/content/en/HomePage/home.mdx +8 -16
  148. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  149. package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
  150. package/templates/database/CHANGELOG.md +7 -0
  151. package/templates/database/package.json +1 -1
  152. package/versions.json +33 -33
  153. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  154. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  155. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  156. package/site-templates/simple-service/DESIGN.md +0 -162
  157. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  158. package/site-templates/simple-service/ailk-template.json +0 -11
  159. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  160. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  161. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  162. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  163. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  164. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  165. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  166. package/site-templates/simple-service/provenance.json +0 -4
  167. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  168. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  169. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
@@ -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: **300 component files** across 16 tiers — 54 sections, 97 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. 297 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
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: **296/300** components carry a capability description derived from source — 43 from the section registry's hand-authored when-to-use text, 222 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.
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, **265** 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%**.
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** 53/54 · **Blocks** 91/97 · **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.
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 (54)
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 (97)
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
 
@@ -150,6 +151,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
150
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` |
151
152
  | `ConsentStatement` | The sentence a person agrees to, with its policy links inline. | `import { ConsentStatement } from "@working-theory/ui/blocks"` |
152
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"` |
153
155
  | `ContactFormFields` | Renders the form field grid (Input / Textarea / Select primitives) + submit Button + optional consent text footer. | `import { ContactFormFields } from "@working-theory/ui/blocks"` |
154
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"` |
155
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"` |
@@ -167,6 +169,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
167
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"` |
168
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"` |
169
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"` |
170
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"` |
171
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"` |
172
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"` |
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ import { fileURLToPath } from "node:url";
37
37
  import { ADD_MODULE_HELP, AddModuleRefusedError, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
38
38
  import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
39
39
  import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
40
- import { helpText, parseCliArgs, parseModules } from "./parse-args.js";
40
+ import { helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
41
41
  import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncRoutesSync, } from "./programmatic.js";
42
42
  import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
43
43
  import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
@@ -141,7 +141,9 @@ async function main() {
141
141
  readStdin: () => readFileSync(0, "utf8"),
142
142
  }));
143
143
  }
144
- const opts = parseCliArgs(argv);
144
+ // No silent default (#6754 D2/D3): a scaffold run naming neither --shape
145
+ // nor --template asks for a shape on a terminal and is refused without one.
146
+ const opts = await resolveCliShape(parseCliArgs(argv, { interactive: process.stdin.isTTY === true }), { input: process.stdin, output: process.stdout });
145
147
  // Deprecation notice for a retired `--surfaces` id (#5563 D2). `surfaces.ts`
146
148
  // stays pure — `canonicalizeSurface` only resolves the alias — so this is
147
149
  // the one place that prints: compare the raw `--surfaces` argv token
@@ -209,23 +211,16 @@ async function main() {
209
211
  // scaffold() gates + fetches any requested paid module (K1, #5089) and any
210
212
  // named site template's own bundle (#5481), then validates and copies the
211
213
  // local template — throws ValidationError or LicenseRequiredError on
212
- // rejection (caught at top level below). Neither AILK_LICENSE_KEY nor
213
- // either registry URL is read, and no network call is made, when the
214
- // effective module set names no paid module — the regression floor, which
215
- // `--template marketing-site-free` deliberately rides (record D11): its
214
+ // rejection (caught at top level below). The network invariant (#6759 D6):
215
+ // no key → no request at all; a key with no --modules → the D4 level probe
216
+ // (3-4 requests); an explicit --modules list → requests only for the paid
217
+ // modules it names, exactly as before this card. `--template
218
+ // marketing-site-free` still rides the no-key case (record D11): its
216
219
  // `requires` is all-free, so it installs from the shipped
217
220
  // `site-templates/` tree with no key and no request.
218
- const { targetDir: targetAbs, surfaces } = await scaffold({
219
- targetDir: opts.target,
220
- modules: opts.modules,
221
- surfaces: opts.surfaces,
222
- withCatalog: opts.withCatalog,
223
- // Spread rather than passed as `undefined` so a run naming no template
224
- // hands `scaffold()` exactly the option object it got before the flag
225
- // existed (spec 5481 § 3 DD3's byte-identity floor).
226
- ...(opts.template !== undefined ? { template: opts.template } : {}),
227
- ...(opts.license !== undefined ? { license: opts.license } : {}),
228
- });
221
+ const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, } = await scaffold(toScaffoldOptions(opts));
222
+ // The starter scaffold() resolved — from --template, or from --shape (#6754 D4).
223
+ const templateName = starter?.template ?? opts.template;
229
224
  if (opts.install) {
230
225
  // Hard-coded argv. User input only flows through `cwd`, never the command.
231
226
  const result = spawnSync("pnpm", ["install"], {
@@ -238,16 +233,19 @@ async function main() {
238
233
  }
239
234
  }
240
235
  // Next-steps message. A licensed install (#6480) is not told it needs no
241
- // licence key, and is not pitched the Pro tier it already owns.
242
- const licensed = isLicensedInstall({
243
- ...(opts.template !== undefined ? { template: opts.template } : {}),
244
- modules: opts.modules,
245
- });
236
+ // licence key, and is not pitched the Pro tier it already owns. Reads the
237
+ // EFFECTIVE module set (#6759 D6) — result.modules already carries any
238
+ // template's requires and the key's derived level, so it stays correct for
239
+ // a keyed default scaffold, which opts.modules alone would not have shown.
240
+ const licensed = isLicensedInstall({ modules: effectiveModules });
246
241
  const lines = [
247
242
  "",
248
- opts.template !== undefined
249
- ? `Scaffolded AI Launch Kit starter from the "${opts.template}" template.`
243
+ templateName !== undefined
244
+ ? `Scaffolded AI Launch Kit starter from the "${templateName}" template.`
250
245
  : "Scaffolded AI Launch Kit starter.",
246
+ ...(starter !== undefined
247
+ ? [`Starter: shape ${starter.shape}, design ${starter.design}.`]
248
+ : []),
251
249
  "",
252
250
  ];
253
251
  // Prerequisites (#6440 AC4) — every one the scaffold enforces, and nothing
@@ -655,6 +655,11 @@ export function installBundleTree(options) {
655
655
  * path-safety check is needed here — `srcRoot` is our own staging tree, not
656
656
  * attacker-controlled input.
657
657
  *
658
+ * A directory is created only as the parent of a file actually copied, never
659
+ * for its own sake: a source directory whose every file is excluded (mapped
660
+ * away) leaves nothing behind at its original path (#6787 — an empty root
661
+ * `content/en/blog/` reads as an unregistered page type).
662
+ *
658
663
  * `markers`, when given, is substituted into every TEXT file on the way
659
664
  * through (#6460) — see `InstallBundleTreeOptions.markers`.
660
665
  */
@@ -669,7 +674,6 @@ function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
669
674
  const rel = relative(srcRoot, srcPath);
670
675
  const destPath = join(destRoot, rel);
671
676
  if (entry.isDirectory()) {
672
- mkdirSync(destPath, { recursive: true });
673
677
  stack.push(srcPath);
674
678
  }
675
679
  else if (entry.isFile()) {
@@ -16,8 +16,14 @@
16
16
  * `resolvePaidModules` returns immediately, before touching `process.env` or
17
17
  * the network, so a bare/all-Free scaffold is provably unaffected by this
18
18
  * gate (K1's regression floor).
19
+ *
20
+ * Also carries the design gate (#6760): `assertDesignAllowed` refuses a
21
+ * non-`standard` design at the `free` level, naming the design and the
22
+ * run's level, before `scaffold()`'s first disk write.
19
23
  */
20
24
  import type { ModuleName } from "../parse-args.js";
25
+ import type { StarterDesign } from "../template-manifest.js";
26
+ import { type Level } from "./module-tier.js";
21
27
  /**
22
28
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
23
29
  * `/pro` route and there will not be one (#6439, operator decision OD-2) —
@@ -33,6 +39,18 @@ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
33
39
  export declare class LicenseRequiredError extends Error {
34
40
  constructor(message: string);
35
41
  }
42
+ /** Designs every level renders (#6760 D1). Anything else in `STARTER_DESIGNS` is Pro and up. */
43
+ export declare const FREE_DESIGNS: readonly StarterDesign[];
44
+ /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
45
+ export declare const DESIGN_LEVELS: readonly Level[];
46
+ /**
47
+ * Refuse a non-`standard` design at a level below Pro (#6760 D1, D6). Pure
48
+ * and synchronous — takes the level `deriveLevel`/`resolveModuleSet` already
49
+ * resolved; never reads a key or derives a level itself (D2, fail-closed:
50
+ * both checks below are allow-lists, so an unrecognized `level` refuses
51
+ * rather than granting it).
52
+ */
53
+ export declare function assertDesignAllowed(design: StarterDesign | undefined, level: Level): void;
36
54
  export interface ResolvePaidModulesOptions {
37
55
  /** The requested module set's paid subset (see `module-tier.ts`). */
38
56
  paidModules: ModuleName[];
@@ -52,7 +70,20 @@ export interface ResolvePaidModulesOptions {
52
70
  * to do. Absent ⟹ a plain `--modules` run, whose messages are unchanged.
53
71
  */
54
72
  templateName?: string;
73
+ /**
74
+ * The key's derived level (#6759 D5), when one was derived on this call's
75
+ * path. Absent on the `--modules`-keep branch, where no level is derived
76
+ * (NFR-2) — the refusal then names only the module and its own level,
77
+ * never the caller's.
78
+ */
79
+ level?: Level;
55
80
  }
81
+ /**
82
+ * The message phrase for one module (D5): what level includes it, or that it
83
+ * is an add-on. Pure — derived from `levelOf`, never from the current run's
84
+ * own derived level.
85
+ */
86
+ export declare function levelPhrase(m: ModuleName): string;
56
87
  /**
57
88
  * Resolve every requested paid module to its fetched bundle bytes, or throw
58
89
  * `LicenseRequiredError` naming exactly why the run cannot proceed. Never
@@ -61,3 +92,51 @@ export interface ResolvePaidModulesOptions {
61
92
  * BEFORE the scaffold copy — see `programmatic.ts`'s `scaffold()`).
62
93
  */
63
94
  export declare function resolvePaidModules(options: ResolvePaidModulesOptions): Promise<Map<ModuleName, Uint8Array>>;
95
+ /** Inputs shared by `deriveLevel` and `resolveModuleSet` (D4/D5). */
96
+ export interface GateOptions {
97
+ /** Defaults to `process.env["AILK_LICENSE_KEY"]`. Injectable for tests. */
98
+ licenseKey?: string | undefined;
99
+ /** Forwarded to `fetchModule`; defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
100
+ registryUrl?: string;
101
+ /** Forwarded to `fetchModule`; defaults to this package's own version. */
102
+ version?: string;
103
+ /** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
104
+ fetchImpl?: typeof fetch;
105
+ }
106
+ /**
107
+ * Derive the key's level from what the registry actually answers (D4) — the
108
+ * level only chooses which modules to ASK FOR; every byte is still
109
+ * authorized by the registry per module. Nothing client-side is an
110
+ * entitlement claim: the JWT is never parsed here.
111
+ *
112
+ * No key ⟹ `"free"`, zero network. A present key is shape-checked first,
113
+ * whichever source it came from (SEC-2). Then: probe every paid member of
114
+ * `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
115
+ * (a key that does not unlock all of Pro is not silently downgraded); any
116
+ * other failure (404, 5xx, unreachable) is wrapped so the caller sees it
117
+ * could not determine the level, and is told the escape hatch (`--modules`).
118
+ * Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
119
+ * there is the expected "this key is Pro, not Pro Plus" answer, not a
120
+ * refusal.
121
+ */
122
+ export declare function deriveLevel(options: GateOptions): Promise<{
123
+ level: Level;
124
+ bundles: Map<ModuleName, Uint8Array>;
125
+ }>;
126
+ /**
127
+ * The one entry point that turns `--modules` + the key into the effective
128
+ * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
129
+ * calls this for every run except a named `--template` alias (NFR-4).
130
+ *
131
+ * A named list with a non-addition (the KEEP branch, D2) resolves it exactly
132
+ * as today, with no level derived and no extra request (NFR-2). Otherwise the
133
+ * level is derived first, the set composed from it, and any named addition
134
+ * not already fetched during the level probe is resolved on top.
135
+ */
136
+ export declare function resolveModuleSet(options: GateOptions & {
137
+ named?: readonly ModuleName[] | undefined;
138
+ }): Promise<{
139
+ modules: ModuleName[];
140
+ bundles: Map<ModuleName, Uint8Array>;
141
+ level?: Level;
142
+ }>;
@@ -1,4 +1,6 @@
1
1
  import { FetchModuleError, fetchModule } from "./fetch-module.js";
2
+ import { assertLicenseKeyShape } from "./license-key-files.js";
3
+ import { ADD_ONS, LEVEL_LABEL, LEVEL_MODULES, OPT_IN_MODULES, PAID_MODULES, classifyModules, composeModuleSet, levelOf, } from "./module-tier.js";
2
4
  /**
3
5
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
4
6
  * `/pro` route and there will not be one (#6439, operator decision OD-2) —
@@ -17,6 +19,53 @@ export class LicenseRequiredError extends Error {
17
19
  this.name = "LicenseRequiredError";
18
20
  }
19
21
  }
22
+ /** Designs every level renders (#6760 D1). Anything else in `STARTER_DESIGNS` is Pro and up. */
23
+ export const FREE_DESIGNS = ["standard"];
24
+ /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
25
+ export const DESIGN_LEVELS = ["pro", "pro-plus"];
26
+ /**
27
+ * Refuse a non-`standard` design at a level below Pro (#6760 D1, D6). Pure
28
+ * and synchronous — takes the level `deriveLevel`/`resolveModuleSet` already
29
+ * resolved; never reads a key or derives a level itself (D2, fail-closed:
30
+ * both checks below are allow-lists, so an unrecognized `level` refuses
31
+ * rather than granting it).
32
+ */
33
+ export function assertDesignAllowed(design, level) {
34
+ if (design === undefined || FREE_DESIGNS.includes(design))
35
+ return;
36
+ if (DESIGN_LEVELS.includes(level))
37
+ return;
38
+ throw new LicenseRequiredError(`design "${design}" is a Pro feature — this run's licence level is ${LEVEL_LABEL[level]}; pass --license <key> or set AILK_LICENSE_KEY with a Pro or Pro Plus key → ${CONVERSION_HINT_URL}`);
39
+ }
40
+ /**
41
+ * The message phrase for one module (D5): what level includes it, or that it
42
+ * is an add-on. Pure — derived from `levelOf`, never from the current run's
43
+ * own derived level.
44
+ */
45
+ export function levelPhrase(m) {
46
+ const level = levelOf(m);
47
+ if (level === "add-on") {
48
+ return `"${m}" is an add-on, bought on top of Pro or Pro Plus`;
49
+ }
50
+ if (level === "free") {
51
+ return `"${m}" is included at ${LEVEL_LABEL.free}`;
52
+ }
53
+ return `"${m}" is included at ${LEVEL_LABEL[level]}`;
54
+ }
55
+ /** M1 / M1t (D5): the no-key refusal, with or without a named template. */
56
+ function noKeyMessage(paidModules, templateName) {
57
+ const phrases = paidModules.map((m) => levelPhrase(m)).join("; ");
58
+ const intro = templateName !== undefined
59
+ ? `template "${templateName}" needs a licence: ${phrases}`
60
+ : `no licence key, so this is a Free scaffold: ${phrases}`;
61
+ return `${intro} — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
62
+ }
63
+ /** M2 (D5): a key the registry rejected for one module. */
64
+ function licenseErrorMessage(err, moduleName, templateName, level) {
65
+ const templateSuffix = templateName !== undefined ? ` (required by template "${templateName}")` : "";
66
+ const levelSuffix = level !== undefined ? ` — your licence is at ${LEVEL_LABEL[level]}` : "";
67
+ return `${err.message}; ${levelPhrase(moduleName)}${templateSuffix}${levelSuffix} → ${CONVERSION_HINT_URL}`;
68
+ }
20
69
  /**
21
70
  * Resolve every requested paid module to its fetched bundle bytes, or throw
22
71
  * `LicenseRequiredError` naming exactly why the run cannot proceed. Never
@@ -33,8 +82,11 @@ export async function resolvePaidModules(options) {
33
82
  }
34
83
  const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
35
84
  if (token === undefined || token.length === 0) {
36
- throw new LicenseRequiredError(conversionHint(paidModules, options.templateName));
85
+ throw new LicenseRequiredError(noKeyMessage(paidModules, options.templateName));
37
86
  }
87
+ // SEC-2 — the `--license` flag and the env var are validated alike,
88
+ // whichever source this token came from, before it reaches a request.
89
+ assertLicenseKeyShape(token);
38
90
  const bundles = new Map();
39
91
  for (const moduleName of paidModules) {
40
92
  try {
@@ -55,11 +107,9 @@ export async function resolvePaidModules(options) {
55
107
  if (err instanceof FetchModuleError) {
56
108
  if (err.isLicenseError) {
57
109
  // DD5 row 2: the mapped 401/403 already names the module the key
58
- // does not cover; the template is appended so the buyer can see
59
- // WHICH of the things they asked for needs it.
60
- throw new LicenseRequiredError(options.templateName !== undefined
61
- ? `${err.message}, required by template "${options.templateName}" → ${CONVERSION_HINT_URL}`
62
- : `${err.message} → ${CONVERSION_HINT_URL}`);
110
+ // does not cover; the template and level are appended so the buyer
111
+ // can see WHICH of the things they asked for needs it.
112
+ throw new LicenseRequiredError(licenseErrorMessage(err, moduleName, options.templateName, options.level));
63
113
  }
64
114
  // Not a license-gating signal (e.g. 404 "not yet available", or the
65
115
  // registry unreachable) — a generic failure, not the honest-denial
@@ -72,10 +122,108 @@ export async function resolvePaidModules(options) {
72
122
  }
73
123
  return bundles;
74
124
  }
75
- function conversionHint(paidModules, templateName) {
76
- const list = paidModules.join(", ");
77
- const requested = templateName !== undefined
78
- ? `template "${templateName}" requires Pro module(s) (${list})`
79
- : `Pro module(s) requested (${list})`;
80
- return `${requested} — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
125
+ function forward(options) {
126
+ return {
127
+ ...(options.registryUrl !== undefined ? { registryUrl: options.registryUrl } : {}),
128
+ ...(options.version !== undefined ? { version: options.version } : {}),
129
+ ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
130
+ };
131
+ }
132
+ /**
133
+ * Derive the key's level from what the registry actually answers (D4) — the
134
+ * level only chooses which modules to ASK FOR; every byte is still
135
+ * authorized by the registry per module. Nothing client-side is an
136
+ * entitlement claim: the JWT is never parsed here.
137
+ *
138
+ * No key ⟹ `"free"`, zero network. A present key is shape-checked first,
139
+ * whichever source it came from (SEC-2). Then: probe every paid member of
140
+ * `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
141
+ * (a key that does not unlock all of Pro is not silently downgraded); any
142
+ * other failure (404, 5xx, unreachable) is wrapped so the caller sees it
143
+ * could not determine the level, and is told the escape hatch (`--modules`).
144
+ * Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
145
+ * there is the expected "this key is Pro, not Pro Plus" answer, not a
146
+ * refusal.
147
+ */
148
+ export async function deriveLevel(options) {
149
+ const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
150
+ if (token === undefined || token.length === 0) {
151
+ return { level: "free", bundles: new Map() };
152
+ }
153
+ assertLicenseKeyShape(token);
154
+ const bundles = new Map();
155
+ const proModules = LEVEL_MODULES.pro.filter((m) => PAID_MODULES.includes(m));
156
+ for (const moduleName of proModules) {
157
+ try {
158
+ const bytes = await fetchModule({ moduleName, token, ...forward(options) });
159
+ bundles.set(moduleName, bytes);
160
+ }
161
+ catch (err) {
162
+ if (err instanceof FetchModuleError) {
163
+ if (err.isLicenseError) {
164
+ throw new LicenseRequiredError(licenseErrorMessage(err, moduleName));
165
+ }
166
+ throw new FetchModuleError(`could not determine your licence level: ${err.message} — name the modules you want with --modules`);
167
+ }
168
+ throw err;
169
+ }
170
+ }
171
+ try {
172
+ const bytes = await fetchModule({
173
+ moduleName: "multi-tenant",
174
+ token,
175
+ ...forward(options),
176
+ });
177
+ bundles.set("multi-tenant", bytes);
178
+ return { level: "pro-plus", bundles };
179
+ }
180
+ catch (err) {
181
+ if (err instanceof FetchModuleError) {
182
+ if (err.isLicenseError) {
183
+ return { level: "pro", bundles };
184
+ }
185
+ throw new FetchModuleError(`could not determine your licence level: ${err.message} — name the modules you want with --modules`);
186
+ }
187
+ throw err;
188
+ }
189
+ }
190
+ /**
191
+ * The one entry point that turns `--modules` + the key into the effective
192
+ * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
193
+ * calls this for every run except a named `--template` alias (NFR-4).
194
+ *
195
+ * A named list with a non-addition (the KEEP branch, D2) resolves it exactly
196
+ * as today, with no level derived and no extra request (NFR-2). Otherwise the
197
+ * level is derived first, the set composed from it, and any named addition
198
+ * not already fetched during the level probe is resolved on top.
199
+ */
200
+ export async function resolveModuleSet(options) {
201
+ const namedList = options.named ?? [];
202
+ const additions = [...ADD_ONS, ...OPT_IN_MODULES];
203
+ const isKeep = namedList.some((m) => !additions.includes(m));
204
+ if (isKeep) {
205
+ const modules = composeModuleSet(namedList, "free");
206
+ const { paid } = classifyModules(modules);
207
+ const bundles = await resolvePaidModules({
208
+ paidModules: paid,
209
+ licenseKey: options.licenseKey,
210
+ ...forward(options),
211
+ });
212
+ return { modules, bundles };
213
+ }
214
+ const { level, bundles } = await deriveLevel(options);
215
+ const modules = composeModuleSet(namedList, level);
216
+ const { paid } = classifyModules(modules);
217
+ const missing = paid.filter((m) => !bundles.has(m));
218
+ if (missing.length > 0) {
219
+ const extra = await resolvePaidModules({
220
+ paidModules: missing,
221
+ licenseKey: options.licenseKey,
222
+ level,
223
+ ...forward(options),
224
+ });
225
+ for (const [k, v] of extra)
226
+ bundles.set(k, v);
227
+ }
228
+ return { modules, bundles, level };
81
229
  }
@@ -1,19 +1,3 @@
1
- /**
2
- * Free vs paid module classification (K1, #5089 — REL-015.3 "AI Launch Kit —
3
- * Commercial line", D-17 "Flavor 3").
4
- *
5
- * The commercial line is decided: the paid modules do NOT ship on public npm
6
- * — delivery itself is the gate. `create-ailk` scaffolds Free's modules from
7
- * `templates/` with no key (today's path, unchanged); a paid module is
8
- * FETCHED from the internal module distribution when `AILK_LICENSE_KEY`
9
- * verifies (see `fetch-module.ts` + `module-license-gate.ts`).
10
- *
11
- * `FREE_MODULES` is the one hand-authored list (today just `marketing`, the
12
- * always-available default). `PAID_MODULES` is derived from it — every other
13
- * `RECOGNIZED_MODULES` entry — so adding a new module to the vocabulary
14
- * (`parse-args.ts`) defaults it to "paid" until someone deliberately adds it
15
- * to `FREE_MODULES`, rather than silently shipping it free.
16
- */
17
1
  import { type ModuleName } from "../parse-args.js";
18
2
  /** Modules scaffolded from `templates/` with no license — today's free path. */
19
3
  export declare const FREE_MODULES: readonly ModuleName[];
@@ -24,6 +8,51 @@ export declare const FREE_MODULES: readonly ModuleName[];
24
8
  * from `RECOGNIZED_MODULES` silently.
25
9
  */
26
10
  export declare const PAID_MODULES: readonly ModuleName[];
11
+ /**
12
+ * The levels a key can be at (#6759 D2). A value, never read from a flag, a
13
+ * manifest or a key claim — it is always DERIVED, by `deriveLevel`
14
+ * (`module-license-gate.ts`), from what the registry actually answers.
15
+ */
16
+ export declare const LEVELS: readonly ["free", "pro", "pro-plus"];
17
+ export type Level = (typeof LEVELS)[number];
18
+ export declare const LEVEL_LABEL: Readonly<Record<Level, string>>;
19
+ /**
20
+ * Bought per site on top of a paid level — never switched on BY a level
21
+ * (OFFERINGS §4a). Naming one in `--modules` adds it without trimming
22
+ * (`composeModuleSet`), and it is refused below Pro naming the level it is
23
+ * an add-on ON TOP of.
24
+ */
25
+ export declare const ADD_ONS: readonly ModuleName[];
26
+ /**
27
+ * Included at Pro and above (the licence manifest's `pro` tier), but not
28
+ * switched on by default at any level (D1) — an authed app surface
29
+ * contributing no PUBLIC page to a marketing site. Naming it in `--modules`
30
+ * adds it without trimming, exactly like an add-on.
31
+ */
32
+ export declare const OPT_IN_MODULES: readonly ModuleName[];
33
+ /**
34
+ * What a key at each level switches on with no `--modules` (D2). Derived —
35
+ * every list is `RECOGNIZED_MODULES.filter(...)`, so declaration order is
36
+ * preserved and a module added to the vocabulary later lands in `pro` by
37
+ * derivation (loud: `module-tier.test.ts`'s table test pins the exact sets).
38
+ */
39
+ export declare const LEVEL_MODULES: Readonly<Record<Level, readonly ModuleName[]>>;
40
+ /**
41
+ * Which level (or add-on status) a module belongs to (D2). `"free"` for
42
+ * `FREE_MODULES`, `"add-on"` for `ADD_ONS`, `"pro-plus"` for
43
+ * `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — D1). Pure.
44
+ */
45
+ export declare function levelOf(m: ModuleName): Level | "add-on";
46
+ /**
47
+ * The pure core of FR-2/FR-3 (D2): compose the effective module set from
48
+ * what `--modules` named and the key's derived level. Naming any module that
49
+ * is not an addition (`ADDITIONS`) is a KEEP — it names exactly the set,
50
+ * unchanged from today (NFR-2), and the level plays no part. Naming only
51
+ * additions (or nothing) takes the level's default plus those additions.
52
+ * Never refuses — refusals belong to the gate (`resolveModuleSet`), because
53
+ * whether a named module is covered is a registry answer, not a table lookup.
54
+ */
55
+ export declare function composeModuleSet(named: readonly ModuleName[] | undefined, level: Level): ModuleName[];
27
56
  export interface ModuleClassification {
28
57
  free: ModuleName[];
29
58
  paid: ModuleName[];
@@ -14,6 +14,7 @@
14
14
  * (`parse-args.ts`) defaults it to "paid" until someone deliberately adds it
15
15
  * to `FREE_MODULES`, rather than silently shipping it free.
16
16
  */
17
+ import { normalizeModules } from "../module-architecture.js";
17
18
  import { RECOGNIZED_MODULES } from "../parse-args.js";
18
19
  /** Modules scaffolded from `templates/` with no license — today's free path. */
19
20
  export const FREE_MODULES = ["marketing"];
@@ -24,6 +25,81 @@ export const FREE_MODULES = ["marketing"];
24
25
  * from `RECOGNIZED_MODULES` silently.
25
26
  */
26
27
  export const PAID_MODULES = RECOGNIZED_MODULES.filter((m) => !FREE_MODULES.includes(m));
28
+ /**
29
+ * The levels a key can be at (#6759 D2). A value, never read from a flag, a
30
+ * manifest or a key claim — it is always DERIVED, by `deriveLevel`
31
+ * (`module-license-gate.ts`), from what the registry actually answers.
32
+ */
33
+ export const LEVELS = ["free", "pro", "pro-plus"];
34
+ export const LEVEL_LABEL = {
35
+ free: "Free",
36
+ pro: "Pro",
37
+ "pro-plus": "Pro Plus",
38
+ };
39
+ /**
40
+ * Bought per site on top of a paid level — never switched on BY a level
41
+ * (OFFERINGS §4a). Naming one in `--modules` adds it without trimming
42
+ * (`composeModuleSet`), and it is refused below Pro naming the level it is
43
+ * an add-on ON TOP of.
44
+ */
45
+ export const ADD_ONS = ["commerce"];
46
+ /**
47
+ * Included at Pro and above (the licence manifest's `pro` tier), but not
48
+ * switched on by default at any level (D1) — an authed app surface
49
+ * contributing no PUBLIC page to a marketing site. Naming it in `--modules`
50
+ * adds it without trimming, exactly like an add-on.
51
+ */
52
+ export const OPT_IN_MODULES = ["workspace"];
53
+ /** The Pro Plus membership: what Pro Plus switches on beyond Pro. */
54
+ const PRO_PLUS_MODULES = ["multi-tenant"];
55
+ /** Named modules that never trim the level default when composing a set (D2). */
56
+ const ADDITIONS = [...ADD_ONS, ...OPT_IN_MODULES];
57
+ const PRO_EXCLUDED = new Set([...ADDITIONS, ...PRO_PLUS_MODULES]);
58
+ /**
59
+ * What a key at each level switches on with no `--modules` (D2). Derived —
60
+ * every list is `RECOGNIZED_MODULES.filter(...)`, so declaration order is
61
+ * preserved and a module added to the vocabulary later lands in `pro` by
62
+ * derivation (loud: `module-tier.test.ts`'s table test pins the exact sets).
63
+ */
64
+ export const LEVEL_MODULES = Object.freeze({
65
+ free: [...FREE_MODULES],
66
+ pro: RECOGNIZED_MODULES.filter((m) => FREE_MODULES.includes(m) ||
67
+ (PAID_MODULES.includes(m) && !PRO_EXCLUDED.has(m))),
68
+ "pro-plus": RECOGNIZED_MODULES.filter((m) => FREE_MODULES.includes(m) ||
69
+ (PAID_MODULES.includes(m) && !ADDITIONS.includes(m))),
70
+ });
71
+ /**
72
+ * Which level (or add-on status) a module belongs to (D2). `"free"` for
73
+ * `FREE_MODULES`, `"add-on"` for `ADD_ONS`, `"pro-plus"` for
74
+ * `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — D1). Pure.
75
+ */
76
+ export function levelOf(m) {
77
+ if (FREE_MODULES.includes(m))
78
+ return "free";
79
+ if (ADD_ONS.includes(m))
80
+ return "add-on";
81
+ if (PRO_PLUS_MODULES.includes(m))
82
+ return "pro-plus";
83
+ return "pro";
84
+ }
85
+ /**
86
+ * The pure core of FR-2/FR-3 (D2): compose the effective module set from
87
+ * what `--modules` named and the key's derived level. Naming any module that
88
+ * is not an addition (`ADDITIONS`) is a KEEP — it names exactly the set,
89
+ * unchanged from today (NFR-2), and the level plays no part. Naming only
90
+ * additions (or nothing) takes the level's default plus those additions.
91
+ * Never refuses — refusals belong to the gate (`resolveModuleSet`), because
92
+ * whether a named module is covered is a registry answer, not a table lookup.
93
+ */
94
+ export function composeModuleSet(named, level) {
95
+ const namedList = [...(named ?? [])];
96
+ const keep = namedList.filter((m) => !ADDITIONS.includes(m));
97
+ if (keep.length > 0) {
98
+ return normalizeModules(namedList);
99
+ }
100
+ const add = namedList.filter((m) => ADDITIONS.includes(m));
101
+ return normalizeModules([...LEVEL_MODULES[level], ...add]);
102
+ }
27
103
  /**
28
104
  * Split a requested (already-validated, `ModuleName`) module set into its
29
105
  * free and paid subsets, each preserving the input's relative order.