create-ailk 0.19.1 → 0.21.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 (199) hide show
  1. package/README.md +84 -7
  2. package/component-catalog.json +184 -6
  3. package/component-catalog.md +20 -9
  4. package/dist/apply-design.d.ts +9 -0
  5. package/dist/apply-design.js +78 -31
  6. package/dist/cli.js +35 -0
  7. package/dist/copy.d.ts +41 -0
  8. package/dist/copy.js +87 -21
  9. package/dist/design-home.d.ts +54 -11
  10. package/dist/design-home.js +160 -22
  11. package/dist/lib/design-checks.d.ts +6 -1
  12. package/dist/lib/design-checks.js +23 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +21 -0
  14. package/dist/lib/fetch-template-snapshot.js +205 -0
  15. package/dist/lib/merge-three-way.d.ts +31 -0
  16. package/dist/lib/merge-three-way.js +65 -0
  17. package/dist/module-architecture.js +55 -2
  18. package/dist/programmatic.d.ts +7 -1
  19. package/dist/programmatic.js +76 -22
  20. package/dist/site-roster.d.ts +4 -2
  21. package/dist/site-roster.js +7 -5
  22. package/dist/spoke-checkout.d.ts +7 -0
  23. package/dist/spoke-checkout.js +8 -1
  24. package/dist/starter-grid.js +16 -0
  25. package/dist/surfaces.js +78 -2
  26. package/dist/sync-routes.d.ts +28 -0
  27. package/dist/sync-routes.js +123 -1
  28. package/dist/template-record.d.ts +59 -0
  29. package/dist/template-record.js +183 -0
  30. package/dist/update.d.ts +53 -0
  31. package/dist/update.js +606 -0
  32. package/package.json +3 -3
  33. package/site-templates/_base/ailk-template.json +2 -2
  34. package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
  35. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  36. package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
  37. package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
  38. package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
  39. package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
  41. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
  43. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
  46. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
  48. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
  49. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
  50. package/templates/.claude/cloudparity-repo-hooks.json +9 -0
  51. package/templates/.claude/hooks/architecture-check.sh +8 -0
  52. package/templates/.claude/rules/loop-contract.md +129 -0
  53. package/templates/.claude/settings.json +3 -2
  54. package/templates/.env.example +13 -0
  55. package/templates/CLAUDE.md +1 -1
  56. package/templates/CONVENTIONS.md +3 -2
  57. package/templates/README.md +26 -25
  58. package/templates/_gitignore +162 -0
  59. package/templates/apps/api/CLAUDE.md +18 -10
  60. package/templates/apps/api/jest.config.cjs +1 -0
  61. package/templates/apps/api/package.json +2 -1
  62. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
  63. package/templates/apps/api/src/__tests__/setup.ts +7 -0
  64. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
  65. package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
  66. package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
  67. package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
  68. package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
  69. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
  70. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
  71. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
  72. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
  73. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
  74. package/templates/apps/api/src/middleware/auth.ts +8 -4
  75. package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
  76. package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
  77. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
  78. package/templates/apps/api/src/routes/webhooks/README.md +33 -5
  79. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
  80. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
  81. package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
  82. package/templates/apps/api/src/server.ts +14 -2
  83. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
  84. package/templates/apps/api/src/services/audit-log.ts +72 -10
  85. package/templates/apps/api/src/services/default-tenant.ts +12 -3
  86. package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
  87. package/templates/apps/api/src/services/project-listings.ts +6 -1
  88. package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
  89. package/templates/apps/api/vercel.json +1 -1
  90. package/templates/apps/mcp/CLAUDE.md +19 -7
  91. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
  92. package/templates/apps/mcp/jest.config.cjs +1 -0
  93. package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
  94. package/templates/apps/mcp/src/tools/proposals.ts +27 -0
  95. package/templates/apps/mcp/src/workspace-server.ts +16 -0
  96. package/templates/apps/web/CLAUDE.md +26 -1
  97. package/templates/apps/web/_gitignore +6 -0
  98. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
  99. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
  100. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
  101. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
  111. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
  115. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
  117. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
  118. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
  119. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
  123. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
  124. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
  125. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
  129. package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
  132. package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
  133. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
  134. package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
  135. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  136. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  137. package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
  138. package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
  139. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
  140. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
  141. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
  143. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
  144. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
  145. package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
  146. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
  147. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
  148. package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
  149. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  150. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
  151. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  152. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  153. package/templates/apps/web/app/[locale]/layout.tsx +46 -4
  154. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
  155. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  156. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  157. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  158. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  159. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  160. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
  161. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
  162. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
  163. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
  164. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
  165. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  166. package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
  167. package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
  168. package/templates/apps/web/app/robots.txt/route.ts +13 -10
  169. package/templates/apps/web/jest.config.cjs +4 -0
  170. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
  171. package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
  172. package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
  173. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
  174. package/templates/apps/web/lib/account-config.ts +31 -16
  175. package/templates/apps/web/lib/consent-visitor.ts +103 -0
  176. package/templates/apps/web/lib/deck-page.ts +186 -0
  177. package/templates/apps/web/lib/relay.ts +20 -0
  178. package/templates/apps/web/messages/en.json +83 -9
  179. package/templates/apps/web/next.config.mjs +8 -2
  180. package/templates/database/CHANGELOG.md +48 -0
  181. package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
  182. package/templates/database/__tests__/person-notes.db.test.ts +249 -0
  183. package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
  184. package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
  185. package/templates/database/__tests__/seed.test.ts +39 -2
  186. package/templates/database/_gitignore +8 -0
  187. package/templates/database/inbox/schema.prisma +38 -12
  188. package/templates/database/jest.config.cjs +1 -0
  189. package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
  190. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
  191. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
  192. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
  193. package/templates/database/ops/schema.prisma +16 -0
  194. package/templates/database/package.json +1 -1
  195. package/templates/database/seed.ts +29 -0
  196. package/templates/project.yaml +6 -3
  197. package/templates/tools/jest/pin-timezone.cjs +34 -0
  198. package/versions.json +35 -35
  199. package/templates/.claude/rules/preserve-existing-style.md +0 -51
@@ -19,7 +19,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
19
19
  import { parseArgs } from "node:util";
20
20
  import { planDesignMd } from "./apply-design-md.js";
21
21
  import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
22
- import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
22
+ import { CONTACT_PAGE_PATH, DESIGN_SEED_PAGES, HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, siteIdentityName, } from "./design-home.js";
23
23
  import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
24
24
  import { resolveDesign, spliceThemeBlock } from "./design-scaffold.js";
25
25
  import { chromeKnobReaders, unreadChromeKnobs } from "./lib/chrome-readers.js";
@@ -194,6 +194,9 @@ export async function applyDesign(opts) {
194
194
  // #7743 D7 — the home seeds (the design's and every shipped design's), validated beside the presets.
195
195
  const designSeeds = designHomeSeeds(resolved, { designsDir: opts.designsDir, catalog });
196
196
  const shippedSeeds = shippedHomeSeeds({ designsDir: opts.designsDir, catalog });
197
+ // #8661 D11 — and the contact seeds, the same way.
198
+ const contactSeeds = designHomeSeeds(resolved, { designsDir: opts.designsDir, catalog, page: "contact" });
199
+ const shippedContactSeeds = shippedHomeSeeds({ designsDir: opts.designsDir, catalog, page: "contact" });
197
200
  // C5 — the design gate, fed the site's own key (D6). A Free design reads no key.
198
201
  if (!resolved.free) {
199
202
  const key = readSiteKey(targetAbs, env);
@@ -243,28 +246,44 @@ export async function applyDesign(opts) {
243
246
  const designMdNext = planDesignMd(resolved.dir, targetAbs);
244
247
  // #7743 D7 — the home is planned before the presets, so a seeded home is
245
248
  // written once, carrying both. The one `planDesignHome` call site here.
249
+ // #8661 D11 — the contact page is planned right after it, its heading named
250
+ // from the site's `siteIdentity.name`.
246
251
  const shape = roster?.template.shape;
247
- const target = designSeeds.find((seed) => seed.shape === shape);
248
- // The arc this site would take: the shape's seed, or (with no shape to pick one) any seed at all.
249
- const homeArc = target !== undefined || (shape === undefined && designSeeds.length > 0);
250
- const homePath = join(targetAbs, ...HOME_PAGE_PATH.split("/"));
251
- const current = existsSync(homePath) ? readFileSync(homePath, "utf8") : undefined;
252
- const homePlan = planDesignHome({
253
- current,
254
- shape,
255
- target,
256
- shapeHome: shape === undefined || current === undefined
257
- ? ""
258
- : readFileSync(shapeHomePath(shape, {
259
- siteTemplatesDir: opts.siteTemplatesDir,
260
- templatesDir,
261
- }), "utf8"),
262
- shippedSeeds,
263
- catalog: catalog(),
252
+ const siteName = siteIdentityName(siteMdxCurrent);
253
+ const plans = [
254
+ ["home", designSeeds, shippedSeeds],
255
+ ["contact", contactSeeds, shippedContactSeeds],
256
+ ].map(([page, pageSeeds, shippedPageSeeds]) => {
257
+ const target = pageSeeds.find((seed) => seed.shape === shape);
258
+ const path = join(targetAbs, ...DESIGN_SEED_PAGES[page].path.split("/"));
259
+ const current = existsSync(path) ? readFileSync(path, "utf8") : undefined;
260
+ const plan = planDesignHome({
261
+ page,
262
+ siteName,
263
+ current,
264
+ shape,
265
+ target,
266
+ shapeHome: shape === undefined || current === undefined
267
+ ? ""
268
+ : readFileSync(shapeHomePath(shape, { siteTemplatesDir: opts.siteTemplatesDir, templatesDir, page }), "utf8"),
269
+ shippedSeeds: shippedPageSeeds,
270
+ catalog: catalog(),
271
+ });
272
+ // The page this site would take: the shape's seed, or (with no shape to pick one) any seed at all.
273
+ const offered = target !== undefined || (shape === undefined && pageSeeds.length > 0);
274
+ const action = plan.outcome === "seeded" || plan.outcome === "restored" ? plan.outcome : undefined;
275
+ return { path, plan, offered, action };
264
276
  });
265
- const homeAction = homePlan.outcome === "seeded" || homePlan.outcome === "restored" ? homePlan.outcome : undefined;
277
+ const [homeSeeding, contactSeeding] = plans;
278
+ const homePlan = homeSeeding.plan;
279
+ const homeArc = homeSeeding.offered;
280
+ const homePath = homeSeeding.path;
281
+ const homeAction = homeSeeding.action;
282
+ const contactPlan = contactSeeding.plan;
283
+ const contactPath = contactSeeding.path;
284
+ const contactAction = contactSeeding.action;
266
285
  const pageWrites = [];
267
- if (roster !== undefined && (hasPresets || homeAction !== undefined)) {
286
+ if (roster !== undefined && (hasPresets || homeAction !== undefined || contactAction !== undefined)) {
268
287
  const pageTypes = new Set();
269
288
  for (const row of roster.rows) {
270
289
  const pageType = row.value["pageType"];
@@ -274,11 +293,17 @@ export async function applyDesign(opts) {
274
293
  const contentDir = join(targetAbs, "content");
275
294
  // An authored home kept from a design's arc is reported as kept, so no preset dresses it either (D3, F4).
276
295
  const homeKept = homePlan.outcome === "authored" && homeArc;
296
+ // #8661 D11 — likewise an authored contact page kept from the design's own.
297
+ const contactKept = contactPlan.outcome === "authored" && contactSeeding.offered;
277
298
  const pages = listMdxFiles(contentDir)
278
- .filter((path) => path !== siteMdxPath && !(homeKept && path === homePath))
299
+ .filter((path) => path !== siteMdxPath && !(homeKept && path === homePath) && !(contactKept && path === contactPath))
279
300
  .map((path) => ({
280
301
  path,
281
- text: path === homePath && homePlan.text !== undefined ? homePlan.text : readFileSync(path, "utf8"),
302
+ text: path === homePath && homePlan.text !== undefined
303
+ ? homePlan.text
304
+ : path === contactPath && contactPlan.text !== undefined
305
+ ? contactPlan.text
306
+ : readFileSync(path, "utf8"),
282
307
  }));
283
308
  const presetResult = hasPresets
284
309
  ? applyDesignPresets(pages, pkg.sections, { pageTypes })
@@ -286,14 +311,21 @@ export async function applyDesign(opts) {
286
311
  for (const page of pages) {
287
312
  const preset = presetResult.pages.find((p) => p.path === page.path);
288
313
  const home = page.path === homePath ? homeAction : undefined;
289
- if (preset === undefined && home === undefined)
314
+ const contact = page.path === contactPath ? contactAction : undefined;
315
+ if (preset === undefined && home === undefined && contact === undefined)
290
316
  continue;
291
317
  const fields = [
292
318
  ...new Set(presetResult.applied
293
319
  .filter((a) => a.path === page.path)
294
320
  .map((a) => `${a.sectionType}.${a.field}`)),
295
321
  ].sort();
296
- pageWrites.push({ path: page.path, text: preset?.text ?? page.text, fields, ...(home !== undefined ? { home } : {}) });
322
+ pageWrites.push({
323
+ path: page.path,
324
+ text: preset?.text ?? page.text,
325
+ fields,
326
+ ...(home !== undefined ? { home } : {}),
327
+ ...(contact !== undefined ? { contact } : {}),
328
+ });
297
329
  }
298
330
  }
299
331
  let manifest = "no-roster";
@@ -303,10 +335,14 @@ export async function applyDesign(opts) {
303
335
  if (roster.path === SITE_ROSTER_PATH) {
304
336
  manifestNext = planSiteManifestDesign(readFileSync(rosterAbs, "utf8"), { design: resolved.recorded, layout: chrome, theme }, rosterAbs);
305
337
  // #7743 D6 — the `/` row records the home as written, in the same single write.
306
- const home = pageWrites.find((page) => page.home !== undefined);
307
- if (home !== undefined) {
338
+ // #8661 — and the `/contact` row the contact page.
339
+ for (const page of ["home", "contact"]) {
340
+ const written = pageWrites.find((write) => write[page] !== undefined);
341
+ if (written === undefined)
342
+ continue;
308
343
  manifestNext =
309
- planHomeRecord(manifestNext ?? readFileSync(rosterAbs, "utf8"), home.text, catalog()) ?? manifestNext;
344
+ planHomeRecord(manifestNext ?? readFileSync(rosterAbs, "utf8"), written.text, catalog(), page) ??
345
+ manifestNext;
310
346
  }
311
347
  manifest = manifestNext === undefined ? "unchanged" : "recorded";
312
348
  }
@@ -321,7 +357,9 @@ export async function applyDesign(opts) {
321
357
  const path = siteRelative(targetAbs, page.path);
322
358
  changes.push(page.home !== undefined
323
359
  ? { path, kind: "home", action: page.home, fields: page.fields }
324
- : { path, kind: "preset", fields: page.fields });
360
+ : page.contact !== undefined
361
+ ? { path, kind: "contact", action: page.contact, fields: page.fields }
362
+ : { path, kind: "preset", fields: page.fields });
325
363
  }
326
364
  if (chromeNext !== chromeCurrent) {
327
365
  mkdirSync(dirname(chromePath), { recursive: true });
@@ -353,6 +391,8 @@ export async function applyDesign(opts) {
353
391
  manifest,
354
392
  home: homePlan.outcome,
355
393
  ...(homeArc ? { homeArc: true } : {}),
394
+ contact: contactPlan.outcome,
395
+ ...(contactSeeding.offered ? { contactSeed: true } : {}),
356
396
  ...(roster !== undefined ? { rosterPath: roster.path } : {}),
357
397
  ...(fontFamilies !== undefined ? { fonts: fontFamilies } : {}),
358
398
  ...(fontsUnwired ? { fontsUnwired: true } : {}),
@@ -370,16 +410,20 @@ export function formatApplyDesignReport(result) {
370
410
  for (const change of result.changes) {
371
411
  const suffix = change.kind === "preset"
372
412
  ? ` (${change.fields.join(", ")})`
373
- : change.kind === "home"
413
+ : change.kind === "home" || change.kind === "contact"
374
414
  ? ` (${[change.action, change.fields.join(", ")].filter((part) => part !== "").join("; ")})`
375
415
  : "";
376
416
  lines.push(` ${change.kind.padEnd(8)} ${change.path}${suffix}`);
377
417
  }
378
418
  const n = result.changes.length;
379
419
  const homeWritten = result.changes.some((change) => change.kind === "home");
420
+ const contactWritten = result.changes.some((change) => change.kind === "contact");
421
+ const contactNote = "the contact page's sections and title were replaced.";
380
422
  lines.push(`${n} file${n === 1 ? "" : "s"} changed. ${homeWritten
381
- ? "Routes are unchanged; the home page's sections were replaced."
382
- : "Routes and structured data are unchanged."}`);
423
+ ? `Routes are unchanged; the home page's sections were replaced.${contactWritten ? ` And ${contactNote}` : ""}`
424
+ : contactWritten
425
+ ? `Routes are unchanged; ${contactNote}`
426
+ : "Routes and structured data are unchanged."}`);
383
427
  }
384
428
  if (result.home === "authored" && result.homeArc === true) {
385
429
  lines.push(`home kept: ${HOME_PAGE_PATH} has authored sections — the ${result.design} home arc was not applied`);
@@ -387,6 +431,9 @@ export function formatApplyDesignReport(result) {
387
431
  else if (result.home === "no-shape" && result.homeArc === true) {
388
432
  lines.push("home not seeded: the site records no shape");
389
433
  }
434
+ if (result.contact === "authored" && result.contactSeed === true) {
435
+ lines.push(`contact kept: ${CONTACT_PAGE_PATH} has an authored title or sections — the ${result.design} contact page was not applied`);
436
+ }
390
437
  if (result.fonts !== undefined) {
391
438
  lines.push(`fonts: ${result.fonts.join(", ")} load at next build through next/font/google — a family that is not a variable Google font fails there`);
392
439
  }
package/dist/cli.js CHANGED
@@ -19,6 +19,9 @@
19
19
  * to a linked Vercel project and redeploy, exit (#6683 D9)
20
20
  * on an `apply-design` subcommand: rewrite an existing site's look,
21
21
  * print the changed-file report, exit (#6819)
22
+ * on an `update` subcommand: bring template fixes into a site's
23
+ * already-copied apps/web, apps/api and apps/mcp code, print the
24
+ * per-file report, exit (2 on a refusal it names) (#8522)
22
25
  * 1. parse argv → CliOptions
23
26
  * 2. on --help: print helpText, exit 0
24
27
  * 3. on --version: print version, exit 0
@@ -58,6 +61,7 @@ import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCata
58
61
  import { SEED_FLOWS_HELP, SeedFlowsRefusedError, parseSeedFlowsArgs, seedFlows, } from "./seed-flows.js";
59
62
  import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
60
63
  import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
64
+ import { UPDATE_HELP, UpdateRefusedError, formatUpdateReport, parseUpdateArgs, updateSite, } from "./update.js";
61
65
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
62
66
  const PKG_JSON = resolve(PKG_ROOT, "package.json");
63
67
  function readPackageVersion() {
@@ -272,6 +276,35 @@ async function main() {
272
276
  process.stdout.write(formatApplyDesignReport(result));
273
277
  process.exit(0);
274
278
  }
279
+ // `update [<target-dir>]` (#8522): bring template fixes into a site's
280
+ // already-copied apps/web, apps/api and apps/mcp code. A refusal it names
281
+ // (no record and no patternVersion, an unparseable record, a downgrade, an
282
+ // unreadable architecture.yaml or manifest topology, a backfill whose
283
+ // fetch fails) exits 2 before anything is written. A clean run exits 0
284
+ // even when it lists files under "Needs review" — the review list is the
285
+ // hand-off, not a failure.
286
+ if (argv[0] === "update") {
287
+ const args = parseUpdateArgs(argv.slice(1));
288
+ if (args.help) {
289
+ process.stdout.write(UPDATE_HELP);
290
+ process.exit(0);
291
+ }
292
+ try {
293
+ const result = await updateSite({
294
+ targetDir: args.target ?? process.cwd(),
295
+ templatesDir: resolve(PKG_ROOT, "templates"),
296
+ version: readPackageVersion(),
297
+ });
298
+ process.stdout.write(formatUpdateReport(result));
299
+ }
300
+ catch (err) {
301
+ if (!(err instanceof UpdateRefusedError))
302
+ throw err;
303
+ process.stderr.write(`[create-ailk] ${err.message}\n`);
304
+ process.exit(err.exitCode);
305
+ }
306
+ process.exit(0);
307
+ }
275
308
  // No silent default (#6754 D2/D3): a scaffold run naming neither --shape
276
309
  // nor --template asks for a shape on a terminal and is refused without one.
277
310
  // A --design or --modules outside the release scope is refused before that
@@ -317,6 +350,8 @@ async function main() {
317
350
  "\n" +
318
351
  APPLY_DESIGN_HELP +
319
352
  "\n" +
353
+ UPDATE_HELP +
354
+ "\n" +
320
355
  LICENSE_SET_HELP);
321
356
  process.exit(0);
322
357
  }
package/dist/copy.d.ts CHANGED
@@ -19,11 +19,52 @@ export interface CopyTemplateOptions {
19
19
  * `.../services` never excludes a sibling `.../services-extra`.
20
20
  */
21
21
  excludePaths?: string[];
22
+ /**
23
+ * Called once per file actually written, with its `/`-separated
24
+ * destination path (relative to `targetDir`) and the exact bytes written
25
+ * (#8522 D2) — `create-ailk update`'s per-file record is built from these
26
+ * calls. No existing caller passes this, so their behaviour is unchanged.
27
+ */
28
+ onWrite?: (relPath: string, bytes: Uint8Array) => void;
22
29
  }
30
+ /**
31
+ * Whether a walked template-relative path is inside (or equal to) an excluded
32
+ * directory. Compares `/`-normalized paths so the check holds on Windows, and
33
+ * only at a directory boundary — a prefix that stops mid-segment is no match.
34
+ * Exported (#8522 D3) so `create-ailk update` applies the same surface-gating
35
+ * boundary to a candidate new file.
36
+ */
37
+ export declare function isExcluded(relativePath: string, excluded: readonly string[]): boolean;
38
+ /**
39
+ * The pack-safe name `build-templates.mjs` ships each snapshot `.gitignore`
40
+ * under (#7366 D1) — npm and pnpm strip a file literally named `.gitignore`
41
+ * from a packed tarball, but keep one named `_gitignore`. One definition,
42
+ * imported by `build-templates.mjs` (from the compiled `dist/copy.js`) and
43
+ * by `copyTemplate` below, so the two can never spell it differently.
44
+ */
45
+ export declare const PACKED_GITIGNORE_NAME = "_gitignore";
23
46
  /**
24
47
  * Whether a scaffolded path's content is text we run markers over. Exported
25
48
  * because the template/base layer (`installBundleTree`) substitutes with the
26
49
  * same classifier — one definition, not two (#6460).
27
50
  */
28
51
  export declare function isTextFile(relativePath: string): boolean;
52
+ /**
53
+ * The destination path a template-relative path (`/`-separated) copies to —
54
+ * `undefined` for a packaging-only `.gitkeep` or `.npmignore` (never a
55
+ * scaffold file), and a final `PACKED_GITIGNORE_NAME` restored to
56
+ * `.gitignore` in the same directory otherwise (#7366 D3) — so a scaffold
57
+ * never carries the packaging-only name. Exported (#8522 D3) as the one
58
+ * renderer of "the template's copy", shared by `copyTemplate` and by
59
+ * `create-ailk update`'s render of a running or an old template version.
60
+ */
61
+ export declare function templateDestination(rel: string): string | undefined;
62
+ /**
63
+ * The bytes a template-relative file copies as, at its destination path
64
+ * (#8522 D3): workspace versions plus markers for `package.json`, markers
65
+ * for `isTextFile`, raw bytes otherwise. Exported so `copyTemplate`'s walk
66
+ * and `create-ailk update`'s render of a running or an old template version
67
+ * share the one renderer — nothing about the copy itself changes.
68
+ */
69
+ export declare function renderTemplateFile(bytes: Uint8Array, destRel: string, versions: WorkspaceVersionMap, markers: MarkerMap): Uint8Array;
29
70
  export declare function copyTemplate(opts: CopyTemplateOptions): void;
package/dist/copy.js CHANGED
@@ -17,8 +17,23 @@
17
17
  * alike. Symlinks are skipped outright (never followed, never copied) —
18
18
  * the snapshotted templates/ tree is built without symlinks; any symlink
19
19
  * present at scaffold time is treated as a packaging accident and ignored.
20
+ *
21
+ * Packed ignore files (#7366):
22
+ * - npm and pnpm strip every file literally named `.gitignore` from a
23
+ * packed tarball, so `build-templates.mjs` ships each snapshot
24
+ * `.gitignore` under `PACKED_GITIGNORE_NAME` (`_gitignore`) instead. A
25
+ * file named `PACKED_GITIGNORE_NAME` is restored to `.gitignore` in the
26
+ * same directory on the way into a scaffold, classified and
27
+ * marker-substituted exactly as a `.gitignore` is today (its destination
28
+ * relative path, not its source name, drives `isTextFile`). A directory
29
+ * carrying both `.gitignore` and `PACKED_GITIGNORE_NAME` is refused
30
+ * outright, before either is written, rather than resolved by
31
+ * `readdir()` order. `.npmignore` — the packaging-only sibling
32
+ * `build-templates.mjs` writes beside each `PACKED_GITIGNORE_NAME` to
33
+ * keep pack-time exclusions working — is skipped like `.gitkeep`: it
34
+ * never belongs in a scaffold.
20
35
  */
21
- import { copyFileSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
36
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
22
37
  import { extname, join, relative, resolve, sep } from "node:path";
23
38
  import pkg from "../package.json" with { type: "json" };
24
39
  import { substituteMarkers, substituteWorkspaceVersions, } from "./substitute.js";
@@ -57,8 +72,10 @@ export function buildScaffoldMarkers(projectName) {
57
72
  * Whether a walked template-relative path is inside (or equal to) an excluded
58
73
  * directory. Compares `/`-normalized paths so the check holds on Windows, and
59
74
  * only at a directory boundary — a prefix that stops mid-segment is no match.
75
+ * Exported (#8522 D3) so `create-ailk update` applies the same surface-gating
76
+ * boundary to a candidate new file.
60
77
  */
61
- function isExcluded(relativePath, excluded) {
78
+ export function isExcluded(relativePath, excluded) {
62
79
  if (excluded.length === 0)
63
80
  return false;
64
81
  const rel = relativePath.split(sep).join("/");
@@ -71,6 +88,14 @@ function isExcluded(relativePath, excluded) {
71
88
  }
72
89
  return false;
73
90
  }
91
+ /**
92
+ * The pack-safe name `build-templates.mjs` ships each snapshot `.gitignore`
93
+ * under (#7366 D1) — npm and pnpm strip a file literally named `.gitignore`
94
+ * from a packed tarball, but keep one named `_gitignore`. One definition,
95
+ * imported by `build-templates.mjs` (from the compiled `dist/copy.js`) and
96
+ * by `copyTemplate` below, so the two can never spell it differently.
97
+ */
98
+ export const PACKED_GITIGNORE_NAME = "_gitignore";
74
99
  /** File extensions whose content we treat as text (and run markers over). */
75
100
  const TEXT_EXTS = new Set([
76
101
  ".md",
@@ -113,6 +138,46 @@ function isPackageJson(relativePath) {
113
138
  const base = relativePath.split(/[\\/]/).pop() ?? "";
114
139
  return base === "package.json";
115
140
  }
141
+ /**
142
+ * The destination path a template-relative path (`/`-separated) copies to —
143
+ * `undefined` for a packaging-only `.gitkeep` or `.npmignore` (never a
144
+ * scaffold file), and a final `PACKED_GITIGNORE_NAME` restored to
145
+ * `.gitignore` in the same directory otherwise (#7366 D3) — so a scaffold
146
+ * never carries the packaging-only name. Exported (#8522 D3) as the one
147
+ * renderer of "the template's copy", shared by `copyTemplate` and by
148
+ * `create-ailk update`'s render of a running or an old template version.
149
+ */
150
+ export function templateDestination(rel) {
151
+ const parts = rel.split("/");
152
+ const last = parts.length - 1;
153
+ if (last < 0)
154
+ return undefined;
155
+ if (parts[last] === ".gitkeep" || parts[last] === ".npmignore") {
156
+ return undefined;
157
+ }
158
+ if (parts[last] === PACKED_GITIGNORE_NAME) {
159
+ parts[last] = ".gitignore";
160
+ }
161
+ return parts.join("/");
162
+ }
163
+ /**
164
+ * The bytes a template-relative file copies as, at its destination path
165
+ * (#8522 D3): workspace versions plus markers for `package.json`, markers
166
+ * for `isTextFile`, raw bytes otherwise. Exported so `copyTemplate`'s walk
167
+ * and `create-ailk update`'s render of a running or an old template version
168
+ * share the one renderer — nothing about the copy itself changes.
169
+ */
170
+ export function renderTemplateFile(bytes, destRel, versions, markers) {
171
+ if (isPackageJson(destRel)) {
172
+ const text = Buffer.from(bytes).toString("utf8");
173
+ return Buffer.from(substituteMarkers(substituteWorkspaceVersions(text, versions), markers), "utf8");
174
+ }
175
+ if (isTextFile(destRel)) {
176
+ const text = Buffer.from(bytes).toString("utf8");
177
+ return Buffer.from(substituteMarkers(text, markers), "utf8");
178
+ }
179
+ return Buffer.from(bytes);
180
+ }
116
181
  export function copyTemplate(opts) {
117
182
  const templatesAbs = resolve(opts.templatesDir);
118
183
  const targetAbs = resolve(opts.targetDir);
@@ -124,19 +189,30 @@ export function copyTemplate(opts) {
124
189
  if (dir === undefined)
125
190
  break;
126
191
  const entries = readdirSync(dir, { withFileTypes: true });
192
+ // A directory carrying both the live name and the packed name would
193
+ // otherwise resolve by `readdir()` order — refuse outright, before
194
+ // writing either (#7366 D3).
195
+ const names = new Set(entries.map((entry) => entry.name));
196
+ if (names.has(".gitignore") && names.has(PACKED_GITIGNORE_NAME)) {
197
+ throw new Error(`templates directory carries both .gitignore and ${PACKED_GITIGNORE_NAME}: ${dir}`);
198
+ }
127
199
  for (const entry of entries) {
128
- // .gitkeep is a packaging artifact for the empty-templates state in git;
129
- // never propagate it to scaffolder output.
130
- if (entry.name === ".gitkeep")
131
- continue;
132
200
  const srcPath = join(dir, entry.name);
133
- const rel = relative(templatesAbs, srcPath);
201
+ const rel = relative(templatesAbs, srcPath).split(sep).join("/");
134
202
  // Surface gating (#4154): a route directory belonging to a surface the
135
203
  // declared architecture does not carry is skipped outright — the route is
136
204
  // ABSENT from the scaffold, not present-and-serving-a-placeholder.
137
205
  if (isExcluded(rel, excluded))
138
206
  continue;
139
- const dstPath = join(targetAbs, rel);
207
+ // The destination path: `undefined` drops a packaging-only .gitkeep /
208
+ // .npmignore (#7366 D3); otherwise a packed .gitignore's name is
209
+ // restored. Everything below — the path-safety check, text
210
+ // classification, and marker substitution — runs on THIS path, never
211
+ // the source name.
212
+ const destRel = templateDestination(rel);
213
+ if (destRel === undefined)
214
+ continue;
215
+ const dstPath = join(targetAbs, destRel);
140
216
  // Path-safety guard: dstPath MUST be within targetAbs. Use `path.sep`
141
217
  // so the check holds on Windows (`\\`) as well as POSIX (`/`).
142
218
  const resolvedDst = resolve(dstPath);
@@ -152,19 +228,9 @@ export function copyTemplate(opts) {
152
228
  stack.push(srcPath);
153
229
  continue;
154
230
  }
155
- if (isPackageJson(rel)) {
156
- const text = readFileSync(srcPath, "utf8");
157
- const transformed = substituteMarkers(substituteWorkspaceVersions(text, opts.versions), opts.markers);
158
- writeFileSync(dstPath, transformed);
159
- }
160
- else if (isTextFile(rel)) {
161
- const text = readFileSync(srcPath, "utf8");
162
- writeFileSync(dstPath, substituteMarkers(text, opts.markers));
163
- }
164
- else {
165
- // Binary or unknown — copy verbatim.
166
- copyFileSync(srcPath, dstPath);
167
- }
231
+ const rendered = renderTemplateFile(readFileSync(srcPath), destRel, opts.versions, opts.markers);
232
+ writeFileSync(dstPath, rendered);
233
+ opts.onWrite?.(destRel, rendered);
168
234
  }
169
235
  }
170
236
  }
@@ -2,7 +2,26 @@ import { type SectionLookCatalog } from "./design-presets.js";
2
2
  import type { ResolvedDesign } from "./design-scaffold.js";
3
3
  /** Where a site's home page lives, site-relative. */
4
4
  export declare const HOME_PAGE_PATH = "content/en/HomePage/home.mdx";
5
- /** One `home/<shape>.mdx` file of a design package (D1). */
5
+ /** Where a site's contact page lives, site-relative (#8661 D11). */
6
+ export declare const CONTACT_PAGE_PATH = "content/en/ContactPage/contact.mdx";
7
+ /**
8
+ * The pages a design may seed (#8661 D11), keyed by the design-package
9
+ * directory that holds their seeds (`home/<shape>.mdx`, `contact/<shape>.mdx`).
10
+ */
11
+ export declare const DESIGN_SEED_PAGES: {
12
+ readonly home: {
13
+ readonly pageType: "HomePage";
14
+ readonly path: "content/en/HomePage/home.mdx";
15
+ readonly route: "/";
16
+ };
17
+ readonly contact: {
18
+ readonly pageType: "ContactPage";
19
+ readonly path: "content/en/ContactPage/contact.mdx";
20
+ readonly route: "/contact";
21
+ };
22
+ };
23
+ export type DesignSeedPage = keyof typeof DESIGN_SEED_PAGES;
24
+ /** One `home/<shape>.mdx` (or `contact/<shape>.mdx`, #8661 D11) file of a design package (D1). */
6
25
  export interface HomeSeed {
7
26
  readonly shape: string;
8
27
  /** Absolute path, for refusals. */
@@ -11,34 +30,46 @@ export interface HomeSeed {
11
30
  }
12
31
  export type HomeSeedOutcome = "seeded" | "restored" | "unchanged" | "authored" | "no-shape" | "no-home";
13
32
  /**
14
- * Read and validate `<dir>/home/*` (D1). Refuses with a `DesignPackageError`
15
- * naming the file, before any write: a basename that is not a recognized
16
- * shape, a page whose `type` is not `HomePage`, `sections` that cannot be
17
- * located (or are not in the flow form a carry can splice), a section type
18
- * the catalog does not carry, or a `logoCloud` whose `logos` is not `[]`.
33
+ * Read and validate `<dir>/home/*` (D1), or `<dir>/contact/*` for
34
+ * `page: "contact"` (#8661 D11). Refuses with a `DesignPackageError` naming
35
+ * the file, before any write: a basename that is not a recognized shape, a
36
+ * page whose `type` is not the page's (`HomePage`, `ContactPage`), `sections`
37
+ * that cannot be located (or are not in the flow form a carry can splice), a
38
+ * section type the catalog does not carry, or a `logoCloud` whose `logos` is
39
+ * not `[]`.
19
40
  */
20
41
  export declare function readDesignHomeSeeds(dir: string, opts: {
21
42
  catalog: () => SectionLookCatalog;
43
+ page?: DesignSeedPage;
22
44
  }): HomeSeed[];
23
45
  /**
24
- * The seeds a resolved design uses (D1): its own `home/`, else — a
25
- * byte-identical copy of a shipped design IS that design — the shipped
26
- * design's, else none.
46
+ * The seeds a resolved design uses (D1): its own `home/` (or `contact/`),
47
+ * else — a byte-identical copy of a shipped design IS that design — the
48
+ * shipped design's, else none.
27
49
  */
28
50
  export declare function designHomeSeeds(resolved: ResolvedDesign, opts: {
29
51
  designsDir: string;
30
52
  catalog: () => SectionLookCatalog;
53
+ page?: DesignSeedPage;
31
54
  }): HomeSeed[];
32
55
  /** Every shipped design's seeds (D3's placeholder set reads them all). */
33
56
  export declare function shippedHomeSeeds(opts: {
34
57
  designsDir: string;
35
58
  catalog: () => SectionLookCatalog;
59
+ page?: DesignSeedPage;
36
60
  }): HomeSeed[];
37
- /** The shape's own home: its shape tree's, else the overlay's (spec § 1.4). */
61
+ /** The shape's own home (or contact page): its shape tree's, else the overlay's (spec § 1.4). */
38
62
  export declare function shapeHomePath(shape: string, opts: {
39
63
  siteTemplatesDir: string;
40
64
  templatesDir: string;
65
+ page?: DesignSeedPage;
41
66
  }): string;
67
+ /**
68
+ * A site's `siteIdentity.name` (#8661 D11, C8): the first `name:` line
69
+ * indented directly under `siteIdentity:` in `content/_site.mdx`'s front
70
+ * matter, quotes stripped. Undefined when there is no such line.
71
+ */
72
+ export declare function siteIdentityName(siteMdx: string | undefined): string | undefined;
42
73
  /**
43
74
  * A home's content fingerprint (D3): per section in order, its type and its
44
75
  * non-look `data` entries as `[key, normalized raw value]`, sorted by key.
@@ -55,9 +86,21 @@ export declare function applySeed(home: string, seed: HomeSeed, file?: string):
55
86
  /**
56
87
  * Plan a site home under a design (D3). Writes nothing: `text` is the new
57
88
  * home when the outcome is `seeded` or `restored`, and absent otherwise.
89
+ *
90
+ * `page: "contact"` (#8661 D11) plans the contact page instead. The seeds'
91
+ * `{{SITE_NAME}}` is filled with `siteName` (absent: `{Your Name}`) before
92
+ * anything is compared; the shape's own page is read with the scaffold
93
+ * markers substituted, as a scaffold installed it; the page is a placeholder
94
+ * only while its title is one too (the shape's own, or a seed's); and a write
95
+ * replaces the `title:` line as well as the sections, and the `description:`
96
+ * line while it is still a placeholder (the shape's own, or a seed's).
58
97
  */
59
98
  export declare function planDesignHome(opts: {
60
- /** The site's content/en/HomePage/home.mdx, if any. */
99
+ /** The page planned (#8661 D11). Default `"home"`. */
100
+ page?: DesignSeedPage;
101
+ /** The site's name, for a contact seed's `{{SITE_NAME}}` (#8661 D11). */
102
+ siteName?: string | undefined;
103
+ /** The site's content/en/HomePage/home.mdx (or ContactPage/contact.mdx), if any. */
61
104
  current: string | undefined;
62
105
  /** The roster's template shape. */
63
106
  shape: string | undefined;