create-ailk 0.8.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 (211) hide show
  1. package/README.md +68 -0
  2. package/component-catalog.json +279 -45
  3. package/dist/add-module.d.ts +20 -0
  4. package/dist/add-module.js +43 -7
  5. package/dist/add-route.d.ts +56 -0
  6. package/dist/add-route.js +286 -0
  7. package/dist/apply-design-md.d.ts +7 -0
  8. package/dist/apply-design-md.js +50 -0
  9. package/dist/apply-design.d.ts +64 -0
  10. package/dist/apply-design.js +273 -0
  11. package/dist/claude-md.d.ts +111 -0
  12. package/dist/claude-md.js +334 -0
  13. package/dist/cli.js +92 -4
  14. package/dist/derive-architecture.js +41 -36
  15. package/dist/design-compile.d.ts +71 -0
  16. package/dist/design-compile.js +98 -0
  17. package/dist/design-package.d.ts +83 -0
  18. package/dist/design-package.js +468 -0
  19. package/dist/design-presets.d.ts +86 -0
  20. package/dist/design-presets.js +642 -0
  21. package/dist/design-scaffold.d.ts +76 -0
  22. package/dist/design-scaffold.js +220 -0
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +4 -1
  25. package/dist/legacy-nav-renderers.d.ts +45 -0
  26. package/dist/legacy-nav-renderers.js +545 -0
  27. package/dist/lib/auth-secret-file.d.ts +19 -0
  28. package/dist/lib/auth-secret-file.js +112 -0
  29. package/dist/lib/design-checks.d.ts +78 -0
  30. package/dist/lib/design-checks.js +174 -0
  31. package/dist/lib/license-key-files.d.ts +9 -0
  32. package/dist/lib/license-key-files.js +2 -2
  33. package/dist/lib/module-license-gate.d.ts +19 -9
  34. package/dist/lib/module-license-gate.js +19 -8
  35. package/dist/lib/render-verdict.d.ts +52 -0
  36. package/dist/lib/render-verdict.js +77 -0
  37. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  38. package/dist/lib/scaffold-render-sweep.js +236 -0
  39. package/dist/module-architecture.d.ts +19 -16
  40. package/dist/module-architecture.js +64 -21
  41. package/dist/parse-args.d.ts +12 -2
  42. package/dist/parse-args.js +74 -13
  43. package/dist/programmatic.d.ts +30 -6
  44. package/dist/programmatic.js +206 -17
  45. package/dist/site-chrome.d.ts +19 -9
  46. package/dist/site-chrome.js +23 -13
  47. package/dist/site-manifest.d.ts +25 -0
  48. package/dist/site-manifest.js +254 -0
  49. package/dist/site-roster.d.ts +41 -0
  50. package/dist/site-roster.js +141 -8
  51. package/dist/starter-grid.d.ts +32 -16
  52. package/dist/starter-grid.js +129 -33
  53. package/dist/surfaces.d.ts +41 -9
  54. package/dist/surfaces.js +200 -83
  55. package/dist/template-manifest.d.ts +30 -13
  56. package/dist/template-manifest.js +49 -24
  57. package/package.json +5 -4
  58. package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
  59. package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
  60. package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
  61. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  62. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  63. package/site-templates/designs/minimalism/sample/README.md +48 -0
  64. package/site-templates/designs/standard/DESIGN.md +98 -6
  65. package/site-templates/marketing-site/DESIGN.md +98 -6
  66. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
  68. package/site-templates/marketing-site-free/DESIGN.md +98 -6
  69. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
  70. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
  71. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  72. package/site-templates/saas-product/DESIGN.md +98 -6
  73. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
  74. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
  75. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  76. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
  77. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
  78. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  79. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
  80. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
  81. package/site-templates/shapes/person-service/ailk-template.json +1 -1
  82. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
  83. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
  84. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
  85. package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
  86. package/templates/.claude/rules/architecture.md +7 -0
  87. package/templates/.claude/skills/README.md +3 -2
  88. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  89. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  90. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  91. package/templates/.claude/skills/add-page/references/page-types.md +85 -76
  92. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  93. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  94. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  95. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  96. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  97. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  98. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  99. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  100. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  101. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +28 -11
  102. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  103. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  104. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  105. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  106. package/templates/.github/workflows/ci.yml +4 -0
  107. package/templates/.prettierignore +1 -0
  108. package/templates/CLAUDE.md +146 -103
  109. package/templates/DESIGN.md +98 -6
  110. package/templates/README.md +50 -38
  111. package/templates/apps/api/.env.example +2 -1
  112. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  113. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  114. package/templates/apps/api/src/lib/stripe.ts +9 -0
  115. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  116. package/templates/apps/api/src/middleware/auth.ts +54 -0
  117. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  118. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  119. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  120. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  121. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  122. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  123. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  124. package/templates/apps/api/src/server.ts +28 -0
  125. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  126. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  127. package/templates/apps/api/src/services/revalidation.ts +60 -15
  128. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  129. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  130. package/templates/apps/web/.env.example +13 -0
  131. package/templates/apps/web/CLAUDE.md +22 -28
  132. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  133. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  134. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  135. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  136. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  137. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  139. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  140. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
  141. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  142. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  143. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  144. package/templates/apps/web/app/[locale]/layout.tsx +35 -19
  145. package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
  146. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
  147. package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
  148. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  149. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  150. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  151. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  152. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  153. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  154. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  155. package/templates/apps/web/lib/locale-guard.ts +18 -0
  156. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  157. package/templates/apps/web/lib/nav-config.ts +14 -13
  158. package/templates/apps/web/lib/site-brand.tsx +13 -9
  159. package/templates/apps/web/lib/site-logo.tsx +36 -0
  160. package/templates/apps/web/lib/site-theme.ts +46 -26
  161. package/templates/apps/web/messages/en.json +23 -9
  162. package/templates/apps/web/middleware.ts +28 -4
  163. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  164. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  165. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  166. package/templates/content/en/BookCall/start-project.mdx +56 -0
  167. package/templates/content/en/ContactPage/contact.mdx +40 -1
  168. package/templates/content/en/HomePage/home.mdx +2 -6
  169. package/templates/content/en/LandingPage/meet.mdx +59 -0
  170. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  171. package/templates/database/CHANGELOG.md +7 -0
  172. package/templates/database/package.json +1 -1
  173. package/templates/package.json +2 -1
  174. package/templates/tools/check-claude-md.ts +128 -0
  175. package/versions.json +33 -33
  176. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
  177. package/site-templates/minimalism-site/DESIGN.md +0 -162
  178. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
  179. package/site-templates/minimalism-site/ailk-template.json +0 -23
  180. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  181. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
  182. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  183. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  184. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  185. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  186. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
  187. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  188. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  189. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  190. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  191. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
  192. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
  193. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
  194. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
  195. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  196. package/site-templates/minimalism-site/provenance.json +0 -4
  197. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  198. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
  199. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  200. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  201. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  202. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  203. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  204. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  205. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  206. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  207. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  208. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  209. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  210. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  211. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
@@ -0,0 +1,545 @@
1
+ /**
2
+ * Frozen, published-generator `renderNavConfig` bodies (#6908 D2) — one per
3
+ * era a create-ailk release ever shipped (E1–E4, `docs/specs/6908-add-module-
4
+ * treats-every-pre-spec.md` § 1.2). `surfaces.ts`'s own `renderNavConfig`
5
+ * self-hashes its output (D1) so a nav it wrote can always prove itself; a
6
+ * nav written before the stamp existed carries none, and this module is the
7
+ * only way `add-module` can still recognize it as unedited.
8
+ *
9
+ * Each `render` below is copied VERBATIM from the named tag's own
10
+ * `packages/create-ailk/src/surfaces.ts`, at the named commit, with exactly
11
+ * two changes from the original:
12
+ *
13
+ * - `GATEABLE_SURFACES` is replaced by a frozen `{ id, navLabelKey }` table
14
+ * of that tag's own entries, in that tag's own order (the era's own
15
+ * `<era>_SURFACES` constant below) — so a later change to today's
16
+ * `GATEABLE_SURFACES` can never move a byte of a legacy render.
17
+ * - the `@working-theory/ui` import line stays fragment-assembled, exactly
18
+ * as the original does, so THIS file never contains the literal import
19
+ * string either (`tools/lint-architecture.ts` matches source text; see
20
+ * the comment on each `navItemImport` below).
21
+ *
22
+ * The list is CLOSED (D2): after D1 ships, every published generator is
23
+ * unstamped only up to 0.8.0 (E4), so a future generator change never needs a
24
+ * fifth entry. Fixtures produced by each tag's own code (never by this
25
+ * vendored copy) pin these bodies exactly — see
26
+ * `__tests__/fixtures/legacy-nav/index.json`.
27
+ */
28
+ // ---------------------------------------------------------------------------
29
+ // E1 — 0.1.1, 0.2.0, 0.3.0, 0.4.0 (commit 52e0685f3bff54832a76b5e0ec737c2f39b9d960)
30
+ // ---------------------------------------------------------------------------
31
+ const E1_SURFACES = [
32
+ { id: "blog", navLabelKey: "blog" },
33
+ { id: "careers" },
34
+ { id: "case-studies" },
35
+ { id: "clients" },
36
+ { id: "courses" },
37
+ { id: "dev" },
38
+ { id: "docs", navLabelKey: "docs" },
39
+ { id: "downloads" },
40
+ { id: "events" },
41
+ { id: "flows" },
42
+ { id: "how-to" },
43
+ { id: "integrations" },
44
+ { id: "launch-service" },
45
+ { id: "partners" },
46
+ { id: "products" },
47
+ { id: "projects" },
48
+ { id: "resources" },
49
+ { id: "schedule", navLabelKey: "schedule" },
50
+ { id: "services" },
51
+ { id: "software" },
52
+ { id: "tech-articles" },
53
+ { id: "testimonials" },
54
+ { id: "use-cases" },
55
+ { id: "whitepapers" },
56
+ ];
57
+ function renderNavConfigE1(enabled) {
58
+ const on = new Set(enabled);
59
+ const linked = E1_SURFACES.filter((s) => s.navLabelKey !== undefined && on.has(s.id));
60
+ const entries = [
61
+ ' { labelKey: "home", path: "/" },',
62
+ ...linked.map((s) => ` { labelKey: "${s.navLabelKey}", path: "/${s.id}" },`),
63
+ ];
64
+ // Fragment-assembled so this vendored copy never contains the literal
65
+ // import string either — see the module comment above.
66
+ const navItemImport = [
67
+ "import type { NavItem } ",
68
+ "from ",
69
+ '"@working-theory/ui";',
70
+ ].join("");
71
+ return `// nav-config.ts — site-nav registry.
72
+ //
73
+ // GENERATED by create-ailk from the declared route surfaces. Every entry here
74
+ // points at a route this site actually carries — the nav is regenerated with
75
+ // the surface set so it can never link a page that was not scaffolded.
76
+ //
77
+ // Adding a link: scaffold the page (the \`scaffold-page\` skill), then add an
78
+ // entry below with a \`labelKey\` that exists in the \`nav\` namespace of
79
+ // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
80
+ // them with the active locale at render.
81
+ ${navItemImport}
82
+
83
+ export const navItems: NavItem[] = [
84
+ ${entries.join("\n")}
85
+ ];
86
+ `;
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // E2 — 0.5.0, 0.6.0, 0.6.1 (commit b0a69cefecf491412aa7e6f1d64464a91620126d)
90
+ // ---------------------------------------------------------------------------
91
+ const E2_SURFACES = [
92
+ { id: "blog", navLabelKey: "blog" },
93
+ { id: "careers" },
94
+ { id: "case-studies" },
95
+ { id: "clients" },
96
+ { id: "courses" },
97
+ { id: "dev" },
98
+ { id: "docs", navLabelKey: "docs" },
99
+ { id: "downloads" },
100
+ { id: "events" },
101
+ { id: "flows" },
102
+ { id: "how-to" },
103
+ { id: "integrations" },
104
+ { id: "launch-service" },
105
+ { id: "meet", navLabelKey: "schedule" },
106
+ { id: "partners" },
107
+ { id: "pricing" },
108
+ { id: "privacy" },
109
+ { id: "products" },
110
+ { id: "projects" },
111
+ { id: "resources" },
112
+ { id: "services" },
113
+ { id: "software" },
114
+ { id: "tech-articles" },
115
+ { id: "terms" },
116
+ { id: "testimonials" },
117
+ { id: "use-cases" },
118
+ { id: "whitepapers" },
119
+ ];
120
+ function renderNavConfigE2(enabled) {
121
+ const on = new Set(enabled);
122
+ const linked = E2_SURFACES.filter((s) => s.navLabelKey !== undefined && on.has(s.id));
123
+ const entries = [
124
+ " {",
125
+ ' labelKey: "product",',
126
+ " flyout: {",
127
+ ' presentation: "list",',
128
+ ' width: "trigger",',
129
+ " features: [",
130
+ ' { labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },',
131
+ ' { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },',
132
+ ' { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },',
133
+ " ],",
134
+ ' actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],',
135
+ " },",
136
+ " },",
137
+ ' { labelKey: "howItWorks", path: "/how-it-works" },',
138
+ ...(on.has("pricing") ? [' { labelKey: "pricing", path: "/pricing" },'] : []),
139
+ ' { labelKey: "about", path: "/about" },',
140
+ ...linked.map((s) => ` { labelKey: "${s.navLabelKey}", path: "/${s.id}" },`),
141
+ ];
142
+ const navItemImport = [
143
+ "import type { NavItem } ",
144
+ "from ",
145
+ '"@working-theory/ui";',
146
+ ].join("");
147
+ return `// nav-config.ts — site-nav registry.
148
+ //
149
+ // GENERATED by create-ailk from the declared route surfaces. Every entry here
150
+ // points at a route this site actually carries — the nav is regenerated with
151
+ // the surface set so it can never link a page that was not scaffolded.
152
+ //
153
+ // Adding a link: scaffold the page (the \`scaffold-page\` skill), then add an
154
+ // entry below with a \`labelKey\` that exists in the \`nav\` namespace of
155
+ // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
156
+ // them with the active locale at render.
157
+ ${navItemImport}
158
+ import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
159
+
160
+ export const navItems: NavItem[] = [
161
+ ${entries.join("\n")}
162
+ ];
163
+ `;
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // E3 — 0.7.0 (commit cac431ba50c4b0ee5130b5bc206196240744be18)
167
+ // ---------------------------------------------------------------------------
168
+ const E3_SURFACES = [
169
+ { id: "blog", navLabelKey: "blog" },
170
+ { id: "book-a-call" },
171
+ { id: "careers" },
172
+ { id: "case-studies" },
173
+ { id: "clients" },
174
+ { id: "courses" },
175
+ { id: "dev" },
176
+ { id: "docs", navLabelKey: "docs" },
177
+ { id: "downloads" },
178
+ { id: "events" },
179
+ { id: "flows" },
180
+ { id: "glossary" },
181
+ { id: "how-to" },
182
+ { id: "integrations" },
183
+ { id: "launch-service" },
184
+ { id: "meet", navLabelKey: "schedule" },
185
+ { id: "newsletter" },
186
+ { id: "partners" },
187
+ { id: "pricing" },
188
+ { id: "privacy" },
189
+ { id: "products" },
190
+ { id: "projects" },
191
+ { id: "resources" },
192
+ { id: "services" },
193
+ { id: "tech-articles" },
194
+ { id: "terms" },
195
+ { id: "testimonials" },
196
+ { id: "use-cases" },
197
+ { id: "whitepapers" },
198
+ ];
199
+ function renderNavConfigE3(enabled) {
200
+ const on = new Set(enabled);
201
+ // `blog` is linked unconditionally below (operator ruling 2026-09-23), so
202
+ // it is excluded here; `use-cases` is the Free roster's own page (a
203
+ // module-less surface the roster enables, like `pricing`), linked in its
204
+ // fixed positions below rather than appended.
205
+ const linked = E3_SURFACES.filter((s) => s.navLabelKey !== undefined &&
206
+ on.has(s.id) &&
207
+ s.id !== "blog" &&
208
+ s.id !== "use-cases");
209
+ const useCases = on.has("use-cases");
210
+ const entries = [
211
+ " {",
212
+ ' labelKey: "product",',
213
+ " flyout: {",
214
+ // SOFT_LOCK_OK: #6508 — width "panel" (D4/D5): a fixed 640px, two-column
215
+ // grid, replacing the "list"/"trigger" one-column menu. `presentation` is
216
+ // omitted — it defaults to "grid" (D5). The `docs` action is dropped (it
217
+ // duplicated the first feature's /how-it-works destination, § 1.4).
218
+ ' width: "panel",',
219
+ " features: [",
220
+ ...(useCases
221
+ ? [' { labelKey: "useCases", descKey: "useCasesDesc", icon: Layers, path: "/use-cases" },']
222
+ : []),
223
+ ' { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/software/product" },',
224
+ ' { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },',
225
+ " ],",
226
+ " actions: [],",
227
+ " },",
228
+ " },",
229
+ // Operator ruling 2026-09-23: Product · Use cases · Blog · Pricing · About.
230
+ // `/blog` is always linked — the `blog` surface serves the real route, and
231
+ // with it off the `[slug]` route serves the overlay's module page.
232
+ ...(useCases ? [' { labelKey: "useCases", path: "/use-cases" },'] : []),
233
+ ' { labelKey: "blog", path: "/blog" },',
234
+ ...(on.has("pricing") ? [' { labelKey: "pricing", path: "/pricing" },'] : []),
235
+ ' { labelKey: "about", path: "/about" },',
236
+ ...linked.map((s) => ` { labelKey: "${s.navLabelKey}", path: "/${s.id}" },`),
237
+ ];
238
+ // The footer's column groups (operator ruling 2026-09-23, replacing #6468's
239
+ // Product/Company pair): Site pages, Features, Resources. Pricing rides the
240
+ // same `pricing` gate as the header item; `/blog` is always linked (above).
241
+ // The product links point at the product's own page, `/software/product`
242
+ // (#6658 D3): `software` is core, so they stay unconditional. Book a call,
243
+ // Glossary and Newsletter (#6658 D8) each ride their own surface's gate.
244
+ const footerEntries = [
245
+ " {",
246
+ ' headingKey: "footerSitePagesHeading",',
247
+ " items: [",
248
+ ' { labelKey: "features", path: "/software/product" },',
249
+ ...(on.has("pricing")
250
+ ? [' { labelKey: "pricing", path: "/pricing" },']
251
+ : []),
252
+ ...(useCases ? [' { labelKey: "useCases", path: "/use-cases" },'] : []),
253
+ ' { labelKey: "about", path: "/about" },',
254
+ ' { labelKey: "contact", path: "/contact" },',
255
+ ...(on.has("book-a-call")
256
+ ? [' { labelKey: "bookACall", path: "/book-a-call" },']
257
+ : []),
258
+ " ],",
259
+ " },",
260
+ " {",
261
+ ' headingKey: "footerFeaturesHeading",',
262
+ " items: [",
263
+ ' { labelKey: "feature1", path: "/software/product" },',
264
+ ' { labelKey: "feature2", path: "/software/product" },',
265
+ ' { labelKey: "feature3", path: "/software/product" },',
266
+ ' { labelKey: "feature4", path: "/software/product" },',
267
+ ' { labelKey: "feature5", path: "/software/product" },',
268
+ ' { labelKey: "allFeatures", path: "/software/product" },',
269
+ " ],",
270
+ " },",
271
+ " {",
272
+ ' headingKey: "footerResourcesHeading",',
273
+ " items: [",
274
+ ' { labelKey: "blog", path: "/blog" },',
275
+ ...(on.has("glossary")
276
+ ? [' { labelKey: "glossary", path: "/glossary" },']
277
+ : []),
278
+ ...(on.has("newsletter")
279
+ ? [' { labelKey: "newsletter", path: "/newsletter" },']
280
+ : []),
281
+ ' { labelKey: "faq", path: "/faq" },',
282
+ " ],",
283
+ " },",
284
+ ];
285
+ const navItemImport = [
286
+ "import type { NavItem } ",
287
+ "from ",
288
+ '"@working-theory/ui";',
289
+ ].join("");
290
+ return `// nav-config.ts — site-nav registry.
291
+ //
292
+ // GENERATED by create-ailk from the declared route surfaces. Every entry here
293
+ // points at a route this site actually carries — the nav is regenerated with
294
+ // the surface set so it can never link a page that was not scaffolded.
295
+ //
296
+ // Adding a link: scaffold the page (the \`add-page\` skill), then add an
297
+ // entry below with a \`labelKey\` that exists in the \`nav\` namespace of
298
+ // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
299
+ // them with the active locale at render.
300
+ ${navItemImport}
301
+ import { HelpCircle, Layers, Sparkles } from "lucide-react";
302
+
303
+ export const navItems: NavItem[] = [
304
+ ${entries.join("\n")}
305
+ ];
306
+
307
+ // Footer column groups. Keys only — the layout resolves each \`headingKey\`
308
+ // and \`labelKey\` through the same \`nav\` labels as the header.
309
+ export interface FooterColumnConfig {
310
+ headingKey: string;
311
+ items: { labelKey: string; path: string }[];
312
+ }
313
+
314
+ export const footerColumns: FooterColumnConfig[] = [
315
+ ${footerEntries.join("\n")}
316
+ ];
317
+ `;
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // E4 — 0.8.0 (commit 060c02b8190090d7495d42e220836141d0549809)
321
+ // ---------------------------------------------------------------------------
322
+ const E4_SURFACES = [
323
+ { id: "blog", navLabelKey: "blog" },
324
+ { id: "book-a-call" },
325
+ { id: "careers" },
326
+ { id: "case-studies" },
327
+ { id: "clients" },
328
+ { id: "courses" },
329
+ { id: "dev" },
330
+ { id: "docs", navLabelKey: "docs" },
331
+ { id: "downloads" },
332
+ { id: "events" },
333
+ { id: "features" },
334
+ { id: "flows" },
335
+ { id: "glossary" },
336
+ { id: "how-to" },
337
+ { id: "integrations" },
338
+ { id: "launch-service" },
339
+ { id: "meet", navLabelKey: "schedule" },
340
+ { id: "newsletter" },
341
+ { id: "partners" },
342
+ { id: "portfolio" },
343
+ { id: "pricing" },
344
+ { id: "privacy" },
345
+ { id: "products" },
346
+ { id: "projects" },
347
+ { id: "resources" },
348
+ { id: "services" },
349
+ { id: "software" },
350
+ { id: "tech-articles" },
351
+ { id: "terms" },
352
+ { id: "testimonials" },
353
+ { id: "use-cases" },
354
+ { id: "whitepapers" },
355
+ ];
356
+ function renderNavConfigE4(enabled) {
357
+ const on = new Set(enabled);
358
+ // `blog` is linked unconditionally below (operator ruling 2026-09-23), so
359
+ // it is excluded here; `use-cases` is the Free roster's own page (a
360
+ // module-less surface the roster enables, like `pricing`), linked in its
361
+ // fixed positions below rather than appended.
362
+ const linked = E4_SURFACES.filter((s) => s.navLabelKey !== undefined &&
363
+ on.has(s.id) &&
364
+ s.id !== "blog" &&
365
+ s.id !== "use-cases");
366
+ const useCases = on.has("use-cases");
367
+ const features = on.has("features");
368
+ // #6721 (operator ruling 2026-09-25): the Product flyout is two columns —
369
+ // Features and Use Cases — each a heading over three label-only links. The
370
+ // `groups` render the desktop panel; `features` carries the same links for
371
+ // the mobile disclosure and the active-path match. "Use cases" left the top
372
+ // nav (it lives in the flyout and its own footer column).
373
+ const featureLinks = features
374
+ ? [
375
+ ' { labelKey: "feature1", path: "/features/feature-1" },',
376
+ ' { labelKey: "feature2", path: "/features/feature-2" },',
377
+ ' { labelKey: "feature3", path: "/features/feature-3" },',
378
+ ]
379
+ : [];
380
+ const useCaseLinks = useCases
381
+ ? [
382
+ ' { labelKey: "useCase1", path: "/use-cases/persona-1" },',
383
+ ' { labelKey: "useCase2", path: "/use-cases/persona-2" },',
384
+ ' { labelKey: "useCase3", path: "/use-cases/persona-3" },',
385
+ ]
386
+ : [];
387
+ const indent = (line) => ` ${line}`;
388
+ const group = (headingKey, links) => links.length === 0
389
+ ? []
390
+ : [
391
+ " {",
392
+ ` headingKey: "${headingKey}",`,
393
+ " items: [",
394
+ ...links.map(indent),
395
+ " ],",
396
+ " },",
397
+ ];
398
+ const entries = [
399
+ " {",
400
+ ' labelKey: "product",',
401
+ " flyout: {",
402
+ // SOFT_LOCK_OK: #6508 — width "panel" (D4/D5): a fixed 640px panel.
403
+ ' width: "panel",',
404
+ " features: [",
405
+ ...featureLinks,
406
+ ...useCaseLinks,
407
+ " ],",
408
+ " groups: [",
409
+ ...group("flyoutFeaturesHeading", featureLinks),
410
+ ...group("flyoutUseCasesHeading", useCaseLinks),
411
+ " ],",
412
+ " actions: [],",
413
+ " },",
414
+ " },",
415
+ // Operator ruling 2026-09-25: Product · Blog · Pricing · About.
416
+ // `/blog` is always linked — the `blog` surface serves the real route, and
417
+ // with it off the `[slug]` route serves the overlay's module page.
418
+ ' { labelKey: "blog", path: "/blog" },',
419
+ ...(on.has("pricing") ? [' { labelKey: "pricing", path: "/pricing" },'] : []),
420
+ ' { labelKey: "about", path: "/about" },',
421
+ ...linked.map((s) => ` { labelKey: "${s.navLabelKey}", path: "/${s.id}" },`),
422
+ ];
423
+ // The footer's column groups (operator rulings 2026-09-23 + 2026-09-25):
424
+ // Site pages, Features, Use Cases, Resources. Pricing rides the same
425
+ // `pricing` gate as the header item; `/blog` is always linked (above). The
426
+ // Features column links the three feature pages and the /features hub; the
427
+ // Use Cases column the three use cases and the /use-cases hub — each rides
428
+ // its surface's gate. Book a call, Glossary and Newsletter (#6658 D8) each
429
+ // ride their own surface's gate.
430
+ const footerEntries = [
431
+ " {",
432
+ ' headingKey: "footerSitePagesHeading",',
433
+ " items: [",
434
+ ...(features ? [' { labelKey: "features", path: "/features" },'] : []),
435
+ ...(on.has("pricing")
436
+ ? [' { labelKey: "pricing", path: "/pricing" },']
437
+ : []),
438
+ ' { labelKey: "about", path: "/about" },',
439
+ ' { labelKey: "contact", path: "/contact" },',
440
+ ...(on.has("book-a-call")
441
+ ? [' { labelKey: "bookACall", path: "/book-a-call" },']
442
+ : []),
443
+ " ],",
444
+ " },",
445
+ ...(features
446
+ ? [
447
+ " {",
448
+ ' headingKey: "footerFeaturesHeading",',
449
+ " items: [",
450
+ ...featureLinks.map((l) => l.slice(2)),
451
+ ' { labelKey: "allFeatures", path: "/features" },',
452
+ " ],",
453
+ " },",
454
+ ]
455
+ : []),
456
+ ...(useCases
457
+ ? [
458
+ " {",
459
+ ' headingKey: "footerUseCasesHeading",',
460
+ " items: [",
461
+ ...useCaseLinks.map((l) => l.slice(2)),
462
+ ' { labelKey: "allUseCases", path: "/use-cases" },',
463
+ " ],",
464
+ " },",
465
+ ]
466
+ : []),
467
+ " {",
468
+ ' headingKey: "footerResourcesHeading",',
469
+ " items: [",
470
+ ' { labelKey: "blog", path: "/blog" },',
471
+ ...(on.has("glossary")
472
+ ? [' { labelKey: "glossary", path: "/glossary" },']
473
+ : []),
474
+ ...(on.has("newsletter")
475
+ ? [' { labelKey: "newsletter", path: "/newsletter" },']
476
+ : []),
477
+ ' { labelKey: "faq", path: "/faq" },',
478
+ " ],",
479
+ " },",
480
+ ];
481
+ const navItemImport = [
482
+ "import type { NavItem } ",
483
+ "from ",
484
+ '"@working-theory/ui";',
485
+ ].join("");
486
+ return `// nav-config.ts — site-nav registry.
487
+ //
488
+ // GENERATED by create-ailk from the declared route surfaces. Every entry here
489
+ // points at a route this site actually carries — the nav is regenerated with
490
+ // the surface set so it can never link a page that was not scaffolded.
491
+ //
492
+ // Adding a link: scaffold the page (the \`add-page\` skill), then add an
493
+ // entry below with a \`labelKey\` that exists in the \`nav\` namespace of
494
+ // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
495
+ // them with the active locale at render.
496
+ ${navItemImport}
497
+
498
+ export const navItems: NavItem[] = [
499
+ ${entries.join("\n")}
500
+ ];
501
+
502
+ // Footer column groups. Keys only — the layout resolves each \`headingKey\`
503
+ // and \`labelKey\` through the same \`nav\` labels as the header.
504
+ export interface FooterColumnConfig {
505
+ headingKey: string;
506
+ items: { labelKey: string; path: string }[];
507
+ }
508
+
509
+ export const footerColumns: FooterColumnConfig[] = [
510
+ ${footerEntries.join("\n")}
511
+ ];
512
+ `;
513
+ }
514
+ /**
515
+ * The closed set of published, pre-stamp `renderNavConfig` bodies (D2).
516
+ * `add-module`'s `isUneditedNav` tries these, in order, against an unstamped
517
+ * nav (`surfaces.ts` `readNavConfigStamp` returning `"absent"`) before giving
518
+ * up and reporting it as customer-edited.
519
+ */
520
+ export const LEGACY_NAV_RENDERERS = [
521
+ {
522
+ era: "E1",
523
+ tags: ["0.1.1", "0.2.0", "0.3.0", "0.4.0"],
524
+ commit: "52e0685f3bff54832a76b5e0ec737c2f39b9d960",
525
+ render: renderNavConfigE1,
526
+ },
527
+ {
528
+ era: "E2",
529
+ tags: ["0.5.0", "0.6.0", "0.6.1"],
530
+ commit: "b0a69cefecf491412aa7e6f1d64464a91620126d",
531
+ render: renderNavConfigE2,
532
+ },
533
+ {
534
+ era: "E3",
535
+ tags: ["0.7.0"],
536
+ commit: "cac431ba50c4b0ee5130b5bc206196240744be18",
537
+ render: renderNavConfigE3,
538
+ },
539
+ {
540
+ era: "E4",
541
+ tags: ["0.8.0"],
542
+ commit: "060c02b8190090d7495d42e220836141d0549809",
543
+ render: renderNavConfigE4,
544
+ },
545
+ ];
@@ -0,0 +1,19 @@
1
+ export declare const AUTH_SECRET_ENV_VAR = "BETTER_AUTH_SECRET";
2
+ /** The files the writer owns, web first. */
3
+ export declare const AUTH_SECRET_ENV_FILES: readonly [string, string];
4
+ /**
5
+ * `.gitignore` lines that already ignore `apps/<app>/.env.local`. An explicit
6
+ * set, never a glob evaluation; `/.env.local` is absent because, anchored to
7
+ * the root, it does not cover `apps/web/.env.local`.
8
+ */
9
+ export declare const ENV_LOCAL_IGNORE_LINES: ReadonlySet<string>;
10
+ export interface WriteAuthSecretOptions {
11
+ /** Injectable for tests; defaults to `node:crypto` `randomBytes`. */
12
+ randomBytes?: (size: number) => Buffer;
13
+ }
14
+ /**
15
+ * Give the scaffold at `dir` a `BETTER_AUTH_SECRET` (F1–F4 of the #6107
16
+ * spec). Generates at most one value, and only when no app already has one.
17
+ * Idempotent — a second call changes no byte.
18
+ */
19
+ export declare function writeAuthSecretFile(dir: string, opts?: WriteAuthSecretOptions): void;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Where a scaffold keeps its `BETTER_AUTH_SECRET` (#6107).
3
+ *
4
+ * `apps/web/auth.ts` refuses to serve in production without the secret
5
+ * (#6044), and so does the Admin app's `auth.ts`. A fresh scaffold therefore
6
+ * gets one random value, written to each app's gitignored `.env.local` — the
7
+ * file `next dev` and `next start` both load:
8
+ *
9
+ * - `apps/web/.env.local` — always;
10
+ * - `apps/admin/.env.local` — when the scaffold carries `apps/admin`, with
11
+ * the SAME value (both apps sign cookies for one site).
12
+ *
13
+ * Never clobber, never shadow: a value already assigned in any env file Next
14
+ * loads for the app under `next start` is kept, and no new `.env.local` is
15
+ * created above it. Never print: no message, log line or return value
16
+ * carries the secret.
17
+ *
18
+ * Pure module: no `import.meta.url`, so jest imports it directly.
19
+ */
20
+ import { randomBytes as nodeRandomBytes } from "node:crypto";
21
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ import { appendLine, writeOwnerOnly } from "./license-key-files.js";
24
+ export const AUTH_SECRET_ENV_VAR = "BETTER_AUTH_SECRET";
25
+ /** The files the writer owns, web first. */
26
+ export const AUTH_SECRET_ENV_FILES = [
27
+ join("apps", "web", ".env.local"),
28
+ join("apps", "admin", ".env.local"),
29
+ ];
30
+ /**
31
+ * `.gitignore` lines that already ignore `apps/<app>/.env.local`. An explicit
32
+ * set, never a glob evaluation; `/.env.local` is absent because, anchored to
33
+ * the root, it does not cover `apps/web/.env.local`.
34
+ */
35
+ export const ENV_LOCAL_IGNORE_LINES = new Set([
36
+ ".env.local",
37
+ "**/.env.local",
38
+ ".env*.local",
39
+ "*.local",
40
+ ]);
41
+ /** Env files `next start` loads, highest precedence first. */
42
+ const NEXT_START_ENV_FILES = [
43
+ ".env.production.local",
44
+ ".env.local",
45
+ ".env.production",
46
+ ".env",
47
+ ];
48
+ /** A line assigning the variable, with or without `export`; group 1 is the value. */
49
+ const ENV_LINE = new RegExp(`^\\s*(?:export\\s+)?${AUTH_SECRET_ENV_VAR}\\s*=(.*)$`);
50
+ /**
51
+ * The value `text` assigns — the LAST assignment, as dotenv reads it — or
52
+ * `undefined` when it assigns none. An empty value counts as none, exactly as
53
+ * `auth.ts` treats it.
54
+ */
55
+ function assignedValue(text) {
56
+ let value;
57
+ for (const line of text.split("\n")) {
58
+ const m = ENV_LINE.exec(line);
59
+ if (m !== null) {
60
+ const raw = (m[1] ?? "").trim();
61
+ const quoted = /^(["'])(.*)\1$/.exec(raw);
62
+ value = quoted !== null ? (quoted[2] ?? "") : raw;
63
+ }
64
+ }
65
+ return value === "" ? undefined : value;
66
+ }
67
+ /** The effective value for the app at `appDir`, by Next's precedence. */
68
+ function effectiveValue(appDir) {
69
+ for (const name of NEXT_START_ENV_FILES) {
70
+ const path = join(appDir, name);
71
+ if (!existsSync(path))
72
+ continue;
73
+ const value = assignedValue(readFileSync(path, "utf8"));
74
+ if (value !== undefined)
75
+ return value;
76
+ }
77
+ return undefined;
78
+ }
79
+ /**
80
+ * Give the scaffold at `dir` a `BETTER_AUTH_SECRET` (F1–F4 of the #6107
81
+ * spec). Generates at most one value, and only when no app already has one.
82
+ * Idempotent — a second call changes no byte.
83
+ */
84
+ export function writeAuthSecretFile(dir, opts = {}) {
85
+ // Web always; Admin only when the scaffold carries `apps/admin`.
86
+ const apps = AUTH_SECRET_ENV_FILES.map((file) => ({
87
+ file: join(dir, file),
88
+ appDir: dirname(join(dir, file)),
89
+ })).filter(({ appDir }, i) => i === 0 || existsSync(appDir));
90
+ // An existing value — web's first — is shared; a fresh one is generated
91
+ // only when no app has one.
92
+ let secret = apps
93
+ .map(({ appDir }) => effectiveValue(appDir))
94
+ .find((v) => v !== undefined);
95
+ const secretFor = () => {
96
+ secret ??= (opts.randomBytes ?? nodeRandomBytes)(32).toString("base64url");
97
+ return secret;
98
+ };
99
+ for (const { file, appDir } of apps) {
100
+ if (effectiveValue(appDir) !== undefined)
101
+ continue;
102
+ const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
103
+ writeOwnerOnly(file, appendLine(existing, `${AUTH_SECRET_ENV_VAR}=${secretFor()}`));
104
+ }
105
+ const ignorePath = join(dir, ".gitignore");
106
+ const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
107
+ const covered = ignore
108
+ .split("\n")
109
+ .some((l) => ENV_LOCAL_IGNORE_LINES.has(l.trim()));
110
+ if (!covered)
111
+ writeFileSync(ignorePath, appendLine(ignore, ".env.local"));
112
+ }