create-ailk 0.7.0 → 0.9.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 (310) hide show
  1. package/README.md +112 -10
  2. package/component-catalog.json +798 -49
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module.d.ts +20 -0
  5. package/dist/add-module.js +43 -7
  6. package/dist/add-route.d.ts +56 -0
  7. package/dist/add-route.js +286 -0
  8. package/dist/apply-design-md.d.ts +7 -0
  9. package/dist/apply-design-md.js +50 -0
  10. package/dist/apply-design.d.ts +64 -0
  11. package/dist/apply-design.js +273 -0
  12. package/dist/claude-md.d.ts +111 -0
  13. package/dist/claude-md.js +334 -0
  14. package/dist/cli.js +114 -28
  15. package/dist/derive-architecture.js +41 -36
  16. package/dist/design-compile.d.ts +71 -0
  17. package/dist/design-compile.js +98 -0
  18. package/dist/design-package.d.ts +83 -0
  19. package/dist/design-package.js +468 -0
  20. package/dist/design-presets.d.ts +86 -0
  21. package/dist/design-presets.js +642 -0
  22. package/dist/design-scaffold.d.ts +76 -0
  23. package/dist/design-scaffold.js +220 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +4 -1
  26. package/dist/legacy-nav-renderers.d.ts +45 -0
  27. package/dist/legacy-nav-renderers.js +545 -0
  28. package/dist/lib/auth-secret-file.d.ts +19 -0
  29. package/dist/lib/auth-secret-file.js +112 -0
  30. package/dist/lib/design-checks.d.ts +78 -0
  31. package/dist/lib/design-checks.js +174 -0
  32. package/dist/lib/extract-module-bundle.js +5 -1
  33. package/dist/lib/license-key-files.d.ts +9 -0
  34. package/dist/lib/license-key-files.js +2 -2
  35. package/dist/lib/module-license-gate.d.ts +89 -0
  36. package/dist/lib/module-license-gate.js +171 -12
  37. package/dist/lib/module-tier.d.ts +45 -16
  38. package/dist/lib/module-tier.js +76 -0
  39. package/dist/lib/pro-callout.js +10 -2
  40. package/dist/lib/render-verdict.d.ts +52 -0
  41. package/dist/lib/render-verdict.js +77 -0
  42. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  43. package/dist/lib/scaffold-render-sweep.js +236 -0
  44. package/dist/module-architecture.d.ts +19 -16
  45. package/dist/module-architecture.js +64 -21
  46. package/dist/parse-args.d.ts +88 -10
  47. package/dist/parse-args.js +272 -21
  48. package/dist/programmatic.d.ts +66 -12
  49. package/dist/programmatic.js +361 -34
  50. package/dist/site-chrome.d.ts +19 -9
  51. package/dist/site-chrome.js +23 -13
  52. package/dist/site-manifest.d.ts +25 -0
  53. package/dist/site-manifest.js +254 -0
  54. package/dist/site-roster.d.ts +41 -0
  55. package/dist/site-roster.js +149 -11
  56. package/dist/starter-grid.d.ts +232 -0
  57. package/dist/starter-grid.js +545 -0
  58. package/dist/surfaces.d.ts +41 -9
  59. package/dist/surfaces.js +268 -92
  60. package/dist/template-manifest.d.ts +65 -29
  61. package/dist/template-manifest.js +161 -382
  62. package/dist/template-names.d.ts +14 -12
  63. package/dist/template-names.js +13 -17
  64. package/package.json +5 -4
  65. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  66. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  67. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  68. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  69. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  70. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  71. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  72. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  73. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  74. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  75. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  76. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  77. package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
  78. package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
  79. package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
  80. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  81. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  82. package/site-templates/designs/minimalism/sample/README.md +48 -0
  83. package/site-templates/designs/standard/DESIGN.md +103 -6
  84. package/site-templates/marketing-site/DESIGN.md +103 -6
  85. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
  86. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
  87. package/site-templates/marketing-site-free/DESIGN.md +103 -6
  88. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
  89. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
  90. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  91. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  92. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  93. package/site-templates/saas-product/DESIGN.md +103 -6
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
  97. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  98. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  99. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  100. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  101. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  102. package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  103. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  104. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  105. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  106. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  107. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  108. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
  110. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  111. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  112. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  113. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  114. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  115. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  116. package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
  117. package/site-templates/shapes/organization-service/provenance.json +4 -0
  118. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
  119. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
  120. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  121. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  122. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  123. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  124. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  125. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
  126. package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
  127. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  128. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  129. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  130. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  131. package/site-templates/shapes/person-service/provenance.json +4 -0
  132. package/templates/.claude/rules/architecture.md +7 -0
  133. package/templates/.claude/skills/README.md +5 -3
  134. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  135. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  136. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  137. package/templates/.claude/skills/add-page/references/page-types.md +85 -72
  138. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  139. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  140. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  141. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  142. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  143. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  144. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  145. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  146. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  147. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  148. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  149. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  150. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
  151. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  152. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  153. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  154. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  155. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  156. package/templates/.github/workflows/ci.yml +4 -0
  157. package/templates/.prettierignore +1 -0
  158. package/templates/CLAUDE.md +146 -103
  159. package/templates/DESIGN.md +103 -6
  160. package/templates/README.md +63 -43
  161. package/templates/apps/api/.env.example +2 -1
  162. package/templates/apps/api/CLAUDE.md +3 -2
  163. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  164. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  165. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  166. package/templates/apps/api/src/config/index.ts +35 -16
  167. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  168. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  169. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  170. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  171. package/templates/apps/api/src/lib/stripe.ts +9 -0
  172. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  173. package/templates/apps/api/src/middleware/auth.ts +54 -0
  174. package/templates/apps/api/src/openapi/spec.ts +63 -12
  175. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  176. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  177. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  178. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  179. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  180. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  181. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  182. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  183. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  184. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  185. package/templates/apps/api/src/server.ts +35 -0
  186. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  187. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  188. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  189. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  190. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  191. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  192. package/templates/apps/api/src/services/revalidation.ts +60 -15
  193. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  194. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  197. package/templates/apps/web/.env.example +13 -0
  198. package/templates/apps/web/CLAUDE.md +22 -28
  199. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  200. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  201. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  202. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  203. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  205. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  206. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  207. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  208. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  209. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  210. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  211. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  212. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  213. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  214. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  215. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  216. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  217. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  218. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
  219. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  220. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  221. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  222. package/templates/apps/web/app/[locale]/layout.tsx +65 -17
  223. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  224. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  225. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  226. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  227. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  228. package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
  229. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  230. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  232. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  233. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  234. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  235. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  236. package/templates/apps/web/lib/locale-guard.ts +18 -0
  237. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  238. package/templates/apps/web/lib/nav-config.ts +50 -37
  239. package/templates/apps/web/lib/site-brand.tsx +13 -9
  240. package/templates/apps/web/lib/site-identity.ts +14 -0
  241. package/templates/apps/web/lib/site-logo.tsx +36 -0
  242. package/templates/apps/web/lib/site-theme.ts +46 -26
  243. package/templates/apps/web/messages/en.json +45 -5
  244. package/templates/apps/web/middleware.ts +28 -4
  245. package/templates/content/_site.mdx +4 -2
  246. package/templates/content/en/BlogList/blog.mdx +37 -16
  247. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  248. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  249. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  250. package/templates/content/en/BookCall/start-project.mdx +56 -0
  251. package/templates/content/en/ContactPage/contact.mdx +43 -34
  252. package/templates/content/en/HomePage/home.mdx +10 -22
  253. package/templates/content/en/LandingPage/meet.mdx +59 -0
  254. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  255. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  256. package/templates/database/CHANGELOG.md +14 -0
  257. package/templates/database/package.json +1 -1
  258. package/templates/package.json +2 -1
  259. package/templates/tools/check-claude-md.ts +128 -0
  260. package/versions.json +33 -33
  261. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
  262. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  263. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  264. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  265. package/site-templates/minimalism-site/DESIGN.md +0 -162
  266. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
  267. package/site-templates/minimalism-site/ailk-template.json +0 -23
  268. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  269. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
  270. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  271. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  272. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  273. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  274. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  275. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  276. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  277. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  278. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  279. package/site-templates/minimalism-site/provenance.json +0 -4
  280. package/site-templates/simple-service/DESIGN.md +0 -162
  281. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  282. package/site-templates/simple-service/ailk-template.json +0 -11
  283. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  284. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  285. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  286. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  287. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  288. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  289. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  290. package/site-templates/simple-service/provenance.json +0 -4
  291. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  292. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  293. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  294. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  295. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
  296. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  297. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  298. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  299. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  300. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  301. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  302. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  303. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  304. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  305. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  306. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  307. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
  308. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
  309. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
  310. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
@@ -0,0 +1,25 @@
1
+ import type { Level } from "./lib/module-tier.js";
2
+ import type { ModuleName } from "./parse-args.js";
3
+ import { type SiteRoster } from "./site-roster.js";
4
+ import type { MarkerMap } from "./substitute.js";
5
+ export interface PlanSiteManifestOptions {
6
+ targetDir: string;
7
+ /** The installed manifest, read after every layer landed. */
8
+ roster: SiteRoster;
9
+ /** The effective module set the scaffold installed. */
10
+ modules: readonly ModuleName[];
11
+ /** The level the key path derived, or undefined when it derived none. */
12
+ level: Level | undefined;
13
+ siteTemplatesDir: string;
14
+ templatesDir: string;
15
+ markers: MarkerMap;
16
+ }
17
+ /**
18
+ * The manifest's next text: `modules` (and `level`, when derived) after
19
+ * `requires`, and a row for every route present on disk.
20
+ *
21
+ * @throws AddModuleRefusedError when the file already records `modules` or
22
+ * `level`, or its layout is not the generator's (`rows` last, `requires`
23
+ * at the top level) — guessing would silently mis-record the site.
24
+ */
25
+ export declare function planSiteManifest(opts: PlanSiteManifestOptions): string;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The site manifest's record of what the scaffold IS (#6944) — written into
3
+ * `<site>/.ailk-site-manifest.json` by `scaffold()` once every layer has
4
+ * landed.
5
+ *
6
+ * A template scaffold's manifest arrives as its template's generated
7
+ * `TEMPLATE-MANIFEST.json` (#6658 D7): the template's own `requires` and
8
+ * rows, and nothing about the run that installed it. A Pro `--shape` run of a
9
+ * Free template recorded `requires: ["marketing"]` and the Free rows, missing
10
+ * every page its module bundles landed. `planSiteManifest` adds:
11
+ *
12
+ * - `modules` — the effective module set, the one `architecture.yaml`
13
+ * declares. Skills read it for what the site can render;
14
+ * - `level` — ONLY when the licence-key path derived one
15
+ * (`resolveModuleSet` / `deriveLevel`). Never inferred from `modules`:
16
+ * the level comes only from the key. An alias or `--modules` keep run
17
+ * derives none and records none;
18
+ * - a row for every route present on disk — each module's contributed rows
19
+ * through `planRosterAppend` (add-module's own append), the sample pages
20
+ * a module's bundle installs (`sampleRows`), and a code-route row for any
21
+ * other page route under `apps/web/app/[locale]/` (`codeRouteRows`).
22
+ *
23
+ * Every byte already in the file is kept; the new rows and keys are rendered
24
+ * the way prettier prints the generator's JSON, so the file stays formatted.
25
+ *
26
+ * Pure module: no `import.meta.url`. Package-rooted directories are supplied
27
+ * by the caller, the same split as `site-roster.ts` / `programmatic.ts`.
28
+ */
29
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
30
+ import { join, relative, sep } from "node:path";
31
+ import { isDeepStrictEqual } from "node:util";
32
+ import { AddModuleRefusedError } from "./add-module-core.js";
33
+ import { MODULE_SLICES, normalizeModules } from "./module-architecture.js";
34
+ import { moduleRosterRows, planRosterAppend, } from "./site-roster.js";
35
+ import { TEMPLATE_MANIFESTS, rosterDeclares, templateTreeDir, } from "./template-manifest.js";
36
+ /** Prettier's default print width — the generator formats the manifest with it. */
37
+ const PRINT_WIDTH = 80;
38
+ /** Where the site's App Router pages live. */
39
+ const ROUTE_ROOT = join("apps", "web", "app", "[locale]");
40
+ const PAGE_FILE = /^page\.(tsx|ts|jsx|js|mdx)$/;
41
+ /**
42
+ * The generator's row for a content-less route (`build-template-manifest.mjs`:
43
+ * every row renders under the locale layout's MarketingShell, chrome on).
44
+ */
45
+ const CODE_ROUTE_ROW = {
46
+ kind: "route",
47
+ pageType: null,
48
+ schema: null,
49
+ shell: "MarketingShell",
50
+ chrome: "on",
51
+ sections: [],
52
+ };
53
+ /**
54
+ * The manifest's next text: `modules` (and `level`, when derived) after
55
+ * `requires`, and a row for every route present on disk.
56
+ *
57
+ * @throws AddModuleRefusedError when the file already records `modules` or
58
+ * `level`, or its layout is not the generator's (`rows` last, `requires`
59
+ * at the top level) — guessing would silently mis-record the site.
60
+ */
61
+ export function planSiteManifest(opts) {
62
+ const { roster } = opts;
63
+ for (const key of ["modules", "level"]) {
64
+ if (key in roster.doc) {
65
+ throw new AddModuleRefusedError(`${roster.path} already records "${key}"`);
66
+ }
67
+ }
68
+ const plan = planRosterAppend({
69
+ targetDir: opts.targetDir,
70
+ roster,
71
+ modules: opts.modules,
72
+ references: opts.modules.flatMap((m) => moduleRosterRows(m, opts.siteTemplatesDir)),
73
+ siteTemplatesDir: opts.siteTemplatesDir,
74
+ templatesDir: opts.templatesDir,
75
+ markers: opts.markers,
76
+ });
77
+ const appended = plan.next ?? roster.text;
78
+ const declared = new Set(JSON.parse(appended).rows.map((row) => row["route"]));
79
+ const extra = [
80
+ ...sampleRows(opts, declared),
81
+ ...codeRouteRows(opts.targetDir, declared),
82
+ ];
83
+ const withRows = appendRows(appended, extra, roster);
84
+ return insertRecord(withRows, opts.modules, opts.level, roster.path);
85
+ }
86
+ /**
87
+ * The rows for the sample pages a module's bundle installs through its
88
+ * `installMap` (`signal-collectors`' /projects family) that no row names yet:
89
+ * each is the row a template requiring the module declares for that content,
90
+ * as the generator wrote it into the template's shipped manifest. A
91
+ * `contentDir` row lists the site's own seeds. `declared` gains each route.
92
+ */
93
+ function sampleRows(opts, declared) {
94
+ const out = [];
95
+ for (const moduleName of normalizeModules([...opts.modules])) {
96
+ const installed = Object.values(MODULE_SLICES[moduleName].installMap ?? {})
97
+ .filter((to) => to.startsWith("content/en/") && to.endsWith(".mdx"))
98
+ .filter((to) => existsSync(join(opts.targetDir, to)));
99
+ if (installed.length === 0)
100
+ continue;
101
+ for (const template of Object.values(TEMPLATE_MANIFESTS)) {
102
+ if (!template.requires.includes(moduleName))
103
+ continue;
104
+ const shippedPath = join(opts.siteTemplatesDir, templateTreeDir(template.name), "TEMPLATE-MANIFEST.json");
105
+ if (!existsSync(shippedPath))
106
+ continue;
107
+ const shipped = JSON.parse(readFileSync(shippedPath, "utf8")).rows;
108
+ for (const row of template.roster) {
109
+ if (declared.has(row.route))
110
+ continue;
111
+ if (!installed.some((path) => rosterDeclares([row], path)))
112
+ continue;
113
+ const value = shipped.find((r) => r["route"] === row.route);
114
+ if (value === undefined)
115
+ continue;
116
+ declared.add(row.route);
117
+ out.push(row.contentDir !== undefined && "seeds" in value
118
+ ? {
119
+ ...value,
120
+ seeds: listMdx(join(opts.targetDir, row.contentDir), opts.targetDir),
121
+ }
122
+ : value);
123
+ }
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ /** A code-route row for every page route on disk that `declared` does not name. */
129
+ function codeRouteRows(targetDir, declared) {
130
+ return routesOnDisk(targetDir)
131
+ .filter((route) => !declared.has(route))
132
+ .map((route) => ({ route, ...CODE_ROUTE_ROW }));
133
+ }
134
+ /**
135
+ * Every route the site's App Router serves from a page file, sorted: route
136
+ * groups `(x)` dropped, a trailing optional catch-all `[[...x]]` folded into
137
+ * its parent (docs-blog's `/blog/[[...slug]]` serves `/blog`). The root
138
+ * content renderers (`/[slug]`, `/[slug]/[detail]`) are not routes of their
139
+ * own — they are how the content rows render — so they are left out.
140
+ */
141
+ function routesOnDisk(targetDir) {
142
+ const root = join(targetDir, ROUTE_ROOT);
143
+ const routes = new Set();
144
+ const stack = existsSync(root) ? [root] : [];
145
+ for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
146
+ const entries = readdirSync(cur, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ if (entry.isDirectory())
149
+ stack.push(join(cur, entry.name));
150
+ }
151
+ if (!entries.some((entry) => entry.isFile() && PAGE_FILE.test(entry.name))) {
152
+ continue;
153
+ }
154
+ const segments = relative(root, cur)
155
+ .split(sep)
156
+ .filter((segment) => segment !== "" && !/^\(.+\)$/.test(segment));
157
+ const last = segments[segments.length - 1];
158
+ if (last !== undefined && /^\[\[\.\.\..+\]\]$/.test(last))
159
+ segments.pop();
160
+ if (segments[0]?.startsWith("["))
161
+ continue;
162
+ routes.add(`/${segments.join("/")}`);
163
+ }
164
+ return [...routes].sort();
165
+ }
166
+ /** Every `.mdx` under `dir`, as sorted `/`-separated paths relative to `base`. */
167
+ function listMdx(dir, base) {
168
+ const out = [];
169
+ const stack = existsSync(dir) ? [dir] : [];
170
+ for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
171
+ for (const entry of readdirSync(cur, { withFileTypes: true })) {
172
+ const abs = join(cur, entry.name);
173
+ if (entry.isDirectory())
174
+ stack.push(abs);
175
+ else if (entry.isFile() && entry.name.endsWith(".mdx")) {
176
+ out.push(relative(base, abs).split(sep).join("/"));
177
+ }
178
+ }
179
+ }
180
+ return out.sort();
181
+ }
182
+ /**
183
+ * `value` as prettier prints the generator's JSON with its value starting at
184
+ * `column` after a `prefix`-wide key: objects expanded one key per line, an
185
+ * array of scalars inline when the line fits, one element per line otherwise.
186
+ */
187
+ function renderJson(value, column, prefix = 0, trailing = 0) {
188
+ const pad = " ".repeat(column + 2);
189
+ const close = " ".repeat(column);
190
+ if (Array.isArray(value)) {
191
+ if (value.length === 0)
192
+ return "[]";
193
+ if (value.every((item) => item === null || typeof item !== "object")) {
194
+ const inline = `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
195
+ if (column + prefix + inline.length + trailing <= PRINT_WIDTH)
196
+ return inline;
197
+ }
198
+ const items = value.map((item, i) => pad + renderJson(item, column + 2, 0, i < value.length - 1 ? 1 : 0));
199
+ return `[\n${items.join(",\n")}\n${close}]`;
200
+ }
201
+ if (value !== null && typeof value === "object") {
202
+ const entries = Object.entries(value);
203
+ if (entries.length === 0)
204
+ return "{}";
205
+ const lines = entries.map(([key, item], i) => {
206
+ const name = `${JSON.stringify(key)}: `;
207
+ return (pad +
208
+ name +
209
+ renderJson(item, column + 2, name.length, i < entries.length - 1 ? 1 : 0));
210
+ });
211
+ return `{\n${lines.join(",\n")}\n${close}}`;
212
+ }
213
+ return JSON.stringify(value);
214
+ }
215
+ /** `text` with `rows` appended to its `rows` array — which the generator writes last. */
216
+ function appendRows(text, rows, roster) {
217
+ if (rows.length === 0)
218
+ return text;
219
+ const keys = Object.keys(JSON.parse(text));
220
+ if (keys[keys.length - 1] !== "rows") {
221
+ throw new AddModuleRefusedError(`${roster.path} does not end with its "rows" array, so rows cannot be appended`);
222
+ }
223
+ const close = text.lastIndexOf("]");
224
+ let end = close;
225
+ while (end > 0 && /\s/.test(text[end - 1]))
226
+ end--;
227
+ const empty = text[end - 1] === "[";
228
+ const column = roster.lead.length - roster.lead.lastIndexOf("\n") - 1;
229
+ const rendered = rows
230
+ .map((row) => roster.lead + renderJson(row, column))
231
+ .join(",");
232
+ return empty
233
+ ? text.slice(0, end) + rendered + roster.trail + text.slice(close)
234
+ : text.slice(0, end) + "," + rendered + text.slice(end);
235
+ }
236
+ /** `text` with `modules` (and `level`, when derived) inserted after the top-level `requires`. */
237
+ function insertRecord(text, modules, level, label) {
238
+ const anchor = '\n "requires": ';
239
+ const at = text.indexOf(anchor);
240
+ if (at === -1 || text.indexOf(anchor, at + 1) !== -1) {
241
+ throw new AddModuleRefusedError(`${label} has no single top-level "requires" key to record the module set beside`);
242
+ }
243
+ const end = text.indexOf("]", at) + 1;
244
+ const name = '"modules": ';
245
+ const record = `,\n ${name}${renderJson([...modules], 2, name.length, 1)}` +
246
+ (level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "");
247
+ const next = text.slice(0, end) + record + text.slice(end);
248
+ const doc = JSON.parse(next);
249
+ if (!isDeepStrictEqual(doc["modules"], [...modules]) ||
250
+ doc["level"] !== level) {
251
+ throw new AddModuleRefusedError(`${label}: the module set could not be recorded beside "requires"`);
252
+ }
253
+ return next;
254
+ }
@@ -1,5 +1,7 @@
1
+ import type { CompiledDesignTheme } from "./design-compile.js";
1
2
  import { type PageRosterRow } from "./module-architecture.js";
2
3
  import type { ModuleName } from "./parse-args.js";
4
+ import type { ResolvedSiteChrome } from "./site-chrome.js";
3
5
  import { type MarkerMap } from "./substitute.js";
4
6
  import { type TemplateManifest } from "./template-manifest.js";
5
7
  /** The site-root file that is the roster of record (#6658 D7). */
@@ -55,6 +57,25 @@ export interface ModuleRosterRow {
55
57
  * not declare is refused: guessing would silently mis-roster the site.
56
58
  */
57
59
  export declare function readSiteRoster(targetDir: string): SiteRoster | undefined;
60
+ /** What `planSiteManifestDesign` writes when it rewrites the manifest. */
61
+ export interface SiteManifestDesignNext {
62
+ design: string;
63
+ layout: ResolvedSiteChrome;
64
+ theme: CompiledDesignTheme;
65
+ }
66
+ /**
67
+ * Plan the site manifest's `design`/`layout`/`theme` keys as a design's
68
+ * applied values (#6813 D6) — `undefined` when the file already carries
69
+ * `next` (no rewrite, so the file keeps its bytes byte-for-byte), else the
70
+ * text with each CHANGED key's value spliced in place, located with the same
71
+ * `findKey`/`endOfValue` scanner `planRosterAppend` uses. `rows`,
72
+ * `generatedHash` and every other byte stay untouched.
73
+ *
74
+ * @throws AddModuleRefusedError when `text` is not a JSON object, or is
75
+ * missing `design`, `layout` or `theme` — a manifest predating #6658 D7
76
+ * never reaches this function (`scaffold()` guards on the file's presence).
77
+ */
78
+ export declare function planSiteManifestDesign(text: string, next: SiteManifestDesignNext, label: string): string | undefined;
58
79
  /**
59
80
  * The generator's row for each of a module's `pageContributions`, in
60
81
  * declaration order — read from the committed manifest of the first
@@ -63,6 +84,26 @@ export declare function readSiteRoster(targetDir: string): SiteRoster | undefine
63
84
  * of its forms. A route no shipped manifest carries is refused.
64
85
  */
65
86
  export declare function moduleRosterRows(moduleName: ModuleName, siteTemplatesDir: string): ModuleRosterRow[];
87
+ /**
88
+ * The Free `/meet` placeholder seeds on `route` (#6942): the keyless Free
89
+ * template's own `content` on a route the `scheduling` module contributes
90
+ * (`FREE_MEET_PLACEHOLDER_ROWS`, `starter-grid.ts`, read through the one
91
+ * registry that composes it). Empty for any other route. Throws when the Free
92
+ * template carries none at all, so a renamed template or a moved row fails
93
+ * loudly instead of re-opening the prune #6942 closed.
94
+ */
95
+ export declare function meetPlaceholderSeeds(route: string): readonly string[];
96
+ /**
97
+ * `roster` with the Free `/meet` placeholder seeds folded onto the
98
+ * `scheduling` module's code-route rows (#6942) — a row on a placeholder
99
+ * route that names no `content` of its own. Unconfigured, the module's
100
+ * `/meet` and `/meet/[slug]` pages hand off to the placeholder routes, so a
101
+ * seed there is still served and still declared: the scaffold's seed prune
102
+ * (`programmatic.ts`) and the site roster check read the roster through
103
+ * this. A row with its own `content` (the Free placeholder row itself) and a
104
+ * row on any other route are returned as is.
105
+ */
106
+ export declare function withMeetPlaceholderSeeds(roster: readonly PageRosterRow[]): PageRosterRow[];
66
107
  /** What becomes of a superseded template row's seed file. */
67
108
  export type SupersededSeed = {
68
109
  path: string;
@@ -30,7 +30,7 @@ import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-cor
30
30
  import { diffTwoStrings } from "./lib/extract-module-bundle.js";
31
31
  import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
32
32
  import { substituteMarkers } from "./substitute.js";
33
- import { BASE_LAYER_DIR, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, } from "./template-manifest.js";
33
+ import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, shippedManifestPath, templateTreeDir, } from "./template-manifest.js";
34
34
  /** The site-root file that is the roster of record (#6658 D7). */
35
35
  export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
36
36
  /**
@@ -39,8 +39,6 @@ export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
39
39
  * `SITE_ROSTER_PATH` is absent, and written back in place.
40
40
  */
41
41
  export const LEGACY_SITE_ROSTER_PATH = "TEMPLATE-MANIFEST.json";
42
- /** A template tree's own generated manifest — the file the site copy is made from. */
43
- const SHIPPED_MANIFEST_PATH = "TEMPLATE-MANIFEST.json";
44
42
  /** Prettier's default print width — the generator formats the manifest with it. */
45
43
  const PRINT_WIDTH = 80;
46
44
  // ── A string-aware JSON scanner, for the text splice ──────────────────────
@@ -168,23 +166,82 @@ export function readSiteRoster(targetDir) {
168
166
  if (path === undefined)
169
167
  return undefined;
170
168
  const file = parseRosterFile(readFileSync(join(targetDir, path), "utf8"), path);
171
- const name = file.doc["template"];
169
+ const name = String(file.doc["template"]);
172
170
  let template;
173
171
  try {
174
- template = resolveTemplate(String(name));
172
+ // #6757 D6 — a bare-shape starter's name (the shape itself) resolves
173
+ // through SHAPE_STARTERS first; `resolveTemplate` alone never sees one
174
+ // (NFR-3).
175
+ template = Object.prototype.hasOwnProperty.call(SHAPE_STARTERS, name)
176
+ ? SHAPE_STARTERS[name]
177
+ : resolveTemplate(name);
175
178
  }
176
179
  catch (err) {
177
180
  throw new AddModuleRefusedError(`${path} names no declared template (${err.message}), so the page roster is unknown`);
178
181
  }
179
182
  return { ...file, template, path };
180
183
  }
184
+ /** `value`, rendered the way prettier prints an object-only JSON value nested one level under the manifest's root object. */
185
+ function renderManifestValue(value) {
186
+ return JSON.stringify(value, null, 2)
187
+ .split("\n")
188
+ .map((line, i) => (i === 0 ? line : ` ${line}`))
189
+ .join("\n");
190
+ }
191
+ /**
192
+ * Plan the site manifest's `design`/`layout`/`theme` keys as a design's
193
+ * applied values (#6813 D6) — `undefined` when the file already carries
194
+ * `next` (no rewrite, so the file keeps its bytes byte-for-byte), else the
195
+ * text with each CHANGED key's value spliced in place, located with the same
196
+ * `findKey`/`endOfValue` scanner `planRosterAppend` uses. `rows`,
197
+ * `generatedHash` and every other byte stay untouched.
198
+ *
199
+ * @throws AddModuleRefusedError when `text` is not a JSON object, or is
200
+ * missing `design`, `layout` or `theme` — a manifest predating #6658 D7
201
+ * never reaches this function (`scaffold()` guards on the file's presence).
202
+ */
203
+ export function planSiteManifestDesign(text, next, label) {
204
+ let doc;
205
+ try {
206
+ doc = JSON.parse(text);
207
+ }
208
+ catch (err) {
209
+ throw new AddModuleRefusedError(`${label} is not JSON (${err.message})`);
210
+ }
211
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
212
+ throw new AddModuleRefusedError(`${label} is not a JSON object`);
213
+ }
214
+ const parsed = doc;
215
+ const keys = ["design", "layout", "theme"];
216
+ for (const key of keys) {
217
+ if (!(key in parsed)) {
218
+ throw new AddModuleRefusedError(`${label} has no "${key}" key`);
219
+ }
220
+ }
221
+ if (keys.every((key) => isDeepStrictEqual(parsed[key], next[key]))) {
222
+ return undefined;
223
+ }
224
+ let result = text;
225
+ for (const key of keys) {
226
+ if (isDeepStrictEqual(parsed[key], next[key]))
227
+ continue;
228
+ const at = findKey(result, result.indexOf("{"), key);
229
+ if (at === undefined) {
230
+ throw new AddModuleRefusedError(`${label} has a "${key}" key the scanner cannot locate`);
231
+ }
232
+ const end = endOfValue(result, at.valueStart);
233
+ result = result.slice(0, at.valueStart) + renderManifestValue(next[key]) + result.slice(end);
234
+ }
235
+ return result;
236
+ }
181
237
  // ── D2: a module's rows, from the shipped manifests ───────────────────────
182
238
  /** A template's shipped manifest, or undefined when its tree ships none. */
183
239
  function readShippedManifest(siteTemplatesDir, templateName) {
184
- const path = join(siteTemplatesDir, templateName, SHIPPED_MANIFEST_PATH);
240
+ const relPath = shippedManifestPath(templateName);
241
+ const path = join(siteTemplatesDir, relPath);
185
242
  if (!existsSync(path))
186
243
  return undefined;
187
- return parseRosterFile(readFileSync(path, "utf8"), `site-templates/${templateName}/${SHIPPED_MANIFEST_PATH}`);
244
+ return parseRosterFile(readFileSync(path, "utf8"), `site-templates/${relPath}`);
188
245
  }
189
246
  /**
190
247
  * The generator's row for each of a module's `pageContributions`, in
@@ -211,14 +268,53 @@ export function moduleRosterRows(moduleName, siteTemplatesDir) {
211
268
  };
212
269
  return contributions.map((contribution) => {
213
270
  const isDir = contribution.contentDir !== undefined;
214
- const row = find(contribution.route, isDir ? "route" : undefined);
215
271
  const seeded = isDir ? find(contribution.route, "page") : undefined;
272
+ let row = find(contribution.route, isDir ? "route" : undefined);
273
+ if (row === undefined && isDir && seeded !== undefined) {
274
+ const contentDir = contribution.contentDir;
275
+ const installMap = MODULE_SLICES[moduleName].installMap ?? {};
276
+ const selfSeeds = Object.values(installMap).some((dest) => dest.startsWith(`${contentDir}/`));
277
+ if (selfSeeds)
278
+ row = deriveRouteRow(contribution.route, seeded);
279
+ }
216
280
  if (row === undefined || (isDir && seeded === undefined)) {
217
281
  throw new AddModuleRefusedError(`module "${moduleName}" contributes ${contribution.route}, but no shipped template manifest carries ${isDir ? "both forms of " : ""}its row, so it cannot be added to ${SITE_ROSTER_PATH}`);
218
282
  }
219
283
  return { module: moduleName, contribution, row, ...(seeded && { seeded }) };
220
284
  });
221
285
  }
286
+ /**
287
+ * Derive a `contentDir` contribution's code-route form from its seeded page
288
+ * form (spec 6892 D9) — for a SELF-SEEDING contribution, one whose
289
+ * contributing module's own `installMap` lands at least one file under the
290
+ * directory, so no template supplies a route form for it (every template
291
+ * taking the module gets the module's seeds, never zero of them). Returns
292
+ * field-for-field the route form `build-template-manifest.mjs` emits for a
293
+ * template that supplies no seeds of its own: `kind: "route"`, no
294
+ * `pageType`/`schema`/`contentDir`/`seeds`, the seeded form's own `shell` and
295
+ * `chrome`, no sections. Rendered the way a generated manifest's row-array
296
+ * element is indented (four spaces beyond `JSON.stringify`'s own two), so a
297
+ * derived row's `text` is byte-identical to the route-form slice a template
298
+ * manifest carries for the same contribution.
299
+ */
300
+ function deriveRouteRow(route, seeded) {
301
+ const value = {
302
+ route,
303
+ kind: "route",
304
+ pageType: null,
305
+ schema: null,
306
+ shell: seeded.value["shell"],
307
+ chrome: seeded.value["chrome"],
308
+ sections: [],
309
+ };
310
+ return {
311
+ value,
312
+ text: JSON.stringify(value, null, 2)
313
+ .split("\n")
314
+ .map((line, i) => (i === 0 ? line : ` ${line}`))
315
+ .join("\n"),
316
+ };
317
+ }
222
318
  /**
223
319
  * Every `.mdx` under `dir`, as `/`-separated paths relative to `base`. Walked
224
320
  * by hand: `Dirent.parentPath` and a reliable recursive `readdirSync` post-date
@@ -276,10 +372,44 @@ function expectedRow(ref, targetDir) {
276
372
  const seeds = listMdx(join(targetDir, dir), targetDir);
277
373
  return seeds.length > 0 ? withSeeds(ref.seeded, seeds) : ref.row;
278
374
  }
375
+ /**
376
+ * The Free `/meet` placeholder seeds on `route` (#6942): the keyless Free
377
+ * template's own `content` on a route the `scheduling` module contributes
378
+ * (`FREE_MEET_PLACEHOLDER_ROWS`, `starter-grid.ts`, read through the one
379
+ * registry that composes it). Empty for any other route. Throws when the Free
380
+ * template carries none at all, so a renamed template or a moved row fails
381
+ * loudly instead of re-opening the prune #6942 closed.
382
+ */
383
+ export function meetPlaceholderSeeds(route) {
384
+ const scheduling = new Set((MODULE_SLICES.scheduling.pageContributions ?? []).map((row) => row.route));
385
+ const placeholders = (TEMPLATE_MANIFESTS["marketing-site-free"]?.roster ?? []).filter((row) => scheduling.has(row.route) && row.content !== undefined);
386
+ if (placeholders.length === 0) {
387
+ throw new Error('meetPlaceholderSeeds: "marketing-site-free" carries no /meet placeholder row on a scheduling route');
388
+ }
389
+ return placeholders.find((row) => row.route === route)?.content ?? [];
390
+ }
391
+ /**
392
+ * `roster` with the Free `/meet` placeholder seeds folded onto the
393
+ * `scheduling` module's code-route rows (#6942) — a row on a placeholder
394
+ * route that names no `content` of its own. Unconfigured, the module's
395
+ * `/meet` and `/meet/[slug]` pages hand off to the placeholder routes, so a
396
+ * seed there is still served and still declared: the scaffold's seed prune
397
+ * (`programmatic.ts`) and the site roster check read the roster through
398
+ * this. A row with its own `content` (the Free placeholder row itself) and a
399
+ * row on any other route are returned as is.
400
+ */
401
+ export function withMeetPlaceholderSeeds(roster) {
402
+ return roster.map((row) => {
403
+ if (row.content !== undefined || row.contentDir !== undefined)
404
+ return row;
405
+ const seeds = meetPlaceholderSeeds(row.route);
406
+ return seeds.length === 0 ? row : { ...row, content: seeds };
407
+ });
408
+ }
279
409
  /** The bytes a scaffold installed at `path` from the template, base or overlay layer. */
280
410
  function installedSeed(opts, path) {
281
411
  const layers = [
282
- join(opts.siteTemplatesDir, opts.roster.template.name),
412
+ join(opts.siteTemplatesDir, templateTreeDir(opts.roster.template.name)),
283
413
  join(opts.siteTemplatesDir, BASE_LAYER_DIR),
284
414
  opts.templatesDir,
285
415
  ];
@@ -345,7 +475,13 @@ export function planRosterAppend(opts) {
345
475
  }
346
476
  const seeds = [];
347
477
  for (const row of superseded) {
478
+ // The module's row on this route still declares a Free `/meet`
479
+ // placeholder seed (#6942): its unconfigured pages serve it, so the seed
480
+ // is kept, never removed or reported.
481
+ const kept = meetPlaceholderSeeds(row.route);
348
482
  for (const path of row.content ?? []) {
483
+ if (kept.includes(path))
484
+ continue;
349
485
  const abs = join(targetDir, path);
350
486
  if (!existsSync(abs))
351
487
  continue;
@@ -393,7 +529,7 @@ function layerInstalledSeeds(template, modules, opts) {
393
529
  }
394
530
  // The template's own tree, as the generator's `ownedContent` reads it: an
395
531
  // `installMap` source lands at its mapped destination instead.
396
- const ownRoot = join(opts.siteTemplatesDir, template.name);
532
+ const ownRoot = join(opts.siteTemplatesDir, templateTreeDir(template.name));
397
533
  const ownManifest = join(ownRoot, "ailk-template.json");
398
534
  const ownMap = existsSync(ownManifest)
399
535
  ? (JSON.parse(readFileSync(ownManifest, "utf8")).installMap ?? {})
@@ -482,11 +618,13 @@ export function siteRosterProblems(opts) {
482
618
  }),
483
619
  };
484
620
  });
621
+ // #6942 — the Free /meet placeholder seeds the scheduling module's rows keep.
622
+ const declared = withMeetPlaceholderSeeds(effective);
485
623
  const installed = [...layerInstalledSeeds(roster.template, modules, opts)]
486
624
  .filter((path) => existsSync(join(opts.targetDir, path)))
487
625
  .sort();
488
626
  for (const path of installed) {
489
- if (!rosterDeclares(effective, path)) {
627
+ if (!rosterDeclares(declared, path)) {
490
628
  problems.push(`${path}: installed but not declared by any roster row`);
491
629
  }
492
630
  }