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.
- package/README.md +1 -1
- package/dist/cli.js +54 -5
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/lib/extract-module-bundle.d.ts +20 -1
- package/dist/lib/extract-module-bundle.js +64 -5
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.js +1 -1
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/programmatic.js +14 -10
- package/package.json +3 -3
- package/site-templates/marketing-site/DESIGN.md +148 -0
- package/site-templates/marketing-site-free/DESIGN.md +148 -0
- package/site-templates/minimalism-site/DESIGN.md +148 -0
- package/site-templates/saas-product/DESIGN.md +148 -0
- package/site-templates/simple-service/DESIGN.md +148 -0
- package/templates/CLAUDE.md +2 -3
- package/templates/DESIGN.md +155 -0
- package/templates/README.md +42 -8
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
- package/templates/content/_site.mdx +18 -3
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +3 -2
- package/versions.json +33 -33
- package/templates/apps/web/public/logo-dark.svg +0 -21
- 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/
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/lib/pro-callout.js
CHANGED
|
@@ -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/
|
|
18
|
+
"free scaffolders one at a time → ailaunchkit.ai/pricing",
|
|
19
19
|
].join("\n");
|
|
20
20
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/programmatic.js
CHANGED
|
@@ -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.
|
|
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.
|
|
29
|
-
"@working-theory/templates": "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.
|