create-ailk 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +71 -3
  2. package/component-catalog.json +524 -8
  3. package/component-catalog.md +8 -5
  4. package/dist/cli.js +99 -0
  5. package/dist/cutover.d.ts +109 -0
  6. package/dist/cutover.js +494 -0
  7. package/dist/design-presets.js +20 -4
  8. package/dist/export-content.d.ts +55 -0
  9. package/dist/export-content.js +303 -0
  10. package/dist/inventory.d.ts +216 -0
  11. package/dist/inventory.js +833 -0
  12. package/dist/language-import.d.ts +114 -0
  13. package/dist/language-import.js +435 -0
  14. package/dist/lib/env-catalog.js +1 -1
  15. package/dist/lib/import-mdx.d.ts +38 -0
  16. package/dist/lib/import-mdx.js +411 -64
  17. package/dist/lib/render-comparison.d.ts +200 -0
  18. package/dist/lib/render-comparison.js +374 -0
  19. package/dist/migrate-content.d.ts +6 -2
  20. package/dist/migrate-content.js +60 -14
  21. package/dist/seed-workspace.d.ts +3 -1
  22. package/dist/seed-workspace.js +45 -16
  23. package/dist/topology.d.ts +12 -0
  24. package/dist/topology.js +24 -10
  25. package/package.json +3 -3
  26. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  27. package/templates/.claude/hooks/content-source-guard.sh +75 -0
  28. package/templates/.claude/settings.json +9 -0
  29. package/templates/.github/workflows/ci.yml +2 -0
  30. package/templates/CLAUDE.md +5 -0
  31. package/templates/apps/mcp/CLAUDE.md +8 -4
  32. package/templates/apps/web/.env.example +4 -2
  33. package/templates/apps/web/CLAUDE.md +16 -5
  34. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +153 -4
  35. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +45 -2
  36. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +49 -0
  37. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +6 -4
  38. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +13 -1
  39. package/templates/apps/web/app/[locale]/events/__tests__/program-event.test.tsx +120 -0
  40. package/templates/apps/web/app/[locale]/events/page.tsx +11 -0
  41. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +44 -5
  42. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.thanks-by-tier.test.tsx +306 -0
  43. package/templates/apps/web/app/[locale]/layout.tsx +122 -27
  44. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +21 -2
  45. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +1 -0
  46. package/templates/apps/web/app/llms.txt/route.ts +16 -9
  47. package/templates/apps/web/app/sitemap.xml/route.ts +3 -0
  48. package/templates/apps/web/jest.config.cjs +46 -1
  49. package/templates/apps/web/lib/__tests__/bot-check.test.ts +247 -4
  50. package/templates/apps/web/lib/__tests__/in-page-magnet-self-request.test.ts +92 -0
  51. package/templates/apps/web/lib/__tests__/page-chrome.test.ts +90 -0
  52. package/templates/apps/web/lib/__tests__/relay.test.ts +33 -8
  53. package/templates/apps/web/lib/__tests__/site-identity-feature-list.test.ts +196 -0
  54. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +11 -1
  55. package/templates/apps/web/lib/bot-check.ts +113 -13
  56. package/templates/apps/web/lib/in-page-magnet.ts +4 -1
  57. package/templates/apps/web/lib/page-chrome.ts +65 -5
  58. package/templates/apps/web/lib/relay.ts +22 -3
  59. package/templates/apps/web/lib/site-identity.ts +75 -24
  60. package/templates/apps/web/messages/en.json +5 -1
  61. package/templates/apps/web/next.config.mjs +15 -1
  62. package/templates/apps/web/package.json +1 -1
  63. package/templates/apps/web/scripts/assert-content-bundled.ts +36 -8
  64. package/templates/apps/web/scripts/content-snapshot.ts +33 -0
  65. package/templates/package.json +2 -1
  66. package/templates/tools/check-content-source.ts +108 -0
  67. package/versions.json +48 -48
@@ -21,6 +21,12 @@ import { readdirSync, readFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
22
 
23
23
  import {
24
+ resolvedAdapterKind,
25
+ type ContentAdapter,
26
+ } from "@working-theory/content-adapters";
27
+ import {
28
+ getContentAdapter,
29
+ setSiteFeatureListReader,
24
30
  setSiteIdentity,
25
31
  type SiteIdentityRegistration,
26
32
  } from "@working-theory/page-renderer";
@@ -46,6 +52,59 @@ function readFrontmatter(path: string): Record<string, unknown> | undefined {
46
52
  }
47
53
  }
48
54
 
55
+ /**
56
+ * A product page's feature titles. A `feature` section in the `steps` variant
57
+ * is how the product is set up, not what it does (the product page carries
58
+ * both, #6658 D3), so its step titles never enter the featureList.
59
+ */
60
+ function featuresOf(page: { sections?: unknown }): string[] {
61
+ return featureListFromSections(
62
+ (Array.isArray(page.sections)
63
+ ? (page.sections as Array<{ type: string; data?: unknown }>)
64
+ : []
65
+ ).filter(
66
+ (section) =>
67
+ !(
68
+ section.type === "feature" &&
69
+ (section.data as { variant?: unknown } | undefined)?.variant === "steps"
70
+ ),
71
+ ),
72
+ );
73
+ }
74
+
75
+ /** Pages in the order the directory scan reads their seeds: by `<slug>.mdx`. */
76
+ function inSeedOrder<T extends { slug: string }>(pages: readonly T[]): T[] {
77
+ const key = (page: T): string => `${page.slug}.mdx`;
78
+ return [...pages].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
79
+ }
80
+
81
+ /**
82
+ * The product's `featureList` read through the content adapter (#10644 D4):
83
+ * the first published `SoftwareProduct` page's features, else the titles of
84
+ * the published `Feature` pages, else none — the directory scan's first two
85
+ * steps, in the same seed order.
86
+ */
87
+ export async function readFeatureListFromAdapter(
88
+ adapter: Pick<ContentAdapter, "listPages">,
89
+ locale: string,
90
+ ): Promise<string[]> {
91
+ const [product] = inSeedOrder(await adapter.listPages("SoftwareProduct", locale));
92
+ if (product !== undefined) return featuresOf(product);
93
+ return inSeedOrder(await adapter.listPages("Feature", locale))
94
+ .map((page) => page.title)
95
+ .filter((title): title is string => typeof title === "string" && title.length > 0);
96
+ }
97
+
98
+ /** Whether the identity carries a product, whose featureList the graph shows. */
99
+ function carriesProduct(identity: SiteIdentityRegistration["identity"]): boolean {
100
+ return identity.kind === "Organization" && identity.secondary?.kind === "SoftwareApplication";
101
+ }
102
+
103
+ /** `_site.mdx`'s `defaultLocale`, else `"en"`. */
104
+ function localeOf(defaultLocale: unknown): string {
105
+ return typeof defaultLocale === "string" ? defaultLocale : "en";
106
+ }
107
+
49
108
  /**
50
109
  * The product's `featureList`: the feature titles of the product's own page —
51
110
  * the first `SoftwareProduct` seed in the default locale, in sorted order
@@ -54,22 +113,6 @@ function readFrontmatter(path: string): Record<string, unknown> | undefined {
54
113
  * first match in sorted order). Empty when there is neither.
55
114
  */
56
115
  function readFeatureList(contentRoot: string, locale: string): string[] {
57
- // A `feature` section in the `steps` variant is how the product is set up,
58
- // not what it does (the product page carries both, #6658 D3), so its step
59
- // titles never enter the featureList.
60
- const featuresOf = (frontmatter: Record<string, unknown>): string[] =>
61
- featureListFromSections(
62
- (Array.isArray(frontmatter.sections)
63
- ? (frontmatter.sections as Array<{ type: string; data?: unknown }>)
64
- : []
65
- ).filter(
66
- (section) =>
67
- !(
68
- section.type === "feature" &&
69
- (section.data as { variant?: unknown } | undefined)?.variant === "steps"
70
- ),
71
- ),
72
- );
73
116
  // #6658 D3: the product's own detail page (the first `SoftwareProduct`
74
117
  // seed in sorted order) is where the feature sections live now.
75
118
  const productDir = join(contentRoot, locale, "SoftwareProduct");
@@ -133,16 +176,24 @@ export function readSiteIdentity(
133
176
  baseUrl: typeof baseUrl === "string" ? baseUrl : identity.url,
134
177
  identity,
135
178
  };
136
- if (
137
- identity.kind === "Organization" &&
138
- identity.secondary?.kind === "SoftwareApplication"
139
- ) {
140
- registration.featureList = readFeatureList(
141
- contentRoot,
142
- typeof defaultLocale === "string" ? defaultLocale : "en",
143
- );
179
+ // Under the database adapter the list is read through the adapter when a
180
+ // page's JSON-LD is composed (#10644 D3), never scanned here.
181
+ if (carriesProduct(identity) && resolvedAdapterKind() !== "database") {
182
+ registration.featureList = readFeatureList(contentRoot, localeOf(defaultLocale));
144
183
  }
145
184
  return registration;
146
185
  }
147
186
 
148
187
  setSiteIdentity(readSiteIdentity());
188
+
189
+ // #10644 D3, D4 — under the database adapter the product's featureList is
190
+ // read through the adapter each time a page's JSON-LD is composed: an async
191
+ // read cannot run at module load.
192
+ if (resolvedAdapterKind() === "database") {
193
+ const contentRoot = resolveContentRoot();
194
+ const site = readSiteIdentity(contentRoot);
195
+ if (site !== undefined && carriesProduct(site.identity)) {
196
+ const locale = localeOf(readFrontmatter(join(contentRoot, "_site.mdx"))?.defaultLocale);
197
+ setSiteFeatureListReader(() => readFeatureListFromAdapter(getContentAdapter(), locale));
198
+ }
199
+ }
@@ -179,7 +179,11 @@
179
179
  },
180
180
  "events": {
181
181
  "heading": "Events",
182
- "emptyState": "Events listing coming soon."
182
+ "emptyState": "Events listing coming soon.",
183
+ "addToCalendar": "Add to calendar",
184
+ "addToGoogleCalendar": "Google Calendar",
185
+ "addToOutlook": "Outlook",
186
+ "addToAppleCalendar": "Apple Calendar (.ics)"
183
187
  },
184
188
  "faq": {
185
189
  "heading": "FAQ",
@@ -6,6 +6,7 @@ import { withSentryConfig } from "@sentry/nextjs";
6
6
  import createNextIntlPlugin from "next-intl/plugin";
7
7
  import { DocsPathSchema, ServicesPathSchema } from "@working-theory/site-config/schema";
8
8
  import { legacyRedirects, ROUTES, SERVICES_ROUTE } from "@working-theory/templates/routes";
9
+ import { assetRemotePattern } from "@working-theory/validation";
9
10
  import { parseStealth } from "@working-theory/web-i18n/stealth";
10
11
  import { withBotId } from "botid/next/config";
11
12
 
@@ -279,6 +280,14 @@ function readServiceParents() {
279
280
  const servicesPathAtBuild = readServicesPathFromSiteMdx();
280
281
  readServiceParents();
281
282
 
283
+ // #10647 D6 — a database spoke's pages name library images as `asset:<key>`,
284
+ // which the hub resolves to its own `/v1/assets/:id` URLs. `next/image` admits
285
+ // that path on the hub's origin, and nothing else, when `AILK_API_URL` (the
286
+ // server-side hub origin this site already reads) is an http(s) URL at build
287
+ // time; otherwise there is no `images` key, as before. A change of hub origin
288
+ // needs a rebuild.
289
+ const assetPatternAtBuild = assetRemotePattern(process.env.AILK_API_URL);
290
+
282
291
  /** @type {import('next').NextConfig} */
283
292
  const nextConfig = {
284
293
  reactStrictMode: true,
@@ -302,7 +311,9 @@ const nextConfig = {
302
311
  // content/ directory that carries the schema-typed pages, so the standalone
303
312
  // output shipped without them and postbuild's assert-content-bundled failed.
304
313
  outputFileTracingIncludes: {
305
- "/**": ["../../content/**/*.mdx"],
314
+ // #10605 D6 — the last-good content snapshot. An exact path, never a glob
315
+ // over `.ailk/`: that directory also holds the lead-inbox fallback.
316
+ "/**": ["../../content/**/*.mdx", "./.ailk/content-snapshot.json"],
306
317
  // #10041 D8 — /sitemap.xml and /llms.txt list the docs by reading this
307
318
  // app's own content/<locale>/docs/ at request time.
308
319
  "/sitemap.xml": ["./content/**/*.mdx"],
@@ -349,6 +360,9 @@ const nextConfig = {
349
360
  // mutates a tracked file as a side effect of running the dev server,
350
361
  // which this scaffold does not want.
351
362
  agentRules: false,
363
+ ...(assetPatternAtBuild === null
364
+ ? {}
365
+ : { images: { remotePatterns: [assetPatternAtBuild] } }),
352
366
  env: {
353
367
  // Comma-separated locale list baked at build time — consumed by middleware
354
368
  // and the sitemap route. NEXT_PUBLIC_ prefix makes it available in Edge
@@ -6,7 +6,7 @@
6
6
  "license": "Apache-2.0",
7
7
  "scripts": {
8
8
  "dev": "next dev --webpack",
9
- "build": "tsx scripts/check-nav-labels.ts && next build --webpack",
9
+ "build": "tsx scripts/check-nav-labels.ts && tsx scripts/content-snapshot.ts && next build --webpack",
10
10
  "postbuild": "tsx scripts/assert-content-bundled.ts",
11
11
  "start": "next start",
12
12
  "type-check": "tsc --noEmit",
@@ -5,6 +5,13 @@
5
5
  * preserving their path relative to outputFileTracingRoot (repo root). So
6
6
  * <repo-root>/content/en/HomePage/home.mdx → .next/standalone/content/en/...
7
7
  *
8
+ * Under CONTENT_ADAPTER=database (#10646 D1) a site's pages live on its hub,
9
+ * so content/ may hold no page at all. The build's own content file is then
10
+ * the snapshot (#10605), traced by exact path: when the build wrote one, the
11
+ * sentinel is apps/web/.ailk/content-snapshot.json →
12
+ * .next/standalone/apps/web/.ailk/content-snapshot.json. Without one (an
13
+ * unreachable hub, #10605 D3), the .mdx walk below still runs.
14
+ *
8
15
  * See: docs/runbooks/page-renderer-deploy-verification.md §1 Cause 2
9
16
  */
10
17
 
@@ -38,12 +45,25 @@ function firstMdx(dir: string): string | undefined {
38
45
  }
39
46
  return undefined;
40
47
  }
41
- const sentinelAbs = firstMdx(contentDir);
42
- if (sentinelAbs === undefined) {
43
- console.error("[assert-content-bundled] ERROR: no .mdx file under content/ to trace.");
44
- process.exit(1);
48
+ const repoRoot = resolve(appWebDir, "..", "..");
49
+ const snapshotAbs = join(appWebDir, ".ailk", "content-snapshot.json");
50
+ const database = process.env.CONTENT_ADAPTER === "database";
51
+ const snapshotSentinel = database && existsSync(snapshotAbs);
52
+ let sentinelRelative: string;
53
+ if (snapshotSentinel) {
54
+ sentinelRelative = relative(repoRoot, snapshotAbs);
55
+ } else {
56
+ const sentinelAbs = firstMdx(contentDir);
57
+ if (sentinelAbs === undefined) {
58
+ console.error(
59
+ database
60
+ ? "[assert-content-bundled] ERROR: no content snapshot (.ailk/content-snapshot.json) and no .mdx file under content/ to trace."
61
+ : "[assert-content-bundled] ERROR: no .mdx file under content/ to trace.",
62
+ );
63
+ process.exit(1);
64
+ }
65
+ sentinelRelative = join("content", relative(contentDir, sentinelAbs));
45
66
  }
46
- const sentinelRelative = join("content", relative(contentDir, sentinelAbs));
47
67
  const sentinelPath = join(standaloneDir, sentinelRelative);
48
68
 
49
69
  if (!existsSync(standaloneDir)) {
@@ -60,11 +80,19 @@ if (!existsSync(sentinelPath)) {
60
80
  );
61
81
  console.error(" Expected path:", sentinelPath);
62
82
  console.error(
63
- " This means outputFileTracingIncludes did not bundle content/ into the build artifact.",
83
+ snapshotSentinel
84
+ ? " This means outputFileTracingIncludes did not bundle the content snapshot into the build artifact."
85
+ : " This means outputFileTracingIncludes did not bundle content/ into the build artifact.",
86
+ );
87
+ console.error(
88
+ snapshotSentinel
89
+ ? " The deployed site cannot fall back to its last-good hub content."
90
+ : " The deployed site cannot serve MDX content.",
64
91
  );
65
- console.error(" The deployed site cannot serve MDX content.");
66
92
  console.error(
67
- " Fix: verify outputFileTracingIncludes in apps/web/next.config.mjs covers '../../content/**/*.mdx'.",
93
+ ` Fix: verify outputFileTracingIncludes in apps/web/next.config.mjs covers ${
94
+ snapshotSentinel ? "'./.ailk/content-snapshot.json'" : "'../../content/**/*.mdx'"
95
+ }.`,
68
96
  );
69
97
  console.error(
70
98
  " See: docs/runbooks/page-renderer-deploy-verification.md §1 Cause 2",
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Write the last-good content snapshot (#10605 D5). Runs in `pnpm build`
3
+ * before `next build` — never in `prebuild`, which the docs-blog module owns
4
+ * in a scaffold.
5
+ *
6
+ * Under `CONTENT_ADAPTER=database` it reads every published page from the hub
7
+ * into `.ailk/content-snapshot.json`; under any other adapter it does nothing.
8
+ * An unreachable hub keeps the previous snapshot and the build goes on; only
9
+ * a configuration fault (a 4xx other than 404/429) fails the build.
10
+ */
11
+
12
+ import { join } from "node:path";
13
+
14
+ import { refreshContentSnapshot } from "@working-theory/page-renderer/snapshot";
15
+ import { readLocales } from "@working-theory/web-i18n";
16
+
17
+ function enabledLocales(): string[] {
18
+ try {
19
+ return readLocales(join(process.cwd(), "..", ".."));
20
+ } catch (error) {
21
+ // next.config.mjs's own fallback when project.yaml has no locales.
22
+ console.warn("[content-snapshot] locales unreadable, using en:", error);
23
+ return ["en"];
24
+ }
25
+ }
26
+
27
+ refreshContentSnapshot({ locales: enabledLocales() }).then(
28
+ (outcome) => console.log(`[content-snapshot] ${outcome}`),
29
+ (error: unknown) => {
30
+ console.error("[content-snapshot] ERROR:", error);
31
+ process.exit(1);
32
+ },
33
+ );
@@ -20,7 +20,8 @@
20
20
  "db:studio": "prisma studio --config database/prisma.config.ts",
21
21
  "architecture": "tsx tools/lint-architecture.ts",
22
22
  "claude-md:check": "tsx tools/check-claude-md.ts",
23
- "preflight": "pnpm architecture && pnpm claude-md:check && pnpm type-check && pnpm lint && pnpm build"
23
+ "content-source:check": "tsx tools/check-content-source.ts",
24
+ "preflight": "pnpm architecture && pnpm claude-md:check && pnpm content-source:check && pnpm type-check && pnpm lint && pnpm build"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@arethetypeswrong/cli": "^0.18.5",
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Content-source check — fails when a site whose pages come from the
4
+ * database (`CONTENT_ADAPTER=database`) carries a page file under
5
+ * `content/en/`.
6
+ *
7
+ * On such a site the page that renders comes from the database, so a
8
+ * `content/en/**\/*.mdx` is a second copy the site never reads. The source is
9
+ * `database` when the environment says so, or when any of Next's eight env
10
+ * files under `apps/web/` (`.env`, `.env.local`, `.env.development`,
11
+ * `.env.development.local`, `.env.production`, `.env.production.local`,
12
+ * `.env.test`, `.env.test.local`) assigns it last in that file. The same rule
13
+ * and the same message as `.claude/hooks/content-source-guard.sh`, which
14
+ * refuses the write. A files site (`CONTENT_ADAPTER` unset or `mdx`) passes.
15
+ *
16
+ * Usage:
17
+ * pnpm content-source:check # or: tsx tools/check-content-source.ts
18
+ *
19
+ * Exit codes:
20
+ * 0 — pages come from files, or from the database with no page file
21
+ * 1 — a page file under `content/en/` on a database-sourced site
22
+ */
23
+
24
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
+ import { dirname, join, resolve } from "node:path";
26
+
27
+ const ROOT = resolve(dirname(process.argv[1] ?? "."), "..");
28
+
29
+ const ENV_FILES = [
30
+ ".env",
31
+ ".env.local",
32
+ ".env.development",
33
+ ".env.development.local",
34
+ ".env.production",
35
+ ".env.production.local",
36
+ ".env.test",
37
+ ".env.test.local",
38
+ ];
39
+ const ASSIGNMENT = /^[ \t]*(export[ \t]+)?CONTENT_ADAPTER[ \t]*=/;
40
+
41
+ /** The value of the file's last `CONTENT_ADAPTER` assignment, or undefined. */
42
+ function lastAssignment(text: string): string | undefined {
43
+ let last: string | undefined;
44
+ for (const line of text.split("\n")) {
45
+ if (!ASSIGNMENT.test(line)) continue;
46
+ let value = line.slice(line.indexOf("=") + 1);
47
+ let quote = "";
48
+ for (let i = 0; i < value.length; i++) {
49
+ const c = value[i]!;
50
+ if (quote !== "") {
51
+ if (c === quote) quote = "";
52
+ } else if (c === '"' || c === "'") {
53
+ quote = c;
54
+ } else if (c === "#" && i > 0 && /[ \t]/.test(value[i - 1]!)) {
55
+ value = value.slice(0, i);
56
+ break;
57
+ }
58
+ }
59
+ value = value.replace(/^[ \t\r]+/, "").replace(/[ \t\r]+$/, "");
60
+ const first = value[0];
61
+ if (value.length >= 2 && (first === '"' || first === "'") && value.endsWith(first)) {
62
+ value = value.slice(1, -1);
63
+ }
64
+ last = value;
65
+ }
66
+ return last;
67
+ }
68
+
69
+ /** Where `CONTENT_ADAPTER=database` comes from, or undefined on a files site. */
70
+ function databaseSource(): string | undefined {
71
+ if (process.env.CONTENT_ADAPTER === "database") return "the environment";
72
+ for (const name of ENV_FILES) {
73
+ const file = join(ROOT, "apps", "web", name);
74
+ if (statSync(file, { throwIfNoEntry: false })?.isFile() !== true) continue;
75
+ if (lastAssignment(readFileSync(file, "utf8")) === "database") return `apps/web/${name}`;
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ function pageFiles(dir: string, rel: string): string[] {
81
+ if (!existsSync(dir)) return [];
82
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
83
+ const childRel = `${rel}/${entry.name}`;
84
+ if (entry.isDirectory()) return pageFiles(join(dir, entry.name), childRel);
85
+ return entry.isFile() && entry.name.endsWith(".mdx") ? [childRel] : [];
86
+ });
87
+ }
88
+
89
+ const source = databaseSource();
90
+ if (source === undefined) {
91
+ console.log("content-source: pages come from files (CONTENT_ADAPTER is not database)");
92
+ process.exit(0);
93
+ }
94
+ const pages = pageFiles(join(ROOT, "content", "en"), "content/en").sort();
95
+ if (pages.length === 0) {
96
+ console.log(`content-source: pages come from the database (${source}); no page file under content/en/`);
97
+ process.exit(0);
98
+ }
99
+ for (const page of pages) {
100
+ console.error(
101
+ `content-source: ${page} is a page file, but this site's pages come from the database (CONTENT_ADAPTER=database in ${source}), so the site never reads it.`,
102
+ );
103
+ }
104
+ console.error(
105
+ "Change a page through the site's MCP tools instead: save_draft writes the draft and publish_draft publishes it; read_content returns the current page.",
106
+ );
107
+ console.error("Pages and layout: words live in the database, layout stays in code (CLAUDE.md § Content).");
108
+ process.exit(1);
package/versions.json CHANGED
@@ -1,66 +1,66 @@
1
1
  {
2
- "@working-theory/admin-ui": "0.53.0",
3
- "@working-theory/aeo": "0.53.0",
4
- "@working-theory/agent-context": "0.53.0",
2
+ "@working-theory/admin-ui": "0.54.0",
3
+ "@working-theory/aeo": "0.54.0",
4
+ "@working-theory/agent-context": "0.54.0",
5
5
  "@working-theory/agent-contract": "0.1.0",
6
6
  "@working-theory/agent-memory": "0.0.0",
7
7
  "@working-theory/ai": "0.1.0",
8
- "@working-theory/analytics": "0.53.0",
9
- "@working-theory/api": "0.53.0",
10
- "@working-theory/api-client": "0.53.0",
8
+ "@working-theory/analytics": "0.54.0",
9
+ "@working-theory/api": "0.54.0",
10
+ "@working-theory/api-client": "0.54.0",
11
11
  "@working-theory/cda-filesystem": "0.0.0",
12
- "@working-theory/cli": "0.53.0",
13
- "@working-theory/common": "0.53.0",
12
+ "@working-theory/cli": "0.54.0",
13
+ "@working-theory/common": "0.54.0",
14
14
  "@working-theory/consent": "0.1.0",
15
- "@working-theory/content-adapters": "0.53.0",
16
- "@working-theory/content-projection": "0.53.0",
17
- "@working-theory/email": "0.53.0",
15
+ "@working-theory/content-adapters": "0.54.0",
16
+ "@working-theory/content-projection": "0.54.0",
17
+ "@working-theory/email": "0.54.0",
18
18
  "@working-theory/env": "0.0.0",
19
- "@working-theory/eslint-config": "0.53.0",
20
- "@working-theory/fumadocs-schema": "0.53.0",
21
- "@working-theory/ids": "0.18.0",
22
- "@working-theory/jsonld": "0.53.0",
23
- "@working-theory/license": "0.53.0",
24
- "@working-theory/mcp-server": "0.53.0",
25
- "@working-theory/metadata": "0.53.0",
26
- "@working-theory/observability": "0.53.0",
27
- "@working-theory/page-renderer": "0.53.0",
19
+ "@working-theory/eslint-config": "0.54.0",
20
+ "@working-theory/fumadocs-schema": "0.54.0",
21
+ "@working-theory/ids": "0.19.0",
22
+ "@working-theory/jsonld": "0.54.0",
23
+ "@working-theory/license": "0.54.0",
24
+ "@working-theory/mcp-server": "0.54.0",
25
+ "@working-theory/metadata": "0.54.0",
26
+ "@working-theory/observability": "0.54.0",
27
+ "@working-theory/page-renderer": "0.54.0",
28
28
  "@working-theory/plugin-mcp": "0.1.3",
29
- "@working-theory/schema": "0.53.0",
30
- "@working-theory/schema-pack-registry": "0.53.0",
31
- "@working-theory/schema-validator": "0.53.0",
32
- "@working-theory/sections-registry": "0.53.0",
33
- "@working-theory/site-config": "0.53.0",
34
- "@working-theory/spoke-checkout": "0.7.9",
35
- "@working-theory/stripe-client": "0.53.0",
36
- "@working-theory/templates": "0.53.0",
37
- "@working-theory/theme": "0.53.0",
38
- "@working-theory/ui": "0.53.0",
39
- "@working-theory/validation": "0.53.0",
40
- "@working-theory/web-i18n": "0.53.0",
41
- "@working-theory/admin": "0.9.0",
42
- "@working-theory/api-commerce": "0.1.7",
43
- "@working-theory/api-content-write": "0.4.0",
44
- "@working-theory/api-hub": "0.3.4",
45
- "@working-theory/api-multi-tenant": "0.2.2",
46
- "@working-theory/api-scheduling": "0.4.4",
47
- "@working-theory/api-signal-collectors": "0.3.0",
48
- "@working-theory/api-workspace": "0.4.0",
29
+ "@working-theory/schema": "0.54.0",
30
+ "@working-theory/schema-pack-registry": "0.54.0",
31
+ "@working-theory/schema-validator": "0.54.0",
32
+ "@working-theory/sections-registry": "0.54.0",
33
+ "@working-theory/site-config": "0.54.0",
34
+ "@working-theory/spoke-checkout": "0.7.10",
35
+ "@working-theory/stripe-client": "0.54.0",
36
+ "@working-theory/templates": "0.54.0",
37
+ "@working-theory/theme": "0.54.0",
38
+ "@working-theory/ui": "0.54.0",
39
+ "@working-theory/validation": "0.54.0",
40
+ "@working-theory/web-i18n": "0.54.0",
41
+ "@working-theory/admin": "0.10.0",
42
+ "@working-theory/api-commerce": "0.1.8",
43
+ "@working-theory/api-content-write": "0.5.0",
44
+ "@working-theory/api-hub": "0.4.0",
45
+ "@working-theory/api-multi-tenant": "0.2.3",
46
+ "@working-theory/api-scheduling": "0.4.5",
47
+ "@working-theory/api-signal-collectors": "0.3.1",
48
+ "@working-theory/api-workspace": "0.5.0",
49
49
  "@working-theory/billing-client": "0.0.0",
50
50
  "@working-theory/branding": "0.0.0",
51
51
  "@working-theory/license-client": "0.0.0",
52
52
  "@working-theory/license-manifest": "0.0.0",
53
- "@working-theory/mcp-scheduling": "0.3.6",
54
- "@working-theory/mcp-signal-collectors": "0.2.7",
55
- "@working-theory/mcp-workspace": "0.5.0",
53
+ "@working-theory/mcp-scheduling": "0.3.7",
54
+ "@working-theory/mcp-signal-collectors": "0.2.8",
55
+ "@working-theory/mcp-workspace": "0.6.0",
56
56
  "@working-theory/partner-session": "0.0.0",
57
57
  "@working-theory/preview-protocol": "0.0.0",
58
- "@working-theory/ui-commerce": "0.23.31",
59
- "@working-theory/ui-docs-blog": "0.23.31",
60
- "@working-theory/ui-scheduling": "0.25.5",
61
- "@working-theory/ui-signal-collectors": "0.27.9",
58
+ "@working-theory/ui-commerce": "0.23.32",
59
+ "@working-theory/ui-docs-blog": "0.23.32",
60
+ "@working-theory/ui-scheduling": "0.25.6",
61
+ "@working-theory/ui-signal-collectors": "0.27.10",
62
62
  "@working-theory/web": "0.0.0",
63
63
  "@working-theory/app-api": "1.0.0",
64
64
  "@working-theory/mcp": "0.0.0",
65
- "@working-theory/database": "0.53.0"
65
+ "@working-theory/database": "0.54.0"
66
66
  }