create-ailk 0.1.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 (533) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +80 -0
  5. package/dist/copy.d.ts +8 -0
  6. package/dist/copy.js +109 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.js +10 -0
  9. package/dist/lib/pro-callout.d.ts +12 -0
  10. package/dist/lib/pro-callout.js +20 -0
  11. package/dist/parse-args.d.ts +13 -0
  12. package/dist/parse-args.js +81 -0
  13. package/dist/programmatic.d.ts +49 -0
  14. package/dist/programmatic.js +89 -0
  15. package/dist/substitute.d.ts +31 -0
  16. package/dist/substitute.js +45 -0
  17. package/dist/validate.d.ts +8 -0
  18. package/dist/validate.js +70 -0
  19. package/package.json +47 -0
  20. package/templates/.claude/.gitkeep +0 -0
  21. package/templates/.claude/agents/web.md +80 -0
  22. package/templates/.claude/rules/architecture.md +62 -0
  23. package/templates/.claude/rules/preserve-existing-style.md +51 -0
  24. package/templates/.claude/skills/README.md +43 -0
  25. package/templates/.claude/skills/add-schema/SKILL.md +270 -0
  26. package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
  27. package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
  28. package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
  29. package/templates/.claude/skills/audit-site/SKILL.md +213 -0
  30. package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
  31. package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
  32. package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
  33. package/templates/.claude/skills/provision-config/SKILL.md +243 -0
  34. package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
  35. package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
  36. package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
  37. package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
  38. package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
  39. package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
  40. package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
  41. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
  42. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
  43. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
  44. package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
  45. package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
  46. package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
  47. package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
  48. package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
  49. package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
  50. package/templates/.env.example +205 -0
  51. package/templates/.gitkeep +2 -0
  52. package/templates/CLAUDE.md +142 -0
  53. package/templates/CONVENTIONS.md +222 -0
  54. package/templates/README.md +110 -0
  55. package/templates/apps/api/.env.example +40 -0
  56. package/templates/apps/api/CLAUDE.md +63 -0
  57. package/templates/apps/api/api/index.ts +144 -0
  58. package/templates/apps/api/jest.config.cjs +74 -0
  59. package/templates/apps/api/package.json +54 -0
  60. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
  61. package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
  62. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
  63. package/templates/apps/api/src/__tests__/index.test.ts +118 -0
  64. package/templates/apps/api/src/__tests__/server.test.ts +207 -0
  65. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
  66. package/templates/apps/api/src/__tests__/setup.ts +24 -0
  67. package/templates/apps/api/src/cli.ts +30 -0
  68. package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
  69. package/templates/apps/api/src/config/index.ts +56 -0
  70. package/templates/apps/api/src/index.ts +32 -0
  71. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
  72. package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
  73. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
  74. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
  75. package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
  76. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
  77. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
  78. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
  79. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
  81. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
  82. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
  83. package/templates/apps/api/src/lib/content-adapter.ts +86 -0
  84. package/templates/apps/api/src/lib/error-serializer.ts +77 -0
  85. package/templates/apps/api/src/lib/errors.ts +95 -0
  86. package/templates/apps/api/src/lib/file-fallback.ts +87 -0
  87. package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
  88. package/templates/apps/api/src/lib/locales.ts +201 -0
  89. package/templates/apps/api/src/lib/outbound-http.ts +122 -0
  90. package/templates/apps/api/src/lib/prisma.ts +21 -0
  91. package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
  92. package/templates/apps/api/src/lib/stripe.ts +30 -0
  93. package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
  94. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
  95. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
  96. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
  97. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
  98. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
  99. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
  100. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
  101. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
  102. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
  103. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
  104. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
  105. package/templates/apps/api/src/middleware/auth.ts +246 -0
  106. package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
  107. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
  108. package/templates/apps/api/src/openapi/index.ts +12 -0
  109. package/templates/apps/api/src/openapi/spec.ts +408 -0
  110. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
  111. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
  112. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
  113. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
  114. package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
  115. package/templates/apps/api/src/routes/auth/user.ts +244 -0
  116. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
  117. package/templates/apps/api/src/routes/checkout/index.ts +13 -0
  118. package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
  119. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
  120. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
  121. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
  122. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
  123. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
  124. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
  125. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
  126. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
  127. package/templates/apps/api/src/routes/content/create.ts +114 -0
  128. package/templates/apps/api/src/routes/content/delete.ts +81 -0
  129. package/templates/apps/api/src/routes/content/get-page.ts +98 -0
  130. package/templates/apps/api/src/routes/content/index.ts +32 -0
  131. package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
  132. package/templates/apps/api/src/routes/content/list-types.ts +19 -0
  133. package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
  134. package/templates/apps/api/src/routes/content/slug.ts +9 -0
  135. package/templates/apps/api/src/routes/content/update.ts +111 -0
  136. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
  137. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
  138. package/templates/apps/api/src/routes/leads/index.ts +130 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
  140. package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
  141. package/templates/apps/api/src/routes/webhooks/README.md +57 -0
  142. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
  143. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
  144. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
  145. package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
  146. package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
  147. package/templates/apps/api/src/server.ts +157 -0
  148. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
  149. package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
  150. package/templates/apps/api/src/services/lead-routing.ts +338 -0
  151. package/templates/apps/api/tsconfig.json +34 -0
  152. package/templates/apps/api/vercel.json +17 -0
  153. package/templates/apps/mcp/.env.example +52 -0
  154. package/templates/apps/mcp/CLAUDE.md +84 -0
  155. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
  156. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
  157. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
  158. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
  159. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
  160. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
  161. package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
  162. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
  163. package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
  164. package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
  165. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
  166. package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
  167. package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
  168. package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
  169. package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
  170. package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
  171. package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
  172. package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
  173. package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
  174. package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
  175. package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
  176. package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
  177. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
  178. package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
  179. package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
  180. package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
  181. package/templates/apps/mcp/__tests__/server.test.ts +115 -0
  182. package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
  183. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
  184. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
  185. package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
  186. package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
  187. package/templates/apps/mcp/jest.config.cjs +82 -0
  188. package/templates/apps/mcp/package.json +37 -0
  189. package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
  190. package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
  191. package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
  192. package/templates/apps/mcp/src/abuse/index.ts +111 -0
  193. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
  194. package/templates/apps/mcp/src/http-transport.ts +303 -0
  195. package/templates/apps/mcp/src/index.ts +67 -0
  196. package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
  197. package/templates/apps/mcp/src/lib/errors.ts +61 -0
  198. package/templates/apps/mcp/src/lib/locales.ts +171 -0
  199. package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
  200. package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
  201. package/templates/apps/mcp/src/server.ts +139 -0
  202. package/templates/apps/mcp/src/tools/audit.ts +103 -0
  203. package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
  204. package/templates/apps/mcp/src/tools/create_page.ts +95 -0
  205. package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
  206. package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
  207. package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
  208. package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
  209. package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
  210. package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
  211. package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
  212. package/templates/apps/mcp/src/tools/index.ts +125 -0
  213. package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
  214. package/templates/apps/mcp/src/tools/purchase.ts +89 -0
  215. package/templates/apps/mcp/src/tools/read_content.ts +61 -0
  216. package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
  217. package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
  218. package/templates/apps/mcp/src/tools/search_content.ts +59 -0
  219. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
  220. package/templates/apps/mcp/src/tools/update_page.ts +91 -0
  221. package/templates/apps/mcp/tsconfig.json +19 -0
  222. package/templates/apps/web/.env.example +47 -0
  223. package/templates/apps/web/CLAUDE.md +81 -0
  224. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
  225. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
  226. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
  227. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
  228. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
  229. package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
  230. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
  231. package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
  232. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
  233. package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
  234. package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
  235. package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
  236. package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
  237. package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
  238. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
  239. package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
  240. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
  241. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
  242. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
  243. package/templates/apps/web/__tests__/page.test.tsx +230 -0
  244. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
  245. package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
  246. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
  247. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
  248. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
  249. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
  250. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
  251. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
  252. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
  253. package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
  254. package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
  255. package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
  256. package/templates/apps/web/__tests__/setup.ts +82 -0
  257. package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
  258. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
  259. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
  260. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
  261. package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
  262. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
  263. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
  264. package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
  266. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
  267. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
  268. package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
  269. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
  270. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
  271. package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
  272. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
  273. package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
  274. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
  275. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
  276. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
  277. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
  278. package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
  279. package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
  280. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
  281. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
  282. package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
  283. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
  284. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
  285. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
  286. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
  287. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
  288. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
  289. package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
  290. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
  291. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
  292. package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
  293. package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
  294. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
  295. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
  296. package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
  297. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
  298. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
  299. package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
  300. package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
  301. package/templates/apps/web/app/[locale]/layout.tsx +241 -0
  302. package/templates/apps/web/app/[locale]/page.tsx +41 -0
  303. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
  304. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
  305. package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
  306. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
  307. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
  308. package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
  309. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
  310. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
  311. package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
  312. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
  313. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
  314. package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
  315. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
  316. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
  317. package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
  318. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
  319. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
  320. package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
  321. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
  322. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
  323. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
  324. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
  325. package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
  326. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
  327. package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
  328. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
  329. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
  330. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
  331. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
  332. package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
  333. package/templates/apps/web/app/error-fallback.tsx +75 -0
  334. package/templates/apps/web/app/error.tsx +23 -0
  335. package/templates/apps/web/app/layout.tsx +18 -0
  336. package/templates/apps/web/app/llms.txt/route.ts +101 -0
  337. package/templates/apps/web/app/not-found.tsx +48 -0
  338. package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
  339. package/templates/apps/web/app/og/route.tsx +135 -0
  340. package/templates/apps/web/app/robots.txt/route.ts +36 -0
  341. package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
  342. package/templates/apps/web/auth.ts +141 -0
  343. package/templates/apps/web/components/ThemeToggle.tsx +60 -0
  344. package/templates/apps/web/components/google-analytics.tsx +34 -0
  345. package/templates/apps/web/content/en/blog/index.mdx +15 -0
  346. package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
  347. package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
  348. package/templates/apps/web/i18n/request.ts +36 -0
  349. package/templates/apps/web/jest.config.cjs +100 -0
  350. package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
  351. package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
  352. package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
  353. package/templates/apps/web/lib/auth-env.ts +66 -0
  354. package/templates/apps/web/lib/blog-list-data.ts +95 -0
  355. package/templates/apps/web/lib/blog-post-data.ts +79 -0
  356. package/templates/apps/web/lib/nav-config.ts +13 -0
  357. package/templates/apps/web/lib/observability/better-stack.ts +27 -0
  358. package/templates/apps/web/lib/og.ts +23 -0
  359. package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
  360. package/templates/apps/web/lib/source.ts +34 -0
  361. package/templates/apps/web/messages/en.json +131 -0
  362. package/templates/apps/web/middleware.ts +80 -0
  363. package/templates/apps/web/next.config.mjs +167 -0
  364. package/templates/apps/web/package.json +62 -0
  365. package/templates/apps/web/public/.well-known/security.txt +17 -0
  366. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  367. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  368. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  369. package/templates/apps/web/public/favicon-16x16.png +0 -0
  370. package/templates/apps/web/public/favicon-32x32.png +0 -0
  371. package/templates/apps/web/public/favicon.ico +0 -0
  372. package/templates/apps/web/public/favicon.svg +4 -0
  373. package/templates/apps/web/public/logo-dark.svg +21 -0
  374. package/templates/apps/web/public/logo-light.svg +21 -0
  375. package/templates/apps/web/public/logomark-dark.svg +4 -0
  376. package/templates/apps/web/public/logomark-light.svg +4 -0
  377. package/templates/apps/web/public/og-default.png +0 -0
  378. package/templates/apps/web/public/site.webmanifest +19 -0
  379. package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
  380. package/templates/apps/web/security-headers.config.ts +132 -0
  381. package/templates/apps/web/sentry.client.config.ts +27 -0
  382. package/templates/apps/web/sentry.edge.config.ts +15 -0
  383. package/templates/apps/web/sentry.server.config.ts +20 -0
  384. package/templates/apps/web/source.config.ts +78 -0
  385. package/templates/apps/web/src/components/SiteNav.tsx +21 -0
  386. package/templates/apps/web/src/lib/routes.ts +37 -0
  387. package/templates/apps/web/tsconfig.json +35 -0
  388. package/templates/apps/web/types/messages.d.ts +19 -0
  389. package/templates/content/.gitkeep +0 -0
  390. package/templates/content/CLAUDE.md +85 -0
  391. package/templates/content/_site.mdx +38 -0
  392. package/templates/content/ar/HomePage/home.mdx +123 -0
  393. package/templates/content/ar/_site.mdx +35 -0
  394. package/templates/content/de/_site.mdx +29 -0
  395. package/templates/content/en/.gitkeep +0 -0
  396. package/templates/content/en/AboutPage/about.mdx +19 -0
  397. package/templates/content/en/AboutPage/about.schema.json +82 -0
  398. package/templates/content/en/BookCall/book-call.mdx +19 -0
  399. package/templates/content/en/BookCall/book-call.schema.json +121 -0
  400. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
  401. package/templates/content/en/CareersList/careers.mdx +24 -0
  402. package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
  403. package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
  404. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
  405. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
  406. package/templates/content/en/ClientsList/clients.mdx +38 -0
  407. package/templates/content/en/ContactPage/contact.mdx +19 -0
  408. package/templates/content/en/ContactPage/contact.schema.json +100 -0
  409. package/templates/content/en/ContactPoint/primary.mdx +14 -0
  410. package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
  411. package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
  412. package/templates/content/en/CoursesList/courses.mdx +22 -0
  413. package/templates/content/en/DownloadsList/downloads.mdx +22 -0
  414. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
  415. package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
  416. package/templates/content/en/EventsList/events.mdx +26 -0
  417. package/templates/content/en/FAQPage/faq.mdx +420 -0
  418. package/templates/content/en/FAQPage/faq.schema.json +231 -0
  419. package/templates/content/en/GlossaryList/glossary.mdx +19 -0
  420. package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
  421. package/templates/content/en/HomePage/home.mdx +312 -0
  422. package/templates/content/en/HomePage/home.schema.json +88 -0
  423. package/templates/content/en/HomePage/launch-service.mdx +475 -0
  424. package/templates/content/en/HomePage/launch-service.schema.json +138 -0
  425. package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
  426. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
  427. package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
  428. package/templates/content/en/Integration/github-actions.mdx +31 -0
  429. package/templates/content/en/Integration/openai-api.mdx +26 -0
  430. package/templates/content/en/Integration/vercel.mdx +32 -0
  431. package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
  432. package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
  433. package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
  434. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
  435. package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
  436. package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
  437. package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
  438. package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
  439. package/templates/content/en/LandingPage/developers.mdx +19 -0
  440. package/templates/content/en/LandingPage/developers.schema.json +109 -0
  441. package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
  442. package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
  443. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
  444. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
  445. package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
  446. package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
  447. package/templates/content/en/LandingPage/platform.mdx +369 -0
  448. package/templates/content/en/LandingPage/platform.schema.json +166 -0
  449. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
  450. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
  451. package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
  452. package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
  453. package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
  454. package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
  455. package/templates/content/en/Organization/site-owner.mdx +16 -0
  456. package/templates/content/en/Partner/prisma.mdx +31 -0
  457. package/templates/content/en/Partner/supabase.mdx +30 -0
  458. package/templates/content/en/Partner/vercel.mdx +28 -0
  459. package/templates/content/en/PartnersList/partners.mdx +22 -0
  460. package/templates/content/en/Person/founder.mdx +18 -0
  461. package/templates/content/en/Pricing/pricing.mdx +538 -0
  462. package/templates/content/en/Pricing/pricing.schema.json +209 -0
  463. package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
  464. package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
  465. package/templates/content/en/Product/ailk.mdx +19 -0
  466. package/templates/content/en/Product/ailk.schema.json +94 -0
  467. package/templates/content/en/Product/api-launch-kit.mdx +26 -0
  468. package/templates/content/en/Product/launch-kit.mdx +28 -0
  469. package/templates/content/en/ProductsList/products.mdx +26 -0
  470. package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
  471. package/templates/content/en/ProjectsList/projects.mdx +25 -0
  472. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
  473. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
  474. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
  475. package/templates/content/en/ResourcesHub/resources.mdx +22 -0
  476. package/templates/content/en/Service/consulting.mdx +22 -0
  477. package/templates/content/en/Service/implementation.mdx +30 -0
  478. package/templates/content/en/Service/launch-service.mdx +232 -0
  479. package/templates/content/en/Service/launch.mdx +228 -0
  480. package/templates/content/en/ServicesList/services.mdx +84 -0
  481. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
  482. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
  483. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
  484. package/templates/content/en/TeamList/team.mdx +63 -0
  485. package/templates/content/en/TeamMember/founder.mdx +72 -0
  486. package/templates/content/en/TermsPage/terms.mdx +19 -0
  487. package/templates/content/en/TermsPage/terms.schema.json +99 -0
  488. package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
  489. package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
  490. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
  491. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
  492. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
  493. package/templates/content/en-XA/AboutPage/about.mdx +8 -0
  494. package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
  495. package/templates/content/en-XA/HomePage/home.mdx +8 -0
  496. package/templates/content/es/_site.mdx +30 -0
  497. package/templates/content/fr/_site.mdx +30 -0
  498. package/templates/content/ja/_site.mdx +30 -0
  499. package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
  500. package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
  501. package/templates/content/pt-BR/HomePage/home.mdx +23 -0
  502. package/templates/database/CHANGELOG.md +5 -0
  503. package/templates/database/README.md +234 -0
  504. package/templates/database/__tests__/user-cascade.test.ts +125 -0
  505. package/templates/database/auth/.gitkeep +0 -0
  506. package/templates/database/config/.gitkeep +0 -0
  507. package/templates/database/content/.gitkeep +0 -0
  508. package/templates/database/inbox/.gitkeep +0 -0
  509. package/templates/database/inbox/README.md +3 -0
  510. package/templates/database/inbox/__tests__/schema.test.ts +40 -0
  511. package/templates/database/inbox/schema.prisma +52 -0
  512. package/templates/database/jest.config.cjs +29 -0
  513. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
  514. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
  515. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
  516. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
  517. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
  518. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
  519. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
  520. package/templates/database/migrations/migration_lock.toml +3 -0
  521. package/templates/database/ops/.gitkeep +0 -0
  522. package/templates/database/package.json +45 -0
  523. package/templates/database/prisma.config.ts +20 -0
  524. package/templates/database/schema.prisma +487 -0
  525. package/templates/database/scripts/db-generate-locked.sh +80 -0
  526. package/templates/database/seed-test.ts +85 -0
  527. package/templates/database/tsconfig.build.json +16 -0
  528. package/templates/database/tsconfig.json +9 -0
  529. package/templates/package.json +125 -0
  530. package/templates/pnpm-workspace.yaml +6 -0
  531. package/templates/project.yaml +59 -0
  532. package/templates/tsconfig.base.json +18 -0
  533. package/templates/tsconfig.json +45 -0
@@ -0,0 +1,25 @@
1
+ // Courses list page. Reads CoursesList MDX via the content adapter and emits
2
+ // a JSON-LD CollectionPage via the dispatcher.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { renderPage } from "@working-theory/page-renderer";
5
+ import type { ReactElement } from "react";
6
+
7
+ interface Props {
8
+ params: Promise<{ locale: string }>;
9
+ getMdxSource?: typeof defaultGetMdxSource;
10
+ }
11
+
12
+ export default async function Page({
13
+ params,
14
+ getMdxSource,
15
+ }: Props): Promise<ReactElement> {
16
+ const { locale } = await params;
17
+ return renderPage({
18
+ type: "CoursesList",
19
+ slug: "courses",
20
+ locale,
21
+ translationsNamespace: "courses",
22
+ bodyMode: "mdx-body",
23
+ ...(getMdxSource !== undefined && { getMdxSource }),
24
+ });
25
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Tests for purchaseAction Server Action.
3
+ *
4
+ * Acceptance criteria verified here:
5
+ * - purchaseAction(validInput) returns { url: string } on success
6
+ * - purchaseAction(invalidInput) returns Zod validation errors
7
+ * - purchaseAction calls @working-theory/api-client checkout helper
8
+ * - Parity shape: purchaseAction returns the same response shape as ailkPurchase
9
+ */
10
+
11
+ import { purchaseAction } from "../purchase.actions.js";
12
+
13
+ // Mock @working-theory/api-client
14
+ jest.mock("@working-theory/api-client", () => ({
15
+ checkout: {
16
+ createSession: jest.fn(),
17
+ },
18
+ }));
19
+
20
+ import { checkout } from "@working-theory/api-client";
21
+ const mockCreateSession = checkout.createSession as jest.Mock;
22
+
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ describe("purchaseAction — valid input", () => {
28
+ it("returns { url } on success", async () => {
29
+ mockCreateSession.mockResolvedValueOnce({
30
+ ok: true,
31
+ body: {
32
+ sessionId: "cs_test_abc123",
33
+ url: "https://checkout.stripe.com/pay/cs_test_abc123",
34
+ },
35
+ });
36
+
37
+ const result = await purchaseAction({
38
+ priceId: "price_test_123",
39
+ quantity: 1,
40
+ successUrl: "https://example.com/success",
41
+ cancelUrl: "https://example.com/cancel",
42
+ });
43
+
44
+ expect(result).toEqual({
45
+ url: "https://checkout.stripe.com/pay/cs_test_abc123",
46
+ });
47
+ });
48
+
49
+ it("returns { url } when quantity is omitted", async () => {
50
+ mockCreateSession.mockResolvedValueOnce({
51
+ ok: true,
52
+ body: {
53
+ sessionId: "cs_test_xyz",
54
+ url: "https://checkout.stripe.com/pay/cs_test_xyz",
55
+ },
56
+ });
57
+
58
+ const result = await purchaseAction({
59
+ priceId: "price_test_abc",
60
+ successUrl: "https://example.com/success",
61
+ cancelUrl: "https://example.com/cancel",
62
+ });
63
+
64
+ expect(result).toEqual({
65
+ url: "https://checkout.stripe.com/pay/cs_test_xyz",
66
+ });
67
+ });
68
+
69
+ it("calls checkout.createSession with mapped payload", async () => {
70
+ mockCreateSession.mockResolvedValueOnce({
71
+ ok: true,
72
+ body: {
73
+ sessionId: "cs_test_def",
74
+ url: "https://checkout.stripe.com/pay/cs_test_def",
75
+ },
76
+ });
77
+
78
+ await purchaseAction({
79
+ priceId: "price_plan_pro",
80
+ quantity: 2,
81
+ successUrl: "https://example.com/success",
82
+ cancelUrl: "https://example.com/cancel",
83
+ });
84
+
85
+ expect(mockCreateSession).toHaveBeenCalledWith({
86
+ productCode: "price_plan_pro",
87
+ quantity: 2,
88
+ successUrl: "https://example.com/success",
89
+ cancelUrl: "https://example.com/cancel",
90
+ });
91
+ });
92
+ });
93
+
94
+ describe("purchaseAction — invalid input (Zod errors)", () => {
95
+ it("returns validation errors when priceId is missing", async () => {
96
+ const result = await purchaseAction({
97
+ successUrl: "https://example.com/success",
98
+ cancelUrl: "https://example.com/cancel",
99
+ } as never);
100
+
101
+ expect(result).toHaveProperty("errors");
102
+ expect(mockCreateSession).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it("returns validation errors when priceId is empty", async () => {
106
+ const result = await purchaseAction({
107
+ priceId: "",
108
+ successUrl: "https://example.com/success",
109
+ cancelUrl: "https://example.com/cancel",
110
+ });
111
+
112
+ expect(result).toHaveProperty("errors");
113
+ expect(mockCreateSession).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it("returns validation errors when priceId contains invalid characters", async () => {
117
+ // priceId must be min length 1; test with a non-string
118
+ const result = await purchaseAction({
119
+ // eslint-disable-next-line no-restricted-syntax -- Phase 2/3 follow-up: pre-existing test-hygiene violation
120
+ priceId: 12345 as unknown as string,
121
+ successUrl: "https://example.com/success",
122
+ cancelUrl: "https://example.com/cancel",
123
+ });
124
+
125
+ expect(result).toHaveProperty("errors");
126
+ expect(mockCreateSession).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it("returns validation errors when quantity is not a positive integer", async () => {
130
+ const result = await purchaseAction({
131
+ priceId: "price_test",
132
+ quantity: 0,
133
+ successUrl: "https://example.com/success",
134
+ cancelUrl: "https://example.com/cancel",
135
+ });
136
+
137
+ expect(result).toHaveProperty("errors");
138
+ expect(mockCreateSession).not.toHaveBeenCalled();
139
+ });
140
+ });
141
+
142
+ describe("purchaseAction — parity shape with ailkPurchase", () => {
143
+ it("success shape matches ailkPurchase success shape: { url: string }", async () => {
144
+ mockCreateSession.mockResolvedValueOnce({
145
+ ok: true,
146
+ body: {
147
+ sessionId: "cs_test_parity",
148
+ url: "https://checkout.stripe.com/pay/cs_test_parity",
149
+ },
150
+ });
151
+
152
+ const result = await purchaseAction({
153
+ priceId: "price_parity",
154
+ successUrl: "https://example.com/success",
155
+ cancelUrl: "https://example.com/cancel",
156
+ });
157
+
158
+ expect(typeof (result as { url: string }).url).toBe("string");
159
+ });
160
+ });
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ // Per-segment error boundary for the /dev/purchase (checkout) route.
3
+ // Does not make any claim about whether payment was processed.
4
+ import type { ReactElement } from "react";
5
+
6
+ import { ErrorFallback } from "../../../error-fallback";
7
+
8
+ interface ErrorProps {
9
+ error: Error & { digest?: string };
10
+ reset: () => void;
11
+ }
12
+
13
+ export default function PurchaseError({
14
+ error,
15
+ reset,
16
+ }: ErrorProps): ReactElement {
17
+ return (
18
+ <ErrorFallback
19
+ error={error}
20
+ reset={reset}
21
+ heading="Checkout couldn't complete"
22
+ body="Something interrupted your purchase attempt. Please try again to continue."
23
+ />
24
+ );
25
+ }
@@ -0,0 +1,117 @@
1
+ /* eslint-disable no-restricted-syntax -- i18n-allow: dev-only route, not in customer navigation (ADR 0019) */
2
+ "use client";
3
+
4
+ /**
5
+ * /dev/purchase — demo route for the purchaseAction Server Action.
6
+ *
7
+ * SD-3d: /dev/-prefixed routes are not in customer-visible navigation.
8
+ * This page exists to exercise the Server Action / MCP tool parity pair
9
+ * in a browser context during Phase 3 development.
10
+ */
11
+
12
+ import { useState } from "react";
13
+
14
+ import { purchaseAction } from "./purchase.actions";
15
+ import type { PurchaseActionResult } from "./purchase.actions";
16
+
17
+ export default function DevPurchasePage() {
18
+ const [priceId, setPriceId] = useState("price_test_123");
19
+ const [result, setResult] = useState<PurchaseActionResult | null>(null);
20
+ const [pending, setPending] = useState(false);
21
+
22
+ async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
23
+ e.preventDefault();
24
+ setPending(true);
25
+ setResult(null);
26
+ try {
27
+ const res = await purchaseAction({ priceId });
28
+ setResult(res);
29
+ if ("url" in res) {
30
+ window.location.href = res.url;
31
+ }
32
+ } finally {
33
+ setPending(false);
34
+ }
35
+ }
36
+
37
+ return (
38
+ <main style={{ padding: "2rem", maxWidth: 480 }}>
39
+ <h1>Dev: Purchase (Server Action smoke test)</h1>
40
+ <p style={{ fontSize: "0.875rem", color: "#666" }}>
41
+ /dev/ — not in customer navigation
42
+ </p>
43
+
44
+ <form
45
+ onSubmit={(e) => void handleSubmit(e)}
46
+ style={{
47
+ display: "flex",
48
+ flexDirection: "column",
49
+ gap: "1rem",
50
+ marginTop: "1.5rem",
51
+ }}
52
+ >
53
+ <label>
54
+ <span
55
+ style={{
56
+ display: "block",
57
+ marginBottom: "0.25rem",
58
+ fontWeight: 500,
59
+ }}
60
+ >
61
+ Price ID
62
+ </span>
63
+ <input
64
+ type="text"
65
+ value={priceId}
66
+ onChange={(e) => setPriceId(e.target.value)}
67
+ placeholder="price_test_..."
68
+ required
69
+ style={{
70
+ width: "100%",
71
+ padding: "0.5rem",
72
+ border: "1px solid #ccc",
73
+ borderRadius: 4,
74
+ }}
75
+ />
76
+ </label>
77
+
78
+ <button
79
+ type="submit"
80
+ disabled={pending}
81
+ style={{
82
+ padding: "0.5rem 1rem",
83
+ background: "#0070f3",
84
+ color: "#fff",
85
+ border: "none",
86
+ borderRadius: 4,
87
+ cursor: pending ? "not-allowed" : "pointer",
88
+ }}
89
+ >
90
+ {pending ? "Creating session…" : "Start checkout"}
91
+ </button>
92
+ </form>
93
+
94
+ {result && (
95
+ <div style={{ marginTop: "1.5rem" }}>
96
+ {"errors" in result ? (
97
+ <div>
98
+ <strong>Validation errors:</strong>
99
+ <ul>
100
+ {result.errors.map((e, i) => (
101
+ <li key={i}>
102
+ {e.path.join(".") || "input"}: {e.message}
103
+ </li>
104
+ ))}
105
+ </ul>
106
+ </div>
107
+ ) : (
108
+ <div>
109
+ <strong>Checkout URL:</strong>{" "}
110
+ <a href={result.url}>{result.url}</a>
111
+ </div>
112
+ )}
113
+ </div>
114
+ )}
115
+ </main>
116
+ );
117
+ }
@@ -0,0 +1,58 @@
1
+ "use server";
2
+
3
+ /**
4
+ * purchaseAction — Server Action paired with ailkPurchase MCP tool.
5
+ *
6
+ * Validates input with purchaseInputSchema (shared schema per ADR 0013),
7
+ * calls POST /api/checkout/sessions via @working-theory/api-client, and returns the
8
+ * Stripe hosted-checkout URL on success or a validation error object.
9
+ *
10
+ * Canonical filename: purchase.actions.ts (*.actions.ts per ADR 0013 / U0 spec
11
+ * parity-test regex `ACTION_FILE_RE = /\.actions\.ts$/`).
12
+ */
13
+
14
+ import { checkout } from "@working-theory/api-client";
15
+ import { purchaseInputSchema } from "@working-theory/validation";
16
+ import type { PurchaseInput, PurchaseOutput } from "@working-theory/validation";
17
+
18
+ export type PurchaseActionResult =
19
+ | PurchaseOutput
20
+ | { errors: { path: (string | number)[]; message: string }[] };
21
+
22
+ /**
23
+ * Initiate a Stripe checkout session for the given price.
24
+ *
25
+ * On success: returns `{ url: string }` — redirect the user to this URL.
26
+ * On validation failure: returns `{ errors: ZodIssue[] }`.
27
+ * On API failure: throws (will surface as a Next.js server error boundary).
28
+ */
29
+ export async function purchaseAction(
30
+ input: PurchaseInput,
31
+ ): Promise<PurchaseActionResult> {
32
+ const parsed = purchaseInputSchema.safeParse(input);
33
+ if (!parsed.success) {
34
+ return {
35
+ errors: parsed.error.issues.map((issue) => ({
36
+ path: issue.path as (string | number)[],
37
+ message: issue.message,
38
+ })),
39
+ };
40
+ }
41
+
42
+ const { priceId, quantity, successUrl, cancelUrl } = parsed.data;
43
+
44
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
45
+
46
+ const result = await checkout.createSession({
47
+ productCode: priceId,
48
+ ...(quantity !== undefined && { quantity }),
49
+ successUrl: successUrl ?? `${baseUrl}/purchase/success`,
50
+ cancelUrl: cancelUrl ?? `${baseUrl}/purchase/cancel`,
51
+ });
52
+
53
+ if (!result.ok) {
54
+ throw new Error(`Checkout session creation failed (${result.status})`);
55
+ }
56
+
57
+ return { url: result.body.url };
58
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * /docs catch-all route. Renders placeholder docs from
3
+ * `apps/web/content/en/docs/` via the Fumadocs docsSource loader.
4
+ *
5
+ * Mirrors the blog route structure — same injectable-source pattern for
6
+ * testability. Index renders a list of all docs pages; slug routes render
7
+ * the individual doc.
8
+ *
9
+ * Issue #1459: per-app content directory divergence (2026-05-25).
10
+ */
11
+ import type { Metadata } from "next";
12
+ import { notFound } from "next/navigation";
13
+ import { getTranslations } from "next-intl/server";
14
+ import type { ReactElement } from "react";
15
+
16
+ import { docsSource as defaultDocsSource } from "../../../../lib/source";
17
+
18
+ /**
19
+ * Frontmatter contract — must match `source.config.ts` `docs.schema`.
20
+ */
21
+ interface DocsFrontmatter {
22
+ title: string;
23
+ description: string;
24
+ }
25
+
26
+ /**
27
+ * Minimal docs-source interface covering the subset consumed by this route.
28
+ * Tests inject a stub without mocking lib/source at the module level.
29
+ */
30
+ interface DocsSource {
31
+ getPages: () => Array<{ url: string; data: unknown }>;
32
+ getPage: (slug: string[]) => { data: unknown } | undefined;
33
+ }
34
+
35
+ interface DocsRouteProps {
36
+ params: Promise<{ locale: string; slug?: string[] }>;
37
+ /**
38
+ * Docs source override for testing. Defaults to the real Fumadocs docsSource.
39
+ */
40
+ docsSource?: DocsSource;
41
+ }
42
+
43
+ export async function generateMetadata(
44
+ props: DocsRouteProps,
45
+ ): Promise<Metadata> {
46
+ const { slug } = await props.params;
47
+ const source = props.docsSource ?? defaultDocsSource;
48
+
49
+ if (slug && slug.length > 0) {
50
+ const page = source.getPage(slug);
51
+ if (page) {
52
+ const data = page.data as unknown as DocsFrontmatter;
53
+ return {
54
+ title: data.title,
55
+ description: data.description,
56
+ };
57
+ }
58
+ }
59
+
60
+ return {
61
+ title: "Docs",
62
+ description: "Documentation for AI Launch Kit.",
63
+ };
64
+ }
65
+
66
+ export default async function DocsRoute(
67
+ props: DocsRouteProps,
68
+ ): Promise<ReactElement> {
69
+ const { slug } = await props.params;
70
+ const t = await getTranslations("docs");
71
+ const source = props.docsSource ?? defaultDocsSource;
72
+
73
+ if (slug && slug.length > 0) {
74
+ const page = source.getPage(slug);
75
+ if (!page) notFound();
76
+
77
+ const data = page.data as unknown as DocsFrontmatter;
78
+
79
+ return (
80
+ <main>
81
+ <h1>{data.title}</h1>
82
+ <p>{data.description}</p>
83
+ </main>
84
+ );
85
+ }
86
+
87
+ const pages = source.getPages();
88
+
89
+ if (pages.length === 0) {
90
+ return (
91
+ <main>
92
+ <h1>{t("heading")}</h1>
93
+ <p>
94
+ {t("emptyState")} <code>{"apps/web/content/en/docs/"}</code>{" "}
95
+ {t("emptyStateTrailing")}
96
+ </p>
97
+ </main>
98
+ );
99
+ }
100
+
101
+ return (
102
+ <main>
103
+ <h1>{t("heading")}</h1>
104
+ <ul>
105
+ {pages.map((page) => {
106
+ const data = page.data as unknown as DocsFrontmatter;
107
+ return (
108
+ <li key={page.url}>
109
+ <a href={page.url}>{data.title}</a>
110
+ {" \u2014 "}
111
+ {data.description}
112
+ </li>
113
+ );
114
+ })}
115
+ </ul>
116
+ </main>
117
+ );
118
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * HTML snapshot regression tests for the downloads route →
3
+ * @working-theory/page-renderer renderPage({ bodyMode: "mdx-body" }) migration.
4
+ *
5
+ * Two-commit strategy:
6
+ * Commit 1: snapshots captured against pre-migration route (baseline).
7
+ * Commit 2: route migrated; same snapshots verify diff=0.
8
+ *
9
+ * Routes covered:
10
+ * - downloads/page.tsx (empty + populated — 2 snapshots)
11
+ *
12
+ * Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
13
+ */
14
+ import React from "react";
15
+ import { render } from "@testing-library/react";
16
+
17
+ // ── next-mdx-remote/rsc mock ──────────────────────────────────────────────────
18
+ jest.mock("next-mdx-remote/rsc", () => ({
19
+ MDXRemote: ({ source }: { source: string }): React.ReactElement =>
20
+ React.createElement("div", { "data-testid": "mdx-rendered" }, source),
21
+ }));
22
+
23
+ // ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
24
+ jest.mock("@working-theory/ui", () => ({
25
+ SectionRenderer: ({ sections }: { sections: unknown[] }) =>
26
+ React.createElement("div", {
27
+ "data-testid": "section-renderer",
28
+ "data-section-count": String(sections.length),
29
+ }),
30
+ }));
31
+
32
+ import DownloadsPage from "../page";
33
+
34
+ const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
35
+
36
+ // ── DownloadsPage ─────────────────────────────────────────────────────────────
37
+
38
+ describe("DownloadsPage — snapshot regression", () => {
39
+ it("empty state renders deterministic HTML", async () => {
40
+ const element = await DownloadsPage({
41
+ params: Promise.resolve({ locale: "en" }),
42
+ getMdxSource: getMdxSourceEmpty,
43
+ });
44
+ const { container } = render(element);
45
+ expect(container.innerHTML).toMatchInlineSnapshot(
46
+ `"<main><h1>Downloads</h1><p data-testid="section-empty-state">Downloads listing coming soon.</p></main>"`,
47
+ );
48
+ });
49
+
50
+ it("populated state renders deterministic HTML", async () => {
51
+ const getMdxSourcePopulated = jest.fn().mockResolvedValue({
52
+ frontmatter: {
53
+ type: "DownloadsList",
54
+ slug: "downloads",
55
+ title: "Downloads",
56
+ description: "Downloadable assets and templates.",
57
+ },
58
+ body: "# Downloads\n\nBody.",
59
+ });
60
+
61
+ const element = await DownloadsPage({
62
+ params: Promise.resolve({ locale: "en" }),
63
+ getMdxSource: getMdxSourcePopulated,
64
+ });
65
+ const { container } = render(element);
66
+ expect(container.innerHTML).toMatchInlineSnapshot(`
67
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Downloads","description":"Downloadable assets and templates.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"DigitalDocument"}]}}</script><main><h1>Downloads</h1><p data-testid="downloads-description">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered"># Downloads
68
+
69
+ Body.</div></article></div></div></main>"
70
+ `);
71
+ });
72
+ });
@@ -0,0 +1,25 @@
1
+ // Downloads list page. Reads DownloadsList MDX via renderPage and emits
2
+ // a JSON-LD CollectionPage via the dispatcher. List-only — no detail route.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { renderPage } from "@working-theory/page-renderer";
5
+ import type { ReactElement } from "react";
6
+
7
+ interface Props {
8
+ params: Promise<{ locale: string }>;
9
+ getMdxSource?: typeof defaultGetMdxSource;
10
+ }
11
+
12
+ export default async function Page({
13
+ params,
14
+ getMdxSource,
15
+ }: Props): Promise<ReactElement> {
16
+ const { locale } = await params;
17
+ return renderPage({
18
+ type: "DownloadsList",
19
+ slug: "downloads",
20
+ locale,
21
+ translationsNamespace: "downloads",
22
+ bodyMode: "mdx-body",
23
+ ...(getMdxSource !== undefined && { getMdxSource }),
24
+ });
25
+ }
@@ -0,0 +1,47 @@
1
+ // Event detail page. Reads the Event MDX for the given slug, renders via
2
+ // renderPage, and emits a JSON-LD <script>. Unknown slugs trigger 404.
3
+ import {
4
+ renderPage,
5
+ getMdxSource as defaultGetMdxSource,
6
+ getMdxSlugs,
7
+ } from "@working-theory/page-renderer";
8
+ import { notFound } from "next/navigation";
9
+ import type { ReactElement } from "react";
10
+
11
+ interface Props {
12
+ params: Promise<{ locale: string; slug: string }>;
13
+ getMdxSource?: typeof defaultGetMdxSource;
14
+ }
15
+
16
+ export async function generateStaticParams({
17
+ params,
18
+ }: {
19
+ params: { locale: string };
20
+ }): Promise<{ slug: string }[]> {
21
+ const slugs = await getMdxSlugs("Event", params.locale);
22
+ return slugs.map((slug) => ({ slug }));
23
+ }
24
+
25
+ export default async function Page({
26
+ params,
27
+ getMdxSource = defaultGetMdxSource,
28
+ }: Props): Promise<ReactElement> {
29
+ const { locale, slug } = await params;
30
+
31
+ // Pre-check: detail pages call notFound() rather than rendering an empty-state.
32
+ // Inject the resolved source so renderPage does not call getMdxSource a second time.
33
+ const source = await getMdxSource("Event", slug, locale);
34
+
35
+ if (!source) {
36
+ notFound();
37
+ }
38
+
39
+ return renderPage({
40
+ type: "Event",
41
+ slug,
42
+ locale,
43
+ translationsNamespace: "events",
44
+ bodyMode: "mdx-body",
45
+ getMdxSource: async () => source,
46
+ });
47
+ }