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
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
|
+
"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. */}
|