create-ailk 0.7.0 → 0.8.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 (169) hide show
  1. package/README.md +44 -10
  2. package/component-catalog.json +520 -5
  3. package/component-catalog.md +9 -6
  4. package/dist/cli.js +22 -24
  5. package/dist/lib/extract-module-bundle.js +5 -1
  6. package/dist/lib/module-license-gate.d.ts +79 -0
  7. package/dist/lib/module-license-gate.js +160 -12
  8. package/dist/lib/module-tier.d.ts +45 -16
  9. package/dist/lib/module-tier.js +76 -0
  10. package/dist/lib/pro-callout.js +10 -2
  11. package/dist/parse-args.d.ts +78 -10
  12. package/dist/parse-args.js +204 -14
  13. package/dist/programmatic.d.ts +39 -9
  14. package/dist/programmatic.js +163 -25
  15. package/dist/site-roster.js +11 -6
  16. package/dist/starter-grid.d.ts +216 -0
  17. package/dist/starter-grid.js +449 -0
  18. package/dist/surfaces.d.ts +4 -4
  19. package/dist/surfaces.js +102 -43
  20. package/dist/template-manifest.d.ts +36 -17
  21. package/dist/template-manifest.js +121 -367
  22. package/dist/template-names.d.ts +14 -12
  23. package/dist/template-names.js +13 -17
  24. package/package.json +3 -3
  25. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
  26. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  27. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  28. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  29. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  30. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  32. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  33. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  34. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  35. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  36. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  37. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  38. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  39. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  40. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  41. package/site-templates/designs/standard/DESIGN.md +5 -0
  42. package/site-templates/marketing-site/DESIGN.md +5 -0
  43. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
  44. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
  45. package/site-templates/marketing-site-free/DESIGN.md +5 -0
  46. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
  48. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  49. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  50. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
  51. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
  52. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  53. package/site-templates/saas-product/DESIGN.md +5 -0
  54. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  55. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
  56. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
  57. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  58. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  59. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  60. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  61. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  62. package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  63. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  64. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  65. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  66. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  67. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  68. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
  69. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
  70. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  71. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  72. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  73. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  74. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  75. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  76. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  77. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  78. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  79. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  80. package/site-templates/shapes/organization-service/provenance.json +4 -0
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  82. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  83. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  84. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  85. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  86. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  87. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  88. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  89. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  90. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  91. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  92. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  93. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  94. package/site-templates/shapes/person-service/provenance.json +4 -0
  95. package/templates/.claude/skills/README.md +3 -2
  96. package/templates/.claude/skills/add-page/references/page-types.md +4 -0
  97. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  98. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  99. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  100. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
  101. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  102. package/templates/DESIGN.md +5 -0
  103. package/templates/README.md +46 -38
  104. package/templates/apps/api/CLAUDE.md +3 -2
  105. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  106. package/templates/apps/api/src/config/index.ts +35 -16
  107. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  108. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  109. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  110. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  111. package/templates/apps/api/src/openapi/spec.ts +63 -12
  112. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  113. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  114. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  115. package/templates/apps/api/src/server.ts +7 -0
  116. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  117. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  118. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  119. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  120. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  121. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  122. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  123. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  124. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  125. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  126. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  127. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  128. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  129. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  130. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  131. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  132. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  133. package/templates/apps/web/app/[locale]/layout.tsx +37 -5
  134. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  135. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  136. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  137. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  138. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  139. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  140. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
  141. package/templates/apps/web/lib/nav-config.ts +48 -36
  142. package/templates/apps/web/lib/site-identity.ts +14 -0
  143. package/templates/apps/web/messages/en.json +28 -2
  144. package/templates/content/_site.mdx +4 -2
  145. package/templates/content/en/BlogList/blog.mdx +37 -16
  146. package/templates/content/en/ContactPage/contact.mdx +14 -44
  147. package/templates/content/en/HomePage/home.mdx +8 -16
  148. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  149. package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
  150. package/templates/database/CHANGELOG.md +7 -0
  151. package/templates/database/package.json +1 -1
  152. package/versions.json +33 -33
  153. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  154. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  155. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  156. package/site-templates/simple-service/DESIGN.md +0 -162
  157. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  158. package/site-templates/simple-service/ailk-template.json +0 -11
  159. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  160. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  161. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  162. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  163. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  164. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  165. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  166. package/site-templates/simple-service/provenance.json +0 -4
  167. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  168. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  169. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
@@ -9,7 +9,7 @@
9
9
  * and a licensed install already owns the tier it pitches.
10
10
  */
11
11
  import { normalizeModules } from "../module-architecture.js";
12
- import { resolveTemplate } from "../template-manifest.js";
12
+ import { SHAPE_STARTERS, resolveTemplate } from "../template-manifest.js";
13
13
  import { classifyModules } from "./module-tier.js";
14
14
  export function proCallout() {
15
15
  return [
@@ -34,7 +34,15 @@ export const LICENSED_PREREQUISITES_NOTE = "No database and no vendor API key ar
34
34
  * `templateDelivery` derives "paid" from `requires` alone.
35
35
  */
36
36
  export function isLicensedInstall(opts) {
37
- const requires = opts.template !== undefined ? resolveTemplate(opts.template).requires : [];
37
+ // #6757 D6 row 4 — a bare-shape starter's name (the shape itself, passed
38
+ // here after #6754 D4 item 3 resolves it) is looked up through
39
+ // SHAPE_STARTERS first; `resolveTemplate` alone throws `unknown template`
40
+ // for one (NFR-3).
41
+ const requires = opts.template !== undefined
42
+ ? Object.prototype.hasOwnProperty.call(SHAPE_STARTERS, opts.template)
43
+ ? SHAPE_STARTERS[opts.template].requires
44
+ : resolveTemplate(opts.template).requires
45
+ : [];
38
46
  const modules = normalizeModules([
39
47
  ...requires,
40
48
  ...(opts.modules ?? ["marketing"]),
@@ -1,3 +1,4 @@
1
+ import type { ScaffoldOptions } from "./programmatic.js";
1
2
  /**
2
3
  * The recognized module vocabulary for the `--modules` selector.
3
4
  *
@@ -9,23 +10,52 @@
9
10
  */
10
11
  export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs-blog", "multi-tenant", "workspace", "scheduling", "signal-collectors", "commerce"];
11
12
  export type ModuleName = (typeof RECOGNIZED_MODULES)[number];
13
+ /**
14
+ * Shapes a starter can be built from today (`--shape`, #6754 D1).
15
+ *
16
+ * Declared HERE rather than imported from `template-manifest.ts` for the
17
+ * reason `RECOGNIZED_MODULES` is: importing the registry closes the runtime
18
+ * cycle `template-names.ts`'s header records. `parse-args.test.ts` pins this
19
+ * list, `RESERVED_SHAPES` and `RECOGNIZED_DESIGNS` to their authorities
20
+ * (`STARTER_SHAPES`, `STARTER_DESIGNS`, `SHAPE_ROSTERS`).
21
+ */
22
+ export declare const RECOGNIZED_SHAPES: readonly ["organization-product", "organization-service", "local-business-service", "person-service"];
23
+ export type ShapeName = (typeof RECOGNIZED_SHAPES)[number];
24
+ /** Shapes the contract names but no release builds yet. */
25
+ export declare const RESERVED_SHAPES: readonly ["local-business-product"];
26
+ /** The `--design` vocabulary (#6754 D1) — `STARTER_DESIGNS`, pinned by test. */
27
+ export declare const RECOGNIZED_DESIGNS: readonly ["standard", "minimalism"];
28
+ export type DesignName = (typeof RECOGNIZED_DESIGNS)[number];
12
29
  export interface CliOptions {
13
30
  /** Positional target directory (the dir to scaffold into). Optional only
14
31
  * when `help` or `version` is set. */
15
32
  target: string | undefined;
16
33
  /**
17
34
  * 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).
35
+ * `RECOGNIZED_TEMPLATES` at parse time. Undefined when the flag is absent.
36
+ * The CLI never scaffolds a run naming neither a template nor a shape
37
+ * (#6754 D2 — refusal R1, or the shape prompt on a terminal).
21
38
  */
22
39
  template: string | undefined;
40
+ /** The starter's shape (`--shape`, #6754), normalized. Never set with `template`. */
41
+ shape?: ShapeName;
42
+ /** The starter's design (`--design`, #6754). Never set with `template`. */
43
+ design?: DesignName;
44
+ /**
45
+ * True when a scaffold run named neither `--shape` nor `--template` on a
46
+ * terminal: `resolveCliShape` must ask before anything is scaffolded.
47
+ */
48
+ needsShapePrompt: boolean;
23
49
  install: boolean;
24
50
  help: boolean;
25
51
  version: boolean;
26
- /** Selected modules (recognized: marketing, docs-blog, multi-tenant, scheduling).
27
- * Defaults to ['marketing'] when the flag is absent. */
28
- modules: string[];
52
+ /**
53
+ * Selected modules (recognized: marketing, docs-blog, multi-tenant,
54
+ * scheduling). `undefined` when `--modules` is absent — the key's level
55
+ * decides (#6759 D3); never read as `['marketing']` here. An empty or
56
+ * all-empty `--modules` value still names the Free set (`parseModules`).
57
+ */
58
+ modules: string[] | undefined;
29
59
  /** Explicitly requested route surfaces (`--surfaces`). Empty when the flag is
30
60
  * absent — the module set alone then decides (see surfaces.ts
31
61
  * `resolveSurfaces`). Validated, lowercased, deduped and sorted. */
@@ -58,16 +88,54 @@ export interface CliOptions {
58
88
  export declare class ParseError extends Error {
59
89
  constructor(message: string);
60
90
  }
91
+ /** The caller's terminal state (#6754 D2). */
92
+ export interface ParseEnv {
93
+ /** A human can answer a prompt: `process.stdin.isTTY === true` in cli.ts. */
94
+ interactive: boolean;
95
+ }
61
96
  export declare const helpText: string;
62
- export declare function parseCliArgs(argv: string[]): CliOptions;
97
+ /**
98
+ * Parse `create-ailk` argv. `env.interactive` defaults to false —
99
+ * fail-closed: a caller that does not assert a human is present gets refusal
100
+ * R1, never a prompt it cannot answer (#6754 D2).
101
+ */
102
+ export declare function parseCliArgs(argv: string[], env?: ParseEnv): CliOptions;
103
+ /** The streams the shape prompt reads and writes (a `PassThrough` pair in tests). */
104
+ export interface PromptIo {
105
+ input: NodeJS.ReadableStream;
106
+ output: NodeJS.WritableStream;
107
+ }
108
+ /**
109
+ * Ask once for a shape (#6754 D3): a number 1–4 or a shape name. A bad answer
110
+ * re-asks, naming what was wrong; the third bad answer, or a closed input,
111
+ * throws. There is no default — an empty line is a bad answer.
112
+ */
113
+ export declare function promptForShape(io: PromptIo): Promise<ShapeName>;
114
+ /**
115
+ * Fill `opts.shape` by prompting when, and only when, `needsShapePrompt` is
116
+ * set (#6754 D3). Help, version, sync and every run naming a shape or a
117
+ * template come back unchanged, with nothing written to `io.output`.
118
+ */
119
+ export declare function resolveCliShape(opts: CliOptions, io: PromptIo): Promise<CliOptions>;
120
+ /**
121
+ * The one mapping from parsed flags to `scaffold()` options (#6754 D4).
122
+ * `template`, `shape`, `design` and `license` are each spread only when set,
123
+ * so a `--template` run hands `scaffold()` the object it did before #6754.
124
+ *
125
+ * @throws ParseError when the shape prompt is unresolved or the target is
126
+ * unset — an unresolved prompt must never fall through to the bare overlay.
127
+ */
128
+ export declare function toScaffoldOptions(opts: CliOptions): ScaffoldOptions;
63
129
  /**
64
130
  * Parse a module CSV (the `--modules` or `--sync-architecture` value) into a
65
131
  * validated, normalized string[].
66
132
  *
67
133
  * Splits on `,`, trims and lowercases each entry, drops empties, and validates
68
- * each against RECOGNIZED_MODULES. An absent flag (or an all-empty value)
69
- * defaults to DEFAULT_MODULES (`['marketing']`) — reproducing today's scaffold.
134
+ * each against RECOGNIZED_MODULES. Takes a PRESENT value only — an ABSENT
135
+ * `--modules` flag is `undefined` (#6759 D3: the key's level decides) and
136
+ * never reaches this function; an all-empty value (`--modules ""`,
137
+ * `--modules ,`) still names the Free set, spelled inline here.
70
138
  *
71
139
  * @throws ParseError on an unrecognized module value, naming the recognized set.
72
140
  */
73
- export declare function parseModules(raw: string | undefined): string[];
141
+ export declare function parseModules(raw: string): string[];
@@ -7,6 +7,7 @@
7
7
  * On any parse / validation failure: throws ParseError. The orchestrator's
8
8
  * top-level catch turns this into a stderr write + non-zero exit.
9
9
  */
10
+ import { createInterface } from "node:readline/promises";
10
11
  import { parseArgs } from "node:util";
11
12
  import { assertLicenseKeyShape } from "./lib/license-key-files.js";
12
13
  import { RECOGNIZED_SURFACES, parseSurfaces } from "./surfaces.js";
@@ -29,8 +30,33 @@ export const RECOGNIZED_MODULES = [
29
30
  "signal-collectors",
30
31
  "commerce",
31
32
  ];
32
- /** The default module selection when `--modules` is absent (reproduces today's scaffold). */
33
- const DEFAULT_MODULES = ["marketing"];
33
+ /**
34
+ * Shapes a starter can be built from today (`--shape`, #6754 D1).
35
+ *
36
+ * Declared HERE rather than imported from `template-manifest.ts` for the
37
+ * reason `RECOGNIZED_MODULES` is: importing the registry closes the runtime
38
+ * cycle `template-names.ts`'s header records. `parse-args.test.ts` pins this
39
+ * list, `RESERVED_SHAPES` and `RECOGNIZED_DESIGNS` to their authorities
40
+ * (`STARTER_SHAPES`, `STARTER_DESIGNS`, `SHAPE_ROSTERS`).
41
+ */
42
+ export const RECOGNIZED_SHAPES = [
43
+ "organization-product",
44
+ "organization-service",
45
+ "local-business-service",
46
+ "person-service",
47
+ ];
48
+ /** Shapes the contract names but no release builds yet. */
49
+ export const RESERVED_SHAPES = ["local-business-product"];
50
+ /** The `--design` vocabulary (#6754 D1) — `STARTER_DESIGNS`, pinned by test. */
51
+ export const RECOGNIZED_DESIGNS = ["standard", "minimalism"];
52
+ /** What each shape is, for the prompt (D3). A shape without one is a type error. */
53
+ const SHAPE_DESCRIPTIONS = {
54
+ "organization-product": "a company selling a product",
55
+ "organization-service": "a company selling a service",
56
+ "local-business-service": "a local business selling a service",
57
+ "person-service": "a person selling their own service",
58
+ };
59
+ const SHAPE_LIST = RECOGNIZED_SHAPES.join(", ");
34
60
  export class ParseError extends Error {
35
61
  constructor(message) {
36
62
  super(message);
@@ -51,11 +77,31 @@ Arguments:
51
77
  Options:
52
78
  --modules <csv> Comma-separated list of modules to compose the site
53
79
  from (recognized: ${RECOGNIZED_MODULES.join(", ")}).
54
- Defaults to \`marketing\`, which reproduces today's
55
- scaffold. A module is a composable unit across the
56
- three tiers (data · API · presentation).
80
+ Trims or ADDS relative to the level your licence
81
+ key unlocks (#6759) — it never replaces it. With no
82
+ key, the level is Free (\`marketing\`). A Pro key
83
+ switches on docs-blog, scheduling and
84
+ signal-collectors too; Pro Plus adds multi-tenant.
85
+ Naming modules here keeps EXACTLY those, trimming
86
+ the level's default. Naming only \`commerce\` (a
87
+ per-site add-on) or \`workspace\` (included at Pro,
88
+ opt-in) ADDS it on top of the level's default
89
+ without trimming. A module your licence does not
90
+ include is refused, naming the level that does. A
91
+ module is a composable unit across the three tiers
92
+ (data · API · presentation).
93
+ --shape <shape> The kind of site to build (recognized:
94
+ ${SHAPE_LIST}).
95
+ Installs that shape's Free starter. A run naming
96
+ neither --shape nor --template asks for a shape on
97
+ a terminal, and refuses without one.
98
+ --design <design> The look to render the starter in (recognized:
99
+ ${RECOGNIZED_DESIGNS.join(", ")}). Defaults to the
100
+ starter's own.
57
101
  --template <name> The site template to install (recognized:
58
102
  ${RECOGNIZED_TEMPLATES.join(", ")}).
103
+ A template already fixes its shape and design, so
104
+ it cannot be combined with --shape or --design.
59
105
  A template is a page roster plus seed content and
60
106
  config — never code. Its required modules are ADDED
61
107
  to whatever \`--modules\` names, so the two compose.
@@ -103,8 +149,8 @@ Options:
103
149
  --version Print the scaffolder version.
104
150
 
105
151
  Examples:
106
- pnpm create ailk@latest my-site
107
- pnpm create ailk@latest /tmp/test-site --install
152
+ pnpm create ailk@latest my-site --shape organization-product
153
+ pnpm create ailk@latest /tmp/test-site --template marketing-site-free --install
108
154
 
109
155
  Next steps after a successful scaffold:
110
156
  cd <target-dir>
@@ -113,7 +159,12 @@ Next steps after a successful scaffold:
113
159
 
114
160
  Reference: ADR 0007 — \`pnpm create ailk@latest\` is the canonical install path.
115
161
  `;
116
- export function parseCliArgs(argv) {
162
+ /**
163
+ * Parse `create-ailk` argv. `env.interactive` defaults to false —
164
+ * fail-closed: a caller that does not assert a human is present gets refusal
165
+ * R1, never a prompt it cannot answer (#6754 D2).
166
+ */
167
+ export function parseCliArgs(argv, env = { interactive: false }) {
117
168
  let parsed;
118
169
  try {
119
170
  parsed = parseArgs({
@@ -123,6 +174,8 @@ export function parseCliArgs(argv) {
123
174
  options: {
124
175
  modules: { type: "string" },
125
176
  template: { type: "string" },
177
+ shape: { type: "string" },
178
+ design: { type: "string" },
126
179
  license: { type: "string" },
127
180
  surfaces: { type: "string" },
128
181
  "sync-architecture": { type: "string" },
@@ -140,8 +193,13 @@ export function parseCliArgs(argv) {
140
193
  const help = parsed.values.help === true;
141
194
  const version = parsed.values.version === true;
142
195
  const install = parsed.values.install === true;
143
- const modules = parseModules(parsed.values.modules);
196
+ // #6759 D3 — an absent flag stays `undefined` (the key's level decides);
197
+ // only a PRESENT value goes through `parseModules`.
198
+ const rawModules = parsed.values.modules;
199
+ const modules = rawModules === undefined ? undefined : parseModules(rawModules);
144
200
  const template = parseTemplate(parsed.values.template);
201
+ const shape = parseShape(parsed.values.shape);
202
+ const design = parseDesign(parsed.values.design);
145
203
  const surfaces = parseSurfaceList(parsed.values.surfaces);
146
204
  const syncArchitecture = parsed.values["sync-architecture"];
147
205
  const syncCatalog = parsed.values["sync-catalog"] === true;
@@ -163,6 +221,7 @@ export function parseCliArgs(argv) {
163
221
  return {
164
222
  target: undefined,
165
223
  template: undefined,
224
+ needsShapePrompt: false,
166
225
  install,
167
226
  help,
168
227
  version,
@@ -181,6 +240,7 @@ export function parseCliArgs(argv) {
181
240
  return {
182
241
  target: parsed.positionals[0],
183
242
  template,
243
+ needsShapePrompt: false,
184
244
  install,
185
245
  help,
186
246
  version,
@@ -197,9 +257,27 @@ export function parseCliArgs(argv) {
197
257
  if (parsed.positionals.length > 1) {
198
258
  throw new ParseError(`expected exactly one target directory, got ${parsed.positionals.length}`);
199
259
  }
260
+ // R3 — a template already fixes its shape and design.
261
+ if (template !== undefined && (shape !== undefined || design !== undefined)) {
262
+ const flags = shape !== undefined && design !== undefined
263
+ ? "--shape and --design"
264
+ : shape !== undefined
265
+ ? "--shape"
266
+ : "--design";
267
+ throw new ParseError(`--template cannot be combined with ${flags}: a template already fixes its shape and design. Pass --template alone, or --shape with an optional --design.`);
268
+ }
269
+ // R1 — no silent default: with neither a template nor a shape, ask on a
270
+ // terminal and refuse without one.
271
+ const needsShapePrompt = template === undefined && shape === undefined;
272
+ if (needsShapePrompt && !env.interactive) {
273
+ throw new ParseError(`no shape given, and there is no terminal to ask on. Pass --shape <shape> (one of: ${SHAPE_LIST}), or --template <name>.`);
274
+ }
200
275
  return {
201
276
  target: parsed.positionals[0],
202
277
  template,
278
+ ...(shape !== undefined ? { shape } : {}),
279
+ ...(design !== undefined ? { design } : {}),
280
+ needsShapePrompt,
203
281
  install,
204
282
  help,
205
283
  version,
@@ -211,6 +289,118 @@ export function parseCliArgs(argv) {
211
289
  ...(license !== undefined ? { license } : {}),
212
290
  };
213
291
  }
292
+ /**
293
+ * Validate `--shape` (#6754 D2 step 2): trimmed and lowercased. A reserved
294
+ * shape is refusal R2; any other unrecognized value names the recognized set.
295
+ */
296
+ function parseShape(raw) {
297
+ if (raw === undefined)
298
+ return undefined;
299
+ const name = raw.trim().toLowerCase();
300
+ if (RESERVED_SHAPES.includes(name)) {
301
+ throw new ParseError(`shape "${name}" is not available yet. Choose one of: ${SHAPE_LIST}.`);
302
+ }
303
+ if (!RECOGNIZED_SHAPES.includes(name)) {
304
+ throw new ParseError(`unknown shape "${raw}". Recognized: ${SHAPE_LIST}`);
305
+ }
306
+ return name;
307
+ }
308
+ /** Validate `--design` (#6754 D2 step 2): trimmed and lowercased. */
309
+ function parseDesign(raw) {
310
+ if (raw === undefined)
311
+ return undefined;
312
+ const name = raw.trim().toLowerCase();
313
+ if (!RECOGNIZED_DESIGNS.includes(name)) {
314
+ throw new ParseError(`unknown design "${raw}". Recognized: ${RECOGNIZED_DESIGNS.join(", ")}`);
315
+ }
316
+ return name;
317
+ }
318
+ /** How many answers the prompt takes before it gives up. */
319
+ const PROMPT_ATTEMPTS = 3;
320
+ /**
321
+ * Ask once for a shape (#6754 D3): a number 1–4 or a shape name. A bad answer
322
+ * re-asks, naming what was wrong; the third bad answer, or a closed input,
323
+ * throws. There is no default — an empty line is a bad answer.
324
+ */
325
+ export async function promptForShape(io) {
326
+ const width = Math.max(...RECOGNIZED_SHAPES.map((s) => s.length));
327
+ io.output.write([
328
+ "Which kind of site is this?",
329
+ ...RECOGNIZED_SHAPES.map((s, i) => ` ${i + 1}) ${s.padEnd(width)} ${SHAPE_DESCRIPTIONS[s]}`),
330
+ "",
331
+ ].join("\n"));
332
+ const noShape = new ParseError(`no shape chosen. Re-run with --shape <shape> (one of: ${SHAPE_LIST}).`);
333
+ const rl = createInterface({ input: io.input, terminal: false });
334
+ // The async iterator buffers every line, so an answer typed (or piped)
335
+ // ahead of its question is never dropped, and a closed input ends it.
336
+ const lines = rl[Symbol.asyncIterator]();
337
+ try {
338
+ for (let attempt = 0; attempt < PROMPT_ATTEMPTS; attempt++) {
339
+ io.output.write(`Shape [1-${RECOGNIZED_SHAPES.length} or name]: `);
340
+ const next = await lines.next();
341
+ if (next.done === true)
342
+ throw noShape;
343
+ const answer = next.value;
344
+ const picked = pickShape(answer);
345
+ if (picked !== undefined)
346
+ return picked;
347
+ const shown = answer.trim();
348
+ io.output.write(shown === ""
349
+ ? "No answer given.\n"
350
+ : `"${shown}" is not a shape. Answer 1-${RECOGNIZED_SHAPES.length} or a shape name.\n`);
351
+ }
352
+ throw noShape;
353
+ }
354
+ finally {
355
+ rl.close();
356
+ }
357
+ }
358
+ /** A prompt answer as a shape, or undefined when it names none. */
359
+ function pickShape(answer) {
360
+ const value = answer.trim().toLowerCase();
361
+ if (/^[0-9]+$/.test(value))
362
+ return RECOGNIZED_SHAPES[Number(value) - 1];
363
+ return RECOGNIZED_SHAPES.includes(value)
364
+ ? value
365
+ : undefined;
366
+ }
367
+ /**
368
+ * Fill `opts.shape` by prompting when, and only when, `needsShapePrompt` is
369
+ * set (#6754 D3). Help, version, sync and every run naming a shape or a
370
+ * template come back unchanged, with nothing written to `io.output`.
371
+ */
372
+ export async function resolveCliShape(opts, io) {
373
+ if (!opts.needsShapePrompt)
374
+ return opts;
375
+ const shape = await promptForShape(io);
376
+ return { ...opts, shape, needsShapePrompt: false };
377
+ }
378
+ /**
379
+ * The one mapping from parsed flags to `scaffold()` options (#6754 D4).
380
+ * `template`, `shape`, `design` and `license` are each spread only when set,
381
+ * so a `--template` run hands `scaffold()` the object it did before #6754.
382
+ *
383
+ * @throws ParseError when the shape prompt is unresolved or the target is
384
+ * unset — an unresolved prompt must never fall through to the bare overlay.
385
+ */
386
+ export function toScaffoldOptions(opts) {
387
+ if (opts.needsShapePrompt) {
388
+ throw new ParseError(`no shape given. Pass --shape <shape> (one of: ${SHAPE_LIST}), or --template <name>.`);
389
+ }
390
+ if (opts.target === undefined) {
391
+ throw new ParseError("missing target directory. Usage: pnpm create ailk@latest <target-dir>");
392
+ }
393
+ return {
394
+ targetDir: opts.target,
395
+ surfaces: opts.surfaces,
396
+ withCatalog: opts.withCatalog,
397
+ ...(opts.modules !== undefined ? { modules: opts.modules } : {}),
398
+ ...(opts.template !== undefined ? { template: opts.template } : {}),
399
+ ...(opts.shape !== undefined ? { shape: opts.shape } : {}),
400
+ ...(opts.design !== undefined ? { design: opts.design } : {}),
401
+ ...(opts.license !== undefined ? { license: opts.license } : {}),
402
+ };
403
+ }
214
404
  /**
215
405
  * Shape-check the `--license` value through the one validator `license set`
216
406
  * also uses, re-thrown as a ParseError. The message never repeats the value.
@@ -270,20 +460,20 @@ function parseSurfaceList(raw) {
270
460
  * validated, normalized string[].
271
461
  *
272
462
  * Splits on `,`, trims and lowercases each entry, drops empties, and validates
273
- * each against RECOGNIZED_MODULES. An absent flag (or an all-empty value)
274
- * defaults to DEFAULT_MODULES (`['marketing']`) — reproducing today's scaffold.
463
+ * each against RECOGNIZED_MODULES. Takes a PRESENT value only — an ABSENT
464
+ * `--modules` flag is `undefined` (#6759 D3: the key's level decides) and
465
+ * never reaches this function; an all-empty value (`--modules ""`,
466
+ * `--modules ,`) still names the Free set, spelled inline here.
275
467
  *
276
468
  * @throws ParseError on an unrecognized module value, naming the recognized set.
277
469
  */
278
470
  export function parseModules(raw) {
279
- if (raw === undefined)
280
- return [...DEFAULT_MODULES];
281
471
  const requested = raw
282
472
  .split(",")
283
473
  .map((m) => m.trim().toLowerCase())
284
474
  .filter(Boolean);
285
475
  if (requested.length === 0)
286
- return [...DEFAULT_MODULES];
476
+ return ["marketing"];
287
477
  for (const m of requested) {
288
478
  if (!RECOGNIZED_MODULES.includes(m)) {
289
479
  throw new ParseError(`unknown module "${m}". Recognized: ${RECOGNIZED_MODULES.join(", ")}`);
@@ -20,7 +20,7 @@ import { CATALOG_TARGET_PATH } from "./component-catalog.js";
20
20
  import { type ModuleName } from "./parse-args.js";
21
21
  import { type TemplateLayout } from "./site-chrome.js";
22
22
  import { type SyncRoutesResult } from "./sync-routes.js";
23
- import { type StarterDesign } from "./template-manifest.js";
23
+ import { type StarterDesign, type StarterShape } from "./template-manifest.js";
24
24
  export interface ScaffoldOptions {
25
25
  /** Absolute or relative path to the target directory. Must not exist or be empty. */
26
26
  targetDir: string;
@@ -36,9 +36,10 @@ export interface ScaffoldOptions {
36
36
  *
37
37
  * Consumed ONLY by the async `scaffold()` below. `scaffoldSync` stays
38
38
  * template-oblivious for exactly the reason it stays licensing-oblivious
39
- * (spec 5481 § 3 DD3): a call naming no template takes a code path with no
40
- * new statement in it, which is a stronger byte-identity guarantee than a
41
- * test asserting the output happens to match.
39
+ * (spec 5481 § 3 DD3): a call naming neither a template nor a `shape`
40
+ * takes a code path with no new statement in it — the bare overlay, the
41
+ * library's default. The CLI never takes that path (#6754 D2: it refuses a
42
+ * run naming neither, or asks for a shape on a terminal).
42
43
  *
43
44
  * What a template DOES change is `modules` — `scaffold()` unions the
44
45
  * template's `requires` with whatever `--modules` named and passes THAT
@@ -46,6 +47,13 @@ export interface ScaffoldOptions {
46
47
  * `architecture.yaml` and the generated nav are all functions of the union.
47
48
  */
48
49
  template?: string;
50
+ /**
51
+ * The starter shape to install (`--shape`, #6754 D5): `scaffold()` resolves
52
+ * it to the one template of that shape whose delivery is `free`, then runs
53
+ * exactly as a `template` naming it would. Mutually exclusive with
54
+ * `template`. A shape with no Free starter yet is refused (D6).
55
+ */
56
+ shape?: StarterShape;
49
57
  /**
50
58
  * Selected modules (recognized: marketing, docs-blog, multi-tenant,
51
59
  * scheduling). Defaults to
@@ -112,10 +120,11 @@ export interface ScaffoldOptions {
112
120
  /**
113
121
  * The design whose layout knobs fill what `layout` leaves unset (#6638,
114
122
  * `DESIGN_LAYOUTS`). `scaffold()` sets it from the resolved template's
115
- * `design`; a caller may name one to OVERRIDE the template's — the CI-only
123
+ * `design`; a caller may name one to OVERRIDE the template's — the
116
124
  * shape × design pairing `verify-scaffold.mjs --design` renders (#6638
117
- * DD-4). No CLI flag sets it. Unset (every existing `scaffoldSync` caller)
118
- * resolves exactly as before.
125
+ * DD-4), and the CLI's `--design` beside `--shape` (#6754). Unset (every
126
+ * existing `scaffoldSync` caller) resolves exactly as before. Gated
127
+ * (#6760): a design outside `FREE_DESIGNS` needs a Pro or Pro Plus level.
119
128
  */
120
129
  design?: StarterDesign;
121
130
  }
@@ -126,6 +135,21 @@ export interface ScaffoldResult {
126
135
  filesCreated: number;
127
136
  /** The gateable route surfaces this scaffold declares (sorted). */
128
137
  surfaces: string[];
138
+ /**
139
+ * The starter this scaffold resolved (#6754 D5) — set whenever a template
140
+ * was resolved, from `template` or from `shape`; absent for the bare overlay.
141
+ */
142
+ starter?: {
143
+ template: string;
144
+ shape: StarterShape;
145
+ design: StarterDesign;
146
+ };
147
+ /**
148
+ * The effective module set this scaffold installed (#6759 D6) — what
149
+ * `scaffoldSync` actually received, never the requested `--modules` alone:
150
+ * a keyed run with no `--modules` carries the key's derived level here.
151
+ */
152
+ modules: ModuleName[];
129
153
  }
130
154
  /**
131
155
  * Scaffold an AI Launch Kit starter project into targetDir (synchronous).
@@ -152,8 +176,14 @@ export declare function scaffoldSync(opts: ScaffoldOptions): ScaffoldResult;
152
176
  * its fetched bundle or — when it requires only free modules — from the
153
177
  * `site-templates/` tree this package ships.
154
178
  *
155
- * And, ahead of all five when `opts.template` is set (0): resolve the
156
- * template and union its `requires` with `opts.modules`, so every step above
179
+ * Between 2 and 3, `assertDesignAllowed` (#6760) refuses a design the caller
180
+ * CHOSE (`opts.design`) for a level below Pro, HERE, also before anything is
181
+ * written to disk. A template's own declared design is never gated (#6778
182
+ * option (c)): the template's `requires` already decide its level.
183
+ *
184
+ * And, ahead of all five when `opts.template` (or `opts.shape`, #6754 D5) is
185
+ * set (0): resolve the template and union its `requires` with `opts.modules`,
186
+ * so every step above
157
187
  * runs over the EFFECTIVE module set rather than the requested one.
158
188
  *
159
189
  * `scaffoldSync` itself stays licensing-oblivious on purpose: every existing