catalyst-core 0.3.0-beta.1 → 0.3.0-beta.3

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.
Files changed (33) hide show
  1. package/dist/globals.d.ts +7 -0
  2. package/dist/native/androidProject/app/build.gradle.kts +45 -4
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +86 -0
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -0
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +173 -13
  6. package/dist/native/androidProject/settings.gradle.kts +10 -0
  7. package/dist/native/androidSetup.js +8 -6
  8. package/dist/native/bridge/constants/NativeInterfaces.js +15 -1
  9. package/dist/native/bridge/hooks/index.js +7 -0
  10. package/dist/native/bridge/hooks/useAI.js +94 -0
  11. package/dist/native/buildAndroid/assets.js +1 -1
  12. package/dist/native/buildAndroid/index.js +32 -0
  13. package/dist/native/buildIos/assets.js +1 -1
  14. package/dist/offline/catalyst-sw.js +1 -0
  15. package/dist/scripts/build.js +15 -11
  16. package/dist/scripts/generateOfflineManifest.js +1 -0
  17. package/dist/scripts/scriptUtils.js +1 -0
  18. package/dist/scripts/serve.js +24 -22
  19. package/dist/scripts/start.js +1 -3
  20. package/dist/server/expressServer.js +47 -0
  21. package/dist/server/renderer/document/Head.jsx +2 -0
  22. package/dist/server/renderer/extract.js +1 -0
  23. package/dist/server/renderer/handler.jsx +29 -11
  24. package/dist/vite/inject-cache-key-plugin.js +7 -0
  25. package/dist/vite/loadCustomViteConfig.js +1 -0
  26. package/dist/vite/manifest-categorization-plugin.js +2 -0
  27. package/dist/vite/node-loader.mjs +6 -0
  28. package/dist/vite/vite.config.js +83 -1
  29. package/mcp_v2/knowledge-base.json +52 -4
  30. package/mcp_v2/mcp.js +36 -4
  31. package/mcp_v2/tools/build.js +46 -18
  32. package/mcp_v2/tools/github.js +11 -0
  33. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  import { defineConfig, transformWithEsbuild } from "vite"
2
2
  import react from "@vitejs/plugin-react"
3
3
  import svgr from "vite-plugin-svgr"
4
- import { builtinModules } from "node:module"
4
+ import { builtinModules, createRequire } from "node:module"
5
5
 
6
6
  /**
7
7
  * Treat all .scss imports as CSS modules (webpack compat).
@@ -113,6 +113,7 @@ try {
113
113
  const packageJson = JSON.parse(packageJsonContent)
114
114
  _moduleAliases = packageJson._moduleAliases || {}
115
115
  } catch (error) {
116
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - packageJsonConfig is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
116
117
  console.warn(`Failed to read or parse package.json from ${packageJsonConfig}:`, error.message)
117
118
  }
118
119
 
@@ -122,6 +123,7 @@ try {
122
123
  catalyst_moduleAliases = catalystPackageJson._moduleAliases || {}
123
124
  } catch (error) {
124
125
  console.warn(
126
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - catalystPackageJsonConfig is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
125
127
  `Failed to read or parse catalyst package.json from ${catalystPackageJsonConfig}:`,
126
128
  error.message
127
129
  )
@@ -131,6 +133,52 @@ const allAliases = { ..._moduleAliases, ...catalyst_moduleAliases }
131
133
 
132
134
  import { imageUrl, fontUrl } from "./scssParams.js"
133
135
 
136
+ // Tailwind v4's zero-config scanner (`@tailwindcss/postcss`) walks up from
137
+ // `dirname(from)` — the directory of the file PostCSS is currently compiling —
138
+ // unless given an explicit `base`. Vite passes each CSS file's own resolved
139
+ // path as `from`, so for catalyst-core's conventional layout (global
140
+ // stylesheet under `src/static/css`, components under `src/js`, no shared
141
+ // parent short of the app root) the scanner never reaches component files and
142
+ // silently drops every class actually used in JSX. Webpack's postcss-loader
143
+ // doesn't populate `from` the same way, which is why this only regresses
144
+ // under Vite. We resolve the app's own postcss.config.js ourselves and patch
145
+ // in `base: process.env.src_path` so the scan root covers the whole app.
146
+ // Only the `{ pluginName: options }` shorthand can be patched this way —
147
+ // array-form configs (`[require(...)()]`) have already closed over their
148
+ // options — so anything else is left alone and Vite falls back to its normal
149
+ // auto-loaded postcss.config.js resolution.
150
+ const resolvePostcssPlugins = () => {
151
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
152
+ const configPath = path.join(process.env.src_path, "postcss.config.js")
153
+ if (!fs.existsSync(configPath)) return null
154
+
155
+ const appRequire = createRequire(configPath)
156
+
157
+ try {
158
+ const userConfig = appRequire(configPath)
159
+ const pluginsConfig = userConfig?.plugins
160
+
161
+ if (!pluginsConfig || Array.isArray(pluginsConfig) || typeof pluginsConfig !== "object") {
162
+ return null
163
+ }
164
+
165
+ return Object.entries(pluginsConfig).map(([name, options]) => {
166
+ const plugin = appRequire(name)
167
+ const finalOptions =
168
+ name === "@tailwindcss/postcss" && (!options || options.base === undefined)
169
+ ? { ...options, base: process.env.src_path }
170
+ : options
171
+ return plugin(finalOptions)
172
+ })
173
+ } catch (error) {
174
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - configPath is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
175
+ console.warn(`Failed to patch postcss config at ${configPath}:`, error.message)
176
+ return null
177
+ }
178
+ }
179
+
180
+ const postcssPlugins = resolvePostcssPlugins()
181
+
134
182
  const alias = () => {
135
183
  if (!allAliases || typeof allAliases !== "object") {
136
184
  return {}
@@ -139,9 +187,11 @@ const alias = () => {
139
187
  return Object.keys(allAliases).reduce((moduleEnvMap, alias) => {
140
188
  if (allAliases[alias] && typeof allAliases[alias] === "string") {
141
189
  try {
190
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root and allAliases comes from the developer's own package.json _moduleAliases config, not request/user input.
142
191
  const aliasPath = path.join(process.env.src_path, ...allAliases[alias].split("/"))
143
192
  moduleEnvMap[alias] = aliasPath
144
193
  } catch (error) {
194
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json _moduleAliases config, not attacker-controlled; console.warn here does no printf-style substitution.
145
195
  console.warn(`Failed to configure alias ${alias}:`, error.message)
146
196
  }
147
197
  }
@@ -177,6 +227,37 @@ export const getClientEnvVariables = () => {
177
227
  process.env["PUBLIC_STATIC_ASSET_URL"]
178
228
  )
179
229
 
230
+ // Derive AI_PUBLIC_CONFIG: only the browser block from AI_CONFIG, no API keys
231
+ let aiConfig = {}
232
+ try {
233
+ const parsedConfig = JSON.parse(process.env.AI_CONFIG || "{}")
234
+ if (parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig)) {
235
+ aiConfig = parsedConfig
236
+ } else {
237
+ console.warn("[catalyst-core] AI_CONFIG must be a JSON object, ignoring it")
238
+ }
239
+ } catch (e) {
240
+ console.warn(`[catalyst-core] Invalid AI_CONFIG JSON, ignoring: ${e.message}`)
241
+ }
242
+ const aiPublicConfig = aiConfig.browser ? { browser: aiConfig.browser } : {}
243
+ envVarDefinitions[`process.env.AI_PUBLIC_CONFIG`] = JSON.stringify(JSON.stringify(aiPublicConfig))
244
+
245
+ // Detect which catalyst-* AI packages are installed for the app, including
246
+ // hoisted/workspace installs — not just a direct app-local node_modules copy.
247
+ // Results are injected as __CATALYST_PACKAGES__.* literals so dead code
248
+ // referencing missing packages can be branched around at runtime.
249
+ const appRequire = createRequire(path.join(process.env.src_path || process.cwd(), "package.json"))
250
+ let aiPackageInstalled = false
251
+ try {
252
+ appRequire.resolve("catalyst-ai")
253
+ aiPackageInstalled = true
254
+ } catch {
255
+ aiPackageInstalled = false
256
+ }
257
+ envVarDefinitions["__CATALYST_PACKAGES__"] = JSON.stringify({
258
+ ai: aiPackageInstalled,
259
+ })
260
+
180
261
  return envVarDefinitions
181
262
  }
182
263
 
@@ -377,6 +458,7 @@ export default defineConfig({
377
458
  localsConvention: "camelCase",
378
459
  generateScopedName: "[name]__[local]___[hash:base64:5]",
379
460
  },
461
+ ...(postcssPlugins ? { postcss: { plugins: postcssPlugins } } : {}),
380
462
  preprocessorOptions: {
381
463
  scss: {
382
464
  additionalData: (content) =>
@@ -170,15 +170,63 @@
170
170
  {
171
171
  "section": "build_system",
172
172
  "title": "iOS Build Pipeline",
173
- "content": "1) Generate ConfigConstants.swift from WEBVIEW_CONFIG 2) Launch simulator 3) Clean Xcode project 4) Compile with Xcode 5) Install app + launch. Located in: src/native/iosnativeWebView/",
173
+ "content": "npm run buildApp:ios generates ConfigConstants.swift and plugin metadata from WEBVIEW_CONFIG, syncs selected public/ios assets into the package-owned Xcode project, detects a physical device or launches a simulator, cleans Xcode artifacts, compiles with the Debug configuration, and installs and launches the .app. It does not bundle the full Catalyst web client and does not create an App Store archive. The generated project is located under the resolved catalyst-core package at dist/native/iosnativeWebView/iosnativeWebView.xcodeproj.",
174
174
  "layer": "Build",
175
175
  "source": "static",
176
- "tags": ["ios", "build"]
176
+ "tags": ["ios", "build", "buildApp:ios", "debug", "xcode"],
177
+ "github_files": [
178
+ "packages/catalyst-core/src/native/buildAppIos.js",
179
+ "packages/catalyst-core/src/native/buildIos/build.js"
180
+ ]
181
+ },
182
+ {
183
+ "section": "build_system",
184
+ "title": "Catalyst iOS Production Release Flow (0.2.x and 0.3.x)",
185
+ "content": "Catalyst 0.2.x and 0.3.x use the same iOS production-release flow: (1) deploy the production web application and verify its HTTPS URL, (2) finalize WEBVIEW_CONFIG, bundle ID, app version/build, plugins, permissions, capabilities, icons, splash assets, access-control rules, and privacy declarations, (3) run npm run buildApp:ios from the consuming app root to prepare the package-owned native project and perform its Debug build, (4) open the resolved catalyst-core package's dist/native/iosnativeWebView/iosnativeWebView.xcodeproj, (5) confirm the iosnativeWebView scheme's Archive action uses Release, configure Release signing and capabilities, choose a generic iOS device destination, and run Product -> Archive, (6) Validate App and upload with Distribute App -> TestFlight & App Store, then (7) finish TestFlight testing, App Store Connect metadata, build selection, and App Review submission. buildApp:ios does not create the final App Store artifact.",
186
+ "layer": "Build",
187
+ "source": "static",
188
+ "tags": [
189
+ "ios",
190
+ "release",
191
+ "production",
192
+ "0.2.x",
193
+ "0.3.x",
194
+ "buildApp:ios",
195
+ "xcode",
196
+ "archive",
197
+ "app-store",
198
+ "testflight"
199
+ ],
200
+ "github_files": [
201
+ "docs/content/08-Deployment/03-iOS-Production-Release.md",
202
+ "packages/catalyst-core/src/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme"
203
+ ]
204
+ },
205
+ {
206
+ "section": "build_system",
207
+ "title": "Generated iOS Project Signing and Persistence",
208
+ "content": "The generated Xcode project is package-owned state. Its shared xcconfig is regenerated with development-oriented code-signing defaults, and the bundled push entitlement can use aps-environment=development. Before an App Store archive, review the Release team, distribution signing, provisioning, capabilities, and entitlements in Xcode. Run the final npm run buildApp:ios before release-only Xcode changes: later Catalyst generation, package upgrades, or dependency installation can replace or regenerate native config constants, shared Xcode configuration, plist and entitlement content, generated plugin code/resources, selected project-file entries, and assets. Do not treat manual edits inside the generated project as durable app configuration.",
209
+ "layer": "Build",
210
+ "source": "static",
211
+ "tags": [
212
+ "ios",
213
+ "release",
214
+ "signing",
215
+ "provisioning",
216
+ "entitlements",
217
+ "push-notifications",
218
+ "generated-project",
219
+ "regeneration"
220
+ ],
221
+ "github_files": [
222
+ "packages/catalyst-core/src/native/buildIos/config.js",
223
+ "packages/catalyst-core/src/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements"
224
+ ]
177
225
  },
178
226
  {
179
227
  "section": "build_system",
180
228
  "title": "Legacy 0.2.x Build NPM Scripts",
181
- "content": "All catalyst npm scripts to add to package.json scripts block (each maps to: catalyst <scriptName>): devBuild \u2014 dev web build. devServe \u2014 dev server. buildApp \u2014 builds native app (both platforms). buildApp:android \u2014 Android native app build. buildApp:ios \u2014 iOS native app build. buildApp:android:release \u2014 Android release build. buildApp:ios:release \u2014 iOS release build. setupEmulator \u2014 sets up both emulators. setupEmulator:android \u2014 sets up Android AVD. setupEmulator:ios \u2014 sets up iOS simulator. NOTE: build:android / build:android:release / build:ios do NOT exist in catalyst \u2014 common mistake. The correct commands are buildApp:android and buildApp:ios. Missing any script = that npm run command fails.",
229
+ "content": "All catalyst npm scripts to add to package.json scripts block (each maps to: catalyst <scriptName>): devBuild \u2014 dev web build. devServe \u2014 dev server. buildApp \u2014 builds native apps. buildApp:android \u2014 Android native app build. buildApp:ios \u2014 iOS native project preparation and Debug build. buildApp:android:release \u2014 Android release build. setupEmulator \u2014 sets up both emulators. setupEmulator:android \u2014 sets up Android AVD. setupEmulator:ios \u2014 sets up iOS simulator. There is no separate buildApp:ios:release flow: run buildApp:ios, then create the production archive from the generated Xcode project's Release Archive action. NOTE: build:android / build:android:release / build:ios do NOT exist in catalyst \u2014 common mistake. Missing any script = that npm run command fails.",
182
230
  "layer": "Build",
183
231
  "source": "static",
184
232
  "tags": ["build", "scripts", "package.json", "native"]
@@ -434,7 +482,7 @@
434
482
  {
435
483
  "section": "config_structure",
436
484
  "title": "WEBVIEW_CONFIG Required Fields",
437
- "content": "WEBVIEW_CONFIG field reference (from buildAppAndroid.js + buildAppIos.js source):\n\nTOP-LEVEL (both platforms):\n- port (string, required) \u2014 web server port e.g. \"3005\"\n- LOCAL_IP (string, required) \u2014 machine LAN IP e.g. \"192.168.0.11\". NOT localhost \u2014 emulator cannot resolve it\n- appInfo (string, required) \u2014 build identifier e.g. \"android-5Feb2026-v2.1.0\". Missing = iOS build breaks\n- useHttps (boolean, optional) \u2014 default false\n- cachePattern (string, optional) \u2014 comma-separated globs e.g. \"*.css\"\n- accessControl.enabled (boolean, optional) \u2014 default false\n- accessControl.allowedUrls (array, required if enabled) \u2014 URL patterns\n\nANDROID (WEBVIEW_CONFIG.android):\n- sdkPath (string, REQUIRED) \u2014 abs path to Android SDK. Build exits immediately without it\n- emulatorName (string, REQUIRED for debug) \u2014 AVD name. Run: emulator -list-avds\n- appName (string, optional) \u2014 default \"Catalyst Application\"\n- packageName (string, optional) \u2014 e.g. com.company.app. Derived from appName if missing\n- buildType (string, optional) \u2014 \"debug\" or \"release\". Default \"debug\"\n- keystore or keystoreConfig (required for release buildType only)\n- buildOptimisation (boolean, optional) \u2014 default false\n- cachePattern (string, optional)\n\nIOS (WEBVIEW_CONFIG.ios):\n- appBundleId (string, optional) \u2014 default \"com.debug.webview\". Must set for distribution\n- appName (string, optional) \u2014 default \"Catalyst Application\"\n- simulatorName (string, optional) \u2014 auto-detected if missing. e.g. \"iPhone 17 Pro\"\n- buildType (string, optional, CASE-SENSITIVE) \u2014 \"Debug\" or \"Release\". NOT lowercase \u2014 common mistake\n- deviceUDID (string, optional) \u2014 physical device builds only\n- developmentTeam (string, required if deviceUDID set) \u2014 Apple Developer Team ID\n- cachePattern (string, optional)",
485
+ "content": "WEBVIEW_CONFIG field reference (from buildAppAndroid.js + buildAppIos.js source):\n\nTOP-LEVEL (both platforms):\n- port (string, required) \u2014 web server port e.g. \"3005\"\n- LOCAL_IP (string, required) \u2014 machine LAN IP e.g. \"192.168.0.11\". NOT localhost \u2014 emulator cannot resolve it\n- appInfo (string, required) \u2014 build identifier e.g. \"android-5Feb2026-v2.1.0\". Missing = iOS build breaks\n- useHttps (boolean, optional) \u2014 default false\n- cachePattern (string, optional) \u2014 comma-separated globs e.g. \"*.css\"\n- accessControl.enabled (boolean, optional) \u2014 default false\n- accessControl.allowedUrls (array, required if enabled) \u2014 URL patterns\n\nANDROID (WEBVIEW_CONFIG.android):\n- sdkPath (string, REQUIRED) \u2014 abs path to Android SDK. Build exits immediately without it\n- emulatorName (string, REQUIRED for debug) \u2014 AVD name. Run: emulator -list-avds\n- appName (string, optional) \u2014 default \"Catalyst Application\"\n- packageName (string, optional) \u2014 e.g. com.company.app. Derived from appName if missing\n- buildType (string, optional) \u2014 \"debug\" or \"release\". Default \"debug\"\n- keystore or keystoreConfig (required for release buildType only)\n- buildOptimisation (boolean, optional) \u2014 default false\n- cachePattern (string, optional)\n\nIOS (WEBVIEW_CONFIG.ios):\n- appBundleId (string, optional) \u2014 default \"com.debug.webview\". Must set for distribution\n- appName (string, optional) \u2014 default \"Catalyst Application\"\n- simulatorName (string, optional) \u2014 auto-detected if missing. e.g. \"iPhone 17 Pro\"\n- buildType (string, optional) \u2014 may label copied Debug output but does not select the Xcode build configuration or create an App Store archive\n- deviceUDID (string, optional) \u2014 physical device builds only\n- developmentTeam (string, required if deviceUDID set) \u2014 Apple Developer Team ID\n- cachePattern (string, optional)\n\nFor iOS production, run buildApp:ios to prepare the generated project, then archive the iosnativeWebView scheme using its Release Archive action in Xcode.",
438
486
  "layer": "Config",
439
487
  "source": "static",
440
488
  "tags": ["config", "WEBVIEW_CONFIG", "android", "ios", "required", "fields"]
package/mcp_v2/mcp.js CHANGED
@@ -62,21 +62,53 @@ if (projectInfo.notInstalled) {
62
62
  process.exit(1)
63
63
  }
64
64
 
65
- if (!fs.existsSync(DB_PATH)) {
65
+ const dbMissing = !fs.existsSync(DB_PATH)
66
+ const dbEmpty = !dbMissing && fs.statSync(DB_PATH).size === 0
67
+
68
+ if (dbMissing || dbEmpty) {
69
+ try {
70
+ require("./setup")
71
+ } catch (setupErr) {
72
+ process.stderr.write(
73
+ JSON.stringify({
74
+ jsonrpc: "2.0",
75
+ error: {
76
+ code: -32000,
77
+ message: `context.db ${dbEmpty ? "is empty" : "not found"} at ${DB_PATH}. Auto-setup failed: ${setupErr.message}. Run manually: node ${path.join(MCP_DIR, "setup.js")}`,
78
+ },
79
+ id: null,
80
+ }) + "\n"
81
+ )
82
+ // process.exit(1) instead of exitCode+return: this is top-level script code
83
+ // (not inside a function), and ESLint here rejects top-level return even
84
+ // though Node's CommonJS wrapper allows it. Matches the exit pattern used
85
+ // by the other startup-validation failures above (L49, L62).
86
+ process.exit(1)
87
+ }
88
+ }
89
+
90
+ let db
91
+ try {
92
+ db = new Database(DB_PATH, { readonly: false })
93
+ } catch (e) {
94
+ const isVersionMismatch =
95
+ e.code === "ERR_DLOPEN_FAILED" ||
96
+ e.message.includes("compiled against a different Node.js version") ||
97
+ e.message.includes("NODE_MODULE_VERSION")
66
98
  process.stderr.write(
67
99
  JSON.stringify({
68
100
  jsonrpc: "2.0",
69
101
  error: {
70
102
  code: -32000,
71
- message: `context.db not found at ${DB_PATH}. Run setup first: node ${path.join(MCP_DIR, "setup.js")}`,
103
+ message: isVersionMismatch
104
+ ? `Native module mismatch for Node ${process.version}. Run: npm rebuild better-sqlite3`
105
+ : `Failed to open context.db: ${e.message}`,
72
106
  },
73
107
  id: null,
74
108
  }) + "\n"
75
109
  )
76
110
  process.exit(1)
77
111
  }
78
-
79
- const db = new Database(DB_PATH, { readonly: false })
80
112
  const CONVERSION_TASKS = JSON.parse(fs.readFileSync(TASKS_PATH, "utf8"))
81
113
 
82
114
  // ── Module init ───────────────────────────────────────────────────────────────
@@ -149,19 +149,19 @@ const MASTER_FLOWS = {
149
149
  step: 1,
150
150
  cmd: null,
151
151
  label: "Pre-check: config",
152
- detail: 'Reads WEBVIEW_CONFIG.ios. Requires: appBundleId, simulatorName, buildType (default "debug").',
152
+ detail: "Reads WEBVIEW_CONFIG and WEBVIEW_CONFIG.ios. Confirm the development URL, appBundleId, simulatorName, plugins, and selected public/ios assets.",
153
153
  },
154
154
  {
155
155
  step: 2,
156
- cmd: "catalyst build",
157
- label: "Build web assets",
158
- detail: "Compiles web app. iOS build embeds these.",
156
+ cmd: "npm run buildApp:ios",
157
+ label: "iOS simulator build",
158
+ detail: "Generates native configuration and plugin code in the package-owned Xcode project, syncs selected assets, compiles with Xcode's Debug configuration, installs the .app, and launches it.",
159
159
  },
160
160
  {
161
161
  step: 3,
162
- cmd: "npm run buildApp:ios",
163
- label: "iOS simulator build",
164
- detail: "Runs buildAppIos.js: generates ConfigConstants.swift from config (bundleId, URL, port, protocol), launches simulator (simulatorName), cleans Xcode artifacts, compiles with xcodebuild, installs .app, launches.",
162
+ cmd: null,
163
+ label: "Remote web application",
164
+ detail: "The iOS shell loads the configured remote URL. buildApp:ios does not bundle the full Catalyst web client, so the target web application must be reachable.",
165
165
  },
166
166
  {
167
167
  step: 4,
@@ -175,27 +175,44 @@ const MASTER_FLOWS = {
175
175
  {
176
176
  step: 1,
177
177
  cmd: null,
178
+ label: "Deploy the production web application",
179
+ detail: "Deploy the web application first and confirm its production URL is reachable over HTTPS. The iOS shell loads this remote URL; it does not embed the full web client bundle.",
180
+ },
181
+ {
182
+ step: 2,
183
+ cmd: null,
178
184
  label: "Pre-check: config",
179
- detail: 'buildType must be "Release" (capital R — case-sensitive). appBundleId required. If googleSignIn enabled, GoogleService-Info.plist must be present.',
185
+ detail: "Finalize WEBVIEW_CONFIG, appBundleId, app name, version, incremented build number, access-control rules, plugins, permissions, capabilities, icons, splash assets, and privacy declarations.",
180
186
  },
181
- { step: 2, cmd: "catalyst build", label: "Build web assets", detail: "Production build." },
182
187
  {
183
188
  step: 3,
184
189
  cmd: "npm run buildApp:ios",
185
- label: "iOS release build",
186
- detail: "Same script — build type is driven by WEBVIEW_CONFIG.ios.buildType. Generates ConfigConstants.swift with production URL and bundleId. Xcode compiles Release scheme.",
190
+ label: "Prepare the native project",
191
+ detail: "Run from the consuming Catalyst app root. This generates native configuration and plugin code, syncs selected assets, and performs a Debug native build. It does not create an App Store archive.",
187
192
  },
188
193
  {
189
194
  step: 4,
190
195
  cmd: null,
191
- label: "Archive + export IPA",
192
- detail: "Post-build: archive via Xcode Organizer or xcodebuild -exportArchive. Requires Apple distribution certificate + provisioning profile configured in Xcode.",
196
+ label: "Open the generated Xcode project",
197
+ detail: "Open node_modules/catalyst-core/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj (or the resolved package equivalent). Confirm the iosnativeWebView scheme's Archive action uses Release.",
193
198
  },
194
199
  {
195
200
  step: 5,
196
201
  cmd: null,
197
- label: "App Store submission",
198
- detail: "Upload IPA via Transporter or Xcode Organizer. TestFlight for beta before release.",
202
+ label: "Configure signing and archive",
203
+ detail: "Select the correct Apple Developer team, distribution signing, capabilities, entitlements, bundle ID, version, and build. Choose a generic iOS device destination, then Product → Archive.",
204
+ },
205
+ {
206
+ step: 6,
207
+ cmd: null,
208
+ label: "Validate and upload",
209
+ detail: "In Xcode Organizer, run Validate App, then Distribute App → TestFlight & App Store and upload the build.",
210
+ },
211
+ {
212
+ step: 7,
213
+ cmd: null,
214
+ label: "Complete the release",
215
+ detail: "Wait for App Store Connect processing, test through TestFlight, complete store metadata and privacy disclosures, select the processed build, and submit it for App Review.",
199
216
  },
200
217
  ],
201
218
  }
@@ -495,6 +512,7 @@ function getProjectContext(root) {
495
512
  iosBuildType: (wv.ios?.buildType || "debug").toLowerCase(),
496
513
  hasKeystore: !!wv.android?.keystoreConfig,
497
514
  hasGoogleSignIn: !!wv.googleSignIn?.enabled,
515
+ hasNotifications: !!wv.notifications?.enabled,
498
516
  hasEcosystem: fileExists("ecosystem.config.js"),
499
517
  port: wv.port,
500
518
  useHttps: !!wv.useHttps,
@@ -618,7 +636,7 @@ function handle_get_build_flow({ platform, mode, symptom } = {}) {
618
636
  )
619
637
  }
620
638
 
621
- const isRelease = m === "release" || ctx.iosBuildType === "release"
639
+ const isRelease = m === "release"
622
640
  flow_key = isRelease ? "ios_release" : "ios_debug"
623
641
 
624
642
  if (!ctx.wv.ios?.appBundleId) {
@@ -634,13 +652,23 @@ function handle_get_build_flow({ platform, mode, symptom } = {}) {
634
652
  "googleSignIn.enabled=true but iosClientId may be missing — check WEBVIEW_CONFIG.googleSignIn.iosClientId."
635
653
  )
636
654
  }
637
- if (isRelease && ctx.iosBuildType !== "release") {
655
+ if (isRelease && !ctx.useHttps) {
638
656
  warnings.push(
639
- 'ios.buildType is not "Release" (case-sensitive). Release builds require exactly "Release" — not "release".'
657
+ "WEBVIEW_CONFIG.useHttps is not enabled — the App Store flow requires a reachable HTTPS production web application."
658
+ )
659
+ }
660
+ if (isRelease && ctx.hasNotifications) {
661
+ warnings.push(
662
+ "Push notifications are enabled — review the generated aps-environment entitlement and Release provisioning before archiving."
640
663
  )
641
664
  }
642
665
 
643
666
  notes.push(`buildType detected: ${ctx.wv.ios?.buildType || "not set (defaults to debug)"}`)
667
+ if (isRelease) {
668
+ notes.push(
669
+ "ios.buildType does not select the App Store build configuration; buildApp:ios still performs a Debug build. Archive the generated project with the scheme's Release action."
670
+ )
671
+ }
644
672
  if (ctx.wv.ios?.appBundleId) notes.push(`appBundleId: ${ctx.wv.ios.appBundleId}`)
645
673
  if (ctx.wv.ios?.simulatorName) notes.push(`simulatorName: ${ctx.wv.ios.simulatorName}`)
646
674
  if (ctx.hasGoogleSignIn) notes.push("googleSignIn: enabled")
@@ -161,6 +161,7 @@ function safeReadText(filePath, maxBytes = 8000) {
161
161
 
162
162
  function runGit(root, command) {
163
163
  try {
164
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - command is always one of the fixed git/node/npm strings hardcoded at call sites, never external input.
164
165
  return execSync(command, {
165
166
  cwd: root,
166
167
  encoding: "utf8",
@@ -183,6 +184,7 @@ function slugify(input) {
183
184
  }
184
185
 
185
186
  function ensureFallbackDir(root) {
187
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter letting a local developer's coding agent point this tool at any project directory it already has filesystem access to (see mcp.js inputSchema). Not a network-facing input; the caller already has direct fs access via other tools. FALLBACK_DIR is a hardcoded literal.
186
188
  const dir = path.join(root, FALLBACK_DIR)
187
189
  fs.mkdirSync(dir, { recursive: true })
188
190
  return dir
@@ -406,7 +408,9 @@ function appendSection(sections, heading, content) {
406
408
  }
407
409
 
408
410
  function isPathInside(root, candidate) {
411
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - this is the containment-check helper itself (used by callers below to reject paths outside root); root/candidate here are the values being validated, not yet trusted for file access.
409
412
  const resolvedRoot = path.resolve(root)
413
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - this is the containment-check helper itself; candidate is the value being validated, not yet trusted for file access.
410
414
  const resolvedCandidate = path.resolve(candidate)
411
415
  const relative = path.relative(resolvedRoot, resolvedCandidate)
412
416
  return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative))
@@ -543,6 +547,7 @@ function normalizeImages(images, root) {
543
547
  continue
544
548
  }
545
549
 
550
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - raw is an MCP tool argument (attachment path) and root the project_path argument; the isPathInside() check on the next line rejects anything that resolves outside root before it is ever read.
546
551
  const absolutePath = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw)
547
552
  if (!isPathInside(root, absolutePath)) {
548
553
  localFiles.push({ path: raw, exists: false, rejected: true, reason: "outside_project_root" })
@@ -789,6 +794,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
789
794
  const candidates = Array.isArray(explicitFiles) ? explicitFiles : []
790
795
  for (const file of candidates) {
791
796
  if (typeof file !== "string") continue
797
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - file is an MCP tool argument (explicit related-files list) and root the project_path argument; the isPathInside() check on the next line rejects anything that resolves outside root before it is ever read.
792
798
  const absolute = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file)
793
799
  if (!isPathInside(root, absolute)) continue
794
800
  const content = safeReadText(absolute, 12000)
@@ -802,6 +808,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
802
808
  const words = normalizeSearchText(query).split(/\s+/).filter(Boolean).slice(0, 5)
803
809
  if (words.length === 0) return files
804
810
 
811
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "src" is a hardcoded literal.
805
812
  const srcDir = path.join(root, "src")
806
813
  const matches = []
807
814
  function walk(dir) {
@@ -813,6 +820,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
813
820
  }
814
821
  for (const entry of entries) {
815
822
  if (entry.name === "node_modules" || entry.name === ".git") continue
823
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from fs.readdirSync(dir) above, i.e. actual filenames already on disk under srcDir, not request input.
816
824
  const full = path.join(dir, entry.name)
817
825
  if (entry.isDirectory()) {
818
826
  walk(full)
@@ -831,7 +839,9 @@ function collectRelatedFiles(root, query, explicitFiles) {
831
839
 
832
840
  function gatherGithubIssueContext(args = {}) {
833
841
  const root = getProjectRoot(args)
842
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "package.json" is a hardcoded literal.
834
843
  const pkg = safeReadJson(path.join(root, "package.json"))
844
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "config/config.json" is a hardcoded literal.
835
845
  const configJson = safeReadJson(path.join(root, "config", "config.json"))
836
846
  const branch = runGit(root, "git rev-parse --abbrev-ref HEAD")
837
847
  const commit = runGit(root, "git rev-parse --short HEAD")
@@ -874,6 +884,7 @@ function generateIssueMarkdown(args = {}) {
874
884
  const built = buildIssueBody(args, { enrich: true })
875
885
  const labels = resolveIssueLabels(args, built.body)
876
886
  const dir = ensureFallbackDir(root)
887
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is the FALLBACK_DIR under the project_path MCP argument; the filename is a generated ISO timestamp plus slugify(title), which strips to [a-z0-9-] only, so it cannot contain path separators.
877
888
  const filePath = path.join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${slugify(title)}.md`)
878
889
 
879
890
  const markdown = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catalyst-core",
3
- "version": "0.3.0-beta.1",
3
+ "version": "0.3.0-beta.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.jsx",
6
6
  "description": "Web framework that provides great performance out of the box",
@@ -31,7 +31,7 @@
31
31
  "lint": "eslint .",
32
32
  "lint-staged": "lint-staged",
33
33
  "prettify": "prettier . --write",
34
- "prepare": "rm -rf ./dist && cp -r ./src ./dist && babel src/native --out-dir dist/native --config-file ./babel.native.config.cjs --ignore '**/.build/**' && babel src/sentry.js --out-file dist/sentry.cjs --config-file ./babel.native.config.cjs",
34
+ "prepare": "node ../../scripts/safe-rimraf.js ./dist && cp -r ./src ./dist && babel src/native --out-dir dist/native --config-file ./babel.native.config.cjs --ignore '**/.build/**' && babel src/sentry.js --out-file dist/sentry.cjs --config-file ./babel.native.config.cjs",
35
35
  "prepublishOnly": "npm run prepare",
36
36
  "test:fixture": "sh ../../scripts/test-catalyst-core.sh"
37
37
  },
@@ -51,7 +51,7 @@
51
51
  "react-refresh": "^0.14.0",
52
52
  "react-router-dom": "^6.16.0",
53
53
  "redux": "^4.2.1",
54
- "sanitize-html": "^2.13.1",
54
+ "sanitize-html": "^2.17.6",
55
55
  "sass": "^1.89.2",
56
56
  "ua-parser-js": "^1.0.37",
57
57
  "vite": "^6.4.3",