@olonjs/cli 3.0.100 → 3.0.104

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.
@@ -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.88",
1649
+ "@olonjs/core": "^1.0.91",
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, '&amp;').replace(/"/g, '&quot;');
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, '&quot;');
2105
+ const safeDescription = String(description).replace(/"/g, '&quot;');
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
- buildPageContract,
1965
- buildPageManifest,
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 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
- ];
2180
+ const rootDir = path.resolve(__dirname, '..');
2181
+ const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:4173';
1981
2182
 
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
- }
2183
+ function pageFilePathFromSlug(slug) {
2184
+ return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
1986
2185
  }
1987
2186
 
1988
- function escapeHtmlAttribute(value) {
1989
- return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
2187
+ function adminUrlFromSlug(slug) {
2188
+ return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
1990
2189
  }
1991
2190
 
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;
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
- 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);
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 files;
2210
+ return null;
2011
2211
  }
2012
2212
 
2013
- async function discoverTargets() {
2014
- let files = [];
2213
+ async function loadPlaywright() {
2214
+ const require = createRequire(import.meta.url);
2015
2215
  try {
2016
- files = await listJsonFilesRecursive(pagesDir);
2017
- } catch {
2018
- files = [];
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
- const rawSlugs = files.map((fullPath) => toCanonicalSlug(path.relative(pagesDir, fullPath)));
2022
- const slugs = Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
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
- 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
- });
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
- console.log('\n[bake] Building client...');
2032
- await build({ root, mode: 'production', logLevel: 'warn' });
2033
- console.log('[bake] Client build done.');
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
- 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.');
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
- const targets = await discoverTargets();
2054
- if (targets.length === 0) {
2055
- throw new Error('[bake] No pages discovered under src/data/pages');
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
- const ssrEntryUrl = pathToFileURL(path.resolve(root, 'dist-ssr/entry-ssg.js')).href;
2060
- const { render, getCss, getPageMeta, getWebMcpBuildState } = await import(ssrEntryUrl);
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 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>.');
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
- const inlinedCss = getCss();
2070
- const styleTag = `<style data-bake="inline">${inlinedCss}</style>`;
2071
- const webMcpBuildState = getWebMcpBuildState();
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
- 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,
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
- await writeJsonTargets(`schemas/${slug}.schema.json`, contract);
2083
- const pageManifest = buildPageManifest({
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 mcpManifest = buildSiteManifest({
2093
- pages: webMcpBuildState.pages,
2094
- schemas: webMcpBuildState.schemas,
2095
- siteConfig: webMcpBuildState.siteConfig,
2096
- });
2097
- await writeJsonTargets('mcp-manifest.json', mcpManifest);
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
- for (const { slug, out, depth } of targets) {
2100
- console.log(`\n[bake] Rendering /${slug === 'home' ? '' : slug}...`);
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
- const appHtml = render(slug);
2103
- const { title, description } = getPageMeta(slug);
2104
- const safeTitle = String(title).replace(/"/g, '&quot;');
2105
- const safeDescription = String(description).replace(/"/g, '&quot;');
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
- 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 ');
2378
+ await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
2128
2379
 
2129
- let bakedHtml = fixedTemplate
2130
- .replace('</head>', ` ${styleTag}\n ${contractLinks}\n</head>`)
2131
- .replace(/<title>.*?<\/title>/, `<title>${safeTitle}</title>\n ${metaTags}`);
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
- 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
- }
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
- 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}"]`);
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
- 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"
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"
@@ -5574,7 +5574,6 @@ export type DevexData = z.infer<typeof DevexSchema>;
5574
5574
  export type DevexSettings = z.infer<typeof BaseSectionSettingsSchema>;
5575
5575
 
5576
5576
  END_OF_FILE_CONTENT
5577
- mkdir -p "src/components/docs-layout"
5578
5577
  mkdir -p "src/components/feature-grid"
5579
5578
  echo "Creating src/components/feature-grid/View.tsx..."
5580
5579
  cat << 'END_OF_FILE_CONTENT' > "src/components/feature-grid/View.tsx"
@@ -10341,24 +10340,24 @@ END_OF_FILE_CONTENT
10341
10340
  # SKIP: src/data/pages/design-system.json:Zone.Identifier is binary and cannot be embedded as text.
10342
10341
  echo "Creating src/data/pages/docs.json..."
10343
10342
  cat << 'END_OF_FILE_CONTENT' > "src/data/pages/docs.json"
10344
- {
10345
- "id": "docs-page",
10346
- "slug": "docs",
10347
- "meta": {
10348
- "title": "OlonJS Architecture Specifications v1.5",
10349
- "description": "Mandatory Standard — Sovereign Core Edition. Canonical Studio actions, SSG, Save to file, and Hot Save."
10350
- },
10351
- "sections": [
10352
- {
10353
- "id": "docs-main",
10354
- "type": "tiptap",
10355
- "data": {
10356
- "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."
10357
- },
10358
- "settings": {}
10359
- }
10360
- ]
10361
- }
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
+ }
10362
10361
 
10363
10362
  END_OF_FILE_CONTENT
10364
10363
  echo "Creating src/data/pages/home.json..."
@@ -12593,7 +12592,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
12593
12592
 
12594
12593
  END_OF_FILE_CONTENT
12595
12594
  # SKIP: src/registry-types.ts is binary and cannot be embedded as text.
12596
- mkdir -p "src/server"
12597
12595
  mkdir -p "src/types"
12598
12596
  echo "Creating src/types.ts..."
12599
12597
  cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
@@ -12688,9 +12686,9 @@ echo "Creating tsconfig.json..."
12688
12686
  cat << 'END_OF_FILE_CONTENT' > "tsconfig.json"
12689
12687
  {
12690
12688
  "compilerOptions": {
12691
- "target": "ES2020",
12689
+ "target": "ES2022",
12692
12690
  "useDefineForClassFields": true,
12693
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
12691
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
12694
12692
  "module": "ESNext",
12695
12693
  "skipLibCheck": true,
12696
12694
  "moduleResolution": "bundler",
@@ -12700,6 +12698,8 @@ cat << 'END_OF_FILE_CONTENT' > "tsconfig.json"
12700
12698
  "noEmit": true,
12701
12699
  "jsx": "react-jsx",
12702
12700
  "strict": true,
12701
+ "forceConsistentCasingInFileNames": true,
12702
+ "esModuleInterop": true,
12703
12703
  "noUnusedLocals": true,
12704
12704
  "noUnusedParameters": true,
12705
12705
  "noFallthroughCasesInSwitch": true,