create-chapter 0.0.0 → 0.1.1

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/index.js ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import * as p from '@clack/prompts';
3
+ import { spawn } from 'node:child_process';
4
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ const TEMPLATE_DIR = fileURLToPath(new URL('../template', import.meta.url));
8
+ const OWN_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
9
+ const DEFAULT_NAME = 'my-chapter-app';
10
+ const RENAME_FILES = {
11
+ _gitignore: '.gitignore',
12
+ };
13
+ const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'];
14
+ function exitIfCancelled(value) {
15
+ if (p.isCancel(value)) {
16
+ p.cancel('Operation cancelled.');
17
+ process.exit(0);
18
+ }
19
+ return value;
20
+ }
21
+ function toValidPackageName(name) {
22
+ return name
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/\s+/g, '-')
26
+ .replace(/^[._]/, '')
27
+ .replace(/[^a-z0-9-~]+/g, '-');
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');
34
+ }
35
+ function validateDir(value) {
36
+ const dir = (value ?? '').trim() || DEFAULT_NAME;
37
+ if (!isEmptyDir(path.resolve(dir))) {
38
+ return dir === '.'
39
+ ? 'The current directory is not empty'
40
+ : `The directory "${dir}" already exists and is not empty`;
41
+ }
42
+ return undefined;
43
+ }
44
+ function detectPackageManager() {
45
+ const agent = process.env.npm_config_user_agent ?? '';
46
+ return PACKAGE_MANAGERS.find(pm => agent.startsWith(`${pm}/`));
47
+ }
48
+ function install(pm, cwd) {
49
+ const log = p.taskLog({
50
+ title: `Installing dependencies with ${pm}`,
51
+ limit: 8,
52
+ });
53
+ return new Promise((resolve, reject) => {
54
+ const child = spawn(pm, ['install'], {
55
+ cwd,
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ // Windows: pnpm/npm are .cmd
58
+ shell: process.platform === 'win32',
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
+ });
88
+ }
89
+ async function main() {
90
+ const args = process.argv.slice(2);
91
+ const noInstall = args.includes('--no-install');
92
+ const argDir = args.find(arg => !arg.startsWith('--'));
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!');
159
+ }
160
+ main().catch(err => {
161
+ p.log.error(err instanceof Error ? err.message : String(err));
162
+ process.exit(1);
163
+ });
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
- "name": "create-chapter",
3
- "version": "0.0.0",
4
- "license": "MIT",
5
- "repository": "github:chapterjs/chapter"
6
- }
2
+ "name": "create-chapter",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-chapter": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "template"
12
+ ],
13
+ "devDependencies": {
14
+ "@types/node": "^24.0.0",
15
+ "typescript": "^7.0.2"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^1.8.1"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsc --watch"
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ # Required — Discord Developer Portal > your application > Bot > Token
2
+ BOT_TOKEN=<YOUR_BOT_TOKEN>
3
+
4
+ # Required — Discord > right-click your dev server > Copy Server ID
5
+ DEV_GUILD_ID=<YOUR_DEV_GUILD_ID>
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ .env
3
+ .DS_Store
@@ -0,0 +1,13 @@
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
+ }
@@ -0,0 +1,28 @@
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: '…' } })] });
@@ -0,0 +1,11 @@
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
+ });
@@ -0,0 +1,25 @@
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
+ // });
@@ -0,0 +1,6 @@
1
+ import { defineEvent } from 'chapterjs';
2
+
3
+ // Discord only sends ids: the deleted message's content is gone.
4
+ export default defineEvent(({ messageId, channel }) => {
5
+ console.log(`Message ${messageId} deleted in ${channel}`);
6
+ });
@@ -0,0 +1,12 @@
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
+ });
@@ -0,0 +1,24 @@
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
+ // });
@@ -0,0 +1,35 @@
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
+ // });
@@ -0,0 +1,15 @@
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
+ }