@olonjs/cli 3.0.159 → 3.0.160
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.
|
@@ -10,6 +10,8 @@ cat << 'END_OF_FILE_CONTENT' > "app/[[...slug]]/page.tsx"
|
|
|
10
10
|
import type { Metadata } from 'next';
|
|
11
11
|
import { notFound } from 'next/navigation';
|
|
12
12
|
import {
|
|
13
|
+
buildPageContractHref,
|
|
14
|
+
buildPageManifestHref,
|
|
13
15
|
resolveCollectionContext,
|
|
14
16
|
resolveRuntimeConfig,
|
|
15
17
|
type PageConfig,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
import { loadVisitorPage } from '@olonjs/next/server';
|
|
18
20
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
19
21
|
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
22
|
+
import { buildVisitorWebPageJsonLd } from '@/lib/buildVisitorWebPageJsonLd';
|
|
20
23
|
import { getFileCollections } from '@/lib/loaders/getFileCollections';
|
|
21
24
|
import { getFilePages } from '@/lib/loaders/getFilePages';
|
|
22
25
|
import { getFileSiteBundle } from '@/lib/loaders/getFileSiteConfig';
|
|
@@ -109,9 +112,23 @@ export default async function VisitorCatchAllPage({ params, searchParams }: Page
|
|
|
109
112
|
const pageNum = Number(firstSearchValue(query.page) ?? '1') || 1;
|
|
110
113
|
const shell = resolveVisitorShell(page, resolved.siteConfig ?? siteConfig);
|
|
111
114
|
const sectionExtras = { authorId, page: pageNum, pathname };
|
|
115
|
+
const title = page.meta?.title ?? result.registrySlug;
|
|
116
|
+
const description = typeof page.meta?.description === 'string' ? page.meta.description : '';
|
|
117
|
+
const jsonLd = buildVisitorWebPageJsonLd({
|
|
118
|
+
title,
|
|
119
|
+
description,
|
|
120
|
+
slug: requestSlug,
|
|
121
|
+
});
|
|
112
122
|
|
|
113
123
|
return (
|
|
114
124
|
<>
|
|
125
|
+
<link rel="mcp-manifest" href={buildPageManifestHref(requestSlug)} />
|
|
126
|
+
<link rel="olon-contract" href={buildPageContractHref(requestSlug)} />
|
|
127
|
+
<script
|
|
128
|
+
type="application/ld+json"
|
|
129
|
+
// eslint-disable-next-line react/no-danger -- Schema.org JSON-LD payload (parity alpha bake)
|
|
130
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
131
|
+
/>
|
|
115
132
|
{shell.header ? (
|
|
116
133
|
<VisitorSection section={shell.header} extras={sectionExtras} />
|
|
117
134
|
) : null}
|
|
@@ -481,20 +498,21 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
481
498
|
"type": "module",
|
|
482
499
|
"scripts": {
|
|
483
500
|
"dev": "next dev",
|
|
484
|
-
"prebuild": "node scripts/sync-pages-to-public.mjs",
|
|
501
|
+
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs && node scripts/bake.mjs && node scripts/sitemap.mjs && node scripts/robots.mjs",
|
|
485
502
|
"build": "next build",
|
|
486
503
|
"start": "next start",
|
|
487
504
|
"lint": "next lint",
|
|
488
505
|
"test": "vitest run",
|
|
489
506
|
"typecheck": "tsc --noEmit",
|
|
507
|
+
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
490
508
|
"dist": "bash ./src2Code.sh --template next app src scripts templates middleware.ts next.config.ts postcss.config.mjs tsconfig.json package.json next-env.d.ts vitest.config.ts",
|
|
491
509
|
"dist:dna": "npm run dist"
|
|
492
510
|
},
|
|
493
511
|
"dependencies": {
|
|
494
|
-
"@olonjs/core": "^1.1.
|
|
495
|
-
"@olonjs/next": "^0.0.
|
|
496
|
-
"@olonjs/react": "^0.1.
|
|
497
|
-
"@olonjs/studio": "^0.1.
|
|
512
|
+
"@olonjs/core": "^1.1.28",
|
|
513
|
+
"@olonjs/next": "^0.0.8",
|
|
514
|
+
"@olonjs/react": "^0.1.11",
|
|
515
|
+
"@olonjs/studio": "^0.1.11",
|
|
498
516
|
"clsx": "^2.1.1",
|
|
499
517
|
"lucide-react": "^0.474.0",
|
|
500
518
|
"next": "^15.5.0",
|
|
@@ -510,6 +528,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
510
528
|
"@types/react": "^19.0.0",
|
|
511
529
|
"@types/react-dom": "^19.0.0",
|
|
512
530
|
"tailwindcss": "^4.0.0",
|
|
531
|
+
"tsx": "^4.20.5",
|
|
513
532
|
"typescript": "^5.7.3",
|
|
514
533
|
"vitest": "^3.0.0"
|
|
515
534
|
}
|
|
@@ -529,6 +548,245 @@ export default config;
|
|
|
529
548
|
|
|
530
549
|
END_OF_FILE_CONTENT
|
|
531
550
|
mkdir -p "scripts"
|
|
551
|
+
echo "Creating scripts/bake.mjs..."
|
|
552
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/bake.mjs"
|
|
553
|
+
/**
|
|
554
|
+
* Next bake entry — agentic WebMCP artifacts only (no Vite / no HTML SSG).
|
|
555
|
+
* Runs scripts/bake.ts via tsx so SECTION_SCHEMAS can be imported from the tenant.
|
|
556
|
+
*/
|
|
557
|
+
import { spawnSync } from 'node:child_process';
|
|
558
|
+
import path from 'node:path';
|
|
559
|
+
import { fileURLToPath } from 'node:url';
|
|
560
|
+
|
|
561
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
562
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
563
|
+
const bakeTs = path.join(__dirname, 'bake.ts');
|
|
564
|
+
|
|
565
|
+
const result = spawnSync(
|
|
566
|
+
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
567
|
+
['tsx', '--tsconfig', 'tsconfig.json', bakeTs],
|
|
568
|
+
{
|
|
569
|
+
cwd: rootDir,
|
|
570
|
+
stdio: 'inherit',
|
|
571
|
+
env: process.env,
|
|
572
|
+
shell: process.platform === 'win32',
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
process.exit(result.status ?? 1);
|
|
577
|
+
|
|
578
|
+
END_OF_FILE_CONTENT
|
|
579
|
+
echo "Creating scripts/bake.ts..."
|
|
580
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/bake.ts"
|
|
581
|
+
/**
|
|
582
|
+
* Next bake — agentic WebMCP artifacts only (no Vite / no HTML SSG).
|
|
583
|
+
* Invoked by scripts/bake.mjs via tsx.
|
|
584
|
+
*/
|
|
585
|
+
import fs from 'node:fs/promises';
|
|
586
|
+
import path from 'node:path';
|
|
587
|
+
import { fileURLToPath } from 'node:url';
|
|
588
|
+
import {
|
|
589
|
+
resolvePageMatchFromRegistry,
|
|
590
|
+
resolvePublicPageDocument,
|
|
591
|
+
webmcp,
|
|
592
|
+
} from '@olonjs/core';
|
|
593
|
+
import { CollectionRegistry } from '../src/lib/CollectionRegistry';
|
|
594
|
+
import { SECTION_SCHEMAS, SECTION_SUBMISSION_SCHEMAS } from '../src/lib/schemas';
|
|
595
|
+
import { getFileCollections } from '../src/lib/loaders/getFileCollections';
|
|
596
|
+
import { getFilePages } from '../src/lib/loaders/getFilePages';
|
|
597
|
+
import { getFileSiteBundle } from '../src/lib/loaders/getFileSiteConfig';
|
|
598
|
+
|
|
599
|
+
const {
|
|
600
|
+
buildPageContract,
|
|
601
|
+
buildPageManifest,
|
|
602
|
+
buildPageManifestHref,
|
|
603
|
+
buildSiteManifest,
|
|
604
|
+
buildLlmsTxt,
|
|
605
|
+
} = webmcp;
|
|
606
|
+
|
|
607
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
608
|
+
const root = path.resolve(__dirname, '..');
|
|
609
|
+
const publicDir = path.join(root, 'public');
|
|
610
|
+
const pagesDir = path.join(root, 'src', 'data', 'pages');
|
|
611
|
+
const collectionsDir = path.join(root, 'src', 'data', 'collections');
|
|
612
|
+
|
|
613
|
+
async function writePublic(relativePath: string, content: string): Promise<void> {
|
|
614
|
+
const target = path.join(publicDir, relativePath);
|
|
615
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
616
|
+
await fs.writeFile(target, content, 'utf-8');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function writePublicJson(relativePath: string, value: unknown): Promise<void> {
|
|
620
|
+
await writePublic(relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function readJsonFile(filePath: string): Promise<unknown> {
|
|
624
|
+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function listJsonFilesRecursive(dir: string): Promise<string[]> {
|
|
628
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
629
|
+
const files: string[] = [];
|
|
630
|
+
for (const item of items) {
|
|
631
|
+
const fullPath = path.join(dir, item.name);
|
|
632
|
+
if (item.isDirectory()) {
|
|
633
|
+
files.push(...(await listJsonFilesRecursive(fullPath)));
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
637
|
+
}
|
|
638
|
+
return files;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function toCanonicalSlug(relativeJsonPath: string): string {
|
|
642
|
+
const slug = relativeJsonPath.replace(/\\/g, '/').replace(/\.json$/i, '').replace(/^\/+|\/+$/g, '');
|
|
643
|
+
if (!slug) throw new Error('[bake] Invalid page slug: empty path segment');
|
|
644
|
+
return slug;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function expandCollectionTarget(slug: string, pageFilePath: string): Promise<string[]> {
|
|
648
|
+
let pageConfig: Record<string, unknown>;
|
|
649
|
+
try {
|
|
650
|
+
pageConfig = (await readJsonFile(pageFilePath)) as Record<string, unknown>;
|
|
651
|
+
} catch {
|
|
652
|
+
return [slug];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const binding = pageConfig?.collection as { source?: string; paramKey?: string } | undefined;
|
|
656
|
+
if (!binding || typeof binding.source !== 'string' || typeof binding.paramKey !== 'string') {
|
|
657
|
+
return [slug];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const token = `[${binding.paramKey}]`;
|
|
661
|
+
const authoredSlug =
|
|
662
|
+
typeof pageConfig.slug === 'string'
|
|
663
|
+
? String(pageConfig.slug).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
|
|
664
|
+
: '';
|
|
665
|
+
const routePattern =
|
|
666
|
+
authoredSlug.includes(token) ? authoredSlug : slug.includes(token) ? slug : '';
|
|
667
|
+
if (!routePattern) return [slug];
|
|
668
|
+
|
|
669
|
+
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
670
|
+
let collection: Record<string, unknown>;
|
|
671
|
+
try {
|
|
672
|
+
collection = (await readJsonFile(collectionPath)) as Record<string, unknown>;
|
|
673
|
+
} catch {
|
|
674
|
+
return [slug];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
678
|
+
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
679
|
+
return itemIds.length > 0
|
|
680
|
+
? itemIds.map((itemId) => routePattern.replace(token, itemId))
|
|
681
|
+
: [slug];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function discoverSlugs(): Promise<string[]> {
|
|
685
|
+
let files: string[] = [];
|
|
686
|
+
try {
|
|
687
|
+
files = await listJsonFilesRecursive(pagesDir);
|
|
688
|
+
} catch {
|
|
689
|
+
files = [];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const rawSlugs = (
|
|
693
|
+
await Promise.all(
|
|
694
|
+
files.map(async (fullPath) => {
|
|
695
|
+
const slug = toCanonicalSlug(path.relative(pagesDir, fullPath));
|
|
696
|
+
return expandCollectionTarget(slug, fullPath);
|
|
697
|
+
}),
|
|
698
|
+
)
|
|
699
|
+
).flat();
|
|
700
|
+
|
|
701
|
+
return Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function main(): Promise<void> {
|
|
705
|
+
console.log('\n[bake] Next agentic artifacts (no Vite SSG)...');
|
|
706
|
+
|
|
707
|
+
const pages = getFilePages(root);
|
|
708
|
+
const collections = getFileCollections(root);
|
|
709
|
+
const { siteConfig, themeConfig, menuConfig } = getFileSiteBundle(root);
|
|
710
|
+
const collectionSchemas = CollectionRegistry as unknown as Record<string, unknown>;
|
|
711
|
+
const schemas = SECTION_SCHEMAS as unknown as Record<string, unknown>;
|
|
712
|
+
const submissionSchemas = SECTION_SUBMISSION_SCHEMAS as unknown as Record<string, unknown>;
|
|
713
|
+
const refDocuments = {
|
|
714
|
+
'menu.json': menuConfig,
|
|
715
|
+
'config/menu.json': menuConfig,
|
|
716
|
+
'src/data/config/menu.json': menuConfig,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const slugs = await discoverSlugs();
|
|
720
|
+
if (slugs.length === 0) {
|
|
721
|
+
throw new Error('[bake] No pages discovered under src/data/pages');
|
|
722
|
+
}
|
|
723
|
+
console.log(`[bake] Targets: ${slugs.join(', ')}`);
|
|
724
|
+
|
|
725
|
+
const pagesForManifest: Record<string, (typeof pages)[string]> = { ...pages };
|
|
726
|
+
|
|
727
|
+
for (const slug of slugs) {
|
|
728
|
+
const pageConfig = resolvePageMatchFromRegistry(pages, slug)?.page;
|
|
729
|
+
if (!pageConfig) continue;
|
|
730
|
+
|
|
731
|
+
const resolvedPageDocument = resolvePublicPageDocument({
|
|
732
|
+
slug,
|
|
733
|
+
pages,
|
|
734
|
+
siteConfig,
|
|
735
|
+
themeConfig,
|
|
736
|
+
menuConfig,
|
|
737
|
+
collections,
|
|
738
|
+
collectionSchemas: collectionSchemas as never,
|
|
739
|
+
refDocuments,
|
|
740
|
+
});
|
|
741
|
+
const publicPageConfig = resolvedPageDocument?.page ?? pageConfig;
|
|
742
|
+
pagesForManifest[slug] = publicPageConfig;
|
|
743
|
+
|
|
744
|
+
await writePublicJson(`pages/${slug}.json`, publicPageConfig);
|
|
745
|
+
|
|
746
|
+
const contract = buildPageContract({
|
|
747
|
+
slug,
|
|
748
|
+
pageConfig: publicPageConfig,
|
|
749
|
+
schemas: schemas as never,
|
|
750
|
+
submissionSchemas: submissionSchemas as never,
|
|
751
|
+
siteConfig,
|
|
752
|
+
});
|
|
753
|
+
await writePublicJson(`schemas/${slug}.schema.json`, contract);
|
|
754
|
+
|
|
755
|
+
const pageManifest = buildPageManifest({
|
|
756
|
+
slug,
|
|
757
|
+
pageConfig: publicPageConfig,
|
|
758
|
+
schemas: schemas as never,
|
|
759
|
+
siteConfig,
|
|
760
|
+
});
|
|
761
|
+
await writePublicJson(buildPageManifestHref(slug).replace(/^\//, ''), pageManifest);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
await writePublicJson('config/site.json', siteConfig);
|
|
765
|
+
|
|
766
|
+
const mcpManifest = buildSiteManifest({
|
|
767
|
+
pages: pagesForManifest,
|
|
768
|
+
schemas: schemas as never,
|
|
769
|
+
siteConfig,
|
|
770
|
+
});
|
|
771
|
+
await writePublicJson('mcp-manifest.json', mcpManifest);
|
|
772
|
+
|
|
773
|
+
const llmsTxtContent = buildLlmsTxt({
|
|
774
|
+
pages: pagesForManifest,
|
|
775
|
+
schemas: schemas as never,
|
|
776
|
+
siteConfig,
|
|
777
|
+
});
|
|
778
|
+
await writePublic('llms.txt', `${llmsTxtContent}\n`);
|
|
779
|
+
|
|
780
|
+
console.log('[bake] Wrote public/mcp-manifest.json, mcp-manifests/, schemas/, pages/, llms.txt, config/site.json');
|
|
781
|
+
console.log('[bake] OK\n');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
main().catch((error) => {
|
|
785
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
786
|
+
process.exit(1);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
END_OF_FILE_CONTENT
|
|
532
790
|
echo "Creating scripts/generate-SystemsArchitect-next.test.mjs..."
|
|
533
791
|
cat << 'END_OF_FILE_CONTENT' > "scripts/generate-SystemsArchitect-next.test.mjs"
|
|
534
792
|
/**
|
|
@@ -581,12 +839,15 @@ describe('generate_SystemsArchitect_next.sh harness gates', () => {
|
|
|
581
839
|
assert.match(src, /cd "\$\(cd "\$\(dirname "\$\{BASH_SOURCE\[0\]\}"\)\/\.\." && pwd\)"/);
|
|
582
840
|
});
|
|
583
841
|
|
|
584
|
-
it('must
|
|
842
|
+
it('must wipe tenant content surfaces without DNA denylist', () => {
|
|
585
843
|
const src = readScript();
|
|
586
|
-
assert.match(src, /
|
|
587
|
-
assert.match(src, /
|
|
844
|
+
assert.match(src, /Wiping tenant content surfaces/);
|
|
845
|
+
assert.match(src, /find src\/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin'/);
|
|
846
|
+
assert.match(src, /rm -rf \\\s*\n\s*src\/collections/m);
|
|
847
|
+
assert.match(src, /rm -rf \\\s*\n[\s\S]*?src\/data\/pages/m);
|
|
588
848
|
assert.match(src, /cat > src\/lib\/VisitorSection\.tsx/);
|
|
589
849
|
assert.doesNotMatch(src, /from '@\/components\/books-list'/);
|
|
850
|
+
assert.doesNotMatch(src, /src\/components\/books-list/);
|
|
590
851
|
});
|
|
591
852
|
|
|
592
853
|
it('must install react-markdown deps used by post-detail', () => {
|
|
@@ -597,11 +858,10 @@ describe('generate_SystemsArchitect_next.sh harness gates', () => {
|
|
|
597
858
|
assert.match(src, /from 'rehype-sanitize'/);
|
|
598
859
|
});
|
|
599
860
|
|
|
600
|
-
it('must not generate empty-tenant registry wiring
|
|
861
|
+
it('must not generate empty-tenant registry wiring', () => {
|
|
601
862
|
const src = readScript();
|
|
602
863
|
assert.doesNotMatch(src, /@\/components\/empty-tenant/);
|
|
603
864
|
assert.doesNotMatch(src, /['"]empty-tenant['"]/);
|
|
604
|
-
assert.match(src, /src\/components\/empty-tenant/);
|
|
605
865
|
});
|
|
606
866
|
|
|
607
867
|
it('must force shadcn radix base non-interactively', () => {
|
|
@@ -664,15 +924,435 @@ describe('generate_inkwell_next.sh harness gates', () => {
|
|
|
664
924
|
assert.match(src, /cd "\$\(cd "\$\(dirname "\$\{BASH_SOURCE\[0\]\}"\)\/\.\." && pwd\)"/);
|
|
665
925
|
});
|
|
666
926
|
|
|
667
|
-
it('must
|
|
927
|
+
it('must wipe tenant content surfaces without DNA denylist', () => {
|
|
668
928
|
const src = readScript();
|
|
669
|
-
assert.match(src, /
|
|
670
|
-
assert.match(src, /
|
|
929
|
+
assert.match(src, /Wiping tenant content surfaces/);
|
|
930
|
+
assert.match(src, /find src\/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin'/);
|
|
931
|
+
assert.match(src, /rm -rf \\\s*\n\s*src\/collections/m);
|
|
932
|
+
assert.match(src, /rm -rf \\\s*\n[\s\S]*?src\/data\/pages/m);
|
|
671
933
|
assert.match(src, /cat > src\/lib\/VisitorSection\.tsx/);
|
|
934
|
+
assert.match(src, /EmptyTenantView empty-branch/);
|
|
672
935
|
assert.doesNotMatch(src, /from '@\/components\/books-list'/);
|
|
936
|
+
assert.doesNotMatch(src, /src\/components\/books-list/);
|
|
673
937
|
});
|
|
674
938
|
});
|
|
675
939
|
|
|
940
|
+
END_OF_FILE_CONTENT
|
|
941
|
+
echo "Creating scripts/generate-llms-txt.mjs..."
|
|
942
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/generate-llms-txt.mjs"
|
|
943
|
+
import fs from 'fs';
|
|
944
|
+
import path from 'path';
|
|
945
|
+
import { fileURLToPath } from 'url';
|
|
946
|
+
import { webmcp } from '@olonjs/core';
|
|
947
|
+
|
|
948
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
949
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
950
|
+
const { buildLlmsTxt } = webmcp;
|
|
951
|
+
|
|
952
|
+
const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
953
|
+
const siteConfig = JSON.parse(fs.readFileSync(path.join(rootDir, 'src', 'data', 'config', 'site.json'), 'utf-8'));
|
|
954
|
+
|
|
955
|
+
function listJsonFilesRecursive(dir) {
|
|
956
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
957
|
+
const files = [];
|
|
958
|
+
for (const item of items) {
|
|
959
|
+
const fullPath = path.join(dir, item.name);
|
|
960
|
+
if (item.isDirectory()) {
|
|
961
|
+
files.push(...listJsonFilesRecursive(fullPath));
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
965
|
+
}
|
|
966
|
+
return files;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const pages = {};
|
|
970
|
+
for (const fullPath of listJsonFilesRecursive(pagesDir)) {
|
|
971
|
+
const slug = path.relative(pagesDir, fullPath).replace(/\\/g, '/').replace(/\.json$/i, '');
|
|
972
|
+
pages[slug] = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const llmsTxt = buildLlmsTxt({ pages, schemas: {}, siteConfig });
|
|
976
|
+
|
|
977
|
+
const outPath = path.join(rootDir, 'public', 'llms.txt');
|
|
978
|
+
fs.writeFileSync(outPath, llmsTxt, 'utf-8');
|
|
979
|
+
console.log('[generate-llms-txt] Written -> public/llms.txt');
|
|
980
|
+
|
|
981
|
+
END_OF_FILE_CONTENT
|
|
982
|
+
echo "Creating scripts/prebuild-bake.test.mjs..."
|
|
983
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-bake.test.mjs"
|
|
984
|
+
/**
|
|
985
|
+
* Gates for bake.mjs (Task 4 — plan 001). Agentic artifacts only; no Vite SSG.
|
|
986
|
+
* Run: node --test scripts/prebuild-bake.test.mjs
|
|
987
|
+
*/
|
|
988
|
+
import assert from 'node:assert/strict';
|
|
989
|
+
import fs from 'node:fs';
|
|
990
|
+
import path from 'node:path';
|
|
991
|
+
import { spawnSync } from 'node:child_process';
|
|
992
|
+
import { describe, it } from 'node:test';
|
|
993
|
+
import { fileURLToPath } from 'node:url';
|
|
994
|
+
|
|
995
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
996
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
997
|
+
const SCRIPT = path.join(__dirname, 'bake.mjs');
|
|
998
|
+
|
|
999
|
+
describe('next bake.mjs (agentic artifacts)', () => {
|
|
1000
|
+
it('exists and does not import vite / write HTML SSG', () => {
|
|
1001
|
+
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1002
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1003
|
+
assert.doesNotMatch(src, /from ['"]vite['"]/);
|
|
1004
|
+
assert.doesNotMatch(src, /vite\.build|entry-ssg/);
|
|
1005
|
+
// May spawn tsx helper — check companion if present
|
|
1006
|
+
const impl = path.join(__dirname, 'bake.ts');
|
|
1007
|
+
if (fs.existsSync(impl)) {
|
|
1008
|
+
const implSrc = fs.readFileSync(impl, 'utf8');
|
|
1009
|
+
assert.doesNotMatch(implSrc, /from ['"]vite['"]/);
|
|
1010
|
+
assert.doesNotMatch(implSrc, /entry-ssg/);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('writes reachable public agentic artifacts', () => {
|
|
1015
|
+
const result = spawnSync(process.execPath, [SCRIPT], {
|
|
1016
|
+
cwd: ROOT,
|
|
1017
|
+
encoding: 'utf8',
|
|
1018
|
+
env: { ...process.env },
|
|
1019
|
+
});
|
|
1020
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1021
|
+
|
|
1022
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'mcp-manifest.json')));
|
|
1023
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'llms.txt')));
|
|
1024
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'schemas', 'home.schema.json')));
|
|
1025
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'mcp-manifests', 'home.json')));
|
|
1026
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'config', 'site.json')));
|
|
1027
|
+
|
|
1028
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'public', 'mcp-manifest.json'), 'utf8'));
|
|
1029
|
+
assert.equal(manifest.kind, 'olonjs-mcp-manifest-index');
|
|
1030
|
+
assert.ok(Array.isArray(manifest.pages));
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
END_OF_FILE_CONTENT
|
|
1035
|
+
echo "Creating scripts/prebuild-generate-llms-txt.test.mjs..."
|
|
1036
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-generate-llms-txt.test.mjs"
|
|
1037
|
+
/**
|
|
1038
|
+
* Gates for generate-llms-txt.mjs (Task 2 — plan 001).
|
|
1039
|
+
* Run: node --test scripts/prebuild-generate-llms-txt.test.mjs
|
|
1040
|
+
*/
|
|
1041
|
+
import assert from 'node:assert/strict';
|
|
1042
|
+
import fs from 'node:fs';
|
|
1043
|
+
import path from 'node:path';
|
|
1044
|
+
import { spawnSync } from 'node:child_process';
|
|
1045
|
+
import { describe, it } from 'node:test';
|
|
1046
|
+
import { fileURLToPath } from 'node:url';
|
|
1047
|
+
|
|
1048
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1049
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1050
|
+
const SCRIPT = path.join(__dirname, 'generate-llms-txt.mjs');
|
|
1051
|
+
|
|
1052
|
+
describe('next prebuild generate-llms-txt', () => {
|
|
1053
|
+
it('script exists and uses @olonjs/core webmcp', () => {
|
|
1054
|
+
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1055
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1056
|
+
assert.match(src, /@olonjs\/core/);
|
|
1057
|
+
assert.match(src, /buildLlmsTxt|webmcp/);
|
|
1058
|
+
assert.match(src, /public\/llms\.txt|llms\.txt/);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('writes public/llms.txt when run', () => {
|
|
1062
|
+
const out = path.join(ROOT, 'public', 'llms.txt');
|
|
1063
|
+
if (fs.existsSync(out)) fs.unlinkSync(out);
|
|
1064
|
+
|
|
1065
|
+
const result = spawnSync(process.execPath, [SCRIPT], { cwd: ROOT, encoding: 'utf8' });
|
|
1066
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1067
|
+
assert.ok(fs.existsSync(out));
|
|
1068
|
+
const body = fs.readFileSync(out, 'utf8');
|
|
1069
|
+
assert.ok(body.trim().length > 0);
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
END_OF_FILE_CONTENT
|
|
1074
|
+
echo "Creating scripts/prebuild-robots-sitemap.test.mjs..."
|
|
1075
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-robots-sitemap.test.mjs"
|
|
1076
|
+
/**
|
|
1077
|
+
* Static + smoke gates for robots.mjs / sitemap.mjs (Task 1 — plan 001).
|
|
1078
|
+
* Run: node --test scripts/prebuild-robots-sitemap.test.mjs
|
|
1079
|
+
*/
|
|
1080
|
+
import assert from 'node:assert/strict';
|
|
1081
|
+
import fs from 'node:fs';
|
|
1082
|
+
import path from 'node:path';
|
|
1083
|
+
import { spawnSync } from 'node:child_process';
|
|
1084
|
+
import { describe, it } from 'node:test';
|
|
1085
|
+
import { fileURLToPath } from 'node:url';
|
|
1086
|
+
|
|
1087
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1088
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1089
|
+
const ROBOTS = path.join(__dirname, 'robots.mjs');
|
|
1090
|
+
const SITEMAP = path.join(__dirname, 'sitemap.mjs');
|
|
1091
|
+
|
|
1092
|
+
describe('next prebuild robots + sitemap', () => {
|
|
1093
|
+
it('scripts exist', () => {
|
|
1094
|
+
assert.ok(fs.existsSync(ROBOTS), `missing ${ROBOTS}`);
|
|
1095
|
+
assert.ok(fs.existsSync(SITEMAP), `missing ${SITEMAP}`);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('defaults to Next localhost:3000 (not Vite 5173)', () => {
|
|
1099
|
+
const robots = fs.readFileSync(ROBOTS, 'utf8');
|
|
1100
|
+
const sitemap = fs.readFileSync(SITEMAP, 'utf8');
|
|
1101
|
+
assert.match(robots, /localhost:3000/);
|
|
1102
|
+
assert.match(sitemap, /localhost:3000/);
|
|
1103
|
+
assert.doesNotMatch(robots, /localhost:5173/);
|
|
1104
|
+
assert.doesNotMatch(sitemap, /localhost:5173/);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('writes public/robots.txt and public/sitemap.xml when run', () => {
|
|
1108
|
+
const robotsOut = path.join(ROOT, 'public', 'robots.txt');
|
|
1109
|
+
const sitemapOut = path.join(ROOT, 'public', 'sitemap.xml');
|
|
1110
|
+
for (const p of [robotsOut, sitemapOut]) {
|
|
1111
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const r1 = spawnSync(process.execPath, [ROBOTS], { cwd: ROOT, encoding: 'utf8' });
|
|
1115
|
+
assert.equal(r1.status, 0, r1.stderr || r1.stdout);
|
|
1116
|
+
const r2 = spawnSync(process.execPath, [SITEMAP], { cwd: ROOT, encoding: 'utf8' });
|
|
1117
|
+
assert.equal(r2.status, 0, r2.stderr || r2.stdout);
|
|
1118
|
+
|
|
1119
|
+
assert.ok(fs.existsSync(robotsOut));
|
|
1120
|
+
assert.ok(fs.existsSync(sitemapOut));
|
|
1121
|
+
const robotsTxt = fs.readFileSync(robotsOut, 'utf8');
|
|
1122
|
+
const sitemapXml = fs.readFileSync(sitemapOut, 'utf8');
|
|
1123
|
+
assert.match(robotsTxt, /Sitemap: http:\/\/localhost:3000\/sitemap\.xml/);
|
|
1124
|
+
assert.match(sitemapXml, /<urlset/);
|
|
1125
|
+
assert.match(sitemapXml, /\/home\.json|PAGE: HOME|loc>http:\/\/localhost:3000\/</);
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
END_OF_FILE_CONTENT
|
|
1130
|
+
echo "Creating scripts/prebuild-webmcp-feature-check.test.mjs..."
|
|
1131
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-webmcp-feature-check.test.mjs"
|
|
1132
|
+
/**
|
|
1133
|
+
* Gates for webmcp-feature-check.mjs (Task 3 — plan 001).
|
|
1134
|
+
* Run: node --test scripts/prebuild-webmcp-feature-check.test.mjs
|
|
1135
|
+
*/
|
|
1136
|
+
import assert from 'node:assert/strict';
|
|
1137
|
+
import fs from 'node:fs';
|
|
1138
|
+
import path from 'node:path';
|
|
1139
|
+
import { describe, it } from 'node:test';
|
|
1140
|
+
import { fileURLToPath } from 'node:url';
|
|
1141
|
+
|
|
1142
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1143
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1144
|
+
const SCRIPT = path.join(__dirname, 'webmcp-feature-check.mjs');
|
|
1145
|
+
const PKG = path.join(ROOT, 'package.json');
|
|
1146
|
+
|
|
1147
|
+
describe('next verify:webmcp script', () => {
|
|
1148
|
+
it('exists and uses document.modelContextTesting only', () => {
|
|
1149
|
+
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1150
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1151
|
+
assert.match(src, /document\.modelContextTesting/);
|
|
1152
|
+
assert.doesNotMatch(src, /navigator\.modelContextTesting/);
|
|
1153
|
+
assert.doesNotMatch(src, /navigator\.modelContext(?!Testing)/);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it('is wired as verify:webmcp and not in prebuild', () => {
|
|
1157
|
+
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
|
|
1158
|
+
assert.equal(pkg.scripts?.['verify:webmcp'], 'node scripts/webmcp-feature-check.mjs');
|
|
1159
|
+
assert.ok(pkg.scripts?.prebuild);
|
|
1160
|
+
assert.doesNotMatch(pkg.scripts.prebuild, /webmcp-feature-check/);
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
END_OF_FILE_CONTENT
|
|
1165
|
+
echo "Creating scripts/prebuild-wire.test.mjs..."
|
|
1166
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-wire.test.mjs"
|
|
1167
|
+
/**
|
|
1168
|
+
* Gates for package.json prebuild wiring (Task 5 — plan 001).
|
|
1169
|
+
* Run: node --test scripts/prebuild-wire.test.mjs
|
|
1170
|
+
*/
|
|
1171
|
+
import assert from 'node:assert/strict';
|
|
1172
|
+
import fs from 'node:fs';
|
|
1173
|
+
import path from 'node:path';
|
|
1174
|
+
import { describe, it } from 'node:test';
|
|
1175
|
+
import { fileURLToPath } from 'node:url';
|
|
1176
|
+
|
|
1177
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1178
|
+
const PKG = path.resolve(__dirname, '../package.json');
|
|
1179
|
+
|
|
1180
|
+
describe('next prebuild wiring', () => {
|
|
1181
|
+
it('runs sync → llms → bake → sitemap → robots (alpha order)', () => {
|
|
1182
|
+
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
|
|
1183
|
+
const prebuild = pkg.scripts?.prebuild ?? '';
|
|
1184
|
+
assert.match(prebuild, /sync-pages-to-public\.mjs/);
|
|
1185
|
+
assert.match(prebuild, /generate-llms-txt\.mjs/);
|
|
1186
|
+
assert.match(prebuild, /bake\.mjs/);
|
|
1187
|
+
assert.match(prebuild, /sitemap\.mjs/);
|
|
1188
|
+
assert.match(prebuild, /robots\.mjs/);
|
|
1189
|
+
assert.doesNotMatch(prebuild, /webmcp-feature-check/);
|
|
1190
|
+
|
|
1191
|
+
const order = ['sync-pages-to-public', 'generate-llms-txt', 'bake', 'sitemap', 'robots'].map((name) =>
|
|
1192
|
+
prebuild.indexOf(name),
|
|
1193
|
+
);
|
|
1194
|
+
for (let i = 1; i < order.length; i += 1) {
|
|
1195
|
+
assert.ok(order[i] > order[i - 1], `expected ${i} after previous in: ${prebuild}`);
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('dist DNA includes scripts/', () => {
|
|
1200
|
+
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
|
|
1201
|
+
assert.match(pkg.scripts?.dist ?? '', /\bscripts\b/);
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
END_OF_FILE_CONTENT
|
|
1206
|
+
echo "Creating scripts/robots.mjs..."
|
|
1207
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/robots.mjs"
|
|
1208
|
+
import fs from 'fs';
|
|
1209
|
+
import path from 'path';
|
|
1210
|
+
import { fileURLToPath } from 'url';
|
|
1211
|
+
|
|
1212
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1213
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1214
|
+
|
|
1215
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
1216
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
1217
|
+
: 'http://localhost:3000';
|
|
1218
|
+
|
|
1219
|
+
const robotsTxt = `User-agent: *
|
|
1220
|
+
Allow: /
|
|
1221
|
+
Disallow: /api/
|
|
1222
|
+
|
|
1223
|
+
User-agent: GPTBot
|
|
1224
|
+
User-agent: ChatGPT-User
|
|
1225
|
+
User-agent: ClaudeBot
|
|
1226
|
+
User-agent: Claude-Web
|
|
1227
|
+
User-agent: PerplexityBot
|
|
1228
|
+
User-agent: OAI-SearchBot
|
|
1229
|
+
Allow: /
|
|
1230
|
+
Allow: /*.json
|
|
1231
|
+
Allow: /schemas/
|
|
1232
|
+
Allow: /llms.txt
|
|
1233
|
+
Allow: /mcp-manifest.json
|
|
1234
|
+
Disallow: /api/
|
|
1235
|
+
|
|
1236
|
+
Sitemap: ${baseUrl}/sitemap.xml
|
|
1237
|
+
`;
|
|
1238
|
+
|
|
1239
|
+
const outPath = path.join(rootDir, 'public', 'robots.txt');
|
|
1240
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1241
|
+
fs.writeFileSync(outPath, robotsTxt, 'utf-8');
|
|
1242
|
+
console.log('[robots] Written -> public/robots.txt');
|
|
1243
|
+
|
|
1244
|
+
END_OF_FILE_CONTENT
|
|
1245
|
+
echo "Creating scripts/sitemap.mjs..."
|
|
1246
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/sitemap.mjs"
|
|
1247
|
+
import fs from 'fs';
|
|
1248
|
+
import path from 'path';
|
|
1249
|
+
import { fileURLToPath } from 'url';
|
|
1250
|
+
|
|
1251
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1252
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1253
|
+
|
|
1254
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
1255
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
1256
|
+
: 'http://localhost:3000';
|
|
1257
|
+
|
|
1258
|
+
function listJsonFilesRecursive(dir) {
|
|
1259
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
1260
|
+
const files = [];
|
|
1261
|
+
for (const item of items) {
|
|
1262
|
+
const fullPath = path.join(dir, item.name);
|
|
1263
|
+
if (item.isDirectory()) {
|
|
1264
|
+
files.push(...listJsonFilesRecursive(fullPath));
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
1268
|
+
}
|
|
1269
|
+
return files;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function toW3CDate(date) {
|
|
1273
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function urlEntry({ loc, lastmod, changefreq, priority, comment }) {
|
|
1277
|
+
const lines = [];
|
|
1278
|
+
if (comment) lines.push(` <!-- ${comment} -->`);
|
|
1279
|
+
lines.push(` <url>`);
|
|
1280
|
+
lines.push(` <loc>${loc}</loc>`);
|
|
1281
|
+
lines.push(` <lastmod>${lastmod}</lastmod>`);
|
|
1282
|
+
lines.push(` <changefreq>${changefreq}</changefreq>`);
|
|
1283
|
+
lines.push(` <priority>${priority}</priority>`);
|
|
1284
|
+
lines.push(` </url>`);
|
|
1285
|
+
return lines.join('\n');
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function sectionComment(label) {
|
|
1289
|
+
const bar = '='.repeat(42);
|
|
1290
|
+
return [
|
|
1291
|
+
` <!-- ${bar} -->`,
|
|
1292
|
+
` <!-- ${label.padEnd(42)} -->`,
|
|
1293
|
+
` <!-- ${bar} -->`,
|
|
1294
|
+
].join('\n');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
1298
|
+
const buildTime = toW3CDate(new Date());
|
|
1299
|
+
|
|
1300
|
+
const pageFiles = listJsonFilesRecursive(pagesDir);
|
|
1301
|
+
const pages = pageFiles.map((fullPath) => {
|
|
1302
|
+
const slug = path
|
|
1303
|
+
.relative(pagesDir, fullPath)
|
|
1304
|
+
.replace(/\\/g, '/')
|
|
1305
|
+
.replace(/\.json$/i, '');
|
|
1306
|
+
const lastmod = toW3CDate(fs.statSync(fullPath).mtime);
|
|
1307
|
+
return { slug, lastmod };
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
const entries = [];
|
|
1311
|
+
|
|
1312
|
+
entries.push(sectionComment('GLOBAL AGENT DISCOVERY NODES'));
|
|
1313
|
+
entries.push(
|
|
1314
|
+
urlEntry({ loc: `${baseUrl}/llms.txt`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
1315
|
+
);
|
|
1316
|
+
entries.push(
|
|
1317
|
+
urlEntry({ loc: `${baseUrl}/mcp-manifest.json`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
1318
|
+
);
|
|
1319
|
+
|
|
1320
|
+
for (const { slug, lastmod } of pages) {
|
|
1321
|
+
const humanPath = slug === 'home' ? '/' : `/${slug}`;
|
|
1322
|
+
const label = `PAGE: ${slug.toUpperCase()}`;
|
|
1323
|
+
|
|
1324
|
+
entries.push(sectionComment(label));
|
|
1325
|
+
entries.push(
|
|
1326
|
+
urlEntry({ loc: `${baseUrl}${humanPath}`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Human UI' }),
|
|
1327
|
+
);
|
|
1328
|
+
entries.push(
|
|
1329
|
+
urlEntry({ loc: `${baseUrl}/${slug}.json`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Machine Payload' }),
|
|
1330
|
+
);
|
|
1331
|
+
entries.push(
|
|
1332
|
+
urlEntry({
|
|
1333
|
+
loc: `${baseUrl}/schemas/${slug}.schema.json`,
|
|
1334
|
+
lastmod: buildTime,
|
|
1335
|
+
changefreq: 'weekly',
|
|
1336
|
+
priority: '0.8',
|
|
1337
|
+
comment: 'Machine Contract (Schema)',
|
|
1338
|
+
}),
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const xml = [
|
|
1343
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
1344
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
|
1345
|
+
``,
|
|
1346
|
+
entries.join('\n'),
|
|
1347
|
+
``,
|
|
1348
|
+
`</urlset>`,
|
|
1349
|
+
].join('\n');
|
|
1350
|
+
|
|
1351
|
+
const outPath = path.join(rootDir, 'public', 'sitemap.xml');
|
|
1352
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1353
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
1354
|
+
console.log('[sitemap] Written -> public/sitemap.xml');
|
|
1355
|
+
|
|
676
1356
|
END_OF_FILE_CONTENT
|
|
677
1357
|
echo "Creating scripts/sync-pages-to-public.mjs..."
|
|
678
1358
|
cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
|
|
@@ -723,6 +1403,308 @@ if (fs.existsSync(sourceSiteConfigPath)) {
|
|
|
723
1403
|
|
|
724
1404
|
console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
|
|
725
1405
|
|
|
1406
|
+
END_OF_FILE_CONTENT
|
|
1407
|
+
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
1408
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
1409
|
+
import fs from 'fs/promises';
|
|
1410
|
+
import path from 'path';
|
|
1411
|
+
import { fileURLToPath } from 'url';
|
|
1412
|
+
import { createRequire } from 'module';
|
|
1413
|
+
|
|
1414
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1415
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1416
|
+
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:3000';
|
|
1417
|
+
|
|
1418
|
+
function pageFilePathFromSlug(slug) {
|
|
1419
|
+
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function adminUrlFromSlug(slug) {
|
|
1423
|
+
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function isStringSchema(schema) {
|
|
1427
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
1428
|
+
if (schema.type === 'string') return true;
|
|
1429
|
+
if (Array.isArray(schema.anyOf)) {
|
|
1430
|
+
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
1431
|
+
}
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function findTopLevelStringField(sectionSchema) {
|
|
1436
|
+
const properties = sectionSchema?.properties;
|
|
1437
|
+
if (!properties || typeof properties !== 'object') return null;
|
|
1438
|
+
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
1439
|
+
for (const key of preferred) {
|
|
1440
|
+
if (isStringSchema(properties[key])) return key;
|
|
1441
|
+
}
|
|
1442
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
1443
|
+
if (isStringSchema(value)) return key;
|
|
1444
|
+
}
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
async function loadPlaywright() {
|
|
1449
|
+
const require = createRequire(import.meta.url);
|
|
1450
|
+
try {
|
|
1451
|
+
return require('playwright');
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
async function readPageJson(slug) {
|
|
1461
|
+
const pageFilePath = pageFilePathFromSlug(slug);
|
|
1462
|
+
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
1463
|
+
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
async function waitFor(predicate, timeoutMs, label) {
|
|
1467
|
+
const startedAt = Date.now();
|
|
1468
|
+
for (;;) {
|
|
1469
|
+
const result = await predicate();
|
|
1470
|
+
if (result) return result;
|
|
1471
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
1472
|
+
throw new Error(`Timed out while waiting for ${label}.`);
|
|
1473
|
+
}
|
|
1474
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
1479
|
+
await waitFor(async () => {
|
|
1480
|
+
const { json } = await readPageJson(slug);
|
|
1481
|
+
const section = Array.isArray(json.sections)
|
|
1482
|
+
? json.sections.find((item) => item?.id === sectionId)
|
|
1483
|
+
: null;
|
|
1484
|
+
return section?.data?.[fieldKey] === expectedValue;
|
|
1485
|
+
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function ensureResponseOk(response, label) {
|
|
1489
|
+
if (!response.ok) {
|
|
1490
|
+
const text = await response.text();
|
|
1491
|
+
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
1492
|
+
}
|
|
1493
|
+
return response;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async function fetchJson(relativePath, label) {
|
|
1497
|
+
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
1498
|
+
return response.json();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
async function selectTarget() {
|
|
1502
|
+
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
1503
|
+
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1504
|
+
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1505
|
+
: null;
|
|
1506
|
+
|
|
1507
|
+
const candidatePages = requestedSlug
|
|
1508
|
+
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
1509
|
+
: (siteIndex.pages ?? []);
|
|
1510
|
+
|
|
1511
|
+
for (const pageEntry of candidatePages) {
|
|
1512
|
+
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
1513
|
+
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
1514
|
+
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
1515
|
+
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
1516
|
+
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
1517
|
+
: [];
|
|
1518
|
+
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
1519
|
+
|
|
1520
|
+
for (const tool of tools) {
|
|
1521
|
+
const sectionType = tool?.sectionType;
|
|
1522
|
+
if (typeof tool?.name !== 'string' || typeof sectionType !== 'string') continue;
|
|
1523
|
+
const targetInstance = localInstances.find((section) => section?.type === sectionType);
|
|
1524
|
+
if (!targetInstance?.id) continue;
|
|
1525
|
+
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
1526
|
+
if (!targetFieldKey) continue;
|
|
1527
|
+
const pageState = await readPageJson(pageEntry.slug);
|
|
1528
|
+
const section = Array.isArray(pageState.json.sections)
|
|
1529
|
+
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
1530
|
+
: null;
|
|
1531
|
+
const originalValue = section?.data?.[targetFieldKey];
|
|
1532
|
+
if (typeof originalValue !== 'string') continue;
|
|
1533
|
+
|
|
1534
|
+
return {
|
|
1535
|
+
slug: pageEntry.slug,
|
|
1536
|
+
manifestHref: pageEntry.manifestHref,
|
|
1537
|
+
contractHref: pageEntry.contractHref,
|
|
1538
|
+
toolName: tool.name,
|
|
1539
|
+
sectionId: targetInstance.id,
|
|
1540
|
+
fieldKey: targetFieldKey,
|
|
1541
|
+
originalValue,
|
|
1542
|
+
originalState: pageState,
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
requestedSlug
|
|
1549
|
+
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
1550
|
+
: 'No valid WebMCP verification target found in manifest index.'
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async function main() {
|
|
1555
|
+
const { chromium } = await loadPlaywright();
|
|
1556
|
+
const target = await selectTarget();
|
|
1557
|
+
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
1558
|
+
const browser = await chromium.launch({ headless: true });
|
|
1559
|
+
const page = await browser.newPage();
|
|
1560
|
+
const consoleEvents = [];
|
|
1561
|
+
let mutationApplied = false;
|
|
1562
|
+
|
|
1563
|
+
page.on('console', (message) => {
|
|
1564
|
+
if (message.type() === 'error' || message.type() === 'warning') {
|
|
1565
|
+
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
page.on('pageerror', (error) => {
|
|
1569
|
+
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
const restoreOriginal = async () => {
|
|
1573
|
+
try {
|
|
1574
|
+
await page.evaluate(
|
|
1575
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
1576
|
+
const runtime = document.modelContextTesting;
|
|
1577
|
+
if (!runtime?.executeTool) return;
|
|
1578
|
+
await runtime.executeTool(
|
|
1579
|
+
toolName,
|
|
1580
|
+
JSON.stringify({
|
|
1581
|
+
slug,
|
|
1582
|
+
sectionId,
|
|
1583
|
+
fieldKey,
|
|
1584
|
+
value,
|
|
1585
|
+
})
|
|
1586
|
+
);
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
toolName: target.toolName,
|
|
1590
|
+
slug: target.slug,
|
|
1591
|
+
sectionId: target.sectionId,
|
|
1592
|
+
fieldKey: target.fieldKey,
|
|
1593
|
+
value: target.originalValue,
|
|
1594
|
+
}
|
|
1595
|
+
);
|
|
1596
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
|
|
1597
|
+
} catch {
|
|
1598
|
+
await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
try {
|
|
1603
|
+
const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
|
|
1604
|
+
if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
|
|
1605
|
+
throw new Error(`Manifest does not expose ${target.toolName}.`);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const pageContract = await fetchJson(target.contractHref, `Page contract request for ${target.slug}`);
|
|
1609
|
+
if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
|
|
1610
|
+
throw new Error(`Page contract does not expose ${target.toolName}.`);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
|
|
1614
|
+
|
|
1615
|
+
try {
|
|
1616
|
+
await page.waitForFunction(
|
|
1617
|
+
({ manifestHref, contractHref }) => {
|
|
1618
|
+
const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
|
|
1619
|
+
const contractLink = document.head.querySelector('link[rel="olon-contract"]');
|
|
1620
|
+
return manifestLink?.getAttribute('href') === manifestHref
|
|
1621
|
+
&& contractLink?.getAttribute('href') === contractHref;
|
|
1622
|
+
},
|
|
1623
|
+
{ manifestHref: target.manifestHref, contractHref: target.contractHref },
|
|
1624
|
+
{ timeout: 10_000 }
|
|
1625
|
+
);
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
const diagnostics = await page.evaluate(() => ({
|
|
1628
|
+
head: document.head.innerHTML,
|
|
1629
|
+
bodyText: document.body.innerText,
|
|
1630
|
+
}));
|
|
1631
|
+
throw new Error(
|
|
1632
|
+
[
|
|
1633
|
+
error instanceof Error ? error.message : String(error),
|
|
1634
|
+
`head=${diagnostics.head}`,
|
|
1635
|
+
`body=${diagnostics.bodyText}`,
|
|
1636
|
+
...consoleEvents,
|
|
1637
|
+
].join('\n')
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const toolNames = await page.evaluate(() => {
|
|
1642
|
+
const runtime = document.modelContextTesting;
|
|
1643
|
+
return runtime?.listTools?.().map((tool) => tool.name) ?? [];
|
|
1644
|
+
});
|
|
1645
|
+
if (!toolNames.includes(target.toolName)) {
|
|
1646
|
+
throw new Error(`Runtime did not register ${target.toolName}. Found: ${toolNames.join(', ')}`);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const rawResult = await page.evaluate(
|
|
1650
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
1651
|
+
const runtime = document.modelContextTesting;
|
|
1652
|
+
if (!runtime?.executeTool) {
|
|
1653
|
+
throw new Error('document.modelContextTesting.executeTool is unavailable.');
|
|
1654
|
+
}
|
|
1655
|
+
return runtime.executeTool(
|
|
1656
|
+
toolName,
|
|
1657
|
+
JSON.stringify({
|
|
1658
|
+
slug,
|
|
1659
|
+
sectionId,
|
|
1660
|
+
fieldKey,
|
|
1661
|
+
value,
|
|
1662
|
+
})
|
|
1663
|
+
);
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
toolName: target.toolName,
|
|
1667
|
+
slug: target.slug,
|
|
1668
|
+
sectionId: target.sectionId,
|
|
1669
|
+
fieldKey: target.fieldKey,
|
|
1670
|
+
value: nextValue,
|
|
1671
|
+
}
|
|
1672
|
+
);
|
|
1673
|
+
|
|
1674
|
+
const parsedResult = JSON.parse(rawResult);
|
|
1675
|
+
if (parsedResult?.isError) {
|
|
1676
|
+
throw new Error(`WebMCP tool returned an error: ${rawResult}`);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
mutationApplied = true;
|
|
1680
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
|
|
1681
|
+
await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
|
|
1682
|
+
|
|
1683
|
+
console.log(
|
|
1684
|
+
JSON.stringify({
|
|
1685
|
+
ok: true,
|
|
1686
|
+
slug: target.slug,
|
|
1687
|
+
manifestHref: target.manifestHref,
|
|
1688
|
+
contractHref: target.contractHref,
|
|
1689
|
+
toolName: target.toolName,
|
|
1690
|
+
sectionId: target.sectionId,
|
|
1691
|
+
fieldKey: target.fieldKey,
|
|
1692
|
+
toolNames,
|
|
1693
|
+
})
|
|
1694
|
+
);
|
|
1695
|
+
} finally {
|
|
1696
|
+
if (mutationApplied) {
|
|
1697
|
+
await restoreOriginal();
|
|
1698
|
+
}
|
|
1699
|
+
await browser.close();
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
main().catch((error) => {
|
|
1704
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
726
1708
|
END_OF_FILE_CONTENT
|
|
727
1709
|
mkdir -p "src"
|
|
728
1710
|
# SKIP: src/App.tsx is binary and cannot be embedded as text.
|
|
@@ -3421,6 +4403,74 @@ export function useCloudSave({ apiUrl, apiKey }: UseCloudSaveOptions) {
|
|
|
3421
4403
|
return { cloudSaveUi, runCloudSave, closeCloudDrawer, retryCloudSave };
|
|
3422
4404
|
}
|
|
3423
4405
|
|
|
4406
|
+
END_OF_FILE_CONTENT
|
|
4407
|
+
echo "Creating src/lib/buildVisitorWebPageJsonLd.test.ts..."
|
|
4408
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/buildVisitorWebPageJsonLd.test.ts"
|
|
4409
|
+
import { describe, expect, it } from 'vitest';
|
|
4410
|
+
import { buildVisitorWebPageJsonLd } from './buildVisitorWebPageJsonLd';
|
|
4411
|
+
|
|
4412
|
+
describe('buildVisitorWebPageJsonLd', () => {
|
|
4413
|
+
it('builds Schema.org WebPage for home at /', () => {
|
|
4414
|
+
expect(
|
|
4415
|
+
buildVisitorWebPageJsonLd({
|
|
4416
|
+
title: 'Home',
|
|
4417
|
+
description: 'Welcome',
|
|
4418
|
+
slug: 'home',
|
|
4419
|
+
}),
|
|
4420
|
+
).toEqual({
|
|
4421
|
+
'@context': 'https://schema.org',
|
|
4422
|
+
'@type': 'WebPage',
|
|
4423
|
+
name: 'Home',
|
|
4424
|
+
description: 'Welcome',
|
|
4425
|
+
url: '/',
|
|
4426
|
+
});
|
|
4427
|
+
});
|
|
4428
|
+
|
|
4429
|
+
it('builds Schema.org WebPage for nested slug paths', () => {
|
|
4430
|
+
expect(
|
|
4431
|
+
buildVisitorWebPageJsonLd({
|
|
4432
|
+
title: 'Dune',
|
|
4433
|
+
description: 'Book',
|
|
4434
|
+
slug: 'libri/dune',
|
|
4435
|
+
}),
|
|
4436
|
+
).toEqual({
|
|
4437
|
+
'@context': 'https://schema.org',
|
|
4438
|
+
'@type': 'WebPage',
|
|
4439
|
+
name: 'Dune',
|
|
4440
|
+
description: 'Book',
|
|
4441
|
+
url: '/libri/dune',
|
|
4442
|
+
});
|
|
4443
|
+
});
|
|
4444
|
+
});
|
|
4445
|
+
|
|
4446
|
+
END_OF_FILE_CONTENT
|
|
4447
|
+
echo "Creating src/lib/buildVisitorWebPageJsonLd.ts..."
|
|
4448
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/buildVisitorWebPageJsonLd.ts"
|
|
4449
|
+
/**
|
|
4450
|
+
* Schema.org WebPage JSON-LD for Next visitor RSC (parity with tenant-alpha bake HTML injection).
|
|
4451
|
+
*/
|
|
4452
|
+
export type VisitorWebPageJsonLd = {
|
|
4453
|
+
'@context': 'https://schema.org';
|
|
4454
|
+
'@type': 'WebPage';
|
|
4455
|
+
name: string;
|
|
4456
|
+
description: string;
|
|
4457
|
+
url: string;
|
|
4458
|
+
};
|
|
4459
|
+
|
|
4460
|
+
export function buildVisitorWebPageJsonLd(input: {
|
|
4461
|
+
title: string;
|
|
4462
|
+
description?: string;
|
|
4463
|
+
slug: string;
|
|
4464
|
+
}): VisitorWebPageJsonLd {
|
|
4465
|
+
return {
|
|
4466
|
+
'@context': 'https://schema.org',
|
|
4467
|
+
'@type': 'WebPage',
|
|
4468
|
+
name: input.title,
|
|
4469
|
+
description: input.description ?? '',
|
|
4470
|
+
url: input.slug === 'home' ? '/' : `/${input.slug}`,
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
|
|
3424
4474
|
END_OF_FILE_CONTENT
|
|
3425
4475
|
mkdir -p "src/lib/css"
|
|
3426
4476
|
echo "Creating src/lib/css/serializeThemeRootCss.test.ts..."
|
|
@@ -4465,38 +5515,26 @@ else
|
|
|
4465
5515
|
fi
|
|
4466
5516
|
|
|
4467
5517
|
# -----------------------------------------------------------------------------
|
|
4468
|
-
#
|
|
4469
|
-
#
|
|
5518
|
+
# WIPE tenant content — no DNA name denylist (orphans break the compiler).
|
|
5519
|
+
# Preserve: src/components/ui (shadcn), src/components/admin (studio).
|
|
5520
|
+
# Wipe includes overlap dirs (e.g. header) — generators rewrite them fresh.
|
|
4470
5521
|
# -----------------------------------------------------------------------------
|
|
4471
|
-
echo "--
|
|
5522
|
+
echo "-- Wiping tenant content surfaces (components/collections/pages/config)..."
|
|
5523
|
+
if [[ -d src/components ]]; then
|
|
5524
|
+
find src/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin' -exec rm -rf {} +
|
|
5525
|
+
fi
|
|
4472
5526
|
rm -rf \
|
|
4473
|
-
src/
|
|
4474
|
-
src/
|
|
4475
|
-
src/
|
|
4476
|
-
src/components/form-demo \
|
|
4477
|
-
src/components/empty-tenant \
|
|
4478
|
-
src/collections/autori \
|
|
4479
|
-
src/collections/libri \
|
|
4480
|
-
src/data/collections/autori \
|
|
4481
|
-
src/data/collections/libri \
|
|
4482
|
-
src/data/pages/authors \
|
|
4483
|
-
src/data/pages/libri \
|
|
4484
|
-
src/data/pages/work \
|
|
4485
|
-
src/data/pages/blog \
|
|
5527
|
+
src/collections \
|
|
5528
|
+
src/data/collections \
|
|
5529
|
+
src/data/pages \
|
|
4486
5530
|
public/pages \
|
|
4487
5531
|
public/collections
|
|
4488
|
-
rm -f
|
|
4489
|
-
|
|
4490
|
-
src/data/
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
src/data/pages/work.json \
|
|
4495
|
-
src/data/pages/blog.json \
|
|
4496
|
-
src/data/pages/contact.json \
|
|
4497
|
-
public/config/site.json
|
|
4498
|
-
|
|
4499
|
-
# Drop books-list special-case so deleted capsules cannot break the visitor RSC path.
|
|
5532
|
+
rm -f public/config/site.json
|
|
5533
|
+
if [[ -d src/data/config ]]; then
|
|
5534
|
+
find src/data/config -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
5535
|
+
fi
|
|
5536
|
+
|
|
5537
|
+
# Drop DNA special-cases so wiped capsules cannot break the visitor RSC path.
|
|
4500
5538
|
echo "-- Resetting VisitorSection to registry-only..."
|
|
4501
5539
|
mkdir -p src/lib
|
|
4502
5540
|
cat > src/lib/VisitorSection.tsx << 'EOF'
|
|
@@ -7157,31 +8195,26 @@ else
|
|
|
7157
8195
|
fi
|
|
7158
8196
|
|
|
7159
8197
|
# -----------------------------------------------------------------------------
|
|
7160
|
-
#
|
|
7161
|
-
#
|
|
8198
|
+
# WIPE tenant content — no DNA name denylist (orphans break the compiler).
|
|
8199
|
+
# Preserve: src/components/ui (shadcn), src/components/admin (studio).
|
|
8200
|
+
# Wipe includes overlap dirs (e.g. header) — generators rewrite them fresh.
|
|
7162
8201
|
# -----------------------------------------------------------------------------
|
|
7163
|
-
echo "--
|
|
8202
|
+
echo "-- Wiping tenant content surfaces (components/collections/pages/config)..."
|
|
8203
|
+
if [[ -d src/components ]]; then
|
|
8204
|
+
find src/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin' -exec rm -rf {} +
|
|
8205
|
+
fi
|
|
7164
8206
|
rm -rf \
|
|
7165
|
-
src/
|
|
7166
|
-
src/
|
|
7167
|
-
src/
|
|
7168
|
-
src/components/form-demo \
|
|
7169
|
-
src/collections/autori \
|
|
7170
|
-
src/collections/libri \
|
|
7171
|
-
src/data/collections/autori \
|
|
7172
|
-
src/data/collections/libri \
|
|
7173
|
-
src/data/pages/authors \
|
|
7174
|
-
src/data/pages/libri \
|
|
8207
|
+
src/collections \
|
|
8208
|
+
src/data/collections \
|
|
8209
|
+
src/data/pages \
|
|
7175
8210
|
public/pages \
|
|
7176
8211
|
public/collections
|
|
7177
|
-
rm -f
|
|
7178
|
-
|
|
7179
|
-
src/data/
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
# Drop books-list special-case so deleted capsules cannot break the visitor RSC path.
|
|
8212
|
+
rm -f public/config/site.json
|
|
8213
|
+
if [[ -d src/data/config ]]; then
|
|
8214
|
+
find src/data/config -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
8215
|
+
fi
|
|
8216
|
+
|
|
8217
|
+
# Drop DNA special-cases so wiped capsules cannot break the visitor RSC path.
|
|
7185
8218
|
echo "-- Resetting VisitorSection to registry-only..."
|
|
7186
8219
|
mkdir -p src/lib
|
|
7187
8220
|
cat > src/lib/VisitorSection.tsx << 'EOF'
|
|
@@ -7217,6 +8250,31 @@ export function VisitorSection({
|
|
|
7217
8250
|
}
|
|
7218
8251
|
EOF
|
|
7219
8252
|
|
|
8253
|
+
# DNA catch-all imports EmptyTenantView — drop it after wipe of that capsule.
|
|
8254
|
+
if [[ -f 'app/[[...slug]]/page.tsx' ]]; then
|
|
8255
|
+
echo "-- Patching app/[[...slug]]/page.tsx empty fallback..."
|
|
8256
|
+
python3 - <<'PY'
|
|
8257
|
+
from pathlib import Path
|
|
8258
|
+
p = Path("app/[[...slug]]/page.tsx")
|
|
8259
|
+
src = p.read_text()
|
|
8260
|
+
capsule = "empty" + "-tenant"
|
|
8261
|
+
src = src.replace(f"import {{ EmptyTenantView }} from '@/components/{capsule}';\n", "")
|
|
8262
|
+
old = " if (result.kind === 'empty') {\n return <EmptyTenantView />;\n }"
|
|
8263
|
+
new = """ if (result.kind === 'empty') {
|
|
8264
|
+
return (
|
|
8265
|
+
<main className="mx-auto max-w-3xl px-8 py-24">
|
|
8266
|
+
<h1 className="text-2xl font-bold">Your tenant is empty.</h1>
|
|
8267
|
+
<p className="mt-2 text-muted-foreground">Create your first page.</p>
|
|
8268
|
+
</main>
|
|
8269
|
+
);
|
|
8270
|
+
}"""
|
|
8271
|
+
if old not in src:
|
|
8272
|
+
raise SystemExit("EmptyTenantView empty-branch not found in page.tsx")
|
|
8273
|
+
p.write_text(src.replace(old, new))
|
|
8274
|
+
print(" page.tsx empty fallback inlined")
|
|
8275
|
+
PY
|
|
8276
|
+
fi
|
|
8277
|
+
|
|
7220
8278
|
# -----------------------------------------------------------------------------
|
|
7221
8279
|
# DIRECTORIES
|
|
7222
8280
|
# -----------------------------------------------------------------------------
|