create-ailk 0.10.0 → 0.11.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 (103) hide show
  1. package/README.md +7 -2
  2. package/dist/cli.js +5 -1
  3. package/dist/content-payloads.d.ts +37 -0
  4. package/dist/content-payloads.js +64 -0
  5. package/dist/lib/fetch-module.d.ts +3 -3
  6. package/dist/lib/fetch-module.js +11 -5
  7. package/dist/lib/fetch-template.d.ts +1 -1
  8. package/dist/lib/fetch-template.js +7 -4
  9. package/dist/lib/scaffold-render-sweep.d.ts +16 -0
  10. package/dist/lib/scaffold-render-sweep.js +52 -3
  11. package/dist/module-architecture.d.ts +9 -0
  12. package/dist/module-architecture.js +43 -12
  13. package/dist/parse-args.d.ts +2 -0
  14. package/dist/parse-args.js +7 -0
  15. package/dist/surfaces.js +2 -3
  16. package/package.json +3 -3
  17. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +4 -3
  18. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +8 -8
  19. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +8 -8
  20. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +8 -8
  21. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +8 -8
  22. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +8 -8
  23. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +8 -8
  24. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +8 -8
  25. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +8 -8
  26. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +8 -8
  27. package/templates/.claude/settings.json +12 -0
  28. package/templates/.env.example +1 -5
  29. package/templates/CLAUDE.md +38 -22
  30. package/templates/README.md +12 -12
  31. package/templates/apps/api/jest.config.cjs +1 -0
  32. package/templates/apps/api/package.json +2 -1
  33. package/templates/apps/mcp/CLAUDE.md +23 -10
  34. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +160 -17
  35. package/templates/apps/mcp/__tests__/index.test.ts +118 -1
  36. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +425 -5
  37. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +67 -3
  38. package/templates/apps/mcp/__tests__/server.test.ts +162 -1
  39. package/templates/apps/mcp/package.json +1 -1
  40. package/templates/apps/mcp/src/index.ts +35 -3
  41. package/templates/apps/mcp/src/lib/commerce-claim.ts +193 -24
  42. package/templates/apps/mcp/src/server.ts +77 -5
  43. package/templates/apps/mcp/src/tools/purchase.ts +1 -1
  44. package/templates/apps/web/CLAUDE.md +1 -1
  45. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +15 -0
  46. package/templates/apps/web/app/.well-known/mcp.json/route.ts +10 -3
  47. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +0 -10
  48. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +0 -10
  49. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +1 -14
  50. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +0 -10
  51. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +0 -10
  52. package/templates/apps/web/app/[locale]/for/[slug]/page.tsx +0 -10
  53. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +1 -14
  54. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +0 -10
  55. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +0 -10
  56. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +0 -10
  57. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +0 -10
  58. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +1 -15
  59. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +0 -10
  60. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +0 -10
  61. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +0 -10
  62. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +1 -14
  63. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +4 -16
  64. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +3 -15
  65. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +0 -10
  66. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +1 -14
  67. package/templates/apps/web/src/lib/commerce-claim.ts +15 -6
  68. package/templates/content/_site.mdx +23 -4
  69. package/templates/database/CHANGELOG.md +7 -0
  70. package/templates/database/package.json +1 -1
  71. package/versions.json +34 -34
  72. package/templates/.claude/skills/README.md +0 -37
  73. package/templates/.claude/skills/add-auth/SKILL.md +0 -313
  74. package/templates/.claude/skills/add-auth/references/env-vars.md +0 -106
  75. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +0 -180
  76. package/templates/.claude/skills/add-page/SKILL.md +0 -260
  77. package/templates/.claude/skills/add-page/references/page-types.md +0 -123
  78. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +0 -87
  79. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +0 -99
  80. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +0 -18
  81. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +0 -43
  82. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +0 -52
  83. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +0 -38
  84. package/templates/.claude/skills/check-site/SKILL.md +0 -245
  85. package/templates/.claude/skills/check-site/references/common-errors.md +0 -116
  86. package/templates/.claude/skills/check-site/references/findings-glossary.md +0 -140
  87. package/templates/.claude/skills/check-site/references/severity-levels.md +0 -114
  88. package/templates/.claude/skills/fill-page/SKILL.md +0 -199
  89. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +0 -80
  90. package/templates/.claude/skills/fill-page/references/section-sources.md +0 -41
  91. package/templates/.claude/skills/setup-database/SKILL.md +0 -156
  92. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +0 -154
  93. package/templates/.claude/skills/setup-deploy/SKILL.md +0 -312
  94. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +0 -204
  95. package/templates/.claude/skills/setup-deploy/references/vercel-json-template.md +0 -98
  96. package/templates/.claude/skills/update-page/SKILL.md +0 -117
  97. package/templates/.claude/skills/update-page/scripts/update-page.ts +0 -640
  98. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +0 -31
  99. package/templates/.claude/skills/upgrade-plan/SKILL.md +0 -121
  100. package/templates/.claude/skills/upgrade-plan/references/options.md +0 -24
  101. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +0 -527
  102. package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +0 -75
  103. package/templates/apps/mcp/src/lib/require-paid-tier.ts +0 -44
package/README.md CHANGED
@@ -275,8 +275,13 @@ gate runs before anything is copied (`scaffold()` in `src/programmatic.ts`), so
275
275
  a refused run leaves no partial scaffold behind.
276
276
 
277
277
  `AILK_MODULE_REGISTRY_URL` is optional: `fetchModule` uses an explicitly passed
278
- `registryUrl`, then that variable, then the built-in
279
- `DEFAULT_AILK_MODULE_REGISTRY_URL` (`src/lib/fetch-module.ts`).
278
+ `registryUrl`, then that variable, then `https://api.ailaunchkit.ai/v1/modules`
279
+ (`DEFAULT_AILK_MODULE_REGISTRY_URL`, `src/lib/fetch-module.ts`).
280
+ `AILK_TEMPLATE_REGISTRY_URL` is optional the same way: `fetchTemplate` uses an
281
+ explicitly passed `registryUrl`, then that variable, then
282
+ `https://api.ailaunchkit.ai/v1/templates` (`DEFAULT_AILK_TEMPLATE_REGISTRY_URL`,
283
+ `src/lib/fetch-template.ts`). Neither client follows a redirect: a 3xx from
284
+ either registry fails the fetch before any second request is sent.
280
285
 
281
286
  ```bash
282
287
  # free-only — no key read, no network call
package/dist/cli.js CHANGED
@@ -46,7 +46,7 @@ import { buildScaffoldMarkers } from "./copy.js";
46
46
  import { assertReleased } from "./lib/module-license-gate.js";
47
47
  import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
48
48
  import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
49
- import { helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
49
+ import { PLUGIN_INSTALL_LINES, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
50
50
  import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
51
51
  import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
52
52
  import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
@@ -381,6 +381,10 @@ async function main() {
381
381
  lines.push("Open http://localhost:3000 to see the home page.");
382
382
  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.");
383
383
  lines.push("");
384
+ lines.push("Claude Code: the AI Launch Kit skills come from the ailk plugin. Trust this folder and Claude Code loads it, or run in a session:");
385
+ for (const line of PLUGIN_INSTALL_LINES)
386
+ lines.push(` ${line}`);
387
+ lines.push("");
384
388
  lines.push(`Route surfaces: ${CORE_SURFACE_PATHS.join(", ")}` +
385
389
  (surfaces.length > 0 ? ` + ${surfaces.join(", ")}` : ""));
386
390
  lines.push("Add another with `create-ailk add-route <id>`, or re-run with --surfaces <id>.");
@@ -0,0 +1,37 @@
1
+ /** The issuer's content kinds (spec 7110 § R1), pinned equal to CONTENT_KINDS by the issuer's test. */
2
+ export declare const CONTENT_PAYLOAD_KINDS: readonly ["skill", "profile", "template-set", "instructions"];
3
+ export type ContentPayloadKind = (typeof CONTENT_PAYLOAD_KINDS)[number];
4
+ export interface ContentPayload {
5
+ readonly kind: ContentPayloadKind;
6
+ /** ^[a-z0-9][a-z0-9-]*$ — the route's :name and the store's path segment. */
7
+ readonly name: string;
8
+ /** The ONE paid feature the issuer's CONTENT_FEATURES row maps this name to. */
9
+ readonly feature: string;
10
+ /** SemVer — the content version, never create-ailk's (spec 7131 § D1). */
11
+ readonly version: string;
12
+ /**
13
+ * 64-hex `contentDigest` of `files` at `version` (§ D5). A change to any
14
+ * payload byte needs a new `version` AND a new `digest`; the build refuses
15
+ * until this agrees with the tree.
16
+ */
17
+ readonly digest: string;
18
+ /** Repo-relative, POSIX; each file's source is `${sourceRoot}/${file}`. */
19
+ readonly sourceRoot: string;
20
+ /** Archive paths, POSIX, relative, sorted, unique — listed, never globbed. */
21
+ readonly files: readonly string[];
22
+ }
23
+ export declare const CONTENT_PAYLOADS: readonly ContentPayload[];
24
+ /** The producer-side cap on one payload's archive (spec 7131 § D6). */
25
+ export declare const MAX_CONTENT_PAYLOAD_BYTES: number;
26
+ /** The keyed route's public base; `AILK_CONTENT_REGISTRY_URL` overrides it. */
27
+ export declare const DEFAULT_AILK_CONTENT_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/content";
28
+ /**
29
+ * sha256 over the sorted `${path}\0${sha256hex(bytes)}\n` lines (§ D5).
30
+ * Covers archive paths and file bytes, never the archive's own bytes — gzip
31
+ * and tar headers differ between machines and must not decide whether the
32
+ * content changed.
33
+ */
34
+ export declare function contentDigest(files: ReadonlyArray<{
35
+ path: string;
36
+ bytes: Uint8Array;
37
+ }>): string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The content-payload manifest (spec 7131 § D2): every licence-gated content
3
+ * payload the issuer's keyed route `GET /v1/content/:kind/:name/:version`
4
+ * serves (spec 7110) — its kind, name, feature, content version, content
5
+ * digest and source files. The ONE place the builder
6
+ * (`scripts/build-content-payloads.mjs`), its `--verify` leg and the issuer's
7
+ * `CONTENT_FEATURES` agreement test read.
8
+ *
9
+ * A build-time registry, not programmatic API: deliberately not exported from
10
+ * `src/index.ts`. It carries names, a feature, a version and a digest — never
11
+ * a payload byte — so the public tarball's `dist/content-payloads.js` ships no
12
+ * paid content (spec 7131 § 7, AC-SPB-05).
13
+ *
14
+ * create-ailk is `oss`-tier and cannot import the issuer or the licence
15
+ * manifest, so the kind list is a pinned COPY of the issuer's `CONTENT_KINDS`
16
+ * and `feature` is a plain string; the issuer's `module-features.test.ts`
17
+ * fails when either drifts.
18
+ */
19
+ import { createHash } from "node:crypto";
20
+ /** The issuer's content kinds (spec 7110 § R1), pinned equal to CONTENT_KINDS by the issuer's test. */
21
+ export const CONTENT_PAYLOAD_KINDS = [
22
+ "skill",
23
+ "profile",
24
+ "template-set",
25
+ "instructions",
26
+ ];
27
+ export const CONTENT_PAYLOADS = Object.freeze([
28
+ Object.freeze({
29
+ kind: "template-set",
30
+ name: "scaffold-commerce",
31
+ feature: "commerce",
32
+ version: "1.0.0",
33
+ digest: "baa4ac6e86ec7e3b367eb67e77c3407da90c7ff9ef51d5f4e1185615194543ee",
34
+ sourceRoot: "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce",
35
+ files: Object.freeze([
36
+ "references/lookup-keys-template.md",
37
+ "references/marketplace-brand-pages.md",
38
+ "templates/checkout-route.template.ts",
39
+ "templates/invoice-route.template.ts",
40
+ "templates/marketplace-cta-route.tsx.tmpl",
41
+ "templates/payment-link-route.template.ts",
42
+ "templates/portal-route.template.ts",
43
+ "templates/stripe-config.template.ts",
44
+ "templates/subscription-route.template.ts",
45
+ ]),
46
+ }),
47
+ ]);
48
+ /** The producer-side cap on one payload's archive (spec 7131 § D6). */
49
+ export const MAX_CONTENT_PAYLOAD_BYTES = 256 * 1024;
50
+ /** The keyed route's public base; `AILK_CONTENT_REGISTRY_URL` overrides it. */
51
+ export const DEFAULT_AILK_CONTENT_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/content";
52
+ const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
53
+ /**
54
+ * sha256 over the sorted `${path}\0${sha256hex(bytes)}\n` lines (§ D5).
55
+ * Covers archive paths and file bytes, never the archive's own bytes — gzip
56
+ * and tar headers differ between machines and must not decide whether the
57
+ * content changed.
58
+ */
59
+ export function contentDigest(files) {
60
+ const lines = files
61
+ .map(({ path, bytes }) => `${path}\0${sha256Hex(bytes)}\n`)
62
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
63
+ return sha256Hex(lines.join(""));
64
+ }
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
1
+ export declare const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/modules";
2
2
  export interface FetchModuleOptions {
3
3
  moduleName: string;
4
4
  /** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
@@ -11,10 +11,10 @@ export interface FetchModuleOptions {
11
11
  fetchImpl?: typeof fetch;
12
12
  }
13
13
  export declare class FetchModuleError extends Error {
14
- /** True iff the failure was a license-gating signal (HTTP 401). */
14
+ /** True iff the failure was a license-gating signal (HTTP 401 or 403). */
15
15
  readonly isLicenseError: boolean;
16
16
  constructor(message: string,
17
- /** True iff the failure was a license-gating signal (HTTP 401). */
17
+ /** True iff the failure was a license-gating signal (HTTP 401 or 403). */
18
18
  isLicenseError?: boolean);
19
19
  }
20
20
  export declare function fetchModule(options: FetchModuleOptions): Promise<Uint8Array>;
@@ -9,8 +9,14 @@
9
9
  * distribution only.
10
10
  * - Empty-token guard before any network call (defense in depth — the
11
11
  * caller should already have gated on an empty/absent key).
12
- * - Status-code mapping: 401 → license invalid/revoked/lapsed, 404 →
13
- * module content not yet available, other 4xx/5xx → upstream body.
12
+ * - Status-code mapping: 401 → license invalid/revoked/lapsed, 403 →
13
+ * not included / term ended, 404 → included, but that release's bundle
14
+ * not yet uploaded, other 4xx/5xx → upstream body. The issuer answers
15
+ * 403 before 404 (spec 7110 § D4), so a 404 reaches only a key that
16
+ * holds the module's feature.
17
+ * - Default registry: the product API gateway (`api.ailaunchkit.ai`, routed
18
+ * to the licence issuer's `/v1/modules`); `AILK_MODULE_REGISTRY_URL`
19
+ * overrides it.
14
20
  *
15
21
  * Deliberately DOES NOT depend on `@working-theory/license` or
16
22
  * `@working-theory/cli` — both are `publishConfig.access: "restricted"` and
@@ -37,7 +43,7 @@
37
43
  */
38
44
  import pkg from "../../package.json" with { type: "json" };
39
45
  import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
40
- export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/api/modules";
46
+ export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/modules";
41
47
  /**
42
48
  * This package's own `package.json` `"version"`. Deliberately a static JSON
43
49
  * import (`resolveJsonModule`), not `import.meta.url` + `fileURLToPath` (the
@@ -53,7 +59,7 @@ const CREATE_AILK_VERSION = pkg.version;
53
59
  export class FetchModuleError extends Error {
54
60
  isLicenseError;
55
61
  constructor(message,
56
- /** True iff the failure was a license-gating signal (HTTP 401). */
62
+ /** True iff the failure was a license-gating signal (HTTP 401 or 403). */
57
63
  isLicenseError = false) {
58
64
  super(message);
59
65
  this.isLicenseError = isLicenseError;
@@ -108,7 +114,7 @@ export async function fetchModule(options) {
108
114
  /* isLicenseError */ true);
109
115
  }
110
116
  if (response.status === 404) {
111
- throw new FetchModuleError(`"${moduleName}" module content not yet available — ping us at hello@ailaunchkit.ai`);
117
+ throw new FetchModuleError(`"${moduleName}" module content not yet available for create-ailk ${version} — your licence includes it, but that release's bundle has not been uploaded yet; ping us at hello@ailaunchkit.ai`);
112
118
  }
113
119
  if (!response.ok) {
114
120
  const body = await safeReadBody(response);
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/api/templates";
1
+ export declare const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/templates";
2
2
  export interface FetchTemplateOptions {
3
3
  templateName: string;
4
4
  /** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
@@ -3,13 +3,16 @@
3
3
  * D1, § 6).
4
4
  *
5
5
  * The client half of `GET /v1/templates/:name/:version` (#5480's route),
6
- * reached publicly at `/api/templates/:path*`. Posture is
6
+ * reached at `https://api.ailaunchkit.ai/v1/templates/:name/:version` through
7
+ * the product API gateway. Posture is
7
8
  * `fetch-module.ts`'s, mirrored one-for-one rather than generalised into a
8
9
  * shared helper: bearer-JWT auth, `redirect: "error"` so a redirected
9
10
  * response cannot drop the bearer token onto an unintended host, an
10
11
  * empty-token guard before any network call, and the same status mapping —
11
12
  * 401 → licence invalid/revoked/lapsed, 403 → not included / term ended,
12
- * 404 → content not yet available, other non-2xx → the upstream body.
13
+ * 404 → included, but that release's bundle not yet uploaded, other non-2xx
14
+ * → the upstream body. The issuer answers 403 before 404 (spec 7110 § D4),
15
+ * so a 404 reaches only a key that holds the template's whole union.
13
16
  *
14
17
  * ONE word differs, and it is the whole reason this is a sibling rather
15
18
  * than a second call site of `fetchModule` (spec 5481 § 3 DD5): the 404
@@ -27,7 +30,7 @@
27
30
  */
28
31
  import pkg from "../../package.json" with { type: "json" };
29
32
  import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
30
- export const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/api/templates";
33
+ export const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/templates";
31
34
  /**
32
35
  * This package's own `package.json` `"version"` — a static JSON import for
33
36
  * the reason `fetch-module.ts`'s `CREATE_AILK_VERSION` doc gives: `import.meta`
@@ -95,7 +98,7 @@ export async function fetchTemplate(options) {
95
98
  /* isLicenseError */ true);
96
99
  }
97
100
  if (response.status === 404) {
98
- throw new FetchTemplateError(`"${templateName}" template content not yet available — ping us at hello@ailaunchkit.ai`);
101
+ throw new FetchTemplateError(`"${templateName}" template content not yet available for create-ailk ${version} — your licence includes it, but that release's bundle has not been uploaded yet; ping us at hello@ailaunchkit.ai`);
99
102
  }
100
103
  if (!response.ok) {
101
104
  const body = await safeReadBody(response);
@@ -103,3 +103,19 @@ export declare function assessConfigGatedRoute(input: {
103
103
  * a socket) throws rather than being silently followed or skipped.
104
104
  */
105
105
  export declare function listPublicFiles(dir: string): string[];
106
+ /** The slug the sweep requests to prove a detail route 404s an unknown slug. */
107
+ export declare const UNKNOWN_SLUG = "ailk-unknown-slug-7379";
108
+ /** `/features/[slug]` → `/features`; null unless `[slug]` is the one dynamic segment and the last. */
109
+ export declare function detailRoutePrefix(route: string): string | null;
110
+ /** The sorted basenames (sans `.mdx`) of the `*.mdx` files directly in `dir`; [] when `dir` is absent. */
111
+ export declare function listSeedSlugs(dir: string): string[];
112
+ /** An unknown slug must answer exactly 404, with no redirect. A 500 is the #7379 failure; a 200 means no 404 branch ran. */
113
+ export declare function assessUnknownSlug(input: {
114
+ route: string;
115
+ hops: RedirectHop[];
116
+ status: number | null;
117
+ }): SweepVerdict;
118
+ /** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
119
+ export declare function assessPrerenderManifest(manifest: {
120
+ dynamicRoutes?: Record<string, unknown>;
121
+ }): SweepVerdict;
@@ -13,10 +13,17 @@
13
13
  * not cost a templates build, an install and a `next build`. Each is a pure
14
14
  * function of what the driver observed, so
15
15
  * `__tests__/verify-scaffold-render-sweep.test.ts` drives them directly. The
16
- * driver loads this module from `dist/`. Only `listPublicFiles` touches the
17
- * filesystem, and none of it touches the network.
16
+ * driver loads this module from `dist/`. Only `listPublicFiles` and
17
+ * `listSeedSlugs` touch the filesystem, and none of it touches the network.
18
+ *
19
+ * #7379 added two sweeps. A starter roster's detail row that names a seed
20
+ * directory (`/features/[slug]` → `content/en/Feature`) is probed: each seed
21
+ * must render, and `UNKNOWN_SLUG` must answer 404. And the build's
22
+ * `prerender-manifest.json` must register no `/[locale]/…` route as an
23
+ * on-demand prerender — under the cookie-reading locale layout, such a route
24
+ * answers 500 for every slug, seeded or not.
18
25
  */
19
- import { readdirSync } from "node:fs";
26
+ import { existsSync, readdirSync } from "node:fs";
20
27
  import { join } from "node:path";
21
28
  /** More hops than this is a redirect chain, not a render. */
22
29
  export const MAX_REDIRECTS = 5;
@@ -234,3 +241,45 @@ function listFilesUnder(dir, relPrefix) {
234
241
  }
235
242
  return out;
236
243
  }
244
+ /** The slug the sweep requests to prove a detail route 404s an unknown slug. */
245
+ export const UNKNOWN_SLUG = "ailk-unknown-slug-7379";
246
+ /** `/features/[slug]` → `/features`; null unless `[slug]` is the one dynamic segment and the last. */
247
+ export function detailRoutePrefix(route) {
248
+ const match = /^((?:\/[^/[\]]+)+)\/\[slug\]$/.exec(route);
249
+ return match?.[1] ?? null;
250
+ }
251
+ /** The sorted basenames (sans `.mdx`) of the `*.mdx` files directly in `dir`; [] when `dir` is absent. */
252
+ export function listSeedSlugs(dir) {
253
+ if (!existsSync(dir))
254
+ return [];
255
+ return readdirSync(dir, { withFileTypes: true })
256
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".mdx"))
257
+ .map((entry) => entry.name.slice(0, -".mdx".length))
258
+ .sort();
259
+ }
260
+ /** An unknown slug must answer exactly 404, with no redirect. A 500 is the #7379 failure; a 200 means no 404 branch ran. */
261
+ export function assessUnknownSlug(input) {
262
+ const { route, hops, status } = input;
263
+ const first = hops[0];
264
+ if (first !== undefined) {
265
+ return { ok: false, reason: `${route} -> ${first.status} redirect (an unknown slug must answer 404, not redirect)` };
266
+ }
267
+ if (status === 200)
268
+ return { ok: false, reason: `${route} -> 200 (no 404 branch ran for an unknown slug)` };
269
+ if (status !== 404)
270
+ return { ok: false, reason: `${route} -> status ${status} (expected 404 for an unknown slug)` };
271
+ return { ok: true, reason: "404" };
272
+ }
273
+ /** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
274
+ export function assessPrerenderManifest(manifest) {
275
+ const locale = Object.keys(manifest.dynamicRoutes ?? {})
276
+ .filter((route) => route.startsWith("/[locale]/"))
277
+ .sort();
278
+ if (locale.length > 0) {
279
+ return {
280
+ ok: false,
281
+ reason: `on-demand prerender under the cookie-reading locale layout (500s every slug, #7379): ${locale.join(", ")}`,
282
+ };
283
+ }
284
+ return { ok: true, reason: "no /[locale] on-demand prerender route" };
285
+ }
@@ -212,6 +212,15 @@ export interface ModuleSlice {
212
212
  * leak class `scripts/check-oss-strip.sh` exists to catch (#5644).
213
213
  */
214
214
  installRoot?: string;
215
+ /**
216
+ * Paths this module's keyed content payloads occupy (src/content-payloads.ts):
217
+ * each payload file's monorepo source AND the scaffold path its skill's fetch
218
+ * writes it to. Never delivered — no build, extract or patch step reads this
219
+ * field. Declared so the paid-path leak guards, which derive from
220
+ * `ownedFiles`/`installMap`, still recognise a leaked copy after the files
221
+ * left `installMap` (#7133).
222
+ */
223
+ contentPayloadPaths?: string[];
215
224
  /**
216
225
  * Plain text `extractModuleBundle` prints to stdout after a successful
217
226
  * merge, with C0/C1 control characters (other than newline) stripped
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { existsSync, readFileSync } from "node:fs";
25
25
  import { join, sep } from "node:path";
26
+ import { CONTENT_PAYLOADS, } from "./content-payloads.js";
26
27
  import { importClosure } from "./lib/import-graph.js";
27
28
  import { PAID_MODULES } from "./lib/module-tier.js";
28
29
  import { RECOGNIZED_MODULES } from "./parse-args.js";
@@ -183,6 +184,21 @@ const ASSET_STORE_FILES = [
183
184
  * tools/lint-architecture.ts fails on (#6649).
184
185
  */
185
186
  const WEB_AUTH_ADAPTER_IMPORT = "database";
187
+ /**
188
+ * A content payload's paths for `ModuleSlice.contentPayloadPaths` (#7133):
189
+ * each file's `${sourceRoot}/${file}` and the `${destRoot}/${file}` its
190
+ * skill's keyed fetch writes it to — read off `CONTENT_PAYLOADS`, never
191
+ * hand-listed, so the leak guards cannot drift from the manifest.
192
+ */
193
+ function payloadPaths(kind, name, destRoot) {
194
+ const payload = CONTENT_PAYLOADS.find((p) => p.kind === kind && p.name === name);
195
+ if (!payload)
196
+ throw new Error(`no content payload ${kind}/${name}`);
197
+ return payload.files.flatMap((file) => [
198
+ `${payload.sourceRoot}/${file}`,
199
+ `${destRoot}/${file}`,
200
+ ]);
201
+ }
186
202
  /**
187
203
  * The per-module slices. Authored for every `RECOGNIZED_MODULES` value; only
188
204
  * `marketing` (the default) is validated end-to-end against a golden.
@@ -1661,31 +1677,31 @@ export const MODULE_SLICES = {
1661
1677
  // it sits outside build-templates.mjs's SNAPSHOT_DIRS/overlay, so
1662
1678
  // ownedFiles/paidModuleTemplateExclusions cannot reach it) to
1663
1679
  // `module-templates/commerce/.claude/skills/scaffold-commerce/`, and
1664
- // delivered into a keyed scaffold by `installMap` instead. Eleven literal
1680
+ // delivered into a keyed scaffold by `installMap` instead. Five literal
1665
1681
  // entries, not one directory entry: `extractModuleBundle`'s
1666
1682
  // `validateInstallMap` requires every source to be a FILE the bundle
1667
1683
  // produced (`extract-module-bundle.ts`'s `!sourceStat.isFile()` check) —
1668
- // a directory source is rejected outright.
1684
+ // a directory source is rejected outright. The skill ships as steps only
1685
+ // (#7133): its `templates/` and `references/` are the keyed content
1686
+ // payload `template-set/scaffold-commerce` (src/content-payloads.ts),
1687
+ // which `scripts/fetch-templates.ts` fetches with the site's licence key;
1688
+ // `link/` carries byte copies of the link path's two files, which need no
1689
+ // key.
1669
1690
  installMap: {
1670
1691
  "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/SKILL.md": ".claude/skills/scaffold-commerce/SKILL.md",
1671
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/references/lookup-keys-template.md": ".claude/skills/scaffold-commerce/references/lookup-keys-template.md",
1672
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md": ".claude/skills/scaffold-commerce/references/marketplace-brand-pages.md",
1673
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts": ".claude/skills/scaffold-commerce/templates/checkout-route.template.ts",
1674
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts": ".claude/skills/scaffold-commerce/templates/invoice-route.template.ts",
1675
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl": ".claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl",
1676
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts": ".claude/skills/scaffold-commerce/templates/payment-link-route.template.ts",
1677
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/portal-route.template.ts": ".claude/skills/scaffold-commerce/templates/portal-route.template.ts",
1678
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts": ".claude/skills/scaffold-commerce/templates/stripe-config.template.ts",
1679
- "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts": ".claude/skills/scaffold-commerce/templates/subscription-route.template.ts",
1692
+ "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/link/marketplace-brand-pages.md": ".claude/skills/scaffold-commerce/link/marketplace-brand-pages.md",
1693
+ "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/link/marketplace-cta-route.tsx.tmpl": ".claude/skills/scaffold-commerce/link/marketplace-cta-route.tsx.tmpl",
1694
+ "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/scripts/fetch-templates.ts": ".claude/skills/scaffold-commerce/scripts/fetch-templates.ts",
1680
1695
  "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/scripts/stripe-catalog.ts": ".claude/skills/scaffold-commerce/scripts/stripe-catalog.ts",
1681
1696
  // The commerce-boundary rule (#6039/#6070, this entry #6041): ships to
1682
1697
  // a commerce-keyed scaffold's project root so an agent working there
1683
1698
  // reads the money/live-mode/personal-data classification before it
1684
- // acts. One more literal entry alongside the eleven scaffold-commerce
1699
+ // acts. One more literal entry alongside the five scaffold-commerce
1685
1700
  // skill entries above, for the same reason — a directory source is
1686
1701
  // rejected by `validateInstallMap`.
1687
1702
  "packages/create-ailk/module-templates/commerce/.claude/rules/commerce-boundary.md": ".claude/rules/commerce-boundary.md",
1688
1703
  },
1704
+ contentPayloadPaths: payloadPaths("template-set", "scaffold-commerce", ".claude/skills/scaffold-commerce"),
1689
1705
  // patches (#5581 U5, D5) — delivers the extracted component package to a
1690
1706
  // keyed scaffold's apps/web, mirroring the workspace slice's identical
1691
1707
  // merge-keys op above.
@@ -1833,12 +1849,27 @@ export function coOwnedModuleDestinations() {
1833
1849
  // see that entry, and sync-routes' AC-5 coverage test, which reads this
1834
1850
  // list so the resulting ROUTE_SINCE-vs-template absence is justified
1835
1851
  // rather than reported stale.
1852
+ // - the three app-side suites of the API ↔ MCP parity guard (#7135, spec
1853
+ // docs/specs/7135-an-api-mcp-parity-guard-spec.md D11) — file entries,
1854
+ // matched exactly by `isExcludedTemplatePath`. Each imports the guard's
1855
+ // core from the monorepo's root `tests/parity/api-parity/` by relative
1856
+ // path, which a scaffold does not carry. A test that imports a slice file
1857
+ // is a dependent, not an import-graph companion, so
1858
+ // `paidModuleTemplateExclusions` never sweeps these along.
1859
+ // - apps/mcp/__tests__/pro-tool-call-gate.test.ts (#7315, spec
1860
+ // docs/specs/7315-a-gated-pro-tool-re-spec.md D4) — same reason as the
1861
+ // three suites above: it imports `SITE_ARGS` from the monorepo's root
1862
+ // `tests/parity/` tree, which no scaffold carries.
1836
1863
  export const EXCLUDE_TEMPLATE_DIRS = [
1837
1864
  "apps/web/__tests__",
1838
1865
  "apps/web/app/__tests__",
1839
1866
  "apps/web/app/[locale]/__tests__",
1840
1867
  "apps/web/app/[locale]/(authed)/edit",
1841
1868
  "apps/web/content",
1869
+ "apps/api/src/__tests__/api-parity.test.ts",
1870
+ "apps/api/src/__tests__/api-parity-paid.test.ts",
1871
+ "apps/mcp/__tests__/api-parity.test.ts",
1872
+ "apps/mcp/__tests__/pro-tool-call-gate.test.ts",
1842
1873
  ];
1843
1874
  export function paidModuleTemplateExclusions(snapshotRoots, repoRoot) {
1844
1875
  const roots = new Set(snapshotRoots);
@@ -103,6 +103,8 @@ export interface ParseEnv {
103
103
  /** A human can answer a prompt: `process.stdin.isTTY === true` in cli.ts. */
104
104
  interactive: boolean;
105
105
  }
106
+ /** The two lines that install the `ailk` plugin in a Claude Code session (#7298). */
107
+ export declare const PLUGIN_INSTALL_LINES: readonly ["/plugin marketplace add working-theory-labs/claude-plugins", "/plugin install ailk@working-theory"];
106
108
  export declare const helpText: string;
107
109
  /**
108
110
  * Parse `create-ailk` argv. `env.interactive` defaults to false —
@@ -65,6 +65,11 @@ export class ParseError extends Error {
65
65
  this.name = "ParseError";
66
66
  }
67
67
  }
68
+ /** The two lines that install the `ailk` plugin in a Claude Code session (#7298). */
69
+ export const PLUGIN_INSTALL_LINES = [
70
+ "/plugin marketplace add working-theory-labs/claude-plugins",
71
+ "/plugin install ailk@working-theory",
72
+ ];
68
73
  export const helpText = `create-ailk — scaffold a new AI Launch Kit site.
69
74
 
70
75
  Usage:
@@ -166,6 +171,8 @@ Next steps after a successful scaffold:
166
171
  cd <target-dir>
167
172
  pnpm install
168
173
  pnpm dev
174
+ Then, in Claude Code, install the ailk plugin:
175
+ ${PLUGIN_INSTALL_LINES.map((line) => ` ${line}`).join("\n")}
169
176
 
170
177
  Reference: ADR 0007 — \`pnpm create ailk@latest\` is the canonical install path.
171
178
  `;
package/dist/surfaces.js CHANGED
@@ -20,8 +20,7 @@
20
20
  *
21
21
  * The default (`--modules marketing`) enables NO gateable surface: every one of
22
22
  * them is a claim about the business that the scaffolder cannot make. The path
23
- * from there is the `add-page` skill that already ships in
24
- * `templates/.claude/skills/`.
23
+ * from there is the `/ailk:add-page` skill in the `ailk` Claude Code plugin.
25
24
  *
26
25
  * Zero runtime dependencies — create-ailk is a published, standalone CLI.
27
26
  *
@@ -1047,7 +1046,7 @@ export function renderNavConfig(enabled, options) {
1047
1046
  // below it, the nav is yours and add-module reports the change it wanted
1048
1047
  // instead.
1049
1048
  //
1050
- // Adding a link: scaffold the page (the \`add-page\` skill), then add an
1049
+ // Adding a link: scaffold the page (the \`/ailk:add-page\` skill), then add an
1051
1050
  // entry below with a \`labelKey\` that exists in the \`nav\` namespace of
1052
1051
  // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
1053
1052
  // them with the active locale at render.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -27,8 +27,8 @@
27
27
  "tar": "^7.5.21"
28
28
  },
29
29
  "devDependencies": {
30
- "@working-theory/schema": "0.29.0",
31
- "@working-theory/templates": "0.29.0"
30
+ "@working-theory/schema": "0.30.0",
31
+ "@working-theory/templates": "0.30.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
@@ -366,10 +366,11 @@ Every content route emits one `application/ld+json` script whose `@graph` carrie
366
366
  | Node | `@id` | Built from | Links |
367
367
  | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
368
368
  | Identity (`Organization` · `Person` · `LocalBusiness`) | `<origin>/#organization` (`#person` for a Person) | `siteIdentity` `name` · `url` · `logo`/`image` · `description` · `sameAs` | — |
369
+ | Identity (`SoftwareApplication`) | `<origin>/#product` | `siteIdentity` fields; `logo` emitted as `image` | `publisher` → the maker's own `@id` (`siteIdentity.publisher`), never a node here; takes no secondary |
369
370
  | Secondary (optional) | `<origin>/#product` (a `SoftwareApplication`) · `<origin>/#organization` (a Person's company) | `siteIdentity.secondary` | product `publisher` → `#organization`, `featureList` from the product page's (`/software/product`) feature items; a Person `worksFor` its company, which has the Person as `founder` |
370
- | `WebSite` | `<origin>/#website` | `siteName` · `baseUrl` · the render locale | `publisher` → the identity node |
371
+ | `WebSite` | `<origin>/#website` | `siteName` · `baseUrl` · the render locale | `publisher` → the identity node (the maker, for a `SoftwareApplication` identity) |
371
372
 
372
- Each page's primary node is `isPartOf` the `WebSite`; the home page's `mainEntity` is the secondary when declared, else the identity. A testimonials `Review` points its `itemReviewed` at the identity node, declared in the same graph. Free declares no secondary, so a fresh site emits the Organization and the WebSite. `/sign-in` emits no JSON-LD.
373
+ Each page's primary node is `isPartOf` the `WebSite`; the home page's `mainEntity` is the secondary when declared, else the identity (`#product` for a `SoftwareApplication` identity, #7103). A testimonials `Review` points its `itemReviewed` at the identity node, declared in the same graph. Free declares no secondary, so a fresh site emits the Organization and the WebSite. `/sign-in` emits no JSON-LD.
373
374
 
374
375
  ## 2a. State and persistence (every interactive element)
375
376
 
@@ -466,7 +467,7 @@ reversed (`docs/decisions/starter-roster-page-set.md` D4, D7).
466
467
 
467
468
  ### 3b. Site entity (operator-disposed 2026-09-24)
468
469
 
469
- `derive-roster.py` names `SoftwareApplication` as the main entity for this template's inputs. The template deliberately declares the **Organization** as the site's primary node and the software as its **secondary** node (`siteIdentity.secondary`, kind `SoftwareApplication`, #6597): the Free template is a company's own site about its product, so the entity the site is _about_ is the maker. The inverse — `SoftwareApplication` primary — is for a product whose canonical site is elsewhere (Positionary, whose maker has its own site). A fresh Free scaffold declares no `secondary` until the owner names the product; the overlay `_site.mdx` documents the field.
470
+ `derive-roster.py` names `SoftwareApplication` as the main entity for this template's inputs. The template deliberately declares the **Organization** as the site's primary node and the software as its **secondary** node (`siteIdentity.secondary`, kind `SoftwareApplication`, #6597): the Free template is a company's own site about its product, so the entity the site is _about_ is the maker. The inverse — `SoftwareApplication` primary — is for a product whose maker has its own site (Positionary): express it as `siteIdentity.kind: SoftwareApplication` with a required `publisher` naming the maker's stable `@id` on that site (e.g. `https://salessmyth.com/#organization`, #7103), so the product is `<origin>/#product` and the maker is a reference, never a second Organization node here. A fresh Free scaffold declares no `secondary` until the owner names the product; the overlay `_site.mdx` documents the field.
470
471
 
471
472
  ## 4. Component inventory (Free, by tier)
472
473
 
@@ -11,29 +11,29 @@ schema:
11
11
  name: "Your Company"
12
12
  ---
13
13
 
14
- # {First job}
14
+ # \{First job}
15
15
 
16
- {Open with the situation a reader is in when they come looking for this. Two or
16
+ \{Open with the situation a reader is in when they come looking for this. Two or
17
17
  three sentences, no more — the detail belongs under the headings below.}
18
18
 
19
19
  ## What it covers
20
20
 
21
- - {The first thing you actually do}
22
- - {The second thing you actually do}
23
- - {The third thing you actually do}
21
+ - \{The first thing you actually do}
22
+ - \{The second thing you actually do}
23
+ - \{The third thing you actually do}
24
24
 
25
25
  ## How it runs
26
26
 
27
- {One paragraph on the shape of the visit: how it is scheduled, how long it
27
+ \{One paragraph on the shape of the visit: how it is scheduled, how long it
28
28
  takes, who turns up, and what you need from the customer beforehand.}
29
29
 
30
30
  ## What you end up with
31
31
 
32
- {One paragraph on the result. Be concrete — describe the property when the
32
+ \{One paragraph on the result. Be concrete — describe the property when the
33
33
  crew drives away.}
34
34
 
35
35
  ## What it costs
36
36
 
37
- {One line on the pricing shape — flat rate, hourly, or by the job — and what it
37
+ \{One line on the pricing shape — flat rate, hourly, or by the job — and what it
38
38
  excludes. Replace with your real numbers, or delete this section and let a
39
39
  call do it.}