dressed
Version:
A sleek, serverless-ready Discord bot framework.
140 lines • 6.5 kB
JavaScript
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { cwd, exit } from "node:process";
import { Command, InvalidArgumentError } from "commander";
import { parse } from "dotenv";
import Enquirer from "enquirer";
import build from "../server/build/build.js";
import bundleFiles from "../server/build/bundle.js";
import { categoryExports, importString } from "../utils/build.js";
import { logDefer, logError, logSuccess } from "../utils/log.js";
const program = new Command().name("dressed").description("A sleek, serverless-ready Discord bot framework.");
program
.command("build")
.description("Builds the bot and writes to .dressed")
.option("-i, --instance", "Include code to start a server instance")
.option("-r, --register", "Include code to register commands")
.option("-e, --endpoint <endpoint>", "The endpoint to listen on, defaults to `/`")
.option("-p, --port <port>", "The port to listen on, defaults to `8000`", (v) => {
const parsed = parseInt(v, 10);
if (Number.isNaN(parsed) || parsed < 0 || parsed > 65535) {
throw new InvalidArgumentError("Port must be a valid TCP/IP network port number (0-65535)");
}
return parsed;
})
.option("-R, --root <root>", "Source root for the bot, defaults to `src`")
.option("-E, --extensions <extensions>", "Comma separated list of file extensions to include when bundling handlers, defaults to `js, ts, mjs`")
.action(async ({ instance, register, endpoint, port, root, extensions, }) => {
const { commands, components, events } = await build({
endpoint,
port,
build: {
root,
extensions: extensions === null || extensions === void 0 ? void 0 : extensions.split(",").map((e) => e.trim()),
},
});
const categories = [commands, components, events];
const outputContent = `
${instance || register
? `import { ${instance ? `createServer${register ? ", installCommands" : ""}` : register ? "installCommands" : ""} } from "dressed/server";`
: ""}
import { serverConfig } from "dressed/utils";
import config from "./dressed.config.mjs";
Object.assign(serverConfig, config);
${[categories.map((c) => c.map(importString)), categoryExports(categories)].flat(2).join("")}
export { config };
${register ? "\ninstallCommands(commands);" : ""}
${instance ? `createServer(commands, components, events);` : ""}`.trim();
const jsContent = 'export * from "./index.mjs";';
const typeContent = 'import type { CommandData, ComponentData, EventData, ServerConfig } from "dressed/server";export declare const commands: CommandData[];export declare const components: ComponentData[];export declare const events: EventData[];export declare const config: ServerConfig;';
writeFileSync(".dressed/tmp/index.ts", outputContent);
await bundleFiles(".dressed/tmp/index.ts", ".dressed");
writeFileSync(".dressed/index.js", jsContent);
writeFileSync(".dressed/index.d.ts", typeContent);
rmSync(".dressed/tmp", { recursive: true, force: true });
logSuccess("Assembled generated build", instance ? `\n${register ? "├" : "└"} Starts a server instance` : "", register ? "\n└ Registers commands" : "");
exit();
});
program
.command("create")
.description("Clone a new bot from the examples repository")
.argument("[name]", "Project name")
.argument("[template]", "Template name (node/deno)")
.action(async (name, template) => {
const { prompt } = Enquirer;
if (!name) {
name = (await prompt({
type: "text",
name: "name",
message: "Project name:",
})).name;
}
if (!template || (!template.startsWith("node/") && !template.startsWith("deno/"))) {
const res = await fetch("https://api.github.com/repos/inbestigator/dressed-examples/contents/node");
if (!res.ok) {
throw new Error("Failed to list templates.");
}
const files = (await res.json()).filter((f) => f.type === "dir");
template = `node/${(await prompt({
name: "template",
type: "select",
message: "Select the template to use",
choices: files.map((f) => f.name),
})).template}`;
}
const res = await fetch(`https://raw.githubusercontent.com/inbestigator/dressed-examples/main/${template}/.env.example`);
if (!res.ok) {
throw new Error("Failed to fetch env template.");
}
const envVars = await prompt(Object.entries(parse(await res.text())).map(([k, v]) => ({
type: /TOKEN|PASSWORD/.test(k) ? "password" : "text",
name: k,
message: k,
initial: v,
})));
logDefer(`Creating files for project: ${name}`);
async function createFiles(path, dest) {
mkdirSync(dest, { recursive: true });
const response = await fetch(path);
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
if (Array.isArray(json)) {
for (const file of json) {
if (file.type === "dir") {
await createFiles(file.url, join(dest, file.name));
}
else {
const fileRes = await fetch(file.download_url);
if (!fileRes.ok) {
throw new Error(fileRes.statusText);
}
const fileContents = await fileRes.text();
const destPath = join(dest, file.name);
if (file.name === ".env.example") {
const destPath = join(dest, ".env");
mkdirSync(dirname(destPath), { recursive: true });
writeFileSync(destPath, Object.entries(envVars)
.map(([k, v]) => `${k}="${v}"`)
.join("\n"));
}
mkdirSync(dirname(destPath), { recursive: true });
writeFileSync(destPath, fileContents);
}
}
}
}
try {
const path = `https://api.github.com/repos/inbestigator/dressed-examples/contents/${template}`;
await createFiles(path, join(cwd(), name));
}
catch (e) {
logError(e);
}
logSuccess("Project created successfully!");
exit();
});
program.parse();
//# sourceMappingURL=dressed.js.map