@wolfstar/cli 2.3.1 → 2.4.0-next-20261006112342

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 (34) hide show
  1. package/README.md +43 -1
  2. package/dist/{bridge-BegCt_n2.js → bridge-B5BP6eXj.js} +2 -235
  3. package/dist/bridge-B5BP6eXj.js.map +1 -0
  4. package/dist/cli.js +1 -1
  5. package/dist/codegen-CF6o8v6A.js +262 -0
  6. package/dist/codegen-CF6o8v6A.js.map +1 -0
  7. package/dist/commands-C_B9ENxc.js +15 -0
  8. package/dist/{commands-CSB-pjgs.js.map → commands-C_B9ENxc.js.map} +1 -1
  9. package/dist/commands-DY-hEtQy.js +3 -0
  10. package/dist/{commands-Bh0K9iWX.js → commands-nGd6jExv.js} +4 -92
  11. package/dist/commands-nGd6jExv.js.map +1 -0
  12. package/dist/{completions-DMPlZ0S1.js → completions-CBQHiSnE.js} +2 -2
  13. package/dist/{completions-DMPlZ0S1.js.map → completions-CBQHiSnE.js.map} +1 -1
  14. package/dist/{dev-Owu3e3AO.js → dev-D9MAtiCk.js} +3 -2
  15. package/dist/{dev-Owu3e3AO.js.map → dev-D9MAtiCk.js.map} +1 -1
  16. package/dist/{doctor-DEtAAknK.js → doctor-DYAdmZZq.js} +3 -2
  17. package/dist/{doctor-DEtAAknK.js.map → doctor-DYAdmZZq.js.map} +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/{info-CKJVMWIT.js → info-CTUGxaHN.js} +2 -2
  20. package/dist/info-CTUGxaHN.js.map +1 -0
  21. package/dist/local-commands-DG_a4XE9.js +95 -0
  22. package/dist/local-commands-DG_a4XE9.js.map +1 -0
  23. package/dist/{run-CHqwvZXf.js → run-CjI3xBby.js} +2 -2
  24. package/dist/{run-CHqwvZXf.js.map → run-CjI3xBby.js.map} +1 -1
  25. package/dist/tunnel-C3bk4dq_.js +236 -0
  26. package/dist/tunnel-C3bk4dq_.js.map +1 -0
  27. package/package.json +5 -5
  28. package/dist/bridge-BegCt_n2.js.map +0 -1
  29. package/dist/codegen-aJSyPz-J.js +0 -124
  30. package/dist/codegen-aJSyPz-J.js.map +0 -1
  31. package/dist/commands-Bh0K9iWX.js.map +0 -1
  32. package/dist/commands-BlDTDvFO.js +0 -3
  33. package/dist/commands-CSB-pjgs.js +0 -15
  34. package/dist/info-CKJVMWIT.js.map +0 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@ package has no install-time dependency on `@wolfstar/http-framework` in return,
24
24
  - `stars dev` builds the project, starts the bot, restarts it (or leaves the change to the bot's hot reload) and shows what is happening in a full-screen dashboard: status, log channels and levels you can filter, and a prompt before changed commands are redeployed (or plain logs).
25
25
  - `stars build` runs the configured build tool once.
26
26
  - `stars info` prints the resolved configuration and environment (`--json` for scripts).
27
- - `stars codegen` runs the configured code generators (`--check` for CI).
27
+ - `stars codegen` runs the configured code generators: i18next types and the typed options of your commands (`--check` for CI).
28
28
  - `stars prepare` generates `.stars/tsconfig.json` and the auto imports declaration file (`--check` for CI).
29
29
  - `stars commands` inspects, compares, deploys and cleans the application commands Discord has deployed.
30
30
  - `stars doctor` checks that the project is ready: runtime, framework, credentials, interactions endpoint, generated files.
@@ -275,6 +275,48 @@ fills in on its own (`id`, `version`, defaults such as `nsfw: false`) are ignore
275
275
  overwrite: a deployed command the project no longer defines is deleted, which is why it asks first and refuses to
276
276
  run without `--yes` outside a terminal, or with `--json`.
277
277
 
278
+ ### `stars codegen`
279
+
280
+ Runs the code generators `codegen` in `stars.config` enables, writing their files (`--check` fails with
281
+ `CODEGEN_OUTDATED` instead when one is stale, `--json` prints `{ check, results }`):
282
+
283
+ - `codegen.i18n` types the i18next resources with `@wolfstar/i18next-type-generator`. It is on by default when
284
+ `src/locales/en-US` exists.
285
+ - `codegen.commands` types the options of every command from its builder. It is off by default: `true` writes
286
+ `src/@types/commands.d.ts`, `{ output }` picks another file.
287
+
288
+ ```typescript
289
+ export default defineConfig({ codegen: { commands: true } });
290
+ ```
291
+
292
+ The generated file augments `CommandOptionsRegistry` with one entry per command path (`'ping'`, `'math add'`,
293
+ `'subscriptions twitch add'`), and `Command.OptionsOf<'math add'>` reads it, so the handler needs no hand-written
294
+ `interface Options` that can drift from the builder:
295
+
296
+ ```typescript
297
+ public add(interaction: Command.ChatInputInteraction, { left, right }: Command.OptionsOf<'math add'>) {}
298
+ ```
299
+
300
+ Every option has the shape the framework resolves it to at runtime (`user` is `TransformedArguments.User`, `role` is
301
+ `APIRole`, ...), a `required` option is not optional, `choices` are a literal union and `channel_types` narrow a
302
+ channel. A localized command is keyed by its default name, the one the handler receives. Context menu commands have no
303
+ options and are skipped. For an autocomplete, `Command.AutocompleteArguments<Command.OptionsOf<'math add'>>` types
304
+ `focused` from the same entry.
305
+
306
+ The types come from what the bot registers, so loops and factories need no special care: `stars codegen` reads the
307
+ commands from the built bot the way `stars commands diff` does (run `stars build` first), by starting it in a mode
308
+ that loads its pieces, reports its registry and exits before it listens or talks to Discord. It does not call
309
+ `client.login`, but the bot still has to get through its own setup: values it reads while starting (`DISCORD_CLIENT_ID`,
310
+ `DISCORD_PUBLIC_KEY`, ...) have to be set, with placeholders in CI where the real secrets are not available:
311
+
312
+ ```sh
313
+ DISCORD_CLIENT_ID=1 DISCORD_TOKEN=x DISCORD_PUBLIC_KEY=0 stars codegen --check
314
+ ```
315
+
316
+ It is not part of `stars dev` or `stars prepare` (which has no build to read from yet): run `stars codegen` after
317
+ changing an option, and `stars codegen --check` in CI to keep the file up to date. A path registered more than once
318
+ (a command restricted to several guilds) keeps its first definition.
319
+
278
320
  ### `stars doctor`
279
321
 
280
322
  Checks what a project needs before `stars dev` can do its job, and says what to do about each problem:
@@ -1,240 +1,7 @@
1
1
  import { t as findInstalledVersion } from "./project-zlQStwsf.js";
2
- import { i as readProjectEnv } from "./hooks-Cy6F2bhH.js";
3
2
  import { pathToFileURL } from "node:url";
4
- import { EventEmitter } from "node:events";
5
3
  import { resolveSync } from "mlly";
6
- import { startTunnel } from "untun";
7
4
 
8
- //#region src/dev/tunnel.ts
9
- const SIGNALS = [
10
- "SIGINT",
11
- "SIGTERM",
12
- "SIGHUP"
13
- ];
14
- function isCloudflaredExit(reason) {
15
- return reason instanceof Error && reason.message.startsWith("cloudflared exited (");
16
- }
17
- /**
18
- * Exposes the bot's interactions endpoint publicly while `stars dev` runs.
19
- *
20
- * `dev.tunnel: true` opens a `cloudflared` quick tunnel via `untun` (it changes on every run); a configured https
21
- * URL is only probed, since the user already serves it. Writing the URL to the Discord application is opt-in
22
- * through `dev.tunnel.updateEndpoint`, because it edits a live application.
23
- */
24
- var Tunnel = class extends EventEmitter {
25
- config;
26
- #startTunnel;
27
- #tunnel = null;
28
- #state = "off";
29
- #url = null;
30
- #wanted = false;
31
- #rejectionGuard = null;
32
- constructor(config, options = {}) {
33
- super();
34
- this.config = config;
35
- this.#startTunnel = options.startTunnel ?? startTunnel;
36
- }
37
- get state() {
38
- return this.#state;
39
- }
40
- /** The public URL of the interactions endpoint, `null` until the tunnel is up. */
41
- get url() {
42
- return this.#url;
43
- }
44
- async start(forceQuick = false) {
45
- if (this.#state === "starting" || this.#state === "up") return;
46
- const configured = this.config.dev.tunnel;
47
- const tunnel = forceQuick && configured.mode === "off" ? {
48
- mode: "quick",
49
- path: "/",
50
- updateEndpoint: false
51
- } : configured;
52
- if (tunnel.mode === "off") return;
53
- this.#wanted = true;
54
- this.#setState("starting", null);
55
- const url = tunnel.mode === "url" ? await this.#useConfiguredUrl(tunnel) : await this.#openQuickTunnel();
56
- if (!url || !this.#wanted) return;
57
- this.#setState("up", endpointUrl(url, tunnel.path));
58
- this.emit("log", "success", `Tunnel ready at ${this.#url}`);
59
- if (tunnel.updateEndpoint) await this.#updateInteractionsEndpoint(this.#url);
60
- }
61
- /** Toggles the configured tunnel, opening a quick tunnel when the project did not configure one. */
62
- toggle() {
63
- return this.#state === "starting" || this.#state === "up" ? this.close() : this.start(true);
64
- }
65
- async close() {
66
- this.#wanted = false;
67
- const tunnel = this.#tunnel;
68
- this.#tunnel = null;
69
- this.#setState("off", null);
70
- if (!tunnel) return;
71
- try {
72
- await tunnel.close();
73
- } finally {
74
- this.#releaseRejectionGuard();
75
- }
76
- }
77
- async #useConfiguredUrl(tunnel) {
78
- if (!await probe(endpointUrl(tunnel.url, tunnel.path))) this.emit("log", "warn", `${tunnel.url} did not answer; make sure it forwards to ${this.config.dev.url ?? "the bot"}`);
79
- return tunnel.url;
80
- }
81
- async #openQuickTunnel() {
82
- const target = this.config.dev.url;
83
- if (!target) {
84
- this.emit("log", "error", "A quick tunnel needs `dev.url` to know what to forward to");
85
- this.#setState("failed", null);
86
- return null;
87
- }
88
- this.emit("log", "info", "Opening a cloudflared quick tunnel…");
89
- this.#installRejectionGuard();
90
- try {
91
- const tunnel = await this.#startWithoutSignalHandlers(target);
92
- if (!this.#wanted) {
93
- await tunnel?.close();
94
- this.#releaseRejectionGuard();
95
- return null;
96
- }
97
- if (!tunnel) {
98
- this.emit("log", "error", "cloudflared setup was cancelled");
99
- this.#setState("failed", null);
100
- this.#releaseRejectionGuard();
101
- return null;
102
- }
103
- this.#tunnel = tunnel;
104
- return await tunnel.getURL();
105
- } catch (error) {
106
- this.#releaseRejectionGuard();
107
- if (!this.#wanted) return null;
108
- this.emit("log", "error", `cloudflared failed: ${error instanceof Error ? error.message : String(error)}`);
109
- this.#setState("failed", null);
110
- return null;
111
- }
112
- }
113
- /**
114
- * `untun` adds its own `SIGINT`/`SIGTERM`/`SIGHUP` listeners that call `process.exit()` once cloudflared is closed,
115
- * which races `stars dev`'s graceful shutdown (it closes the tunnel through {@link close} anyway), so they are removed.
116
- */
117
- async #startWithoutSignalHandlers(target) {
118
- const before = SIGNALS.map((signal) => new Set(process.listeners(signal)));
119
- try {
120
- return await this.#startTunnel({
121
- url: target,
122
- acceptCloudflareNotice: true
123
- });
124
- } finally {
125
- SIGNALS.forEach((signal, index) => {
126
- for (const listener of process.listeners(signal)) if (!before[index].has(listener)) process.off(signal, listener);
127
- });
128
- }
129
- }
130
- /**
131
- * `untun` rejects a promise per connection when cloudflared exits and never handles it (and its location pattern
132
- * does not match lowercase codes such as `mxp03`, so that promise is never settled before), which Node reports as an
133
- * unhandled rejection that ends the process, whenever the tunnel closes. The guard swallows that one error for as
134
- * long as this tunnel exists; anything else it sees is thrown again, as if it was not installed.
135
- */
136
- #installRejectionGuard() {
137
- if (this.#rejectionGuard) return;
138
- const guard = (reason) => {
139
- if (!isCloudflaredExit(reason)) {
140
- if (process.listeners("unhandledRejection").length === 1) throw reason;
141
- return;
142
- }
143
- if (this.#state !== "up" || !this.#wanted) return;
144
- this.#tunnel = null;
145
- this.emit("log", "error", `cloudflared stopped unexpectedly: ${reason.message}`);
146
- this.#setState("failed", null);
147
- this.#releaseRejectionGuard();
148
- };
149
- this.#rejectionGuard = guard;
150
- process.on("unhandledRejection", guard);
151
- }
152
- /** Node reports an unhandled rejection after the microtasks that follow it, so the guard has to outlive them. */
153
- #releaseRejectionGuard() {
154
- const guard = this.#rejectionGuard;
155
- this.#rejectionGuard = null;
156
- if (guard) setImmediate(() => process.off("unhandledRejection", guard));
157
- }
158
- /**
159
- * Points the Discord application's interactions endpoint at the tunnel. Discord validates the endpoint with a
160
- * signed ping before accepting it, so the bot has to be up already — this runs after the first successful build.
161
- */
162
- async #updateInteractionsEndpoint(url) {
163
- const credentials = readDiscordCredentials(this.config);
164
- if (!credentials) {
165
- this.emit("log", "warn", "Skipped the interactions endpoint update: DISCORD_TOKEN is not set");
166
- return;
167
- }
168
- try {
169
- const response = await fetch("https://discord.com/api/v10/applications/@me", {
170
- method: "PATCH",
171
- headers: {
172
- authorization: `Bot ${credentials.token}`,
173
- "content-type": "application/json"
174
- },
175
- body: JSON.stringify({ interactions_endpoint_url: url }),
176
- signal: AbortSignal.timeout(1e4)
177
- });
178
- if (response.ok) this.emit("log", "success", `Interactions endpoint set to ${url}`);
179
- else this.emit("log", "error", `Discord rejected the interactions endpoint (${response.status}): ${(await response.text()).slice(0, 300)}`);
180
- } catch (error) {
181
- this.emit("log", "error", `Failed to update the interactions endpoint: ${error instanceof Error ? error.message : String(error)}`);
182
- }
183
- }
184
- #setState(state, url) {
185
- this.#state = state;
186
- this.#url = url;
187
- this.emit("state", state, url);
188
- }
189
- };
190
- /**
191
- * Joins the tunnel's origin with the path the interactions endpoint is served on.
192
- */
193
- function endpointUrl(origin, path) {
194
- return path === "/" ? origin : new URL(path, origin.endsWith("/") ? origin : `${origin}/`).href;
195
- }
196
- async function probe(url) {
197
- try {
198
- return (await fetch(url, {
199
- method: "HEAD",
200
- signal: AbortSignal.timeout(5e3)
201
- })).status < 500;
202
- } catch {
203
- return false;
204
- }
205
- }
206
- /**
207
- * Reads the Discord credentials the CLI needs from the environment, falling back to the project's `.env` files the
208
- * way the bot itself does once it starts.
209
- */
210
- function readDiscordCredentials(config, env = process.env) {
211
- const fromFiles = readProjectEnvFilesCached(config);
212
- const read = (...keys) => {
213
- for (const key of keys) {
214
- const value = config.dev.env[key] ?? env[key] ?? fromFiles[key];
215
- if (value) return value;
216
- }
217
- return null;
218
- };
219
- const token = read("DISCORD_TOKEN", "TOKEN");
220
- if (!token) return null;
221
- return {
222
- token,
223
- applicationId: read("DISCORD_APPLICATION_ID", "APPLICATION_ID", "DISCORD_CLIENT_ID", "CLIENT_ID")
224
- };
225
- }
226
- let envFileCache = null;
227
- /** The variables the bot loads (see {@link readProjectEnv}), once per project and `env` options. */
228
- function readProjectEnvFilesCached(config) {
229
- const key = JSON.stringify([config.root, config.env.options]);
230
- if (envFileCache?.key !== key) envFileCache = {
231
- key,
232
- values: readProjectEnv(config)
233
- };
234
- return envFileCache.values;
235
- }
236
-
237
- //#endregion
238
5
  //#region src/dev/bridge.ts
239
6
  /** Marks a message of the bridge, so a bot using the IPC channel for something else is left alone. */
240
7
  const BRIDGE_SOURCE = "stars:bridge";
@@ -467,5 +234,5 @@ function isAtLeast(version, minimum) {
467
234
  }
468
235
 
469
236
  //#endregion
470
- export { isAtLeast as a, endpointUrl as c, bridgeImportArgs as i, readDiscordCredentials as l, CLI_SOURCE as n, parseBridgeMessage as o, DUMP_ENV as r, Tunnel as s, BRIDGE_FRAMEWORK_VERSION as t };
471
- //# sourceMappingURL=bridge-BegCt_n2.js.map
237
+ export { isAtLeast as a, bridgeImportArgs as i, CLI_SOURCE as n, parseBridgeMessage as o, DUMP_ENV as r, BRIDGE_FRAMEWORK_VERSION as t };
238
+ //# sourceMappingURL=bridge-B5BP6eXj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge-B5BP6eXj.js","names":[],"sources":["../src/dev/bridge.ts"],"sourcesContent":["import type { ResolvedStarsConfig } from '@wolfstar/schema';\nimport { resolveSync } from 'mlly';\nimport { pathToFileURL } from 'node:url';\nimport { findInstalledVersion } from '../utils/project.js';\n\n/** Marks a message of the bridge, so a bot using the IPC channel for something else is left alone. */\nexport const BRIDGE_SOURCE = 'stars:bridge';\n/** Marks a message `stars dev` sends to the bot. */\nexport const CLI_SOURCE = 'stars:cli';\n/** Set on a bot started only to report the commands it registers (see `readLocalCommands`). */\nexport const DUMP_ENV = 'STARS_COMMANDS_DUMP';\n/** The first `@wolfstar/http-framework` with object plugins (`Client.use({ name, postListen })`), which the bridge is. */\nexport const BRIDGE_FRAMEWORK_VERSION = '6.1.0';\n\n/** An application command as the bot would register it: the JSON body Discord receives. */\nexport interface CommandData {\n\tname: string;\n\ttype?: number;\n\t[key: string]: unknown;\n}\n\nexport interface CommandSnapshot {\n\tglobal: CommandData[];\n\t/** Guild-restricted commands, by guild id. */\n\tguilds: Record<string, CommandData[]>;\n}\n\nexport interface BridgeError {\n\tmessage: string;\n\t/** The first stack frames, without the message line. */\n\tstack: string[];\n}\n\n/**\n * What the bot tells `stars dev` over the IPC channel. The bot's stdout only carries text a level is guessed for; this\n * is everything the dev UI shows that happens inside the bot: hot reloads, requests, dispatches, its commands.\n */\nexport type BridgeMessage =\n\t| { type: 'ready'; clientId: string; port: number | null }\n\t| { type: 'pieces'; store: string; pieces: { name: string; path: string }[] }\n\t| { type: 'hmr'; event: 'start'; paths: string[] }\n\t| { type: 'hmr'; event: 'stop' }\n\t| { type: 'hmr'; event: 'reloaded' | 'loaded' | 'unloaded'; store: string; names: string[]; path: string }\n\t| ({ type: 'hmr'; event: 'error'; path: string } & BridgeError)\n\t| ({ type: 'commands' } & CommandSnapshot)\n\t| { type: 'dispatch'; phase: 'run'; route: string; piece: string }\n\t| { type: 'dispatch'; phase: 'success'; route: string; piece: string; ms: number | null }\n\t| { type: 'dispatch'; phase: 'error'; route: string; piece: string; ms: number | null; error: BridgeError }\n\t| { type: 'request'; method: string; path: string; status: number; ms: number }\n\t| { type: 'refreshed'; ok: true; global: number; guilds: number; cleared: number }\n\t| ({ type: 'refreshed'; ok: false } & BridgeError)\n\t/** A line on a channel of the sender's choice: how a plugin gets its own channel in the dev UI. */\n\t| { type: 'log'; channel: string; level: string; text: string; detail?: string[] };\n\n/** What the CLI asks of the bot. `clearGuilds` are guilds whose last command was removed: nothing registers them any more. */\nexport interface RefreshRequest {\n\tsource: typeof CLI_SOURCE;\n\ttype: 'commands:refresh';\n\tclearGuilds: string[];\n}\n\ntype Loose = Record<string, any>;\n\nconst isString = (value: unknown): value is string => typeof value === 'string';\nconst isNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);\nconst isStrings = (value: unknown): value is string[] => Array.isArray(value) && value.every(isString);\nconst isRecord = (value: unknown): value is Loose => value !== null && typeof value === 'object' && !Array.isArray(value);\nconst isCommands = (value: unknown): boolean => Array.isArray(value) && value.every((command) => isRecord(command) && isString(command.name));\nconst isError = (value: Loose): boolean => isString(value.message) && isStrings(value.stack);\n\n/** The shape each message must have. A plugin can send `log`, so nothing here is taken on trust. */\nconst SHAPES: Record<BridgeMessage['type'], (message: Loose) => boolean> = {\n\tready: (message) => isString(message.clientId) && (message.port === null || isNumber(message.port)),\n\tpieces: (message) =>\n\t\tisString(message.store) &&\n\t\tArray.isArray(message.pieces) &&\n\t\tmessage.pieces.every((piece: unknown) => isRecord(piece) && isString(piece.name) && isString(piece.path)),\n\thmr: (message) => {\n\t\tswitch (message.event) {\n\t\t\tcase 'start':\n\t\t\t\treturn isStrings(message.paths);\n\t\t\tcase 'stop':\n\t\t\t\treturn true;\n\t\t\tcase 'error':\n\t\t\t\treturn isString(message.path) && isError(message);\n\t\t\tcase 'reloaded':\n\t\t\tcase 'loaded':\n\t\t\tcase 'unloaded':\n\t\t\t\treturn isString(message.store) && isStrings(message.names) && isString(message.path);\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t},\n\tcommands: (message) => isCommands(message.global) && isRecord(message.guilds) && Object.values(message.guilds).every(isCommands),\n\tdispatch: (message) => {\n\t\tif (!isString(message.route) || !isString(message.piece)) return false;\n\t\tif (message.phase === 'run') return true;\n\t\tif (message.ms !== null && !isNumber(message.ms)) return false;\n\t\treturn message.phase === 'success' || (message.phase === 'error' && isRecord(message.error) && isError(message.error));\n\t},\n\trequest: (message) => isString(message.method) && isString(message.path) && isNumber(message.status) && isNumber(message.ms),\n\trefreshed: (message) =>\n\t\tmessage.ok === true\n\t\t\t? isNumber(message.global) && isNumber(message.guilds) && isNumber(message.cleared)\n\t\t\t: message.ok === false && isError(message),\n\tlog: (message) =>\n\t\tisString(message.channel) &&\n\t\tmessage.channel.length > 0 &&\n\t\tisString(message.level) &&\n\t\tisString(message.text) &&\n\t\t(message.detail === undefined || isStrings(message.detail))\n};\n\n/**\n * Narrows an IPC message to one of the bridge's, `null` for anything else the bot sends and for a bridge message that\n * does not have the shape of its type.\n */\nexport function parseBridgeMessage(value: unknown): BridgeMessage | null {\n\tif (!isRecord(value) || value.source !== BRIDGE_SOURCE || !isString(value.type) || !Object.hasOwn(SHAPES, value.type)) return null;\n\treturn SHAPES[value.type as BridgeMessage['type']](value) ? (value as BridgeMessage) : null;\n}\n\n/**\n * The source of the bridge: a module preloaded into the bot (`node --import`) that registers a plugin on the bot's own\n * `Client` and reports what happens there with `process.send`. It never writes to stdout, and does nothing when the\n * process has no IPC channel, so it is inert outside `stars dev`.\n *\n * It only uses what `@wolfstar/http-framework` already exposes — the client's events, its registry and its stores —\n * which is what keeps `@wolfstar/cli` free of a dependency on the framework: `framework` is the project's own copy.\n */\nexport function bridgeModuleSource(framework: string): string {\n\treturn `import { Client, container } from ${JSON.stringify(framework)};\n\nif (typeof process.send === 'function') {\n\tconst send = (type, data) => {\n\t\ttry {\n\t\t\tprocess.send({ source: ${JSON.stringify(BRIDGE_SOURCE)}, type, ...data });\n\t\t} catch {}\n\t};\n\tconst describe = (error) => ({\n\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\tstack:\n\t\t\terror instanceof Error && typeof error.stack === 'string'\n\t\t\t\t? error.stack.split('\\\\n').slice(1, 6).map((line) => line.trim())\n\t\t\t\t: []\n\t});\n\tconst commandRoute = (context) => {\n\t\tconst data = context.interaction.data ?? {};\n\t\tconst kind = context.interaction.type === 4 ? 'autocomplete' : data.type === 2 ? 'user' : data.type === 3 ? 'message' : 'slash';\n\t\tlet name = String(data.name ?? 'unknown');\n\t\tlet options = data.options;\n\t\twhile (Array.isArray(options) && options.length === 1 && (options[0].type === 1 || options[0].type === 2)) {\n\t\t\tname += ' ' + options[0].name;\n\t\t\toptions = options[0].options;\n\t\t}\n\t\treturn kind + ':' + name;\n\t};\n\tconst handlerRoute = (context) => (context.interaction.type === 5 ? 'modal' : 'component') + ':' + context.handler.name;\n\tconst commandsOf = (client) => {\n\t\tconst guilds = {};\n\t\tfor (const [id, commands] of client.registry.getLoadedGuildCommands()) guilds[id] = commands;\n\t\treturn { global: client.registry.getLoadedGlobalCommands(), guilds };\n\t};\n\tconst snapshot = (client) => {\n\t\ttry {\n\t\t\tsend('commands', commandsOf(client));\n\t\t} catch {}\n\t};\n\n\ttry {\n\t\tClient.use({\n\t\t\tname: 'stars:dev-bridge',\n\t\t\tenforce: 'post',\n\t\t\tpostInitialization(client) {\n\t\t\t\tif (process.env[${JSON.stringify(DUMP_ENV)}] === '1') {\n\t\t\t\t\t// \\`stars commands diff\\` only wants to know what the bot would register: once its pieces are loaded\n\t\t\t\t\t// it has the answer, and exits before it listens or talks to Discord.\n\t\t\t\t\tconst load = client.load.bind(client);\n\t\t\t\t\tclient.load = async (...args) => {\n\t\t\t\t\t\tawait load(...args);\n\t\t\t\t\t\tprocess.send({ source: ${JSON.stringify(BRIDGE_SOURCE)}, type: 'commands', ...commandsOf(client) }, () => process.exit(0));\n\t\t\t\t\t\tawait new Promise(() => {});\n\t\t\t\t\t};\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst started = new WeakMap();\n\t\t\t\tconst track = (prefix, route, pieceOf) => {\n\t\t\t\t\tclient.on(prefix + 'Run', (context) => {\n\t\t\t\t\t\tstarted.set(context.interaction, performance.now());\n\t\t\t\t\t\tsend('dispatch', { phase: 'run', route: route(context), piece: pieceOf(context).name });\n\t\t\t\t\t});\n\t\t\t\t\tconst done = (phase) => (first, second) => {\n\t\t\t\t\t\tconst context = phase === 'error' ? second : first;\n\t\t\t\t\t\tconst at = started.get(context.interaction);\n\t\t\t\t\t\tsend('dispatch', {\n\t\t\t\t\t\t\tphase,\n\t\t\t\t\t\t\troute: route(context),\n\t\t\t\t\t\t\tpiece: pieceOf(context).name,\n\t\t\t\t\t\t\tms: at === undefined ? null : Math.round(performance.now() - at),\n\t\t\t\t\t\t\t...(phase === 'error' ? { error: describe(first) } : {})\n\t\t\t\t\t\t});\n\t\t\t\t\t};\n\t\t\t\t\tclient.on(prefix + 'Success', done('success'));\n\t\t\t\t\tclient.on(prefix + 'Error', done('error'));\n\t\t\t\t};\n\t\t\t\ttrack('command', commandRoute, (context) => context.command);\n\t\t\t\ttrack('autocomplete', commandRoute, (context) => context.command);\n\t\t\t\ttrack('interactionHandler', handlerRoute, (context) => context.handler);\n\n\t\t\t\tconst changed = (event) => (pieces, path) => {\n\t\t\t\t\tconst list = Array.isArray(pieces) ? pieces : [pieces];\n\t\t\t\t\tconst store = list[0]?.store?.name ?? 'pieces';\n\t\t\t\t\tsend('hmr', { event, path, store, names: list.map((piece) => piece.name) });\n\t\t\t\t\tif (store === 'commands') snapshot(client);\n\t\t\t\t};\n\t\t\t\tclient.on('hmrStart', (paths) => send('hmr', { event: 'start', paths }));\n\t\t\t\tclient.on('hmrStop', () => send('hmr', { event: 'stop' }));\n\t\t\t\tclient.on('hmrPieceReloaded', changed('reloaded'));\n\t\t\t\tclient.on('hmrPiecesLoaded', changed('loaded'));\n\t\t\t\tclient.on('hmrPieceUnloaded', changed('unloaded'));\n\t\t\t\tclient.on('hmrError', (error, path) => send('hmr', { event: 'error', path, ...describe(error) }));\n\t\t\t},\n\t\t\tpostListen(client) {\n\t\t\t\tif (process.env[${JSON.stringify(DUMP_ENV)}] === '1') return;\n\t\t\t\tconst server = client.server;\n\t\t\t\tserver?.on?.('request', (request, response) => {\n\t\t\t\t\tconst at = performance.now();\n\t\t\t\t\tresponse.once('finish', () =>\n\t\t\t\t\t\tsend('request', {\n\t\t\t\t\t\t\tmethod: String(request.method),\n\t\t\t\t\t\t\tpath: String(request.url),\n\t\t\t\t\t\t\tstatus: response.statusCode,\n\t\t\t\t\t\t\tms: Math.round(performance.now() - at)\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t\tfor (const store of container.stores.values()) {\n\t\t\t\t\tsend('pieces', {\n\t\t\t\t\t\tstore: store.name,\n\t\t\t\t\t\tpieces: [...store.values()].map((piece) => ({ name: piece.name, path: piece.location.full }))\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tsnapshot(client);\n\t\t\t\tconst address = server?.address?.();\n\t\t\t\tsend('ready', { clientId: client.id, port: address !== null && typeof address === 'object' ? address.port : null });\n\t\t\t}\n\t\t});\n\n\t\tprocess.on('message', (message) => {\n\t\t\tif (message === null || typeof message !== 'object' || message.source !== ${JSON.stringify(CLI_SOURCE)}) return;\n\t\t\tif (message.type !== 'commands:refresh') return;\n\t\t\tPromise.resolve()\n\t\t\t\t.then(async () => {\n\t\t\t\t\tconst client = container.client;\n\t\t\t\t\tconst global = await client.registry.pushGlobalCommands();\n\t\t\t\t\tconst guilds = await client.registry.pushGuildRestrictedCommands();\n\t\t\t\t\tconst failed = guilds.find((result) => result.status === 'rejected');\n\t\t\t\t\tif (failed) throw failed.reason;\n\t\t\t\t\t// A guild that lost its last command is no longer in the registry, so nothing above overwrote it.\n\t\t\t\t\tconst clear = Array.isArray(message.clearGuilds) ? message.clearGuilds.filter((id) => typeof id === 'string') : [];\n\t\t\t\t\tfor (const id of clear) {\n\t\t\t\t\t\tawait container.rest.put('/applications/' + client.id + '/guilds/' + id + '/commands', { body: [] });\n\t\t\t\t\t}\n\t\t\t\t\tsend('refreshed', { ok: true, global: global.length, guilds: guilds.length, cleared: clear.length });\n\t\t\t\t})\n\t\t\t\t.catch((error) => send('refreshed', { ok: false, ...describe(error) }));\n\t\t});\n\t\t// Listening for messages keeps the channel, and with it the process, alive: a bot that is done must still exit.\n\t\tprocess.channel?.unref?.();\n\t} catch {}\n}\n`;\n}\n\n/**\n * `node` arguments that preload the bridge into the bot. Nothing is preloaded when the project's framework cannot be\n * resolved or predates object plugins: `stars dev` then shows what the bot prints, as it did before the bridge.\n *\n * The framework is resolved from the project with the `import` condition, the way `moduleImportArgs` does, so the\n * plugin lands on the `Client` the bot constructs. A build that bundles the framework into its output (Vite, Nitro)\n * has its own copy, which the preload cannot reach: the bridge stays silent there too.\n */\nexport function bridgeImportArgs(config: ResolvedStarsConfig): string[] {\n\tconst version = findInstalledVersion(config.root, '@wolfstar/http-framework');\n\tif (version === null || !isAtLeast(version, BRIDGE_FRAMEWORK_VERSION)) return [];\n\n\tlet framework: string;\n\ttry {\n\t\tframework = resolveSync('@wolfstar/http-framework', { url: pathToFileURL(`${config.root}/package.json`).href });\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn ['--import', `data:text/javascript,${encodeURIComponent(bridgeModuleSource(framework))}`];\n}\n\n/** Compares the `major.minor.patch` of two versions; a prerelease counts as its release. */\nexport function isAtLeast(version: string, minimum: string): boolean {\n\tconst parse = (value: string) =>\n\t\tvalue\n\t\t\t.split('-', 1)[0]!\n\t\t\t.split('.')\n\t\t\t.map((part) => Number.parseInt(part, 10) || 0);\n\tconst [a, b] = [parse(version), parse(minimum)];\n\tfor (let index = 0; index < 3; index++) {\n\t\tconst difference = (a[index] ?? 0) - (b[index] ?? 0);\n\t\tif (difference !== 0) return difference > 0;\n\t}\n\n\treturn true;\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runMain } from "./run-CHqwvZXf.js";
2
+ import { t as runMain } from "./run-CjI3xBby.js";
3
3
 
4
4
  //#region src/cli.ts
5
5
  await runMain();
@@ -0,0 +1,262 @@
1
+ import { i as shouldUseColor } from "./output-mode-Bp7Da8vJ.js";
2
+ import { t as cliDiagnostics } from "./diagnostics-BysbNnPD.js";
3
+ import { i as resolveFromProject } from "./project-zlQStwsf.js";
4
+ import { d as resolveCwd, n as loadProject, r as withProjectEnv, t as applyEnvOptions, u as projectArgs } from "./hooks-Cy6F2bhH.js";
5
+ import { t as readLocalCommands } from "./local-commands-DG_a4XE9.js";
6
+ import { defineCommand } from "citty";
7
+ import { dirname, join } from "node:path";
8
+ import { displayPath } from "@wolfstar/schema";
9
+ import { createColors } from "colorette";
10
+ import { tmpdir } from "node:os";
11
+ import { spawn } from "node:child_process";
12
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
13
+
14
+ //#region src/utils/command-types.ts
15
+ /** `ApplicationCommandOptionType`, which the CLI cannot import: it has no dependency on `discord-api-types`. */
16
+ const OptionType = {
17
+ Subcommand: 1,
18
+ SubcommandGroup: 2,
19
+ String: 3,
20
+ Integer: 4,
21
+ Boolean: 5,
22
+ User: 6,
23
+ Channel: 7,
24
+ Role: 8,
25
+ Mentionable: 9,
26
+ Number: 10,
27
+ Attachment: 11
28
+ };
29
+ /** `ApplicationCommandType.ChatInput`: the only type with options, and the one a command without `type` is. */
30
+ const CHAT_INPUT = 1;
31
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
32
+ /**
33
+ * The declaration file `stars codegen` writes for the commands a project registers: one `CommandOptionsRegistry` entry
34
+ * (`Command.OptionsOf<'path'>`) per leaf, that is a command without subcommands, `parent sub` or `parent group sub`.
35
+ *
36
+ * It reads the JSON Discord receives rather than the builders, so what the handler is typed with cannot drift from what
37
+ * is registered, and loops and factories need no special care. The option types point at the framework's
38
+ * `TransformedArguments`, which is what the router resolves them to, instead of repeating those shapes here.
39
+ *
40
+ * Context menu commands have no options and are skipped. A path registered more than once (a command restricted to
41
+ * several guilds is the same command in each) keeps its first definition, global commands first. The output depends
42
+ * on nothing but the commands, in sorted order, so `stars codegen --check` is a string comparison.
43
+ */
44
+ function generateCommandTypes(snapshot) {
45
+ const entries = /* @__PURE__ */ new Map();
46
+ const scopes = [snapshot.global, ...Object.keys(snapshot.guilds).sort().map((guild) => snapshot.guilds[guild])];
47
+ for (const commands of scopes) for (const command of commands) collectLeaves(command, entries);
48
+ const rendering = { usesResolvedTypes: false };
49
+ const lines = [...entries.keys()].sort().map((path) => ` ${renderEntry(path, entries.get(path), rendering)}`);
50
+ return [
51
+ "// This file is automatically generated by `stars codegen`, do not edit it.",
52
+ "/* eslint-disable */",
53
+ "/* oxlint-disable */",
54
+ "// oxfmt-ignore",
55
+ "// prettier-ignore",
56
+ rendering.usesResolvedTypes ? "import type { TransformedArguments } from '@wolfstar/http-framework';" : "export {};",
57
+ "",
58
+ "// oxfmt-ignore",
59
+ "// prettier-ignore",
60
+ "declare module '@wolfstar/http-framework' {",
61
+ " interface CommandOptionsRegistry {",
62
+ ...lines,
63
+ " }",
64
+ "}",
65
+ ""
66
+ ].join("\n");
67
+ }
68
+ function collectLeaves(command, entries) {
69
+ if ((command.type ?? CHAT_INPUT) !== CHAT_INPUT) return;
70
+ const options = command.options ?? [];
71
+ const add = (path, leaf) => {
72
+ if (!entries.has(path)) entries.set(path, leaf ?? []);
73
+ };
74
+ if (!options.some((option) => option.type === OptionType.Subcommand || option.type === OptionType.SubcommandGroup)) {
75
+ add(command.name, options);
76
+ return;
77
+ }
78
+ for (const option of options) if (option.type === OptionType.Subcommand) add(`${command.name} ${option.name}`, option.options);
79
+ else if (option.type === OptionType.SubcommandGroup) {
80
+ for (const sub of option.options ?? []) if (sub.type === OptionType.Subcommand) add(`${command.name} ${option.name} ${sub.name}`, sub.options);
81
+ }
82
+ }
83
+ /** Only the first line of an entry is indented by the caller: the options go on lines of their own. */
84
+ function renderEntry(path, options, rendering) {
85
+ const properties = [...options].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((option) => ` ${key(option.name)}${option.required ? "" : "?"}: ${renderOption(option, rendering)};`);
86
+ return properties.length === 0 ? `${quote(path)}: {};` : `${quote(path)}: {\n${properties.join("\n")}\n };`;
87
+ }
88
+ function renderOption(option, rendering) {
89
+ const resolved = (name) => {
90
+ rendering.usesResolvedTypes = true;
91
+ return `TransformedArguments.${name}`;
92
+ };
93
+ switch (option.type) {
94
+ case OptionType.String:
95
+ case OptionType.Integer:
96
+ case OptionType.Number: {
97
+ const fallback = option.type === OptionType.String ? "string" : "number";
98
+ if (!option.choices?.length) return fallback;
99
+ const values = option.choices.map((choice) => typeof choice.value === "string" ? quote(choice.value) : String(choice.value));
100
+ return [...new Set(values)].join(" | ");
101
+ }
102
+ case OptionType.Boolean: return "boolean";
103
+ case OptionType.User: return resolved("User");
104
+ case OptionType.Role: return resolved("Role");
105
+ case OptionType.Mentionable: return resolved("Mentionable");
106
+ case OptionType.Attachment: return resolved("Attachment");
107
+ case OptionType.Channel: {
108
+ const channel = resolved("Channel");
109
+ if (!option.channel_types?.length) return channel;
110
+ return `${channel} & { type: Extract<${channel}['type'], ${[...new Set(option.channel_types)].sort((a, b) => a - b).join(" | ")}> }`;
111
+ }
112
+ default: return "unknown";
113
+ }
114
+ }
115
+ function key(name) {
116
+ return IDENTIFIER.test(name) ? name : quote(name);
117
+ }
118
+ function quote(value) {
119
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'").replaceAll("\n", "\\n").replaceAll("\r", "\\r").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029")}'`;
120
+ }
121
+
122
+ //#endregion
123
+ //#region src/commands/codegen.ts
124
+ async function runCodegen(options) {
125
+ const stdout = options.stdout ?? process.stdout;
126
+ const colors = createColors({ useColor: shouldUseColor() && !options.json });
127
+ const project = await loadProject({
128
+ cwd: resolveCwd(options),
129
+ configFile: options.config
130
+ });
131
+ const { config } = project;
132
+ const results = [];
133
+ if (config.codegen.i18n) results.push(await runI18n(config, Boolean(options.check)));
134
+ if (config.codegen.commands) {
135
+ const botConfig = withProjectEnv(await applyEnvOptions(config, project.hooks));
136
+ results.push(await runCommands(botConfig, Boolean(options.check), options.commands));
137
+ }
138
+ if (options.json) stdout.write(`${JSON.stringify({
139
+ check: Boolean(options.check),
140
+ results
141
+ }, null, 2)}\n`);
142
+ else if (results.length === 0) stdout.write(`${colors.dim("stars")} nothing to generate (no code generators are configured)\n`);
143
+ else for (const result of results) {
144
+ const paint = result.status === "outdated" ? colors.red : colors.green;
145
+ stdout.write(`${colors.dim("stars")} ${result.generator}: ${paint(result.status)} ${displayPath(config.root, result.output)}\n`);
146
+ }
147
+ if (results.some((result) => result.status === "outdated")) throw cliDiagnostics.CODEGEN_OUTDATED({});
148
+ }
149
+ /**
150
+ * Types the options of the commands the project registers. The commands are read from the built bot, the way
151
+ * `stars commands diff` reads them: nothing connects to Discord, but the bot still runs its own env setup, so `--check`
152
+ * in CI needs placeholders for the values it reads while starting (`DISCORD_CLIENT_ID`, `DISCORD_PUBLIC_KEY`, ...).
153
+ */
154
+ async function runCommands(config, check, local) {
155
+ const { output } = config.codegen.commands;
156
+ const generated = generateCommandTypes(local ?? await readLocalCommands(config));
157
+ if (!check) {
158
+ await mkdir(dirname(output), { recursive: true });
159
+ await writeFile(output, generated);
160
+ return {
161
+ generator: "commands",
162
+ output,
163
+ status: "written"
164
+ };
165
+ }
166
+ return {
167
+ generator: "commands",
168
+ output,
169
+ status: generated === await readFile(output, "utf-8").catch(() => null) ? "up-to-date" : "outdated"
170
+ };
171
+ }
172
+ async function runI18n(config, check) {
173
+ const { locales, output } = config.codegen.i18n;
174
+ const cli = resolveFromProject(config.root, "@wolfstar/i18next-type-generator");
175
+ if (!cli) throw cliDiagnostics.DEPENDENCY_MISSING({
176
+ name: "@wolfstar/i18next-type-generator",
177
+ root: config.root,
178
+ hint: "Install it with `pnpm add -D @wolfstar/i18next-type-generator`, or set `codegen.i18n` to false."
179
+ });
180
+ if (!check) {
181
+ await generate(config.root, cli, locales, output);
182
+ return {
183
+ generator: "i18n",
184
+ output,
185
+ status: "written"
186
+ };
187
+ }
188
+ const directory = await mkdtemp(join(tmpdir(), "stars-codegen-"));
189
+ try {
190
+ const candidate = join(directory, "i18next.d.ts");
191
+ await generate(config.root, cli, locales, candidate);
192
+ const [expected, actual] = await Promise.all([readFile(candidate, "utf-8"), readFile(output, "utf-8").catch(() => null)]);
193
+ return {
194
+ generator: "i18n",
195
+ output,
196
+ status: expected === actual ? "up-to-date" : "outdated"
197
+ };
198
+ } finally {
199
+ await rm(directory, {
200
+ recursive: true,
201
+ force: true
202
+ });
203
+ }
204
+ }
205
+ function generate(root, cli, locales, output) {
206
+ return new Promise((resolve, reject) => {
207
+ const child = spawn(process.execPath, [
208
+ cli,
209
+ locales,
210
+ output
211
+ ], {
212
+ cwd: root,
213
+ stdio: [
214
+ "ignore",
215
+ "ignore",
216
+ "pipe"
217
+ ],
218
+ windowsHide: true
219
+ });
220
+ let stderr = "";
221
+ child.stderr?.on("data", (chunk) => stderr += chunk.toString());
222
+ child.once("error", reject);
223
+ child.once("exit", (code) => {
224
+ if (code === 0) resolve();
225
+ else reject(cliDiagnostics.CODEGEN_FAILED({
226
+ code,
227
+ stderr: stderr.trim()
228
+ }));
229
+ });
230
+ });
231
+ }
232
+ var codegen_default = defineCommand({
233
+ meta: {
234
+ name: "codegen",
235
+ description: "Run the configured code generators (i18next types, command options)"
236
+ },
237
+ args: {
238
+ ...projectArgs,
239
+ check: {
240
+ type: "boolean",
241
+ description: "Fail when the generated files are out of date instead of writing them",
242
+ default: false
243
+ },
244
+ json: {
245
+ type: "boolean",
246
+ description: "Print machine-readable JSON",
247
+ default: false
248
+ }
249
+ },
250
+ async run({ args }) {
251
+ await runCodegen({
252
+ config: args.config,
253
+ cwd: args.cwd,
254
+ check: args.check,
255
+ json: args.json
256
+ });
257
+ }
258
+ });
259
+
260
+ //#endregion
261
+ export { codegen_default as default };
262
+ //# sourceMappingURL=codegen-CF6o8v6A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codegen-CF6o8v6A.js","names":[],"sources":["../src/utils/command-types.ts","../src/commands/codegen.ts"],"sourcesContent":["import type { CommandData, CommandSnapshot } from '../dev/bridge.js';\n\n/** `ApplicationCommandOptionType`, which the CLI cannot import: it has no dependency on `discord-api-types`. */\nconst OptionType = {\n\tSubcommand: 1,\n\tSubcommandGroup: 2,\n\tString: 3,\n\tInteger: 4,\n\tBoolean: 5,\n\tUser: 6,\n\tChannel: 7,\n\tRole: 8,\n\tMentionable: 9,\n\tNumber: 10,\n\tAttachment: 11\n} as const;\n\n/** `ApplicationCommandType.ChatInput`: the only type with options, and the one a command without `type` is. */\nconst CHAT_INPUT = 1;\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/;\n\ninterface OptionData {\n\tname: string;\n\ttype: number;\n\trequired?: boolean;\n\tchoices?: { value: string | number }[];\n\tchannel_types?: number[];\n\toptions?: OptionData[];\n}\n\ninterface Rendering {\n\t/** Set once something refers to `TransformedArguments`, which the file then has to import. */\n\tusesResolvedTypes: boolean;\n}\n\n/**\n * The declaration file `stars codegen` writes for the commands a project registers: one `CommandOptionsRegistry` entry\n * (`Command.OptionsOf<'path'>`) per leaf, that is a command without subcommands, `parent sub` or `parent group sub`.\n *\n * It reads the JSON Discord receives rather than the builders, so what the handler is typed with cannot drift from what\n * is registered, and loops and factories need no special care. The option types point at the framework's\n * `TransformedArguments`, which is what the router resolves them to, instead of repeating those shapes here.\n *\n * Context menu commands have no options and are skipped. A path registered more than once (a command restricted to\n * several guilds is the same command in each) keeps its first definition, global commands first. The output depends\n * on nothing but the commands, in sorted order, so `stars codegen --check` is a string comparison.\n */\nexport function generateCommandTypes(snapshot: CommandSnapshot): string {\n\tconst entries = new Map<string, OptionData[]>();\n\tconst scopes = [\n\t\tsnapshot.global,\n\t\t...Object.keys(snapshot.guilds)\n\t\t\t.sort()\n\t\t\t.map((guild) => snapshot.guilds[guild])\n\t];\n\tfor (const commands of scopes) {\n\t\tfor (const command of commands) collectLeaves(command, entries);\n\t}\n\n\tconst rendering: Rendering = { usesResolvedTypes: false };\n\tconst lines = [...entries.keys()].sort().map((path) => ` ${renderEntry(path, entries.get(path)!, rendering)}`);\n\n\treturn [\n\t\t'// This file is automatically generated by `stars codegen`, do not edit it.',\n\t\t'/* eslint-disable */',\n\t\t'/* oxlint-disable */',\n\t\t'// oxfmt-ignore',\n\t\t'// prettier-ignore',\n\t\trendering.usesResolvedTypes ? \"import type { TransformedArguments } from '@wolfstar/http-framework';\" : 'export {};',\n\t\t'',\n\t\t'// oxfmt-ignore',\n\t\t'// prettier-ignore',\n\t\t\"declare module '@wolfstar/http-framework' {\",\n\t\t' interface CommandOptionsRegistry {',\n\t\t...lines,\n\t\t' }',\n\t\t'}',\n\t\t''\n\t].join('\\n');\n}\n\nfunction collectLeaves(command: CommandData, entries: Map<string, OptionData[]>): void {\n\tif ((command.type ?? CHAT_INPUT) !== CHAT_INPUT) return;\n\n\tconst options = (command.options ?? []) as OptionData[];\n\tconst add = (path: string, leaf: OptionData[] | undefined) => {\n\t\tif (!entries.has(path)) entries.set(path, leaf ?? []);\n\t};\n\n\tif (!options.some((option) => option.type === OptionType.Subcommand || option.type === OptionType.SubcommandGroup)) {\n\t\tadd(command.name, options);\n\t\treturn;\n\t}\n\n\tfor (const option of options) {\n\t\tif (option.type === OptionType.Subcommand) {\n\t\t\tadd(`${command.name} ${option.name}`, option.options);\n\t\t} else if (option.type === OptionType.SubcommandGroup) {\n\t\t\tfor (const sub of option.options ?? []) {\n\t\t\t\tif (sub.type === OptionType.Subcommand) add(`${command.name} ${option.name} ${sub.name}`, sub.options);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Only the first line of an entry is indented by the caller: the options go on lines of their own. */\nfunction renderEntry(path: string, options: readonly OptionData[], rendering: Rendering): string {\n\tconst sorted = [...options].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n\tconst properties = sorted.map((option) => ` ${key(option.name)}${option.required ? '' : '?'}: ${renderOption(option, rendering)};`);\n\treturn properties.length === 0 ? `${quote(path)}: {};` : `${quote(path)}: {\\n${properties.join('\\n')}\\n };`;\n}\n\nfunction renderOption(option: OptionData, rendering: Rendering): string {\n\tconst resolved = (name: string) => {\n\t\trendering.usesResolvedTypes = true;\n\t\treturn `TransformedArguments.${name}`;\n\t};\n\n\tswitch (option.type) {\n\t\tcase OptionType.String:\n\t\tcase OptionType.Integer:\n\t\tcase OptionType.Number: {\n\t\t\tconst fallback = option.type === OptionType.String ? 'string' : 'number';\n\t\t\tif (!option.choices?.length) return fallback;\n\t\t\tconst values = option.choices.map((choice) => (typeof choice.value === 'string' ? quote(choice.value) : String(choice.value)));\n\t\t\treturn [...new Set(values)].join(' | ');\n\t\t}\n\t\tcase OptionType.Boolean:\n\t\t\treturn 'boolean';\n\t\tcase OptionType.User:\n\t\t\treturn resolved('User');\n\t\tcase OptionType.Role:\n\t\t\treturn resolved('Role');\n\t\tcase OptionType.Mentionable:\n\t\t\treturn resolved('Mentionable');\n\t\tcase OptionType.Attachment:\n\t\t\treturn resolved('Attachment');\n\t\tcase OptionType.Channel: {\n\t\t\tconst channel = resolved('Channel');\n\t\t\tif (!option.channel_types?.length) return channel;\n\t\t\t// `ChannelType` is not imported on purpose: its members are the numbers they are in the payload.\n\t\t\tconst types = [...new Set(option.channel_types)].sort((a, b) => a - b).join(' | ');\n\t\t\treturn `${channel} & { type: Extract<${channel}['type'], ${types}> }`;\n\t\t}\n\t\tdefault:\n\t\t\t// An option type Discord added after this was written: the handler still gets something.\n\t\t\treturn 'unknown';\n\t}\n}\n\nfunction key(name: string): string {\n\treturn IDENTIFIER.test(name) ? name : quote(name);\n}\n\nfunction quote(value: string): string {\n\tconst escaped = value\n\t\t.replaceAll('\\\\', '\\\\\\\\')\n\t\t.replaceAll(\"'\", \"\\\\'\")\n\t\t.replaceAll('\\n', '\\\\n')\n\t\t.replaceAll('\\r', '\\\\r')\n\t\t.replaceAll('
', '\\\\u2028')\n\t\t.replaceAll('
', '\\\\u2029');\n\treturn `'${escaped}'`;\n}\n","import { displayPath, type ResolvedStarsConfig } from '@wolfstar/schema';\nimport { applyEnvOptions, loadProject, withProjectEnv } from '../utils/hooks.js';\nimport { defineCommand } from 'citty';\nimport { createColors } from 'colorette';\nimport { spawn } from 'node:child_process';\nimport { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { projectArgs, resolveCwd, type ProjectArgs } from '../utils/args.js';\nimport type { CommandSnapshot } from '../dev/bridge.js';\nimport { generateCommandTypes } from '../utils/command-types.js';\nimport { cliDiagnostics } from '../utils/diagnostics.js';\nimport { shouldUseColor } from '../utils/output-mode.js';\nimport { readLocalCommands } from '../utils/local-commands.js';\nimport { resolveFromProject } from '../utils/project.js';\n\nexport interface CodegenTaskOptions extends ProjectArgs {\n\tcheck?: boolean;\n\tjson?: boolean;\n\tstdout?: NodeJS.WritableStream;\n\t/** Overrides the commands read from the built bot, for tests. */\n\tcommands?: CommandSnapshot;\n}\n\nexport interface CodegenResult {\n\tgenerator: 'i18n' | 'commands';\n\toutput: string;\n\tstatus: 'written' | 'up-to-date' | 'outdated';\n}\n\nexport async function runCodegen(options: CodegenTaskOptions): Promise<void> {\n\tconst stdout = options.stdout ?? process.stdout;\n\tconst colors = createColors({ useColor: shouldUseColor() && !options.json });\n\tconst project = await loadProject({ cwd: resolveCwd(options), configFile: options.config });\n\tconst { config } = project;\n\tconst results: CodegenResult[] = [];\n\n\tif (config.codegen.i18n) results.push(await runI18n(config, Boolean(options.check)));\n\tif (config.codegen.commands) {\n\t\t// What the bot is started with: `env:options` (and varlock) may pick other env files than the defaults.\n\t\tconst botConfig = withProjectEnv(await applyEnvOptions(config, project.hooks));\n\t\tresults.push(await runCommands(botConfig, Boolean(options.check), options.commands));\n\t}\n\n\tif (options.json) {\n\t\tstdout.write(`${JSON.stringify({ check: Boolean(options.check), results }, null, 2)}\\n`);\n\t} else if (results.length === 0) {\n\t\tstdout.write(`${colors.dim('stars')} nothing to generate (no code generators are configured)\\n`);\n\t} else {\n\t\tfor (const result of results) {\n\t\t\tconst paint = result.status === 'outdated' ? colors.red : colors.green;\n\t\t\tstdout.write(`${colors.dim('stars')} ${result.generator}: ${paint(result.status)} ${displayPath(config.root, result.output)}\\n`);\n\t\t}\n\t}\n\n\tif (results.some((result) => result.status === 'outdated')) {\n\t\tthrow cliDiagnostics.CODEGEN_OUTDATED({});\n\t}\n}\n\n/**\n * Types the options of the commands the project registers. The commands are read from the built bot, the way\n * `stars commands diff` reads them: nothing connects to Discord, but the bot still runs its own env setup, so `--check`\n * in CI needs placeholders for the values it reads while starting (`DISCORD_CLIENT_ID`, `DISCORD_PUBLIC_KEY`, ...).\n */\nasync function runCommands(config: ResolvedStarsConfig, check: boolean, local?: CommandSnapshot): Promise<CodegenResult> {\n\tconst { output } = config.codegen.commands!;\n\tconst generated = generateCommandTypes(local ?? (await readLocalCommands(config)));\n\n\tif (!check) {\n\t\tawait mkdir(dirname(output), { recursive: true });\n\t\tawait writeFile(output, generated);\n\t\treturn { generator: 'commands', output, status: 'written' };\n\t}\n\n\tconst actual = await readFile(output, 'utf-8').catch(() => null);\n\treturn { generator: 'commands', output, status: generated === actual ? 'up-to-date' : 'outdated' };\n}\n\nasync function runI18n(config: ResolvedStarsConfig, check: boolean): Promise<CodegenResult> {\n\tconst { locales, output } = config.codegen.i18n!;\n\tconst cli = resolveFromProject(config.root, '@wolfstar/i18next-type-generator');\n\tif (!cli) {\n\t\tthrow cliDiagnostics.DEPENDENCY_MISSING({\n\t\t\tname: '@wolfstar/i18next-type-generator',\n\t\t\troot: config.root,\n\t\t\thint: 'Install it with `pnpm add -D @wolfstar/i18next-type-generator`, or set `codegen.i18n` to false.'\n\t\t});\n\t}\n\n\tif (!check) {\n\t\tawait generate(config.root, cli, locales, output);\n\t\treturn { generator: 'i18n', output, status: 'written' };\n\t}\n\n\tconst directory = await mkdtemp(join(tmpdir(), 'stars-codegen-'));\n\ttry {\n\t\tconst candidate = join(directory, 'i18next.d.ts');\n\t\tawait generate(config.root, cli, locales, candidate);\n\t\tconst [expected, actual] = await Promise.all([readFile(candidate, 'utf-8'), readFile(output, 'utf-8').catch(() => null)]);\n\t\treturn { generator: 'i18n', output, status: expected === actual ? 'up-to-date' : 'outdated' };\n\t} finally {\n\t\tawait rm(directory, { recursive: true, force: true });\n\t}\n}\n\nfunction generate(root: string, cli: string, locales: string, output: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(process.execPath, [cli, locales, output], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });\n\t\tlet stderr = '';\n\t\tchild.stderr?.on('data', (chunk: Buffer) => (stderr += chunk.toString()));\n\t\tchild.once('error', reject);\n\t\tchild.once('exit', (code) => {\n\t\t\tif (code === 0) resolve();\n\t\t\telse reject(cliDiagnostics.CODEGEN_FAILED({ code, stderr: stderr.trim() }));\n\t\t});\n\t});\n}\n\nexport default defineCommand({\n\tmeta: {\n\t\tname: 'codegen',\n\t\tdescription: 'Run the configured code generators (i18next types, command options)'\n\t},\n\targs: {\n\t\t...projectArgs,\n\t\tcheck: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Fail when the generated files are out of date instead of writing them',\n\t\t\tdefault: false\n\t\t},\n\t\tjson: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Print machine-readable JSON',\n\t\t\tdefault: false\n\t\t}\n\t},\n\tasync run({ args }) {\n\t\tawait runCodegen({ config: args.config, cwd: args.cwd, check: args.check, json: args.json });\n\t}\n});\n"],"mappings":"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"}