taptap-cli
Version:
A simple and powerful CLI tool to deploy static HTML/CSS/JS projects directly from your terminal.
688 lines (606 loc) โข 20.1 kB
JavaScript
const inquirer = require("inquirer");
const chalk = require("chalk");
const ora = require("ora");
const path = require("path");
const fs = require("fs");
const axios = require("axios");
const FormData = require("form-data");
const { v4: uuidv4 } = require("uuid");
const zipFolder = require("../utils/zipFolder");
const previewSite = require("../utils/localServer");
const pkg = require("../package.json");
const auth = require("../utils/auth");
const API_BASE = "https://api.checkscript.site";
const open = require("open");
const semver = require("semver");
const { showHelp, showVersion, showAbout } = require("../utils/help");
const handleAgentCommand = require("../utils/agent-handler");
(async () => {
const args = process.argv.slice(2);
let user = null;
const safeArgs = [
// Authentication commands
"--login",
"login",
"-l",
"--l",
"--logout",
"logout",
"--logout --silent",
"logout -s",
"--register",
"register",
"-r",
// Utility commands (no auth required)
"--version",
"version",
"-v",
"--v",
"--about",
"about",
"-a",
"--a",
"--help",
"help",
"-h",
"--h",
"--update",
"update",
"-u",
"--u",
// Core commands (no auth required)
"--init",
"init",
"-i",
"--i",
"--preview",
"preview",
"-p",
"--p",
];
const isSafe = args.some((arg) => safeArgs.includes(arg));
const isSafeCommand = args.some((arg) => safeArgs.includes(arg));
const CLI_ROOT = path.resolve(__dirname, "..");
const consentFile = path.join(CLI_ROOT, ".taptap_mit_consent");
async function ensureLicenseAccepted() {
if (!fs.existsSync(consentFile)) {
console.log(
"\n๐ This software is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC-BY-NC-ND-4.0).\n"
);
console.log("By using Taptap CLI, you agree to the following terms:\n");
console.log(
"โ
Attribution: You must give appropriate credit to the original author"
);
console.log(
"โ NonCommercial: You may not use this software for commercial purposes"
);
console.log(
"โ NoDerivatives: You may not modify or create derivative works\n"
);
console.log(
"๐ By using this tool, you also agree to the terms in terms.txt or policy.md\n"
);
console.log(
"๐ Full license: https://creativecommons.org/licenses/by-nc-nd/4.0/\n"
);
const { accepted } = await inquirer.prompt([
{
type: "confirm",
name: "accepted",
message: "Do you accept the CC-BY-NC-ND-4.0 license terms?",
default: false,
},
]);
if (!accepted) {
console.log(
"โ You must accept the license terms to use this software."
);
process.exit(1);
}
fs.writeFileSync(consentFile, "CC-BY-NC-ND-4.0 license accepted");
}
}
await ensureLicenseAccepted();
if (!isSafeCommand) {
user = auth.getAuth();
if (!user) {
console.log(
chalk.red(
"๐ซ You must be logged in to use this command. Run `taptap login` first."
)
);
process.exit(1);
}
}
if (
args.includes("--register") ||
args[0] === "register" ||
args[0] === "--r"
) {
const { registerCLIUser } = require("../utils/auth");
await registerCLIUser();
return;
}
// โ
Login handler
if (args.includes("--login") || args[0] === "login") {
const auth = require("../utils/auth");
if (args.includes("--direct")) {
await auth.directLogin();
} else {
await auth.browserLogin(args.includes("--open"));
}
return;
}
// โ
Logout handler
if (args.includes("--logout") || args[0] === "logout" || args[0] === "--l") {
const silent = args.includes("--silent") || args.includes("-s");
const auth = require("../utils/auth");
auth.logout();
if (!silent) console.log("๐ Logged out successfully.");
process.exit(0);
}
const notifierModule = await import("update-notifier");
const updateNotifier = notifierModule.default;
const notifier = updateNotifier({ pkg, updateCheckInterval: 0 });
if (notifier.update && semver.gt(notifier.update.latest, pkg.version)) {
console.log(
chalk.yellow(
`\n๐จ New version available: ${notifier.update.latest}. You're using ${pkg.version}`
)
);
console.log(`Run ${chalk.cyan(`npm i -g taptap-cli`)} to update.\n`);
const isMajorUpdate =
semver.major(notifier.update.latest) > semver.major(pkg.version);
if (isMajorUpdate) {
console.log(
chalk.red(
"โ ๏ธ Mandatory major update required! Please update the CLI.\n"
)
);
process.exit(1);
}
}
function initProject() {
const files = {
"index.html":
'<!DOCTYPE html>\n<html>\n<head>\n <title>My Site</title>\n <link rel="stylesheet" href="style.css">\n</head>\n<body>\n <h1>Hello from Taptap CLI!</h1>\n <script src="script.js"></script>\n</body>\n</html>',
"style.css":
"body { font-family: Arial; background: #f2f2f2; text-align: center; padding: 50px; }",
"script.js": 'console.log("Welcome to Taptap CLI Project!");',
"README.md": "# My Static Site\nDeployed with Taptap CLI.",
};
for (const [file, content] of Object.entries(files)) {
if (!fs.existsSync(file)) {
fs.writeFileSync(file, content);
console.log(chalk.green(`โ Created ${file}`));
} else {
console.log(chalk.yellow(`โ ${file} already exists, skipping.`));
}
}
console.log(
chalk.blueBright("\nโ
Project initialized. Start building!\n")
);
}
function showLogs() {
const logFile = path.join(__dirname, "..", ".taptap-logs.json");
if (!fs.existsSync(logFile)) {
console.log(chalk.red("๐ซ No deploy logs found."));
return;
}
let logs;
try {
const raw = fs.readFileSync(logFile, "utf-8").trim();
if (!raw) throw new Error("Empty log file");
logs = JSON.parse(raw);
} catch (err) {
console.log(chalk.red("๐ซ Failed to read logs: Invalid or empty JSON."));
return;
}
if (!logs.length) {
console.log(chalk.red("๐ซ No entries in logs."));
return;
}
console.log(chalk.blueBright("\n๐ฆ Deployment Logs:\n"));
logs.forEach((log, index) => {
console.log(`${index + 1}. ${chalk.green(log.project)} - ${log.url}`);
console.log(` ${chalk.gray(new Date(log.timestamp).toLocaleString())}`);
});
}
if (args.includes("--whoami") || args[0] === "whoami" || args[0] === "-w") {
const { getAuth } = require("../utils/auth");
const auth = getAuth();
if (!auth) {
console.log(chalk.red("๐ซ Not logged in. Use `taptap login` first."));
process.exit(1);
}
console.log(chalk.green.bold("\n๐ค Logged in as:"));
console.log(`๐ง Email : ${chalk.cyan(auth.email)}`);
console.log(`๐ Name : ${chalk.cyan(auth.name || "Unknown")}`);
console.log(`๐ Account Number : ${chalk.cyan(auth.unique_id || "N/A")}`);
console.log(`๐ Since : ${new Date(auth.loggedInAt).toLocaleString()}\n`);
process.exit(0);
}
if (
args.includes("--init") ||
args[0] === "init" ||
args[0] === "-i" ||
args[0] === "--i"
) {
initProject();
} else if (
args.includes("--logs") ||
args[0] === "logs" ||
args[0] === "-l" ||
args[0] === "--l"
) {
showLogs();
} else if (
args.includes("--update") ||
args[0] === "update" ||
args[0] === "-u"
) {
console.log(
chalk.cyan(
`\nYou're on v${pkg.version}. Latest is ${
notifier.update?.latest || "same"
}.\n`
)
);
process.exit(0);
}
if (
args.includes("--preview") ||
args[0] === "preview" ||
args[0] === "-p" ||
args[0] === "--p"
) {
await previewSite();
return;
}
// --deploy-list
if (
args.includes("--deploy-list") ||
args[0] === "deploy-list" ||
args[0] === "-dl" ||
args[0] === "--dl"
) {
const { confirm } = await inquirer.prompt([
{
type: "input",
name: "confirm",
message: "Fetch your deployments linked to your account? (y/n):",
validate: (input) => {
const val = input.trim().toLowerCase();
return ["y", "n", "yes", "no"].includes(val) || "Please enter y/n";
},
},
]);
const answer = confirm.trim().toLowerCase();
if (answer !== "y" && answer !== "yes") {
console.log(chalk.yellow("โ Operation cancelled."));
return;
}
const spinner = ora("๐ Fetching your deployments...").start();
try {
const { data } = await axios.get(
`${API_BASE}/deployments/${user.unique_id}`,
{
headers: {
"x-user-uuid": user.uuid,
"x-user-email": user.email,
},
}
);
spinner.succeed(`โ
Found ${data.deployments.length} deployment(s):\n`);
data.deployments.forEach((entry, idx) => {
console.log(`${idx + 1}. ๐ ${chalk.green(entry.site)}`);
console.log(` ๐ Inspect: ${chalk.yellow(entry.inspect)}\n`);
});
} catch (err) {
spinner.fail("No deployments found.");
console.error(chalk.red(err.message));
}
return;
}
if (
args.includes("--open") ||
args[0] === "open" ||
args[0] === "-o" ||
args[0] === "--o"
) {
const logFile = path.join(__dirname, "..", ".taptap-logs.json");
if (!fs.existsSync(logFile)) {
console.log(chalk.red("๐ซ No deploy logs found."));
process.exit(1);
}
const logs = JSON.parse(fs.readFileSync(logFile));
if (!logs.length) {
console.log(chalk.red("๐ซ No deployments found."));
process.exit(1);
}
const latest = logs[logs.length - 1];
console.log(chalk.blue(`๐ Opening ${latest.url}...`));
await open(latest.url);
return;
}
// --agent
if (args.includes("--agent" ) || args[0] === "agent" ) {
const promptIndex = args.findIndex(arg => arg === '--prompt' || arg === 'prompt');
if (promptIndex === -1 || !args[promptIndex + 1]) {
console.log(chalk.red("โ Error: The --agent flag requires a --prompt."));
console.log(chalk.yellow("๐ก Usage: taptap --agent --prompt \"Your request\""));
process.exit(1);
}
const userPrompt = args[promptIndex + 1];
await handleAgentCommand(userPrompt);
return;
}
// --delete
if (
args.includes("--delete") ||
args[0] === "delete" ||
args[0] === "-del" ||
args[0] === "-d" ||
args[0] === "--d"
) {
const spinner = ora("๐ Fetching deployments...").start();
try {
const { data } = await axios.get(
`${API_BASE}/deployments/${user.unique_id}`,
{
headers: {
"x-user-uuid": user.uuid,
"x-user-email": user.email,
},
}
);
spinner.stop();
if (!data.deployments.length) {
console.log("๐ซ No deployments found.");
return;
}
const choices = data.deployments.map((entry, i) => ({
name: `${i + 1}. ${entry.site}`,
value: entry.site.split("/")[4], // project name or domain
}));
const { uuid } = await inquirer.prompt([
{
name: "uuid",
type: "list",
message: "Select deployment to delete:",
choices,
},
]);
const deleteSpinner = ora("๐๏ธ Deleting deployment...").start();
await axios.delete(`${API_BASE}/deployments/${uuid}`, {
headers: {
"x-user-uuid": user.uuid,
"x-user-email": user.email,
},
});
deleteSpinner.succeed(chalk.red(`โ
Deleted deployment: ${uuid}`));
} catch (err) {
spinner.fail("Failed to fetch or delete.");
console.error(chalk.red(err.message));
}
return;
}
// --version
if (
args.includes("--version") ||
args[0] === "version" ||
args[0] === "-v" ||
args[0] === "--v"
) {
showVersion();
process.exit(0);
}
if (
process.argv.length === 2 ||
process.argv.includes("--help") ||
process.argv.includes("-h") ||
args[0] === "help" ||
args[0] === "--h"
) {
showHelp();
process.exit(0);
}
// --about
if (
args.includes("--about") ||
args[0] === "about" ||
args[0] === "-a" ||
args[0] === "--a"
) {
showAbout();
process.exit(0);
}
// --deploy
if (
args.includes("--deploy") ||
args[0] === "-d" ||
args[0] === "deploy" ||
args[0] === "--d"
) {
const { confirm, title } = await inquirer.prompt([
{
type: "input",
name: "confirm",
message: "Proceed with deployment linked to your account? (y/n):",
validate: (input) => {
const val = input.trim().toLowerCase();
return ["y", "n", "yes", "no"].includes(val) || "Please enter y/n";
},
},
{
name: "title",
message: "Optional: Enter project title (for logs only):",
},
]);
const confirmAnswer = confirm.trim().toLowerCase();
if (confirmAnswer !== "y" && confirmAnswer !== "yes") {
console.log(chalk.yellow("โ Deployment cancelled."));
return;
}
let projectId = uuidv4();
const domainIndex = args.findIndex((arg) => arg === "--domain");
if (domainIndex !== -1 && args[domainIndex + 1]) {
const customId = args[domainIndex + 1].trim().toLowerCase();
if (!/^[a-z0-9\-]+$/i.test(customId)) {
console.log(
chalk.red(
"โ Invalid domain name. Use only letters, numbers, and dashes."
)
);
return;
}
projectId = customId;
}
const zipPath = path.resolve(__dirname, "..", `${projectId}.zip`);
// Step 1: Check for index.html
const spinner = ora("๐ Looking for index.html...").start();
const indexPath = path.join(process.cwd(), "index.html");
if (!fs.existsSync(indexPath)) {
spinner.fail("index.html not found in current folder!");
return;
}
spinner.succeed("โ
Found index.html");
// Step 2: Zip folder
const zipSpinner = ora("").start();
try {
// const files = fs.readdirSync(process.cwd());
// files.forEach(f => console.log(` - ${f}`));
await zipFolder(process.cwd(), zipPath);
zipSpinner.succeed("โ
Folder zipped");
} catch (err) {
zipSpinner.fail("โ Zipping failed");
console.error(chalk.red(err.message));
return;
}
// Step 3: Validate zip file
const validateSpinner = ora("๐ Validating zip file...").start();
try {
const zipStats = fs.statSync(zipPath);
if (zipStats.size === 0) {
validateSpinner.fail("โ Zip file is empty!");
return;
}
if (zipStats.size > 250 * 1024 * 1024) {
// 250MB limit
validateSpinner.fail("โ Zip file too large (>250MB)!");
return;
}
validateSpinner.succeed("โ
Zip file validated");
} catch (err) {
validateSpinner.fail("โ Zip validation failed");
console.error(chalk.red(err.message));
return;
}
// Step 4: Upload with better error handling
const uploadSpinner = ora("๐ Uploading to live server...").start();
try {
const form = new FormData();
form.append("site", fs.createReadStream(zipPath));
form.append("reg_no", user.unique_id);
form.append("project_name", projectId); // UUID
// ๐ง Add timeout and better headers
const config = {
headers: {
...form.getHeaders(),
"User-Agent": "LiveServe-CLI/1.0.16",
"x-user-uuid": user.uuid,
"x-user-email": user.email,
"x-endpoint": "deploy",
},
timeout: 60000,
maxContentLength: 250 * 1024 * 1024,
maxBodyLength: 250 * 1024 * 1024,
};
// console.log(chalk.blue(`๐ค Uploading ${(fs.statSync(zipPath).size / 1024).toFixed(2)} KB...`));
const res = await axios.post(`${API_BASE}/upload`, form, config ,{ timeout: 30000 });
uploadSpinner.succeed("โ
Upload successful!");
console.log(`\n๐ ${chalk.green(res.data.url)}`);
console.log(`๐ Inspect: ${chalk.yellow(`${res.data.url}list/`)}\n`);
const logFile = path.join(__dirname, "..", ".taptap-logs.json");
let logs = [];
if (fs.existsSync(logFile)) {
logs = JSON.parse(fs.readFileSync(logFile));
}
logs.push({
project: title || "Untitled",
url: res.data.url,
timestamp: Date.now(),
});
fs.writeFileSync(logFile, JSON.stringify(logs, null, 2));
// ๐ง Test the deployed site
const testSpinner = ora("๐งช Testing deployed site...").start();
try {
const testRes = await axios.get(res.data.url, { timeout: 30000 });
if (testRes.status === 200) {
testSpinner.succeed("โ
Site is live and accessible!");
}
} catch (testErr) {
testSpinner.warn(
"โ ๏ธ Site deployed but may not be immediately accessible"
);
}
} catch (err) {
uploadSpinner.fail("โ Deployment failed.");
if (err.code === "ECONNABORTED") {
console.error(
chalk.red(
"โ Upload timeout - file may be too large or connection slow"
)
);
} else if (err.response) {
console.error(
chalk.red(
`โ Server error ${err.response.status}: ${err.response.statusText}`
)
);
// ๐ง Better error details
if (err.response.status === 502) {
console.error(chalk.yellow("๐ก 502 Bad Gateway usually means:"));
console.error(chalk.yellow(" โข Server is temporarily down"));
console.error(chalk.yellow(" โข Zip file format issue"));
console.error(chalk.yellow(" โข Try again in a few minutes"));
}
if (
err.response.data &&
typeof err.response.data === "string" &&
err.response.data.length < 500
) {
console.error(chalk.red(`Response: ${err.response.data}`));
}
if (err.response) {
const status = err.response.status;
const msg = err.response.data?.message || "";
if (status === 409) {
console.error(
chalk.yellow("โ ๏ธ The domain name you provided is already in use.")
);
console.error(
chalk.yellow("๐ก Tip: Use a different domain with: ") +
chalk.cyan("--domain yourname")
);
}
if (msg && msg.length < 500) {
console.error(chalk.red(`๐ข Message: ${msg}`));
}
}
} else if (err.request) {
console.error(
chalk.red("โ No response from server - check internet connection")
);
} else {
console.error(chalk.red(`โ Error: ${err.message}`));
}
} finally {
// Step 5: Cleanup (but keep debug copy)
const cleanSpinner = ora("๐งน Cleaning up temp files...").start();
cleanSpinner.succeed("โ
Cleanup done ");
}
return;
}
})();