@olonjs/cli 3.0.103 → 3.0.105
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/assets/src_tenant_alpha.sh +510 -511
- package/assets/templates/agritourism/src_tenant.sh +301 -303
- package/assets/templates/alpha/src_tenant.sh +510 -511
- package/package.json +1 -1
|
@@ -1646,7 +1646,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1646
1646
|
"@tiptap/extension-link": "^2.11.5",
|
|
1647
1647
|
"@tiptap/react": "^2.11.5",
|
|
1648
1648
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1649
|
-
"@olonjs/core": "^1.0.
|
|
1649
|
+
"@olonjs/core": "^1.0.92",
|
|
1650
1650
|
"class-variance-authority": "^0.7.1",
|
|
1651
1651
|
"clsx": "^2.1.1",
|
|
1652
1652
|
"lucide-react": "^0.474.0",
|
|
@@ -1947,528 +1947,528 @@ main().catch((error) => {
|
|
|
1947
1947
|
END_OF_FILE_CONTENT
|
|
1948
1948
|
echo "Creating scripts/bake.mjs..."
|
|
1949
1949
|
cat << 'END_OF_FILE_CONTENT' > "scripts/bake.mjs"
|
|
1950
|
-
/**
|
|
1951
|
-
* olon bake - production SSG
|
|
1952
|
-
*
|
|
1953
|
-
* 1) Build client bundle (dist/)
|
|
1954
|
-
* 2) Build SSR entry bundle (dist-ssr/)
|
|
1955
|
-
* 3) Discover all page slugs from JSON files under src/data/pages
|
|
1956
|
-
* 4) Render each slug via SSR and write dist/<slug>/index.html
|
|
1957
|
-
*/
|
|
1958
|
-
|
|
1959
|
-
import { build } from 'vite';
|
|
1960
|
-
import path from 'path';
|
|
1961
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
|
1950
|
+
/**
|
|
1951
|
+
* olon bake - production SSG
|
|
1952
|
+
*
|
|
1953
|
+
* 1) Build client bundle (dist/)
|
|
1954
|
+
* 2) Build SSR entry bundle (dist-ssr/)
|
|
1955
|
+
* 3) Discover all page slugs from JSON files under src/data/pages
|
|
1956
|
+
* 4) Render each slug via SSR and write dist/<slug>/index.html
|
|
1957
|
+
*/
|
|
1958
|
+
|
|
1959
|
+
import { build } from 'vite';
|
|
1960
|
+
import path from 'path';
|
|
1961
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
1962
|
+
import fs from 'fs/promises';
|
|
1963
|
+
import {
|
|
1964
|
+
buildPageContract,
|
|
1965
|
+
buildPageManifest,
|
|
1966
|
+
buildPageManifestHref,
|
|
1967
|
+
buildSiteManifest,
|
|
1968
|
+
} from '../../../packages/core/src/lib/webmcp-contracts.mjs';
|
|
1969
|
+
|
|
1970
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1971
|
+
const root = path.resolve(__dirname, '..');
|
|
1972
|
+
const pagesDir = path.resolve(root, 'src/data/pages');
|
|
1973
|
+
const publicDir = path.resolve(root, 'public');
|
|
1974
|
+
const distDir = path.resolve(root, 'dist');
|
|
1975
|
+
|
|
1976
|
+
async function writeJsonTargets(relativePath, value) {
|
|
1977
|
+
const targets = [
|
|
1978
|
+
path.resolve(publicDir, relativePath),
|
|
1979
|
+
path.resolve(distDir, relativePath),
|
|
1980
|
+
];
|
|
1981
|
+
|
|
1982
|
+
for (const targetPath of targets) {
|
|
1983
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1984
|
+
await fs.writeFile(targetPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function escapeHtmlAttribute(value) {
|
|
1989
|
+
return String(value).replace(/&/g, '&').replace(/"/g, '"');
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function toCanonicalSlug(relativeJsonPath) {
|
|
1993
|
+
const normalized = relativeJsonPath.replace(/\\/g, '/');
|
|
1994
|
+
const slug = normalized.replace(/\.json$/i, '').replace(/^\/+|\/+$/g, '');
|
|
1995
|
+
if (!slug) throw new Error('[bake] Invalid page slug: empty path segment');
|
|
1996
|
+
return slug;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async function listJsonFilesRecursive(dir) {
|
|
2000
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
2001
|
+
const files = [];
|
|
2002
|
+
for (const item of items) {
|
|
2003
|
+
const fullPath = path.join(dir, item.name);
|
|
2004
|
+
if (item.isDirectory()) {
|
|
2005
|
+
files.push(...(await listJsonFilesRecursive(fullPath)));
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
2009
|
+
}
|
|
2010
|
+
return files;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
async function discoverTargets() {
|
|
2014
|
+
let files = [];
|
|
2015
|
+
try {
|
|
2016
|
+
files = await listJsonFilesRecursive(pagesDir);
|
|
2017
|
+
} catch {
|
|
2018
|
+
files = [];
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const rawSlugs = files.map((fullPath) => toCanonicalSlug(path.relative(pagesDir, fullPath)));
|
|
2022
|
+
const slugs = Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
|
|
2023
|
+
|
|
2024
|
+
return slugs.map((slug) => {
|
|
2025
|
+
const depth = slug === 'home' ? 0 : slug.split('/').length;
|
|
2026
|
+
const out = slug === 'home' ? 'dist/index.html' : `dist/${slug}/index.html`;
|
|
2027
|
+
return { slug, out, depth };
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
console.log('\n[bake] Building client...');
|
|
2032
|
+
await build({ root, mode: 'production', logLevel: 'warn' });
|
|
2033
|
+
console.log('[bake] Client build done.');
|
|
2034
|
+
|
|
2035
|
+
console.log('\n[bake] Building SSR bundle...');
|
|
2036
|
+
await build({
|
|
2037
|
+
root,
|
|
2038
|
+
mode: 'production',
|
|
2039
|
+
logLevel: 'warn',
|
|
2040
|
+
build: {
|
|
2041
|
+
ssr: 'src/entry-ssg.tsx',
|
|
2042
|
+
outDir: 'dist-ssr',
|
|
2043
|
+
rollupOptions: {
|
|
2044
|
+
output: { format: 'esm' },
|
|
2045
|
+
},
|
|
2046
|
+
},
|
|
2047
|
+
ssr: {
|
|
2048
|
+
noExternal: ['@olonjs/core'],
|
|
2049
|
+
},
|
|
2050
|
+
});
|
|
2051
|
+
console.log('[bake] SSR build done.');
|
|
2052
|
+
|
|
2053
|
+
const targets = await discoverTargets();
|
|
2054
|
+
if (targets.length === 0) {
|
|
2055
|
+
throw new Error('[bake] No pages discovered under src/data/pages');
|
|
2056
|
+
}
|
|
2057
|
+
console.log(`[bake] Targets: ${targets.map((t) => t.slug).join(', ')}`);
|
|
2058
|
+
|
|
2059
|
+
const ssrEntryUrl = pathToFileURL(path.resolve(root, 'dist-ssr/entry-ssg.js')).href;
|
|
2060
|
+
const { render, getCss, getPageMeta, getWebMcpBuildState } = await import(ssrEntryUrl);
|
|
2061
|
+
|
|
2062
|
+
const template = await fs.readFile(path.resolve(root, 'dist/index.html'), 'utf-8');
|
|
2063
|
+
const hasCommentMarker = template.includes('<!--app-html-->');
|
|
2064
|
+
const hasRootDivMarker = template.includes('<div id="root"></div>');
|
|
2065
|
+
if (!hasCommentMarker && !hasRootDivMarker) {
|
|
2066
|
+
throw new Error('[bake] Missing template marker. Expected <!--app-html--> or <div id="root"></div>.');
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const inlinedCss = getCss();
|
|
2070
|
+
const styleTag = `<style data-bake="inline">${inlinedCss}</style>`;
|
|
2071
|
+
const webMcpBuildState = getWebMcpBuildState();
|
|
2072
|
+
|
|
2073
|
+
for (const { slug } of targets) {
|
|
2074
|
+
const pageConfig = webMcpBuildState.pages[slug];
|
|
2075
|
+
if (!pageConfig) continue;
|
|
2076
|
+
const contract = buildPageContract({
|
|
2077
|
+
slug,
|
|
2078
|
+
pageConfig,
|
|
2079
|
+
schemas: webMcpBuildState.schemas,
|
|
2080
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2081
|
+
});
|
|
2082
|
+
await writeJsonTargets(`schemas/${slug}.schema.json`, contract);
|
|
2083
|
+
const pageManifest = buildPageManifest({
|
|
2084
|
+
slug,
|
|
2085
|
+
pageConfig,
|
|
2086
|
+
schemas: webMcpBuildState.schemas,
|
|
2087
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2088
|
+
});
|
|
2089
|
+
await writeJsonTargets(buildPageManifestHref(slug).replace(/^\//, ''), pageManifest);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const mcpManifest = buildSiteManifest({
|
|
2093
|
+
pages: webMcpBuildState.pages,
|
|
2094
|
+
schemas: webMcpBuildState.schemas,
|
|
2095
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2096
|
+
});
|
|
2097
|
+
await writeJsonTargets('mcp-manifest.json', mcpManifest);
|
|
2098
|
+
|
|
2099
|
+
for (const { slug, out, depth } of targets) {
|
|
2100
|
+
console.log(`\n[bake] Rendering /${slug === 'home' ? '' : slug}...`);
|
|
2101
|
+
|
|
2102
|
+
const appHtml = render(slug);
|
|
2103
|
+
const { title, description } = getPageMeta(slug);
|
|
2104
|
+
const safeTitle = String(title).replace(/"/g, '"');
|
|
2105
|
+
const safeDescription = String(description).replace(/"/g, '"');
|
|
2106
|
+
const metaTags = [
|
|
2107
|
+
`<meta name="description" content="${safeDescription}">`,
|
|
2108
|
+
`<meta property="og:title" content="${safeTitle}">`,
|
|
2109
|
+
`<meta property="og:description" content="${safeDescription}">`,
|
|
2110
|
+
].join('\n ');
|
|
2111
|
+
const jsonLd = JSON.stringify({
|
|
2112
|
+
'@context': 'https://schema.org',
|
|
2113
|
+
'@type': 'WebPage',
|
|
2114
|
+
name: title,
|
|
2115
|
+
description,
|
|
2116
|
+
url: slug === 'home' ? '/' : `/${slug}`,
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
const prefix = depth > 0 ? '../'.repeat(depth) : './';
|
|
2120
|
+
const fixedTemplate = depth > 0 ? template.replace(/(['"])\.\//g, `$1${prefix}`) : template;
|
|
2121
|
+
const mcpManifestHref = `${prefix}${buildPageManifestHref(slug).replace(/^\//, '')}`;
|
|
2122
|
+
const contractHref = `${prefix}schemas/${slug}.schema.json`;
|
|
2123
|
+
const contractLinks = [
|
|
2124
|
+
`<link rel="mcp-manifest" href="${escapeHtmlAttribute(mcpManifestHref)}">`,
|
|
2125
|
+
`<link rel="olon-contract" href="${escapeHtmlAttribute(contractHref)}">`,
|
|
2126
|
+
`<script type="application/ld+json">${jsonLd}</script>`,
|
|
2127
|
+
].join('\n ');
|
|
2128
|
+
|
|
2129
|
+
let bakedHtml = fixedTemplate
|
|
2130
|
+
.replace('</head>', ` ${styleTag}\n ${contractLinks}\n</head>`)
|
|
2131
|
+
.replace(/<title>.*?<\/title>/, `<title>${safeTitle}</title>\n ${metaTags}`);
|
|
2132
|
+
|
|
2133
|
+
if (hasCommentMarker) {
|
|
2134
|
+
bakedHtml = bakedHtml.replace('<!--app-html-->', appHtml);
|
|
2135
|
+
} else {
|
|
2136
|
+
bakedHtml = bakedHtml.replace('<div id="root"></div>', `<div id="root">${appHtml}</div>`);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
const outPath = path.resolve(root, out);
|
|
2140
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
2141
|
+
await fs.writeFile(outPath, bakedHtml, 'utf-8');
|
|
2142
|
+
console.log(`[bake] Written -> ${out} [title: "${safeTitle}"]`);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
console.log('\n[bake] All pages baked. OK\n');
|
|
2146
|
+
|
|
2147
|
+
END_OF_FILE_CONTENT
|
|
2148
|
+
echo "Creating scripts/sync-pages-to-public.mjs..."
|
|
2149
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
|
|
2150
|
+
import fs from 'fs';
|
|
2151
|
+
import path from 'path';
|
|
2152
|
+
import { fileURLToPath } from 'url';
|
|
2153
|
+
|
|
2154
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2155
|
+
const __dirname = path.dirname(__filename);
|
|
2156
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2157
|
+
const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2158
|
+
const targetDir = path.join(rootDir, 'public', 'pages');
|
|
2159
|
+
|
|
2160
|
+
if (!fs.existsSync(sourceDir)) {
|
|
2161
|
+
console.warn('[sync-pages-to-public] Source directory not found:', sourceDir);
|
|
2162
|
+
process.exit(0);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
2166
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
2167
|
+
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
2168
|
+
|
|
2169
|
+
console.log('[sync-pages-to-public] Synced pages to public/pages');
|
|
2170
|
+
|
|
2171
|
+
END_OF_FILE_CONTENT
|
|
2172
|
+
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
2173
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
1962
2174
|
import fs from 'fs/promises';
|
|
1963
|
-
import
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
buildPageManifestHref,
|
|
1967
|
-
buildSiteManifest,
|
|
1968
|
-
} from '../../../packages/core/src/lib/webmcp-contracts.mjs';
|
|
2175
|
+
import path from 'path';
|
|
2176
|
+
import { fileURLToPath } from 'url';
|
|
2177
|
+
import { createRequire } from 'module';
|
|
1969
2178
|
|
|
1970
2179
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1971
|
-
const
|
|
1972
|
-
const
|
|
1973
|
-
const publicDir = path.resolve(root, 'public');
|
|
1974
|
-
const distDir = path.resolve(root, 'dist');
|
|
1975
|
-
|
|
1976
|
-
async function writeJsonTargets(relativePath, value) {
|
|
1977
|
-
const targets = [
|
|
1978
|
-
path.resolve(publicDir, relativePath),
|
|
1979
|
-
path.resolve(distDir, relativePath),
|
|
1980
|
-
];
|
|
2180
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
2181
|
+
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:4173';
|
|
1981
2182
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
await fs.writeFile(targetPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
1985
|
-
}
|
|
2183
|
+
function pageFilePathFromSlug(slug) {
|
|
2184
|
+
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
1986
2185
|
}
|
|
1987
2186
|
|
|
1988
|
-
function
|
|
1989
|
-
return
|
|
2187
|
+
function adminUrlFromSlug(slug) {
|
|
2188
|
+
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
1990
2189
|
}
|
|
1991
2190
|
|
|
1992
|
-
function
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
if (
|
|
1996
|
-
|
|
2191
|
+
function isStringSchema(schema) {
|
|
2192
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
2193
|
+
if (schema.type === 'string') return true;
|
|
2194
|
+
if (Array.isArray(schema.anyOf)) {
|
|
2195
|
+
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
2196
|
+
}
|
|
2197
|
+
return false;
|
|
1997
2198
|
}
|
|
1998
2199
|
|
|
1999
|
-
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
if (
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
2200
|
+
function findTopLevelStringField(sectionSchema) {
|
|
2201
|
+
const properties = sectionSchema?.properties;
|
|
2202
|
+
if (!properties || typeof properties !== 'object') return null;
|
|
2203
|
+
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
2204
|
+
for (const key of preferred) {
|
|
2205
|
+
if (isStringSchema(properties[key])) return key;
|
|
2206
|
+
}
|
|
2207
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
2208
|
+
if (isStringSchema(value)) return key;
|
|
2009
2209
|
}
|
|
2010
|
-
return
|
|
2210
|
+
return null;
|
|
2011
2211
|
}
|
|
2012
2212
|
|
|
2013
|
-
async function
|
|
2014
|
-
|
|
2213
|
+
async function loadPlaywright() {
|
|
2214
|
+
const require = createRequire(import.meta.url);
|
|
2015
2215
|
try {
|
|
2016
|
-
|
|
2017
|
-
} catch {
|
|
2018
|
-
|
|
2216
|
+
return require('playwright');
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2219
|
+
throw new Error(
|
|
2220
|
+
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
2221
|
+
);
|
|
2019
2222
|
}
|
|
2223
|
+
}
|
|
2020
2224
|
|
|
2021
|
-
|
|
2022
|
-
const
|
|
2225
|
+
async function readPageJson(slug) {
|
|
2226
|
+
const pageFilePath = pageFilePathFromSlug(slug);
|
|
2227
|
+
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
2228
|
+
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
2229
|
+
}
|
|
2023
2230
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2231
|
+
async function waitFor(predicate, timeoutMs, label) {
|
|
2232
|
+
const startedAt = Date.now();
|
|
2233
|
+
for (;;) {
|
|
2234
|
+
const result = await predicate();
|
|
2235
|
+
if (result) return result;
|
|
2236
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
2237
|
+
throw new Error(`Timed out while waiting for ${label}.`);
|
|
2238
|
+
}
|
|
2239
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2240
|
+
}
|
|
2029
2241
|
}
|
|
2030
2242
|
|
|
2031
|
-
|
|
2032
|
-
await
|
|
2033
|
-
|
|
2243
|
+
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
2244
|
+
await waitFor(async () => {
|
|
2245
|
+
const { json } = await readPageJson(slug);
|
|
2246
|
+
const section = Array.isArray(json.sections)
|
|
2247
|
+
? json.sections.find((item) => item?.id === sectionId)
|
|
2248
|
+
: null;
|
|
2249
|
+
return section?.data?.[fieldKey] === expectedValue;
|
|
2250
|
+
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
2251
|
+
}
|
|
2034
2252
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
outDir: 'dist-ssr',
|
|
2043
|
-
rollupOptions: {
|
|
2044
|
-
output: { format: 'esm' },
|
|
2045
|
-
},
|
|
2046
|
-
},
|
|
2047
|
-
ssr: {
|
|
2048
|
-
noExternal: ['@olonjs/core'],
|
|
2049
|
-
},
|
|
2050
|
-
});
|
|
2051
|
-
console.log('[bake] SSR build done.');
|
|
2253
|
+
async function ensureResponseOk(response, label) {
|
|
2254
|
+
if (!response.ok) {
|
|
2255
|
+
const text = await response.text();
|
|
2256
|
+
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
2257
|
+
}
|
|
2258
|
+
return response;
|
|
2259
|
+
}
|
|
2052
2260
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2261
|
+
async function fetchJson(relativePath, label) {
|
|
2262
|
+
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
2263
|
+
return response.json();
|
|
2056
2264
|
}
|
|
2057
|
-
console.log(`[bake] Targets: ${targets.map((t) => t.slug).join(', ')}`);
|
|
2058
2265
|
|
|
2059
|
-
|
|
2060
|
-
const
|
|
2266
|
+
async function selectTarget() {
|
|
2267
|
+
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
2268
|
+
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
2269
|
+
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
2270
|
+
: null;
|
|
2271
|
+
|
|
2272
|
+
const candidatePages = requestedSlug
|
|
2273
|
+
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
2274
|
+
: (siteIndex.pages ?? []);
|
|
2061
2275
|
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2276
|
+
for (const pageEntry of candidatePages) {
|
|
2277
|
+
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
2278
|
+
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
2279
|
+
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
2280
|
+
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
2281
|
+
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
2282
|
+
: [];
|
|
2283
|
+
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
2284
|
+
|
|
2285
|
+
for (const tool of tools) {
|
|
2286
|
+
const sectionType = tool?.sectionType;
|
|
2287
|
+
if (typeof tool?.name !== 'string' || typeof sectionType !== 'string') continue;
|
|
2288
|
+
const targetInstance = localInstances.find((section) => section?.type === sectionType);
|
|
2289
|
+
if (!targetInstance?.id) continue;
|
|
2290
|
+
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
2291
|
+
if (!targetFieldKey) continue;
|
|
2292
|
+
const pageState = await readPageJson(pageEntry.slug);
|
|
2293
|
+
const section = Array.isArray(pageState.json.sections)
|
|
2294
|
+
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
2295
|
+
: null;
|
|
2296
|
+
const originalValue = section?.data?.[targetFieldKey];
|
|
2297
|
+
if (typeof originalValue !== 'string') continue;
|
|
2298
|
+
|
|
2299
|
+
return {
|
|
2300
|
+
slug: pageEntry.slug,
|
|
2301
|
+
manifestHref: pageEntry.manifestHref,
|
|
2302
|
+
contractHref: pageEntry.contractHref,
|
|
2303
|
+
toolName: tool.name,
|
|
2304
|
+
sectionId: targetInstance.id,
|
|
2305
|
+
fieldKey: targetFieldKey,
|
|
2306
|
+
originalValue,
|
|
2307
|
+
originalState: pageState,
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
requestedSlug
|
|
2314
|
+
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
2315
|
+
: 'No valid WebMCP verification target found in manifest index.'
|
|
2316
|
+
);
|
|
2067
2317
|
}
|
|
2068
2318
|
|
|
2069
|
-
|
|
2070
|
-
const
|
|
2071
|
-
const
|
|
2319
|
+
async function main() {
|
|
2320
|
+
const { chromium } = await loadPlaywright();
|
|
2321
|
+
const target = await selectTarget();
|
|
2322
|
+
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
2323
|
+
const browser = await chromium.launch({ headless: true });
|
|
2324
|
+
const page = await browser.newPage();
|
|
2325
|
+
const consoleEvents = [];
|
|
2326
|
+
let mutationApplied = false;
|
|
2072
2327
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
slug,
|
|
2078
|
-
pageConfig,
|
|
2079
|
-
schemas: webMcpBuildState.schemas,
|
|
2080
|
-
siteConfig: webMcpBuildState.siteConfig,
|
|
2328
|
+
page.on('console', (message) => {
|
|
2329
|
+
if (message.type() === 'error' || message.type() === 'warning') {
|
|
2330
|
+
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
2331
|
+
}
|
|
2081
2332
|
});
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
slug,
|
|
2085
|
-
pageConfig,
|
|
2086
|
-
schemas: webMcpBuildState.schemas,
|
|
2087
|
-
siteConfig: webMcpBuildState.siteConfig,
|
|
2333
|
+
page.on('pageerror', (error) => {
|
|
2334
|
+
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
2088
2335
|
});
|
|
2089
|
-
await writeJsonTargets(buildPageManifestHref(slug).replace(/^\//, ''), pageManifest);
|
|
2090
|
-
}
|
|
2091
2336
|
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2337
|
+
const restoreOriginal = async () => {
|
|
2338
|
+
try {
|
|
2339
|
+
await page.evaluate(
|
|
2340
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
2341
|
+
const runtime = navigator.modelContextTesting;
|
|
2342
|
+
if (!runtime?.executeTool) return;
|
|
2343
|
+
await runtime.executeTool(
|
|
2344
|
+
toolName,
|
|
2345
|
+
JSON.stringify({
|
|
2346
|
+
slug,
|
|
2347
|
+
sectionId,
|
|
2348
|
+
fieldKey,
|
|
2349
|
+
value,
|
|
2350
|
+
})
|
|
2351
|
+
);
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
toolName: target.toolName,
|
|
2355
|
+
slug: target.slug,
|
|
2356
|
+
sectionId: target.sectionId,
|
|
2357
|
+
fieldKey: target.fieldKey,
|
|
2358
|
+
value: target.originalValue,
|
|
2359
|
+
}
|
|
2360
|
+
);
|
|
2361
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
|
|
2362
|
+
} catch {
|
|
2363
|
+
await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2098
2366
|
|
|
2099
|
-
|
|
2100
|
-
|
|
2367
|
+
try {
|
|
2368
|
+
const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
|
|
2369
|
+
if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
|
|
2370
|
+
throw new Error(`Manifest does not expose ${target.toolName}.`);
|
|
2371
|
+
}
|
|
2101
2372
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
const metaTags = [
|
|
2107
|
-
`<meta name="description" content="${safeDescription}">`,
|
|
2108
|
-
`<meta property="og:title" content="${safeTitle}">`,
|
|
2109
|
-
`<meta property="og:description" content="${safeDescription}">`,
|
|
2110
|
-
].join('\n ');
|
|
2111
|
-
const jsonLd = JSON.stringify({
|
|
2112
|
-
'@context': 'https://schema.org',
|
|
2113
|
-
'@type': 'WebPage',
|
|
2114
|
-
name: title,
|
|
2115
|
-
description,
|
|
2116
|
-
url: slug === 'home' ? '/' : `/${slug}`,
|
|
2117
|
-
});
|
|
2373
|
+
const pageContract = await fetchJson(target.contractHref, `Contract request for ${target.slug}`);
|
|
2374
|
+
if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
|
|
2375
|
+
throw new Error(`Page contract does not expose ${target.toolName}.`);
|
|
2376
|
+
}
|
|
2118
2377
|
|
|
2119
|
-
|
|
2120
|
-
const fixedTemplate = depth > 0 ? template.replace(/(['"])\.\//g, `$1${prefix}`) : template;
|
|
2121
|
-
const mcpManifestHref = `${prefix}${buildPageManifestHref(slug).replace(/^\//, '')}`;
|
|
2122
|
-
const contractHref = `${prefix}schemas/${slug}.schema.json`;
|
|
2123
|
-
const contractLinks = [
|
|
2124
|
-
`<link rel="mcp-manifest" href="${escapeHtmlAttribute(mcpManifestHref)}">`,
|
|
2125
|
-
`<link rel="olon-contract" href="${escapeHtmlAttribute(contractHref)}">`,
|
|
2126
|
-
`<script type="application/ld+json">${jsonLd}</script>`,
|
|
2127
|
-
].join('\n ');
|
|
2378
|
+
await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
|
|
2128
2379
|
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2380
|
+
try {
|
|
2381
|
+
await page.waitForFunction(
|
|
2382
|
+
({ manifestHref, contractHref }) => {
|
|
2383
|
+
const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
|
|
2384
|
+
const contractLink = document.head.querySelector('link[rel="olon-contract"]');
|
|
2385
|
+
return manifestLink?.getAttribute('href') === manifestHref
|
|
2386
|
+
&& contractLink?.getAttribute('href') === contractHref;
|
|
2387
|
+
},
|
|
2388
|
+
{ manifestHref: target.manifestHref, contractHref: target.contractHref },
|
|
2389
|
+
{ timeout: 10_000 }
|
|
2390
|
+
);
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
const diagnostics = await page.evaluate(() => ({
|
|
2393
|
+
head: document.head.innerHTML,
|
|
2394
|
+
bodyText: document.body.innerText,
|
|
2395
|
+
}));
|
|
2396
|
+
throw new Error(
|
|
2397
|
+
[
|
|
2398
|
+
error instanceof Error ? error.message : String(error),
|
|
2399
|
+
`head=${diagnostics.head}`,
|
|
2400
|
+
`body=${diagnostics.bodyText}`,
|
|
2401
|
+
...consoleEvents,
|
|
2402
|
+
].join('\n')
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2132
2405
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2406
|
+
const toolNames = await page.evaluate(() => {
|
|
2407
|
+
const runtime = navigator.modelContextTesting;
|
|
2408
|
+
return runtime?.listTools?.().map((tool) => tool.name) ?? [];
|
|
2409
|
+
});
|
|
2410
|
+
if (!toolNames.includes(target.toolName)) {
|
|
2411
|
+
throw new Error(`Runtime did not register ${target.toolName}. Found: ${toolNames.join(', ')}`);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
const rawResult = await page.evaluate(
|
|
2415
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
2416
|
+
const runtime = navigator.modelContextTesting;
|
|
2417
|
+
if (!runtime?.executeTool) {
|
|
2418
|
+
throw new Error('navigator.modelContextTesting.executeTool is unavailable.');
|
|
2419
|
+
}
|
|
2420
|
+
return runtime.executeTool(
|
|
2421
|
+
toolName,
|
|
2422
|
+
JSON.stringify({
|
|
2423
|
+
slug,
|
|
2424
|
+
sectionId,
|
|
2425
|
+
fieldKey,
|
|
2426
|
+
value,
|
|
2427
|
+
})
|
|
2428
|
+
);
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
toolName: target.toolName,
|
|
2432
|
+
slug: target.slug,
|
|
2433
|
+
sectionId: target.sectionId,
|
|
2434
|
+
fieldKey: target.fieldKey,
|
|
2435
|
+
value: nextValue,
|
|
2436
|
+
}
|
|
2437
|
+
);
|
|
2138
2438
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2439
|
+
const parsedResult = JSON.parse(rawResult);
|
|
2440
|
+
if (parsedResult?.isError) {
|
|
2441
|
+
throw new Error(`WebMCP tool returned an error: ${rawResult}`);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
mutationApplied = true;
|
|
2445
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
|
|
2446
|
+
await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
|
|
2447
|
+
|
|
2448
|
+
console.log(
|
|
2449
|
+
JSON.stringify({
|
|
2450
|
+
ok: true,
|
|
2451
|
+
slug: target.slug,
|
|
2452
|
+
manifestHref: target.manifestHref,
|
|
2453
|
+
contractHref: target.contractHref,
|
|
2454
|
+
toolName: target.toolName,
|
|
2455
|
+
sectionId: target.sectionId,
|
|
2456
|
+
fieldKey: target.fieldKey,
|
|
2457
|
+
toolNames,
|
|
2458
|
+
})
|
|
2459
|
+
);
|
|
2460
|
+
} finally {
|
|
2461
|
+
if (mutationApplied) {
|
|
2462
|
+
await restoreOriginal();
|
|
2463
|
+
}
|
|
2464
|
+
await browser.close();
|
|
2465
|
+
}
|
|
2143
2466
|
}
|
|
2144
2467
|
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
|
|
2150
|
-
import fs from 'fs';
|
|
2151
|
-
import path from 'path';
|
|
2152
|
-
import { fileURLToPath } from 'url';
|
|
2153
|
-
|
|
2154
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
2155
|
-
const __dirname = path.dirname(__filename);
|
|
2156
|
-
const rootDir = path.resolve(__dirname, '..');
|
|
2157
|
-
const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2158
|
-
const targetDir = path.join(rootDir, 'public', 'pages');
|
|
2159
|
-
|
|
2160
|
-
if (!fs.existsSync(sourceDir)) {
|
|
2161
|
-
console.warn('[sync-pages-to-public] Source directory not found:', sourceDir);
|
|
2162
|
-
process.exit(0);
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
2166
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
2167
|
-
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
2168
|
-
|
|
2169
|
-
console.log('[sync-pages-to-public] Synced pages to public/pages');
|
|
2170
|
-
|
|
2171
|
-
END_OF_FILE_CONTENT
|
|
2172
|
-
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
2173
|
-
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
2174
|
-
import fs from 'fs/promises';
|
|
2175
|
-
import path from 'path';
|
|
2176
|
-
import { fileURLToPath } from 'url';
|
|
2177
|
-
import { createRequire } from 'module';
|
|
2178
|
-
|
|
2179
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2180
|
-
const rootDir = path.resolve(__dirname, '..');
|
|
2181
|
-
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:4173';
|
|
2182
|
-
|
|
2183
|
-
function pageFilePathFromSlug(slug) {
|
|
2184
|
-
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
function adminUrlFromSlug(slug) {
|
|
2188
|
-
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
function isStringSchema(schema) {
|
|
2192
|
-
if (!schema || typeof schema !== 'object') return false;
|
|
2193
|
-
if (schema.type === 'string') return true;
|
|
2194
|
-
if (Array.isArray(schema.anyOf)) {
|
|
2195
|
-
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
2196
|
-
}
|
|
2197
|
-
return false;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
function findTopLevelStringField(sectionSchema) {
|
|
2201
|
-
const properties = sectionSchema?.properties;
|
|
2202
|
-
if (!properties || typeof properties !== 'object') return null;
|
|
2203
|
-
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
2204
|
-
for (const key of preferred) {
|
|
2205
|
-
if (isStringSchema(properties[key])) return key;
|
|
2206
|
-
}
|
|
2207
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
2208
|
-
if (isStringSchema(value)) return key;
|
|
2209
|
-
}
|
|
2210
|
-
return null;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
async function loadPlaywright() {
|
|
2214
|
-
const require = createRequire(import.meta.url);
|
|
2215
|
-
try {
|
|
2216
|
-
return require('playwright');
|
|
2217
|
-
} catch (error) {
|
|
2218
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2219
|
-
throw new Error(
|
|
2220
|
-
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
2221
|
-
);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
async function readPageJson(slug) {
|
|
2226
|
-
const pageFilePath = pageFilePathFromSlug(slug);
|
|
2227
|
-
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
2228
|
-
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
async function waitFor(predicate, timeoutMs, label) {
|
|
2232
|
-
const startedAt = Date.now();
|
|
2233
|
-
for (;;) {
|
|
2234
|
-
const result = await predicate();
|
|
2235
|
-
if (result) return result;
|
|
2236
|
-
if (Date.now() - startedAt > timeoutMs) {
|
|
2237
|
-
throw new Error(`Timed out while waiting for ${label}.`);
|
|
2238
|
-
}
|
|
2239
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
2244
|
-
await waitFor(async () => {
|
|
2245
|
-
const { json } = await readPageJson(slug);
|
|
2246
|
-
const section = Array.isArray(json.sections)
|
|
2247
|
-
? json.sections.find((item) => item?.id === sectionId)
|
|
2248
|
-
: null;
|
|
2249
|
-
return section?.data?.[fieldKey] === expectedValue;
|
|
2250
|
-
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
async function ensureResponseOk(response, label) {
|
|
2254
|
-
if (!response.ok) {
|
|
2255
|
-
const text = await response.text();
|
|
2256
|
-
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
2257
|
-
}
|
|
2258
|
-
return response;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
async function fetchJson(relativePath, label) {
|
|
2262
|
-
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
2263
|
-
return response.json();
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
async function selectTarget() {
|
|
2267
|
-
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
2268
|
-
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
2269
|
-
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
2270
|
-
: null;
|
|
2271
|
-
|
|
2272
|
-
const candidatePages = requestedSlug
|
|
2273
|
-
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
2274
|
-
: (siteIndex.pages ?? []);
|
|
2275
|
-
|
|
2276
|
-
for (const pageEntry of candidatePages) {
|
|
2277
|
-
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
2278
|
-
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
2279
|
-
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
2280
|
-
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
2281
|
-
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
2282
|
-
: [];
|
|
2283
|
-
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
2284
|
-
|
|
2285
|
-
for (const tool of tools) {
|
|
2286
|
-
const sectionType = tool?.sectionType;
|
|
2287
|
-
if (typeof tool?.name !== 'string' || typeof sectionType !== 'string') continue;
|
|
2288
|
-
const targetInstance = localInstances.find((section) => section?.type === sectionType);
|
|
2289
|
-
if (!targetInstance?.id) continue;
|
|
2290
|
-
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
2291
|
-
if (!targetFieldKey) continue;
|
|
2292
|
-
const pageState = await readPageJson(pageEntry.slug);
|
|
2293
|
-
const section = Array.isArray(pageState.json.sections)
|
|
2294
|
-
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
2295
|
-
: null;
|
|
2296
|
-
const originalValue = section?.data?.[targetFieldKey];
|
|
2297
|
-
if (typeof originalValue !== 'string') continue;
|
|
2298
|
-
|
|
2299
|
-
return {
|
|
2300
|
-
slug: pageEntry.slug,
|
|
2301
|
-
manifestHref: pageEntry.manifestHref,
|
|
2302
|
-
contractHref: pageEntry.contractHref,
|
|
2303
|
-
toolName: tool.name,
|
|
2304
|
-
sectionId: targetInstance.id,
|
|
2305
|
-
fieldKey: targetFieldKey,
|
|
2306
|
-
originalValue,
|
|
2307
|
-
originalState: pageState,
|
|
2308
|
-
};
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
throw new Error(
|
|
2313
|
-
requestedSlug
|
|
2314
|
-
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
2315
|
-
: 'No valid WebMCP verification target found in manifest index.'
|
|
2316
|
-
);
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
async function main() {
|
|
2320
|
-
const { chromium } = await loadPlaywright();
|
|
2321
|
-
const target = await selectTarget();
|
|
2322
|
-
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
2323
|
-
const browser = await chromium.launch({ headless: true });
|
|
2324
|
-
const page = await browser.newPage();
|
|
2325
|
-
const consoleEvents = [];
|
|
2326
|
-
let mutationApplied = false;
|
|
2327
|
-
|
|
2328
|
-
page.on('console', (message) => {
|
|
2329
|
-
if (message.type() === 'error' || message.type() === 'warning') {
|
|
2330
|
-
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
2331
|
-
}
|
|
2332
|
-
});
|
|
2333
|
-
page.on('pageerror', (error) => {
|
|
2334
|
-
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
2335
|
-
});
|
|
2336
|
-
|
|
2337
|
-
const restoreOriginal = async () => {
|
|
2338
|
-
try {
|
|
2339
|
-
await page.evaluate(
|
|
2340
|
-
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
2341
|
-
const runtime = navigator.modelContextTesting;
|
|
2342
|
-
if (!runtime?.executeTool) return;
|
|
2343
|
-
await runtime.executeTool(
|
|
2344
|
-
toolName,
|
|
2345
|
-
JSON.stringify({
|
|
2346
|
-
slug,
|
|
2347
|
-
sectionId,
|
|
2348
|
-
fieldKey,
|
|
2349
|
-
value,
|
|
2350
|
-
})
|
|
2351
|
-
);
|
|
2352
|
-
},
|
|
2353
|
-
{
|
|
2354
|
-
toolName: target.toolName,
|
|
2355
|
-
slug: target.slug,
|
|
2356
|
-
sectionId: target.sectionId,
|
|
2357
|
-
fieldKey: target.fieldKey,
|
|
2358
|
-
value: target.originalValue,
|
|
2359
|
-
}
|
|
2360
|
-
);
|
|
2361
|
-
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
|
|
2362
|
-
} catch {
|
|
2363
|
-
await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
|
|
2364
|
-
}
|
|
2365
|
-
};
|
|
2366
|
-
|
|
2367
|
-
try {
|
|
2368
|
-
const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
|
|
2369
|
-
if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
|
|
2370
|
-
throw new Error(`Manifest does not expose ${target.toolName}.`);
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
const pageContract = await fetchJson(target.contractHref, `Contract request for ${target.slug}`);
|
|
2374
|
-
if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
|
|
2375
|
-
throw new Error(`Page contract does not expose ${target.toolName}.`);
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
|
|
2379
|
-
|
|
2380
|
-
try {
|
|
2381
|
-
await page.waitForFunction(
|
|
2382
|
-
({ manifestHref, contractHref }) => {
|
|
2383
|
-
const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
|
|
2384
|
-
const contractLink = document.head.querySelector('link[rel="olon-contract"]');
|
|
2385
|
-
return manifestLink?.getAttribute('href') === manifestHref
|
|
2386
|
-
&& contractLink?.getAttribute('href') === contractHref;
|
|
2387
|
-
},
|
|
2388
|
-
{ manifestHref: target.manifestHref, contractHref: target.contractHref },
|
|
2389
|
-
{ timeout: 10_000 }
|
|
2390
|
-
);
|
|
2391
|
-
} catch (error) {
|
|
2392
|
-
const diagnostics = await page.evaluate(() => ({
|
|
2393
|
-
head: document.head.innerHTML,
|
|
2394
|
-
bodyText: document.body.innerText,
|
|
2395
|
-
}));
|
|
2396
|
-
throw new Error(
|
|
2397
|
-
[
|
|
2398
|
-
error instanceof Error ? error.message : String(error),
|
|
2399
|
-
`head=${diagnostics.head}`,
|
|
2400
|
-
`body=${diagnostics.bodyText}`,
|
|
2401
|
-
...consoleEvents,
|
|
2402
|
-
].join('\n')
|
|
2403
|
-
);
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
const toolNames = await page.evaluate(() => {
|
|
2407
|
-
const runtime = navigator.modelContextTesting;
|
|
2408
|
-
return runtime?.listTools?.().map((tool) => tool.name) ?? [];
|
|
2409
|
-
});
|
|
2410
|
-
if (!toolNames.includes(target.toolName)) {
|
|
2411
|
-
throw new Error(`Runtime did not register ${target.toolName}. Found: ${toolNames.join(', ')}`);
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
const rawResult = await page.evaluate(
|
|
2415
|
-
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
2416
|
-
const runtime = navigator.modelContextTesting;
|
|
2417
|
-
if (!runtime?.executeTool) {
|
|
2418
|
-
throw new Error('navigator.modelContextTesting.executeTool is unavailable.');
|
|
2419
|
-
}
|
|
2420
|
-
return runtime.executeTool(
|
|
2421
|
-
toolName,
|
|
2422
|
-
JSON.stringify({
|
|
2423
|
-
slug,
|
|
2424
|
-
sectionId,
|
|
2425
|
-
fieldKey,
|
|
2426
|
-
value,
|
|
2427
|
-
})
|
|
2428
|
-
);
|
|
2429
|
-
},
|
|
2430
|
-
{
|
|
2431
|
-
toolName: target.toolName,
|
|
2432
|
-
slug: target.slug,
|
|
2433
|
-
sectionId: target.sectionId,
|
|
2434
|
-
fieldKey: target.fieldKey,
|
|
2435
|
-
value: nextValue,
|
|
2436
|
-
}
|
|
2437
|
-
);
|
|
2438
|
-
|
|
2439
|
-
const parsedResult = JSON.parse(rawResult);
|
|
2440
|
-
if (parsedResult?.isError) {
|
|
2441
|
-
throw new Error(`WebMCP tool returned an error: ${rawResult}`);
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
mutationApplied = true;
|
|
2445
|
-
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
|
|
2446
|
-
await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
|
|
2447
|
-
|
|
2448
|
-
console.log(
|
|
2449
|
-
JSON.stringify({
|
|
2450
|
-
ok: true,
|
|
2451
|
-
slug: target.slug,
|
|
2452
|
-
manifestHref: target.manifestHref,
|
|
2453
|
-
contractHref: target.contractHref,
|
|
2454
|
-
toolName: target.toolName,
|
|
2455
|
-
sectionId: target.sectionId,
|
|
2456
|
-
fieldKey: target.fieldKey,
|
|
2457
|
-
toolNames,
|
|
2458
|
-
})
|
|
2459
|
-
);
|
|
2460
|
-
} finally {
|
|
2461
|
-
if (mutationApplied) {
|
|
2462
|
-
await restoreOriginal();
|
|
2463
|
-
}
|
|
2464
|
-
await browser.close();
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
main().catch((error) => {
|
|
2469
|
-
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
2470
|
-
process.exit(1);
|
|
2471
|
-
});
|
|
2468
|
+
main().catch((error) => {
|
|
2469
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
});
|
|
2472
2472
|
|
|
2473
2473
|
END_OF_FILE_CONTENT
|
|
2474
2474
|
mkdir -p "specs"
|
|
@@ -10340,24 +10340,24 @@ END_OF_FILE_CONTENT
|
|
|
10340
10340
|
# SKIP: src/data/pages/design-system.json:Zone.Identifier is binary and cannot be embedded as text.
|
|
10341
10341
|
echo "Creating src/data/pages/docs.json..."
|
|
10342
10342
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/docs.json"
|
|
10343
|
-
{
|
|
10344
|
-
"id": "docs-page",
|
|
10345
|
-
"slug": "docs",
|
|
10346
|
-
"meta": {
|
|
10347
|
-
"title": "OlonJS Architecture Specifications v1.5",
|
|
10348
|
-
"description": "Mandatory Standard — Sovereign Core Edition. Canonical Studio actions, SSG, Save to file, and Hot Save."
|
|
10349
|
-
},
|
|
10350
|
-
"sections": [
|
|
10351
|
-
{
|
|
10352
|
-
"id": "docs-main",
|
|
10353
|
-
"type": "tiptap",
|
|
10354
|
-
"data": {
|
|
10355
|
-
"content": "# 📐 OlonJS Architecture Specifications v1.5\n\n**Status:** Mandatory Standard\\\n**Version:** 1.5.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing, Deterministic Local Design Tokens, Three-Layer CSS Bridge Contract)\\\n**Target:** Senior Architects / AI Agents / Enterprise Governance\n\nThis tenant follows the current OlonJS source-of-truth model: the tenant app owns content, schemas, theme, and persistence wiring; `@olonjs/core` owns the Studio shell, routing, preview, and editing engine.\n\n---\n\n## Canonical Editorial Flows\n\nThe supported Studio flows are now:\n\n- `SSG` for static HTML and route output.\n- `Save to file` for local JSON persistence back into tenant source files.\n- `Hot Save` for cloud/editorial persistence when the tenant config provides it.\n- `Add Section` for deterministic section lifecycle management inside Studio.\n\nPrevious one-off bake and JSON export paths are no longer part of Studio.\n\n---\n\n## Persistence Model\n\n`@olonjs/core` no longer performs HTML bake or ZIP export. Studio now invokes tenant-provided persistence callbacks:\n\n- `saveToFile(state, slug)`\n- `hotSave(state, slug)`\n\nThis keeps persistence explicit, tenant-owned, and aligned with the current `JsonPagesConfig` contract.\n\n---\n\n## Tenant Source Of Truth\n\n`apps/tenant-alpha` is the DNA source of truth for this tenant. Generated CLI templates are downstream artifacts and should be regenerated from source apps instead of being edited manually.\n\nThe canonical content and design files remain:\n\n- `src/data/config/site.json`\n- `src/data/config/menu.json`\n- `src/data/config/theme.json`\n- `src/data/pages/<slug>.json`\n\n---\n\n## Reference Specs\n\nUse these monorepo sources for the full protocol and architecture details:\n\n- `specs/olonjsSpecs_V_1_5.md`\n- `apps/tenant-alpha/specs/olonjsSpecs_V.1.3.md`\n\nThese source specs are the maintained references for architecture, Studio behavior, and tenant compliance."
|
|
10356
|
-
},
|
|
10357
|
-
"settings": {}
|
|
10358
|
-
}
|
|
10359
|
-
]
|
|
10360
|
-
}
|
|
10343
|
+
{
|
|
10344
|
+
"id": "docs-page",
|
|
10345
|
+
"slug": "docs",
|
|
10346
|
+
"meta": {
|
|
10347
|
+
"title": "OlonJS Architecture Specifications v1.5",
|
|
10348
|
+
"description": "Mandatory Standard — Sovereign Core Edition. Canonical Studio actions, SSG, Save to file, and Hot Save."
|
|
10349
|
+
},
|
|
10350
|
+
"sections": [
|
|
10351
|
+
{
|
|
10352
|
+
"id": "docs-main",
|
|
10353
|
+
"type": "tiptap",
|
|
10354
|
+
"data": {
|
|
10355
|
+
"content": "# 📐 OlonJS Architecture Specifications v1.5\n\n**Status:** Mandatory Standard\\\n**Version:** 1.5.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing, Deterministic Local Design Tokens, Three-Layer CSS Bridge Contract)\\\n**Target:** Senior Architects / AI Agents / Enterprise Governance\n\nThis tenant follows the current OlonJS source-of-truth model: the tenant app owns content, schemas, theme, and persistence wiring; `@olonjs/core` owns the Studio shell, routing, preview, and editing engine.\n\n---\n\n## Canonical Editorial Flows\n\nThe supported Studio flows are now:\n\n- `SSG` for static HTML and route output.\n- `Save to file` for local JSON persistence back into tenant source files.\n- `Hot Save` for cloud/editorial persistence when the tenant config provides it.\n- `Add Section` for deterministic section lifecycle management inside Studio.\n\nPrevious one-off bake and JSON export paths are no longer part of Studio.\n\n---\n\n## Persistence Model\n\n`@olonjs/core` no longer performs HTML bake or ZIP export. Studio now invokes tenant-provided persistence callbacks:\n\n- `saveToFile(state, slug)`\n- `hotSave(state, slug)`\n\nThis keeps persistence explicit, tenant-owned, and aligned with the current `JsonPagesConfig` contract.\n\n---\n\n## Tenant Source Of Truth\n\n`apps/tenant-alpha` is the DNA source of truth for this tenant. Generated CLI templates are downstream artifacts and should be regenerated from source apps instead of being edited manually.\n\nThe canonical content and design files remain:\n\n- `src/data/config/site.json`\n- `src/data/config/menu.json`\n- `src/data/config/theme.json`\n- `src/data/pages/<slug>.json`\n\n---\n\n## Reference Specs\n\nUse these monorepo sources for the full protocol and architecture details:\n\n- `specs/olonjsSpecs_V_1_5.md`\n- `apps/tenant-alpha/specs/olonjsSpecs_V.1.3.md`\n\nThese source specs are the maintained references for architecture, Studio behavior, and tenant compliance."
|
|
10356
|
+
},
|
|
10357
|
+
"settings": {}
|
|
10358
|
+
}
|
|
10359
|
+
]
|
|
10360
|
+
}
|
|
10361
10361
|
|
|
10362
10362
|
END_OF_FILE_CONTENT
|
|
10363
10363
|
echo "Creating src/data/pages/home.json..."
|
|
@@ -12686,9 +12686,9 @@ echo "Creating tsconfig.json..."
|
|
|
12686
12686
|
cat << 'END_OF_FILE_CONTENT' > "tsconfig.json"
|
|
12687
12687
|
{
|
|
12688
12688
|
"compilerOptions": {
|
|
12689
|
-
"target": "
|
|
12689
|
+
"target": "ES2022",
|
|
12690
12690
|
"useDefineForClassFields": true,
|
|
12691
|
-
"lib": ["
|
|
12691
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
12692
12692
|
"module": "ESNext",
|
|
12693
12693
|
"skipLibCheck": true,
|
|
12694
12694
|
"moduleResolution": "bundler",
|
|
@@ -12698,13 +12698,14 @@ cat << 'END_OF_FILE_CONTENT' > "tsconfig.json"
|
|
|
12698
12698
|
"noEmit": true,
|
|
12699
12699
|
"jsx": "react-jsx",
|
|
12700
12700
|
"strict": true,
|
|
12701
|
+
"forceConsistentCasingInFileNames": true,
|
|
12702
|
+
"esModuleInterop": true,
|
|
12701
12703
|
"noUnusedLocals": true,
|
|
12702
12704
|
"noUnusedParameters": true,
|
|
12703
12705
|
"noFallthroughCasesInSwitch": true,
|
|
12704
12706
|
"baseUrl": ".",
|
|
12705
12707
|
"paths": {
|
|
12706
|
-
"@/*": ["./src/*"]
|
|
12707
|
-
"@olonjs/core": ["../../packages/core/src/index.ts"]
|
|
12708
|
+
"@/*": ["./src/*"]
|
|
12708
12709
|
}
|
|
12709
12710
|
},
|
|
12710
12711
|
"include": ["src"],
|
|
@@ -12825,9 +12826,8 @@ function normalizeManifestSlug(raw) {
|
|
|
12825
12826
|
}
|
|
12826
12827
|
|
|
12827
12828
|
async function loadWebMcpBuilders() {
|
|
12828
|
-
const
|
|
12829
|
-
|
|
12830
|
-
).href;
|
|
12829
|
+
const corePkgPath = path.dirname(fileURLToPath(import.meta.resolve('@olonjs/core/package.json')));
|
|
12830
|
+
const moduleUrl = pathToFileURL(path.resolve(corePkgPath, 'src', 'lib', 'webmcp-contracts.mjs')).href;
|
|
12831
12831
|
return import(moduleUrl);
|
|
12832
12832
|
}
|
|
12833
12833
|
export default defineConfig({
|
|
@@ -12975,7 +12975,6 @@ export default defineConfig({
|
|
|
12975
12975
|
resolve: {
|
|
12976
12976
|
alias: {
|
|
12977
12977
|
'@': path.resolve(__dirname, './src'),
|
|
12978
|
-
'@olonjs/core': path.resolve(__dirname, '..', '..', 'packages', 'core', 'src', 'index.ts'),
|
|
12979
12978
|
'next/link': path.resolve(__dirname, './src/shims/next-link.tsx'),
|
|
12980
12979
|
},
|
|
12981
12980
|
},
|