create-ailk 0.14.0 → 0.16.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 (184) hide show
  1. package/README.md +124 -0
  2. package/component-catalog.json +3 -3
  3. package/dist/add-module.d.ts +7 -0
  4. package/dist/add-module.js +80 -3
  5. package/dist/add-route.js +26 -2
  6. package/dist/adopt.d.ts +74 -0
  7. package/dist/adopt.js +314 -0
  8. package/dist/apply-design.d.ts +12 -1
  9. package/dist/apply-design.js +36 -4
  10. package/dist/cli.js +103 -3
  11. package/dist/derive-architecture.d.ts +37 -2
  12. package/dist/derive-architecture.js +92 -7
  13. package/dist/design-package.js +3 -2
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/lib/chrome-readers.d.ts +21 -0
  17. package/dist/lib/chrome-readers.js +100 -0
  18. package/dist/lib/declared-env.d.ts +142 -0
  19. package/dist/lib/declared-env.js +985 -0
  20. package/dist/lib/env-catalog.d.ts +17 -0
  21. package/dist/lib/env-catalog.js +234 -0
  22. package/dist/lib/extract-module-bundle.d.ts +20 -0
  23. package/dist/lib/extract-module-bundle.js +53 -6
  24. package/dist/lib/license-key-files.d.ts +6 -1
  25. package/dist/lib/license-key-files.js +9 -4
  26. package/dist/lib/module-tier.d.ts +2 -0
  27. package/dist/lib/module-tier.js +4 -0
  28. package/dist/module-architecture.d.ts +9 -0
  29. package/dist/module-architecture.js +82 -0
  30. package/dist/parse-args.d.ts +27 -0
  31. package/dist/parse-args.js +89 -0
  32. package/dist/programmatic.d.ts +28 -0
  33. package/dist/programmatic.js +181 -11
  34. package/dist/seed-flows.d.ts +65 -0
  35. package/dist/seed-flows.js +350 -0
  36. package/dist/site-chrome.d.ts +4 -0
  37. package/dist/site-chrome.js +34 -3
  38. package/dist/site-manifest.d.ts +55 -5
  39. package/dist/site-manifest.js +121 -12
  40. package/dist/site-roster.d.ts +13 -0
  41. package/dist/site-roster.js +22 -8
  42. package/dist/spoke-checkout.d.ts +42 -0
  43. package/dist/spoke-checkout.js +359 -0
  44. package/dist/surfaces.js +7 -0
  45. package/dist/sync-routes.js +29 -5
  46. package/dist/topology.d.ts +246 -0
  47. package/dist/topology.js +942 -0
  48. package/package.json +3 -3
  49. package/site-templates/designs/minimalism/DESIGN.md +3 -3
  50. package/templates/.env.example +10 -0
  51. package/templates/apps/api/CLAUDE.md +21 -4
  52. package/templates/apps/api/package.json +1 -0
  53. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
  54. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
  55. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
  56. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
  57. package/templates/apps/api/src/bin/spokes.ts +62 -1
  58. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
  59. package/templates/apps/api/src/config/index.ts +35 -0
  60. package/templates/apps/api/src/lib/__mocks__/prisma.ts +99 -0
  61. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
  62. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
  63. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
  64. package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
  65. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
  66. package/templates/apps/api/src/lib/agent-store.ts +95 -3
  67. package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
  68. package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
  69. package/templates/apps/api/src/lib/site.ts +37 -0
  70. package/templates/apps/api/src/lib/spoke-store.ts +41 -3
  71. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
  72. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
  73. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
  74. package/templates/apps/api/src/middleware/auth.ts +129 -0
  75. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
  76. package/templates/apps/api/src/openapi/spec.ts +38 -2
  77. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
  78. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
  80. package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
  81. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +282 -0
  82. package/templates/apps/api/src/routes/leads/index.ts +69 -6
  83. package/templates/apps/api/src/routes/webhooks/README.md +39 -0
  84. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
  85. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
  86. package/templates/apps/api/src/routes/webhooks/stripe.ts +133 -2
  87. package/templates/apps/api/src/server.ts +105 -0
  88. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
  89. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
  90. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +492 -0
  91. package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
  92. package/templates/apps/api/src/services/default-tenant.ts +41 -7
  93. package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
  94. package/templates/apps/api/src/services/lead-routing.ts +278 -30
  95. package/templates/apps/api/src/services/person.ts +541 -0
  96. package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
  97. package/templates/apps/mcp/__tests__/index.test.ts +5 -5
  98. package/templates/apps/mcp/src/index.ts +6 -4
  99. package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
  100. package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
  101. package/templates/apps/web/.env.example +58 -5
  102. package/templates/apps/web/CLAUDE.md +1 -0
  103. package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
  106. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
  107. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
  110. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
  111. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
  112. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
  113. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
  114. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
  115. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
  116. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +105 -11
  117. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +12 -8
  118. package/templates/apps/web/app/[locale]/layout.tsx +83 -18
  119. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
  120. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
  121. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
  122. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
  123. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
  124. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
  125. package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
  126. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
  127. package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
  128. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +104 -1
  129. package/templates/apps/web/app/openapi.json/route.ts +72 -7
  130. package/templates/apps/web/auth.ts +29 -2
  131. package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
  132. package/templates/apps/web/components/signed-in-nav.tsx +17 -3
  133. package/templates/apps/web/instrumentation.ts +7 -4
  134. package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
  135. package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
  136. package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
  137. package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
  138. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
  139. package/templates/apps/web/lib/__tests__/relay.test.ts +317 -0
  140. package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
  141. package/templates/apps/web/lib/account-config.ts +130 -1
  142. package/templates/apps/web/lib/api-origin.generated.ts +11 -0
  143. package/templates/apps/web/lib/closing-footer-path.ts +50 -0
  144. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
  145. package/templates/apps/web/lib/hub-session/config.ts +104 -0
  146. package/templates/apps/web/lib/hub-session/index.ts +23 -0
  147. package/templates/apps/web/lib/hub-session/seal.ts +82 -0
  148. package/templates/apps/web/lib/hub-session/session.ts +180 -0
  149. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
  150. package/templates/apps/web/lib/listings-config.ts +31 -0
  151. package/templates/apps/web/lib/listings.tsx +7 -3
  152. package/templates/apps/web/lib/meeting-slugs.ts +11 -1
  153. package/templates/apps/web/lib/next-path.ts +35 -0
  154. package/templates/apps/web/lib/relay.ts +209 -3
  155. package/templates/apps/web/lib/server-api.ts +11 -1
  156. package/templates/apps/web/lib/site-brand.tsx +22 -1
  157. package/templates/apps/web/lib/site-chrome.ts +3 -0
  158. package/templates/apps/web/lib/topology.generated.ts +33 -0
  159. package/templates/apps/web/messages/en.json +32 -3
  160. package/templates/apps/web/package.json +1 -1
  161. package/templates/database/CHANGELOG.md +51 -0
  162. package/templates/database/__tests__/agents.db.test.ts +85 -0
  163. package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
  164. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
  165. package/templates/database/__tests__/member-area.db.test.ts +245 -0
  166. package/templates/database/__tests__/person.db.test.ts +584 -0
  167. package/templates/database/auth/schema.prisma +24 -0
  168. package/templates/database/config/schema.prisma +13 -0
  169. package/templates/database/inbox/schema.prisma +111 -0
  170. package/templates/database/jest.config.cjs +7 -11
  171. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
  172. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
  173. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
  174. package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
  175. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
  176. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
  177. package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
  178. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
  179. package/templates/database/package.json +1 -1
  180. package/templates/database/scripts/person-backfill.sql +132 -0
  181. package/templates/database/tenancy.ts +53 -0
  182. package/templates/tools/check-declared-env.ts +37 -0
  183. package/templates/tools/declared-env.ts +1163 -0
  184. package/versions.json +35 -34
package/README.md CHANGED
@@ -262,6 +262,130 @@ the route-surface registry in the same file.
262
262
  `ROUTE_SINCE` entry — `__tests__/sync-routes.test.ts` fails otherwise (and it
263
263
  fails on a stale entry for a route the template no longer ships).
264
264
 
265
+ ### Adopting an existing site
266
+
267
+ A site that create-ailk did not scaffold, or that was scaffolded before
268
+ `--topology` existed, does not record its topology. Every create-ailk command
269
+ then treats it as a hub — wrong for a site that was trimmed to a spoke by hand.
270
+ `adopt` records it, once:
271
+
272
+ ```bash
273
+ # a site with a .ailk-site-manifest.json (default target: cwd)
274
+ create-ailk adopt ./my-site
275
+ # a site with none: name its template, and its modules when
276
+ # architecture.yaml's "# Modules:" header is missing or out of date
277
+ create-ailk adopt ./my-site --template person-service --modules marketing,docs-blog
278
+ ```
279
+
280
+ It writes two fields into `.ailk-site-manifest.json` (and, on a spoke, the
281
+ `env_contract` rows below):
282
+
283
+ - `topology` is read from the tree. No `apps/api` and no `database/` is a
284
+ spoke; both is a hub; one without the other is refused.
285
+ - `patternVersion` is the create-ailk version you ran — the version the site
286
+ was adopted against.
287
+
288
+ A site with a manifest keeps every other byte of it. A site with none gets one,
289
+ built the way a scaffold builds it, from `--template` and the module set
290
+ (`--modules`, else the `architecture.yaml` header). `--template` and
291
+ `--modules` are refused on a site that already has a manifest, because the
292
+ manifest already records both. A root `TEMPLATE-MANIFEST.json` (the roster's
293
+ older name) is refused until you rename it to `.ailk-site-manifest.json`.
294
+
295
+ The report lists each generated seam file (`apps/web/lib/topology.generated.ts`,
296
+ and the `apps/mcp` one when `apps/mcp` exists) as `present`, `differs` or
297
+ `absent` against the render for the site's topology. On a spoke it also warns
298
+ about each recorded module a spoke cannot carry. Neither changes the exit code:
299
+ adopt records the site, it does not admit modules — `add-module` does that.
300
+
301
+ On a spoke with a `project.yaml`, adopt also appends an `env_contract` row
302
+ for every environment variable the site's code reads that has none, so the
303
+ spoke's `pnpm env:declared` check has a contract to read. It never edits or
304
+ removes a row, and a block it cannot read is refused with exit 2 before
305
+ anything is written. A hub's `project.yaml` is never touched, and adopt does
306
+ not install the check itself.
307
+
308
+ Beyond that, adopt never writes a seam file, `architecture.yaml` or anything
309
+ under `apps/api`, and never runs another command (`sync-routes --apply`
310
+ included). A second run is refused with exit 2 and the manifest untouched.
311
+
312
+ ### Keeping a spoke current
313
+
314
+ A spoke's scaffold-written files are few: the generated seams, the
315
+ declared-env check (`tools/declared-env.ts`, `tools/check-declared-env.ts`)
316
+ and the one-line route re-exports; everything else is content, or code that arrives
317
+ through a `@working-theory/*` package bump. create-ailk does not re-scaffold a
318
+ site in place, and there is no re-sync command: `add-module` never overwrites
319
+ an edited file, so a re-sync could only report. Moving a spoke to a new
320
+ create-ailk version is a per-version review instead:
321
+
322
+ 1. **Read the changesets.** A create-ailk changeset that changes what
323
+ create-ailk writes into a scaffold-written spoke file names each such file
324
+ on a line beginning `Scaffold-written files:`. Search the CHANGELOG between
325
+ your site's `patternVersion` and the new version for that line.
326
+ 2. **Check the routes.** `create-ailk sync-routes ./my-site` lists missing and
327
+ locally modified route directories without writing anything.
328
+ 3. **Apply each named change by hand**, then set `patternVersion` in
329
+ `.ailk-site-manifest.json` to the version the changes came from. It is a
330
+ one-value edit; `adopt` will not do it, because it refuses a second run.
331
+
332
+ **Maintainer rule:** a create-ailk changeset that changes the content of a file
333
+ create-ailk writes into a spoke scaffold — a seam render, a route re-export, a
334
+ root-file transform — MUST carry a `Scaffold-written files:` line naming each
335
+ such file, site-relative. This is a convention, not a lint.
336
+
337
+ ### Seeding a spoke's flows
338
+
339
+ A spoke's flows live in its own repo as **declared data**: `flows/<slug>.json`,
340
+ each one flow config exactly as the hub stores it — `slug`, `defaultLocale`,
341
+ `steps`, and optionally `thankYou` and `flags`. The file name is the config's
342
+ `slug` plus `.json`. A spoke carrying `signal-collectors` gets the module's
343
+ `referral` and `fit-check` flows there (a hub gets them under
344
+ `database/flows/`), plus a `flows/README.md`.
345
+
346
+ `seed-flows` writes them into the spoke's hub workspace, as one rollout step:
347
+
348
+ ```bash
349
+ AILK_SPOKE_KEY=<the spoke's key> create-ailk seed-flows ./my-spoke
350
+ create-ailk seed-flows ./my-spoke --dry-run # check the files, send nothing
351
+ ```
352
+
353
+ - The key is read from `AILK_SPOKE_KEY` only — never a flag or a file — and
354
+ never printed.
355
+ - The hub origin is `--hub-url`, else `AILK_API_URL`, else the `AILK_API_URL=`
356
+ line of `apps/web/.env.local`; it must be `https:` (or `http:` on a loopback
357
+ host). The first line printed names it and its source, before any request.
358
+ - Each flow is sent to `PUT <hub>/v1/flows/<slug>/definition` and prints
359
+ `created`, `updated` or `unchanged`, so a re-run is safe. A failure prints
360
+ only its status and code, and the rest still run.
361
+ - Exit 0 when every flow was accepted, 1 when any failed, 2 on a refusal (a hub
362
+ site, a bad file, no key, no valid hub origin), which sends nothing.
363
+
364
+ The hub must have `AILK_SPOKE_FLOW_SEED=true`; until it does and this has run,
365
+ the spoke's `/flows/<slug>` links reach no flow. The repo owns each flow's
366
+ config: the next seed that differs overwrites an Admin edit to it. The hub
367
+ refuses any block it acts on outside the spoke's own pages — emails,
368
+ deliverables, pricing, registers, listings — so a flow that needs one is
369
+ authored in the hub's Admin; once it carries one, the hub refuses a seed of
370
+ that flow (`409 slug_owned`) rather than delete the block.
371
+
372
+ ### A spoke's declared environment
373
+
374
+ A spoke scaffold's `pnpm preflight` and CI run `pnpm env:declared`: every
375
+ literal `process.env` read in the site's code needs a row in `project.yaml`'s
376
+ `env_contract` block (`name`, a one-line `description`, `secret: true` or
377
+ `false`, and optionally `scope: web | api | mcp`). Platform names (`NODE_ENV`,
378
+ `CI`, `PORT`, `VERCEL` and `VERCEL_*`, `NEXT_RUNTIME`, `NEXT_PHASE`) need
379
+ none. `NEXT_PUBLIC_AILK_API_URL` is refused on a spoke, read or declared: a
380
+ spoke reaches its hub through the server-only `AILK_API_URL`.
381
+
382
+ create-ailk writes the rows for what it ships — at scaffold time, and again
383
+ whenever `add-module`, `add-route` or `sync-routes --apply` adds code to a
384
+ spoke — and only ever appends: a row you edit is yours. Add a row by hand for
385
+ a variable your own code reads. `env_contract` is its own key; it does not
386
+ change what `required_secrets` means. A hub scaffold carries no block, no
387
+ check and no step.
388
+
265
389
  ## Paid modules require `AILK_LICENSE_KEY`
266
390
 
267
391
  `src/lib/module-tier.ts` splits a requested module set into its free and paid
@@ -5235,8 +5235,7 @@
5235
5235
  }
5236
5236
  },
5237
5237
  "required": [
5238
- "title",
5239
- "description"
5238
+ "title"
5240
5239
  ]
5241
5240
  }
5242
5241
  },
@@ -7378,7 +7377,8 @@
7378
7377
  "type": "string",
7379
7378
  "enum": [
7380
7379
  "default",
7381
- "sm"
7380
+ "sm",
7381
+ "lg"
7382
7382
  ]
7383
7383
  },
7384
7384
  "surface": {
@@ -4,6 +4,13 @@ import { type ModulePatch } from "./module-architecture.js";
4
4
  import { type ModuleName } from "./parse-args.js";
5
5
  import type { MarkerMap, WorkspaceVersionMap } from "./substitute.js";
6
6
  import { type SyncRoutesResult } from "./sync-routes.js";
7
+ import { type Topology } from "./topology.js";
8
+ /**
9
+ * The site's topology for add-module (#7426 D13): an unreadable manifest is a
10
+ * refusal this command names (exit 2), before the first write — the same
11
+ * posture as its unreadable-roster refusal.
12
+ */
13
+ export declare function addModuleSiteTopology(targetDir: string): Topology;
7
14
  export { AddModuleRefusedError, readArchitectureHeader };
8
15
  /**
9
16
  * One report row: a bundle file written, patched or skipped, or a seed the
@@ -32,13 +32,46 @@ import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-cor
32
32
  import { deriveArchitecture } from "./derive-architecture.js";
33
33
  import { LEGACY_NAV_RENDERERS } from "./legacy-nav-renderers.js";
34
34
  import { writeAuthSecretFile } from "./lib/auth-secret-file.js";
35
+ import { appendEnvRows } from "./lib/declared-env.js";
35
36
  import { diffTwoStrings, extractModuleBundle, } from "./lib/extract-module-bundle.js";
36
37
  import { normalizeModules } from "./module-architecture.js";
37
38
  import { PAID_SECTIONS_PATH, writePaidSections } from "./paid-sections.js";
38
39
  import { ParseError, parseModules } from "./parse-args.js";
40
+ import { writeSpokeFlowsReadme } from "./seed-flows.js";
39
41
  import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
42
+ import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
40
43
  import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
41
44
  import { syncRoutes } from "./sync-routes.js";
45
+ import { TopologyError, isSpokeExcluded, readSiteTopology, readSpokeShape, spokeDestination, spokeModuleRefusal, } from "./topology.js";
46
+ /**
47
+ * The site's topology for add-module (#7426 D13): an unreadable manifest is a
48
+ * refusal this command names (exit 2), before the first write — the same
49
+ * posture as its unreadable-roster refusal.
50
+ */
51
+ export function addModuleSiteTopology(targetDir) {
52
+ try {
53
+ return readSiteTopology(targetDir);
54
+ }
55
+ catch (err) {
56
+ if (err instanceof TopologyError)
57
+ throw new AddModuleRefusedError(err.message);
58
+ throw err;
59
+ }
60
+ }
61
+ /**
62
+ * The site's spoke options for add-module (#7894 D1) — `readSpokeShape`, with
63
+ * an unreadable manifest refused the way `addModuleSiteTopology` refuses it.
64
+ */
65
+ function addModuleSpokeShape(targetDir) {
66
+ try {
67
+ return readSpokeShape(targetDir);
68
+ }
69
+ catch (err) {
70
+ if (err instanceof TopologyError)
71
+ throw new AddModuleRefusedError(err.message);
72
+ throw err;
73
+ }
74
+ }
42
75
  export { AddModuleRefusedError, readArchitectureHeader };
43
76
  /** `package.json` paths a `json` patch may write under the plus-config policy. */
44
77
  const PACKAGE_JSON_PATCH_KEYS = new Set([
@@ -86,6 +119,16 @@ export function checkPlusConfigPatches(patches) {
86
119
  export async function applyModuleToScaffold(opts) {
87
120
  const targetAbs = resolve(opts.targetDir);
88
121
  const archPath = join(targetAbs, "architecture.yaml");
122
+ // #7426 D13 — a spoke stays a spoke: a module it does not admit is refused
123
+ // before the first write, and an admitted one lands through the spoke
124
+ // filter. `mcp` follows whether the spoke still carries apps/mcp, and the
125
+ // sign-in mode is the manifest's (#7894 D1).
126
+ const spoke = addModuleSpokeShape(targetAbs);
127
+ if (spoke !== undefined) {
128
+ const refusal = spokeModuleRefusal(opts.moduleName);
129
+ if (refusal !== undefined)
130
+ throw new AddModuleRefusedError(refusal);
131
+ }
89
132
  const current = readArchitectureHeader(readFileSync(archPath, "utf8"));
90
133
  const modules = normalizeModules([...current.modules, opts.moduleName]);
91
134
  // Resolved before the first write: a header surface that is unknown, or
@@ -112,7 +155,29 @@ export async function applyModuleToScaffold(opts) {
112
155
  markers: opts.markers,
113
156
  existingFilePolicy: "skip-existing",
114
157
  checkPatches: checkPlusConfigPatches,
158
+ ...(spoke !== undefined
159
+ ? {
160
+ skip: (destination) => isSpokeExcluded(destination, spoke),
161
+ // #7876 D10 — a module flow file lands in flows/ as declared data.
162
+ remapDestination: spokeDestination,
163
+ }
164
+ : {}),
115
165
  });
166
+ // #7838 D8 — commerce on a spoke brings the spoke's own checkout, stamped
167
+ // from the site directory's name (the `projectName` scaffold() defaults to).
168
+ // A file the site already has is left as it is.
169
+ const spokeCommerce = spoke !== undefined && opts.moduleName === "commerce";
170
+ const checkoutRows = spokeCommerce
171
+ ? writeSpokeCheckoutSlice(targetAbs, {
172
+ site: spokeCheckoutSite(basename(targetAbs)),
173
+ versions: opts.versions,
174
+ })
175
+ : [];
176
+ // #7876 D9 — a spoke that just gained the flow stepper says what its
177
+ // flows/ is and how its rollout seeds it; an edited README is kept.
178
+ if (spoke !== undefined && opts.moduleName === "signal-collectors") {
179
+ writeSpokeFlowsReadme(targetAbs);
180
+ }
116
181
  const routes = syncRoutes({
117
182
  targetDir: targetAbs,
118
183
  templatesDir: opts.templatesDir,
@@ -123,10 +188,12 @@ export async function applyModuleToScaffold(opts) {
123
188
  // #6107 D8 — a module that brings `apps/admin` brings its auth hard-fail;
124
189
  // the Admin app gets the web app's effective secret, and a secret already
125
190
  // held in any Next-loaded env file is never shadowed.
126
- writeAuthSecretFile(targetAbs);
191
+ // A spoke has no sign-in (#7426 D13), so it gets no secret.
192
+ if (spoke === undefined)
193
+ writeAuthSecretFile(targetAbs);
127
194
  // The same composition `syncArchitectureSync` writes — a pure function of
128
195
  // the module set, so an unchanged set rewrites identical bytes.
129
- writeFileSync(archPath, deriveArchitecture(modules, surfaces));
196
+ writeFileSync(archPath, deriveArchitecture(modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
130
197
  // #7536 D3 — the paid-sections registry follows `apps/web/package.json`,
131
198
  // which the bundle's `patches` just edited. Generated and always
132
199
  // overwritten, like `architecture.yaml` above.
@@ -137,7 +204,7 @@ export async function applyModuleToScaffold(opts) {
137
204
  writePaidSections(targetAbs);
138
205
  const registryChanged = existsSync(registryPath) &&
139
206
  readFileSync(registryPath, "utf8") !== registryBefore;
140
- const rows = [...report];
207
+ const rows = [...report, ...checkoutRows];
141
208
  if (registryChanged)
142
209
  rows.push({ path: PAID_SECTIONS_PATH, action: "written" });
143
210
  const navRow = syncNavConfig(targetAbs, current.surfaces, surfaces);
@@ -191,6 +258,16 @@ export async function applyModuleToScaffold(opts) {
191
258
  templatesDir: opts.templatesDir,
192
259
  });
193
260
  }
261
+ // #7874 D9 — a spoke declares what the bundle reads, as the last write. A
262
+ // malformed env_contract block throws EnvContractError naming its line;
263
+ // the files written above stay, and `pnpm env:declared` names the same fault.
264
+ // A commerce spoke also declares what the checkout package reads (#7838 D9).
265
+ if (spoke !== undefined) {
266
+ appendEnvRows(targetAbs, {
267
+ topology: "spoke",
268
+ ...spokeEnvRowOptions(spokeCommerce),
269
+ });
270
+ }
194
271
  return { targetDir: targetAbs, modules, report: rows, routes, roster };
195
272
  }
196
273
  /**
package/dist/add-route.js CHANGED
@@ -33,12 +33,15 @@ import { parseArgs } from "node:util";
33
33
  import { AddModuleRefusedError, readArchitectureHeader, syncNavConfig, } from "./add-module.js";
34
34
  import { isTextFile } from "./copy.js";
35
35
  import { deriveArchitecture } from "./derive-architecture.js";
36
+ import { appendEnvRows } from "./lib/declared-env.js";
37
+ import { ENV_CATALOG } from "./lib/env-catalog.js";
36
38
  import { diffTwoStrings, } from "./lib/extract-module-bundle.js";
37
39
  import { ParseError } from "./parse-args.js";
38
40
  import { substituteMarkers } from "./substitute.js";
39
41
  import { GATEABLE_SURFACES, MODULE_SURFACES, parseSurfaces, resolveSurfaces, } from "./surfaces.js";
40
42
  import { syncRoutes } from "./sync-routes.js";
41
43
  import { BASE_LAYER_DIR } from "./template-manifest.js";
44
+ import { TopologyError, readSiteTopology } from "./topology.js";
42
45
  /** A refusal the CLI exits 2 on, thrown before anything is written. */
43
46
  export class AddRouteRefusedError extends Error {
44
47
  exitCode = 2;
@@ -64,6 +67,20 @@ export function addRouteToScaffold(opts) {
64
67
  if (!existsSync(archPath)) {
65
68
  throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
66
69
  }
70
+ // #7426 D13 — a spoke stays a spoke. Read before the first write: an
71
+ // unreadable manifest is a refusal this command names (exit 2).
72
+ let spoke;
73
+ try {
74
+ spoke =
75
+ readSiteTopology(targetAbs) === "spoke"
76
+ ? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
77
+ : undefined;
78
+ }
79
+ catch (err) {
80
+ if (err instanceof TopologyError)
81
+ throw new AddRouteRefusedError(err.message);
82
+ throw err;
83
+ }
67
84
  const route = canonicalRoute(opts.route);
68
85
  const owner = Object.entries(MODULE_SURFACES).find(([, surfaces]) => surfaces.includes(route))?.[0];
69
86
  let current;
@@ -106,8 +123,15 @@ export function addRouteToScaffold(opts) {
106
123
  if (navRow !== undefined)
107
124
  report.push(navRow);
108
125
  // Last: the declared header is what makes a rerun a no-op, so it is
109
- // written only once every other step has landed.
110
- writeFileSync(archPath, deriveArchitecture(current.modules, surfaces));
126
+ // written only once every other step has landed. A spoke's `syncRoutes`
127
+ // call above already skipped what it lacks (#7426 D13).
128
+ writeFileSync(archPath, deriveArchitecture(current.modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
129
+ // #7874 D9 — a spoke declares what the route reads. A malformed
130
+ // env_contract block throws EnvContractError naming its line; the files
131
+ // written above stay, and `pnpm env:declared` names the same fault.
132
+ if (spoke !== undefined) {
133
+ appendEnvRows(targetAbs, { topology: "spoke", catalog: ENV_CATALOG });
134
+ }
111
135
  return { targetDir: targetAbs, route, surfaces, report, routes };
112
136
  }
113
137
  /** The canonical id for one route argument, refusing an unknown one. */
@@ -0,0 +1,74 @@
1
+ import { type Topology } from "./topology.js";
2
+ /** A refusal the CLI exits 2 on, thrown before anything is written. */
3
+ export declare class AdoptRefusedError extends Error {
4
+ readonly exitCode = 2;
5
+ constructor(message: string);
6
+ }
7
+ export interface AdoptOptions {
8
+ /** The existing site. */
9
+ targetDir: string;
10
+ /** The package's `templates/` dir, for the manifest writer. */
11
+ templatesDir: string;
12
+ /** The package's `site-templates/` dir, for the manifest writer. */
13
+ siteTemplatesDir: string;
14
+ /** `--template` — required, and only allowed, when the site has no manifest. */
15
+ template?: string | undefined;
16
+ /** `--modules`, comma-separated — only allowed when the site has no manifest. */
17
+ modules?: string | undefined;
18
+ }
19
+ /** A generated seam file's state against the render for the derived topology. */
20
+ export interface AdoptSeam {
21
+ path: string;
22
+ state: "present" | "differs" | "absent";
23
+ }
24
+ export interface AdoptResult {
25
+ /** Resolved absolute path to the site. */
26
+ targetDir: string;
27
+ /** The site-relative manifest written. */
28
+ path: string;
29
+ topology: Topology;
30
+ patternVersion: string;
31
+ /** Whether the manifest was created (no manifest before) or gained the two fields. */
32
+ created: boolean;
33
+ /** One per seam file whose app directory exists (D8) — information only. */
34
+ seams: AdoptSeam[];
35
+ /** One per recorded module a spoke cannot carry (D6) — never a refusal. */
36
+ warnings: string[];
37
+ /** What adopt did to a spoke's `project.yaml` `env_contract` (#7874 D9). */
38
+ env: AdoptEnvResult;
39
+ }
40
+ /** What adopt did to `project.yaml` (#7874 D9): only a spoke's is written. */
41
+ export type AdoptEnvResult = {
42
+ state: "written";
43
+ added: string[];
44
+ } | {
45
+ state: "unchanged";
46
+ } | {
47
+ state: "no-project-yaml";
48
+ } | {
49
+ state: "hub";
50
+ };
51
+ /**
52
+ * Adopt the site at `opts.targetDir`: write the manifest recording its
53
+ * topology and pattern version. Every refusal throws before the one write.
54
+ *
55
+ * @throws AdoptRefusedError (exit 2) on a refusal this command names, and
56
+ * the `AddModuleRefusedError` (exit 2) the roster readers and manifest
57
+ * writer throw on a manifest they cannot read or extend.
58
+ */
59
+ export declare function adoptSite(opts: AdoptOptions): AdoptResult;
60
+ /** The printed report: the path written, the record, the seams, the warnings. */
61
+ export declare function formatAdoptReport(result: AdoptResult): string;
62
+ export interface AdoptArgs {
63
+ /** Positional target directory; undefined = the caller's cwd. */
64
+ target: string | undefined;
65
+ template: string | undefined;
66
+ modules: string | undefined;
67
+ help: boolean;
68
+ }
69
+ export declare const ADOPT_HELP = "create-ailk adopt \u2014 record what an existing site is, once.\n\nUsage:\n create-ailk adopt [<target-dir>] [--template <name>] [--modules <a,b>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --template <name> The template the site was built from. Required\n when the site has no .ailk-site-manifest.json;\n refused when it has one.\n --modules <a,b> The site's module set, when it has no manifest.\n Overrides architecture.yaml's \"# Modules:\" header.\n --help, -h Print this message.\n\nWrites \"topology\" and \"patternVersion\" into .ailk-site-manifest.json. On a\nspoke it also appends an env_contract row to project.yaml for every\nenvironment variable the site's code reads that has none, so pnpm\nenv:declared has a contract to check; it never edits or removes a row, and\nit never touches a hub's project.yaml. Nothing else is written. The\ntopology is read from the tree: no apps/api and no database/ is a spoke,\nboth is a hub, one without the other is refused.\npatternVersion is this create-ailk's version. A site with no manifest gets\none, built the way a scaffold builds it. A manifest that already records\neither field is refused: adopt runs once. A recorded module a spoke cannot\ncarry is a warning, not a refusal.\n";
70
+ /**
71
+ * Parse the argv AFTER the `adopt` word. Strict `node:util` parseArgs, like
72
+ * `parseAddRouteArgs`; the values themselves are validated by `adoptSite`.
73
+ */
74
+ export declare function parseAdoptArgs(argv: string[]): AdoptArgs;