chapterjs 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.
Files changed (320) 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 +206 -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 +252 -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 +122 -0
  25. package/dist/cli/project.js +452 -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 +346 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/convention.d.ts +64 -0
  33. package/dist/commands/convention.js +464 -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 +43 -0
  39. package/dist/commands/tree.js +206 -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 +108 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/convention.d.ts +35 -0
  47. package/dist/components/convention.js +339 -0
  48. package/dist/components/custom-id.d.ts +72 -0
  49. package/dist/components/custom-id.js +128 -0
  50. package/dist/components/embed.d.ts +28 -0
  51. package/dist/components/embed.js +11 -0
  52. package/dist/components/file.d.ts +30 -0
  53. package/dist/components/file.js +52 -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 +244 -0
  59. package/dist/components/modal.js +399 -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 +18 -0
  65. package/dist/components/router.js +256 -0
  66. package/dist/components/select.d.ts +144 -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/convention.d.ts +25 -0
  155. package/dist/events/convention.js +97 -0
  156. package/dist/events/event.d.ts +8 -0
  157. package/dist/events/event.js +20 -0
  158. package/dist/events/registry.d.ts +706 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +45 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/events/types.d.ts +7 -0
  163. package/dist/events/types.js +48 -0
  164. package/dist/gateway/errors.d.ts +18 -0
  165. package/dist/gateway/errors.js +47 -0
  166. package/dist/gateway/gateway.d.ts +64 -0
  167. package/dist/gateway/gateway.js +117 -0
  168. package/dist/gateway/identify-queue.d.ts +25 -0
  169. package/dist/gateway/identify-queue.js +29 -0
  170. package/dist/gateway/shard.d.ts +94 -0
  171. package/dist/gateway/shard.js +414 -0
  172. package/dist/gateway/state.d.ts +11 -0
  173. package/dist/gateway/state.js +184 -0
  174. package/dist/index.d.ts +55 -18
  175. package/dist/index.js +38 -12
  176. package/dist/interactions/cooldown.d.ts +33 -0
  177. package/dist/interactions/cooldown.js +74 -0
  178. package/dist/interactions/dispatch.d.ts +91 -0
  179. package/dist/interactions/dispatch.js +110 -0
  180. package/dist/loader/generated.d.ts +37 -0
  181. package/dist/loader/generated.js +114 -0
  182. package/dist/loader/hot.d.ts +13 -0
  183. package/dist/loader/hot.js +85 -0
  184. package/dist/loader/loader.d.ts +72 -0
  185. package/dist/loader/loader.js +135 -0
  186. package/dist/loader/locate.d.ts +10 -0
  187. package/dist/loader/locate.js +36 -0
  188. package/dist/loader/watch.d.ts +17 -0
  189. package/dist/loader/watch.js +66 -0
  190. package/dist/main.d.ts +3 -0
  191. package/dist/main.js +6 -0
  192. package/dist/messages/commands.d.ts +23 -0
  193. package/dist/messages/commands.js +123 -0
  194. package/dist/messages/convention.d.ts +89 -0
  195. package/dist/messages/convention.js +347 -0
  196. package/dist/messages/messages.d.ts +219 -0
  197. package/dist/messages/messages.js +30 -0
  198. package/dist/messages/phrases.d.ts +23 -0
  199. package/dist/messages/phrases.js +44 -0
  200. package/dist/messages/translate.d.ts +100 -0
  201. package/dist/messages/translate.js +249 -0
  202. package/dist/presence/convention.d.ts +26 -0
  203. package/dist/presence/convention.js +133 -0
  204. package/dist/presence/presence.d.ts +57 -0
  205. package/dist/presence/presence.js +27 -0
  206. package/dist/rest/errors.d.ts +55 -0
  207. package/dist/rest/errors.js +92 -0
  208. package/dist/rest/rate-limiter.d.ts +46 -0
  209. package/dist/rest/rate-limiter.js +153 -0
  210. package/dist/rest/rest.d.ts +63 -0
  211. package/dist/rest/rest.js +194 -0
  212. package/dist/structures/base.d.ts +48 -0
  213. package/dist/structures/base.js +84 -0
  214. package/dist/structures/channel.d.ts +380 -0
  215. package/dist/structures/channel.js +627 -0
  216. package/dist/structures/context.d.ts +30 -0
  217. package/dist/structures/context.js +7 -0
  218. package/dist/structures/emoji.d.ts +46 -0
  219. package/dist/structures/emoji.js +65 -0
  220. package/dist/structures/entities.d.ts +62 -0
  221. package/dist/structures/entities.js +226 -0
  222. package/dist/structures/guild.d.ts +348 -0
  223. package/dist/structures/guild.js +466 -0
  224. package/dist/structures/interaction.d.ts +197 -0
  225. package/dist/structures/interaction.js +396 -0
  226. package/dist/structures/invite.d.ts +38 -0
  227. package/dist/structures/invite.js +57 -0
  228. package/dist/structures/known.d.ts +28 -0
  229. package/dist/structures/known.js +58 -0
  230. package/dist/structures/member.d.ts +140 -0
  231. package/dist/structures/member.js +260 -0
  232. package/dist/structures/message.d.ts +197 -0
  233. package/dist/structures/message.js +268 -0
  234. package/dist/structures/payload.d.ts +91 -0
  235. package/dist/structures/payload.js +184 -0
  236. package/dist/structures/role.d.ts +69 -0
  237. package/dist/structures/role.js +112 -0
  238. package/dist/structures/user.d.ts +59 -0
  239. package/dist/structures/user.js +94 -0
  240. package/dist/structures/webhook.d.ts +46 -0
  241. package/dist/structures/webhook.js +79 -0
  242. package/dist/tasks/convention.d.ts +26 -0
  243. package/dist/tasks/convention.js +78 -0
  244. package/dist/tasks/schedule.d.ts +54 -0
  245. package/dist/tasks/schedule.js +236 -0
  246. package/dist/tasks/scheduler.d.ts +33 -0
  247. package/dist/tasks/scheduler.js +107 -0
  248. package/dist/tasks/task.d.ts +76 -0
  249. package/dist/tasks/task.js +28 -0
  250. package/dist/util/case.d.ts +13 -0
  251. package/dist/util/case.js +54 -0
  252. package/dist/util/duration.d.ts +8 -0
  253. package/dist/util/duration.js +29 -0
  254. package/dist/util/websocket.d.ts +9 -0
  255. package/dist/util/websocket.js +31 -0
  256. package/dist/voice/connection.d.ts +90 -0
  257. package/dist/voice/connection.js +494 -0
  258. package/dist/voice/dave.d.ts +109 -0
  259. package/dist/voice/dave.js +245 -0
  260. package/dist/voice/opus.d.ts +19 -0
  261. package/dist/voice/opus.js +273 -0
  262. package/dist/voice/session.d.ts +55 -0
  263. package/dist/voice/session.js +405 -0
  264. package/dist/voice/source.d.ts +22 -0
  265. package/dist/voice/source.js +195 -0
  266. package/dist/voice/transport.d.ts +43 -0
  267. package/dist/voice/transport.js +140 -0
  268. package/dist/voice/usage.d.ts +4 -0
  269. package/dist/voice/usage.js +33 -0
  270. package/package.json +26 -7
  271. package/vendor/dave/LICENSE +231 -0
  272. package/vendor/dave/libdave.d.mts +99 -0
  273. package/vendor/dave/libdave.mjs +2 -0
  274. package/vendor/dave/libdave.wasm +0 -0
  275. package/dist/components.d.ts +0 -97
  276. package/dist/components.js +0 -260
  277. package/dist/context.d.ts +0 -72
  278. package/dist/context.js +0 -273
  279. package/dist/discord/client.d.ts +0 -35
  280. package/dist/discord/client.js +0 -66
  281. package/dist/discord/constants.d.ts +0 -49
  282. package/dist/discord/constants.js +0 -61
  283. package/dist/discord/events.d.ts +0 -129
  284. package/dist/discord/events.js +0 -102
  285. package/dist/discord/gateway.d.ts +0 -61
  286. package/dist/discord/gateway.js +0 -320
  287. package/dist/discord/payloads.d.ts +0 -275
  288. package/dist/discord/payloads.js +0 -3
  289. package/dist/discord/presence.d.ts +0 -42
  290. package/dist/discord/presence.js +0 -51
  291. package/dist/discord/rest.d.ts +0 -49
  292. package/dist/discord/rest.js +0 -271
  293. package/dist/discord/types.d.ts +0 -334
  294. package/dist/discord/types.js +0 -4
  295. package/dist/events.d.ts +0 -94
  296. package/dist/events.js +0 -255
  297. package/dist/presence.d.ts +0 -92
  298. package/dist/presence.js +0 -250
  299. package/dist/structures/Channel.d.ts +0 -110
  300. package/dist/structures/Channel.js +0 -171
  301. package/dist/structures/Guild.d.ts +0 -70
  302. package/dist/structures/Guild.js +0 -105
  303. package/dist/structures/Interaction.d.ts +0 -113
  304. package/dist/structures/Interaction.js +0 -274
  305. package/dist/structures/Member.d.ts +0 -70
  306. package/dist/structures/Member.js +0 -124
  307. package/dist/structures/Message.d.ts +0 -87
  308. package/dist/structures/Message.js +0 -182
  309. package/dist/structures/Reaction.d.ts +0 -38
  310. package/dist/structures/Reaction.js +0 -64
  311. package/dist/structures/Role.d.ts +0 -43
  312. package/dist/structures/Role.js +0 -77
  313. package/dist/structures/User.d.ts +0 -41
  314. package/dist/structures/User.js +0 -79
  315. package/dist/structures/utils.d.ts +0 -72
  316. package/dist/structures/utils.js +0 -59
  317. package/dist/utils/errors.d.ts +0 -5
  318. package/dist/utils/errors.js +0 -28
  319. package/dist/utils/modules.d.ts +0 -39
  320. package/dist/utils/modules.js +0 -156
package/dist/cli/dev.js CHANGED
@@ -1,206 +1,260 @@
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 { messageOf } from '../loader/locate.js';
13
+ import { watchPublic } from '../assets/public.js';
14
+ import { snapshotFolder, watchFolder } from '../loader/watch.js';
15
+ import { RestClient } from '../rest/rest.js';
16
+ import { readDevEnv } from './env.js';
17
+ import { describeIntents, ensureInDevGuild, ensurePrivilegedIntents, fetchApplication, } from './preflight.js';
18
+ import { createProject, explain, hasSources, writeTypes } from './project.js';
19
+ /** Runs until stopped. Resolves with the exit code of the process. */
20
+ export async function dev(options) {
21
+ const { cwd, log, signal } = options;
22
+ // 1. What the project must have before anything is tried.
23
+ const read = await readDevEnv(cwd, options.env);
24
+ if ('problems' in read) {
25
+ for (const problem of read.problems)
26
+ log.error(problem);
27
+ return 1;
74
28
  }
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({
29
+ const { token, devGuildId } = read.env;
30
+ const src = join(cwd, 'src');
31
+ if (!(await hasSources(cwd, log)))
32
+ return 1;
33
+ // 2. The files of the project.
34
+ const project = createProject({
35
+ cwd,
36
+ version: options.version,
37
+ log,
38
+ ...(options.deferAfter === undefined
39
+ ? {}
40
+ : { deferAfter: options.deferAfter }),
41
+ });
42
+ await writeTypes(cwd);
43
+ enableProjectLoader(src, { reload: true });
44
+ // Taken before loading: what is saved from now on must be reloaded.
45
+ const loaded = snapshotFolder(src);
46
+ for (const failure of await project.load())
47
+ project.report(failure);
48
+ // Now that the command files ran, a language file only offers the ones
49
+ // they do not describe.
50
+ await writeTypes(cwd, project.commands, project.messages, project.languageDefaults);
51
+ // 3. What Discord must agree with before connecting.
52
+ const apiUrl = options.env.CHAPTERJS_API_URL;
53
+ const rest = new RestClient({
83
54
  token,
84
- devGuildId,
85
- dev: true,
86
- presence: presence?.initial,
87
- intents: eventIntents(events),
55
+ version: options.version,
56
+ ...(apiUrl ? { baseUrl: apiUrl } : {}),
88
57
  });
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);
58
+ const preflight = {
59
+ rest,
60
+ log,
61
+ interactive: options.interactive,
62
+ pollInterval: options.pollInterval ?? 3000,
63
+ signal,
64
+ };
65
+ let bot = null;
66
+ let watcher = null;
67
+ // The files of public/ are listed in the types: a file added or removed
68
+ // is offered (or not) by the editor at once.
69
+ const assets = watchPublic(cwd, () => {
70
+ writeTypes(cwd, project.commands, project.messages, project.languageDefaults)
71
+ .then(written => {
72
+ if (written > 0)
73
+ log.reload('public/ changed: types updated');
74
+ })
75
+ .catch(() => { });
126
76
  });
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);
77
+ const memory = project.watchMemory(() => bot);
78
+ // Resolved with the exit code when something ends the command: asked for
79
+ // from the very start, so nothing that happens early is lost.
80
+ let fatal;
81
+ const ended = new Promise(resolve => (fatal = resolve));
82
+ /** The intents of the current connection. */
83
+ let connectedIntents = 0;
84
+ const connect = async (known) => {
85
+ const application = known ?? (await fetchApplication(rest));
86
+ const intents = project.intents(false);
87
+ await ensurePrivilegedIntents(preflight, application, intents, project.filesNeeding);
88
+ const created = await project.connect({
89
+ token,
90
+ apiUrl,
91
+ // The dev bot only sees its dev server, and nothing outside servers:
92
+ // another process can run the same bot for everyone else, and what
93
+ // happens in private is answered by that one.
94
+ guildFilter: id => id === devGuildId,
95
+ privateEvents: false,
96
+ onFatal: failed => {
97
+ if (bot === failed)
98
+ fatal(1);
99
+ },
100
+ });
101
+ connectedIntents = intents;
102
+ return created;
135
103
  };
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.`);
104
+ try {
105
+ const application = await fetchApplication(rest);
106
+ const guild = await ensureInDevGuild(preflight, application.id, devGuildId);
107
+ bot = await connect(application);
108
+ // What the bot already has for everyone (a bot in production with the
109
+ // same token): asked once, so the dev server does not show it twice.
110
+ const global = await rest
111
+ .request(GetGlobalApplicationCommands, [application.id], {
112
+ query: { with_localizations: true },
113
+ })
114
+ .catch(() => []);
115
+ /** The commands shown twice, already said. */
116
+ const saidTwice = new Set();
117
+ /**
118
+ * Tells Discord the commands of the project, on the dev server only:
119
+ * they show up there at once. Nothing is sent when they did not change.
120
+ */
121
+ const syncCommands = async () => {
122
+ const { kept, changed } = withoutGlobalTwins(buildCommands([...project.commands.values()], { guild: true }), global);
123
+ for (const name of changed) {
124
+ if (saidTwice.has(name))
125
+ continue;
126
+ saidTwice.add(name);
127
+ 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.`);
128
+ }
129
+ try {
130
+ return await registerCommands({
131
+ rest,
132
+ projectDir: cwd,
133
+ applicationId: application.id,
134
+ guildId: devGuildId,
135
+ commands: kept,
136
+ });
137
+ }
138
+ catch (error) {
139
+ // The bot keeps running with the commands Discord already has.
140
+ log.error(`Discord refused the commands of your project: ${messageOf(error)}`);
141
+ return false;
142
+ }
143
+ };
144
+ const self = bot.ctx.cache.users.get(bot.ctx.self.userId);
145
+ log.success(`Connected to ${guild.name} as ${self.username}`);
146
+ log.info(`Intents computed from your files: ${describeIntents(connectedIntents)}`);
147
+ (project.isEmpty() ? log.info : log.success)(project.summary());
148
+ project.ready(bot);
149
+ // Tasks follow the bot through reconnections: they read the current one.
150
+ project.startTasks(() => bot);
151
+ // Said once per file: what only happens in private messages can't be
152
+ // tried here. Discord only offers the commands of a server in that
153
+ // server, and private messages are answered by the bot in production.
154
+ const noted = new Set();
155
+ const notePrivateOnly = () => {
156
+ const note = (file, what) => {
157
+ if (noted.has(file))
158
+ return;
159
+ noted.add(file);
160
+ log.info(`${file} ${what} only works in private messages, and chapterjs dev only runs your bot in ${guild.name}. Try it with chapterjs start.`);
161
+ };
162
+ for (const { file, command } of privateOnly([
163
+ ...project.commands.values(),
164
+ ])) {
165
+ note(file, commandName(command.path));
166
+ }
167
+ for (const { file, event } of project.events.values()) {
168
+ if (EVENTS[event.name].options &&
169
+ event.options.where === 'dm') {
170
+ note(file, `this ${event.name} file`);
171
+ }
172
+ }
173
+ for (const { file, component } of project.components.values()) {
174
+ if (component.kind !== 'embed' && component.where === 'dm') {
175
+ note(file, `the ${component.kind} ${component.path}`);
176
+ }
177
+ }
178
+ };
179
+ notePrivateOnly();
180
+ // 4. Every save reloads the project. Watching starts right away; the
181
+ // first thing done is to tell Discord the commands.
182
+ let reloading = syncCommands().then(updated => {
183
+ if (updated)
184
+ log.success(`Commands updated on ${guild.name}`);
185
+ });
186
+ watcher = watchFolder(src, () => {
187
+ reloading = reloading
188
+ .then(async () => {
189
+ const started = performance.now();
190
+ nextGeneration();
191
+ const failures = await project.load();
192
+ for (const failure of failures)
193
+ project.report(failure);
194
+ notePrivateOnly();
195
+ if (bot) {
196
+ configure(bot.ctx.cache, limitsFor(project.events.values()));
197
+ if (project.applyPresence(bot))
198
+ log.reload('Presence updated');
199
+ }
200
+ // A language that appeared or went: `t` is typed from the
201
+ // folder. A command described in its file, or no longer: the
202
+ // language files offer it, or not.
203
+ if ((await writeTypes(cwd, project.commands, project.messages, project.languageDefaults)) > 0)
204
+ log.reload('Types updated');
205
+ const intents = project.intents(false);
206
+ if ((intents & ~connectedIntents) !== 0 && bot) {
207
+ // Intents are given when connecting: new ones need a new session.
208
+ log.reload(`Your files now need more from Discord (${describeIntents(intents & ~connectedIntents)}): reconnecting...`);
209
+ const previous = bot;
210
+ bot = null;
211
+ await previous.close();
212
+ bot = await connect();
213
+ log.success(`Reconnected, ${project.summary()}`);
214
+ if (await syncCommands())
215
+ log.reload('Commands updated on Discord');
216
+ return;
217
+ }
218
+ const ms = Math.max(1, Math.round(performance.now() - started));
219
+ const updated = await syncCommands();
220
+ if (failures.length === 0)
221
+ log.reload(`Reloaded in ${ms} ms, ${project.summary()}`);
222
+ else {
223
+ 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`);
224
+ }
225
+ if (updated)
226
+ log.reload('Commands updated on Discord');
227
+ })
228
+ .catch(error => {
229
+ if (!signal.aborted)
230
+ explain(error, log);
231
+ fatal(1);
232
+ });
233
+ }, { loaded });
234
+ // 5. Until the user stops it, or Discord refuses the bot for good.
235
+ if (signal.aborted)
236
+ fatal(0);
237
+ signal.addEventListener('abort', () => fatal(0), { once: true });
238
+ const code = await ended;
239
+ await reloading.catch(() => { });
240
+ project.stopTasks();
241
+ watcher.close();
242
+ assets.close();
243
+ memory.stop();
244
+ await bot?.close();
245
+ if (code === 0)
246
+ log.success('Disconnected');
247
+ return code;
152
248
  }
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(', ')}`);
249
+ catch (error) {
250
+ project.stopTasks();
251
+ watcher?.close();
252
+ assets.close();
253
+ memory.stop();
254
+ await bot?.close();
255
+ if (signal.aborted)
256
+ return 0;
257
+ explain(error, log);
258
+ return 1;
159
259
  }
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
260
  }
@@ -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
+ }