#!/usr/bin/env bun // Migration script: converts apps from versioned directory structure // (apps/// with `current` symlink) to flat structure (apps//). // // 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`)