@ton-contests/create-task
Version:
CLI tool to create new task for TON Contests platform
246 lines (245 loc) • 7.51 kB
JavaScript
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();