Files
orca/config/scripts/dev-fresh-profile.mjs
T
Neil fef00e4461 feat(dev): give each worktree its own pnpm dev profile and ports (#33416)
Linked worktrees now get a per-worktree userData profile under
orca-dev-wt/<hash>, a deterministic collision-checked renderer port
(15200+) and CDP port (9333+), so parallel `pnpm dev` runs no longer hit
the single-instance lock. The main checkout keeps the shared orca-dev
profile; --shared-profile / --worktree-profile / ORCA_DEV_PROFILE override.

The runner prints `ORCA_DEV_INSTANCE {json}` on stdout and mirrors it to
out/dev-instance.json so agents can attach over CDP. orca-dev.mjs and the
terminal wrappers follow the profile the worktree's dev app used.

Adds profile seeding (pnpm dev:seed-profile, pnpm dev --seed-profile,
pnpm dev:fresh --seed-from): an allowlisted slim copy of projects,
workspaces, settings, SSH targets, and the account session, written as
orca-data.json for Orca's own import; the source is only read.

Storage reclaim removes per-worktree profiles once their worktree is gone
and no app, runtime, or daemon still uses them. dev-fresh-profile.sh is
replaced by a cross-platform dev-fresh-profile.mjs.
2026-10-24 23:08:52 -07:00

96 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
// Launch `pnpm dev` on a fresh, isolated userData profile so the app behaves like a first-time
// install (onboarding paints, no persisted repos or sessions). Other dev profiles stay untouched.
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { spawnProcess } from '@orca/process-host'
import { getDefaultSeedSource, seedDevProfile } from './dev-profile-seed.mjs'
const USAGE = `Usage: pnpm dev:fresh [--keep] [--seed-from[=<profile dir>]] [-- <pnpm dev args>]
--keep keep the temporary profile after exit
--seed-from[=<dir>] start from a slim copy of a profile (projects, workspaces, settings,
SSH targets, account session); default: orca-dev, else the installed app
ORCA_FRESH_PROFILE_DIR=<dir> use (and keep) a fixed profile directory`
const TEMP_PREFIX = 'orca-fresh-profile-'
function parseArgs(argv) {
const options = { keep: false, seedFrom: null, devArgs: [] }
for (let index = 0; index < argv.length; index++) {
const arg = argv[index]
if (arg === '--') {
options.devArgs = argv.slice(index + 1)
break
}
if (arg === '--keep') {
options.keep = true
} else if (arg === '--seed-from') {
options.seedFrom = getDefaultSeedSource()
} else if (arg.startsWith('--seed-from=')) {
options.seedFrom = path.resolve(arg.slice('--seed-from='.length))
} else if (arg === '-h' || arg === '--help') {
options.help = true
} else {
throw new Error(`Unknown argument: ${arg}\n${USAGE}`)
}
}
return options
}
const options = parseArgs(process.argv.slice(2))
if (options.help) {
console.log(USAGE)
process.exit(0)
}
const fixedDir = process.env.ORCA_FRESH_PROFILE_DIR
const profileDir = fixedDir ? path.resolve(fixedDir) : mkdtempSync(path.join(tmpdir(), TEMP_PREFIX))
mkdirSync(profileDir, { recursive: true })
function cleanup() {
const removable = !options.keep && !fixedDir && path.basename(profileDir).startsWith(TEMP_PREFIX)
if (removable && existsSync(profileDir)) {
rmSync(profileDir, { recursive: true, force: true })
console.error(`[dev-fresh-profile] removed ${profileDir}`)
} else {
console.error(`[dev-fresh-profile] kept ${profileDir}`)
}
}
try {
if (options.seedFrom) {
await seedDevProfile({ sourceDir: options.seedFrom, targetDir: profileDir })
}
} catch (error) {
console.error(`[dev-fresh-profile] ${error.message}`)
cleanup()
process.exit(1)
}
console.error(`[dev-fresh-profile] using userData=${profileDir}`)
const child = spawnProcess({
program: 'pnpm',
args: ['dev', ...options.devArgs],
env: {
...process.env,
ORCA_DEV_USER_DATA_PATH: profileDir,
// Why: seeded profiles already finished onboarding; only empty ones should show first-run education.
...(options.seedFrom ? {} : { ORCA_DEV_SHOW_FIRST_RUN_EDUCATION: '1' })
},
stdio: 'inherit'
})
// Why: Ctrl+C reaches pnpm through the shared terminal; wait for it so cleanup sees a closed profile.
process.on('SIGINT', () => {})
process.on('SIGTERM', () => child.kill('SIGTERM'))
child.on('exit', (code, signal) => {
cleanup()
process.exit(code ?? (signal ? 1 : 0))
})
child.on('error', (error) => {
console.error(`[dev-fresh-profile] ${error.message}`)
cleanup()
process.exit(1)
})