autosite-client
Version:
Client package for deploying React apps to Autosite servers
418 lines (361 loc) • 11.9 kB
JavaScript
/**
* 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();
}