UNPKG

@ton-contests/create-task

Version:

CLI tool to create new task for TON Contests platform

246 lines (245 loc) 7.51 kB
#!/usr/bin/env node import { rm } from "fs/promises"; import { resolve, join } from "path"; import { existsSync } from "fs"; import chalk from "chalk"; import { program } from "commander"; import { spawn } from "node:child_process"; import ora from "ora"; import { createPrompt, makeTheme, useState, useEffect, useKeypress, isEnterKey } from "@inquirer/core"; import figures from "figures"; function formatError(e) { return e instanceof Error ? e.message : typeof e === "string" ? e : JSON.stringify(e); } function spawnWithSpinner({ command, message, messageFail, messageSuccess, theme: { style, spinner: themeSpinner } }) { const spinner = ora({ text: style.message(message, "loading"), hideCursor: false, spinner: themeSpinner }).start(); const success = messageSuccess && style.message(messageSuccess, "done"); if (typeof command === "function") { return command().then(() => { spinner.succeed(success); }).catch((e) => { const errString = formatError(e); spinner.fail( style.error( messageFail ? typeof messageFail === "string" ? messageFail : messageFail(errString) : errString ) ); throw e; }); } return new Promise((res, rej) => { const proc = spawn(command, { shell: true }); let errBuf = Buffer.from([]); proc.stderr.on("data", (buf) => { errBuf = Buffer.concat([errBuf, buf]); spinner.suffixText = chalk.bgGray.italic(buf.toString()); }); proc.on("exit", (code) => { spinner.suffixText = ""; if (!code) { spinner.succeed(success); res(); return; } const errString = errBuf.length ? errBuf.toString() : `Error code: ${code}`; const errorMessage = style.error( messageFail ? typeof messageFail === "string" ? messageFail : messageFail(errString) : errString ); spinner.fail(errorMessage); rej(new Error(errorMessage)); }); }); } async function cloneTaskTemplate(rootDir, { clone: { https, ssh }, link }, theme) { const messageSuccess = `Cloned template: ${chalk.blue(link)}`; try { await spawnWithSpinner({ command: `git clone "${https}" "${rootDir}"`, message: `Cloning the template from GitHub (HTTPS): ${chalk.bold.blue( https )}`, messageFail: (error) => `Failed to load the template using HTTPS. ${error}`, messageSuccess, theme }); return; } catch { } } function isGitInstalled() { return new Promise((res) => { spawn("git --version", { shell: true }).on("exit", (code) => { res(!code); }); }); } function lines(...arr) { return arr.flat(1).filter((v) => typeof v === "string").join("\n"); } function spaces(...arr) { return arr.flat(1).filter((v) => typeof v === "string").join(" "); } const input = createPrompt( ({ theme, default: defaultValue, message, validate, hint, required }, done) => { const { style, prefix } = makeTheme(theme); const [value, setValue] = useState(""); const [error, setError] = useState(); const [completed, setCompleted] = useState(false); function confirm(value2) { setValue(value2); setError(void 0); setCompleted(true); done(value2); } useEffect(() => { if (completed) done(value); }, [completed, done, value]); useKeypress((key, rl) => { if (isEnterKey(key)) { if (error) { return rl.write(value); } return value ? confirm(value) : defaultValue ? confirm(defaultValue) : required ? setError("The value must be provided") : confirm(""); } if (key.name === "tab" && !value) { rl.clearLine(0); const v = defaultValue || ""; rl.write(v); setValue(v); return; } const input2 = rl.line; setError(input2 && validate && validate(input2) || void 0); setValue(input2); }); return [ spaces( prefix[completed ? "done" : "idle"], message && style.message(message, "idle"), // TODO: We need some specific style for it. style.placeholder(completed ? figures.ellipsis : figures.pointerSmall), completed ? style.answer(value) : value || (defaultValue ? style.defaultAnswer(defaultValue) : "") ), completed ? void 0 : lines(hint && style.help(hint), error && style.error(error)) ]; } ); function createCustomTheme() { return makeTheme({ style: { error(text) { return chalk.red(text); }, success(text) { return chalk.green(text); }, placeholder(text) { return chalk.dim(text); }, warning(text) { return chalk.yellow(`${figures.warning} ${text}`); } }, prefix: { idle: chalk.blue("?"), done: chalk.green(figures.tick), pointer: figures.arrowRight } }); } const templates = [ { dir: "/tact", clone: { https: "https://github.com/divatech-io/ton-contests-tact-template.git", ssh: "git@github.com:divatech-io/ton-contests-tact-template.git" }, link: "https://github.com/divatech-io/ton-contests-tact-template" }, { dir: "/tolk", clone: { https: "https://github.com/divatech-io/ton-contests-tolk-template.git", ssh: "git@github.com:divatech-io/ton-contests-tolk-template.git" }, link: "https://github.com/divatech-io/ton-contests-tolk-template" } ]; program.name("@ton-contests/create-task").description("CLI tool to create new task for TON Contests platform"); program.action(async () => { const theme = createCustomTheme(); if (!await isGitInstalled()) { console.log( theme.style.error( "To run this CLI tool, you must have git installed. Installation guide: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git" ) ); process.exit(1); } let rootDir = null; try { rootDir = await input({ message: "Directory name:", required: true, default: "ton-task", hint: "This directory will be used as a root directory for the task. It is allowed to use alphanumeric latin letters, dashes and dots.", theme, validate(value) { if ([".", ".."].includes(value)) { return "Value is not a valid directory name"; } if (!value.match(/^[a-zA-Z0-9\-.]+$/)) { return "Value contains invalid symbols"; } if (existsSync(resolve(value))) { return `Directory "${value}" already exists`; } } }); if (existsSync(resolve(rootDir))) { console.log(theme.style.error(`Directory "${rootDir}" already exists`)); process.exit(1); } } catch { process.exit(0); } try { for (const template of templates) { await cloneTaskTemplate(join(rootDir, template.dir), template, theme); } } catch { process.exit(1); } try { for (const template of templates) { await spawnWithSpinner({ message: "Removing the .git directory.", command: () => rm(resolve(join(rootDir, template.dir), ".git"), { recursive: true }), messageFail: (err) => `Failed to remove the .git directory. Error: ${err}`, messageSuccess: `${join(template.dir, ".git")} directory removed.`, theme }); } } catch { process.exit(1); } console.log( lines( theme.style.success("Task has been successfully initialized!"), chalk.bold("Happy coding! 🚀") ) ); }); program.parse();