@nuxfly/cli
Version:
CLI tool for deploying Nuxt applications to Fly.io
271 lines (250 loc) • 7.16 kB
JavaScript
#!/usr/bin/env node
import { defineCommand, runMain } from 'citty';
import consola from 'consola';
import { loadConfig } from './utils/config.mjs';
import { NuxflyError } from './utils/errors.mjs';
// Import command handlers
import { launch } from './commands/launch.mjs';
import { importConfig } from './commands/import.mjs';
import { generate } from './commands/generate.mjs';
import { deploy } from './commands/deploy.mjs';
import { studio } from './commands/studio.mjs';
import { proxy, shouldProxy } from './commands/proxy.mjs';
// Global configuration
let globalConfig = null;
// Helper function to load config with error handling
async function ensureConfig() {
if (!globalConfig) {
try {
globalConfig = await loadConfig();
} catch (error) {
throw new NuxflyError(`Failed to load configuration: ${error.message}`);
}
}
return globalConfig;
}
// Define main command
const main = defineCommand({
meta: {
name: 'nuxfly',
version: '1.0.0',
description: 'A CLI tool for deploying Nuxt apps to Fly.io',
},
args: {
verbose: {
type: 'boolean',
description: 'Enable verbose logging',
alias: 'v',
},
config: {
type: 'string',
description: 'Path to fly.toml config file',
},
app: {
type: 'string',
description: 'Fly app name',
alias: 'a',
},
},
subCommands: {
launch: defineCommand({
meta: {
name: 'launch',
description: 'Launch a new Fly app and save config to environment-specific fly.toml',
},
args: {
name: {
type: 'string',
description: 'App name',
},
region: {
type: 'string',
description: 'Region to deploy to',
},
'deploy': {
type: 'boolean',
description: 'Skip deployment after launch',
default: true,
},
size: {
type: 'string',
description: 'Size in GB for SQLite volume (default: 1)',
default: '1',
},
},
async run({ args }) {
const config = await ensureConfig();
await launch(args, config);
},
}),
import: defineCommand({
meta: {
name: 'import',
description: 'Import existing Fly app config to environment-specific fly.toml',
},
args: {
app: {
type: 'string',
description: 'App name to import',
},
},
async run({ args }) {
const config = await ensureConfig();
await importConfig(args, config);
},
}),
generate: defineCommand({
meta: {
name: 'generate',
description: 'Generate Fly deployment files in .nuxfly directory',
},
args: {
'build': {
type: 'boolean',
description: 'Build the application before generating files',
default: false,
},
},
async run({ args }) {
const config = await ensureConfig();
await generate(args, config);
},
}),
deploy: defineCommand({
meta: {
name: 'deploy',
description: 'Generate files and deploy to Fly.io',
},
args: {
strategy: {
type: 'string',
description: 'Deployment strategy',
},
'build': {
type: 'boolean',
description: 'Build the application before deploying',
default: false,
},
},
async run({ args }) {
const config = await ensureConfig();
await deploy(args, config);
},
}),
studio: defineCommand({
meta: {
name: 'studio',
description: 'Open Drizzle Studio with secure tunnel',
},
args: {
port: {
type: 'string',
description: 'Local port for studio',
default: '4983',
},
'remote-port': {
type: 'string',
description: 'Remote tunnel port',
default: '5432',
},
},
async run({ args }) {
const config = await ensureConfig();
await studio(args, config);
},
}),
},
async run({ args }) {
// Set up logging level
if (args.verbose) {
consola.level = 4; // Debug level
}
// If no arguments provided, show help
consola.log('Use --help to see available commands');
},
});
// Handle unknown commands before running citty
async function handleUnknownCommand() {
const args = process.argv.slice(2);
if (args.length === 0) return false;
const command = args[0];
// If it's a help flag, let citty handle it
if (command === '--help' || command === '-h' || command === '--version' || command === '-v') {
return false;
}
// If it's a known command, let citty handle it
if (!shouldProxy(command)) {
return false;
}
// Handle unknown command by proxying to flyctl
try {
// Try to load config, but don't fail if not in a Nuxt project
let config = {};
try {
config = await loadConfig();
} catch (error) {
// Some flyctl commands don't require Nuxt project (like apps, auth, etc.)
consola.debug('Could not load config, proceeding without:', error.message);
config = { _runtime: {} };
}
// Parse the arguments manually for proxy
const parsedArgs = { _: args.slice(1) };
// Simple argument parsing for flags
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const nextArg = args[i + 1];
if (nextArg && !nextArg.startsWith('-')) {
parsedArgs[key] = nextArg;
i++; // Skip next arg as it's the value
} else {
parsedArgs[key] = true;
}
} else if (arg.startsWith('-') && arg.length === 2) {
const key = arg.slice(1);
const nextArg = args[i + 1];
if (nextArg && !nextArg.startsWith('-')) {
parsedArgs[key] = nextArg;
i++; // Skip next arg as it's the value
} else {
parsedArgs[key] = true;
}
}
}
await proxy(parsedArgs, config, command);
return true;
} catch (error) {
if (error instanceof NuxflyError) {
consola.error(error.message);
if (error.suggestion) {
consola.info(`💡 ${error.suggestion}`);
}
} else {
consola.error('An unexpected error occurred:', error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
}
process.exit(error.exitCode || 1);
}
}
// Check for unknown commands first
handleUnknownCommand().then((handled) => {
if (handled) return;
// Run the normal CLI if no unknown command was handled
runMain(main).catch((error) => {
if (error instanceof NuxflyError) {
consola.error(error.message);
if (error.suggestion) {
consola.info(`💡 ${error.suggestion}`);
}
} else {
consola.error('An unexpected error occurred:', error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
}
process.exit(error.exitCode || 1);
});
});