@wolfstar/cli 2.2.0 → 2.3.0-next-20261004142621

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 (90) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +465 -304
  3. package/dist/{_shared-DJrpalNQ.js → _shared-BIQK0Bhm.js} +2 -2
  4. package/dist/_shared-BIQK0Bhm.js.map +1 -0
  5. package/dist/bridge-BvkmlAOZ.js +410 -0
  6. package/dist/bridge-BvkmlAOZ.js.map +1 -0
  7. package/dist/{build-CluOR8dJ.js → build-DJcc95Yq.js} +5 -5
  8. package/dist/build-DJcc95Yq.js.map +1 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{codegen-D6V1i_fa.js → codegen-aJSyPz-J.js} +4 -4
  12. package/dist/codegen-aJSyPz-J.js.map +1 -0
  13. package/dist/command-diff-BK912Hjc.js +78 -0
  14. package/dist/command-diff-BK912Hjc.js.map +1 -0
  15. package/dist/commands-CIPWQtF0.js +3 -0
  16. package/dist/commands-Clpbqq3K.js +15 -0
  17. package/dist/commands-Clpbqq3K.js.map +1 -0
  18. package/dist/{commands-BJ589_AC.js → commands-DVC7ky6f.js} +259 -9
  19. package/dist/commands-DVC7ky6f.js.map +1 -0
  20. package/dist/completions-D2KYtrrv.js +161 -0
  21. package/dist/completions-D2KYtrrv.js.map +1 -0
  22. package/dist/{dev-DJJZN4NJ.js → dev-oPPVR7d_.js} +434 -28
  23. package/dist/dev-oPPVR7d_.js.map +1 -0
  24. package/dist/{diagnostics-3JRAy8Xj.js → diagnostics-BysbNnPD.js} +22 -1
  25. package/dist/diagnostics-BysbNnPD.js.map +1 -0
  26. package/dist/doctor-DkVUwgDd.js +334 -0
  27. package/dist/doctor-DkVUwgDd.js.map +1 -0
  28. package/dist/errors-COFWKKKw.js.map +1 -1
  29. package/dist/external-CxFKxwi3.js.map +1 -1
  30. package/dist/framework-auto-imports-Gu_PXSyr.js.map +1 -1
  31. package/dist/{hooks-BfDFM38k.js → hooks-Cy6F2bhH.js} +20 -4
  32. package/dist/hooks-Cy6F2bhH.js.map +1 -0
  33. package/dist/index.d.ts +53 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -3
  36. package/dist/{info-C1Kx_4Og.js → info-CKJVMWIT.js} +15 -4
  37. package/dist/info-CKJVMWIT.js.map +1 -0
  38. package/dist/{locales-BzOLRBVg.js → locales-BJAP4HYf.js} +5 -5
  39. package/dist/locales-BJAP4HYf.js.map +1 -0
  40. package/dist/log-view-B7uJEnMR.js +342 -0
  41. package/dist/log-view-B7uJEnMR.js.map +1 -0
  42. package/dist/{nitro-yjqZBh5L.js → nitro-B8u1iVgf.js} +2 -2
  43. package/dist/nitro-B8u1iVgf.js.map +1 -0
  44. package/dist/none-uC_wi_l4.js.map +1 -1
  45. package/dist/output-mode-Bp7Da8vJ.js.map +1 -1
  46. package/dist/{plain-B6z85uQP.js → plain-DG7cy0Kk.js} +7 -2
  47. package/dist/plain-DG7cy0Kk.js.map +1 -0
  48. package/dist/{prepare-Cb45Z4re.js → prepare-BM93i3Rm.js} +4 -4
  49. package/dist/prepare-BM93i3Rm.js.map +1 -0
  50. package/dist/{project-D-8VIcyF.js → project-zlQStwsf.js} +2 -2
  51. package/dist/project-zlQStwsf.js.map +1 -0
  52. package/dist/{run-BiLZqib3.js → run-DMM2YlP3.js} +2 -12
  53. package/dist/run-DMM2YlP3.js.map +1 -0
  54. package/dist/{theme-3Fc_LI-2.js → theme-yS2h5dd_.js} +25 -1
  55. package/dist/theme-yS2h5dd_.js.map +1 -0
  56. package/dist/tsc-C5l5TQGC.js +3 -0
  57. package/dist/{tsc-DEBcwIO0.js → tsc-puVAcwmo.js} +24 -5
  58. package/dist/tsc-puVAcwmo.js.map +1 -0
  59. package/dist/{tsdown-DrWXld54.js → tsdown-Dwt3JhPw.js} +2 -2
  60. package/dist/tsdown-Dwt3JhPw.js.map +1 -0
  61. package/dist/{tui-BRddrLQP.js → tui-nldGSD4c.js} +913 -141
  62. package/dist/tui-nldGSD4c.js.map +1 -0
  63. package/dist/version-BzclCE19.js.map +1 -1
  64. package/dist/{vite-Bf7nrJx7.js → vite-YZUtv2Hk.js} +2 -2
  65. package/dist/vite-YZUtv2Hk.js.map +1 -0
  66. package/package.json +5 -5
  67. package/dist/_shared-DJrpalNQ.js.map +0 -1
  68. package/dist/build-CluOR8dJ.js.map +0 -1
  69. package/dist/codegen-D6V1i_fa.js.map +0 -1
  70. package/dist/commands-BJ589_AC.js.map +0 -1
  71. package/dist/dev-DJJZN4NJ.js.map +0 -1
  72. package/dist/diagnostics-3JRAy8Xj.js.map +0 -1
  73. package/dist/hooks-BfDFM38k.js.map +0 -1
  74. package/dist/info-C1Kx_4Og.js.map +0 -1
  75. package/dist/locales-BzOLRBVg.js.map +0 -1
  76. package/dist/log-buffer-CY82aqnb.js +0 -72
  77. package/dist/log-buffer-CY82aqnb.js.map +0 -1
  78. package/dist/nitro-yjqZBh5L.js.map +0 -1
  79. package/dist/plain-B6z85uQP.js.map +0 -1
  80. package/dist/prepare-Cb45Z4re.js.map +0 -1
  81. package/dist/project-D-8VIcyF.js.map +0 -1
  82. package/dist/run-BiLZqib3.js.map +0 -1
  83. package/dist/theme-3Fc_LI-2.js.map +0 -1
  84. package/dist/tsc-B931Ezh7.js +0 -3
  85. package/dist/tsc-DEBcwIO0.js.map +0 -1
  86. package/dist/tsdown-DrWXld54.js.map +0 -1
  87. package/dist/tui-BRddrLQP.js.map +0 -1
  88. package/dist/tunnel-DHSxOZKr.js +0 -175
  89. package/dist/tunnel-DHSxOZKr.js.map +0 -1
  90. package/dist/vite-Bf7nrJx7.js.map +0 -1
@@ -1,22 +1,97 @@
1
1
  import { i as shouldUseColor, n as prefersReducedMotion, r as resolveOutputMode } from "./output-mode-Bp7Da8vJ.js";
2
2
  import { i as renderCrashReport, t as ExitCode } from "./errors-COFWKKKw.js";
3
- import { t as cliDiagnostics } from "./diagnostics-3JRAy8Xj.js";
4
- import { a as readSavedTheme, c as saveTheme, i as isThemeSetting, n as THEME_SETTINGS, s as resolveThemeSetting } from "./theme-3Fc_LI-2.js";
5
- import { r as resolveBinary } from "./project-D-8VIcyF.js";
6
- import { i as createLineSplitter, n as resolveTscBinary, r as ProcessSupervisor } from "./tsc-DEBcwIO0.js";
7
- import { n as createBuilder, t as Locales } from "./locales-BzOLRBVg.js";
8
- import { c as moduleImportArgs, l as projectArgs, n as loadProject, r as withProjectEnv, s as envImportArgs, t as applyEnvOptions, u as resolveCwd } from "./hooks-BfDFM38k.js";
9
- import { n as reportWarnings, t as prepareProject } from "./_shared-DJrpalNQ.js";
10
- import { t as Tunnel } from "./tunnel-DHSxOZKr.js";
11
- import { n as classifyAppLine, t as LogBuffer } from "./log-buffer-CY82aqnb.js";
3
+ import { t as cliDiagnostics } from "./diagnostics-BysbNnPD.js";
4
+ import { a as readSavedTheme, c as saveTheme, i as isThemeSetting, n as THEME_SETTINGS, s as resolveThemeSetting } from "./theme-yS2h5dd_.js";
5
+ import { r as resolveBinary } from "./project-zlQStwsf.js";
6
+ import { i as createLineSplitter, n as resolveTscBinary, r as ProcessSupervisor } from "./tsc-puVAcwmo.js";
7
+ import { n as createBuilder, t as Locales } from "./locales-BJAP4HYf.js";
8
+ import { c as moduleImportArgs, d as resolveCwd, l as collectFlag, n as loadProject, r as withProjectEnv, s as envImportArgs, t as applyEnvOptions, u as projectArgs } from "./hooks-Cy6F2bhH.js";
9
+ import { n as reportWarnings, t as prepareProject } from "./_shared-BIQK0Bhm.js";
10
+ import { r as diffSnapshots, t as describeCommand } from "./command-diff-BK912Hjc.js";
11
+ import { i as bridgeImportArgs, n as CLI_SOURCE, o as parseBridgeMessage, s as Tunnel } from "./bridge-BvkmlAOZ.js";
12
+ import { a as initialLogView, s as isLogLevel, x as classifyAppLine, y as LogBuffer } from "./log-view-B7uJEnMR.js";
12
13
  import { defineCommand } from "citty";
13
- import { createWriteStream, mkdirSync } from "node:fs";
14
- import { dirname } from "node:path";
14
+ import { createWriteStream, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
15
+ import { basename, dirname, extname, join, sep } from "node:path";
15
16
  import { displayPath } from "@wolfstar/schema";
16
17
  import { EventEmitter } from "node:events";
17
18
  import { spawn } from "node:child_process";
19
+ import { stripVTControlCharacters } from "node:util";
20
+ import { createHash } from "node:crypto";
18
21
  import { lookup } from "node:dns/promises";
19
22
 
23
+ //#region src/dev/changed-files.ts
24
+ const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git"]);
25
+ /**
26
+ * `tsc -b` rewrites its build info on every build, whatever changed; a source map only ever changes with the file it
27
+ * maps, and is most of what there would be to read.
28
+ */
29
+ const SKIPPED_FILES = /\.(?:tsbuildinfo|map)$/;
30
+ /**
31
+ * Hashes every file under `roots`, skipping `node_modules`, `.git` and everything under `exclude`.
32
+ *
33
+ * The builders only report that a build finished, and a bundler rewrites files it did not change (`tsdown` writes
34
+ * its whole output on every rebuild), so neither an event nor a modification time says what a build changed: two
35
+ * snapshots do (see {@link changedFiles}).
36
+ */
37
+ function snapshotFiles(roots, exclude = [], cache = /* @__PURE__ */ new Map()) {
38
+ const snapshot = /* @__PURE__ */ new Map();
39
+ const visit = (directory) => {
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(directory, { withFileTypes: true });
43
+ } catch {
44
+ return;
45
+ }
46
+ for (const entry of entries) {
47
+ const path = join(directory, entry.name);
48
+ if (exclude.some((excluded) => isInside(path, excluded))) continue;
49
+ if (entry.isDirectory()) {
50
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) visit(path);
51
+ } else if (entry.isFile() && !SKIPPED_FILES.test(entry.name)) try {
52
+ const { size, mtimeMs } = statSync(path);
53
+ const known = cache.get(path);
54
+ const hash = known?.size === size && known.mtimeMs === mtimeMs ? known.hash : createHash("sha1").update(readFileSync(path)).digest("base64");
55
+ cache.set(path, {
56
+ size,
57
+ mtimeMs,
58
+ hash
59
+ });
60
+ snapshot.set(path, hash);
61
+ } catch {}
62
+ }
63
+ };
64
+ for (const root of new Set(roots)) visit(root);
65
+ return snapshot;
66
+ }
67
+ /** The files that were added, removed, or whose content differs between two snapshots. */
68
+ function changedFiles(previous, next) {
69
+ const changed = [...next].filter(([path, hash]) => previous.get(path) !== hash).map(([path]) => path);
70
+ for (const path of previous.keys()) if (!next.has(path)) changed.push(path);
71
+ return changed;
72
+ }
73
+ const PIECE_EXTENSIONS = /* @__PURE__ */ new Set([
74
+ ".js",
75
+ ".mjs",
76
+ ".cjs",
77
+ ".ts",
78
+ ".mts",
79
+ ".cts"
80
+ ]);
81
+ /**
82
+ * Whether a file a store has not loaded yet could be a piece, by the rule the stores filter files with: a script
83
+ * that is not a declaration file and whose name does not start with `_`.
84
+ */
85
+ function couldBePiece(path) {
86
+ const extension = extname(path);
87
+ return PIECE_EXTENSIONS.has(extension) && !path.endsWith(".d.ts") && !basename(path, extension).startsWith("_");
88
+ }
89
+ /** Whether `path` is `directory` itself or lies under it. */
90
+ function isInside(path, directory) {
91
+ return path === directory || path.startsWith(directory.endsWith(sep) ? directory : `${directory}${sep}`);
92
+ }
93
+
94
+ //#endregion
20
95
  //#region src/dev/typechecker.ts
21
96
  const WATCH_START = /Starting (incremental )?compilation/;
22
97
  const WATCH_DONE = /Found (\d+) errors?\. Watching for file changes/;
@@ -232,6 +307,8 @@ var DevService = class extends EventEmitter {
232
307
  typechecker;
233
308
  tunnel;
234
309
  locales;
310
+ /** Whether something can answer a {@link DevPrompt}: the interactive UI sets it, the plain renderer cannot. */
311
+ promptable = false;
235
312
  #hooks;
236
313
  /** Runs hooks one at a time, so an async `build:done` settles before the next build's hooks or a restart. */
237
314
  #hookChain = Promise.resolve();
@@ -252,6 +329,23 @@ var DevService = class extends EventEmitter {
252
329
  startedAt: Date.now(),
253
330
  readyMs: null
254
331
  };
332
+ #ready = false;
333
+ #port = null;
334
+ /** The store paths the running bot watches for hot reloads, empty when it does not. */
335
+ #hmrPaths = [];
336
+ #paused = false;
337
+ #prompt = null;
338
+ /** The commands the bot last reported, kept across restarts: a restart is how most changes arrive. */
339
+ #commands = null;
340
+ /** What the build output held after the previous build, `null` before the first one. */
341
+ #files = null;
342
+ #hashes = /* @__PURE__ */ new Map();
343
+ /** The files the running bot loaded pieces from: what its hot reload can replace. */
344
+ #pieces = /* @__PURE__ */ new Set();
345
+ /** The commands Discord is assumed to have: the first report of the session, then whatever was last deployed. */
346
+ #deployed = null;
347
+ /** A refresh of the commands asked for while the bot was not listening; sent once it is. */
348
+ #refreshPending = false;
255
349
  constructor(config, options) {
256
350
  super();
257
351
  this.config = config;
@@ -292,6 +386,15 @@ var DevService = class extends EventEmitter {
292
386
  });
293
387
  this.supervisor.on("stdout", (line) => this.log("app", classifyAppLine(line, "info"), line));
294
388
  this.supervisor.on("stderr", (line) => this.log("app", classifyAppLine(line, "error"), line));
389
+ this.supervisor.on("message", (message) => {
390
+ const parsed = parseBridgeMessage(message);
391
+ if (parsed === null) return;
392
+ try {
393
+ this.#onBridge(parsed);
394
+ } catch (error) {
395
+ this.log("stars", "warn", `Ignored a ${parsed.type} message from the bot: ${error instanceof Error ? error.message : String(error)}`);
396
+ }
397
+ });
295
398
  this.supervisor.on("error", (error) => this.log("stars", "error", `Failed to start the bot: ${error.message}`));
296
399
  this.supervisor.on("exit", (exit) => this.#onExit(exit));
297
400
  this.typechecker.on("log", (level, text) => this.log("tsc", level, text));
@@ -324,14 +427,20 @@ var DevService = class extends EventEmitter {
324
427
  typecheck: this.typechecker.state,
325
428
  typeErrors: this.typechecker.errors,
326
429
  tunnel: this.tunnel.state,
327
- tunnelUrl: this.tunnel.url
430
+ tunnelUrl: this.tunnel.url,
431
+ ready: this.#ready,
432
+ port: this.#port,
433
+ hmr: this.#hmrPaths.length > 0,
434
+ paused: this.#paused,
435
+ prompt: this.#prompt
328
436
  };
329
437
  }
330
- log(source, level, text) {
438
+ log(source, level, text, extra = {}) {
331
439
  this.logs.push({
332
440
  source,
333
441
  level,
334
- text
442
+ text,
443
+ ...extra
335
444
  });
336
445
  }
337
446
  /**
@@ -374,10 +483,51 @@ var DevService = class extends EventEmitter {
374
483
  message: "starting the bot"
375
484
  };
376
485
  this.#health = "unknown";
486
+ this.#paused = false;
377
487
  this.supervisor.start();
378
488
  this.#emitStatus();
379
489
  });
380
490
  }
491
+ /**
492
+ * Stops the bot and leaves it stopped (the `d` key): builds keep running, but nothing starts the bot again until
493
+ * {@link restart}. For when the bot must be off without ending the session.
494
+ */
495
+ disconnect() {
496
+ this.#clearRestartTimer();
497
+ return this.#enqueue(async () => {
498
+ if (this.#stopped || this.#paused) return;
499
+ this.#paused = true;
500
+ this.log("stars", "info", "Stopped the bot, press r to start it again");
501
+ await this.supervisor.stop();
502
+ this.#emitStatus();
503
+ });
504
+ }
505
+ /** Settles the pending {@link DevPrompt}: `true` does what it asks, `false` dismisses it. */
506
+ answerPrompt(accept) {
507
+ if (this.#prompt === null) return;
508
+ this.#prompt = null;
509
+ if (accept) this.refreshCommands();
510
+ else this.log("stars", "info", "Kept the deployed commands as they are", { channel: "commands" });
511
+ this.#emitStatus();
512
+ }
513
+ /** Asks the bot to register its commands with Discord again. The bot answers on the `commands` channel. */
514
+ refreshCommands() {
515
+ if (!this.#ready) {
516
+ this.#refreshPending = true;
517
+ const when = this.supervisor.running ? "once the bot is ready" : "when the bot starts again";
518
+ this.log("stars", "info", `Refreshing commands ${when}…`, { channel: "commands" });
519
+ return;
520
+ }
521
+ this.#refreshPending = false;
522
+ const current = this.#commands?.guilds ?? {};
523
+ const request = {
524
+ source: CLI_SOURCE,
525
+ type: "commands:refresh",
526
+ clearGuilds: Object.keys(this.#deployed?.guilds ?? {}).filter((guild) => !Object.hasOwn(current, guild))
527
+ };
528
+ if (this.supervisor.send(request)) this.log("stars", "info", "Refreshing commands…", { channel: "commands" });
529
+ else this.log("stars", "warn", "Could not reach the bot to refresh its commands", { channel: "commands" });
530
+ }
381
531
  /** Opens or closes the public tunnel (used by the `t` key). */
382
532
  toggleTunnel() {
383
533
  return this.tunnel.toggle();
@@ -414,6 +564,7 @@ var DevService = class extends EventEmitter {
414
564
  this.logs.clear();
415
565
  }
416
566
  #onBuildSuccess(outcome) {
567
+ const hot = this.#hotReloadable();
417
568
  try {
418
569
  this.locales.copy();
419
570
  } catch (error) {
@@ -435,8 +586,35 @@ var DevService = class extends EventEmitter {
435
586
  if (this.#stopped) return;
436
587
  this.typechecker.check();
437
588
  this.log("stars", "success", this.builder.tool === "none" ? "Sources changed" : `Build succeeded in ${outcome.durationMs}ms`);
589
+ if (this.#paused) return;
590
+ if (hot !== null) {
591
+ for (const file of hot) this.log("stars", "trace", `UPDATE ${displayPath(this.config.root, file)}`, { channel: "hmr" });
592
+ this.#settleProgress();
593
+ this.#emitStatus();
594
+ return;
595
+ }
438
596
  this.#scheduleRestart(this.supervisor.state === "idle" ? "initial" : "build");
439
597
  }
598
+ /**
599
+ * The files the build rewrote when all of them are pieces the running bot hot reloads, `null` when the bot has to
600
+ * be restarted instead: it is not running, it does not hot reload, `dev.hmr` is off, nothing changed (a rebuild for
601
+ * a reason that cannot be seen from the output), or a file changed that the bot cannot replace. What a build changed
602
+ * is the difference in content with the previous output, since a bundler rewrites files it did not change.
603
+ *
604
+ * The bot replaces the files it loaded pieces from, and loads a new file that could be one. A helper module next to
605
+ * the pieces (`_shared.js`, or any file no piece came from) is imported once and stays as it was: changing it
606
+ * needs a restart, even though it lies in a store path.
607
+ */
608
+ #hotReloadable() {
609
+ if (this.config.dev.hmr === false) return null;
610
+ const roots = this.config.build.tool === "none" ? this.config.dev.watch : [this.config.build.outDir];
611
+ const previous = this.#files;
612
+ this.#files = snapshotFiles(roots, [this.locales.destination], this.#hashes);
613
+ if (previous === null || this.#hmrPaths.length === 0 || this.supervisor.state !== "running") return null;
614
+ const changed = changedFiles(previous, this.#files);
615
+ const replaceable = (file) => this.#hmrPaths.some((path) => isInside(file, path)) && (this.#pieces.has(file) || !previous.has(file) && couldBePiece(file));
616
+ return changed.length > 0 && changed.every(replaceable) ? changed : null;
617
+ }
440
618
  #onBuildFailure(outcome) {
441
619
  this.#lastBuild = outcome;
442
620
  this.#queueHook("build:done", outcome, this.config);
@@ -447,6 +625,10 @@ var DevService = class extends EventEmitter {
447
625
  #onExit(exit) {
448
626
  this.#lastExit = exit;
449
627
  this.#health = "unknown";
628
+ this.#ready = false;
629
+ this.#port = null;
630
+ this.#hmrPaths = [];
631
+ this.#pieces.clear();
450
632
  if (!exit.requested) {
451
633
  const how = exit.signal ? `signal ${exit.signal}` : `code ${exit.code}`;
452
634
  this.log("stars", exit.code === 0 ? "warn" : "error", `The bot exited with ${how}, waiting for changes (press r to restart)`);
@@ -454,6 +636,7 @@ var DevService = class extends EventEmitter {
454
636
  this.#emitStatus();
455
637
  }
456
638
  #scheduleRestart(reason) {
639
+ if (this.#paused) return;
457
640
  this.#pendingReason = reason;
458
641
  this.#clearRestartTimer();
459
642
  this.#restartTimer = setTimeout(() => {
@@ -502,6 +685,141 @@ var DevService = class extends EventEmitter {
502
685
  readyMs: this.#progress.readyMs ?? Date.now() - this.#progress.startedAt
503
686
  };
504
687
  }
688
+ /** Turns what the bot reports over the bridge into log entries and status. */
689
+ #onBridge(message) {
690
+ const relative = (path) => displayPath(this.config.root, path);
691
+ switch (message.type) {
692
+ case "ready":
693
+ this.#ready = true;
694
+ this.#port = message.port;
695
+ this.log("stars", "success", message.port === null ? "The bot is ready" : `Listening on port ${message.port}`, { channel: "lifecycle" });
696
+ if (this.#refreshPending) this.refreshCommands();
697
+ this.#emitStatus();
698
+ return;
699
+ case "pieces":
700
+ for (const piece of message.pieces) this.#pieces.add(piece.path);
701
+ if (message.pieces.length === 0) return;
702
+ this.log("stars", "debug", `Loaded ${message.store}: ${message.pieces.length} ${message.pieces.length === 1 ? "piece" : "pieces"}`, {
703
+ channel: message.store === "commands" ? "commands" : "lifecycle",
704
+ detail: message.pieces.map((piece) => `${piece.name} ${relative(piece.path)}`)
705
+ });
706
+ return;
707
+ case "hmr":
708
+ this.#onHmr(message);
709
+ return;
710
+ case "commands":
711
+ this.#onCommands({
712
+ global: message.global,
713
+ guilds: message.guilds
714
+ });
715
+ return;
716
+ case "dispatch":
717
+ if (message.phase === "run") this.log("stars", "debug", `Processing ${message.route} with ${message.piece}`, { channel: "interactions" });
718
+ else if (message.phase === "success") this.log("stars", "trace", `${message.route} handled${formatMs(message.ms)}`, { channel: "interactions" });
719
+ else this.log("stars", "error", `${message.route} failed${formatMs(message.ms)}: ${message.error.message}`, {
720
+ channel: "interactions",
721
+ detail: message.error.stack
722
+ });
723
+ return;
724
+ case "request":
725
+ this.log("stars", message.status >= 500 ? "error" : message.status >= 400 ? "warn" : "trace", formatRequest(message), { channel: "http" });
726
+ return;
727
+ case "refreshed":
728
+ if (message.ok) {
729
+ this.#deployed = this.#commands;
730
+ const guilds = (message.guilds > 0 ? `, ${message.guilds} guild ${message.guilds === 1 ? "group" : "groups"}` : "") + (message.cleared > 0 ? `, cleared ${message.cleared} ${message.cleared === 1 ? "guild" : "guilds"}` : "");
731
+ this.log("stars", "success", `Deployed ${message.global} global ${message.global === 1 ? "command" : "commands"}${guilds}`, { channel: "commands" });
732
+ } else this.log("stars", "error", `Could not refresh the commands: ${message.message}`, {
733
+ channel: "commands",
734
+ detail: message.stack
735
+ });
736
+ return;
737
+ case "log":
738
+ this.log("app", toLogLevel(message.level), message.text, {
739
+ channel: message.channel,
740
+ detail: message.detail
741
+ });
742
+ return;
743
+ }
744
+ }
745
+ #onHmr(message) {
746
+ const relative = (path) => displayPath(this.config.root, path);
747
+ if (message.event === "start") {
748
+ this.#hmrPaths = message.paths;
749
+ this.log("stars", "debug", `Hot reload on, watching ${message.paths.length} ${message.paths.length === 1 ? "path" : "paths"}`, {
750
+ channel: "hmr",
751
+ detail: message.paths.map(relative)
752
+ });
753
+ this.#emitStatus();
754
+ return;
755
+ }
756
+ if (message.event === "stop") {
757
+ this.#hmrPaths = [];
758
+ this.log("stars", "debug", "Hot reload off", { channel: "hmr" });
759
+ this.#emitStatus();
760
+ return;
761
+ }
762
+ if (message.event === "error") {
763
+ this.log("stars", "error", `Could not reload ${relative(message.path)}: ${message.message}`, {
764
+ channel: "hmr",
765
+ detail: message.stack
766
+ });
767
+ return;
768
+ }
769
+ if (message.event === "unloaded") this.#pieces.delete(message.path);
770
+ else this.#pieces.add(message.path);
771
+ const verb = message.event === "reloaded" ? "Reloaded" : message.event === "loaded" ? "Loaded" : "Unloaded";
772
+ this.log("stars", "debug", `${verb} ${message.names.join(", ")} from ${relative(message.path)}`, { channel: "hmr" });
773
+ }
774
+ /**
775
+ * Compares the commands the bot registers with the ones it reported before. The first report of a session is the
776
+ * baseline; a later one that differs is acted on as `dev.commands.refresh` says.
777
+ */
778
+ #onCommands(snapshot) {
779
+ const previous = this.#commands;
780
+ this.#commands = snapshot;
781
+ if (previous === null) {
782
+ this.#deployed = snapshot;
783
+ const guilds = Object.keys(snapshot.guilds).length;
784
+ this.log("stars", "debug", `Loaded commands: ${snapshot.global.length} global, ${guilds} guild ${guilds === 1 ? "group" : "groups"}`, {
785
+ channel: "commands",
786
+ detail: snapshot.global.map((command) => describeCommand({
787
+ name: command.name,
788
+ type: command.type ?? 1,
789
+ guild: null
790
+ }))
791
+ });
792
+ return;
793
+ }
794
+ const changes = diffSnapshots(previous, snapshot);
795
+ if (changes.length === 0) return;
796
+ const detail = changes.map((change) => `${change.kind} ${describeCommand(change)}`);
797
+ const mode = this.config.dev.commands?.refresh ?? "prompt";
798
+ if (mode === "auto") {
799
+ this.log("stars", "info", "Commands updated", {
800
+ channel: "commands",
801
+ detail
802
+ });
803
+ this.refreshCommands();
804
+ return;
805
+ }
806
+ if (mode === "off" || !this.promptable) {
807
+ this.log("stars", "warn", "Commands updated, Discord still has the previous ones", {
808
+ channel: "commands",
809
+ detail: [...detail, mode === "off" ? "dev.commands.refresh is off" : "Set dev.commands.refresh to \"auto\" to redeploy without asking"]
810
+ });
811
+ return;
812
+ }
813
+ this.#prompt = {
814
+ kind: "commands",
815
+ changes: mergeChanges(this.#prompt?.changes ?? [], changes)
816
+ };
817
+ this.log("stars", "info", "Commands updated", {
818
+ channel: "commands",
819
+ detail
820
+ });
821
+ this.#emitStatus();
822
+ }
505
823
  /**
506
824
  * Runs a hook behind the ones already running (see {@link DevService.#hookChain}), e.g. `dev:close` so shutdown
507
825
  * waits for an unfinished `build:done`.
@@ -546,6 +864,7 @@ function createSupervisor(config) {
546
864
  args: [
547
865
  ...envImportArgs(config),
548
866
  ...moduleImportArgs(config),
867
+ ...bridgeImportArgs(config),
549
868
  ...config.dev.nodeArgs,
550
869
  config.build.output,
551
870
  ...config.dev.args
@@ -558,9 +877,34 @@ function createSupervisor(config) {
558
877
  NODE_ENV: "development",
559
878
  FORCE_COLOR: process.env.NO_COLOR !== void 0 ? void 0 : process.env.FORCE_COLOR ?? (process.stdout.isTTY ? "1" : void 0)
560
879
  },
561
- killTimeout: config.dev.killTimeout
880
+ killTimeout: config.dev.killTimeout,
881
+ ipc: true
562
882
  });
563
883
  }
884
+ function formatMs(ms) {
885
+ return ms === null ? "" : ` in ${ms}ms`;
886
+ }
887
+ function formatRequest(message) {
888
+ return `${message.method} ${message.path} ${message.status} in ${message.ms}ms`;
889
+ }
890
+ const BRIDGE_LEVELS = /* @__PURE__ */ new Set([
891
+ "trace",
892
+ "debug",
893
+ "info",
894
+ "success",
895
+ "warn",
896
+ "error"
897
+ ]);
898
+ function toLogLevel(level) {
899
+ return BRIDGE_LEVELS.has(level) ? level : "info";
900
+ }
901
+ /** The latest change of each command wins: `added` then `changed` is still one line of the prompt. */
902
+ function mergeChanges(previous, next) {
903
+ const key = (change) => `${change.guild ?? ""}:${change.type}:${change.name}`;
904
+ const merged = new Map(previous.map((change) => [key(change), change]));
905
+ for (const change of next) merged.set(key(change), change);
906
+ return [...merged.values()];
907
+ }
564
908
  function describeReason(reason) {
565
909
  switch (reason) {
566
910
  case "initial": return "first build";
@@ -603,8 +947,8 @@ async function withResolvedLocalhost(url) {
603
947
  //#region src/dev/log-file.ts
604
948
  /**
605
949
  * Mirrors a dev session's logs into a file, so a run can be read back after the terminal UI is gone (the TUI takes
606
- * over the alternate screen and its scrollback disappears on exit). Seedcord keeps a `logs/` directory for the same
607
- * reason; this writes a single, truncated-per-run file instead.
950
+ * over the alternate screen and its scrollback disappears on exit). `dev.logFile` is a single file truncated on every
951
+ * run; `dev.logs.dir` adds one file per run (see {@link runLogFile}).
608
952
  */
609
953
  var LogFileWriter = class {
610
954
  file;
@@ -630,20 +974,57 @@ var LogFileWriter = class {
630
974
  stream.end();
631
975
  }
632
976
  #write = (entry) => {
633
- this.#stream?.write(`${format(entry)}\n`);
977
+ this.#stream?.write(`${formatLogLine(entry)}\n`);
634
978
  };
635
979
  };
636
- function format(entry) {
637
- return `${new Date(entry.time).toISOString()} ${entry.level.padEnd(7)} ${entry.source.padEnd(6)} ${entry.text}`;
980
+ /** One line per entry, then its detail lines indented under it. Never filtered: the file is the whole session. */
981
+ function formatLogLine(entry) {
982
+ return [`${new Date(entry.time).toISOString()} ${entry.level.padEnd(7)} ${entry.channel.padEnd(12)} ${stripVTControlCharacters(entry.text)}`, ...(entry.detail ?? []).map((line) => ` ${stripVTControlCharacters(line)}`)].join("\n");
983
+ }
984
+ const RUN_FILE = /^dev-.+\.log$/;
985
+ /**
986
+ * The file of this run inside `dev.logs.dir` (`dev-<timestamp>.log`), after deleting the oldest ones so the directory
987
+ * holds at most `keep` files with the new one. The names sort by time, so no file needs to be opened to order them.
988
+ */
989
+ function runLogFile(directory, keep, now = /* @__PURE__ */ new Date()) {
990
+ let existing = [];
991
+ try {
992
+ existing = readdirSync(directory).filter((name) => RUN_FILE.test(name)).sort();
993
+ } catch {}
994
+ for (const name of existing.slice(0, Math.max(0, existing.length - Math.max(0, keep - 1)))) rmSync(join(directory, name), { force: true });
995
+ return join(directory, `dev-${now.toISOString().replaceAll(":", "-")}.log`);
638
996
  }
639
997
 
640
998
  //#endregion
641
999
  //#region src/commands/dev.ts
1000
+ const LAYOUTS = [
1001
+ "auto",
1002
+ "dashboard",
1003
+ "panel"
1004
+ ];
1005
+ const LEVELS = [
1006
+ "trace",
1007
+ "debug",
1008
+ "info",
1009
+ "warn",
1010
+ "error"
1011
+ ];
642
1012
  async function runDev(options) {
643
1013
  if (options.theme !== void 0 && !isThemeSetting(options.theme)) throw cliDiagnostics.INVALID_THEME({
644
1014
  theme: options.theme,
645
1015
  themes: THEME_SETTINGS.join(", ")
646
1016
  });
1017
+ const layout = options.layout;
1018
+ if (layout !== void 0 && !LAYOUTS.includes(layout)) throw cliDiagnostics.INVALID_OPTION({
1019
+ option: "--layout",
1020
+ value: options.layout,
1021
+ allowed: LAYOUTS.join(", ")
1022
+ });
1023
+ if (options.level !== void 0 && !isLogLevel(options.level)) throw cliDiagnostics.INVALID_OPTION({
1024
+ option: "--level",
1025
+ value: options.level,
1026
+ allowed: LEVELS.join(", ")
1027
+ });
647
1028
  process.env.NODE_ENV = "development";
648
1029
  const project = await loadProject({
649
1030
  cwd: resolveCwd(options),
@@ -668,23 +1049,32 @@ async function runDev(options) {
668
1049
  hooks
669
1050
  });
670
1051
  await reportWarnings(config, (text) => service.log("stars", "warn", text), { production: false });
671
- const logFile = config.dev.logFile ? new LogFileWriter(config.dev.logFile, service.logs) : null;
672
- logFile?.open();
673
- const renderer = mode === "tui" ? (await import("./tui-BRddrLQP.js")).createTuiRenderer(service, {
1052
+ const logFiles = [...config.dev.logFile ? [config.dev.logFile] : [], ...config.dev.logs?.dir ? [runLogFile(config.dev.logs.dir, config.dev.logs.keep)] : []].map((file) => new LogFileWriter(file, service.logs));
1053
+ for (const file of logFiles) file.open();
1054
+ const filter = initialLogView(config, {
1055
+ channels: options.channel,
1056
+ level: options.level
1057
+ });
1058
+ const renderer = mode === "tui" ? (await import("./tui-nldGSD4c.js")).createTuiRenderer(service, {
674
1059
  color,
675
1060
  theme,
1061
+ layout,
1062
+ filter,
676
1063
  reducedMotion: prefersReducedMotion(),
677
1064
  onThemeSave: (setting) => {
678
1065
  if (!saveTheme(setting)) service.log("stars", "warn", "Could not save the theme preference.");
679
1066
  }
680
- }) : (await import("./plain-B6z85uQP.js")).createPlainRenderer(service, { color });
1067
+ }) : (await import("./plain-DG7cy0Kk.js")).createPlainRenderer(service, {
1068
+ color,
1069
+ filter
1070
+ });
681
1071
  let exiting = null;
682
1072
  const shutdown = (code) => {
683
1073
  exiting ??= (async () => {
684
1074
  renderer.stop();
685
1075
  await service.runHook("dev:close", config);
686
1076
  await service.stop();
687
- logFile?.close();
1077
+ for (const file of logFiles) file.close();
688
1078
  process.exit(code);
689
1079
  })();
690
1080
  return exiting;
@@ -746,18 +1136,34 @@ var dev_default = defineCommand({
746
1136
  theme: {
747
1137
  type: "string",
748
1138
  description: `Colour theme of the interactive UI: ${THEME_SETTINGS.join(", ")} (or STARS_THEME; press T in the UI to pick one)`
1139
+ },
1140
+ layout: {
1141
+ type: "string",
1142
+ description: `Layout of the interactive UI: ${LAYOUTS.join(", ")} (defaults to dev.layout; press v in the UI to switch)`
1143
+ },
1144
+ channel: {
1145
+ type: "string",
1146
+ description: "Only show these log channels at start (repeatable, or comma-separated; defaults to dev.logs.channels)"
1147
+ },
1148
+ level: {
1149
+ type: "string",
1150
+ description: `Only show this log level and the more severe ones at start: ${LEVELS.join(", ")} (defaults to dev.logs.levels)`
749
1151
  }
750
1152
  },
751
- async run({ args }) {
1153
+ async run({ args, rawArgs }) {
1154
+ const channel = collectFlag(rawArgs, "channel");
752
1155
  await runDev({
753
1156
  config: args.config,
754
1157
  cwd: args.cwd,
755
1158
  tui: args.tui ? void 0 : false,
756
- theme: args.theme
1159
+ theme: args.theme,
1160
+ layout: args.layout,
1161
+ channel: channel.length > 0 ? channel : void 0,
1162
+ level: args.level
757
1163
  });
758
1164
  }
759
1165
  });
760
1166
 
761
1167
  //#endregion
762
1168
  export { dev_default as default };
763
- //# sourceMappingURL=dev-DJJZN4NJ.js.map
1169
+ //# sourceMappingURL=dev-oPPVR7d_.js.map