# Replit Migrate

Migrate Replit apps to web (Vercel, Cloudflare, standalone) or native iOS/macOS. Risk-ordered plans with lessons encoded from real migrations.

Status: active

## Install

```bash
claude plugin marketplace add tyroneross/RossLabs-AI-Toolkit
claude plugin install replit-migrate@rosslabs-ai-toolkit
```

## The Problem

Replit apps mix runtime, database, secrets, and deployment into a single environment. Lifting one out touches all four at once. First-time migrations miss the weird parts: the in-memory session store, the bundled SQLite, the vendored Python dependency, the Replit Auth cookie that stops working on Vercel.

## What I Built

Scan a Replit project, produce a risk-ordered migration plan for web or native, and avoid redoing the mistakes that bit earlier migrations.

## Impact

Lessons from real migrations ship as hardcoded plan-generator logic, not prose the model might drop: auth-pattern weighting tuned on a false positive from the SpeakSavvy migration, schema-file detection that distinguishes Drizzle's actual schema from its config, and a browser-API → native-framework catalog with a risk rating and a one-line lesson per entry. The CLI (`replit-migrate scan|plan|progress`) and the MCP server the plugin wraps around call the same `src/analyzers` and `src/generators` code, so a terminal run and a Claude Code session produce byte-identical plans.

## Architecture

Replit Migrate does no LLM inference of its own — every classification is regex/glob pattern matching over the project's source tree, and every migration plan is assembled from typed templates keyed off the scan result. The "AI" in the workflow is the calling agent (Claude Code or Codex) deciding when to invoke `migrate_scan` vs `migrate_plan_web`, then reading the plan back to the user — the tool itself is a deterministic static analyzer plus a plan-template engine, packaged as an MCP server (JSON-RPC 2.0 over stdio) so either host can drive it through the same six tools.

```mermaid
flowchart LR
  A[Replit project on disk] --> B["migrate_scan\n(replit-scanner.ts)"]
  B --> C{"pattern match:\nstack / auth / db /\nroutes / env / browser APIs"}
  C --> D[("scan-report.json")]
  D --> E{target}
  E -->|web| F["migrate_plan_web\nProductPilot lessons"]
  E -->|native| G["migrate_plan_native\nFloDoro + SpeakSavvy lessons"]
  F --> H[("WEB_MIGRATION_PLAN.md\n+ web-plan.json")]
  G --> I[("NATIVE_MIGRATION_PLAN.md\n+ native-plan.json")]
  D -.-> J["migrate_map_apis /\nmigrate_map_models\n(on-demand detail)"]
  H --> K["migrate_check_progress\nfilesystem heuristics"]
  I --> K
  K --> L[("progress.json")]
```

**Models — which, where, why:**

Models: none. Every stage — stack detection, auth-method scoring, schema parsing, route extraction, model-type translation, task-completion heuristics — is regex/glob matching or template substitution in TypeScript. No `src/` file imports an LLM SDK or makes a network call; the only I/O is local filesystem reads/writes under `.replit-migrate/`. This is a deliberate design choice, not a gap: a migration scanner that hallucinates a route or a schema field is worse than one that misses it, so detection stays mechanical and the calling agent (Claude Code/Codex, whichever host invoked the plugin) supplies the judgment on top of the scan output.

**Tools & infra — which, why:**

- **Node.js + TypeScript (`commander`, `glob`)** — the CLI and analyzers; `commander` parses `scan|plan|progress` subcommands, `glob` walks the project tree with `node_modules`/`dist`/`.git` excluded.
- **MCP server, hand-rolled JSON-RPC 2.0 over stdio (`src/mcp/server.ts`)** — no MCP SDK dependency; a ~110-line `readline`-based transport implements `initialize`, `tools/list`, `tools/call` directly, exposing the same six tools (`migrate_scan`, `migrate_plan_web`, `migrate_plan_native`, `migrate_map_apis`, `migrate_map_models`, `migrate_check_progress`) to any MCP-speaking host.
- **Dual plugin packaging (`.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`)** — same `dist/`, `skills/`, `commands/`, `agents/`, and `.mcp.json` ship to both Claude Code and Codex; each host manifest just points at the shared MCP server entry (`node dist/mcp/server.js`).
- **Filesystem as the only datastore** — `.replit-migrate/{scan-report,web-plan,native-plan,progress}.json` plus the human-readable `.md` twins; no database, because a single-project, single-session scan has no cross-project state to persist.
- **`migrate_check_progress` heuristics** — no test-runner integration; "done" is inferred from file existence, `package.json` dependency presence (e.g. `@clerk/nextjs` for a Clerk auth task), and a residual `REPLIT_`/`@replit/` reference count across the tree, re-run on every progress check rather than tracked as a one-time flag.

## How it works

1. `/replit-migrate:scan` (or `replit-migrate scan ./app` from the CLI) calls `migrate_scan`, which walks the project with `glob` and classifies it against fixed pattern tables: runtime (`node`/`python` by `package.json` vs `requirements.txt`), server framework, frontend framework, bundler, ORM, and styling — each a first-match-wins scan over `package.json` dependencies and config files.
2. Auth detection runs five regex patterns (Replit OIDC, magic link, JWT, session, Passport) against every source file, then weights each hit by file location — route-level files (`server/routes`, `server/auth`, `api/`) score 3×, auth UI 2×, ambient references 1×, and config/build-tooling references score 0 — so a stray mention of `passport` in a build script can't outrank real route-level auth code. The highest-scoring method wins.
3. Database detection matches driver packages (`pg`, `@neondatabase/serverless`, `better-sqlite3`, `mongoose`, …) to a type, then locates the *actual* schema file — for Drizzle specifically, it globs `{shared,src,server,db}/schema.ts` rather than trusting `drizzle.config.ts`, a fix encoded after the SpeakSavvy migration mistook the config file for the schema.
4. API routes are extracted with an Express-shaped regex (`app|router.<method>(path, ...)`) across `server/**`, `routes/**`, `api/**`; a 30-line lookahead from each match flags `auth_required` (`req.user`, `requireAuth`, `isAuthenticated`) and `replit_specific_code` (`REPLIT_` references) per route.
5. Ten browser-API patterns (SpeechRecognition, geolocation, mediaDevices, localStorage, WebSocket, Notification, clipboard, canvas, IntersectionObserver, speechSynthesis) are matched and each hit is pre-mapped to a native iOS framework, import statement, and risk tier — SpeechRecognition → `SFSpeechRecognizer`/`Speech`, high risk, "test on a real device."
6. The full `ScanReport` — stack, auth, database, routes, env vars, external services, browser APIs, frontend pages, file stats — writes to `.replit-migrate/scan-report.json`, and a Replit lock-in score (0–10, weighted on Replit-specific auth, hosted DB, `.replit`/`replit.nix` presence, Replit env vars, `@replit/*` packages) prints in the scan summary.
7. `migrate_plan_web` (target: `vercel`/`cloudflare`/`standalone`) reads that report and assembles a `WebMigrationPlan` from typed task templates: auth-migration steps carry hardcoded lesson text ("spike auth first — ProductPilot burned 6+ fix commits coding before understanding the OIDC→Neon Auth diff"), and default auth-replacement strategy is target-dependent (Clerk for Vercel, Auth.js for Cloudflare/standalone) unless the caller overrides it.
8. `migrate_plan_native` (platforms: `ios`/`macos`/`watchos`) instead builds a `NativeMigrationPlan`: an architecture-doc task first (FloDoro lesson), a platform-constraints checklist before any view code, a field-by-field SQL/ORM-type → Swift-type table (`jsonb` → `Data` + "define a Codable struct"), and only the browser-API mappings the scan actually detected — unmatched catalog entries are never emitted.
9. `migrate_map_apis` and `migrate_map_models` are on-demand detail tools, not part of the default flow — `map_apis` re-parses route files for path/query/body params and a response-shape guess; `map_models` re-parses the schema files found in step 3 (Drizzle table-definition regex or Prisma model-block regex) into a field table targeting SwiftData, Drizzle, or Prisma types.
10. `migrate_check_progress` loads whichever plan exists (web takes precedence if both do), runs per-task heuristics against the live filesystem and `package.json` (file existence for `files_affected`, dependency presence for auth/db/bundling tasks), auto-marks matching tasks done, counts remaining `REPLIT_`/`@replit/` references as a blocker, and writes the updated state to `.replit-migrate/progress.json`.

## Tech stack

Node.js + TypeScript CLI (`commander` for subcommands, `glob` for tree walks), packaged as both a Claude Code plugin and a Codex plugin sharing one `dist/` build. A hand-rolled MCP server (JSON-RPC 2.0 over stdio, no MCP SDK) exposes six tools — `migrate_scan`, `migrate_plan_web`, `migrate_plan_native`, `migrate_map_apis`, `migrate_map_models`, `migrate_check_progress`. No LLM, no database — scan reports and plans are JSON + Markdown files under `.replit-migrate/` in the target project. Three slash commands (`/replit-migrate:scan`, `/replit-migrate:migrate`, and a `/replit-migrate` router that dispatches to one of those two by intent) and three skills (`migrate-web`, `migrate-ios`, `migration-scan`) route natural-language requests to the right tool. XcodeGen appears only as guidance inside generated native plans (a documented `project.yml`-over-`Info.plist` convention, from a real regression during the SpeakSavvy migration) — the tool never shells out to XcodeGen itself.

## Links

- [HTML project page](https://rosslabs.ai/projects/replit-migrate/)
- [Source code](https://github.com/tyroneross/replit-migrate)
