toes/scripts/migrate.ts

112 lines
3.2 KiB
TypeScript

#!/usr/bin/env bun
// Migration script: converts apps from versioned directory structure
// (apps/<name>/<timestamp>/ with `current` symlink) to flat structure (apps/<name>/).
//
// Usage: bun run remote:migrate
//
// What it does:
// 1. Scans APPS_DIR for apps with a `current` symlink
// 2. Resolves the symlink to find the active version directory
// 3. Moves files from the active version into the app root (preserving node_modules/logs)
// 4. Removes old version directories and the symlink
//
// Safe to run multiple times -- skips apps already in flat structure.
import { existsSync, mkdirSync, readdirSync, renameSync, lstatSync, readlinkSync, rmSync } from 'fs'
import { join, resolve } from 'path'
const APPS_DIR = process.env.APPS_DIR ?? join(process.env.HOME!, 'apps')
const VERSION_RE = /^\d{8}-\d{6}$/
if (!existsSync(APPS_DIR)) {
console.error(`APPS_DIR does not exist: ${APPS_DIR}`)
process.exit(1)
}
const apps = readdirSync(APPS_DIR, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name)
let migrated = 0
let skipped = 0
for (const name of apps) {
const appDir = join(APPS_DIR, name)
const currentLink = join(appDir, 'current')
// Already flat -- has package.json at root with no current symlink
if (!existsSync(currentLink)) {
if (existsSync(join(appDir, 'package.json'))) {
skipped++
}
continue
}
// Verify it's actually a symlink
let stat
try {
stat = lstatSync(currentLink)
} catch {
continue
}
if (!stat.isSymbolicLink()) {
console.warn(` [skip] ${name}: 'current' exists but is not a symlink`)
skipped++
continue
}
// Resolve the symlink to get the active version directory
const target = readlinkSync(currentLink)
const activeDir = resolve(appDir, target)
if (!existsSync(activeDir)) {
console.warn(` [skip] ${name}: symlink target does not exist: ${target}`)
skipped++
continue
}
console.log(` [migrate] ${name} (active version: ${target})`)
// Collect all version directories and other entries
const entries = readdirSync(appDir, { withFileTypes: true })
const versionDirs = entries
.filter(e => e.isDirectory() && VERSION_RE.test(e.name) && e.name !== target)
.map(e => e.name)
// Remove old (non-active) version directories first
for (const ver of versionDirs) {
const verPath = join(appDir, ver)
rmSync(verPath, { recursive: true, force: true })
console.log(` removed old version: ${ver}`)
}
// Remove the current symlink
rmSync(currentLink)
// Move files from active version directory into app root
const activeEntries = readdirSync(activeDir)
for (const entry of activeEntries) {
const src = join(activeDir, entry)
const dest = join(appDir, entry)
// Skip if destination already exists (e.g. node_modules, logs at root level)
if (existsSync(dest)) {
console.log(` skip (exists): ${entry}`)
continue
}
renameSync(src, dest)
}
// Clean up the now-empty active version directory
rmSync(activeDir, { recursive: true, force: true })
migrated++
console.log(` done`)
}
console.log()
console.log(`Migration complete: ${migrated} migrated, ${skipped} skipped`)