UNPKG

toystack

Version:

![Toystack CLI](https://framerusercontent.com/images/6KcXakCf8FI9LB2mJXNjcHIreDA.png)

492 lines (491 loc) 22.1 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkAuth = checkAuth; exports.checkForToken = checkForToken; const prompts_1 = require("@clack/prompts"); const prompts_2 = require("@clack/prompts"); const archiver_1 = __importDefault(require("archiver")); const axios_1 = __importDefault(require("axios")); const cli_table3_1 = __importDefault(require("cli-table3")); const dotenv_1 = __importDefault(require("dotenv")); const fs_1 = require("fs"); const fs_2 = require("fs"); const os_1 = require("os"); const path_1 = __importStar(require("path")); const process_1 = __importDefault(require("process")); const promises_1 = require("timers/promises"); const uuid_1 = require("uuid"); const yaml_1 = __importDefault(require("yaml")); const operations_1 = require("./operations"); const utils_1 = require("./utils"); const CLI_DIR = (0, path_1.join)((0, os_1.homedir)(), ".toystack"); const CONFIG_FILE = (0, path_1.join)(CLI_DIR, "cli.yaml"); dotenv_1.default.config(); const API_BASE_URL = process_1.default.env.API_BASE_URL || "https://core.toystack.ai"; const ROUTE_ID = process_1.default.env.ROUTE_ID || "LOqub2Hr6NyBOIPYASlbfYGq22VOmzzxjdrRlc"; const currentDir = process_1.default.cwd(); /* eslint-disable no-undef */ const packagePath = path_1.default.resolve(__dirname, "../package.json"); const pkg = JSON.parse((0, fs_1.readFileSync)(packagePath, "utf8")); /* eslint-enable no-undef */ function cli() { return __awaiter(this, void 0, void 0, function* () { const args = process_1.default.argv.slice(2); if (args.length > 0 && args[0] === "deploy") { const userName = (0, utils_1.getGitUserName)(); const framework = (0, utils_1.identifyFramework)(currentDir); if (args.length === 1) { if ((0, utils_1.pathExists)(CONFIG_FILE)) { //deploy const repositoryDetails = (0, utils_1.getCurrentRepositoryAndBranch)(); if (repositoryDetails) { const { repositoryName, branchName } = repositoryDetails; (0, prompts_1.outro)(`Deploying current repository: ${userName}/${repositoryName} on branch: ${branchName}`); yield executeCommand("deployApp", `${userName}/${repositoryName}`, branchName); } else { (0, prompts_1.outro)("Error: Could not determine repository or branch. Are you inside a Git repository?"); } } else if (framework === "React") { console.log(`Framework ${framework}`); yield deploy("./build"); } else if (framework === "Astro") { console.log(`Framework ${framework}`); yield deploy("./dist"); } else { (0, prompts_1.outro)("Please log in to deploy backend apps."); process_1.default.exit(0); } } else if (args.length === 2) { const [, buildFolder] = args; yield deploy(`./${buildFolder}`); } else if (args.length === 3) { //deploy repo branch const [, repositoryName, branchName] = args; (0, prompts_1.outro)(`Deploying specified repository: ${userName}/${repositoryName} on branch: ${branchName}`); yield executeCommand("deployApp", `${userName}/${repositoryName}`, branchName); } else { (0, prompts_1.outro)("Usage: npx toystack deploy [repo-name branch-name]"); } } else if (args.length > 0 && (args[0] === "-v" || args[0] === "--version")) { console.log(pkg.version); } else if (args.length > 0) { executeCommand(args[0]); } else { (0, prompts_1.intro)("Welcome to Toystack CLI!"); const command = yield (0, prompts_1.select)({ message: "Choose a command to execute:", options: [ { value: "login", label: "Login" }, { value: "deploy", label: "Deploy" }, { value: "ls", label: "List" }, { value: "help", label: "Help" }, { value: "logout", label: "Logout" }, ], }); if (command) { yield executeCommand(command); } else { (0, prompts_1.outro)("Operation cancelled"); process_1.default.exit(0); } } }); } function executeCommand(command, repositoryName, branchName) { return __awaiter(this, void 0, void 0, function* () { let framework = (0, utils_1.identifyFramework)(currentDir); switch (command) { case "help": (0, prompts_1.outro)("help queried"); break; case "deploy": if ((0, utils_1.pathExists)(CONFIG_FILE)) { const userName = (0, utils_1.getGitUserName)(); const repositoryDetails = (0, utils_1.getCurrentRepositoryAndBranch)(); if (repositoryDetails) { const { repositoryName, branchName } = repositoryDetails; (0, prompts_1.outro)(`Deploying current repository: ${userName}/${repositoryName} on branch: ${branchName}`); yield executeCommand("deployApp", `${userName}/${repositoryName}`, branchName); } else { (0, prompts_1.outro)("Error: Could not determine repository or branch. Are you inside a Git repository?"); } } else if (framework === "React" || framework === "Astro") { console.log(`Framework ${framework}`); yield deploy(framework === "React" ? "./build" : "./dist"); } else { (0, prompts_1.outro)("Please log in to deploy backend apps."); process_1.default.exit(0); } break; case "ls": { const repositories = yield (0, operations_1.listAllRepositories)(); const table = new cli_table3_1.default({ head: ["ID", "Name"], }); repositories.forEach((repo) => { table.push([repo.id, repo.name]); }); console.log(table.toString()); break; } case "login": yield login(); break; case "deployApp": { const s = (0, prompts_1.spinner)(); //for auth if (repositoryName && branchName) { if (framework === "React" || framework === "Astro") { // frontend deployment console.log(`Framework : ${framework}`); s.message(`Building project in ${currentDir}`); console.log(`Building project in ${currentDir}`); const buildFolder = framework === "React" ? `./build` : `./dist`; const zipFileName = `${repositoryName}-${Date.now()}.zip`; const repositoryNameWithoutOwner = path_1.default.basename(zipFileName); const zipFilePath = (0, path_1.join)(currentDir, repositoryNameWithoutOwner); yield (0, utils_1.buildProject)(currentDir); if (!(0, fs_2.existsSync)(buildFolder)) throw new Error(`Build folder ${buildFolder} not found`); yield zipDirectory(buildFolder, zipFilePath); s.message("zipping completed. Preparing to deploy..."); console.log("zipping completed. Preparing to deploy..."); const result = yield (0, operations_1.uploadUserCode)(zipFilePath, repositoryName, branchName); s.message("Deploying..."); if (!result.deploymentId) s.stop("Deployment failed"); s.start("Waiting for deployment..."); const output = yield (0, operations_1.pullForDeploymentStatus)(result.deploymentId); if (output === null || output === void 0 ? void 0 : output.url) { s.stop(`Application deployed at ${output.url}`); } else { s.stop("Deployment failed"); } } else { //backend const result = yield (0, operations_1.deployBranch)(repositoryName, branchName); (0, prompts_1.outro)(`Deployment of ${repositoryName} (branch: ${branchName}) initiated, id: ${result.deployBranchViaCLI.id}`); s.start("Waiting for deployment..."); const output = yield (0, operations_1.pullForDeploymentStatus)(result.deployBranchViaCLI.id); if (output === null || output === void 0 ? void 0 : output.url) { s.stop(`Application deployed at ${output.url}`); } else { s.stop("Deployment failed"); } } } else { (0, prompts_1.outro)("Repository name and branch name are required for deployment.."); } break; } case "logout": if ((0, utils_1.pathExists)(CLI_DIR)) logout(); break; case "-v": case "--version": console.log(pkg.version); break; default: (0, prompts_1.outro)("unknown"); } }); } function deploy(buildFolder) { return __awaiter(this, void 0, void 0, function* () { const s = (0, prompts_1.spinner)(); s.start(); const repositoryName = (0, utils_1.getCurrentRepositoryName)(); const zipFileName = `${repositoryName}-${Date.now()}.zip`; const zipFilePath = (0, path_1.join)(currentDir, zipFileName); try { s.message("preparing to zip your codebase..."); s.message(`Building project in ${currentDir}`); yield (0, utils_1.buildProject)(currentDir); if (!(0, fs_2.existsSync)(buildFolder)) throw new Error("Build folder not found"); yield zipDirectory(buildFolder, zipFilePath); s.message("zipping completed. Preparing to deploy..."); yield (0, operations_1.uploadUserCode)(zipFilePath); s.message("Deploying..."); const { url, timeToLive } = yield (0, operations_1.createCLIFrontendDeployment)(zipFileName); if (!url) { s.stop("Deployment failed"); } s.stop(`✨ Deployed successfully!\n` + `🌐 URL: ${url}\n` + `⏳ Link active for: ${timeToLive} minutes\n` + `💡 Log in or sign up to unlock the full benefits of Toystack`); yield fs_2.promises.unlink(zipFilePath); } catch (err) { console.log(err); s.stop(`Deployment Failed`); throw new Error("Failed to deploy"); } }); } function zipDirectory(sourceDir, outputPath) { return __awaiter(this, void 0, void 0, function* () { const archive = (0, archiver_1.default)("zip", { zlib: { level: 9 } }); // Compression level const output = (0, fs_1.createWriteStream)(outputPath); return new Promise((resolve, reject) => { output.on("close", () => { console.log("\nZip file created successfully!"); resolve(); }); archive.on("error", (err) => { reject(err); }); archive.pipe(output); archive.directory(sourceDir, false); // false: don't include the source directory itself archive.finalize(); }); }); } function login() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; (0, prompts_1.intro)("Toystack CLI Login"); const s = (0, prompts_1.spinner)(); try { const clientId = (0, uuid_1.v4)(); s.start("Initializing login..."); const { data: authData } = yield axios_1.default.post(`${API_BASE_URL}/auth/device/code`, { clientId, }, { headers: { "x-route-id": ROUTE_ID, }, }); const deviceCode = authData.deviceCode; s.stop(); (0, prompts_2.note)(`Complete login in the Browser with URL: ${authData.verificationUri}/${clientId}`); // Open browser // await openUrl(`${authData.verificationUri}/${clientId}`); s.start("Waiting for login..."); const tokens = yield pollForAuthentication(clientId, deviceCode); yield saveConfig({ version: 1, workspace: "", workspace_name: "", api: { key: tokens.token, expires_at: Math.floor(Date.now() / 1000) + tokens.expiresIn, host: API_BASE_URL || ``, refresh_token: tokens.refreshToken, }, dashboard_url: "https://dashboard.toystack.ai/", }); s.stop("Successfully authenticated! 🎉"); } catch (error) { console.log("error", error); s.stop("Login failed!"); if (axios_1.default.isAxiosError(error)) { (0, prompts_1.outro)(`Error: ${((_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || error.message}`); } else { (0, prompts_1.outro)("An unexpected error occurred"); } process_1.default.exit(1); } }); } function pollForAuthentication(clientId, deviceCode) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const pollInterval = 5000; let isPolling = true; // Handle SIGINT (Ctrl+C) process_1.default.on("SIGINT", () => { isPolling = false; (0, prompts_1.outro)("Login cancelled"); process_1.default.exit(0); }); // Handle process termination process_1.default.on("SIGTERM", () => { isPolling = false; (0, prompts_1.outro)("Login cancelled"); process_1.default.exit(0); }); const s = (0, prompts_1.spinner)(); s.start("Waiting for authentication..."); try { while (isPolling) { try { yield (0, promises_1.setTimeout)(pollInterval); const { data } = yield axios_1.default.post(`${API_BASE_URL}/auth/device/token`, { clientId, deviceCode, grantType: "urn:ietf:params:oauth:grant-type:device_code", }, { headers: { "x-route-id": ROUTE_ID, }, }); s.stop("Authentication successful!"); return { token: data.token, accessToken: data.accessToken, refreshToken: data.refreshToken, expiresIn: data.expiresIn, }; } catch (error) { if (axios_1.default.isAxiosError(error) && ((_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) === "authorization_pending") { yield (0, promises_1.setTimeout)(pollInterval); continue; } s.stop("Authentication failed"); throw error; } } throw new Error("Polling stopped"); } catch (error) { s.stop("Authentication failed"); throw error; } finally { process_1.default.removeAllListeners("SIGINT"); process_1.default.removeAllListeners("SIGTERM"); } }); } function logout() { return __awaiter(this, void 0, void 0, function* () { const s = (0, prompts_1.spinner)(); s.start("Logging out..."); try { (0, fs_2.rmSync)(CLI_DIR, { recursive: true, force: true }); s.stop("Successfully logged out!"); } catch (error) { console.log(error); s.stop("Logout failed!"); (0, prompts_1.outro)("Error clearing credentials"); process_1.default.exit(1); } }); } // Utility functions function saveConfig(config) { return __awaiter(this, void 0, void 0, function* () { try { yield fs_2.promises.mkdir(CLI_DIR, { recursive: true }); yield fs_2.promises.writeFile(CONFIG_FILE, yaml_1.default.stringify(config)); } catch (error) { throw new Error("Failed to save CLI configuration"); } }); } function loadConfig() { return __awaiter(this, void 0, void 0, function* () { try { const content = yield fs_2.promises.readFile(CONFIG_FILE, "utf8"); return yaml_1.default.parse(content); } catch (error) { return null; } }); } function checkForToken() { return __awaiter(this, void 0, void 0, function* () { const config = yield loadConfig(); return config === null || config === void 0 ? void 0 : config.api.key; }); } function checkAuth() { return __awaiter(this, void 0, void 0, function* () { var _a; const config = yield loadConfig(); if (!((_a = config === null || config === void 0 ? void 0 : config.api) === null || _a === void 0 ? void 0 : _a.key)) { (0, prompts_1.outro)("Please login first using: toystack login"); process_1.default.exit(1); } // Check if token is expired if (config.api.expires_at && Date.now() / 1000 > config.api.expires_at) { try { const s = (0, prompts_1.spinner)(); s.start("Refreshing session..."); // Attempt to refresh token const { data } = yield axios_1.default.post(`${config.api.host}/auth/refresh`, { refresh_token: config.api.refresh_token, }); yield saveConfig(Object.assign(Object.assign({}, config), { api: Object.assign(Object.assign({}, config.api), { key: data.token, // Expires in 30 days expires_at: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, refresh_token: data.refresh_token }) })); s.stop("Session refreshed successfully!"); return data.access_token; } catch (error) { (0, prompts_1.outro)("Session expired. Please login again."); process_1.default.exit(1); } } return config.api.key; }); } const openUrl = (url) => __awaiter(void 0, void 0, void 0, function* () { const open = (yield Promise.resolve().then(() => __importStar(require("open")))).default; return open(url); }); cli().catch(console.error);