create-ailk 0.3.0 → 0.4.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/dist/cli.js +19 -9
- package/dist/lib/extract-module-bundle.d.ts +39 -0
- package/dist/lib/extract-module-bundle.js +165 -78
- package/dist/lib/fetch-template.d.ts +20 -0
- package/dist/lib/fetch-template.js +129 -0
- package/dist/lib/module-license-gate.d.ts +8 -0
- package/dist/lib/module-license-gate.js +12 -4
- package/dist/module-architecture.d.ts +58 -0
- package/dist/module-architecture.js +156 -4
- package/dist/parse-args.d.ts +8 -1
- package/dist/parse-args.js +43 -0
- package/dist/programmatic.d.ts +26 -1
- package/dist/programmatic.js +100 -4
- package/dist/surfaces.js +42 -0
- package/dist/template-manifest.d.ts +92 -0
- package/dist/template-manifest.js +168 -0
- package/dist/template-names.d.ts +41 -0
- package/dist/template-names.js +44 -0
- package/package.json +2 -1
- package/site-templates/marketing-site-free/ailk-template.json +8 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
- package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
- package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
- package/site-templates/marketing-site-free/provenance.json +4 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
- package/templates/apps/api/src/config/modules.ts +1 -1
- package/templates/apps/api/src/server.ts +46 -3
- package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
- package/templates/apps/web/jest.config.cjs +5 -0
- package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
- package/templates/apps/web/lib/server-api.ts +28 -12
- package/templates/apps/web/messages/en.json +58 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +1 -1
|
@@ -29,6 +29,39 @@ export interface ArchNamespaces {
|
|
|
29
29
|
members: Record<string, ArchNamespaceMember>;
|
|
30
30
|
rules: string[];
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* One page a site template's roster declares (spec 5478 § D1).
|
|
34
|
+
*
|
|
35
|
+
* Both producers emit this same row type: a template's BASE roster
|
|
36
|
+
* (`TemplateManifest.roster`, template-manifest.ts) and a module's
|
|
37
|
+
* `pageContributions` below. One vocabulary, so the merge has exactly one
|
|
38
|
+
* collision key to reconcile rather than two (§ D1/D2).
|
|
39
|
+
*
|
|
40
|
+
* Declared HERE rather than in `template-manifest.ts` (§ D3) because
|
|
41
|
+
* `ModuleSlice` carries it and `template-manifest.ts` already imports
|
|
42
|
+
* `MODULE_SLICES` + `normalizeModules` from this file — so the dependency
|
|
43
|
+
* stays a DAG (`parse-args` ← `module-architecture` ← `template-manifest`)
|
|
44
|
+
* instead of becoming a cycle in the value graph.
|
|
45
|
+
*
|
|
46
|
+
* A row carries no nav label: `GATEABLE_SURFACES` already carries
|
|
47
|
+
* `navLabelKey?` per surface (`surfaces.ts`) and the nav is derived from the
|
|
48
|
+
* roster (parent spec 5462 § D9), so a label here would be a second authority
|
|
49
|
+
* for one fact.
|
|
50
|
+
*/
|
|
51
|
+
export interface PageRosterRow {
|
|
52
|
+
/**
|
|
53
|
+
* Site-relative route path, e.g. `/blog` or `/flows/[slug]/start`. This is
|
|
54
|
+
* the row's IDENTITY and the merge's collision key (§ D2): a path claimed by
|
|
55
|
+
* two sources is refused at build time, never resolved by a precedence rule
|
|
56
|
+
* (parent § D8).
|
|
57
|
+
*/
|
|
58
|
+
route: string;
|
|
59
|
+
/**
|
|
60
|
+
* Repo-relative seed content this row renders. Absent for an API-backed row
|
|
61
|
+
* (`/schedule` renders from the scheduling API, not from an MDX file).
|
|
62
|
+
*/
|
|
63
|
+
content?: readonly string[];
|
|
64
|
+
}
|
|
32
65
|
/**
|
|
33
66
|
* The architecture fragment one module contributes.
|
|
34
67
|
*
|
|
@@ -104,6 +137,31 @@ export interface ModuleSlice {
|
|
|
104
137
|
* which miss eleven and over-mark three.
|
|
105
138
|
*/
|
|
106
139
|
ownedFiles?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* The page rows and seed content this module contributes to any site
|
|
142
|
+
* template that takes it (spec 5478; parent spec 5462 § D5) — `ownedFiles`'s
|
|
143
|
+
* sibling, and a DIFFERENT kind of fact again from every field above.
|
|
144
|
+
* `ownedFiles` names which files in THIS monorepo physically belong to the
|
|
145
|
+
* module; `pageContributions` names which PAGES the module brings to a
|
|
146
|
+
* template's roster. `template-manifest.ts`'s `mergeRoster` reads it.
|
|
147
|
+
*
|
|
148
|
+
* A contribution never carries route CODE. How the route code reaches a
|
|
149
|
+
* scaffold is a property of the module, not of the contribution (parent
|
|
150
|
+
* § D6): `docs-blog` and `scheduling` list their route files in `ownedFiles`
|
|
151
|
+
* so the code rides their own licence-checked bundle, while
|
|
152
|
+
* `signal-collectors` deliberately does not — its `flows` routes reach a
|
|
153
|
+
* scaffold from the `templates/` snapshot, retained because
|
|
154
|
+
* `MODULE_SURFACES` enables the surface (see this file's
|
|
155
|
+
* `signal-collectors.ownedFiles` comment for why listing them there broke
|
|
156
|
+
* `sync-routes`, #5366). Both shapes declare their rows here identically.
|
|
157
|
+
*
|
|
158
|
+
* Unset for a module that brings no pages to any declared template:
|
|
159
|
+
* `marketing` (base, no `ownedFiles` and `MODULE_SURFACES.marketing` is
|
|
160
|
+
* `[]`), and — deferred, not absent — `multi-tenant` and `commerce`, whose
|
|
161
|
+
* rows are their own instances' to enumerate. An unset field contributes
|
|
162
|
+
* zero rows openly; it is not a partial merge.
|
|
163
|
+
*/
|
|
164
|
+
pageContributions?: readonly PageRosterRow[];
|
|
107
165
|
/**
|
|
108
166
|
* Bundle-relative source → scaffold-relative destination (spec 5373 § D7).
|
|
109
167
|
* Every source must be a file the validated tar extraction produced in the
|
|
@@ -266,6 +266,28 @@ export const MODULE_SLICES = {
|
|
|
266
266
|
"packages/ui/src/chrome/DocsSidebar.tsx",
|
|
267
267
|
"packages/ui/src/chrome/slices/docs-blog.ts",
|
|
268
268
|
],
|
|
269
|
+
// pageContributions (spec 5478; parent § D6 shape 1) — the two route
|
|
270
|
+
// surfaces this module mounts, each with the seed MDX it renders. The
|
|
271
|
+
// three MDX files are exactly the three on the tree and cannot come from
|
|
272
|
+
// the `templates/` snapshot: `apps/web/content` is in build-templates.mjs's
|
|
273
|
+
// EXCLUDE_TEMPLATE_DIRS and `template-overlay/` carries no blog or docs
|
|
274
|
+
// stub. They ride this module's own bundle, consistent with the route
|
|
275
|
+
// files above — STAGING them into it is #5481's change to
|
|
276
|
+
// build-module-bundles.mjs, since `collectModuleFiles` unions `ownedFiles`
|
|
277
|
+
// with `installMap` keys and nothing else. This unit only DECLARES them.
|
|
278
|
+
pageContributions: [
|
|
279
|
+
{
|
|
280
|
+
route: "/blog",
|
|
281
|
+
content: [
|
|
282
|
+
"apps/web/content/en/blog/index.mdx",
|
|
283
|
+
"apps/web/content/en/blog/launching-ailk.mdx",
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
route: "/docs",
|
|
288
|
+
content: ["apps/web/content/en/docs/getting-started.mdx"],
|
|
289
|
+
},
|
|
290
|
+
],
|
|
269
291
|
// patches (spec 5418 § 3 D1, Group G) — every Group G row that edits a
|
|
270
292
|
// file the buyer's scaffold already has. Values transcribed verbatim
|
|
271
293
|
// from docs/specs/5365-docs-blog-module-spec.md § 4 Group G via
|
|
@@ -387,6 +409,12 @@ export const MODULE_SLICES = {
|
|
|
387
409
|
"packages/theme",
|
|
388
410
|
"packages/ui",
|
|
389
411
|
"packages/validation",
|
|
412
|
+
// The `workspace` module's shared settings components (#5371 req. 6)
|
|
413
|
+
// — the scaffolded-site twin of the root architecture.yaml grant on
|
|
414
|
+
// `apps_paid.workspace`. Pro Plus inherits Pro's module set, so a
|
|
415
|
+
// saas-product scaffold carries both modules and this app composes
|
|
416
|
+
// the identical panels apps/web does, with no forked copy.
|
|
417
|
+
"packages/workspace-ui",
|
|
390
418
|
],
|
|
391
419
|
may_not_import_from: [
|
|
392
420
|
"apps/api",
|
|
@@ -649,6 +677,90 @@ export const MODULE_SLICES = {
|
|
|
649
677
|
"apps-paid/workspace/**",
|
|
650
678
|
],
|
|
651
679
|
},
|
|
680
|
+
// ── workspace — Pro module: the single-workspace settings shell (D-27/D-49)
|
|
681
|
+
//
|
|
682
|
+
// `/workspace` in apps/web's `(authed)` segment — General (rename), Site
|
|
683
|
+
// (identity / SEO / theme, over the already-shipped apps/api config routes)
|
|
684
|
+
// and Team (a read-only roster). The three panels live in the shared
|
|
685
|
+
// `packages/workspace-ui` package so the multi-tenant workspace app (the
|
|
686
|
+
// Pro Plus app) composes the identical components rather than a fork.
|
|
687
|
+
//
|
|
688
|
+
// No `apps` / `namespaces` / `forbiddenPatterns` block: this module
|
|
689
|
+
// contributes to already-declared apps only (apps/web is BASE; apps/api is
|
|
690
|
+
// declared by multi-tenant / scheduling / commerce and is snapshotted whole).
|
|
691
|
+
workspace: {
|
|
692
|
+
// A PERMISSION, not a physical slice (#5371 D1a) — the identical shape
|
|
693
|
+
// docs-blog uses for `packages/fumadocs-schema`. `packages/` is never in
|
|
694
|
+
// SNAPSHOT_DIRS, so the package reaches a buyer as an npm/bundle
|
|
695
|
+
// dependency under `node_modules/`, never as a `packages/workspace-ui`
|
|
696
|
+
// source sibling; the `patches` entry below is what puts it in the
|
|
697
|
+
// buyer's apps/web/package.json.
|
|
698
|
+
webImports: ["packages/workspace-ui"],
|
|
699
|
+
packages: {
|
|
700
|
+
"workspace-ui": {
|
|
701
|
+
description: "Shared workspace-settings components — the settings shell plus the General / Site / Team panels, presentational only (data + save callbacks as props)",
|
|
702
|
+
// packages/templates is required because WorkspaceSettingsShell
|
|
703
|
+
// composes AppShell from @working-theory/templates/shells — the same
|
|
704
|
+
// composition apps/web's own account page uses.
|
|
705
|
+
may_import_from: [
|
|
706
|
+
"packages/templates",
|
|
707
|
+
"packages/theme",
|
|
708
|
+
"packages/ui",
|
|
709
|
+
"packages/validation",
|
|
710
|
+
],
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
// SOURCE files only, and only the ones this module physically owns: the
|
|
714
|
+
// three apps/web route files and the two apps/api route files.
|
|
715
|
+
//
|
|
716
|
+
// `packages/workspace-ui/**` is deliberately NOT listed (D1a). It is a
|
|
717
|
+
// whole package that is paid end to end, whose strip disposition is
|
|
718
|
+
// carried by architecture.yaml's `commercial_tier_moves_to` field (its
|
|
719
|
+
// future paid-tree home) rather than by a per-file inventory —
|
|
720
|
+
// exactly the disposition `packages/fumadocs-schema` carries for
|
|
721
|
+
// docs-blog. `multi-tenant`'s whole-app glob (above) works because
|
|
722
|
+
// SNAPSHOT_DIRS physically copies apps wholesale; a package is never in a
|
|
723
|
+
// snapshot root, so an ownedFiles enumeration would add nothing.
|
|
724
|
+
//
|
|
725
|
+
// `packages/api-client/src/workspace.ts` is NOT listed either (D1b):
|
|
726
|
+
// api-client is a base package every scaffold receives regardless of
|
|
727
|
+
// `--modules`, and the field doc above names it by name as a shared
|
|
728
|
+
// package deliberately left out even when genuinely single-purpose. The
|
|
729
|
+
// wrapper is harmless on a scaffold without this module — its calls 404,
|
|
730
|
+
// the same non-event any client wrapper produces against an unregistered
|
|
731
|
+
// route.
|
|
732
|
+
//
|
|
733
|
+
// Test companions (`__tests__/`) are never listed — the SOURCE-files-only
|
|
734
|
+
// rule above.
|
|
735
|
+
ownedFiles: [
|
|
736
|
+
"apps/web/app/[locale]/(authed)/workspace/page.tsx",
|
|
737
|
+
"apps/web/app/[locale]/(authed)/workspace/site/page.tsx",
|
|
738
|
+
"apps/web/app/[locale]/(authed)/workspace/team/page.tsx",
|
|
739
|
+
"apps/api/src/routes/workspaces/general.ts",
|
|
740
|
+
"apps/api/src/routes/workspaces/members.ts",
|
|
741
|
+
],
|
|
742
|
+
// patches — the dependency add that delivers the shared package to the
|
|
743
|
+
// buyer's apps/web, mirroring docs-blog's own fumadocs-schema merge-keys
|
|
744
|
+
// op. No next.config.mjs `add-transpile-package` op: the package ships
|
|
745
|
+
// compiled ESM in dist/ the same way every other @working-theory/*
|
|
746
|
+
// dependency apps/web already consumes does, and docs-blog's transpile op
|
|
747
|
+
// exists for the fumadocs SDK boundary, not for the dependency add itself.
|
|
748
|
+
patches: [
|
|
749
|
+
{
|
|
750
|
+
target: "apps/web/package.json",
|
|
751
|
+
kind: "json",
|
|
752
|
+
ops: [
|
|
753
|
+
{
|
|
754
|
+
op: "merge-keys",
|
|
755
|
+
path: ["dependencies"],
|
|
756
|
+
entries: {
|
|
757
|
+
"@working-theory/workspace-ui": "workspace:*",
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
},
|
|
652
764
|
// ── scheduling — full-stack module (provider + routes + booker pages) ────
|
|
653
765
|
//
|
|
654
766
|
// The #4416 scheduling seam, made installable. Three tiers into the
|
|
@@ -816,6 +928,14 @@ export const MODULE_SLICES = {
|
|
|
816
928
|
"apps/web/app/[locale]/schedule/page.tsx",
|
|
817
929
|
"apps/web/app/[locale]/schedule/confirmed/page.tsx",
|
|
818
930
|
],
|
|
931
|
+
// pageContributions (spec 5478; parent § D6 shape 1) — the booker and its
|
|
932
|
+
// confirmation page, whose route files `ownedFiles` above already carries.
|
|
933
|
+
// Neither row names seed content: both render from the /v1/schedule/*
|
|
934
|
+
// routes, not from an MDX file.
|
|
935
|
+
pageContributions: [
|
|
936
|
+
{ route: "/schedule" },
|
|
937
|
+
{ route: "/schedule/confirmed" },
|
|
938
|
+
],
|
|
819
939
|
},
|
|
820
940
|
// ── signal-collectors — flows, deliverables, project listings, waitlist
|
|
821
941
|
// signups (D-48, #5366) ───────────────────────────────────────────────
|
|
@@ -898,10 +1018,23 @@ export const MODULE_SLICES = {
|
|
|
898
1018
|
// packages/ui: eleven components (D8) plus the NEW slice registration
|
|
899
1019
|
// entry point (D8a, mirroring sections/slices/scheduling.ts). Four of the
|
|
900
1020
|
// eleven (ProjectListingShelves, ListingPrivacyNotice, ProjectListingCard,
|
|
901
|
-
// ResultsReveal)
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
1021
|
+
// ResultsReveal) had no consumer anywhere IN THIS REPO at D8 authoring
|
|
1022
|
+
// time — grep-confirmed, a module-boundary fact only in-repo. Three of the
|
|
1023
|
+
// four (ProjectListingShelves, ProjectListingCard, ListingPrivacyNotice —
|
|
1024
|
+
// plus the ListingWindowBand type ProjectListingCard exports) are, in
|
|
1025
|
+
// fact, consumed CROSS-ORG: the samhenry site (salessmyth org) imports all
|
|
1026
|
+
// four from the signal-collectors slice's re-export entry point
|
|
1027
|
+
// (packages/ui/src/sections/slices/signal-collectors.ts, rules/cross-org-
|
|
1028
|
+
// sharing.md), which #5488 additionally re-exports them from so that
|
|
1029
|
+
// consumer can reach them by subpath. "No consumer anywhere in the repo"
|
|
1030
|
+
// was therefore true in-repo and false across the org boundary — not a
|
|
1031
|
+
// reachability fact a repo-scoped grep can see. ResultsReveal remains the
|
|
1032
|
+
// one component of the four with no known consumer anywhere (in-repo or
|
|
1033
|
+
// out) and is NOT re-exported by #5488 — it is not this card's claim to
|
|
1034
|
+
// make it non-orphaned. D8's placement rule (by subject — project-listing
|
|
1035
|
+
// / results-reveal UI — rather than by reachability, since reachability
|
|
1036
|
+
// cannot place an unreached file either) is unchanged and still governs
|
|
1037
|
+
// where all eleven live.
|
|
905
1038
|
"signal-collectors": {
|
|
906
1039
|
ownedFiles: [
|
|
907
1040
|
"apps/api/src/routes/flows/index.ts",
|
|
@@ -995,6 +1128,25 @@ export const MODULE_SLICES = {
|
|
|
995
1128
|
"packages/ui/src/blocks/ResultsReveal.tsx",
|
|
996
1129
|
"packages/ui/src/sections/slices/signal-collectors.ts",
|
|
997
1130
|
],
|
|
1131
|
+
// pageContributions (spec 5478; parent § D6 shape 2) — the three flow
|
|
1132
|
+
// routes, named exactly as `surfaces.ts`'s ROUTE_SINCE keys name them.
|
|
1133
|
+
// ROWS AND SEED ONLY: unlike docs-blog and scheduling above, this module's
|
|
1134
|
+
// route code is deliberately NOT in `ownedFiles` (see the comment inside
|
|
1135
|
+
// that array). The first two rows' code reaches a scaffold from the
|
|
1136
|
+
// `templates/` snapshot, retained because the `flows` surface is enabled;
|
|
1137
|
+
// the results row's code is `commerce`'s (`commerce.ownedFiles` carries
|
|
1138
|
+
// `results/[payload]/page.tsx` + its two client files below, and
|
|
1139
|
+
// `paidModuleTemplateExclusions` strips every paid module's owned files
|
|
1140
|
+
// from the snapshot unconditionally), so that one row rides a DIFFERENT
|
|
1141
|
+
// module's bundle — recorded in spec 5478 § 8 item 1, whose delivery
|
|
1142
|
+
// consequence is #5481's and #5464's. Declaring the rows here changes none
|
|
1143
|
+
// of it: a contribution is data, and which delivery shape carries the code
|
|
1144
|
+
// is a property of the module, not of the contribution.
|
|
1145
|
+
pageContributions: [
|
|
1146
|
+
{ route: "/flows/[slug]" },
|
|
1147
|
+
{ route: "/flows/[slug]/start" },
|
|
1148
|
+
{ route: "/flows/[slug]/results/[payload]" },
|
|
1149
|
+
],
|
|
998
1150
|
},
|
|
999
1151
|
// ── commerce — per-site paid add-on (D-47, amending D-26) ─────────────────
|
|
1000
1152
|
//
|
package/dist/parse-args.d.ts
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
* composition is downstream work, so the default (`marketing`) reproduces
|
|
8
8
|
* today's fixed-tree scaffold exactly.
|
|
9
9
|
*/
|
|
10
|
-
export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs-blog", "multi-tenant", "scheduling", "signal-collectors", "commerce"];
|
|
10
|
+
export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs-blog", "multi-tenant", "workspace", "scheduling", "signal-collectors", "commerce"];
|
|
11
11
|
export type ModuleName = (typeof RECOGNIZED_MODULES)[number];
|
|
12
12
|
export interface CliOptions {
|
|
13
13
|
/** Positional target directory (the dir to scaffold into). Optional only
|
|
14
14
|
* when `help` or `version` is set. */
|
|
15
15
|
target: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The site template to install (`--template`), validated against
|
|
18
|
+
* `RECOGNIZED_TEMPLATES` at parse time. Undefined when the flag is absent —
|
|
19
|
+
* and a run naming no template scaffolds exactly what it did before the
|
|
20
|
+
* flag existed (spec 5481 § 3 DD3).
|
|
21
|
+
*/
|
|
22
|
+
template: string | undefined;
|
|
16
23
|
install: boolean;
|
|
17
24
|
help: boolean;
|
|
18
25
|
version: boolean;
|
package/dist/parse-args.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
import { RECOGNIZED_SURFACES, parseSurfaces } from "./surfaces.js";
|
|
12
|
+
import { RECOGNIZED_TEMPLATES } from "./template-names.js";
|
|
12
13
|
/**
|
|
13
14
|
* The recognized module vocabulary for the `--modules` selector.
|
|
14
15
|
*
|
|
@@ -22,6 +23,7 @@ export const RECOGNIZED_MODULES = [
|
|
|
22
23
|
"marketing",
|
|
23
24
|
"docs-blog",
|
|
24
25
|
"multi-tenant",
|
|
26
|
+
"workspace",
|
|
25
27
|
"scheduling",
|
|
26
28
|
"signal-collectors",
|
|
27
29
|
"commerce",
|
|
@@ -51,6 +53,16 @@ Options:
|
|
|
51
53
|
Defaults to \`marketing\`, which reproduces today's
|
|
52
54
|
scaffold. A module is a composable unit across the
|
|
53
55
|
three tiers (data · API · presentation).
|
|
56
|
+
--template <name> The site template to install (recognized:
|
|
57
|
+
${RECOGNIZED_TEMPLATES.join(", ")}).
|
|
58
|
+
A template is a page roster plus seed content and
|
|
59
|
+
config — never code. Its required modules are ADDED
|
|
60
|
+
to whatever \`--modules\` names, so the two compose.
|
|
61
|
+
A template needing only free modules installs from
|
|
62
|
+
the tree this package ships, with no key and no
|
|
63
|
+
network call; one needing a paid module needs
|
|
64
|
+
AILK_LICENSE_KEY to cover every module it requires,
|
|
65
|
+
and refuses naming whichever it does not.
|
|
54
66
|
--surfaces <csv> Comma-separated route surfaces to scaffold IN
|
|
55
67
|
ADDITION to the core set (/, /about, /contact, /faq,
|
|
56
68
|
/thanks). Recognized: ${RECOGNIZED_SURFACES.join(", ")}.
|
|
@@ -102,6 +114,7 @@ export function parseCliArgs(argv) {
|
|
|
102
114
|
allowPositionals: true,
|
|
103
115
|
options: {
|
|
104
116
|
modules: { type: "string" },
|
|
117
|
+
template: { type: "string" },
|
|
105
118
|
surfaces: { type: "string" },
|
|
106
119
|
"sync-architecture": { type: "string" },
|
|
107
120
|
"sync-catalog": { type: "boolean", default: false },
|
|
@@ -119,6 +132,7 @@ export function parseCliArgs(argv) {
|
|
|
119
132
|
const version = parsed.values.version === true;
|
|
120
133
|
const install = parsed.values.install === true;
|
|
121
134
|
const modules = parseModules(parsed.values.modules);
|
|
135
|
+
const template = parseTemplate(parsed.values.template);
|
|
122
136
|
const surfaces = parseSurfaceList(parsed.values.surfaces);
|
|
123
137
|
const syncArchitecture = parsed.values["sync-architecture"];
|
|
124
138
|
const syncCatalog = parsed.values["sync-catalog"] === true;
|
|
@@ -138,6 +152,7 @@ export function parseCliArgs(argv) {
|
|
|
138
152
|
if (help || version) {
|
|
139
153
|
return {
|
|
140
154
|
target: undefined,
|
|
155
|
+
template: undefined,
|
|
141
156
|
install,
|
|
142
157
|
help,
|
|
143
158
|
version,
|
|
@@ -155,6 +170,7 @@ export function parseCliArgs(argv) {
|
|
|
155
170
|
}
|
|
156
171
|
return {
|
|
157
172
|
target: parsed.positionals[0],
|
|
173
|
+
template,
|
|
158
174
|
install,
|
|
159
175
|
help,
|
|
160
176
|
version,
|
|
@@ -173,6 +189,7 @@ export function parseCliArgs(argv) {
|
|
|
173
189
|
}
|
|
174
190
|
return {
|
|
175
191
|
target: parsed.positionals[0],
|
|
192
|
+
template,
|
|
176
193
|
install,
|
|
177
194
|
help,
|
|
178
195
|
version,
|
|
@@ -183,6 +200,32 @@ export function parseCliArgs(argv) {
|
|
|
183
200
|
syncArchitecture: undefined,
|
|
184
201
|
};
|
|
185
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Validate the `--template` value against the declared template set
|
|
205
|
+
* (spec 5481 § 3 DD1).
|
|
206
|
+
*
|
|
207
|
+
* Validated HERE, in the parser, so an unrecognized template fails at parse
|
|
208
|
+
* time as a `ParseError` exactly like an unrecognized module or surface does.
|
|
209
|
+
* Splitting it out to `programmatic.ts` would make `--template` the one flag
|
|
210
|
+
* in this CLI whose bad value surfaces somewhere else.
|
|
211
|
+
*
|
|
212
|
+
* The names come from `template-names.ts` rather than from
|
|
213
|
+
* `TEMPLATE_MANIFESTS` itself, and that file's doc comment records the
|
|
214
|
+
* measured reason: importing the registry here closes a runtime cycle
|
|
215
|
+
* through `lib/module-tier.ts`. `TEMPLATE_MANIFESTS` remains the registry;
|
|
216
|
+
* the two sets are asserted equal by the suite.
|
|
217
|
+
*
|
|
218
|
+
* @throws ParseError on an undeclared template name, naming the declared set.
|
|
219
|
+
*/
|
|
220
|
+
function parseTemplate(raw) {
|
|
221
|
+
if (raw === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
const name = raw.trim();
|
|
224
|
+
if (!RECOGNIZED_TEMPLATES.includes(name)) {
|
|
225
|
+
throw new ParseError(`unknown template "${raw}". Recognized: ${RECOGNIZED_TEMPLATES.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
return name;
|
|
228
|
+
}
|
|
186
229
|
/**
|
|
187
230
|
* Parse + validate the `--surfaces` CSV, re-throwing an unrecognized-surface
|
|
188
231
|
* failure as a ParseError so every CLI parse failure reaches the orchestrator's
|
package/dist/programmatic.d.ts
CHANGED
|
@@ -28,6 +28,21 @@ export interface ScaffoldOptions {
|
|
|
28
28
|
* Ignored at runtime until named presets are implemented.
|
|
29
29
|
*/
|
|
30
30
|
preset?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The site template to install (`--template`), a `TEMPLATE_MANIFESTS` key.
|
|
33
|
+
*
|
|
34
|
+
* Consumed ONLY by the async `scaffold()` below. `scaffoldSync` stays
|
|
35
|
+
* template-oblivious for exactly the reason it stays licensing-oblivious
|
|
36
|
+
* (spec 5481 § 3 DD3): a call naming no template takes a code path with no
|
|
37
|
+
* new statement in it, which is a stronger byte-identity guarantee than a
|
|
38
|
+
* test asserting the output happens to match.
|
|
39
|
+
*
|
|
40
|
+
* What a template DOES change is `modules` — `scaffold()` unions the
|
|
41
|
+
* template's `requires` with whatever `--modules` named and passes THAT
|
|
42
|
+
* list to `scaffoldSync` (DD2), so the resolved surfaces, the emitted
|
|
43
|
+
* `architecture.yaml` and the generated nav are all functions of the union.
|
|
44
|
+
*/
|
|
45
|
+
template?: string;
|
|
31
46
|
/**
|
|
32
47
|
* Selected modules (recognized: marketing, docs-blog, multi-tenant,
|
|
33
48
|
* scheduling). Defaults to
|
|
@@ -70,6 +85,8 @@ export interface ScaffoldOptions {
|
|
|
70
85
|
licenseKey?: string;
|
|
71
86
|
/** Overrides `AILK_MODULE_REGISTRY_URL` / the built-in module-distribution default. */
|
|
72
87
|
moduleRegistryUrl?: string;
|
|
88
|
+
/** Overrides `AILK_TEMPLATE_REGISTRY_URL` / the built-in template-distribution default. */
|
|
89
|
+
templateRegistryUrl?: string;
|
|
73
90
|
/** Injected fetch for tests. Never a live network call in CI or in this repo's evidence runs. */
|
|
74
91
|
fetchImpl?: typeof fetch;
|
|
75
92
|
}
|
|
@@ -98,9 +115,17 @@ export declare function scaffoldSync(opts: ScaffoldOptions): ScaffoldResult;
|
|
|
98
115
|
* read and no network call (the regression floor). A paid module with
|
|
99
116
|
* no key, or a licence the module distribution rejects, throws
|
|
100
117
|
* `LicenseRequiredError` HERE — before anything is written to disk.
|
|
101
|
-
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy
|
|
118
|
+
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy, over the
|
|
119
|
+
* EFFECTIVE module set (see 0 below).
|
|
102
120
|
* 4. Merge each resolved paid module's fetched bundle into the freshly
|
|
103
121
|
* scaffolded tree (`extractModuleBundle`).
|
|
122
|
+
* 5. Install the named site template LAST (#5481; parent record D9), from
|
|
123
|
+
* its fetched bundle or — when it requires only free modules — from the
|
|
124
|
+
* `site-templates/` tree this package ships.
|
|
125
|
+
*
|
|
126
|
+
* And, ahead of all five when `opts.template` is set (0): resolve the
|
|
127
|
+
* template and union its `requires` with `opts.modules`, so every step above
|
|
128
|
+
* runs over the EFFECTIVE module set rather than the requested one.
|
|
104
129
|
*
|
|
105
130
|
* `scaffoldSync` itself stays licensing-oblivious on purpose: every existing
|
|
106
131
|
* synchronous caller (this package's own test suite, overwhelmingly) is
|
package/dist/programmatic.js
CHANGED
|
@@ -21,15 +21,26 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog.js";
|
|
22
22
|
import { copyTemplate } from "./copy.js";
|
|
23
23
|
import { deriveArchitecture } from "./derive-architecture.js";
|
|
24
|
-
import { extractModuleBundle } from "./lib/extract-module-bundle.js";
|
|
24
|
+
import { extractModuleBundle, installBundleTree, } from "./lib/extract-module-bundle.js";
|
|
25
|
+
import { fetchTemplate } from "./lib/fetch-template.js";
|
|
25
26
|
import { resolvePaidModules } from "./lib/module-license-gate.js";
|
|
26
27
|
import { classifyModules } from "./lib/module-tier.js";
|
|
27
28
|
import { normalizeModules } from "./module-architecture.js";
|
|
28
29
|
import { NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
29
30
|
import { syncRoutes } from "./sync-routes.js";
|
|
31
|
+
import { resolveTemplate, templateDelivery } from "./template-manifest.js";
|
|
30
32
|
import { validateTarget } from "./validate.js";
|
|
31
33
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
34
|
const TEMPLATES_DIR = resolve(PKG_ROOT, "templates");
|
|
35
|
+
/**
|
|
36
|
+
* Where a Free site template's files are read from on the keyless path — and
|
|
37
|
+
* why it is BESIDE `templates/` rather than inside it (parent record D14):
|
|
38
|
+
* `copyTemplate` copies the whole `templates/` tree into EVERY scaffold, so a
|
|
39
|
+
* per-template subtree there would land in scaffolds that asked for a
|
|
40
|
+
* different template, or for none. It reaches the published tarball through
|
|
41
|
+
* `package.json` `files` instead.
|
|
42
|
+
*/
|
|
43
|
+
const SITE_TEMPLATES_DIR = resolve(PKG_ROOT, "site-templates");
|
|
33
44
|
const VERSIONS_FILE = resolve(PKG_ROOT, "versions.json");
|
|
34
45
|
function readVersionMap() {
|
|
35
46
|
if (!existsSync(VERSIONS_FILE)) {
|
|
@@ -114,9 +125,17 @@ export function scaffoldSync(opts) {
|
|
|
114
125
|
* read and no network call (the regression floor). A paid module with
|
|
115
126
|
* no key, or a licence the module distribution rejects, throws
|
|
116
127
|
* `LicenseRequiredError` HERE — before anything is written to disk.
|
|
117
|
-
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy
|
|
128
|
+
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy, over the
|
|
129
|
+
* EFFECTIVE module set (see 0 below).
|
|
118
130
|
* 4. Merge each resolved paid module's fetched bundle into the freshly
|
|
119
131
|
* scaffolded tree (`extractModuleBundle`).
|
|
132
|
+
* 5. Install the named site template LAST (#5481; parent record D9), from
|
|
133
|
+
* its fetched bundle or — when it requires only free modules — from the
|
|
134
|
+
* `site-templates/` tree this package ships.
|
|
135
|
+
*
|
|
136
|
+
* And, ahead of all five when `opts.template` is set (0): resolve the
|
|
137
|
+
* template and union its `requires` with `opts.modules`, so every step above
|
|
138
|
+
* runs over the EFFECTIVE module set rather than the requested one.
|
|
120
139
|
*
|
|
121
140
|
* `scaffoldSync` itself stays licensing-oblivious on purpose: every existing
|
|
122
141
|
* synchronous caller (this package's own test suite, overwhelmingly) is
|
|
@@ -124,16 +143,59 @@ export function scaffoldSync(opts) {
|
|
|
124
143
|
* with a paid module in `opts.modules` exercises the gate at all.
|
|
125
144
|
*/
|
|
126
145
|
export async function scaffold(opts) {
|
|
127
|
-
|
|
146
|
+
// Resolved first, and by the registry rather than re-validated here: the
|
|
147
|
+
// name already passed `parse-args.ts`, and `resolveTemplate` throws a
|
|
148
|
+
// `TemplateError` for a programmatic caller that skipped the parser.
|
|
149
|
+
const template = opts.template !== undefined ? resolveTemplate(opts.template) : undefined;
|
|
150
|
+
// The effective module set: union(template.requires, parsed --modules),
|
|
151
|
+
// normalized through `normalizeModules` — the single ordering authority
|
|
152
|
+
// (parent record D7). Order-independence falls out rather than being
|
|
153
|
+
// engineered: it returns `RECOGNIZED_MODULES.filter(...)`, declaration-
|
|
154
|
+
// ordered and deduped, so the same SET in any argument order is the same
|
|
155
|
+
// list.
|
|
156
|
+
const modules = normalizeModules([
|
|
157
|
+
...(template?.requires ?? []),
|
|
158
|
+
...(opts.modules ?? ["marketing"]),
|
|
159
|
+
]);
|
|
160
|
+
const { paid } = classifyModules(modules);
|
|
128
161
|
const bundles = await resolvePaidModules({
|
|
129
162
|
paidModules: paid,
|
|
130
163
|
licenseKey: opts.licenseKey,
|
|
164
|
+
...(template !== undefined ? { templateName: template.name } : {}),
|
|
131
165
|
...(opts.moduleRegistryUrl !== undefined
|
|
132
166
|
? { registryUrl: opts.moduleRegistryUrl }
|
|
133
167
|
: {}),
|
|
134
168
|
...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
|
|
135
169
|
});
|
|
136
|
-
|
|
170
|
+
// The template's OWN bundle, fetched here — before `scaffoldSync` — so the
|
|
171
|
+
// fourth refusal (the template's bundle unpublished, parent record D17 /
|
|
172
|
+
// spec 5481 § 3 DD5 row 4) throws with the target still non-existent, like
|
|
173
|
+
// the three module refusals above it (DD6). It is EXTRACTED last, below.
|
|
174
|
+
//
|
|
175
|
+
// The keyless path never reaches this at all: a template requiring only
|
|
176
|
+
// free modules classifies to an empty paid set, so `resolvePaidModules`
|
|
177
|
+
// returned at its first branch without reading `process.env` or the
|
|
178
|
+
// network, and `templateDelivery` sends us past this too (DD7). That is
|
|
179
|
+
// what makes record D11's no-network guarantee structural rather than
|
|
180
|
+
// merely likely.
|
|
181
|
+
const templateBundle = template !== undefined && templateDelivery(template) === "paid"
|
|
182
|
+
? await fetchTemplate({
|
|
183
|
+
templateName: template.name,
|
|
184
|
+
token: opts.licenseKey ?? process.env["AILK_LICENSE_KEY"] ?? "",
|
|
185
|
+
...(opts.templateRegistryUrl !== undefined
|
|
186
|
+
? { registryUrl: opts.templateRegistryUrl }
|
|
187
|
+
: {}),
|
|
188
|
+
...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
|
|
189
|
+
})
|
|
190
|
+
: undefined;
|
|
191
|
+
// `modules` — the union — is what `scaffoldSync` receives, not `opts`
|
|
192
|
+
// (spec 5481 § 3 DD2). `resolveSurfaces`, `deriveArchitecture` and
|
|
193
|
+
// `renderNavConfig` are all computed INSIDE `scaffoldSync` from
|
|
194
|
+
// `opts.modules`, so a union computed for the licence classification alone
|
|
195
|
+
// would gate correctly and emit the wrong surfaces, the wrong
|
|
196
|
+
// `architecture.yaml` and the wrong nav — a silent wrong result, not a
|
|
197
|
+
// failure.
|
|
198
|
+
const result = scaffoldSync({ ...opts, modules });
|
|
137
199
|
// Threaded to extractModuleBundle for `package.json` patch targets'
|
|
138
200
|
// `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
|
|
139
201
|
// `scaffoldSync` already reads for `copyTemplate`, re-read here since
|
|
@@ -147,6 +209,40 @@ export async function scaffold(opts) {
|
|
|
147
209
|
versions,
|
|
148
210
|
});
|
|
149
211
|
}
|
|
212
|
+
// The template lands LAST — after `scaffoldSync` and after every module
|
|
213
|
+
// bundle (parent record D9) — so its content and config files sit on top of
|
|
214
|
+
// everything derived. It shapes the derived artifacts by DECLARING (its
|
|
215
|
+
// `requires` are in the union above; its roster rows are what the nav is
|
|
216
|
+
// generated from), never by landing a code file: the nav is already written
|
|
217
|
+
// by `scaffoldSync` from `renderNavConfig(enabledSurfaces)`, and record
|
|
218
|
+
// §5.2 forbids a `.ts` in any `site-templates/` `installMap` destination.
|
|
219
|
+
//
|
|
220
|
+
// One install step, two source roots (DD7): an extracted staging dir on the
|
|
221
|
+
// paid path, `site-templates/<name>/` on disk on the Free one.
|
|
222
|
+
if (template !== undefined) {
|
|
223
|
+
if (templateBundle !== undefined) {
|
|
224
|
+
await extractModuleBundle({
|
|
225
|
+
bytes: templateBundle,
|
|
226
|
+
targetDir: result.targetDir,
|
|
227
|
+
moduleName: template.name,
|
|
228
|
+
templateRequires: template.requires,
|
|
229
|
+
versions,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const sourceRoot = join(SITE_TEMPLATES_DIR, template.name);
|
|
234
|
+
if (!existsSync(sourceRoot)) {
|
|
235
|
+
throw new Error(`template "${template.name}": ${sourceRoot} is missing — the published package must carry site-templates/ in its "files" list`);
|
|
236
|
+
}
|
|
237
|
+
installBundleTree({
|
|
238
|
+
sourceRoot,
|
|
239
|
+
targetDir: result.targetDir,
|
|
240
|
+
name: template.name,
|
|
241
|
+
templateRequires: template.requires,
|
|
242
|
+
versions,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
150
246
|
return result;
|
|
151
247
|
}
|
|
152
248
|
// Re-exported so a consumer of the async `scaffold()` above can type its own
|