create-ailk 0.15.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.
- package/README.md +124 -0
- package/dist/add-module.js +52 -7
- package/dist/add-route.js +8 -0
- package/dist/adopt.d.ts +74 -0
- package/dist/adopt.js +314 -0
- package/dist/cli.js +82 -2
- package/dist/derive-architecture.d.ts +22 -0
- package/dist/derive-architecture.js +58 -1
- package/dist/lib/declared-env.d.ts +142 -0
- package/dist/lib/declared-env.js +985 -0
- package/dist/lib/env-catalog.d.ts +17 -0
- package/dist/lib/env-catalog.js +234 -0
- package/dist/lib/extract-module-bundle.d.ts +9 -0
- package/dist/lib/extract-module-bundle.js +18 -1
- package/dist/lib/module-tier.d.ts +2 -0
- package/dist/lib/module-tier.js +4 -0
- package/dist/module-architecture.js +76 -0
- package/dist/parse-args.d.ts +5 -0
- package/dist/parse-args.js +17 -1
- package/dist/programmatic.d.ts +8 -0
- package/dist/programmatic.js +78 -9
- package/dist/seed-flows.d.ts +65 -0
- package/dist/seed-flows.js +350 -0
- package/dist/site-manifest.d.ts +43 -0
- package/dist/site-manifest.js +98 -3
- package/dist/site-roster.d.ts +13 -0
- package/dist/site-roster.js +22 -8
- package/dist/spoke-checkout.d.ts +42 -0
- package/dist/spoke-checkout.js +359 -0
- package/dist/surfaces.js +7 -0
- package/dist/sync-routes.js +23 -8
- package/dist/topology.d.ts +124 -9
- package/dist/topology.js +449 -40
- package/package.json +3 -3
- package/templates/.env.example +10 -0
- package/templates/apps/api/CLAUDE.md +19 -4
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
- package/templates/apps/api/src/bin/spokes.ts +62 -1
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +89 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
- package/templates/apps/api/src/lib/spoke-store.ts +25 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
- package/templates/apps/api/src/middleware/auth.ts +126 -0
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +7 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +72 -1
- package/templates/apps/api/src/server.ts +105 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +293 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
- package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
- package/templates/apps/api/src/services/lead-routing.ts +131 -2
- package/templates/apps/api/src/services/person.ts +541 -0
- package/templates/apps/web/.env.example +58 -5
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
- package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
- package/templates/apps/web/app/[locale]/layout.tsx +28 -15
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
- package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
- package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +15 -0
- package/templates/apps/web/app/openapi.json/route.ts +3 -6
- package/templates/apps/web/auth.ts +29 -2
- package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
- package/templates/apps/web/components/signed-in-nav.tsx +17 -3
- package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
- package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
- package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
- package/templates/apps/web/lib/__tests__/relay.test.ts +27 -0
- package/templates/apps/web/lib/account-config.ts +130 -1
- package/templates/apps/web/lib/api-origin.generated.ts +11 -0
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
- package/templates/apps/web/lib/hub-session/config.ts +104 -0
- package/templates/apps/web/lib/hub-session/index.ts +23 -0
- package/templates/apps/web/lib/hub-session/seal.ts +82 -0
- package/templates/apps/web/lib/hub-session/session.ts +180 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
- package/templates/apps/web/lib/listings-config.ts +31 -0
- package/templates/apps/web/lib/listings.tsx +7 -3
- package/templates/apps/web/lib/meeting-slugs.ts +11 -1
- package/templates/apps/web/lib/next-path.ts +35 -0
- package/templates/apps/web/lib/relay.ts +3 -1
- package/templates/apps/web/lib/server-api.ts +11 -1
- package/templates/apps/web/lib/topology.generated.ts +15 -5
- package/templates/apps/web/messages/en.json +32 -3
- package/templates/database/CHANGELOG.md +26 -0
- package/templates/database/__tests__/agents.db.test.ts +25 -5
- package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
- package/templates/database/__tests__/member-area.db.test.ts +245 -0
- package/templates/database/__tests__/person.db.test.ts +584 -0
- package/templates/database/auth/schema.prisma +3 -2
- package/templates/database/config/schema.prisma +13 -0
- package/templates/database/inbox/schema.prisma +98 -0
- package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
- package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/person-backfill.sql +132 -0
- package/templates/database/tenancy.ts +53 -0
- package/templates/tools/check-declared-env.ts +37 -0
- package/templates/tools/declared-env.ts +1163 -0
- package/versions.json +35 -35
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
|
package/dist/add-module.js
CHANGED
|
@@ -32,14 +32,17 @@ 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";
|
|
42
|
-
import { TopologyError, isSpokeExcluded, readSiteTopology, spokeModuleRefusal, } from "./topology.js";
|
|
45
|
+
import { TopologyError, isSpokeExcluded, readSiteTopology, readSpokeShape, spokeDestination, spokeModuleRefusal, } from "./topology.js";
|
|
43
46
|
/**
|
|
44
47
|
* The site's topology for add-module (#7426 D13): an unreadable manifest is a
|
|
45
48
|
* refusal this command names (exit 2), before the first write — the same
|
|
@@ -55,6 +58,20 @@ export function addModuleSiteTopology(targetDir) {
|
|
|
55
58
|
throw err;
|
|
56
59
|
}
|
|
57
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
|
+
}
|
|
58
75
|
export { AddModuleRefusedError, readArchitectureHeader };
|
|
59
76
|
/** `package.json` paths a `json` patch may write under the plus-config policy. */
|
|
60
77
|
const PACKAGE_JSON_PATCH_KEYS = new Set([
|
|
@@ -104,10 +121,9 @@ export async function applyModuleToScaffold(opts) {
|
|
|
104
121
|
const archPath = join(targetAbs, "architecture.yaml");
|
|
105
122
|
// #7426 D13 — a spoke stays a spoke: a module it does not admit is refused
|
|
106
123
|
// before the first write, and an admitted one lands through the spoke
|
|
107
|
-
// filter. `mcp` follows whether the spoke still carries apps/mcp
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
: undefined;
|
|
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);
|
|
111
127
|
if (spoke !== undefined) {
|
|
112
128
|
const refusal = spokeModuleRefusal(opts.moduleName);
|
|
113
129
|
if (refusal !== undefined)
|
|
@@ -140,9 +156,28 @@ export async function applyModuleToScaffold(opts) {
|
|
|
140
156
|
existingFilePolicy: "skip-existing",
|
|
141
157
|
checkPatches: checkPlusConfigPatches,
|
|
142
158
|
...(spoke !== undefined
|
|
143
|
-
? {
|
|
159
|
+
? {
|
|
160
|
+
skip: (destination) => isSpokeExcluded(destination, spoke),
|
|
161
|
+
// #7876 D10 — a module flow file lands in flows/ as declared data.
|
|
162
|
+
remapDestination: spokeDestination,
|
|
163
|
+
}
|
|
144
164
|
: {}),
|
|
145
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
|
+
}
|
|
146
181
|
const routes = syncRoutes({
|
|
147
182
|
targetDir: targetAbs,
|
|
148
183
|
templatesDir: opts.templatesDir,
|
|
@@ -169,7 +204,7 @@ export async function applyModuleToScaffold(opts) {
|
|
|
169
204
|
writePaidSections(targetAbs);
|
|
170
205
|
const registryChanged = existsSync(registryPath) &&
|
|
171
206
|
readFileSync(registryPath, "utf8") !== registryBefore;
|
|
172
|
-
const rows = [...report];
|
|
207
|
+
const rows = [...report, ...checkoutRows];
|
|
173
208
|
if (registryChanged)
|
|
174
209
|
rows.push({ path: PAID_SECTIONS_PATH, action: "written" });
|
|
175
210
|
const navRow = syncNavConfig(targetAbs, current.surfaces, surfaces);
|
|
@@ -223,6 +258,16 @@ export async function applyModuleToScaffold(opts) {
|
|
|
223
258
|
templatesDir: opts.templatesDir,
|
|
224
259
|
});
|
|
225
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
|
+
}
|
|
226
271
|
return { targetDir: targetAbs, modules, report: rows, routes, roster };
|
|
227
272
|
}
|
|
228
273
|
/**
|
package/dist/add-route.js
CHANGED
|
@@ -33,6 +33,8 @@ 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";
|
|
@@ -124,6 +126,12 @@ export function addRouteToScaffold(opts) {
|
|
|
124
126
|
// written only once every other step has landed. A spoke's `syncRoutes`
|
|
125
127
|
// call above already skipped what it lacks (#7426 D13).
|
|
126
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
|
+
}
|
|
127
135
|
return { targetDir: targetAbs, route, surfaces, report, routes };
|
|
128
136
|
}
|
|
129
137
|
/** The canonical id for one route argument, refusing an unknown one. */
|
package/dist/adopt.d.ts
ADDED
|
@@ -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;
|
package/dist/adopt.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create-ailk adopt [<target-dir>]` — record, once, what an existing site
|
|
3
|
+
* is (#7875).
|
|
4
|
+
*
|
|
5
|
+
* A site scaffolded before #7426, or not scaffolded by create-ailk at all,
|
|
6
|
+
* has no `topology` in its `.ailk-site-manifest.json`, so every create-ailk
|
|
7
|
+
* command that asks `readSiteTopology` treats it as a hub — wrong for a site
|
|
8
|
+
* trimmed to a spoke by hand. Adopt writes the two fields the keystone added
|
|
9
|
+
* (D10 of spec 7426), and nothing else:
|
|
10
|
+
*
|
|
11
|
+
* - `topology` is derived from the tree against `SPOKE_REMOVED_ROOTS`
|
|
12
|
+
* (D1) — all removed roots absent is a spoke, all present a hub, a mix
|
|
13
|
+
* is refused. Never a flag: the tree is the evidence;
|
|
14
|
+
* - `patternVersion` is the running create-ailk version, the value a
|
|
15
|
+
* scaffold records (D8 — on an adopted site, the version it was adopted
|
|
16
|
+
* against);
|
|
17
|
+
* - an existing manifest gains the two fields, every other byte kept
|
|
18
|
+
* (`planTopologyRecord`, D3); a site with none gets one built by the
|
|
19
|
+
* scaffold's own writer from `--template` and the module set
|
|
20
|
+
* (`planAdoptedSiteManifest`, D4/D5);
|
|
21
|
+
* - a manifest already recording either field is refused (D2): adopt runs
|
|
22
|
+
* once. A recorded module a spoke cannot carry is a warning, never a
|
|
23
|
+
* refusal (D6) — `add-module` is the admission gate, not adopt.
|
|
24
|
+
*
|
|
25
|
+
* It writes the manifest — never a seam file, `architecture.yaml`, anything
|
|
26
|
+
* under `apps/api`, and never another subcommand (`sync-routes --apply`
|
|
27
|
+
* included). The one exception is a spoke's `project.yaml` (#7874 D9): on a
|
|
28
|
+
* site it derives as a spoke, adopt also appends an `env_contract` row for
|
|
29
|
+
* every env var the site's code reads that has none, so its `pnpm
|
|
30
|
+
* env:declared` check has a contract to read. A hub's `project.yaml` is never
|
|
31
|
+
* touched. Every refusal throws before any write.
|
|
32
|
+
*
|
|
33
|
+
* Pure module: no `import.meta.url`, so jest can import it directly — the
|
|
34
|
+
* package-rooted directories are supplied by the CLI, the `add-route.ts`
|
|
35
|
+
* split. Spec: docs/specs/7875-create-ailk-adopt-spec.md.
|
|
36
|
+
*/
|
|
37
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { basename, join, resolve } from "node:path";
|
|
39
|
+
import { parseArgs } from "node:util";
|
|
40
|
+
import { readArchitectureHeader } from "./add-module-core.js";
|
|
41
|
+
import { buildScaffoldMarkers } from "./copy.js";
|
|
42
|
+
import { EnvContractError, planEnvRows } from "./lib/declared-env.js";
|
|
43
|
+
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
44
|
+
import { normalizeModules } from "./module-architecture.js";
|
|
45
|
+
import { ParseError, RECOGNIZED_MODULES, } from "./parse-args.js";
|
|
46
|
+
import { planAdoptedSiteManifest, planTopologyRecord, } from "./site-manifest.js";
|
|
47
|
+
import { LEGACY_SITE_ROSTER_PATH, SITE_ROSTER_PATH, readSiteRoster, resolveRosterTemplate, } from "./site-roster.js";
|
|
48
|
+
import { SHAPE_STARTERS } from "./template-manifest.js";
|
|
49
|
+
import { MCP_SEAM_PATH, SPOKE_REMOVED_ROOTS, WEB_SEAM_PATH, renderMcpSeam, renderWebSeam, spokeModuleRefusal, } from "./topology.js";
|
|
50
|
+
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
51
|
+
export class AdoptRefusedError extends Error {
|
|
52
|
+
exitCode = 2;
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "AdoptRefusedError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The generated seam files, by the app directory each lives in, with their renders. */
|
|
59
|
+
const SEAMS = [
|
|
60
|
+
{ app: "apps/web", path: WEB_SEAM_PATH, render: renderWebSeam },
|
|
61
|
+
{ app: "apps/mcp", path: MCP_SEAM_PATH, render: renderMcpSeam },
|
|
62
|
+
];
|
|
63
|
+
function isDirectory(path) {
|
|
64
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* D1 — the topology the tree shows: every `SPOKE_REMOVED_ROOTS` entry absent
|
|
68
|
+
* is a spoke, every one present a hub. Anything else is refused.
|
|
69
|
+
*/
|
|
70
|
+
function deriveTopology(targetDir) {
|
|
71
|
+
const present = SPOKE_REMOVED_ROOTS.filter((root) => isDirectory(join(targetDir, root)));
|
|
72
|
+
if (present.length === 0)
|
|
73
|
+
return "spoke";
|
|
74
|
+
if (present.length === SPOKE_REMOVED_ROOTS.length)
|
|
75
|
+
return "hub";
|
|
76
|
+
const absent = SPOKE_REMOVED_ROOTS.filter((root) => !present.includes(root));
|
|
77
|
+
throw new AdoptRefusedError(`adopt: ${targetDir} has ${present.join(", ")} but not ${absent.join(", ")}, so it is neither a hub nor a spoke`);
|
|
78
|
+
}
|
|
79
|
+
/** D5 — the module set for a site with no manifest: `--modules`, else the header. */
|
|
80
|
+
function moduleSet(targetDir, flag) {
|
|
81
|
+
if (flag !== undefined) {
|
|
82
|
+
try {
|
|
83
|
+
return normalizeModules(flag.split(","));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new AdoptRefusedError(`--modules is not a module list: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const archPath = join(targetDir, "architecture.yaml");
|
|
90
|
+
if (!existsSync(archPath)) {
|
|
91
|
+
throw new AdoptRefusedError(`${targetDir} has no architecture.yaml to read its module set from; name it with --modules <a,b>`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return readArchitectureHeader(readFileSync(archPath, "utf8")).modules;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw new AdoptRefusedError(`${err.message.replace(/\.$/, "")}. Name the module set with --modules <a,b> instead.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** The recorded module names this package recognizes; anything else is ignored. */
|
|
101
|
+
function recordedModules(value) {
|
|
102
|
+
if (!Array.isArray(value))
|
|
103
|
+
return [];
|
|
104
|
+
return value.filter((m) => RECOGNIZED_MODULES.includes(m));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Adopt the site at `opts.targetDir`: write the manifest recording its
|
|
108
|
+
* topology and pattern version. Every refusal throws before the one write.
|
|
109
|
+
*
|
|
110
|
+
* @throws AdoptRefusedError (exit 2) on a refusal this command names, and
|
|
111
|
+
* the `AddModuleRefusedError` (exit 2) the roster readers and manifest
|
|
112
|
+
* writer throw on a manifest they cannot read or extend.
|
|
113
|
+
*/
|
|
114
|
+
export function adoptSite(opts) {
|
|
115
|
+
const targetDir = resolve(opts.targetDir);
|
|
116
|
+
if (!isDirectory(targetDir)) {
|
|
117
|
+
throw new Error(`adopt target is not a directory: ${targetDir}. Point adopt at an existing site.`);
|
|
118
|
+
}
|
|
119
|
+
const topology = deriveTopology(targetDir);
|
|
120
|
+
const markers = buildScaffoldMarkers(basename(targetDir));
|
|
121
|
+
const patternVersion = markers["CREATE_AILK_VERSION"];
|
|
122
|
+
if (patternVersion === undefined || patternVersion === "") {
|
|
123
|
+
// A once-only field: never record an empty version a second run cannot fix.
|
|
124
|
+
throw new Error("adopt: this create-ailk has no version to record");
|
|
125
|
+
}
|
|
126
|
+
const record = { topology, patternVersion };
|
|
127
|
+
const roster = readSiteRoster(targetDir);
|
|
128
|
+
let text;
|
|
129
|
+
let modules;
|
|
130
|
+
if (roster !== undefined) {
|
|
131
|
+
if (roster.path === LEGACY_SITE_ROSTER_PATH) {
|
|
132
|
+
throw new AdoptRefusedError(`${LEGACY_SITE_ROSTER_PATH} is the pre-#6658 roster name; rename it to ${SITE_ROSTER_PATH}, then re-run adopt.`);
|
|
133
|
+
}
|
|
134
|
+
for (const key of ["topology", "patternVersion"]) {
|
|
135
|
+
if (key in roster.doc) {
|
|
136
|
+
throw new AdoptRefusedError(`${roster.path} already records "${key}", so this site is already adopted. To move it to a new create-ailk version, see the README § Keeping a spoke current.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const [flag, value] of [
|
|
140
|
+
["--template", opts.template],
|
|
141
|
+
["--modules", opts.modules],
|
|
142
|
+
]) {
|
|
143
|
+
if (value !== undefined) {
|
|
144
|
+
throw new AdoptRefusedError(`${roster.path} already records the site's template and modules; ${flag} would contradict it. Re-run adopt without ${flag}.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
text = planTopologyRecord(roster.text, record, roster.path);
|
|
148
|
+
modules = recordedModules(roster.doc["modules"]);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (opts.template === undefined) {
|
|
152
|
+
throw new AdoptRefusedError(`${targetDir} has no ${SITE_ROSTER_PATH}; name the template it was built from with --template <name> (one of: ${Object.keys(SHAPE_STARTERS).join(", ")}).`);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
resolveRosterTemplate(opts.template);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
throw new AdoptRefusedError(`--template "${opts.template}" names no declared template: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
modules = moduleSet(targetDir, opts.modules);
|
|
161
|
+
text = planAdoptedSiteManifest({
|
|
162
|
+
targetDir,
|
|
163
|
+
template: opts.template,
|
|
164
|
+
modules,
|
|
165
|
+
...record,
|
|
166
|
+
siteTemplatesDir: opts.siteTemplatesDir,
|
|
167
|
+
templatesDir: opts.templatesDir,
|
|
168
|
+
markers,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const warnings = topology === "spoke"
|
|
172
|
+
? modules.flatMap((m) => spokeModuleRefusal(m) ?? [])
|
|
173
|
+
: [];
|
|
174
|
+
const seams = SEAMS.filter(({ app }) => isDirectory(join(targetDir, app))).map(({ path, render }) => {
|
|
175
|
+
const abs = join(targetDir, path);
|
|
176
|
+
if (!existsSync(abs))
|
|
177
|
+
return { path, state: "absent" };
|
|
178
|
+
return {
|
|
179
|
+
path,
|
|
180
|
+
state: readFileSync(abs, "utf8") === render(topology) ? "present" : "differs",
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
// #7874 D9 — a spoke's env_contract rows, planned before any write: a
|
|
184
|
+
// block adopt cannot read is a refusal, and nothing is written.
|
|
185
|
+
const yamlPath = join(targetDir, "project.yaml");
|
|
186
|
+
let env = { state: "hub" };
|
|
187
|
+
let yamlNext;
|
|
188
|
+
if (topology === "spoke") {
|
|
189
|
+
if (!existsSync(yamlPath)) {
|
|
190
|
+
env = { state: "no-project-yaml" };
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const yamlText = readFileSync(yamlPath, "utf8");
|
|
194
|
+
try {
|
|
195
|
+
const plan = planEnvRows(targetDir, yamlText, {
|
|
196
|
+
topology,
|
|
197
|
+
catalog: ENV_CATALOG,
|
|
198
|
+
});
|
|
199
|
+
if (plan.text === yamlText) {
|
|
200
|
+
env = { state: "unchanged" };
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
env = { state: "written", added: plan.added };
|
|
204
|
+
yamlNext = plan.text;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (err instanceof EnvContractError) {
|
|
209
|
+
throw new AdoptRefusedError(`adopt: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
writeFileSync(join(targetDir, SITE_ROSTER_PATH), text);
|
|
216
|
+
if (yamlNext !== undefined)
|
|
217
|
+
writeFileSync(yamlPath, yamlNext);
|
|
218
|
+
return {
|
|
219
|
+
targetDir,
|
|
220
|
+
path: SITE_ROSTER_PATH,
|
|
221
|
+
topology,
|
|
222
|
+
patternVersion,
|
|
223
|
+
created: roster === undefined,
|
|
224
|
+
seams,
|
|
225
|
+
warnings,
|
|
226
|
+
env,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** The printed report: the path written, the record, the seams, the warnings. */
|
|
230
|
+
export function formatAdoptReport(result) {
|
|
231
|
+
const lines = [
|
|
232
|
+
`[create-ailk] adopt → ${result.targetDir}`,
|
|
233
|
+
`${result.created ? "Wrote" : "Updated"} ${result.path}`,
|
|
234
|
+
` topology: ${result.topology}`,
|
|
235
|
+
` patternVersion: ${result.patternVersion}`,
|
|
236
|
+
];
|
|
237
|
+
if (result.seams.length > 0) {
|
|
238
|
+
lines.push(`Seam files (against the ${result.topology} render):`);
|
|
239
|
+
const width = Math.max(...result.seams.map((s) => s.path.length));
|
|
240
|
+
lines.push(...result.seams.map((s) => ` ${s.path.padEnd(width)} ${s.state}`));
|
|
241
|
+
}
|
|
242
|
+
const env = formatAdoptEnv(result.env);
|
|
243
|
+
if (env !== undefined)
|
|
244
|
+
lines.push(env);
|
|
245
|
+
lines.push(...result.warnings.map((w) => `warning: ${w}`));
|
|
246
|
+
return lines.join("\n") + "\n";
|
|
247
|
+
}
|
|
248
|
+
/** The report's one env_contract line (#7874 D9); none on a hub. */
|
|
249
|
+
function formatAdoptEnv(env) {
|
|
250
|
+
switch (env.state) {
|
|
251
|
+
case "written":
|
|
252
|
+
return `env_contract: ${env.added.length} rows added (${env.added.join(", ")})`;
|
|
253
|
+
case "unchanged":
|
|
254
|
+
return "env_contract: every read already declared";
|
|
255
|
+
case "no-project-yaml":
|
|
256
|
+
return "env_contract: no project.yaml, nothing written";
|
|
257
|
+
case "hub":
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export const ADOPT_HELP = `create-ailk adopt — record what an existing site is, once.
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
create-ailk adopt [<target-dir>] [--template <name>] [--modules <a,b>]
|
|
265
|
+
|
|
266
|
+
Arguments:
|
|
267
|
+
<target-dir> The existing site. Defaults to the current dir.
|
|
268
|
+
|
|
269
|
+
Options:
|
|
270
|
+
--template <name> The template the site was built from. Required
|
|
271
|
+
when the site has no .ailk-site-manifest.json;
|
|
272
|
+
refused when it has one.
|
|
273
|
+
--modules <a,b> The site's module set, when it has no manifest.
|
|
274
|
+
Overrides architecture.yaml's "# Modules:" header.
|
|
275
|
+
--help, -h Print this message.
|
|
276
|
+
|
|
277
|
+
Writes "topology" and "patternVersion" into .ailk-site-manifest.json. On a
|
|
278
|
+
spoke it also appends an env_contract row to project.yaml for every
|
|
279
|
+
environment variable the site's code reads that has none, so pnpm
|
|
280
|
+
env:declared has a contract to check; it never edits or removes a row, and
|
|
281
|
+
it never touches a hub's project.yaml. Nothing else is written. The
|
|
282
|
+
topology is read from the tree: no apps/api and no database/ is a spoke,
|
|
283
|
+
both is a hub, one without the other is refused.
|
|
284
|
+
patternVersion is this create-ailk's version. A site with no manifest gets
|
|
285
|
+
one, built the way a scaffold builds it. A manifest that already records
|
|
286
|
+
either field is refused: adopt runs once. A recorded module a spoke cannot
|
|
287
|
+
carry is a warning, not a refusal.
|
|
288
|
+
`;
|
|
289
|
+
/**
|
|
290
|
+
* Parse the argv AFTER the `adopt` word. Strict `node:util` parseArgs, like
|
|
291
|
+
* `parseAddRouteArgs`; the values themselves are validated by `adoptSite`.
|
|
292
|
+
*/
|
|
293
|
+
export function parseAdoptArgs(argv) {
|
|
294
|
+
const parsed = parseArgs({
|
|
295
|
+
args: argv,
|
|
296
|
+
strict: true,
|
|
297
|
+
allowPositionals: true,
|
|
298
|
+
options: {
|
|
299
|
+
template: { type: "string" },
|
|
300
|
+
modules: { type: "string" },
|
|
301
|
+
help: { type: "boolean", default: false, short: "h" },
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
const [target, ...rest] = parsed.positionals;
|
|
305
|
+
if (rest.length > 0) {
|
|
306
|
+
throw new ParseError(`adopt takes at most one target directory, got ${parsed.positionals.length} arguments`);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
target,
|
|
310
|
+
template: parsed.values.template,
|
|
311
|
+
modules: parsed.values.modules,
|
|
312
|
+
help: parsed.values.help === true,
|
|
313
|
+
};
|
|
314
|
+
}
|