create-ailk 0.6.0 → 0.6.1

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +54 -5
  3. package/dist/copy.d.ts +14 -0
  4. package/dist/copy.js +38 -1
  5. package/dist/lib/extract-module-bundle.d.ts +20 -1
  6. package/dist/lib/extract-module-bundle.js +64 -5
  7. package/dist/lib/module-license-gate.d.ts +6 -1
  8. package/dist/lib/module-license-gate.js +6 -1
  9. package/dist/lib/pro-callout.js +1 -1
  10. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  11. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  12. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  13. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  14. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  15. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  16. package/dist/programmatic.js +14 -10
  17. package/package.json +3 -3
  18. package/site-templates/marketing-site/DESIGN.md +148 -0
  19. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  20. package/site-templates/minimalism-site/DESIGN.md +148 -0
  21. package/site-templates/saas-product/DESIGN.md +148 -0
  22. package/site-templates/simple-service/DESIGN.md +148 -0
  23. package/templates/CLAUDE.md +2 -3
  24. package/templates/DESIGN.md +155 -0
  25. package/templates/README.md +42 -8
  26. package/templates/apps/web/PRODUCT.md +86 -0
  27. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  28. package/templates/apps/web/lib/site-brand.tsx +4 -8
  29. package/templates/apps/web/middleware.ts +1 -1
  30. package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
  31. package/templates/content/_site.mdx +18 -3
  32. package/templates/database/CHANGELOG.md +7 -0
  33. package/templates/database/package.json +3 -2
  34. package/versions.json +33 -33
  35. package/templates/apps/web/public/logo-dark.svg +0 -21
  36. package/templates/apps/web/public/logo-light.svg +0 -21
package/README.md CHANGED
@@ -133,7 +133,7 @@ fetched at scaffold time. `resolvePaidModules` in
133
133
  `src/lib/module-license-gate.ts` reads `AILK_LICENSE_KEY` from the environment
134
134
  (or an injected `licenseKey` option) and refuses the run when it is unset or
135
135
  empty, naming the paid modules requested and pointing at
136
- `https://ailaunchkit.ai/pro`. An invalid or lapsed key is refused the same way,
136
+ `https://ailaunchkit.ai/pricing`. An invalid or lapsed key is refused the same way,
137
137
  from the module distribution's own 401/403 (`src/lib/fetch-module.ts`). The
138
138
  gate runs before anything is copied (`scaffold()` in `src/programmatic.ts`), so
139
139
  a refused run leaves no partial scaffold behind.
package/dist/cli.js CHANGED
@@ -52,6 +52,35 @@ function readRawFlagValue(argv, flag) {
52
52
  }
53
53
  return undefined;
54
54
  }
55
+ /**
56
+ * The prerequisites a freshly-scaffolded starter ACTUALLY has before
57
+ * `pnpm dev` / `pnpm build` will run (#6440 AC4).
58
+ *
59
+ * Read back off the scaffold that was just written rather than hard-coded
60
+ * here, so the printed floors can never drift from the ones the scaffold's own
61
+ * `engines` / `packageManager` fields enforce. Both fields are optional in
62
+ * principle; a missing one is simply not named, since naming a prerequisite
63
+ * the scaffold does not enforce is the other half of the defect this fixes.
64
+ */
65
+ function readScaffoldPrerequisites(targetAbs) {
66
+ try {
67
+ const pkg = JSON.parse(readFileSync(resolve(targetAbs, "package.json"), "utf8"));
68
+ const node = pkg.engines?.node;
69
+ // `packageManager` is "pnpm@<version>[+<integrity>]" — the version only.
70
+ const pnpm = pkg.packageManager?.startsWith("pnpm@")
71
+ ? pkg.packageManager.slice("pnpm@".length).split("+")[0]
72
+ : undefined;
73
+ return {
74
+ ...(node !== undefined ? { node } : {}),
75
+ ...(pnpm !== undefined ? { pnpm } : {}),
76
+ };
77
+ }
78
+ catch {
79
+ // A scaffold with no readable root package.json is a bug the copy step
80
+ // would already have thrown on; next-steps is not the place to fail.
81
+ return {};
82
+ }
83
+ }
55
84
  async function main() {
56
85
  const argv = process.argv.slice(2);
57
86
  // Subcommand dispatch, ahead of the flag parser: `sync-routes` has its own
@@ -166,14 +195,34 @@ async function main() {
166
195
  ? `Scaffolded AI Launch Kit starter from the "${opts.template}" template.`
167
196
  : "Scaffolded AI Launch Kit starter.",
168
197
  "",
169
- "Next steps:",
170
- ` cd ${targetAbs}`,
171
198
  ];
172
- if (!opts.install)
173
- lines.push(" pnpm install");
174
- lines.push(" pnpm dev");
199
+ // Prerequisites (#6440 AC4) — every one the scaffold enforces, and nothing
200
+ // it does not. A starter reaches `pnpm dev` AND `pnpm build` on a toolchain
201
+ // alone: the Prisma client is generated by `database`'s own `postinstall`,
202
+ // so there is no generator step to remember, and no database, licence key or
203
+ // vendor API key gates either command. Saying so is the point — the previous
204
+ // output named no prerequisites at all, which left a customer to discover
205
+ // the toolchain floor from a failure.
206
+ const prereqs = readScaffoldPrerequisites(targetAbs);
207
+ const toolchain = [
208
+ prereqs.node !== undefined ? `Node.js ${prereqs.node}` : undefined,
209
+ prereqs.pnpm !== undefined ? `pnpm ${prereqs.pnpm}` : "pnpm",
210
+ ].filter((part) => part !== undefined);
211
+ if (toolchain.length > 0) {
212
+ lines.push(`Prerequisites: ${toolchain.join(" and ")} — that is all.`);
213
+ lines.push("No database, no licence key and no vendor API key are needed to run or build this site.");
214
+ lines.push("");
215
+ }
216
+ lines.push("Next steps:");
217
+ lines.push(` cd ${targetAbs}`);
218
+ if (!opts.install) {
219
+ lines.push(" pnpm install # also generates the Prisma client");
220
+ }
221
+ lines.push(" pnpm dev # http://localhost:3000");
222
+ lines.push(" pnpm build # production build, when you are ready to deploy");
175
223
  lines.push("");
176
224
  lines.push("Open http://localhost:3000 to see the home page.");
225
+ lines.push("Adding auth, saved flows, checkout or database-backed content later? README.md \u00a7 \"When you need a database\" has the one-command setup.");
177
226
  lines.push("");
178
227
  lines.push(`Route surfaces: ${CORE_SURFACE_PATHS.join(", ")}` +
179
228
  (surfaces.length > 0 ? ` + ${surfaces.join(", ")}` : ""));
package/dist/copy.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import { type MarkerMap, type WorkspaceVersionMap } from "./substitute.js";
2
+ /**
3
+ * THE marker map every layer of a scaffold substitutes with (#6460) — the base
4
+ * `templates/` copy below, and (threaded through `installBundleTree`) the base
5
+ * layer and site-template trees that land on top of it. One definition, so a
6
+ * `{{MARKER}}` resolves to the same value whichever layer authored the file it
7
+ * sits in; a second copy would let the two drift silently.
8
+ */
9
+ export declare function buildScaffoldMarkers(projectName: string): MarkerMap;
2
10
  export interface CopyTemplateOptions {
3
11
  templatesDir: string;
4
12
  targetDir: string;
@@ -12,4 +20,10 @@ export interface CopyTemplateOptions {
12
20
  */
13
21
  excludePaths?: string[];
14
22
  }
23
+ /**
24
+ * Whether a scaffolded path's content is text we run markers over. Exported
25
+ * because the template/base layer (`installBundleTree`) substitutes with the
26
+ * same classifier — one definition, not two (#6460).
27
+ */
28
+ export declare function isTextFile(relativePath: string): boolean;
15
29
  export declare function copyTemplate(opts: CopyTemplateOptions): void;
package/dist/copy.js CHANGED
@@ -20,7 +20,39 @@
20
20
  */
21
21
  import { copyFileSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
22
22
  import { extname, join, relative, resolve, sep } from "node:path";
23
+ import pkg from "../package.json" with { type: "json" };
23
24
  import { substituteMarkers, substituteWorkspaceVersions, } from "./substitute.js";
25
+ /**
26
+ * This package's own `package.json` `"version"` — the value the
27
+ * `{{CREATE_AILK_VERSION}}` marker is stamped with at scaffold time (#6460).
28
+ * Deliberately a static JSON import rather than `import.meta.url` +
29
+ * `fileURLToPath`, for the reason `lib/fetch-module.ts`'s own
30
+ * `CREATE_AILK_VERSION` doc records: `copy.ts` is loaded directly by
31
+ * `copy.test.ts`, and `import.meta` is a SyntaxError under ts-jest's CommonJS
32
+ * transform.
33
+ */
34
+ const CREATE_AILK_VERSION = pkg.version;
35
+ /**
36
+ * THE marker map every layer of a scaffold substitutes with (#6460) — the base
37
+ * `templates/` copy below, and (threaded through `installBundleTree`) the base
38
+ * layer and site-template trees that land on top of it. One definition, so a
39
+ * `{{MARKER}}` resolves to the same value whichever layer authored the file it
40
+ * sits in; a second copy would let the two drift silently.
41
+ */
42
+ export function buildScaffoldMarkers(projectName) {
43
+ return {
44
+ PROJECT_NAME: projectName,
45
+ COMPANY_NAME: "Your Company",
46
+ COMPANY_DOMAIN: "example.com",
47
+ // The social-profile handle the Free chrome's footer links carry
48
+ // (`https://x.com/{{COMPANY_HANDLE}}`, #5469 D5) — a placeholder the
49
+ // site owner replaces, never the kit vendor's own handle.
50
+ COMPANY_HANDLE: "yourcompany",
51
+ // Stamped into each DESIGN.md header so an installed scaffold records the
52
+ // create-ailk it was generated by (#6460).
53
+ CREATE_AILK_VERSION,
54
+ };
55
+ }
24
56
  /**
25
57
  * Whether a walked template-relative path is inside (or equal to) an excluded
26
58
  * directory. Compares `/`-normalized paths so the check holds on Windows, and
@@ -65,7 +97,12 @@ const DOTFILES_AS_TEXT = new Set([
65
97
  ".prettierrc",
66
98
  ".eslintrc",
67
99
  ]);
68
- function isTextFile(relativePath) {
100
+ /**
101
+ * Whether a scaffolded path's content is text we run markers over. Exported
102
+ * because the template/base layer (`installBundleTree`) substitutes with the
103
+ * same classifier — one definition, not two (#6460).
104
+ */
105
+ export function isTextFile(relativePath) {
69
106
  const ext = extname(relativePath);
70
107
  if (TEXT_EXTS.has(ext))
71
108
  return true;
@@ -1,5 +1,5 @@
1
1
  import { type TemplateLayout } from "../site-chrome.js";
2
- import type { WorkspaceVersionMap } from "../substitute.js";
2
+ import { type MarkerMap, type WorkspaceVersionMap } from "../substitute.js";
3
3
  /** Exported for `fetch-template.ts`/`fetch-module.ts`'s Content-Length guard (#5514) — one cap, not two literals. */
4
4
  export declare const MAX_TOTAL_SIZE_BYTES: number;
5
5
  /**
@@ -42,6 +42,8 @@ export interface ExtractModuleBundleOptions {
42
42
  templateExtends?: string;
43
43
  /** The call-scoped ownership registry (#5618 D4). See `InstallBundleTreeOptions`. */
44
44
  ownership?: LayerOwnership;
45
+ /** The scaffold's marker map (#6460). See `InstallBundleTreeOptions`. */
46
+ markers?: MarkerMap;
45
47
  }
46
48
  /**
47
49
  * Extract a fetched module bundle into `targetDir`, merging its files onto
@@ -89,6 +91,23 @@ export interface InstallBundleTreeOptions {
89
91
  ownership?: LayerOwnership;
90
92
  /** Canonical `@working-theory/*` versions for `workspace:*` substitution in patch targets. */
91
93
  versions?: WorkspaceVersionMap;
94
+ /**
95
+ * The scaffold's marker map (#6460) — `copy.ts`'s `buildScaffoldMarkers`,
96
+ * the SAME object `copyTemplate` substituted the base `templates/` copy
97
+ * with, threaded here so a `{{MARKER}}` resolves to the same value whichever
98
+ * layer authored the file it sits in. Applied by `copyTree` and the
99
+ * installMap loop below, to text files only (`copy.ts`'s own `isTextFile`),
100
+ * so the classifier is one definition rather than two. Absent ⟹ files land
101
+ * byte-for-byte as committed, the pre-#6460 behaviour.
102
+ *
103
+ * EVERY layer that lands on the snapshot gets it, modules included — not
104
+ * just the base layer and the site template. A bundle fetched by version
105
+ * from the paid registry may have been produced by an older create-ailk
106
+ * that never saw this pass, so a bundle author should know that the five
107
+ * mapped keys, and only those, now resolve in their `.md`/`.json`/`.ts`
108
+ * files; `substituteMarkers` leaves every other `{{KEY}}` alone.
109
+ */
110
+ markers?: MarkerMap;
92
111
  }
93
112
  /** `installBundleTree`'s result — presently only the template-only `layout` knobs. */
94
113
  export interface InstallBundleTreeResult {
@@ -83,8 +83,10 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
83
83
  import { pipeline } from "node:stream/promises";
84
84
  import { createGunzip } from "node:zlib";
85
85
  import * as tar from "tar";
86
+ import { isTextFile } from "../copy.js";
86
87
  import { coOwnedModuleDestinations, } from "../module-architecture.js";
87
88
  import { validateTemplateLayout, } from "../site-chrome.js";
89
+ import { substituteMarkers, } from "../substitute.js";
88
90
  import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
89
91
  /** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
90
92
  const MAX_ENTRIES = 1000;
@@ -446,6 +448,7 @@ export async function extractModuleBundle(options) {
446
448
  : {}),
447
449
  ...(options.ownership !== undefined ? { ownership: options.ownership } : {}),
448
450
  ...(options.versions !== undefined ? { versions: options.versions } : {}),
451
+ ...(options.markers !== undefined ? { markers: options.markers } : {}),
449
452
  });
450
453
  }
451
454
  finally {
@@ -463,7 +466,7 @@ export async function extractModuleBundle(options) {
463
466
  * rejected tree leaves `targetDir` exactly as it was.
464
467
  */
465
468
  export function installBundleTree(options) {
466
- const { sourceRoot, name, templateRequires, templateExtends, ownership } = options;
469
+ const { sourceRoot, name, templateRequires, templateExtends, ownership, markers } = options;
467
470
  const targetAbs = resolve(options.targetDir);
468
471
  const isTemplate = templateRequires !== undefined;
469
472
  const subject = isTemplate ? `template "${name}"` : `module "${name}"`;
@@ -526,6 +529,17 @@ export function installBundleTree(options) {
526
529
  // installMap destination) is read from the STAGED tree here, since
527
530
  // targetDir would not have it yet at this point.
528
531
  const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
532
+ // #6460 — a patch target is read RAW from the staged tree here, while
533
+ // `copyTree` below writes the same path SUBSTITUTED, and
534
+ // `writeModulePatches` runs after `copyTree`. So a file a bundle both
535
+ // delivers as plain content AND patches would end up carrying unsubstituted
536
+ // markers. Unreachable today and asserted as such rather than guarded: the
537
+ // only patch targets any slice declares are `apps/web/package.json`,
538
+ // `next.config.mjs` and `jest.config.cjs`, all three delivered by the base
539
+ // `templates/` snapshot rather than by a bundle — so this reader falls
540
+ // through to the `targetAbs` branch and reads already-substituted bytes —
541
+ // and a template is refused `patches` outright above. A bundle that starts
542
+ // delivering one of its own patch targets must substitute here too.
529
543
  const readPatchTarget = (target) => {
530
544
  const mappedSource = installMapDestinationToSource.get(target);
531
545
  if (mappedSource !== undefined) {
@@ -550,11 +564,18 @@ export function installBundleTree(options) {
550
564
  // every installMap source are excluded from the whole-tree copy — the
551
565
  // manifest never lands in the scaffold, and a mapped source lands ONLY
552
566
  // at its mapped destination, never also at its original path.
553
- copyTree(sourceRoot, targetAbs, excluded);
567
+ copyTree(sourceRoot, targetAbs, excluded, markers);
554
568
  for (const [source, destination] of Object.entries(installMap)) {
555
569
  const destAbs = resolve(targetAbs, destination);
556
570
  mkdirSync(join(destAbs, ".."), { recursive: true });
557
- copyFileSync(resolve(sourceRoot, source), destAbs);
571
+ // BOTH ends decide text-ness (#6460), because an installMap entry is free
572
+ // to rename across the boundary and the two mismatches are not
573
+ // symmetric: a text source landing at a binary name merely misses a
574
+ // substitution, while a BINARY source landing at a text name would be
575
+ // read as utf8 and rewritten, turning every invalid byte sequence into
576
+ // U+FFFD — silent, unrecoverable corruption. Requiring both makes the
577
+ // safe direction structural rather than a property of today's manifests.
578
+ writeOneFile(resolve(sourceRoot, source), destAbs, [source, destination], markers);
558
579
  }
559
580
  // patches write AFTER every file the tree brought is in place (D5) —
560
581
  // after installMap, before postInstall — so a patch can never race a
@@ -589,8 +610,11 @@ export function installBundleTree(options) {
589
610
  * `safeExtractTarGz` has validated every entry, so no additional
590
611
  * path-safety check is needed here — `srcRoot` is our own staging tree, not
591
612
  * attacker-controlled input.
613
+ *
614
+ * `markers`, when given, is substituted into every TEXT file on the way
615
+ * through (#6460) — see `InstallBundleTreeOptions.markers`.
592
616
  */
593
- function copyTree(srcRoot, destRoot, excluded = new Set()) {
617
+ function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
594
618
  const stack = [srcRoot];
595
619
  while (stack.length > 0) {
596
620
  const dir = stack.pop();
@@ -608,12 +632,47 @@ function copyTree(srcRoot, destRoot, excluded = new Set()) {
608
632
  if (excluded.has(rel.split(sep).join("/")))
609
633
  continue;
610
634
  mkdirSync(join(destPath, ".."), { recursive: true });
611
- copyFileSync(srcPath, destPath);
635
+ writeOneFile(srcPath, destPath, [rel], markers);
612
636
  }
613
637
  // symlinks: never present — safeExtractTarGz rejects them outright.
614
638
  }
615
639
  }
616
640
  }
641
+ /**
642
+ * Land one file at `destAbs`, substituting `markers` into it when EVERY path
643
+ * in `relsForType` is text by `copy.ts`'s own `isTextFile` (#6460) — the
644
+ * source's relative path alone for a plain-content copy, source AND
645
+ * destination for an installMap entry, which may rename across the
646
+ * text/binary boundary. Unanimity rather than either-or, so binary content is
647
+ * never round-tripped through a utf8 string by construction; see the
648
+ * installMap loop for why the two mismatch directions are not symmetric.
649
+ *
650
+ * With no `markers`, or with any non-text path, this is exactly the
651
+ * `copyFileSync` it replaced.
652
+ *
653
+ * `isTextFile` is an allowlist of extensions, NOT "anything that parses as
654
+ * text": `.css`, `.html`, `.svg`, `.sh`, `.toml` and `.sql` are outside it, so
655
+ * a marker authored in one of those survives verbatim. That is pre-existing
656
+ * `copyTemplate` behaviour, deliberately shared rather than forked here.
657
+ *
658
+ * The source's mode rides along, because `copyFileSync` preserves it and
659
+ * `writeFileSync` does not: no tree this installs carries an executable file
660
+ * today, but a module slice is free to ship one, and a substituted `.mjs`
661
+ * silently losing its `+x` surfaces as "the scaffold builds but the hook never
662
+ * runs". Masked to the permission bits — `statSync().mode` carries the
663
+ * `S_IFREG` type bits too, which `open(2)` ignores but which have no business
664
+ * being passed. Like `copyFileSync`, this only sets the mode when the file is
665
+ * CREATED: an overwrite (the layer-over-layer case) keeps the existing one.
666
+ */
667
+ function writeOneFile(srcAbs, destAbs, relsForType, markers) {
668
+ if (markers === undefined || !relsForType.every(isTextFile)) {
669
+ copyFileSync(srcAbs, destAbs);
670
+ return;
671
+ }
672
+ writeFileSync(destAbs, substituteMarkers(readFileSync(srcAbs, "utf8"), markers), {
673
+ mode: statSync(srcAbs).mode & 0o777,
674
+ });
675
+ }
617
676
  /**
618
677
  * `extractBundle`'s validating-filter tar extraction, ported verbatim (see
619
678
  * file doc for why this is a port and not an import). Extracts `tarGzPath`
@@ -18,7 +18,12 @@
18
18
  * gate (K1's regression floor).
19
19
  */
20
20
  import type { ModuleName } from "../parse-args.js";
21
- export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pro";
21
+ /**
22
+ * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
23
+ * `/pro` route and there will not be one (#6439, operator decision OD-2) —
24
+ * pricing is where every level and the commerce add-on are actually listed.
25
+ */
26
+ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
22
27
  /**
23
28
  * Thrown for both honest-denial states (no key, and an invalid/lapsed key).
24
29
  * The CLI does not distinguish them in its exit code — both are "this run
@@ -1,5 +1,10 @@
1
1
  import { FetchModuleError, fetchModule } from "./fetch-module.js";
2
- export const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pro";
2
+ /**
3
+ * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
4
+ * `/pro` route and there will not be one (#6439, operator decision OD-2) —
5
+ * pricing is where every level and the commerce add-on are actually listed.
6
+ */
7
+ export const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
3
8
  /**
4
9
  * Thrown for both honest-denial states (no key, and an invalid/lapsed key).
5
10
  * The CLI does not distinguish them in its exit code — both are "this run
@@ -15,6 +15,6 @@ export function proCallout() {
15
15
  "your business, generates a vertical-aware page plan, configures",
16
16
  "Stripe / auth / lead routing / analytics, and sets up your",
17
17
  "GitHub→Vercel deploy — all in one orchestrated flow. Or invoke the",
18
- "free scaffolders one at a time → ailaunchkit.ai/pro",
18
+ "free scaffolders one at a time → ailaunchkit.ai/pricing",
19
19
  ].join("\n");
20
20
  }
@@ -19,7 +19,7 @@ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, } from
19
19
  import { basename, dirname, join, resolve } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog.js";
22
- import { copyTemplate } from "./copy.js";
22
+ import { buildScaffoldMarkers, copyTemplate } from "./copy.js";
23
23
  import { deriveArchitecture } from "./derive-architecture.js";
24
24
  import { extractModuleBundle, installBundleTree, } from "./lib/extract-module-bundle.js";
25
25
  import { fetchTemplate } from "./lib/fetch-template.js";
@@ -92,15 +92,7 @@ export function scaffoldSync(opts) {
92
92
  targetDir: targetAbs,
93
93
  versions,
94
94
  excludePaths: excludedRoutePaths(enabledSurfaces),
95
- markers: {
96
- PROJECT_NAME: projectName,
97
- COMPANY_NAME: "Your Company",
98
- COMPANY_DOMAIN: "example.com",
99
- // The social-profile handle the Free chrome's footer links carry
100
- // (`https://x.com/{{COMPANY_HANDLE}}`, #5469 D5) — a placeholder the
101
- // site owner replaces, never the kit vendor's own handle.
102
- COMPANY_HANDLE: "yourcompany",
103
- },
95
+ markers: buildScaffoldMarkers(projectName),
104
96
  });
105
97
  // Derive + write the module-set-driven architecture.yaml (spec #3787). This
106
98
  // is the single source of the scaffolded site's code-boundary config and
@@ -241,6 +233,14 @@ export async function scaffold(opts) {
241
233
  // `scaffoldSync` already reads for `copyTemplate`, re-read here since
242
234
  // `scaffold()` doesn't share `scaffoldSync`'s call frame.
243
235
  const versions = readVersionMap();
236
+ // #6460 — the SAME marker map `scaffoldSync` just substituted the base
237
+ // `templates/` copy with, rebuilt here from the same inputs (`scaffold()`
238
+ // does not share `scaffoldSync`'s call frame, exactly as `versions` above
239
+ // does not) and threaded into every layer that lands on top of it. Without
240
+ // it a `{{MARKER}}` authored in a base-layer or site-template file would
241
+ // survive into the scaffold verbatim — the state before this card, and the
242
+ // reason a template file could not carry the version stamp.
243
+ const markers = buildScaffoldMarkers(opts.projectName ?? basename(result.targetDir));
244
244
  // #5618 D4 — the file-grain ownership registry for THIS scaffold: which
245
245
  // layer wrote which destination. Call-scoped (a fresh Map per scaffold(),
246
246
  // never module-level), threaded through every layer install below in the
@@ -267,6 +267,7 @@ export async function scaffold(opts) {
267
267
  name: BASE_LAYER_DIR,
268
268
  templateRequires: [],
269
269
  versions,
270
+ markers,
270
271
  ownership,
271
272
  });
272
273
  }
@@ -276,6 +277,7 @@ export async function scaffold(opts) {
276
277
  targetDir: result.targetDir,
277
278
  moduleName,
278
279
  versions,
280
+ markers,
279
281
  ownership,
280
282
  });
281
283
  }
@@ -305,6 +307,7 @@ export async function scaffold(opts) {
305
307
  templateRequires: template.requires,
306
308
  templateExtends: template.extends,
307
309
  versions,
310
+ markers,
308
311
  ownership,
309
312
  })
310
313
  : (() => {
@@ -319,6 +322,7 @@ export async function scaffold(opts) {
319
322
  templateRequires: template.requires,
320
323
  templateExtends: template.extends,
321
324
  versions,
325
+ markers,
322
326
  ownership,
323
327
  });
324
328
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",
@@ -25,8 +25,8 @@
25
25
  "tar": "^7.5.21"
26
26
  },
27
27
  "devDependencies": {
28
- "@working-theory/schema": "0.25.0",
29
- "@working-theory/templates": "0.25.0"
28
+ "@working-theory/schema": "0.26.0",
29
+ "@working-theory/templates": "0.26.0"
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: Marketing site (Pro) — marketing-site
3
+ description: The Pro flagship: the Standard marketing pages plus blog, docs, booking and signal-collector pages, on the base foundation.
4
+ colors:
5
+ bg: "#ffffff"
6
+ fg: "#000000"
7
+ surface: "#ffffff"
8
+ surface-secondary: "#ffffff"
9
+ border: "#000000"
10
+ text-muted: "#333333"
11
+ brand: "#000000"
12
+ fg-on-brand: "#ffffff"
13
+ accent: "#000000"
14
+ bg-dark: "#000000"
15
+ fg-dark: "#ffffff"
16
+ border-dark: "#ffffff"
17
+ brand-dark: "#ffffff"
18
+ typography:
19
+ display:
20
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
21
+ fontSize: "6rem"
22
+ fontWeight: 400
23
+ lineHeight: 1
24
+ letterSpacing: "-0.05em"
25
+ heading-section:
26
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
27
+ fontSize: "2.25rem"
28
+ fontWeight: 400
29
+ lineHeight: 1.25
30
+ letterSpacing: "-0.05em"
31
+ heading-item:
32
+ fontSize: "1.25rem"
33
+ fontWeight: 600
34
+ lineHeight: 1.375
35
+ body:
36
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
37
+ fontSize: "1.125rem"
38
+ fontWeight: 400
39
+ lineHeight: 1.5
40
+ eyebrow:
41
+ fontSize: "0.875rem"
42
+ fontWeight: 600
43
+ lineHeight: 1.4
44
+ letterSpacing: "0.05em"
45
+ rounded:
46
+ button: "0.25rem"
47
+ card: "0.75rem"
48
+ spacing:
49
+ section: "4rem"
50
+ card: "1.5rem"
51
+ grid: "1.5rem"
52
+ container: "1280px"
53
+ ---
54
+
55
+ <!--
56
+ Source template: marketing-site
57
+ Version: create-ailk {{CREATE_AILK_VERSION}}
58
+ Extends: _base — installing this template replaces the base scaffold's
59
+ DESIGN.md with this file. Its tokens are the base foundation's, unchanged:
60
+ this template carries no theme of its own.
61
+
62
+ The tokens above describe what the site RENDERS today. Colours come from
63
+ content/_site.mdx (`theme.colors`); type, radius and spacing are platform
64
+ defaults a site cannot override yet — see "Where this is heading" below.
65
+ -->
66
+
67
+ # Marketing site (Pro)
68
+
69
+ ## Overview
70
+
71
+ The Pro flagship template. It carries the same nine-beat home and marketing pages as Standard (Free), and adds the pages its four Pro modules bring: the blog, the docs, the booking pages under `/meet`, and the signal-collector surfaces. It runs on the base foundation unchanged.
72
+
73
+ ## Colors
74
+
75
+ ### Primary
76
+
77
+ **Black** (`brand` `#000000`) fills the primary button; its label is white. The secondary button is white with a black hairline and black label.
78
+
79
+ ### Neutral
80
+
81
+ Pure white ground (`bg` `#ffffff`), pure black ink for headings (`fg` `#000000`), and near-black `#333333` for body copy — well above AA on white. **Borders are black** (`#000000`): card outlines, the rule under the navigation, the rules between sections and the rule above the footer are all 1px black lines. Cards are white, not tinted (`surface-secondary` is white too). There is no accent colour — `accent` is black.
82
+
83
+ Dark mode inverts: black ground, white ink, white rules, white primary button with black label.
84
+
85
+ ### Named Rules
86
+
87
+ - **Rules, not fills.** A container is drawn with a black hairline on the white ground. Grey fills are reserved for image placeholders.
88
+
89
+ ## Typography
90
+
91
+ System sans throughout; no web font.
92
+
93
+ ### Hierarchy
94
+
95
+ | Role | Size | Weight | Notes |
96
+ | --------------- | -------- | ------ | --------------------------------------------------------------------------- |
97
+ | Hero headline | 6rem | 400 | Tracking -0.05em, line-height 1 |
98
+ | Section heading | 2.25rem | 400 | Tracking -0.05em — light and tight, not bold |
99
+ | Item heading | 1.25rem | 600 | Card, feature and step titles |
100
+ | Body | 1.125rem | 400 | `#333333`, line-height 1.5 |
101
+ | Section label | 0.875rem | 600 | Uppercase, +0.05em tracking today — the small label above a section heading |
102
+
103
+ Measured from the rendered scaffold (1440px wide), not transcribed from tokens.
104
+
105
+ The small label above a section heading is **kept** as part of this foundation's voice (operator-disposed 2026-09-22) and is headed toward the treatment below.
106
+
107
+ ## Layout
108
+
109
+ Full navigation with the announcement banner; a multi-column footer; the default home. Every page and its sections are listed in `TEMPLATE-MANIFEST.md` in this directory. Shared foundation: a 1280px container, sections at 4rem, card grids at 1.5rem, breakpoints 320 / 768 / 1024 / 1440.
110
+
111
+ ## Shapes
112
+
113
+ Two radii today: buttons at 0.25rem, cards and outlined boxes at 0.75rem. Headed toward square corners — see below.
114
+
115
+ ## Components
116
+
117
+ ### Buttons
118
+
119
+ Primary: black fill, white label, 0.25rem corners. Secondary: transparent with a black 1px outline and black label. Both are the library `Button`.
120
+
121
+ ### Cards / Containers
122
+
123
+ White panels with a black 1px outline, 0.75rem corners, `card` (1.5rem) padding. The one exception today: the "how it works" steps render as dark filled cards (see below).
124
+
125
+ ### Navigation
126
+
127
+ Full navigation: wordmark left, page links, an outlined and a filled button at the far right, the announcement banner above. A black hairline separates it from the page.
128
+
129
+ ## Where this is heading
130
+
131
+ The foundation's pinned direction is the pacing of a classic wireframe marketing kit — `standardissue.webflow.io` is the reference for rhythm, type scale, spacing density and section pacing only; no image, copy or code is taken from it. Six parts of that direction cannot be expressed through a site's theme today, so they wait on a platform change rather than being hand-coded. Until they land, this file describes the rendered site above, not these targets.
132
+
133
+ | Target | Reference value | Blocked by |
134
+ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
135
+ | Square corners on buttons, cards and inputs | radius 0 | A site's `theme:` block accepts colours only — no radius |
136
+ | Bold, tighter headings | hero ~3.6rem and section headings ~2.9rem, both at weight 700 with tracking about -0.02em (today 6rem and 2.25rem at weight 400) | No per-site type-role override |
137
+ | Generous section pacing | about 10rem between sections, not 4rem | No per-site section-spacing override; `spacious` (6rem) is not author-selectable |
138
+ | Section label in sentence case, bold, no tracking | "Core Features", not "CORE FEATURES" | The eyebrow role bakes in uppercase and tracking |
139
+ | Whitespace between sections, not rules | no rule between bands | Section dividers share the one `border` colour with card outlines, so black outlines make black dividers |
140
+ | Outlined steps in "how it works" | three plain outlined columns | The steps section renders dark filled cards; no outlined variant |
141
+
142
+ ## Do's and Don'ts
143
+
144
+ - **Do** keep every page built from registered sections. If a section you need does not exist, request it rather than writing one-off markup.
145
+ - **Do** change colours in `content/_site.mdx` `theme.colors`; they reach every section.
146
+ - **Don't** add a colour accent. The foundation's one idea is black on white; a brand pass adds colour deliberately, through the theme.
147
+ - **Don't** replace a black rule with a shadow or a grey fill.
148
+ - **Don't** hard-code the "heading toward" values above into a page. They arrive through the platform, or not at all.