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.
Files changed (37) hide show
  1. package/dist/cli.js +19 -9
  2. package/dist/lib/extract-module-bundle.d.ts +39 -0
  3. package/dist/lib/extract-module-bundle.js +165 -78
  4. package/dist/lib/fetch-template.d.ts +20 -0
  5. package/dist/lib/fetch-template.js +129 -0
  6. package/dist/lib/module-license-gate.d.ts +8 -0
  7. package/dist/lib/module-license-gate.js +12 -4
  8. package/dist/module-architecture.d.ts +58 -0
  9. package/dist/module-architecture.js +156 -4
  10. package/dist/parse-args.d.ts +8 -1
  11. package/dist/parse-args.js +43 -0
  12. package/dist/programmatic.d.ts +26 -1
  13. package/dist/programmatic.js +100 -4
  14. package/dist/surfaces.js +42 -0
  15. package/dist/template-manifest.d.ts +92 -0
  16. package/dist/template-manifest.js +168 -0
  17. package/dist/template-names.d.ts +41 -0
  18. package/dist/template-names.js +44 -0
  19. package/package.json +2 -1
  20. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  21. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  22. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  23. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  24. package/site-templates/marketing-site-free/provenance.json +4 -0
  25. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  26. package/templates/apps/api/src/config/modules.ts +1 -1
  27. package/templates/apps/api/src/server.ts +46 -3
  28. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  29. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  30. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  31. package/templates/apps/web/jest.config.cjs +5 -0
  32. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  33. package/templates/apps/web/lib/server-api.ts +28 -12
  34. package/templates/apps/web/messages/en.json +58 -0
  35. package/templates/database/CHANGELOG.md +14 -0
  36. package/templates/database/package.json +1 -1
  37. package/templates/package.json +1 -1
package/dist/surfaces.js CHANGED
@@ -164,6 +164,19 @@ export const GATEABLE_SURFACES = [
164
164
  routes: [`${ROUTE_ROOT}/partners`],
165
165
  content: ["content/<locale>/PartnersList", "content/<locale>/Partner"],
166
166
  },
167
+ {
168
+ // Module-LESS by construction (#5479 D16): no MODULE_SURFACES value names
169
+ // it, so a site template may enable it by roster alone. Same for
170
+ // `privacy` and `terms` below.
171
+ id: "pricing",
172
+ routes: [`${ROUTE_ROOT}/pricing`],
173
+ content: ["content/<locale>/Pricing"],
174
+ },
175
+ {
176
+ id: "privacy",
177
+ routes: [`${ROUTE_ROOT}/privacy`],
178
+ content: ["content/<locale>/PrivacyPage"],
179
+ },
167
180
  {
168
181
  id: "products",
169
182
  routes: [`${ROUTE_ROOT}/products`],
@@ -208,6 +221,11 @@ export const GATEABLE_SURFACES = [
208
221
  routes: [`${ROUTE_ROOT}/tech-articles`],
209
222
  content: ["content/<locale>/TechArticle"],
210
223
  },
224
+ {
225
+ id: "terms",
226
+ routes: [`${ROUTE_ROOT}/terms`],
227
+ content: ["content/<locale>/TermsPage"],
228
+ },
211
229
  {
212
230
  id: "testimonials",
213
231
  routes: [`${ROUTE_ROOT}/testimonials`],
@@ -247,6 +265,12 @@ export const MODULE_SURFACES = {
247
265
  marketing: [],
248
266
  "docs-blog": ["docs", "blog"],
249
267
  "multi-tenant": [],
268
+ // `workspace` contributes zero gateable surfaces (#5371 D4): every route it
269
+ // owns lives in the `(authed)` segment, never in the public marketing route
270
+ // tree a template's roster composes — the same reason `multi-tenant` above
271
+ // carries an empty list. The entry itself is still required: surface-gating's
272
+ // vocabulary assertion covers exactly the recognized module set.
273
+ workspace: [],
250
274
  scheduling: ["schedule"],
251
275
  "signal-collectors": ["flows"],
252
276
  commerce: [],
@@ -409,6 +433,11 @@ export const ROUTE_SINCE = {
409
433
  "launch-service": "0.0.0",
410
434
  partners: "0.0.0",
411
435
  "partners/[slug]": "0.0.0",
436
+ // #5479 — three genuinely new route directories, so they attach at today's
437
+ // fixed-group version rather than the "predates the manifest" 0.0.0 above,
438
+ // the same rule #5048 ("projects/apply") and #5014 ("(authed)") applied.
439
+ pricing: "0.22.0",
440
+ privacy: "0.22.0",
412
441
  products: "0.0.0",
413
442
  "products/[slug]": "0.0.0",
414
443
  projects: "0.0.0",
@@ -428,6 +457,7 @@ export const ROUTE_SINCE = {
428
457
  "software/[slug]": "0.0.0",
429
458
  "tech-articles": "0.0.0",
430
459
  "tech-articles/[slug]": "0.0.0",
460
+ terms: "0.22.0",
431
461
  testimonials: "0.0.0",
432
462
  "thanks/[slug]": "0.14.0",
433
463
  "use-cases": "0.0.0",
@@ -444,6 +474,12 @@ export const ROUTE_SINCE = {
444
474
  // choice #5048's "projects/apply" entry already made for the same reason.
445
475
  "(authed)": "0.18.0",
446
476
  "(authed)/account": "0.18.0",
477
+ // #5371 — the workspace module's three settings routes. Genuinely new route
478
+ // directories, so they attach at today's fixed-group version rather than the
479
+ // "predates the manifest" 0.0.0 above, the same rule #5479 and #5048 applied.
480
+ "(authed)/workspace": "0.22.1",
481
+ "(authed)/workspace/site": "0.22.1",
482
+ "(authed)/workspace/team": "0.22.1",
447
483
  "(authed)/waitlist": "0.18.0",
448
484
  "(authed)/waitlist/[experimentId]": "0.18.0",
449
485
  "(authed)/waitlist/[experimentId]/signups/[leadId]": "0.18.0",
@@ -490,6 +526,9 @@ export const ROUTE_DEPENDENCIES = {
490
526
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
491
527
  `${ROUTE_ROOT}/(authed)/waitlist/format.ts`,
492
528
  ],
529
+ "(authed)/workspace": ["apps/web/lib/server-api.ts"],
530
+ "(authed)/workspace/site": ["apps/web/lib/server-api.ts"],
531
+ "(authed)/workspace/team": ["apps/web/lib/server-api.ts"],
493
532
  "(authed)/waitlist": ["apps/web/lib/server-api.ts"],
494
533
  };
495
534
  /**
@@ -504,6 +543,9 @@ export const ROUTE_DEPENDENCIES = {
504
543
  export const ROUTE_MESSAGE_NAMESPACES = {
505
544
  "sign-in": ["signIn"],
506
545
  "(authed)/account": ["account"],
546
+ "(authed)/workspace": ["workspace"],
547
+ "(authed)/workspace/site": ["workspace"],
548
+ "(authed)/workspace/team": ["workspace"],
507
549
  "(authed)/waitlist": ["waitlistDashboard.gate"],
508
550
  };
509
551
  /** Template-relative path of the generated site-nav registry. */
@@ -0,0 +1,92 @@
1
+ import { type PageRosterRow } from "./module-architecture.js";
2
+ import type { ModuleName } from "./parse-args.js";
3
+ /**
4
+ * One site template: its fixed module list and the page rows it brings itself.
5
+ *
6
+ * THERE IS NO `tier`, `level` OR `price` MEMBER, and there never is one
7
+ * (parent § D4). Delivery tier is DERIVED from `requires` — by the client via
8
+ * `templateDelivery` below, and by the issuer via `TIER_SPECS` (#5480) — so
9
+ * declaring it here would be a second authority for a fact the module list
10
+ * already settles. The type, not a convention, is what makes adding one a
11
+ * compile error: a convention is a thing someone must remember.
12
+ *
13
+ * There is likewise no optional-module member (parent § D12). A template's
14
+ * level must be readable from ONE fixed list; a soft list would make the same
15
+ * template Free or Pro depending on the key.
16
+ */
17
+ export interface TemplateManifest {
18
+ /** Template name. Equals this manifest's `TEMPLATE_MANIFESTS` key. */
19
+ name: string;
20
+ /**
21
+ * The modules whose PAGES this template's roster uses (parent § D15) — not
22
+ * the modules its level bundles. Typed against `ModuleName`, so an
23
+ * unrecognized name fails type-check rather than a runtime validation.
24
+ */
25
+ requires: readonly ModuleName[];
26
+ /** Base rows, before any module's contribution is merged. */
27
+ roster: readonly PageRosterRow[];
28
+ /** Bundle-relative source → scaffold-relative destination. */
29
+ installMap?: Record<string, string>;
30
+ }
31
+ /** Thrown on an undeclared template name or a route claimed twice. Mirrors `SurfaceError`'s shape. */
32
+ export declare class TemplateError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /**
36
+ * The three settled templates and their fixed `requires` lists — settled by
37
+ * Sam, 2026-09-14 (parent § D12). The plain name is the Pro one: it is the
38
+ * flagship and the demo.
39
+ *
40
+ * `workspace` appears in none of them. It is a Pro module, but an authed app
41
+ * surface contributing no PUBLIC page to a marketing site, and `requires`
42
+ * names what the roster uses (parent § D15). A Pro key unlocks it regardless
43
+ * of any template. `ModuleName` could not express it in any case —
44
+ * `RECOGNIZED_MODULES` is the six names and `workspace` is not among them.
45
+ *
46
+ * `commerce` is likewise in none of them: it is a per-site add-on available at
47
+ * any level, so a template for it would contradict the offering model and
48
+ * multiply the set combinatorially (parent § D13). It still contributes rows
49
+ * to whichever template takes it, through `mergeRoster` below.
50
+ */
51
+ export declare const TEMPLATE_MANIFESTS: Record<string, TemplateManifest>;
52
+ /** Look up a declared template. @throws TemplateError on an undeclared name. */
53
+ export declare function resolveTemplate(name: string): TemplateManifest;
54
+ /**
55
+ * Compose a template's roster: its base rows, then each taken module's
56
+ * contributed rows in `normalizeModules` order (parent § D7).
57
+ *
58
+ * `normalizeModules` is REUSED rather than re-sorted here. It already returns
59
+ * the unique set ordered by `RECOGNIZED_MODULES`, and `scaffold()` already
60
+ * calls it — a second sort at this call site would be a second ordering
61
+ * authority that could drift from the one `deriveArchitecture` depends on.
62
+ * That also makes the merge a pure function of the module SET: the same set in
63
+ * any argument order returns identical rows.
64
+ *
65
+ * The merge is pure and writes nothing. Staging a contributed row's seed
66
+ * content is the bundle producer's job (`build-module-bundles.mjs`, #5481);
67
+ * doing it here would put a filesystem dependency in a function three sibling
68
+ * units import for its return value.
69
+ *
70
+ * @param modules the modules this scaffold takes. A template's `requires` are
71
+ * taken by definition — a required module whose bundle cannot be had refuses
72
+ * the install outright (parent § D17, #5481) rather than merging short.
73
+ * @throws TemplateError when two sources claim one route (parent § D8). Both
74
+ * the base roster and every contribution are compile-time known, so the
75
+ * collision is caught by a test before a bundle exists — never resolved at
76
+ * runtime by a base-wins or module-wins precedence rule, either of which
77
+ * silently produces a site missing a page someone declared.
78
+ */
79
+ export declare function mergeRoster(manifest: TemplateManifest, modules: readonly string[]): PageRosterRow[];
80
+ /**
81
+ * Whether a template installs on the keyless path or needs a verified
82
+ * `AILK_LICENSE_KEY` — derived from `requires`, never declared (parent § D3).
83
+ *
84
+ * The derivation goes through the existing `classifyModules` rather than a
85
+ * second list of paid names, which would drift. This is deliberately the
86
+ * CLIENT half of a two-derivation split: the issuer derives the commercial
87
+ * LEVEL from the same `requires` via `TIER_SPECS` (#5480). One shared
88
+ * derivation would force this package to import
89
+ * `@working-theory/license-manifest` — a `restricted` package — breaking
90
+ * `pnpm install` for every Free consumer, not merely gating the paid path.
91
+ */
92
+ export declare function templateDelivery(manifest: TemplateManifest): "free" | "paid";
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Site-template registry, and the roster merge that composes a template with
3
+ * the modules it takes (spec 5478, unit A of the site-templates mechanism;
4
+ * design record docs/specs/5462-site-templates-module-bundles-spec.md).
5
+ *
6
+ * A site template is DATA AND CONTENT, NOT CODE: a page roster, seed content
7
+ * and config — never a `page.tsx` (parent § D16). This file declares what a
8
+ * template IS. It declares no delivery channel, reads no licence, and touches
9
+ * no disk: the `--template` flag, the bundle fetch and the landing order are
10
+ * #5481's, and `GET /v1/templates/:name/:version` is #5480's.
11
+ *
12
+ * A CODE registry, not per-template JSON (parent § D2): in TypeScript,
13
+ * `requires` is checked against `RECOGNIZED_MODULES` at compile time, and this
14
+ * package already twice records that a fixed template FILE is the drift a
15
+ * registry removes.
16
+ */
17
+ import { classifyModules } from "./lib/module-tier.js";
18
+ import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
19
+ /** Thrown on an undeclared template name or a route claimed twice. Mirrors `SurfaceError`'s shape. */
20
+ export class TemplateError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "TemplateError";
24
+ }
25
+ }
26
+ /**
27
+ * `/pricing`, `/privacy` and `/terms` — the three module-less rows every
28
+ * declared template brings itself.
29
+ *
30
+ * Module-less by construction (parent § D16): no module owns these surfaces,
31
+ * their route files live in `apps/web` and are snapshotted into the starter
32
+ * like every other route, and a template enables them by NAMING them in its
33
+ * roster. Registering the three as `GATEABLE_SURFACES` entries is #5479's; the
34
+ * rows are declared here because the roster is what a template is.
35
+ */
36
+ const TEMPLATE_BASE_ROSTER = [
37
+ { route: "/pricing", content: ["content/en/Pricing/pricing.mdx"] },
38
+ { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
39
+ { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
40
+ ];
41
+ /**
42
+ * The three settled templates and their fixed `requires` lists — settled by
43
+ * Sam, 2026-09-14 (parent § D12). The plain name is the Pro one: it is the
44
+ * flagship and the demo.
45
+ *
46
+ * `workspace` appears in none of them. It is a Pro module, but an authed app
47
+ * surface contributing no PUBLIC page to a marketing site, and `requires`
48
+ * names what the roster uses (parent § D15). A Pro key unlocks it regardless
49
+ * of any template. `ModuleName` could not express it in any case —
50
+ * `RECOGNIZED_MODULES` is the six names and `workspace` is not among them.
51
+ *
52
+ * `commerce` is likewise in none of them: it is a per-site add-on available at
53
+ * any level, so a template for it would contradict the offering model and
54
+ * multiply the set combinatorially (parent § D13). It still contributes rows
55
+ * to whichever template takes it, through `mergeRoster` below.
56
+ */
57
+ export const TEMPLATE_MANIFESTS = {
58
+ /**
59
+ * The Free template, and the mechanism's own proof (parent § D11): it
60
+ * requires only `marketing`, so the keyless merge path is exercised by a
61
+ * template we ship anyway rather than by a throwaway fixture.
62
+ */
63
+ "marketing-site-free": {
64
+ name: "marketing-site-free",
65
+ requires: ["marketing"],
66
+ roster: TEMPLATE_BASE_ROSTER,
67
+ },
68
+ /**
69
+ * The Pro flagship. Its module list is OFFERINGS § 4 offering 3's tiers line
70
+ * — "docs, scheduling and flows modules" — where "flows" is
71
+ * `signal-collectors` (`MODULE_SURFACES` maps it to `["flows"]`).
72
+ */
73
+ "marketing-site": {
74
+ name: "marketing-site",
75
+ requires: ["marketing", "docs-blog", "scheduling", "signal-collectors"],
76
+ roster: TEMPLATE_BASE_ROSTER,
77
+ },
78
+ /**
79
+ * Pro Plus — the Pro list plus `multi-tenant`, which is correctly in the
80
+ * list because it DOES contribute pages (the org and workspace surfaces).
81
+ * Its `pageContributions` is nonetheless unset today, deferred to its own
82
+ * instance, so this template's merged roster carries the same contributed
83
+ * rows as `marketing-site`'s and no more. That is a module which has
84
+ * declared no rows yet — visible in the diff — not a merge that dropped
85
+ * declared rows.
86
+ */
87
+ "saas-product": {
88
+ name: "saas-product",
89
+ requires: [
90
+ "marketing",
91
+ "docs-blog",
92
+ "scheduling",
93
+ "signal-collectors",
94
+ "multi-tenant",
95
+ ],
96
+ roster: TEMPLATE_BASE_ROSTER,
97
+ },
98
+ };
99
+ /** Look up a declared template. @throws TemplateError on an undeclared name. */
100
+ export function resolveTemplate(name) {
101
+ const manifest = TEMPLATE_MANIFESTS[name];
102
+ if (!manifest) {
103
+ throw new TemplateError(`unknown template "${name}". Declared: ${Object.keys(TEMPLATE_MANIFESTS).join(", ")}`);
104
+ }
105
+ return manifest;
106
+ }
107
+ /**
108
+ * Compose a template's roster: its base rows, then each taken module's
109
+ * contributed rows in `normalizeModules` order (parent § D7).
110
+ *
111
+ * `normalizeModules` is REUSED rather than re-sorted here. It already returns
112
+ * the unique set ordered by `RECOGNIZED_MODULES`, and `scaffold()` already
113
+ * calls it — a second sort at this call site would be a second ordering
114
+ * authority that could drift from the one `deriveArchitecture` depends on.
115
+ * That also makes the merge a pure function of the module SET: the same set in
116
+ * any argument order returns identical rows.
117
+ *
118
+ * The merge is pure and writes nothing. Staging a contributed row's seed
119
+ * content is the bundle producer's job (`build-module-bundles.mjs`, #5481);
120
+ * doing it here would put a filesystem dependency in a function three sibling
121
+ * units import for its return value.
122
+ *
123
+ * @param modules the modules this scaffold takes. A template's `requires` are
124
+ * taken by definition — a required module whose bundle cannot be had refuses
125
+ * the install outright (parent § D17, #5481) rather than merging short.
126
+ * @throws TemplateError when two sources claim one route (parent § D8). Both
127
+ * the base roster and every contribution are compile-time known, so the
128
+ * collision is caught by a test before a bundle exists — never resolved at
129
+ * runtime by a base-wins or module-wins precedence rule, either of which
130
+ * silently produces a site missing a page someone declared.
131
+ */
132
+ export function mergeRoster(manifest, modules) {
133
+ const merged = [];
134
+ /** route → the source that claimed it, for the collision message. */
135
+ const claimedBy = new Map();
136
+ const take = (rows, source) => {
137
+ for (const row of rows) {
138
+ const owner = claimedBy.get(row.route);
139
+ if (owner !== undefined) {
140
+ throw new TemplateError(`route "${row.route}" is claimed by both ${owner} and ${source}`);
141
+ }
142
+ claimedBy.set(row.route, source);
143
+ merged.push(row);
144
+ }
145
+ };
146
+ take(manifest.roster, `template "${manifest.name}"`);
147
+ for (const module of normalizeModules([...modules])) {
148
+ take(MODULE_SLICES[module].pageContributions ?? [], `module "${module}"`);
149
+ }
150
+ return merged;
151
+ }
152
+ /**
153
+ * Whether a template installs on the keyless path or needs a verified
154
+ * `AILK_LICENSE_KEY` — derived from `requires`, never declared (parent § D3).
155
+ *
156
+ * The derivation goes through the existing `classifyModules` rather than a
157
+ * second list of paid names, which would drift. This is deliberately the
158
+ * CLIENT half of a two-derivation split: the issuer derives the commercial
159
+ * LEVEL from the same `requires` via `TIER_SPECS` (#5480). One shared
160
+ * derivation would force this package to import
161
+ * `@working-theory/license-manifest` — a `restricted` package — breaking
162
+ * `pnpm install` for every Free consumer, not merely gating the paid path.
163
+ */
164
+ export function templateDelivery(manifest) {
165
+ return classifyModules([...manifest.requires]).paid.length > 0
166
+ ? "paid"
167
+ : "free";
168
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The `--template` flag's vocabulary, and nothing else.
3
+ *
4
+ * WHY THIS FILE EXISTS, measured rather than assumed. Spec 5481 § 3 DD1 has
5
+ * `parse-args.ts` validate `--template` against `TEMPLATE_MANIFESTS` through a
6
+ * runtime import, on the grounds that the registry's own dependency on
7
+ * `parse-args.ts` (for `ModuleName`) is `import type` and so erases at
8
+ * compile. That is true of the DIRECT edge and false of the graph:
9
+ * `template-manifest.ts` imports `lib/module-tier.js` and
10
+ * `module-architecture.js` as RUNTIME values, and BOTH of those import
11
+ * `RECOGNIZED_MODULES` — a runtime value — back from `parse-args.ts`.
12
+ * `module-tier.ts` then reads it at MODULE SCOPE to derive `PAID_MODULES`.
13
+ * So `parse-args.ts` importing the registry closes a runtime cycle whose
14
+ * first read lands in the temporal dead zone:
15
+ *
16
+ * ReferenceError: Cannot access 'RECOGNIZED_MODULES' before initialization
17
+ *
18
+ * — reproduced against the built `dist/` while implementing this unit, not
19
+ * theorised. `surfaces.ts`'s header warns about exactly this relationship
20
+ * ("Deliberately imports nothing from `parse-args.ts`: that module imports
21
+ * THIS one … so a back-import would close a cycle"), and `surfaces.ts` is the
22
+ * precedent this file follows: the flag's vocabulary lives in a module the
23
+ * parser can import because it imports nothing at all.
24
+ *
25
+ * THE REGISTRY IS STILL `template-manifest.ts` (parent record D2). This file
26
+ * declares NAMES; that one declares what a template IS — `requires`, the
27
+ * roster, the `installMap`, the free-vs-paid derivation — and it stays the
28
+ * single place a template is defined. The two cannot drift: the names here
29
+ * are asserted equal to `Object.keys(TEMPLATE_MANIFESTS)` by
30
+ * `__tests__/template-scaffold.test.ts`, so adding a template without listing
31
+ * it here (or the reverse) fails the suite rather than shipping a flag that
32
+ * accepts a name the registry does not know.
33
+ *
34
+ * Rejected: lazifying `module-tier.ts`'s `PAID_MODULES` to break the cycle at
35
+ * its source. It is the correct structural fix and it is not this unit's to
36
+ * make — `module-tier.ts` is a READ row in spec 5481 § 4, and `PAID_MODULES`
37
+ * is consumed as a plain array across the package.
38
+ */
39
+ /** The declared site templates, in `TEMPLATE_MANIFESTS` declaration order. */
40
+ export declare const RECOGNIZED_TEMPLATES: readonly ["marketing-site-free", "marketing-site", "saas-product"];
41
+ export type TemplateName = (typeof RECOGNIZED_TEMPLATES)[number];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The `--template` flag's vocabulary, and nothing else.
3
+ *
4
+ * WHY THIS FILE EXISTS, measured rather than assumed. Spec 5481 § 3 DD1 has
5
+ * `parse-args.ts` validate `--template` against `TEMPLATE_MANIFESTS` through a
6
+ * runtime import, on the grounds that the registry's own dependency on
7
+ * `parse-args.ts` (for `ModuleName`) is `import type` and so erases at
8
+ * compile. That is true of the DIRECT edge and false of the graph:
9
+ * `template-manifest.ts` imports `lib/module-tier.js` and
10
+ * `module-architecture.js` as RUNTIME values, and BOTH of those import
11
+ * `RECOGNIZED_MODULES` — a runtime value — back from `parse-args.ts`.
12
+ * `module-tier.ts` then reads it at MODULE SCOPE to derive `PAID_MODULES`.
13
+ * So `parse-args.ts` importing the registry closes a runtime cycle whose
14
+ * first read lands in the temporal dead zone:
15
+ *
16
+ * ReferenceError: Cannot access 'RECOGNIZED_MODULES' before initialization
17
+ *
18
+ * — reproduced against the built `dist/` while implementing this unit, not
19
+ * theorised. `surfaces.ts`'s header warns about exactly this relationship
20
+ * ("Deliberately imports nothing from `parse-args.ts`: that module imports
21
+ * THIS one … so a back-import would close a cycle"), and `surfaces.ts` is the
22
+ * precedent this file follows: the flag's vocabulary lives in a module the
23
+ * parser can import because it imports nothing at all.
24
+ *
25
+ * THE REGISTRY IS STILL `template-manifest.ts` (parent record D2). This file
26
+ * declares NAMES; that one declares what a template IS — `requires`, the
27
+ * roster, the `installMap`, the free-vs-paid derivation — and it stays the
28
+ * single place a template is defined. The two cannot drift: the names here
29
+ * are asserted equal to `Object.keys(TEMPLATE_MANIFESTS)` by
30
+ * `__tests__/template-scaffold.test.ts`, so adding a template without listing
31
+ * it here (or the reverse) fails the suite rather than shipping a flag that
32
+ * accepts a name the registry does not know.
33
+ *
34
+ * Rejected: lazifying `module-tier.ts`'s `PAID_MODULES` to break the cycle at
35
+ * its source. It is the correct structural fix and it is not this unit's to
36
+ * make — `module-tier.ts` is a READ row in spec 5481 § 4, and `PAID_MODULES`
37
+ * is consumed as a plain array across the package.
38
+ */
39
+ /** The declared site templates, in `TEMPLATE_MANIFESTS` declaration order. */
40
+ export const RECOGNIZED_TEMPLATES = [
41
+ "marketing-site-free",
42
+ "marketing-site",
43
+ "saas-product",
44
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "templates",
14
+ "site-templates",
14
15
  "component-catalog.md",
15
16
  "README.md",
16
17
  "LICENSE"
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "marketing-site-free",
3
+ "requires": ["marketing"],
4
+ "installMap": {
5
+ "provenance.json": ".ailk-template.json"
6
+ },
7
+ "postInstall": "Template marketing-site-free installed. Its seed content for /pricing, /privacy and /terms is in content/en/ - replace the placeholder copy before you launch."
8
+ }
@@ -0,0 +1,103 @@
1
+ ---
2
+ type: "Pricing"
3
+ slug: "pricing"
4
+ title: "Pricing"
5
+ description: "Replace this with one sentence naming how Your Company charges — the unit, the range, and what moves the number. It is the description answer engines quote."
6
+ sections:
7
+ [
8
+ {
9
+ "id": "hero",
10
+ "type": "hero",
11
+ "data":
12
+ {
13
+ "headline": "Pricing",
14
+ "subheadline": "Replace this with the one thing a buyer most needs to know about what they will pay.",
15
+ },
16
+ },
17
+ {
18
+ "id": "plans",
19
+ "type": "pricing",
20
+ "data":
21
+ {
22
+ "headline": "Replace these plans with your own",
23
+ "description": "Three placeholder plans. Rename them, set real prices, and list what each one actually includes. Delete any plan you do not offer — an empty tier is worse than one fewer.",
24
+ "plans":
25
+ [
26
+ {
27
+ "name": "Plan one",
28
+ "description": "Replace this with who this plan is for.",
29
+ "price": { "custom": "Your price" },
30
+ "features":
31
+ [
32
+ "Replace this line with something the plan includes.",
33
+ "One line per included thing.",
34
+ "Name the limit, not just the feature.",
35
+ ],
36
+ "cta": { "label": "Get in touch", "href": "/contact" },
37
+ },
38
+ {
39
+ "name": "Plan two",
40
+ "description": "Replace this with who this plan is for.",
41
+ "price": { "custom": "Your price" },
42
+ "features":
43
+ [
44
+ "Everything in plan one.",
45
+ "Replace this line with what the step up buys.",
46
+ "Say the number when there is one.",
47
+ ],
48
+ "cta": { "label": "Get in touch", "href": "/contact" },
49
+ "highlighted": true,
50
+ },
51
+ {
52
+ "name": "Plan three",
53
+ "description": "Replace this with who this plan is for.",
54
+ "price": { "custom": "Let's talk" },
55
+ "features":
56
+ [
57
+ "Everything in plan two.",
58
+ "Replace this line with the work you quote per engagement.",
59
+ "Scope, not adjectives.",
60
+ ],
61
+ "cta":
62
+ {
63
+ "label": "Book a call",
64
+ "href": "/contact",
65
+ "variant": "outline",
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ },
71
+ {
72
+ "id": "pricing-faq",
73
+ "type": "faq",
74
+ "data":
75
+ {
76
+ "headline": "Pricing questions",
77
+ "items":
78
+ [
79
+ {
80
+ "question": "Replace this with the pricing question you are actually asked.",
81
+ "answer": "Answer it in the first sentence, with the number in it. A pricing answer that opens with context is the one buyers stop reading.",
82
+ },
83
+ {
84
+ "question": "What happens when the scope changes?",
85
+ "answer": "Replace this with how you handle a change in scope — who decides, how it is priced, and when the new price starts. Say it plainly here and you will not have to say it awkwardly later.",
86
+ },
87
+ ],
88
+ },
89
+ },
90
+ {
91
+ "id": "cta",
92
+ "type": "cta",
93
+ "data":
94
+ {
95
+ "headline": "Not sure which plan fits?",
96
+ "description": "Replace this with an invitation to the conversation you actually want to have.",
97
+ "primaryCta": { "label": "Get in touch", "href": "/contact" },
98
+ },
99
+ },
100
+ ]
101
+ ---
102
+
103
+ {/* Rendered from frontmatter `sections[]`. Seed content — replace the plan names, prices and features above with your own before you launch. */}
@@ -0,0 +1,79 @@
1
+ ---
2
+ type: "PrivacyPage"
3
+ slug: "privacy"
4
+ title: "Privacy Policy"
5
+ description: "Placeholder privacy policy for Your Company. Replace every section with a policy that describes what your site actually collects before you launch."
6
+ sections:
7
+ [
8
+ {
9
+ "id": "privacy-document",
10
+ "type": "document-chassis",
11
+ "data":
12
+ {
13
+ "eyebrow": "PLACEHOLDER — REPLACE BEFORE LAUNCH",
14
+ "headline": "Privacy Policy",
15
+ "backHref": "/",
16
+ "backLabel": "Back home",
17
+ "navGroupLabel": "Contents",
18
+ "sections":
19
+ [
20
+ {
21
+ "id": "placeholder-notice",
22
+ "heading": "This is placeholder text",
23
+ "blocks":
24
+ [
25
+ {
26
+ "kind": "paragraph",
27
+ "text": "This page is starter content that shipped with your site. It is not a privacy policy, it makes no commitment on behalf of Your Company, and it is not legal advice. Replace every section below with a policy that describes what your site actually collects, and have someone qualified read it before you publish.",
28
+ },
29
+ {
30
+ "kind": "paragraph",
31
+ "text": "Until you do, leave this notice in place so nobody mistakes the page for a finished policy.",
32
+ },
33
+ ],
34
+ },
35
+ {
36
+ "id": "what-to-write-here",
37
+ "heading": "What belongs on this page",
38
+ "blocks":
39
+ [
40
+ {
41
+ "kind": "paragraph",
42
+ "text": "A privacy policy answers a short list of questions. Write each answer in plain language, and write it about the site you actually run — not about the site you plan to run.",
43
+ },
44
+ {
45
+ "kind": "list",
46
+ "listStyle": "number",
47
+ "items":
48
+ [
49
+ "Who operates this site, and how to reach them.",
50
+ "What the site collects — forms you publish, server logs, analytics.",
51
+ "Why you collect each thing, and what you do with it.",
52
+ "Which third parties process it on your behalf.",
53
+ "How long you keep it, and how someone asks for a copy or a deletion.",
54
+ ],
55
+ },
56
+ ],
57
+ },
58
+ {
59
+ "id": "contact",
60
+ "heading": "How to reach us",
61
+ "blocks":
62
+ [
63
+ {
64
+ "kind": "paragraph",
65
+ "text": "Replace this with the address you actually monitor — for example privacy@example.com — and say how quickly someone can expect a reply. A published address nobody watches is worse than no address.",
66
+ },
67
+ {
68
+ "kind": "paragraph",
69
+ "text": "Replace this with the date you last updated the policy, and say how you will announce future changes.",
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ ]
77
+ ---
78
+
79
+ {/* Rendered from frontmatter `sections[]`. Placeholder seed content — this is not a privacy policy and not legal advice. Replace every section before you launch. */}