scripts/sandbox-sweep.mjs
scripts/sandbox-sweep.mjsBrowse 13 files
818 tokens
3,009 bytes
Token encoding: o200k_base
Snapshot bfcf687
← Back to SKILL.md
1// List/remove leaked bench sandboxes. Every VM the skill's scripts2// create is named sbench-* and tagged purpose=sandbox-bench; the3// harnesses remove their VMs in finally blocks, but a killed local4// process leaks them until their timeout.5//6// The project may be shared, so removal is deliberately conservative:7// a VM is only listed when its name matches, its tag matches, AND it is8// older than --min-age-hours (default 3 — longer than any healthy run),9// and removal is per exact listed name.10//11// node sandbox-sweep.mjs # list what would be removed12// node sandbox-sweep.mjs --yes # remove13// node sandbox-sweep.mjs --min-age-hours 0 # include fresh VMs (danger:14// # only when nothing is active)15import { execFile } from 'child_process'16import { promisify } from 'util'17import { loadConfig, sandboxScope } from './config.mjs'18 19const execFileP = promisify(execFile)20const CONFIG = loadConfig()21const SCOPE = sandboxScope(CONFIG)22const argv = process.argv.slice(2)23const minAgeHours = argv.includes('--min-age-hours')24 ? Number(argv[argv.indexOf('--min-age-hours') + 1])25 : 326 27// Age from the CLI's "CREATED" column ("32 minutes ago", "2 hours ago").28function ageHours(words) {29 const i = words.findIndex((w) => w === 'ago')30 if (i < 2) return Infinity31 const n = Number(words[i - 2])32 const unit = words[i - 1]33 if (!Number.isFinite(n)) return Infinity34 if (unit.startsWith('second')) return n / 360035 if (unit.startsWith('minute')) return n / 6036 if (unit.startsWith('hour')) return n37 return n * 2438}39 40// Paginate: the CLI prints a cursor hint line when more pages exist.41const found = []42let cursor43for (let page = 0; page < 50; page++) {44 const args = ['sandbox', 'list', '--limit', '100', ...SCOPE]45 if (cursor) args.push('--cursor', cursor)46 const { stdout, stderr } = await execFileP(CONFIG.vercelBin, args, {47 maxBuffer: 1 << 24,48 })49 for (const line of stdout.split('\n')) {50 const words = line.trim().split(/\s+/)51 const name = words[0]52 if (!/^sbench-/.test(name)) continue53 if (!line.includes('purpose:sandbox-bench')) continue54 const age = ageHours(words)55 if (age < minAgeHours) {56 console.log(57 `skipping ${name} (${age.toFixed(1)}h old < --min-age-hours ${minAgeHours})`58 )59 continue60 }61 found.push(name)62 }63 cursor = `${stdout}\n${stderr}`.match(/--cursor\s+(\S+)/)?.[1]64 if (!cursor) break65}66 67if (!found.length) {68 console.log('no leaked bench sandboxes found')69 process.exit(0)70}71const doIt = argv.includes('--yes')72let failures = 073for (const name of found) {74 if (doIt) {75 try {76 await execFileP(CONFIG.vercelBin, ['sandbox', 'rm', name, ...SCOPE])77 console.log(`removed ${name}`)78 } catch (e) {79 failures++80 console.error(`could not remove ${name}: ${e.message.split('\n')[0]}`)81 }82 } else {83 console.log(`would remove ${name} (pass --yes)`)84 }85}86process.exit(failures ? 1 : 0)87