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.
- package/README.md +71 -3
- package/component-catalog.json +524 -8
- package/component-catalog.md +8 -5
- package/dist/cli.js +99 -0
- package/dist/cutover.d.ts +109 -0
- package/dist/cutover.js +494 -0
- package/dist/design-presets.js +20 -4
- package/dist/export-content.d.ts +55 -0
- package/dist/export-content.js +303 -0
- package/dist/inventory.d.ts +216 -0
- package/dist/inventory.js +833 -0
- package/dist/language-import.d.ts +114 -0
- package/dist/language-import.js +435 -0
- package/dist/lib/env-catalog.js +1 -1
- package/dist/lib/import-mdx.d.ts +38 -0
- package/dist/lib/import-mdx.js +411 -64
- package/dist/lib/render-comparison.d.ts +200 -0
- package/dist/lib/render-comparison.js +374 -0
- package/dist/migrate-content.d.ts +6 -2
- package/dist/migrate-content.js +60 -14
- package/dist/seed-workspace.d.ts +3 -1
- package/dist/seed-workspace.js +45 -16
- package/dist/topology.d.ts +12 -0
- package/dist/topology.js +24 -10
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/content-source-guard.sh +75 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.github/workflows/ci.yml +2 -0
- package/templates/CLAUDE.md +5 -0
- package/templates/apps/mcp/CLAUDE.md +8 -4
- package/templates/apps/web/.env.example +4 -2
- package/templates/apps/web/CLAUDE.md +16 -5
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +153 -4
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +45 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +6 -4
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/__tests__/program-event.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +11 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +44 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.thanks-by-tier.test.tsx +306 -0
- package/templates/apps/web/app/[locale]/layout.tsx +122 -27
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +21 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +16 -9
- package/templates/apps/web/app/sitemap.xml/route.ts +3 -0
- package/templates/apps/web/jest.config.cjs +46 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +247 -4
- package/templates/apps/web/lib/__tests__/in-page-magnet-self-request.test.ts +92 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +90 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +33 -8
- package/templates/apps/web/lib/__tests__/site-identity-feature-list.test.ts +196 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +11 -1
- package/templates/apps/web/lib/bot-check.ts +113 -13
- package/templates/apps/web/lib/in-page-magnet.ts +4 -1
- package/templates/apps/web/lib/page-chrome.ts +65 -5
- package/templates/apps/web/lib/relay.ts +22 -3
- package/templates/apps/web/lib/site-identity.ts +75 -24
- package/templates/apps/web/messages/en.json +5 -1
- package/templates/apps/web/next.config.mjs +15 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +36 -8
- package/templates/apps/web/scripts/content-snapshot.ts +33 -0
- package/templates/package.json +2 -1
- package/templates/tools/check-content-source.ts +108 -0
- 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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
);
|
package/templates/package.json
CHANGED
|
@@ -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
|
-
"
|
|
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.
|
|
3
|
-
"@working-theory/aeo": "0.
|
|
4
|
-
"@working-theory/agent-context": "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.
|
|
9
|
-
"@working-theory/api": "0.
|
|
10
|
-
"@working-theory/api-client": "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.
|
|
13
|
-
"@working-theory/common": "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.
|
|
16
|
-
"@working-theory/content-projection": "0.
|
|
17
|
-
"@working-theory/email": "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.
|
|
20
|
-
"@working-theory/fumadocs-schema": "0.
|
|
21
|
-
"@working-theory/ids": "0.
|
|
22
|
-
"@working-theory/jsonld": "0.
|
|
23
|
-
"@working-theory/license": "0.
|
|
24
|
-
"@working-theory/mcp-server": "0.
|
|
25
|
-
"@working-theory/metadata": "0.
|
|
26
|
-
"@working-theory/observability": "0.
|
|
27
|
-
"@working-theory/page-renderer": "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.
|
|
30
|
-
"@working-theory/schema-pack-registry": "0.
|
|
31
|
-
"@working-theory/schema-validator": "0.
|
|
32
|
-
"@working-theory/sections-registry": "0.
|
|
33
|
-
"@working-theory/site-config": "0.
|
|
34
|
-
"@working-theory/spoke-checkout": "0.7.
|
|
35
|
-
"@working-theory/stripe-client": "0.
|
|
36
|
-
"@working-theory/templates": "0.
|
|
37
|
-
"@working-theory/theme": "0.
|
|
38
|
-
"@working-theory/ui": "0.
|
|
39
|
-
"@working-theory/validation": "0.
|
|
40
|
-
"@working-theory/web-i18n": "0.
|
|
41
|
-
"@working-theory/admin": "0.
|
|
42
|
-
"@working-theory/api-commerce": "0.1.
|
|
43
|
-
"@working-theory/api-content-write": "0.
|
|
44
|
-
"@working-theory/api-hub": "0.
|
|
45
|
-
"@working-theory/api-multi-tenant": "0.2.
|
|
46
|
-
"@working-theory/api-scheduling": "0.4.
|
|
47
|
-
"@working-theory/api-signal-collectors": "0.3.
|
|
48
|
-
"@working-theory/api-workspace": "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.
|
|
54
|
-
"@working-theory/mcp-signal-collectors": "0.2.
|
|
55
|
-
"@working-theory/mcp-workspace": "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.
|
|
59
|
-
"@working-theory/ui-docs-blog": "0.23.
|
|
60
|
-
"@working-theory/ui-scheduling": "0.25.
|
|
61
|
-
"@working-theory/ui-signal-collectors": "0.27.
|
|
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.
|
|
65
|
+
"@working-theory/database": "0.54.0"
|
|
66
66
|
}
|