@nuxfly/cli
Version:
CLI tool for deploying Nuxt applications to Fly.io
293 lines (248 loc) • 7.5 kB
JavaScript
import { accessSync, existsSync, constants } from 'fs';
import consola from 'consola';
import { FlyTomlNotFoundError, NotNuxtProjectError, NuxflyError, withErrorHandling } from './errors.mjs';
import { checkAppAccess, checkFlyAuth } from './flyctl.mjs';
import { getFlyTomlPath, getAppName, validateNuxflyEnv } from './config.mjs';
/**
* Validate that fly.toml exists in project root
*/
export const validateFlyTomlExists = withErrorHandling((config) => {
const flyTomlPath = getFlyTomlPath(config);
if (!existsSync(flyTomlPath)) {
throw new FlyTomlNotFoundError();
}
consola.debug(`Found fly.toml at: ${flyTomlPath}`);
return true;
});
/**
* Validate that this is a Nuxt project
*/
export const validateNuxtProject = withErrorHandling((cwd = process.cwd()) => {
const nuxtConfigFiles = [
'nuxt.config.js',
'nuxt.config.ts',
'nuxt.config.mjs',
'nuxt.config.cjs',
];
const hasNuxtConfig = nuxtConfigFiles.some(filename =>
existsSync(`${cwd}/${filename}`)
);
if (!hasNuxtConfig) {
throw new NotNuxtProjectError();
}
consola.debug('Validated Nuxt project');
return true;
});
/**
* Validate user has access to the specified fly app
*/
export const validateAppAccess = withErrorHandling(async (appName, config = {}) => {
if (!appName) {
throw new NuxflyError('No app name specified', {
suggestion: 'Set app name in your nuxfly config or use --app flag',
});
}
// Check if user is authenticated
const user = await checkFlyAuth();
if (!user) {
throw new NuxflyError('Not authenticated with Fly.io', {
suggestion: 'Run "flyctl auth login" to authenticate',
});
}
// Check if user has access to the app
const hasAccess = await checkAppAccess(appName, config);
if (!hasAccess) {
throw new NuxflyError(`No access to app "${appName}"`, {
suggestion: 'Check that the app exists and you have permission to access it',
});
}
consola.debug(`Validated access to app: ${appName}`);
return true;
});
/**
* Validate required configuration for deployment
*/
export const validateDeploymentConfig = withErrorHandling(async (config) => {
// Check for fly.toml
validateFlyTomlExists(config);
// Check app access if app is configured
const appName = getAppName(config);
if (appName) {
await validateAppAccess(appName, config);
}
return true;
});
/**
* Validate command-specific requirements
*/
export const validateCommand = withErrorHandling(async (command, config, args = {}) => {
switch (command) {
case 'launch':
// Launch can be run without existing fly.toml but needs NUXFLY_ENV
validateNuxtProject();
validateNuxflyEnv(command);
break;
case 'import': {
// Import requires app name but not existing fly.toml
validateNuxtProject();
const importAppName = args.app || getAppName(config);
if (importAppName) {
await validateAppAccess(importAppName, config);
}
break;
}
case 'generate':
// Generate requires existing fly.toml and NUXFLY_ENV
validateNuxtProject();
validateNuxflyEnv(command);
validateFlyTomlExists(config);
break;
case 'deploy':
// Deploy requires full deployment config and NUXFLY_ENV
validateNuxtProject();
validateNuxflyEnv(command);
await validateDeploymentConfig(config);
break;
case 'studio':
// Studio requires deployed app and NUXFLY_ENV
validateNuxtProject();
validateNuxflyEnv(command);
await validateDeploymentConfig(config);
break;
default:
// Other commands (proxy) require fly.toml but not NUXFLY_ENV
validateFlyTomlExists(config);
break;
}
return true;
});
/**
* Validate that required dependencies are available
*/
export const validateDependencies = withErrorHandling(async (command) => {
// flyctl is always required
const { checkFlyctlAvailable } = await import('./flyctl.mjs');
await checkFlyctlAvailable();
// Check command-specific dependencies
if (command === 'studio') {
await validateDrizzleKit();
}
return true;
});
/**
* Validate drizzle-kit is available for studio command
*/
const validateDrizzleKit = withErrorHandling(async () => {
try {
const { execa } = await import('execa');
await execa('drizzle-kit', ['--version'], { stdio: 'pipe' });
return true;
} catch (error) {
if (error.code === 'ENOENT') {
throw new NuxflyError('drizzle-kit not found', {
suggestion: 'Install drizzle-kit: npm install -g drizzle-kit',
exitCode: 127,
});
}
// If drizzle-kit exists but version command fails, still return true
return true;
}
});
/**
* Validate port numbers
*/
export function validatePort(port, name = 'port') {
const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
throw new NuxflyError(`Invalid ${name}: ${port}`, {
suggestion: `${name} must be a number between 1 and 65535`,
});
}
return portNum;
}
/**
* Validate app name format
*/
export function validateAppName(name) {
if (!name) {
throw new NuxflyError('App name is required');
}
// Fly.io app name requirements
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) || name.length > 30) {
throw new NuxflyError(`Invalid app name: ${name}`, {
suggestion: 'App names must be lowercase, alphanumeric, max 30 chars, and cannot start/end with hyphens',
});
}
return name;
}
/**
* Validate region code
*/
export function validateRegion(region) {
if (!region) {
return 'ord'; // Default region
}
// Basic validation - just check it's a reasonable string
if (!/^[a-z]{3}$/.test(region)) {
consola.warn(`Region "${region}" may not be valid. Common regions: ord, dfw, lax, iad, lhr, nrt, syd`);
}
return region;
}
/**
* Pre-flight checks before running any command
*/
export const preflightChecks = withErrorHandling(async (command, config, args = {}) => {
consola.debug(`Running preflight checks for command: ${command}`);
// Validate dependencies
await validateDependencies(command);
// Validate command requirements
await validateCommand(command, config, args);
consola.debug('Preflight checks passed');
return true;
});
/**
* Validate file permissions
*/
export function validateFilePermissions(filepath) {
try {
accessSync(filepath, constants.R_OK | constants.W_OK);
return true;
} catch {
throw new NuxflyError(`No read/write access to ${filepath}`, {
suggestion: 'Check file permissions',
});
}
}
/**
* Validate directory is writable
*/
export function validateDirectoryWritable(dirpath) {
try {
accessSync(dirpath, constants.W_OK);
return true;
} catch {
throw new NuxflyError(`Directory not writable: ${dirpath}`, {
suggestion: 'Check directory permissions',
});
}
}
/**
* Comprehensive validation for launch command
*/
export const validateLaunchCommand = withErrorHandling(async (args) => {
validateNuxtProject();
if (args.name) {
validateAppName(args.name);
}
if (args.region) {
validateRegion(args.region);
}
// Check if user is authenticated
const user = await checkFlyAuth();
if (!user) {
throw new NuxflyError('Not authenticated with Fly.io', {
suggestion: 'Run "flyctl auth login" to authenticate',
});
}
return true;
});