create-bot-ts 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -12
- package/index.js +727 -276
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
# create-bot-ts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffold a modern, **type-driven** Discord.js bot with the folder structure MahoMuri likes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What you get
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
├── index.ts # entry point: creates BotClient, calls init()
|
|
10
|
+
├── client/ # BotClient class - extends Client; init() loads + logs in
|
|
11
|
+
├── commands/ # slash commands, auto-loaded from this folder
|
|
12
|
+
├── events/ # event handlers, auto-loaded from this folder
|
|
13
|
+
├── interfaces/ # shared types: Command, BotEvent
|
|
14
|
+
└── utils/ # module loaders: loadCommands(), loadEvents()
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- Strict, type-driven TypeScript — `verbatimModuleSyntax`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `NodeNext` module resolution
|
|
18
|
+
- Named exports only — every command/event file exports a single `const` named after the file (`ping.ts` → `ping`), so imports autocomplete and nothing depends on a default export
|
|
19
|
+
- `BotClient extends Client` with `init()` — calls `loadCommands` + `loadEvents` from `src/utils`, then logs in
|
|
20
|
+
- discord.js v14 + `bufferutil` / `zlib-sync`, `dotenv`, `consola`, `common-tags`, `table`
|
|
21
|
+
- ESLint 9 **flat config** with type-aware linting from `typescript-eslint` (works out of the box, no manual config step)
|
|
22
|
+
- Prettier + Husky + lint-staged pre-commit hook
|
|
23
|
+
- Yarn 4 using the `node-modules` linker (fewer tooling quirks than PnP)
|
|
24
|
+
- `tsx watch` for a modern dev runner (no `ts-node`)
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Run it inside an **empty directory**:
|
|
17
29
|
|
|
18
30
|
```sh
|
|
19
|
-
|
|
31
|
+
# npm
|
|
32
|
+
npx create-bot-ts@latest
|
|
33
|
+
|
|
34
|
+
# yarn
|
|
35
|
+
yarn create bot-ts
|
|
20
36
|
```
|
|
37
|
+
|
|
38
|
+
The script refuses to run in a non-empty directory unless you pass `--force`.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Node.js >= 24.17 required (discord.js v14 minimum)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Structure
|
|
45
|
+
|
|
46
|
+
| Location | Purpose |
|
|
47
|
+
| ---------- | -------------------------------- |
|
|
48
|
+
| `bin` | `create-bot-ts` → `index.js` |
|
|
49
|
+
| `index.js` | the scaffolder itself |
|
package/index.js
CHANGED
|
@@ -1,294 +1,745 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const
|
|
5
|
-
const
|
|
2
|
+
const { exec } = require("node:child_process");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
6
|
const { createSpinner } = require("nanospinner");
|
|
7
|
+
const outdent = require("outdent");
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
outdent`${f.content}`
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Small helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Promisified exec. Resolves with trimmed stdout and rejects on non-zero exit.
|
|
15
|
+
* stderr is appended to the error message so failures stay debuggable.
|
|
16
|
+
*/
|
|
17
|
+
const run = (command, options = {}) =>
|
|
18
|
+
new Promise((resolve, reject) => {
|
|
19
|
+
exec(
|
|
20
|
+
command,
|
|
21
|
+
{
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
24
|
+
...options,
|
|
25
|
+
},
|
|
26
|
+
(error, stdout, stderr) => {
|
|
27
|
+
if (error) {
|
|
28
|
+
const detail = `${stderr || stdout}`.trim();
|
|
29
|
+
error.message = detail
|
|
30
|
+
? `${error.message}\n${detail}`
|
|
31
|
+
: error.message;
|
|
32
|
+
reject(error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolve(stdout.trim());
|
|
37
|
+
},
|
|
38
38
|
);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
/** Write a batch of templated files, each ending with a single newline. */
|
|
42
|
+
const writeFiles = (files) => {
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const content = outdent`${file.content}`.trimEnd() + os.EOL;
|
|
45
|
+
fs.writeFileSync(path.join(process.cwd(), file.path), content);
|
|
46
|
+
}
|
|
40
47
|
};
|
|
41
48
|
|
|
42
|
-
//
|
|
43
|
-
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Preflight — refuse to scaffold into a non-empty directory unless forced
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const FORCE = process.argv.includes("--force");
|
|
54
|
+
|
|
55
|
+
const existingEntries = fs
|
|
56
|
+
.readdirSync(process.cwd())
|
|
57
|
+
.filter((entry) => entry !== ".git");
|
|
58
|
+
|
|
59
|
+
if (existingEntries.length > 0 && !FORCE) {
|
|
60
|
+
const list = existingEntries
|
|
61
|
+
.slice(0, 4)
|
|
62
|
+
.map((entry) => `"${entry}"`)
|
|
63
|
+
.join(", ");
|
|
64
|
+
const more =
|
|
65
|
+
existingEntries.length > 4
|
|
66
|
+
? `, and ${existingEntries.length - 4} more`
|
|
67
|
+
: "";
|
|
68
|
+
console.error(
|
|
69
|
+
`[create-bot-ts] refusing to scaffold into a non-empty directory (${list}${more}).\n` +
|
|
70
|
+
"Run this inside an empty directory, or pass --force to overwrite.",
|
|
71
|
+
);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projectName =
|
|
76
|
+
path
|
|
77
|
+
.basename(process.cwd())
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9-_]+/g, "-")
|
|
80
|
+
.replace(/-+/g, "-")
|
|
81
|
+
.replace(/^-+|-+$/g, "") || "discord-bot";
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Generated file templates (kept formatting-clean for prettier --check)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
const TS_CONFIG = outdent`
|
|
44
88
|
{
|
|
45
|
-
|
|
46
|
-
content: `{
|
|
47
|
-
"compilerOptions": {
|
|
89
|
+
"compilerOptions": {
|
|
48
90
|
"lib": ["ESNext"],
|
|
49
|
-
"module": "commonjs",
|
|
50
|
-
"moduleResolution": "node",
|
|
51
91
|
"target": "ESNext",
|
|
52
|
-
"
|
|
92
|
+
"module": "NodeNext",
|
|
93
|
+
"moduleResolution": "NodeNext",
|
|
94
|
+
"moduleDetection": "force",
|
|
95
|
+
"rootDir": "src",
|
|
96
|
+
"outDir": "dist",
|
|
97
|
+
"strict": true,
|
|
98
|
+
"noUncheckedIndexedAccess": true,
|
|
99
|
+
"exactOptionalPropertyTypes": true,
|
|
100
|
+
"noImplicitOverride": true,
|
|
101
|
+
"noFallthroughCasesInSwitch": true,
|
|
102
|
+
"noUnusedLocals": true,
|
|
103
|
+
"noUnusedParameters": true,
|
|
104
|
+
"verbatimModuleSyntax": true,
|
|
105
|
+
"resolveJsonModule": true,
|
|
53
106
|
"esModuleInterop": true,
|
|
54
|
-
"
|
|
55
|
-
"emitDecoratorMetadata": true,
|
|
56
|
-
"allowSyntheticDefaultImports": true,
|
|
107
|
+
"forceConsistentCasingInFileNames": true,
|
|
57
108
|
"skipLibCheck": true,
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`,
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
path: ".prettierrc",
|
|
68
|
-
content: `{
|
|
69
|
-
"semi": true,
|
|
70
|
-
"singleQuote": false,
|
|
71
|
-
"tabWidth": 4,
|
|
72
|
-
"printWidth": 80,
|
|
73
|
-
"useTabs": false,
|
|
74
|
-
"endOfLine": "lf"
|
|
75
|
-
}
|
|
109
|
+
"sourceMap": true,
|
|
110
|
+
"noEmitOnError": true
|
|
111
|
+
},
|
|
112
|
+
"include": ["src"]
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
},
|
|
116
|
+
const PRETTIER_CONFIG = outdent`
|
|
79
117
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
node_modules
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
118
|
+
"semi": true,
|
|
119
|
+
"singleQuote": true,
|
|
120
|
+
"tabWidth": 2,
|
|
121
|
+
"printWidth": 100,
|
|
122
|
+
"trailingComma": "all",
|
|
123
|
+
"endOfLine": "lf"
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const PRETTIER_IGNORE = outdent`
|
|
128
|
+
dist
|
|
129
|
+
node_modules
|
|
130
|
+
.yarn
|
|
131
|
+
.husky
|
|
132
|
+
package.json
|
|
133
|
+
yarn.lock
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
const GITIGNORE = outdent`
|
|
137
|
+
# Dependencies
|
|
138
|
+
node_modules
|
|
139
|
+
|
|
140
|
+
# Yarn (node-modules linker keeps some metadata in .yarn)
|
|
141
|
+
.yarn/*
|
|
142
|
+
!.yarn/plugins
|
|
143
|
+
!.yarn/releases
|
|
144
|
+
!.yarn/sdks
|
|
145
|
+
!.yarn/patches
|
|
146
|
+
!.yarn/versions
|
|
147
|
+
|
|
148
|
+
# Build output
|
|
149
|
+
dist
|
|
150
|
+
|
|
151
|
+
# Environment
|
|
152
|
+
.env
|
|
153
|
+
.env.*
|
|
154
|
+
!.env.example
|
|
155
|
+
|
|
156
|
+
# Logs
|
|
157
|
+
logs
|
|
158
|
+
*.log
|
|
159
|
+
|
|
160
|
+
# Husky runtime
|
|
161
|
+
.husky/_
|
|
162
|
+
|
|
163
|
+
# Editor directories and files
|
|
164
|
+
.idea
|
|
165
|
+
.vscode/*
|
|
166
|
+
!.vscode/extensions.json
|
|
167
|
+
.DS_Store
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const ENV_EXAMPLE = outdent`
|
|
171
|
+
# Bot token from the Discord Developer Portal
|
|
172
|
+
# https://discord.com/developers/applications
|
|
173
|
+
DISCORD_TOKEN=
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const ESLINT_CONFIG = outdent`
|
|
177
|
+
// @ts-check
|
|
178
|
+
import eslint from '@eslint/js';
|
|
179
|
+
import prettierConfig from 'eslint-config-prettier/flat';
|
|
180
|
+
import globals from 'globals';
|
|
181
|
+
import tseslint from 'typescript-eslint';
|
|
182
|
+
|
|
183
|
+
export default tseslint.config(
|
|
184
|
+
{
|
|
185
|
+
ignores: ['dist', 'node_modules', '.yarn', '.husky', 'eslint.config.mjs'],
|
|
186
|
+
},
|
|
187
|
+
eslint.configs.recommended,
|
|
188
|
+
...tseslint.configs.recommended,
|
|
189
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
190
|
+
{
|
|
191
|
+
languageOptions: {
|
|
192
|
+
globals: globals.node,
|
|
193
|
+
parserOptions: {
|
|
194
|
+
projectService: true,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
rules: {
|
|
198
|
+
// Type-driven codebase: type-only imports must use \`import type\`.
|
|
199
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
200
|
+
// discord.js is promise based; never drop a floating promise.
|
|
201
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
202
|
+
// Async event handlers are idiomatic in discord.js — keep the rule
|
|
203
|
+
// for promise misuse, but allow promise-returning listeners (\`checksVoidReturn\`).
|
|
204
|
+
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
|
205
|
+
// Event listeners often take unused args - allow the \`_\` prefix.
|
|
206
|
+
'@typescript-eslint/no-unused-vars': [
|
|
207
|
+
'error',
|
|
208
|
+
{
|
|
209
|
+
argsIgnorePattern: '^_',
|
|
210
|
+
caughtErrorsIgnorePattern: '^_',
|
|
211
|
+
varsIgnorePattern: '^_',
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
prettierConfig,
|
|
217
|
+
);
|
|
218
|
+
`;
|
|
219
|
+
|
|
220
|
+
const CLIENT_TS = outdent`
|
|
221
|
+
import { dirname, join } from 'node:path';
|
|
222
|
+
import { fileURLToPath } from 'node:url';
|
|
223
|
+
|
|
224
|
+
import { Client, Collection } from 'discord.js';
|
|
225
|
+
|
|
226
|
+
import type { ClientOptions } from 'discord.js';
|
|
227
|
+
|
|
228
|
+
import { loadCommands } from '../utils/loadCommands.js';
|
|
229
|
+
import { loadEvents } from '../utils/loadEvents.js';
|
|
230
|
+
|
|
231
|
+
import type { Command } from '../interfaces/command.js';
|
|
232
|
+
|
|
233
|
+
export class BotClient extends Client {
|
|
234
|
+
/** Commands loaded from src/commands, keyed by command name. */
|
|
235
|
+
public readonly commands: Collection<string, Command> = new Collection();
|
|
236
|
+
|
|
237
|
+
private readonly commandsPath: string;
|
|
238
|
+
private readonly eventsPath: string;
|
|
239
|
+
|
|
240
|
+
public constructor(options: ClientOptions) {
|
|
241
|
+
super(options);
|
|
242
|
+
|
|
243
|
+
// Anchor to this file so paths resolve from src/ (tsx) and dist/ (build).
|
|
244
|
+
const directory = dirname(fileURLToPath(import.meta.url));
|
|
245
|
+
this.commandsPath = join(directory, '..', 'commands');
|
|
246
|
+
this.eventsPath = join(directory, '..', 'events');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Loads every command + event, then logs the bot in. */
|
|
250
|
+
public async init(token: string): Promise<void> {
|
|
251
|
+
await Promise.all([
|
|
252
|
+
loadCommands(this.commandsPath, this.commands),
|
|
253
|
+
loadEvents(this, this.eventsPath),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
await super.login(token);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
`;
|
|
260
|
+
|
|
261
|
+
const COMMAND_INTERFACE_TS = outdent`
|
|
262
|
+
import type { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
|
263
|
+
|
|
264
|
+
export interface Command {
|
|
265
|
+
data: SlashCommandBuilder;
|
|
266
|
+
execute(interaction: ChatInputCommandInteraction): Promise<void>;
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
|
|
270
|
+
const EVENT_INTERFACE_TS = outdent`
|
|
271
|
+
import type { ClientEvents } from 'discord.js';
|
|
272
|
+
|
|
273
|
+
export interface BotEvent<T extends keyof ClientEvents> {
|
|
274
|
+
name: T;
|
|
275
|
+
once?: boolean;
|
|
276
|
+
execute(...args: ClientEvents[T]): void | Promise<void>;
|
|
277
|
+
}
|
|
278
|
+
`;
|
|
279
|
+
|
|
280
|
+
const MODULES_TS = outdent`
|
|
281
|
+
import { readdir } from 'node:fs/promises';
|
|
282
|
+
import { join, parse } from 'node:path';
|
|
283
|
+
import { pathToFileURL } from 'node:url';
|
|
284
|
+
|
|
285
|
+
/** Dev runs \`.ts\` sources, a build runs \`.js\`. Accept both. */
|
|
286
|
+
export const isModuleFile = (file: string): boolean => file.endsWith('.ts') || file.endsWith('.js');
|
|
287
|
+
|
|
288
|
+
/** \`ping.ts\` -> \`ping\`: the named export every module is expected to provide. */
|
|
289
|
+
export const moduleName = (file: string): string => parse(file).name;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Lists the loadable files in \`folder\`, sorted so the load order stays stable
|
|
293
|
+
* instead of depending on the order the filesystem hands entries back.
|
|
294
|
+
*/
|
|
295
|
+
export const listModules = async (folder: string): Promise<string[]> =>
|
|
296
|
+
(await readdir(folder)).filter(isModuleFile).sort();
|
|
297
|
+
|
|
298
|
+
/** Imports a module namespace. A file URL keeps this working on every platform. */
|
|
299
|
+
export const importModule = async (
|
|
300
|
+
folder: string,
|
|
301
|
+
file: string,
|
|
302
|
+
): Promise<Record<string, unknown>> => {
|
|
303
|
+
const target = pathToFileURL(join(folder, file)).href;
|
|
304
|
+
return (await import(target)) as Record<string, unknown>;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Picks the value a module exports under \`name\`, falling back to the first export
|
|
309
|
+
* that \`isModule\` accepts. The convention is that \`ping.ts\` exports \`ping\`, but a
|
|
310
|
+
* mismatch still loads instead of silently registering nothing.
|
|
311
|
+
*/
|
|
312
|
+
export const resolveExport = <T>(
|
|
313
|
+
loaded: Record<string, unknown>,
|
|
314
|
+
name: string,
|
|
315
|
+
isModule: (value: unknown) => value is T,
|
|
316
|
+
): T | undefined => {
|
|
317
|
+
const named = loaded[name];
|
|
318
|
+
if (isModule(named)) return named;
|
|
319
|
+
return Object.values(loaded).find(isModule);
|
|
320
|
+
};
|
|
321
|
+
`;
|
|
322
|
+
|
|
323
|
+
const LOAD_COMMANDS_TS = outdent`
|
|
324
|
+
import { consola } from 'consola';
|
|
325
|
+
|
|
326
|
+
import type { Collection } from 'discord.js';
|
|
327
|
+
|
|
328
|
+
import type { Command } from '../interfaces/command.js';
|
|
329
|
+
|
|
330
|
+
import { importModule, listModules, moduleName, resolveExport } from './modules.js';
|
|
331
|
+
|
|
332
|
+
/** A module is a command when it exposes both \`data\` and \`execute\`. */
|
|
333
|
+
const isCommand = (value: unknown): value is Command =>
|
|
334
|
+
typeof value === 'object' && value !== null && 'data' in value && 'execute' in value;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Imports every command in \`folder\` into \`commands\`, keyed by \`data.name\`.
|
|
338
|
+
* A broken file is logged and skipped so one bad command never stops the bot.
|
|
339
|
+
*/
|
|
340
|
+
export const loadCommands = async (
|
|
341
|
+
folder: string,
|
|
342
|
+
commands: Collection<string, Command>,
|
|
343
|
+
): Promise<Collection<string, Command>> => {
|
|
344
|
+
for (const file of await listModules(folder)) {
|
|
345
|
+
const name = moduleName(file);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const loaded = await importModule(folder, file);
|
|
349
|
+
const command = resolveExport(loaded, name, isCommand);
|
|
350
|
+
|
|
351
|
+
if (command === undefined) {
|
|
352
|
+
consola.warn(\`[commands] skipping "\${file}": no "\${name}" command export\`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
commands.set(command.data.name, command);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
consola.error(\`[commands] failed to load "\${file}"\`);
|
|
359
|
+
consola.error(error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
consola.info(\`[commands] loaded \${commands.size} command(s)\`);
|
|
364
|
+
|
|
365
|
+
return commands;
|
|
366
|
+
};
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
const LOAD_EVENTS_TS = outdent`
|
|
370
|
+
import { consola } from 'consola';
|
|
371
|
+
|
|
372
|
+
import type { Client, ClientEvents } from 'discord.js';
|
|
373
|
+
|
|
374
|
+
import type { BotEvent } from '../interfaces/event.js';
|
|
375
|
+
|
|
376
|
+
import { importModule, listModules, moduleName, resolveExport } from './modules.js';
|
|
377
|
+
|
|
378
|
+
/** A module is an event when it exposes both \`name\` and \`execute\`. */
|
|
379
|
+
const isBotEvent = (value: unknown): value is BotEvent<keyof ClientEvents> =>
|
|
380
|
+
typeof value === 'object' && value !== null && 'name' in value && 'execute' in value;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Registers every event in \`folder\` on \`client\`, honouring \`once\`.
|
|
384
|
+
* A broken file is logged and skipped so one bad handler never stops the bot.
|
|
385
|
+
*/
|
|
386
|
+
export const loadEvents = async (client: Client, folder: string): Promise<number> => {
|
|
387
|
+
let registered = 0;
|
|
388
|
+
|
|
389
|
+
for (const file of await listModules(folder)) {
|
|
390
|
+
const name = moduleName(file);
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const loaded = await importModule(folder, file);
|
|
394
|
+
const botEvent = resolveExport(loaded, name, isBotEvent);
|
|
395
|
+
|
|
396
|
+
if (botEvent === undefined) {
|
|
397
|
+
consola.warn(\`[events] skipping "\${file}": no "\${name}" event export\`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (botEvent.once) {
|
|
402
|
+
client.once(botEvent.name, (...args: ClientEvents[typeof botEvent.name]) => {
|
|
403
|
+
void botEvent.execute(...args);
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
client.on(botEvent.name, (...args: ClientEvents[typeof botEvent.name]) => {
|
|
407
|
+
void botEvent.execute(...args);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
registered += 1;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
consola.error(\`[events] failed to load "\${file}"\`);
|
|
414
|
+
consola.error(error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
consola.info(\`[events] registered \${registered} event(s)\`);
|
|
419
|
+
|
|
420
|
+
return registered;
|
|
421
|
+
};
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
const PING_COMMAND_TS = outdent`
|
|
425
|
+
import { SlashCommandBuilder } from 'discord.js';
|
|
426
|
+
|
|
427
|
+
import type { Command } from '../interfaces/command.js';
|
|
428
|
+
|
|
429
|
+
export const ping: Command = {
|
|
430
|
+
data: new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!'),
|
|
431
|
+
async execute(interaction) {
|
|
432
|
+
await interaction.reply({ content: 'Pong!' });
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
const READY_EVENT_TS = outdent`
|
|
438
|
+
import { consola } from 'consola';
|
|
439
|
+
import { Events } from 'discord.js';
|
|
440
|
+
|
|
441
|
+
import type { BotEvent } from '../interfaces/event.js';
|
|
442
|
+
|
|
443
|
+
export const ready: BotEvent<Events.ClientReady> = {
|
|
444
|
+
name: Events.ClientReady,
|
|
445
|
+
once: true,
|
|
446
|
+
execute(client) {
|
|
447
|
+
consola.success(\`Logged in as \${client.user.username}\`);
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
const INTERACTION_CREATE_TS = outdent`
|
|
453
|
+
import { consola } from 'consola';
|
|
454
|
+
import { Events } from 'discord.js';
|
|
455
|
+
|
|
456
|
+
import type { BotClient } from '../client/index.js';
|
|
457
|
+
import type { BotEvent } from '../interfaces/event.js';
|
|
458
|
+
|
|
459
|
+
export const interactionCreate: BotEvent<Events.InteractionCreate> = {
|
|
460
|
+
name: Events.InteractionCreate,
|
|
461
|
+
async execute(interaction) {
|
|
462
|
+
if (!interaction.isChatInputCommand()) return;
|
|
463
|
+
|
|
464
|
+
const client = interaction.client as BotClient;
|
|
465
|
+
const command = client.commands.get(interaction.commandName);
|
|
466
|
+
|
|
467
|
+
if (command === undefined) {
|
|
468
|
+
await interaction.reply({ content: 'Command not found.', ephemeral: true });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
await command.execute(interaction);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
consola.error(\`[commands] "\${interaction.commandName}" threw an error\`);
|
|
476
|
+
consola.error(error);
|
|
477
|
+
|
|
478
|
+
const message = 'There was an error while executing this command.';
|
|
479
|
+
if (interaction.replied || interaction.deferred) {
|
|
480
|
+
await interaction.followUp({ content: message, ephemeral: true });
|
|
481
|
+
} else {
|
|
482
|
+
await interaction.reply({ content: message, ephemeral: true });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
`;
|
|
488
|
+
|
|
489
|
+
const INDEX_TS = outdent`
|
|
490
|
+
import 'dotenv/config';
|
|
491
|
+
|
|
492
|
+
import { consola } from 'consola';
|
|
493
|
+
import { GatewayIntentBits } from 'discord.js';
|
|
494
|
+
|
|
495
|
+
import { BotClient } from './client/index.js';
|
|
496
|
+
|
|
497
|
+
const token = process.env.DISCORD_TOKEN;
|
|
498
|
+
|
|
499
|
+
if (token === undefined) {
|
|
500
|
+
consola.error('DISCORD_TOKEN is missing. Copy .env.example to .env and set your bot token.');
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const client = new BotClient({
|
|
505
|
+
intents: [GatewayIntentBits.Guilds],
|
|
153
506
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
507
|
+
|
|
508
|
+
await client.init(token);
|
|
509
|
+
`;
|
|
510
|
+
|
|
511
|
+
const README = outdent`
|
|
512
|
+
# ${projectName}
|
|
513
|
+
|
|
514
|
+
A Discord bot scaffolded with [create-bot-ts](https://github.com/MahoMuri/create-bot-ts).
|
|
515
|
+
|
|
516
|
+
## Features
|
|
517
|
+
|
|
518
|
+
- strict, type-driven TypeScript (\`verbatimModuleSyntax\`, \`exactOptionalPropertyTypes\`, \`noUncheckedIndexedAccess\`)
|
|
519
|
+
- discord.js v14 with typed events
|
|
520
|
+
- ESLint 9 flat config with type-aware linting (typescript-eslint)
|
|
521
|
+
- Prettier for consistent formatting
|
|
522
|
+
- Husky + lint-staged pre-commit hook
|
|
523
|
+
- Yarn 4 with the node-modules linker
|
|
524
|
+
- \`dotenv\`, \`consola\`, \`common-tags\`, \`table\` utilities
|
|
525
|
+
|
|
526
|
+
## Getting started
|
|
527
|
+
|
|
528
|
+
\`\`\`sh
|
|
529
|
+
cp .env.example .env
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
Open \`.env\` and set \`DISCORD_TOKEN\` to your bot token, then run:
|
|
533
|
+
|
|
534
|
+
\`\`\`sh
|
|
535
|
+
yarn dev
|
|
536
|
+
\`\`\`
|
|
537
|
+
|
|
538
|
+
## Scripts
|
|
539
|
+
|
|
540
|
+
- \`yarn dev\` — start the bot with hot-reload (\`tsx watch\`)
|
|
541
|
+
- \`yarn build\` — type-check and compile to \`dist/\`
|
|
542
|
+
- \`yarn start\` — run the compiled bot
|
|
543
|
+
- \`yarn typecheck\` — run \`tsc --noEmit\`
|
|
544
|
+
- \`yarn lint\` / \`yarn lint:fix\` — lint with ESLint
|
|
545
|
+
- \`yarn format\` / \`yarn format:check\` — format with Prettier
|
|
546
|
+
|
|
547
|
+
## Project structure
|
|
548
|
+
|
|
549
|
+
\`\`\`
|
|
550
|
+
src/
|
|
551
|
+
├── index.ts # entry point: creates BotClient, calls init()
|
|
552
|
+
├── client/ # BotClient class - extends Client; init() loads + logs in
|
|
553
|
+
├── commands/ # slash commands, auto-loaded from this folder
|
|
554
|
+
├── events/ # event handlers, auto-loaded from this folder
|
|
555
|
+
├── interfaces/ # shared types: Command, BotEvent
|
|
556
|
+
└── utils/ # module loaders: loadCommands(), loadEvents(), resolveExport()
|
|
557
|
+
\`\`\`
|
|
558
|
+
|
|
559
|
+
## How it works
|
|
560
|
+
|
|
561
|
+
- Every file in \`src/commands\` exports one named \`Command\` (\`ping.ts\` -> \`ping\`).
|
|
562
|
+
\`loadCommands\` (\`src/utils/loadCommands.ts\`) imports them into \`client.commands\`,
|
|
563
|
+
keyed by \`data.name\`.
|
|
564
|
+
- Every file in \`src/events\` exports one named \`BotEvent\` (\`ready.ts\` -> \`ready\`).
|
|
565
|
+
\`loadEvents\` (\`src/utils/loadEvents.ts\`) registers them, honouring \`once\`.
|
|
566
|
+
- \`client.init(token)\` runs both loaders, then calls \`client.login(token)\`.
|
|
567
|
+
|
|
568
|
+
Each command is a \`data\` (\`SlashCommandBuilder\`) + \`execute\` pair. To make slash
|
|
569
|
+
commands show up in your server, register each command's \`data\` JSON with the
|
|
570
|
+
Discord REST API (see the discord.js command deployment guide).
|
|
571
|
+
`;
|
|
572
|
+
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Main flow
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
const main = async () => {
|
|
578
|
+
const spinner = createSpinner(
|
|
579
|
+
"Creating template! Do not exit the process...",
|
|
580
|
+
).start({ color: "green" });
|
|
581
|
+
|
|
582
|
+
const startedAt = process.hrtime.bigint();
|
|
583
|
+
|
|
584
|
+
const step = async (label, color, fn) => {
|
|
585
|
+
spinner.update({ text: label, color });
|
|
586
|
+
try {
|
|
587
|
+
await fn();
|
|
588
|
+
} catch (error) {
|
|
589
|
+
spinner.error({
|
|
590
|
+
text: `Failed to ${label.toLowerCase()}`,
|
|
591
|
+
color: "red",
|
|
168
592
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
\`\`\`
|
|
272
|
-
|
|
273
|
-
to fully initialize eslint.
|
|
274
|
-
`
|
|
275
|
-
);
|
|
276
|
-
const diff = process.hrtime(time);
|
|
277
|
-
const seconds =
|
|
278
|
-
(diff[0] * NS_PER_SEC +
|
|
279
|
-
diff[1] * MS_PER_NS) /
|
|
280
|
-
NS_PER_SEC;
|
|
281
|
-
spinner.success({
|
|
282
|
-
text: `Successfully Generated ${
|
|
283
|
-
package.name
|
|
284
|
-
} template in ${seconds.toFixed(3)}s`,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
);
|
|
290
|
-
}
|
|
593
|
+
const message =
|
|
594
|
+
error instanceof Error ? error.message : String(error);
|
|
595
|
+
console.error(`\n${message}`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// 1. Folder structure (kept from the original template on purpose)
|
|
601
|
+
await step("Scaffolding project structure", "green", () => {
|
|
602
|
+
for (const dir of [
|
|
603
|
+
"src",
|
|
604
|
+
"src/client",
|
|
605
|
+
"src/commands",
|
|
606
|
+
"src/events",
|
|
607
|
+
"src/utils",
|
|
608
|
+
"src/interfaces",
|
|
609
|
+
]) {
|
|
610
|
+
fs.mkdirSync(path.join(process.cwd(), dir), { recursive: true });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
writeFiles([
|
|
614
|
+
{ path: "src/index.ts", content: INDEX_TS },
|
|
615
|
+
{ path: "src/client/index.ts", content: CLIENT_TS },
|
|
616
|
+
{ path: "src/commands/ping.ts", content: PING_COMMAND_TS },
|
|
617
|
+
{ path: "src/events/ready.ts", content: READY_EVENT_TS },
|
|
618
|
+
{
|
|
619
|
+
path: "src/events/interactionCreate.ts",
|
|
620
|
+
content: INTERACTION_CREATE_TS,
|
|
621
|
+
},
|
|
622
|
+
{ path: "src/interfaces/command.ts", content: COMMAND_INTERFACE_TS },
|
|
623
|
+
{ path: "src/interfaces/event.ts", content: EVENT_INTERFACE_TS },
|
|
624
|
+
{ path: "src/utils/modules.ts", content: MODULES_TS },
|
|
625
|
+
{ path: "src/utils/loadCommands.ts", content: LOAD_COMMANDS_TS },
|
|
626
|
+
{ path: "src/utils/loadEvents.ts", content: LOAD_EVENTS_TS },
|
|
627
|
+
]);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// 2. Config files + package.json
|
|
631
|
+
await step("Writing configuration files", "cyan", () => {
|
|
632
|
+
writeFiles([
|
|
633
|
+
{ path: "tsconfig.json", content: TS_CONFIG },
|
|
634
|
+
{ path: ".prettierrc.json", content: PRETTIER_CONFIG },
|
|
635
|
+
{ path: ".prettierignore", content: PRETTIER_IGNORE },
|
|
636
|
+
{ path: ".gitignore", content: GITIGNORE },
|
|
637
|
+
{ path: ".env.example", content: ENV_EXAMPLE },
|
|
638
|
+
{ path: "eslint.config.mjs", content: ESLINT_CONFIG },
|
|
639
|
+
]);
|
|
640
|
+
|
|
641
|
+
fs.writeFileSync(
|
|
642
|
+
path.join(process.cwd(), "package.json"),
|
|
643
|
+
`${JSON.stringify(
|
|
644
|
+
{
|
|
645
|
+
name: projectName,
|
|
646
|
+
version: "0.1.0",
|
|
647
|
+
private: true,
|
|
648
|
+
description: "A Discord bot scaffolded with create-bot-ts",
|
|
649
|
+
type: "module",
|
|
650
|
+
engines: { node: ">=24.17.0" },
|
|
651
|
+
scripts: {
|
|
652
|
+
dev: "tsx watch src/index.ts",
|
|
653
|
+
build: "tsc",
|
|
654
|
+
start: "node dist/index.js",
|
|
655
|
+
typecheck: "tsc --noEmit",
|
|
656
|
+
lint: "eslint .",
|
|
657
|
+
"lint:fix": "eslint . --fix",
|
|
658
|
+
format: "prettier --write .",
|
|
659
|
+
"format:check": "prettier --check .",
|
|
660
|
+
},
|
|
661
|
+
"lint-staged": {
|
|
662
|
+
"*.ts": ["eslint --fix", "prettier --write"],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
null,
|
|
666
|
+
2,
|
|
667
|
+
)}\n`,
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// 3. Git + Yarn 4
|
|
672
|
+
await step("Initializing git repository", "red", () =>
|
|
673
|
+
run("git init -b main"),
|
|
674
|
+
);
|
|
675
|
+
await step("Enabling Yarn 4 (stable)", "blue", () =>
|
|
676
|
+
run("yarn set version stable"),
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// Keep whatever `yarn set version` wrote (e.g. yarnPath), then add the linker.
|
|
680
|
+
await step("Configuring Yarn (node-modules linker)", "blue", () => {
|
|
681
|
+
const yarnrcPath = path.join(process.cwd(), ".yarnrc.yml");
|
|
682
|
+
const existing = fs.existsSync(yarnrcPath)
|
|
683
|
+
? fs.readFileSync(yarnrcPath, "utf8")
|
|
684
|
+
: "";
|
|
685
|
+
if (!existing.includes("nodeLinker")) {
|
|
686
|
+
const linker =
|
|
687
|
+
"# Install dependencies into node_modules for\n" +
|
|
688
|
+
"# maximum tooling compatibility.\n" +
|
|
689
|
+
"nodeLinker: node-modules\n";
|
|
690
|
+
fs.writeFileSync(
|
|
691
|
+
yarnrcPath,
|
|
692
|
+
existing.trimEnd() === ""
|
|
693
|
+
? linker
|
|
694
|
+
: `${existing.trimEnd()}\n\n${linker}`,
|
|
291
695
|
);
|
|
292
|
-
}
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// 4. Dependencies
|
|
700
|
+
await step("Adding runtime dependencies", "blue", () =>
|
|
701
|
+
run(
|
|
702
|
+
"yarn add discord.js bufferutil zlib-sync consola common-tags table dotenv",
|
|
703
|
+
),
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
await step("Adding development dependencies", "yellow", () =>
|
|
707
|
+
run(
|
|
708
|
+
"yarn add -D 'typescript@^6' tsx @types/node eslint @eslint/js typescript-eslint eslint-config-prettier prettier globals husky lint-staged",
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
// 5. Husky. Yarn does not run the `prepare` lifecycle, so husky is wired
|
|
713
|
+
// through `postinstall` instead (https://typicode.github.io/husky/how-to.html).
|
|
714
|
+
await step("Setting up husky + lint-staged", "cyan", async () => {
|
|
715
|
+
fs.mkdirSync(path.join(process.cwd(), ".husky"), { recursive: true });
|
|
716
|
+
fs.writeFileSync(
|
|
717
|
+
path.join(process.cwd(), ".husky", "pre-commit"),
|
|
718
|
+
"yarn lint-staged\n",
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
722
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
723
|
+
pkg.scripts.postinstall = "husky";
|
|
724
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
725
|
+
|
|
726
|
+
await run("yarn postinstall");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// 6. README
|
|
730
|
+
await step("Generating README.md", "magenta", () => {
|
|
731
|
+
fs.writeFileSync(
|
|
732
|
+
path.join(process.cwd(), "README.md"),
|
|
733
|
+
`${README.trimEnd()}\n`,
|
|
734
|
+
);
|
|
293
735
|
});
|
|
294
|
-
|
|
736
|
+
|
|
737
|
+
const seconds = Number(process.hrtime.bigint() - startedAt) / 1_000_000_000;
|
|
738
|
+
spinner.success({
|
|
739
|
+
text: `Successfully generated ${projectName} template in ${seconds.toFixed(3)}s`,
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
if (require.main === module) {
|
|
744
|
+
main();
|
|
745
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bot-ts",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MahoMuri's personal template for Discord bots",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "MahoMuri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"create-bot-ts": "index.js"
|
|
10
10
|
},
|
|
11
11
|
"private": false,
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24.17.0"
|
|
14
|
+
},
|
|
12
15
|
"dependencies": {
|
|
13
16
|
"nanospinner": "^1.0.0",
|
|
14
17
|
"outdent": "^0.8.0"
|