@mindees/cli 0.23.0 → 0.25.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.
@@ -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.23.0\",\n \"@mindees/atlas\": \"0.23.0\",\n \"@mindees/renderer\": \"0.23.0\",\n \"@mindees/router\": \"0.23.0\",\n \"@mindees/compiler\": \"0.23.0\"\n },\n \"devDependencies\": {\n \"tsdown\": \"0.22.1\",\n \"typescript\": \"6.0.3\"\n }\n}\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.25.0\",\n \"@mindees/atlas\": \"0.25.0\",\n \"@mindees/renderer\": \"0.25.0\",\n \"@mindees/router\": \"0.25.0\",\n \"@mindees/compiler\": \"0.25.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",
@@ -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.23.0\\\",\\n \\\"@mindees/atlas\\\": \\\"0.23.0\\\",\\n \\\"@mindees/renderer\\\": \\\"0.23.0\\\",\\n \\\"@mindees/router\\\": \\\"0.23.0\\\",\\n \\\"@mindees/compiler\\\": \\\"0.23.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.23.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.25.0\\\",\\n \\\"@mindees/atlas\\\": \\\"0.25.0\\\",\\n \\\"@mindees/renderer\\\": \\\"0.25.0\\\",\\n \\\"@mindees/router\\\": \\\"0.25.0\\\",\\n \\\"@mindees/compiler\\\": \\\"0.25.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.25.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/bin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { VERSION } from "./version.js";
3
+ import { loadConfig } from "./config.js";
3
4
  import { runCliAsync } from "./cli.js";
4
5
  import { startDev } from "./dev.js";
5
6
  import { createDevServer, createNodeWatcher, renderDevPage } from "./dev-server.js";
@@ -90,15 +91,20 @@ function runDevServer(ctx) {
90
91
  return out;
91
92
  };
92
93
  const watcher = createNodeWatcher(["src"], { watch: (path, opts, listener) => watch(path, { recursive: opts.recursive ?? false }, (event, filename) => listener(event, typeof filename === "string" ? filename : null)) });
93
- startDev(ctx.fs, watcher, { onRebuild: (result) => {
94
- if (result.ok) server.setFiles(collectDist());
95
- else server.setError(renderDevPage(result));
96
- server.bump();
97
- ctx.write({
98
- text: result.ok ? `rebuilt: ${result.compiled.length} file(s) ok` : `rebuild failed: ${result.diagnostics.filter((d) => d.severity === "error").length} error(s)`,
99
- stream: result.ok ? "out" : "err"
100
- });
101
- } });
94
+ const config = loadConfig(ctx.fs, ctx.cwd);
95
+ startDev(ctx.fs, watcher, {
96
+ perf: config.perf ?? true,
97
+ ...config.budget ? { budget: config.budget } : {},
98
+ onRebuild: (result) => {
99
+ if (result.ok) server.setFiles(collectDist());
100
+ else server.setError(renderDevPage(result));
101
+ server.bump();
102
+ ctx.write({
103
+ text: result.ok ? `rebuilt: ${result.compiled.length} file(s) ok` : `rebuild failed: ${result.diagnostics.filter((d) => d.severity === "error").length} error(s)`,
104
+ stream: result.ok ? "out" : "err"
105
+ });
106
+ }
107
+ });
102
108
  createServer((req, res) => {
103
109
  const out = server.handle(req.method ?? "GET", req.url ?? "/");
104
110
  res.writeHead(out.status, out.headers);
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","names":["fsWatch"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The `mindees` executable — a thin adapter that wires real Node capabilities\n * (filesystem, environment probe, stdout/stderr, AI backend) into the tested\n * {@link runCliAsync} core. All logic lives in the core; this file only does I/O wiring.\n *\n * @module\n */\n\nimport {\n existsSync,\n watch as fsWatch,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { createServer } from 'node:http'\nimport { dirname, join, relative, sep } from 'node:path'\nimport process from 'node:process'\nimport type { AiBackend } from '@mindees/ai'\nimport { type AdapterName, createServerBackend, type FetchLike } from '@mindees/ai/server'\nimport { detectImageSupport, itermImage, renderBanner } from './banner'\nimport { type CliContext, runCliAsync } from './cli'\nimport { startDev } from './dev'\nimport { createDevServer, createNodeWatcher, renderDevPage } from './dev-server'\nimport type { FileSystem } from './fs'\nimport { VERSION } from './index'\nimport type { EnvProbe, OutputLine } from './types'\n\n/** A `node:fs`-backed {@link FileSystem}. */\nfunction nodeFileSystem(): FileSystem {\n return {\n exists: (path) => existsSync(path),\n readFile: (path) => readFileSync(path, 'utf8'),\n writeFile: (path, contents) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, contents, 'utf8')\n },\n mkdir: (path) => {\n mkdirSync(path, { recursive: true })\n },\n readDir: (dir) => {\n const walk = (current: string, acc: string[]): string[] => {\n if (!existsSync(current)) return acc\n for (const entry of readdirSync(current)) {\n const full = join(current, entry)\n if (statSync(full).isDirectory()) walk(full, acc)\n else acc.push(relative(dir, full).split(sep).join('/'))\n }\n return acc\n }\n return walk(dir, []).sort()\n },\n }\n}\n\n/** Probe the real environment for `doctor`/`info`. */\nfunction probeEnv(cwd: string): EnvProbe {\n const pmSpec = process.env.npm_config_user_agent ?? ''\n // user agent looks like \"pnpm/11.5.0 npm/? node/v24 ...\".\n const pmMatch = pmSpec.match(/^(\\w+)\\/(\\S+)/)\n return {\n nodeVersion: process.version,\n packageManager: pmMatch?.[1] && pmMatch[2] ? { name: pmMatch[1], version: pmMatch[2] } : null,\n hasPackageJson: existsSync(join(cwd, 'package.json')),\n hasNodeModules: existsSync(join(cwd, 'node_modules')),\n }\n}\n\n/** Build a server AI backend from `MINDEES_AI_*` env, or `undefined` if not configured. */\nfunction aiBackendFromEnv(): AiBackend | undefined {\n const baseUrl = process.env.MINDEES_AI_BASE_URL\n const model = process.env.MINDEES_AI_MODEL\n if (!baseUrl || !model) return undefined\n // Fail loud on a mistyped adapter rather than silently defaulting to openai (which would\n // build the wrong auth headers and yield confusing HTTP errors). Empty/unset → openai.\n const adapterEnv = process.env.MINDEES_AI_ADAPTER\n if (adapterEnv && adapterEnv !== 'openai' && adapterEnv !== 'anthropic') {\n process.stderr.write(\n `mindees: unknown MINDEES_AI_ADAPTER \"${adapterEnv}\" (expected \"openai\" or \"anthropic\"); AI backend not configured.\\n`,\n )\n return undefined\n }\n const adapter: AdapterName = adapterEnv === 'anthropic' ? 'anthropic' : 'openai'\n return createServerBackend({\n // The global `fetch` is structurally compatible at runtime; the minimal FetchLike\n // intentionally avoids the DOM lib, so cast rather than pull in those types.\n fetch: globalThis.fetch as unknown as FetchLike,\n baseUrl,\n model,\n adapter,\n ...(process.env.MINDEES_AI_API_KEY ? { apiKey: process.env.MINDEES_AI_API_KEY } : {}),\n })\n}\n\n/**\n * `mindees dev` — the long-running transport over the tested {@link startDev} orchestrator:\n * build + watch `src/`, serve a live-reload preview, and reload the browser on each rebuild. This\n * is the I/O glue; the watcher, server, and orchestrator it wires are unit-tested in their modules.\n */\nfunction runDevServer(ctx: CliContext): void {\n const port = Number(process.env.MINDEES_DEV_PORT ?? 3000) || 3000\n const server = createDevServer()\n // Collect the freshly-built `dist/` tree (recursive, POSIX-relative) to serve as the app.\n const collectDist = (dir = 'dist'): Record<string, string> => {\n if (!ctx.fs.exists(dir)) return {}\n const out: Record<string, string> = {}\n for (const rel of ctx.fs.readDir(dir)) out[rel] = ctx.fs.readFile(`${dir}/${rel}`)\n return out\n }\n const watcher = createNodeWatcher(['src'], {\n watch: (path, opts, listener) =>\n fsWatch(path, { recursive: opts.recursive ?? false }, (event, filename) =>\n listener(event, typeof filename === 'string' ? filename : null),\n ),\n })\n startDev(ctx.fs, watcher, {\n onRebuild: (result) => {\n // Serve the built app on success; show the diagnostics overlay (at `/`) on failure.\n if (result.ok) server.setFiles(collectDist())\n else server.setError(renderDevPage(result))\n server.bump()\n ctx.write({\n text: result.ok\n ? `rebuilt: ${result.compiled.length} file(s) ok`\n : `rebuild failed: ${result.diagnostics.filter((d) => d.severity === 'error').length} error(s)`,\n stream: result.ok ? 'out' : 'err',\n })\n },\n })\n createServer((req, res) => {\n const out = server.handle(req.method ?? 'GET', req.url ?? '/')\n res.writeHead(out.status, out.headers)\n res.end(out.body)\n }).listen(port, () => {\n ctx.write({\n text: `mindees dev — serving http://localhost:${port} (live reload on)`,\n stream: 'out',\n })\n })\n}\n\n/**\n * Build the welcome banner for an interactive (TTY) session: the ANSI wordmark always, plus the\n * actual logo PNG inline on image-capable terminals (iTerm2 / WezTerm). Returns `undefined` when\n * stdout is piped (scripts get clean, parseable output).\n */\nfunction buildBanner(): string | undefined {\n if (!process.stdout.isTTY) return undefined\n const color = !process.env.NO_COLOR\n let image: string | null = null\n if (detectImageSupport(process.env)) {\n try {\n const bytes = readFileSync(new URL('../assets/logo.png', import.meta.url))\n image = itermImage(bytes.toString('base64'), { width: 14 })\n } catch {\n image = null // asset missing → wordmark only\n }\n }\n return renderBanner({ color, image, version: VERSION })\n}\n\nasync function main(): Promise<void> {\n const cwd = process.cwd()\n const write = (line: OutputLine): void => {\n const stream = line.stream === 'err' ? process.stderr : process.stdout\n stream.write(`${line.text}\\n`)\n }\n const backend = aiBackendFromEnv()\n const banner = buildBanner()\n const ctx: CliContext = {\n fs: nodeFileSystem(),\n env: probeEnv(cwd),\n cwd,\n version: VERSION,\n write,\n ...(backend ? { aiBackend: backend } : {}),\n ...(banner ? { banner } : {}),\n }\n // `dev` is a long-running transport (not a one-shot command), so it's wired here in the I/O entry\n // rather than the synchronous CLI dispatch. Everything else goes through the tested core.\n if (process.argv[2] === 'dev') {\n runDevServer(ctx)\n return\n }\n const { exitCode } = await runCliAsync(process.argv.slice(2), ctx)\n process.exitCode = exitCode\n}\n\nmain()\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAiCA,SAAS,iBAA6B;CACpC,OAAO;EACL,SAAS,SAAS,WAAW,IAAI;EACjC,WAAW,SAAS,aAAa,MAAM,MAAM;EAC7C,YAAY,MAAM,aAAa;GAC7B,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GAC5C,cAAc,MAAM,UAAU,MAAM;EACtC;EACA,QAAQ,SAAS;GACf,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;EACrC;EACA,UAAU,QAAQ;GAChB,MAAM,QAAQ,SAAiB,QAA4B;IACzD,IAAI,CAAC,WAAW,OAAO,GAAG,OAAO;IACjC,KAAK,MAAM,SAAS,YAAY,OAAO,GAAG;KACxC,MAAM,OAAO,KAAK,SAAS,KAAK;KAChC,IAAI,SAAS,IAAI,EAAE,YAAY,GAAG,KAAK,MAAM,GAAG;UAC3C,IAAI,KAAK,SAAS,KAAK,IAAI,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG,CAAC;IACxD;IACA,OAAO;GACT;GACA,OAAO,KAAK,KAAK,CAAC,CAAC,EAAE,KAAK;EAC5B;CACF;AACF;;AAGA,SAAS,SAAS,KAAuB;CAGvC,MAAM,WAFS,QAAQ,IAAI,yBAAyB,IAE7B,MAAM,eAAe;CAC5C,OAAO;EACL,aAAa,QAAQ;EACrB,gBAAgB,UAAU,MAAM,QAAQ,KAAK;GAAE,MAAM,QAAQ;GAAI,SAAS,QAAQ;EAAG,IAAI;EACzF,gBAAgB,WAAW,KAAK,KAAK,cAAc,CAAC;EACpD,gBAAgB,WAAW,KAAK,KAAK,cAAc,CAAC;CACtD;AACF;;AAGA,SAAS,mBAA0C;CACjD,MAAM,UAAU,QAAQ,IAAI;CAC5B,MAAM,QAAQ,QAAQ,IAAI;CAC1B,IAAI,CAAC,WAAW,CAAC,OAAO,OAAO,KAAA;CAG/B,MAAM,aAAa,QAAQ,IAAI;CAC/B,IAAI,cAAc,eAAe,YAAY,eAAe,aAAa;EACvE,QAAQ,OAAO,MACb,wCAAwC,WAAW,mEACrD;EACA;CACF;CACA,MAAM,UAAuB,eAAe,cAAc,cAAc;CACxE,OAAO,oBAAoB;EAGzB,OAAO,WAAW;EAClB;EACA;EACA;EACA,GAAI,QAAQ,IAAI,qBAAqB,EAAE,QAAQ,QAAQ,IAAI,mBAAmB,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;AAOA,SAAS,aAAa,KAAuB;CAC3C,MAAM,OAAO,OAAO,QAAQ,IAAI,oBAAoB,GAAI,KAAK;CAC7D,MAAM,SAAS,gBAAgB;CAE/B,MAAM,eAAe,MAAM,WAAmC;EAC5D,IAAI,CAAC,IAAI,GAAG,OAAO,GAAG,GAAG,OAAO,CAAC;EACjC,MAAM,MAA8B,CAAC;EACrC,KAAK,MAAM,OAAO,IAAI,GAAG,QAAQ,GAAG,GAAG,IAAI,OAAO,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,KAAK;EACjF,OAAO;CACT;CACA,MAAM,UAAU,kBAAkB,CAAC,KAAK,GAAG,EACzC,QAAQ,MAAM,MAAM,aAClBA,MAAQ,MAAM,EAAE,WAAW,KAAK,aAAa,MAAM,IAAI,OAAO,aAC5D,SAAS,OAAO,OAAO,aAAa,WAAW,WAAW,IAAI,CAChE,EACJ,CAAC;CACD,SAAS,IAAI,IAAI,SAAS,EACxB,YAAY,WAAW;EAErB,IAAI,OAAO,IAAI,OAAO,SAAS,YAAY,CAAC;OACvC,OAAO,SAAS,cAAc,MAAM,CAAC;EAC1C,OAAO,KAAK;EACZ,IAAI,MAAM;GACR,MAAM,OAAO,KACT,YAAY,OAAO,SAAS,OAAO,eACnC,mBAAmB,OAAO,YAAY,QAAQ,MAAM,EAAE,aAAa,OAAO,EAAE,OAAO;GACvF,QAAQ,OAAO,KAAK,QAAQ;EAC9B,CAAC;CACH,EACF,CAAC;CACD,cAAc,KAAK,QAAQ;EACzB,MAAM,MAAM,OAAO,OAAO,IAAI,UAAU,OAAO,IAAI,OAAO,GAAG;EAC7D,IAAI,UAAU,IAAI,QAAQ,IAAI,OAAO;EACrC,IAAI,IAAI,IAAI,IAAI;CAClB,CAAC,EAAE,OAAO,YAAY;EACpB,IAAI,MAAM;GACR,MAAM,0CAA0C,KAAK;GACrD,QAAQ;EACV,CAAC;CACH,CAAC;AACH;;;;;;AAOA,SAAS,cAAkC;CACzC,IAAI,CAAC,QAAQ,OAAO,OAAO,OAAO,KAAA;CAClC,MAAM,QAAQ,CAAC,QAAQ,IAAI;CAC3B,IAAI,QAAuB;CAC3B,IAAI,mBAAmB,QAAQ,GAAG,GAChC,IAAI;EAEF,QAAQ,WADM,aAAa,IAAI,IAAI,sBAAsB,OAAO,KAAK,GAAG,CACjD,EAAE,SAAS,QAAQ,GAAG,EAAE,OAAO,GAAG,CAAC;CAC5D,QAAQ;EACN,QAAQ;CACV;CAEF,OAAO,aAAa;EAAE;EAAO;EAAO,SAAS;CAAQ,CAAC;AACxD;AAEA,eAAe,OAAsB;CACnC,MAAM,MAAM,QAAQ,IAAI;CACxB,MAAM,SAAS,SAA2B;EAExC,CADe,KAAK,WAAW,QAAQ,QAAQ,SAAS,QAAQ,QACzD,MAAM,GAAG,KAAK,KAAK,GAAG;CAC/B;CACA,MAAM,UAAU,iBAAiB;CACjC,MAAM,SAAS,YAAY;CAC3B,MAAM,MAAkB;EACtB,IAAI,eAAe;EACnB,KAAK,SAAS,GAAG;EACjB;EACA,SAAS;EACT;EACA,GAAI,UAAU,EAAE,WAAW,QAAQ,IAAI,CAAC;EACxC,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC7B;CAGA,IAAI,QAAQ,KAAK,OAAO,OAAO;EAC7B,aAAa,GAAG;EAChB;CACF;CACA,MAAM,EAAE,aAAa,MAAM,YAAY,QAAQ,KAAK,MAAM,CAAC,GAAG,GAAG;CACjE,QAAQ,WAAW;AACrB;AAEA,KAAK"}
1
+ {"version":3,"file":"bin.js","names":["fsWatch"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The `mindees` executable — a thin adapter that wires real Node capabilities\n * (filesystem, environment probe, stdout/stderr, AI backend) into the tested\n * {@link runCliAsync} core. All logic lives in the core; this file only does I/O wiring.\n *\n * @module\n */\n\nimport {\n existsSync,\n watch as fsWatch,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { createServer } from 'node:http'\nimport { dirname, join, relative, sep } from 'node:path'\nimport process from 'node:process'\nimport type { AiBackend } from '@mindees/ai'\nimport { type AdapterName, createServerBackend, type FetchLike } from '@mindees/ai/server'\nimport { detectImageSupport, itermImage, renderBanner } from './banner'\nimport { type CliContext, runCliAsync } from './cli'\nimport { loadConfig } from './config'\nimport { startDev } from './dev'\nimport { createDevServer, createNodeWatcher, renderDevPage } from './dev-server'\nimport type { FileSystem } from './fs'\nimport { VERSION } from './index'\nimport type { EnvProbe, OutputLine } from './types'\n\n/** A `node:fs`-backed {@link FileSystem}. */\nfunction nodeFileSystem(): FileSystem {\n return {\n exists: (path) => existsSync(path),\n readFile: (path) => readFileSync(path, 'utf8'),\n writeFile: (path, contents) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, contents, 'utf8')\n },\n mkdir: (path) => {\n mkdirSync(path, { recursive: true })\n },\n readDir: (dir) => {\n const walk = (current: string, acc: string[]): string[] => {\n if (!existsSync(current)) return acc\n for (const entry of readdirSync(current)) {\n const full = join(current, entry)\n if (statSync(full).isDirectory()) walk(full, acc)\n else acc.push(relative(dir, full).split(sep).join('/'))\n }\n return acc\n }\n return walk(dir, []).sort()\n },\n }\n}\n\n/** Probe the real environment for `doctor`/`info`. */\nfunction probeEnv(cwd: string): EnvProbe {\n const pmSpec = process.env.npm_config_user_agent ?? ''\n // user agent looks like \"pnpm/11.5.0 npm/? node/v24 ...\".\n const pmMatch = pmSpec.match(/^(\\w+)\\/(\\S+)/)\n return {\n nodeVersion: process.version,\n packageManager: pmMatch?.[1] && pmMatch[2] ? { name: pmMatch[1], version: pmMatch[2] } : null,\n hasPackageJson: existsSync(join(cwd, 'package.json')),\n hasNodeModules: existsSync(join(cwd, 'node_modules')),\n }\n}\n\n/** Build a server AI backend from `MINDEES_AI_*` env, or `undefined` if not configured. */\nfunction aiBackendFromEnv(): AiBackend | undefined {\n const baseUrl = process.env.MINDEES_AI_BASE_URL\n const model = process.env.MINDEES_AI_MODEL\n if (!baseUrl || !model) return undefined\n // Fail loud on a mistyped adapter rather than silently defaulting to openai (which would\n // build the wrong auth headers and yield confusing HTTP errors). Empty/unset → openai.\n const adapterEnv = process.env.MINDEES_AI_ADAPTER\n if (adapterEnv && adapterEnv !== 'openai' && adapterEnv !== 'anthropic') {\n process.stderr.write(\n `mindees: unknown MINDEES_AI_ADAPTER \"${adapterEnv}\" (expected \"openai\" or \"anthropic\"); AI backend not configured.\\n`,\n )\n return undefined\n }\n const adapter: AdapterName = adapterEnv === 'anthropic' ? 'anthropic' : 'openai'\n return createServerBackend({\n // The global `fetch` is structurally compatible at runtime; the minimal FetchLike\n // intentionally avoids the DOM lib, so cast rather than pull in those types.\n fetch: globalThis.fetch as unknown as FetchLike,\n baseUrl,\n model,\n adapter,\n ...(process.env.MINDEES_AI_API_KEY ? { apiKey: process.env.MINDEES_AI_API_KEY } : {}),\n })\n}\n\n/**\n * `mindees dev` — the long-running transport over the tested {@link startDev} orchestrator:\n * build + watch `src/`, serve a live-reload preview, and reload the browser on each rebuild. This\n * is the I/O glue; the watcher, server, and orchestrator it wires are unit-tested in their modules.\n */\nfunction runDevServer(ctx: CliContext): void {\n const port = Number(process.env.MINDEES_DEV_PORT ?? 3000) || 3000\n const server = createDevServer()\n // Collect the freshly-built `dist/` tree (recursive, POSIX-relative) to serve as the app.\n const collectDist = (dir = 'dist'): Record<string, string> => {\n if (!ctx.fs.exists(dir)) return {}\n const out: Record<string, string> = {}\n for (const rel of ctx.fs.readDir(dir)) out[rel] = ctx.fs.readFile(`${dir}/${rel}`)\n return out\n }\n const watcher = createNodeWatcher(['src'], {\n watch: (path, opts, listener) =>\n fsWatch(path, { recursive: opts.recursive ?? false }, (event, filename) =>\n listener(event, typeof filename === 'string' ? filename : null),\n ),\n })\n const config = loadConfig(ctx.fs, ctx.cwd)\n startDev(ctx.fs, watcher, {\n perf: config.perf ?? true, // perf-lint ON in dev (warnings surfaced in the rebuild console)\n ...(config.budget ? { budget: config.budget } : {}),\n onRebuild: (result) => {\n // Serve the built app on success; show the diagnostics overlay (at `/`) on failure.\n if (result.ok) server.setFiles(collectDist())\n else server.setError(renderDevPage(result))\n server.bump()\n ctx.write({\n text: result.ok\n ? `rebuilt: ${result.compiled.length} file(s) ok`\n : `rebuild failed: ${result.diagnostics.filter((d) => d.severity === 'error').length} error(s)`,\n stream: result.ok ? 'out' : 'err',\n })\n },\n })\n createServer((req, res) => {\n const out = server.handle(req.method ?? 'GET', req.url ?? '/')\n res.writeHead(out.status, out.headers)\n res.end(out.body)\n }).listen(port, () => {\n ctx.write({\n text: `mindees dev — serving http://localhost:${port} (live reload on)`,\n stream: 'out',\n })\n })\n}\n\n/**\n * Build the welcome banner for an interactive (TTY) session: the ANSI wordmark always, plus the\n * actual logo PNG inline on image-capable terminals (iTerm2 / WezTerm). Returns `undefined` when\n * stdout is piped (scripts get clean, parseable output).\n */\nfunction buildBanner(): string | undefined {\n if (!process.stdout.isTTY) return undefined\n const color = !process.env.NO_COLOR\n let image: string | null = null\n if (detectImageSupport(process.env)) {\n try {\n const bytes = readFileSync(new URL('../assets/logo.png', import.meta.url))\n image = itermImage(bytes.toString('base64'), { width: 14 })\n } catch {\n image = null // asset missing → wordmark only\n }\n }\n return renderBanner({ color, image, version: VERSION })\n}\n\nasync function main(): Promise<void> {\n const cwd = process.cwd()\n const write = (line: OutputLine): void => {\n const stream = line.stream === 'err' ? process.stderr : process.stdout\n stream.write(`${line.text}\\n`)\n }\n const backend = aiBackendFromEnv()\n const banner = buildBanner()\n const ctx: CliContext = {\n fs: nodeFileSystem(),\n env: probeEnv(cwd),\n cwd,\n version: VERSION,\n write,\n ...(backend ? { aiBackend: backend } : {}),\n ...(banner ? { banner } : {}),\n }\n // `dev` is a long-running transport (not a one-shot command), so it's wired here in the I/O entry\n // rather than the synchronous CLI dispatch. Everything else goes through the tested core.\n if (process.argv[2] === 'dev') {\n runDevServer(ctx)\n return\n }\n const { exitCode } = await runCliAsync(process.argv.slice(2), ctx)\n process.exitCode = exitCode\n}\n\nmain()\n"],"mappings":"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"}
package/dist/build.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FileSystem } from "./fs.js";
2
- import { Diagnostic, buildRouteManifest } from "@mindees/compiler";
2
+ import { BudgetOptions, Diagnostic, PerfLintOptions, buildRouteManifest } from "@mindees/compiler";
3
3
 
4
4
  //#region src/build.d.ts
5
5
  /** Options for {@link buildProject}. */
@@ -14,6 +14,17 @@ interface BuildOptions {
14
14
  html?: boolean;
15
15
  /** Title for the emitted `index.html`. Default `"Mindees App"`. */
16
16
  appName?: string;
17
+ /**
18
+ * Run the MDC perf-lint (build-time advice neither RN nor Flutter ships, e.g. `MDC_PERF_001`: a bare
19
+ * `.map()` re-mounts every row). Emits **warnings** only (never fails the build). `true` for defaults,
20
+ * or pass {@link PerfLintOptions} to tune. The CLI enables this by default.
21
+ */
22
+ perf?: boolean | PerfLintOptions;
23
+ /**
24
+ * Enforce a per-module performance budget (spec §12: "100% optimized, enforced"). A violation is an
25
+ * **error** that fails the build (non-zero exit). Opt-in via `mindees.config`.
26
+ */
27
+ budget?: BudgetOptions;
17
28
  }
18
29
  /** Result of a project build. */
19
30
  interface BuildResult {
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;;;;UAsGiB,YAAA;EAcA;EAZf,IAAA;;EAEA,MAAA;EAiB2B;EAf3B,SAAA;EAemB;EAbnB,IAAA;EAOA;EALA,OAAA;AAAA;;UAIe,WAAA;EACf,EAAA;EAM2B;EAJ3B,QAAA;EAMS;EAJT,WAAA,EAAa,UAAA;EAMb;EAJA,MAAA,GAAS,UAAA,QAAkB,kBAAA;EAIhB;EAFX,KAAA;IAAS,cAAA;IAAwB,aAAA;EAAA;EAgCmB;EA9BpD,WAAA;AAAA;;;;;;iBA8Bc,YAAA,CAAa,EAAA,EAAI,UAAA,EAAY,OAAA,GAAS,YAAA,GAAoB,WAAA"}
1
+ {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;;;;UA+GiB,YAAA;EAqBf;EAnBA,IAAA;EAmBsB;EAjBtB,MAAA;EAqBe;EAnBf,SAAA;;EAEA,IAAA;EAwB2B;EAtB3B,OAAA;EAsBmB;;;;;EAhBnB,IAAA,aAAiB,eAAA;EAgBjB;;;;EAXA,MAAA,GAAS,aAAa;AAAA;;UAIP,WAAA;EACf,EAAA;EAwCc;EAtCd,QAAA;;EAEA,WAAA,EAAa,UAAA;EAoCuC;EAlCpD,MAAA,GAAS,UAAA,QAAkB,kBAAA;EAkCwD;EAhCnF,KAAA;IAAS,cAAA;IAAwB,aAAA;EAAA;EAgCU;EA9B3C,WAAA;AAAA;AA8BmF;;;;;AAAA,iBAArE,YAAA,CAAa,EAAA,EAAI,UAAA,EAAY,OAAA,GAAS,YAAA,GAAoB,WAAA"}
package/dist/build.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { VERSION } from "./version.js";
2
- import { buildRouteManifest, compile, typecheck } from "@mindees/compiler";
2
+ import { buildRouteManifest, checkBudget, compile, perfLint, typecheck } from "@mindees/compiler";
3
3
  //#region src/build.ts
4
4
  /**
5
5
  * `buildProject` — compile a project's sources with the Mindees Compiler.
@@ -108,7 +108,7 @@ function relativePath(fromDir, toPath) {
108
108
  * errors but collects diagnostics from all modules so the report is complete.
109
109
  */
110
110
  function buildProject(fs, options = {}) {
111
- const { root = ".", outDir = "dist", sourceMap = true, html = true, appName = "Mindees App" } = options;
111
+ const { root = ".", outDir = "dist", sourceMap = true, html = true, appName = "Mindees App", perf = false, budget } = options;
112
112
  const srcDir = root === "." ? "src" : `${root}/src`;
113
113
  const diagnostics = [];
114
114
  const compiled = [];
@@ -132,6 +132,11 @@ function buildProject(fs, options = {}) {
132
132
  fileName: rel,
133
133
  sourceMap
134
134
  });
135
+ if (perf) diagnostics.push(...perfLint(source, rel, typeof perf === "object" ? perf : {}));
136
+ if (budget) for (const d of checkBudget(emitted, budget)) diagnostics.push({
137
+ ...d,
138
+ file: rel
139
+ });
135
140
  stats.flattenedNodes += emitted.stats.flattenedNodes;
136
141
  stats.totalElements += emitted.stats.totalElements;
137
142
  const outPath = `${outDir}/${rel.replace(COMPILABLE, ".js")}`;
package/dist/build.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","names":[],"sources":["../src/build.ts"],"sourcesContent":["/**\n * `buildProject` — compile a project's sources with the Mindees Compiler.\n *\n * Walks `src/**` via an injected {@link FileSystem}, runs each TS/TSX module\n * through `@mindees/compiler`'s `typecheck` gate and then `compile` (emit),\n * writes the JS (and source map) to `dist/`, and — if a `src/routes/` dir\n * exists — emits a per-route manifest. Returns structured results so the CLI can\n * report diagnostics and fail the build on type errors.\n *\n * @module\n */\n\nimport { buildRouteManifest, compile, type Diagnostic, typecheck } from '@mindees/compiler'\nimport type { FileSystem } from './fs'\nimport { VERSION } from './version'\n\n/** Runtime packages a web app imports — mapped in the emitted index.html's import-map (to the esm.sh CDN). */\nconst WEB_RUNTIME_PACKAGES = [\n 'core',\n 'renderer',\n 'router',\n 'atlas',\n 'data',\n 'updates',\n 'ai',\n] as const\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/**\n * The runnable HTML shell. Loads the compiled entry as a native ES module; the bare `@mindees/*`\n * specifiers in the compiled output resolve via the import-map to the published packages on the esm.sh\n * CDN (which serves their transitive graph), so the app runs in a browser with no bundler step. Both the\n * bare name and a trailing-slash mapping are emitted so subpath imports (e.g. `@mindees/atlas/list`) work.\n */\nfunction renderIndexHtml(appName: string, entry: string, version: string): string {\n const imports = WEB_RUNTIME_PACKAGES.flatMap((p) => [\n ` \"@mindees/${p}\": \"https://esm.sh/@mindees/${p}@${version}\"`,\n ` \"@mindees/${p}/\": \"https://esm.sh/@mindees/${p}@${version}/\"`,\n ]).join(',\\n')\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${escapeHtml(appName)}</title>\n <script type=\"importmap\">\n{\n \"imports\": {\n${imports}\n }\n}\n </script>\n </head>\n <body>\n <div id=\"app\"></div>\n <script type=\"module\" src=\"./${entry}\"></script>\n </body>\n</html>\n`\n}\n\n/**\n * Add explicit `.js` extensions to RELATIVE import/export specifiers in emitted code. The compiler emits\n * `from './App'` (TS preserves the extensionless source specifier), which a browser's native ESM loader\n * cannot resolve. Bare specifiers (`@mindees/*`) are left untouched (the import-map resolves them). Only\n * touches `./`/`../` specifiers that lack a file extension. (Minor source-map drift on import lines only.)\n */\nfunction rewriteRelativeImports(code: string): string {\n const addExt = (spec: string): string => (/\\.[a-zA-Z0-9]+$/.test(spec) ? spec : `${spec}.js`)\n return code.replace(\n /(\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\bimport\\s+)(['\"])(\\.\\.?\\/[^'\"]*)\\2/g,\n (_m, pre: string, q: string, spec: string) => `${pre}${q}${addExt(spec)}${q}`,\n )\n}\n\n/**\n * Diagnostic codes that are artifacts of type-checking a module **in isolation**\n * (without the project's dependency graph or ambient JSX types), not real app\n * errors. If any ever reaches the build it is **downgraded to a warning** rather\n * than failing the build:\n *\n * - `TS2307` — \"Cannot find module '…'\": imports aren't resolved in single-module mode.\n * - `TS7026` — \"no interface 'JSX.IntrinsicElements'\": the framework's JSX env\n * types aren't loaded in isolation.\n *\n * In practice the compiler's single-module gate already **filters** these upstream\n * (it drops unresolved-import codes and injects ambient JSX types), so they\n * normally don't appear here at all — this set is a defensive backstop in case a\n * future gate surfaces them. Genuine type errors (e.g. `TS2322` not-assignable)\n * are untouched and still fail the build. A full project-graph type-check is\n * future work (see ROADMAP).\n */\nconst ISOLATION_NOISE = new Set(['TS2307', 'TS7026'])\n\n/** Options for {@link buildProject}. */\nexport interface BuildOptions {\n /** Project root (contains `src/`). Default `\".\"`. */\n root?: string\n /** Output directory. Default `\"dist\"`. */\n outDir?: string\n /** Emit source maps. Default `true`. */\n sourceMap?: boolean\n /** Emit a runnable `index.html` (web target) when an app entry compiled. Default `true`. */\n html?: boolean\n /** Title for the emitted `index.html`. Default `\"Mindees App\"`. */\n appName?: string\n}\n\n/** Result of a project build. */\nexport interface BuildResult {\n ok: boolean\n /** Source files compiled (relative paths), sorted. */\n compiled: string[]\n /** All diagnostics across modules (errors fail the build). */\n diagnostics: Diagnostic[]\n /** Route manifest, if `src/routes/` existed. */\n routes?: ReturnType<typeof buildRouteManifest>\n /** Optimizer totals across all modules. */\n stats: { flattenedNodes: number; totalElements: number }\n /** Whether a runnable `index.html` was emitted (an app entry `src/main.{tsx,ts}` compiled). */\n htmlEmitted?: boolean\n}\n\nconst COMPILABLE = /\\.(tsx|ts)$/\nconst DECLARATION = /\\.d\\.ts$/\n\n/** The directory portion of a POSIX path (`a/b/c.js` → `a/b`; no slash → `''`). */\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n/** The final segment of a POSIX path (`a/b/c.js` → `c.js`). */\nfunction baseOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? path : path.slice(i + 1)\n}\n/** Relative POSIX path from `fromDir` to `toPath` (e.g. `dist/routes` → `src/routes/x.tsx`). */\nfunction relativePath(fromDir: string, toPath: string): string {\n const from = fromDir.split('/').filter(Boolean)\n const to = toPath.split('/').filter(Boolean)\n let i = 0\n while (i < from.length && i < to.length && from[i] === to[i]) i++\n return [...Array(from.length - i).fill('..'), ...to.slice(i)].join('/') || '.'\n}\n\n/**\n * Build the project at `root`. Compiles every `src/**\\/*.{ts,tsx}` (except\n * `.d.ts`) and writes outputs to `outDir`. Stops emitting a module on type\n * errors but collects diagnostics from all modules so the report is complete.\n */\nexport function buildProject(fs: FileSystem, options: BuildOptions = {}): BuildResult {\n const {\n root = '.',\n outDir = 'dist',\n sourceMap = true,\n html = true,\n appName = 'Mindees App',\n } = options\n const srcDir = root === '.' ? 'src' : `${root}/src`\n\n const diagnostics: Diagnostic[] = []\n const compiled: string[] = []\n const stats = { flattenedNodes: 0, totalElements: 0 }\n const writtenOutPaths = new Map<string, string>() // outPath → the rel that emitted it (collision guard)\n\n const entries = fs.exists(srcDir) ? fs.readDir(srcDir) : []\n for (const rel of entries) {\n if (!COMPILABLE.test(rel) || DECLARATION.test(rel)) continue\n const srcPath = `${srcDir}/${rel}`\n const source = fs.readFile(srcPath)\n\n // Type-check for diagnostics, but downgrade isolation-only noise to warnings\n // so a normal app (with cross-module imports + JSX) still builds. Genuine\n // type errors remain errors and fail the build below.\n const moduleDiags = typecheck(source, rel).map((d) =>\n d.severity === 'error' && ISOLATION_NOISE.has(d.code)\n ? { ...d, severity: 'warning' as const }\n : d,\n )\n diagnostics.push(...moduleDiags)\n\n // Emit only if this module has no real (post-downgrade) error.\n const moduleHasError = moduleDiags.some((d) => d.severity === 'error')\n if (!moduleHasError) {\n const emitted = compile(source, { fileName: rel, sourceMap })\n stats.flattenedNodes += emitted.stats.flattenedNodes\n stats.totalElements += emitted.stats.totalElements\n const outPath = `${outDir}/${rel.replace(COMPILABLE, '.js')}`\n // Two sources whose basenames differ only by extension (App.ts + App.tsx) map to one dist/App.js\n // — fail loudly instead of silently overwriting one with the other.\n const collidedWith = writtenOutPaths.get(outPath)\n if (collidedWith !== undefined) {\n diagnostics.push({\n severity: 'error',\n code: 'MDC_OUTPUT_COLLISION',\n message: `Output collision: \"${rel}\" and \"${collidedWith}\" both emit \"${outPath}\". Rename one.`,\n })\n continue\n }\n writtenOutPaths.set(outPath, rel)\n let code = emitted.code\n if (emitted.map) {\n // Rewrite the map so `sources` resolves to the real src/ file (TS emits a bare basename that\n // resolves to a non-existent dist/*.tsx), and point the sourceMappingURL comment at the LITERAL\n // .map filename (TS percent-encodes special chars like `[ ]`, which won't match the written file).\n let mapText = emitted.map\n try {\n const map = JSON.parse(emitted.map) as { sources?: string[]; sourceRoot?: string }\n map.sources = [relativePath(dirOf(outPath), `${srcDir}/${rel}`)]\n map.sourceRoot = ''\n mapText = JSON.stringify(map)\n } catch {\n // leave the map untouched if it isn't parseable (shouldn't happen)\n }\n fs.writeFile(`${outPath}.map`, mapText)\n code = code.replace(\n /\\/\\/# sourceMappingURL=.*$/m,\n `//# sourceMappingURL=${baseOf(outPath)}.map`,\n )\n }\n // Make relative specifiers browser-resolvable (`./App` → `./App.js`) so the output runs as native ESM.\n fs.writeFile(outPath, rewriteRelativeImports(code))\n compiled.push(rel)\n }\n }\n\n // Per-route manifest, if a routes dir is present. buildRouteManifest THROWS on\n // a malformed routes dir (duplicate route path, or a non-terminal catch-all) —\n // ordinary user misconfigurations that `mindees build` exists to report. Turn\n // them into a build diagnostic + failing result so the CLI prints a clean error\n // and exits non-zero, honoring runCli's \"never throws for expected failures\"\n // contract instead of crashing with a raw stack trace.\n let routes: BuildResult['routes']\n const routesDir = `${srcDir}/routes`\n if (fs.exists(routesDir)) {\n try {\n // Only manifest routes the build actually COMPILES (.tsx/.ts) — buildRouteManifest otherwise\n // accepts .jsx/.js too, which the compile loop skips, leaving a manifest entry with no emitted chunk.\n routes = buildRouteManifest(fs.readDir(routesDir).filter((f) => COMPILABLE.test(f)))\n fs.writeFile(`${outDir}/routes.manifest.json`, `${JSON.stringify(routes, null, 2)}\\n`)\n } catch (e) {\n diagnostics.push({\n severity: 'error',\n code: 'MDC_ROUTES',\n message: e instanceof Error ? e.message : String(e),\n file: routesDir,\n })\n }\n }\n\n // Emit a runnable index.html when an app entry (`src/main.{tsx,ts}` → `dist/main.js`) compiled, so\n // `mindees build`/`dev` produce something that actually renders in a browser (import-map → CDN; no bundler).\n let htmlEmitted = false\n if (html && writtenOutPaths.has(`${outDir}/main.js`)) {\n fs.writeFile(`${outDir}/index.html`, renderIndexHtml(appName, 'main.js', VERSION))\n htmlEmitted = true\n }\n\n compiled.sort()\n const ok = !diagnostics.some((d) => d.severity === 'error')\n const result: BuildResult = { ok, compiled, diagnostics, stats }\n if (routes) result.routes = routes\n if (htmlEmitted) result.htmlEmitted = true\n return result\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build.js","names":[],"sources":["../src/build.ts"],"sourcesContent":["/**\n * `buildProject` — compile a project's sources with the Mindees Compiler.\n *\n * Walks `src/**` via an injected {@link FileSystem}, runs each TS/TSX module\n * through `@mindees/compiler`'s `typecheck` gate and then `compile` (emit),\n * writes the JS (and source map) to `dist/`, and — if a `src/routes/` dir\n * exists — emits a per-route manifest. Returns structured results so the CLI can\n * report diagnostics and fail the build on type errors.\n *\n * @module\n */\n\nimport {\n type BudgetOptions,\n buildRouteManifest,\n checkBudget,\n compile,\n type Diagnostic,\n type PerfLintOptions,\n perfLint,\n typecheck,\n} from '@mindees/compiler'\nimport type { FileSystem } from './fs'\nimport { VERSION } from './version'\n\n/** Runtime packages a web app imports — mapped in the emitted index.html's import-map (to the esm.sh CDN). */\nconst WEB_RUNTIME_PACKAGES = [\n 'core',\n 'renderer',\n 'router',\n 'atlas',\n 'data',\n 'updates',\n 'ai',\n] as const\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/**\n * The runnable HTML shell. Loads the compiled entry as a native ES module; the bare `@mindees/*`\n * specifiers in the compiled output resolve via the import-map to the published packages on the esm.sh\n * CDN (which serves their transitive graph), so the app runs in a browser with no bundler step. Both the\n * bare name and a trailing-slash mapping are emitted so subpath imports (e.g. `@mindees/atlas/list`) work.\n */\nfunction renderIndexHtml(appName: string, entry: string, version: string): string {\n const imports = WEB_RUNTIME_PACKAGES.flatMap((p) => [\n ` \"@mindees/${p}\": \"https://esm.sh/@mindees/${p}@${version}\"`,\n ` \"@mindees/${p}/\": \"https://esm.sh/@mindees/${p}@${version}/\"`,\n ]).join(',\\n')\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${escapeHtml(appName)}</title>\n <script type=\"importmap\">\n{\n \"imports\": {\n${imports}\n }\n}\n </script>\n </head>\n <body>\n <div id=\"app\"></div>\n <script type=\"module\" src=\"./${entry}\"></script>\n </body>\n</html>\n`\n}\n\n/**\n * Add explicit `.js` extensions to RELATIVE import/export specifiers in emitted code. The compiler emits\n * `from './App'` (TS preserves the extensionless source specifier), which a browser's native ESM loader\n * cannot resolve. Bare specifiers (`@mindees/*`) are left untouched (the import-map resolves them). Only\n * touches `./`/`../` specifiers that lack a file extension. (Minor source-map drift on import lines only.)\n */\nfunction rewriteRelativeImports(code: string): string {\n const addExt = (spec: string): string => (/\\.[a-zA-Z0-9]+$/.test(spec) ? spec : `${spec}.js`)\n return code.replace(\n /(\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\bimport\\s+)(['\"])(\\.\\.?\\/[^'\"]*)\\2/g,\n (_m, pre: string, q: string, spec: string) => `${pre}${q}${addExt(spec)}${q}`,\n )\n}\n\n/**\n * Diagnostic codes that are artifacts of type-checking a module **in isolation**\n * (without the project's dependency graph or ambient JSX types), not real app\n * errors. If any ever reaches the build it is **downgraded to a warning** rather\n * than failing the build:\n *\n * - `TS2307` — \"Cannot find module '…'\": imports aren't resolved in single-module mode.\n * - `TS7026` — \"no interface 'JSX.IntrinsicElements'\": the framework's JSX env\n * types aren't loaded in isolation.\n *\n * In practice the compiler's single-module gate already **filters** these upstream\n * (it drops unresolved-import codes and injects ambient JSX types), so they\n * normally don't appear here at all — this set is a defensive backstop in case a\n * future gate surfaces them. Genuine type errors (e.g. `TS2322` not-assignable)\n * are untouched and still fail the build. A full project-graph type-check is\n * future work (see ROADMAP).\n */\nconst ISOLATION_NOISE = new Set(['TS2307', 'TS7026'])\n\n/** Options for {@link buildProject}. */\nexport interface BuildOptions {\n /** Project root (contains `src/`). Default `\".\"`. */\n root?: string\n /** Output directory. Default `\"dist\"`. */\n outDir?: string\n /** Emit source maps. Default `true`. */\n sourceMap?: boolean\n /** Emit a runnable `index.html` (web target) when an app entry compiled. Default `true`. */\n html?: boolean\n /** Title for the emitted `index.html`. Default `\"Mindees App\"`. */\n appName?: string\n /**\n * Run the MDC perf-lint (build-time advice neither RN nor Flutter ships, e.g. `MDC_PERF_001`: a bare\n * `.map()` re-mounts every row). Emits **warnings** only (never fails the build). `true` for defaults,\n * or pass {@link PerfLintOptions} to tune. The CLI enables this by default.\n */\n perf?: boolean | PerfLintOptions\n /**\n * Enforce a per-module performance budget (spec §12: \"100% optimized, enforced\"). A violation is an\n * **error** that fails the build (non-zero exit). Opt-in via `mindees.config`.\n */\n budget?: BudgetOptions\n}\n\n/** Result of a project build. */\nexport interface BuildResult {\n ok: boolean\n /** Source files compiled (relative paths), sorted. */\n compiled: string[]\n /** All diagnostics across modules (errors fail the build). */\n diagnostics: Diagnostic[]\n /** Route manifest, if `src/routes/` existed. */\n routes?: ReturnType<typeof buildRouteManifest>\n /** Optimizer totals across all modules. */\n stats: { flattenedNodes: number; totalElements: number }\n /** Whether a runnable `index.html` was emitted (an app entry `src/main.{tsx,ts}` compiled). */\n htmlEmitted?: boolean\n}\n\nconst COMPILABLE = /\\.(tsx|ts)$/\nconst DECLARATION = /\\.d\\.ts$/\n\n/** The directory portion of a POSIX path (`a/b/c.js` → `a/b`; no slash → `''`). */\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n/** The final segment of a POSIX path (`a/b/c.js` → `c.js`). */\nfunction baseOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? path : path.slice(i + 1)\n}\n/** Relative POSIX path from `fromDir` to `toPath` (e.g. `dist/routes` → `src/routes/x.tsx`). */\nfunction relativePath(fromDir: string, toPath: string): string {\n const from = fromDir.split('/').filter(Boolean)\n const to = toPath.split('/').filter(Boolean)\n let i = 0\n while (i < from.length && i < to.length && from[i] === to[i]) i++\n return [...Array(from.length - i).fill('..'), ...to.slice(i)].join('/') || '.'\n}\n\n/**\n * Build the project at `root`. Compiles every `src/**\\/*.{ts,tsx}` (except\n * `.d.ts`) and writes outputs to `outDir`. Stops emitting a module on type\n * errors but collects diagnostics from all modules so the report is complete.\n */\nexport function buildProject(fs: FileSystem, options: BuildOptions = {}): BuildResult {\n const {\n root = '.',\n outDir = 'dist',\n sourceMap = true,\n html = true,\n appName = 'Mindees App',\n perf = false,\n budget,\n } = options\n const srcDir = root === '.' ? 'src' : `${root}/src`\n\n const diagnostics: Diagnostic[] = []\n const compiled: string[] = []\n const stats = { flattenedNodes: 0, totalElements: 0 }\n const writtenOutPaths = new Map<string, string>() // outPath → the rel that emitted it (collision guard)\n\n const entries = fs.exists(srcDir) ? fs.readDir(srcDir) : []\n for (const rel of entries) {\n if (!COMPILABLE.test(rel) || DECLARATION.test(rel)) continue\n const srcPath = `${srcDir}/${rel}`\n const source = fs.readFile(srcPath)\n\n // Type-check for diagnostics, but downgrade isolation-only noise to warnings\n // so a normal app (with cross-module imports + JSX) still builds. Genuine\n // type errors remain errors and fail the build below.\n const moduleDiags = typecheck(source, rel).map((d) =>\n d.severity === 'error' && ISOLATION_NOISE.has(d.code)\n ? { ...d, severity: 'warning' as const }\n : d,\n )\n diagnostics.push(...moduleDiags)\n\n // Emit only if this module has no real (post-downgrade) error.\n const moduleHasError = moduleDiags.some((d) => d.severity === 'error')\n if (!moduleHasError) {\n const emitted = compile(source, { fileName: rel, sourceMap })\n // Flagship build-time DX: perf-lint (warnings — advice, never blocks) + an enforced perf budget\n // (errors — fails the build). Previously wired only into compiler unit tests; now reachable from\n // `mindees build`/`dev` so the \"100% optimized, enforced\" claim is real, not aspirational.\n if (perf) diagnostics.push(...perfLint(source, rel, typeof perf === 'object' ? perf : {}))\n if (budget) {\n for (const d of checkBudget(emitted, budget)) diagnostics.push({ ...d, file: rel })\n }\n stats.flattenedNodes += emitted.stats.flattenedNodes\n stats.totalElements += emitted.stats.totalElements\n const outPath = `${outDir}/${rel.replace(COMPILABLE, '.js')}`\n // Two sources whose basenames differ only by extension (App.ts + App.tsx) map to one dist/App.js\n // — fail loudly instead of silently overwriting one with the other.\n const collidedWith = writtenOutPaths.get(outPath)\n if (collidedWith !== undefined) {\n diagnostics.push({\n severity: 'error',\n code: 'MDC_OUTPUT_COLLISION',\n message: `Output collision: \"${rel}\" and \"${collidedWith}\" both emit \"${outPath}\". Rename one.`,\n })\n continue\n }\n writtenOutPaths.set(outPath, rel)\n let code = emitted.code\n if (emitted.map) {\n // Rewrite the map so `sources` resolves to the real src/ file (TS emits a bare basename that\n // resolves to a non-existent dist/*.tsx), and point the sourceMappingURL comment at the LITERAL\n // .map filename (TS percent-encodes special chars like `[ ]`, which won't match the written file).\n let mapText = emitted.map\n try {\n const map = JSON.parse(emitted.map) as { sources?: string[]; sourceRoot?: string }\n map.sources = [relativePath(dirOf(outPath), `${srcDir}/${rel}`)]\n map.sourceRoot = ''\n mapText = JSON.stringify(map)\n } catch {\n // leave the map untouched if it isn't parseable (shouldn't happen)\n }\n fs.writeFile(`${outPath}.map`, mapText)\n code = code.replace(\n /\\/\\/# sourceMappingURL=.*$/m,\n `//# sourceMappingURL=${baseOf(outPath)}.map`,\n )\n }\n // Make relative specifiers browser-resolvable (`./App` → `./App.js`) so the output runs as native ESM.\n fs.writeFile(outPath, rewriteRelativeImports(code))\n compiled.push(rel)\n }\n }\n\n // Per-route manifest, if a routes dir is present. buildRouteManifest THROWS on\n // a malformed routes dir (duplicate route path, or a non-terminal catch-all) —\n // ordinary user misconfigurations that `mindees build` exists to report. Turn\n // them into a build diagnostic + failing result so the CLI prints a clean error\n // and exits non-zero, honoring runCli's \"never throws for expected failures\"\n // contract instead of crashing with a raw stack trace.\n let routes: BuildResult['routes']\n const routesDir = `${srcDir}/routes`\n if (fs.exists(routesDir)) {\n try {\n // Only manifest routes the build actually COMPILES (.tsx/.ts) — buildRouteManifest otherwise\n // accepts .jsx/.js too, which the compile loop skips, leaving a manifest entry with no emitted chunk.\n routes = buildRouteManifest(fs.readDir(routesDir).filter((f) => COMPILABLE.test(f)))\n fs.writeFile(`${outDir}/routes.manifest.json`, `${JSON.stringify(routes, null, 2)}\\n`)\n } catch (e) {\n diagnostics.push({\n severity: 'error',\n code: 'MDC_ROUTES',\n message: e instanceof Error ? e.message : String(e),\n file: routesDir,\n })\n }\n }\n\n // Emit a runnable index.html when an app entry (`src/main.{tsx,ts}` → `dist/main.js`) compiled, so\n // `mindees build`/`dev` produce something that actually renders in a browser (import-map → CDN; no bundler).\n let htmlEmitted = false\n if (html && writtenOutPaths.has(`${outDir}/main.js`)) {\n fs.writeFile(`${outDir}/index.html`, renderIndexHtml(appName, 'main.js', VERSION))\n htmlEmitted = true\n }\n\n compiled.sort()\n const ok = !diagnostics.some((d) => d.severity === 'error')\n const result: BuildResult = { ok, compiled, diagnostics, stats }\n if (routes) result.routes = routes\n if (htmlEmitted) result.htmlEmitted = true\n return result\n}\n"],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";;;;;;UAwBiB,UAAA;EACf,EAAA,EAAI,UAAA;EACJ,GAAA,EAAK,QAAA;EAAA;EAEL,GAAA;EAEA;EAAA,OAAA;EAEO;EAAP,KAAA,EAAO,MAAA;EAEK;EAAZ,SAAA,GAAY,SAAA;EAEN;EAAN,MAAA;AAAA;;;;;iBAyCc,MAAA,CAAO,IAAA,qBAAyB,GAAA,EAAK,UAAA,GAAa,aAAa;;;;AAAA;AAoC/E;iBAAgB,WAAA,CAAY,IAAA,qBAAyB,GAAA,EAAK,UAAA,GAAa,OAAA,CAAQ,aAAA"}
1
+ {"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";;;;;;UAyBiB,UAAA;EACf,EAAA,EAAI,UAAA;EACJ,GAAA,EAAK,QAAA;EAAA;EAEL,GAAA;EAEA;EAAA,OAAA;EAEO;EAAP,KAAA,EAAO,MAAA;EAEK;EAAZ,SAAA,GAAY,SAAA;EAEN;EAAN,MAAA;AAAA;;;;;iBAyCc,MAAA,CAAO,IAAA,qBAAyB,GAAA,EAAK,UAAA,GAAa,aAAa;;;;AAAA;AAoC/E;iBAAgB,WAAA,CAAY,IAAA,qBAAyB,GAAA,EAAK,UAAA,GAAa,OAAA,CAAQ,aAAA"}
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { runAiCommand } from "./ai.js";
2
2
  import { buildProject } from "./build.js";
3
+ import { loadConfig } from "./config.js";
3
4
  import { quoteShellPath, resolveCreateTarget } from "./create-target.js";
4
5
  import { doctorSummary, renderDoctor, runDoctor } from "./doctor.js";
5
6
  import { templateNames } from "./templates.js";
@@ -161,10 +162,14 @@ function cmdBuild(args, ctx) {
161
162
  "no-source-map": { type: "boolean" }
162
163
  }
163
164
  });
165
+ const config = loadConfig(ctx.fs, ctx.cwd);
164
166
  const result = buildProject(ctx.fs, {
165
167
  root: ctx.cwd,
166
168
  outDir: typeof values["out-dir"] === "string" ? values["out-dir"] : `${ctx.cwd}/dist`,
167
- sourceMap: values["no-source-map"] !== true
169
+ sourceMap: values["no-source-map"] !== true,
170
+ perf: config.perf ?? true,
171
+ ...config.budget ? { budget: config.budget } : {},
172
+ ...config.appName ? { appName: config.appName } : {}
168
173
  });
169
174
  for (const d of result.diagnostics) {
170
175
  const where = d.file ? `${d.file}${d.position ? `:${d.position.line}` : ""}: ` : "";
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 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":"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"}
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 { loadConfig } from './config'\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 config = loadConfig(ctx.fs, ctx.cwd)\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 perf: config.perf ?? true, // perf-lint ON by default (warnings, never blocks)\n ...(config.budget ? { budget: config.budget } : {}),\n ...(config.appName ? { appName: config.appName } : {}),\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":"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"}
package/dist/config.js ADDED
@@ -0,0 +1,21 @@
1
+ //#region src/config.ts
2
+ /** The config filename, resolved against the project root. */
3
+ const CONFIG_FILE = "mindees.config.json";
4
+ /**
5
+ * Load {@link CONFIG_FILE} from `root`. Returns `{}` when absent or unparseable (never throws), so the
6
+ * build always proceeds with defaults.
7
+ */
8
+ function loadConfig(fs, root) {
9
+ const path = root === "." ? CONFIG_FILE : `${root}/${CONFIG_FILE}`;
10
+ if (!fs.exists(path)) return {};
11
+ try {
12
+ const parsed = JSON.parse(fs.readFile(path));
13
+ return parsed !== null && typeof parsed === "object" ? parsed : {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+ //#endregion
19
+ export { loadConfig };
20
+
21
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * `mindees.config.json` — optional per-project build configuration.\n *\n * Pure over an injected {@link FileSystem} so it's testable. Tolerant by design: a missing or malformed\n * config never throws and never breaks the build (it degrades to defaults) — the CLI's \"never throws for\n * expected failures\" contract.\n *\n * @module\n */\n\nimport type { BudgetOptions, PerfLintOptions } from '@mindees/compiler'\nimport type { FileSystem } from './fs'\n\n/** Shape of `mindees.config.json`. All fields optional. */\nexport interface MindeesConfig {\n /** Perf-lint: `true`/`false`, or {@link PerfLintOptions} to tune. CLI default is `true` (warnings). */\n perf?: boolean | PerfLintOptions\n /** Enforce a per-module performance budget (violations fail the build). Opt-in. */\n budget?: BudgetOptions\n /** Title for the emitted `index.html`. */\n appName?: string\n}\n\n/** The config filename, resolved against the project root. */\nexport const CONFIG_FILE = 'mindees.config.json'\n\n/**\n * Load {@link CONFIG_FILE} from `root`. Returns `{}` when absent or unparseable (never throws), so the\n * build always proceeds with defaults.\n */\nexport function loadConfig(fs: FileSystem, root: string): MindeesConfig {\n const path = root === '.' ? CONFIG_FILE : `${root}/${CONFIG_FILE}`\n if (!fs.exists(path)) return {}\n try {\n const parsed: unknown = JSON.parse(fs.readFile(path))\n return parsed !== null && typeof parsed === 'object' ? (parsed as MindeesConfig) : {}\n } catch {\n return {}\n }\n}\n"],"mappings":";;AAwBA,MAAa,cAAc;;;;;AAM3B,SAAgB,WAAW,IAAgB,MAA6B;CACtE,MAAM,OAAO,SAAS,MAAM,cAAc,GAAG,KAAK,GAAG;CACrD,IAAI,CAAC,GAAG,OAAO,IAAI,GAAG,OAAO,CAAC;CAC9B,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,GAAG,SAAS,IAAI,CAAC;EACpD,OAAO,WAAW,QAAQ,OAAO,WAAW,WAAY,SAA2B,CAAC;CACtF,QAAQ;EACN,OAAO,CAAC;CACV;AACF"}
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.23.0";
14
+ declare const VERSION = "0.25.0";
15
15
  //#endregion
16
16
  export { VERSION };
17
17
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * @module
12
12
  */
13
13
  /** The package version. All `@mindees/*` packages share one locked version line. */
14
- const VERSION = "0.23.0";
14
+ const VERSION = "0.25.0";
15
15
  //#endregion
16
16
  export { VERSION };
17
17
 
@@ -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.23.0'\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,UAAU"}
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.25.0'\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,UAAU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/cli",
3
- "version": "0.23.0",
3
+ "version": "0.25.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.23.0",
31
- "@mindees/compiler": "0.23.0",
32
- "@mindees/ai": "0.23.0"
30
+ "@mindees/core": "0.25.0",
31
+ "@mindees/compiler": "0.25.0",
32
+ "@mindees/ai": "0.25.0"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsdown",