Skip to content

Commit 3e6d052

Browse files
Initialize
1 parent e0cf748 commit 3e6d052

File tree

11 files changed

+632
-1
lines changed

11 files changed

+632
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package-lock.json
22
tsconfig.tsbuildinfo
3-
index.js
3+
./index.js
44
node_modules

index.js

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
#!/usr/bin/env node
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
import mri from "mri";
6+
import * as prompts from "@clack/prompts";
7+
import colors from "picocolors";
8+
const { blueBright, greenBright, magenta, reset, yellow, white, yellowBright, cyanBright, } = colors;
9+
const argv = mri(process.argv.slice(2), {
10+
alias: { h: "help", t: "template" },
11+
boolean: ["help", "overwrite"],
12+
string: ["template"],
13+
});
14+
const cwd = process.cwd();
15+
// prettier-ignore
16+
const helpMessage = `\
17+
Usage: create-discord-https [OPTION]... [DIRECTORY]
18+
19+
Create a new discord.https project in JavaScript or TypeScript.
20+
With no arguments, start the CLI in interactive mode.
21+
22+
Options:
23+
-t, --template NAME use a specific template
24+
25+
Available templates:
26+
${greenBright('node-js')}
27+
${white('vercel-js')}
28+
${yellowBright('cloudflare-js')}
29+
${blueBright('node-ts')}
30+
${magenta('vercel-ts')}
31+
${cyanBright('cloudflare-ts')}`;
32+
const SERVERTYPE = [
33+
{
34+
name: "node",
35+
display: "⬡ Nodejs (persistent)",
36+
color: greenBright,
37+
flavour: [
38+
{
39+
name: "node",
40+
display: "JavaScript",
41+
color: yellow,
42+
secret: {
43+
token: [10 - 1, 11 - 1],
44+
publicKey: [11 - 1, 15 - 1],
45+
},
46+
},
47+
{
48+
name: "node-ts",
49+
display: "TypeScript (recommend)",
50+
color: blueBright,
51+
secret: {
52+
token: [10 - 1, 11 - 1],
53+
publicKey: [11 - 1, 15 - 1],
54+
},
55+
},
56+
],
57+
},
58+
{
59+
name: "vercel",
60+
display: "▲ Vercel serverless",
61+
color: white,
62+
flavour: [
63+
{
64+
name: "vercel",
65+
display: "JavaScript",
66+
color: yellow,
67+
secret: {
68+
token: [10 - 1, 11 - 1],
69+
publicKey: [11 - 1, 15 - 1],
70+
},
71+
},
72+
{
73+
name: "vercel-ts",
74+
display: "TypeScript (recommend)",
75+
color: blueBright,
76+
secret: {
77+
token: [10 - 1, 11 - 1],
78+
publicKey: [11 - 1, 15 - 1],
79+
},
80+
},
81+
],
82+
},
83+
{
84+
name: "cloudflare",
85+
// https://www.compart.com/en/unicode/U+2601
86+
display: "☁ Cloudflare worker",
87+
color: yellowBright,
88+
flavour: [
89+
{
90+
name: "cloudflare",
91+
display: "JavaScript",
92+
color: yellow,
93+
secret: {
94+
token: [12 - 1, 15 - 1],
95+
publicKey: [13 - 1, 19 - 1],
96+
},
97+
},
98+
{
99+
name: "cloudflare-ts",
100+
display: "TypeScript (recommend)",
101+
color: blueBright,
102+
secret: {
103+
token: [12 - 1, 15 - 1],
104+
publicKey: [13 - 1, 19 - 1],
105+
},
106+
},
107+
],
108+
},
109+
];
110+
const TEMPLATES = SERVERTYPE.map((f) => f.flavour.map((v) => v.name)).reduce((a, b) => a.concat(b), []);
111+
const renameFiles = {
112+
_gitignore: ".gitignore",
113+
};
114+
const defaultTargetDir = "mybot";
115+
async function init() {
116+
const argTargetDir = argv._[0]
117+
? formatTargetDir(String(argv._[0]))
118+
: undefined;
119+
const argTemplate = argv.template;
120+
const argOverwrite = argv.overwrite;
121+
const help = argv.help;
122+
if (help) {
123+
console.log(helpMessage);
124+
return;
125+
}
126+
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
127+
const cancel = () => prompts.cancel("Operation cancelled");
128+
// 1. Get project name and target dir
129+
let targetDir = argTargetDir;
130+
if (!targetDir) {
131+
const projectName = await prompts.text({
132+
message: "Project name:",
133+
defaultValue: defaultTargetDir,
134+
placeholder: defaultTargetDir,
135+
validate: (value) => {
136+
return value.length === 0 || formatTargetDir(value).length > 0
137+
? undefined
138+
: "Invalid project name";
139+
},
140+
});
141+
if (prompts.isCancel(projectName))
142+
return cancel();
143+
targetDir = formatTargetDir(projectName);
144+
}
145+
// 2. Handle directory if exist and not empty
146+
if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
147+
const overwrite = argOverwrite
148+
? "yes"
149+
: await prompts.select({
150+
message: (targetDir === "."
151+
? "Current directory"
152+
: `Target directory "${targetDir}"`) +
153+
` is not empty. choose how to proceed:`,
154+
options: [
155+
{
156+
label: "Cancel operation",
157+
value: "no",
158+
},
159+
{
160+
label: "Remove existing files and continue",
161+
value: "yes",
162+
},
163+
{
164+
label: "Ignore files and continue",
165+
value: "ignore",
166+
},
167+
],
168+
});
169+
if (prompts.isCancel(overwrite))
170+
return cancel();
171+
switch (overwrite) {
172+
case "yes":
173+
emptyDir(targetDir);
174+
break;
175+
case "no":
176+
cancel();
177+
return;
178+
}
179+
}
180+
// 3. Get package name
181+
let packageName = path.basename(path.resolve(targetDir));
182+
if (!isValidPackageName(packageName)) {
183+
const packageNameResult = await prompts.text({
184+
message: "Package name:",
185+
defaultValue: toValidPackageName(packageName),
186+
placeholder: toValidPackageName(packageName),
187+
validate(dir) {
188+
if (!isValidPackageName(dir)) {
189+
return "Invalid package.json name";
190+
}
191+
},
192+
});
193+
if (prompts.isCancel(packageNameResult))
194+
return cancel();
195+
packageName = packageNameResult;
196+
}
197+
// 4. Choose a framework and flavour
198+
let template = argTemplate;
199+
let hasInvalidArgTemplate = false;
200+
if (argTemplate && !TEMPLATES.includes(argTemplate)) {
201+
template = undefined;
202+
hasInvalidArgTemplate = true;
203+
}
204+
if (!template) {
205+
const framework = await prompts.select({
206+
message: hasInvalidArgTemplate
207+
? `"${argTemplate}" isn't a valid template. choose from below: `
208+
: "Select a environment:",
209+
options: SERVERTYPE.map((framework) => {
210+
const frameworkColor = framework.color;
211+
return {
212+
label: frameworkColor(framework.display || framework.name),
213+
value: framework,
214+
};
215+
}),
216+
});
217+
if (prompts.isCancel(framework))
218+
return cancel();
219+
const flavour = await prompts.select({
220+
message: "Select a flavour:",
221+
options: framework.flavour.map((flavour) => {
222+
const flavourColor = flavour.color;
223+
return {
224+
label: flavourColor(flavour.display || flavour.name),
225+
value: flavour.name,
226+
};
227+
}),
228+
});
229+
if (prompts.isCancel(flavour))
230+
return cancel();
231+
template = flavour;
232+
}
233+
const root = path.join(cwd, targetDir);
234+
fs.mkdirSync(root, { recursive: true });
235+
const pkgManager = pkgInfo ? pkgInfo.name : "npm";
236+
const { secret } = SERVERTYPE.flatMap((f) => f.flavour).find((v) => v.name === template);
237+
prompts.log.step(`Initializing project...`);
238+
const templateDir = path.resolve(fileURLToPath(import.meta.url), "../", `templates/${template}`);
239+
const write = (file, content) => {
240+
const targetPath = path.join(root, renameFiles[file] ?? file);
241+
if (content) {
242+
fs.writeFileSync(targetPath, content);
243+
}
244+
else {
245+
copy(path.join(templateDir, file), targetPath);
246+
}
247+
};
248+
const files = fs.readdirSync(templateDir);
249+
for (const file of files.filter((f) => f !== "package.json")) {
250+
write(file);
251+
}
252+
const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), "utf-8"));
253+
pkg.name = packageName;
254+
write("package.json", JSON.stringify(pkg, null, 2) + "\n");
255+
const ext = template.split("-")[1] ? "ts" : "js";
256+
await setupSubdomain(root, `src/DevLayer.${ext}`);
257+
await setupSecret(root, `src/index.${ext}`, secret);
258+
let doneMessage = "";
259+
const cdProjectName = path.relative(cwd, root);
260+
doneMessage += `All done! Execute:\n`;
261+
if (root !== cwd) {
262+
doneMessage += `\n cd ${cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName}`;
263+
}
264+
switch (pkgManager) {
265+
case "yarn":
266+
doneMessage += "\n yarn";
267+
doneMessage += "\n yarn dev";
268+
break;
269+
default:
270+
doneMessage += `\n ${pkgManager} install`;
271+
doneMessage += `\n ${pkgManager} run dev`;
272+
break;
273+
}
274+
prompts.outro(doneMessage);
275+
}
276+
function formatTargetDir(targetDir) {
277+
return targetDir.trim().replace(/\/+$/g, "");
278+
}
279+
function copy(src, dest) {
280+
const stat = fs.statSync(src);
281+
if (stat.isDirectory()) {
282+
copyDir(src, dest);
283+
}
284+
else {
285+
fs.copyFileSync(src, dest);
286+
}
287+
}
288+
function isValidPackageName(projectName) {
289+
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName);
290+
}
291+
function toValidPackageName(projectName) {
292+
return projectName
293+
.trim()
294+
.toLowerCase()
295+
.replace(/\s+/g, "-")
296+
.replace(/^[._]/, "")
297+
.replace(/[^a-z\d\-~]+/g, "-");
298+
}
299+
function copyDir(srcDir, destDir) {
300+
fs.mkdirSync(destDir, { recursive: true });
301+
for (const file of fs.readdirSync(srcDir)) {
302+
const srcFile = path.resolve(srcDir, file);
303+
const destFile = path.resolve(destDir, file);
304+
copy(srcFile, destFile);
305+
}
306+
}
307+
function isEmpty(path) {
308+
const files = fs.readdirSync(path);
309+
return files.length === 0 || (files.length === 1 && files[0] === ".git");
310+
}
311+
function emptyDir(dir) {
312+
if (!fs.existsSync(dir)) {
313+
return;
314+
}
315+
for (const file of fs.readdirSync(dir)) {
316+
if (file === ".git") {
317+
continue;
318+
}
319+
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
320+
}
321+
}
322+
function pkgFromUserAgent(userAgent) {
323+
if (!userAgent)
324+
return undefined;
325+
const pkgSpec = userAgent.split(" ")[0];
326+
const pkgSpecArr = pkgSpec.split("/");
327+
return {
328+
name: pkgSpecArr[0],
329+
version: pkgSpecArr[1],
330+
};
331+
}
332+
async function setupSecret(root, entryPoint, secret) {
333+
const botToken = await prompts.text({
334+
message: "Enter your Discord Bot Token:",
335+
validate: (value) => value.length === 0 ? "Token cannot be empty" : undefined,
336+
});
337+
if (prompts.isCancel(botToken))
338+
return prompts.cancel("Operation cancelled");
339+
const publicKey = await prompts.text({
340+
message: "Enter your Discord Public Key:",
341+
validate: (value) => value.length === 0 ? "Public Key cannot be empty" : undefined,
342+
});
343+
if (prompts.isCancel(publicKey))
344+
return prompts.cancel("Operation cancelled");
345+
const entryFile = path.join(root, entryPoint);
346+
if (!fs.existsSync(entryFile)) {
347+
prompts.cancel("Error while writing token: file not found.");
348+
return; // stop execution
349+
}
350+
let fileContent = fs.readFileSync(entryFile, "utf-8").split("\n");
351+
const tokenPlaceholder = "PUT_YOUR_TOKEN_HERE";
352+
const publickeyPlaceholder = "PUT_YOUR_PUBLIC_KEY_HERE";
353+
const tokenLine = fileContent[secret.token[0]];
354+
fileContent[secret.token[0]] =
355+
tokenLine.slice(0, secret.token[1]) +
356+
botToken +
357+
tokenLine.slice(secret.token[1] + tokenPlaceholder.length);
358+
const publicKeyLine = fileContent[secret.publicKey[0]];
359+
fileContent[secret.publicKey[0]] =
360+
publicKeyLine.slice(0, secret.publicKey[1]) +
361+
publicKey +
362+
publicKeyLine.slice(secret.publicKey[1] + publickeyPlaceholder.length);
363+
fs.writeFileSync(entryFile, fileContent.join("\n"));
364+
prompts.log.success("Done, Secrets have been hardcoded however, it is recommended to switch to environment variables!");
365+
}
366+
async function setupSubdomain(root, DevLayerEntryPoint) {
367+
const entryFile = path.join(root, DevLayerEntryPoint);
368+
if (!fs.existsSync(entryFile)) {
369+
prompts.cancel("Error while writing sub domain: file not found.");
370+
return; // stop execution
371+
}
372+
let fileContent = fs.readFileSync(entryFile, "utf-8").split("\n");
373+
const SubDomainPrefix = "discord-https";
374+
// discord-https-timestamp-number
375+
const subdomain = `${SubDomainPrefix}-${Date.now()}-${Math.floor(Math.random() * 9)}`;
376+
const row = 48 - 1; //0th index
377+
const column = 19 - 1; //0th index
378+
const tokenLine = fileContent[row];
379+
fileContent[row] =
380+
tokenLine.slice(0, column) +
381+
subdomain +
382+
tokenLine.slice(column + SubDomainPrefix.length);
383+
fs.writeFileSync(entryFile, fileContent.join("\n"));
384+
prompts.log.info("Use `npm run dev` over directly invoking the command, as this will create a tunnel layer except for server environment `nodejs` with a flavor of `typescript`.");
385+
prompts.log.info("For more information, see the README.md file.");
386+
}
387+
init().catch((e) => {
388+
console.error(e);
389+
});

0 commit comments

Comments
 (0)