What the agent does
The agent reads the shape of the app before it renames anything. In a Vue app like realworld-apps/vue-realworld-example-app, package.json lists vue ^3.5, pinia ^3, vue-router ^5.1 and vite ^8 with @vitejs/plugin-vue, a bun.lock, and scripts for build, lint, format and a Playwright test; there is no typescript, no tsconfig.json and not one .ts file under src, only twelve .js files and twenty .vue components, all written as <script setup>. vite.config.js already lists .ts and .tsx in resolve.extensions, and its comment explains why .vue is there too: the router imports views as @/views/Home with no extension, a Vue CLI habit the conversion has to keep working.
It sets up the toolchain in one commit: typescript and vue-tsc as dev dependencies, a tsconfig.json with strict on, the @/* path alias that matches the Vite config, and allowJs so the build and the type check pass while files are still mixed. It adds a typecheck script that runs vue-tsc --noEmit, changes the ESLint files glob in eslint.config.mjs from src/**/*.{js,vue} to include .ts, adds the TypeScript parser for <script lang="ts"> blocks, and widens the Prettier globs in the format scripts the same way. env.d.ts declares *.vue modules and the window.__conduit_debug__ object that main.js installs for the end-to-end tests.
Then it converts from the leaves inward. src/common/jwt.service.js and config.js are first because everything imports them. api.service.js is the interesting one: its request function sets error.response = { status, data } on a plain Error, so the agent writes an ApiError class with a typed response and makes extractErrors in errors.js take it, instead of sprinkling as any. A src/types module declares User, Article, Comment, Profile and the paginated article list the way the stores already use them, the Pinia stores in src/store get typed state() returns and typed action parameters, and router/index.js becomes index.ts with the RouteRecordRaw array and a typed meta.requiresAuth.
Components come last. Each <script setup> gains lang="ts", and runtime prop declarations such as the pages: { type: Array, required: true } in VPagination.vue become defineProps<{ pages: number[]; currentPage: number }>(). After every batch the agent runs bun run typecheck, bun run lint and bun run build, and it keeps allowJs until the last file is converted, then removes it so a stray .js cannot come back unnoticed. It runs bun run test:e2e where the sandbox can reach the API the app talks to, and otherwise says so in the pull request rather than claiming the suite passed.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Convert this Vue 3 app from JavaScript to TypeScript and open a pull request. Read package.json, vite.config.js, eslint.config.mjs, playwright.config.ts, src/main.js, src/router/index.js, every file under src/common and src/store, and the script blocks of every .vue file under src/components and src/views first. Add typescript and vue-tsc as dev dependencies with bun, a strict tsconfig.json whose @/* alias matches vite.config.js, an env.d.ts for .vue modules and the window.__conduit_debug__ global, a typecheck script that runs vue-tsc --noEmit, and extend the ESLint and Prettier globs to .ts files. Convert the .js files under src/common, src/store and src/router to .ts, replacing the response property patched onto Error in api.service.js with a typed ApiError class, and declare User, Article, Comment and Profile types from the shapes the stores use. Then convert every <script setup> block to lang=\"ts\" with typed defineProps and defineEmits. Keep imports without extensions working. Run bun run typecheck, bun run lint and bun run build after each batch and before you push; run bun run test:e2e if the sandbox can reach the API, and say in the pull request if it cannot. Do not change behaviour, rename components or reformat files you did not convert.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request with one commit for the toolchain and one commit per batch of files, so a reviewer can read the api.service.ts change on its own instead of inside a rename of thirty files. The description lists every renamed file, the new tsconfig.json, env.d.ts and src/types module, and the exact bun run typecheck, bun run lint and bun run build output from the last commit, plus whether the Playwright suite ran.
Git sees most of the conversion as renames with small edits, because the agent changes the extension and the types but not the logic, the formatting or the import order. Where it had to decide on a type, such as the errors object the auth store starts as null and later assigns an object to, the pull request says what it chose and why.
A reviewer checks that strict is on and no any was used to make a file compile, reads the ApiError class and the src/types module since those shapes outlive this change, and runs the existing CI workflows, which still install with bun and run the same scripts. Merging leaves allowJs off, so the next JavaScript file someone adds fails typecheck at once.
Run it on every repository still in JavaScript
A team with several Vue apps to convert starts one run per repository with POST /o/{org}/sessions, this prompt and that repository in resources. Each run gets its own sandbox and its own pull request, and a script can start them all in a minute; the key's max_concurrent limit decides how many run at once, and one more answers 429 until a run finishes.
With a webhook on the key, each pull request's arrival comes as a run.completed event carrying the session_url, so the script can collect the pull request links as they land instead of polling.
The use case this job follows, with its automation and trigger: Hand off from your terminal.
Why run it on Rush?
A conversion like this is hours of mechanical work interrupted by a few real decisions, and on a laptop it competes with everything else; the half-converted branch sits for a week and rots against main. In a Rush sandbox the agent does the whole pass in one sitting, runs vue-tsc, ESLint and the Vite build after each batch, and the output of every run is kept in the transcript, so a type error that appeared and was fixed is visible rather than lost in a terminal.
The result is a pull request whose checks are the proof. The typecheck script the pull request adds is the same one the agent ran, so the reviewer can see on the branch that strict mode passes, and the per-batch commits let the team merge the toolchain and the services now and the components next week if that is how they want to land it.
Frequently asked questions
Will the app behave differently after the conversion?
No. The agent changes types, extensions and script attributes and leaves the logic alone; bun run build and the Playwright suite from the realworld submodule, when the sandbox can reach the API, confirm it. Where an untyped value was being used two different ways, the pull request points at it rather than silently changing one path.
Does it use `any` to get files compiling?
The prompt forbids it and strict is on, so each shape has to be declared. The ApiError class replaces the response property that api.service.js patches onto a plain Error, and src/types declares the API responses once so the stores and components share them.
Can I merge the conversion in pieces?
Yes. The commits are ordered toolchain first, then src/common, stores and router, then components, and allowJs stays on until the final commit, so the branch type-checks and builds at every commit and the team can land it in parts.
What about the ESLint and Prettier setup?
eslint.config.mjs currently lints src/**/*.{js,vue} with @eslint/js and eslint-plugin-vue. The pull request extends the glob to .ts, adds the TypeScript parser for the <script lang="ts"> blocks, and widens the format and format:check globs in package.json, so the existing lint.yml workflow keeps checking every file.