create-ailk 0.18.0 → 0.19.1

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 (218) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +11 -3
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/lib/license-key-files.d.ts +6 -3
  11. package/dist/lib/license-key-files.js +8 -2
  12. package/dist/lib/scaffold-render-sweep.d.ts +32 -0
  13. package/dist/lib/scaffold-render-sweep.js +76 -3
  14. package/dist/license-set.d.ts +1 -1
  15. package/dist/license-set.js +13 -8
  16. package/dist/module-architecture.js +128 -3
  17. package/dist/programmatic.js +11 -1
  18. package/dist/site-roster.js +21 -4
  19. package/dist/spoke-checkout.d.ts +11 -3
  20. package/dist/spoke-checkout.js +62 -12
  21. package/dist/starter-grid.d.ts +1 -1
  22. package/dist/surfaces.d.ts +14 -0
  23. package/dist/surfaces.js +170 -1
  24. package/dist/template-manifest.d.ts +4 -3
  25. package/dist/topology.d.ts +7 -2
  26. package/dist/topology.js +88 -3
  27. package/package.json +5 -4
  28. package/templates/.env.example +3 -3
  29. package/templates/.github/workflows/ci.yml +5 -0
  30. package/templates/apps/api/.env.example +17 -1
  31. package/templates/apps/api/CLAUDE.md +31 -7
  32. package/templates/apps/api/LICENSE +6 -5
  33. package/templates/apps/api/NOTICE +2 -0
  34. package/templates/apps/api/jest.config.cjs +4 -29
  35. package/templates/apps/api/package.json +1 -0
  36. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
  37. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
  38. package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
  39. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
  40. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
  41. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  42. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
  43. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
  44. package/templates/apps/api/src/lib/consent-store.ts +335 -0
  45. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  46. package/templates/apps/api/src/lib/stripe.ts +176 -3
  47. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
  48. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
  49. package/templates/apps/api/src/middleware/auth.ts +214 -46
  50. package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
  51. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
  52. package/templates/apps/api/src/routes/checkout/session.ts +3 -3
  53. package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
  54. package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
  55. package/templates/apps/api/src/routes/consent/index.ts +18 -0
  56. package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
  57. package/templates/apps/api/src/routes/leads/index.ts +19 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
  59. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
  60. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
  61. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
  62. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
  63. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
  64. package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
  65. package/templates/apps/api/src/server.ts +218 -16
  66. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
  67. package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
  68. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
  69. package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
  70. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
  71. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
  72. package/templates/apps/api/src/services/consent.ts +488 -3
  73. package/templates/apps/api/src/services/lead-routing.ts +135 -14
  74. package/templates/apps/api/src/services/person.ts +501 -1
  75. package/templates/apps/api/src/services/project-listings.ts +7 -2
  76. package/templates/apps/api/src/services/request-confirm.ts +52 -0
  77. package/templates/apps/api/src/services/revalidation.ts +11 -1
  78. package/templates/apps/api/vercel.json +2 -2
  79. package/templates/apps/mcp/.env.example +10 -0
  80. package/templates/apps/mcp/CLAUDE.md +9 -4
  81. package/templates/apps/mcp/LICENSE +6 -5
  82. package/templates/apps/mcp/NOTICE +2 -0
  83. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  84. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  85. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  86. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  87. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  88. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  89. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  90. package/templates/apps/mcp/jest.config.cjs +3 -44
  91. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  92. package/templates/apps/mcp/src/boot.ts +5 -23
  93. package/templates/apps/mcp/src/http-transport.ts +93 -73
  94. package/templates/apps/mcp/src/index.ts +2 -0
  95. package/templates/apps/mcp/src/request-context.ts +52 -0
  96. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  97. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  98. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  99. package/templates/apps/web/LICENSE +6 -5
  100. package/templates/apps/web/NOTICE +2 -0
  101. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
  116. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  117. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  118. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
  119. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  125. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  132. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  135. package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
  138. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  139. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  140. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  141. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  142. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
  143. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  144. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  145. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  146. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  147. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  148. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  149. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
  150. package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
  151. package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
  152. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  153. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
  154. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  155. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  156. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  157. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  158. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  159. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  160. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  161. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  162. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  163. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  164. package/templates/apps/web/auth.ts +25 -2
  165. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  166. package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
  167. package/templates/apps/web/lib/account-config.ts +188 -52
  168. package/templates/apps/web/lib/auth-email.ts +45 -10
  169. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  170. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  171. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  172. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  173. package/templates/apps/web/lib/topology.generated.ts +33 -0
  174. package/templates/apps/web/messages/en.json +194 -33
  175. package/templates/database/CHANGELOG.md +37 -0
  176. package/templates/database/LICENSE +6 -5
  177. package/templates/database/NOTICE +2 -0
  178. package/templates/database/README.md +1 -1
  179. package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
  180. package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
  181. package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
  182. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  183. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  184. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  185. package/templates/database/__tests__/person-contact.db.test.ts +563 -0
  186. package/templates/database/__tests__/person.db.test.ts +3 -1
  187. package/templates/database/__tests__/seed.test.ts +54 -2
  188. package/templates/database/inbox/schema.prisma +166 -25
  189. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  190. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  191. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
  192. package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
  193. package/templates/database/package.json +3 -2
  194. package/templates/database/seed.ts +34 -2
  195. package/versions.json +36 -36
  196. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  197. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  198. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  199. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  200. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  201. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  202. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  203. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  204. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  205. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  206. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  207. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  208. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  209. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  210. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  211. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  212. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  213. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  214. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  215. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  216. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  217. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  218. package/templates/database/__tests__/build.test.ts +0 -131
package/LICENSE CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  Apache License
2
3
  Version 2.0, January 2004
3
4
  http://www.apache.org/licenses/
@@ -137,8 +138,8 @@
137
138
 
138
139
  6. Trademarks. This License does not grant permission to use the trade
139
140
  names, trademarks, service marks, or product names of the Licensor,
140
- except as required for describing the origin of the Work and
141
- reproducing the content of the NOTICE file.
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
142
143
 
143
144
  7. Disclaimer of Warranty. Unless required by applicable law or
144
145
  agreed to in writing, Licensor provides the Work (and each
@@ -162,7 +163,7 @@
162
163
  other commercial damages or losses), even if such Contributor
163
164
  has been advised of the possibility of such damages.
164
165
 
165
- 9. Accepting Warranty or Support. While redistributing
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
167
  the Work or Derivative Works thereof, You may choose to offer,
167
168
  and charge a fee for, acceptance of support, warranty, indemnity,
168
169
  or other liability obligations and/or rights consistent with this
@@ -171,7 +172,7 @@
171
172
  of any other Contributor, and only if You agree to indemnify,
172
173
  defend, and hold each Contributor harmless for any liability
173
174
  incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or support.
175
+ of your accepting any such warranty or additional liability.
175
176
 
176
177
  END OF TERMS AND CONDITIONS
177
178
 
@@ -186,7 +187,7 @@
186
187
  same "printed page" as the copyright notice for easier
187
188
  identification within third-party archives.
188
189
 
189
- Copyright 2026 Tailwind AI
190
+ Copyright [yyyy] [name of copyright owner]
190
191
 
191
192
  Licensed under the Apache License, Version 2.0 (the "License");
192
193
  you may not use this file except in compliance with the License.
package/NOTICE ADDED
@@ -0,0 +1,2 @@
1
+ AI Launch Kit
2
+ Copyright 2026 SalesSmyth LLC
@@ -40,7 +40,7 @@ import { ParseError, parseModules } from "./parse-args.js";
40
40
  import { writeSpokeFlowsReadme } from "./seed-flows.js";
41
41
  import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
42
42
  import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
43
- import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
43
+ import { MODULE_SURFACES, NAV_CONFIG_PATH, isUnservedSurfaceContent, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
44
44
  import { syncRoutes } from "./sync-routes.js";
45
45
  import { TopologyError, isSpokeExcluded, readSiteTopology, readSpokeShape, spokeDestination, spokeModuleRefusal, } from "./topology.js";
46
46
  /**
@@ -155,9 +155,14 @@ export async function applyModuleToScaffold(opts) {
155
155
  markers: opts.markers,
156
156
  existingFilePolicy: "skip-existing",
157
157
  checkPatches: checkPlusConfigPatches,
158
+ // #8427 D4 — composed with the spoke filter: a module bundle never
159
+ // writes a seed under a gateable surface this site's own
160
+ // `architecture.yaml` header (resolved above, into `surfaces`) does
161
+ // not enable.
162
+ skip: (destination) => (spoke !== undefined && isSpokeExcluded(destination, spoke)) ||
163
+ isUnservedSurfaceContent(destination, surfaces),
158
164
  ...(spoke !== undefined
159
165
  ? {
160
- skip: (destination) => isSpokeExcluded(destination, spoke),
161
166
  // #7876 D10 — a module flow file lands in flows/ as declared data.
162
167
  remapDestination: spokeDestination,
163
168
  }
@@ -165,12 +170,15 @@ export async function applyModuleToScaffold(opts) {
165
170
  });
166
171
  // #7838 D8 — commerce on a spoke brings the spoke's own checkout, stamped
167
172
  // from the site directory's name (the `projectName` scaffold() defaults to).
168
- // A file the site already has is left as it is.
173
+ // A file the site already has is left as it is. #8217 D8 — on a spoke that
174
+ // signs in through its hub, the billing action and its seam render too, so
175
+ // re-running add-module commerce gives an existing commerce spoke the action.
169
176
  const spokeCommerce = spoke !== undefined && opts.moduleName === "commerce";
170
177
  const checkoutRows = spokeCommerce
171
178
  ? writeSpokeCheckoutSlice(targetAbs, {
172
179
  site: spokeCheckoutSite(basename(targetAbs)),
173
180
  versions: opts.versions,
181
+ signIn: spoke.signIn,
174
182
  })
175
183
  : [];
176
184
  // #7876 D9 — a spoke that just gained the flow stepper says what its
@@ -50,6 +50,16 @@ export declare const CATEGORY_PROFILE_SHAPES: {
50
50
  * set-equal to the `schema-dts` closure, so it cannot drift silently.
51
51
  */
52
52
  export declare const LOCAL_BUSINESS_TYPES: ReadonlySet<string>;
53
+ /**
54
+ * The leaf category slugs, in registry order — create-ailk's own copy of the
55
+ * site-category registry's leaves (`SITE_CATEGORIES`, #5830). A literal, not
56
+ * a type import, so the emitted `.d.ts` of the public tarball names no
57
+ * `@working-theory/*` package (#8412); `category-slugs.test.ts` proves it
58
+ * equal to the registry, so it cannot drift silently.
59
+ */
60
+ export declare const CATEGORY_SLUGS: readonly ["accounting", "legal", "consulting", "hvac", "roofing", "plumbing", "electrical", "landscaping", "auto-repair", "real-estate", "technology", "medical-specialty"];
61
+ /** A leaf category slug — the only thing a template's `categories` names. */
62
+ export type CategorySlug = (typeof CATEGORY_SLUGS)[number];
53
63
  export declare const OFFERING_PRICING: readonly ["quote", "fixed", "hourly", "subscription"];
54
64
  export declare const PROOF_KINDS: readonly ["credential", "reviews", "before-after", "response-time", "guarantee", "experience"];
55
65
  export declare const CONTENT_POSTURES: readonly ["instructions", "sample"];
@@ -71,6 +71,27 @@ export const LOCAL_BUSINESS_TYPES = new Set([
71
71
  "TennisComplex", "TireShop", "TouristInformationCenter", "ToyStore",
72
72
  "TravelAgency", "VacationRental", "WholesaleStore", "Winery",
73
73
  ]);
74
+ /**
75
+ * The leaf category slugs, in registry order — create-ailk's own copy of the
76
+ * site-category registry's leaves (`SITE_CATEGORIES`, #5830). A literal, not
77
+ * a type import, so the emitted `.d.ts` of the public tarball names no
78
+ * `@working-theory/*` package (#8412); `category-slugs.test.ts` proves it
79
+ * equal to the registry, so it cannot drift silently.
80
+ */
81
+ export const CATEGORY_SLUGS = [
82
+ "accounting",
83
+ "legal",
84
+ "consulting",
85
+ "hvac",
86
+ "roofing",
87
+ "plumbing",
88
+ "electrical",
89
+ "landscaping",
90
+ "auto-repair",
91
+ "real-estate",
92
+ "technology",
93
+ "medical-specialty",
94
+ ];
74
95
  export const OFFERING_PRICING = ["quote", "fixed", "hourly", "subscription"];
75
96
  export const PROOF_KINDS = [
76
97
  "credential",
@@ -22,7 +22,11 @@ export interface DeriveArchitectureOptions {
22
22
  /** Spoke only: whether the spoke keeps `apps/mcp` (default true). */
23
23
  mcp?: boolean;
24
24
  }
25
- /** The only spoke files that may import @working-theory/spoke-checkout (#7873). */
25
+ /**
26
+ * The only spoke files that may import @working-theory/spoke-checkout (#7873):
27
+ * the buy route, the webhook route and — on a spoke that signs in through its
28
+ * hub — the billing action (#8217 D9, a reviewed change to the #7873 rule).
29
+ */
26
30
  export declare const SPOKE_CHECKOUT_ROUTE_FILES: readonly string[];
27
31
  /** The forbidden patterns a spoke's apps/web carries (#7873, brief Part 2 §2). */
28
32
  export declare const SPOKE_WEB_FORBIDDEN_PATTERNS: readonly ForbiddenPattern[];
@@ -27,10 +27,15 @@ const SPOKE_TIERS = {
27
27
  // import only to a workspace on disk, so the one way to ban an npm package is
28
28
  // a line-level forbidden pattern. These go under the spoke's own `apps.web`
29
29
  // and `apps.mcp` only — never top level, and never on a hub derivation.
30
- /** The only spoke files that may import @working-theory/spoke-checkout (#7873). */
30
+ /**
31
+ * The only spoke files that may import @working-theory/spoke-checkout (#7873):
32
+ * the buy route, the webhook route and — on a spoke that signs in through its
33
+ * hub — the billing action (#8217 D9, a reviewed change to the #7873 rule).
34
+ */
31
35
  export const SPOKE_CHECKOUT_ROUTE_FILES = Object.freeze([
32
36
  "apps/web/app/api/checkout/[offering]/route.ts",
33
37
  "apps/web/app/api/webhooks/stripe/route.ts",
38
+ "apps/web/app/[locale]/(authed)/account/billing/spoke-billing.action.ts",
34
39
  ]);
35
40
  /**
36
41
  * A line-level regex for an import of `specifier` or one of its subpaths, in
@@ -45,10 +50,10 @@ function specifierForm(specifier) {
45
50
  const literal = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
51
  return `\\b(?:from|import|require)\\s*\\(?\\s*${q}${literal}(?:/[^'"\`]*)?${q}`;
47
52
  }
48
- /** P1 with no exclusions — a kept apps/mcp's form; apps/web adds the two routes. */
53
+ /** P1 with no exclusions — a kept apps/mcp's form; apps/web adds the route files. */
49
54
  const SPOKE_CHECKOUT_PATTERN = Object.freeze({
50
55
  pattern: specifierForm("@working-theory/spoke-checkout"),
51
- reason: "Spoke checkout: only the buy route and the webhook route may import @working-theory/spoke-checkout (brief Part 2 §2, #7873)",
56
+ reason: "Spoke checkout: only the buy route, the webhook route and the billing action may import @working-theory/spoke-checkout (brief Part 2 §2, #7873, #8217)",
52
57
  });
53
58
  const STRIPE_CLIENT_PATTERN = Object.freeze({
54
59
  pattern: specifierForm("@working-theory/stripe-client"),
package/dist/index.d.ts CHANGED
@@ -14,6 +14,6 @@ export type { HomeSeedOutcome } from "./design-home.js";
14
14
  export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
15
15
  export { SYNC_ROUTES_HELP, formatSyncRoutesReport, listTemplateRouteDirs, parseSyncRoutesArgs, syncRoutes, type RouteReport, type SyncRoutesOptions, type SyncRoutesResult, } from "./sync-routes.js";
16
16
  export { deriveArchitecture } from "./derive-architecture.js";
17
- export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, type Surface, } from "./surfaces.js";
17
+ export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, isUnservedSurfaceContent, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, type Surface, } from "./surfaces.js";
18
18
  export { substituteMarkers, substituteWorkspaceVersions, type MarkerMap, type WorkspaceVersionMap, } from "./substitute.js";
19
19
  export { validateTarget, ValidationError } from "./validate.js";
package/dist/index.js CHANGED
@@ -30,6 +30,6 @@ export { deriveArchitecture } from "./derive-architecture.js";
30
30
  // The route-surface registry (#4154) — which public routes a scaffold carries.
31
31
  // Exported so tooling can answer "what surfaces does this site declare" without
32
32
  // re-parsing architecture.yaml.
33
- export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, } from "./surfaces.js";
33
+ export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, isUnservedSurfaceContent, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, } from "./surfaces.js";
34
34
  export { substituteMarkers, substituteWorkspaceVersions, } from "./substitute.js";
35
35
  export { validateTarget, ValidationError } from "./validate.js";
@@ -21,9 +21,12 @@ export declare function appendLine(text: string, line: string): string;
21
21
  * `ailk.key` when no existing line covers it. Idempotent — a second call with
22
22
  * the same key changes no byte.
23
23
  *
24
- * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
25
- * `apps/api`, so it keeps the key in `ailk.key` alone.
24
+ * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) or any
25
+ * site without `apps/api` (#7779) keeps the key in `ailk.key` alone.
26
+ *
27
+ * Returns the key files it holds the key in, relative to `dir`
28
+ * (`LICENSE_KEY_FILE`, then `LICENSE_ENV_FILE` when written).
26
29
  */
27
30
  export declare function writeLicenseKeyFiles(dir: string, key: string, opts?: {
28
31
  apiEnv?: boolean;
29
- }): void;
32
+ }): string[];
@@ -86,16 +86,21 @@ function upsertEnvLine(existing, key) {
86
86
  * `ailk.key` when no existing line covers it. Idempotent — a second call with
87
87
  * the same key changes no byte.
88
88
  *
89
- * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
90
- * `apps/api`, so it keeps the key in `ailk.key` alone.
89
+ * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) or any
90
+ * site without `apps/api` (#7779) keeps the key in `ailk.key` alone.
91
+ *
92
+ * Returns the key files it holds the key in, relative to `dir`
93
+ * (`LICENSE_KEY_FILE`, then `LICENSE_ENV_FILE` when written).
91
94
  */
92
95
  export function writeLicenseKeyFiles(dir, key, opts = {}) {
93
96
  assertLicenseKeyShape(key);
94
97
  writeOwnerOnly(join(dir, LICENSE_KEY_FILE), `${key}\n`);
98
+ const written = [LICENSE_KEY_FILE];
95
99
  if (opts.apiEnv !== false) {
96
100
  const envPath = join(dir, LICENSE_ENV_FILE);
97
101
  const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
98
102
  writeOwnerOnly(envPath, upsertEnvLine(env, key));
103
+ written.push(LICENSE_ENV_FILE);
99
104
  }
100
105
  const ignorePath = join(dir, ".gitignore");
101
106
  const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
@@ -104,4 +109,5 @@ export function writeLicenseKeyFiles(dir, key, opts = {}) {
104
109
  .some((l) => KEY_IGNORE_LINES.has(l.trim()));
105
110
  if (!covered)
106
111
  writeFileSync(ignorePath, appendLine(ignore, LICENSE_KEY_FILE));
112
+ return written;
107
113
  }
@@ -53,6 +53,15 @@ export declare function assessClaimGatedRoute(input: {
53
53
  hops: RedirectHop[];
54
54
  status: number | null;
55
55
  }): SweepVerdict;
56
+ /**
57
+ * A `redirect()` thrown by a Server Component after the shell has flushed is
58
+ * not an HTTP 3xx: the response is a 200 whose flight payload carries the
59
+ * digest `NEXT_REDIRECT;<replace|push>;<url>;<status>;`. The driver follows it
60
+ * as a hop exactly like a `Location` header (#8311 — `/account/settings`
61
+ * forwards to `/account/general` this way). Null when the HTML carries no
62
+ * such digest.
63
+ */
64
+ export declare function streamedRedirect(html: string): RedirectHop | null;
56
65
  /**
57
66
  * Decide whether a page route rendered, under the real locale, with its own
58
67
  * content. The first failed rule is the reason:
@@ -96,6 +105,20 @@ export declare function assessConfigGatedRoute(input: {
96
105
  html: string;
97
106
  locale: string;
98
107
  }): SweepVerdict;
108
+ /**
109
+ * The environment the sitemap sweep's server runs under (#8501): `env` with
110
+ * every `CONFIG_GATED_ROUTES` variable removed.
111
+ *
112
+ * `/sitemap.xml` is prerendered at `next build`, from the content seeds, so it
113
+ * describes the scaffold AS GENERATED — and a config-gated route's seeds are
114
+ * served only while its variable is unset (the Free placeholder hand-off,
115
+ * #6942). Set it and the route serves its live module page instead: `/meet`
116
+ * becomes the booker for the operator's enabled events, `/meet/<seed>` a 404.
117
+ * A leg that configures the module for its booking steps would therefore score
118
+ * the sitemap against pages it was never written for, so the sweep runs
119
+ * against a server that leaves the gated variables unset. Pure.
120
+ */
121
+ export declare function sitemapSweepEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string | undefined>;
99
122
  /**
100
123
  * Every FILE under `dir`, recursing into directories, as sorted slash-joined
101
124
  * paths relative to it — so `public/.well-known/security.txt` is listed and
@@ -115,6 +138,15 @@ export declare function assessUnknownSlug(input: {
115
138
  hops: RedirectHop[];
116
139
  status: number | null;
117
140
  }): SweepVerdict;
141
+ /**
142
+ * Every `<loc>` path in a sitemap XML document, in order, deduped (#8427
143
+ * D6). Pure — no network or filesystem access, like its neighbours. Each
144
+ * `<loc>` is un-escaped against the five entities `escapeXml` writes, then
145
+ * read as `new URL(loc).pathname` — so a query string contributes nothing
146
+ * and an `<xhtml:link rel="alternate">` sibling, which carries no `<loc>` of
147
+ * its own, contributes nothing either.
148
+ */
149
+ export declare function sitemapLocPaths(xml: string): string[];
118
150
  /** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
119
151
  export declare function assessPrerenderManifest(manifest: {
120
152
  dynamicRoutes?: Record<string, unknown>;
@@ -122,6 +122,24 @@ function htmlLang(html) {
122
122
  function flightDigest(html) {
123
123
  return /\\"digest\\":\\"([^"\\]*)\\"/.exec(html)?.[1] ?? null;
124
124
  }
125
+ /**
126
+ * A `redirect()` thrown by a Server Component after the shell has flushed is
127
+ * not an HTTP 3xx: the response is a 200 whose flight payload carries the
128
+ * digest `NEXT_REDIRECT;<replace|push>;<url>;<status>;`. The driver follows it
129
+ * as a hop exactly like a `Location` header (#8311 — `/account/settings`
130
+ * forwards to `/account/general` this way). Null when the HTML carries no
131
+ * such digest.
132
+ */
133
+ export function streamedRedirect(html) {
134
+ const digest = flightDigest(html);
135
+ if (digest === null || !digest.startsWith("NEXT_REDIRECT;"))
136
+ return null;
137
+ const [, , location = "", status = ""] = digest.split(";");
138
+ if (location === "")
139
+ return null;
140
+ const code = Number.parseInt(status, 10);
141
+ return { status: Number.isFinite(code) ? code : 307, location };
142
+ }
125
143
  /**
126
144
  * Whether an `<h1>`'s inner HTML carries a name: text once tags are stripped,
127
145
  * or an image with a non-empty `alt` (the Hero's lockup renders the wordmark
@@ -170,9 +188,12 @@ export function assessPageRender(input) {
170
188
  return { ok: false, reason: `${route} -> rendered under locale "${lang}"` };
171
189
  const digest = flightDigest(html);
172
190
  if (digest !== null) {
173
- const why = digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;404")
174
- ? "streamed not-found"
175
- : `streamed error (digest ${digest})`;
191
+ const redirect = streamedRedirect(html);
192
+ const why = redirect !== null
193
+ ? `streamed redirect to ${redirect.location} that was not followed`
194
+ : digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;404")
195
+ ? "streamed not-found"
196
+ : `streamed error (digest ${digest})`;
176
197
  return { ok: false, reason: `${route} -> ${why}` };
177
198
  }
178
199
  const marker = ownContentMarker(html);
@@ -218,6 +239,23 @@ export function assessConfigGatedRoute(input) {
218
239
  function headingTexts(html) {
219
240
  return [...html.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi)].map((m) => (m[1] ?? "").replace(/<[^>]*>/g, ""));
220
241
  }
242
+ /**
243
+ * The environment the sitemap sweep's server runs under (#8501): `env` with
244
+ * every `CONFIG_GATED_ROUTES` variable removed.
245
+ *
246
+ * `/sitemap.xml` is prerendered at `next build`, from the content seeds, so it
247
+ * describes the scaffold AS GENERATED — and a config-gated route's seeds are
248
+ * served only while its variable is unset (the Free placeholder hand-off,
249
+ * #6942). Set it and the route serves its live module page instead: `/meet`
250
+ * becomes the booker for the operator's enabled events, `/meet/<seed>` a 404.
251
+ * A leg that configures the module for its booking steps would therefore score
252
+ * the sitemap against pages it was never written for, so the sweep runs
253
+ * against a server that leaves the gated variables unset. Pure.
254
+ */
255
+ export function sitemapSweepEnv(env) {
256
+ const gated = new Set(Object.values(CONFIG_GATED_ROUTES));
257
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !gated.has(name)));
258
+ }
221
259
  /**
222
260
  * Every FILE under `dir`, recursing into directories, as sorted slash-joined
223
261
  * paths relative to it — so `public/.well-known/security.txt` is listed and
@@ -270,6 +308,41 @@ export function assessUnknownSlug(input) {
270
308
  return { ok: false, reason: `${route} -> status ${status} (expected 404 for an unknown slug)` };
271
309
  return { ok: true, reason: "404" };
272
310
  }
311
+ /**
312
+ * The five entities `escapeXml` (`apps/web/app/sitemap.xml/route.ts`) writes,
313
+ * reversed.
314
+ */
315
+ const XML_ENTITIES = {
316
+ "&amp;": "&",
317
+ "&lt;": "<",
318
+ "&gt;": ">",
319
+ "&quot;": '"',
320
+ "&apos;": "'",
321
+ };
322
+ function unescapeXml(value) {
323
+ return value.replace(/&amp;|&lt;|&gt;|&quot;|&apos;/g, (entity) => XML_ENTITIES[entity] ?? entity);
324
+ }
325
+ /**
326
+ * Every `<loc>` path in a sitemap XML document, in order, deduped (#8427
327
+ * D6). Pure — no network or filesystem access, like its neighbours. Each
328
+ * `<loc>` is un-escaped against the five entities `escapeXml` writes, then
329
+ * read as `new URL(loc).pathname` — so a query string contributes nothing
330
+ * and an `<xhtml:link rel="alternate">` sibling, which carries no `<loc>` of
331
+ * its own, contributes nothing either.
332
+ */
333
+ export function sitemapLocPaths(xml) {
334
+ const seen = new Set();
335
+ const paths = [];
336
+ for (const match of xml.matchAll(/<loc>([^<]*)<\/loc>/g)) {
337
+ const loc = unescapeXml(match[1] ?? "");
338
+ const path = new URL(loc).pathname;
339
+ if (seen.has(path))
340
+ continue;
341
+ seen.add(path);
342
+ paths.push(path);
343
+ }
344
+ return paths;
345
+ }
273
346
  /** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
274
347
  export function assessPrerenderManifest(manifest) {
275
348
  const locale = Object.keys(manifest.dynamicRoutes ?? {})
@@ -24,7 +24,7 @@ export interface LicenseSetIO {
24
24
  /** Defaults to `spawnSync` with `shell: false` (see `defaultRunner`). */
25
25
  runner?: LicenseSetRunner;
26
26
  }
27
- export declare const LICENSE_SET_HELP = "create-ailk license set \u2014 put your licence key into an existing site.\n\nUsage:\n pnpm create ailk@latest license set <key> [<target-dir>]\n pnpm create ailk@latest license set - [<target-dir>] < key.txt\n\nArguments:\n <key> Your licence key, or `-` to read it from stdin\n (keeps it out of your shell history).\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nWrites the key to ailk.key and apps/api/.env (owner-only, gitignored). When the\nsite (or apps/api, or apps/web) is linked to Vercel, it also replaces the\nproduction AILK_LICENSE_KEY variable and redeploys.\n";
27
+ export declare const LICENSE_SET_HELP = "create-ailk license set \u2014 put your licence key into an existing site.\n\nUsage:\n pnpm create ailk@latest license set <key> [<target-dir>]\n pnpm create ailk@latest license set - [<target-dir>] < key.txt\n\nArguments:\n <key> Your licence key, or `-` to read it from stdin\n (keeps it out of your shell history).\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nWrites the key to ailk.key, and to apps/api/.env when the site has apps/api\n(owner-only, gitignored). When the site (or apps/api, or apps/web) is linked to\nVercel, it also replaces the production AILK_LICENSE_KEY variable and redeploys.\n";
28
28
  /** `spawnSync`, never through a shell; stdin piped only when there is input. */
29
29
  export declare const defaultRunner: LicenseSetRunner;
30
30
  /**
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * 1. The target (default: cwd) must contain `architecture.yaml` — the
9
9
  * `sync-routes` precondition — else exit 1 with nothing written.
10
- * 2. `writeLicenseKeyFiles` — `ailk.key` + `apps/api/.env`, both 0600 and
11
- * gitignored; the same writer `--license` uses.
10
+ * 2. `writeLicenseKeyFiles` — `ailk.key`, plus `apps/api/.env` only when
11
+ * `apps/api` already exists (#7779), both 0600 and gitignored; the same
12
+ * writer `--license` uses. The output names only the files written.
12
13
  * 3. For each of `<dir>`, `<dir>/apps/api`, `<dir>/apps/web` holding
13
14
  * `.vercel/project.json`, with that directory as `cwd`:
14
15
  * a. print the linked `projectId` about to change (SEC-13);
@@ -31,7 +32,7 @@
31
32
  import { spawnSync } from "node:child_process";
32
33
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
33
34
  import { join, relative, resolve } from "node:path";
34
- import { LICENSE_ENV_FILE, LICENSE_ENV_VAR, LICENSE_KEY_FILE, assertLicenseKeyShape, writeLicenseKeyFiles, } from "./lib/license-key-files.js";
35
+ import { LICENSE_ENV_VAR, LICENSE_KEY_FILE, assertLicenseKeyShape, writeLicenseKeyFiles, } from "./lib/license-key-files.js";
35
36
  export const LICENSE_SET_HELP = `create-ailk license set — put your licence key into an existing site.
36
37
 
37
38
  Usage:
@@ -44,9 +45,9 @@ Arguments:
44
45
  <target-dir> An already-scaffolded repo (contains
45
46
  architecture.yaml). Defaults to the current dir.
46
47
 
47
- Writes the key to ailk.key and apps/api/.env (owner-only, gitignored). When the
48
- site (or apps/api, or apps/web) is linked to Vercel, it also replaces the
49
- production AILK_LICENSE_KEY variable and redeploys.
48
+ Writes the key to ailk.key, and to apps/api/.env when the site has apps/api
49
+ (owner-only, gitignored). When the site (or apps/api, or apps/web) is linked to
50
+ Vercel, it also replaces the production AILK_LICENSE_KEY variable and redeploys.
50
51
  `;
51
52
  const LINK_DIRS = [".", join("apps", "api"), join("apps", "web")];
52
53
  const VERCELIGNORE_LINES = [LICENSE_KEY_FILE, ".env"];
@@ -151,9 +152,13 @@ export function runLicenseSet(argv, io) {
151
152
  if (!existsSync(join(target, "architecture.yaml"))) {
152
153
  return fail(`not a scaffolded repo: no architecture.yaml at ${target}. Run it from your site's root, or pass the site's directory.`);
153
154
  }
154
- writeLicenseKeyFiles(target, key);
155
+ // A site without apps/api (a web-only spoke) keeps the key in ailk.key
156
+ // alone — writing apps/api/.env would create an app directory it never had.
157
+ const files = writeLicenseKeyFiles(target, key, {
158
+ apiEnv: existsSync(join(target, "apps", "api")),
159
+ });
155
160
  const keyFile = join(target, LICENSE_KEY_FILE);
156
- const written = `Wrote ${LICENSE_KEY_FILE} and ${LICENSE_ENV_FILE.split("\\").join("/")} in ${target}`;
161
+ const written = `Wrote ${files.map((f) => f.split("\\").join("/")).join(" and ")} in ${target}`;
157
162
  io.stdout(`[create-ailk] ${written} (owner-only, gitignored).\n`);
158
163
  const linked = LINK_DIRS.map((d) => join(target, d)).filter((d) => existsSync(join(d, ".vercel", "project.json")));
159
164
  if (linked.length === 0) {