create-chapter 0.1.2 → 0.1.4
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/dist/choose.js +27 -0
- package/dist/index.js +84 -151
- package/dist/node-version.js +13 -0
- package/dist/package-manager.js +59 -0
- package/dist/project.js +82 -0
- package/dist/templates.js +31 -0
- package/package.json +21 -4
- package/templates/default/.env.example +4 -0
- package/{template → templates/default}/_gitignore +2 -1
- package/templates/default/package.json +23 -0
- package/templates/default/public/hello.ogg +0 -0
- package/templates/default/src/commands/hello.ts +22 -0
- package/templates/default/src/commands/ping.ts +37 -0
- package/templates/default/src/commands/play.ts +40 -0
- package/templates/default/src/components/buttons/again.ts +19 -0
- package/templates/default/src/events/reactionAdd/wave.ts +11 -0
- package/templates/default/src/events/ready/online.ts +30 -0
- package/templates/default/src/messages/en-US.ts +33 -0
- package/templates/default/src/messages/fr.ts +36 -0
- package/templates/default/src/presence.ts +8 -0
- package/templates/default/tsconfig.json +20 -0
- package/template/.env.example +0 -5
- package/template/package.json +0 -17
- package/template/src/components/buttons/sayHi.ts +0 -28
- package/template/src/events/interactionCreate.ts +0 -11
- package/template/src/events/messageCreate.ts +0 -25
- package/template/src/events/messageDelete.ts +0 -6
- package/template/src/events/messageReactionAdd.ts +0 -12
- package/template/src/events/ready.ts +0 -24
- package/template/src/presence.ts +0 -35
- package/template/tsconfig.json +0 -15
package/dist/choose.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
/**
|
|
3
|
+
* A select menu that never asks a question with only one answer: when a single
|
|
4
|
+
* choice can be picked, it is taken right away and the user is told why.
|
|
5
|
+
* Returns `undefined` when no choice can be picked, and the cancel symbol when
|
|
6
|
+
* the user presses Ctrl+C or Esc.
|
|
7
|
+
*/
|
|
8
|
+
export async function choose({ message, choices, initialValue, onlyChoice, }) {
|
|
9
|
+
const enabled = choices.filter(choice => !choice.disabled);
|
|
10
|
+
const [first] = enabled;
|
|
11
|
+
if (!first)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (enabled.length === 1) {
|
|
14
|
+
p.log.info(onlyChoice(first.value));
|
|
15
|
+
return first.value;
|
|
16
|
+
}
|
|
17
|
+
// clack's option type can't be resolved for a generic `Value`, so the menu is
|
|
18
|
+
// typed with `string`: it only ever returns one of `choices`.
|
|
19
|
+
const picked = await p.select({
|
|
20
|
+
message,
|
|
21
|
+
initialValue: enabled.some(choice => choice.value === initialValue)
|
|
22
|
+
? initialValue
|
|
23
|
+
: first.value,
|
|
24
|
+
options: choices,
|
|
25
|
+
});
|
|
26
|
+
return picked;
|
|
27
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,163 +1,96 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
14
|
-
function exitIfCancelled(value) {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { relative, resolve } from 'node:path';
|
|
5
|
+
import { detectPackageManager, install, installedVersions, packageManagers, runScript, } from './package-manager.js';
|
|
6
|
+
import { choose } from './choose.js';
|
|
7
|
+
import { createProject, validateTargetDir } from './project.js';
|
|
8
|
+
import { listTemplates } from './templates.js';
|
|
9
|
+
import { nodeTooOld } from './node-version.js';
|
|
10
|
+
/** Stops cleanly when the user presses Ctrl+C or Esc on a prompt. */
|
|
11
|
+
function orExit(value) {
|
|
15
12
|
if (p.isCancel(value)) {
|
|
16
|
-
p.cancel('
|
|
13
|
+
p.cancel('Cancelled, nothing was created.');
|
|
17
14
|
process.exit(0);
|
|
18
15
|
}
|
|
19
16
|
return value;
|
|
20
17
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
function isEmptyDir(dir) {
|
|
30
|
-
if (!existsSync(dir))
|
|
31
|
-
return true;
|
|
32
|
-
// We tolerate .git to be able to do `git init` before `pnpm create chapter .`
|
|
33
|
-
return readdirSync(dir).every(f => f === '.git');
|
|
18
|
+
// create-chapter and chapterjs are always released with the same version.
|
|
19
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
20
|
+
p.intro(`Create a ChapterJS bot (v${version})`);
|
|
21
|
+
const tooOld = nodeTooOld();
|
|
22
|
+
if (tooOld) {
|
|
23
|
+
p.cancel(tooOld);
|
|
24
|
+
process.exit(1);
|
|
34
25
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
// Started now so it runs while the user answers the first question.
|
|
27
|
+
const versionsCheck = installedVersions();
|
|
28
|
+
// `pnpm create chapter my-bot` skips the first question when the folder is valid.
|
|
29
|
+
const argDir = process.argv[2];
|
|
30
|
+
const argError = argDir === undefined ? undefined : validateTargetDir(argDir);
|
|
31
|
+
if (argError)
|
|
32
|
+
p.log.warn(argError);
|
|
33
|
+
const dir = argDir !== undefined && argError === undefined
|
|
34
|
+
? argDir.trim()
|
|
35
|
+
: orExit(await p.text({
|
|
36
|
+
message: 'Where should the project be created? (. for the current folder)',
|
|
37
|
+
placeholder: 'my-bot',
|
|
38
|
+
defaultValue: 'my-bot',
|
|
39
|
+
validate: value => validateTargetDir(value || 'my-bot'),
|
|
40
|
+
})).trim();
|
|
41
|
+
const versions = await versionsCheck;
|
|
42
|
+
const pm = orExit(await choose({
|
|
43
|
+
message: 'Which package manager do you want to use?',
|
|
44
|
+
initialValue: detectPackageManager(),
|
|
45
|
+
choices: packageManagers.map(value => {
|
|
46
|
+
const version = versions[value];
|
|
47
|
+
return version === undefined
|
|
48
|
+
? { value, label: `${value} (not installed)`, disabled: true }
|
|
49
|
+
: { value, hint: `v${version}` };
|
|
50
|
+
}),
|
|
51
|
+
onlyChoice: value => `Using ${value}, the only package manager installed`,
|
|
52
|
+
}));
|
|
53
|
+
if (pm === undefined) {
|
|
54
|
+
p.cancel(`No package manager found. Install one of ${packageManagers.join(', ')}, then run this command again.`);
|
|
55
|
+
process.exit(1);
|
|
43
56
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
const templates = await listTemplates();
|
|
58
|
+
const template = orExit(await choose({
|
|
59
|
+
message: 'Which template do you want to start from?',
|
|
60
|
+
choices: templates.map(({ name, description }) => ({
|
|
61
|
+
value: name,
|
|
62
|
+
hint: description,
|
|
63
|
+
})),
|
|
64
|
+
onlyChoice: value => `Using the ${value} template`,
|
|
65
|
+
}));
|
|
66
|
+
if (template === undefined) {
|
|
67
|
+
p.cancel('No template found. Reinstall create-chapter and try again.');
|
|
68
|
+
process.exit(1);
|
|
47
69
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Ctrl+C during install: stop the child before exiting
|
|
61
|
-
const onSigint = () => {
|
|
62
|
-
child.kill('SIGINT');
|
|
63
|
-
log.error('Installation cancelled.');
|
|
64
|
-
process.exit(130);
|
|
65
|
-
};
|
|
66
|
-
process.once('SIGINT', onSigint);
|
|
67
|
-
// raw: keep the chunks as-is (line breaks included)
|
|
68
|
-
const onData = (chunk) => log.message(chunk.toString(), { raw: true });
|
|
69
|
-
child.stdout.on('data', onData);
|
|
70
|
-
child.stderr.on('data', onData);
|
|
71
|
-
child.on('error', err => {
|
|
72
|
-
process.off('SIGINT', onSigint);
|
|
73
|
-
log.error(`Could not run ${pm}: ${err.message}`);
|
|
74
|
-
reject(err);
|
|
75
|
-
});
|
|
76
|
-
child.on('close', code => {
|
|
77
|
-
process.off('SIGINT', onSigint);
|
|
78
|
-
if (code === 0) {
|
|
79
|
-
log.success('Dependencies installed');
|
|
80
|
-
resolve();
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
log.error(`${pm} install failed (code ${code})`, { showLog: true });
|
|
84
|
-
reject(new Error(`${pm} install failed (code ${code})`));
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
});
|
|
70
|
+
await createProject({
|
|
71
|
+
dir,
|
|
72
|
+
template,
|
|
73
|
+
chapterjsVersion: `^${version}`,
|
|
74
|
+
packageManager: pm,
|
|
75
|
+
});
|
|
76
|
+
p.log.success(`Project created in ${resolve(dir)}`);
|
|
77
|
+
const spinner = p.spinner();
|
|
78
|
+
spinner.start(`Installing dependencies with ${pm}`);
|
|
79
|
+
const result = await install(pm, dir);
|
|
80
|
+
if (result.ok) {
|
|
81
|
+
spinner.stop(`Dependencies installed with ${pm}`);
|
|
88
82
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const interactive = p.isTTY(process.stdout) && !p.isCI();
|
|
94
|
-
let projectDir;
|
|
95
|
-
if (argDir && !validateDir(argDir)) {
|
|
96
|
-
projectDir = argDir;
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
if (argDir)
|
|
100
|
-
p.log.warn(validateDir(argDir));
|
|
101
|
-
const answer = exitIfCancelled(await p.text({
|
|
102
|
-
message: 'What is the name of your project? (. for the current directory)',
|
|
103
|
-
placeholder: DEFAULT_NAME,
|
|
104
|
-
defaultValue: DEFAULT_NAME,
|
|
105
|
-
validate: validateDir,
|
|
106
|
-
}));
|
|
107
|
-
projectDir = answer.trim() || DEFAULT_NAME;
|
|
108
|
-
}
|
|
109
|
-
const targetDir = path.resolve(projectDir);
|
|
110
|
-
const isCurrentDir = targetDir === process.cwd();
|
|
111
|
-
// "." -> current directory
|
|
112
|
-
const packageName = toValidPackageName(path.basename(targetDir));
|
|
113
|
-
const s = p.spinner();
|
|
114
|
-
s.start('Copying template');
|
|
115
|
-
cpSync(TEMPLATE_DIR, targetDir, { recursive: true });
|
|
116
|
-
for (const [from, to] of Object.entries(RENAME_FILES)) {
|
|
117
|
-
const src = path.join(targetDir, from);
|
|
118
|
-
if (existsSync(src))
|
|
119
|
-
renameSync(src, path.join(targetDir, to));
|
|
120
|
-
}
|
|
121
|
-
const pkgPath = path.join(targetDir, 'package.json');
|
|
122
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
123
|
-
pkg.name = packageName;
|
|
124
|
-
// The template depends on `workspace:*` inside the monorepo; chapterjs and
|
|
125
|
-
// create-chapter are released together, so pin the same version.
|
|
126
|
-
pkg.dependencies.chapterjs = `^${OWN_VERSION}`;
|
|
127
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
128
|
-
s.stop(`Project "${packageName}" created in ${targetDir}`);
|
|
129
|
-
let pm = detectPackageManager();
|
|
130
|
-
if (!pm && interactive) {
|
|
131
|
-
pm = exitIfCancelled(await p.select({
|
|
132
|
-
message: 'Which package manager do you want to use?',
|
|
133
|
-
options: PACKAGE_MANAGERS.map(value => ({ value, label: value })),
|
|
134
|
-
initialValue: 'pnpm',
|
|
135
|
-
}));
|
|
136
|
-
}
|
|
137
|
-
pm ??= 'pnpm';
|
|
138
|
-
let shouldInstall = !noInstall;
|
|
139
|
-
if (shouldInstall && interactive) {
|
|
140
|
-
shouldInstall = exitIfCancelled(await p.confirm({ message: `Install dependencies with ${pm}?` }));
|
|
141
|
-
}
|
|
142
|
-
let installed = false;
|
|
143
|
-
if (shouldInstall) {
|
|
144
|
-
try {
|
|
145
|
-
await install(pm, targetDir);
|
|
146
|
-
installed = true;
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// Error already shown by the task log
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const steps = [
|
|
153
|
-
...(isCurrentDir ? [] : [`cd ${projectDir}`]),
|
|
154
|
-
...(installed ? [] : [`${pm} install`]),
|
|
155
|
-
`${pm === 'npm' ? 'npm run' : pm} dev`,
|
|
156
|
-
];
|
|
157
|
-
p.note(steps.join('\n'), 'Next steps');
|
|
158
|
-
p.outro('Happy dev!');
|
|
83
|
+
else {
|
|
84
|
+
spinner.error(`The dependencies could not be installed with ${pm}`);
|
|
85
|
+
if (result.output)
|
|
86
|
+
p.log.message(result.output);
|
|
159
87
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
})
|
|
88
|
+
const cd = relative(process.cwd(), resolve(dir));
|
|
89
|
+
const steps = [
|
|
90
|
+
...(cd ? [`cd ${cd.includes(' ') ? `"${cd}"` : cd}`] : []),
|
|
91
|
+
...(result.ok ? [] : [`${pm} install`]),
|
|
92
|
+
'Copy .env.example to .env and fill in BOT_TOKEN and DEV_GUILD_ID',
|
|
93
|
+
runScript(pm, 'dev'),
|
|
94
|
+
];
|
|
95
|
+
p.note(steps.join('\n'), 'Next steps');
|
|
96
|
+
p.outro('Happy building!');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The oldest Node.js a ChapterJS bot runs on, as `engines` says. */
|
|
2
|
+
export const MINIMUM_NODE = 24;
|
|
3
|
+
/**
|
|
4
|
+
* What to tell the person when this Node.js is older than ChapterJS needs,
|
|
5
|
+
* or `null` when it is recent enough: refused before creating anything,
|
|
6
|
+
* since the bot could not run.
|
|
7
|
+
*/
|
|
8
|
+
export function nodeTooOld(version = process.versions.node) {
|
|
9
|
+
const major = Number(version.split('.')[0]);
|
|
10
|
+
if (major >= MINIMUM_NODE)
|
|
11
|
+
return null;
|
|
12
|
+
return `ChapterJS needs Node.js ${MINIMUM_NODE} or newer, and this is Node.js ${version}. Install the LTS version from https://nodejs.org, then run the command again.`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
/** In menu order: pnpm, the one ChapterJS itself uses, comes first. */
|
|
4
|
+
export const packageManagers = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
5
|
+
/**
|
|
6
|
+
* The package manager that launched `create chapter`, read from the user agent
|
|
7
|
+
* it sets (e.g. `pnpm/11.0.0 npm/? node/v24.0.0 darwin arm64`).
|
|
8
|
+
*/
|
|
9
|
+
export function detectPackageManager() {
|
|
10
|
+
const name = process.env.npm_config_user_agent?.split('/')[0];
|
|
11
|
+
return packageManagers.find(pm => pm === name) ?? 'pnpm';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The installed version of each package manager, or `undefined` when it isn't
|
|
15
|
+
* installed. All are checked at once, so this takes as long as the slowest one.
|
|
16
|
+
*/
|
|
17
|
+
export async function installedVersions() {
|
|
18
|
+
const versions = await Promise.all(packageManagers.map(pm => new Promise(resolve => {
|
|
19
|
+
let output = '';
|
|
20
|
+
const child = spawn(pm, ['--version'], {
|
|
21
|
+
// Outside the current folder: a `packageManager` field in a parent
|
|
22
|
+
// package.json makes corepack refuse to run any other manager.
|
|
23
|
+
cwd: tmpdir(),
|
|
24
|
+
// Corepack must never stop to ask before downloading a manager.
|
|
25
|
+
env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' },
|
|
26
|
+
shell: process.platform === 'win32',
|
|
27
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
28
|
+
});
|
|
29
|
+
child.stdout.on('data', (chunk) => (output += chunk));
|
|
30
|
+
child.on('error', () => resolve(undefined));
|
|
31
|
+
child.on('close', code => resolve(code === 0 ? output.trim() || undefined : undefined));
|
|
32
|
+
})));
|
|
33
|
+
return Object.fromEntries(packageManagers.map((pm, i) => [pm, versions[i]]));
|
|
34
|
+
}
|
|
35
|
+
/** The command that runs a package.json script, e.g. `pnpm dev` or `npm run dev`. */
|
|
36
|
+
export function runScript(pm, script) {
|
|
37
|
+
return pm === 'npm' ? `npm run ${script}` : `${pm} ${script}`;
|
|
38
|
+
}
|
|
39
|
+
/** Runs `<pm> install` in `cwd`, capturing the output instead of printing it. */
|
|
40
|
+
export function install(pm, cwd) {
|
|
41
|
+
return new Promise(resolve => {
|
|
42
|
+
let output = '';
|
|
43
|
+
const child = spawn(pm, ['install'], {
|
|
44
|
+
cwd,
|
|
45
|
+
// Package managers are `.cmd` shims on Windows, which only a shell can run.
|
|
46
|
+
shell: process.platform === 'win32',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
child.stdout.on('data', (chunk) => (output += chunk));
|
|
50
|
+
child.stderr.on('data', (chunk) => (output += chunk));
|
|
51
|
+
child.on('error', (error) => {
|
|
52
|
+
const reason = error.code === 'ENOENT'
|
|
53
|
+
? `${pm} is not installed on this computer.`
|
|
54
|
+
: error.message;
|
|
55
|
+
resolve({ ok: false, output: reason });
|
|
56
|
+
});
|
|
57
|
+
child.on('close', code => resolve({ ok: code === 0, output: output.trim() }));
|
|
58
|
+
});
|
|
59
|
+
}
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import { templatesDir } from './templates.js';
|
|
5
|
+
/** Files that don't make a folder "used": a project can still be created there. */
|
|
6
|
+
const ignoredFiles = new Set([
|
|
7
|
+
'.git',
|
|
8
|
+
'.DS_Store',
|
|
9
|
+
'Thumbs.db',
|
|
10
|
+
'.idea',
|
|
11
|
+
'.vscode',
|
|
12
|
+
]);
|
|
13
|
+
/** Returns why `input` can't hold a new project, or `undefined` when it can. */
|
|
14
|
+
export function validateTargetDir(input) {
|
|
15
|
+
const dir = input?.trim();
|
|
16
|
+
if (!dir)
|
|
17
|
+
return 'Type a folder name, or . to use the current folder.';
|
|
18
|
+
const path = resolve(dir);
|
|
19
|
+
if (!existsSync(path))
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!statSync(path).isDirectory())
|
|
22
|
+
return `${dir} is a file, not a folder. Pick another name.`;
|
|
23
|
+
const used = readdirSync(path).some(file => !ignoredFiles.has(file));
|
|
24
|
+
if (used) {
|
|
25
|
+
return path === process.cwd()
|
|
26
|
+
? 'The current folder is not empty. Type a name to create a new folder instead.'
|
|
27
|
+
: `The folder ${dir} is not empty. Pick another name.`;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** A valid npm package name from a folder name, e.g. `Mon Bot Été` → `mon-bot-ete`. */
|
|
32
|
+
export function toPackageName(dir) {
|
|
33
|
+
const name = basename(resolve(dir))
|
|
34
|
+
// `é` → `e`: split letters from their accents, then drop the accents.
|
|
35
|
+
.normalize('NFD')
|
|
36
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9-~._]+/g, '-')
|
|
39
|
+
.replace(/-{2,}/g, '-')
|
|
40
|
+
// npm names can't start with `.` or `_`, and 214 characters is the maximum.
|
|
41
|
+
.replace(/^[-._]+/, '')
|
|
42
|
+
.slice(0, 214)
|
|
43
|
+
.replace(/[-.]+$/, '');
|
|
44
|
+
return name || 'chapter-bot';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* What pnpm needs to install a project whose framework comes with esbuild
|
|
48
|
+
* (which `chapterjs build` compiles the bot with). esbuild has an install
|
|
49
|
+
* script it does not need; pnpm refuses to go on until the project says whether that
|
|
50
|
+
* script may run. It may not: no script of a dependency runs.
|
|
51
|
+
* `allowBuilds` is read by pnpm 11, `ignoredBuiltDependencies` by pnpm 10.
|
|
52
|
+
*/
|
|
53
|
+
export const PNPM_WORKSPACE = `# esbuild works without its install script: pnpm is told not to run it.
|
|
54
|
+
allowBuilds:
|
|
55
|
+
esbuild: false
|
|
56
|
+
ignoredBuiltDependencies:
|
|
57
|
+
- esbuild
|
|
58
|
+
`;
|
|
59
|
+
/** Copies the template into `dir` and names the project after its folder. */
|
|
60
|
+
export async function createProject({ dir, template, chapterjsVersion, packageManager, templatesRoot = templatesDir, }) {
|
|
61
|
+
const root = resolve(dir);
|
|
62
|
+
await cp(join(templatesRoot, template), root, { recursive: true });
|
|
63
|
+
// Where the files the bot sends go (`asset()`): git and npm keep no empty
|
|
64
|
+
// folder, so no template can ship it; every project gets it here.
|
|
65
|
+
await mkdir(join(root, 'public'), { recursive: true });
|
|
66
|
+
// npm strips `.gitignore` from published packages, so templates ship it as `_gitignore`.
|
|
67
|
+
const gitignore = join(root, '_gitignore');
|
|
68
|
+
if (existsSync(gitignore))
|
|
69
|
+
await rename(gitignore, join(root, '.gitignore'));
|
|
70
|
+
const pkgPath = join(root, 'package.json');
|
|
71
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
72
|
+
pkg.name = toPackageName(dir);
|
|
73
|
+
// The template's description is for the menu, not for the user's bot.
|
|
74
|
+
delete pkg.description;
|
|
75
|
+
pkg.dependencies = { ...pkg.dependencies, chapterjs: chapterjsVersion };
|
|
76
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
77
|
+
// Only for the package manager that reads it: nothing to explain to the
|
|
78
|
+
// others.
|
|
79
|
+
if (packageManager === 'pnpm') {
|
|
80
|
+
await writeFile(join(root, 'pnpm-workspace.yaml'), PNPM_WORKSPACE);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/** `templates/` ships next to `dist/` in the published package. */
|
|
6
|
+
export const templatesDir = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Every folder of `templates/` with a package.json is a template: adding one
|
|
9
|
+
* needs no code change.
|
|
10
|
+
*/
|
|
11
|
+
export async function listTemplates(dir = templatesDir) {
|
|
12
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
13
|
+
const templates = await Promise.all(entries
|
|
14
|
+
.filter(entry => entry.isDirectory())
|
|
15
|
+
.map(async (entry) => {
|
|
16
|
+
const pkgPath = join(dir, entry.name, 'package.json');
|
|
17
|
+
if (!existsSync(pkgPath))
|
|
18
|
+
return undefined;
|
|
19
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
20
|
+
const description = typeof pkg.description === 'string' ? pkg.description : undefined;
|
|
21
|
+
return { name: entry.name, description };
|
|
22
|
+
}));
|
|
23
|
+
// `default` first, so it is the preselected choice.
|
|
24
|
+
return templates
|
|
25
|
+
.filter(template => template !== undefined)
|
|
26
|
+
.sort((a, b) => a.name === 'default'
|
|
27
|
+
? -1
|
|
28
|
+
: b.name === 'default'
|
|
29
|
+
? 1
|
|
30
|
+
: a.name.localeCompare(b.name));
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-chapter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/chapterjs/chapterjs.git",
|
|
8
|
+
"directory": "packages/create-chapter"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://chapterjs.dev",
|
|
11
|
+
"bugs": "https://github.com/chapterjs/chapterjs/issues",
|
|
5
12
|
"type": "module",
|
|
6
13
|
"bin": {
|
|
7
14
|
"create-chapter": "./dist/index.js"
|
|
8
15
|
},
|
|
9
16
|
"files": [
|
|
10
17
|
"dist",
|
|
11
|
-
"
|
|
18
|
+
"templates"
|
|
12
19
|
],
|
|
13
20
|
"devDependencies": {
|
|
21
|
+
"@chapterjs/test-utils": "0.0.0",
|
|
14
22
|
"@types/node": "^24.0.0",
|
|
15
|
-
"
|
|
23
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
24
|
+
"typescript": "^7.0.2",
|
|
25
|
+
"vitest": "^5.0.3"
|
|
16
26
|
},
|
|
17
27
|
"dependencies": {
|
|
18
28
|
"@clack/prompts": "^1.8.1"
|
|
19
29
|
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=24"
|
|
32
|
+
},
|
|
20
33
|
"scripts": {
|
|
21
34
|
"build": "tsc",
|
|
22
|
-
"dev": "tsc --watch"
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"check-types": "tsc --noEmit && tsc -p test",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"coverage": "vitest run --coverage"
|
|
23
40
|
}
|
|
24
41
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chapter-bot",
|
|
3
|
+
"description": "A minimal bot, ready to grow",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "chapterjs dev",
|
|
8
|
+
"start": "chapterjs start",
|
|
9
|
+
"build": "chapterjs build",
|
|
10
|
+
"typecheck": "tsc -b",
|
|
11
|
+
"postinstall": "chapterjs sync"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chapterjs": "latest"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^24.0.0",
|
|
18
|
+
"typescript": "^7.0.2"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { asset, command } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// /hello joins your voice channel and plays public/hello.ogg. An Opus file
|
|
4
|
+
// (.ogg, .webm) plays as it is; an .mp3 or a link needs ffmpeg installed.
|
|
5
|
+
export default command({
|
|
6
|
+
async run({ interaction, member, t }) {
|
|
7
|
+
const channel = member.voiceChannel;
|
|
8
|
+
if (!channel) {
|
|
9
|
+
await interaction.reply({ content: t('joinVoice'), ephemeral: true });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Answer first: a command has 3 seconds, joining takes a moment.
|
|
13
|
+
await interaction.reply(t('hello', { channel: channel.name }));
|
|
14
|
+
const voice = await channel.join();
|
|
15
|
+
// Whatever happens while it plays, the bot leaves after.
|
|
16
|
+
try {
|
|
17
|
+
await voice.play(asset('hello.ogg'));
|
|
18
|
+
} finally {
|
|
19
|
+
await voice.leave();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
import again from '../components/buttons/again';
|
|
3
|
+
|
|
4
|
+
// The path of the file is the name of the command: this one is /ping.
|
|
5
|
+
// A file in a folder is a subcommand: src/commands/mod/ban.ts is /mod ban.
|
|
6
|
+
export default command({
|
|
7
|
+
// Only the person sees the answer, so `t` speaks their language.
|
|
8
|
+
ephemeral: true,
|
|
9
|
+
async run({ interaction, t }) {
|
|
10
|
+
// A button of src/components/buttons/, with the data it carries, and a
|
|
11
|
+
// text of src/messages/ in the language of who will read the answer.
|
|
12
|
+
await interaction.reply({
|
|
13
|
+
content: t('pong'),
|
|
14
|
+
components: [again({ count: 1 })],
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// A command can ask for options, typed for you in `run`, suggest values
|
|
20
|
+
// while the person types, and make each person wait between two uses:
|
|
21
|
+
//
|
|
22
|
+
// export default command({
|
|
23
|
+
// description: 'Says hello to someone',
|
|
24
|
+
// cooldown: '10s',
|
|
25
|
+
// options: {
|
|
26
|
+
// who: { type: 'user', description: 'Who to greet', required: true },
|
|
27
|
+
// greeting: { type: 'string', description: 'How to greet' },
|
|
28
|
+
// },
|
|
29
|
+
// autocomplete: {
|
|
30
|
+
// // Called while the person types in "greeting", with what they typed.
|
|
31
|
+
// greeting: ({ value }) =>
|
|
32
|
+
// ['Hello', 'Hi', 'Hey'].filter(word => word.startsWith(value)),
|
|
33
|
+
// },
|
|
34
|
+
// async run({ interaction, options }) {
|
|
35
|
+
// await interaction.reply(`${options.greeting ?? 'Hello'} ${options.who}!`);
|
|
36
|
+
// },
|
|
37
|
+
// });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// /play plays a song in your voice channel: a file you send with the
|
|
4
|
+
// command, or a link to an audio file or a stream. play() takes both.
|
|
5
|
+
export default command({
|
|
6
|
+
options: {
|
|
7
|
+
file: { type: 'attachment' },
|
|
8
|
+
url: { type: 'string' },
|
|
9
|
+
},
|
|
10
|
+
async run({ interaction, member, options, t }) {
|
|
11
|
+
const { file, url } = options;
|
|
12
|
+
const song = file ?? url;
|
|
13
|
+
if (
|
|
14
|
+
!song ||
|
|
15
|
+
(file && !file.contentType?.startsWith('audio/')) ||
|
|
16
|
+
(url && !/^https?:\/\//.test(url))
|
|
17
|
+
) {
|
|
18
|
+
await interaction.reply({ content: t('nothingToPlay'), ephemeral: true });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const channel = member.voiceChannel;
|
|
22
|
+
if (!channel) {
|
|
23
|
+
await interaction.reply({ content: t('joinVoice'), ephemeral: true });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const name = file?.filename ?? url ?? '';
|
|
27
|
+
await interaction.reply(
|
|
28
|
+
t('playing', { song: name, channel: channel.name })
|
|
29
|
+
);
|
|
30
|
+
// Joining the channel the bot is already in changes nothing, and a new
|
|
31
|
+
// play() replaces what was playing.
|
|
32
|
+
const voice = await channel.join();
|
|
33
|
+
// Whatever happens while it plays, the bot leaves after.
|
|
34
|
+
try {
|
|
35
|
+
await voice.play(song);
|
|
36
|
+
} finally {
|
|
37
|
+
await voice.leave();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { button } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// The folder is the kind of component (buttons, selects, modals, embeds)
|
|
4
|
+
// and the path of the file is what tells which one was used: you never
|
|
5
|
+
// write an id. `data` is what the button carries from one click to the next.
|
|
6
|
+
const again = button({
|
|
7
|
+
label: 'Again',
|
|
8
|
+
data: { count: 'number' },
|
|
9
|
+
async run({ interaction, data, t }) {
|
|
10
|
+
const count = data.count + 1;
|
|
11
|
+
// Changes the message the button is on, instead of sending a new one.
|
|
12
|
+
await interaction.update({
|
|
13
|
+
content: t('again', { count }),
|
|
14
|
+
components: [again({ count })],
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default again;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { event } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Runs every time someone adds a reaction to a message of your server.
|
|
4
|
+
// Reactions of bots, yours included, are left out: when yours waves back
|
|
5
|
+
// below, this file does not run again. Other folders work the same way:
|
|
6
|
+
// voiceJoin, pollVoteAdd, banAdd... (see the list of events in the docs).
|
|
7
|
+
export default event(async ({ emoji, channel, messageId }) => {
|
|
8
|
+
if (emoji.name !== '👋') return;
|
|
9
|
+
const message = await channel.fetchMessage(messageId);
|
|
10
|
+
await message.react('👋');
|
|
11
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { event } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// The folder is the event: every file of src/events/ready/ runs once, when
|
|
4
|
+
// your bot is connected. The name of the file is yours to choose.
|
|
5
|
+
export default event(({ user, guilds }) => {
|
|
6
|
+
console.log(`${user.username} is online in ${guilds.size} server(s)`);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Something to do by itself, every so often or at given times, is a task:
|
|
10
|
+
// a file of src/tasks/, like src/tasks/report.ts:
|
|
11
|
+
//
|
|
12
|
+
// import { task } from 'chapterjs';
|
|
13
|
+
//
|
|
14
|
+
// export default task({
|
|
15
|
+
// cron: '0 9 * * 1', // every Monday at 9:00
|
|
16
|
+
// async run({ guilds }) {
|
|
17
|
+
// console.log(`A new week in ${guilds.size} server(s)`);
|
|
18
|
+
// },
|
|
19
|
+
// });
|
|
20
|
+
//
|
|
21
|
+
// To react to something else, create a folder named after the event. For
|
|
22
|
+
// example src/events/messageCreate/ping.ts, which answers "Pong!" to "!ping"
|
|
23
|
+
// (reading messages needs the Message Content intent: `chapterjs dev` tells
|
|
24
|
+
// you how to enable it):
|
|
25
|
+
//
|
|
26
|
+
// import { event } from 'chapterjs';
|
|
27
|
+
//
|
|
28
|
+
// export default event(async ({ message }) => {
|
|
29
|
+
// if (message.content === '!ping') await message.reply('Pong!');
|
|
30
|
+
// });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { language } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// One file per language in src/messages/, named after the language. Each
|
|
4
|
+
// function of your project receives `t`, which picks the language of who
|
|
5
|
+
// will read the message: t('pong'), t('again', { count }).
|
|
6
|
+
export default language({
|
|
7
|
+
// The language used when the one of a person or of a server is not here.
|
|
8
|
+
default: true,
|
|
9
|
+
texts: {
|
|
10
|
+
pong: 'Pong! in en-US',
|
|
11
|
+
again: 'Pong in en-US ! ×{count}',
|
|
12
|
+
joinVoice: 'Join a voice channel first, then ask again.',
|
|
13
|
+
hello: 'Coming to {channel}!',
|
|
14
|
+
nothingToPlay:
|
|
15
|
+
'Send an audio file (.mp3, .ogg...) or a link starting with https://.',
|
|
16
|
+
playing: 'Playing {song} in {channel}.',
|
|
17
|
+
},
|
|
18
|
+
commands: {
|
|
19
|
+
ping: {
|
|
20
|
+
description: 'Replies with Pong!',
|
|
21
|
+
},
|
|
22
|
+
hello: {
|
|
23
|
+
description: 'Says hello in your voice channel',
|
|
24
|
+
},
|
|
25
|
+
play: {
|
|
26
|
+
description: 'Plays a song in your voice channel',
|
|
27
|
+
options: {
|
|
28
|
+
file: { description: 'A song to play' },
|
|
29
|
+
url: { description: 'Or the link of a song or a stream' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { language } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Every language has the texts of the default one, with the same
|
|
4
|
+
// {placeholders}: a missing one is reported when the file loads.
|
|
5
|
+
export default language({
|
|
6
|
+
texts: {
|
|
7
|
+
pong: 'Pong! en fr',
|
|
8
|
+
again: 'Pong en fr ! ×{count}',
|
|
9
|
+
joinVoice: 'Rejoignez d’abord un salon vocal, puis redemandez.',
|
|
10
|
+
hello: 'J’arrive dans {channel} !',
|
|
11
|
+
nothingToPlay:
|
|
12
|
+
'Envoyez un fichier audio (.mp3, .ogg...) ou un lien qui commence par https://.',
|
|
13
|
+
playing: 'Lecture de {song} dans {channel}.',
|
|
14
|
+
},
|
|
15
|
+
// What the bot answers by itself can be translated too, phrase by phrase:
|
|
16
|
+
//
|
|
17
|
+
// framework: {
|
|
18
|
+
// command: 'commande',
|
|
19
|
+
// failed: "Quelque chose s'est mal passé avec cette {what}.",
|
|
20
|
+
// },
|
|
21
|
+
commands: {
|
|
22
|
+
ping: {
|
|
23
|
+
description: 'Répond avec Pong !',
|
|
24
|
+
},
|
|
25
|
+
hello: {
|
|
26
|
+
description: 'Dit bonjour dans votre salon vocal',
|
|
27
|
+
},
|
|
28
|
+
play: {
|
|
29
|
+
description: 'Joue une musique dans votre salon vocal',
|
|
30
|
+
options: {
|
|
31
|
+
file: { description: 'Une musique à jouer' },
|
|
32
|
+
url: { description: 'Ou le lien d’une musique ou d’un flux' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { presence } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// What your bot shows under its name: its status, and what it is doing.
|
|
4
|
+
// Save this file and chapterjs dev changes it at once.
|
|
5
|
+
export default presence({
|
|
6
|
+
status: 'online',
|
|
7
|
+
activity: { type: 'playing', name: 'with ChapterJS' },
|
|
8
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "Preserve",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"erasableSyntaxOnly": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"files": [],
|
|
15
|
+
"references": [
|
|
16
|
+
{
|
|
17
|
+
"path": "./.chapterjs"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/template/.env.example
DELETED
package/template/package.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "my-chapter-app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "chapterjs dev",
|
|
8
|
-
"build": "chapterjs build"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"chapterjs": "workspace:*"
|
|
12
|
-
},
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"@types/node": "^24.0.0",
|
|
15
|
-
"typescript": "^7.0.2"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { defineButton } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// A button that handles its own clicks: they don't reach interactionCreate.
|
|
4
|
-
// Its custom_id is generated from this file's path. Send it with
|
|
5
|
-
// `components: [SayHi]`, or change it where it's used:
|
|
6
|
-
// `new SayHi({ label: 'Hello', style: 'success' })`.
|
|
7
|
-
export default defineButton({
|
|
8
|
-
label: 'Say hi',
|
|
9
|
-
emoji: '👋',
|
|
10
|
-
async handler({ interaction, user }) {
|
|
11
|
-
await interaction.reply({
|
|
12
|
-
content: `Hi ${user}! Only you can see this message.`,
|
|
13
|
-
ephemeral: true,
|
|
14
|
-
});
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// Buttons can carry data, given back to the handler:
|
|
19
|
-
//
|
|
20
|
-
// export default defineButton<{ roleId: string }>({
|
|
21
|
-
// label: 'Get the role',
|
|
22
|
-
// async handler({ interaction, member, data }) {
|
|
23
|
-
// await member?.addRole(data.roleId);
|
|
24
|
-
// await interaction.reply({ content: 'Done!', ephemeral: true });
|
|
25
|
-
// },
|
|
26
|
-
// });
|
|
27
|
-
//
|
|
28
|
-
// message.reply({ components: [new GetRole({ data: { roleId: '…' } })] });
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { defineEvent } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// Slash commands, and components whose custom_id you wrote yourself. Clicks
|
|
4
|
-
// on components of src/components/ go to their own handler instead.
|
|
5
|
-
// Answer within 3 seconds, or call `interaction.defer()` first and
|
|
6
|
-
// `interaction.editReply()` later.
|
|
7
|
-
export default defineEvent(async ({ interaction }) => {
|
|
8
|
-
if (interaction.commandName === 'ping') {
|
|
9
|
-
await interaction.reply({ content: 'Pong!', ephemeral: true });
|
|
10
|
-
}
|
|
11
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { defineEvent } from 'chapterjs';
|
|
2
|
-
import SayHi from '../components/buttons/sayHi.ts';
|
|
3
|
-
|
|
4
|
-
// Mention the bot: it reacts and replies with a button (src/components/buttons/sayHi.ts).
|
|
5
|
-
// Messages from bots are ignored, unless you add allowBots: true.
|
|
6
|
-
export default defineEvent(async ({ message, client }) => {
|
|
7
|
-
if (!message.mentions.some(user => user.id === client.user?.id)) return;
|
|
8
|
-
|
|
9
|
-
await message.react('👋');
|
|
10
|
-
await message.reply({
|
|
11
|
-
content: `Hi ${message.author.displayName}!`,
|
|
12
|
-
mention: false,
|
|
13
|
-
components: [SayHi],
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// Without the privileged MessageContent intent, `message.content` is empty
|
|
18
|
-
// except in DMs and messages mentioning the bot. To read every message:
|
|
19
|
-
//
|
|
20
|
-
// export default defineEvent({
|
|
21
|
-
// intents: ['MessageContent'],
|
|
22
|
-
// async handler({ message }) {
|
|
23
|
-
// if (message.content === '!ping') await message.reply('Pong!');
|
|
24
|
-
// },
|
|
25
|
-
// });
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { defineEvent } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
export default defineEvent(async ({ reaction, client }) => {
|
|
4
|
-
if (reaction.userId === client.user?.id) return;
|
|
5
|
-
const name = reaction.member?.displayName ?? reaction.userId;
|
|
6
|
-
console.log(`${name} reacted ${reaction} in ${reaction.channel}`);
|
|
7
|
-
|
|
8
|
-
// Reaction roles: give a role when reacting on a given message
|
|
9
|
-
// if (reaction.messageId === ROLES_MESSAGE_ID && reaction.emoji.name === '🎮') {
|
|
10
|
-
// await reaction.member?.addRole(GAMER_ROLE_ID);
|
|
11
|
-
// }
|
|
12
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { defineEvent } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// Each file in src/events/ handles the Discord event it is named after
|
|
4
|
-
// (messageCreate.ts, guildMemberAdd.ts…), reloaded on save. Several handlers
|
|
5
|
-
// for one event go in a folder: src/events/messageCreate/*.ts. Files and
|
|
6
|
-
// folders starting with _ are ignored, for shared code.
|
|
7
|
-
// The gateway intents each event needs are requested automatically.
|
|
8
|
-
//
|
|
9
|
-
// Handlers receive what the event is about: { user, guilds } here,
|
|
10
|
-
// { message, channel } in messageCreate, { interaction } in
|
|
11
|
-
// interactionCreate… plus `client` and the raw `data` sent by Discord.
|
|
12
|
-
export default defineEvent(({ user, guilds }) => {
|
|
13
|
-
console.log(`${user.displayName} is ready in ${guilds.length} server(s)`);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
// Some events need a privileged intent, enabled in the Developer Portal
|
|
17
|
-
// (Bot > Privileged Gateway Intents), otherwise the bot can't connect:
|
|
18
|
-
//
|
|
19
|
-
// src/events/guildMemberAdd.ts — GuildMembers intent:
|
|
20
|
-
//
|
|
21
|
-
// export default defineEvent(async ({ member, client }) => {
|
|
22
|
-
// await member.addRole(MEMBER_ROLE_ID, 'New member');
|
|
23
|
-
// await client.channel(WELCOME_CHANNEL_ID).send(`Welcome ${member}! 👋`);
|
|
24
|
-
// });
|
package/template/src/presence.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { definePresence } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// Status and activity shown on the bot's profile, reloaded on save.
|
|
4
|
-
export default definePresence({
|
|
5
|
-
status: 'online', // 'online' | 'idle' | 'dnd' | 'invisible'
|
|
6
|
-
// 'playing' | 'listening' | 'watching' | 'competing' | 'streaming' (+ url),
|
|
7
|
-
// or a plain string for a custom status: activity: 'Coding 🚀'
|
|
8
|
-
activity: { type: 'playing', name: 'with ChapterJS' },
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// Rotation, each step shown for its own duration (minimum 5s):
|
|
12
|
-
//
|
|
13
|
-
// export default definePresence([
|
|
14
|
-
// { duration: 5_000, status: 'online', activity: 'Hello everyone 👋' },
|
|
15
|
-
// { duration: 10_000, status: 'dnd', activity: { type: 'watching', name: 'the server' } },
|
|
16
|
-
// ]);
|
|
17
|
-
|
|
18
|
-
// Dynamic presence, e.g. a rotation stored in a database. It is reloaded
|
|
19
|
-
// after each full rotation, or right away when `watch` calls `refresh`
|
|
20
|
-
// (you can also call `refreshPresence()` from anywhere in your bot):
|
|
21
|
-
//
|
|
22
|
-
// export default definePresence({
|
|
23
|
-
// async presence({ client }) {
|
|
24
|
-
// const rows = await db.presence.findMany();
|
|
25
|
-
// return rows.map((row) => ({
|
|
26
|
-
// duration: row.duration,
|
|
27
|
-
// status: row.status, // 'online' | 'idle' | 'dnd' | 'invisible'
|
|
28
|
-
// activity: row.message,
|
|
29
|
-
// }));
|
|
30
|
-
// },
|
|
31
|
-
// watch(refresh) {
|
|
32
|
-
// const unsubscribe = db.onPresenceChange(refresh);
|
|
33
|
-
// return unsubscribe;
|
|
34
|
-
// },
|
|
35
|
-
// });
|
package/template/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"rewriteRelativeImportExtensions": true,
|
|
11
|
-
"erasableSyntaxOnly": true,
|
|
12
|
-
"types": ["node"]
|
|
13
|
-
},
|
|
14
|
-
"include": ["src"]
|
|
15
|
-
}
|