@mindees/cli 0.20.0 → 0.21.0
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/dist/android-template.generated.js +3 -3
- package/dist/android-template.generated.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/templates.d.ts +3 -2
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +16 -3
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +4 -4
|
@@ -5,7 +5,7 @@ const ANDROID_TEMPLATE_FILES = {
|
|
|
5
5
|
"README.md": "# MindeesNative — Android app (experimental)\n\nA standalone native **Android** app built with MindeesNative. The UI is authored in\nTypeScript/TSX (Atlas components + the Quantum file-based router) and runs on a real\nAndroid view tree through an **embedded QuickJS** runtime. The native host\n(`mindees-host/`) is **vendored as Kotlin source** — no Maven dependency on\nMindeesNative is required.\n\n> **Experimental.** This scaffold is verified end-to-end in CI (the app-js bundle\n> builds from npm and `gradle assembleDebug` produces an APK containing it), but the\n> native mobile product is pre-1.0. Expect rough edges.\n\n## Prerequisites\n\n- **JDK 17**\n- **Gradle 9.4.1+** (AGP 9.2 requires it) — used once to bootstrap the wrapper\n- **Android SDK** with `platforms;android-36`\n- **Node 20+** (for the app-js bundle)\n\n## Build (two phases, in order)\n\nThe native app loads `mindees-example-app/src/main/assets/mindees-app.bundle.js`,\nwhich is **generated** from the TSX app — it is git-ignored and absent until you build\nit. Build the JS bundle **first**, then the APK:\n\n```sh\n# 1. Build the JS app bundle (installs @mindees/* from npm, runs route codegen + tsdown)\ncd mindees-example-app/app-js\nnpm install\nnpm run build # → ../src/main/assets/mindees-app.bundle.js\n\n# 2. Build the Android APK\ncd ../..\ngradle wrapper --gradle-version 9.4.1 # one-time: the wrapper jar isn't vendored\n./gradlew :mindees-example-app:assembleDebug\n# → mindees-example-app/build/outputs/apk/debug/*.apk\n```\n\nInstall the APK on a device/emulator with `./gradlew :mindees-example-app:installDebug`.\n\n## Project layout\n\n```\nmindees-host/ # the native renderer + JS↔native bridge — VENDORED Kotlin source\nmindees-example-app/\n app-js/ # your TypeScript/TSX app (Atlas + router); builds the JS bundle\n src/app/ # file-based routes — drop a .tsx file to add a screen\n src/main.tsx # entry: createNativeApp(...)\n src/main/kotlin/ # the Android host shell (MainActivity, QuickJS bridge)\n```\n\nEdit your UI under `mindees-example-app/app-js/src/`, re-run the app-js build, then\nre-run `assembleDebug`.\n\n## Notes & limitations (experimental)\n\n- The Android package id is `dev.mindees.example` and `rootProject.name` is fixed, so\n two scaffolds can't be installed side-by-side yet (namespace parameterization is a\n planned enhancement).\n- App-level Android dependencies (QuickJS, AndroidX, FlexboxLayout) still come from\n Maven Central / Google — \"no Maven\" applies to the **MindeesNative host**, which is\n vendored as source.\n",
|
|
6
6
|
"build.gradle.kts": "// Root build file. Plugin versions are declared in settings.gradle.kts\n// (pluginManagement), so modules apply them without a version here.\n",
|
|
7
7
|
"gradle.properties": "android.useAndroidX=true\nkotlin.code.style=official\norg.gradle.jvmargs=-Xmx2048m\n",
|
|
8
|
-
"mindees-example-app/app-js/package.json": "{\n \"name\": \"mindees-android-app-js\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"gen-routes\": \"node scripts/gen-routes.mjs\",\n \"build\": \"npm run gen-routes && tsdown --config tsdown.config.ts\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"@mindees/core\": \"0.
|
|
8
|
+
"mindees-example-app/app-js/package.json": "{\n \"name\": \"mindees-android-app-js\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"gen-routes\": \"node scripts/gen-routes.mjs\",\n \"build\": \"npm run gen-routes && tsdown --config tsdown.config.ts\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"@mindees/core\": \"0.21.0\",\n \"@mindees/atlas\": \"0.21.0\",\n \"@mindees/renderer\": \"0.21.0\",\n \"@mindees/router\": \"0.21.0\",\n \"@mindees/compiler\": \"0.21.0\"\n },\n \"devDependencies\": {\n \"tsdown\": \"0.22.1\",\n \"typescript\": \"6.0.3\"\n }\n}\n",
|
|
9
9
|
"mindees-example-app/app-js/scripts/gen-routes.mjs": "/**\n * File-based-routing codegen. Scans `src/app/` and writes `src/routes.gen.ts` — a\n * static-import module map the app feeds to `createFileRouter` (the QuickJS bundle has\n * no `import.meta.glob`). Run by `npm run build` before bundling. The reusable codegen\n * lives in `@mindees/compiler` (`generateRouteModule`); this script supplies the file\n * list. `routes.gen.ts` is generated (git-ignored) — never edit it by hand.\n */\n\nimport { readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { generateRouteModule } from '@mindees/compiler'\n\nconst here = dirname(fileURLToPath(import.meta.url))\nconst appDir = join(here, '..', 'src', 'app')\nconst outFile = join(here, '..', 'src', 'routes.gen.ts')\n\n/** Collect route files (relative, POSIX), skipping tests. */\nfunction collect(dir, base = '') {\n const out = []\n for (const name of readdirSync(dir)) {\n const full = join(dir, name)\n const rel = base ? `${base}/${name}` : name\n if (statSync(full).isDirectory()) out.push(...collect(full, rel))\n else if (/\\.(tsx|ts|jsx|js)$/.test(name) && !/\\.test\\./.test(name)) out.push(rel)\n }\n return out\n}\n\nconst files = collect(appDir)\nwriteFileSync(outFile, generateRouteModule(files, { importBase: './app' }))\nprocess.stdout.write(`gen-routes: wrote ${files.length} route(s) to src/routes.gen.ts\\n`)\n",
|
|
10
10
|
"mindees-example-app/app-js/src/App.tsx": "/**\n * The example app — file-based routing, `@mindees/*` only, plain TSX.\n *\n * Routes live in `app/` (app/index.tsx → `/`, app/about.tsx → `/about`); the screens\n * use the `useRouter()` hook (no router prop-drilling). `createFileRouter` turns the\n * module map into a Quantum router with Expo-style conventions but a stronger core\n * (validated params, fine-grained reads, codegen-free typing).\n *\n * @module\n */\n\nimport { Column, space, useTheme } from '@mindees/atlas'\nimport { createFileRouter, createMemoryHistory, createRouterView } from '@mindees/router'\nimport { routes } from './routes.gen'\n\n// `routes` is generated from the `app/` directory (scripts/gen-routes.mjs +\n// @mindees/compiler `generateRouteModule`), so adding a file under `app/` adds a route\n// with no edits here. createFileRouter applies the Expo-style conventions.\nconst router = createFileRouter(routes, {\n history: createMemoryHistory({ initialEntries: ['/'] }),\n})\n\n/** Full-screen shell: themed background (re-themes light↔dark), centers the active route. */\nexport function App() {\n const theme = useTheme()\n return (\n <Column\n style={() => ({\n flexGrow: 1,\n width: '100%',\n padding: space.lg,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: theme().color.bg,\n })}\n >\n {createRouterView(router)}\n </Column>\n )\n}\n",
|
|
11
11
|
"mindees-example-app/app-js/src/app/about.tsx": "/**\n * About route — `app/about.tsx` maps to `/about`. Showcases Atlas components\n * (Card, Badge, Divider, Switch, ProgressBar) + design-token theming: the Switch\n * toggles the device color scheme, so the whole UI re-themes light↔dark.\n *\n * @module\n */\n\nimport {\n ActivityIndicator,\n Badge,\n Button,\n Card,\n Divider,\n fontSize,\n ProgressBar,\n Row,\n Switch,\n setEnvironment,\n space,\n Text,\n useColorScheme,\n useTheme,\n} from '@mindees/atlas'\nimport { useRouter } from '@mindees/router'\nimport { buttonShape } from '../theme'\n\nexport default function About() {\n const router = useRouter()\n const theme = useTheme()\n const colorScheme = useColorScheme()\n return (\n <Card variant=\"filled\" style={{ minWidth: 300, gap: space.md, alignItems: 'stretch' }}>\n <Row style={{ justifyContent: 'space-between', alignItems: 'center' }}>\n <Text\n style={() => ({ fontSize: fontSize.title2, fontWeight: 800, color: theme().color.text })}\n >\n About\n </Text>\n <Badge tone=\"info\">v0.1.0</Badge>\n </Row>\n <Divider />\n <Text\n style={() => ({ fontSize: fontSize.body, color: theme().color.textMuted, lineHeight: 22 })}\n >\n File-based routes navigated by the Quantum router via the useRouter hook — built from themed\n Atlas components, all TypeScript, running native in an embedded engine.\n </Text>\n <Row style={{ justifyContent: 'space-between', alignItems: 'center' }}>\n <Text style={() => ({ fontSize: fontSize.body, color: theme().color.text })}>\n Dark mode\n </Text>\n <Switch\n value={() => colorScheme() === 'dark'}\n onValueChange={(v) => setEnvironment({ colorScheme: v ? 'dark' : 'light' })}\n label=\"Dark mode\"\n />\n </Row>\n <Row style={{ gap: space.sm, alignItems: 'center' }}>\n <ActivityIndicator size={20} />\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\n Syncing…\n </Text>\n </Row>\n <ProgressBar value={0.6} />\n <Button\n title=\"← Home\"\n onPress={() => router.navigate('/')}\n style={() => ({\n ...buttonShape,\n backgroundColor: theme().color.primary,\n color: theme().color.onPrimary,\n })}\n />\n </Card>\n )\n}\n",
|
|
@@ -14,7 +14,7 @@ const ANDROID_TEMPLATE_FILES = {
|
|
|
14
14
|
"mindees-example-app/app-js/src/theme.ts": "/** Token-based shape helpers for the example (colors come from `useTheme` per screen). */\n\nimport { fontWeight, radius, space } from '@mindees/atlas'\n\n/** Shared button shape (background/foreground colors applied per screen from the theme). */\nexport const buttonShape = {\n paddingTop: space.sm,\n paddingBottom: space.sm,\n paddingLeft: space.lg,\n paddingRight: space.lg,\n borderRadius: radius.md,\n fontWeight: fontWeight.semibold,\n} as const\n",
|
|
15
15
|
"mindees-example-app/app-js/tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Bundler\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"@mindees/core\",\n \"strict\": true,\n \"skipLibCheck\": true,\n \"noEmit\": true\n },\n \"include\": [\"src\"]\n}\n",
|
|
16
16
|
"mindees-example-app/app-js/tsdown.config.ts": "import { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { defineConfig } from 'tsdown'\n\nconst here = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Bundles the TSX-authored Atlas + Helix + Quantum app (src/main.tsx) into a single\n * QuickJS-safe IIFE the Android host loads from assets. `@mindees/*` resolve from\n * node_modules via their package `exports` (incl. the automatic JSX runtime subpath\n * `@mindees/core/jsx-runtime`, see tsconfig.json) and are inlined into the IIFE — the\n * framework has no node-builtin runtime deps. A banner polyfills `queueMicrotask`\n * (some embedded engines lack it) before any module initializes.\n */\nexport default defineConfig({\n entry: [resolve(here, 'src/main.tsx')],\n outDir: resolve(here, '..', 'src', 'main', 'assets'),\n format: ['iife'],\n globalName: 'MindeesAppBundle',\n platform: 'node',\n target: 'es2020',\n // Inline the framework packages into the IIFE (they are package.json `dependencies`,\n // which tsdown would otherwise treat as external bare imports — the embedded QuickJS\n // engine has no module loader, so everything must be bundled).\n noExternal: [/^@mindees\\//],\n dts: false,\n clean: false,\n minify: false,\n define: { 'process.env.NODE_ENV': JSON.stringify('production') },\n outputOptions: {\n entryFileNames: 'mindees-app.bundle.js',\n banner:\n \"if (typeof globalThis !== 'undefined' && typeof globalThis.queueMicrotask !== 'function') { globalThis.queueMicrotask = function (cb) { Promise.resolve().then(cb); }; }\",\n },\n})\n",
|
|
17
|
-
"mindees-example-app/build.gradle.kts": "plugins {\n id(\"com.android.application\")\n}\n\nandroid {\n namespace = \"dev.mindees.example\"\n compileSdk = 36\n\n defaultConfig {\n applicationId = \"
|
|
17
|
+
"mindees-example-app/build.gradle.kts": "plugins {\n id(\"com.android.application\")\n}\n\nandroid {\n namespace = \"dev.mindees.example\"\n compileSdk = 36\n\n defaultConfig {\n applicationId = \"{{androidAppId}}\"\n minSdk = 24\n targetSdk = 36\n versionCode = 1\n versionName = \"0.0.0\"\n testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n }\n\n compileOptions {\n sourceCompatibility = JavaVersion.VERSION_17\n targetCompatibility = JavaVersion.VERSION_17\n }\n\n testOptions {\n unitTests {\n isReturnDefaultValues = true\n }\n }\n}\n\nkotlin {\n jvmToolchain(17)\n}\n\ndependencies {\n implementation(project(\":mindees-host\"))\n implementation(\"app.cash.quickjs:quickjs-android:0.9.2\")\n // FlexboxLayout (used by the renderer + the root container) needs androidx.core (ViewCompat)\n // on the runtime classpath; this app is otherwise framework-only, so declare it explicitly.\n implementation(\"androidx.core:core:1.13.1\")\n\n testImplementation(\"junit:junit:4.13.2\")\n testImplementation(\"org.json:json:20231013\")\n\n androidTestImplementation(\"androidx.test:core:1.7.0\")\n androidTestImplementation(\"androidx.test:runner:1.7.0\")\n androidTestImplementation(\"androidx.test.ext:junit:1.3.0\")\n}\n",
|
|
18
18
|
"mindees-example-app/src/main/AndroidManifest.xml": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <application\n android:allowBackup=\"false\"\n android:label=\"Mindees Native Example\"\n android:supportsRtl=\"true\"\n android:theme=\"@android:style/Theme.Material.Light.NoActionBar\">\n <activity\n android:name=\".MainActivity\"\n android:exported=\"true\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n </activity>\n </application>\n</manifest>\n",
|
|
19
19
|
"mindees-example-app/src/main/kotlin/dev/mindees/example/FrameDriver.kt": "package dev.mindees.example\n\nimport android.view.Choreographer\n\n/**\n * Drives the JS animation frame loop from Android vsync via [Choreographer] — but **only while\n * active**. The JS side (`createNativeApp`) calls `MindeesHostFrame.setFrameLoopActive(true)` the\n * instant an animation arms and `(false)` the instant the last one settles, so an idle app posts\n * **zero** frame callbacks (no 60fps busy-loop). Construct and use on the UI thread (Choreographer is\n * UI-thread-only).\n *\n * @param tick called once per vsync with the frame time in milliseconds (`frameTimeNanos / 1e6`).\n */\nclass FrameDriver(private val tick: (Double) -> Unit) {\n private var running = false\n\n // An explicit anonymous object (NOT a SAM lambda): the body re-posts `this`, and a lambda that\n // referenced its own `val` would make Kotlin's type inference recurse.\n private val callback: Choreographer.FrameCallback = object : Choreographer.FrameCallback {\n override fun doFrame(frameTimeNanos: Long) {\n if (!running) return\n tick(frameTimeNanos / 1_000_000.0)\n // Re-perpetuate ONLY while running; setActive(false) simply stops re-posting, so the loop\n // dies after the in-flight frame — nothing to cancel, no leaked callback.\n Choreographer.getInstance().postFrameCallback(this)\n }\n }\n\n /** Start (true) or stop (false) the vsync loop. Idempotent. */\n fun setActive(active: Boolean) {\n if (active == running) return\n running = active\n if (active) Choreographer.getInstance().postFrameCallback(callback)\n }\n}\n",
|
|
20
20
|
"mindees-example-app/src/main/kotlin/dev/mindees/example/MainActivity.kt": "package dev.mindees.example\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport dev.mindees.host.AndroidViewRenderer\nimport dev.mindees.host.MindeesNativeHost\n\nclass MainActivity : Activity() {\n private var bridge: MindeesRuntimeBridge<View>? = null\n private var frameDriver: FrameDriver? = null\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n // Fill the window and paint a dark base: the Atlas app controls its own layout and\n // background edge-to-edge, so the host window should match it (no light gaps showing\n // through where content hasn't laid out yet).\n // A FrameLayout root z-stacks the app content + the portal `overlay` layer, so Modal/Toast\n // (which the renderer mounts into a full-screen `overlay` node, kept painting last) overlap\n // the content instead of being laid out beneath it.\n val root = FrameLayout(this).apply {\n layoutParams = ViewGroup.LayoutParams(\n ViewGroup.LayoutParams.MATCH_PARENT,\n ViewGroup.LayoutParams.MATCH_PARENT,\n )\n setBackgroundColor(Color.parseColor(\"#0b1021\"))\n }\n val renderer = AndroidViewRenderer(this)\n val mainHandler = Handler(Looper.getMainLooper())\n\n val host = MindeesNativeHost<View>(\n rootId = HOST_ROOT_ID,\n root = root,\n renderer = renderer,\n onEvent = { handlerId, value ->\n bridge?.dispatchEvent(handlerId, value)\n ?: error(\"Mindees runtime bridge has not started\")\n },\n )\n\n // The real Atlas + Helix app, bundled to a QuickJS-safe IIFE (app-js/, regenerate\n // with `pnpm run build:android-example-js`). It runs @mindees/core signals +\n // @mindees/atlas primitives + the @mindees/renderer reconciler inside QuickJS and\n // emits the native command stream this host materializes — not hand-written commands.\n val appJs = assets.open(APP_BUNDLE_ASSET).bufferedReader().use { it.readText() }\n\n // Drives JS animation frames from vsync; constructed on the UI thread. `bridge` is assigned\n // just below and is non-null by the time the first frame callback fires (next vsync).\n val driver = FrameDriver { nowMs -> bridge?.frameTick(nowMs) }\n frameDriver = driver\n\n bridge = MindeesRuntimeBridge(\n host = host,\n // The JS engine arms/sleeps the vsync loop through setFrameLoopActive → FrameDriver.\n runtime = QuickJsMindeesRuntime(\n appJs,\n environmentJson(),\n onFrameLoopActive = { active -> driver.setActive(active) },\n ),\n applyOnHostThread = { action ->\n if (Looper.myLooper() == Looper.getMainLooper()) {\n action()\n } else {\n mainHandler.post(action)\n }\n },\n ).also { it.start() }\n\n setContentView(root)\n }\n\n override fun onDestroy() {\n frameDriver?.setActive(false) // stop vsync before tearing down the engine\n frameDriver = null\n bridge?.close()\n bridge = null\n super.onDestroy()\n }\n\n /**\n * The platform environment for `@mindees/atlas` device hooks (useWindowDimensions,\n * useColorScheme, …), as JSON. Logical dp = pixels / density.\n */\n private fun environmentJson(): String {\n val dm = resources.displayMetrics\n val cfg = resources.configuration\n val widthDp = (dm.widthPixels / dm.density)\n val heightDp = (dm.heightPixels / dm.density)\n val isDark =\n (cfg.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\n return \"\"\"\n {\"window\":{\"width\":$widthDp,\"height\":$heightDp,\"scale\":${dm.density},\"\"\" +\n \"\"\"\"fontScale\":${cfg.fontScale}},\"colorScheme\":\"${if (isDark) \"dark\" else \"light\"}\"}\n \"\"\".trimIndent()\n }\n\n private companion object {\n const val HOST_ROOT_ID = \"host-root\"\n\n /** The bundled real Atlas + Helix app (see app-js/ + tsdown.config.ts). */\n const val APP_BUNDLE_ASSET = \"mindees-app.bundle.js\"\n }\n}\n",
|
|
@@ -24,7 +24,7 @@ const ANDROID_TEMPLATE_FILES = {
|
|
|
24
24
|
"mindees-host/src/main/kotlin/dev/mindees/host/AndroidViewRenderer.kt": "/*\n * AndroidViewRenderer.kt — a HostRenderer that builds real android.view widgets from\n * the MindeesNative command stream, mapping Atlas's curated cross-platform `StyleObject`\n * onto native layout + visuals.\n *\n * Layout uses Google FlexboxLayout for full flex parity: flexDirection,\n * justifyContent (incl. space-between/around/evenly), alignItems, flexWrap, alignSelf,\n * gap (→ child margins), flex/flexGrow (→ FlexboxLayout.LayoutParams.flexGrow) — plus\n * the box model, background/radius/border, opacity, and text styling.\n *\n * Device-facing, but JVM-testable via Robolectric (AndroidRenderTest) and on-device\n * by the native Android workflow. See the module README.\n */\n\npackage dev.mindees.host\n\nimport android.content.Context\nimport android.content.res.ColorStateList\nimport android.graphics.BitmapFactory\nimport android.graphics.Color\nimport android.graphics.PorterDuff\nimport android.graphics.Typeface\nimport android.graphics.drawable.GradientDrawable\nimport android.text.Editable\nimport android.text.InputType\nimport android.text.TextUtils\nimport android.text.TextWatcher\nimport android.util.Base64\nimport android.util.TypedValue\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport android.widget.EditText\nimport android.widget.FrameLayout\nimport android.widget.HorizontalScrollView\nimport android.widget.ImageView\nimport android.widget.ProgressBar\nimport android.widget.ScrollView\nimport android.widget.TextView\nimport com.google.android.flexbox.AlignItems\nimport com.google.android.flexbox.AlignSelf\nimport com.google.android.flexbox.FlexDirection\nimport com.google.android.flexbox.FlexWrap\nimport com.google.android.flexbox.FlexboxLayout\nimport com.google.android.flexbox.JustifyContent\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.util.concurrent.ExecutorService\nimport java.util.concurrent.Executors\n\n/** Loads bytes for a remote image `url`, invoking `onResult` with the bytes (or null on failure). */\ntypealias ImageLoader = (url: String, onResult: (ByteArray?) -> Unit) -> Unit\n\n/** Builds Android views from the command stream. Pair with [MindeesNativeHost]. */\nclass AndroidViewRenderer(\n private val context: Context,\n /** Override how remote (`http(s)`) images are fetched (e.g. to inject a cache or a test stub). */\n private val imageLoader: ImageLoader? = null,\n) : HostRenderer<View> {\n\n private val density: Float = context.resources.displayMetrics.density\n\n /** Off-main fetch pool for remote images (created lazily; one idle thread for the renderer's life). */\n private val imageExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }\n /** Per-ImageView load generation, so a slow/stale fetch never overwrites a newer src or a disposed view. */\n private val imageGen = HashMap<View, Long>()\n\n /** dp → px (Atlas numeric style values are density-independent pixels on native). */\n private fun dp(value: Double): Int = Math.round(value * density).toInt()\n\n /**\n * Layout intent we must (re)apply via LayoutParams when a child is inserted, or\n * when a container's gap/direction changes. Kept off the View so the mapping stays\n * explicit and testable.\n */\n private class Layout {\n var horizontal = false\n var gapPx = 0\n var widthSpec = ViewGroup.LayoutParams.WRAP_CONTENT\n var heightSpec = ViewGroup.LayoutParams.WRAP_CONTENT\n var grow = 0f\n var alignSelf = AlignSelf.AUTO\n }\n\n private val layouts = HashMap<View, Layout>()\n private fun layoutOf(view: View): Layout = layouts.getOrPut(view) { Layout() }\n\n /**\n * A `scrollview` node is a ScrollView whose ONE child is a FlexboxLayout \"content\" host. Children\n * and flex/layout styles route to that content; the ScrollView itself is just the viewport.\n */\n private val scrollContent = HashMap<View, FlexboxLayout>()\n private fun contentFor(view: View): View = scrollContent[view] ?: view\n\n /** Per-EditText keyboard/secure/multiline intent, recomputed into inputType (compose any order). */\n private class InputSpec {\n var keyboard = \"text\"\n var secure = false\n var multiline = false\n }\n private val inputSpecs = HashMap<View, InputSpec>()\n private fun inputSpecOf(view: View): InputSpec = inputSpecs.getOrPut(view) { InputSpec() }\n /** Active text-change watchers keyed by view then eventName, so onInput and onChange are\n * independent (both can be registered) and removeEvent/dispose detach precisely (no leaks). */\n private val textWatchers = HashMap<View, HashMap<String, TextWatcher>>()\n\n /**\n * Text composition. A leaf widget (TextView/Button/EditText) can't hold child views,\n * but the element model nests text *nodes* inside a `text` *element* (e.g. Atlas's\n * `Text` → `<text>\"hello\"</text>`). So we compose a text element's text-node children\n * into its own `text` instead of attaching them as views. `textParts` keeps each\n * element's ordered text-node children; `textOwner` is the reverse lookup so an\n * `updateText` on a child re-composes its owner.\n */\n private val textParts = HashMap<View, MutableList<View>>()\n private val textOwner = HashMap<View, View>()\n\n private fun recomposeText(element: View) {\n val tv = element as? TextView ?: return\n val parts = textParts[element] ?: return\n tv.text = parts.joinToString(\"\") { (it as? TextView)?.text ?: \"\" }\n }\n\n // --- Node creation ---\n\n override fun makeElement(tag: String): View = when (tag) {\n \"text\" -> TextView(context)\n \"image\" -> ImageView(context)\n \"textinput\" -> EditText(context)\n \"button\" -> Button(context) // direct use; Atlas Button renders as a clickable 'view'\n // Atlas ActivityIndicator → a native indeterminate spinner.\n \"activityindicator\" -> ProgressBar(context).apply {\n isIndeterminate = true\n layoutOf(this)\n }\n // 'scrollview' → a real vertical ScrollView wrapping a FlexboxLayout content host.\n // Children/styles route to the inner content (see contentFor); the ScrollView is the viewport.\n \"scrollview\" -> ScrollView(context).apply {\n isFillViewport = true\n val content = FlexboxLayout(context).apply { flexDirection = FlexDirection.COLUMN }\n content.layoutParams = FrameLayout.LayoutParams(\n ViewGroup.LayoutParams.MATCH_PARENT,\n ViewGroup.LayoutParams.WRAP_CONTENT,\n )\n addView(content)\n scrollContent[this] = content\n layoutOf(content)\n }\n // 'horizontalscrollview' → a HorizontalScrollView wrapping a ROW content host. Mirror of the\n // vertical case: content is WRAP width / MATCH height so the row can grow past the viewport on X.\n \"horizontalscrollview\" -> HorizontalScrollView(context).apply {\n isFillViewport = true\n val content = FlexboxLayout(context).apply { flexDirection = FlexDirection.ROW }\n content.layoutParams = FrameLayout.LayoutParams(\n ViewGroup.LayoutParams.WRAP_CONTENT,\n ViewGroup.LayoutParams.MATCH_PARENT,\n )\n addView(content)\n scrollContent[this] = content\n layoutOf(content).horizontal = true // gaps go on the X axis even before any style arrives\n }\n // 'overlay' → the portal layer (Modal/Toast mount here). A full-screen flex container that\n // FILLS its parent so it overlaps the app content (the host root is a FrameLayout that\n // z-stacks; the renderer keeps the overlay painting last, so it sits on top).\n \"overlay\" -> FlexboxLayout(context).apply {\n flexDirection = FlexDirection.COLUMN\n layoutOf(this).apply {\n widthSpec = ViewGroup.LayoutParams.MATCH_PARENT\n heightSpec = ViewGroup.LayoutParams.MATCH_PARENT\n }\n }\n // 'view' / unknown → a real flex container (FlexboxLayout): full\n // flexDirection / justifyContent (incl. space-*) / alignItems / flexWrap / alignSelf.\n else -> FlexboxLayout(context).apply {\n flexDirection = FlexDirection.COLUMN\n layoutOf(this)\n }\n }\n\n override fun makeText(text: String): View = TextView(context).apply { this.text = text }\n\n override fun setText(view: View, text: String) {\n (view as? TextView)?.text = text\n // If this text node is composed into a parent text element, re-compose the parent.\n textOwner[view]?.let { recomposeText(it) }\n }\n\n // --- Props ---\n\n override fun setProp(view: View, name: String, value: NativeProp) {\n when (name) {\n \"style\" -> (value as? NativeProp.Obj)?.let { applyStyle(view, it.value) }\n \"accessibilityLabel\", \"aria-label\", \"label\" -> view.contentDescription = strOf(value)\n \"title\", \"text\" -> (view as? TextView)?.text = strOf(value)\n \"placeholder\" -> (view as? EditText)?.hint = strOf(value)\n \"value\" -> (view as? EditText)?.let { if (it.text.toString() != strOf(value)) it.setText(strOf(value)) }\n \"hidden\" -> view.visibility = if (boolOf(value)) View.GONE else View.VISIBLE\n // Image source (data:/base64 + bundled asset load synchronously; remote is a follow-up).\n \"src\", \"source\" -> (view as? ImageView)?.let { applyImageSource(it, value) }\n \"resizeMode\" -> (view as? ImageView)?.let { it.scaleType = scaleTypeFor(strOf(value) ?: \"contain\") }\n \"tintColor\" -> (view as? ImageView)?.let { iv ->\n val c = color(value)\n if (c != null) iv.setColorFilter(c, PorterDuff.Mode.SRC_IN) else iv.clearColorFilter()\n }\n // TextInput: keyboard / multiline / secure / enabled / focus. `type`=\"password\" → secure.\n \"keyboardType\", \"inputMode\", \"type\" -> (view as? EditText)?.let {\n val s = strOf(value) ?: \"text\"\n if (s == \"password\") inputSpecOf(it).secure = true else inputSpecOf(it).keyboard = s\n applyInputType(it)\n }\n \"multiline\" -> (view as? EditText)?.let { inputSpecOf(it).multiline = boolOf(value); applyInputType(it) }\n \"secureTextEntry\" -> (view as? EditText)?.let { inputSpecOf(it).secure = boolOf(value); applyInputType(it) }\n \"editable\" -> (view as? EditText)?.let { it.isEnabled = boolOf(value) }\n \"disabled\" -> (view as? EditText)?.let { it.isEnabled = !boolOf(value) }\n \"autoFocus\" -> (view as? EditText)?.let { if (boolOf(value)) it.requestFocus() }\n // a11y/meta hints the DOM backend also emits — no-ops on this host.\n else -> Unit\n }\n }\n\n override fun removeProp(view: View, name: String) {\n when (name) {\n \"accessibilityLabel\", \"aria-label\", \"label\" -> view.contentDescription = null\n \"hidden\" -> view.visibility = View.VISIBLE\n \"editable\", \"disabled\" -> (view as? EditText)?.isEnabled = true\n \"secureTextEntry\" -> (view as? EditText)?.let { inputSpecOf(it).secure = false; applyInputType(it) }\n \"multiline\" -> (view as? EditText)?.let { inputSpecOf(it).multiline = false; applyInputType(it) }\n \"tintColor\" -> (view as? ImageView)?.clearColorFilter()\n else -> Unit\n }\n }\n\n // --- Tree structure ---\n\n override fun insert(parent: View, child: View, index: Int) {\n val target = contentFor(parent) // route scrollview children into its content host\n if (target is ViewGroup) {\n applyLayoutParams(child) // carry the child width/height/grow/alignSelf into FlexboxLayout params\n target.addView(child, index.coerceIn(0, target.childCount))\n reapplyGaps(target)\n return\n }\n // Leaf (e.g. a `text` element): compose text-node children into its text.\n val parts = textParts.getOrPut(parent) { mutableListOf() }\n parts.add(index.coerceIn(0, parts.size), child)\n textOwner[child] = parent\n recomposeText(parent)\n }\n\n override fun remove(parent: View, child: View) {\n val target = contentFor(parent)\n if (target is ViewGroup) {\n target.removeView(child)\n reapplyGaps(target)\n return\n }\n textOwner.remove(child)\n textParts[parent]?.remove(child)\n recomposeText(parent)\n }\n\n override fun dispose(view: View) {\n (view.parent as? ViewGroup)?.removeView(view)\n layouts.remove(view)\n imageGen.remove(view) // invalidate any in-flight remote image fetch for this view\n // A scrollview owns an inner content host — drop both.\n scrollContent.remove(view)?.let { layouts.remove(it) }\n // TextInput state: detach every watcher + drop the input spec (no View leaks).\n textWatchers.remove(view)?.let { byEvent ->\n (view as? EditText)?.let { et -> byEvent.values.forEach { et.removeTextChangedListener(it) } }\n }\n inputSpecs.remove(view)\n // Detach from any text composition it participated in (as child or as owner).\n textOwner.remove(view)?.let { owner ->\n textParts[owner]?.remove(view)\n recomposeText(owner)\n }\n textParts.remove(view)\n }\n\n // --- Events ---\n\n override fun addEvent(view: View, eventName: String, handlerId: String, fire: (value: String?) -> Unit) {\n // Atlas's Pressable emits `onClick` → `click`; the older hand-written demo used\n // `press`. Accept both; ignore pointer/keyboard/focus events this host doesn't model.\n when (eventName) {\n \"click\", \"press\" -> {\n view.isClickable = true\n view.setOnClickListener { fire(null) } // notify-only → no value\n }\n // Text change (Atlas onInput→\"input\", onChange→\"change\"). fire() carries the field's current\n // text, which the JS host wraps as `{ target: { value } }` so onInput/onChange receive it.\n \"input\", \"change\" -> (view as? EditText)?.let { et ->\n val byEvent = textWatchers.getOrPut(et) { HashMap() }\n byEvent.remove(eventName)?.let { et.removeTextChangedListener(it) } // replace only THIS event\n val watcher = object : TextWatcher {\n // Read the authoritative current field text (not `s`), coalescing null → \"\".\n override fun afterTextChanged(s: Editable?) = fire(et.text?.toString() ?: \"\")\n override fun beforeTextChanged(s: CharSequence?, st: Int, c: Int, a: Int) = Unit\n override fun onTextChanged(s: CharSequence?, st: Int, b: Int, c: Int) = Unit\n }\n et.addTextChangedListener(watcher)\n byEvent[eventName] = watcher\n }\n else -> Unit\n }\n }\n\n override fun removeEvent(view: View, eventName: String, handlerId: String) {\n when (eventName) {\n \"click\", \"press\" -> view.setOnClickListener(null)\n \"input\", \"change\" -> (view as? EditText)?.let { et ->\n textWatchers[et]?.let { byEvent ->\n byEvent.remove(eventName)?.let { et.removeTextChangedListener(it) }\n if (byEvent.isEmpty()) textWatchers.remove(et)\n }\n }\n else -> Unit\n }\n }\n\n // --- Style application ---\n\n private fun applyStyle(view: View, style: Map<String, NativeProp>) {\n // The flex container is `view` itself, OR a scrollview's inner content host. Sizing/visual\n // props stay on `view` (the viewport for a scrollview); flex props target the container.\n val container = contentFor(view) as? FlexboxLayout\n val text = view as? TextView // Button/EditText are TextView subclasses\n val selfLay = layoutOf(view) // this view's own size/grow/alignSelf as a child of its parent\n val contentLay = container?.let { layoutOf(it) } ?: selfLay // the flex container's direction/gap\n\n // Flex container: direction, justify (incl. space-*), align, wrap.\n (style[\"flexDirection\"] as? NativeProp.Str)?.value?.let { dir ->\n contentLay.horizontal = dir.startsWith(\"row\")\n container?.flexDirection = flexDirectionFor(dir)\n }\n (dimen(style[\"gap\"]) ?: dimen(style[\"rowGap\"]) ?: dimen(style[\"columnGap\"]))?.let {\n contentLay.gapPx = it\n reapplyGaps(container)\n }\n strOf(style[\"justifyContent\"])?.let { container?.justifyContent = justifyContentFor(it) }\n strOf(style[\"alignItems\"])?.let { container?.alignItems = alignItemsFor(it) }\n strOf(style[\"flexWrap\"])?.let { container?.flexWrap = flexWrapFor(it) }\n // Per-child cross-axis override (applied via the child's FlexboxLayout.LayoutParams on insert).\n strOf(style[\"alignSelf\"])?.let {\n selfLay.alignSelf = alignSelfFor(it)\n applyLayoutParams(view)\n }\n (numOf(style[\"flexGrow\"]) ?: numOf(style[\"flex\"]))?.let {\n selfLay.grow = it.toFloat()\n applyLayoutParams(view)\n }\n\n // Box model.\n sizeOf(style[\"width\"])?.let { selfLay.widthSpec = it; applyLayoutParams(view) }\n sizeOf(style[\"height\"])?.let { selfLay.heightSpec = it; applyLayoutParams(view) }\n dimen(style[\"minWidth\"])?.let { view.minimumWidth = it }\n dimen(style[\"minHeight\"])?.let { view.minimumHeight = it }\n applyPadding(view, style)\n\n // Visual.\n applyBackground(view, style)\n numOf(style[\"opacity\"])?.let { view.alpha = it.toFloat() }\n dimen(style[\"elevation\"])?.let { view.elevation = it.toFloat() } // shadow (px)\n\n // Text.\n if (text != null) applyText(text, style)\n\n // Spinner tint (ActivityIndicator → ProgressBar): `color` drives the indeterminate arc.\n (view as? ProgressBar)?.let { bar ->\n color(style[\"color\"])?.let { bar.indeterminateTintList = ColorStateList.valueOf(it) }\n }\n }\n\n private fun applyPadding(view: View, style: Map<String, NativeProp>) {\n if (style.keys.none { it == \"padding\" || it.startsWith(\"padding\") }) return\n val all = dimen(style[\"padding\"])\n val l = dimen(style[\"paddingLeft\"]) ?: all ?: view.paddingLeft\n val t = dimen(style[\"paddingTop\"]) ?: all ?: view.paddingTop\n val r = dimen(style[\"paddingRight\"]) ?: all ?: view.paddingRight\n val b = dimen(style[\"paddingBottom\"]) ?: all ?: view.paddingBottom\n view.setPadding(l, t, r, b)\n }\n\n private fun applyBackground(view: View, style: Map<String, NativeProp>) {\n val bg = color(style[\"backgroundColor\"])\n val radius = dimen(style[\"borderRadius\"])\n val borderW = dimen(style[\"borderWidth\"])\n val borderC = color(style[\"borderColor\"])\n val tl = dimen(style[\"borderTopLeftRadius\"])\n val tr = dimen(style[\"borderTopRightRadius\"])\n val br = dimen(style[\"borderBottomRightRadius\"])\n val bl = dimen(style[\"borderBottomLeftRadius\"])\n val perCorner = tl != null || tr != null || br != null || bl != null\n if (bg == null && radius == null && borderW == null && borderC == null && !perCorner) return\n val drawable = (view.background as? GradientDrawable) ?: GradientDrawable()\n bg?.let { drawable.setColor(it) }\n if (perCorner) {\n // Per-corner radii fall back to the uniform borderRadius (or 0). Order: TL, TR, BR, BL.\n val t = (tl ?: radius ?: 0).toFloat()\n val r = (tr ?: radius ?: 0).toFloat()\n val b = (br ?: radius ?: 0).toFloat()\n val l = (bl ?: radius ?: 0).toFloat()\n drawable.cornerRadii = floatArrayOf(t, t, r, r, b, b, l, l)\n } else {\n radius?.let { drawable.cornerRadius = it.toFloat() }\n }\n if (borderW != null || borderC != null) {\n drawable.setStroke(borderW ?: 0, borderC ?: Color.TRANSPARENT)\n }\n view.background = drawable\n }\n\n private fun applyText(text: TextView, style: Map<String, NativeProp>) {\n color(style[\"color\"])?.let { text.setTextColor(it) }\n numOf(style[\"fontSize\"])?.let { text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, it.toFloat()) }\n fontWeightBold(style[\"fontWeight\"])?.let { bold ->\n // Two-arg setTypeface: uses a real bold face when available, else falls back to\n // synthetic (fake) bold — so weight always takes visible effect.\n text.setTypeface(text.typeface, if (bold) Typeface.BOLD else Typeface.NORMAL)\n }\n (style[\"textAlign\"] as? NativeProp.Str)?.value?.let { text.gravity = textGravity(it) }\n // numberOfLines → clamp lines + ellipsize the tail (RN's `numberOfLines`).\n numOf(style[\"numberOfLines\"])?.let { n ->\n val lines = n.toInt()\n if (lines > 0) {\n text.maxLines = lines\n text.ellipsize = TextUtils.TruncateAt.END\n }\n }\n }\n\n // --- Image + TextInput ---\n\n /**\n * Load an image `src`/`source` into an ImageView. `data:`/base64 + bundled assets decode\n * synchronously (deterministic, no I/O on the network); remote http(s) is a deliberate follow-up\n * (it needs an off-main fetch executor + a renderer lifecycle hook to reclaim it). Any\n * unresolvable/garbage source is ignored — never crashes the host.\n */\n private fun applyImageSource(iv: ImageView, value: NativeProp) {\n val uri = strOf(value)\n ?: (value as? NativeProp.Obj)?.value?.get(\"uri\")?.let { strOf(it) }\n ?: return\n try {\n val bitmap = when {\n uri.startsWith(\"data:\") -> {\n val payload = uri.substringAfter(\"base64,\", \"\")\n if (payload.isEmpty()) {\n null\n } else {\n val bytes = Base64.decode(payload, Base64.DEFAULT)\n BitmapFactory.decodeByteArray(bytes, 0, bytes.size)\n }\n }\n // Remote: fetch off-main (via imageLoader, default HTTP), decode, set on the main thread —\n // guarded by a generation token so a stale/slow fetch never clobbers a newer src/disposed view.\n uri.startsWith(\"http://\") || uri.startsWith(\"https://\") -> {\n val token = (imageGen[iv] ?: 0L) + 1\n imageGen[iv] = token\n (imageLoader ?: ::defaultImageLoad)(uri) { bytes ->\n val bmp = bytes?.let {\n runCatching { BitmapFactory.decodeByteArray(it, 0, it.size) }.getOrNull()\n }\n if (bmp != null) iv.post { if (imageGen[iv] == token) iv.setImageBitmap(bmp) }\n }\n null // async — nothing to set synchronously\n }\n else -> {\n val name = uri.removePrefix(\"asset:///\").removePrefix(\"file:///android_asset/\")\n context.assets.open(name).use { BitmapFactory.decodeStream(it) }\n }\n }\n bitmap?.let { iv.setImageBitmap(it) }\n } catch (_: Throwable) {\n // ignore: an unresolvable/garbage src must never crash the host\n }\n }\n\n /** Default remote loader: fetch the URL off the main thread via HttpURLConnection. */\n private fun defaultImageLoad(url: String, onResult: (ByteArray?) -> Unit) {\n imageExecutor.execute {\n val bytes = runCatching {\n val conn = (URL(url).openConnection() as HttpURLConnection).apply {\n connectTimeout = 10_000\n readTimeout = 10_000\n doInput = true\n }\n try {\n conn.inputStream.use { it.readBytes() }\n } finally {\n conn.disconnect()\n }\n }.getOrNull()\n onResult(bytes)\n }\n }\n\n private fun scaleTypeFor(mode: String): ImageView.ScaleType = when (mode) {\n \"cover\" -> ImageView.ScaleType.CENTER_CROP\n \"stretch\" -> ImageView.ScaleType.FIT_XY\n \"center\" -> ImageView.ScaleType.CENTER\n else -> ImageView.ScaleType.FIT_CENTER // \"contain\" / default\n }\n\n /** Recompute an EditText's inputType from its {keyboard, secure, multiline} spec (order-independent). */\n private fun applyInputType(et: EditText) {\n val spec = inputSpecs[et] ?: return\n var type = when (spec.keyboard) {\n \"number\", \"numeric\", \"number-pad\" -> InputType.TYPE_CLASS_NUMBER\n \"decimal\", \"decimal-pad\" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL\n \"phone\", \"tel\" -> InputType.TYPE_CLASS_PHONE\n \"email\", \"email-address\" ->\n InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS\n \"url\" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI\n else -> InputType.TYPE_CLASS_TEXT\n }\n if (spec.multiline) {\n et.isSingleLine = false // set before inputType (singleLine mutates inputType)\n et.gravity = Gravity.TOP or Gravity.START\n type = type or InputType.TYPE_TEXT_FLAG_MULTI_LINE\n }\n if (spec.secure) {\n // Clear any existing variation (email/url/phone) and apply ONLY the password variation —\n // OR-ing onto a non-default variation yields a combined value Android won't mask.\n val isNumber = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER\n type = type and InputType.TYPE_MASK_VARIATION.inv()\n type = type or\n if (isNumber) InputType.TYPE_NUMBER_VARIATION_PASSWORD\n else InputType.TYPE_TEXT_VARIATION_PASSWORD\n }\n et.inputType = type\n }\n\n // --- LayoutParams + gaps ---\n\n private fun applyLayoutParams(view: View) {\n val lay = layoutOf(view)\n val lp = (view.layoutParams as? FlexboxLayout.LayoutParams)\n ?: FlexboxLayout.LayoutParams(lay.widthSpec, lay.heightSpec)\n lp.width = lay.widthSpec\n lp.height = lay.heightSpec\n lp.flexGrow = lay.grow\n lp.alignSelf = lay.alignSelf\n view.layoutParams = lp\n }\n\n /** Recreate gaps as leading margins on every child (all but the first along the main axis). */\n private fun reapplyGaps(parent: View?) {\n val container = parent as? FlexboxLayout ?: return\n val lay = layouts[container] ?: return\n for (i in 0 until container.childCount) {\n val child = container.getChildAt(i)\n val lp = child.layoutParams as? FlexboxLayout.LayoutParams ?: continue\n val lead = if (i == 0) 0 else lay.gapPx\n if (lay.horizontal) {\n lp.leftMargin = lead\n lp.topMargin = 0\n } else {\n lp.topMargin = lead\n lp.leftMargin = 0\n }\n child.layoutParams = lp\n }\n }\n\n // --- Flex enum mappings (Atlas/CSS strings → FlexboxLayout constants) ---\n\n private fun flexDirectionFor(dir: String): Int = when (dir) {\n \"row\" -> FlexDirection.ROW\n \"row-reverse\" -> FlexDirection.ROW_REVERSE\n \"column-reverse\" -> FlexDirection.COLUMN_REVERSE\n else -> FlexDirection.COLUMN\n }\n\n private fun justifyContentFor(justify: String): Int = when (justify) {\n \"flex-end\", \"end\" -> JustifyContent.FLEX_END\n \"center\" -> JustifyContent.CENTER\n \"space-between\" -> JustifyContent.SPACE_BETWEEN\n \"space-around\" -> JustifyContent.SPACE_AROUND\n \"space-evenly\" -> JustifyContent.SPACE_EVENLY\n else -> JustifyContent.FLEX_START\n }\n\n private fun alignItemsFor(align: String): Int = when (align) {\n \"flex-end\", \"end\" -> AlignItems.FLEX_END\n \"center\" -> AlignItems.CENTER\n \"baseline\" -> AlignItems.BASELINE\n \"flex-start\", \"start\" -> AlignItems.FLEX_START\n else -> AlignItems.STRETCH\n }\n\n private fun flexWrapFor(wrap: String): Int = when (wrap) {\n \"wrap\" -> FlexWrap.WRAP\n \"wrap-reverse\" -> FlexWrap.WRAP_REVERSE\n else -> FlexWrap.NOWRAP\n }\n\n private fun alignSelfFor(align: String): Int = when (align) {\n \"flex-end\", \"end\" -> AlignSelf.FLEX_END\n \"center\" -> AlignSelf.CENTER\n \"baseline\" -> AlignSelf.BASELINE\n \"stretch\" -> AlignSelf.STRETCH\n \"flex-start\", \"start\" -> AlignSelf.FLEX_START\n else -> AlignSelf.AUTO\n }\n\n // --- Value coercion (defensive: unknown/ill-typed values are ignored, never crash) ---\n\n private fun numOf(prop: NativeProp?): Double? = (prop as? NativeProp.Num)?.value\n private fun strOf(prop: NativeProp?): String? = (prop as? NativeProp.Str)?.value\n private fun boolOf(prop: NativeProp?): Boolean = (prop as? NativeProp.Bool)?.value == true\n\n /** A numeric dp dimension, or null for strings/absent (percent sizes go via [sizeOf]). */\n private fun dimen(prop: NativeProp?): Int? = (prop as? NativeProp.Num)?.let { dp(it.value) }\n\n /** A width/height spec: number → dp, `\"100%\"` → MATCH_PARENT, `\"auto\"` → WRAP_CONTENT. */\n private fun sizeOf(prop: NativeProp?): Int? = when (prop) {\n is NativeProp.Num -> dp(prop.value)\n is NativeProp.Str -> when (prop.value) {\n \"100%\" -> ViewGroup.LayoutParams.MATCH_PARENT\n \"auto\" -> ViewGroup.LayoutParams.WRAP_CONTENT\n else -> null\n }\n else -> null\n }\n\n private fun color(prop: NativeProp?): Int? {\n val raw = (prop as? NativeProp.Str)?.value ?: return null\n return try {\n Color.parseColor(raw)\n } catch (_: IllegalArgumentException) {\n null\n }\n }\n\n /** true → bold, false → normal, null → leave unchanged. `>= 600` (or `\"bold\"`) is bold. */\n private fun fontWeightBold(prop: NativeProp?): Boolean? = when (prop) {\n is NativeProp.Num -> prop.value >= 600\n is NativeProp.Str -> prop.value == \"bold\" || (prop.value.toIntOrNull() ?: 0) >= 600\n else -> null\n }\n\n private fun textGravity(align: String): Int = when (align) {\n \"center\" -> Gravity.CENTER\n \"right\" -> Gravity.END\n \"justify\" -> Gravity.START // no native justify on older APIs; left-align\n else -> Gravity.START\n }\n\n /** main axis ← justifyContent, cross axis ← alignItems. `space-*` falls back to center. */\n}\n",
|
|
25
25
|
"mindees-host/src/main/kotlin/dev/mindees/host/MindeesNativeHost.kt": "/*\n * MindeesNativeHost.kt — applies a NativeCommand stream to a pluggable renderer,\n * with strict validation mirroring @mindees/renderer's reference host.\n *\n * The host owns identity + structure bookkeeping (and validates the stream); a\n * [HostRenderer] builds/mutates the actual views. Use [AndroidViewRenderer] on a\n * device, or [ModelRenderer] in JVM unit tests (`./gradlew test`, no device needed).\n *\n * CI-verified by the native Android workflow; see the module README.\n */\n\npackage dev.mindees.host\n\n/** Thrown when a command stream violates the host contract. */\nclass NativeHostException(message: String) : RuntimeException(message)\n\n/**\n * A pluggable platform renderer. The host calls these to materialize the UI.\n * [V] is the platform node type (`android.view.View`, or `ModelNode` in tests).\n */\ninterface HostRenderer<V> {\n fun makeElement(tag: String): V\n fun makeText(text: String): V\n fun setText(view: V, text: String)\n fun setProp(view: V, name: String, value: NativeProp)\n fun removeProp(view: V, name: String)\n fun insert(parent: V, child: V, index: Int)\n fun remove(parent: V, child: V)\n /**\n * Wire a native event; the renderer calls [fire] when it occurs, passing an optional value\n * (the text for an `input`/`change` event; `null` for notify-only events like press/click).\n */\n fun addEvent(view: V, eventName: String, handlerId: String, fire: (value: String?) -> Unit)\n fun removeEvent(view: V, eventName: String, handlerId: String)\n /** Free a node's resources (already detached from its parent). */\n fun dispose(view: V)\n}\n\n/**\n * Applies a [NativeCommand] stream to a [HostRenderer], strictly validating it\n * (throws [NativeHostException] on any malformed/leaking sequence) — the contract\n * `@mindees/renderer`'s `createReferenceHost()` enforces and tests.\n *\n * @param onEvent invoked with a `handlerId` (and an optional text `value` for input/change events,\n * `null` for notify-only events) when a wired native event fires; forward it to the JS runtime's\n * `MindeesApp.dispatchEvent(handlerId, value)`.\n */\nclass MindeesNativeHost<V>(\n private val rootId: String,\n root: V,\n private val renderer: HostRenderer<V>,\n private val onEvent: (handlerId: String, value: String?) -> Unit,\n) {\n private val views = HashMap<String, V>()\n private val parentOf = HashMap<String, String>()\n private val childrenOf = HashMap<String, MutableList<String>>()\n\n init {\n views[rootId] = root\n childrenOf[rootId] = mutableListOf()\n }\n\n /** Live (created, not yet disposed) node count, excluding the root. */\n val liveNodeCount: Int get() = views.size - 1\n\n fun apply(batch: List<NativeCommand>) {\n for (command in batch) apply(command)\n }\n\n fun apply(command: NativeCommand) {\n when (command) {\n is NativeCommand.CreateNode -> {\n requireAbsent(command.id)\n views[command.id] = renderer.makeElement(command.tag)\n childrenOf[command.id] = mutableListOf()\n }\n is NativeCommand.CreateText -> {\n requireAbsent(command.id)\n views[command.id] = renderer.makeText(command.text)\n childrenOf[command.id] = mutableListOf()\n }\n is NativeCommand.UpdateText -> renderer.setText(view(command.id), command.text)\n is NativeCommand.SetProp -> renderer.setProp(view(command.id), command.name, command.value)\n is NativeCommand.RemoveProp -> renderer.removeProp(view(command.id), command.name)\n is NativeCommand.InsertChild -> {\n if (parentOf.containsKey(command.childId)) {\n throw NativeHostException(\"insertChild: ${command.childId} already has a parent\")\n }\n val parent = view(command.parentId)\n val child = view(command.childId)\n val kids = childrenOf.getOrPut(command.parentId) { mutableListOf() }\n if (command.index < 0 || command.index > kids.size) {\n throw NativeHostException(\"insertChild: index ${command.index} out of range\")\n }\n kids.add(command.index, command.childId)\n parentOf[command.childId] = command.parentId\n renderer.insert(parent, child, command.index)\n }\n is NativeCommand.RemoveChild -> {\n val kids = childrenOf[command.parentId]\n if (parentOf[command.childId] != command.parentId || kids == null ||\n !kids.remove(command.childId)\n ) {\n throw NativeHostException(\"removeChild: ${command.childId} is not a child of ${command.parentId}\")\n }\n parentOf.remove(command.childId)\n renderer.remove(view(command.parentId), view(command.childId))\n }\n is NativeCommand.DisposeNode -> {\n if (command.id == rootId) throw NativeHostException(\"cannot dispose the root node\")\n val v = views[command.id] ?: throw NativeHostException(\"double dispose of ${command.id}\")\n // Interior subtree nodes are freed without an explicit removeChild, so\n // detach from BOTH the bookkeeping and the renderer tree here. (A renderer\n // whose dispose() is a no-op — e.g. ModelRenderer — would otherwise leave\n // the node in its parent's children even though we consider it detached.)\n parentOf.remove(command.id)?.let { pid ->\n childrenOf[pid]?.remove(command.id)\n views[pid]?.let { parentView -> renderer.remove(parentView, v) }\n }\n renderer.dispose(v)\n views.remove(command.id)\n childrenOf.remove(command.id)\n }\n is NativeCommand.RegisterEvent -> {\n val target = view(command.id)\n val handlerId = command.handlerId\n renderer.addEvent(target, command.eventName, handlerId) { value -> onEvent(handlerId, value) }\n }\n is NativeCommand.UnregisterEvent ->\n renderer.removeEvent(view(command.id), command.eventName, command.handlerId)\n }\n }\n\n private fun view(id: String): V = views[id] ?: throw NativeHostException(\"unknown node $id\")\n\n private fun requireAbsent(id: String) {\n if (views.containsKey(id)) throw NativeHostException(\"duplicate node id $id\")\n }\n}\n\n// --- In-memory renderer for JVM unit tests (no Android) ---\n\n/** An in-memory model node the [ModelRenderer] builds. */\nclass ModelNode(val kind: String, var tag: String, var text: String) {\n val props = HashMap<String, NativeProp>()\n val events = HashMap<String, String>() // eventName -> handlerId\n val children = ArrayList<ModelNode>()\n\n /** A compact structural string (tags + text) for assertions. */\n fun serialize(): String =\n if (kind == \"text\") text\n else \"<$tag>\" + children.joinToString(\"\") { it.serialize() } + \"</$tag>\"\n}\n\n/** A [HostRenderer] that builds a [ModelNode] tree — used by unit tests. */\nclass ModelRenderer : HostRenderer<ModelNode> {\n override fun makeElement(tag: String) = ModelNode(\"element\", tag, \"\")\n override fun makeText(text: String) = ModelNode(\"text\", \"\", text)\n override fun setText(view: ModelNode, text: String) { view.text = text }\n override fun setProp(view: ModelNode, name: String, value: NativeProp) { view.props[name] = value }\n override fun removeProp(view: ModelNode, name: String) { view.props.remove(name) }\n override fun insert(parent: ModelNode, child: ModelNode, index: Int) { parent.children.add(index, child) }\n override fun remove(parent: ModelNode, child: ModelNode) { parent.children.remove(child) }\n override fun addEvent(view: ModelNode, eventName: String, handlerId: String, fire: (value: String?) -> Unit) {\n view.events[eventName] = handlerId\n }\n override fun removeEvent(view: ModelNode, eventName: String, handlerId: String) {\n view.events.remove(eventName)\n }\n override fun dispose(view: ModelNode) { view.children.clear() }\n}\n",
|
|
26
26
|
"mindees-host/src/main/kotlin/dev/mindees/host/NativeCommand.kt": "/*\n * NativeCommand.kt — the wire model for the MindeesNative native command protocol.\n *\n * Mirrors `@mindees/renderer`'s `native-protocol.ts`. The sealed types are pure\n * Kotlin (unit-testable on the JVM); `NativeCommandCodec` decodes JSON via\n * org.json (Android) and is exercised on-device.\n *\n * CI-verified by the native Android workflow; see the module README.\n */\n\npackage dev.mindees.host\n\nimport org.json.JSONArray\nimport org.json.JSONObject\n\n/** A serializable prop value (mirrors `NativePropValue`). Carries no functions. */\nsealed interface NativeProp {\n data class Str(val value: String) : NativeProp\n data class Num(val value: Double) : NativeProp\n data class Bool(val value: Boolean) : NativeProp\n data object Null : NativeProp\n data class Arr(val value: List<NativeProp>) : NativeProp\n data class Obj(val value: Map<String, NativeProp>) : NativeProp\n}\n\n/** One native command (mirrors the `NativeCommand` union). */\nsealed interface NativeCommand {\n data class CreateNode(val id: String, val tag: String) : NativeCommand\n data class CreateText(val id: String, val text: String) : NativeCommand\n data class SetProp(val id: String, val name: String, val value: NativeProp) : NativeCommand\n data class RemoveProp(val id: String, val name: String) : NativeCommand\n data class InsertChild(val parentId: String, val childId: String, val index: Int) : NativeCommand\n data class RemoveChild(val parentId: String, val childId: String) : NativeCommand\n data class UpdateText(val id: String, val text: String) : NativeCommand\n data class DisposeNode(val id: String) : NativeCommand\n data class RegisterEvent(val id: String, val eventName: String, val handlerId: String) : NativeCommand\n data class UnregisterEvent(val id: String, val eventName: String, val handlerId: String) : NativeCommand\n}\n\n/** Decodes a JSON command stream into [NativeCommand]s (Android: uses org.json). */\nobject NativeCommandCodec {\n fun decodeBatch(json: String): List<NativeCommand> {\n val array = JSONArray(json)\n return (0 until array.length()).map { decode(array.getJSONObject(it)) }\n }\n\n fun decode(o: JSONObject): NativeCommand {\n // Node ids are string|number on the wire; normalize to String.\n fun id(key: String): String = o.get(key).toString()\n return when (val type = o.getString(\"type\")) {\n \"createNode\" -> NativeCommand.CreateNode(id(\"id\"), o.getString(\"tag\"))\n \"createText\" -> NativeCommand.CreateText(id(\"id\"), o.getString(\"text\"))\n \"setProp\" -> NativeCommand.SetProp(id(\"id\"), o.getString(\"name\"), decodeProp(o.get(\"value\")))\n \"removeProp\" -> NativeCommand.RemoveProp(id(\"id\"), o.getString(\"name\"))\n \"insertChild\" -> NativeCommand.InsertChild(id(\"parentId\"), id(\"childId\"), o.getInt(\"index\"))\n \"removeChild\" -> NativeCommand.RemoveChild(id(\"parentId\"), id(\"childId\"))\n \"updateText\" -> NativeCommand.UpdateText(id(\"id\"), o.getString(\"text\"))\n \"disposeNode\" -> NativeCommand.DisposeNode(id(\"id\"))\n \"registerEvent\" ->\n NativeCommand.RegisterEvent(id(\"id\"), o.getString(\"eventName\"), o.getString(\"handlerId\"))\n \"unregisterEvent\" ->\n NativeCommand.UnregisterEvent(id(\"id\"), o.getString(\"eventName\"), o.getString(\"handlerId\"))\n else -> throw IllegalArgumentException(\"unknown command type $type\")\n }\n }\n\n private fun decodeProp(value: Any?): NativeProp = when (value) {\n null, JSONObject.NULL -> NativeProp.Null\n is Boolean -> NativeProp.Bool(value)\n is Int -> NativeProp.Num(value.toDouble())\n is Long -> NativeProp.Num(value.toDouble())\n is Double -> NativeProp.Num(value)\n is String -> NativeProp.Str(value)\n is JSONArray -> NativeProp.Arr((0 until value.length()).map { decodeProp(value.get(it)) })\n is JSONObject -> NativeProp.Obj(value.keys().asSequence().associateWith { decodeProp(value.get(it)) })\n else -> NativeProp.Str(value.toString())\n }\n}\n",
|
|
27
|
-
"settings.gradle.kts": "// CI-verified through .github/workflows/native-android.yml. Open in Android\n// Studio (it provides Gradle + the wrapper) or run with a local Gradle. Align\n// the plugin/SDK versions below with your installed toolchain if needed.\n\npluginManagement {\n repositories {\n google()\n mavenCentral()\n gradlePluginPortal()\n }\n plugins {\n // Latest stable as of June 2026. AGP 9 has BUILT-IN Kotlin support, so the\n // org.jetbrains.kotlin.android plugin is no longer applied (AGP errors if it\n // is). Align AGP with your toolchain if Gradle complains (AGP 9.x needs\n // JDK 17+ and a recent Gradle).\n id(\"com.android.application\") version \"9.2.0\"\n id(\"com.android.library\") version \"9.2.0\"\n }\n}\n\ndependencyResolutionManagement {\n repositories {\n google()\n mavenCentral()\n }\n}\n\nrootProject.name = \"
|
|
27
|
+
"settings.gradle.kts": "// CI-verified through .github/workflows/native-android.yml. Open in Android\n// Studio (it provides Gradle + the wrapper) or run with a local Gradle. Align\n// the plugin/SDK versions below with your installed toolchain if needed.\n\npluginManagement {\n repositories {\n google()\n mavenCentral()\n gradlePluginPortal()\n }\n plugins {\n // Latest stable as of June 2026. AGP 9 has BUILT-IN Kotlin support, so the\n // org.jetbrains.kotlin.android plugin is no longer applied (AGP errors if it\n // is). Align AGP with your toolchain if Gradle complains (AGP 9.x needs\n // JDK 17+ and a recent Gradle).\n id(\"com.android.application\") version \"9.2.0\"\n id(\"com.android.library\") version \"9.2.0\"\n }\n}\n\ndependencyResolutionManagement {\n repositories {\n google()\n mavenCentral()\n }\n}\n\nrootProject.name = \"{{appName}}\"\ninclude(\":mindees-host\")\ninclude(\":mindees-example-app\")\n"
|
|
28
28
|
};
|
|
29
29
|
//#endregion
|
|
30
30
|
export { ANDROID_TEMPLATE_FILES };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"android-template.generated.js","names":[],"sources":["../src/android-template.generated.ts"],"sourcesContent":["// @generated by scripts/gen-android-template.mjs — do not edit by hand.\n// Regenerate: `pnpm --filter @mindees/cli run gen:android-template`\n// Source of truth: examples/native-hosts/android/ (the CI-verified reference host).\n\n/** Files the experimental `android` template scaffolds (project-relative path → contents). */\nexport const ANDROID_TEMPLATE_FILES: Record<string, string> = {\n \".gitignore\": \"# Android build outputs and local caches\\n.gradle/\\n.kotlin/\\nbuild/\\n\\n# The Gradle wrapper is bootstrapped in CI (see .github/workflows/native-android.yml)\\n# and locally on demand, not committed.\\n/gradlew\\n/gradlew.bat\\n/gradle/wrapper/\\n\\n# Local SDK pointer\\nlocal.properties\\n\\n# Generated file-based route map (codegen: app-js/scripts/gen-routes.mjs)\\nmindees-example-app/app-js/src/routes.gen.ts\\n\\n# The JS bundle is generated by the app-js build (never vendored).\\nmindees-example-app/src/main/assets/mindees-app.bundle.js\\n\",\n \"README.md\": \"# MindeesNative — Android app (experimental)\\n\\nA standalone native **Android** app built with MindeesNative. The UI is authored in\\nTypeScript/TSX (Atlas components + the Quantum file-based router) and runs on a real\\nAndroid view tree through an **embedded QuickJS** runtime. The native host\\n(`mindees-host/`) is **vendored as Kotlin source** — no Maven dependency on\\nMindeesNative is required.\\n\\n> **Experimental.** This scaffold is verified end-to-end in CI (the app-js bundle\\n> builds from npm and `gradle assembleDebug` produces an APK containing it), but the\\n> native mobile product is pre-1.0. Expect rough edges.\\n\\n## Prerequisites\\n\\n- **JDK 17**\\n- **Gradle 9.4.1+** (AGP 9.2 requires it) — used once to bootstrap the wrapper\\n- **Android SDK** with `platforms;android-36`\\n- **Node 20+** (for the app-js bundle)\\n\\n## Build (two phases, in order)\\n\\nThe native app loads `mindees-example-app/src/main/assets/mindees-app.bundle.js`,\\nwhich is **generated** from the TSX app — it is git-ignored and absent until you build\\nit. Build the JS bundle **first**, then the APK:\\n\\n```sh\\n# 1. Build the JS app bundle (installs @mindees/* from npm, runs route codegen + tsdown)\\ncd mindees-example-app/app-js\\nnpm install\\nnpm run build # → ../src/main/assets/mindees-app.bundle.js\\n\\n# 2. Build the Android APK\\ncd ../..\\ngradle wrapper --gradle-version 9.4.1 # one-time: the wrapper jar isn't vendored\\n./gradlew :mindees-example-app:assembleDebug\\n# → mindees-example-app/build/outputs/apk/debug/*.apk\\n```\\n\\nInstall the APK on a device/emulator with `./gradlew :mindees-example-app:installDebug`.\\n\\n## Project layout\\n\\n```\\nmindees-host/ # the native renderer + JS↔native bridge — VENDORED Kotlin source\\nmindees-example-app/\\n app-js/ # your TypeScript/TSX app (Atlas + router); builds the JS bundle\\n src/app/ # file-based routes — drop a .tsx file to add a screen\\n src/main.tsx # entry: createNativeApp(...)\\n src/main/kotlin/ # the Android host shell (MainActivity, QuickJS bridge)\\n```\\n\\nEdit your UI under `mindees-example-app/app-js/src/`, re-run the app-js build, then\\nre-run `assembleDebug`.\\n\\n## Notes & limitations (experimental)\\n\\n- The Android package id is `dev.mindees.example` and `rootProject.name` is fixed, so\\n two scaffolds can't be installed side-by-side yet (namespace parameterization is a\\n planned enhancement).\\n- App-level Android dependencies (QuickJS, AndroidX, FlexboxLayout) still come from\\n Maven Central / Google — \\\"no Maven\\\" applies to the **MindeesNative host**, which is\\n vendored as source.\\n\",\n \"build.gradle.kts\": \"// Root build file. Plugin versions are declared in settings.gradle.kts\\n// (pluginManagement), so modules apply them without a version here.\\n\",\n \"gradle.properties\": \"android.useAndroidX=true\\nkotlin.code.style=official\\norg.gradle.jvmargs=-Xmx2048m\\n\",\n \"mindees-example-app/app-js/package.json\": \"{\\n \\\"name\\\": \\\"mindees-android-app-js\\\",\\n \\\"version\\\": \\\"0.1.0\\\",\\n \\\"private\\\": true,\\n \\\"type\\\": \\\"module\\\",\\n \\\"scripts\\\": {\\n \\\"gen-routes\\\": \\\"node scripts/gen-routes.mjs\\\",\\n \\\"build\\\": \\\"npm run gen-routes && tsdown --config tsdown.config.ts\\\",\\n \\\"typecheck\\\": \\\"tsc --noEmit\\\"\\n },\\n \\\"dependencies\\\": {\\n \\\"@mindees/core\\\": \\\"0.19.0\\\",\\n \\\"@mindees/atlas\\\": \\\"0.19.0\\\",\\n \\\"@mindees/renderer\\\": \\\"0.19.0\\\",\\n \\\"@mindees/router\\\": \\\"0.19.0\\\",\\n \\\"@mindees/compiler\\\": \\\"0.19.0\\\"\\n },\\n \\\"devDependencies\\\": {\\n \\\"tsdown\\\": \\\"0.22.1\\\",\\n \\\"typescript\\\": \\\"6.0.3\\\"\\n }\\n}\\n\",\n \"mindees-example-app/app-js/scripts/gen-routes.mjs\": \"/**\\n * File-based-routing codegen. Scans `src/app/` and writes `src/routes.gen.ts` — a\\n * static-import module map the app feeds to `createFileRouter` (the QuickJS bundle has\\n * no `import.meta.glob`). Run by `npm run build` before bundling. The reusable codegen\\n * lives in `@mindees/compiler` (`generateRouteModule`); this script supplies the file\\n * list. `routes.gen.ts` is generated (git-ignored) — never edit it by hand.\\n */\\n\\nimport { readdirSync, statSync, writeFileSync } from 'node:fs'\\nimport { dirname, join } from 'node:path'\\nimport { fileURLToPath } from 'node:url'\\nimport { generateRouteModule } from '@mindees/compiler'\\n\\nconst here = dirname(fileURLToPath(import.meta.url))\\nconst appDir = join(here, '..', 'src', 'app')\\nconst outFile = join(here, '..', 'src', 'routes.gen.ts')\\n\\n/** Collect route files (relative, POSIX), skipping tests. */\\nfunction collect(dir, base = '') {\\n const out = []\\n for (const name of readdirSync(dir)) {\\n const full = join(dir, name)\\n const rel = base ? `${base}/${name}` : name\\n if (statSync(full).isDirectory()) out.push(...collect(full, rel))\\n else if (/\\\\.(tsx|ts|jsx|js)$/.test(name) && !/\\\\.test\\\\./.test(name)) out.push(rel)\\n }\\n return out\\n}\\n\\nconst files = collect(appDir)\\nwriteFileSync(outFile, generateRouteModule(files, { importBase: './app' }))\\nprocess.stdout.write(`gen-routes: wrote ${files.length} route(s) to src/routes.gen.ts\\\\n`)\\n\",\n \"mindees-example-app/app-js/src/App.tsx\": \"/**\\n * The example app — file-based routing, `@mindees/*` only, plain TSX.\\n *\\n * Routes live in `app/` (app/index.tsx → `/`, app/about.tsx → `/about`); the screens\\n * use the `useRouter()` hook (no router prop-drilling). `createFileRouter` turns the\\n * module map into a Quantum router with Expo-style conventions but a stronger core\\n * (validated params, fine-grained reads, codegen-free typing).\\n *\\n * @module\\n */\\n\\nimport { Column, space, useTheme } from '@mindees/atlas'\\nimport { createFileRouter, createMemoryHistory, createRouterView } from '@mindees/router'\\nimport { routes } from './routes.gen'\\n\\n// `routes` is generated from the `app/` directory (scripts/gen-routes.mjs +\\n// @mindees/compiler `generateRouteModule`), so adding a file under `app/` adds a route\\n// with no edits here. createFileRouter applies the Expo-style conventions.\\nconst router = createFileRouter(routes, {\\n history: createMemoryHistory({ initialEntries: ['/'] }),\\n})\\n\\n/** Full-screen shell: themed background (re-themes light↔dark), centers the active route. */\\nexport function App() {\\n const theme = useTheme()\\n return (\\n <Column\\n style={() => ({\\n flexGrow: 1,\\n width: '100%',\\n padding: space.lg,\\n alignItems: 'center',\\n justifyContent: 'center',\\n backgroundColor: theme().color.bg,\\n })}\\n >\\n {createRouterView(router)}\\n </Column>\\n )\\n}\\n\",\n \"mindees-example-app/app-js/src/app/about.tsx\": \"/**\\n * About route — `app/about.tsx` maps to `/about`. Showcases Atlas components\\n * (Card, Badge, Divider, Switch, ProgressBar) + design-token theming: the Switch\\n * toggles the device color scheme, so the whole UI re-themes light↔dark.\\n *\\n * @module\\n */\\n\\nimport {\\n ActivityIndicator,\\n Badge,\\n Button,\\n Card,\\n Divider,\\n fontSize,\\n ProgressBar,\\n Row,\\n Switch,\\n setEnvironment,\\n space,\\n Text,\\n useColorScheme,\\n useTheme,\\n} from '@mindees/atlas'\\nimport { useRouter } from '@mindees/router'\\nimport { buttonShape } from '../theme'\\n\\nexport default function About() {\\n const router = useRouter()\\n const theme = useTheme()\\n const colorScheme = useColorScheme()\\n return (\\n <Card variant=\\\"filled\\\" style={{ minWidth: 300, gap: space.md, alignItems: 'stretch' }}>\\n <Row style={{ justifyContent: 'space-between', alignItems: 'center' }}>\\n <Text\\n style={() => ({ fontSize: fontSize.title2, fontWeight: 800, color: theme().color.text })}\\n >\\n About\\n </Text>\\n <Badge tone=\\\"info\\\">v0.1.0</Badge>\\n </Row>\\n <Divider />\\n <Text\\n style={() => ({ fontSize: fontSize.body, color: theme().color.textMuted, lineHeight: 22 })}\\n >\\n File-based routes navigated by the Quantum router via the useRouter hook — built from themed\\n Atlas components, all TypeScript, running native in an embedded engine.\\n </Text>\\n <Row style={{ justifyContent: 'space-between', alignItems: 'center' }}>\\n <Text style={() => ({ fontSize: fontSize.body, color: theme().color.text })}>\\n Dark mode\\n </Text>\\n <Switch\\n value={() => colorScheme() === 'dark'}\\n onValueChange={(v) => setEnvironment({ colorScheme: v ? 'dark' : 'light' })}\\n label=\\\"Dark mode\\\"\\n />\\n </Row>\\n <Row style={{ gap: space.sm, alignItems: 'center' }}>\\n <ActivityIndicator size={20} />\\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\\n Syncing…\\n </Text>\\n </Row>\\n <ProgressBar value={0.6} />\\n <Button\\n title=\\\"← Home\\\"\\n onPress={() => router.navigate('/')}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.primary,\\n color: theme().color.onPrimary,\\n })}\\n />\\n </Card>\\n )\\n}\\n\",\n \"mindees-example-app/app-js/src/app/index.tsx\": \"/**\\n * Home route — `app/index.tsx` maps to `/` (file-based routing). Themed via design\\n * tokens (`useTheme`), so it re-themes light↔dark with the device color scheme.\\n *\\n * @module\\n */\\n\\nimport {\\n Button,\\n Card,\\n fontSize,\\n Row,\\n space,\\n Text,\\n useColorScheme,\\n useTheme,\\n useWindowDimensions,\\n View,\\n} from '@mindees/atlas'\\nimport { animate, signal, spring } from '@mindees/core'\\nimport { useRouter } from '@mindees/router'\\nimport { buttonShape } from '../theme'\\n\\n/** Module-scoped state survives navigation. */\\nconst done = signal(0)\\n/** An animated bar width — springs on press, driven by vsync on a native host (see FrameDriver). */\\nconst barWidth = animate(48)\\n\\nexport default function Home() {\\n const router = useRouter()\\n const theme = useTheme()\\n const dimensions = useWindowDimensions()\\n const colorScheme = useColorScheme()\\n return (\\n <Card style={{ minWidth: 300, gap: space.md, alignItems: 'center' }}>\\n <Text\\n style={() => ({ fontSize: fontSize.title2, fontWeight: 800, color: theme().color.text })}\\n >\\n MindeesNative\\n </Text>\\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\\n File-based routing · native · TypeScript\\n </Text>\\n <Text style={() => ({ fontSize: 36, fontWeight: 800, color: theme().color.primary })}>\\n {() => `Done today: ${done()}`}\\n </Text>\\n <Row style={{ gap: space.sm, justifyContent: 'center' }}>\\n <Button\\n title=\\\"Mark done\\\"\\n onPress={() => done.set(done() + 1)}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.primary,\\n color: theme().color.onPrimary,\\n })}\\n />\\n <Button\\n title=\\\"About →\\\"\\n onPress={() => router.navigate('/about')}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.surfaceVariant,\\n color: theme().color.text,\\n })}\\n />\\n </Row>\\n <View\\n testID=\\\"pulse-bar\\\"\\n style={() => ({\\n width: barWidth(),\\n height: 8,\\n borderRadius: 4,\\n backgroundColor: theme().color.primary,\\n })}\\n />\\n <Button\\n title=\\\"Animate ✨\\\"\\n onPress={() => spring(barWidth, { to: barWidth() > 160 ? 48 : 240 })}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.surfaceVariant,\\n color: theme().color.text,\\n })}\\n />\\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\\n {() =>\\n `Screen ${Math.round(dimensions().width)}×${Math.round(dimensions().height)} · ${colorScheme()}`\\n }\\n </Text>\\n </Card>\\n )\\n}\\n\",\n \"mindees-example-app/app-js/src/main.tsx\": \"/**\\n * App entry. The whole native wiring — command backend, render, flush, the\\n * `MindeesApp.start()/dispatchEvent()` contract the host calls — is handled by\\n * `createNativeApp`. An app author writes only this.\\n *\\n * @module\\n */\\n\\nimport { setEnvironment } from '@mindees/atlas'\\nimport { createNativeApp } from '@mindees/renderer'\\nimport { App } from './App'\\n\\n// The host injects the platform environment (window size, color scheme) before this\\n// bundle evaluates; apply it so the device hooks (useWindowDimensions/useColorScheme)\\n// read real values from the first render.\\nconst envHost = (globalThis as { MindeesEnv?: { get(): string } }).MindeesEnv\\nif (envHost) {\\n try {\\n setEnvironment(JSON.parse(envHost.get()))\\n } catch {\\n // No/!invalid environment — hooks fall back to defaults.\\n }\\n}\\n\\ncreateNativeApp(<App />)\\n\",\n \"mindees-example-app/app-js/src/theme.ts\": \"/** Token-based shape helpers for the example (colors come from `useTheme` per screen). */\\n\\nimport { fontWeight, radius, space } from '@mindees/atlas'\\n\\n/** Shared button shape (background/foreground colors applied per screen from the theme). */\\nexport const buttonShape = {\\n paddingTop: space.sm,\\n paddingBottom: space.sm,\\n paddingLeft: space.lg,\\n paddingRight: space.lg,\\n borderRadius: radius.md,\\n fontWeight: fontWeight.semibold,\\n} as const\\n\",\n \"mindees-example-app/app-js/tsconfig.json\": \"{\\n \\\"compilerOptions\\\": {\\n \\\"target\\\": \\\"ES2020\\\",\\n \\\"module\\\": \\\"ESNext\\\",\\n \\\"moduleResolution\\\": \\\"Bundler\\\",\\n \\\"jsx\\\": \\\"react-jsx\\\",\\n \\\"jsxImportSource\\\": \\\"@mindees/core\\\",\\n \\\"strict\\\": true,\\n \\\"skipLibCheck\\\": true,\\n \\\"noEmit\\\": true\\n },\\n \\\"include\\\": [\\\"src\\\"]\\n}\\n\",\n \"mindees-example-app/app-js/tsdown.config.ts\": \"import { dirname, resolve } from 'node:path'\\nimport { fileURLToPath } from 'node:url'\\nimport { defineConfig } from 'tsdown'\\n\\nconst here = dirname(fileURLToPath(import.meta.url))\\n\\n/**\\n * Bundles the TSX-authored Atlas + Helix + Quantum app (src/main.tsx) into a single\\n * QuickJS-safe IIFE the Android host loads from assets. `@mindees/*` resolve from\\n * node_modules via their package `exports` (incl. the automatic JSX runtime subpath\\n * `@mindees/core/jsx-runtime`, see tsconfig.json) and are inlined into the IIFE — the\\n * framework has no node-builtin runtime deps. A banner polyfills `queueMicrotask`\\n * (some embedded engines lack it) before any module initializes.\\n */\\nexport default defineConfig({\\n entry: [resolve(here, 'src/main.tsx')],\\n outDir: resolve(here, '..', 'src', 'main', 'assets'),\\n format: ['iife'],\\n globalName: 'MindeesAppBundle',\\n platform: 'node',\\n target: 'es2020',\\n // Inline the framework packages into the IIFE (they are package.json `dependencies`,\\n // which tsdown would otherwise treat as external bare imports — the embedded QuickJS\\n // engine has no module loader, so everything must be bundled).\\n noExternal: [/^@mindees\\\\//],\\n dts: false,\\n clean: false,\\n minify: false,\\n define: { 'process.env.NODE_ENV': JSON.stringify('production') },\\n outputOptions: {\\n entryFileNames: 'mindees-app.bundle.js',\\n banner:\\n \\\"if (typeof globalThis !== 'undefined' && typeof globalThis.queueMicrotask !== 'function') { globalThis.queueMicrotask = function (cb) { Promise.resolve().then(cb); }; }\\\",\\n },\\n})\\n\",\n \"mindees-example-app/build.gradle.kts\": \"plugins {\\n id(\\\"com.android.application\\\")\\n}\\n\\nandroid {\\n namespace = \\\"dev.mindees.example\\\"\\n compileSdk = 36\\n\\n defaultConfig {\\n applicationId = \\\"dev.mindees.example\\\"\\n minSdk = 24\\n targetSdk = 36\\n versionCode = 1\\n versionName = \\\"0.0.0\\\"\\n testInstrumentationRunner = \\\"androidx.test.runner.AndroidJUnitRunner\\\"\\n }\\n\\n compileOptions {\\n sourceCompatibility = JavaVersion.VERSION_17\\n targetCompatibility = JavaVersion.VERSION_17\\n }\\n\\n testOptions {\\n unitTests {\\n isReturnDefaultValues = true\\n }\\n }\\n}\\n\\nkotlin {\\n jvmToolchain(17)\\n}\\n\\ndependencies {\\n implementation(project(\\\":mindees-host\\\"))\\n implementation(\\\"app.cash.quickjs:quickjs-android:0.9.2\\\")\\n // FlexboxLayout (used by the renderer + the root container) needs androidx.core (ViewCompat)\\n // on the runtime classpath; this app is otherwise framework-only, so declare it explicitly.\\n implementation(\\\"androidx.core:core:1.13.1\\\")\\n\\n testImplementation(\\\"junit:junit:4.13.2\\\")\\n testImplementation(\\\"org.json:json:20231013\\\")\\n\\n androidTestImplementation(\\\"androidx.test:core:1.7.0\\\")\\n androidTestImplementation(\\\"androidx.test:runner:1.7.0\\\")\\n androidTestImplementation(\\\"androidx.test.ext:junit:1.3.0\\\")\\n}\\n\",\n \"mindees-example-app/src/main/AndroidManifest.xml\": \"<manifest xmlns:android=\\\"http://schemas.android.com/apk/res/android\\\">\\n <application\\n android:allowBackup=\\\"false\\\"\\n android:label=\\\"Mindees Native Example\\\"\\n android:supportsRtl=\\\"true\\\"\\n android:theme=\\\"@android:style/Theme.Material.Light.NoActionBar\\\">\\n <activity\\n android:name=\\\".MainActivity\\\"\\n android:exported=\\\"true\\\">\\n <intent-filter>\\n <action android:name=\\\"android.intent.action.MAIN\\\" />\\n <category android:name=\\\"android.intent.category.LAUNCHER\\\" />\\n </intent-filter>\\n </activity>\\n </application>\\n</manifest>\\n\",\n \"mindees-example-app/src/main/kotlin/dev/mindees/example/FrameDriver.kt\": \"package dev.mindees.example\\n\\nimport android.view.Choreographer\\n\\n/**\\n * Drives the JS animation frame loop from Android vsync via [Choreographer] — but **only while\\n * active**. The JS side (`createNativeApp`) calls `MindeesHostFrame.setFrameLoopActive(true)` the\\n * instant an animation arms and `(false)` the instant the last one settles, so an idle app posts\\n * **zero** frame callbacks (no 60fps busy-loop). Construct and use on the UI thread (Choreographer is\\n * UI-thread-only).\\n *\\n * @param tick called once per vsync with the frame time in milliseconds (`frameTimeNanos / 1e6`).\\n */\\nclass FrameDriver(private val tick: (Double) -> Unit) {\\n private var running = false\\n\\n // An explicit anonymous object (NOT a SAM lambda): the body re-posts `this`, and a lambda that\\n // referenced its own `val` would make Kotlin's type inference recurse.\\n private val callback: Choreographer.FrameCallback = object : Choreographer.FrameCallback {\\n override fun doFrame(frameTimeNanos: Long) {\\n if (!running) return\\n tick(frameTimeNanos / 1_000_000.0)\\n // Re-perpetuate ONLY while running; setActive(false) simply stops re-posting, so the loop\\n // dies after the in-flight frame — nothing to cancel, no leaked callback.\\n Choreographer.getInstance().postFrameCallback(this)\\n }\\n }\\n\\n /** Start (true) or stop (false) the vsync loop. Idempotent. */\\n fun setActive(active: Boolean) {\\n if (active == running) return\\n running = active\\n if (active) Choreographer.getInstance().postFrameCallback(callback)\\n }\\n}\\n\",\n \"mindees-example-app/src/main/kotlin/dev/mindees/example/MainActivity.kt\": \"package dev.mindees.example\\n\\nimport android.app.Activity\\nimport android.content.res.Configuration\\nimport android.graphics.Color\\nimport android.os.Bundle\\nimport android.os.Handler\\nimport android.os.Looper\\nimport android.view.View\\nimport android.view.ViewGroup\\nimport android.widget.FrameLayout\\nimport dev.mindees.host.AndroidViewRenderer\\nimport dev.mindees.host.MindeesNativeHost\\n\\nclass MainActivity : Activity() {\\n private var bridge: MindeesRuntimeBridge<View>? = null\\n private var frameDriver: FrameDriver? = null\\n\\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n\\n // Fill the window and paint a dark base: the Atlas app controls its own layout and\\n // background edge-to-edge, so the host window should match it (no light gaps showing\\n // through where content hasn't laid out yet).\\n // A FrameLayout root z-stacks the app content + the portal `overlay` layer, so Modal/Toast\\n // (which the renderer mounts into a full-screen `overlay` node, kept painting last) overlap\\n // the content instead of being laid out beneath it.\\n val root = FrameLayout(this).apply {\\n layoutParams = ViewGroup.LayoutParams(\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n )\\n setBackgroundColor(Color.parseColor(\\\"#0b1021\\\"))\\n }\\n val renderer = AndroidViewRenderer(this)\\n val mainHandler = Handler(Looper.getMainLooper())\\n\\n val host = MindeesNativeHost<View>(\\n rootId = HOST_ROOT_ID,\\n root = root,\\n renderer = renderer,\\n onEvent = { handlerId, value ->\\n bridge?.dispatchEvent(handlerId, value)\\n ?: error(\\\"Mindees runtime bridge has not started\\\")\\n },\\n )\\n\\n // The real Atlas + Helix app, bundled to a QuickJS-safe IIFE (app-js/, regenerate\\n // with `pnpm run build:android-example-js`). It runs @mindees/core signals +\\n // @mindees/atlas primitives + the @mindees/renderer reconciler inside QuickJS and\\n // emits the native command stream this host materializes — not hand-written commands.\\n val appJs = assets.open(APP_BUNDLE_ASSET).bufferedReader().use { it.readText() }\\n\\n // Drives JS animation frames from vsync; constructed on the UI thread. `bridge` is assigned\\n // just below and is non-null by the time the first frame callback fires (next vsync).\\n val driver = FrameDriver { nowMs -> bridge?.frameTick(nowMs) }\\n frameDriver = driver\\n\\n bridge = MindeesRuntimeBridge(\\n host = host,\\n // The JS engine arms/sleeps the vsync loop through setFrameLoopActive → FrameDriver.\\n runtime = QuickJsMindeesRuntime(\\n appJs,\\n environmentJson(),\\n onFrameLoopActive = { active -> driver.setActive(active) },\\n ),\\n applyOnHostThread = { action ->\\n if (Looper.myLooper() == Looper.getMainLooper()) {\\n action()\\n } else {\\n mainHandler.post(action)\\n }\\n },\\n ).also { it.start() }\\n\\n setContentView(root)\\n }\\n\\n override fun onDestroy() {\\n frameDriver?.setActive(false) // stop vsync before tearing down the engine\\n frameDriver = null\\n bridge?.close()\\n bridge = null\\n super.onDestroy()\\n }\\n\\n /**\\n * The platform environment for `@mindees/atlas` device hooks (useWindowDimensions,\\n * useColorScheme, …), as JSON. Logical dp = pixels / density.\\n */\\n private fun environmentJson(): String {\\n val dm = resources.displayMetrics\\n val cfg = resources.configuration\\n val widthDp = (dm.widthPixels / dm.density)\\n val heightDp = (dm.heightPixels / dm.density)\\n val isDark =\\n (cfg.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\\n return \\\"\\\"\\\"\\n {\\\"window\\\":{\\\"width\\\":$widthDp,\\\"height\\\":$heightDp,\\\"scale\\\":${dm.density},\\\"\\\"\\\" +\\n \\\"\\\"\\\"\\\"fontScale\\\":${cfg.fontScale}},\\\"colorScheme\\\":\\\"${if (isDark) \\\"dark\\\" else \\\"light\\\"}\\\"}\\n \\\"\\\"\\\".trimIndent()\\n }\\n\\n private companion object {\\n const val HOST_ROOT_ID = \\\"host-root\\\"\\n\\n /** The bundled real Atlas + Helix app (see app-js/ + tsdown.config.ts). */\\n const val APP_BUNDLE_ASSET = \\\"mindees-app.bundle.js\\\"\\n }\\n}\\n\",\n \"mindees-example-app/src/main/kotlin/dev/mindees/example/MindeesRuntimeBridge.kt\": \"package dev.mindees.example\\n\\nimport app.cash.quickjs.QuickJs\\nimport dev.mindees.host.MindeesNativeHost\\nimport dev.mindees.host.NativeCommandCodec\\nimport java.io.Closeable\\n\\ninterface NativeCommandSink {\\n fun applyBatch(json: String)\\n}\\n\\ninterface MindeesScriptRuntime : Closeable {\\n fun start(sink: NativeCommandSink)\\n fun dispatchEvent(handlerId: String, value: String?)\\n\\n /** Advance animations by one vsync frame (time in ms). Drives `MindeesApp.frameTick`. */\\n fun frameTick(nowMs: Double)\\n}\\n\\nclass MindeesRuntimeBridge<V>(\\n private val host: MindeesNativeHost<V>,\\n private val runtime: MindeesScriptRuntime,\\n private val applyOnHostThread: ((() -> Unit) -> Unit) = { it() },\\n) : NativeCommandSink, Closeable {\\n private var started = false\\n\\n fun start() {\\n check(!started) { \\\"Mindees runtime bridge already started\\\" }\\n try {\\n runtime.start(this)\\n started = true\\n } catch (t: Throwable) {\\n try {\\n runtime.close()\\n } catch (closeError: Throwable) {\\n t.addSuppressed(closeError)\\n }\\n throw t\\n }\\n }\\n\\n override fun applyBatch(json: String) {\\n applyOnHostThread {\\n host.apply(NativeCommandCodec.decodeBatch(json))\\n }\\n }\\n\\n fun dispatchEvent(handlerId: String, value: String?) {\\n check(started) { \\\"Mindees runtime bridge has not started\\\" }\\n runtime.dispatchEvent(handlerId, value)\\n }\\n\\n /** Forward a vsync frame to the JS animation engine (called by the [FrameDriver]). */\\n fun frameTick(nowMs: Double) {\\n if (started) runtime.frameTick(nowMs)\\n }\\n\\n override fun close() {\\n if (started) {\\n try {\\n runtime.close()\\n } finally {\\n started = false\\n }\\n }\\n }\\n}\\n\\ninterface MindeesHostApi {\\n fun emit(json: String)\\n}\\n\\n/** Supplies the platform environment (window size, color scheme, …) to the bundle. */\\ninterface MindeesEnvApi {\\n fun get(): String\\n}\\n\\n/** The JS→host battery signal: `createNativeApp` asks the host to run / stop its vsync loop. */\\ninterface MindeesFrameApi {\\n fun setFrameLoopActive(active: Boolean)\\n}\\n\\ninterface MindeesAppApi {\\n fun start()\\n fun dispatchEvent(handlerId: String, value: String?)\\n fun frameTick(nowMs: Double)\\n}\\n\\nclass QuickJsMindeesRuntime(\\n private val source: String,\\n /** JSON for `setEnvironment` (window dimensions, color scheme, …). Default: empty. */\\n private val environmentJson: String = \\\"{}\\\",\\n /** Called when the JS engine arms (true) / sleeps (false) its animation loop — drive the [FrameDriver]. */\\n private val onFrameLoopActive: (Boolean) -> Unit = {},\\n) : MindeesScriptRuntime {\\n private var engine: QuickJs? = null\\n private var app: MindeesAppApi? = null\\n\\n override fun start(sink: NativeCommandSink) {\\n check(engine == null) { \\\"QuickJS runtime already started\\\" }\\n\\n val quickJs = QuickJs.create()\\n try {\\n quickJs.set(\\n \\\"MindeesHost\\\",\\n MindeesHostApi::class.java,\\n object : MindeesHostApi {\\n override fun emit(json: String) {\\n sink.applyBatch(json)\\n }\\n },\\n )\\n // Injected before evaluate so the bundle's entry can read it and call\\n // setEnvironment() before the first render.\\n quickJs.set(\\n \\\"MindeesEnv\\\",\\n MindeesEnvApi::class.java,\\n object : MindeesEnvApi {\\n override fun get(): String = environmentJson\\n },\\n )\\n // Injected before evaluate so createNativeApp installs the vsync frame source. The JS\\n // engine calls this to start/stop the host's Choreographer loop (battery: only while\\n // something is animating).\\n quickJs.set(\\n \\\"MindeesHostFrame\\\",\\n MindeesFrameApi::class.java,\\n object : MindeesFrameApi {\\n override fun setFrameLoopActive(active: Boolean) {\\n onFrameLoopActive(active)\\n }\\n },\\n )\\n quickJs.evaluate(source, \\\"mindees-example.js\\\")\\n val mindeesApp = quickJs.get(\\\"MindeesApp\\\", MindeesAppApi::class.java)\\n mindeesApp.start()\\n engine = quickJs\\n app = mindeesApp\\n } catch (t: Throwable) {\\n quickJs.close()\\n throw t\\n }\\n }\\n\\n override fun dispatchEvent(handlerId: String, value: String?) {\\n app?.dispatchEvent(handlerId, value)\\n ?: error(\\\"QuickJS MindeesApp has not started\\\")\\n }\\n\\n override fun frameTick(nowMs: Double) {\\n app?.frameTick(nowMs)\\n }\\n\\n override fun close() {\\n app = null\\n engine?.close()\\n engine = null\\n }\\n}\\n\",\n \"mindees-host/build.gradle.kts\": \"// CI-verified by .github/workflows/native-android.yml. Align compileSdk / Java /\\n// plugin versions with your installed toolchain if needed.\\n\\nplugins {\\n // AGP 9 includes built-in Kotlin support — no separate kotlin-android plugin.\\n id(\\\"com.android.library\\\")\\n}\\n\\nandroid {\\n namespace = \\\"dev.mindees.host\\\"\\n compileSdk = 36\\n\\n defaultConfig {\\n minSdk = 24\\n }\\n\\n compileOptions {\\n sourceCompatibility = JavaVersion.VERSION_17\\n targetCompatibility = JavaVersion.VERSION_17\\n }\\n\\n testOptions {\\n unitTests {\\n // The host + ModelRenderer are pure Kotlin; JSON-codec tests use the real\\n // org.json (added below). Defaults keep any stray android stub call quiet.\\n isReturnDefaultValues = true\\n // Robolectric renders AndroidViewRenderer against real android.view on the JVM.\\n isIncludeAndroidResources = true\\n }\\n }\\n}\\n\\n// AGP's built-in Kotlin exposes the standard `kotlin {}` DSL.\\nkotlin {\\n jvmToolchain(17)\\n}\\n\\ndependencies {\\n // Google FlexboxLayout backs the renderer's flex containers (flex-wrap + space-* + alignSelf).\\n // `api` (not `implementation`) so a host app's root container can be a FlexboxLayout too.\\n api(\\\"com.google.android.flexbox:flexbox:3.0.0\\\")\\n\\n testImplementation(\\\"junit:junit:4.13.2\\\") // JUnit 4 = AGP's default unit-test framework\\n testImplementation(\\\"org.json:json:20231013\\\") // real org.json for codec unit tests\\n // Robolectric runs AndroidViewRenderer against real android.view classes on the JVM\\n // (no emulator/device needed) so the render test runs in `./gradlew :mindees-host:test`.\\n testImplementation(\\\"org.robolectric:robolectric:4.14.1\\\")\\n}\\n\",\n \"mindees-host/src/main/AndroidManifest.xml\": \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"?>\\n<!-- Library manifest. The package namespace is set in build.gradle.kts. -->\\n<manifest xmlns:android=\\\"http://schemas.android.com/apk/res/android\\\" />\\n\",\n \"mindees-host/src/main/kotlin/dev/mindees/host/AndroidViewRenderer.kt\": \"/*\\n * AndroidViewRenderer.kt — a HostRenderer that builds real android.view widgets from\\n * the MindeesNative command stream, mapping Atlas's curated cross-platform `StyleObject`\\n * onto native layout + visuals.\\n *\\n * Layout uses Google FlexboxLayout for full flex parity: flexDirection,\\n * justifyContent (incl. space-between/around/evenly), alignItems, flexWrap, alignSelf,\\n * gap (→ child margins), flex/flexGrow (→ FlexboxLayout.LayoutParams.flexGrow) — plus\\n * the box model, background/radius/border, opacity, and text styling.\\n *\\n * Device-facing, but JVM-testable via Robolectric (AndroidRenderTest) and on-device\\n * by the native Android workflow. See the module README.\\n */\\n\\npackage dev.mindees.host\\n\\nimport android.content.Context\\nimport android.content.res.ColorStateList\\nimport android.graphics.BitmapFactory\\nimport android.graphics.Color\\nimport android.graphics.PorterDuff\\nimport android.graphics.Typeface\\nimport android.graphics.drawable.GradientDrawable\\nimport android.text.Editable\\nimport android.text.InputType\\nimport android.text.TextUtils\\nimport android.text.TextWatcher\\nimport android.util.Base64\\nimport android.util.TypedValue\\nimport android.view.Gravity\\nimport android.view.View\\nimport android.view.ViewGroup\\nimport android.widget.Button\\nimport android.widget.EditText\\nimport android.widget.FrameLayout\\nimport android.widget.HorizontalScrollView\\nimport android.widget.ImageView\\nimport android.widget.ProgressBar\\nimport android.widget.ScrollView\\nimport android.widget.TextView\\nimport com.google.android.flexbox.AlignItems\\nimport com.google.android.flexbox.AlignSelf\\nimport com.google.android.flexbox.FlexDirection\\nimport com.google.android.flexbox.FlexWrap\\nimport com.google.android.flexbox.FlexboxLayout\\nimport com.google.android.flexbox.JustifyContent\\nimport java.net.HttpURLConnection\\nimport java.net.URL\\nimport java.util.concurrent.ExecutorService\\nimport java.util.concurrent.Executors\\n\\n/** Loads bytes for a remote image `url`, invoking `onResult` with the bytes (or null on failure). */\\ntypealias ImageLoader = (url: String, onResult: (ByteArray?) -> Unit) -> Unit\\n\\n/** Builds Android views from the command stream. Pair with [MindeesNativeHost]. */\\nclass AndroidViewRenderer(\\n private val context: Context,\\n /** Override how remote (`http(s)`) images are fetched (e.g. to inject a cache or a test stub). */\\n private val imageLoader: ImageLoader? = null,\\n) : HostRenderer<View> {\\n\\n private val density: Float = context.resources.displayMetrics.density\\n\\n /** Off-main fetch pool for remote images (created lazily; one idle thread for the renderer's life). */\\n private val imageExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }\\n /** Per-ImageView load generation, so a slow/stale fetch never overwrites a newer src or a disposed view. */\\n private val imageGen = HashMap<View, Long>()\\n\\n /** dp → px (Atlas numeric style values are density-independent pixels on native). */\\n private fun dp(value: Double): Int = Math.round(value * density).toInt()\\n\\n /**\\n * Layout intent we must (re)apply via LayoutParams when a child is inserted, or\\n * when a container's gap/direction changes. Kept off the View so the mapping stays\\n * explicit and testable.\\n */\\n private class Layout {\\n var horizontal = false\\n var gapPx = 0\\n var widthSpec = ViewGroup.LayoutParams.WRAP_CONTENT\\n var heightSpec = ViewGroup.LayoutParams.WRAP_CONTENT\\n var grow = 0f\\n var alignSelf = AlignSelf.AUTO\\n }\\n\\n private val layouts = HashMap<View, Layout>()\\n private fun layoutOf(view: View): Layout = layouts.getOrPut(view) { Layout() }\\n\\n /**\\n * A `scrollview` node is a ScrollView whose ONE child is a FlexboxLayout \\\"content\\\" host. Children\\n * and flex/layout styles route to that content; the ScrollView itself is just the viewport.\\n */\\n private val scrollContent = HashMap<View, FlexboxLayout>()\\n private fun contentFor(view: View): View = scrollContent[view] ?: view\\n\\n /** Per-EditText keyboard/secure/multiline intent, recomputed into inputType (compose any order). */\\n private class InputSpec {\\n var keyboard = \\\"text\\\"\\n var secure = false\\n var multiline = false\\n }\\n private val inputSpecs = HashMap<View, InputSpec>()\\n private fun inputSpecOf(view: View): InputSpec = inputSpecs.getOrPut(view) { InputSpec() }\\n /** Active text-change watchers keyed by view then eventName, so onInput and onChange are\\n * independent (both can be registered) and removeEvent/dispose detach precisely (no leaks). */\\n private val textWatchers = HashMap<View, HashMap<String, TextWatcher>>()\\n\\n /**\\n * Text composition. A leaf widget (TextView/Button/EditText) can't hold child views,\\n * but the element model nests text *nodes* inside a `text` *element* (e.g. Atlas's\\n * `Text` → `<text>\\\"hello\\\"</text>`). So we compose a text element's text-node children\\n * into its own `text` instead of attaching them as views. `textParts` keeps each\\n * element's ordered text-node children; `textOwner` is the reverse lookup so an\\n * `updateText` on a child re-composes its owner.\\n */\\n private val textParts = HashMap<View, MutableList<View>>()\\n private val textOwner = HashMap<View, View>()\\n\\n private fun recomposeText(element: View) {\\n val tv = element as? TextView ?: return\\n val parts = textParts[element] ?: return\\n tv.text = parts.joinToString(\\\"\\\") { (it as? TextView)?.text ?: \\\"\\\" }\\n }\\n\\n // --- Node creation ---\\n\\n override fun makeElement(tag: String): View = when (tag) {\\n \\\"text\\\" -> TextView(context)\\n \\\"image\\\" -> ImageView(context)\\n \\\"textinput\\\" -> EditText(context)\\n \\\"button\\\" -> Button(context) // direct use; Atlas Button renders as a clickable 'view'\\n // Atlas ActivityIndicator → a native indeterminate spinner.\\n \\\"activityindicator\\\" -> ProgressBar(context).apply {\\n isIndeterminate = true\\n layoutOf(this)\\n }\\n // 'scrollview' → a real vertical ScrollView wrapping a FlexboxLayout content host.\\n // Children/styles route to the inner content (see contentFor); the ScrollView is the viewport.\\n \\\"scrollview\\\" -> ScrollView(context).apply {\\n isFillViewport = true\\n val content = FlexboxLayout(context).apply { flexDirection = FlexDirection.COLUMN }\\n content.layoutParams = FrameLayout.LayoutParams(\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n ViewGroup.LayoutParams.WRAP_CONTENT,\\n )\\n addView(content)\\n scrollContent[this] = content\\n layoutOf(content)\\n }\\n // 'horizontalscrollview' → a HorizontalScrollView wrapping a ROW content host. Mirror of the\\n // vertical case: content is WRAP width / MATCH height so the row can grow past the viewport on X.\\n \\\"horizontalscrollview\\\" -> HorizontalScrollView(context).apply {\\n isFillViewport = true\\n val content = FlexboxLayout(context).apply { flexDirection = FlexDirection.ROW }\\n content.layoutParams = FrameLayout.LayoutParams(\\n ViewGroup.LayoutParams.WRAP_CONTENT,\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n )\\n addView(content)\\n scrollContent[this] = content\\n layoutOf(content).horizontal = true // gaps go on the X axis even before any style arrives\\n }\\n // 'overlay' → the portal layer (Modal/Toast mount here). A full-screen flex container that\\n // FILLS its parent so it overlaps the app content (the host root is a FrameLayout that\\n // z-stacks; the renderer keeps the overlay painting last, so it sits on top).\\n \\\"overlay\\\" -> FlexboxLayout(context).apply {\\n flexDirection = FlexDirection.COLUMN\\n layoutOf(this).apply {\\n widthSpec = ViewGroup.LayoutParams.MATCH_PARENT\\n heightSpec = ViewGroup.LayoutParams.MATCH_PARENT\\n }\\n }\\n // 'view' / unknown → a real flex container (FlexboxLayout): full\\n // flexDirection / justifyContent (incl. space-*) / alignItems / flexWrap / alignSelf.\\n else -> FlexboxLayout(context).apply {\\n flexDirection = FlexDirection.COLUMN\\n layoutOf(this)\\n }\\n }\\n\\n override fun makeText(text: String): View = TextView(context).apply { this.text = text }\\n\\n override fun setText(view: View, text: String) {\\n (view as? TextView)?.text = text\\n // If this text node is composed into a parent text element, re-compose the parent.\\n textOwner[view]?.let { recomposeText(it) }\\n }\\n\\n // --- Props ---\\n\\n override fun setProp(view: View, name: String, value: NativeProp) {\\n when (name) {\\n \\\"style\\\" -> (value as? NativeProp.Obj)?.let { applyStyle(view, it.value) }\\n \\\"accessibilityLabel\\\", \\\"aria-label\\\", \\\"label\\\" -> view.contentDescription = strOf(value)\\n \\\"title\\\", \\\"text\\\" -> (view as? TextView)?.text = strOf(value)\\n \\\"placeholder\\\" -> (view as? EditText)?.hint = strOf(value)\\n \\\"value\\\" -> (view as? EditText)?.let { if (it.text.toString() != strOf(value)) it.setText(strOf(value)) }\\n \\\"hidden\\\" -> view.visibility = if (boolOf(value)) View.GONE else View.VISIBLE\\n // Image source (data:/base64 + bundled asset load synchronously; remote is a follow-up).\\n \\\"src\\\", \\\"source\\\" -> (view as? ImageView)?.let { applyImageSource(it, value) }\\n \\\"resizeMode\\\" -> (view as? ImageView)?.let { it.scaleType = scaleTypeFor(strOf(value) ?: \\\"contain\\\") }\\n \\\"tintColor\\\" -> (view as? ImageView)?.let { iv ->\\n val c = color(value)\\n if (c != null) iv.setColorFilter(c, PorterDuff.Mode.SRC_IN) else iv.clearColorFilter()\\n }\\n // TextInput: keyboard / multiline / secure / enabled / focus. `type`=\\\"password\\\" → secure.\\n \\\"keyboardType\\\", \\\"inputMode\\\", \\\"type\\\" -> (view as? EditText)?.let {\\n val s = strOf(value) ?: \\\"text\\\"\\n if (s == \\\"password\\\") inputSpecOf(it).secure = true else inputSpecOf(it).keyboard = s\\n applyInputType(it)\\n }\\n \\\"multiline\\\" -> (view as? EditText)?.let { inputSpecOf(it).multiline = boolOf(value); applyInputType(it) }\\n \\\"secureTextEntry\\\" -> (view as? EditText)?.let { inputSpecOf(it).secure = boolOf(value); applyInputType(it) }\\n \\\"editable\\\" -> (view as? EditText)?.let { it.isEnabled = boolOf(value) }\\n \\\"disabled\\\" -> (view as? EditText)?.let { it.isEnabled = !boolOf(value) }\\n \\\"autoFocus\\\" -> (view as? EditText)?.let { if (boolOf(value)) it.requestFocus() }\\n // a11y/meta hints the DOM backend also emits — no-ops on this host.\\n else -> Unit\\n }\\n }\\n\\n override fun removeProp(view: View, name: String) {\\n when (name) {\\n \\\"accessibilityLabel\\\", \\\"aria-label\\\", \\\"label\\\" -> view.contentDescription = null\\n \\\"hidden\\\" -> view.visibility = View.VISIBLE\\n \\\"editable\\\", \\\"disabled\\\" -> (view as? EditText)?.isEnabled = true\\n \\\"secureTextEntry\\\" -> (view as? EditText)?.let { inputSpecOf(it).secure = false; applyInputType(it) }\\n \\\"multiline\\\" -> (view as? EditText)?.let { inputSpecOf(it).multiline = false; applyInputType(it) }\\n \\\"tintColor\\\" -> (view as? ImageView)?.clearColorFilter()\\n else -> Unit\\n }\\n }\\n\\n // --- Tree structure ---\\n\\n override fun insert(parent: View, child: View, index: Int) {\\n val target = contentFor(parent) // route scrollview children into its content host\\n if (target is ViewGroup) {\\n applyLayoutParams(child) // carry the child width/height/grow/alignSelf into FlexboxLayout params\\n target.addView(child, index.coerceIn(0, target.childCount))\\n reapplyGaps(target)\\n return\\n }\\n // Leaf (e.g. a `text` element): compose text-node children into its text.\\n val parts = textParts.getOrPut(parent) { mutableListOf() }\\n parts.add(index.coerceIn(0, parts.size), child)\\n textOwner[child] = parent\\n recomposeText(parent)\\n }\\n\\n override fun remove(parent: View, child: View) {\\n val target = contentFor(parent)\\n if (target is ViewGroup) {\\n target.removeView(child)\\n reapplyGaps(target)\\n return\\n }\\n textOwner.remove(child)\\n textParts[parent]?.remove(child)\\n recomposeText(parent)\\n }\\n\\n override fun dispose(view: View) {\\n (view.parent as? ViewGroup)?.removeView(view)\\n layouts.remove(view)\\n imageGen.remove(view) // invalidate any in-flight remote image fetch for this view\\n // A scrollview owns an inner content host — drop both.\\n scrollContent.remove(view)?.let { layouts.remove(it) }\\n // TextInput state: detach every watcher + drop the input spec (no View leaks).\\n textWatchers.remove(view)?.let { byEvent ->\\n (view as? EditText)?.let { et -> byEvent.values.forEach { et.removeTextChangedListener(it) } }\\n }\\n inputSpecs.remove(view)\\n // Detach from any text composition it participated in (as child or as owner).\\n textOwner.remove(view)?.let { owner ->\\n textParts[owner]?.remove(view)\\n recomposeText(owner)\\n }\\n textParts.remove(view)\\n }\\n\\n // --- Events ---\\n\\n override fun addEvent(view: View, eventName: String, handlerId: String, fire: (value: String?) -> Unit) {\\n // Atlas's Pressable emits `onClick` → `click`; the older hand-written demo used\\n // `press`. Accept both; ignore pointer/keyboard/focus events this host doesn't model.\\n when (eventName) {\\n \\\"click\\\", \\\"press\\\" -> {\\n view.isClickable = true\\n view.setOnClickListener { fire(null) } // notify-only → no value\\n }\\n // Text change (Atlas onInput→\\\"input\\\", onChange→\\\"change\\\"). fire() carries the field's current\\n // text, which the JS host wraps as `{ target: { value } }` so onInput/onChange receive it.\\n \\\"input\\\", \\\"change\\\" -> (view as? EditText)?.let { et ->\\n val byEvent = textWatchers.getOrPut(et) { HashMap() }\\n byEvent.remove(eventName)?.let { et.removeTextChangedListener(it) } // replace only THIS event\\n val watcher = object : TextWatcher {\\n // Read the authoritative current field text (not `s`), coalescing null → \\\"\\\".\\n override fun afterTextChanged(s: Editable?) = fire(et.text?.toString() ?: \\\"\\\")\\n override fun beforeTextChanged(s: CharSequence?, st: Int, c: Int, a: Int) = Unit\\n override fun onTextChanged(s: CharSequence?, st: Int, b: Int, c: Int) = Unit\\n }\\n et.addTextChangedListener(watcher)\\n byEvent[eventName] = watcher\\n }\\n else -> Unit\\n }\\n }\\n\\n override fun removeEvent(view: View, eventName: String, handlerId: String) {\\n when (eventName) {\\n \\\"click\\\", \\\"press\\\" -> view.setOnClickListener(null)\\n \\\"input\\\", \\\"change\\\" -> (view as? EditText)?.let { et ->\\n textWatchers[et]?.let { byEvent ->\\n byEvent.remove(eventName)?.let { et.removeTextChangedListener(it) }\\n if (byEvent.isEmpty()) textWatchers.remove(et)\\n }\\n }\\n else -> Unit\\n }\\n }\\n\\n // --- Style application ---\\n\\n private fun applyStyle(view: View, style: Map<String, NativeProp>) {\\n // The flex container is `view` itself, OR a scrollview's inner content host. Sizing/visual\\n // props stay on `view` (the viewport for a scrollview); flex props target the container.\\n val container = contentFor(view) as? FlexboxLayout\\n val text = view as? TextView // Button/EditText are TextView subclasses\\n val selfLay = layoutOf(view) // this view's own size/grow/alignSelf as a child of its parent\\n val contentLay = container?.let { layoutOf(it) } ?: selfLay // the flex container's direction/gap\\n\\n // Flex container: direction, justify (incl. space-*), align, wrap.\\n (style[\\\"flexDirection\\\"] as? NativeProp.Str)?.value?.let { dir ->\\n contentLay.horizontal = dir.startsWith(\\\"row\\\")\\n container?.flexDirection = flexDirectionFor(dir)\\n }\\n (dimen(style[\\\"gap\\\"]) ?: dimen(style[\\\"rowGap\\\"]) ?: dimen(style[\\\"columnGap\\\"]))?.let {\\n contentLay.gapPx = it\\n reapplyGaps(container)\\n }\\n strOf(style[\\\"justifyContent\\\"])?.let { container?.justifyContent = justifyContentFor(it) }\\n strOf(style[\\\"alignItems\\\"])?.let { container?.alignItems = alignItemsFor(it) }\\n strOf(style[\\\"flexWrap\\\"])?.let { container?.flexWrap = flexWrapFor(it) }\\n // Per-child cross-axis override (applied via the child's FlexboxLayout.LayoutParams on insert).\\n strOf(style[\\\"alignSelf\\\"])?.let {\\n selfLay.alignSelf = alignSelfFor(it)\\n applyLayoutParams(view)\\n }\\n (numOf(style[\\\"flexGrow\\\"]) ?: numOf(style[\\\"flex\\\"]))?.let {\\n selfLay.grow = it.toFloat()\\n applyLayoutParams(view)\\n }\\n\\n // Box model.\\n sizeOf(style[\\\"width\\\"])?.let { selfLay.widthSpec = it; applyLayoutParams(view) }\\n sizeOf(style[\\\"height\\\"])?.let { selfLay.heightSpec = it; applyLayoutParams(view) }\\n dimen(style[\\\"minWidth\\\"])?.let { view.minimumWidth = it }\\n dimen(style[\\\"minHeight\\\"])?.let { view.minimumHeight = it }\\n applyPadding(view, style)\\n\\n // Visual.\\n applyBackground(view, style)\\n numOf(style[\\\"opacity\\\"])?.let { view.alpha = it.toFloat() }\\n dimen(style[\\\"elevation\\\"])?.let { view.elevation = it.toFloat() } // shadow (px)\\n\\n // Text.\\n if (text != null) applyText(text, style)\\n\\n // Spinner tint (ActivityIndicator → ProgressBar): `color` drives the indeterminate arc.\\n (view as? ProgressBar)?.let { bar ->\\n color(style[\\\"color\\\"])?.let { bar.indeterminateTintList = ColorStateList.valueOf(it) }\\n }\\n }\\n\\n private fun applyPadding(view: View, style: Map<String, NativeProp>) {\\n if (style.keys.none { it == \\\"padding\\\" || it.startsWith(\\\"padding\\\") }) return\\n val all = dimen(style[\\\"padding\\\"])\\n val l = dimen(style[\\\"paddingLeft\\\"]) ?: all ?: view.paddingLeft\\n val t = dimen(style[\\\"paddingTop\\\"]) ?: all ?: view.paddingTop\\n val r = dimen(style[\\\"paddingRight\\\"]) ?: all ?: view.paddingRight\\n val b = dimen(style[\\\"paddingBottom\\\"]) ?: all ?: view.paddingBottom\\n view.setPadding(l, t, r, b)\\n }\\n\\n private fun applyBackground(view: View, style: Map<String, NativeProp>) {\\n val bg = color(style[\\\"backgroundColor\\\"])\\n val radius = dimen(style[\\\"borderRadius\\\"])\\n val borderW = dimen(style[\\\"borderWidth\\\"])\\n val borderC = color(style[\\\"borderColor\\\"])\\n val tl = dimen(style[\\\"borderTopLeftRadius\\\"])\\n val tr = dimen(style[\\\"borderTopRightRadius\\\"])\\n val br = dimen(style[\\\"borderBottomRightRadius\\\"])\\n val bl = dimen(style[\\\"borderBottomLeftRadius\\\"])\\n val perCorner = tl != null || tr != null || br != null || bl != null\\n if (bg == null && radius == null && borderW == null && borderC == null && !perCorner) return\\n val drawable = (view.background as? GradientDrawable) ?: GradientDrawable()\\n bg?.let { drawable.setColor(it) }\\n if (perCorner) {\\n // Per-corner radii fall back to the uniform borderRadius (or 0). Order: TL, TR, BR, BL.\\n val t = (tl ?: radius ?: 0).toFloat()\\n val r = (tr ?: radius ?: 0).toFloat()\\n val b = (br ?: radius ?: 0).toFloat()\\n val l = (bl ?: radius ?: 0).toFloat()\\n drawable.cornerRadii = floatArrayOf(t, t, r, r, b, b, l, l)\\n } else {\\n radius?.let { drawable.cornerRadius = it.toFloat() }\\n }\\n if (borderW != null || borderC != null) {\\n drawable.setStroke(borderW ?: 0, borderC ?: Color.TRANSPARENT)\\n }\\n view.background = drawable\\n }\\n\\n private fun applyText(text: TextView, style: Map<String, NativeProp>) {\\n color(style[\\\"color\\\"])?.let { text.setTextColor(it) }\\n numOf(style[\\\"fontSize\\\"])?.let { text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, it.toFloat()) }\\n fontWeightBold(style[\\\"fontWeight\\\"])?.let { bold ->\\n // Two-arg setTypeface: uses a real bold face when available, else falls back to\\n // synthetic (fake) bold — so weight always takes visible effect.\\n text.setTypeface(text.typeface, if (bold) Typeface.BOLD else Typeface.NORMAL)\\n }\\n (style[\\\"textAlign\\\"] as? NativeProp.Str)?.value?.let { text.gravity = textGravity(it) }\\n // numberOfLines → clamp lines + ellipsize the tail (RN's `numberOfLines`).\\n numOf(style[\\\"numberOfLines\\\"])?.let { n ->\\n val lines = n.toInt()\\n if (lines > 0) {\\n text.maxLines = lines\\n text.ellipsize = TextUtils.TruncateAt.END\\n }\\n }\\n }\\n\\n // --- Image + TextInput ---\\n\\n /**\\n * Load an image `src`/`source` into an ImageView. `data:`/base64 + bundled assets decode\\n * synchronously (deterministic, no I/O on the network); remote http(s) is a deliberate follow-up\\n * (it needs an off-main fetch executor + a renderer lifecycle hook to reclaim it). Any\\n * unresolvable/garbage source is ignored — never crashes the host.\\n */\\n private fun applyImageSource(iv: ImageView, value: NativeProp) {\\n val uri = strOf(value)\\n ?: (value as? NativeProp.Obj)?.value?.get(\\\"uri\\\")?.let { strOf(it) }\\n ?: return\\n try {\\n val bitmap = when {\\n uri.startsWith(\\\"data:\\\") -> {\\n val payload = uri.substringAfter(\\\"base64,\\\", \\\"\\\")\\n if (payload.isEmpty()) {\\n null\\n } else {\\n val bytes = Base64.decode(payload, Base64.DEFAULT)\\n BitmapFactory.decodeByteArray(bytes, 0, bytes.size)\\n }\\n }\\n // Remote: fetch off-main (via imageLoader, default HTTP), decode, set on the main thread —\\n // guarded by a generation token so a stale/slow fetch never clobbers a newer src/disposed view.\\n uri.startsWith(\\\"http://\\\") || uri.startsWith(\\\"https://\\\") -> {\\n val token = (imageGen[iv] ?: 0L) + 1\\n imageGen[iv] = token\\n (imageLoader ?: ::defaultImageLoad)(uri) { bytes ->\\n val bmp = bytes?.let {\\n runCatching { BitmapFactory.decodeByteArray(it, 0, it.size) }.getOrNull()\\n }\\n if (bmp != null) iv.post { if (imageGen[iv] == token) iv.setImageBitmap(bmp) }\\n }\\n null // async — nothing to set synchronously\\n }\\n else -> {\\n val name = uri.removePrefix(\\\"asset:///\\\").removePrefix(\\\"file:///android_asset/\\\")\\n context.assets.open(name).use { BitmapFactory.decodeStream(it) }\\n }\\n }\\n bitmap?.let { iv.setImageBitmap(it) }\\n } catch (_: Throwable) {\\n // ignore: an unresolvable/garbage src must never crash the host\\n }\\n }\\n\\n /** Default remote loader: fetch the URL off the main thread via HttpURLConnection. */\\n private fun defaultImageLoad(url: String, onResult: (ByteArray?) -> Unit) {\\n imageExecutor.execute {\\n val bytes = runCatching {\\n val conn = (URL(url).openConnection() as HttpURLConnection).apply {\\n connectTimeout = 10_000\\n readTimeout = 10_000\\n doInput = true\\n }\\n try {\\n conn.inputStream.use { it.readBytes() }\\n } finally {\\n conn.disconnect()\\n }\\n }.getOrNull()\\n onResult(bytes)\\n }\\n }\\n\\n private fun scaleTypeFor(mode: String): ImageView.ScaleType = when (mode) {\\n \\\"cover\\\" -> ImageView.ScaleType.CENTER_CROP\\n \\\"stretch\\\" -> ImageView.ScaleType.FIT_XY\\n \\\"center\\\" -> ImageView.ScaleType.CENTER\\n else -> ImageView.ScaleType.FIT_CENTER // \\\"contain\\\" / default\\n }\\n\\n /** Recompute an EditText's inputType from its {keyboard, secure, multiline} spec (order-independent). */\\n private fun applyInputType(et: EditText) {\\n val spec = inputSpecs[et] ?: return\\n var type = when (spec.keyboard) {\\n \\\"number\\\", \\\"numeric\\\", \\\"number-pad\\\" -> InputType.TYPE_CLASS_NUMBER\\n \\\"decimal\\\", \\\"decimal-pad\\\" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL\\n \\\"phone\\\", \\\"tel\\\" -> InputType.TYPE_CLASS_PHONE\\n \\\"email\\\", \\\"email-address\\\" ->\\n InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS\\n \\\"url\\\" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI\\n else -> InputType.TYPE_CLASS_TEXT\\n }\\n if (spec.multiline) {\\n et.isSingleLine = false // set before inputType (singleLine mutates inputType)\\n et.gravity = Gravity.TOP or Gravity.START\\n type = type or InputType.TYPE_TEXT_FLAG_MULTI_LINE\\n }\\n if (spec.secure) {\\n // Clear any existing variation (email/url/phone) and apply ONLY the password variation —\\n // OR-ing onto a non-default variation yields a combined value Android won't mask.\\n val isNumber = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER\\n type = type and InputType.TYPE_MASK_VARIATION.inv()\\n type = type or\\n if (isNumber) InputType.TYPE_NUMBER_VARIATION_PASSWORD\\n else InputType.TYPE_TEXT_VARIATION_PASSWORD\\n }\\n et.inputType = type\\n }\\n\\n // --- LayoutParams + gaps ---\\n\\n private fun applyLayoutParams(view: View) {\\n val lay = layoutOf(view)\\n val lp = (view.layoutParams as? FlexboxLayout.LayoutParams)\\n ?: FlexboxLayout.LayoutParams(lay.widthSpec, lay.heightSpec)\\n lp.width = lay.widthSpec\\n lp.height = lay.heightSpec\\n lp.flexGrow = lay.grow\\n lp.alignSelf = lay.alignSelf\\n view.layoutParams = lp\\n }\\n\\n /** Recreate gaps as leading margins on every child (all but the first along the main axis). */\\n private fun reapplyGaps(parent: View?) {\\n val container = parent as? FlexboxLayout ?: return\\n val lay = layouts[container] ?: return\\n for (i in 0 until container.childCount) {\\n val child = container.getChildAt(i)\\n val lp = child.layoutParams as? FlexboxLayout.LayoutParams ?: continue\\n val lead = if (i == 0) 0 else lay.gapPx\\n if (lay.horizontal) {\\n lp.leftMargin = lead\\n lp.topMargin = 0\\n } else {\\n lp.topMargin = lead\\n lp.leftMargin = 0\\n }\\n child.layoutParams = lp\\n }\\n }\\n\\n // --- Flex enum mappings (Atlas/CSS strings → FlexboxLayout constants) ---\\n\\n private fun flexDirectionFor(dir: String): Int = when (dir) {\\n \\\"row\\\" -> FlexDirection.ROW\\n \\\"row-reverse\\\" -> FlexDirection.ROW_REVERSE\\n \\\"column-reverse\\\" -> FlexDirection.COLUMN_REVERSE\\n else -> FlexDirection.COLUMN\\n }\\n\\n private fun justifyContentFor(justify: String): Int = when (justify) {\\n \\\"flex-end\\\", \\\"end\\\" -> JustifyContent.FLEX_END\\n \\\"center\\\" -> JustifyContent.CENTER\\n \\\"space-between\\\" -> JustifyContent.SPACE_BETWEEN\\n \\\"space-around\\\" -> JustifyContent.SPACE_AROUND\\n \\\"space-evenly\\\" -> JustifyContent.SPACE_EVENLY\\n else -> JustifyContent.FLEX_START\\n }\\n\\n private fun alignItemsFor(align: String): Int = when (align) {\\n \\\"flex-end\\\", \\\"end\\\" -> AlignItems.FLEX_END\\n \\\"center\\\" -> AlignItems.CENTER\\n \\\"baseline\\\" -> AlignItems.BASELINE\\n \\\"flex-start\\\", \\\"start\\\" -> AlignItems.FLEX_START\\n else -> AlignItems.STRETCH\\n }\\n\\n private fun flexWrapFor(wrap: String): Int = when (wrap) {\\n \\\"wrap\\\" -> FlexWrap.WRAP\\n \\\"wrap-reverse\\\" -> FlexWrap.WRAP_REVERSE\\n else -> FlexWrap.NOWRAP\\n }\\n\\n private fun alignSelfFor(align: String): Int = when (align) {\\n \\\"flex-end\\\", \\\"end\\\" -> AlignSelf.FLEX_END\\n \\\"center\\\" -> AlignSelf.CENTER\\n \\\"baseline\\\" -> AlignSelf.BASELINE\\n \\\"stretch\\\" -> AlignSelf.STRETCH\\n \\\"flex-start\\\", \\\"start\\\" -> AlignSelf.FLEX_START\\n else -> AlignSelf.AUTO\\n }\\n\\n // --- Value coercion (defensive: unknown/ill-typed values are ignored, never crash) ---\\n\\n private fun numOf(prop: NativeProp?): Double? = (prop as? NativeProp.Num)?.value\\n private fun strOf(prop: NativeProp?): String? = (prop as? NativeProp.Str)?.value\\n private fun boolOf(prop: NativeProp?): Boolean = (prop as? NativeProp.Bool)?.value == true\\n\\n /** A numeric dp dimension, or null for strings/absent (percent sizes go via [sizeOf]). */\\n private fun dimen(prop: NativeProp?): Int? = (prop as? NativeProp.Num)?.let { dp(it.value) }\\n\\n /** A width/height spec: number → dp, `\\\"100%\\\"` → MATCH_PARENT, `\\\"auto\\\"` → WRAP_CONTENT. */\\n private fun sizeOf(prop: NativeProp?): Int? = when (prop) {\\n is NativeProp.Num -> dp(prop.value)\\n is NativeProp.Str -> when (prop.value) {\\n \\\"100%\\\" -> ViewGroup.LayoutParams.MATCH_PARENT\\n \\\"auto\\\" -> ViewGroup.LayoutParams.WRAP_CONTENT\\n else -> null\\n }\\n else -> null\\n }\\n\\n private fun color(prop: NativeProp?): Int? {\\n val raw = (prop as? NativeProp.Str)?.value ?: return null\\n return try {\\n Color.parseColor(raw)\\n } catch (_: IllegalArgumentException) {\\n null\\n }\\n }\\n\\n /** true → bold, false → normal, null → leave unchanged. `>= 600` (or `\\\"bold\\\"`) is bold. */\\n private fun fontWeightBold(prop: NativeProp?): Boolean? = when (prop) {\\n is NativeProp.Num -> prop.value >= 600\\n is NativeProp.Str -> prop.value == \\\"bold\\\" || (prop.value.toIntOrNull() ?: 0) >= 600\\n else -> null\\n }\\n\\n private fun textGravity(align: String): Int = when (align) {\\n \\\"center\\\" -> Gravity.CENTER\\n \\\"right\\\" -> Gravity.END\\n \\\"justify\\\" -> Gravity.START // no native justify on older APIs; left-align\\n else -> Gravity.START\\n }\\n\\n /** main axis ← justifyContent, cross axis ← alignItems. `space-*` falls back to center. */\\n}\\n\",\n \"mindees-host/src/main/kotlin/dev/mindees/host/MindeesNativeHost.kt\": \"/*\\n * MindeesNativeHost.kt — applies a NativeCommand stream to a pluggable renderer,\\n * with strict validation mirroring @mindees/renderer's reference host.\\n *\\n * The host owns identity + structure bookkeeping (and validates the stream); a\\n * [HostRenderer] builds/mutates the actual views. Use [AndroidViewRenderer] on a\\n * device, or [ModelRenderer] in JVM unit tests (`./gradlew test`, no device needed).\\n *\\n * CI-verified by the native Android workflow; see the module README.\\n */\\n\\npackage dev.mindees.host\\n\\n/** Thrown when a command stream violates the host contract. */\\nclass NativeHostException(message: String) : RuntimeException(message)\\n\\n/**\\n * A pluggable platform renderer. The host calls these to materialize the UI.\\n * [V] is the platform node type (`android.view.View`, or `ModelNode` in tests).\\n */\\ninterface HostRenderer<V> {\\n fun makeElement(tag: String): V\\n fun makeText(text: String): V\\n fun setText(view: V, text: String)\\n fun setProp(view: V, name: String, value: NativeProp)\\n fun removeProp(view: V, name: String)\\n fun insert(parent: V, child: V, index: Int)\\n fun remove(parent: V, child: V)\\n /**\\n * Wire a native event; the renderer calls [fire] when it occurs, passing an optional value\\n * (the text for an `input`/`change` event; `null` for notify-only events like press/click).\\n */\\n fun addEvent(view: V, eventName: String, handlerId: String, fire: (value: String?) -> Unit)\\n fun removeEvent(view: V, eventName: String, handlerId: String)\\n /** Free a node's resources (already detached from its parent). */\\n fun dispose(view: V)\\n}\\n\\n/**\\n * Applies a [NativeCommand] stream to a [HostRenderer], strictly validating it\\n * (throws [NativeHostException] on any malformed/leaking sequence) — the contract\\n * `@mindees/renderer`'s `createReferenceHost()` enforces and tests.\\n *\\n * @param onEvent invoked with a `handlerId` (and an optional text `value` for input/change events,\\n * `null` for notify-only events) when a wired native event fires; forward it to the JS runtime's\\n * `MindeesApp.dispatchEvent(handlerId, value)`.\\n */\\nclass MindeesNativeHost<V>(\\n private val rootId: String,\\n root: V,\\n private val renderer: HostRenderer<V>,\\n private val onEvent: (handlerId: String, value: String?) -> Unit,\\n) {\\n private val views = HashMap<String, V>()\\n private val parentOf = HashMap<String, String>()\\n private val childrenOf = HashMap<String, MutableList<String>>()\\n\\n init {\\n views[rootId] = root\\n childrenOf[rootId] = mutableListOf()\\n }\\n\\n /** Live (created, not yet disposed) node count, excluding the root. */\\n val liveNodeCount: Int get() = views.size - 1\\n\\n fun apply(batch: List<NativeCommand>) {\\n for (command in batch) apply(command)\\n }\\n\\n fun apply(command: NativeCommand) {\\n when (command) {\\n is NativeCommand.CreateNode -> {\\n requireAbsent(command.id)\\n views[command.id] = renderer.makeElement(command.tag)\\n childrenOf[command.id] = mutableListOf()\\n }\\n is NativeCommand.CreateText -> {\\n requireAbsent(command.id)\\n views[command.id] = renderer.makeText(command.text)\\n childrenOf[command.id] = mutableListOf()\\n }\\n is NativeCommand.UpdateText -> renderer.setText(view(command.id), command.text)\\n is NativeCommand.SetProp -> renderer.setProp(view(command.id), command.name, command.value)\\n is NativeCommand.RemoveProp -> renderer.removeProp(view(command.id), command.name)\\n is NativeCommand.InsertChild -> {\\n if (parentOf.containsKey(command.childId)) {\\n throw NativeHostException(\\\"insertChild: ${command.childId} already has a parent\\\")\\n }\\n val parent = view(command.parentId)\\n val child = view(command.childId)\\n val kids = childrenOf.getOrPut(command.parentId) { mutableListOf() }\\n if (command.index < 0 || command.index > kids.size) {\\n throw NativeHostException(\\\"insertChild: index ${command.index} out of range\\\")\\n }\\n kids.add(command.index, command.childId)\\n parentOf[command.childId] = command.parentId\\n renderer.insert(parent, child, command.index)\\n }\\n is NativeCommand.RemoveChild -> {\\n val kids = childrenOf[command.parentId]\\n if (parentOf[command.childId] != command.parentId || kids == null ||\\n !kids.remove(command.childId)\\n ) {\\n throw NativeHostException(\\\"removeChild: ${command.childId} is not a child of ${command.parentId}\\\")\\n }\\n parentOf.remove(command.childId)\\n renderer.remove(view(command.parentId), view(command.childId))\\n }\\n is NativeCommand.DisposeNode -> {\\n if (command.id == rootId) throw NativeHostException(\\\"cannot dispose the root node\\\")\\n val v = views[command.id] ?: throw NativeHostException(\\\"double dispose of ${command.id}\\\")\\n // Interior subtree nodes are freed without an explicit removeChild, so\\n // detach from BOTH the bookkeeping and the renderer tree here. (A renderer\\n // whose dispose() is a no-op — e.g. ModelRenderer — would otherwise leave\\n // the node in its parent's children even though we consider it detached.)\\n parentOf.remove(command.id)?.let { pid ->\\n childrenOf[pid]?.remove(command.id)\\n views[pid]?.let { parentView -> renderer.remove(parentView, v) }\\n }\\n renderer.dispose(v)\\n views.remove(command.id)\\n childrenOf.remove(command.id)\\n }\\n is NativeCommand.RegisterEvent -> {\\n val target = view(command.id)\\n val handlerId = command.handlerId\\n renderer.addEvent(target, command.eventName, handlerId) { value -> onEvent(handlerId, value) }\\n }\\n is NativeCommand.UnregisterEvent ->\\n renderer.removeEvent(view(command.id), command.eventName, command.handlerId)\\n }\\n }\\n\\n private fun view(id: String): V = views[id] ?: throw NativeHostException(\\\"unknown node $id\\\")\\n\\n private fun requireAbsent(id: String) {\\n if (views.containsKey(id)) throw NativeHostException(\\\"duplicate node id $id\\\")\\n }\\n}\\n\\n// --- In-memory renderer for JVM unit tests (no Android) ---\\n\\n/** An in-memory model node the [ModelRenderer] builds. */\\nclass ModelNode(val kind: String, var tag: String, var text: String) {\\n val props = HashMap<String, NativeProp>()\\n val events = HashMap<String, String>() // eventName -> handlerId\\n val children = ArrayList<ModelNode>()\\n\\n /** A compact structural string (tags + text) for assertions. */\\n fun serialize(): String =\\n if (kind == \\\"text\\\") text\\n else \\\"<$tag>\\\" + children.joinToString(\\\"\\\") { it.serialize() } + \\\"</$tag>\\\"\\n}\\n\\n/** A [HostRenderer] that builds a [ModelNode] tree — used by unit tests. */\\nclass ModelRenderer : HostRenderer<ModelNode> {\\n override fun makeElement(tag: String) = ModelNode(\\\"element\\\", tag, \\\"\\\")\\n override fun makeText(text: String) = ModelNode(\\\"text\\\", \\\"\\\", text)\\n override fun setText(view: ModelNode, text: String) { view.text = text }\\n override fun setProp(view: ModelNode, name: String, value: NativeProp) { view.props[name] = value }\\n override fun removeProp(view: ModelNode, name: String) { view.props.remove(name) }\\n override fun insert(parent: ModelNode, child: ModelNode, index: Int) { parent.children.add(index, child) }\\n override fun remove(parent: ModelNode, child: ModelNode) { parent.children.remove(child) }\\n override fun addEvent(view: ModelNode, eventName: String, handlerId: String, fire: (value: String?) -> Unit) {\\n view.events[eventName] = handlerId\\n }\\n override fun removeEvent(view: ModelNode, eventName: String, handlerId: String) {\\n view.events.remove(eventName)\\n }\\n override fun dispose(view: ModelNode) { view.children.clear() }\\n}\\n\",\n \"mindees-host/src/main/kotlin/dev/mindees/host/NativeCommand.kt\": \"/*\\n * NativeCommand.kt — the wire model for the MindeesNative native command protocol.\\n *\\n * Mirrors `@mindees/renderer`'s `native-protocol.ts`. The sealed types are pure\\n * Kotlin (unit-testable on the JVM); `NativeCommandCodec` decodes JSON via\\n * org.json (Android) and is exercised on-device.\\n *\\n * CI-verified by the native Android workflow; see the module README.\\n */\\n\\npackage dev.mindees.host\\n\\nimport org.json.JSONArray\\nimport org.json.JSONObject\\n\\n/** A serializable prop value (mirrors `NativePropValue`). Carries no functions. */\\nsealed interface NativeProp {\\n data class Str(val value: String) : NativeProp\\n data class Num(val value: Double) : NativeProp\\n data class Bool(val value: Boolean) : NativeProp\\n data object Null : NativeProp\\n data class Arr(val value: List<NativeProp>) : NativeProp\\n data class Obj(val value: Map<String, NativeProp>) : NativeProp\\n}\\n\\n/** One native command (mirrors the `NativeCommand` union). */\\nsealed interface NativeCommand {\\n data class CreateNode(val id: String, val tag: String) : NativeCommand\\n data class CreateText(val id: String, val text: String) : NativeCommand\\n data class SetProp(val id: String, val name: String, val value: NativeProp) : NativeCommand\\n data class RemoveProp(val id: String, val name: String) : NativeCommand\\n data class InsertChild(val parentId: String, val childId: String, val index: Int) : NativeCommand\\n data class RemoveChild(val parentId: String, val childId: String) : NativeCommand\\n data class UpdateText(val id: String, val text: String) : NativeCommand\\n data class DisposeNode(val id: String) : NativeCommand\\n data class RegisterEvent(val id: String, val eventName: String, val handlerId: String) : NativeCommand\\n data class UnregisterEvent(val id: String, val eventName: String, val handlerId: String) : NativeCommand\\n}\\n\\n/** Decodes a JSON command stream into [NativeCommand]s (Android: uses org.json). */\\nobject NativeCommandCodec {\\n fun decodeBatch(json: String): List<NativeCommand> {\\n val array = JSONArray(json)\\n return (0 until array.length()).map { decode(array.getJSONObject(it)) }\\n }\\n\\n fun decode(o: JSONObject): NativeCommand {\\n // Node ids are string|number on the wire; normalize to String.\\n fun id(key: String): String = o.get(key).toString()\\n return when (val type = o.getString(\\\"type\\\")) {\\n \\\"createNode\\\" -> NativeCommand.CreateNode(id(\\\"id\\\"), o.getString(\\\"tag\\\"))\\n \\\"createText\\\" -> NativeCommand.CreateText(id(\\\"id\\\"), o.getString(\\\"text\\\"))\\n \\\"setProp\\\" -> NativeCommand.SetProp(id(\\\"id\\\"), o.getString(\\\"name\\\"), decodeProp(o.get(\\\"value\\\")))\\n \\\"removeProp\\\" -> NativeCommand.RemoveProp(id(\\\"id\\\"), o.getString(\\\"name\\\"))\\n \\\"insertChild\\\" -> NativeCommand.InsertChild(id(\\\"parentId\\\"), id(\\\"childId\\\"), o.getInt(\\\"index\\\"))\\n \\\"removeChild\\\" -> NativeCommand.RemoveChild(id(\\\"parentId\\\"), id(\\\"childId\\\"))\\n \\\"updateText\\\" -> NativeCommand.UpdateText(id(\\\"id\\\"), o.getString(\\\"text\\\"))\\n \\\"disposeNode\\\" -> NativeCommand.DisposeNode(id(\\\"id\\\"))\\n \\\"registerEvent\\\" ->\\n NativeCommand.RegisterEvent(id(\\\"id\\\"), o.getString(\\\"eventName\\\"), o.getString(\\\"handlerId\\\"))\\n \\\"unregisterEvent\\\" ->\\n NativeCommand.UnregisterEvent(id(\\\"id\\\"), o.getString(\\\"eventName\\\"), o.getString(\\\"handlerId\\\"))\\n else -> throw IllegalArgumentException(\\\"unknown command type $type\\\")\\n }\\n }\\n\\n private fun decodeProp(value: Any?): NativeProp = when (value) {\\n null, JSONObject.NULL -> NativeProp.Null\\n is Boolean -> NativeProp.Bool(value)\\n is Int -> NativeProp.Num(value.toDouble())\\n is Long -> NativeProp.Num(value.toDouble())\\n is Double -> NativeProp.Num(value)\\n is String -> NativeProp.Str(value)\\n is JSONArray -> NativeProp.Arr((0 until value.length()).map { decodeProp(value.get(it)) })\\n is JSONObject -> NativeProp.Obj(value.keys().asSequence().associateWith { decodeProp(value.get(it)) })\\n else -> NativeProp.Str(value.toString())\\n }\\n}\\n\",\n \"settings.gradle.kts\": \"// CI-verified through .github/workflows/native-android.yml. Open in Android\\n// Studio (it provides Gradle + the wrapper) or run with a local Gradle. Align\\n// the plugin/SDK versions below with your installed toolchain if needed.\\n\\npluginManagement {\\n repositories {\\n google()\\n mavenCentral()\\n gradlePluginPortal()\\n }\\n plugins {\\n // Latest stable as of June 2026. AGP 9 has BUILT-IN Kotlin support, so the\\n // org.jetbrains.kotlin.android plugin is no longer applied (AGP errors if it\\n // is). Align AGP with your toolchain if Gradle complains (AGP 9.x needs\\n // JDK 17+ and a recent Gradle).\\n id(\\\"com.android.application\\\") version \\\"9.2.0\\\"\\n id(\\\"com.android.library\\\") version \\\"9.2.0\\\"\\n }\\n}\\n\\ndependencyResolutionManagement {\\n repositories {\\n google()\\n mavenCentral()\\n }\\n}\\n\\nrootProject.name = \\\"mindees-native-host-android\\\"\\ninclude(\\\":mindees-host\\\")\\ninclude(\\\":mindees-example-app\\\")\\n\",\n}\n\n/** The @mindees/* version the scaffolded app-js pins to (the line that generated it). */\nexport const ANDROID_TEMPLATE_VERSION = \"0.19.0\"\n"],"mappings":";;AAKA,MAAa,yBAAiD;CAC5D,cAAc;CACd,aAAa;CACb,oBAAoB;CACpB,qBAAqB;CACrB,2CAA2C;CAC3C,qDAAqD;CACrD,0CAA0C;CAC1C,gDAAgD;CAChD,gDAAgD;CAChD,2CAA2C;CAC3C,2CAA2C;CAC3C,4CAA4C;CAC5C,+CAA+C;CAC/C,wCAAwC;CACxC,oDAAoD;CACpD,0EAA0E;CAC1E,2EAA2E;CAC3E,mFAAmF;CACnF,iCAAiC;CACjC,6CAA6C;CAC7C,wEAAwE;CACxE,sEAAsE;CACtE,kEAAkE;CAClE,uBAAuB;AACzB"}
|
|
1
|
+
{"version":3,"file":"android-template.generated.js","names":[],"sources":["../src/android-template.generated.ts"],"sourcesContent":["// @generated by scripts/gen-android-template.mjs — do not edit by hand.\n// Regenerate: `pnpm --filter @mindees/cli run gen:android-template`\n// Source of truth: examples/native-hosts/android/ (the CI-verified reference host).\n\n/** Files the experimental `android` template scaffolds (project-relative path → contents). */\nexport const ANDROID_TEMPLATE_FILES: Record<string, string> = {\n \".gitignore\": \"# Android build outputs and local caches\\n.gradle/\\n.kotlin/\\nbuild/\\n\\n# The Gradle wrapper is bootstrapped in CI (see .github/workflows/native-android.yml)\\n# and locally on demand, not committed.\\n/gradlew\\n/gradlew.bat\\n/gradle/wrapper/\\n\\n# Local SDK pointer\\nlocal.properties\\n\\n# Generated file-based route map (codegen: app-js/scripts/gen-routes.mjs)\\nmindees-example-app/app-js/src/routes.gen.ts\\n\\n# The JS bundle is generated by the app-js build (never vendored).\\nmindees-example-app/src/main/assets/mindees-app.bundle.js\\n\",\n \"README.md\": \"# MindeesNative — Android app (experimental)\\n\\nA standalone native **Android** app built with MindeesNative. The UI is authored in\\nTypeScript/TSX (Atlas components + the Quantum file-based router) and runs on a real\\nAndroid view tree through an **embedded QuickJS** runtime. The native host\\n(`mindees-host/`) is **vendored as Kotlin source** — no Maven dependency on\\nMindeesNative is required.\\n\\n> **Experimental.** This scaffold is verified end-to-end in CI (the app-js bundle\\n> builds from npm and `gradle assembleDebug` produces an APK containing it), but the\\n> native mobile product is pre-1.0. Expect rough edges.\\n\\n## Prerequisites\\n\\n- **JDK 17**\\n- **Gradle 9.4.1+** (AGP 9.2 requires it) — used once to bootstrap the wrapper\\n- **Android SDK** with `platforms;android-36`\\n- **Node 20+** (for the app-js bundle)\\n\\n## Build (two phases, in order)\\n\\nThe native app loads `mindees-example-app/src/main/assets/mindees-app.bundle.js`,\\nwhich is **generated** from the TSX app — it is git-ignored and absent until you build\\nit. Build the JS bundle **first**, then the APK:\\n\\n```sh\\n# 1. Build the JS app bundle (installs @mindees/* from npm, runs route codegen + tsdown)\\ncd mindees-example-app/app-js\\nnpm install\\nnpm run build # → ../src/main/assets/mindees-app.bundle.js\\n\\n# 2. Build the Android APK\\ncd ../..\\ngradle wrapper --gradle-version 9.4.1 # one-time: the wrapper jar isn't vendored\\n./gradlew :mindees-example-app:assembleDebug\\n# → mindees-example-app/build/outputs/apk/debug/*.apk\\n```\\n\\nInstall the APK on a device/emulator with `./gradlew :mindees-example-app:installDebug`.\\n\\n## Project layout\\n\\n```\\nmindees-host/ # the native renderer + JS↔native bridge — VENDORED Kotlin source\\nmindees-example-app/\\n app-js/ # your TypeScript/TSX app (Atlas + router); builds the JS bundle\\n src/app/ # file-based routes — drop a .tsx file to add a screen\\n src/main.tsx # entry: createNativeApp(...)\\n src/main/kotlin/ # the Android host shell (MainActivity, QuickJS bridge)\\n```\\n\\nEdit your UI under `mindees-example-app/app-js/src/`, re-run the app-js build, then\\nre-run `assembleDebug`.\\n\\n## Notes & limitations (experimental)\\n\\n- The Android package id is `dev.mindees.example` and `rootProject.name` is fixed, so\\n two scaffolds can't be installed side-by-side yet (namespace parameterization is a\\n planned enhancement).\\n- App-level Android dependencies (QuickJS, AndroidX, FlexboxLayout) still come from\\n Maven Central / Google — \\\"no Maven\\\" applies to the **MindeesNative host**, which is\\n vendored as source.\\n\",\n \"build.gradle.kts\": \"// Root build file. Plugin versions are declared in settings.gradle.kts\\n// (pluginManagement), so modules apply them without a version here.\\n\",\n \"gradle.properties\": \"android.useAndroidX=true\\nkotlin.code.style=official\\norg.gradle.jvmargs=-Xmx2048m\\n\",\n \"mindees-example-app/app-js/package.json\": \"{\\n \\\"name\\\": \\\"mindees-android-app-js\\\",\\n \\\"version\\\": \\\"0.1.0\\\",\\n \\\"private\\\": true,\\n \\\"type\\\": \\\"module\\\",\\n \\\"scripts\\\": {\\n \\\"gen-routes\\\": \\\"node scripts/gen-routes.mjs\\\",\\n \\\"build\\\": \\\"npm run gen-routes && tsdown --config tsdown.config.ts\\\",\\n \\\"typecheck\\\": \\\"tsc --noEmit\\\"\\n },\\n \\\"dependencies\\\": {\\n \\\"@mindees/core\\\": \\\"0.21.0\\\",\\n \\\"@mindees/atlas\\\": \\\"0.21.0\\\",\\n \\\"@mindees/renderer\\\": \\\"0.21.0\\\",\\n \\\"@mindees/router\\\": \\\"0.21.0\\\",\\n \\\"@mindees/compiler\\\": \\\"0.21.0\\\"\\n },\\n \\\"devDependencies\\\": {\\n \\\"tsdown\\\": \\\"0.22.1\\\",\\n \\\"typescript\\\": \\\"6.0.3\\\"\\n }\\n}\\n\",\n \"mindees-example-app/app-js/scripts/gen-routes.mjs\": \"/**\\n * File-based-routing codegen. Scans `src/app/` and writes `src/routes.gen.ts` — a\\n * static-import module map the app feeds to `createFileRouter` (the QuickJS bundle has\\n * no `import.meta.glob`). Run by `npm run build` before bundling. The reusable codegen\\n * lives in `@mindees/compiler` (`generateRouteModule`); this script supplies the file\\n * list. `routes.gen.ts` is generated (git-ignored) — never edit it by hand.\\n */\\n\\nimport { readdirSync, statSync, writeFileSync } from 'node:fs'\\nimport { dirname, join } from 'node:path'\\nimport { fileURLToPath } from 'node:url'\\nimport { generateRouteModule } from '@mindees/compiler'\\n\\nconst here = dirname(fileURLToPath(import.meta.url))\\nconst appDir = join(here, '..', 'src', 'app')\\nconst outFile = join(here, '..', 'src', 'routes.gen.ts')\\n\\n/** Collect route files (relative, POSIX), skipping tests. */\\nfunction collect(dir, base = '') {\\n const out = []\\n for (const name of readdirSync(dir)) {\\n const full = join(dir, name)\\n const rel = base ? `${base}/${name}` : name\\n if (statSync(full).isDirectory()) out.push(...collect(full, rel))\\n else if (/\\\\.(tsx|ts|jsx|js)$/.test(name) && !/\\\\.test\\\\./.test(name)) out.push(rel)\\n }\\n return out\\n}\\n\\nconst files = collect(appDir)\\nwriteFileSync(outFile, generateRouteModule(files, { importBase: './app' }))\\nprocess.stdout.write(`gen-routes: wrote ${files.length} route(s) to src/routes.gen.ts\\\\n`)\\n\",\n \"mindees-example-app/app-js/src/App.tsx\": \"/**\\n * The example app — file-based routing, `@mindees/*` only, plain TSX.\\n *\\n * Routes live in `app/` (app/index.tsx → `/`, app/about.tsx → `/about`); the screens\\n * use the `useRouter()` hook (no router prop-drilling). `createFileRouter` turns the\\n * module map into a Quantum router with Expo-style conventions but a stronger core\\n * (validated params, fine-grained reads, codegen-free typing).\\n *\\n * @module\\n */\\n\\nimport { Column, space, useTheme } from '@mindees/atlas'\\nimport { createFileRouter, createMemoryHistory, createRouterView } from '@mindees/router'\\nimport { routes } from './routes.gen'\\n\\n// `routes` is generated from the `app/` directory (scripts/gen-routes.mjs +\\n// @mindees/compiler `generateRouteModule`), so adding a file under `app/` adds a route\\n// with no edits here. createFileRouter applies the Expo-style conventions.\\nconst router = createFileRouter(routes, {\\n history: createMemoryHistory({ initialEntries: ['/'] }),\\n})\\n\\n/** Full-screen shell: themed background (re-themes light↔dark), centers the active route. */\\nexport function App() {\\n const theme = useTheme()\\n return (\\n <Column\\n style={() => ({\\n flexGrow: 1,\\n width: '100%',\\n padding: space.lg,\\n alignItems: 'center',\\n justifyContent: 'center',\\n backgroundColor: theme().color.bg,\\n })}\\n >\\n {createRouterView(router)}\\n </Column>\\n )\\n}\\n\",\n \"mindees-example-app/app-js/src/app/about.tsx\": \"/**\\n * About route — `app/about.tsx` maps to `/about`. Showcases Atlas components\\n * (Card, Badge, Divider, Switch, ProgressBar) + design-token theming: the Switch\\n * toggles the device color scheme, so the whole UI re-themes light↔dark.\\n *\\n * @module\\n */\\n\\nimport {\\n ActivityIndicator,\\n Badge,\\n Button,\\n Card,\\n Divider,\\n fontSize,\\n ProgressBar,\\n Row,\\n Switch,\\n setEnvironment,\\n space,\\n Text,\\n useColorScheme,\\n useTheme,\\n} from '@mindees/atlas'\\nimport { useRouter } from '@mindees/router'\\nimport { buttonShape } from '../theme'\\n\\nexport default function About() {\\n const router = useRouter()\\n const theme = useTheme()\\n const colorScheme = useColorScheme()\\n return (\\n <Card variant=\\\"filled\\\" style={{ minWidth: 300, gap: space.md, alignItems: 'stretch' }}>\\n <Row style={{ justifyContent: 'space-between', alignItems: 'center' }}>\\n <Text\\n style={() => ({ fontSize: fontSize.title2, fontWeight: 800, color: theme().color.text })}\\n >\\n About\\n </Text>\\n <Badge tone=\\\"info\\\">v0.1.0</Badge>\\n </Row>\\n <Divider />\\n <Text\\n style={() => ({ fontSize: fontSize.body, color: theme().color.textMuted, lineHeight: 22 })}\\n >\\n File-based routes navigated by the Quantum router via the useRouter hook — built from themed\\n Atlas components, all TypeScript, running native in an embedded engine.\\n </Text>\\n <Row style={{ justifyContent: 'space-between', alignItems: 'center' }}>\\n <Text style={() => ({ fontSize: fontSize.body, color: theme().color.text })}>\\n Dark mode\\n </Text>\\n <Switch\\n value={() => colorScheme() === 'dark'}\\n onValueChange={(v) => setEnvironment({ colorScheme: v ? 'dark' : 'light' })}\\n label=\\\"Dark mode\\\"\\n />\\n </Row>\\n <Row style={{ gap: space.sm, alignItems: 'center' }}>\\n <ActivityIndicator size={20} />\\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\\n Syncing…\\n </Text>\\n </Row>\\n <ProgressBar value={0.6} />\\n <Button\\n title=\\\"← Home\\\"\\n onPress={() => router.navigate('/')}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.primary,\\n color: theme().color.onPrimary,\\n })}\\n />\\n </Card>\\n )\\n}\\n\",\n \"mindees-example-app/app-js/src/app/index.tsx\": \"/**\\n * Home route — `app/index.tsx` maps to `/` (file-based routing). Themed via design\\n * tokens (`useTheme`), so it re-themes light↔dark with the device color scheme.\\n *\\n * @module\\n */\\n\\nimport {\\n Button,\\n Card,\\n fontSize,\\n Row,\\n space,\\n Text,\\n useColorScheme,\\n useTheme,\\n useWindowDimensions,\\n View,\\n} from '@mindees/atlas'\\nimport { animate, signal, spring } from '@mindees/core'\\nimport { useRouter } from '@mindees/router'\\nimport { buttonShape } from '../theme'\\n\\n/** Module-scoped state survives navigation. */\\nconst done = signal(0)\\n/** An animated bar width — springs on press, driven by vsync on a native host (see FrameDriver). */\\nconst barWidth = animate(48)\\n\\nexport default function Home() {\\n const router = useRouter()\\n const theme = useTheme()\\n const dimensions = useWindowDimensions()\\n const colorScheme = useColorScheme()\\n return (\\n <Card style={{ minWidth: 300, gap: space.md, alignItems: 'center' }}>\\n <Text\\n style={() => ({ fontSize: fontSize.title2, fontWeight: 800, color: theme().color.text })}\\n >\\n MindeesNative\\n </Text>\\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\\n File-based routing · native · TypeScript\\n </Text>\\n <Text style={() => ({ fontSize: 36, fontWeight: 800, color: theme().color.primary })}>\\n {() => `Done today: ${done()}`}\\n </Text>\\n <Row style={{ gap: space.sm, justifyContent: 'center' }}>\\n <Button\\n title=\\\"Mark done\\\"\\n onPress={() => done.set(done() + 1)}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.primary,\\n color: theme().color.onPrimary,\\n })}\\n />\\n <Button\\n title=\\\"About →\\\"\\n onPress={() => router.navigate('/about')}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.surfaceVariant,\\n color: theme().color.text,\\n })}\\n />\\n </Row>\\n <View\\n testID=\\\"pulse-bar\\\"\\n style={() => ({\\n width: barWidth(),\\n height: 8,\\n borderRadius: 4,\\n backgroundColor: theme().color.primary,\\n })}\\n />\\n <Button\\n title=\\\"Animate ✨\\\"\\n onPress={() => spring(barWidth, { to: barWidth() > 160 ? 48 : 240 })}\\n style={() => ({\\n ...buttonShape,\\n backgroundColor: theme().color.surfaceVariant,\\n color: theme().color.text,\\n })}\\n />\\n <Text style={() => ({ fontSize: fontSize.footnote, color: theme().color.textMuted })}>\\n {() =>\\n `Screen ${Math.round(dimensions().width)}×${Math.round(dimensions().height)} · ${colorScheme()}`\\n }\\n </Text>\\n </Card>\\n )\\n}\\n\",\n \"mindees-example-app/app-js/src/main.tsx\": \"/**\\n * App entry. The whole native wiring — command backend, render, flush, the\\n * `MindeesApp.start()/dispatchEvent()` contract the host calls — is handled by\\n * `createNativeApp`. An app author writes only this.\\n *\\n * @module\\n */\\n\\nimport { setEnvironment } from '@mindees/atlas'\\nimport { createNativeApp } from '@mindees/renderer'\\nimport { App } from './App'\\n\\n// The host injects the platform environment (window size, color scheme) before this\\n// bundle evaluates; apply it so the device hooks (useWindowDimensions/useColorScheme)\\n// read real values from the first render.\\nconst envHost = (globalThis as { MindeesEnv?: { get(): string } }).MindeesEnv\\nif (envHost) {\\n try {\\n setEnvironment(JSON.parse(envHost.get()))\\n } catch {\\n // No/!invalid environment — hooks fall back to defaults.\\n }\\n}\\n\\ncreateNativeApp(<App />)\\n\",\n \"mindees-example-app/app-js/src/theme.ts\": \"/** Token-based shape helpers for the example (colors come from `useTheme` per screen). */\\n\\nimport { fontWeight, radius, space } from '@mindees/atlas'\\n\\n/** Shared button shape (background/foreground colors applied per screen from the theme). */\\nexport const buttonShape = {\\n paddingTop: space.sm,\\n paddingBottom: space.sm,\\n paddingLeft: space.lg,\\n paddingRight: space.lg,\\n borderRadius: radius.md,\\n fontWeight: fontWeight.semibold,\\n} as const\\n\",\n \"mindees-example-app/app-js/tsconfig.json\": \"{\\n \\\"compilerOptions\\\": {\\n \\\"target\\\": \\\"ES2020\\\",\\n \\\"module\\\": \\\"ESNext\\\",\\n \\\"moduleResolution\\\": \\\"Bundler\\\",\\n \\\"jsx\\\": \\\"react-jsx\\\",\\n \\\"jsxImportSource\\\": \\\"@mindees/core\\\",\\n \\\"strict\\\": true,\\n \\\"skipLibCheck\\\": true,\\n \\\"noEmit\\\": true\\n },\\n \\\"include\\\": [\\\"src\\\"]\\n}\\n\",\n \"mindees-example-app/app-js/tsdown.config.ts\": \"import { dirname, resolve } from 'node:path'\\nimport { fileURLToPath } from 'node:url'\\nimport { defineConfig } from 'tsdown'\\n\\nconst here = dirname(fileURLToPath(import.meta.url))\\n\\n/**\\n * Bundles the TSX-authored Atlas + Helix + Quantum app (src/main.tsx) into a single\\n * QuickJS-safe IIFE the Android host loads from assets. `@mindees/*` resolve from\\n * node_modules via their package `exports` (incl. the automatic JSX runtime subpath\\n * `@mindees/core/jsx-runtime`, see tsconfig.json) and are inlined into the IIFE — the\\n * framework has no node-builtin runtime deps. A banner polyfills `queueMicrotask`\\n * (some embedded engines lack it) before any module initializes.\\n */\\nexport default defineConfig({\\n entry: [resolve(here, 'src/main.tsx')],\\n outDir: resolve(here, '..', 'src', 'main', 'assets'),\\n format: ['iife'],\\n globalName: 'MindeesAppBundle',\\n platform: 'node',\\n target: 'es2020',\\n // Inline the framework packages into the IIFE (they are package.json `dependencies`,\\n // which tsdown would otherwise treat as external bare imports — the embedded QuickJS\\n // engine has no module loader, so everything must be bundled).\\n noExternal: [/^@mindees\\\\//],\\n dts: false,\\n clean: false,\\n minify: false,\\n define: { 'process.env.NODE_ENV': JSON.stringify('production') },\\n outputOptions: {\\n entryFileNames: 'mindees-app.bundle.js',\\n banner:\\n \\\"if (typeof globalThis !== 'undefined' && typeof globalThis.queueMicrotask !== 'function') { globalThis.queueMicrotask = function (cb) { Promise.resolve().then(cb); }; }\\\",\\n },\\n})\\n\",\n \"mindees-example-app/build.gradle.kts\": \"plugins {\\n id(\\\"com.android.application\\\")\\n}\\n\\nandroid {\\n namespace = \\\"dev.mindees.example\\\"\\n compileSdk = 36\\n\\n defaultConfig {\\n applicationId = \\\"{{androidAppId}}\\\"\\n minSdk = 24\\n targetSdk = 36\\n versionCode = 1\\n versionName = \\\"0.0.0\\\"\\n testInstrumentationRunner = \\\"androidx.test.runner.AndroidJUnitRunner\\\"\\n }\\n\\n compileOptions {\\n sourceCompatibility = JavaVersion.VERSION_17\\n targetCompatibility = JavaVersion.VERSION_17\\n }\\n\\n testOptions {\\n unitTests {\\n isReturnDefaultValues = true\\n }\\n }\\n}\\n\\nkotlin {\\n jvmToolchain(17)\\n}\\n\\ndependencies {\\n implementation(project(\\\":mindees-host\\\"))\\n implementation(\\\"app.cash.quickjs:quickjs-android:0.9.2\\\")\\n // FlexboxLayout (used by the renderer + the root container) needs androidx.core (ViewCompat)\\n // on the runtime classpath; this app is otherwise framework-only, so declare it explicitly.\\n implementation(\\\"androidx.core:core:1.13.1\\\")\\n\\n testImplementation(\\\"junit:junit:4.13.2\\\")\\n testImplementation(\\\"org.json:json:20231013\\\")\\n\\n androidTestImplementation(\\\"androidx.test:core:1.7.0\\\")\\n androidTestImplementation(\\\"androidx.test:runner:1.7.0\\\")\\n androidTestImplementation(\\\"androidx.test.ext:junit:1.3.0\\\")\\n}\\n\",\n \"mindees-example-app/src/main/AndroidManifest.xml\": \"<manifest xmlns:android=\\\"http://schemas.android.com/apk/res/android\\\">\\n <application\\n android:allowBackup=\\\"false\\\"\\n android:label=\\\"Mindees Native Example\\\"\\n android:supportsRtl=\\\"true\\\"\\n android:theme=\\\"@android:style/Theme.Material.Light.NoActionBar\\\">\\n <activity\\n android:name=\\\".MainActivity\\\"\\n android:exported=\\\"true\\\">\\n <intent-filter>\\n <action android:name=\\\"android.intent.action.MAIN\\\" />\\n <category android:name=\\\"android.intent.category.LAUNCHER\\\" />\\n </intent-filter>\\n </activity>\\n </application>\\n</manifest>\\n\",\n \"mindees-example-app/src/main/kotlin/dev/mindees/example/FrameDriver.kt\": \"package dev.mindees.example\\n\\nimport android.view.Choreographer\\n\\n/**\\n * Drives the JS animation frame loop from Android vsync via [Choreographer] — but **only while\\n * active**. The JS side (`createNativeApp`) calls `MindeesHostFrame.setFrameLoopActive(true)` the\\n * instant an animation arms and `(false)` the instant the last one settles, so an idle app posts\\n * **zero** frame callbacks (no 60fps busy-loop). Construct and use on the UI thread (Choreographer is\\n * UI-thread-only).\\n *\\n * @param tick called once per vsync with the frame time in milliseconds (`frameTimeNanos / 1e6`).\\n */\\nclass FrameDriver(private val tick: (Double) -> Unit) {\\n private var running = false\\n\\n // An explicit anonymous object (NOT a SAM lambda): the body re-posts `this`, and a lambda that\\n // referenced its own `val` would make Kotlin's type inference recurse.\\n private val callback: Choreographer.FrameCallback = object : Choreographer.FrameCallback {\\n override fun doFrame(frameTimeNanos: Long) {\\n if (!running) return\\n tick(frameTimeNanos / 1_000_000.0)\\n // Re-perpetuate ONLY while running; setActive(false) simply stops re-posting, so the loop\\n // dies after the in-flight frame — nothing to cancel, no leaked callback.\\n Choreographer.getInstance().postFrameCallback(this)\\n }\\n }\\n\\n /** Start (true) or stop (false) the vsync loop. Idempotent. */\\n fun setActive(active: Boolean) {\\n if (active == running) return\\n running = active\\n if (active) Choreographer.getInstance().postFrameCallback(callback)\\n }\\n}\\n\",\n \"mindees-example-app/src/main/kotlin/dev/mindees/example/MainActivity.kt\": \"package dev.mindees.example\\n\\nimport android.app.Activity\\nimport android.content.res.Configuration\\nimport android.graphics.Color\\nimport android.os.Bundle\\nimport android.os.Handler\\nimport android.os.Looper\\nimport android.view.View\\nimport android.view.ViewGroup\\nimport android.widget.FrameLayout\\nimport dev.mindees.host.AndroidViewRenderer\\nimport dev.mindees.host.MindeesNativeHost\\n\\nclass MainActivity : Activity() {\\n private var bridge: MindeesRuntimeBridge<View>? = null\\n private var frameDriver: FrameDriver? = null\\n\\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n\\n // Fill the window and paint a dark base: the Atlas app controls its own layout and\\n // background edge-to-edge, so the host window should match it (no light gaps showing\\n // through where content hasn't laid out yet).\\n // A FrameLayout root z-stacks the app content + the portal `overlay` layer, so Modal/Toast\\n // (which the renderer mounts into a full-screen `overlay` node, kept painting last) overlap\\n // the content instead of being laid out beneath it.\\n val root = FrameLayout(this).apply {\\n layoutParams = ViewGroup.LayoutParams(\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n )\\n setBackgroundColor(Color.parseColor(\\\"#0b1021\\\"))\\n }\\n val renderer = AndroidViewRenderer(this)\\n val mainHandler = Handler(Looper.getMainLooper())\\n\\n val host = MindeesNativeHost<View>(\\n rootId = HOST_ROOT_ID,\\n root = root,\\n renderer = renderer,\\n onEvent = { handlerId, value ->\\n bridge?.dispatchEvent(handlerId, value)\\n ?: error(\\\"Mindees runtime bridge has not started\\\")\\n },\\n )\\n\\n // The real Atlas + Helix app, bundled to a QuickJS-safe IIFE (app-js/, regenerate\\n // with `pnpm run build:android-example-js`). It runs @mindees/core signals +\\n // @mindees/atlas primitives + the @mindees/renderer reconciler inside QuickJS and\\n // emits the native command stream this host materializes — not hand-written commands.\\n val appJs = assets.open(APP_BUNDLE_ASSET).bufferedReader().use { it.readText() }\\n\\n // Drives JS animation frames from vsync; constructed on the UI thread. `bridge` is assigned\\n // just below and is non-null by the time the first frame callback fires (next vsync).\\n val driver = FrameDriver { nowMs -> bridge?.frameTick(nowMs) }\\n frameDriver = driver\\n\\n bridge = MindeesRuntimeBridge(\\n host = host,\\n // The JS engine arms/sleeps the vsync loop through setFrameLoopActive → FrameDriver.\\n runtime = QuickJsMindeesRuntime(\\n appJs,\\n environmentJson(),\\n onFrameLoopActive = { active -> driver.setActive(active) },\\n ),\\n applyOnHostThread = { action ->\\n if (Looper.myLooper() == Looper.getMainLooper()) {\\n action()\\n } else {\\n mainHandler.post(action)\\n }\\n },\\n ).also { it.start() }\\n\\n setContentView(root)\\n }\\n\\n override fun onDestroy() {\\n frameDriver?.setActive(false) // stop vsync before tearing down the engine\\n frameDriver = null\\n bridge?.close()\\n bridge = null\\n super.onDestroy()\\n }\\n\\n /**\\n * The platform environment for `@mindees/atlas` device hooks (useWindowDimensions,\\n * useColorScheme, …), as JSON. Logical dp = pixels / density.\\n */\\n private fun environmentJson(): String {\\n val dm = resources.displayMetrics\\n val cfg = resources.configuration\\n val widthDp = (dm.widthPixels / dm.density)\\n val heightDp = (dm.heightPixels / dm.density)\\n val isDark =\\n (cfg.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\\n return \\\"\\\"\\\"\\n {\\\"window\\\":{\\\"width\\\":$widthDp,\\\"height\\\":$heightDp,\\\"scale\\\":${dm.density},\\\"\\\"\\\" +\\n \\\"\\\"\\\"\\\"fontScale\\\":${cfg.fontScale}},\\\"colorScheme\\\":\\\"${if (isDark) \\\"dark\\\" else \\\"light\\\"}\\\"}\\n \\\"\\\"\\\".trimIndent()\\n }\\n\\n private companion object {\\n const val HOST_ROOT_ID = \\\"host-root\\\"\\n\\n /** The bundled real Atlas + Helix app (see app-js/ + tsdown.config.ts). */\\n const val APP_BUNDLE_ASSET = \\\"mindees-app.bundle.js\\\"\\n }\\n}\\n\",\n \"mindees-example-app/src/main/kotlin/dev/mindees/example/MindeesRuntimeBridge.kt\": \"package dev.mindees.example\\n\\nimport app.cash.quickjs.QuickJs\\nimport dev.mindees.host.MindeesNativeHost\\nimport dev.mindees.host.NativeCommandCodec\\nimport java.io.Closeable\\n\\ninterface NativeCommandSink {\\n fun applyBatch(json: String)\\n}\\n\\ninterface MindeesScriptRuntime : Closeable {\\n fun start(sink: NativeCommandSink)\\n fun dispatchEvent(handlerId: String, value: String?)\\n\\n /** Advance animations by one vsync frame (time in ms). Drives `MindeesApp.frameTick`. */\\n fun frameTick(nowMs: Double)\\n}\\n\\nclass MindeesRuntimeBridge<V>(\\n private val host: MindeesNativeHost<V>,\\n private val runtime: MindeesScriptRuntime,\\n private val applyOnHostThread: ((() -> Unit) -> Unit) = { it() },\\n) : NativeCommandSink, Closeable {\\n private var started = false\\n\\n fun start() {\\n check(!started) { \\\"Mindees runtime bridge already started\\\" }\\n try {\\n runtime.start(this)\\n started = true\\n } catch (t: Throwable) {\\n try {\\n runtime.close()\\n } catch (closeError: Throwable) {\\n t.addSuppressed(closeError)\\n }\\n throw t\\n }\\n }\\n\\n override fun applyBatch(json: String) {\\n applyOnHostThread {\\n host.apply(NativeCommandCodec.decodeBatch(json))\\n }\\n }\\n\\n fun dispatchEvent(handlerId: String, value: String?) {\\n check(started) { \\\"Mindees runtime bridge has not started\\\" }\\n runtime.dispatchEvent(handlerId, value)\\n }\\n\\n /** Forward a vsync frame to the JS animation engine (called by the [FrameDriver]). */\\n fun frameTick(nowMs: Double) {\\n if (started) runtime.frameTick(nowMs)\\n }\\n\\n override fun close() {\\n if (started) {\\n try {\\n runtime.close()\\n } finally {\\n started = false\\n }\\n }\\n }\\n}\\n\\ninterface MindeesHostApi {\\n fun emit(json: String)\\n}\\n\\n/** Supplies the platform environment (window size, color scheme, …) to the bundle. */\\ninterface MindeesEnvApi {\\n fun get(): String\\n}\\n\\n/** The JS→host battery signal: `createNativeApp` asks the host to run / stop its vsync loop. */\\ninterface MindeesFrameApi {\\n fun setFrameLoopActive(active: Boolean)\\n}\\n\\ninterface MindeesAppApi {\\n fun start()\\n fun dispatchEvent(handlerId: String, value: String?)\\n fun frameTick(nowMs: Double)\\n}\\n\\nclass QuickJsMindeesRuntime(\\n private val source: String,\\n /** JSON for `setEnvironment` (window dimensions, color scheme, …). Default: empty. */\\n private val environmentJson: String = \\\"{}\\\",\\n /** Called when the JS engine arms (true) / sleeps (false) its animation loop — drive the [FrameDriver]. */\\n private val onFrameLoopActive: (Boolean) -> Unit = {},\\n) : MindeesScriptRuntime {\\n private var engine: QuickJs? = null\\n private var app: MindeesAppApi? = null\\n\\n override fun start(sink: NativeCommandSink) {\\n check(engine == null) { \\\"QuickJS runtime already started\\\" }\\n\\n val quickJs = QuickJs.create()\\n try {\\n quickJs.set(\\n \\\"MindeesHost\\\",\\n MindeesHostApi::class.java,\\n object : MindeesHostApi {\\n override fun emit(json: String) {\\n sink.applyBatch(json)\\n }\\n },\\n )\\n // Injected before evaluate so the bundle's entry can read it and call\\n // setEnvironment() before the first render.\\n quickJs.set(\\n \\\"MindeesEnv\\\",\\n MindeesEnvApi::class.java,\\n object : MindeesEnvApi {\\n override fun get(): String = environmentJson\\n },\\n )\\n // Injected before evaluate so createNativeApp installs the vsync frame source. The JS\\n // engine calls this to start/stop the host's Choreographer loop (battery: only while\\n // something is animating).\\n quickJs.set(\\n \\\"MindeesHostFrame\\\",\\n MindeesFrameApi::class.java,\\n object : MindeesFrameApi {\\n override fun setFrameLoopActive(active: Boolean) {\\n onFrameLoopActive(active)\\n }\\n },\\n )\\n quickJs.evaluate(source, \\\"mindees-example.js\\\")\\n val mindeesApp = quickJs.get(\\\"MindeesApp\\\", MindeesAppApi::class.java)\\n mindeesApp.start()\\n engine = quickJs\\n app = mindeesApp\\n } catch (t: Throwable) {\\n quickJs.close()\\n throw t\\n }\\n }\\n\\n override fun dispatchEvent(handlerId: String, value: String?) {\\n app?.dispatchEvent(handlerId, value)\\n ?: error(\\\"QuickJS MindeesApp has not started\\\")\\n }\\n\\n override fun frameTick(nowMs: Double) {\\n app?.frameTick(nowMs)\\n }\\n\\n override fun close() {\\n app = null\\n engine?.close()\\n engine = null\\n }\\n}\\n\",\n \"mindees-host/build.gradle.kts\": \"// CI-verified by .github/workflows/native-android.yml. Align compileSdk / Java /\\n// plugin versions with your installed toolchain if needed.\\n\\nplugins {\\n // AGP 9 includes built-in Kotlin support — no separate kotlin-android plugin.\\n id(\\\"com.android.library\\\")\\n}\\n\\nandroid {\\n namespace = \\\"dev.mindees.host\\\"\\n compileSdk = 36\\n\\n defaultConfig {\\n minSdk = 24\\n }\\n\\n compileOptions {\\n sourceCompatibility = JavaVersion.VERSION_17\\n targetCompatibility = JavaVersion.VERSION_17\\n }\\n\\n testOptions {\\n unitTests {\\n // The host + ModelRenderer are pure Kotlin; JSON-codec tests use the real\\n // org.json (added below). Defaults keep any stray android stub call quiet.\\n isReturnDefaultValues = true\\n // Robolectric renders AndroidViewRenderer against real android.view on the JVM.\\n isIncludeAndroidResources = true\\n }\\n }\\n}\\n\\n// AGP's built-in Kotlin exposes the standard `kotlin {}` DSL.\\nkotlin {\\n jvmToolchain(17)\\n}\\n\\ndependencies {\\n // Google FlexboxLayout backs the renderer's flex containers (flex-wrap + space-* + alignSelf).\\n // `api` (not `implementation`) so a host app's root container can be a FlexboxLayout too.\\n api(\\\"com.google.android.flexbox:flexbox:3.0.0\\\")\\n\\n testImplementation(\\\"junit:junit:4.13.2\\\") // JUnit 4 = AGP's default unit-test framework\\n testImplementation(\\\"org.json:json:20231013\\\") // real org.json for codec unit tests\\n // Robolectric runs AndroidViewRenderer against real android.view classes on the JVM\\n // (no emulator/device needed) so the render test runs in `./gradlew :mindees-host:test`.\\n testImplementation(\\\"org.robolectric:robolectric:4.14.1\\\")\\n}\\n\",\n \"mindees-host/src/main/AndroidManifest.xml\": \"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"?>\\n<!-- Library manifest. The package namespace is set in build.gradle.kts. -->\\n<manifest xmlns:android=\\\"http://schemas.android.com/apk/res/android\\\" />\\n\",\n \"mindees-host/src/main/kotlin/dev/mindees/host/AndroidViewRenderer.kt\": \"/*\\n * AndroidViewRenderer.kt — a HostRenderer that builds real android.view widgets from\\n * the MindeesNative command stream, mapping Atlas's curated cross-platform `StyleObject`\\n * onto native layout + visuals.\\n *\\n * Layout uses Google FlexboxLayout for full flex parity: flexDirection,\\n * justifyContent (incl. space-between/around/evenly), alignItems, flexWrap, alignSelf,\\n * gap (→ child margins), flex/flexGrow (→ FlexboxLayout.LayoutParams.flexGrow) — plus\\n * the box model, background/radius/border, opacity, and text styling.\\n *\\n * Device-facing, but JVM-testable via Robolectric (AndroidRenderTest) and on-device\\n * by the native Android workflow. See the module README.\\n */\\n\\npackage dev.mindees.host\\n\\nimport android.content.Context\\nimport android.content.res.ColorStateList\\nimport android.graphics.BitmapFactory\\nimport android.graphics.Color\\nimport android.graphics.PorterDuff\\nimport android.graphics.Typeface\\nimport android.graphics.drawable.GradientDrawable\\nimport android.text.Editable\\nimport android.text.InputType\\nimport android.text.TextUtils\\nimport android.text.TextWatcher\\nimport android.util.Base64\\nimport android.util.TypedValue\\nimport android.view.Gravity\\nimport android.view.View\\nimport android.view.ViewGroup\\nimport android.widget.Button\\nimport android.widget.EditText\\nimport android.widget.FrameLayout\\nimport android.widget.HorizontalScrollView\\nimport android.widget.ImageView\\nimport android.widget.ProgressBar\\nimport android.widget.ScrollView\\nimport android.widget.TextView\\nimport com.google.android.flexbox.AlignItems\\nimport com.google.android.flexbox.AlignSelf\\nimport com.google.android.flexbox.FlexDirection\\nimport com.google.android.flexbox.FlexWrap\\nimport com.google.android.flexbox.FlexboxLayout\\nimport com.google.android.flexbox.JustifyContent\\nimport java.net.HttpURLConnection\\nimport java.net.URL\\nimport java.util.concurrent.ExecutorService\\nimport java.util.concurrent.Executors\\n\\n/** Loads bytes for a remote image `url`, invoking `onResult` with the bytes (or null on failure). */\\ntypealias ImageLoader = (url: String, onResult: (ByteArray?) -> Unit) -> Unit\\n\\n/** Builds Android views from the command stream. Pair with [MindeesNativeHost]. */\\nclass AndroidViewRenderer(\\n private val context: Context,\\n /** Override how remote (`http(s)`) images are fetched (e.g. to inject a cache or a test stub). */\\n private val imageLoader: ImageLoader? = null,\\n) : HostRenderer<View> {\\n\\n private val density: Float = context.resources.displayMetrics.density\\n\\n /** Off-main fetch pool for remote images (created lazily; one idle thread for the renderer's life). */\\n private val imageExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }\\n /** Per-ImageView load generation, so a slow/stale fetch never overwrites a newer src or a disposed view. */\\n private val imageGen = HashMap<View, Long>()\\n\\n /** dp → px (Atlas numeric style values are density-independent pixels on native). */\\n private fun dp(value: Double): Int = Math.round(value * density).toInt()\\n\\n /**\\n * Layout intent we must (re)apply via LayoutParams when a child is inserted, or\\n * when a container's gap/direction changes. Kept off the View so the mapping stays\\n * explicit and testable.\\n */\\n private class Layout {\\n var horizontal = false\\n var gapPx = 0\\n var widthSpec = ViewGroup.LayoutParams.WRAP_CONTENT\\n var heightSpec = ViewGroup.LayoutParams.WRAP_CONTENT\\n var grow = 0f\\n var alignSelf = AlignSelf.AUTO\\n }\\n\\n private val layouts = HashMap<View, Layout>()\\n private fun layoutOf(view: View): Layout = layouts.getOrPut(view) { Layout() }\\n\\n /**\\n * A `scrollview` node is a ScrollView whose ONE child is a FlexboxLayout \\\"content\\\" host. Children\\n * and flex/layout styles route to that content; the ScrollView itself is just the viewport.\\n */\\n private val scrollContent = HashMap<View, FlexboxLayout>()\\n private fun contentFor(view: View): View = scrollContent[view] ?: view\\n\\n /** Per-EditText keyboard/secure/multiline intent, recomputed into inputType (compose any order). */\\n private class InputSpec {\\n var keyboard = \\\"text\\\"\\n var secure = false\\n var multiline = false\\n }\\n private val inputSpecs = HashMap<View, InputSpec>()\\n private fun inputSpecOf(view: View): InputSpec = inputSpecs.getOrPut(view) { InputSpec() }\\n /** Active text-change watchers keyed by view then eventName, so onInput and onChange are\\n * independent (both can be registered) and removeEvent/dispose detach precisely (no leaks). */\\n private val textWatchers = HashMap<View, HashMap<String, TextWatcher>>()\\n\\n /**\\n * Text composition. A leaf widget (TextView/Button/EditText) can't hold child views,\\n * but the element model nests text *nodes* inside a `text` *element* (e.g. Atlas's\\n * `Text` → `<text>\\\"hello\\\"</text>`). So we compose a text element's text-node children\\n * into its own `text` instead of attaching them as views. `textParts` keeps each\\n * element's ordered text-node children; `textOwner` is the reverse lookup so an\\n * `updateText` on a child re-composes its owner.\\n */\\n private val textParts = HashMap<View, MutableList<View>>()\\n private val textOwner = HashMap<View, View>()\\n\\n private fun recomposeText(element: View) {\\n val tv = element as? TextView ?: return\\n val parts = textParts[element] ?: return\\n tv.text = parts.joinToString(\\\"\\\") { (it as? TextView)?.text ?: \\\"\\\" }\\n }\\n\\n // --- Node creation ---\\n\\n override fun makeElement(tag: String): View = when (tag) {\\n \\\"text\\\" -> TextView(context)\\n \\\"image\\\" -> ImageView(context)\\n \\\"textinput\\\" -> EditText(context)\\n \\\"button\\\" -> Button(context) // direct use; Atlas Button renders as a clickable 'view'\\n // Atlas ActivityIndicator → a native indeterminate spinner.\\n \\\"activityindicator\\\" -> ProgressBar(context).apply {\\n isIndeterminate = true\\n layoutOf(this)\\n }\\n // 'scrollview' → a real vertical ScrollView wrapping a FlexboxLayout content host.\\n // Children/styles route to the inner content (see contentFor); the ScrollView is the viewport.\\n \\\"scrollview\\\" -> ScrollView(context).apply {\\n isFillViewport = true\\n val content = FlexboxLayout(context).apply { flexDirection = FlexDirection.COLUMN }\\n content.layoutParams = FrameLayout.LayoutParams(\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n ViewGroup.LayoutParams.WRAP_CONTENT,\\n )\\n addView(content)\\n scrollContent[this] = content\\n layoutOf(content)\\n }\\n // 'horizontalscrollview' → a HorizontalScrollView wrapping a ROW content host. Mirror of the\\n // vertical case: content is WRAP width / MATCH height so the row can grow past the viewport on X.\\n \\\"horizontalscrollview\\\" -> HorizontalScrollView(context).apply {\\n isFillViewport = true\\n val content = FlexboxLayout(context).apply { flexDirection = FlexDirection.ROW }\\n content.layoutParams = FrameLayout.LayoutParams(\\n ViewGroup.LayoutParams.WRAP_CONTENT,\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n )\\n addView(content)\\n scrollContent[this] = content\\n layoutOf(content).horizontal = true // gaps go on the X axis even before any style arrives\\n }\\n // 'overlay' → the portal layer (Modal/Toast mount here). A full-screen flex container that\\n // FILLS its parent so it overlaps the app content (the host root is a FrameLayout that\\n // z-stacks; the renderer keeps the overlay painting last, so it sits on top).\\n \\\"overlay\\\" -> FlexboxLayout(context).apply {\\n flexDirection = FlexDirection.COLUMN\\n layoutOf(this).apply {\\n widthSpec = ViewGroup.LayoutParams.MATCH_PARENT\\n heightSpec = ViewGroup.LayoutParams.MATCH_PARENT\\n }\\n }\\n // 'view' / unknown → a real flex container (FlexboxLayout): full\\n // flexDirection / justifyContent (incl. space-*) / alignItems / flexWrap / alignSelf.\\n else -> FlexboxLayout(context).apply {\\n flexDirection = FlexDirection.COLUMN\\n layoutOf(this)\\n }\\n }\\n\\n override fun makeText(text: String): View = TextView(context).apply { this.text = text }\\n\\n override fun setText(view: View, text: String) {\\n (view as? TextView)?.text = text\\n // If this text node is composed into a parent text element, re-compose the parent.\\n textOwner[view]?.let { recomposeText(it) }\\n }\\n\\n // --- Props ---\\n\\n override fun setProp(view: View, name: String, value: NativeProp) {\\n when (name) {\\n \\\"style\\\" -> (value as? NativeProp.Obj)?.let { applyStyle(view, it.value) }\\n \\\"accessibilityLabel\\\", \\\"aria-label\\\", \\\"label\\\" -> view.contentDescription = strOf(value)\\n \\\"title\\\", \\\"text\\\" -> (view as? TextView)?.text = strOf(value)\\n \\\"placeholder\\\" -> (view as? EditText)?.hint = strOf(value)\\n \\\"value\\\" -> (view as? EditText)?.let { if (it.text.toString() != strOf(value)) it.setText(strOf(value)) }\\n \\\"hidden\\\" -> view.visibility = if (boolOf(value)) View.GONE else View.VISIBLE\\n // Image source (data:/base64 + bundled asset load synchronously; remote is a follow-up).\\n \\\"src\\\", \\\"source\\\" -> (view as? ImageView)?.let { applyImageSource(it, value) }\\n \\\"resizeMode\\\" -> (view as? ImageView)?.let { it.scaleType = scaleTypeFor(strOf(value) ?: \\\"contain\\\") }\\n \\\"tintColor\\\" -> (view as? ImageView)?.let { iv ->\\n val c = color(value)\\n if (c != null) iv.setColorFilter(c, PorterDuff.Mode.SRC_IN) else iv.clearColorFilter()\\n }\\n // TextInput: keyboard / multiline / secure / enabled / focus. `type`=\\\"password\\\" → secure.\\n \\\"keyboardType\\\", \\\"inputMode\\\", \\\"type\\\" -> (view as? EditText)?.let {\\n val s = strOf(value) ?: \\\"text\\\"\\n if (s == \\\"password\\\") inputSpecOf(it).secure = true else inputSpecOf(it).keyboard = s\\n applyInputType(it)\\n }\\n \\\"multiline\\\" -> (view as? EditText)?.let { inputSpecOf(it).multiline = boolOf(value); applyInputType(it) }\\n \\\"secureTextEntry\\\" -> (view as? EditText)?.let { inputSpecOf(it).secure = boolOf(value); applyInputType(it) }\\n \\\"editable\\\" -> (view as? EditText)?.let { it.isEnabled = boolOf(value) }\\n \\\"disabled\\\" -> (view as? EditText)?.let { it.isEnabled = !boolOf(value) }\\n \\\"autoFocus\\\" -> (view as? EditText)?.let { if (boolOf(value)) it.requestFocus() }\\n // a11y/meta hints the DOM backend also emits — no-ops on this host.\\n else -> Unit\\n }\\n }\\n\\n override fun removeProp(view: View, name: String) {\\n when (name) {\\n \\\"accessibilityLabel\\\", \\\"aria-label\\\", \\\"label\\\" -> view.contentDescription = null\\n \\\"hidden\\\" -> view.visibility = View.VISIBLE\\n \\\"editable\\\", \\\"disabled\\\" -> (view as? EditText)?.isEnabled = true\\n \\\"secureTextEntry\\\" -> (view as? EditText)?.let { inputSpecOf(it).secure = false; applyInputType(it) }\\n \\\"multiline\\\" -> (view as? EditText)?.let { inputSpecOf(it).multiline = false; applyInputType(it) }\\n \\\"tintColor\\\" -> (view as? ImageView)?.clearColorFilter()\\n else -> Unit\\n }\\n }\\n\\n // --- Tree structure ---\\n\\n override fun insert(parent: View, child: View, index: Int) {\\n val target = contentFor(parent) // route scrollview children into its content host\\n if (target is ViewGroup) {\\n applyLayoutParams(child) // carry the child width/height/grow/alignSelf into FlexboxLayout params\\n target.addView(child, index.coerceIn(0, target.childCount))\\n reapplyGaps(target)\\n return\\n }\\n // Leaf (e.g. a `text` element): compose text-node children into its text.\\n val parts = textParts.getOrPut(parent) { mutableListOf() }\\n parts.add(index.coerceIn(0, parts.size), child)\\n textOwner[child] = parent\\n recomposeText(parent)\\n }\\n\\n override fun remove(parent: View, child: View) {\\n val target = contentFor(parent)\\n if (target is ViewGroup) {\\n target.removeView(child)\\n reapplyGaps(target)\\n return\\n }\\n textOwner.remove(child)\\n textParts[parent]?.remove(child)\\n recomposeText(parent)\\n }\\n\\n override fun dispose(view: View) {\\n (view.parent as? ViewGroup)?.removeView(view)\\n layouts.remove(view)\\n imageGen.remove(view) // invalidate any in-flight remote image fetch for this view\\n // A scrollview owns an inner content host — drop both.\\n scrollContent.remove(view)?.let { layouts.remove(it) }\\n // TextInput state: detach every watcher + drop the input spec (no View leaks).\\n textWatchers.remove(view)?.let { byEvent ->\\n (view as? EditText)?.let { et -> byEvent.values.forEach { et.removeTextChangedListener(it) } }\\n }\\n inputSpecs.remove(view)\\n // Detach from any text composition it participated in (as child or as owner).\\n textOwner.remove(view)?.let { owner ->\\n textParts[owner]?.remove(view)\\n recomposeText(owner)\\n }\\n textParts.remove(view)\\n }\\n\\n // --- Events ---\\n\\n override fun addEvent(view: View, eventName: String, handlerId: String, fire: (value: String?) -> Unit) {\\n // Atlas's Pressable emits `onClick` → `click`; the older hand-written demo used\\n // `press`. Accept both; ignore pointer/keyboard/focus events this host doesn't model.\\n when (eventName) {\\n \\\"click\\\", \\\"press\\\" -> {\\n view.isClickable = true\\n view.setOnClickListener { fire(null) } // notify-only → no value\\n }\\n // Text change (Atlas onInput→\\\"input\\\", onChange→\\\"change\\\"). fire() carries the field's current\\n // text, which the JS host wraps as `{ target: { value } }` so onInput/onChange receive it.\\n \\\"input\\\", \\\"change\\\" -> (view as? EditText)?.let { et ->\\n val byEvent = textWatchers.getOrPut(et) { HashMap() }\\n byEvent.remove(eventName)?.let { et.removeTextChangedListener(it) } // replace only THIS event\\n val watcher = object : TextWatcher {\\n // Read the authoritative current field text (not `s`), coalescing null → \\\"\\\".\\n override fun afterTextChanged(s: Editable?) = fire(et.text?.toString() ?: \\\"\\\")\\n override fun beforeTextChanged(s: CharSequence?, st: Int, c: Int, a: Int) = Unit\\n override fun onTextChanged(s: CharSequence?, st: Int, b: Int, c: Int) = Unit\\n }\\n et.addTextChangedListener(watcher)\\n byEvent[eventName] = watcher\\n }\\n else -> Unit\\n }\\n }\\n\\n override fun removeEvent(view: View, eventName: String, handlerId: String) {\\n when (eventName) {\\n \\\"click\\\", \\\"press\\\" -> view.setOnClickListener(null)\\n \\\"input\\\", \\\"change\\\" -> (view as? EditText)?.let { et ->\\n textWatchers[et]?.let { byEvent ->\\n byEvent.remove(eventName)?.let { et.removeTextChangedListener(it) }\\n if (byEvent.isEmpty()) textWatchers.remove(et)\\n }\\n }\\n else -> Unit\\n }\\n }\\n\\n // --- Style application ---\\n\\n private fun applyStyle(view: View, style: Map<String, NativeProp>) {\\n // The flex container is `view` itself, OR a scrollview's inner content host. Sizing/visual\\n // props stay on `view` (the viewport for a scrollview); flex props target the container.\\n val container = contentFor(view) as? FlexboxLayout\\n val text = view as? TextView // Button/EditText are TextView subclasses\\n val selfLay = layoutOf(view) // this view's own size/grow/alignSelf as a child of its parent\\n val contentLay = container?.let { layoutOf(it) } ?: selfLay // the flex container's direction/gap\\n\\n // Flex container: direction, justify (incl. space-*), align, wrap.\\n (style[\\\"flexDirection\\\"] as? NativeProp.Str)?.value?.let { dir ->\\n contentLay.horizontal = dir.startsWith(\\\"row\\\")\\n container?.flexDirection = flexDirectionFor(dir)\\n }\\n (dimen(style[\\\"gap\\\"]) ?: dimen(style[\\\"rowGap\\\"]) ?: dimen(style[\\\"columnGap\\\"]))?.let {\\n contentLay.gapPx = it\\n reapplyGaps(container)\\n }\\n strOf(style[\\\"justifyContent\\\"])?.let { container?.justifyContent = justifyContentFor(it) }\\n strOf(style[\\\"alignItems\\\"])?.let { container?.alignItems = alignItemsFor(it) }\\n strOf(style[\\\"flexWrap\\\"])?.let { container?.flexWrap = flexWrapFor(it) }\\n // Per-child cross-axis override (applied via the child's FlexboxLayout.LayoutParams on insert).\\n strOf(style[\\\"alignSelf\\\"])?.let {\\n selfLay.alignSelf = alignSelfFor(it)\\n applyLayoutParams(view)\\n }\\n (numOf(style[\\\"flexGrow\\\"]) ?: numOf(style[\\\"flex\\\"]))?.let {\\n selfLay.grow = it.toFloat()\\n applyLayoutParams(view)\\n }\\n\\n // Box model.\\n sizeOf(style[\\\"width\\\"])?.let { selfLay.widthSpec = it; applyLayoutParams(view) }\\n sizeOf(style[\\\"height\\\"])?.let { selfLay.heightSpec = it; applyLayoutParams(view) }\\n dimen(style[\\\"minWidth\\\"])?.let { view.minimumWidth = it }\\n dimen(style[\\\"minHeight\\\"])?.let { view.minimumHeight = it }\\n applyPadding(view, style)\\n\\n // Visual.\\n applyBackground(view, style)\\n numOf(style[\\\"opacity\\\"])?.let { view.alpha = it.toFloat() }\\n dimen(style[\\\"elevation\\\"])?.let { view.elevation = it.toFloat() } // shadow (px)\\n\\n // Text.\\n if (text != null) applyText(text, style)\\n\\n // Spinner tint (ActivityIndicator → ProgressBar): `color` drives the indeterminate arc.\\n (view as? ProgressBar)?.let { bar ->\\n color(style[\\\"color\\\"])?.let { bar.indeterminateTintList = ColorStateList.valueOf(it) }\\n }\\n }\\n\\n private fun applyPadding(view: View, style: Map<String, NativeProp>) {\\n if (style.keys.none { it == \\\"padding\\\" || it.startsWith(\\\"padding\\\") }) return\\n val all = dimen(style[\\\"padding\\\"])\\n val l = dimen(style[\\\"paddingLeft\\\"]) ?: all ?: view.paddingLeft\\n val t = dimen(style[\\\"paddingTop\\\"]) ?: all ?: view.paddingTop\\n val r = dimen(style[\\\"paddingRight\\\"]) ?: all ?: view.paddingRight\\n val b = dimen(style[\\\"paddingBottom\\\"]) ?: all ?: view.paddingBottom\\n view.setPadding(l, t, r, b)\\n }\\n\\n private fun applyBackground(view: View, style: Map<String, NativeProp>) {\\n val bg = color(style[\\\"backgroundColor\\\"])\\n val radius = dimen(style[\\\"borderRadius\\\"])\\n val borderW = dimen(style[\\\"borderWidth\\\"])\\n val borderC = color(style[\\\"borderColor\\\"])\\n val tl = dimen(style[\\\"borderTopLeftRadius\\\"])\\n val tr = dimen(style[\\\"borderTopRightRadius\\\"])\\n val br = dimen(style[\\\"borderBottomRightRadius\\\"])\\n val bl = dimen(style[\\\"borderBottomLeftRadius\\\"])\\n val perCorner = tl != null || tr != null || br != null || bl != null\\n if (bg == null && radius == null && borderW == null && borderC == null && !perCorner) return\\n val drawable = (view.background as? GradientDrawable) ?: GradientDrawable()\\n bg?.let { drawable.setColor(it) }\\n if (perCorner) {\\n // Per-corner radii fall back to the uniform borderRadius (or 0). Order: TL, TR, BR, BL.\\n val t = (tl ?: radius ?: 0).toFloat()\\n val r = (tr ?: radius ?: 0).toFloat()\\n val b = (br ?: radius ?: 0).toFloat()\\n val l = (bl ?: radius ?: 0).toFloat()\\n drawable.cornerRadii = floatArrayOf(t, t, r, r, b, b, l, l)\\n } else {\\n radius?.let { drawable.cornerRadius = it.toFloat() }\\n }\\n if (borderW != null || borderC != null) {\\n drawable.setStroke(borderW ?: 0, borderC ?: Color.TRANSPARENT)\\n }\\n view.background = drawable\\n }\\n\\n private fun applyText(text: TextView, style: Map<String, NativeProp>) {\\n color(style[\\\"color\\\"])?.let { text.setTextColor(it) }\\n numOf(style[\\\"fontSize\\\"])?.let { text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, it.toFloat()) }\\n fontWeightBold(style[\\\"fontWeight\\\"])?.let { bold ->\\n // Two-arg setTypeface: uses a real bold face when available, else falls back to\\n // synthetic (fake) bold — so weight always takes visible effect.\\n text.setTypeface(text.typeface, if (bold) Typeface.BOLD else Typeface.NORMAL)\\n }\\n (style[\\\"textAlign\\\"] as? NativeProp.Str)?.value?.let { text.gravity = textGravity(it) }\\n // numberOfLines → clamp lines + ellipsize the tail (RN's `numberOfLines`).\\n numOf(style[\\\"numberOfLines\\\"])?.let { n ->\\n val lines = n.toInt()\\n if (lines > 0) {\\n text.maxLines = lines\\n text.ellipsize = TextUtils.TruncateAt.END\\n }\\n }\\n }\\n\\n // --- Image + TextInput ---\\n\\n /**\\n * Load an image `src`/`source` into an ImageView. `data:`/base64 + bundled assets decode\\n * synchronously (deterministic, no I/O on the network); remote http(s) is a deliberate follow-up\\n * (it needs an off-main fetch executor + a renderer lifecycle hook to reclaim it). Any\\n * unresolvable/garbage source is ignored — never crashes the host.\\n */\\n private fun applyImageSource(iv: ImageView, value: NativeProp) {\\n val uri = strOf(value)\\n ?: (value as? NativeProp.Obj)?.value?.get(\\\"uri\\\")?.let { strOf(it) }\\n ?: return\\n try {\\n val bitmap = when {\\n uri.startsWith(\\\"data:\\\") -> {\\n val payload = uri.substringAfter(\\\"base64,\\\", \\\"\\\")\\n if (payload.isEmpty()) {\\n null\\n } else {\\n val bytes = Base64.decode(payload, Base64.DEFAULT)\\n BitmapFactory.decodeByteArray(bytes, 0, bytes.size)\\n }\\n }\\n // Remote: fetch off-main (via imageLoader, default HTTP), decode, set on the main thread —\\n // guarded by a generation token so a stale/slow fetch never clobbers a newer src/disposed view.\\n uri.startsWith(\\\"http://\\\") || uri.startsWith(\\\"https://\\\") -> {\\n val token = (imageGen[iv] ?: 0L) + 1\\n imageGen[iv] = token\\n (imageLoader ?: ::defaultImageLoad)(uri) { bytes ->\\n val bmp = bytes?.let {\\n runCatching { BitmapFactory.decodeByteArray(it, 0, it.size) }.getOrNull()\\n }\\n if (bmp != null) iv.post { if (imageGen[iv] == token) iv.setImageBitmap(bmp) }\\n }\\n null // async — nothing to set synchronously\\n }\\n else -> {\\n val name = uri.removePrefix(\\\"asset:///\\\").removePrefix(\\\"file:///android_asset/\\\")\\n context.assets.open(name).use { BitmapFactory.decodeStream(it) }\\n }\\n }\\n bitmap?.let { iv.setImageBitmap(it) }\\n } catch (_: Throwable) {\\n // ignore: an unresolvable/garbage src must never crash the host\\n }\\n }\\n\\n /** Default remote loader: fetch the URL off the main thread via HttpURLConnection. */\\n private fun defaultImageLoad(url: String, onResult: (ByteArray?) -> Unit) {\\n imageExecutor.execute {\\n val bytes = runCatching {\\n val conn = (URL(url).openConnection() as HttpURLConnection).apply {\\n connectTimeout = 10_000\\n readTimeout = 10_000\\n doInput = true\\n }\\n try {\\n conn.inputStream.use { it.readBytes() }\\n } finally {\\n conn.disconnect()\\n }\\n }.getOrNull()\\n onResult(bytes)\\n }\\n }\\n\\n private fun scaleTypeFor(mode: String): ImageView.ScaleType = when (mode) {\\n \\\"cover\\\" -> ImageView.ScaleType.CENTER_CROP\\n \\\"stretch\\\" -> ImageView.ScaleType.FIT_XY\\n \\\"center\\\" -> ImageView.ScaleType.CENTER\\n else -> ImageView.ScaleType.FIT_CENTER // \\\"contain\\\" / default\\n }\\n\\n /** Recompute an EditText's inputType from its {keyboard, secure, multiline} spec (order-independent). */\\n private fun applyInputType(et: EditText) {\\n val spec = inputSpecs[et] ?: return\\n var type = when (spec.keyboard) {\\n \\\"number\\\", \\\"numeric\\\", \\\"number-pad\\\" -> InputType.TYPE_CLASS_NUMBER\\n \\\"decimal\\\", \\\"decimal-pad\\\" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL\\n \\\"phone\\\", \\\"tel\\\" -> InputType.TYPE_CLASS_PHONE\\n \\\"email\\\", \\\"email-address\\\" ->\\n InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS\\n \\\"url\\\" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI\\n else -> InputType.TYPE_CLASS_TEXT\\n }\\n if (spec.multiline) {\\n et.isSingleLine = false // set before inputType (singleLine mutates inputType)\\n et.gravity = Gravity.TOP or Gravity.START\\n type = type or InputType.TYPE_TEXT_FLAG_MULTI_LINE\\n }\\n if (spec.secure) {\\n // Clear any existing variation (email/url/phone) and apply ONLY the password variation —\\n // OR-ing onto a non-default variation yields a combined value Android won't mask.\\n val isNumber = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER\\n type = type and InputType.TYPE_MASK_VARIATION.inv()\\n type = type or\\n if (isNumber) InputType.TYPE_NUMBER_VARIATION_PASSWORD\\n else InputType.TYPE_TEXT_VARIATION_PASSWORD\\n }\\n et.inputType = type\\n }\\n\\n // --- LayoutParams + gaps ---\\n\\n private fun applyLayoutParams(view: View) {\\n val lay = layoutOf(view)\\n val lp = (view.layoutParams as? FlexboxLayout.LayoutParams)\\n ?: FlexboxLayout.LayoutParams(lay.widthSpec, lay.heightSpec)\\n lp.width = lay.widthSpec\\n lp.height = lay.heightSpec\\n lp.flexGrow = lay.grow\\n lp.alignSelf = lay.alignSelf\\n view.layoutParams = lp\\n }\\n\\n /** Recreate gaps as leading margins on every child (all but the first along the main axis). */\\n private fun reapplyGaps(parent: View?) {\\n val container = parent as? FlexboxLayout ?: return\\n val lay = layouts[container] ?: return\\n for (i in 0 until container.childCount) {\\n val child = container.getChildAt(i)\\n val lp = child.layoutParams as? FlexboxLayout.LayoutParams ?: continue\\n val lead = if (i == 0) 0 else lay.gapPx\\n if (lay.horizontal) {\\n lp.leftMargin = lead\\n lp.topMargin = 0\\n } else {\\n lp.topMargin = lead\\n lp.leftMargin = 0\\n }\\n child.layoutParams = lp\\n }\\n }\\n\\n // --- Flex enum mappings (Atlas/CSS strings → FlexboxLayout constants) ---\\n\\n private fun flexDirectionFor(dir: String): Int = when (dir) {\\n \\\"row\\\" -> FlexDirection.ROW\\n \\\"row-reverse\\\" -> FlexDirection.ROW_REVERSE\\n \\\"column-reverse\\\" -> FlexDirection.COLUMN_REVERSE\\n else -> FlexDirection.COLUMN\\n }\\n\\n private fun justifyContentFor(justify: String): Int = when (justify) {\\n \\\"flex-end\\\", \\\"end\\\" -> JustifyContent.FLEX_END\\n \\\"center\\\" -> JustifyContent.CENTER\\n \\\"space-between\\\" -> JustifyContent.SPACE_BETWEEN\\n \\\"space-around\\\" -> JustifyContent.SPACE_AROUND\\n \\\"space-evenly\\\" -> JustifyContent.SPACE_EVENLY\\n else -> JustifyContent.FLEX_START\\n }\\n\\n private fun alignItemsFor(align: String): Int = when (align) {\\n \\\"flex-end\\\", \\\"end\\\" -> AlignItems.FLEX_END\\n \\\"center\\\" -> AlignItems.CENTER\\n \\\"baseline\\\" -> AlignItems.BASELINE\\n \\\"flex-start\\\", \\\"start\\\" -> AlignItems.FLEX_START\\n else -> AlignItems.STRETCH\\n }\\n\\n private fun flexWrapFor(wrap: String): Int = when (wrap) {\\n \\\"wrap\\\" -> FlexWrap.WRAP\\n \\\"wrap-reverse\\\" -> FlexWrap.WRAP_REVERSE\\n else -> FlexWrap.NOWRAP\\n }\\n\\n private fun alignSelfFor(align: String): Int = when (align) {\\n \\\"flex-end\\\", \\\"end\\\" -> AlignSelf.FLEX_END\\n \\\"center\\\" -> AlignSelf.CENTER\\n \\\"baseline\\\" -> AlignSelf.BASELINE\\n \\\"stretch\\\" -> AlignSelf.STRETCH\\n \\\"flex-start\\\", \\\"start\\\" -> AlignSelf.FLEX_START\\n else -> AlignSelf.AUTO\\n }\\n\\n // --- Value coercion (defensive: unknown/ill-typed values are ignored, never crash) ---\\n\\n private fun numOf(prop: NativeProp?): Double? = (prop as? NativeProp.Num)?.value\\n private fun strOf(prop: NativeProp?): String? = (prop as? NativeProp.Str)?.value\\n private fun boolOf(prop: NativeProp?): Boolean = (prop as? NativeProp.Bool)?.value == true\\n\\n /** A numeric dp dimension, or null for strings/absent (percent sizes go via [sizeOf]). */\\n private fun dimen(prop: NativeProp?): Int? = (prop as? NativeProp.Num)?.let { dp(it.value) }\\n\\n /** A width/height spec: number → dp, `\\\"100%\\\"` → MATCH_PARENT, `\\\"auto\\\"` → WRAP_CONTENT. */\\n private fun sizeOf(prop: NativeProp?): Int? = when (prop) {\\n is NativeProp.Num -> dp(prop.value)\\n is NativeProp.Str -> when (prop.value) {\\n \\\"100%\\\" -> ViewGroup.LayoutParams.MATCH_PARENT\\n \\\"auto\\\" -> ViewGroup.LayoutParams.WRAP_CONTENT\\n else -> null\\n }\\n else -> null\\n }\\n\\n private fun color(prop: NativeProp?): Int? {\\n val raw = (prop as? NativeProp.Str)?.value ?: return null\\n return try {\\n Color.parseColor(raw)\\n } catch (_: IllegalArgumentException) {\\n null\\n }\\n }\\n\\n /** true → bold, false → normal, null → leave unchanged. `>= 600` (or `\\\"bold\\\"`) is bold. */\\n private fun fontWeightBold(prop: NativeProp?): Boolean? = when (prop) {\\n is NativeProp.Num -> prop.value >= 600\\n is NativeProp.Str -> prop.value == \\\"bold\\\" || (prop.value.toIntOrNull() ?: 0) >= 600\\n else -> null\\n }\\n\\n private fun textGravity(align: String): Int = when (align) {\\n \\\"center\\\" -> Gravity.CENTER\\n \\\"right\\\" -> Gravity.END\\n \\\"justify\\\" -> Gravity.START // no native justify on older APIs; left-align\\n else -> Gravity.START\\n }\\n\\n /** main axis ← justifyContent, cross axis ← alignItems. `space-*` falls back to center. */\\n}\\n\",\n \"mindees-host/src/main/kotlin/dev/mindees/host/MindeesNativeHost.kt\": \"/*\\n * MindeesNativeHost.kt — applies a NativeCommand stream to a pluggable renderer,\\n * with strict validation mirroring @mindees/renderer's reference host.\\n *\\n * The host owns identity + structure bookkeeping (and validates the stream); a\\n * [HostRenderer] builds/mutates the actual views. Use [AndroidViewRenderer] on a\\n * device, or [ModelRenderer] in JVM unit tests (`./gradlew test`, no device needed).\\n *\\n * CI-verified by the native Android workflow; see the module README.\\n */\\n\\npackage dev.mindees.host\\n\\n/** Thrown when a command stream violates the host contract. */\\nclass NativeHostException(message: String) : RuntimeException(message)\\n\\n/**\\n * A pluggable platform renderer. The host calls these to materialize the UI.\\n * [V] is the platform node type (`android.view.View`, or `ModelNode` in tests).\\n */\\ninterface HostRenderer<V> {\\n fun makeElement(tag: String): V\\n fun makeText(text: String): V\\n fun setText(view: V, text: String)\\n fun setProp(view: V, name: String, value: NativeProp)\\n fun removeProp(view: V, name: String)\\n fun insert(parent: V, child: V, index: Int)\\n fun remove(parent: V, child: V)\\n /**\\n * Wire a native event; the renderer calls [fire] when it occurs, passing an optional value\\n * (the text for an `input`/`change` event; `null` for notify-only events like press/click).\\n */\\n fun addEvent(view: V, eventName: String, handlerId: String, fire: (value: String?) -> Unit)\\n fun removeEvent(view: V, eventName: String, handlerId: String)\\n /** Free a node's resources (already detached from its parent). */\\n fun dispose(view: V)\\n}\\n\\n/**\\n * Applies a [NativeCommand] stream to a [HostRenderer], strictly validating it\\n * (throws [NativeHostException] on any malformed/leaking sequence) — the contract\\n * `@mindees/renderer`'s `createReferenceHost()` enforces and tests.\\n *\\n * @param onEvent invoked with a `handlerId` (and an optional text `value` for input/change events,\\n * `null` for notify-only events) when a wired native event fires; forward it to the JS runtime's\\n * `MindeesApp.dispatchEvent(handlerId, value)`.\\n */\\nclass MindeesNativeHost<V>(\\n private val rootId: String,\\n root: V,\\n private val renderer: HostRenderer<V>,\\n private val onEvent: (handlerId: String, value: String?) -> Unit,\\n) {\\n private val views = HashMap<String, V>()\\n private val parentOf = HashMap<String, String>()\\n private val childrenOf = HashMap<String, MutableList<String>>()\\n\\n init {\\n views[rootId] = root\\n childrenOf[rootId] = mutableListOf()\\n }\\n\\n /** Live (created, not yet disposed) node count, excluding the root. */\\n val liveNodeCount: Int get() = views.size - 1\\n\\n fun apply(batch: List<NativeCommand>) {\\n for (command in batch) apply(command)\\n }\\n\\n fun apply(command: NativeCommand) {\\n when (command) {\\n is NativeCommand.CreateNode -> {\\n requireAbsent(command.id)\\n views[command.id] = renderer.makeElement(command.tag)\\n childrenOf[command.id] = mutableListOf()\\n }\\n is NativeCommand.CreateText -> {\\n requireAbsent(command.id)\\n views[command.id] = renderer.makeText(command.text)\\n childrenOf[command.id] = mutableListOf()\\n }\\n is NativeCommand.UpdateText -> renderer.setText(view(command.id), command.text)\\n is NativeCommand.SetProp -> renderer.setProp(view(command.id), command.name, command.value)\\n is NativeCommand.RemoveProp -> renderer.removeProp(view(command.id), command.name)\\n is NativeCommand.InsertChild -> {\\n if (parentOf.containsKey(command.childId)) {\\n throw NativeHostException(\\\"insertChild: ${command.childId} already has a parent\\\")\\n }\\n val parent = view(command.parentId)\\n val child = view(command.childId)\\n val kids = childrenOf.getOrPut(command.parentId) { mutableListOf() }\\n if (command.index < 0 || command.index > kids.size) {\\n throw NativeHostException(\\\"insertChild: index ${command.index} out of range\\\")\\n }\\n kids.add(command.index, command.childId)\\n parentOf[command.childId] = command.parentId\\n renderer.insert(parent, child, command.index)\\n }\\n is NativeCommand.RemoveChild -> {\\n val kids = childrenOf[command.parentId]\\n if (parentOf[command.childId] != command.parentId || kids == null ||\\n !kids.remove(command.childId)\\n ) {\\n throw NativeHostException(\\\"removeChild: ${command.childId} is not a child of ${command.parentId}\\\")\\n }\\n parentOf.remove(command.childId)\\n renderer.remove(view(command.parentId), view(command.childId))\\n }\\n is NativeCommand.DisposeNode -> {\\n if (command.id == rootId) throw NativeHostException(\\\"cannot dispose the root node\\\")\\n val v = views[command.id] ?: throw NativeHostException(\\\"double dispose of ${command.id}\\\")\\n // Interior subtree nodes are freed without an explicit removeChild, so\\n // detach from BOTH the bookkeeping and the renderer tree here. (A renderer\\n // whose dispose() is a no-op — e.g. ModelRenderer — would otherwise leave\\n // the node in its parent's children even though we consider it detached.)\\n parentOf.remove(command.id)?.let { pid ->\\n childrenOf[pid]?.remove(command.id)\\n views[pid]?.let { parentView -> renderer.remove(parentView, v) }\\n }\\n renderer.dispose(v)\\n views.remove(command.id)\\n childrenOf.remove(command.id)\\n }\\n is NativeCommand.RegisterEvent -> {\\n val target = view(command.id)\\n val handlerId = command.handlerId\\n renderer.addEvent(target, command.eventName, handlerId) { value -> onEvent(handlerId, value) }\\n }\\n is NativeCommand.UnregisterEvent ->\\n renderer.removeEvent(view(command.id), command.eventName, command.handlerId)\\n }\\n }\\n\\n private fun view(id: String): V = views[id] ?: throw NativeHostException(\\\"unknown node $id\\\")\\n\\n private fun requireAbsent(id: String) {\\n if (views.containsKey(id)) throw NativeHostException(\\\"duplicate node id $id\\\")\\n }\\n}\\n\\n// --- In-memory renderer for JVM unit tests (no Android) ---\\n\\n/** An in-memory model node the [ModelRenderer] builds. */\\nclass ModelNode(val kind: String, var tag: String, var text: String) {\\n val props = HashMap<String, NativeProp>()\\n val events = HashMap<String, String>() // eventName -> handlerId\\n val children = ArrayList<ModelNode>()\\n\\n /** A compact structural string (tags + text) for assertions. */\\n fun serialize(): String =\\n if (kind == \\\"text\\\") text\\n else \\\"<$tag>\\\" + children.joinToString(\\\"\\\") { it.serialize() } + \\\"</$tag>\\\"\\n}\\n\\n/** A [HostRenderer] that builds a [ModelNode] tree — used by unit tests. */\\nclass ModelRenderer : HostRenderer<ModelNode> {\\n override fun makeElement(tag: String) = ModelNode(\\\"element\\\", tag, \\\"\\\")\\n override fun makeText(text: String) = ModelNode(\\\"text\\\", \\\"\\\", text)\\n override fun setText(view: ModelNode, text: String) { view.text = text }\\n override fun setProp(view: ModelNode, name: String, value: NativeProp) { view.props[name] = value }\\n override fun removeProp(view: ModelNode, name: String) { view.props.remove(name) }\\n override fun insert(parent: ModelNode, child: ModelNode, index: Int) { parent.children.add(index, child) }\\n override fun remove(parent: ModelNode, child: ModelNode) { parent.children.remove(child) }\\n override fun addEvent(view: ModelNode, eventName: String, handlerId: String, fire: (value: String?) -> Unit) {\\n view.events[eventName] = handlerId\\n }\\n override fun removeEvent(view: ModelNode, eventName: String, handlerId: String) {\\n view.events.remove(eventName)\\n }\\n override fun dispose(view: ModelNode) { view.children.clear() }\\n}\\n\",\n \"mindees-host/src/main/kotlin/dev/mindees/host/NativeCommand.kt\": \"/*\\n * NativeCommand.kt — the wire model for the MindeesNative native command protocol.\\n *\\n * Mirrors `@mindees/renderer`'s `native-protocol.ts`. The sealed types are pure\\n * Kotlin (unit-testable on the JVM); `NativeCommandCodec` decodes JSON via\\n * org.json (Android) and is exercised on-device.\\n *\\n * CI-verified by the native Android workflow; see the module README.\\n */\\n\\npackage dev.mindees.host\\n\\nimport org.json.JSONArray\\nimport org.json.JSONObject\\n\\n/** A serializable prop value (mirrors `NativePropValue`). Carries no functions. */\\nsealed interface NativeProp {\\n data class Str(val value: String) : NativeProp\\n data class Num(val value: Double) : NativeProp\\n data class Bool(val value: Boolean) : NativeProp\\n data object Null : NativeProp\\n data class Arr(val value: List<NativeProp>) : NativeProp\\n data class Obj(val value: Map<String, NativeProp>) : NativeProp\\n}\\n\\n/** One native command (mirrors the `NativeCommand` union). */\\nsealed interface NativeCommand {\\n data class CreateNode(val id: String, val tag: String) : NativeCommand\\n data class CreateText(val id: String, val text: String) : NativeCommand\\n data class SetProp(val id: String, val name: String, val value: NativeProp) : NativeCommand\\n data class RemoveProp(val id: String, val name: String) : NativeCommand\\n data class InsertChild(val parentId: String, val childId: String, val index: Int) : NativeCommand\\n data class RemoveChild(val parentId: String, val childId: String) : NativeCommand\\n data class UpdateText(val id: String, val text: String) : NativeCommand\\n data class DisposeNode(val id: String) : NativeCommand\\n data class RegisterEvent(val id: String, val eventName: String, val handlerId: String) : NativeCommand\\n data class UnregisterEvent(val id: String, val eventName: String, val handlerId: String) : NativeCommand\\n}\\n\\n/** Decodes a JSON command stream into [NativeCommand]s (Android: uses org.json). */\\nobject NativeCommandCodec {\\n fun decodeBatch(json: String): List<NativeCommand> {\\n val array = JSONArray(json)\\n return (0 until array.length()).map { decode(array.getJSONObject(it)) }\\n }\\n\\n fun decode(o: JSONObject): NativeCommand {\\n // Node ids are string|number on the wire; normalize to String.\\n fun id(key: String): String = o.get(key).toString()\\n return when (val type = o.getString(\\\"type\\\")) {\\n \\\"createNode\\\" -> NativeCommand.CreateNode(id(\\\"id\\\"), o.getString(\\\"tag\\\"))\\n \\\"createText\\\" -> NativeCommand.CreateText(id(\\\"id\\\"), o.getString(\\\"text\\\"))\\n \\\"setProp\\\" -> NativeCommand.SetProp(id(\\\"id\\\"), o.getString(\\\"name\\\"), decodeProp(o.get(\\\"value\\\")))\\n \\\"removeProp\\\" -> NativeCommand.RemoveProp(id(\\\"id\\\"), o.getString(\\\"name\\\"))\\n \\\"insertChild\\\" -> NativeCommand.InsertChild(id(\\\"parentId\\\"), id(\\\"childId\\\"), o.getInt(\\\"index\\\"))\\n \\\"removeChild\\\" -> NativeCommand.RemoveChild(id(\\\"parentId\\\"), id(\\\"childId\\\"))\\n \\\"updateText\\\" -> NativeCommand.UpdateText(id(\\\"id\\\"), o.getString(\\\"text\\\"))\\n \\\"disposeNode\\\" -> NativeCommand.DisposeNode(id(\\\"id\\\"))\\n \\\"registerEvent\\\" ->\\n NativeCommand.RegisterEvent(id(\\\"id\\\"), o.getString(\\\"eventName\\\"), o.getString(\\\"handlerId\\\"))\\n \\\"unregisterEvent\\\" ->\\n NativeCommand.UnregisterEvent(id(\\\"id\\\"), o.getString(\\\"eventName\\\"), o.getString(\\\"handlerId\\\"))\\n else -> throw IllegalArgumentException(\\\"unknown command type $type\\\")\\n }\\n }\\n\\n private fun decodeProp(value: Any?): NativeProp = when (value) {\\n null, JSONObject.NULL -> NativeProp.Null\\n is Boolean -> NativeProp.Bool(value)\\n is Int -> NativeProp.Num(value.toDouble())\\n is Long -> NativeProp.Num(value.toDouble())\\n is Double -> NativeProp.Num(value)\\n is String -> NativeProp.Str(value)\\n is JSONArray -> NativeProp.Arr((0 until value.length()).map { decodeProp(value.get(it)) })\\n is JSONObject -> NativeProp.Obj(value.keys().asSequence().associateWith { decodeProp(value.get(it)) })\\n else -> NativeProp.Str(value.toString())\\n }\\n}\\n\",\n \"settings.gradle.kts\": \"// CI-verified through .github/workflows/native-android.yml. Open in Android\\n// Studio (it provides Gradle + the wrapper) or run with a local Gradle. Align\\n// the plugin/SDK versions below with your installed toolchain if needed.\\n\\npluginManagement {\\n repositories {\\n google()\\n mavenCentral()\\n gradlePluginPortal()\\n }\\n plugins {\\n // Latest stable as of June 2026. AGP 9 has BUILT-IN Kotlin support, so the\\n // org.jetbrains.kotlin.android plugin is no longer applied (AGP errors if it\\n // is). Align AGP with your toolchain if Gradle complains (AGP 9.x needs\\n // JDK 17+ and a recent Gradle).\\n id(\\\"com.android.application\\\") version \\\"9.2.0\\\"\\n id(\\\"com.android.library\\\") version \\\"9.2.0\\\"\\n }\\n}\\n\\ndependencyResolutionManagement {\\n repositories {\\n google()\\n mavenCentral()\\n }\\n}\\n\\nrootProject.name = \\\"{{appName}}\\\"\\ninclude(\\\":mindees-host\\\")\\ninclude(\\\":mindees-example-app\\\")\\n\",\n}\n\n/** The @mindees/* version the scaffolded app-js pins to (the line that generated it). */\nexport const ANDROID_TEMPLATE_VERSION = \"0.21.0\"\n"],"mappings":";;AAKA,MAAa,yBAAiD;CAC5D,cAAc;CACd,aAAa;CACb,oBAAoB;CACpB,qBAAqB;CACrB,2CAA2C;CAC3C,qDAAqD;CACrD,0CAA0C;CAC1C,gDAAgD;CAChD,gDAAgD;CAChD,2CAA2C;CAC3C,2CAA2C;CAC3C,4CAA4C;CAC5C,+CAA+C;CAC/C,wCAAwC;CACxC,oDAAoD;CACpD,0EAA0E;CAC1E,2EAA2E;CAC3E,mFAAmF;CACnF,iCAAiC;CACjC,6CAA6C;CAC7C,wEAAwE;CACxE,sEAAsE;CACtE,kEAAkE;CAClE,uBAAuB;AACzB"}
|
package/dist/cli.js
CHANGED
|
@@ -147,7 +147,8 @@ function cmdCreate(args, ctx) {
|
|
|
147
147
|
}
|
|
148
148
|
printBanner(ctx);
|
|
149
149
|
out(ctx.write, `Created "${target.packageName}" from the ${result.template} template (${result.written.length} files).`);
|
|
150
|
-
|
|
150
|
+
const dir = quoteShellPath(target.displayDir);
|
|
151
|
+
out(ctx.write, result.template === "android" ? `Next: cd ${dir} — build the JS bundle (cd mindees-example-app/app-js && npm install && npm run build), then the APK (gradle wrapper --gradle-version 9.4.1 && ./gradlew :mindees-example-app:assembleDebug). See README.md.` : `Next: cd ${dir} && pnpm install && mindees dev`);
|
|
151
152
|
return { exitCode: 0 };
|
|
152
153
|
}
|
|
153
154
|
function cmdBuild(args, ctx) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["/**\n * Forge CLI dispatch — `mindees <command> [args]`.\n *\n * `runCli` is a pure function of (argv, context) → exit code, writing structured\n * output through an injected {@link Writer}. All side-effecting capabilities\n * (filesystem, env probe) are injected via {@link CliContext}, so the entire CLI\n * is deterministically testable; the thin `bin` entrypoint wires real adapters.\n *\n * @module\n */\n\nimport { parseArgs } from 'node:util'\nimport type { AiBackend } from '@mindees/ai'\nimport { runAiCommand } from './ai'\nimport { buildProject } from './build'\nimport { quoteShellPath, resolveCreateTarget } from './create-target'\nimport { doctorSummary, renderDoctor, runDoctor } from './doctor'\nimport type { FileSystem } from './fs'\nimport { naturalLanguageToTemplate } from './nl'\nimport { scaffold } from './scaffold'\nimport { DEFAULT_TEMPLATE, templateNames } from './templates'\nimport type { CommandResult, EnvProbe, Writer } from './types'\n\n/** Everything the CLI needs from the outside world (injected for testability). */\nexport interface CliContext {\n fs: FileSystem\n env: EnvProbe\n /** Working directory (where `create` writes, what `build` reads). */\n cwd: string\n /** CLI version string (from package metadata). */\n version: string\n /** Output sink. */\n write: Writer\n /** AI backend for `ai` commands (wired from `MINDEES_AI_*` env in `bin`). */\n aiBackend?: AiBackend\n /** Pre-rendered welcome banner (the MindeesNative logo). Shown on `help` + `create` success; injected by `bin` for TTYs. */\n banner?: string\n}\n\nconst HELP = `mindees — the MindeesNative CLI (Forge)\n\nUsage: mindees <command> [options]\n\nCommands:\n create <name> Scaffold a new app (--template <name>, --force)\n build Type-check + compile the project (--out-dir <dir>)\n dev Build and rebuild on change (developer preview)\n doctor Diagnose your environment\n info Show CLI + environment info\n ai explain <err> Explain an error with AI (needs MINDEES_AI_* env)\n help Show this help\n\nRun \\`mindees create --help\\` style flags inline. Templates: ${templateNames().join(', ')}.`\n\nconst CREATE_HELP = `Usage: mindees create <name-or-path> [options]\n\nOptions:\n -t, --template <name> Template to scaffold (${templateNames().join(', ')})\n -p, --prompt <text> Pick a template from a short prompt\n --force Overwrite a non-empty target directory\n -h, --help Show this help`\n\nfunction out(write: Writer, text: string): void {\n write({ stream: 'out', text })\n}\n/** Print the welcome banner (the logo), if one was injected (TTY sessions). */\nfunction printBanner(ctx: CliContext): void {\n if (ctx.banner) out(ctx.write, ctx.banner)\n}\nfunction err(write: Writer, text: string): void {\n write({ stream: 'err', text })\n}\n\n/**\n * Run the CLI. Returns a {@link CommandResult} with the process exit code.\n * Never throws for expected failures — it reports them and returns non-zero.\n */\nexport function runCli(argv: readonly string[], ctx: CliContext): CommandResult {\n const [command, ...rest] = argv\n\n if (!command || command === 'help' || command === '--help' || command === '-h') {\n printBanner(ctx)\n out(ctx.write, HELP)\n return { exitCode: 0 }\n }\n\n if (command === '--version' || command === '-v' || command === 'version') {\n out(ctx.write, ctx.version)\n return { exitCode: 0 }\n }\n\n switch (command) {\n case 'create':\n return cmdCreate(rest, ctx)\n case 'build':\n return cmdBuild(rest, ctx)\n case 'dev':\n return cmdDev(ctx)\n case 'doctor':\n return cmdDoctor(ctx)\n case 'info':\n return cmdInfo(ctx)\n default:\n err(ctx.write, `Unknown command \"${command}\". Run \\`mindees help\\`.`)\n return { exitCode: 1 }\n }\n}\n\n/**\n * The async CLI entry. Handles the model-calling `ai` command (which is asynchronous) and\n * delegates every synchronous command to {@link runCli}. The `bin` calls this; tests can call\n * either (sync commands stay testable through `runCli`).\n */\nexport function runCliAsync(argv: readonly string[], ctx: CliContext): Promise<CommandResult> {\n const [command, ...rest] = argv\n if (command === 'ai') {\n return runAiCommand(rest, {\n write: ctx.write,\n ...(ctx.aiBackend ? { backend: ctx.aiBackend } : {}),\n })\n }\n return Promise.resolve(runCli(argv, ctx))\n}\n\nfunction cmdCreate(args: readonly string[], ctx: CliContext): CommandResult {\n const { values, positionals } = parseArgs({\n args: [...args],\n allowPositionals: true,\n strict: false,\n options: {\n help: { type: 'boolean', short: 'h' },\n template: { type: 'string', short: 't' },\n force: { type: 'boolean' },\n prompt: { type: 'string', short: 'p' },\n },\n })\n\n if (values.help === true || positionals[0] === 'help') {\n out(ctx.write, CREATE_HELP)\n return { exitCode: 0 }\n }\n\n const name = positionals[0]\n if (!name) {\n err(ctx.write, 'create: missing app name or target path. Usage: mindees create <name-or-path>')\n return { exitCode: 1 }\n }\n\n // NL → template: `--prompt \"a counter app\"` picks a template deterministically\n // (offline). Real AI generation arrives with Synapse in Phase 10; until then\n // this is an honest keyword-based mapping that never blocks `create`. An\n // explicit `--template` always wins; the prompt only resolves a template when\n // the caller didn't choose one (mirrors `create-mindees`'s runCreate so both\n // entrypoints agree on precedence).\n // Treat a present-but-empty `--template \"\"` as \"not chosen\" (defer to prompt/default),\n // matching create-mindees's runCreate so both entrypoints agree on precedence.\n const explicitTemplate =\n typeof values.template === 'string' && values.template.length > 0 ? values.template : undefined\n let template = explicitTemplate ?? DEFAULT_TEMPLATE\n if (\n explicitTemplate === undefined &&\n typeof values.prompt === 'string' &&\n values.prompt.length > 0\n ) {\n const picked = naturalLanguageToTemplate(values.prompt)\n template = picked.template\n out(ctx.write, `Interpreted prompt → \"${picked.template}\" template (${picked.reason}).`)\n }\n\n const target = resolveCreateTarget(name, ctx.cwd)\n if (!target.ok) {\n err(ctx.write, target.error)\n return { exitCode: 1 }\n }\n\n const result = scaffold(ctx.fs, {\n appName: target.packageName,\n targetDir: target.targetDir,\n template,\n force: values.force === true,\n })\n\n if (!result.ok) {\n err(ctx.write, result.error ?? 'create failed')\n return { exitCode: 1 }\n }\n\n printBanner(ctx)\n out(\n ctx.write,\n `Created \"${target.packageName}\" from the ${result.template} template (${result.written.length} files).`,\n )\n out(ctx.write, `Next: cd ${quoteShellPath(target.displayDir)} && pnpm install && mindees dev`)\n return { exitCode: 0 }\n}\n\nfunction cmdBuild(args: readonly string[], ctx: CliContext): CommandResult {\n const { values } = parseArgs({\n args: [...args],\n allowPositionals: true,\n strict: false,\n options: { 'out-dir': { type: 'string' }, 'no-source-map': { type: 'boolean' } },\n })\n\n const result = buildProject(ctx.fs, {\n root: ctx.cwd,\n outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : `${ctx.cwd}/dist`,\n sourceMap: values['no-source-map'] !== true,\n })\n\n for (const d of result.diagnostics) {\n const where = d.file ? `${d.file}${d.position ? `:${d.position.line}` : ''}: ` : ''\n err(ctx.write, `${d.severity} ${d.code} ${where}${d.message}`)\n }\n\n if (!result.ok) {\n err(ctx.write, 'Build failed: fix the type errors above.')\n return { exitCode: 1 }\n }\n out(\n ctx.write,\n `Built ${result.compiled.length} module(s); flattened ${result.stats.flattenedNodes}/${result.stats.totalElements} elements.`,\n )\n return { exitCode: 0 }\n}\n\nfunction cmdDev(ctx: CliContext): CommandResult {\n // The dev server transport (HTTP + HMR socket) is a developer-preview layer in\n // `bin`. The CLI command here reports how to use it; the tested rebuild\n // orchestrator lives in `dev.ts` (`startDev`).\n out(ctx.write, 'mindees dev — build + watch + live-reload preview.')\n out(\n ctx.write,\n 'Run via the `mindees` binary (it starts the HTTP server); set MINDEES_DEV_PORT to',\n )\n out(\n ctx.write,\n 'override the port (default 3000). The browser reloads automatically on each rebuild.',\n )\n return { exitCode: 0 }\n}\n\nfunction cmdDoctor(ctx: CliContext): CommandResult {\n const checks = runDoctor(ctx.env)\n for (const line of renderDoctor(checks)) out(ctx.write, line)\n const summary = doctorSummary(checks)\n return { exitCode: summary === 'fail' ? 1 : 0 }\n}\n\nfunction cmdInfo(ctx: CliContext): CommandResult {\n out(ctx.write, `mindees CLI ${ctx.version}`)\n out(ctx.write, `Node ${ctx.env.nodeVersion}`)\n out(\n ctx.write,\n `Package manager: ${ctx.env.packageManager ? `${ctx.env.packageManager.name} ${ctx.env.packageManager.version}` : 'none'}`,\n )\n out(ctx.write, `Templates: ${templateNames().join(', ')}`)\n return { exitCode: 0 }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuCA,MAAM,OAAO;;;;;;;;;;;;;+DAakD,cAAc,EAAE,KAAK,IAAI,EAAE;AAE1F,MAAM,cAAc;;;iDAG6B,cAAc,EAAE,KAAK,IAAI,EAAE;;;;AAK5E,SAAS,IAAI,OAAe,MAAoB;CAC9C,MAAM;EAAE,QAAQ;EAAO;CAAK,CAAC;AAC/B;;AAEA,SAAS,YAAY,KAAuB;CAC1C,IAAI,IAAI,QAAQ,IAAI,IAAI,OAAO,IAAI,MAAM;AAC3C;AACA,SAAS,IAAI,OAAe,MAAoB;CAC9C,MAAM;EAAE,QAAQ;EAAO;CAAK,CAAC;AAC/B;;;;;AAMA,SAAgB,OAAO,MAAyB,KAAgC;CAC9E,MAAM,CAAC,SAAS,GAAG,QAAQ;CAE3B,IAAI,CAAC,WAAW,YAAY,UAAU,YAAY,YAAY,YAAY,MAAM;EAC9E,YAAY,GAAG;EACf,IAAI,IAAI,OAAO,IAAI;EACnB,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,IAAI,YAAY,eAAe,YAAY,QAAQ,YAAY,WAAW;EACxE,IAAI,IAAI,OAAO,IAAI,OAAO;EAC1B,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,QAAQ,SAAR;EACE,KAAK,UACH,OAAO,UAAU,MAAM,GAAG;EAC5B,KAAK,SACH,OAAO,SAAS,MAAM,GAAG;EAC3B,KAAK,OACH,OAAO,OAAO,GAAG;EACnB,KAAK,UACH,OAAO,UAAU,GAAG;EACtB,KAAK,QACH,OAAO,QAAQ,GAAG;EACpB;GACE,IAAI,IAAI,OAAO,oBAAoB,QAAQ,yBAAyB;GACpE,OAAO,EAAE,UAAU,EAAE;CACzB;AACF;;;;;;AAOA,SAAgB,YAAY,MAAyB,KAAyC;CAC5F,MAAM,CAAC,SAAS,GAAG,QAAQ;CAC3B,IAAI,YAAY,MACd,OAAO,aAAa,MAAM;EACxB,OAAO,IAAI;EACX,GAAI,IAAI,YAAY,EAAE,SAAS,IAAI,UAAU,IAAI,CAAC;CACpD,CAAC;CAEH,OAAO,QAAQ,QAAQ,OAAO,MAAM,GAAG,CAAC;AAC1C;AAEA,SAAS,UAAU,MAAyB,KAAgC;CAC1E,MAAM,EAAE,QAAQ,gBAAgB,UAAU;EACxC,MAAM,CAAC,GAAG,IAAI;EACd,kBAAkB;EAClB,QAAQ;EACR,SAAS;GACP,MAAM;IAAE,MAAM;IAAW,OAAO;GAAI;GACpC,UAAU;IAAE,MAAM;IAAU,OAAO;GAAI;GACvC,OAAO,EAAE,MAAM,UAAU;GACzB,QAAQ;IAAE,MAAM;IAAU,OAAO;GAAI;EACvC;CACF,CAAC;CAED,IAAI,OAAO,SAAS,QAAQ,YAAY,OAAO,QAAQ;EACrD,IAAI,IAAI,OAAO,WAAW;EAC1B,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,MAAM,OAAO,YAAY;CACzB,IAAI,CAAC,MAAM;EACT,IAAI,IAAI,OAAO,+EAA+E;EAC9F,OAAO,EAAE,UAAU,EAAE;CACvB;CAUA,MAAM,mBACJ,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS,IAAI,OAAO,WAAW,KAAA;CACxF,IAAI,WAAW,oBAAA;CACf,IACE,qBAAqB,KAAA,KACrB,OAAO,OAAO,WAAW,YACzB,OAAO,OAAO,SAAS,GACvB;EACA,MAAM,SAAS,0BAA0B,OAAO,MAAM;EACtD,WAAW,OAAO;EAClB,IAAI,IAAI,OAAO,yBAAyB,OAAO,SAAS,cAAc,OAAO,OAAO,GAAG;CACzF;CAEA,MAAM,SAAS,oBAAoB,MAAM,IAAI,GAAG;CAChD,IAAI,CAAC,OAAO,IAAI;EACd,IAAI,IAAI,OAAO,OAAO,KAAK;EAC3B,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,MAAM,SAAS,SAAS,IAAI,IAAI;EAC9B,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB;EACA,OAAO,OAAO,UAAU;CAC1B,CAAC;CAED,IAAI,CAAC,OAAO,IAAI;EACd,IAAI,IAAI,OAAO,OAAO,SAAS,eAAe;EAC9C,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,YAAY,GAAG;CACf,IACE,IAAI,OACJ,YAAY,OAAO,YAAY,aAAa,OAAO,SAAS,aAAa,OAAO,QAAQ,OAAO,SACjG;CACA,IAAI,IAAI,OAAO,YAAY,eAAe,OAAO,UAAU,EAAE,gCAAgC;CAC7F,OAAO,EAAE,UAAU,EAAE;AACvB;AAEA,SAAS,SAAS,MAAyB,KAAgC;CACzE,MAAM,EAAE,WAAW,UAAU;EAC3B,MAAM,CAAC,GAAG,IAAI;EACd,kBAAkB;EAClB,QAAQ;EACR,SAAS;GAAE,WAAW,EAAE,MAAM,SAAS;GAAG,iBAAiB,EAAE,MAAM,UAAU;EAAE;CACjF,CAAC;CAED,MAAM,SAAS,aAAa,IAAI,IAAI;EAClC,MAAM,IAAI;EACV,QAAQ,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa,GAAG,IAAI,IAAI;EAC/E,WAAW,OAAO,qBAAqB;CACzC,CAAC;CAED,KAAK,MAAM,KAAK,OAAO,aAAa;EAClC,MAAM,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE,SAAS,SAAS,GAAG,MAAM;EACjF,IAAI,IAAI,OAAO,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,QAAQ,EAAE,SAAS;CAC/D;CAEA,IAAI,CAAC,OAAO,IAAI;EACd,IAAI,IAAI,OAAO,0CAA0C;EACzD,OAAO,EAAE,UAAU,EAAE;CACvB;CACA,IACE,IAAI,OACJ,SAAS,OAAO,SAAS,OAAO,wBAAwB,OAAO,MAAM,eAAe,GAAG,OAAO,MAAM,cAAc,WACpH;CACA,OAAO,EAAE,UAAU,EAAE;AACvB;AAEA,SAAS,OAAO,KAAgC;CAI9C,IAAI,IAAI,OAAO,oDAAoD;CACnE,IACE,IAAI,OACJ,mFACF;CACA,IACE,IAAI,OACJ,sFACF;CACA,OAAO,EAAE,UAAU,EAAE;AACvB;AAEA,SAAS,UAAU,KAAgC;CACjD,MAAM,SAAS,UAAU,IAAI,GAAG;CAChC,KAAK,MAAM,QAAQ,aAAa,MAAM,GAAG,IAAI,IAAI,OAAO,IAAI;CAE5D,OAAO,EAAE,UADO,cAAc,MACL,MAAM,SAAS,IAAI,EAAE;AAChD;AAEA,SAAS,QAAQ,KAAgC;CAC/C,IAAI,IAAI,OAAO,eAAe,IAAI,SAAS;CAC3C,IAAI,IAAI,OAAO,QAAQ,IAAI,IAAI,aAAa;CAC5C,IACE,IAAI,OACJ,oBAAoB,IAAI,IAAI,iBAAiB,GAAG,IAAI,IAAI,eAAe,KAAK,GAAG,IAAI,IAAI,eAAe,YAAY,QACpH;CACA,IAAI,IAAI,OAAO,cAAc,cAAc,EAAE,KAAK,IAAI,GAAG;CACzD,OAAO,EAAE,UAAU,EAAE;AACvB"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["/**\n * Forge CLI dispatch — `mindees <command> [args]`.\n *\n * `runCli` is a pure function of (argv, context) → exit code, writing structured\n * output through an injected {@link Writer}. All side-effecting capabilities\n * (filesystem, env probe) are injected via {@link CliContext}, so the entire CLI\n * is deterministically testable; the thin `bin` entrypoint wires real adapters.\n *\n * @module\n */\n\nimport { parseArgs } from 'node:util'\nimport type { AiBackend } from '@mindees/ai'\nimport { runAiCommand } from './ai'\nimport { buildProject } from './build'\nimport { quoteShellPath, resolveCreateTarget } from './create-target'\nimport { doctorSummary, renderDoctor, runDoctor } from './doctor'\nimport type { FileSystem } from './fs'\nimport { naturalLanguageToTemplate } from './nl'\nimport { scaffold } from './scaffold'\nimport { DEFAULT_TEMPLATE, templateNames } from './templates'\nimport type { CommandResult, EnvProbe, Writer } from './types'\n\n/** Everything the CLI needs from the outside world (injected for testability). */\nexport interface CliContext {\n fs: FileSystem\n env: EnvProbe\n /** Working directory (where `create` writes, what `build` reads). */\n cwd: string\n /** CLI version string (from package metadata). */\n version: string\n /** Output sink. */\n write: Writer\n /** AI backend for `ai` commands (wired from `MINDEES_AI_*` env in `bin`). */\n aiBackend?: AiBackend\n /** Pre-rendered welcome banner (the MindeesNative logo). Shown on `help` + `create` success; injected by `bin` for TTYs. */\n banner?: string\n}\n\nconst HELP = `mindees — the MindeesNative CLI (Forge)\n\nUsage: mindees <command> [options]\n\nCommands:\n create <name> Scaffold a new app (--template <name>, --force)\n build Type-check + compile the project (--out-dir <dir>)\n dev Build and rebuild on change (developer preview)\n doctor Diagnose your environment\n info Show CLI + environment info\n ai explain <err> Explain an error with AI (needs MINDEES_AI_* env)\n help Show this help\n\nRun \\`mindees create --help\\` style flags inline. Templates: ${templateNames().join(', ')}.`\n\nconst CREATE_HELP = `Usage: mindees create <name-or-path> [options]\n\nOptions:\n -t, --template <name> Template to scaffold (${templateNames().join(', ')})\n -p, --prompt <text> Pick a template from a short prompt\n --force Overwrite a non-empty target directory\n -h, --help Show this help`\n\nfunction out(write: Writer, text: string): void {\n write({ stream: 'out', text })\n}\n/** Print the welcome banner (the logo), if one was injected (TTY sessions). */\nfunction printBanner(ctx: CliContext): void {\n if (ctx.banner) out(ctx.write, ctx.banner)\n}\nfunction err(write: Writer, text: string): void {\n write({ stream: 'err', text })\n}\n\n/**\n * Run the CLI. Returns a {@link CommandResult} with the process exit code.\n * Never throws for expected failures — it reports them and returns non-zero.\n */\nexport function runCli(argv: readonly string[], ctx: CliContext): CommandResult {\n const [command, ...rest] = argv\n\n if (!command || command === 'help' || command === '--help' || command === '-h') {\n printBanner(ctx)\n out(ctx.write, HELP)\n return { exitCode: 0 }\n }\n\n if (command === '--version' || command === '-v' || command === 'version') {\n out(ctx.write, ctx.version)\n return { exitCode: 0 }\n }\n\n switch (command) {\n case 'create':\n return cmdCreate(rest, ctx)\n case 'build':\n return cmdBuild(rest, ctx)\n case 'dev':\n return cmdDev(ctx)\n case 'doctor':\n return cmdDoctor(ctx)\n case 'info':\n return cmdInfo(ctx)\n default:\n err(ctx.write, `Unknown command \"${command}\". Run \\`mindees help\\`.`)\n return { exitCode: 1 }\n }\n}\n\n/**\n * The async CLI entry. Handles the model-calling `ai` command (which is asynchronous) and\n * delegates every synchronous command to {@link runCli}. The `bin` calls this; tests can call\n * either (sync commands stay testable through `runCli`).\n */\nexport function runCliAsync(argv: readonly string[], ctx: CliContext): Promise<CommandResult> {\n const [command, ...rest] = argv\n if (command === 'ai') {\n return runAiCommand(rest, {\n write: ctx.write,\n ...(ctx.aiBackend ? { backend: ctx.aiBackend } : {}),\n })\n }\n return Promise.resolve(runCli(argv, ctx))\n}\n\nfunction cmdCreate(args: readonly string[], ctx: CliContext): CommandResult {\n const { values, positionals } = parseArgs({\n args: [...args],\n allowPositionals: true,\n strict: false,\n options: {\n help: { type: 'boolean', short: 'h' },\n template: { type: 'string', short: 't' },\n force: { type: 'boolean' },\n prompt: { type: 'string', short: 'p' },\n },\n })\n\n if (values.help === true || positionals[0] === 'help') {\n out(ctx.write, CREATE_HELP)\n return { exitCode: 0 }\n }\n\n const name = positionals[0]\n if (!name) {\n err(ctx.write, 'create: missing app name or target path. Usage: mindees create <name-or-path>')\n return { exitCode: 1 }\n }\n\n // NL → template: `--prompt \"a counter app\"` picks a template deterministically\n // (offline). Real AI generation arrives with Synapse in Phase 10; until then\n // this is an honest keyword-based mapping that never blocks `create`. An\n // explicit `--template` always wins; the prompt only resolves a template when\n // the caller didn't choose one (mirrors `create-mindees`'s runCreate so both\n // entrypoints agree on precedence).\n // Treat a present-but-empty `--template \"\"` as \"not chosen\" (defer to prompt/default),\n // matching create-mindees's runCreate so both entrypoints agree on precedence.\n const explicitTemplate =\n typeof values.template === 'string' && values.template.length > 0 ? values.template : undefined\n let template = explicitTemplate ?? DEFAULT_TEMPLATE\n if (\n explicitTemplate === undefined &&\n typeof values.prompt === 'string' &&\n values.prompt.length > 0\n ) {\n const picked = naturalLanguageToTemplate(values.prompt)\n template = picked.template\n out(ctx.write, `Interpreted prompt → \"${picked.template}\" template (${picked.reason}).`)\n }\n\n const target = resolveCreateTarget(name, ctx.cwd)\n if (!target.ok) {\n err(ctx.write, target.error)\n return { exitCode: 1 }\n }\n\n const result = scaffold(ctx.fs, {\n appName: target.packageName,\n targetDir: target.targetDir,\n template,\n force: values.force === true,\n })\n\n if (!result.ok) {\n err(ctx.write, result.error ?? 'create failed')\n return { exitCode: 1 }\n }\n\n printBanner(ctx)\n out(\n ctx.write,\n `Created \"${target.packageName}\" from the ${result.template} template (${result.written.length} files).`,\n )\n const dir = quoteShellPath(target.displayDir)\n // The `android` template is a native multi-module project with a two-phase build\n // (app-js bundle from npm, then the APK) — not the `mindees dev` web flow.\n out(\n ctx.write,\n result.template === 'android'\n ? `Next: cd ${dir} — build the JS bundle (cd mindees-example-app/app-js && npm install && npm run build), then the APK (gradle wrapper --gradle-version 9.4.1 && ./gradlew :mindees-example-app:assembleDebug). See README.md.`\n : `Next: cd ${dir} && pnpm install && mindees dev`,\n )\n return { exitCode: 0 }\n}\n\nfunction cmdBuild(args: readonly string[], ctx: CliContext): CommandResult {\n const { values } = parseArgs({\n args: [...args],\n allowPositionals: true,\n strict: false,\n options: { 'out-dir': { type: 'string' }, 'no-source-map': { type: 'boolean' } },\n })\n\n const result = buildProject(ctx.fs, {\n root: ctx.cwd,\n outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : `${ctx.cwd}/dist`,\n sourceMap: values['no-source-map'] !== true,\n })\n\n for (const d of result.diagnostics) {\n const where = d.file ? `${d.file}${d.position ? `:${d.position.line}` : ''}: ` : ''\n err(ctx.write, `${d.severity} ${d.code} ${where}${d.message}`)\n }\n\n if (!result.ok) {\n err(ctx.write, 'Build failed: fix the type errors above.')\n return { exitCode: 1 }\n }\n out(\n ctx.write,\n `Built ${result.compiled.length} module(s); flattened ${result.stats.flattenedNodes}/${result.stats.totalElements} elements.`,\n )\n return { exitCode: 0 }\n}\n\nfunction cmdDev(ctx: CliContext): CommandResult {\n // The dev server transport (HTTP + HMR socket) is a developer-preview layer in\n // `bin`. The CLI command here reports how to use it; the tested rebuild\n // orchestrator lives in `dev.ts` (`startDev`).\n out(ctx.write, 'mindees dev — build + watch + live-reload preview.')\n out(\n ctx.write,\n 'Run via the `mindees` binary (it starts the HTTP server); set MINDEES_DEV_PORT to',\n )\n out(\n ctx.write,\n 'override the port (default 3000). The browser reloads automatically on each rebuild.',\n )\n return { exitCode: 0 }\n}\n\nfunction cmdDoctor(ctx: CliContext): CommandResult {\n const checks = runDoctor(ctx.env)\n for (const line of renderDoctor(checks)) out(ctx.write, line)\n const summary = doctorSummary(checks)\n return { exitCode: summary === 'fail' ? 1 : 0 }\n}\n\nfunction cmdInfo(ctx: CliContext): CommandResult {\n out(ctx.write, `mindees CLI ${ctx.version}`)\n out(ctx.write, `Node ${ctx.env.nodeVersion}`)\n out(\n ctx.write,\n `Package manager: ${ctx.env.packageManager ? `${ctx.env.packageManager.name} ${ctx.env.packageManager.version}` : 'none'}`,\n )\n out(ctx.write, `Templates: ${templateNames().join(', ')}`)\n return { exitCode: 0 }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuCA,MAAM,OAAO;;;;;;;;;;;;;+DAakD,cAAc,EAAE,KAAK,IAAI,EAAE;AAE1F,MAAM,cAAc;;;iDAG6B,cAAc,EAAE,KAAK,IAAI,EAAE;;;;AAK5E,SAAS,IAAI,OAAe,MAAoB;CAC9C,MAAM;EAAE,QAAQ;EAAO;CAAK,CAAC;AAC/B;;AAEA,SAAS,YAAY,KAAuB;CAC1C,IAAI,IAAI,QAAQ,IAAI,IAAI,OAAO,IAAI,MAAM;AAC3C;AACA,SAAS,IAAI,OAAe,MAAoB;CAC9C,MAAM;EAAE,QAAQ;EAAO;CAAK,CAAC;AAC/B;;;;;AAMA,SAAgB,OAAO,MAAyB,KAAgC;CAC9E,MAAM,CAAC,SAAS,GAAG,QAAQ;CAE3B,IAAI,CAAC,WAAW,YAAY,UAAU,YAAY,YAAY,YAAY,MAAM;EAC9E,YAAY,GAAG;EACf,IAAI,IAAI,OAAO,IAAI;EACnB,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,IAAI,YAAY,eAAe,YAAY,QAAQ,YAAY,WAAW;EACxE,IAAI,IAAI,OAAO,IAAI,OAAO;EAC1B,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,QAAQ,SAAR;EACE,KAAK,UACH,OAAO,UAAU,MAAM,GAAG;EAC5B,KAAK,SACH,OAAO,SAAS,MAAM,GAAG;EAC3B,KAAK,OACH,OAAO,OAAO,GAAG;EACnB,KAAK,UACH,OAAO,UAAU,GAAG;EACtB,KAAK,QACH,OAAO,QAAQ,GAAG;EACpB;GACE,IAAI,IAAI,OAAO,oBAAoB,QAAQ,yBAAyB;GACpE,OAAO,EAAE,UAAU,EAAE;CACzB;AACF;;;;;;AAOA,SAAgB,YAAY,MAAyB,KAAyC;CAC5F,MAAM,CAAC,SAAS,GAAG,QAAQ;CAC3B,IAAI,YAAY,MACd,OAAO,aAAa,MAAM;EACxB,OAAO,IAAI;EACX,GAAI,IAAI,YAAY,EAAE,SAAS,IAAI,UAAU,IAAI,CAAC;CACpD,CAAC;CAEH,OAAO,QAAQ,QAAQ,OAAO,MAAM,GAAG,CAAC;AAC1C;AAEA,SAAS,UAAU,MAAyB,KAAgC;CAC1E,MAAM,EAAE,QAAQ,gBAAgB,UAAU;EACxC,MAAM,CAAC,GAAG,IAAI;EACd,kBAAkB;EAClB,QAAQ;EACR,SAAS;GACP,MAAM;IAAE,MAAM;IAAW,OAAO;GAAI;GACpC,UAAU;IAAE,MAAM;IAAU,OAAO;GAAI;GACvC,OAAO,EAAE,MAAM,UAAU;GACzB,QAAQ;IAAE,MAAM;IAAU,OAAO;GAAI;EACvC;CACF,CAAC;CAED,IAAI,OAAO,SAAS,QAAQ,YAAY,OAAO,QAAQ;EACrD,IAAI,IAAI,OAAO,WAAW;EAC1B,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,MAAM,OAAO,YAAY;CACzB,IAAI,CAAC,MAAM;EACT,IAAI,IAAI,OAAO,+EAA+E;EAC9F,OAAO,EAAE,UAAU,EAAE;CACvB;CAUA,MAAM,mBACJ,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS,IAAI,OAAO,WAAW,KAAA;CACxF,IAAI,WAAW,oBAAA;CACf,IACE,qBAAqB,KAAA,KACrB,OAAO,OAAO,WAAW,YACzB,OAAO,OAAO,SAAS,GACvB;EACA,MAAM,SAAS,0BAA0B,OAAO,MAAM;EACtD,WAAW,OAAO;EAClB,IAAI,IAAI,OAAO,yBAAyB,OAAO,SAAS,cAAc,OAAO,OAAO,GAAG;CACzF;CAEA,MAAM,SAAS,oBAAoB,MAAM,IAAI,GAAG;CAChD,IAAI,CAAC,OAAO,IAAI;EACd,IAAI,IAAI,OAAO,OAAO,KAAK;EAC3B,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,MAAM,SAAS,SAAS,IAAI,IAAI;EAC9B,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB;EACA,OAAO,OAAO,UAAU;CAC1B,CAAC;CAED,IAAI,CAAC,OAAO,IAAI;EACd,IAAI,IAAI,OAAO,OAAO,SAAS,eAAe;EAC9C,OAAO,EAAE,UAAU,EAAE;CACvB;CAEA,YAAY,GAAG;CACf,IACE,IAAI,OACJ,YAAY,OAAO,YAAY,aAAa,OAAO,SAAS,aAAa,OAAO,QAAQ,OAAO,SACjG;CACA,MAAM,MAAM,eAAe,OAAO,UAAU;CAG5C,IACE,IAAI,OACJ,OAAO,aAAa,YAChB,YAAY,IAAI,gNAChB,YAAY,IAAI,gCACtB;CACA,OAAO,EAAE,UAAU,EAAE;AACvB;AAEA,SAAS,SAAS,MAAyB,KAAgC;CACzE,MAAM,EAAE,WAAW,UAAU;EAC3B,MAAM,CAAC,GAAG,IAAI;EACd,kBAAkB;EAClB,QAAQ;EACR,SAAS;GAAE,WAAW,EAAE,MAAM,SAAS;GAAG,iBAAiB,EAAE,MAAM,UAAU;EAAE;CACjF,CAAC;CAED,MAAM,SAAS,aAAa,IAAI,IAAI;EAClC,MAAM,IAAI;EACV,QAAQ,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa,GAAG,IAAI,IAAI;EAC/E,WAAW,OAAO,qBAAqB;CACzC,CAAC;CAED,KAAK,MAAM,KAAK,OAAO,aAAa;EAClC,MAAM,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE,SAAS,SAAS,GAAG,MAAM;EACjF,IAAI,IAAI,OAAO,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,QAAQ,EAAE,SAAS;CAC/D;CAEA,IAAI,CAAC,OAAO,IAAI;EACd,IAAI,IAAI,OAAO,0CAA0C;EACzD,OAAO,EAAE,UAAU,EAAE;CACvB;CACA,IACE,IAAI,OACJ,SAAS,OAAO,SAAS,OAAO,wBAAwB,OAAO,MAAM,eAAe,GAAG,OAAO,MAAM,cAAc,WACpH;CACA,OAAO,EAAE,UAAU,EAAE;AACvB;AAEA,SAAS,OAAO,KAAgC;CAI9C,IAAI,IAAI,OAAO,oDAAoD;CACnE,IACE,IAAI,OACJ,mFACF;CACA,IACE,IAAI,OACJ,sFACF;CACA,OAAO,EAAE,UAAU,EAAE;AACvB;AAEA,SAAS,UAAU,KAAgC;CACjD,MAAM,SAAS,UAAU,IAAI,GAAG;CAChC,KAAK,MAAM,QAAQ,aAAa,MAAM,GAAG,IAAI,IAAI,OAAO,IAAI;CAE5D,OAAO,EAAE,UADO,cAAc,MACL,MAAM,SAAS,IAAI,EAAE;AAChD;AAEA,SAAS,QAAQ,KAAgC;CAC/C,IAAI,IAAI,OAAO,eAAe,IAAI,SAAS;CAC3C,IAAI,IAAI,OAAO,QAAQ,IAAI,IAAI,aAAa;CAC5C,IACE,IAAI,OACJ,oBAAoB,IAAI,IAAI,iBAAiB,GAAG,IAAI,IAAI,eAAe,KAAK,GAAG,IAAI,IAAI,eAAe,YAAY,QACpH;CACA,IAAI,IAAI,OAAO,cAAc,cAAc,EAAE,KAAK,IAAI,GAAG;CACzD,OAAO,EAAE,UAAU,EAAE;AACvB"}
|
package/dist/templates.d.ts
CHANGED
|
@@ -22,8 +22,9 @@ declare function getTemplate(name: string): Template | undefined;
|
|
|
22
22
|
/** Names of all available templates. */
|
|
23
23
|
declare function templateNames(): string[];
|
|
24
24
|
/**
|
|
25
|
-
* Materialize a template's files for `appName`: substitutes the `{{appName}}`
|
|
26
|
-
*
|
|
25
|
+
* Materialize a template's files for `appName`: substitutes the `{{appName}}` and
|
|
26
|
+
* `{{androidAppId}}` placeholders and returns a fresh path → contents map. Pure (no I/O).
|
|
27
|
+
* Placeholders a template doesn't use are simply no-ops.
|
|
27
28
|
*/
|
|
28
29
|
declare function materialize(template: Template, appName: string): Record<string, string>;
|
|
29
30
|
//#endregion
|
package/dist/templates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","names":[],"sources":["../src/templates.ts"],"mappings":";;AAcA;;;;;;;;;UAAiB,QAAA;EACf,IAAA;EACA,WAAA;EACA,KAAA,EAAO,MAAM;AAAA;AAAA,cA4KF,SAAA,EAAW,MAAM,SAAS,QAAA;AAQvC;AAAA,cAAa,gBAAA;;iBAGG,WAAA,CAAY,IAAA,WAAe,QAAQ;AAHtB;AAAA,iBAQb,aAAA;;;;AALmC;
|
|
1
|
+
{"version":3,"file":"templates.d.ts","names":[],"sources":["../src/templates.ts"],"mappings":";;AAcA;;;;;;;;;UAAiB,QAAA;EACf,IAAA;EACA,WAAA;EACA,KAAA,EAAO,MAAM;AAAA;AAAA,cA4KF,SAAA,EAAW,MAAM,SAAS,QAAA;AAQvC;AAAA,cAAa,gBAAA;;iBAGG,WAAA,CAAY,IAAA,WAAe,QAAQ;AAHtB;AAAA,iBAQb,aAAA;;;;AALmC;AAKnD;iBAqBgB,WAAA,CAAY,QAAA,EAAU,QAAA,EAAU,OAAA,WAAkB,MAAM"}
|
package/dist/templates.js
CHANGED
|
@@ -153,12 +153,25 @@ function templateNames() {
|
|
|
153
153
|
return Object.keys(TEMPLATES);
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
156
|
+
* A valid Android `applicationId` derived from the app name (reverse-DNS, sanitized to a Java
|
|
157
|
+
* identifier segment). Gives each scaffold a unique install id so two MindeesNative Android apps
|
|
158
|
+
* coexist on a device. The `com.example.*` prefix is a conventional placeholder users can change.
|
|
159
|
+
*/
|
|
160
|
+
function androidAppId(appName) {
|
|
161
|
+
let segment = appName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
162
|
+
if (segment.length === 0) segment = "app";
|
|
163
|
+
if (/^[0-9]/.test(segment)) segment = `app${segment}`;
|
|
164
|
+
return `com.example.${segment}`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Materialize a template's files for `appName`: substitutes the `{{appName}}` and
|
|
168
|
+
* `{{androidAppId}}` placeholders and returns a fresh path → contents map. Pure (no I/O).
|
|
169
|
+
* Placeholders a template doesn't use are simply no-ops.
|
|
158
170
|
*/
|
|
159
171
|
function materialize(template, appName) {
|
|
172
|
+
const appId = androidAppId(appName);
|
|
160
173
|
const out = {};
|
|
161
|
-
for (const [path, contents] of Object.entries(template.files)) out[path] = contents.replaceAll("{{appName}}", appName);
|
|
174
|
+
for (const [path, contents] of Object.entries(template.files)) out[path] = contents.replaceAll("{{appName}}", appName).replaceAll("{{androidAppId}}", appId);
|
|
162
175
|
return out;
|
|
163
176
|
}
|
|
164
177
|
//#endregion
|
package/dist/templates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","names":[],"sources":["../src/templates.ts"],"sourcesContent":["/**\n * Project templates for `mindees create` / `create-mindees`.\n *\n * Templates are in-memory maps of relative path → file contents, so they're\n * deterministic and need no on-disk fixtures. Each scaffolds a runnable\n * MindeesNative app skeleton wired to the current `@mindees/*` packages.\n *\n * @module\n */\n\nimport { ANDROID_TEMPLATE_FILES } from './android-template.generated'\nimport { VERSION } from './version'\n\n/** A template: a name, a description, and its files (relative path → contents). */\nexport interface Template {\n name: string\n description: string\n files: Record<string, string>\n}\n\n/**\n * Version pinned for scaffolded apps. Derives from the CLI's own\n * {@link VERSION} (the single locked `@mindees/*` version line) so generated\n * projects always pin the framework packages to the same release that\n * scaffolded them, instead of a hardcoded literal.\n */\nconst PKG_VERSION = VERSION\n\nfunction appPackageJson(appName: string, extraDeps: Record<string, string> = {}): string {\n const deps = {\n '@mindees/core': PKG_VERSION,\n '@mindees/renderer': PKG_VERSION,\n ...extraDeps,\n }\n // The `dev`/`build` scripts invoke the `mindees` binary, so the CLI must be a\n // (dev) dependency of the generated app — otherwise the commands only resolve\n // when the CLI happens to be installed globally.\n const devDeps = {\n '@mindees/cli': PKG_VERSION,\n }\n return `${JSON.stringify(\n {\n name: appName,\n version: '0.1.0',\n private: true,\n type: 'module',\n scripts: {\n dev: 'mindees dev',\n build: 'mindees build',\n },\n dependencies: deps,\n devDependencies: devDeps,\n },\n null,\n 2,\n )}\\n`\n}\n\nconst GITIGNORE = 'node_modules/\\ndist/\\n*.log\\n'\n\nconst TSCONFIG = `${JSON.stringify(\n {\n compilerOptions: {\n strict: true,\n module: 'esnext',\n moduleResolution: 'bundler',\n target: 'es2023',\n jsx: 'react',\n jsxFactory: 'createElement',\n jsxFragmentFactory: 'Fragment',\n verbatimModuleSyntax: true,\n },\n include: ['src'],\n },\n null,\n 2,\n)}\\n`\n\n/** The `blank` template: the minimal runnable app. */\nconst blank: Template = {\n name: 'blank',\n description: 'A minimal MindeesNative app (one screen).',\n files: {\n 'package.json': appPackageJson('{{appName}}'),\n '.gitignore': GITIGNORE,\n 'tsconfig.json': TSCONFIG,\n 'README.md': `# {{appName}}\\n\\nA MindeesNative app. Start with \\`mindees dev\\`.\\n`,\n 'src/App.tsx': `import { createElement } from '@mindees/core'\n\nexport function App() {\n return (\n <view>\n <text>Hello from {{appName}} 👋</text>\n </view>\n )\n}\n`,\n 'src/main.tsx': `import { createDomBackend, render } from '@mindees/renderer'\nimport { App } from './App'\n\nconst root = document.getElementById('app')\nif (root) render(App, {}, createDomBackend(), root)\n`,\n },\n}\n\n/** The `counter` template: shows fine-grained reactivity (signals). */\nconst counter: Template = {\n name: 'counter',\n description: 'A reactive counter — demonstrates signals + fine-grained updates.',\n files: {\n 'package.json': appPackageJson('{{appName}}'),\n '.gitignore': GITIGNORE,\n 'tsconfig.json': TSCONFIG,\n 'README.md': `# {{appName}}\\n\\nA reactive counter built with MindeesNative signals.\\n`,\n 'src/App.tsx': `import { createElement, signal } from '@mindees/core'\n\nexport function App() {\n const count = signal(0)\n return (\n <view>\n <text>{() => \\`Count: \\${count()}\\`}</text>\n <button onClick={() => count.set(count() + 1)}>Increment</button>\n </view>\n )\n}\n`,\n 'src/main.tsx': `import { createDomBackend, render } from '@mindees/renderer'\nimport { App } from './App'\n\nconst root = document.getElementById('app')\nif (root) render(App, {}, createDomBackend(), root)\n`,\n },\n}\n\n/**\n * The `app` template: a polished starter showing the batteries — Atlas components,\n * a standard hook, and theming — so an ordinary developer has a real screen in seconds.\n */\nconst app: Template = {\n name: 'app',\n description: 'A polished starter — Atlas components, hooks, and theming (batteries included).',\n files: {\n 'package.json': appPackageJson('{{appName}}', { '@mindees/atlas': PKG_VERSION }),\n '.gitignore': GITIGNORE,\n 'tsconfig.json': TSCONFIG,\n 'README.md': `# {{appName}}\\n\\nA MindeesNative app using the Atlas UI kit. Run \\`mindees dev\\`.\\n`,\n 'src/App.tsx': `import { createElement } from '@mindees/core'\nimport { Button, Card, Switch, Text, useToggle } from '@mindees/atlas'\n\nexport function App() {\n const dark = useToggle(false)\n return (\n <Card style={{ gap: 12, maxWidth: 360 }}>\n <Text style={{ fontSize: 24, fontWeight: 700 }}>Welcome to {{appName}} ✨</Text>\n <Text>Signals, native UI, and batteries included — from one TypeScript codebase.</Text>\n <Switch value={dark.value} onValueChange={dark.set} />\n <Button title=\"Get started\" onPress={() => console.log('Let us build!')} />\n </Card>\n )\n}\n`,\n 'src/main.tsx': `import { createDomBackend, render } from '@mindees/renderer'\nimport { App } from './App'\n\nconst root = document.getElementById('app')\nif (root) render(App, {}, createDomBackend(), root)\n`,\n },\n}\n\n/** All built-in templates, keyed by name. */\n/**\n * The `android` template (EXPERIMENTAL): a standalone native Android app you build with\n * `gradle assembleDebug`. The UI is TSX (Atlas + the Quantum router) running on a real\n * Android view tree via an embedded QuickJS runtime; the native host is vendored as\n * Kotlin source (no Maven dependency on MindeesNative). Files are codegen'd from the\n * CI-verified reference host (see scripts/gen-android-template.mjs), so they can't drift.\n * Unlike the web templates this carries no root package.json/tsconfig — the app-js build\n * lives under `mindees-example-app/app-js/`. See the scaffolded README for the build flow.\n */\nconst android: Template = {\n name: 'android',\n description:\n 'EXPERIMENTAL — a standalone native Android app (gradle assembleDebug; QuickJS-hosted, host vendored as source).',\n files: ANDROID_TEMPLATE_FILES,\n}\n\nexport const TEMPLATES: Record<string, Template> = {\n blank,\n counter,\n app,\n android,\n}\n\n/** The default template name used when none is specified. */\nexport const DEFAULT_TEMPLATE = 'blank'\n\n/** Look up a template by name, or `undefined` if unknown. */\nexport function getTemplate(name: string): Template | undefined {\n return TEMPLATES[name]\n}\n\n/** Names of all available templates. */\nexport function templateNames(): string[] {\n return Object.keys(TEMPLATES)\n}\n\n/**\n * Materialize a template's files for `appName`: substitutes the `{{appName}}
|
|
1
|
+
{"version":3,"file":"templates.js","names":[],"sources":["../src/templates.ts"],"sourcesContent":["/**\n * Project templates for `mindees create` / `create-mindees`.\n *\n * Templates are in-memory maps of relative path → file contents, so they're\n * deterministic and need no on-disk fixtures. Each scaffolds a runnable\n * MindeesNative app skeleton wired to the current `@mindees/*` packages.\n *\n * @module\n */\n\nimport { ANDROID_TEMPLATE_FILES } from './android-template.generated'\nimport { VERSION } from './version'\n\n/** A template: a name, a description, and its files (relative path → contents). */\nexport interface Template {\n name: string\n description: string\n files: Record<string, string>\n}\n\n/**\n * Version pinned for scaffolded apps. Derives from the CLI's own\n * {@link VERSION} (the single locked `@mindees/*` version line) so generated\n * projects always pin the framework packages to the same release that\n * scaffolded them, instead of a hardcoded literal.\n */\nconst PKG_VERSION = VERSION\n\nfunction appPackageJson(appName: string, extraDeps: Record<string, string> = {}): string {\n const deps = {\n '@mindees/core': PKG_VERSION,\n '@mindees/renderer': PKG_VERSION,\n ...extraDeps,\n }\n // The `dev`/`build` scripts invoke the `mindees` binary, so the CLI must be a\n // (dev) dependency of the generated app — otherwise the commands only resolve\n // when the CLI happens to be installed globally.\n const devDeps = {\n '@mindees/cli': PKG_VERSION,\n }\n return `${JSON.stringify(\n {\n name: appName,\n version: '0.1.0',\n private: true,\n type: 'module',\n scripts: {\n dev: 'mindees dev',\n build: 'mindees build',\n },\n dependencies: deps,\n devDependencies: devDeps,\n },\n null,\n 2,\n )}\\n`\n}\n\nconst GITIGNORE = 'node_modules/\\ndist/\\n*.log\\n'\n\nconst TSCONFIG = `${JSON.stringify(\n {\n compilerOptions: {\n strict: true,\n module: 'esnext',\n moduleResolution: 'bundler',\n target: 'es2023',\n jsx: 'react',\n jsxFactory: 'createElement',\n jsxFragmentFactory: 'Fragment',\n verbatimModuleSyntax: true,\n },\n include: ['src'],\n },\n null,\n 2,\n)}\\n`\n\n/** The `blank` template: the minimal runnable app. */\nconst blank: Template = {\n name: 'blank',\n description: 'A minimal MindeesNative app (one screen).',\n files: {\n 'package.json': appPackageJson('{{appName}}'),\n '.gitignore': GITIGNORE,\n 'tsconfig.json': TSCONFIG,\n 'README.md': `# {{appName}}\\n\\nA MindeesNative app. Start with \\`mindees dev\\`.\\n`,\n 'src/App.tsx': `import { createElement } from '@mindees/core'\n\nexport function App() {\n return (\n <view>\n <text>Hello from {{appName}} 👋</text>\n </view>\n )\n}\n`,\n 'src/main.tsx': `import { createDomBackend, render } from '@mindees/renderer'\nimport { App } from './App'\n\nconst root = document.getElementById('app')\nif (root) render(App, {}, createDomBackend(), root)\n`,\n },\n}\n\n/** The `counter` template: shows fine-grained reactivity (signals). */\nconst counter: Template = {\n name: 'counter',\n description: 'A reactive counter — demonstrates signals + fine-grained updates.',\n files: {\n 'package.json': appPackageJson('{{appName}}'),\n '.gitignore': GITIGNORE,\n 'tsconfig.json': TSCONFIG,\n 'README.md': `# {{appName}}\\n\\nA reactive counter built with MindeesNative signals.\\n`,\n 'src/App.tsx': `import { createElement, signal } from '@mindees/core'\n\nexport function App() {\n const count = signal(0)\n return (\n <view>\n <text>{() => \\`Count: \\${count()}\\`}</text>\n <button onClick={() => count.set(count() + 1)}>Increment</button>\n </view>\n )\n}\n`,\n 'src/main.tsx': `import { createDomBackend, render } from '@mindees/renderer'\nimport { App } from './App'\n\nconst root = document.getElementById('app')\nif (root) render(App, {}, createDomBackend(), root)\n`,\n },\n}\n\n/**\n * The `app` template: a polished starter showing the batteries — Atlas components,\n * a standard hook, and theming — so an ordinary developer has a real screen in seconds.\n */\nconst app: Template = {\n name: 'app',\n description: 'A polished starter — Atlas components, hooks, and theming (batteries included).',\n files: {\n 'package.json': appPackageJson('{{appName}}', { '@mindees/atlas': PKG_VERSION }),\n '.gitignore': GITIGNORE,\n 'tsconfig.json': TSCONFIG,\n 'README.md': `# {{appName}}\\n\\nA MindeesNative app using the Atlas UI kit. Run \\`mindees dev\\`.\\n`,\n 'src/App.tsx': `import { createElement } from '@mindees/core'\nimport { Button, Card, Switch, Text, useToggle } from '@mindees/atlas'\n\nexport function App() {\n const dark = useToggle(false)\n return (\n <Card style={{ gap: 12, maxWidth: 360 }}>\n <Text style={{ fontSize: 24, fontWeight: 700 }}>Welcome to {{appName}} ✨</Text>\n <Text>Signals, native UI, and batteries included — from one TypeScript codebase.</Text>\n <Switch value={dark.value} onValueChange={dark.set} />\n <Button title=\"Get started\" onPress={() => console.log('Let us build!')} />\n </Card>\n )\n}\n`,\n 'src/main.tsx': `import { createDomBackend, render } from '@mindees/renderer'\nimport { App } from './App'\n\nconst root = document.getElementById('app')\nif (root) render(App, {}, createDomBackend(), root)\n`,\n },\n}\n\n/** All built-in templates, keyed by name. */\n/**\n * The `android` template (EXPERIMENTAL): a standalone native Android app you build with\n * `gradle assembleDebug`. The UI is TSX (Atlas + the Quantum router) running on a real\n * Android view tree via an embedded QuickJS runtime; the native host is vendored as\n * Kotlin source (no Maven dependency on MindeesNative). Files are codegen'd from the\n * CI-verified reference host (see scripts/gen-android-template.mjs), so they can't drift.\n * Unlike the web templates this carries no root package.json/tsconfig — the app-js build\n * lives under `mindees-example-app/app-js/`. See the scaffolded README for the build flow.\n */\nconst android: Template = {\n name: 'android',\n description:\n 'EXPERIMENTAL — a standalone native Android app (gradle assembleDebug; QuickJS-hosted, host vendored as source).',\n files: ANDROID_TEMPLATE_FILES,\n}\n\nexport const TEMPLATES: Record<string, Template> = {\n blank,\n counter,\n app,\n android,\n}\n\n/** The default template name used when none is specified. */\nexport const DEFAULT_TEMPLATE = 'blank'\n\n/** Look up a template by name, or `undefined` if unknown. */\nexport function getTemplate(name: string): Template | undefined {\n return TEMPLATES[name]\n}\n\n/** Names of all available templates. */\nexport function templateNames(): string[] {\n return Object.keys(TEMPLATES)\n}\n\n/**\n * A valid Android `applicationId` derived from the app name (reverse-DNS, sanitized to a Java\n * identifier segment). Gives each scaffold a unique install id so two MindeesNative Android apps\n * coexist on a device. The `com.example.*` prefix is a conventional placeholder users can change.\n */\nfunction androidAppId(appName: string): string {\n let segment = appName.toLowerCase().replace(/[^a-z0-9]/g, '')\n if (segment.length === 0) segment = 'app'\n if (/^[0-9]/.test(segment)) segment = `app${segment}`\n return `com.example.${segment}`\n}\n\n/**\n * Materialize a template's files for `appName`: substitutes the `{{appName}}` and\n * `{{androidAppId}}` placeholders and returns a fresh path → contents map. Pure (no I/O).\n * Placeholders a template doesn't use are simply no-ops.\n */\nexport function materialize(template: Template, appName: string): Record<string, string> {\n const appId = androidAppId(appName)\n const out: Record<string, string> = {}\n for (const [path, contents] of Object.entries(template.files)) {\n out[path] = contents.replaceAll('{{appName}}', appName).replaceAll('{{androidAppId}}', appId)\n }\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0BA,MAAM,cAAc;AAEpB,SAAS,eAAe,SAAiB,YAAoC,CAAC,GAAW;CACvF,MAAM,OAAO;EACX,iBAAiB;EACjB,qBAAqB;EACrB,GAAG;CACL;CAOA,OAAO,GAAG,KAAK,UACb;EACE,MAAM;EACN,SAAS;EACT,SAAS;EACT,MAAM;EACN,SAAS;GACP,KAAK;GACL,OAAO;EACT;EACA,cAAc;EACd,iBAAiB,EAbnB,gBAAgB,YAaS;CACzB,GACA,MACA,CACF,EAAE;AACJ;AAEA,MAAM,YAAY;AAElB,MAAM,WAAW,GAAG,KAAK,UACvB;CACE,iBAAiB;EACf,QAAQ;EACR,QAAQ;EACR,kBAAkB;EAClB,QAAQ;EACR,KAAK;EACL,YAAY;EACZ,oBAAoB;EACpB,sBAAsB;CACxB;CACA,SAAS,CAAC,KAAK;AACjB,GACA,MACA,CACF,EAAE;AAiHF,MAAa,YAAsC;CACjD;EA9GA,MAAM;EACN,aAAa;EACb,OAAO;GACL,gBAAgB,eAAe,aAAa;GAC5C,cAAc;GACd,iBAAiB;GACjB,aAAa;GACb,eAAe;;;;;;;;;;GAUf,gBAAgB;;;;;;EAMlB;CAuFA;CACA;EAnFA,MAAM;EACN,aAAa;EACb,OAAO;GACL,gBAAgB,eAAe,aAAa;GAC5C,cAAc;GACd,iBAAiB;GACjB,aAAa;GACb,eAAe;;;;;;;;;;;;GAYf,gBAAgB;;;;;;EAMlB;CA0DA;CACA;EAnDA,MAAM;EACN,aAAa;EACb,OAAO;GACL,gBAAgB,eAAe,eAAe,EAAE,kBAAkB,YAAY,CAAC;GAC/E,cAAc;GACd,iBAAiB;GACjB,aAAa;GACb,eAAe;;;;;;;;;;;;;;;GAef,gBAAgB;;;;;;EAMlB;CAuBA;CACA;EAVA,MAAM;EACN,aACE;EACF,OAAO;CAOP;AACF;;AAGA,MAAa,mBAAmB;;AAGhC,SAAgB,YAAY,MAAoC;CAC9D,OAAO,UAAU;AACnB;;AAGA,SAAgB,gBAA0B;CACxC,OAAO,OAAO,KAAK,SAAS;AAC9B;;;;;;AAOA,SAAS,aAAa,SAAyB;CAC7C,IAAI,UAAU,QAAQ,YAAY,EAAE,QAAQ,cAAc,EAAE;CAC5D,IAAI,QAAQ,WAAW,GAAG,UAAU;CACpC,IAAI,SAAS,KAAK,OAAO,GAAG,UAAU,MAAM;CAC5C,OAAO,eAAe;AACxB;;;;;;AAOA,SAAgB,YAAY,UAAoB,SAAyC;CACvF,MAAM,QAAQ,aAAa,OAAO;CAClC,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,SAAS,KAAK,GAC1D,IAAI,QAAQ,SAAS,WAAW,eAAe,OAAO,EAAE,WAAW,oBAAoB,KAAK;CAE9F,OAAO;AACT"}
|
package/dist/version.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
-
declare const VERSION = "0.
|
|
14
|
+
declare const VERSION = "0.21.0";
|
|
15
15
|
//#endregion
|
|
16
16
|
export { VERSION };
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the `@mindees/cli` package version.\n *\n * All `@mindees/*` packages share one locked version line (Changesets, fixed\n * group). Keeping it in its own dependency-free module lets both the public\n * package metadata ({@link index}) and the scaffolder's generated\n * `package.json` ({@link templates}) pin to the same value without a circular\n * import.\n *\n * @module\n */\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.
|
|
1
|
+
{"version":3,"file":"version.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the `@mindees/cli` package version.\n *\n * All `@mindees/*` packages share one locked version line (Changesets, fixed\n * group). Keeping it in its own dependency-free module lets both the public\n * package metadata ({@link index}) and the scaffolder's generated\n * `package.json` ({@link templates}) pin to the same value without a circular\n * import.\n *\n * @module\n */\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.21.0'\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,UAAU"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "MindeesNative Forge — the `mindees` CLI: create, build (via the compiler), dev (rebuild orchestrator), doctor, info.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"directory": "packages/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@mindees/core": "0.
|
|
31
|
-
"@mindees/compiler": "0.
|
|
32
|
-
"@mindees/ai": "0.
|
|
30
|
+
"@mindees/core": "0.21.0",
|
|
31
|
+
"@mindees/compiler": "0.21.0",
|
|
32
|
+
"@mindees/ai": "0.21.0"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsdown",
|