Back to projects
Active Started Feb 2026

Replit Migrate

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

Claude Code Plugin Codex Plugin Node.js CLI TypeScript MCP Server

Install

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.

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 (jsonbData + “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.