catalyst-core 1.0.2-canary.2 → 1.0.2
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/changelog.md +34 -0
- package/dist/cli/appConfig.js +1 -0
- package/dist/native/buildIos/build.js +10 -6
- package/dist/native/utils.js +8 -3
- package/dist/scripts/build.js +1 -0
- package/dist/vite/vite.config.js +1 -0
- package/dist/webmcp/WebMcpProvider.jsx +3 -2
- package/dist/webmcp/useTool.js +1 -1
- package/mcp_v2/conversion-tasks.json +6 -5
- package/mcp_v2/knowledge-base.json +124 -7
- package/mcp_v2/test/config.test.ts +59 -0
- package/mcp_v2/tools/build.js +6 -6
- package/mcp_v2/tools/config.js +35 -18
- package/mcp_v2/tools/conversion.js +7 -13
- package/package.json +4 -4
package/changelog.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.2] - unreleased
|
|
4
|
+
|
|
5
|
+
This release adds two opt-in features, an animated HTML splash screen for universal apps and experimental WebMCP support, and gives every CLI command the same terminal output. There are no breaking API changes.
|
|
6
|
+
|
|
7
|
+
### HTML splash screen
|
|
8
|
+
|
|
9
|
+
A universal app can ship `public/splashscreen.html`. When `WEBVIEW_CONFIG.splashScreen` is set, the native splash image hands over to that page, which stays up until the app's first page has loaded. The new optional `splashScreen.minDuration` keeps it on screen for at least that many milliseconds, and it is dismissed after 10 seconds regardless. The file must be self-contained, and on Android it cannot make network requests. Without the file, the splash works as before.
|
|
10
|
+
|
|
11
|
+
### WebMCP (experimental)
|
|
12
|
+
|
|
13
|
+
The new `catalyst-core/webmcp` subpath lets an app expose its actions to AI agents running in the browser. `useTool()` registers a tool from a component, a `tool` field on a route derives one from the route, and `WebMcpProvider` adds the built-in `navigate`, `get_current_route` and `get_page_info` tools. `catalyst-core/webmcp/shim` installs an opt-in `document.modelContext` shim. The API may change in a minor release.
|
|
14
|
+
|
|
15
|
+
### New root export
|
|
16
|
+
|
|
17
|
+
The root entry of `catalyst-core` adds `useRouterState`, which returns `{ location, params, navigate, matchedRoutes }` through Catalyst's router context. `catalyst-core/webmcp` uses it to read router state without importing `react-router` from a subpath. The root entry now exports nine names; nothing was removed.
|
|
18
|
+
|
|
19
|
+
### CLI output
|
|
20
|
+
|
|
21
|
+
`catalyst start`, `build`, `serve`, the native build and setup commands, and `create-catalyst-app` now print a short header, one line per step with its timing, and one diagnostic block on failure with the error code, location, hint and docs link. The full bundler and native build output moved behind `--verbose`, and `--debug` adds stacks and the environment. `catalyst --help`, `--version` and a "did you mean" suggestion for mistyped commands are new. Once `catalyst serve` has started the production server, the app's own output passes through unchanged.
|
|
22
|
+
|
|
23
|
+
### Fixes
|
|
24
|
+
|
|
25
|
+
- `catalyst serve` refuses to serve a build whose inlined values (`PUBLIC_STATIC_ASSET_URL`, `PUBLIC_STATIC_ASSET_PATH`, `CLIENT_ENV_VARIABLES`) differ from the current `config/config.json`, with `PREFLIGHT-022`. It also reports a missing build or a taken port before starting.
|
|
26
|
+
- Fast Refresh now works for components written as JSX in `.js` files instead of reloading the page.
|
|
27
|
+
- `MetaTag` no longer depends on `react-helmet-async`, which is removed from the dependencies. Route metadata from `setMetaData` renders through React 19's `<head>` support, and server-rendered head tags no longer outlive their page after client navigation.
|
|
28
|
+
- `catalyst-core` no longer imports `prop-types` at runtime without declaring it.
|
|
29
|
+
- `Otel.init()` no longer throws `ReferenceError: logger is not defined` when the app has not called `configureLogger()`. It logs to `console` until a global logger is set.
|
|
30
|
+
- Signing passwords are redacted from the output of a failed signed build or `keytool` run.
|
|
31
|
+
- File logs written by `configureLogger()` keep structured fields for plain objects again.
|
|
32
|
+
|
|
33
|
+
### create-catalyst-app
|
|
34
|
+
|
|
35
|
+
New apps declare `"type": "module"`, and the Tailwind template's config is `postcss.config.cjs`. `catalyst-core` loads either `postcss.config.cjs` or `postcss.config.js`, so existing apps need no change. `create-catalyst-app` declares `node >= 20.12.0` and runs across that range.
|
|
36
|
+
|
|
3
37
|
## [1.0.0] - 2026-09-29
|
|
4
38
|
|
|
5
39
|
This release freezes the public API. The root entry of `catalyst-core` now exports exactly eight names and nothing else: `RouterDataProvider`, `useCurrentRouteData`, `useRouterData`, `MetaTag`, `split`, `hydrationReady`, `Head`, and `Body`. Anything previously reachable through the root entry but not in that list is no longer public. Applications that import a removed name will fail at build time rather than silently resolving to `undefined`.
|
package/dist/cli/appConfig.js
CHANGED
|
@@ -18,6 +18,7 @@ const { diagnostic } = require("./diagnostic.js")
|
|
|
18
18
|
* @param {string} [cwd] - app root; defaults to the current working directory
|
|
19
19
|
*/
|
|
20
20
|
function loadAppConfig(cwd = process.cwd()) {
|
|
21
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - cwd is the local project directory; the file name is a fixed literal.
|
|
21
22
|
const configPath = path.join(cwd, "config", "config.json")
|
|
22
23
|
|
|
23
24
|
let raw
|
|
@@ -4,7 +4,8 @@ const fs = require("fs");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const {
|
|
7
|
-
execSync
|
|
7
|
+
execSync,
|
|
8
|
+
execFileSync
|
|
8
9
|
} = require("child_process");
|
|
9
10
|
const {
|
|
10
11
|
runInteractiveCommand
|
|
@@ -222,7 +223,9 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
222
223
|
// The app root, not process.cwd(): the iOS build chdirs into the
|
|
223
224
|
// Xcode project inside node_modules, so the artifact landed there
|
|
224
225
|
// and was wiped by the next npm install.
|
|
225
|
-
const destinationDir = path.join(
|
|
226
|
+
const destinationDir = path.join(
|
|
227
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local project root from the CLI context.
|
|
228
|
+
ctx.appRoot || process.cwd(), BUILD_OUTPUT_PATH,
|
|
226
229
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is trusted application config.
|
|
227
230
|
"native", "ios", currentDate, buildType // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildType is "debug" or "release" from config.
|
|
228
231
|
);
|
|
@@ -249,9 +252,10 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
249
252
|
progress.status("waiting for install");
|
|
250
253
|
for (let i = 0; i < 30; i++) {
|
|
251
254
|
try {
|
|
252
|
-
|
|
255
|
+
// No shell: the bundle id is passed as an argv entry, never interpolated.
|
|
256
|
+
execFileSync("xcrun", ["simctl", "get_app_container", "booted", APP_BUNDLE_ID], {
|
|
253
257
|
stdio: ["ignore", "pipe", "ignore"]
|
|
254
|
-
});
|
|
258
|
+
});
|
|
255
259
|
break;
|
|
256
260
|
} catch (error) {
|
|
257
261
|
if (i === 29) throw new Error("Timeout: App installation took too long.");
|
|
@@ -272,9 +276,9 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
272
276
|
progress.status("waiting for launch");
|
|
273
277
|
for (let i = 0; i < 10; i++) {
|
|
274
278
|
try {
|
|
275
|
-
const launchResult =
|
|
279
|
+
const launchResult = execFileSync("xcrun", ["simctl", "launch", "booted", APP_BUNDLE_ID], {
|
|
276
280
|
stdio: ["ignore", "pipe", "ignore"]
|
|
277
|
-
}).toString();
|
|
281
|
+
}).toString();
|
|
278
282
|
if (launchResult.includes("already launched")) {
|
|
279
283
|
break;
|
|
280
284
|
}
|
package/dist/native/utils.js
CHANGED
|
@@ -87,11 +87,11 @@ function runCommand(command, {
|
|
|
87
87
|
// Redacted before it reaches the log file too -- the log is a build
|
|
88
88
|
// artifact that gets attached to bug reports and uploaded by CI.
|
|
89
89
|
try {
|
|
90
|
-
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native setup/build uses fixed internal command strings; preserve existing shell behavior.
|
|
91
90
|
// stderr is captured, not inherited. Many tools (java -version, for
|
|
92
91
|
// one) write to stderr on success, and an inherited fd puts that text
|
|
93
92
|
// at column 0 -- straight through whatever the spinner is drawing.
|
|
94
93
|
// The caller decides what, if anything, is worth showing.
|
|
94
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native setup/build uses fixed internal command strings; preserve existing shell behavior.
|
|
95
95
|
return (0, _child_process.execSync)(command, {
|
|
96
96
|
encoding: "utf8",
|
|
97
97
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -187,8 +187,11 @@ async function promptUser(question, {
|
|
|
187
187
|
*/
|
|
188
188
|
function detectAndroidSdk(env = process.env) {
|
|
189
189
|
const home = _os.default.homedir();
|
|
190
|
-
const candidates = [env.ANDROID_HOME, env.ANDROID_SDK_ROOT, _path.default.join(home, "Library", "Android", "sdk"), _path.default.join(home, "Android", "Sdk"),
|
|
190
|
+
const candidates = [env.ANDROID_HOME, env.ANDROID_SDK_ROOT, _path.default.join(home, "Library", "Android", "sdk"), _path.default.join(home, "Android", "Sdk"),
|
|
191
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
192
|
+
env.LOCALAPPDATA && _path.default.join(env.LOCALAPPDATA, "Android", "Sdk")].filter(Boolean);
|
|
191
193
|
const adb = process.platform === "win32" ? "adb.exe" : "adb";
|
|
194
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
192
195
|
return candidates.find(dir => _fs.default.existsSync(_path.default.join(dir, "platform-tools", adb))) || null;
|
|
193
196
|
}
|
|
194
197
|
|
|
@@ -196,7 +199,7 @@ function detectAndroidSdk(env = process.env) {
|
|
|
196
199
|
function listAvds(sdkPath) {
|
|
197
200
|
if (!sdkPath) return [];
|
|
198
201
|
try {
|
|
199
|
-
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - sdkPath is a local SDK directory from config or auto-detection.
|
|
202
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process, javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sdkPath is a local SDK directory from config or auto-detection, never remote input.
|
|
200
203
|
const out = (0, _child_process.execSync)(`"${_path.default.join(expandHome(sdkPath), "emulator", "emulator")}" -list-avds`, {
|
|
201
204
|
encoding: "utf8",
|
|
202
205
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -212,6 +215,7 @@ function listAvds(sdkPath) {
|
|
|
212
215
|
function expandHome(dir) {
|
|
213
216
|
if (typeof dir !== "string") return dir;
|
|
214
217
|
if (dir === "~") return _os.default.homedir();
|
|
218
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
215
219
|
return dir.startsWith("~/") || dir.startsWith("~\\") ? _path.default.join(_os.default.homedir(), dir.slice(2)) : dir;
|
|
216
220
|
}
|
|
217
221
|
|
|
@@ -224,6 +228,7 @@ const ANDROID_SDK_HINT = exports.ANDROID_SDK_HINT = 'Install Android Studio, or
|
|
|
224
228
|
* in gradle with "SDK location not found" despite a configured sdkPath.
|
|
225
229
|
*/
|
|
226
230
|
function ensureLocalProperties(projectDir, sdkPath) {
|
|
231
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
|
|
227
232
|
const file = _path.default.join(projectDir, "local.properties");
|
|
228
233
|
const line = `sdk.dir=${expandHome(sdkPath).replace(/\\/g, "\\\\")}`;
|
|
229
234
|
let content = "";
|
package/dist/scripts/build.js
CHANGED
|
@@ -170,6 +170,7 @@ function reportBundleFailures(failures, cwd) {
|
|
|
170
170
|
message: shortenPaths(cause.message, cwd),
|
|
171
171
|
code,
|
|
172
172
|
scope: `${cause.bundles.join(" + ")} bundle`,
|
|
173
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - cause.file is a build-tool diagnostic path, only used to display a location.
|
|
173
174
|
file: cause.file ? path.resolve(cwd, cause.file.replace(/^(\.\.\/)+/, "")) : undefined,
|
|
174
175
|
line: cause.line,
|
|
175
176
|
// esbuild reports columns 0-based; the frame's caret is 1-based.
|
package/dist/vite/vite.config.js
CHANGED
|
@@ -283,6 +283,7 @@ const getScssResourcesImport = () => {
|
|
|
283
283
|
* half of them opened with 23 warning lines it could do nothing about.
|
|
284
284
|
*/
|
|
285
285
|
function resolvableFrom(candidates, appRoot) {
|
|
286
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local project root resolved by the CLI.
|
|
286
287
|
const resolver = createRequire(path.join(appRoot, "package.json"))
|
|
287
288
|
return candidates.filter((name) => {
|
|
288
289
|
try {
|
|
@@ -127,7 +127,7 @@ export function WebMcpProvider({ children, routes, filterNavigable }) {
|
|
|
127
127
|
if (res.ok) owners.push(key)
|
|
128
128
|
else
|
|
129
129
|
// eslint-disable-next-line no-console
|
|
130
|
-
console.warn(
|
|
130
|
+
console.warn('[webmcp] declarative tool "%s" failed to register:', spec.name, res.error)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// Framework: navigate + get_current_route + get_page_info.
|
|
@@ -146,7 +146,8 @@ export function WebMcpProvider({ children, routes, filterNavigable }) {
|
|
|
146
146
|
else
|
|
147
147
|
// eslint-disable-next-line no-console
|
|
148
148
|
console.warn(
|
|
149
|
-
|
|
149
|
+
'[webmcp] framework tool "%s" failed to register — a route tool may already use that name:',
|
|
150
|
+
spec.name,
|
|
150
151
|
res.error
|
|
151
152
|
)
|
|
152
153
|
}
|
package/dist/webmcp/useTool.js
CHANGED
|
@@ -89,7 +89,7 @@ export function useTool(spec, deps) {
|
|
|
89
89
|
} else {
|
|
90
90
|
setState({ status: "error", receipt: null, error: res.error })
|
|
91
91
|
// eslint-disable-next-line no-console
|
|
92
|
-
console.warn(
|
|
92
|
+
console.warn('[webmcp] useTool("%s") failed to register:', s.name, res.error)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
return () => {
|
|
@@ -206,12 +206,13 @@
|
|
|
206
206
|
"detect_pattern": "field_check + file_exists",
|
|
207
207
|
"how_to_check": [
|
|
208
208
|
"file: config/config.json",
|
|
209
|
-
"field: splashScreen must exist at top level
|
|
210
|
-
"file: public/android/splashscreen.png
|
|
211
|
-
"file: public/ios/splashscreen.png
|
|
209
|
+
"field: WEBVIEW_CONFIG.splashScreen must exist (NOT at the top level of config.json)",
|
|
210
|
+
"file (optional): public/android/splashscreen.{png|jpg|jpeg|gif|bmp|webp}",
|
|
211
|
+
"file (optional): public/ios/splashscreen.{png|jpg|jpeg}",
|
|
212
|
+
"file (optional, 1.0.2+): public/splashscreen.html"
|
|
212
213
|
],
|
|
213
|
-
"fix_guide": "1) Add to
|
|
214
|
-
"verify_hint": "Build and install the app on a device/emulator. On cold launch (after force-closing), you should see the splash image for ~1-2 seconds before the app loads. If you see
|
|
214
|
+
"fix_guide": "1) Add WEBVIEW_CONFIG.splashScreen to config/config.json, e.g. { backgroundColor: '#ffffff', duration: 1000, imageWidth: 120, imageHeight: 120 }. There is no path field: images are found by filename. 2) Place the images at public/android/splashscreen.<ext> and public/ios/splashscreen.<ext>; without one, Android shows the launcher icon and iOS a loader. 3) Optional on catalyst-core 1.0.2+: add a self-contained public/splashscreen.html (inline CSS/JS, data: URI images, no network) for an animated splash shown after the native image until the first page loads; set splashScreen.minDuration (ms) to hold it longer. duration is ignored in that mode.",
|
|
215
|
+
"verify_hint": "Build and install the app on a device/emulator. On cold launch (after force-closing), you should see the splash image for ~1-2 seconds before the app loads. If you see no splash, check that splashScreen sits inside WEBVIEW_CONFIG (not at the top level of config.json) and that the image filename is splashscreen.<ext>.",
|
|
215
216
|
"depends_on": ["T1_CONFIG"]
|
|
216
217
|
},
|
|
217
218
|
{
|
|
@@ -490,7 +490,7 @@
|
|
|
490
490
|
{
|
|
491
491
|
"section": "config_structure",
|
|
492
492
|
"title": "splashScreen Config Structure",
|
|
493
|
-
"content": "splashScreen sits INSIDE WEBVIEW_CONFIG (not at top
|
|
493
|
+
"content": "splashScreen sits INSIDE WEBVIEW_CONFIG (not at the top level of config.json); the Android and iOS builds read only WEBVIEW_CONFIG.splashScreen. All fields are optional. Omitting splashScreen turns the Catalyst splash off and the app goes straight to the WebView. Fields: backgroundColor (string hex, default '#ffffff'), duration (number ms; Android defaults to 1000; on iOS, when unset, the splash is dismissed once the WebView finishes loading; ignored when public/splashscreen.html exists), minDuration (number ms, default 0, catalyst-core 1.0.2+; the minimum time the HTML splash stays up; a negative or non-number value is ignored with a build warning), imageWidth (number px, default 120), imageHeight (number px, default 120), cornerRadius (number px, default 20; 0 for square corners). Example: WEBVIEW_CONFIG.splashScreen = { backgroundColor: '#ffffff', duration: 2000, imageWidth: 400, imageHeight: 200, cornerRadius: 20 }. Asset placement: put image at public/android/splashscreen.{png|jpg|jpeg|gif|bmp|webp} for Android, public/ios/splashscreen.{png|jpg|jpeg} for iOS. Asset is auto-discovered by filename \u2014 NO path field in config. Missing image = build proceeds; Android shows the launcher icon, iOS a loader. backgroundColor is applied to Android themes.xml (windowSplashScreenBackground). Optional animated splash (1.0.2+): public/splashscreen.html, see 'HTML Splash Screen (1.0.2)'. Pfizer example: splashScreen: { imageHeight: 200, imageWidth: 400 } (backgroundColor omits = defaults to white).",
|
|
494
494
|
"layer": "Config",
|
|
495
495
|
"source": "static",
|
|
496
496
|
"tags": ["splashscreen", "splash", "config", "webview_config", "android", "ios", "native", "assets"]
|
|
@@ -958,7 +958,7 @@
|
|
|
958
958
|
{
|
|
959
959
|
"section": "observability",
|
|
960
960
|
"title": "OpenTelemetry Setup in Catalyst",
|
|
961
|
-
"content": "Catalyst provides built-in OpenTelemetry support via catalyst-core/otel. Server-side only.\n\nUSAGE: Call Otel.init() inside preServerInit() in server/index.js. Guard with an ENABLE flag and skip in development.\n\nimport Otel from 'catalyst-core/otel'\n\nexport const preServerInit = () => {\n const otelConfig = JSON.parse(process.env.OTEL)\n if (otelConfig.ENABLE && process.env.NODE_ENV !== 'development') {\n Otel.init({\n serviceName: 'my-catalyst-app',\n serviceVersion: process.env.RELEASE_TAG,\n environment: process.env.ENV_NAME,\n traceUrl: otelConfig.ENDPOINT,\n instrumentations: [ new HttpInstrumentation({ ... }) ],\n })\n }\n}\n\nCONFIG SHAPE (all optional, defaults shown):\n- serviceName: 'catalyst-server'\n- serviceVersion: '1.0.0'\n- environment: 'development'\n- traceUrl: 'http://localhost:4318/v1/traces'\n- metricUrl: 'http://localhost:4318/v1/metrics'\n- traceHeaders: {}\n- metricHeaders: {}\n- exportIntervalMillis: 10000\n- exportTimeoutMillis: 10000\n- instrumentations: [getNodeAutoInstrumentations()] by default\n\nAUTO METRICS: init() automatically collects process_cpu_usage_percent, process_memory_usage_bytes, process_memory_heap_used_bytes, process_memory_heap_total_bytes \u2014 no extra config needed.\n\nGRACEFUL SHUTDOWN: SIGTERM and SIGINT handlers registered automatically \u2014 sdk.shutdown() is called on process exit.\n\nRETURN VALUE: { sdk, meter } \u2014 use meter (OTel Meter instance) to create your own custom metrics via meter.createObservableGauge(), createCounter(), etc.\n\nPEER DEPS: Install @opentelemetry/* packages yourself \u2014 not bundled by catalyst-core.",
|
|
961
|
+
"content": "Catalyst provides built-in OpenTelemetry support via catalyst-core/otel. Server-side only.\n\nUSAGE: Call Otel.init() inside preServerInit() in server/index.js. Guard with an ENABLE flag and skip in development.\n\nimport Otel from 'catalyst-core/otel'\n\nexport const preServerInit = () => {\n const otelConfig = JSON.parse(process.env.OTEL)\n if (otelConfig.ENABLE && process.env.NODE_ENV !== 'development') {\n Otel.init({\n serviceName: 'my-catalyst-app',\n serviceVersion: process.env.RELEASE_TAG,\n environment: process.env.ENV_NAME,\n traceUrl: otelConfig.ENDPOINT,\n instrumentations: [ new HttpInstrumentation({ ... }) ],\n })\n }\n}\n\nCONFIG SHAPE (all optional, defaults shown):\n- serviceName: 'catalyst-server'\n- serviceVersion: '1.0.0'\n- environment: 'development'\n- traceUrl: 'http://localhost:4318/v1/traces'\n- metricUrl: 'http://localhost:4318/v1/metrics'\n- traceHeaders: {}\n- metricHeaders: {}\n- exportIntervalMillis: 10000\n- exportTimeoutMillis: 10000\n- instrumentations: [getNodeAutoInstrumentations()] by default\n\nAUTO METRICS: init() automatically collects process_cpu_usage_percent, process_memory_usage_bytes, process_memory_heap_used_bytes, process_memory_heap_total_bytes \u2014 no extra config needed.\n\nGRACEFUL SHUTDOWN: SIGTERM and SIGINT handlers registered automatically \u2014 sdk.shutdown() is called on process exit.\n\nRETURN VALUE: { sdk, meter } \u2014 use meter (OTel Meter instance) to create your own custom metrics via meter.createObservableGauge(), createCounter(), etc.\n\nPEER DEPS: Install @opentelemetry/* packages yourself \u2014 not bundled by catalyst-core.\n\nLOGGING: init() logs through the app-wide logger that configureLogger() from catalyst-core/logger installs. Before 1.0.2, calling Otel.init() in an app that never called configureLogger() threw ReferenceError: logger is not defined. From 1.0.2 it falls back to console until a global logger is set, and picks up a logger configured later. On older versions, call configureLogger() before Otel.init().",
|
|
962
962
|
"layer": "Observability",
|
|
963
963
|
"source": "static",
|
|
964
964
|
"tags": [
|
|
@@ -1257,7 +1257,7 @@
|
|
|
1257
1257
|
{
|
|
1258
1258
|
"section": "framework_identity",
|
|
1259
1259
|
"title": "Catalyst Runtime Generations",
|
|
1260
|
-
"content": "Always determine the installed catalyst-core version before giving web runtime guidance. Catalyst 0.2.x is the legacy webpack generation and uses @tata1mg/router plus @loadable/component. Catalyst 0.3.x, beginning with 0.3.0-beta.1, is the Vite/ESM generation: router APIs are exported by catalyst-core and split()/hydrationReady() replace loadable APIs. Catalyst 1.0.0 is the API freeze (there is no 0.4.0 release); see 'Upgrading catalyst-core 0.3.x to 1.0.0'. Do not combine instructions from different generations. If no version can be detected, ask which version the project targets.",
|
|
1260
|
+
"content": "Always determine the installed catalyst-core version before giving web runtime guidance. Catalyst 0.2.x is the legacy webpack generation and uses @tata1mg/router plus @loadable/component. Catalyst 0.3.x, beginning with 0.3.0-beta.1, is the Vite/ESM generation: router APIs are exported by catalyst-core and split()/hydrationReady() replace loadable APIs. Catalyst 1.0.0 is the API freeze (there is no 0.4.0 release); see 'Upgrading catalyst-core 0.3.x to 1.0.0'. Do not combine instructions from different generations. If no version can be detected, ask which version the project targets. Patch releases after 1.0.0 (1.0.1, 1.0.2) keep the frozen API; see 'Upgrading catalyst-core 1.0.x to 1.0.2'.",
|
|
1261
1261
|
"layer": "Framework",
|
|
1262
1262
|
"source": "static",
|
|
1263
1263
|
"tags": ["versions", "0.2.x", "0.3.x", "1.0.0", "migration", "webpack", "vite"],
|
|
@@ -1281,10 +1281,22 @@
|
|
|
1281
1281
|
{
|
|
1282
1282
|
"section": "build_system",
|
|
1283
1283
|
"title": "Catalyst 0.3.x CLI Commands",
|
|
1284
|
-
"content": "Current web commands are catalyst start, catalyst build, catalyst serve, and catalyst serve:inspect. devBuild and devServe are legacy 0.2.x commands and are not accepted by the 0.3.x CLI. Native commands remain buildApp, buildApp:ios, buildApp:android, setupEmulator, setupEmulator:ios, and setupEmulator:android.",
|
|
1284
|
+
"content": "Current web commands are catalyst start, catalyst build, catalyst serve, and catalyst serve:inspect. devBuild and devServe are legacy 0.2.x commands and are not accepted by the 0.3.x CLI. Native commands remain buildApp, buildApp:ios, buildApp:android, setupEmulator, setupEmulator:ios, and setupEmulator:android. catalyst-core 1.0.2 OUTPUT: every command prints a short header, one line per step with timing, and one diagnostic block on failure. Flags: --verbose shows every step and, on failure, the full tool output (bundler, Gradle); --debug also prints stacks and the environment. Child processes inherit the mode through CATALYST_OUTPUT_MODE=verbose|debug. Failures carry registry codes from the error catalog (for example BUNDLE-000, ANDROID-000, IOS-000, CCA-001); use the explain_error tool for any code. catalyst serve (1.0.2+) exits when there is no build in BUILD_OUTPUT_PATH and refuses a stale build with PREFLIGHT-022 when PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH or a CLIENT_ENV_VARIABLES value in config/config.json changed after catalyst build (the build records them in <BUILD_OUTPUT_PATH>/.catalyst-build.json; a build from an older version has no record, so serve warns and skips the check until you rebuild). Fix: run catalyst build again, then catalyst serve. catalyst serve passes the app's own stdout/stderr through unchanged, so JSON log shippers keep working. Scripts that parsed the old CLI text output must be updated.",
|
|
1285
1285
|
"layer": "Build",
|
|
1286
1286
|
"source": "static",
|
|
1287
|
-
"tags": [
|
|
1287
|
+
"tags": [
|
|
1288
|
+
"0.3.x",
|
|
1289
|
+
"1.0.2",
|
|
1290
|
+
"cli",
|
|
1291
|
+
"start",
|
|
1292
|
+
"build",
|
|
1293
|
+
"serve",
|
|
1294
|
+
"native",
|
|
1295
|
+
"verbose",
|
|
1296
|
+
"debug",
|
|
1297
|
+
"stale-build",
|
|
1298
|
+
"PREFLIGHT-022"
|
|
1299
|
+
],
|
|
1288
1300
|
"github_files": ["packages/catalyst-core/bin/catalyst.js"]
|
|
1289
1301
|
},
|
|
1290
1302
|
{
|
|
@@ -1303,7 +1315,7 @@
|
|
|
1303
1315
|
{
|
|
1304
1316
|
"section": "routing",
|
|
1305
1317
|
"title": "Catalyst 0.3.x Integrated Router",
|
|
1306
|
-
"content": "Catalyst 0.3.x removes the @tata1mg/router package dependency. As of 1.0.0 (the API freeze), catalyst-core does NOT re-export React Router. Its root entry
|
|
1318
|
+
"content": "Catalyst 0.3.x removes the @tata1mg/router package dependency. As of 1.0.0 (the API freeze), catalyst-core does NOT re-export React Router. Its root entry exported exactly eight names in 1.0.0: RouterDataProvider, useCurrentRouteData, useRouterData, MetaTag, split, hydrationReady, Head, Body. 1.0.2 adds a ninth, useRouterState (returns { location, params, navigate, matchedRoutes } through Catalyst's router context; used by catalyst-core/webmcp; throws outside RouterDataProvider). App components should still use react-router's useLocation/useParams/useNavigate. There is no default export (split was previously also the default). React Router is a peerDependency (^7.18.2): install react-router in the application and import Outlet, Link, useParams, useNavigate, useLocation, Navigate and every other router name from 'react-router' directly. Import RouterProvider in client/index.js from 'react-router/dom', which passes React DOM's flushSync to the router. Metadata utilities (getMetaData, mergeHeadElements, deleteHeadTagsByDataAttribute), RouterContext, serverDataFetcher and useNavigateWithTransition are no longer public. Existing route arrays and preparedRoutes structure remain unchanged. Keeping both router packages can create duplicate runtime contracts and invalid React element errors.",
|
|
1307
1319
|
"layer": "Runtime",
|
|
1308
1320
|
"source": "static",
|
|
1309
1321
|
"tags": [
|
|
@@ -1377,7 +1389,7 @@
|
|
|
1377
1389
|
{
|
|
1378
1390
|
"section": "version_migration",
|
|
1379
1391
|
"title": "Upgrading catalyst-core 0.3.x to 1.0.0",
|
|
1380
|
-
"content": "Upgrade path for an application on catalyst-core 0.3.x to 1.0.0, the stable release. Read the installed catalyst-core version from package.json first. 0.3.0-beta.5 upgrades directly. 0.3.0-beta.1 to 0.3.0-beta.4 used react-router-dom 6, so those also cross React Router 6 to 7 (step 1). Older applications first move to 0.3.x (see 'Legacy 0.2.x Migration Phase 0' and 'Catalyst 0.3.x React Runtime Contract'). 1.0.0 is the API freeze; there is no 0.4.0 release. Do the steps in order.\n\n1. DEPENDENCIES: set catalyst-core to 1.0.0. Add react-router ^7.18.2 as an application dependency: it is now a peer dependency and the server exits at startup if it is missing or outside that range. Keep react and react-dom pinned to 19.0.0. TypeScript applications also need @types/react ^19.0.0. catalyst-core no longer installs react-router-dom, so change any import from 'react-router-dom' to 'react-router', except RouterProvider, which comes from 'react-router/dom' (step 3).\n\n2. NODE: run node -v. catalyst-core 1.0.0 requires >=22.12; on Node 20, importing catalyst-core/hooks or catalyst-core/WebBridge fails with ERR_REQUIRE_ESM. If the runtime is older, stop and tell the user; do not change the project's Node pin yourself.\n\n3. IMPORTS: the root entry exports exactly RouterDataProvider, useCurrentRouteData, useRouterData, MetaTag, split, hydrationReady, Head, Body. Move every React Router name (Link, Outlet, useParams, useNavigate, useLocation, Navigate, ...) from 'catalyst-core' to 'react-router'. In client/index.js import RouterProvider from 'react-router/dom', not 'react-router': only the /dom export passes React DOM's flushSync to the router, and the wrong one renders without any error. Replace `import split from 'catalyst-core'` with `import { split } from 'catalyst-core'`; there is no default export. Replace imports from 'catalyst-core/document' with Head and Body from 'catalyst-core'. 'catalyst-core/caching' is removed with no replacement. RouterContext, serverDataFetcher, mergeHeadElements, deleteHeadTagsByDataAttribute, getMetaData, useNavigateWithTransition and sanitizeFilePickerOptions (catalyst-core/hooks) are internal with no replacement: if the application uses one, stop and tell the user instead of inventing a substitute.\n\n4. CONFIG: catalyst start, build and serve now run a preflight and exit before starting when config/config.json lacks NODE_SERVER_HOSTNAME, NODE_SERVER_PORT, BUILD_OUTPUT_PATH, PUBLIC_STATIC_ASSET_PATH, PUBLIC_STATIC_ASSET_URL or CLIENT_ENV_VARIABLES, or when package.json _moduleAliases lacks @api, @containers, @server, @config, @css or @routes. Apps created from the templates already have all of them. WEBPACK_DEV_SERVER_HOSTNAME, WEBPACK_DEV_SERVER_PORT and ANALYZE_BUNDLE are no longer read and can be removed.\n\n5. BUILD AND START: run the build, then start the server once. The build catches removed imports (they fail instead of resolving to undefined); startup catches the react-router range and the preflight. Nothing checks the Node version at startup; an old Node fails only when hooks or WebBridge is imported (step 2).\n\n6. HOOK ERROR FIELDS (not caught by the build): hook errors are now CatalystError instances. Rename reads of error.action to error.suggestedAction and error.nativeError to error.cause; error.recoverable and error.timestamp no longer exist. error.code values are registry codes (RUNTIME-NATIVE-xxx) instead of names like PERMISSION_DENIED, so replace string-literal comparisons with ERROR_CODES from catalyst-core/errors (for example ERROR_CODES.RUNTIME_NATIVE_PERMISSION_DENIED). error.category is now 'RUNTIME-NATIVE' instead of PERMISSION, NETWORK, FILE_SYSTEM and the other old values. JSON.stringify(error) no longer includes the message; log error.message. See 'CatalystError shape (1.0.0)'.\n\n7. OPTIONAL, NON-BREAKING: rename deprecated hook alias keys to the canonical ones (see 'Native hooks \u2014 aliases and deprecations'); aliases keep working until 2.0.\n\nUnchanged: route arrays, preparedRoutes, server/index.js, server/server.js and server/document.js contracts, server hooks, and WEBVIEW_CONFIG. Full guide: docs/content/12-Migration/02-Version-Upgrades.md.",
|
|
1392
|
+
"content": "Upgrade path for an application on catalyst-core 0.3.x to 1.0.0, the stable release. Read the installed catalyst-core version from package.json first. 0.3.0-beta.5 upgrades directly. 0.3.0-beta.1 to 0.3.0-beta.4 used react-router-dom 6, so those also cross React Router 6 to 7 (step 1). Older applications first move to 0.3.x (see 'Legacy 0.2.x Migration Phase 0' and 'Catalyst 0.3.x React Runtime Contract'). 1.0.0 is the API freeze; there is no 0.4.0 release. Do the steps in order.\n\n1. DEPENDENCIES: set catalyst-core to 1.0.0. Add react-router ^7.18.2 as an application dependency: it is now a peer dependency and the server exits at startup if it is missing or outside that range. Keep react and react-dom pinned to 19.0.0. TypeScript applications also need @types/react ^19.0.0. catalyst-core no longer installs react-router-dom, so change any import from 'react-router-dom' to 'react-router', except RouterProvider, which comes from 'react-router/dom' (step 3).\n\n2. NODE: run node -v. catalyst-core 1.0.0 requires >=22.12; on Node 20, importing catalyst-core/hooks or catalyst-core/WebBridge fails with ERR_REQUIRE_ESM. If the runtime is older, stop and tell the user; do not change the project's Node pin yourself.\n\n3. IMPORTS: in 1.0.0 the root entry exports exactly RouterDataProvider, useCurrentRouteData, useRouterData, MetaTag, split, hydrationReady, Head, Body (1.0.2 adds useRouterState). Move every React Router name (Link, Outlet, useParams, useNavigate, useLocation, Navigate, ...) from 'catalyst-core' to 'react-router'. In client/index.js import RouterProvider from 'react-router/dom', not 'react-router': only the /dom export passes React DOM's flushSync to the router, and the wrong one renders without any error. Replace `import split from 'catalyst-core'` with `import { split } from 'catalyst-core'`; there is no default export. Replace imports from 'catalyst-core/document' with Head and Body from 'catalyst-core'. 'catalyst-core/caching' is removed with no replacement. RouterContext, serverDataFetcher, mergeHeadElements, deleteHeadTagsByDataAttribute, getMetaData, useNavigateWithTransition and sanitizeFilePickerOptions (catalyst-core/hooks) are internal with no replacement: if the application uses one, stop and tell the user instead of inventing a substitute.\n\n4. CONFIG: catalyst start, build and serve now run a preflight and exit before starting when config/config.json lacks NODE_SERVER_HOSTNAME, NODE_SERVER_PORT, BUILD_OUTPUT_PATH, PUBLIC_STATIC_ASSET_PATH, PUBLIC_STATIC_ASSET_URL or CLIENT_ENV_VARIABLES, or when package.json _moduleAliases lacks @api, @containers, @server, @config, @css or @routes. Apps created from the templates already have all of them. WEBPACK_DEV_SERVER_HOSTNAME, WEBPACK_DEV_SERVER_PORT and ANALYZE_BUNDLE are no longer read and can be removed.\n\n5. BUILD AND START: run the build, then start the server once. The build catches removed imports (they fail instead of resolving to undefined); startup catches the react-router range and the preflight. Nothing checks the Node version at startup; an old Node fails only when hooks or WebBridge is imported (step 2).\n\n6. HOOK ERROR FIELDS (not caught by the build): hook errors are now CatalystError instances. Rename reads of error.action to error.suggestedAction and error.nativeError to error.cause; error.recoverable and error.timestamp no longer exist. error.code values are registry codes (RUNTIME-NATIVE-xxx) instead of names like PERMISSION_DENIED, so replace string-literal comparisons with ERROR_CODES from catalyst-core/errors (for example ERROR_CODES.RUNTIME_NATIVE_PERMISSION_DENIED). error.category is now 'RUNTIME-NATIVE' instead of PERMISSION, NETWORK, FILE_SYSTEM and the other old values. JSON.stringify(error) no longer includes the message; log error.message. See 'CatalystError shape (1.0.0)'.\n\n7. OPTIONAL, NON-BREAKING: rename deprecated hook alias keys to the canonical ones (see 'Native hooks \u2014 aliases and deprecations'); aliases keep working until 2.0.\n\nUnchanged: route arrays, preparedRoutes, server/index.js, server/server.js and server/document.js contracts, server hooks, and WEBVIEW_CONFIG. Full guide: docs/content/12-Migration/02-Version-Upgrades.md.",
|
|
1381
1393
|
"layer": "Framework",
|
|
1382
1394
|
"source": "static",
|
|
1383
1395
|
"tags": [
|
|
@@ -1457,5 +1469,110 @@
|
|
|
1457
1469
|
"layer": "Bridge",
|
|
1458
1470
|
"source": "static",
|
|
1459
1471
|
"tags": "ai,catalyst-ai,native ai,on-device,gemma,gemma 4,e2b,litert,litert-lm,foundation models,apple intelligence,useAI,useNativeAI"
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
"section": "config_structure",
|
|
1475
|
+
"title": "HTML Splash Screen (1.0.2)",
|
|
1476
|
+
"content": "Available from catalyst-core 1.0.2 on Android and iOS. Add public/splashscreen.html (the root of public/, not public/android or public/ios; one file shared by both platforms) to show an HTML/CSS animation while the app loads. It takes effect only when WEBVIEW_CONFIG.splashScreen is set; without that key the file is ignored.\n\nSEQUENCE: native splash image (public/<platform>/splashscreen.<ext>) \u2192 the HTML splash \u2192 the app. The HTML splash sits above the main WebView, which loads underneath, and is dismissed once the first page has finished loading and minDuration has passed. Hard cap: it is dismissed after 10 seconds even if the page is still loading.\n\nCONFIG: WEBVIEW_CONFIG.splashScreen.minDuration (ms, default 0) keeps the animation on screen at least that long. A negative or non-number value is ignored (0) with a build warning. duration is ignored in HTML mode. Example: { \"WEBVIEW_CONFIG\": { \"splashScreen\": { \"backgroundColor\": \"#ffffff\", \"minDuration\": 1200 } } }.\n\nFILE RULES: one self-contained file: inline CSS and JS, images as data: URIs. Paint your own opaque, full-screen background (the native image sits underneath). No network requests (Android blocks network loads in the splash WebView). No native bridge (no JS interfaces are exposed). Prefer CSS transform and opacity animations, since the app is loading at the same time.\n\nBUILD: iOS embeds the file in the app binary at build time and warns above 1 MB; Android copies it to assets/splash/. Rebuild the native app after editing it. Delete the file to go back to the image-only splash (the Android build also removes its copy). check_config reports the file and validates minDuration.",
|
|
1477
|
+
"layer": "Config",
|
|
1478
|
+
"source": "static",
|
|
1479
|
+
"tags": [
|
|
1480
|
+
"1.0.2",
|
|
1481
|
+
"splashscreen",
|
|
1482
|
+
"splash",
|
|
1483
|
+
"splashscreen.html",
|
|
1484
|
+
"html-splash",
|
|
1485
|
+
"animated-splash",
|
|
1486
|
+
"minDuration",
|
|
1487
|
+
"native",
|
|
1488
|
+
"android",
|
|
1489
|
+
"ios"
|
|
1490
|
+
],
|
|
1491
|
+
"github_files": [
|
|
1492
|
+
"docs/content/02-Guides and Tutorials/02-First Universal App/03-Configuration.mdx",
|
|
1493
|
+
"packages/catalyst-core/src/native/buildIos/config.js",
|
|
1494
|
+
"packages/catalyst-core/src/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt"
|
|
1495
|
+
]
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
"section": "webmcp",
|
|
1499
|
+
"title": "WebMCP (experimental, 1.0.2) \u2014 Setup",
|
|
1500
|
+
"content": "WebMCP lets a page expose tools (named actions with a JSON-schema input) to an AI agent running in the browser. Catalyst ships it from catalyst-core 1.0.2 as catalyst-core/webmcp. EXPERIMENTAL: useTool, WebMcpProvider, the route `tool` field, the webmcp:* document events and inspect/invokeTool may change in a minor release. NOT the same as this MCP server: this server helps AI coding tools understand a Catalyst project (Node, build time); WebMCP runs in the app's browser tab and exposes the app's own actions to agents.\n\nSETUP: call installShim() from catalyst-core/webmcp/shim once, before the provider mounts. Catalyst targets this opt-in DOM shim; native browser document.modelContext is intentionally unsupported for now. Then wrap the app in WebMcpProvider inside RouterDataProvider, passing the app's own route table:\n\nimport { WebMcpProvider } from 'catalyst-core/webmcp'\nimport { installShim } from 'catalyst-core/webmcp/shim'\nimport routes from './routes'\ninstallShim()\nexport const App = ({ children }) => (\n <WebMcpProvider routes={routes} filterNavigable={(route) => !route.path.startsWith('/docs/')}>{children}</WebMcpProvider>\n)\n\nfilterNavigable is optional and keeps large page sets out of the navigate tool's enum.\n\nBUILT-IN TOOLS (registered by the provider): navigate, get_current_route, get_page_info (built from the page's setMetaData). Agent side (shim): document.modelContext.getTools() and document.modelContext.executeTool(name, args).\n\nEXPORTS of catalyst-core/webmcp: WebMcpProvider, useTool, WebMcpError, WEBMCP_ERROR_CODES, inspect, invokeTool, isNative, plus helpers (flattenRoutes, declarativeToolFor, frameworkTools, toolNameFromPath, paramNames, fillPath, buildInputSchema, pageInfoTool, flattenHeadElements). catalyst-core/webmcp/shim exports installShim. A dev panel is app-level: listen for webmcp:register, webmcp:unregister, webmcp:execute:start and webmcp:execute:end on document and call inspect()/invokeTool(). Full guide: docs/content/20-WebMCP.md; example app: examples/webmcp-poc.",
|
|
1501
|
+
"layer": "Runtime",
|
|
1502
|
+
"source": "static",
|
|
1503
|
+
"tags": [
|
|
1504
|
+
"1.0.2",
|
|
1505
|
+
"webmcp",
|
|
1506
|
+
"web-mcp",
|
|
1507
|
+
"agents",
|
|
1508
|
+
"ai-agent",
|
|
1509
|
+
"modelContext",
|
|
1510
|
+
"WebMcpProvider",
|
|
1511
|
+
"installShim",
|
|
1512
|
+
"shim",
|
|
1513
|
+
"experimental",
|
|
1514
|
+
"tools"
|
|
1515
|
+
],
|
|
1516
|
+
"github_files": [
|
|
1517
|
+
"docs/content/20-WebMCP.md",
|
|
1518
|
+
"packages/catalyst-core/src/webmcp/index.js",
|
|
1519
|
+
"packages/catalyst-core/src/webmcp/shim.js"
|
|
1520
|
+
]
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
"section": "webmcp",
|
|
1524
|
+
"title": "WebMCP \u2014 Authoring Tools (route tool, useTool, errors)",
|
|
1525
|
+
"content": "Three ways to author WebMCP tools (catalyst-core 1.0.2+, experimental):\n\n1. DECLARATIVE, on the route: add `tool` to a route and Catalyst derives the name from the path (static segments joined by '_', '/' is 'home'), the input schema (path params required, `searchParams` optional) and an execute that navigates. Route tools default to annotations { readOnlyHint: true } and are available app-wide.\n{ path: '/products', component: ProductList, tool: { description: 'Browse the catalogue, optionally filtered.', searchParams: { category: { type: 'string', enum: ['footwear', 'apparel'] } } } }\n\n2. IMPERATIVE, in a component: useTool(spec) registers an action tool for the component's lifetime and removes it on unmount or when the user leaves the route. spec = { name, description, inputSchema, annotations?, execute }. Returns { status, receipt, error }; the receipt is a registration handle, not an execution result.\nuseTool({ name: 'add_to_cart', description: 'Add the current product to the cart.', inputSchema: { type: 'object', properties: { quantity: { type: 'number', minimum: 1 } } }, execute: async ({ quantity = 1 }) => { dispatch(addItem({ id, quantity })); return { added: true } } })\n\n3. STATEFUL, with deps: useTool(spec, deps) refreshes description, inputSchema, annotations and execute in place when a dep changes (for example an enum of cart item ids), so a call already in flight still completes.\n\nERRORS: throw new WebMcpError with a code from WEBMCP_ERROR_CODES (TOOL_NOT_FOUND, INVALID_ARGS, EXECUTE_FAILED, ABORTED, DUPLICATE_TOOL; values are prefixed WEBMCP_, e.g. 'WEBMCP_INVALID_ARGS'). Arguments in the schema's `required` array are checked before execute runs.\n\nRULES: do side effects inside the tool (agents ignore 'now call X' hints in results). Mark intent with annotations: { readOnlyHint: true } for reads, { consequentialHint: true } for irreversible actions such as checkout. Keep names unique: route tool names must not collide with navigate, get_current_route or get_page_info; a collision logs a warning and the later tool is dropped.",
|
|
1526
|
+
"layer": "Runtime",
|
|
1527
|
+
"source": "static",
|
|
1528
|
+
"tags": [
|
|
1529
|
+
"1.0.2",
|
|
1530
|
+
"webmcp",
|
|
1531
|
+
"useTool",
|
|
1532
|
+
"route-tool",
|
|
1533
|
+
"declarative",
|
|
1534
|
+
"WebMcpError",
|
|
1535
|
+
"WEBMCP_ERROR_CODES",
|
|
1536
|
+
"readOnlyHint",
|
|
1537
|
+
"consequentialHint",
|
|
1538
|
+
"experimental",
|
|
1539
|
+
"tools"
|
|
1540
|
+
],
|
|
1541
|
+
"github_files": [
|
|
1542
|
+
"docs/content/20-WebMCP.md",
|
|
1543
|
+
"packages/catalyst-core/src/webmcp/useTool.js",
|
|
1544
|
+
"packages/catalyst-core/src/webmcp/declarative.js",
|
|
1545
|
+
"packages/catalyst-core/src/webmcp/errors.js"
|
|
1546
|
+
]
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
"section": "version_migration",
|
|
1550
|
+
"title": "Upgrading catalyst-core 1.0.x to 1.0.2",
|
|
1551
|
+
"content": "1.0.2 is a patch release on the frozen 1.0 API; most applications need no code change. Read the installed catalyst-core version from package.json first; apps on 0.3.x follow 'Upgrading catalyst-core 0.3.x to 1.0.0' first.\n\n1. DEPENDENCIES: set catalyst-core to 1.0.2. Node stays >=22.12; react and react-dom stay 19.0.0; react-router stays ^7.18.2.\n\n2. react-helmet-async REMOVED: catalyst-core no longer depends on it. MetaTag and setMetaData work unchanged; the tags are now rendered through React 19 document metadata, and the server-rendered copies are removed once the client copies are in, so stale head tags no longer survive navigation. If the application imports react-helmet-async itself (it used to arrive transitively), add it to the application's own dependencies.\n\n3. CLI OUTPUT: start, build, serve and the native builds print a short header, one line per step and one diagnostic block on failure; --verbose and --debug show more (see 'Catalyst 0.3.x CLI Commands'). Update any script that parsed the old text output. catalyst serve now refuses a stale build (PREFLIGHT-022): after changing PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH or CLIENT_ENV_VARIABLES values, rebuild before serving. Run catalyst build once after upgrading so serve can run that check.\n\n4. FIXES, nothing to do: Fast Refresh works for JSX in .js files; Otel.init() no longer throws without configureLogger() (falls back to console); Head and Body no longer import prop-types at runtime, so apps that do not install prop-types no longer crash in SSR.\n\n5. NEW, OPT-IN: an animated HTML splash via public/splashscreen.html plus WEBVIEW_CONFIG.splashScreen.minDuration ('HTML Splash Screen (1.0.2)'); WebMCP via catalyst-core/webmcp, experimental ('WebMCP (experimental, 1.0.2) \u2014 Setup').\n\n6. NEW APPS ONLY: apps created by the create-catalyst-app released with catalyst-core 1.0.2 declare \"type\": \"module\" in package.json, and the Tailwind template ships postcss.config.cjs. catalyst-core reads postcss.config.cjs or postcss.config.js, so existing apps need no change. create-catalyst-app runs on Node >=20.12. NEW ROOT EXPORT: useRouterState is added to the root entry (now nine names); existing imports are unaffected.",
|
|
1552
|
+
"layer": "Framework",
|
|
1553
|
+
"source": "static",
|
|
1554
|
+
"tags": [
|
|
1555
|
+
"migration",
|
|
1556
|
+
"upgrade",
|
|
1557
|
+
"update",
|
|
1558
|
+
"bump",
|
|
1559
|
+
"1.0.2",
|
|
1560
|
+
"1.0.1",
|
|
1561
|
+
"1.0.x",
|
|
1562
|
+
"patch",
|
|
1563
|
+
"react-helmet-async",
|
|
1564
|
+
"helmet",
|
|
1565
|
+
"verbose",
|
|
1566
|
+
"stale-build",
|
|
1567
|
+
"PREFLIGHT-022",
|
|
1568
|
+
"splashscreen.html",
|
|
1569
|
+
"webmcp",
|
|
1570
|
+
"latest",
|
|
1571
|
+
"useRouterState"
|
|
1572
|
+
],
|
|
1573
|
+
"github_files": [
|
|
1574
|
+
"packages/catalyst-core/changelog.md",
|
|
1575
|
+
"docs/content/12-Migration/02-Version-Upgrades.md"
|
|
1576
|
+
]
|
|
1460
1577
|
}
|
|
1461
1578
|
]
|
|
@@ -50,3 +50,62 @@ describe("check_config: on-device AI", () => {
|
|
|
50
50
|
expect(aiFields(r)).toEqual([])
|
|
51
51
|
})
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
function makeSplashProject(config: unknown, files: string[] = []): string {
|
|
55
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-cfg-"))
|
|
56
|
+
fs.mkdirSync(path.join(root, "config"))
|
|
57
|
+
fs.writeFileSync(path.join(root, "config", "config.json"), JSON.stringify(config))
|
|
58
|
+
for (const f of files) {
|
|
59
|
+
fs.mkdirSync(path.dirname(path.join(root, f)), { recursive: true })
|
|
60
|
+
fs.writeFileSync(path.join(root, f), "")
|
|
61
|
+
}
|
|
62
|
+
return root
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const findings = (r: ReturnType<typeof handle_check_config>) => [...r.issues, ...r.warnings, ...r.passed]
|
|
66
|
+
|
|
67
|
+
describe("check_config: splash screen", () => {
|
|
68
|
+
it("reads splashScreen from WEBVIEW_CONFIG and treats missing images as fine", () => {
|
|
69
|
+
const r = handle_check_config({
|
|
70
|
+
project_path: makeSplashProject({
|
|
71
|
+
WEBVIEW_CONFIG: { splashScreen: { backgroundColor: "#fff" } },
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
expect(r.passed.some((p) => p.field === "WEBVIEW_CONFIG.splashScreen")).toBe(true)
|
|
75
|
+
expect(r.issues.some((i) => /splash/i.test(i.field))).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("flags a top-level splashScreen as misplaced", () => {
|
|
79
|
+
const r = handle_check_config({
|
|
80
|
+
project_path: makeSplashProject({ WEBVIEW_CONFIG: {}, splashScreen: { duration: 1000 } }),
|
|
81
|
+
})
|
|
82
|
+
expect(
|
|
83
|
+
r.warnings.some((w) => w.field === "WEBVIEW_CONFIG.splashScreen" && /top level/.test(w.message!))
|
|
84
|
+
).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("recognises public/splashscreen.html", () => {
|
|
88
|
+
const r = handle_check_config({
|
|
89
|
+
project_path: makeSplashProject({ WEBVIEW_CONFIG: { splashScreen: { minDuration: 1200 } } }, [
|
|
90
|
+
"public/splashscreen.html",
|
|
91
|
+
]),
|
|
92
|
+
})
|
|
93
|
+
expect(r.passed.some((p) => p.field === "public/splashscreen.html")).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("warns on an invalid minDuration", () => {
|
|
97
|
+
for (const minDuration of [-1, "1200"]) {
|
|
98
|
+
const r = handle_check_config({
|
|
99
|
+
project_path: makeSplashProject({ WEBVIEW_CONFIG: { splashScreen: { minDuration } } }),
|
|
100
|
+
})
|
|
101
|
+
expect(r.warnings.some((w) => w.field === "WEBVIEW_CONFIG.splashScreen.minDuration")).toBe(true)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("accepts a valid minDuration silently", () => {
|
|
106
|
+
const r = handle_check_config({
|
|
107
|
+
project_path: makeSplashProject({ WEBVIEW_CONFIG: { splashScreen: { minDuration: 0 } } }),
|
|
108
|
+
})
|
|
109
|
+
expect(findings(r).some((f) => f.field === "WEBVIEW_CONFIG.splashScreen.minDuration")).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
})
|
package/mcp_v2/tools/build.js
CHANGED
|
@@ -23,7 +23,7 @@ const MASTER_FLOWS = {
|
|
|
23
23
|
step: 1,
|
|
24
24
|
cmd: "catalyst start",
|
|
25
25
|
label: "Start dev server",
|
|
26
|
-
detail: "Starts the Vite middleware-mode development server and Catalyst SSR server on NODE_SERVER_PORT. Hot reload and React Fast Refresh are enabled.",
|
|
26
|
+
detail: "Starts the Vite middleware-mode development server and Catalyst SSR server on NODE_SERVER_PORT. Hot reload and React Fast Refresh are enabled, including for JSX in .js files (1.0.2+). Since 1.0.2 the terminal shows a short header and one line per step; add --verbose for every step and the full tool output, or --debug for stacks and the environment.",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
step: 2,
|
|
@@ -38,13 +38,13 @@ const MASTER_FLOWS = {
|
|
|
38
38
|
step: 1,
|
|
39
39
|
cmd: "catalyst build",
|
|
40
40
|
label: "Production build",
|
|
41
|
-
detail: "Runs parallel Vite client and SSR builds. Outputs client assets to build/client, the renderer to build/server, Vite manifests under build/.vite, and offline runtime files at the build root.",
|
|
41
|
+
detail: "Runs parallel Vite client and SSR builds. Outputs client assets to build/client, the renderer to build/server, Vite manifests under build/.vite, and offline runtime files at the build root. Since 1.0.2 it prints one line per step with timing and, on failure, the extracted errors with file, line and a hint; --verbose or --debug add the error code (BUNDLE-000), its docs link and the bundler's full output. It also writes <BUILD_OUTPUT_PATH>/.catalyst-build.json with the config values inlined into the bundles.",
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
step: 2,
|
|
45
45
|
cmd: "catalyst serve",
|
|
46
46
|
label: "Serve built app (local)",
|
|
47
|
-
detail: "Serves from build/ — same as production but local. Use for smoke-testing the build before deploy.",
|
|
47
|
+
detail: "Serves from build/ — same as production but local. Use for smoke-testing the build before deploy. Since 1.0.2 it exits if there is no build, and refuses a stale build (PREFLIGHT-022) when PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH or a CLIENT_ENV_VARIABLES value changed in config/config.json after the build; rebuild, then serve. The app's own output passes through unchanged.",
|
|
48
48
|
},
|
|
49
49
|
],
|
|
50
50
|
|
|
@@ -101,7 +101,7 @@ const MASTER_FLOWS = {
|
|
|
101
101
|
step: 3,
|
|
102
102
|
cmd: "npm run buildApp:android",
|
|
103
103
|
label: "Android debug build",
|
|
104
|
-
detail: "Runs buildAppAndroid.js: validates ADB + emulator path from sdkPath, launches emulator (emulatorName), copies web assets to androidProject/, runs Gradle debug build, installs APK.",
|
|
104
|
+
detail: "Runs buildAppAndroid.js: validates ADB + emulator path from sdkPath, launches emulator (emulatorName), copies web assets to androidProject/, runs Gradle debug build, installs APK. Since 1.0.2 Gradle runs behind one progress line that shows the current task; on failure --verbose or --debug add the ANDROID-000 report with the full Gradle output.",
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
step: 4,
|
|
@@ -301,7 +301,7 @@ const MASTER_DIAGRAMS = {
|
|
|
301
301
|
"LOCAL_IP — override auto-detected IP for device builds",
|
|
302
302
|
"accessControl.allowedUrls — whitelist for outbound requests from WebView",
|
|
303
303
|
"googleSignIn.enabled / clientId / iosClientId",
|
|
304
|
-
"splashScreen — duration, backgroundColor,
|
|
304
|
+
"splashScreen — duration, minDuration, backgroundColor, image size; optional animated public/splashscreen.html (1.0.2+)",
|
|
305
305
|
"notifications.enabled — push notification permission prompt",
|
|
306
306
|
"android.buildOptimisation — enables build caching",
|
|
307
307
|
"android.keystoreConfig — signing for release builds",
|
|
@@ -760,7 +760,7 @@ function handle_get_architecture_diagram({ feature, symptom } = {}) {
|
|
|
760
760
|
project_context.push(`iOS configured: ${ctx.hasIos}`)
|
|
761
761
|
if (ctx.hasGoogleSignIn) project_context.push("googleSignIn: enabled")
|
|
762
762
|
const { readJson } = makeProjectHelpers(catalystRoot.dir)
|
|
763
|
-
const splash = (readJson("config/config.json") || {}).splashScreen
|
|
763
|
+
const splash = (readJson("config/config.json") || {}).WEBVIEW_CONFIG?.splashScreen
|
|
764
764
|
if (splash)
|
|
765
765
|
project_context.push(`splashScreen: configured (duration: ${splash.duration || "default"})`)
|
|
766
766
|
}
|
package/mcp_v2/tools/config.js
CHANGED
|
@@ -389,28 +389,45 @@ function handle_check_config({ project_path, platform = "both" } = {}) {
|
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
// ── splashScreen (
|
|
393
|
-
|
|
392
|
+
// ── splashScreen (inside WEBVIEW_CONFIG; the native builds read only that) ──
|
|
393
|
+
const splash = wc.splashScreen
|
|
394
|
+
const hasSplashHtml = fs.existsSync(path.join(root, "public", "splashscreen.html"))
|
|
395
|
+
if (!splash) {
|
|
394
396
|
warnings.push({
|
|
395
|
-
field: "splashScreen",
|
|
396
|
-
message:
|
|
397
|
-
|
|
397
|
+
field: "WEBVIEW_CONFIG.splashScreen",
|
|
398
|
+
message: config.splashScreen
|
|
399
|
+
? "splashScreen is at the top level of config/config.json, where the native builds never read it. Move it into WEBVIEW_CONFIG.splashScreen."
|
|
400
|
+
: `WEBVIEW_CONFIG.splashScreen not set, so the native splash screen is off${hasSplashHtml ? " and public/splashscreen.html is ignored" : ""}. Add WEBVIEW_CONFIG.splashScreen (e.g. { "backgroundColor": "#ffffff" }) to turn it on.`,
|
|
398
401
|
})
|
|
399
402
|
} else {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
403
|
+
// Images are optional: Android falls back to the launcher icon, iOS to a loader.
|
|
404
|
+
const found = (dir, exts) =>
|
|
405
|
+
exts
|
|
406
|
+
.map((ext) => `public/${dir}/splashscreen.${ext}`)
|
|
407
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the local project root; f comes from a fixed candidate list.
|
|
408
|
+
.find((f) => fs.existsSync(path.join(root, f)))
|
|
409
|
+
const androidImage = found("android", ["png", "jpg", "jpeg", "gif", "bmp", "webp"])
|
|
410
|
+
const iosImage = found("ios", ["png", "jpg", "jpeg"])
|
|
411
|
+
passed.push({
|
|
412
|
+
field: "WEBVIEW_CONFIG.splashScreen",
|
|
413
|
+
note: `android image: ${androidImage || "none (launcher icon)"}, ios image: ${iosImage || "none (loader)"}`,
|
|
414
|
+
})
|
|
415
|
+
// Same rule as the iOS build (buildIos/config.js): anything else is ignored with a warning.
|
|
416
|
+
const { minDuration } = splash
|
|
417
|
+
if (
|
|
418
|
+
minDuration !== undefined &&
|
|
419
|
+
!(typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration >= 0)
|
|
420
|
+
) {
|
|
421
|
+
warnings.push({
|
|
422
|
+
field: "WEBVIEW_CONFIG.splashScreen.minDuration",
|
|
423
|
+
message: `splashScreen.minDuration must be a non-negative number of milliseconds. Got ${JSON.stringify(minDuration)}; the build ignores it and uses 0.`,
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
if (hasSplashHtml) {
|
|
427
|
+
passed.push({
|
|
428
|
+
field: "public/splashscreen.html",
|
|
429
|
+
note: `HTML splash on (catalyst-core 1.0.2+): shown after the native image until the first page loads, for at least minDuration (${minDuration || 0} ms), at most 10 s; duration is ignored`,
|
|
411
430
|
})
|
|
412
|
-
} else {
|
|
413
|
-
passed.push({ field: "splashScreen", note: "config and both asset files present" })
|
|
414
431
|
}
|
|
415
432
|
}
|
|
416
433
|
|
|
@@ -390,23 +390,17 @@ function handle_get_conversion_status({ project_path, include_not_applicable = f
|
|
|
390
390
|
native_risk: "No config — splash screen config cannot be read.",
|
|
391
391
|
reason: "config/config.json not found",
|
|
392
392
|
}
|
|
393
|
-
if (!
|
|
393
|
+
if (!webviewConfig || !webviewConfig.splashScreen)
|
|
394
394
|
return {
|
|
395
395
|
status: "gap",
|
|
396
396
|
native_risk:
|
|
397
|
-
"Without splashScreen
|
|
398
|
-
reason:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
(f) => !fileExists(f)
|
|
402
|
-
)
|
|
403
|
-
if (missing.length)
|
|
404
|
-
return {
|
|
405
|
-
status: "gap",
|
|
406
|
-
native_risk:
|
|
407
|
-
"Splash config exists but asset files missing — build will fail when packaging the native app.",
|
|
408
|
-
reason: `Splash asset files missing: ${missing.join(", ")}`,
|
|
397
|
+
"Without WEBVIEW_CONFIG.splashScreen the Catalyst splash is skipped and the app goes straight to the WebView while JS loads. Not a crash, but poor UX on slow devices.",
|
|
398
|
+
reason: config.splashScreen
|
|
399
|
+
? "splashScreen is at the top level of config/config.json; the native builds read only WEBVIEW_CONFIG.splashScreen"
|
|
400
|
+
: "WEBVIEW_CONFIG.splashScreen missing from config/config.json",
|
|
409
401
|
}
|
|
402
|
+
// Images are optional (Android falls back to the launcher icon, iOS to a loader), so a
|
|
403
|
+
// missing image is not a gap.
|
|
410
404
|
return { status: "completed" }
|
|
411
405
|
},
|
|
412
406
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "catalyst-core",
|
|
3
|
-
"version": "1.0.2
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.jsx",
|
|
6
6
|
"description": "Web framework that provides great performance out of the box",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
"react-redux": "^9.2.0",
|
|
84
84
|
"react-refresh": "^0.14.0",
|
|
85
85
|
"redux": "^4.2.1",
|
|
86
|
-
"sanitize-html": "^2.
|
|
86
|
+
"sanitize-html": "^2.18.1",
|
|
87
87
|
"sass": "^1.89.2",
|
|
88
88
|
"ua-parser-js": "^1.0.37",
|
|
89
|
-
"vite": "^6.4.
|
|
89
|
+
"vite": "^6.4.5",
|
|
90
90
|
"vite-plugin-svgr": "^5.0.0",
|
|
91
91
|
"winston": "^3.11.0",
|
|
92
92
|
"winston-daily-rotate-file": "^5.0.0"
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"eslint-plugin-security": "^3.0.0",
|
|
126
126
|
"husky": "^9.0.11",
|
|
127
127
|
"jsdom": "30.0.1",
|
|
128
|
-
"lint-staged": "^
|
|
128
|
+
"lint-staged": "^17.6.0",
|
|
129
129
|
"prettier": "^3.2.5",
|
|
130
130
|
"react-router": "^7.18.2",
|
|
131
131
|
"typescript": "^7.0.2",
|