@wolfstar/cli 2.4.0-next-20261006112342 → 2.6.0-next-20261007120201

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 (70) hide show
  1. package/README.md +40 -12
  2. package/dist/{_shared-BIQK0Bhm.js → _shared-CyzTojMA.js} +95 -20
  3. package/dist/_shared-CyzTojMA.js.map +1 -0
  4. package/dist/{bridge-B5BP6eXj.js → bridge-Cbm0jX9h.js} +2 -2
  5. package/dist/{bridge-B5BP6eXj.js.map → bridge-Cbm0jX9h.js.map} +1 -1
  6. package/dist/{build-DJcc95Yq.js → build-BbIEbOu3.js} +5 -5
  7. package/dist/{build-DJcc95Yq.js.map → build-BbIEbOu3.js.map} +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/{codegen-CF6o8v6A.js → codegen-C2PY7RXA.js} +5 -5
  10. package/dist/{codegen-CF6o8v6A.js.map → codegen-C2PY7RXA.js.map} +1 -1
  11. package/dist/commands-3Tgwy6cj.js +16 -0
  12. package/dist/commands-3Tgwy6cj.js.map +1 -0
  13. package/dist/commands-3qXbxhXx.js +3 -0
  14. package/dist/{commands-nGd6jExv.js → commands-C-i3qYOz.js} +5 -5
  15. package/dist/{commands-nGd6jExv.js.map → commands-C-i3qYOz.js.map} +1 -1
  16. package/dist/{completions-CBQHiSnE.js → completions-Cs6YbuXO.js} +3 -3
  17. package/dist/{completions-CBQHiSnE.js.map → completions-Cs6YbuXO.js.map} +1 -1
  18. package/dist/{dev-D9MAtiCk.js → dev-9YvzhtkU.js} +22 -216
  19. package/dist/dev-9YvzhtkU.js.map +1 -0
  20. package/dist/{diagnostics-BysbNnPD.js → diagnostics-Dn3CXa8J.js} +11 -2
  21. package/dist/diagnostics-Dn3CXa8J.js.map +1 -0
  22. package/dist/{doctor-DYAdmZZq.js → doctor-CMGfbXS5.js} +15 -8
  23. package/dist/doctor-CMGfbXS5.js.map +1 -0
  24. package/dist/{hooks-Cy6F2bhH.js → hooks-D_RVS0ba.js} +13 -5
  25. package/dist/hooks-D_RVS0ba.js.map +1 -0
  26. package/dist/index.d.ts +21 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/{info-CTUGxaHN.js → info-eXsx1AfE.js} +4 -3
  30. package/dist/info-eXsx1AfE.js.map +1 -0
  31. package/dist/{local-commands-DG_a4XE9.js → local-commands-CGBjc8KJ.js} +4 -4
  32. package/dist/{local-commands-DG_a4XE9.js.map → local-commands-CGBjc8KJ.js.map} +1 -1
  33. package/dist/{locales-BJAP4HYf.js → locales-BzyEMQnX.js} +5 -5
  34. package/dist/{locales-BJAP4HYf.js.map → locales-BzyEMQnX.js.map} +1 -1
  35. package/dist/{nitro-B8u1iVgf.js → nitro-DufOgx8s.js} +2 -2
  36. package/dist/{nitro-B8u1iVgf.js.map → nitro-DufOgx8s.js.map} +1 -1
  37. package/dist/{prepare-BM93i3Rm.js → prepare-DKH0UIYA.js} +12 -8
  38. package/dist/prepare-DKH0UIYA.js.map +1 -0
  39. package/dist/{project-zlQStwsf.js → project-DBqQMFCk.js} +14 -3
  40. package/dist/project-DBqQMFCk.js.map +1 -0
  41. package/dist/{run-CjI3xBby.js → run-DlNY1QM1.js} +2 -2
  42. package/dist/{run-CjI3xBby.js.map → run-DlNY1QM1.js.map} +1 -1
  43. package/dist/tsc-9yHyx0L6.js +3 -0
  44. package/dist/{tsc-puVAcwmo.js → tsc-BArammxe.js} +3 -3
  45. package/dist/{tsc-puVAcwmo.js.map → tsc-BArammxe.js.map} +1 -1
  46. package/dist/{tsdown-Dwt3JhPw.js → tsdown-2i5h88vT.js} +2 -2
  47. package/dist/{tsdown-Dwt3JhPw.js.map → tsdown-2i5h88vT.js.map} +1 -1
  48. package/dist/{tui-nldGSD4c.js → tui-CB_qk2Z1.js} +2 -2
  49. package/dist/{tui-nldGSD4c.js.map → tui-CB_qk2Z1.js.map} +1 -1
  50. package/dist/{tunnel-C3bk4dq_.js → tunnel-BNV5cuJz.js} +2 -2
  51. package/dist/{tunnel-C3bk4dq_.js.map → tunnel-BNV5cuJz.js.map} +1 -1
  52. package/dist/typecheck-p_2rj9lL.js +66 -0
  53. package/dist/typecheck-p_2rj9lL.js.map +1 -0
  54. package/dist/typechecker-C3UOI2LF.js +213 -0
  55. package/dist/typechecker-C3UOI2LF.js.map +1 -0
  56. package/dist/{vite-YZUtv2Hk.js → vite-C5CN3xUu.js} +2 -2
  57. package/dist/{vite-YZUtv2Hk.js.map → vite-C5CN3xUu.js.map} +1 -1
  58. package/package.json +9 -9
  59. package/dist/_shared-BIQK0Bhm.js.map +0 -1
  60. package/dist/commands-C_B9ENxc.js +0 -15
  61. package/dist/commands-C_B9ENxc.js.map +0 -1
  62. package/dist/commands-DY-hEtQy.js +0 -3
  63. package/dist/dev-D9MAtiCk.js.map +0 -1
  64. package/dist/diagnostics-BysbNnPD.js.map +0 -1
  65. package/dist/doctor-DYAdmZZq.js.map +0 -1
  66. package/dist/hooks-Cy6F2bhH.js.map +0 -1
  67. package/dist/info-CTUGxaHN.js.map +0 -1
  68. package/dist/prepare-BM93i3Rm.js.map +0 -1
  69. package/dist/project-zlQStwsf.js.map +0 -1
  70. package/dist/tsc-C5l5TQGC.js +0 -3
package/README.md CHANGED
@@ -25,7 +25,8 @@ package has no install-time dependency on `@wolfstar/http-framework` in return,
25
25
  - `stars build` runs the configured build tool once.
26
26
  - `stars info` prints the resolved configuration and environment (`--json` for scripts).
27
27
  - `stars codegen` runs the configured code generators: i18next types and the typed options of your commands (`--check` for CI).
28
- - `stars prepare` generates `.stars/tsconfig.json` and the auto imports declaration file (`--check` for CI).
28
+ - `stars prepare` generates the TypeScript configuration in `.stars/` (`tsconfig.app.json` and `tsconfig.node.json`) and the auto imports declaration file (`--check` for CI).
29
+ - `stars typecheck` regenerates `.stars/` and type-checks every project once, the way `nuxt typecheck` does.
29
30
  - `stars commands` inspects, compares, deploys and cleans the application commands Discord has deployed.
30
31
  - `stars doctor` checks that the project is ready: runtime, framework, credentials, interactions endpoint, generated files.
31
32
  - `stars completions` prints the shell completion script for bash, zsh or fish.
@@ -66,14 +67,17 @@ const config = await loadStarsConfig({ cwd: process.cwd() });
66
67
  console.log(config.entry, config.build.output);
67
68
  ```
68
69
 
70
+ `loadStarsConfig` only validates what the schema owns. A top-level key that is not built in is kept as `config.moduleOptions` (it may be the options of a module, `meta.configKey`), not rejected, so a misspelled key is reported by the `stars` commands once the modules are set up, not by `loadStarsConfig`. A tool that loads the configuration on its own and wants that check calls `assertModuleOptionsClaimed(config, claimedKeys)` from `@wolfstar/schema` (also exported by `@wolfstar/http-framework/config`) with the `meta.configKey`s of the modules it installed, or `[]` when it installs none.
71
+
69
72
  ## Commands
70
73
 
71
74
  ```sh
72
- stars dev [--no-tui] [--layout <auto|dashboard|panel>] [--channel <name>] [--level <level>] [--theme <name>] [--config <file>] [--cwd <dir>]
75
+ stars dev [--no-tui] [--tunnel|--no-tunnel] [--layout <auto|dashboard|panel>] [--channel <name>] [--level <level>] [--theme <name>] [--config <file>] [--cwd <dir>]
73
76
  stars build [--config <file>] [--cwd <dir>]
74
77
  stars info [--json] [--config <file>] [--cwd <dir>]
75
78
  stars codegen [--check] [--json] [--config <file>] [--cwd <dir>]
76
79
  stars prepare [--check] [--json] [--config <file>] [--cwd <dir>]
80
+ stars typecheck [--config <file>] [--cwd <dir>]
77
81
  stars commands [list|clean|diff|deploy] [--guild <id>] [--name <name>] [--check] [--yes] [--json]
78
82
  stars doctor [--online] [--json] [--config <file>] [--cwd <dir>]
79
83
  stars completions <bash|zsh|fish>
@@ -333,7 +337,7 @@ stars doctor v2.3.0
333
337
  → Stop it, or set another HTTP_PORT, unless it is this bot running.
334
338
  ℹ tunnel No tunnel: Discord cannot reach a bot on localhost
335
339
  → Set `dev.tunnel: true` for a cloudflared quick tunnel, or press t in `stars dev`.
336
- ⚠ prepare Out of date: .stars/tsconfig.json
340
+ ⚠ prepare Out of date: .stars/tsconfig.app.json, .stars/tsconfig.node.json
337
341
  → Run `stars prepare`.
338
342
 
339
343
  1 error(s), 2 warning(s)
@@ -367,7 +371,8 @@ Three `dev` options round out the dev loop (all documented in
367
371
  mode), `tsz` (the tsc-compatible checker, re-run after every build since it has no watch mode), or `auto` — the
368
372
  default, which uses `golar` when the project depends on it and `tsc` otherwise.
369
373
  - Pressing `t` opens and closes a `cloudflared` quick tunnel without configuration. `dev.tunnel: true` opens it at startup so Discord can reach the bot's interactions endpoint from the
370
- internet; a string is an https URL you already serve, which the CLI only probes. `dev.tunnel.updateEndpoint` writes
374
+ internet, and so does `stars dev --tunnel` for one session, whatever `dev.tunnel` says (`--no-tunnel` keeps it closed at
375
+ start; `t` still opens it). With `--no-tui` the URL is printed on the `tunnel` channel; a string is an https URL you already serve, which the CLI only probes. `dev.tunnel.updateEndpoint` writes
371
376
  the URL to the Discord application, and is opt-in because it edits a live application.
372
377
  - `dev.logFile` (default `.stars/dev.log`) mirrors the session's logs to disk, so a run can be read back after the
373
378
  terminal UI is gone. Set it to `false` to disable it. It is truncated on every run; `dev.logs.dir` (for example
@@ -410,7 +415,8 @@ and which options the block sets.
410
415
  | ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
411
416
  | `3` (EOL) | Legacy behaviour: a `tsdown.config.*` drives the build, auto imports off unless asked for; prints `COMPATIBILITY_VERSION_EOL` |
412
417
  | `4` | `tsdown` configured from `stars.config` alone, auto imports on and wired in, `'auto'` picks `tsdown` for TypeScript |
413
- | `5` (default) | Everything in `4`, plus `env` registered automatically when the project depends on `@wolfstar/env-utilities` |
418
+ | `5` | Everything in `4`, plus `env` registered automatically when the project depends on `@wolfstar/env-utilities` |
419
+ | `6` (default) | Everything in `5`, plus the generated tsconfig split into `.stars/tsconfig.app.json` and `.stars/tsconfig.node.json` |
414
420
 
415
421
  ### Environment and hooks
416
422
 
@@ -477,7 +483,30 @@ package.json imports, and ESNext/DOM libraries. Use `import type` and `export ty
477
483
  These options apply to tsdown and Vite; tsc keeps its emit-compatible settings. Sapphire's decorator options and
478
484
  the ES2022 target remain in effect. This does not enable Stars' experimental Nitro runtime integration.
479
485
 
480
- Run `stars prepare` and extend the generated config from your project's `tsconfig.json`:
486
+ From compatibility version 6, `stars prepare` writes two files, the way a Nuxt 4 project has them:
487
+
488
+ - `.stars/tsconfig.app.json` is the bot: everything under the entry's directory plus the auto imports declaration,
489
+ with the `~`/`@`/`~~`/`@@` paths and the options above. `tsdown` builds with it.
490
+ - `.stars/tsconfig.node.json` is what Node runs without a bundler: `stars.config.*`, `vitest.config.*`,
491
+ `tsdown.config.*`, `vite.config.*` and `scripts/**`. It uses `NodeNext` resolution, `types: ["node"]`, no DOM
492
+ library, no aliases or auto imports and no decorators, and leaves the entry's directory to the app project.
493
+
494
+ Your own `tsconfig.json` only references them:
495
+
496
+ ```json
497
+ {
498
+ "files": [],
499
+ "references": [{ "path": "./.stars/tsconfig.app.json" }, { "path": "./.stars/tsconfig.node.json" }]
500
+ }
501
+ ```
502
+
503
+ `stars typecheck` regenerates `.stars/` and runs your checker (`dev.typecheck.checker`: `golar` or `tsc`; bare `tsc -p`
504
+ on that root would check nothing) once per project. `stars dev`'s `dev.typecheck` watches the app project. A root
505
+ `tsconfig.json` that still extends `./.stars/tsconfig.json` gets a `TSCONFIG_LEGACY_EXTENDS` warning from
506
+ `stars prepare` and `stars doctor`: update it once, or keep `future.compatibilityVersion` at `5`.
507
+
508
+ On `5` and `4` nothing changes: `stars prepare` writes the single `.stars/tsconfig.json`, which you extend from your
509
+ project's `tsconfig.json`:
481
510
 
482
511
  ```json
483
512
  {
@@ -485,18 +514,17 @@ Run `stars prepare` and extend the generated config from your project's `tsconfi
485
514
  }
486
515
  ```
487
516
 
488
- New tsdown projects already extend this file and run `stars prepare` through `postinstall`.
489
- Keep your existing compiler options alongside `extends`. `stars dev` and `stars build` also regenerate this file.
490
- For tsdown builds, `@/` and `~/` resolve to the entry file's directory (normally `src/`), while `@@/` and `~~/`
517
+ Keep your existing compiler options alongside `extends`. `stars dev` and `stars build` also regenerate the generated
518
+ files. For tsdown builds, `@/` and `~/` resolve to the entry file's directory (normally `src/`), while `@@/` and `~~/`
491
519
  resolve to the project root. Filesystem aliases in `stars.config.ts`'s `tsdown.alias` are included too, with custom
492
520
  values taking precedence. Legacy builds using a separate tsdown config only include aliases declared in
493
521
  `stars.config.ts`. Other build tools do not get tsdown aliases, since TypeScript alone does not rewrite imports.
494
522
 
495
- The generated config includes source files and the auto imports declaration, including a custom `imports.dts`
523
+ The generated app config includes source files and the auto imports declaration, including a custom `imports.dts`
496
524
  location. Explicit `include` or `compilerOptions.paths` in your own tsconfig replace the inherited values;
497
525
  remove manually duplicated paths to use the generated aliases. Generation works with `imports: false` too.
498
- Use `stars prepare --check` to check both generated files without writing them. Do not edit `.stars/tsconfig.json`
499
- by hand; keep `.stars/` ignored by Git and run `stars prepare` after installing dependencies on a fresh checkout.
526
+ Use `stars prepare --check` to check the generated files without writing them. Do not edit anything in `.stars/` by
527
+ hand; keep `.stars/` ignored by Git and run `stars prepare` after installing dependencies on a fresh checkout.
500
528
 
501
529
  ## Server integrations
502
530
 
@@ -1,20 +1,69 @@
1
1
  import { r as formatError } from "./errors-COFWKKKw.js";
2
+ import { t as cliDiagnostics } from "./diagnostics-Dn3CXa8J.js";
2
3
  import { t as loadAutoImportsModule } from "./framework-auto-imports-Gu_PXSyr.js";
3
- import { a as modulesPreloadWarning, o as prepareModulesPreload } from "./hooks-Cy6F2bhH.js";
4
+ import { a as modulesPreloadWarning, o as prepareModulesPreload } from "./hooks-D_RVS0ba.js";
4
5
  import { createRequire } from "node:module";
5
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
7
+ import { SPLIT_TSCONFIG_VERSION } from "@wolfstar/schema";
6
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
7
9
 
8
10
  //#region src/utils/tsconfig.ts
9
11
  const require = createRequire(import.meta.url);
10
- const sapphireOptions = Object.assign({}, ...[
12
+ const presetOptions = (ids) => Object.assign({}, ...ids.map((id) => require(id).compilerOptions));
13
+ const strictOptions = presetOptions(["@sapphire/ts-config", "@sapphire/ts-config/extra-strict"]);
14
+ const sapphireOptions = presetOptions([
11
15
  "@sapphire/ts-config",
12
16
  "@sapphire/ts-config/extra-strict",
13
17
  "@sapphire/ts-config/decorators"
14
- ].map((id) => require(id).compilerOptions));
15
- /** Generates an extendable config without changing the project's own tsconfig. */
18
+ ]);
19
+ /** The root config files that run in Node and never go through the bundler. */
20
+ const NODE_PROJECT_FILES = [
21
+ "stars.config.*",
22
+ "vitest.config.*",
23
+ "tsdown.config.*",
24
+ "vite.config.*",
25
+ "scripts/**/*"
26
+ ];
27
+ /**
28
+ * Generates the TypeScript configuration without changing the project's own tsconfig: one extendable
29
+ * `.stars/tsconfig.json` below compatibility version 6, and from 6 the `.stars/tsconfig.app.json` (the bot sources) and
30
+ * `.stars/tsconfig.node.json` (the root files that run in Node) a solution-style `tsconfig.json` references.
31
+ * Returns one status per file.
32
+ */
16
33
  async function prepareTsconfig(config, check = false) {
17
- const path = join(config.root, ".stars", "tsconfig.json");
34
+ if (config.future.compatibilityVersion < SPLIT_TSCONFIG_VERSION) return [await emit(config, "tsconfig", appTsconfig(config, "tsconfig"), check)];
35
+ return [await emit(config, "app", appTsconfig(config, "app"), check), await emit(config, "node", nodeTsconfig(config), check)];
36
+ }
37
+ /**
38
+ * Reports a root `tsconfig.json` that still extends the single `.stars/tsconfig.json` of compatibility versions below 6,
39
+ * a file `stars prepare` no longer writes from version 6 on. A project moving to 6 updates it once.
40
+ */
41
+ async function findLegacyRootTsconfig(config) {
42
+ if (config.future.compatibilityVersion < SPLIT_TSCONFIG_VERSION) return null;
43
+ const path = join(config.root, "tsconfig.json");
44
+ const content = await readFile(path, "utf-8").catch(() => null);
45
+ if (content === null || !/["']extends["']\s*:\s*(?:\[[^\]]*)?["']\.\/\.stars\/tsconfig\.json["']/.test(content)) return null;
46
+ return cliDiagnostics.TSCONFIG_LEGACY_EXTENDS({ file: relative(config.root, path) || "tsconfig.json" });
47
+ }
48
+ async function emit(config, name, compilerConfig, check) {
49
+ const path = join(config.root, ".stars", name === "tsconfig" ? "tsconfig.json" : `tsconfig.${name}.json`);
50
+ const content = `${JSON.stringify(compilerConfig, null, 2)}\n`;
51
+ if (check) return {
52
+ name,
53
+ path,
54
+ status: await readFile(path, "utf-8").catch(() => null) === content ? "up-to-date" : "outdated"
55
+ };
56
+ await mkdir(dirname(path), { recursive: true });
57
+ await writeFile(path, content);
58
+ return {
59
+ name,
60
+ path,
61
+ status: "written"
62
+ };
63
+ }
64
+ /** What `.stars/tsconfig.json` has always been: the bot sources, with the aliases and the bundler's compiler options. */
65
+ function appTsconfig(config, name) {
66
+ const path = join(config.root, ".stars", name === "tsconfig" ? "tsconfig.json" : `tsconfig.${name}.json`);
18
67
  const fromGenerated = (target) => `./${relative(dirname(path), target).replaceAll("\\", "/")}`;
19
68
  const paths = {};
20
69
  if (config.build.tool === "tsdown") {
@@ -48,7 +97,7 @@ async function prepareTsconfig(config, check = false) {
48
97
  paths[`${alias}/*`] = [`${resolved}/*`];
49
98
  }
50
99
  }
51
- const content = `${JSON.stringify({
100
+ return {
52
101
  compilerOptions: {
53
102
  ...sapphireOptions,
54
103
  ...config.build.tool === "tsdown" || config.build.tool === "vite" ? {
@@ -66,21 +115,44 @@ async function prepareTsconfig(config, check = false) {
66
115
  target: "ES2022",
67
116
  forceConsistentCasingInFileNames: true,
68
117
  skipLibCheck: true,
69
- tsBuildInfoFile: "./tsconfig.tsbuildinfo",
118
+ tsBuildInfoFile: name === "tsconfig" ? "./tsconfig.tsbuildinfo" : `./tsconfig.${name}.tsbuildinfo`,
70
119
  paths
71
120
  },
72
121
  include: [`${fromGenerated(dirname(config.entry))}/**/*`, ...config.imports.enabled ? [fromGenerated(config.imports.dts)] : []],
73
122
  exclude: [fromGenerated(join(config.root, "node_modules")), fromGenerated(config.build.outDir)]
74
- }, null, 2)}\n`;
75
- if (check) return {
76
- path,
77
- status: await readFile(path, "utf-8").catch(() => null) === content ? "up-to-date" : "outdated"
78
123
  };
79
- await mkdir(dirname(path), { recursive: true });
80
- await writeFile(path, content);
124
+ }
125
+ /**
126
+ * The root files that run in Node: `stars.config.*`, `vitest.config.*`, `scripts/**` and the like. No DOM, no
127
+ * bundler resolution, no aliases or auto imports (none of it applies to a file Node loads itself), and no decorators.
128
+ */
129
+ function nodeTsconfig(config) {
130
+ const path = join(config.root, ".stars", "tsconfig.node.json");
131
+ const fromGenerated = (target) => `./${relative(dirname(path), target).replaceAll("\\", "/")}`;
132
+ const source = dirname(config.entry);
133
+ const include = NODE_PROJECT_FILES.map((pattern) => fromGenerated(join(config.root, pattern)));
134
+ if (config.configFile !== null) include.push(fromGenerated(config.configFile));
81
135
  return {
82
- path,
83
- status: "written"
136
+ compilerOptions: {
137
+ ...strictOptions,
138
+ target: "ES2022",
139
+ module: "NodeNext",
140
+ moduleResolution: "NodeNext",
141
+ moduleDetection: "force",
142
+ lib: ["ESNext"],
143
+ types: ["node"],
144
+ allowJs: true,
145
+ noEmit: true,
146
+ forceConsistentCasingInFileNames: true,
147
+ skipLibCheck: true,
148
+ tsBuildInfoFile: "./tsconfig.node.tsbuildinfo"
149
+ },
150
+ include: [...new Set(include)],
151
+ exclude: [
152
+ fromGenerated(join(config.root, "node_modules")),
153
+ fromGenerated(config.build.outDir),
154
+ ...source === config.root ? [] : [fromGenerated(source)]
155
+ ]
84
156
  };
85
157
  }
86
158
 
@@ -120,10 +192,11 @@ async function prepareAutoImports(config, check = false) {
120
192
  /** Prepares TypeScript configuration and auto import declarations before building. */
121
193
  async function prepareProject(config, hooks, check = false) {
122
194
  await hooks?.callHook("prepare:before", config);
123
- const tsconfig = await prepareTsconfig(config, check);
195
+ const tsconfigs = await prepareTsconfig(config, check);
124
196
  const result = {
125
197
  ...await prepareAutoImports(config, check),
126
- tsconfig,
198
+ tsconfig: tsconfigs[0],
199
+ tsconfigs,
127
200
  modules: await prepareModulesPreload(config, check)
128
201
  };
129
202
  await hooks?.callHook("prepare:done", config, {
@@ -133,15 +206,17 @@ async function prepareProject(config, hooks, check = false) {
133
206
  return result;
134
207
  }
135
208
  /**
136
- * Prints the non-fatal configuration diagnostics (e.g. an end-of-life compatibility version), and, unless `production`
209
+ * Prints the non-fatal configuration diagnostics (e.g. an end-of-life compatibility version, or a root `tsconfig.json` still extending `.stars/tsconfig.json`), and, unless `production`
137
210
  * is `false` (`stars dev`, which does that itself), what a production start of the project still needs to do.
138
211
  */
139
212
  async function reportWarnings(config, write, options = {}) {
140
213
  for (const warning of config.warnings) write(await formatError(warning));
214
+ const legacyTsconfig = await findLegacyRootTsconfig(config);
215
+ if (legacyTsconfig) write(await formatError(legacyTsconfig));
141
216
  const preload = options.production === false ? null : modulesPreloadWarning(config);
142
217
  if (preload) write(await formatError(preload));
143
218
  }
144
219
 
145
220
  //#endregion
146
- export { reportWarnings as n, prepareProject as t };
147
- //# sourceMappingURL=_shared-BIQK0Bhm.js.map
221
+ export { reportWarnings as n, findLegacyRootTsconfig as r, prepareProject as t };
222
+ //# sourceMappingURL=_shared-CyzTojMA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_shared-CyzTojMA.js","names":[],"sources":["../src/utils/tsconfig.ts","../src/commands/_shared.ts"],"sourcesContent":["import { SPLIT_TSCONFIG_VERSION, type ResolvedStarsConfig } from '@wolfstar/schema';\nimport { createRequire } from 'node:module';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path';\nimport type { Diagnostic } from 'nostics';\nimport { cliDiagnostics } from './diagnostics.js';\n\nconst require = createRequire(import.meta.url);\nconst presetOptions = (ids: string[]) =>\n\tObject.assign({}, ...ids.map((id) => (require(id) as { compilerOptions: Record<string, unknown> }).compilerOptions)) as Record<string, unknown>;\n// Strictness belongs to every project; the decorator options only to the code the bundler compiles.\nconst strictOptions = presetOptions(['@sapphire/ts-config', '@sapphire/ts-config/extra-strict']);\nconst sapphireOptions = presetOptions(['@sapphire/ts-config', '@sapphire/ts-config/extra-strict', '@sapphire/ts-config/decorators']);\n\nexport type TsconfigName = 'tsconfig' | 'app' | 'node';\n\nexport interface TsconfigStatus {\n\t/** `tsconfig` is the single file of compatibility versions below 6, `app` and `node` the split ones. */\n\tname: TsconfigName;\n\tpath: string;\n\tstatus: 'written' | 'up-to-date' | 'outdated';\n}\n\n/** The root config files that run in Node and never go through the bundler. */\nconst NODE_PROJECT_FILES = ['stars.config.*', 'vitest.config.*', 'tsdown.config.*', 'vite.config.*', 'scripts/**/*'] as const;\n\n/**\n * Generates the TypeScript configuration without changing the project's own tsconfig: one extendable\n * `.stars/tsconfig.json` below compatibility version 6, and from 6 the `.stars/tsconfig.app.json` (the bot sources) and\n * `.stars/tsconfig.node.json` (the root files that run in Node) a solution-style `tsconfig.json` references.\n * Returns one status per file.\n */\nexport async function prepareTsconfig(config: ResolvedStarsConfig, check = false): Promise<TsconfigStatus[]> {\n\tif (config.future.compatibilityVersion < SPLIT_TSCONFIG_VERSION) {\n\t\treturn [await emit(config, 'tsconfig', appTsconfig(config, 'tsconfig'), check)];\n\t}\n\n\t// Written for every build tool: the node project is about files Node runs whatever builds the bot. A `tsc` or `none`\n\t// project keeps its own tsconfig for the bot sources, so for it the app file is generated but nothing references it.\n\treturn [await emit(config, 'app', appTsconfig(config, 'app'), check), await emit(config, 'node', nodeTsconfig(config), check)];\n}\n\n/**\n * Reports a root `tsconfig.json` that still extends the single `.stars/tsconfig.json` of compatibility versions below 6,\n * a file `stars prepare` no longer writes from version 6 on. A project moving to 6 updates it once.\n */\nexport async function findLegacyRootTsconfig(config: ResolvedStarsConfig): Promise<Diagnostic | null> {\n\tif (config.future.compatibilityVersion < SPLIT_TSCONFIG_VERSION) return null;\n\n\tconst path = join(config.root, 'tsconfig.json');\n\tconst content = await readFile(path, 'utf-8').catch(() => null);\n\tif (content === null || !/[\"']extends[\"']\\s*:\\s*(?:\\[[^\\]]*)?[\"']\\.\\/\\.stars\\/tsconfig\\.json[\"']/.test(content)) return null;\n\n\treturn cliDiagnostics.TSCONFIG_LEGACY_EXTENDS({ file: relative(config.root, path) || 'tsconfig.json' });\n}\n\nasync function emit(config: ResolvedStarsConfig, name: TsconfigName, compilerConfig: object, check: boolean): Promise<TsconfigStatus> {\n\tconst path = join(config.root, '.stars', name === 'tsconfig' ? 'tsconfig.json' : `tsconfig.${name}.json`);\n\tconst content = `${JSON.stringify(compilerConfig, null, 2)}\\n`;\n\tif (check) {\n\t\tconst existing = await readFile(path, 'utf-8').catch(() => null);\n\t\treturn { name, path, status: existing === content ? 'up-to-date' : 'outdated' };\n\t}\n\tawait mkdir(dirname(path), { recursive: true });\n\tawait writeFile(path, content);\n\treturn { name, path, status: 'written' };\n}\n\n/** What `.stars/tsconfig.json` has always been: the bot sources, with the aliases and the bundler's compiler options. */\nfunction appTsconfig(config: ResolvedStarsConfig, name: TsconfigName) {\n\tconst path = join(config.root, '.stars', name === 'tsconfig' ? 'tsconfig.json' : `tsconfig.${name}.json`);\n\tconst fromGenerated = (target: string) => `./${relative(dirname(path), target).replaceAll('\\\\', '/')}`;\n\tconst paths: Record<string, string[]> = {};\n\tif (config.build.tool === 'tsdown') {\n\t\tconst source = dirname(config.entry);\n\t\tconst defaults = config.build.configFile === null ? { '~': source, '@': source, '~~': config.root, '@@': config.root } : {};\n\t\tconst aliases = { ...defaults, ...(config.tsdown.alias as Record<string, unknown> | undefined) };\n\t\tfor (const [alias, target] of Object.entries(aliases)) {\n\t\t\t// Module redirects belong to the bundler; only filesystem targets become TypeScript paths.\n\t\t\tif (typeof target !== 'string' || (!isAbsolute(target) && !/^\\.\\.?[/\\\\]/.test(target))) continue;\n\t\t\tconst resolved = fromGenerated(resolve(config.root, target));\n\t\t\tpaths[alias] = [resolved];\n\t\t\tpaths[`${alias}/*`] = [`${resolved}/*`];\n\t\t}\n\t} else if (config.experimental.enableNitro) {\n\t\t// `NitroBuilder` turns on Vite's native `resolve.tsconfigPaths` (see https://nitro.build/examples/import-alias),\n\t\t// which reads these `paths` straight out of the generated tsconfig — no `tsdown.alias` equivalent to merge\n\t\t// with, `vite`/Nitro projects only have this file's own `compilerOptions.paths` to extend or replace.\n\t\tconst source = dirname(config.entry);\n\t\tconst defaults = { '~': source, '@': source, '~~': config.root, '@@': config.root };\n\t\tfor (const [alias, target] of Object.entries(defaults)) {\n\t\t\tconst resolved = fromGenerated(resolve(config.root, target));\n\t\t\tpaths[alias] = [resolved];\n\t\t\tpaths[`${alias}/*`] = [`${resolved}/*`];\n\t\t}\n\t}\n\treturn {\n\t\tcompilerOptions: {\n\t\t\t...sapphireOptions,\n\t\t\t// Bundlers emit the application; TypeScript only checks it. Keep Node16 emit for tsc.\n\t\t\t...(config.build.tool === 'tsdown' || config.build.tool === 'vite'\n\t\t\t\t? {\n\t\t\t\t\t\tmodule: 'ESNext',\n\t\t\t\t\t\tmoduleResolution: 'Bundler',\n\t\t\t\t\t\tmoduleDetection: 'force',\n\t\t\t\t\t\tisolatedModules: true,\n\t\t\t\t\t\tverbatimModuleSyntax: true,\n\t\t\t\t\t\tallowJs: true,\n\t\t\t\t\t\tallowImportingTsExtensions: true,\n\t\t\t\t\t\tresolvePackageJsonImports: true,\n\t\t\t\t\t\tlib: ['ESNext', 'DOM'],\n\t\t\t\t\t\tnoEmit: true\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\ttarget: 'ES2022',\n\t\t\tforceConsistentCasingInFileNames: true,\n\t\t\tskipLibCheck: true,\n\t\t\ttsBuildInfoFile: name === 'tsconfig' ? './tsconfig.tsbuildinfo' : `./tsconfig.${name}.tsbuildinfo`,\n\t\t\tpaths\n\t\t},\n\t\tinclude: [`${fromGenerated(dirname(config.entry))}/**/*`, ...(config.imports.enabled ? [fromGenerated(config.imports.dts)] : [])],\n\t\texclude: [fromGenerated(join(config.root, 'node_modules')), fromGenerated(config.build.outDir)]\n\t};\n}\n\n/**\n * The root files that run in Node: `stars.config.*`, `vitest.config.*`, `scripts/**` and the like. No DOM, no\n * bundler resolution, no aliases or auto imports (none of it applies to a file Node loads itself), and no decorators.\n */\nfunction nodeTsconfig(config: ResolvedStarsConfig) {\n\tconst path = join(config.root, '.stars', 'tsconfig.node.json');\n\tconst fromGenerated = (target: string) => `./${relative(dirname(path), target).replaceAll('\\\\', '/')}`;\n\tconst source = dirname(config.entry);\n\tconst include = NODE_PROJECT_FILES.map((pattern) => fromGenerated(join(config.root, pattern)));\n\t// A custom `--config` file name is not covered by the globs.\n\tif (config.configFile !== null) include.push(fromGenerated(config.configFile));\n\n\treturn {\n\t\tcompilerOptions: {\n\t\t\t...strictOptions,\n\t\t\ttarget: 'ES2022',\n\t\t\tmodule: 'NodeNext',\n\t\t\tmoduleResolution: 'NodeNext',\n\t\t\tmoduleDetection: 'force',\n\t\t\tlib: ['ESNext'],\n\t\t\ttypes: ['node'],\n\t\t\tallowJs: true,\n\t\t\tnoEmit: true,\n\t\t\tforceConsistentCasingInFileNames: true,\n\t\t\tskipLibCheck: true,\n\t\t\ttsBuildInfoFile: './tsconfig.node.tsbuildinfo'\n\t\t},\n\t\tinclude: [...new Set(include)],\n\t\t// The two projects must not overlap: the bot sources belong to the app one. An entry in the project root has\n\t\t// no directory of its own to leave out.\n\t\texclude: [\n\t\t\tfromGenerated(join(config.root, 'node_modules')),\n\t\t\tfromGenerated(config.build.outDir),\n\t\t\t...(source === config.root ? [] : [fromGenerated(source)])\n\t\t]\n\t};\n}\n","import type { ResolvedStarsConfig } from '@wolfstar/schema';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { loadAutoImportsModule } from '../utils/framework-auto-imports.js';\nimport { formatError } from '../utils/errors.js';\nimport type { StarsHookable } from '../utils/hooks.js';\nimport { modulesPreloadWarning, prepareModulesPreload } from '../utils/modules.js';\nimport { findLegacyRootTsconfig, prepareTsconfig } from '../utils/tsconfig.js';\n\nexport type PrepareResult =\n\t| { enabled: false; dts: null; status: null }\n\t| { enabled: true; dts: string; status: 'written' | 'up-to-date' | 'outdated' };\n\n/**\n * Regenerates `imports.dts` unconditionally, used by `stars dev`/`stars build` before the first build so the\n * declaration file exists (and is current) even when the user never ran `stars prepare` themselves.\n */\nexport async function prepareAutoImports(config: ResolvedStarsConfig, check = false): Promise<PrepareResult> {\n\tif (!config.imports.enabled) return { enabled: false, dts: null, status: null };\n\n\tconst { dirs, presets, exclude, dts } = config.imports;\n\tconst { generateAutoImportsDts } = await loadAutoImportsModule(config.root);\n\tconst content = await generateAutoImportsDts({ root: config.root, dirs, presets, exclude });\n\n\tif (check) {\n\t\tconst existing = await readFile(dts, 'utf-8').catch(() => null);\n\t\treturn { enabled: true, dts, status: existing === content ? 'up-to-date' : 'outdated' };\n\t}\n\n\tawait mkdir(dirname(dts), { recursive: true });\n\tawait writeFile(dts, content);\n\treturn { enabled: true, dts, status: 'written' };\n}\n\n/** Prepares TypeScript configuration and auto import declarations before building. */\nexport async function prepareProject(config: ResolvedStarsConfig, hooks?: StarsHookable, check = false) {\n\tawait hooks?.callHook('prepare:before', config);\n\tconst tsconfigs = await prepareTsconfig(config, check);\n\tconst result = {\n\t\t...(await prepareAutoImports(config, check)),\n\t\t// The app config from version 6, the single file below it; `tsconfigs` has every generated file.\n\t\ttsconfig: tsconfigs[0]!,\n\t\ttsconfigs,\n\t\tmodules: await prepareModulesPreload(config, check)\n\t};\n\tawait hooks?.callHook('prepare:done', config, { dts: result.dts, status: result.status });\n\treturn result;\n}\n\n/**\n * Prints the non-fatal configuration diagnostics (e.g. an end-of-life compatibility version, or a root `tsconfig.json` still extending `.stars/tsconfig.json`), and, unless `production`\n * is `false` (`stars dev`, which does that itself), what a production start of the project still needs to do.\n */\nexport async function reportWarnings(\n\tconfig: ResolvedStarsConfig,\n\twrite: (text: string) => void,\n\toptions: { production?: boolean } = {}\n): Promise<void> {\n\tfor (const warning of config.warnings) write(await formatError(warning));\n\n\tconst legacyTsconfig = await findLegacyRootTsconfig(config);\n\tif (legacyTsconfig) write(await formatError(legacyTsconfig));\n\n\tconst preload = options.production === false ? null : modulesPreloadWarning(config);\n\tif (preload) write(await formatError(preload));\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as findInstalledVersion } from "./project-zlQStwsf.js";
1
+ import { t as findInstalledVersion } from "./project-DBqQMFCk.js";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { resolveSync } from "mlly";
4
4
 
@@ -235,4 +235,4 @@ function isAtLeast(version, minimum) {
235
235
 
236
236
  //#endregion
237
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
238
+ //# sourceMappingURL=bridge-Cbm0jX9h.js.map
@@ -1 +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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"}
1
+ {"version":3,"file":"bridge-Cbm0jX9h.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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"}
@@ -1,8 +1,8 @@
1
1
  import { i as shouldUseColor } from "./output-mode-Bp7Da8vJ.js";
2
- import { t as cliDiagnostics } from "./diagnostics-BysbNnPD.js";
3
- import { n as createBuilder, t as Locales } from "./locales-BJAP4HYf.js";
4
- import { d as resolveCwd, n as loadProject, t as applyEnvOptions, u as projectArgs } from "./hooks-Cy6F2bhH.js";
5
- import { n as reportWarnings, t as prepareProject } from "./_shared-BIQK0Bhm.js";
2
+ import { t as cliDiagnostics } from "./diagnostics-Dn3CXa8J.js";
3
+ import { n as createBuilder, t as Locales } from "./locales-BzyEMQnX.js";
4
+ import { d as resolveCwd, n as loadProject, t as applyEnvOptions, u as projectArgs } from "./hooks-D_RVS0ba.js";
5
+ import { n as reportWarnings, t as prepareProject } from "./_shared-CyzTojMA.js";
6
6
  import { defineCommand } from "citty";
7
7
  import { displayPath } from "@wolfstar/schema";
8
8
  import { createColors } from "colorette";
@@ -53,4 +53,4 @@ var build_default = defineCommand({
53
53
 
54
54
  //#endregion
55
55
  export { build_default as default };
56
- //# sourceMappingURL=build-DJcc95Yq.js.map
56
+ //# sourceMappingURL=build-BbIEbOu3.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-DJcc95Yq.js","names":[],"sources":["../src/commands/build.ts"],"sourcesContent":["import { displayPath } from '@wolfstar/schema';\nimport { defineCommand } from 'citty';\nimport { createColors } from 'colorette';\nimport { createBuilder } from '../builders/index.js';\nimport { projectArgs, resolveCwd, type ProjectArgs } from '../utils/args.js';\nimport { cliDiagnostics } from '../utils/diagnostics.js';\nimport { applyEnvOptions, loadProject } from '../utils/hooks.js';\nimport { Locales } from '../utils/locales.js';\nimport { shouldUseColor } from '../utils/output-mode.js';\nimport { prepareProject, reportWarnings } from './_shared.js';\n\nexport interface BuildTaskOptions extends ProjectArgs {\n\tstdout?: NodeJS.WritableStream;\n}\n\nexport async function runBuild(options: BuildTaskOptions): Promise<void> {\n\tconst stdout = options.stdout ?? process.stdout;\n\tconst colors = createColors({ useColor: shouldUseColor() });\n\tconst { config: resolved, hooks } = await loadProject({ cwd: resolveCwd(options), configFile: options.config });\n\tawait reportWarnings(resolved, (text) => process.stderr.write(`${text}\\n`));\n\n\tif (resolved.build.tool === 'none') {\n\t\tawait prepareProject(resolved, hooks);\n\t\tstdout.write(`${colors.dim('stars')} nothing to build, ${displayPath(resolved.root, resolved.entry)} runs as-is (build.tool is 'none')\\n`);\n\t\treturn;\n\t}\n\n\t// Same hook order as `stars dev`: `env:options` and `builder:created` before `prepare:*`.\n\tconst config = await applyEnvOptions(resolved, hooks);\n\tconst builder = await createBuilder(config, hooks);\n\tawait hooks.callHook('builder:created', builder, config);\n\tawait prepareProject(config, hooks);\n\tbuilder.on('log', (level, text) => {\n\t\tconst paint = level === 'error' ? colors.red : level === 'warn' ? colors.yellow : (value: string) => value;\n\t\tstdout.write(`${paint(text)}\\n`);\n\t});\n\n\tstdout.write(`${colors.dim('stars')} building with ${colors.bold(config.build.tool)}…\\n`);\n\tawait hooks.callHook('build:before', config);\n\tconst outcome = await builder.build();\n\tawait hooks.callHook('build:done', outcome, config);\n\tif (!outcome.ok) {\n\t\tthrow cliDiagnostics.BUILD_FAILED({ message: outcome.message ?? '' });\n\t}\n\tnew Locales(config).copy();\n\n\tstdout.write(`${colors.dim('stars')} ${colors.green(`built in ${outcome.durationMs}ms`)} → ${displayPath(config.root, config.build.output)}\\n`);\n}\n\nexport default defineCommand({\n\tmeta: {\n\t\tname: 'build',\n\t\tdescription: 'Build the project once with the configured build tool'\n\t},\n\targs: { ...projectArgs },\n\tasync run({ args }) {\n\t\tawait runBuild({ config: args.config, cwd: args.cwd });\n\t}\n});\n"],"mappings":";;;;;;;;;;AAeA,eAAsB,SAAS,SAA0C;CACxE,MAAM,SAAS,QAAQ,UAAU,QAAQ;CACzC,MAAM,SAAS,aAAa,EAAE,UAAU,eAAe,EAAE,CAAC;CAC1D,MAAM,EAAE,QAAQ,UAAU,UAAU,MAAM,YAAY;EAAE,KAAK,WAAW,OAAO;EAAG,YAAY,QAAQ;CAAO,CAAC;CAC9G,MAAM,eAAe,WAAW,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG,CAAC;CAE1E,IAAI,SAAS,MAAM,SAAS,QAAQ;EACnC,MAAM,eAAe,UAAU,KAAK;EACpC,OAAO,MAAM,GAAG,OAAO,IAAI,OAAO,EAAE,qBAAqB,YAAY,SAAS,MAAM,SAAS,KAAK,EAAE,qCAAqC;EACzI;CACD;CAGA,MAAM,SAAS,MAAM,gBAAgB,UAAU,KAAK;CACpD,MAAM,UAAU,MAAM,cAAc,QAAQ,KAAK;CACjD,MAAM,MAAM,SAAS,mBAAmB,SAAS,MAAM;CACvD,MAAM,eAAe,QAAQ,KAAK;CAClC,QAAQ,GAAG,QAAQ,OAAO,SAAS;EAClC,MAAM,QAAQ,UAAU,UAAU,OAAO,MAAM,UAAU,SAAS,OAAO,UAAU,UAAkB;EACrG,OAAO,MAAM,GAAG,MAAM,IAAI,EAAE,GAAG;CAChC,CAAC;CAED,OAAO,MAAM,GAAG,OAAO,IAAI,OAAO,EAAE,iBAAiB,OAAO,KAAK,OAAO,MAAM,IAAI,EAAE,IAAI;CACxF,MAAM,MAAM,SAAS,gBAAgB,MAAM;CAC3C,MAAM,UAAU,MAAM,QAAQ,MAAM;CACpC,MAAM,MAAM,SAAS,cAAc,SAAS,MAAM;CAClD,IAAI,CAAC,QAAQ,IACZ,MAAM,eAAe,aAAa,EAAE,SAAS,QAAQ,WAAW,GAAG,CAAC;CAErE,IAAI,QAAQ,MAAM,CAAC,CAAC,KAAK;CAEzB,OAAO,MAAM,GAAG,OAAO,IAAI,OAAO,EAAE,GAAG,OAAO,MAAM,YAAY,QAAQ,WAAW,GAAG,EAAE,KAAK,YAAY,OAAO,MAAM,OAAO,MAAM,MAAM,EAAE,GAAG;AAC/I;AAEA,oBAAe,cAAc;CAC5B,MAAM;EACL,MAAM;EACN,aAAa;CACd;CACA,MAAM,EAAE,GAAG,YAAY;CACvB,MAAM,IAAI,EAAE,QAAQ;EACnB,MAAM,SAAS;GAAE,QAAQ,KAAK;GAAQ,KAAK,KAAK;EAAI,CAAC;CACtD;AACD,CAAC"}
1
+ {"version":3,"file":"build-BbIEbOu3.js","names":[],"sources":["../src/commands/build.ts"],"sourcesContent":["import { displayPath } from '@wolfstar/schema';\nimport { defineCommand } from 'citty';\nimport { createColors } from 'colorette';\nimport { createBuilder } from '../builders/index.js';\nimport { projectArgs, resolveCwd, type ProjectArgs } from '../utils/args.js';\nimport { cliDiagnostics } from '../utils/diagnostics.js';\nimport { applyEnvOptions, loadProject } from '../utils/hooks.js';\nimport { Locales } from '../utils/locales.js';\nimport { shouldUseColor } from '../utils/output-mode.js';\nimport { prepareProject, reportWarnings } from './_shared.js';\n\nexport interface BuildTaskOptions extends ProjectArgs {\n\tstdout?: NodeJS.WritableStream;\n}\n\nexport async function runBuild(options: BuildTaskOptions): Promise<void> {\n\tconst stdout = options.stdout ?? process.stdout;\n\tconst colors = createColors({ useColor: shouldUseColor() });\n\tconst { config: resolved, hooks } = await loadProject({ cwd: resolveCwd(options), configFile: options.config });\n\tawait reportWarnings(resolved, (text) => process.stderr.write(`${text}\\n`));\n\n\tif (resolved.build.tool === 'none') {\n\t\tawait prepareProject(resolved, hooks);\n\t\tstdout.write(`${colors.dim('stars')} nothing to build, ${displayPath(resolved.root, resolved.entry)} runs as-is (build.tool is 'none')\\n`);\n\t\treturn;\n\t}\n\n\t// Same hook order as `stars dev`: `env:options` and `builder:created` before `prepare:*`.\n\tconst config = await applyEnvOptions(resolved, hooks);\n\tconst builder = await createBuilder(config, hooks);\n\tawait hooks.callHook('builder:created', builder, config);\n\tawait prepareProject(config, hooks);\n\tbuilder.on('log', (level, text) => {\n\t\tconst paint = level === 'error' ? colors.red : level === 'warn' ? colors.yellow : (value: string) => value;\n\t\tstdout.write(`${paint(text)}\\n`);\n\t});\n\n\tstdout.write(`${colors.dim('stars')} building with ${colors.bold(config.build.tool)}…\\n`);\n\tawait hooks.callHook('build:before', config);\n\tconst outcome = await builder.build();\n\tawait hooks.callHook('build:done', outcome, config);\n\tif (!outcome.ok) {\n\t\tthrow cliDiagnostics.BUILD_FAILED({ message: outcome.message ?? '' });\n\t}\n\tnew Locales(config).copy();\n\n\tstdout.write(`${colors.dim('stars')} ${colors.green(`built in ${outcome.durationMs}ms`)} → ${displayPath(config.root, config.build.output)}\\n`);\n}\n\nexport default defineCommand({\n\tmeta: {\n\t\tname: 'build',\n\t\tdescription: 'Build the project once with the configured build tool'\n\t},\n\targs: { ...projectArgs },\n\tasync run({ args }) {\n\t\tawait runBuild({ config: args.config, cwd: args.cwd });\n\t}\n});\n"],"mappings":";;;;;;;;;;AAeA,eAAsB,SAAS,SAA0C;CACxE,MAAM,SAAS,QAAQ,UAAU,QAAQ;CACzC,MAAM,SAAS,aAAa,EAAE,UAAU,eAAe,EAAE,CAAC;CAC1D,MAAM,EAAE,QAAQ,UAAU,UAAU,MAAM,YAAY;EAAE,KAAK,WAAW,OAAO;EAAG,YAAY,QAAQ;CAAO,CAAC;CAC9G,MAAM,eAAe,WAAW,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG,CAAC;CAE1E,IAAI,SAAS,MAAM,SAAS,QAAQ;EACnC,MAAM,eAAe,UAAU,KAAK;EACpC,OAAO,MAAM,GAAG,OAAO,IAAI,OAAO,EAAE,qBAAqB,YAAY,SAAS,MAAM,SAAS,KAAK,EAAE,qCAAqC;EACzI;CACD;CAGA,MAAM,SAAS,MAAM,gBAAgB,UAAU,KAAK;CACpD,MAAM,UAAU,MAAM,cAAc,QAAQ,KAAK;CACjD,MAAM,MAAM,SAAS,mBAAmB,SAAS,MAAM;CACvD,MAAM,eAAe,QAAQ,KAAK;CAClC,QAAQ,GAAG,QAAQ,OAAO,SAAS;EAClC,MAAM,QAAQ,UAAU,UAAU,OAAO,MAAM,UAAU,SAAS,OAAO,UAAU,UAAkB;EACrG,OAAO,MAAM,GAAG,MAAM,IAAI,EAAE,GAAG;CAChC,CAAC;CAED,OAAO,MAAM,GAAG,OAAO,IAAI,OAAO,EAAE,iBAAiB,OAAO,KAAK,OAAO,MAAM,IAAI,EAAE,IAAI;CACxF,MAAM,MAAM,SAAS,gBAAgB,MAAM;CAC3C,MAAM,UAAU,MAAM,QAAQ,MAAM;CACpC,MAAM,MAAM,SAAS,cAAc,SAAS,MAAM;CAClD,IAAI,CAAC,QAAQ,IACZ,MAAM,eAAe,aAAa,EAAE,SAAS,QAAQ,WAAW,GAAG,CAAC;CAErE,IAAI,QAAQ,MAAM,CAAC,CAAC,KAAK;CAEzB,OAAO,MAAM,GAAG,OAAO,IAAI,OAAO,EAAE,GAAG,OAAO,MAAM,YAAY,QAAQ,WAAW,GAAG,EAAE,KAAK,YAAY,OAAO,MAAM,OAAO,MAAM,MAAM,EAAE,GAAG;AAC/I;AAEA,oBAAe,cAAc;CAC5B,MAAM;EACL,MAAM;EACN,aAAa;CACd;CACA,MAAM,EAAE,GAAG,YAAY;CACvB,MAAM,IAAI,EAAE,QAAQ;EACnB,MAAM,SAAS;GAAE,QAAQ,KAAK;GAAQ,KAAK,KAAK;EAAI,CAAC;CACtD;AACD,CAAC"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runMain } from "./run-CjI3xBby.js";
2
+ import { t as runMain } from "./run-DlNY1QM1.js";
3
3
 
4
4
  //#region src/cli.ts
5
5
  await runMain();
@@ -1,8 +1,8 @@
1
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";
2
+ import { t as cliDiagnostics } from "./diagnostics-Dn3CXa8J.js";
3
+ import { i as resolveFromProject } from "./project-DBqQMFCk.js";
4
+ import { d as resolveCwd, n as loadProject, r as withProjectEnv, t as applyEnvOptions, u as projectArgs } from "./hooks-D_RVS0ba.js";
5
+ import { t as readLocalCommands } from "./local-commands-CGBjc8KJ.js";
6
6
  import { defineCommand } from "citty";
7
7
  import { dirname, join } from "node:path";
8
8
  import { displayPath } from "@wolfstar/schema";
@@ -259,4 +259,4 @@ var codegen_default = defineCommand({
259
259
 
260
260
  //#endregion
261
261
  export { codegen_default as default };
262
- //# sourceMappingURL=codegen-CF6o8v6A.js.map
262
+ //# sourceMappingURL=codegen-C2PY7RXA.js.map
@@ -1 +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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"}
1
+ {"version":3,"file":"codegen-C2PY7RXA.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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"}