UNPKG

autosite-client

Version:

Client package for deploying React apps to Autosite servers

418 lines (361 loc) 11.9 kB
#!/usr/bin/env node /** * AutoSite Client * A CLI tool for easily deploying React applications to AutoSite servers * * Usage: * npx autosite-client setup - Configure deployment settings * npx autosite-client deploy - Deploy the application * npx autosite-client status - Check server status * npx autosite-client config - Show or update configuration * * @license MIT * @version 1.0.0 */ const { program } = require("commander"); const chalk = require("chalk"); const inquirer = require("inquirer"); const fs = require("fs"); const path = require("path"); const archiver = require("archiver"); const FormData = require("form-data"); const fetch = require("node-fetch"); const ora = require("ora"); const Conf = require("conf"); // Configuration store const config = new Conf({ projectName: "autosite-client", defaults: { serverUrl: null, apiKey: null, }, }); // Version info program.version("1.0.0"); // Utility functions const findReactBuildDir = () => { // Common React build directories const possibleDirs = [ "build", // Create React App "dist", // Vite, Rollup, etc. "out", // Next.js "public/build", // Some custom configs ]; for (const dir of possibleDirs) { if (fs.existsSync(path.join(process.cwd(), dir))) { return dir; } } return null; }; const createZipArchive = async (buildDir) => { const spinner = ora("Creating deployment package...").start(); return new Promise((resolve, reject) => { try { const output = fs.createWriteStream( path.join(process.cwd(), "deploy.zip") ); const archive = archiver("zip", { zlib: { level: 9 }, // Maximum compression }); output.on("close", () => { spinner.succeed( `Created deployment package (${archive.pointer()} bytes)` ); resolve(path.join(process.cwd(), "deploy.zip")); }); archive.on("error", (err) => { spinner.fail("Failed to create deployment package"); reject(err); }); archive.pipe(output); archive.directory(path.join(process.cwd(), buildDir), false); archive.finalize(); } catch (error) { spinner.fail("Failed to create deployment package"); reject(error); } }); }; const uploadToServer = async (zipFile) => { const serverUrl = config.get("serverUrl"); const apiKey = config.get("apiKey"); if (!serverUrl || !apiKey) { throw new Error( 'Server URL and API key not configured. Run "autosite-client setup" first.' ); } const deployEndpoint = `${serverUrl.replace(/\/$/, "")}/deploy`; const spinner = ora(`Uploading to ${deployEndpoint}...`).start(); try { const formData = new FormData(); formData.append("bundle", fs.createReadStream(zipFile)); const response = await fetch(deployEndpoint, { method: "POST", body: formData, headers: { "X-API-Key": apiKey, }, }); const result = await response.json(); if (!response.ok) { spinner.fail(`Deployment failed: ${result.error || response.statusText}`); throw new Error(result.error || response.statusText); } spinner.succeed("Deployment successful!"); return result; } catch (error) { spinner.fail(`Deployment failed: ${error.message}`); throw error; } finally { // Clean up the zip file try { fs.unlinkSync(zipFile); } catch (err) { console.warn("Warning: Could not clean up temporary deployment package"); } } }; const checkServerStatus = async () => { const serverUrl = config.get("serverUrl"); const apiKey = config.get("apiKey"); if (!serverUrl || !apiKey) { throw new Error( 'Server URL and API key not configured. Run "autosite-client setup" first.' ); } const statusEndpoint = `${serverUrl.replace(/\/$/, "")}/status`; const spinner = ora("Checking server status...").start(); try { const response = await fetch(statusEndpoint, { headers: { "X-API-Key": apiKey, }, }); if (!response.ok) { spinner.fail(`Failed to check server status: ${response.statusText}`); throw new Error(response.statusText); } const status = await response.json(); spinner.succeed("Server status checked successfully"); return status; } catch (error) { spinner.fail(`Server status check failed: ${error.message}`); throw error; } }; // Commands program .command("setup") .description("Configure deployment settings") .option("-u, --url <url>", "Server URL") .option("-k, --key <key>", "API key") .action(async (options) => { try { let serverUrl = options.url; let apiKey = options.key; if (!serverUrl || !apiKey) { const answers = await inquirer.prompt([ { type: "input", name: "serverUrl", message: "Enter the server URL (e.g. http://example.com:8080):", default: config.get("serverUrl") || "http://localhost:8080", validate: (input) => input.trim() ? true : "Server URL is required", }, { type: "input", name: "apiKey", message: "Enter the API key:", default: config.get("apiKey") || "", validate: (input) => (input.trim() ? true : "API key is required"), }, ]); serverUrl = answers.serverUrl; apiKey = answers.apiKey; } // Clean the URL (remove trailing slash if present) serverUrl = serverUrl.replace(/\/$/, ""); config.set("serverUrl", serverUrl); config.set("apiKey", apiKey); console.log(chalk.green("✅ Configuration saved successfully")); console.log(`Server URL: ${chalk.blue(serverUrl)}`); console.log(`API Key: ${chalk.yellow(apiKey)}`); // Verify the configuration by checking the server status try { const result = await checkServerStatus(); console.log(chalk.green("✅ Server connection verified")); console.log("Server status:", result); } catch (error) { console.error( chalk.red("❌ Failed to connect to server:"), error.message ); console.log( chalk.yellow( "Configuration saved, but server connection failed. Please check your settings." ) ); } } catch (error) { console.error(chalk.red("❌ Setup failed:"), error.message); } }); program .command("deploy") .description("Deploy the React application to the configured server") .option("-d, --dir <directory>", "Custom build directory path") .action(async (options) => { try { // Find the build directory let buildDir = options.dir || findReactBuildDir(); if (!buildDir) { // If not found, prompt the user const answers = await inquirer.prompt([ { type: "input", name: "buildDir", message: "Build directory not found. Please specify the build directory:", validate: (input) => { const dirPath = path.join(process.cwd(), input.trim()); return fs.existsSync(dirPath) ? true : "Directory does not exist"; }, }, ]); buildDir = answers.buildDir; } console.log(chalk.blue(`📦 Using build directory: ${buildDir}`)); // Create a zip file of the build directory const zipFile = await createZipArchive(buildDir); // Upload to the server const result = await uploadToServer(zipFile); console.log(chalk.green("✅ Deployment completed successfully!")); console.log( `Deployed ${chalk.blue(result.files.length)} files at ${chalk.yellow( result.timestamp )}` ); console.log( chalk.green( `Your application is now live at: ${config.get("serverUrl")}` ) ); } catch (error) { console.error(chalk.red("❌ Deployment failed:"), error.message); } }); program .command("status") .description("Check the status of the configured server") .action(async () => { try { const status = await checkServerStatus(); console.log(chalk.green("✅ Server status:")); console.log( `Server uptime: ${chalk.blue( Math.floor(status.server.uptime / 60) )} minutes` ); console.log( `Application deployed: ${ status.deployment.deployed ? chalk.green("Yes") : chalk.yellow("No") }` ); if (status.deployment.deployed) { console.log(`Deployed files: ${chalk.blue(status.deployment.files)}`); console.log( `Last modified: ${chalk.blue(status.deployment.lastModified)}` ); } console.log(`\nServer: ${chalk.green(config.get("serverUrl"))}`); } catch (error) { console.error(chalk.red("❌ Status check failed:"), error.message); } }); program .command("config") .description("Show or update current configuration") .option("--show", "Show current configuration") .option("--clear", "Clear current configuration") .action(async (options) => { if (options.clear) { config.clear(); console.log(chalk.yellow("Configuration cleared")); return; } if (options.show || !options.clear) { const currentConfig = { serverUrl: config.get("serverUrl"), apiKey: config.get("apiKey"), }; console.log(chalk.green("Current configuration:")); Object.entries(currentConfig).forEach(([key, value]) => { console.log( `${key}: ${value ? chalk.blue(value) : chalk.grey("Not set")}` ); }); } }); // Development utilities program .command("dev-package") .description("Create a development package for testing") .action(async () => { try { // Get current directory name (project name) const projectName = path.basename(process.cwd()); console.log( chalk.blue(`Creating development package for ${projectName}...`) ); // Find the build directory or build the project let buildDir = findReactBuildDir(); if (!buildDir) { const { runBuild } = await inquirer.prompt([ { type: "confirm", name: "runBuild", message: "Build directory not found. Would you like to run the build command now?", default: true, }, ]); if (runBuild) { const spinner = ora("Running npm build...").start(); try { require("child_process").execSync("npm run build", { stdio: "inherit", }); spinner.succeed("Build completed"); buildDir = findReactBuildDir(); if (!buildDir) { spinner.fail("Build completed but build directory not found"); throw new Error("Build directory not found after build command"); } } catch (error) { spinner.fail("Build failed"); throw new Error(`Build command failed: ${error.message}`); } } else { throw new Error("Cannot continue without build directory"); } } // Create a zip file of the build directory const zipFile = await createZipArchive(buildDir); console.log(chalk.green(`✅ Development package created at: ${zipFile}`)); console.log( chalk.yellow( "You can manually upload this file to your deployment server for testing" ) ); } catch (error) { console.error(chalk.red("❌ Dev packaging failed:"), error.message); } }); // Parse arguments program.parse(process.argv); // Display help if no arguments provided if (!process.argv.slice(2).length) { program.outputHelp(); }