chapterjs 0.1.2 → 0.2.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.
Files changed (318) hide show
  1. package/dist/assets/asset.d.ts +46 -0
  2. package/dist/assets/asset.js +86 -0
  3. package/dist/assets/public.d.ts +19 -0
  4. package/dist/assets/public.js +120 -0
  5. package/dist/cache/cache.d.ts +62 -0
  6. package/dist/cache/cache.js +80 -0
  7. package/dist/cache/store.d.ts +45 -0
  8. package/dist/cache/store.js +53 -0
  9. package/dist/cli/build.d.ts +41 -0
  10. package/dist/cli/build.js +205 -0
  11. package/dist/cli/cli.js +108 -25
  12. package/dist/cli/cluster.d.ts +70 -0
  13. package/dist/cli/cluster.js +213 -0
  14. package/dist/cli/dev.d.ts +18 -1
  15. package/dist/cli/dev.js +253 -198
  16. package/dist/cli/env.d.ts +31 -0
  17. package/dist/cli/env.js +93 -0
  18. package/dist/cli/log.d.ts +13 -0
  19. package/dist/cli/log.js +17 -0
  20. package/dist/cli/node-version.d.ts +8 -0
  21. package/dist/cli/node-version.js +13 -0
  22. package/dist/cli/preflight.d.ts +36 -0
  23. package/dist/cli/preflight.js +122 -0
  24. package/dist/cli/project.d.ts +135 -0
  25. package/dist/cli/project.js +517 -0
  26. package/dist/cli/start.d.ts +25 -0
  27. package/dist/cli/start.js +225 -0
  28. package/dist/commands/autocomplete.d.ts +13 -0
  29. package/dist/commands/autocomplete.js +159 -0
  30. package/dist/commands/command.d.ts +360 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/declaration.d.ts +77 -0
  33. package/dist/commands/declaration.js +492 -0
  34. package/dist/commands/register.d.ts +18 -0
  35. package/dist/commands/register.js +42 -0
  36. package/dist/commands/router.d.ts +20 -0
  37. package/dist/commands/router.js +228 -0
  38. package/dist/commands/tree.d.ts +47 -0
  39. package/dist/commands/tree.js +216 -0
  40. package/dist/commands/twins.d.ts +15 -0
  41. package/dist/commands/twins.js +78 -0
  42. package/dist/components/button.d.ts +79 -0
  43. package/dist/components/button.js +106 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/custom-id.d.ts +72 -0
  47. package/dist/components/custom-id.js +128 -0
  48. package/dist/components/declaration.d.ts +42 -0
  49. package/dist/components/declaration.js +342 -0
  50. package/dist/components/declared.d.ts +29 -0
  51. package/dist/components/declared.js +49 -0
  52. package/dist/components/embed.d.ts +29 -0
  53. package/dist/components/embed.js +11 -0
  54. package/dist/components/instance.d.ts +100 -0
  55. package/dist/components/instance.js +86 -0
  56. package/dist/components/layout.d.ts +103 -0
  57. package/dist/components/layout.js +279 -0
  58. package/dist/components/modal.d.ts +245 -0
  59. package/dist/components/modal.js +398 -0
  60. package/dist/components/options.d.ts +41 -0
  61. package/dist/components/options.js +101 -0
  62. package/dist/components/render.d.ts +12 -0
  63. package/dist/components/render.js +58 -0
  64. package/dist/components/router.d.ts +19 -0
  65. package/dist/components/router.js +251 -0
  66. package/dist/components/select.d.ts +146 -0
  67. package/dist/components/select.js +179 -0
  68. package/dist/core/bot.d.ts +99 -0
  69. package/dist/core/bot.js +174 -0
  70. package/dist/core/memory.d.ts +66 -0
  71. package/dist/core/memory.js +115 -0
  72. package/dist/discord/api.d.ts +112 -0
  73. package/dist/discord/api.js +115 -0
  74. package/dist/discord/cdn.d.ts +67 -0
  75. package/dist/discord/cdn.js +100 -0
  76. package/dist/discord/codes.d.ts +171 -0
  77. package/dist/discord/codes.js +421 -0
  78. package/dist/discord/endpoint.d.ts +56 -0
  79. package/dist/discord/endpoint.js +53 -0
  80. package/dist/discord/endpoints.d.ts +1529 -0
  81. package/dist/discord/endpoints.js +1139 -0
  82. package/dist/discord/formatting.d.ts +67 -0
  83. package/dist/discord/formatting.js +87 -0
  84. package/dist/discord/index.d.ts +10 -0
  85. package/dist/discord/index.js +13 -0
  86. package/dist/discord/intents.d.ts +48 -0
  87. package/dist/discord/intents.js +128 -0
  88. package/dist/discord/permissions.d.ts +94 -0
  89. package/dist/discord/permissions.js +185 -0
  90. package/dist/discord/snowflake.d.ts +21 -0
  91. package/dist/discord/snowflake.js +36 -0
  92. package/dist/discord/types/application-command.d.ts +332 -0
  93. package/dist/discord/types/application-command.js +48 -0
  94. package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
  95. package/dist/discord/types/application-role-connection-metadata.js +22 -0
  96. package/dist/discord/types/application.d.ts +208 -0
  97. package/dist/discord/types/application.js +48 -0
  98. package/dist/discord/types/audit-log.d.ts +255 -0
  99. package/dist/discord/types/audit-log.js +144 -0
  100. package/dist/discord/types/auto-moderation.d.ts +167 -0
  101. package/dist/discord/types/auto-moderation.js +52 -0
  102. package/dist/discord/types/channel.d.ts +557 -0
  103. package/dist/discord/types/channel.js +88 -0
  104. package/dist/discord/types/common.d.ts +54 -0
  105. package/dist/discord/types/common.js +38 -0
  106. package/dist/discord/types/component.d.ts +775 -0
  107. package/dist/discord/types/component.js +76 -0
  108. package/dist/discord/types/emoji.d.ts +74 -0
  109. package/dist/discord/types/emoji.js +1 -0
  110. package/dist/discord/types/entitlement.d.ts +84 -0
  111. package/dist/discord/types/entitlement.js +22 -0
  112. package/dist/discord/types/gateway-events.d.ts +1148 -0
  113. package/dist/discord/types/gateway-events.js +54 -0
  114. package/dist/discord/types/gateway.d.ts +38 -0
  115. package/dist/discord/types/gateway.js +1 -0
  116. package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
  117. package/dist/discord/types/guild-scheduled-event.js +68 -0
  118. package/dist/discord/types/guild-template.d.ts +51 -0
  119. package/dist/discord/types/guild-template.js +1 -0
  120. package/dist/discord/types/guild.d.ts +910 -0
  121. package/dist/discord/types/guild.js +153 -0
  122. package/dist/discord/types/index.d.ts +31 -0
  123. package/dist/discord/types/index.js +31 -0
  124. package/dist/discord/types/interaction.d.ts +321 -0
  125. package/dist/discord/types/interaction.js +47 -0
  126. package/dist/discord/types/invite.d.ts +158 -0
  127. package/dist/discord/types/invite.js +39 -0
  128. package/dist/discord/types/message.d.ts +834 -0
  129. package/dist/discord/types/message.js +159 -0
  130. package/dist/discord/types/oauth2.d.ts +22 -0
  131. package/dist/discord/types/oauth2.js +1 -0
  132. package/dist/discord/types/permissions.d.ts +183 -0
  133. package/dist/discord/types/permissions.js +118 -0
  134. package/dist/discord/types/poll.d.ts +106 -0
  135. package/dist/discord/types/poll.js +8 -0
  136. package/dist/discord/types/sku.d.ts +47 -0
  137. package/dist/discord/types/sku.js +26 -0
  138. package/dist/discord/types/soundboard.d.ts +71 -0
  139. package/dist/discord/types/soundboard.js +1 -0
  140. package/dist/discord/types/stage-instance.d.ts +58 -0
  141. package/dist/discord/types/stage-instance.js +10 -0
  142. package/dist/discord/types/sticker.d.ts +115 -0
  143. package/dist/discord/types/sticker.js +20 -0
  144. package/dist/discord/types/subscription.d.ts +54 -0
  145. package/dist/discord/types/subscription.js +12 -0
  146. package/dist/discord/types/team.d.ts +55 -0
  147. package/dist/discord/types/team.js +21 -0
  148. package/dist/discord/types/user.d.ts +273 -0
  149. package/dist/discord/types/user.js +83 -0
  150. package/dist/discord/types/voice.d.ts +72 -0
  151. package/dist/discord/types/voice.js +1 -0
  152. package/dist/discord/types/webhook.d.ts +184 -0
  153. package/dist/discord/types/webhook.js +12 -0
  154. package/dist/events/declaration.d.ts +16 -0
  155. package/dist/events/declaration.js +80 -0
  156. package/dist/events/event.d.ts +45 -0
  157. package/dist/events/event.js +34 -0
  158. package/dist/events/registry.d.ts +697 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +46 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/gateway/errors.d.ts +18 -0
  163. package/dist/gateway/errors.js +47 -0
  164. package/dist/gateway/gateway.d.ts +64 -0
  165. package/dist/gateway/gateway.js +117 -0
  166. package/dist/gateway/identify-queue.d.ts +25 -0
  167. package/dist/gateway/identify-queue.js +29 -0
  168. package/dist/gateway/shard.d.ts +94 -0
  169. package/dist/gateway/shard.js +414 -0
  170. package/dist/gateway/state.d.ts +11 -0
  171. package/dist/gateway/state.js +184 -0
  172. package/dist/index.d.ts +57 -18
  173. package/dist/index.js +40 -12
  174. package/dist/interactions/cooldown.d.ts +33 -0
  175. package/dist/interactions/cooldown.js +74 -0
  176. package/dist/interactions/dispatch.d.ts +91 -0
  177. package/dist/interactions/dispatch.js +110 -0
  178. package/dist/loader/generated.d.ts +10 -0
  179. package/dist/loader/generated.js +80 -0
  180. package/dist/loader/hot.d.ts +13 -0
  181. package/dist/loader/hot.js +85 -0
  182. package/dist/loader/loader.d.ts +85 -0
  183. package/dist/loader/loader.js +119 -0
  184. package/dist/loader/locate.d.ts +10 -0
  185. package/dist/loader/locate.js +36 -0
  186. package/dist/loader/watch.d.ts +17 -0
  187. package/dist/loader/watch.js +66 -0
  188. package/dist/main.d.ts +2 -0
  189. package/dist/main.js +5 -0
  190. package/dist/messages/commands.d.ts +23 -0
  191. package/dist/messages/commands.js +142 -0
  192. package/dist/messages/declaration.d.ts +89 -0
  193. package/dist/messages/declaration.js +455 -0
  194. package/dist/messages/messages.d.ts +241 -0
  195. package/dist/messages/messages.js +31 -0
  196. package/dist/messages/phrases.d.ts +23 -0
  197. package/dist/messages/phrases.js +44 -0
  198. package/dist/messages/translate.d.ts +100 -0
  199. package/dist/messages/translate.js +249 -0
  200. package/dist/presence/declaration.d.ts +19 -0
  201. package/dist/presence/declaration.js +118 -0
  202. package/dist/presence/presence.d.ts +57 -0
  203. package/dist/presence/presence.js +27 -0
  204. package/dist/rest/errors.d.ts +55 -0
  205. package/dist/rest/errors.js +92 -0
  206. package/dist/rest/rate-limiter.d.ts +46 -0
  207. package/dist/rest/rate-limiter.js +153 -0
  208. package/dist/rest/rest.d.ts +63 -0
  209. package/dist/rest/rest.js +194 -0
  210. package/dist/structures/base.d.ts +48 -0
  211. package/dist/structures/base.js +84 -0
  212. package/dist/structures/channel.d.ts +380 -0
  213. package/dist/structures/channel.js +627 -0
  214. package/dist/structures/context.d.ts +30 -0
  215. package/dist/structures/context.js +7 -0
  216. package/dist/structures/emoji.d.ts +46 -0
  217. package/dist/structures/emoji.js +65 -0
  218. package/dist/structures/entities.d.ts +62 -0
  219. package/dist/structures/entities.js +226 -0
  220. package/dist/structures/guild.d.ts +348 -0
  221. package/dist/structures/guild.js +466 -0
  222. package/dist/structures/interaction.d.ts +197 -0
  223. package/dist/structures/interaction.js +396 -0
  224. package/dist/structures/invite.d.ts +38 -0
  225. package/dist/structures/invite.js +57 -0
  226. package/dist/structures/known.d.ts +28 -0
  227. package/dist/structures/known.js +58 -0
  228. package/dist/structures/member.d.ts +140 -0
  229. package/dist/structures/member.js +260 -0
  230. package/dist/structures/message.d.ts +197 -0
  231. package/dist/structures/message.js +268 -0
  232. package/dist/structures/payload.d.ts +91 -0
  233. package/dist/structures/payload.js +184 -0
  234. package/dist/structures/role.d.ts +69 -0
  235. package/dist/structures/role.js +112 -0
  236. package/dist/structures/user.d.ts +59 -0
  237. package/dist/structures/user.js +94 -0
  238. package/dist/structures/webhook.d.ts +46 -0
  239. package/dist/structures/webhook.js +79 -0
  240. package/dist/tasks/declaration.d.ts +16 -0
  241. package/dist/tasks/declaration.js +56 -0
  242. package/dist/tasks/schedule.d.ts +54 -0
  243. package/dist/tasks/schedule.js +236 -0
  244. package/dist/tasks/scheduler.d.ts +35 -0
  245. package/dist/tasks/scheduler.js +107 -0
  246. package/dist/tasks/task.d.ts +75 -0
  247. package/dist/tasks/task.js +27 -0
  248. package/dist/util/case.d.ts +13 -0
  249. package/dist/util/case.js +54 -0
  250. package/dist/util/duration.d.ts +8 -0
  251. package/dist/util/duration.js +29 -0
  252. package/dist/util/websocket.d.ts +9 -0
  253. package/dist/util/websocket.js +31 -0
  254. package/dist/voice/connection.d.ts +90 -0
  255. package/dist/voice/connection.js +494 -0
  256. package/dist/voice/dave.d.ts +109 -0
  257. package/dist/voice/dave.js +245 -0
  258. package/dist/voice/opus.d.ts +19 -0
  259. package/dist/voice/opus.js +273 -0
  260. package/dist/voice/session.d.ts +55 -0
  261. package/dist/voice/session.js +405 -0
  262. package/dist/voice/source.d.ts +22 -0
  263. package/dist/voice/source.js +195 -0
  264. package/dist/voice/transport.d.ts +43 -0
  265. package/dist/voice/transport.js +140 -0
  266. package/dist/voice/usage.d.ts +4 -0
  267. package/dist/voice/usage.js +33 -0
  268. package/package.json +26 -7
  269. package/vendor/dave/LICENSE +231 -0
  270. package/vendor/dave/libdave.d.mts +99 -0
  271. package/vendor/dave/libdave.mjs +2 -0
  272. package/vendor/dave/libdave.wasm +0 -0
  273. package/dist/components.d.ts +0 -97
  274. package/dist/components.js +0 -260
  275. package/dist/context.d.ts +0 -72
  276. package/dist/context.js +0 -273
  277. package/dist/discord/client.d.ts +0 -35
  278. package/dist/discord/client.js +0 -66
  279. package/dist/discord/constants.d.ts +0 -49
  280. package/dist/discord/constants.js +0 -61
  281. package/dist/discord/events.d.ts +0 -129
  282. package/dist/discord/events.js +0 -102
  283. package/dist/discord/gateway.d.ts +0 -61
  284. package/dist/discord/gateway.js +0 -320
  285. package/dist/discord/payloads.d.ts +0 -275
  286. package/dist/discord/payloads.js +0 -3
  287. package/dist/discord/presence.d.ts +0 -42
  288. package/dist/discord/presence.js +0 -51
  289. package/dist/discord/rest.d.ts +0 -49
  290. package/dist/discord/rest.js +0 -271
  291. package/dist/discord/types.d.ts +0 -334
  292. package/dist/discord/types.js +0 -4
  293. package/dist/events.d.ts +0 -94
  294. package/dist/events.js +0 -255
  295. package/dist/presence.d.ts +0 -92
  296. package/dist/presence.js +0 -250
  297. package/dist/structures/Channel.d.ts +0 -110
  298. package/dist/structures/Channel.js +0 -171
  299. package/dist/structures/Guild.d.ts +0 -70
  300. package/dist/structures/Guild.js +0 -105
  301. package/dist/structures/Interaction.d.ts +0 -113
  302. package/dist/structures/Interaction.js +0 -274
  303. package/dist/structures/Member.d.ts +0 -70
  304. package/dist/structures/Member.js +0 -124
  305. package/dist/structures/Message.d.ts +0 -87
  306. package/dist/structures/Message.js +0 -182
  307. package/dist/structures/Reaction.d.ts +0 -38
  308. package/dist/structures/Reaction.js +0 -64
  309. package/dist/structures/Role.d.ts +0 -43
  310. package/dist/structures/Role.js +0 -77
  311. package/dist/structures/User.d.ts +0 -41
  312. package/dist/structures/User.js +0 -79
  313. package/dist/structures/utils.d.ts +0 -72
  314. package/dist/structures/utils.js +0 -59
  315. package/dist/utils/errors.d.ts +0 -5
  316. package/dist/utils/errors.js +0 -28
  317. package/dist/utils/modules.d.ts +0 -39
  318. package/dist/utils/modules.js +0 -156
package/dist/cli/dev.js CHANGED
@@ -1,206 +1,261 @@
1
- import { handleComponentInteraction, loadComponents, watchComponents, } from '../components.js';
2
- import { Gateway, GatewayCloseError } from '../discord/gateway.js';
3
- import { GatewayCloseCodes } from '../discord/constants.js';
4
- import { EventRouter, eventIntents, filesMissingMessageContent, loadEvents, privilegedIntentsUsage, watchEvents, } from '../events.js';
5
- import { loadPresence, startPresenceLoop, watchPresence } from '../presence.js';
6
- import { describeRuntimeError } from '../utils/modules.js';
7
- /** Returns false when there is no `.env` file. */
8
- function loadEnv() {
9
- try {
10
- process.loadEnvFile('.env');
11
- return true;
12
- }
13
- catch (error) {
14
- if (error.code !== 'ENOENT')
15
- throw error;
16
- return false;
17
- }
18
- }
19
- /** The Bot page of the app in the Developer Portal, where the token and privileged intents are. */
20
- const botPage = (applicationId) => applicationId
21
- ? `https://discord.com/developers/applications/${applicationId}/bot`
22
- : 'https://discord.com/developers/applications > your application > Bot';
23
- const errorMessage = (error) => error instanceof Error ? error.message : String(error);
24
- function summarizeEvents(events) {
25
- const counts = new Map();
26
- for (const event of events)
27
- counts.set(event.name, (counts.get(event.name) ?? 0) + 1);
28
- return [...counts]
29
- .map(([name, count]) => (count > 1 ? `${name} ×${count}` : name))
30
- .join(', ');
31
- }
32
- let lastMessageContentNote = '';
33
- /** Explains it once per set of files, not on every reload. */
34
- function noteMessageContent(events, applicationId) {
35
- const files = filesMissingMessageContent(events);
36
- const key = files.join(',');
37
- if (key === lastMessageContentNote)
38
- return;
39
- lastMessageContentNote = key;
40
- if (files.length === 0)
41
- return;
42
- console.log(`ℹ ${files.join(', ')}: message.content is empty, except in DMs and in messages that mention the bot.\n` +
43
- ` Nothing to do if that's enough for you. To read the text of every message, add\n` +
44
- ` intents: ['MessageContent'] to defineEvent, then enable "Message Content Intent" in\n` +
45
- ` ${botPage(applicationId)} (Privileged Gateway Intents).`);
46
- }
47
- /** After a reload error: the bot keeps the file's previous version. */
48
- const KEPT = '\n Until you fix it, the bot keeps the last working version of this file, if any.';
49
- const COPY_GUILD_ID_HELP = 'To get it: Discord > User Settings > Advanced > enable Developer Mode, then right-click your server > Copy Server ID.';
50
- export async function dev() {
51
- const hasEnvFile = loadEnv();
52
- if (!hasEnvFile && !process.env.BOT_TOKEN) {
53
- throw new Error(`No .env file in ${process.cwd()}.\n` +
54
- ' Copy .env.example to .env, then fill in BOT_TOKEN and DEV_GUILD_ID.');
55
- }
56
- const token = process.env.BOT_TOKEN?.trim();
57
- if (!token) {
58
- throw new Error('BOT_TOKEN is missing from your .env file.\n' +
59
- ` To get it: ${botPage(null)} > Reset Token.`);
60
- }
61
- if (token.startsWith('<')) {
62
- throw new Error(`BOT_TOKEN still has the example value ${token}: replace it in your .env file with your bot's token.\n` +
63
- ` To get it: ${botPage(null)} > Reset Token.`);
64
- }
65
- const devGuildId = process.env.DEV_GUILD_ID?.trim();
66
- if (!devGuildId) {
67
- throw new Error('DEV_GUILD_ID is missing from your .env file: it is the ID of the server where you test your bot.\n' +
68
- ` ${COPY_GUILD_ID_HELP}`);
69
- }
70
- // https://docs.discord.com/developers/reference#snowflakes
71
- if (!/^\d{17,20}$/.test(devGuildId)) {
72
- throw new Error(`DEV_GUILD_ID "${devGuildId}" in your .env file is not a server ID (a number of 17 to 20 digits).\n` +
73
- ` ${COPY_GUILD_ID_HELP}`);
1
+ // `chapterjs dev`: runs the bot of the current folder against its dev
2
+ // server, and reloads every file of the project when it is saved.
3
+ import { join } from 'node:path';
4
+ import { registerCommands } from '../commands/register.js';
5
+ import { buildCommands, commandName, privateOnly } from '../commands/tree.js';
6
+ import { withoutGlobalTwins } from '../commands/twins.js';
7
+ import { configure } from '../core/memory.js';
8
+ import { GetGlobalApplicationCommands } from '../discord/endpoints.js';
9
+ import { EVENTS } from '../events/registry.js';
10
+ import { limitsFor } from '../events/router.js';
11
+ import { enableProjectLoader, nextGeneration } from '../loader/hot.js';
12
+ import { siteName } from '../loader/loader.js';
13
+ import { messageOf } from '../loader/locate.js';
14
+ import { watchPublic } from '../assets/public.js';
15
+ import { snapshotFolder, watchFolder } from '../loader/watch.js';
16
+ import { RestClient } from '../rest/rest.js';
17
+ import { readDevEnv } from './env.js';
18
+ import { describeIntents, ensureInDevGuild, ensurePrivilegedIntents, fetchApplication, } from './preflight.js';
19
+ import { createProject, explain, hasSources, writeTypes } from './project.js';
20
+ /** Runs until stopped. Resolves with the exit code of the process. */
21
+ export async function dev(options) {
22
+ const { cwd, log, signal } = options;
23
+ // 1. What the project must have before anything is tried.
24
+ const read = await readDevEnv(cwd, options.env);
25
+ if ('problems' in read) {
26
+ for (const problem of read.problems)
27
+ log.error(problem);
28
+ return 1;
74
29
  }
75
- let presence = await loadPresence();
76
- const loaded = await loadEvents();
77
- const components = await loadComponents();
78
- const loadErrors = [...loaded.errors, ...components.errors];
79
- if (loadErrors.length > 0)
80
- throw new Error(loadErrors.join('\n✗ '));
81
- let events = loaded.events;
82
- const gateway = new Gateway({
30
+ const { token, devGuildId } = read.env;
31
+ const src = join(cwd, 'src');
32
+ if (!(await hasSources(cwd, log)))
33
+ return 1;
34
+ // 2. The files of the project.
35
+ const project = createProject({
36
+ cwd,
37
+ version: options.version,
38
+ log,
39
+ ...(options.deferAfter === undefined
40
+ ? {}
41
+ : { deferAfter: options.deferAfter }),
42
+ });
43
+ await writeTypes(cwd);
44
+ enableProjectLoader(src, { reload: true });
45
+ // Taken before loading: what is saved from now on must be reloaded.
46
+ const loaded = snapshotFolder(src);
47
+ for (const failure of await project.load())
48
+ project.report(failure);
49
+ // Now that the files ran, a language only offers the commands they do
50
+ // not describe, and `t` is typed from the default language.
51
+ await writeTypes(cwd, project.typesInfo());
52
+ // 3. What Discord must agree with before connecting.
53
+ const apiUrl = options.env.CHAPTERJS_API_URL;
54
+ const rest = new RestClient({
83
55
  token,
84
- devGuildId,
85
- dev: true,
86
- presence: presence?.initial,
87
- intents: eventIntents(events),
56
+ version: options.version,
57
+ ...(apiUrl ? { baseUrl: apiUrl } : {}),
88
58
  });
89
- const { client } = gateway;
90
- const onHandlerError = (file, error) => console.error(`✗ ${file}: ${describeRuntimeError(error)}`);
91
- const router = new EventRouter(client, events, onHandlerError);
92
- // Clicks on components of src/components/ go to their handler, not to interactionCreate
93
- router.intercept = (event, data) => event === 'INTERACTION_CREATE' &&
94
- handleComponentInteraction(client, data, onHandlerError);
95
- gateway.on('dispatch', router.dispatch);
96
- gateway.on('warning', (message) => console.warn(`⚠ ${message}`));
97
- /** Intents known to work, kept while a reload tries new ones. */
98
- let fallbackIntents = null;
99
- /** Intents Discord refused after a reload, not retried until restart (identifies are limited per day). */
100
- let refusedIntents = null;
101
- gateway.on('ready', () => (fallbackIntents = null));
102
- gateway.on('fatal', (error) => {
103
- const disallowed = error instanceof GatewayCloseError &&
104
- error.code === GatewayCloseCodes.DisallowedIntents;
105
- const usage = disallowed ? privilegedIntentsUsage(events) : [];
106
- console.error(`✗ ${error.message}`);
107
- if (usage.length > 0) {
108
- console.error(` Requested by: ${usage.join(', ')}\n` +
109
- ` Enable them in ${botPage(gateway.applicationId)} (Privileged Gateway Intents),\n` +
110
- ' or remove them from your code.');
111
- }
112
- if (disallowed && fallbackIntents !== null) {
113
- // Added by a reload: keep the bot running with the previous intents
114
- console.error(" The bot keeps running without them, but the events that need them won't be received.\n" +
115
- ' Restart chapterjs dev once they are enabled.');
116
- refusedIntents = gateway.intents;
117
- gateway.setIntents(fallbackIntents);
118
- fallbackIntents = null;
119
- gateway.login().catch(error => {
120
- console.error(`✗ ${errorMessage(error)}`);
121
- process.exit(1);
122
- });
123
- return;
124
- }
125
- process.exit(1);
59
+ const preflight = {
60
+ rest,
61
+ log,
62
+ interactive: options.interactive,
63
+ pollInterval: options.pollInterval ?? 3000,
64
+ signal,
65
+ };
66
+ let bot = null;
67
+ let watcher = null;
68
+ // The files of public/ are listed in the types: a file added or removed
69
+ // is offered (or not) by the editor at once.
70
+ const assets = watchPublic(cwd, () => {
71
+ writeTypes(cwd, project.typesInfo())
72
+ .then(written => {
73
+ if (written > 0)
74
+ log.reload('public/ changed: types updated');
75
+ })
76
+ .catch(() => { });
126
77
  });
127
- gateway.on('resumed', () => console.log('↻ Reconnected to Discord'));
128
- if (process.env.CHAPTER_DEBUG) {
129
- gateway.on('dispatch', (event) => console.log(`← event ${event}`));
130
- gateway.on('presenceUpdate', (presence) => console.log(`→ presence sent: ${presence.status} ${JSON.stringify(presence.activities)}`));
131
- }
132
- const shutdown = async () => {
133
- await gateway.destroy();
134
- process.exit(0);
78
+ const memory = project.watchMemory(() => bot);
79
+ // Resolved with the exit code when something ends the command: asked for
80
+ // from the very start, so nothing that happens early is lost.
81
+ let fatal;
82
+ const ended = new Promise(resolve => (fatal = resolve));
83
+ /** The intents of the current connection. */
84
+ let connectedIntents = 0;
85
+ const connect = async (known) => {
86
+ const application = known ?? (await fetchApplication(rest));
87
+ const intents = project.intents(false);
88
+ await ensurePrivilegedIntents(preflight, application, intents, project.filesNeeding);
89
+ const created = await project.connect({
90
+ token,
91
+ apiUrl,
92
+ // The dev bot only sees its dev server, and nothing outside servers:
93
+ // another process can run the same bot for everyone else, and what
94
+ // happens in private is answered by that one.
95
+ guildFilter: id => id === devGuildId,
96
+ privateEvents: false,
97
+ onFatal: failed => {
98
+ if (bot === failed)
99
+ fatal(1);
100
+ },
101
+ });
102
+ connectedIntents = intents;
103
+ return created;
135
104
  };
136
- process.once('SIGINT', shutdown);
137
- process.once('SIGTERM', shutdown);
138
- console.log('Connecting to Discord...');
139
- const user = await gateway.login();
140
- if (!gateway.guildIds.has(devGuildId)) {
141
- await gateway.destroy();
142
- // https://docs.discord.com/developers/topics/oauth2#bot-authorization-flow
143
- const invite = new URL('https://discord.com/oauth2/authorize');
144
- invite.searchParams.set('client_id', gateway.applicationId);
145
- invite.searchParams.set('scope', 'bot applications.commands');
146
- invite.searchParams.set('integration_type', '0'); // GUILD_INSTALL
147
- invite.searchParams.set('guild_id', devGuildId);
148
- invite.searchParams.set('disable_guild_select', 'true');
149
- throw new Error(`${user.username} is not in your dev server ${devGuildId} (DEV_GUILD_ID) yet. Open this link to add it:\n ${invite}\n\n` +
150
- ` You need the "Manage Server" permission on this server, otherwise Discord won't let you continue.\n` +
151
- ` Ask the server owner to open the link, or use a server you own as DEV_GUILD_ID.`);
105
+ try {
106
+ const application = await fetchApplication(rest);
107
+ const guild = await ensureInDevGuild(preflight, application.id, devGuildId);
108
+ bot = await connect(application);
109
+ // What the bot already has for everyone (a bot in production with the
110
+ // same token): asked once, so the dev server does not show it twice.
111
+ const global = await rest
112
+ .request(GetGlobalApplicationCommands, [application.id], {
113
+ query: { with_localizations: true },
114
+ })
115
+ .catch(() => []);
116
+ /** The commands shown twice, already said. */
117
+ const saidTwice = new Set();
118
+ /**
119
+ * Tells Discord the commands of the project, on the dev server only:
120
+ * they show up there at once. Nothing is sent when they did not change.
121
+ */
122
+ const syncCommands = async () => {
123
+ const { kept, changed } = withoutGlobalTwins(buildCommands([...project.commands.values()], { guild: true }), global);
124
+ for (const name of changed) {
125
+ if (saidTwice.has(name))
126
+ continue;
127
+ saidTwice.add(name);
128
+ log.info(`/${name} is not the same here as in production, so ${guild.name} shows it twice: yours, and the one everyone has. It is shown once again when production runs your version.`);
129
+ }
130
+ try {
131
+ return await registerCommands({
132
+ rest,
133
+ projectDir: cwd,
134
+ applicationId: application.id,
135
+ guildId: devGuildId,
136
+ commands: kept,
137
+ });
138
+ }
139
+ catch (error) {
140
+ // The bot keeps running with the commands Discord already has.
141
+ log.error(`Discord refused the commands of your project: ${messageOf(error)}`);
142
+ return false;
143
+ }
144
+ };
145
+ const self = bot.ctx.cache.users.get(bot.ctx.self.userId);
146
+ log.success(`Connected to ${guild.name} as ${self.username}`);
147
+ log.info(`Intents computed from your files: ${describeIntents(connectedIntents)}`);
148
+ (project.isEmpty() ? log.info : log.success)(project.summary());
149
+ project.ready(bot);
150
+ // Tasks follow the bot through reconnections: they read the current one.
151
+ project.startTasks(() => bot);
152
+ // Said once per file: what only happens in private messages can't be
153
+ // tried here. Discord only offers the commands of a server in that
154
+ // server, and private messages are answered by the bot in production.
155
+ const noted = new Set();
156
+ const notePrivateOnly = () => {
157
+ const note = (file, what) => {
158
+ if (noted.has(file))
159
+ return;
160
+ noted.add(file);
161
+ log.info(`${file} ${what} only works in private messages, and chapterjs dev only runs your bot in ${guild.name}. Try it with chapterjs start.`);
162
+ };
163
+ for (const entry of privateOnly([...project.commands.values()])) {
164
+ note(siteName(entry), commandName(entry.command.path));
165
+ }
166
+ for (const entry of project.events.values()) {
167
+ const { event } = entry;
168
+ if (EVENTS[event.name].options &&
169
+ event.options.where === 'dm') {
170
+ note(siteName(entry), `this ${event.name} event`);
171
+ }
172
+ }
173
+ for (const entry of project.components.values()) {
174
+ const { component } = entry;
175
+ if (component.kind !== 'embed' && component.where === 'dm') {
176
+ note(siteName(entry), `the ${component.kind} ${component.name}`);
177
+ }
178
+ }
179
+ };
180
+ notePrivateOnly();
181
+ // 4. Every save reloads the project. Watching starts right away; the
182
+ // first thing done is to tell Discord the commands.
183
+ let reloading = syncCommands().then(updated => {
184
+ if (updated)
185
+ log.success(`Commands updated on ${guild.name}`);
186
+ });
187
+ watcher = watchFolder(src, () => {
188
+ reloading = reloading
189
+ .then(async () => {
190
+ const started = performance.now();
191
+ nextGeneration();
192
+ const failures = await project.load();
193
+ for (const failure of failures)
194
+ project.report(failure);
195
+ notePrivateOnly();
196
+ if (bot) {
197
+ configure(bot.ctx.cache, limitsFor(project.events.values()));
198
+ if (project.applyPresence(bot))
199
+ log.reload('Presence updated');
200
+ }
201
+ // A language that appeared or went: `t` is typed from it. A
202
+ // command described in its file, or no longer: the languages
203
+ // offer it, or not.
204
+ if ((await writeTypes(cwd, project.typesInfo())) > 0)
205
+ log.reload('Types updated');
206
+ const intents = project.intents(false);
207
+ if ((intents & ~connectedIntents) !== 0 && bot) {
208
+ // Intents are given when connecting: new ones need a new session.
209
+ log.reload(`Your files now need more from Discord (${describeIntents(intents & ~connectedIntents)}): reconnecting...`);
210
+ const previous = bot;
211
+ bot = null;
212
+ await previous.close();
213
+ bot = await connect();
214
+ log.success(`Reconnected, ${project.summary()}`);
215
+ if (await syncCommands())
216
+ log.reload('Commands updated on Discord');
217
+ return;
218
+ }
219
+ const ms = Math.max(1, Math.round(performance.now() - started));
220
+ const updated = await syncCommands();
221
+ if (failures.length === 0)
222
+ log.reload(`Reloaded in ${ms} ms, ${project.summary()}`);
223
+ else {
224
+ log.warn(`Reloaded with ${failures.length === 1 ? 'an error' : `${failures.length} errors`}: ${failures.length === 1 ? 'that file keeps' : 'those files keep'} running ${failures.length === 1 ? 'its' : 'their'} last working version`);
225
+ }
226
+ if (updated)
227
+ log.reload('Commands updated on Discord');
228
+ })
229
+ .catch(error => {
230
+ if (!signal.aborted)
231
+ explain(error, log);
232
+ fatal(1);
233
+ });
234
+ }, { loaded });
235
+ // 5. Until the user stops it, or Discord refuses the bot for good.
236
+ if (signal.aborted)
237
+ fatal(0);
238
+ signal.addEventListener('abort', () => fatal(0), { once: true });
239
+ const code = await ended;
240
+ await reloading.catch(() => { });
241
+ project.stopTasks();
242
+ watcher.close();
243
+ assets.close();
244
+ memory.stop();
245
+ await bot?.close();
246
+ if (code === 0)
247
+ log.success('Disconnected');
248
+ return code;
152
249
  }
153
- console.log(`✓ Logged in as ${user.username} (${user.id})`);
154
- console.log(` Listening to server ${devGuildId} (DEV_GUILD_ID)`);
155
- if (events.length > 0)
156
- console.log(` Events: ${summarizeEvents(events)}`);
157
- if (components.ids.length > 0) {
158
- console.log(` Components: ${components.ids.join(', ')}`);
250
+ catch (error) {
251
+ project.stopTasks();
252
+ watcher?.close();
253
+ assets.close();
254
+ memory.stop();
255
+ await bot?.close();
256
+ if (signal.aborted)
257
+ return 0;
258
+ explain(error, log);
259
+ return 1;
159
260
  }
160
- noteMessageContent(events, gateway.applicationId);
161
- router.start();
162
- watchComponents(async () => {
163
- const result = await loadComponents();
164
- for (const error of result.errors)
165
- console.error(`✗ ${error}${KEPT}`);
166
- console.log(`↻ src/components reloaded${result.ids.length > 0 ? ` (${result.ids.join(', ')})` : ''}`);
167
- });
168
- watchEvents(async () => {
169
- const result = await loadEvents(process.cwd(), events);
170
- for (const error of result.errors)
171
- console.error(`✗ ${error}${KEPT}`);
172
- events = result.events;
173
- router.setEvents(events);
174
- const intents = eventIntents(events);
175
- if (intents !== gateway.intents && intents !== refusedIntents) {
176
- console.log('↻ Intents changed: reconnecting to Discord');
177
- fallbackIntents ??= gateway.intents;
178
- gateway.setIntents(intents);
179
- }
180
- console.log(`↻ src/events reloaded${events.length > 0 ? ` (${summarizeEvents(events)})` : ''}`);
181
- noteMessageContent(events, gateway.applicationId);
182
- });
183
- const presenceLogger = {
184
- error: (error) => console.error(`✗ src/presence: ${errorMessage(error)}`),
185
- warn: (message) => console.warn(`⚠ src/presence: ${message}`),
186
- };
187
- let stopPresenceLoop = presence
188
- ? startPresenceLoop(gateway, presence, presenceLogger)
189
- : null;
190
- watchPresence(async () => {
191
- try {
192
- presence = await loadPresence();
193
- stopPresenceLoop?.();
194
- stopPresenceLoop = null;
195
- if (presence)
196
- stopPresenceLoop = startPresenceLoop(gateway, presence, presenceLogger);
197
- else
198
- gateway.setPresence({});
199
- console.log('↻ src/presence reloaded');
200
- }
201
- catch (error) {
202
- // Keep the bot running with the previous presence until the file is fixed
203
- console.error(`✗ ${errorMessage(error)}${KEPT}`);
204
- }
205
- });
206
261
  }
@@ -0,0 +1,31 @@
1
+ export interface DevEnv {
2
+ token: string;
3
+ devGuildId: string;
4
+ }
5
+ /** What `chapterjs start` runs with. */
6
+ export interface StartEnv {
7
+ token: string;
8
+ /** The dev server, when the project has one: production leaves it alone. */
9
+ devGuildId: string | null;
10
+ }
11
+ /**
12
+ * Reads what `chapterjs dev` needs from the `.env` file of the project (the
13
+ * real environment wins when a value is set in both). Returns every problem
14
+ * at once, each one saying how to fix it.
15
+ */
16
+ export declare function readDevEnv(projectDir: string, processEnv: Record<string, string | undefined>): Promise<{
17
+ env: DevEnv;
18
+ } | {
19
+ problems: string[];
20
+ }>;
21
+ /**
22
+ * Reads what `chapterjs start` needs. In production the values often come
23
+ * from the host and not from a file: only the token is needed. The dev
24
+ * server is optional: when it is known, production leaves it to
25
+ * `chapterjs dev`.
26
+ */
27
+ export declare function readStartEnv(projectDir: string, processEnv: Record<string, string | undefined>): Promise<{
28
+ env: StartEnv;
29
+ } | {
30
+ problems: string[];
31
+ }>;
@@ -0,0 +1,93 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parseEnv } from 'node:util';
4
+ import { isSnowflake } from '../discord/snowflake.js';
5
+ /**
6
+ * Values people leave in a `.env` copied from an example. Whole values only:
7
+ * a real token may start with any letters.
8
+ */
9
+ const PLACEHOLDER = /^(?:(?:your|my)[-_ ]?(?:bot[-_ ]?)?token(?:[-_ ]?here)?|x{3,}|todo|token|changeme|change-me|<.*>|\.{3})$/i;
10
+ /** The `.env` file of a project, with the real environment on top. */
11
+ async function readEnv(projectDir, processEnv) {
12
+ let file = {};
13
+ let hasFile = true;
14
+ try {
15
+ file = parseEnv(await readFile(join(projectDir, '.env'), 'utf8'));
16
+ }
17
+ catch (error) {
18
+ if (error.code !== 'ENOENT')
19
+ throw error;
20
+ hasFile = false;
21
+ }
22
+ return {
23
+ read: name => (processEnv[name] ?? file[name] ?? '').trim(),
24
+ hasFile,
25
+ };
26
+ }
27
+ const TOKEN_HELP = 'Copy the token of your bot from https://discord.com/developers/applications → your application → Bot → Reset Token.';
28
+ /** What is wrong with a token, if anything. */
29
+ function tokenProblem(token, where) {
30
+ if (token === '' || PLACEHOLDER.test(token)) {
31
+ return `${where}: BOT_TOKEN is ${token === '' ? 'empty' : 'still the example value'}.\n${TOKEN_HELP}`;
32
+ }
33
+ if (/\s/.test(token)) {
34
+ return `${where}: BOT_TOKEN contains a space, so it is not a whole token.\nCopy it again from https://discord.com/developers/applications → your application → Bot → Reset Token.`;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Reads what `chapterjs dev` needs from the `.env` file of the project (the
40
+ * real environment wins when a value is set in both). Returns every problem
41
+ * at once, each one saying how to fix it.
42
+ */
43
+ export async function readDevEnv(projectDir, processEnv) {
44
+ const { read, hasFile } = await readEnv(projectDir, processEnv);
45
+ const token = read('BOT_TOKEN').replace(/^Bot\s+/i, '');
46
+ const devGuildId = read('DEV_GUILD_ID');
47
+ const problems = [];
48
+ if (!hasFile && token === '' && devGuildId === '') {
49
+ return {
50
+ problems: [
51
+ 'There is no .env file in this folder.\nCopy .env.example to .env, then fill in BOT_TOKEN and DEV_GUILD_ID.',
52
+ ],
53
+ };
54
+ }
55
+ const wrongToken = tokenProblem(token, '.env');
56
+ if (wrongToken)
57
+ problems.push(wrongToken);
58
+ if (devGuildId === '') {
59
+ problems.push(`.env: DEV_GUILD_ID is empty.\nIn Discord, right-click your test server → Copy Server ID (enable Settings → Advanced → Developer Mode to see it).`);
60
+ }
61
+ else if (!isSnowflake(devGuildId)) {
62
+ problems.push(`.env: DEV_GUILD_ID is "${devGuildId}", which is not a server ID (an ID only has digits).\nIn Discord, right-click your test server → Copy Server ID.`);
63
+ }
64
+ return problems.length > 0 ? { problems } : { env: { token, devGuildId } };
65
+ }
66
+ /**
67
+ * Reads what `chapterjs start` needs. In production the values often come
68
+ * from the host and not from a file: only the token is needed. The dev
69
+ * server is optional: when it is known, production leaves it to
70
+ * `chapterjs dev`.
71
+ */
72
+ export async function readStartEnv(projectDir, processEnv) {
73
+ const { read, hasFile } = await readEnv(projectDir, processEnv);
74
+ const token = read('BOT_TOKEN').replace(/^Bot\s+/i, '');
75
+ const devGuildId = read('DEV_GUILD_ID');
76
+ if (!hasFile && token === '') {
77
+ return {
78
+ problems: [
79
+ `BOT_TOKEN is not set.\nGive it to the bot the way your host does it (its "environment variables" or "secrets"), or in a .env file in this folder.\n${TOKEN_HELP}`,
80
+ ],
81
+ };
82
+ }
83
+ const problems = [];
84
+ const wrongToken = tokenProblem(token, hasFile ? '.env' : 'Environment');
85
+ if (wrongToken)
86
+ problems.push(wrongToken);
87
+ if (devGuildId !== '' && !isSnowflake(devGuildId)) {
88
+ problems.push(`DEV_GUILD_ID is "${devGuildId}", which is not a server ID (an ID only has digits).\nRemove it, or copy it again: in Discord, right-click your test server → Copy Server ID.`);
89
+ }
90
+ return problems.length > 0
91
+ ? { problems }
92
+ : { env: { token, devGuildId: devGuildId === '' ? null : devGuildId } };
93
+ }
@@ -0,0 +1,13 @@
1
+ export interface Log {
2
+ /** `✓` something worked. */
3
+ success(message: string): void;
4
+ /** `ℹ` something worth knowing. */
5
+ info(message: string): void;
6
+ /** `↻` a reload or a reconnection. */
7
+ reload(message: string): void;
8
+ /** `⚠` something to look at, that does not stop the bot. */
9
+ warn(message: string): void;
10
+ /** `✗` something failed: what happened, then what to do. */
11
+ error(message: string): void;
12
+ }
13
+ export declare function createLog(write: (line: string) => void, colors: boolean): Log;
@@ -0,0 +1,17 @@
1
+ // Everything the CLI prints goes through here: one symbol per kind of
2
+ // message, colors only when a person is reading.
3
+ const CODES = { green: 32, blue: 34, yellow: 33, red: 31 };
4
+ export function createLog(write, colors) {
5
+ const line = (symbol, color, message) => {
6
+ const shown = colors ? `\x1b[${CODES[color]}m${symbol}\x1b[0m` : symbol;
7
+ // Following lines are aligned under the first one.
8
+ write(`${shown} ${message.split('\n').join('\n ')}`);
9
+ };
10
+ return {
11
+ success: message => line('✓', 'green', message),
12
+ info: message => line('ℹ', 'blue', message),
13
+ reload: message => line('↻', 'blue', message),
14
+ warn: message => line('⚠', 'yellow', message),
15
+ error: message => line('✗', 'red', message),
16
+ };
17
+ }
@@ -0,0 +1,8 @@
1
+ /** The oldest Node.js ChapterJS runs on, as `engines` says in package.json. */
2
+ export declare const MINIMUM_NODE = 24;
3
+ /**
4
+ * What to tell the developer when this Node.js is older than ChapterJS
5
+ * needs, or `null` when it is recent enough. `engines` is only a warning
6
+ * when installing, which npm does not even show: this is what refuses.
7
+ */
8
+ export declare function nodeTooOld(version?: string): string | null;
@@ -0,0 +1,13 @@
1
+ /** The oldest Node.js ChapterJS runs on, as `engines` says in package.json. */
2
+ export const MINIMUM_NODE = 24;
3
+ /**
4
+ * What to tell the developer when this Node.js is older than ChapterJS
5
+ * needs, or `null` when it is recent enough. `engines` is only a warning
6
+ * when installing, which npm does not even show: this is what refuses.
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
+ }