Replit Migrate
Migrate Replit apps to web (Vercel, Cloudflare, standalone) or native iOS/macOS. Risk-ordered plans with lessons encoded from real migrations.
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;commanderparsesscan|plan|progresssubcommands,globwalks the project tree withnode_modules/dist/.gitexcluded. - MCP server, hand-rolled JSON-RPC 2.0 over stdio (
src/mcp/server.ts) — no MCP SDK dependency; a ~110-linereadline-based transport implementsinitialize,tools/list,tools/calldirectly, 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) — samedist/,skills/,commands/,agents/, and.mcp.jsonship 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}.jsonplus the human-readable.mdtwins; no database, because a single-project, single-session scan has no cross-project state to persist. migrate_check_progressheuristics — no test-runner integration; “done” is inferred from file existence,package.jsondependency presence (e.g.@clerk/nextjsfor a Clerk auth task), and a residualREPLIT_/@replit/reference count across the tree, re-run on every progress check rather than tracked as a one-time flag.
How it works
/replit-migrate:scan(orreplit-migrate scan ./appfrom the CLI) callsmigrate_scan, which walks the project withgloband classifies it against fixed pattern tables: runtime (node/pythonbypackage.jsonvsrequirements.txt), server framework, frontend framework, bundler, ORM, and styling — each a first-match-wins scan overpackage.jsondependencies and config files.- 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 ofpassportin a build script can’t outrank real route-level auth code. The highest-scoring method wins. - 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.tsrather than trustingdrizzle.config.ts, a fix encoded after the SpeakSavvy migration mistook the config file for the schema. - API routes are extracted with an Express-shaped regex (
app|router.<method>(path, ...)) acrossserver/**,routes/**,api/**; a 30-line lookahead from each match flagsauth_required(req.user,requireAuth,isAuthenticated) andreplit_specific_code(REPLIT_references) per route. - 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.” - 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.nixpresence, Replit env vars,@replit/*packages) prints in the scan summary. migrate_plan_web(target:vercel/cloudflare/standalone) reads that report and assembles aWebMigrationPlanfrom 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.migrate_plan_native(platforms:ios/macos/watchos) instead builds aNativeMigrationPlan: 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.migrate_map_apisandmigrate_map_modelsare on-demand detail tools, not part of the default flow —map_apisre-parses route files for path/query/body params and a response-shape guess;map_modelsre-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.migrate_check_progressloads whichever plan exists (web takes precedence if both do), runs per-task heuristics against the live filesystem andpackage.json(file existence forfiles_affected, dependency presence for auth/db/bundling tasks), auto-marks matching tasks done, counts remainingREPLIT_/@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.