toystack
Version:

492 lines (491 loc) • 22.1 kB
JavaScript
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);
;