No description
Find a file
Dunemask 63686c3720 [FEATURE] show the DevShell in the README
Adds a DevShell section with four captures from examples/todo — device
switching across iOS/Android/desktop and the console tab — plus the CDP
capture script that regenerates them, so they track UI changes instead of
going stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 17:01:38 -06:00
.claude [FEATURE] Initial Demo 2026-05-06 17:27:54 -06:00
demo/web-vs-native [FIX] Example/demo configs to proxy port 52100 2026-07-14 15:42:05 -06:00
docs [FEATURE] show the DevShell in the README 2026-07-31 17:01:38 -06:00
examples/todo [FIX] Example/demo configs to proxy port 52100 2026-07-14 15:42:05 -06:00
packages/vite-plugin [FIX] DevShell real-device overlay must be backend-agnostic 2026-07-27 12:29:25 -06:00
scripts [FEATURE] show the DevShell in the README 2026-07-31 17:01:38 -06:00
.gitignore [CHORE] Minor adjustments 2026-05-07 11:48:42 -06:00
biome.json [CHORE] slim root scripts; menu-driven sub-clis; biome + tsgo (#1) 2026-05-07 00:09:33 +00:00
bun.lock [FEATURE] Neutral Plugins (#5) 2026-05-14 16:26:33 +00:00
package.json [CHORE] slim root scripts; menu-driven sub-clis; biome + tsgo (#1) 2026-05-07 00:09:33 +00:00
README.md [FEATURE] show the DevShell in the README 2026-07-31 17:01:38 -06:00
tsconfig.base.json [FEATURE] wire VitePWA + workbox-window into todo example + web-vs-native demo 2026-05-07 13:03:51 -06:00

lion-turtle

Mobile-shell platform. Flutter native shell wraps any Vite + React app via a single plugin. Local-first storage with sync hooks. Browser-side dev mock so you can iterate without rebuilding the native shell. Full Flutter control + log viewer + tunnel + device picker built into the dev tooling.

DevShell

The browser-side mock. Same running app, retargeted per device — no native rebuild, no reinstall.

DevShell running the todo example in an iPhone 15 Pro frame The same app reframed as a Pixel 9 Pro
iOS — iPhone 15 Pro. Logical viewport 393×852 with the Dynamic Island cutout. Picking a device also sets the spoofed platform, so isNative() / getPlatform() branches run in the browser. Android — Pixel 9 Pro. One click reframes to 412×915 and swaps the spoofed platform. Same process, same state.
DevShell in desktop mode with the frame removed Console tab showing console output forwarded out of the iframe
Desktop. Frame drops away and the app fills the canvas. console tab. console.* forwarded out of the iframe via installLogPipe(), next to the bridge log of every shell↔app message.

Screens above are from examples/todo. Regenerate them with scripts/shoot-devshell.mjs — it drives headless Chrome over CDP against a running dev server, so the shots track UI changes instead of going stale.

Pieces

Package Purpose
@lion-turtle/vite Vite plugin (dev-proxy, DevShell iframe mock, web-side bridge runtime, Flutter control plane, env sniff, tunnel) + bundled Flutter scaffold template. Bridge protocol + store + extension types re-exported from @lion-turtle/vite/runtime.

Layout

demo/                  spike — native widgets vs React-in-WebView, no server, no auth
  web/ + shell/
examples/
  todo/                full reference app — auth, offline-first, sync, prod build
    web/ + server/ + shell-app/  + dev.ts + build.ts
docs/                  AI-agent docs (read first)
packages/              @lion-turtle/vite

Quick start (consumer)

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { lionTurtle } from '@lion-turtle/vite';

export default defineConfig({
  plugins: [
    react(),
    lionTurtle({
      devProxy: { preferredPort: 9999, portRange: 100 },
      devShell: { enabled: true, frame: 'iphone' },
      scaffold: { shellDir: '../shell-app' },
      flutter: {
        enabled: true,
        shellDir: '../shell-app',
        prodRootTarget: 'https://app.example.com',  // injected at build time
      },
    }),
  ],
});
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import {
  DevShell,
  ShellProvider,
  isNative,
  installDevShellControl,
  installLogPipe,
} from '@lion-turtle/vite/runtime';
import { App } from './App';

if (import.meta.env.DEV) {
  installLogPipe();
  installDevShellControl();
}

const params = new URLSearchParams(location.search);
const showDevShell = import.meta.env.DEV && !isNative() && !params.has('_embed');

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ShellProvider>{showDevShell ? <DevShell /> : <App />}</ShellProvider>
  </StrictMode>,
);

On first vite dev:

  • dev-proxy probes ports 9999..10098, takes first free
  • if <project>/<shellDir>/ is empty → scaffolds @lion-turtle/vite/scaffold/ Flutter app into it (consumer owns the copy after that)
  • DevShell renders at /, the iframe inside loads /?_embed=1

Run the demo (spike)

Native vs React rendering, same screens, side-by-side. No server.

bun install
bun run demo                    # http://localhost:5173

Run the example (TODO with auth + offline + sync)

bun run todo                    # interactive: pick device, asks about flutter shell
# OR
bun run todo:server             # server alone (port 3001)
bun run todo:web                # vite alone (port 5173 + dev-proxy 9999)

Build for production (ROOT_TARGET injected at build time):

PROD_ROOT_TARGET=https://todo.example.com bun run todo:build:apk
PROD_ROOT_TARGET=https://todo.example.com bun run todo:build:ios
# or:
bun examples/todo/build.ts ipa --root-target https://todo.example.com

What's in the box

  • DevShell — phone-frame iframe mock with bridge log, console viewer, env editor, dev-forward routes editor, device picker, tunnel control, Flutter control. Auto-scales phone to fit canvas. Sleek scrollbars.
  • Dev-proxy — Bun child process on port 9999 (auto-bumps if taken). /__via/<scheme>/<host>:<port>/ path forwarding, vite fallback for unmapped paths.
  • Local-first storeuseStore(table) hook, localStorage backing in v0.1, sync replay + pull against a server, lenient reconciliation.
  • Auto ROOT_TARGET resolution — picks the right URL for android emulator (10.0.2.2) vs physical (localhost + adb reverse) vs iOS sim vs iOS phone (tunnel).
  • Offline-first WebView — scaffold runs a local cache+proxy server on 127.0.0.1:<random>; WebView loads through it. GETs cached on disk, non-GETs proxied. On cache miss + offline → 503 → Flutter shows "MAINFRAME UNREACHABLE" screen.
  • Flutter control plane — start / stop / hot-reload / hot-restart from DevShell. Logs streamed live, persisted, copyable as raw text or AI-prompt markdown.
  • Tunnel — cloudflared / ngrok integration. Auto-feeds tunnel URL into Flutter ROOT_TARGET when a physical iOS device is selected.
  • Env sniff — reads VITE_* from process.env, lets you override per-key in DevShell, exposes useEnv("VITE_FOO") reactive hook.
  • Build scriptbun run todo:build:apk injects prodRootTarget via --dart-define=ROOT_TARGET=… at build time.
  • Interactive bun run todo — prompts for device, sets up adb reverse if Android physical, launches server + vite + offers Flutter via DevShell.

Docs

File Topic
docs/architecture.md Layout + extension points
docs/devshell.md DevShell tabs, controls, persistence, native-mode overlay
docs/flutter-shell.md Flutter integration, ROOT_TARGET resolution, cache server, prod builds, debug checklist
docs/integration-guide.md Plug into a Vite project from scratch
docs/bridge-protocol.md Wire format + message kinds + handshake
docs/store-api.md Local-first store CRUD + replay
docs/extension-api.md Custom bridge handlers, store schemas
docs/file-map.md Layout reference
docs/conventions.md Naming, patterns, style
docs/glossary.md Domain terms

Status

Pre-1.0. Local-only, no remote. APIs may shift.