@nuxfly/cli
Version:
CLI tool for deploying Nuxt applications to Fly.io
322 lines (275 loc) • 8.54 kB
JavaScript
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, statSync } from 'fs';
import { join, dirname, relative } from 'path';
import { cp } from 'fs/promises';
import consola from 'consola';
import { loadConfig } from 'c12';
import { NuxflyError, PermissionError, withErrorHandling } from './errors.mjs';
import { getNuxflyDir } from './config.mjs';
/**
* Ensure the .nuxfly directory exists
*/
export const ensureNuxflyDir = withErrorHandling(async (config) => {
const nuxflyDir = getNuxflyDir(config);
if (!existsSync(nuxflyDir)) {
consola.debug(`Creating .nuxfly directory: ${nuxflyDir}`);
mkdirSync(nuxflyDir, { recursive: true });
}
return nuxflyDir;
});
/**
* Copy .output directory to .nuxfly/.output if it exists
*/
export const copyDistDir = withErrorHandling(async (config) => {
const distPath = config._runtime?.distPath;
const nuxflyDir = getNuxflyDir(config);
const targetDistPath = join(nuxflyDir, '.output');
if (!existsSync(distPath)) {
consola.debug('No .output directory found, skipping copy');
return false;
}
consola.debug(`Copying .output directory from ${distPath} to ${targetDistPath}`);
try {
// Remove existing dist directory if it exists
if (existsSync(targetDistPath)) {
await removeDirectory(targetDistPath);
}
// Copy directory recursively
await cp(distPath, targetDistPath, { recursive: true, verbatimSymlinks: true });
consola.success('Copied .output directory to .nuxfly/.output');
return true;
} catch (error) {
if (error.code === 'EACCES') {
throw new PermissionError(distPath);
}
throw new NuxflyError(`Failed to copy .output directory: ${error.message}`);
}
});
/**
* Write content to a file, creating directories as needed
*/
export const writeFile = withErrorHandling(async (filepath, content) => {
const dir = dirname(filepath);
// Ensure directory exists
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
try {
writeFileSync(filepath, content, 'utf8');
consola.debug(`Written file: ${filepath}`);
return filepath;
} catch (error) {
if (error.code === 'EACCES') {
throw new PermissionError(filepath);
}
throw new NuxflyError(`Failed to write file ${filepath}: ${error.message}`);
}
});
/**
* Read file content safely
*/
export const readFile = withErrorHandling((filepath) => {
try {
return readFileSync(filepath, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
throw new NuxflyError(`File not found: ${filepath}`);
}
if (error.code === 'EACCES') {
throw new PermissionError(filepath);
}
throw new NuxflyError(`Failed to read file ${filepath}: ${error.message}`);
}
});
/**
* Copy a single file
*/
export const copyFile = withErrorHandling((src, dest) => {
const destDir = dirname(dest);
// Ensure destination directory exists
if (!existsSync(destDir)) {
mkdirSync(destDir, { recursive: true });
}
try {
copyFileSync(src, dest);
consola.debug(`Copied file: ${src} -> ${dest}`);
return dest;
} catch (error) {
if (error.code === 'ENOENT') {
throw new NuxflyError(`Source file not found: ${src}`);
}
if (error.code === 'EACCES') {
throw new PermissionError(src);
}
throw new NuxflyError(`Failed to copy file ${src} to ${dest}: ${error.message}`);
}
});
/**
* Check if a file exists and is readable
*/
export function fileExists(filepath) {
try {
return existsSync(filepath) && statSync(filepath).isFile();
} catch {
return false;
}
}
/**
* Check if a directory exists
*/
export function directoryExists(dirpath) {
try {
return existsSync(dirpath) && statSync(dirpath).isDirectory();
} catch {
return false;
}
}
/**
* Get relative path from cwd
*/
export function getRelativePath(filepath) {
return relative(process.cwd(), filepath);
}
/**
* Remove directory recursively (Node.js 14+ has rmSync, but we'll use a compatible approach)
*/
async function removeDirectory(dirpath) {
if (!existsSync(dirpath)) {
return;
}
const { rm } = await import('fs/promises');
await rm(dirpath, { recursive: true, force: true });
}
/**
* Create backup of a file
*/
export const backupFile = withErrorHandling((filepath) => {
if (!fileExists(filepath)) {
return null;
}
const backupPath = `${filepath}.backup`;
copyFile(filepath, backupPath);
consola.debug(`Created backup: ${backupPath}`);
return backupPath;
});
/**
* Restore file from backup
*/
export const restoreFromBackup = withErrorHandling((filepath) => {
const backupPath = `${filepath}.backup`;
if (!fileExists(backupPath)) {
throw new NuxflyError(`No backup found for ${filepath}`);
}
copyFile(backupPath, filepath);
consola.debug(`Restored from backup: ${backupPath} -> ${filepath}`);
return filepath;
});
/**
* Get file modification time
*/
export function getFileModTime(filepath) {
try {
return statSync(filepath).mtime;
} catch {
return null;
}
}
/**
* Check if source is newer than target
*/
export function isNewer(sourcePath, targetPath) {
const sourceTime = getFileModTime(sourcePath);
const targetTime = getFileModTime(targetPath);
if (!sourceTime || !targetTime) {
return true; // If either doesn't exist, consider source newer
}
return sourceTime > targetTime;
}
/**
* Safe file operations with atomic writes
*/
export const atomicWrite = withErrorHandling(async (filepath, content) => {
const tempPath = `${filepath}.tmp`;
try {
// Write to temporary file first
await writeFile(tempPath, content);
// Move temp file to final location (atomic on most filesystems)
copyFile(tempPath, filepath);
// Clean up temp file
if (existsSync(tempPath)) {
const { unlink } = await import('fs/promises');
await unlink(tempPath);
}
return filepath;
} catch (error) {
// Clean up temp file on error
if (existsSync(tempPath)) {
try {
const { unlink } = await import('fs/promises');
await unlink(tempPath);
} catch {
// Ignore cleanup errors
}
}
throw error;
}
});
/**
* Load drizzle config and get migrations output directory
*/
export const getDrizzleMigrationsPath = withErrorHandling(async () => {
try {
consola.debug('Loading drizzle config...');
// Try to load drizzle.config.ts first
const { config } = await loadConfig({
name: 'drizzle.config',
cwd: process.cwd(),
configFile: 'drizzle.config.ts',
defaults: {},
});
consola.debug('Loaded drizzle config:', config);
if (!config || !config.out) {
consola.debug('No drizzle config found or no "out" property specified');
return null;
}
const migrationsPath = join(process.cwd(), config.out);
consola.debug(`Checking migrations path: ${migrationsPath}`);
if (!directoryExists(migrationsPath)) {
consola.debug(`Migrations directory does not exist: ${migrationsPath}`);
return null;
}
consola.debug(`Found drizzle migrations path: ${migrationsPath}`);
return migrationsPath;
} catch (error) {
consola.debug(`Failed to load drizzle config: ${error.message}`);
return null;
}
});
/**
* Copy drizzle migrations from parent project to .nuxfly directory
*/
export const copyDrizzleMigrations = withErrorHandling(async (config) => {
const migrationsPath = await getDrizzleMigrationsPath();
if (!migrationsPath) {
consola.debug('No drizzle migrations found to copy');
return false;
}
const nuxflyDir = getNuxflyDir(config);
const targetMigrationsPath = join(nuxflyDir, 'migrations');
consola.info('📦 Copying drizzle migrations...');
consola.debug(`Copying migrations from ${migrationsPath} to ${targetMigrationsPath}`);
try {
// Remove existing migrations directory if it exists
if (existsSync(targetMigrationsPath)) {
await removeDirectory(targetMigrationsPath);
}
// Copy migrations directory recursively
await cp(migrationsPath, targetMigrationsPath, { recursive: true, verbatimSymlinks: true });
consola.success('✅ Copied drizzle migrations to .nuxfly/migrations');
return true;
} catch (error) {
if (error.code === 'EACCES') {
throw new PermissionError(migrationsPath);
}
throw new NuxflyError(`Failed to copy drizzle migrations: ${error.message}`);
}
});