@santerzet/botkit
Version:
CLI for Discord Bot
318 lines (257 loc) • 7.54 kB
text/typescript
import { fromAsync, isOk, isErr } from '@sapphire/result';
import { blueBright, red } from 'colorette';
import { execa } from 'execa';
import { cp, readFile, rm, writeFile } from 'node:fs/promises';
import prompts from 'prompts';
import { PromptNew, PromptNewObjectKey } from '#prompts/PromptNew';
import { Spinner } from '#functions/Spinner';
import { fileExists } from '#functions/FileExists';
import { repoUrl } from '#constants';
import { CommandExists } from '#functions/CommandExists';
async function editPackageJson(
location: string,
name: string,
description: string,
version: string,
heroku: boolean
) {
const pjLocation = `${location}/package.json`;
const output = JSON.parse(await readFile(pjLocation, 'utf8'));
if (!output) throw new Error("Can't read file.");
output.name = name.toLowerCase();
output.description = description;
output.version = version;
if (heroku)
output.scripts.heroku = `git add . && git commit -m \"Update bot\" && git push heroku main`;
const result = await fromAsync(() =>
writeFile(pjLocation, JSON.stringify(output, null, 2))
);
return isOk(result);
}
async function cloneRepo(location: string, verbose: boolean) {
const result = await fromAsync(async () =>
execa('git', ['clone', repoUrl, `${location}/ghr`], {
stdio: verbose ? 'inherit' : undefined,
})
);
if (isErr(result)) {
throw result.error;
}
if (result.value.exitCode !== 0) {
throw new Error(
'An unknown error occurred while cloning the repository. Try running BotKit CLI with "--verbose" flag.'
);
}
}
async function initializeGitRepo(location: string) {
await execa('git', ['init'], { cwd: `./${location}/` });
return true;
}
async function herokuLogin() {
const result = await fromAsync(() =>
execa('heroku', ['login'], {
stdin: 'inherit',
})
);
if (isErr(result)) {
throw result.error;
}
if (result.value.exitCode !== 0) {
throw result.error;
}
return true;
}
async function initializeHeroku(location: string, verbose: boolean) {
const result = await fromAsync(() =>
execa('heroku', ['create'], {
stdio: verbose ? 'inherit' : undefined,
cwd: `./${location}/`,
})
);
if (isErr(result)) {
throw result.error;
}
if (result.value.exitCode !== 0) {
throw new Error(
'An unknown error occurred while creating Heroku app. Try running BotKit CLI with "--verbose" flag.'
);
}
return true;
}
async function pushToHeroku(location: string, verbose: boolean) {
await execa('git', ['add', '.'], { cwd: `./${location}/` });
await execa('git', ['commit', '-m', '"Initial commit"'], {
cwd: `./${location}/`,
});
const result = await fromAsync(async () =>
execa('git', ['push', 'heroku', 'main'], {
stdio: verbose ? 'inherit' : undefined,
cwd: `./${location}/`,
})
);
if (isErr(result)) {
throw result.error;
}
if (result.value.exitCode !== 0) {
throw new Error(
'An unknown error occurred while pushing to Heroku. Try running BotKit CLI with "--verbose" flag.'
);
}
return true;
}
async function configureHeroku(location: string, verbose: boolean) {
const result = await fromAsync(async () =>
execa('heroku', ['ps:scale', 'web=0', 'worker=1'], {
stdio: verbose ? 'inherit' : undefined,
cwd: `./${location}/`,
})
);
if (isErr(result)) {
throw result.error;
}
if (result.value.exitCode !== 0) {
throw new Error(
'An unknown error occurred while configuring Heroku. Try running BotKit CLI with "--verbose" flag.'
);
}
return true;
}
async function installDeps(location: string, verbose: boolean) {
const result = await fromAsync(() =>
execa('npm', ['install'], {
stdio: verbose ? 'inherit' : undefined,
cwd: `./${location}/`,
})
);
if (isErr(result)) {
throw result.error;
}
if (result.value.exitCode !== 0) {
throw new Error(
'An unknown error occurred while installing dependencies. Try running BotKit CLI with "--verbose" flag.'
);
}
const oppositeLockfile = `./${location}/package-lock.json`;
if (await fileExists(oppositeLockfile)) {
await rm(oppositeLockfile);
}
return true;
}
async function runJob(
job: () => Promise<any>,
name: string,
showError: boolean = true
) {
const spinner = new Spinner(name).start();
const result = await fromAsync(async () => job());
if (isErr(result)) {
spinner.error();
if (showError) {
console.error(red((result.error as Error).message));
}
process.exit(1);
}
spinner.success();
return true;
}
export default async (name: string, flags: Record<string, boolean>) => {
const response = await prompts<PromptNewObjectKey>(
PromptNew(name, await CommandExists('heroku'))
);
if (!response.botName || !response.botStructure || !response.commands) {
process.exit(1);
}
let botTemplate = '';
if (response.botStructure === 'basic') {
if (response.commands === 'slashCmd') {
botTemplate = 'with-basic-slash-commands';
} else if (response.commands === 'prefixCmd') {
botTemplate = 'with-basic-prefix-commands';
} else if (response.commands === 'bothCmd') {
botTemplate = 'with-basic-both-commands';
}
} else if (response.botStructure === 'advanced') {
if (response.commands === 'slashCmd') {
botTemplate = 'with-advanced-slash-commands';
} else if (response.commands === 'prefixCmd') {
botTemplate = 'with-advanced-prefix-commands';
} else if (response.commands === 'bothCmd') {
botTemplate = 'with-advanced-both-commands';
}
}
if (response.heroku) response.git = true;
const botName = response.botName === '.' ? '' : response.botName;
const stpJob = async () => {
await cp(
`./${response.botName}/ghr/templates/${response.botStructure}/${botTemplate}/.`,
`./${response.botName}/`,
{ recursive: true }
);
if (response.git && !response.heroku) {
await cp(
`./${response.botName}/ghr/.gitignore`,
`./${response.botName}/.gitignore`
);
}
if (response.heroku) {
for (const p of ['.gitignore', 'Procfile', 'HEROKU.md']) {
await cp(
`./${response.botName}/ghr/resources/with-heroku/${p}`,
`./${response.botName}/${p}`
);
}
}
await rm(`./${response.botName}/ghr`, { recursive: true, force: true });
await editPackageJson(
response.botName,
botName,
response.botDescription,
response.botVersion,
response.heroku
);
};
const jobs: [() => any, string, boolean?][] = [];
if (response.heroku) {
jobs.push([
() => herokuLogin(),
'Logging in to Heroku, press any key to open the browser, or q to quit',
false,
]);
}
jobs.push([
() => cloneRepo(response.botName, flags.verbose),
'Cloning repository',
]);
jobs.push([stpJob, 'Setting up project']);
if (response.git) {
jobs.push([() => initializeGitRepo(response.botName), 'Initializing git']);
}
if (response.heroku) {
jobs.push([
() => initializeHeroku(response.botName, flags.verbose),
'Initializing Heroku',
]);
jobs.push([
() => pushToHeroku(response.botName, flags.verbose),
'Pushing to Heroku',
]);
jobs.push([
() => configureHeroku(response.botName, flags.verbose),
'Configuring Heroku',
]);
}
jobs.push([
() => installDeps(response.botName, flags.verbose),
'Installing dependencies',
]);
for (const [job, name, showError] of jobs) {
await runJob(job, name, showError).catch(() => process.exit(1));
}
if (response.heroku) {
const { stdout } = await execa('heroku', ['info'], {
cwd: `./${response.botName}/`,
});
console.log(`Information about your bot in Heroku:\n${stdout}`);
}
console.log(blueBright('Done!'));
};