@wolfstar/cli 2.1.0 → 2.3.0-next-20261004131917

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 (79) hide show
  1. package/README.md +203 -42
  2. package/dist/{_shared-B-hRZDRS.js → _shared-BIQK0Bhm.js} +12 -5
  3. package/dist/_shared-BIQK0Bhm.js.map +1 -0
  4. package/dist/bridge-BvkmlAOZ.js +410 -0
  5. package/dist/bridge-BvkmlAOZ.js.map +1 -0
  6. package/dist/{build-CPnSLYME.js → build-DJcc95Yq.js} +5 -5
  7. package/dist/{build-CPnSLYME.js.map → build-DJcc95Yq.js.map} +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/{codegen-BqpLGLLs.js → codegen-aJSyPz-J.js} +4 -4
  10. package/dist/{codegen-BqpLGLLs.js.map → codegen-aJSyPz-J.js.map} +1 -1
  11. package/dist/command-diff-BK912Hjc.js +78 -0
  12. package/dist/command-diff-BK912Hjc.js.map +1 -0
  13. package/dist/commands-CIPWQtF0.js +3 -0
  14. package/dist/commands-Clpbqq3K.js +15 -0
  15. package/dist/commands-Clpbqq3K.js.map +1 -0
  16. package/dist/{commands-ExM8HILu.js → commands-DVC7ky6f.js} +259 -9
  17. package/dist/commands-DVC7ky6f.js.map +1 -0
  18. package/dist/completions-D2KYtrrv.js +161 -0
  19. package/dist/completions-D2KYtrrv.js.map +1 -0
  20. package/dist/{dev-4wpEQC7l.js → dev-oPPVR7d_.js} +432 -45
  21. package/dist/dev-oPPVR7d_.js.map +1 -0
  22. package/dist/{diagnostics-CZBVxlpj.js → diagnostics-BysbNnPD.js} +38 -1
  23. package/dist/diagnostics-BysbNnPD.js.map +1 -0
  24. package/dist/doctor-DkVUwgDd.js +334 -0
  25. package/dist/doctor-DkVUwgDd.js.map +1 -0
  26. package/dist/{errors-Crt4hu0b.js → errors-COFWKKKw.js} +5 -2
  27. package/dist/errors-COFWKKKw.js.map +1 -0
  28. package/dist/hooks-Cy6F2bhH.js +332 -0
  29. package/dist/hooks-Cy6F2bhH.js.map +1 -0
  30. package/dist/index.d.ts +75 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/dist/{info-CdiQajgn.js → info-CKJVMWIT.js} +16 -5
  34. package/dist/info-CKJVMWIT.js.map +1 -0
  35. package/dist/{locales-C7X8VNmK.js → locales-BJAP4HYf.js} +5 -5
  36. package/dist/{locales-C7X8VNmK.js.map → locales-BJAP4HYf.js.map} +1 -1
  37. package/dist/log-view-B7uJEnMR.js +342 -0
  38. package/dist/log-view-B7uJEnMR.js.map +1 -0
  39. package/dist/{nitro-sP2F3SQu.js → nitro-B8u1iVgf.js} +2 -2
  40. package/dist/{nitro-sP2F3SQu.js.map → nitro-B8u1iVgf.js.map} +1 -1
  41. package/dist/{plain-B6z85uQP.js → plain-DG7cy0Kk.js} +7 -2
  42. package/dist/plain-DG7cy0Kk.js.map +1 -0
  43. package/dist/{prepare-B4ysyG6H.js → prepare-BM93i3Rm.js} +9 -5
  44. package/dist/prepare-BM93i3Rm.js.map +1 -0
  45. package/dist/{project-MD4dIo2X.js → project-zlQStwsf.js} +2 -2
  46. package/dist/{project-MD4dIo2X.js.map → project-zlQStwsf.js.map} +1 -1
  47. package/dist/{run-Ddwrxe1O.js → run-DMM2YlP3.js} +3 -13
  48. package/dist/run-DMM2YlP3.js.map +1 -0
  49. package/dist/{theme-3Fc_LI-2.js → theme-yS2h5dd_.js} +25 -1
  50. package/dist/theme-yS2h5dd_.js.map +1 -0
  51. package/dist/tsc-C5l5TQGC.js +3 -0
  52. package/dist/{tsc-DO7D_uCl.js → tsc-puVAcwmo.js} +24 -5
  53. package/dist/tsc-puVAcwmo.js.map +1 -0
  54. package/dist/{tsdown-CD68G8_U.js → tsdown-Dwt3JhPw.js} +2 -2
  55. package/dist/{tsdown-CD68G8_U.js.map → tsdown-Dwt3JhPw.js.map} +1 -1
  56. package/dist/{tui-BRddrLQP.js → tui-nldGSD4c.js} +913 -141
  57. package/dist/tui-nldGSD4c.js.map +1 -0
  58. package/dist/{vite-CLyB6wl7.js → vite-YZUtv2Hk.js} +2 -2
  59. package/dist/{vite-CLyB6wl7.js.map → vite-YZUtv2Hk.js.map} +1 -1
  60. package/package.json +5 -4
  61. package/dist/_shared-B-hRZDRS.js.map +0 -1
  62. package/dist/commands-ExM8HILu.js.map +0 -1
  63. package/dist/dev-4wpEQC7l.js.map +0 -1
  64. package/dist/diagnostics-CZBVxlpj.js.map +0 -1
  65. package/dist/errors-Crt4hu0b.js.map +0 -1
  66. package/dist/hooks-H9kjLCMk.js +0 -148
  67. package/dist/hooks-H9kjLCMk.js.map +0 -1
  68. package/dist/info-CdiQajgn.js.map +0 -1
  69. package/dist/log-buffer-CY82aqnb.js +0 -72
  70. package/dist/log-buffer-CY82aqnb.js.map +0 -1
  71. package/dist/plain-B6z85uQP.js.map +0 -1
  72. package/dist/prepare-B4ysyG6H.js.map +0 -1
  73. package/dist/run-Ddwrxe1O.js.map +0 -1
  74. package/dist/theme-3Fc_LI-2.js.map +0 -1
  75. package/dist/tsc-DO7D_uCl.js.map +0 -1
  76. package/dist/tsc-YnAWKn3n.js +0 -3
  77. package/dist/tui-BRddrLQP.js.map +0 -1
  78. package/dist/tunnel-DT1FH6oA.js +0 -175
  79. package/dist/tunnel-DT1FH6oA.js.map +0 -1
@@ -1,39 +1,94 @@
1
1
  import { i as shouldUseColor, n as prefersReducedMotion, r as resolveOutputMode } from "./output-mode-Bp7Da8vJ.js";
2
- import { i as renderCrashReport, t as ExitCode } from "./errors-Crt4hu0b.js";
3
- import { t as cliDiagnostics } from "./diagnostics-CZBVxlpj.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 { i as resolveFromProject, r as resolveBinary } from "./project-MD4dIo2X.js";
6
- import { i as createLineSplitter, n as resolveTscBinary, r as ProcessSupervisor } from "./tsc-DO7D_uCl.js";
7
- import { n as createBuilder, t as Locales } from "./locales-C7X8VNmK.js";
8
- import { a as projectArgs, n as loadProject, o as resolveCwd, r as withProjectEnv, t as applyEnvOptions } from "./hooks-H9kjLCMk.js";
9
- import { n as reportWarnings, t as prepareProject } from "./_shared-B-hRZDRS.js";
10
- import { t as Tunnel } from "./tunnel-DT1FH6oA.js";
11
- import { n as classifyAppLine, t as LogBuffer } from "./log-buffer-CY82aqnb.js";
2
+ import { i as renderCrashReport, t as ExitCode } from "./errors-COFWKKKw.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";
15
- import { pathToFileURL } from "node:url";
14
+ import { createWriteStream, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
15
+ import { basename, dirname, extname, join, sep } from "node:path";
16
16
  import { displayPath } from "@wolfstar/schema";
17
17
  import { EventEmitter } from "node:events";
18
- import { envModuleSource } from "@wolfstar/vite-server/internal";
19
18
  import { spawn } from "node:child_process";
19
+ import { stripVTControlCharacters } from "node:util";
20
+ import { createHash } from "node:crypto";
20
21
  import { lookup } from "node:dns/promises";
21
22
 
22
- //#region src/utils/env-import.ts
23
- /** Build tools whose output never passes through the entry transform that registers `env`. */
24
- const UNTRANSFORMED_TOOLS = /* @__PURE__ */ new Set(["none", "tsc"]);
23
+ //#region src/dev/changed-files.ts
24
+ const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git"]);
25
25
  /**
26
- * `node` arguments that register `env` before the entry runs, for the build tools the entry transform cannot reach.
27
- * The preload is a `data:` URL, so `.stars/` gets no generated runtime file; a `data:` URL cannot resolve bare
28
- * specifiers, so `@wolfstar/env-utilities` is resolved from the project first. When it cannot be resolved nothing is
29
- * preloaded, and the bot's own import of it reports the missing install.
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.
30
28
  */
31
- function envImportArgs(config) {
32
- if (!config.env.enabled || !UNTRANSFORMED_TOOLS.has(config.build.tool)) return [];
33
- const resolved = resolveFromProject(config.root, "@wolfstar/env-utilities");
34
- if (resolved === null) return [];
35
- const source = envModuleSource(config.env.options, pathToFileURL(resolved).href);
36
- return ["--import", `data:text/javascript,${encodeURIComponent(source)}`];
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}`);
37
92
  }
38
93
 
39
94
  //#endregion
@@ -252,6 +307,8 @@ var DevService = class extends EventEmitter {
252
307
  typechecker;
253
308
  tunnel;
254
309
  locales;
310
+ /** Whether something can answer a {@link DevPrompt}: the interactive UI sets it, the plain renderer cannot. */
311
+ promptable = false;
255
312
  #hooks;
256
313
  /** Runs hooks one at a time, so an async `build:done` settles before the next build's hooks or a restart. */
257
314
  #hookChain = Promise.resolve();
@@ -272,6 +329,23 @@ var DevService = class extends EventEmitter {
272
329
  startedAt: Date.now(),
273
330
  readyMs: null
274
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;
275
349
  constructor(config, options) {
276
350
  super();
277
351
  this.config = config;
@@ -312,6 +386,15 @@ var DevService = class extends EventEmitter {
312
386
  });
313
387
  this.supervisor.on("stdout", (line) => this.log("app", classifyAppLine(line, "info"), line));
314
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
+ });
315
398
  this.supervisor.on("error", (error) => this.log("stars", "error", `Failed to start the bot: ${error.message}`));
316
399
  this.supervisor.on("exit", (exit) => this.#onExit(exit));
317
400
  this.typechecker.on("log", (level, text) => this.log("tsc", level, text));
@@ -344,14 +427,20 @@ var DevService = class extends EventEmitter {
344
427
  typecheck: this.typechecker.state,
345
428
  typeErrors: this.typechecker.errors,
346
429
  tunnel: this.tunnel.state,
347
- 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
348
436
  };
349
437
  }
350
- log(source, level, text) {
438
+ log(source, level, text, extra = {}) {
351
439
  this.logs.push({
352
440
  source,
353
441
  level,
354
- text
442
+ text,
443
+ ...extra
355
444
  });
356
445
  }
357
446
  /**
@@ -394,10 +483,51 @@ var DevService = class extends EventEmitter {
394
483
  message: "starting the bot"
395
484
  };
396
485
  this.#health = "unknown";
486
+ this.#paused = false;
397
487
  this.supervisor.start();
398
488
  this.#emitStatus();
399
489
  });
400
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
+ }
401
531
  /** Opens or closes the public tunnel (used by the `t` key). */
402
532
  toggleTunnel() {
403
533
  return this.tunnel.toggle();
@@ -434,6 +564,7 @@ var DevService = class extends EventEmitter {
434
564
  this.logs.clear();
435
565
  }
436
566
  #onBuildSuccess(outcome) {
567
+ const hot = this.#hotReloadable();
437
568
  try {
438
569
  this.locales.copy();
439
570
  } catch (error) {
@@ -455,8 +586,35 @@ var DevService = class extends EventEmitter {
455
586
  if (this.#stopped) return;
456
587
  this.typechecker.check();
457
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
+ }
458
596
  this.#scheduleRestart(this.supervisor.state === "idle" ? "initial" : "build");
459
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
+ }
460
618
  #onBuildFailure(outcome) {
461
619
  this.#lastBuild = outcome;
462
620
  this.#queueHook("build:done", outcome, this.config);
@@ -467,6 +625,10 @@ var DevService = class extends EventEmitter {
467
625
  #onExit(exit) {
468
626
  this.#lastExit = exit;
469
627
  this.#health = "unknown";
628
+ this.#ready = false;
629
+ this.#port = null;
630
+ this.#hmrPaths = [];
631
+ this.#pieces.clear();
470
632
  if (!exit.requested) {
471
633
  const how = exit.signal ? `signal ${exit.signal}` : `code ${exit.code}`;
472
634
  this.log("stars", exit.code === 0 ? "warn" : "error", `The bot exited with ${how}, waiting for changes (press r to restart)`);
@@ -474,6 +636,7 @@ var DevService = class extends EventEmitter {
474
636
  this.#emitStatus();
475
637
  }
476
638
  #scheduleRestart(reason) {
639
+ if (this.#paused) return;
477
640
  this.#pendingReason = reason;
478
641
  this.#clearRestartTimer();
479
642
  this.#restartTimer = setTimeout(() => {
@@ -522,6 +685,141 @@ var DevService = class extends EventEmitter {
522
685
  readyMs: this.#progress.readyMs ?? Date.now() - this.#progress.startedAt
523
686
  };
524
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
+ }
525
823
  /**
526
824
  * Runs a hook behind the ones already running (see {@link DevService.#hookChain}), e.g. `dev:close` so shutdown
527
825
  * waits for an unfinished `build:done`.
@@ -565,6 +863,8 @@ function createSupervisor(config) {
565
863
  command: process.execPath,
566
864
  args: [
567
865
  ...envImportArgs(config),
866
+ ...moduleImportArgs(config),
867
+ ...bridgeImportArgs(config),
568
868
  ...config.dev.nodeArgs,
569
869
  config.build.output,
570
870
  ...config.dev.args
@@ -577,9 +877,34 @@ function createSupervisor(config) {
577
877
  NODE_ENV: "development",
578
878
  FORCE_COLOR: process.env.NO_COLOR !== void 0 ? void 0 : process.env.FORCE_COLOR ?? (process.stdout.isTTY ? "1" : void 0)
579
879
  },
580
- killTimeout: config.dev.killTimeout
880
+ killTimeout: config.dev.killTimeout,
881
+ ipc: true
581
882
  });
582
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
+ }
583
908
  function describeReason(reason) {
584
909
  switch (reason) {
585
910
  case "initial": return "first build";
@@ -622,8 +947,8 @@ async function withResolvedLocalhost(url) {
622
947
  //#region src/dev/log-file.ts
623
948
  /**
624
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
625
- * over the alternate screen and its scrollback disappears on exit). Seedcord keeps a `logs/` directory for the same
626
- * 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}).
627
952
  */
628
953
  var LogFileWriter = class {
629
954
  file;
@@ -649,20 +974,57 @@ var LogFileWriter = class {
649
974
  stream.end();
650
975
  }
651
976
  #write = (entry) => {
652
- this.#stream?.write(`${format(entry)}\n`);
977
+ this.#stream?.write(`${formatLogLine(entry)}\n`);
653
978
  };
654
979
  };
655
- function format(entry) {
656
- 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`);
657
996
  }
658
997
 
659
998
  //#endregion
660
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
+ ];
661
1012
  async function runDev(options) {
662
1013
  if (options.theme !== void 0 && !isThemeSetting(options.theme)) throw cliDiagnostics.INVALID_THEME({
663
1014
  theme: options.theme,
664
1015
  themes: THEME_SETTINGS.join(", ")
665
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
+ });
666
1028
  process.env.NODE_ENV = "development";
667
1029
  const project = await loadProject({
668
1030
  cwd: resolveCwd(options),
@@ -686,24 +1048,33 @@ async function runDev(options) {
686
1048
  builder,
687
1049
  hooks
688
1050
  });
689
- await reportWarnings(config, (text) => service.log("stars", "warn", text));
690
- const logFile = config.dev.logFile ? new LogFileWriter(config.dev.logFile, service.logs) : null;
691
- logFile?.open();
692
- const renderer = mode === "tui" ? (await import("./tui-BRddrLQP.js")).createTuiRenderer(service, {
1051
+ await reportWarnings(config, (text) => service.log("stars", "warn", text), { production: false });
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, {
693
1059
  color,
694
1060
  theme,
1061
+ layout,
1062
+ filter,
695
1063
  reducedMotion: prefersReducedMotion(),
696
1064
  onThemeSave: (setting) => {
697
1065
  if (!saveTheme(setting)) service.log("stars", "warn", "Could not save the theme preference.");
698
1066
  }
699
- }) : (await import("./plain-B6z85uQP.js")).createPlainRenderer(service, { color });
1067
+ }) : (await import("./plain-DG7cy0Kk.js")).createPlainRenderer(service, {
1068
+ color,
1069
+ filter
1070
+ });
700
1071
  let exiting = null;
701
1072
  const shutdown = (code) => {
702
1073
  exiting ??= (async () => {
703
1074
  renderer.stop();
704
1075
  await service.runHook("dev:close", config);
705
1076
  await service.stop();
706
- logFile?.close();
1077
+ for (const file of logFiles) file.close();
707
1078
  process.exit(code);
708
1079
  })();
709
1080
  return exiting;
@@ -765,18 +1136,34 @@ var dev_default = defineCommand({
765
1136
  theme: {
766
1137
  type: "string",
767
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)`
768
1151
  }
769
1152
  },
770
- async run({ args }) {
1153
+ async run({ args, rawArgs }) {
1154
+ const channel = collectFlag(rawArgs, "channel");
771
1155
  await runDev({
772
1156
  config: args.config,
773
1157
  cwd: args.cwd,
774
1158
  tui: args.tui ? void 0 : false,
775
- theme: args.theme
1159
+ theme: args.theme,
1160
+ layout: args.layout,
1161
+ channel: channel.length > 0 ? channel : void 0,
1162
+ level: args.level
776
1163
  });
777
1164
  }
778
1165
  });
779
1166
 
780
1167
  //#endregion
781
1168
  export { dev_default as default };
782
- //# sourceMappingURL=dev-4wpEQC7l.js.map
1169
+ //# sourceMappingURL=dev-oPPVR7d_.js.map