seedcord 0.16.0 → 0.17.0

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.
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
- import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-pMIoZiL7.mjs";
1
+ import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-C9EuHVMn.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "@commander-js/extra-typings";
4
- import { LEVEL_COLOR, Logger, LoggerChannelRegistry } from "@seedcord/logger";
4
+ import { LEVEL_COLOR, Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
5
5
  import { formatBody, installNodeDefaults } from "@seedcord/logger/node";
6
6
  import { SeedcordErrorCode, isSeedcordError } from "@seedcord/errors";
7
7
  import { existsSync } from "node:fs";
8
8
  import { SeedcordError, validateDiscordToken } from "@seedcord/errors/internal";
9
- import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
10
  import { pathToFileURL } from "node:url";
11
11
  import { createJiti } from "jiti";
12
12
  import { tsImport } from "tsx/esm/api";
@@ -14,22 +14,25 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
14
14
  import { spawn } from "node:child_process";
15
15
  import { SeedcordBrand } from "@seedcord/types/internal";
16
16
  import { isTsOrJsFile } from "@seedcord/utils/node";
17
- import { ApplicationCommandOptionType, ApplicationCommandType, Routes } from "discord-api-types/v10";
17
+ import { ApplicationCommandOptionType, ApplicationCommandType, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
18
18
  import { routeLeavesOf } from "@seedcord/utils/internal";
19
19
  import { Envapter } from "envapt";
20
- import { REST } from "@discordjs/rest";
20
+ import { DiscordAPIError, REST } from "@discordjs/rest";
21
21
  import chalk from "chalk";
22
22
  import { autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, spinner } from "@clack/prompts";
23
23
  import { createInterface } from "node:readline";
24
24
  import { Box, Text, measureElement, render, useAnimation, useInput, useWindowSize } from "ink";
25
25
  import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
26
26
  import { TypedEventEmitter } from "@seedcord/event-emitter";
27
+ import wrapAnsi from "wrap-ansi";
27
28
  import Spinner from "ink-spinner";
28
29
  import { assertNever } from "@seedcord/utils";
30
+ import { settleWithin } from "@seedcord/core/node/internal";
29
31
  import { createServer, createServerModuleRunner, defineConfig, mergeConfig } from "vite";
30
32
  import { EvaluatedModules } from "vite/module-runner";
31
33
  import { wrapHot } from "@seedcord/core/internal";
32
34
  import { minimatch } from "minimatch";
35
+ import { createServer as createServer$1 } from "node:net";
33
36
 
34
37
  //#region src/core/BaseCommand.ts
35
38
  var BaseCommand = class {
@@ -83,11 +86,24 @@ function validateHmr(value) {
83
86
  if (value.restart !== void 0 && !isStringArray(value.restart)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidHmrRestart);
84
87
  if (value.rollback !== void 0 && typeof value.rollback !== "boolean") throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidHmrRollback);
85
88
  }
89
+ function validateTunnel(value) {
90
+ if (value === void 0 || typeof value === "boolean") return;
91
+ if (typeof value !== "string" || URL.parse(value)?.protocol !== "https:") throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidTunnel);
92
+ }
93
+ function resolveTunnel(value) {
94
+ if (value === false) return { mode: "off" };
95
+ if (typeof value === "string") return {
96
+ mode: "url",
97
+ url: value
98
+ };
99
+ return { mode: "quick" };
100
+ }
86
101
  function validateConfig(raw) {
87
102
  if (!isPlainObject(raw)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidExport);
88
103
  if (typeof raw.instance !== "string" || raw.instance.length === 0) throw new SeedcordError(SeedcordErrorCode.CliConfigMissingInstance);
89
104
  if (typeof raw.entry !== "string" || raw.entry.length === 0) throw new SeedcordError(SeedcordErrorCode.CliConfigMissingEntry);
90
105
  if (!isOptionalString(raw.root)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidRoot);
106
+ validateTunnel(raw.tunnel);
91
107
  validateBuild(raw.build);
92
108
  validateHmr(raw.hmr);
93
109
  }
@@ -109,14 +125,14 @@ var ConfigLoader = class {
109
125
  this.assertEntryWithinRoot(root, entry);
110
126
  const build = this.resolveBuildOptions(configDir, config.build);
111
127
  const tsconfig = config.hmr?.tsconfig ? resolve(root, config.hmr.tsconfig) : void 0;
112
- this.logger.info(`Loaded configuration from ${configPath}`);
113
- this.logger.debug(`Resolved root: ${root}`);
114
- this.logger.debug(`Resolved instance: ${instance}`);
115
- this.logger.debug(`Resolved entry: ${entry}`);
116
- this.logger.debug(`Resolved build outDir: ${build.outDir}`);
117
- if (build.tsconfig) this.logger.debug(`Resolved build tsconfig: ${build.tsconfig}`);
118
- if (tsconfig) this.logger.debug(`Resolved dev tsconfig: ${tsconfig}`);
119
- this.logger.debug(`Resolved bootstrap: ${build.bootstrap}`);
128
+ this.logger.debug(`Loaded configuration from ${configPath}`);
129
+ this.logger.trace(`Resolved root: ${root}`);
130
+ this.logger.trace(`Resolved instance: ${instance}`);
131
+ this.logger.trace(`Resolved entry: ${entry}`);
132
+ this.logger.trace(`Resolved build outDir: ${build.outDir}`);
133
+ if (build.tsconfig) this.logger.trace(`Resolved build tsconfig: ${build.tsconfig}`);
134
+ if (tsconfig) this.logger.trace(`Resolved dev tsconfig: ${tsconfig}`);
135
+ this.logger.trace(`Resolved bootstrap: ${build.bootstrap}`);
120
136
  return {
121
137
  instance,
122
138
  root,
@@ -124,6 +140,7 @@ var ConfigLoader = class {
124
140
  entry,
125
141
  build,
126
142
  tsconfig,
143
+ tunnel: resolveTunnel(config.tunnel),
127
144
  hmr: config.hmr
128
145
  };
129
146
  }
@@ -167,7 +184,7 @@ var ConfigLocator = class {
167
184
  for (const candidate of SEEDCORD_CONFIG_FILENAMES) {
168
185
  const fullPath = join(normalizedBase, candidate);
169
186
  if (existsSync(fullPath)) {
170
- this.logger.debug(`Found config at ${fullPath}`);
187
+ this.logger.trace(`Found config at ${fullPath}`);
171
188
  return fullPath;
172
189
  }
173
190
  }
@@ -177,7 +194,7 @@ var ConfigLocator = class {
177
194
 
178
195
  //#endregion
179
196
  //#region src/core/modules/RuntimeModuleLoader.ts
180
- const TS_EXTENSIONS = new Set([
197
+ const TS_EXTENSIONS = /* @__PURE__ */ new Set([
181
198
  ".ts",
182
199
  ".tsx",
183
200
  ".mts",
@@ -438,7 +455,11 @@ var BuildRunner = class BuildRunner {
438
455
  }
439
456
  static create(logger) {
440
457
  const moduleLoader = new RuntimeModuleLoader();
441
- return new BuildRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), new TypeScriptProjectBuilder(logger), new BootstrapWriter(logger), logger);
458
+ const locator = new ConfigLocator(logger);
459
+ const configLoader = new ConfigLoader(moduleLoader, logger);
460
+ const builder = new TypeScriptProjectBuilder(logger);
461
+ const bootstrapWriter = new BootstrapWriter(logger);
462
+ return new BuildRunner(locator, configLoader, builder, bootstrapWriter, logger);
442
463
  }
443
464
  async run() {
444
465
  const config = await this.loadConfig();
@@ -521,9 +542,7 @@ var AugmentationBuilder = class {
521
542
  this.collectContextMenu("message", json, command.sourceFile, sourceByMessageName);
522
543
  break;
523
544
  case void 0:
524
- case ApplicationCommandType.ChatInput:
525
- this.collectSlash(json, command.sourceFile, slash, sourceByRoute);
526
- break;
545
+ case ApplicationCommandType.ChatInput: this.collectSlash(json, command.sourceFile, slash, sourceByRoute);
527
546
  }
528
547
  }
529
548
  const userContextMenus = [...sourceByUserName.keys()];
@@ -588,7 +607,7 @@ var AugmentationBuilder = class {
588
607
  //#endregion
589
608
  //#region src/commands/codegen/renderAugmentation.ts
590
609
  const BANNER = `// Generated by \`seedcord codegen\`. Do not edit by hand.
591
- // Run \`seedcord codegen\` after changing your commands or emoji config.`;
610
+ // Run \`seedcord codegen\` after changing your commands, plugins, or emoji config.`;
592
611
  const BOT_BINDING = "Bot";
593
612
  const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
594
613
  const CONTROL_CHAR = 32;
@@ -693,7 +712,10 @@ var CodegenRunner = class CodegenRunner {
693
712
  }
694
713
  static create(logger) {
695
714
  const moduleLoader = new RuntimeModuleLoader();
696
- return new CodegenRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), moduleLoader, new AugmentationBuilder(logger), logger);
715
+ const locator = new ConfigLocator(logger);
716
+ const configLoader = new ConfigLoader(moduleLoader, logger);
717
+ const generator = new AugmentationBuilder(logger);
718
+ return new CodegenRunner(locator, configLoader, moduleLoader, generator, logger);
697
719
  }
698
720
  async run(check) {
699
721
  const config = await this.loadConfig();
@@ -751,8 +773,9 @@ var CodegenRunner = class CodegenRunner {
751
773
  }
752
774
  }
753
775
  async resolveInstance(config) {
754
- this.logger.info("Loading instance to resolve the commands directory");
755
- const instance = resolveDefaultExport(await this.moduleLoader.importModule(config.instance));
776
+ this.logger.debug("Loading instance to resolve the commands directory");
777
+ const module = await this.moduleLoader.importModule(config.instance);
778
+ const instance = resolveDefaultExport(module);
756
779
  if (!this.isSeedcordInstance(instance)) throw new SeedcordError(SeedcordErrorCode.CliInstanceInvalid);
757
780
  const commandsPath = instance.config.bot.commands.path;
758
781
  return {
@@ -1018,7 +1041,7 @@ async function confirm$1(opts) {
1018
1041
  }
1019
1042
 
1020
1043
  //#endregion
1021
- //#region \0@oxc-project+runtime@0.135.0/helpers/esm/usingCtx.js
1044
+ //#region \0@oxc-project+runtime@0.142.0/helpers/esm/usingCtx.js
1022
1045
  function _usingCtx() {
1023
1046
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1024
1047
  var n = Error();
@@ -1148,7 +1171,8 @@ var InteractivePresenter = class {
1148
1171
  }
1149
1172
  }
1150
1173
  preview(scan) {
1151
- note(groupByGuild(scan.flagged).map((group) => `${chalk.bold(group.guildName)}\n${group.commands.map((c) => ` ${c.name}`).join("\n")}`).join("\n\n"), `Will delete ${plural(scan.flagged.length, "guild command")} (nothing deleted yet)`);
1174
+ const body = groupByGuild(scan.flagged).map((group) => `${chalk.bold(group.guildName)}\n${group.commands.map((c) => ` ${c.name}`).join("\n")}`).join("\n\n");
1175
+ note(body, `Will delete ${plural(scan.flagged.length, "guild command")} (nothing deleted yet)`);
1152
1176
  if (scan.skipped.length > 0) log.warn(skippedSummary(scan.skipped));
1153
1177
  }
1154
1178
  largeBotGuard(guildCount) {
@@ -1421,6 +1445,20 @@ var CommandsCommand = class extends BaseCommand {
1421
1445
  }
1422
1446
  };
1423
1447
 
1448
+ //#endregion
1449
+ //#region src/ui/components/primitives/Rule.tsx
1450
+ function Rule() {
1451
+ return /* @__PURE__ */ React.createElement(Box, {
1452
+ flexShrink: 0,
1453
+ borderStyle: "single",
1454
+ borderTop: true,
1455
+ borderBottom: false,
1456
+ borderLeft: false,
1457
+ borderRight: false,
1458
+ borderDimColor: true
1459
+ });
1460
+ }
1461
+
1424
1462
  //#endregion
1425
1463
  //#region src/ui/filterCursor.ts
1426
1464
  const INITIAL_CURSOR = {
@@ -1453,16 +1491,19 @@ function focusedIn(cursor, group, count) {
1453
1491
  //#endregion
1454
1492
  //#region src/ui/hooks/useDevState.ts
1455
1493
  function useDevState(store) {
1456
- return useSyncExternalStore(useCallback((onChange) => {
1494
+ const subscribe = useCallback((onChange) => {
1457
1495
  store.on("change", onChange);
1458
1496
  return () => {
1459
1497
  store.off("change", onChange);
1460
1498
  };
1461
- }, [store]), useCallback(() => store.getState(), [store]));
1499
+ }, [store]);
1500
+ const getSnapshot = useCallback(() => store.getState(), [store]);
1501
+ return useSyncExternalStore(subscribe, getSnapshot);
1462
1502
  }
1463
1503
 
1464
1504
  //#endregion
1465
1505
  //#region src/ui/stores/LogStore.ts
1506
+ const STACK_FRAME = /^\s+at\s/;
1466
1507
  const UPDATE_DEBOUNCE_MS = 30;
1467
1508
  const ESC = String.fromCharCode(27);
1468
1509
  var LogStore = class LogStore extends TypedEventEmitter {
@@ -1494,15 +1535,19 @@ var LogStore = class LogStore extends TypedEventEmitter {
1494
1535
  }
1495
1536
  onLog(record) {
1496
1537
  const lines = formatBody(record).split(/\r\n|\r|\n/);
1497
- for (const [index, line] of lines.entries()) this.buffer.push({
1498
- id: this.nextId++,
1499
- channel: record.channel,
1500
- level: record.level,
1501
- label: record.label,
1502
- head: index === 0,
1503
- text: line.replaceAll(/\p{Cc}/gu, (char) => char === ESC ? char : ""),
1504
- timestamp: record.timestamp
1505
- });
1538
+ for (const [index, line] of lines.entries()) {
1539
+ const text = line.replaceAll(/\p{Cc}/gu, (char) => char === ESC ? char : "");
1540
+ this.buffer.push({
1541
+ id: this.nextId++,
1542
+ channel: record.channel,
1543
+ level: record.level,
1544
+ label: record.label,
1545
+ head: index === 0,
1546
+ frame: index > 0 && STACK_FRAME.test(text),
1547
+ text,
1548
+ timestamp: record.timestamp
1549
+ });
1550
+ }
1506
1551
  this.scheduleUpdate();
1507
1552
  }
1508
1553
  getLogs() {
@@ -1559,12 +1604,14 @@ function filterLogs(logs, channels, levels) {
1559
1604
  }
1560
1605
  function useLogs(channels, levels) {
1561
1606
  const store = LogStore.instance;
1562
- const allLogs = useSyncExternalStore(useCallback((cb) => {
1607
+ const subscribe = useCallback((cb) => {
1563
1608
  store.on("change", cb);
1564
1609
  return () => {
1565
1610
  store.off("change", cb);
1566
1611
  };
1567
- }, [store]), useCallback(() => store.getLogs(), [store]));
1612
+ }, [store]);
1613
+ const getSnapshot = useCallback(() => store.getLogs(), [store]);
1614
+ const allLogs = useSyncExternalStore(subscribe, getSnapshot);
1568
1615
  return useMemo(() => filterLogs(allLogs, channels ?? EMPTY_CHANNELS, levels ?? EMPTY_LEVELS), [
1569
1616
  allLogs,
1570
1617
  channels,
@@ -1573,22 +1620,22 @@ function useLogs(channels, levels) {
1573
1620
  }
1574
1621
 
1575
1622
  //#endregion
1576
- //#region src/ui/hooks/useMeasuredHeight.ts
1577
- /**
1578
- * The rendered height of a box, 0 until the first measurement lands.
1579
- *
1580
- * No dep array. A box moves for reasons its caller cannot enumerate, and one of them can be derived from the
1581
- * height itself, which rules out a dependency list. The equality bail is what keeps the per-render measurement
1582
- * from forming an update chain.
1583
- */
1584
- function useMeasuredHeight(ref) {
1585
- const [height, setHeight] = useState(0);
1623
+ //#region src/ui/hooks/useMeasuredBox.ts
1624
+ const UNMEASURED = {
1625
+ width: 0,
1626
+ height: 0
1627
+ };
1628
+ function useMeasuredBox(ref) {
1629
+ const [box, setBox] = useState(UNMEASURED);
1586
1630
  useLayoutEffect(() => {
1587
1631
  if (!ref.current) return;
1588
- const next = measureElement(ref.current).height;
1589
- setHeight((prev) => prev === next ? prev : next);
1632
+ const { width, height } = measureElement(ref.current);
1633
+ setBox((prev) => prev.width === width && prev.height === height ? prev : {
1634
+ width,
1635
+ height
1636
+ });
1590
1637
  });
1591
- return height;
1638
+ return box;
1592
1639
  }
1593
1640
 
1594
1641
  //#endregion
@@ -1856,21 +1903,20 @@ function toggleLevel(prev, level) {
1856
1903
  return base.size === FILTER_LEVELS.length ? /* @__PURE__ */ new Set() : base;
1857
1904
  }
1858
1905
  function soloChannel(prev, channel) {
1859
- return prev.size === 1 && prev.has(channel) ? /* @__PURE__ */ new Set() : new Set([channel]);
1906
+ return prev.size === 1 && prev.has(channel) ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([channel]);
1860
1907
  }
1861
1908
  function soloLevel(prev, level) {
1862
- return prev.size === 1 && prev.has(level) ? /* @__PURE__ */ new Set() : new Set([level]);
1909
+ return prev.size === 1 && prev.has(level) ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([level]);
1863
1910
  }
1864
1911
  function quit(ctx) {
1865
1912
  ctx.store.beginQuit();
1866
1913
  ctx.onQuit?.();
1867
1914
  }
1868
1915
  function handleQuitSignal(ctx) {
1869
- if (ctx.key.ctrl && ctx.input === "c") {
1870
- quit(ctx);
1871
- return true;
1872
- }
1873
- return false;
1916
+ if (!ctx.key.ctrl || ctx.input !== "c") return false;
1917
+ if (ctx.state.quitArmed) quit(ctx);
1918
+ else ctx.store.setQuitArmed(true);
1919
+ return true;
1874
1920
  }
1875
1921
  function handlePrompt(ctx) {
1876
1922
  if (!ctx.state.commandUpdatePrompt) return false;
@@ -1934,14 +1980,14 @@ function handleActions(ctx) {
1934
1980
  store.beginRestart();
1935
1981
  ctx.onRestart?.();
1936
1982
  break;
1937
- case "l":
1983
+ case "c":
1938
1984
  LogStore.instance.clear();
1939
1985
  ctx.scroll.toBottom();
1940
- break;
1941
1986
  }
1942
1987
  }
1943
1988
  function dispatchHotkey(ctx) {
1944
1989
  if (handleQuitSignal(ctx)) return;
1990
+ if (ctx.state.quitArmed) ctx.store.setQuitArmed(false);
1945
1991
  if (handlePrompt(ctx)) return;
1946
1992
  if (handleFilters(ctx)) return;
1947
1993
  if (handleScroll(ctx)) return;
@@ -1968,20 +2014,6 @@ function NotificationStack({ notices }) {
1968
2014
  }, notices.map((notice) => /* @__PURE__ */ React.createElement(Fragment, { key: notice.key }, notice.card)));
1969
2015
  }
1970
2016
 
1971
- //#endregion
1972
- //#region src/ui/components/primitives/Rule.tsx
1973
- function Rule() {
1974
- return /* @__PURE__ */ React.createElement(Box, {
1975
- flexShrink: 0,
1976
- borderStyle: "single",
1977
- borderTop: true,
1978
- borderBottom: false,
1979
- borderLeft: false,
1980
- borderRight: false,
1981
- borderDimColor: true
1982
- });
1983
- }
1984
-
1985
2017
  //#endregion
1986
2018
  //#region src/ui/format.ts
1987
2019
  const MS_PER_SECOND = 1e3;
@@ -2005,6 +2037,74 @@ function formatClock(ts) {
2005
2037
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
2006
2038
  }
2007
2039
 
2040
+ //#endregion
2041
+ //#region src/ui/logRows.ts
2042
+ const DOT_WIDTH = 1;
2043
+ const BAR_WIDTH = 1;
2044
+ const MIN_TEXT_WIDTH = 8;
2045
+ const messageColumn = (labelWidth) => 13 + labelWidth + 1 + DOT_WIDTH + 1;
2046
+ function isBlockLine(entry) {
2047
+ return entry !== void 0 && entry.level !== "error";
2048
+ }
2049
+ function opensBlock(logs, index) {
2050
+ const log = logs[index];
2051
+ const next = logs[index + 1];
2052
+ return log?.head === true && isBlockLine(log) && next !== void 0 && !next.head && isBlockLine(next);
2053
+ }
2054
+ function closesBlock(logs, index) {
2055
+ const log = logs[index];
2056
+ const next = logs[index + 1];
2057
+ return log !== void 0 && !log.head && isBlockLine(log) && (next === void 0 || next.head);
2058
+ }
2059
+ function prevClosedBlock(logs, index) {
2060
+ const prev = logs[index - 1];
2061
+ return prev !== void 0 && !prev.head && isBlockLine(prev);
2062
+ }
2063
+ function expandRows(logs) {
2064
+ const rows = [];
2065
+ for (const [index, entry] of logs.entries()) {
2066
+ if (opensBlock(logs, index) && !prevClosedBlock(logs, index)) rows.push({
2067
+ kind: "rule",
2068
+ key: `top-${entry.id}`
2069
+ });
2070
+ rows.push({
2071
+ kind: "entry",
2072
+ key: String(entry.id),
2073
+ entry,
2074
+ text: entry.text,
2075
+ first: true
2076
+ });
2077
+ if (closesBlock(logs, index)) rows.push({
2078
+ kind: "rule",
2079
+ key: `bottom-${entry.id}`
2080
+ });
2081
+ }
2082
+ return rows;
2083
+ }
2084
+ function textWidth(entry, width, labelWidth) {
2085
+ return width - (!entry.head && entry.level === "error" ? BAR_WIDTH : messageColumn(labelWidth));
2086
+ }
2087
+ function wrapRows(rows, width, labelWidth) {
2088
+ return rows.flatMap((row) => {
2089
+ if (row.kind === "rule") return row;
2090
+ const available = textWidth(row.entry, width, labelWidth);
2091
+ if (available < MIN_TEXT_WIDTH) return row;
2092
+ return wrapAnsi(row.text, available, {
2093
+ hard: true,
2094
+ trim: false
2095
+ }).split("\n").map((text, index) => ({
2096
+ kind: "entry",
2097
+ key: `${row.key}:${String(index)}`,
2098
+ entry: row.entry,
2099
+ text: index === 0 ? text : text.replace(/^ +/, ""),
2100
+ first: index === 0
2101
+ }));
2102
+ });
2103
+ }
2104
+ function rowKey(row) {
2105
+ return row.key;
2106
+ }
2107
+
2008
2108
  //#endregion
2009
2109
  //#region src/ui/components/primitives/ScrollableLogView.tsx
2010
2110
  const MIN_LOG_LINES = 5;
@@ -2014,8 +2114,6 @@ const BAR = "▌";
2014
2114
  const GUIDE = "│";
2015
2115
  const ELLIPSIS = "…";
2016
2116
  const CHIP_TEXT = "#1a1a1a";
2017
- const DOT_WIDTH = 1;
2018
- const messageColumn = (labelWidth) => 13 + labelWidth + 1 + DOT_WIDTH + 1;
2019
2117
  const LEVEL_LETTER = {
2020
2118
  error: "E",
2021
2119
  warn: "W",
@@ -2041,23 +2139,26 @@ function Prefix({ log, labelWidth }) {
2041
2139
  const label = truncate(log.label, labelWidth).padStart(labelWidth);
2042
2140
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Chip, { level: log.level }), " ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, formatClock(log.timestamp)), " ", /* @__PURE__ */ React.createElement(Text, null, label), " ", /* @__PURE__ */ React.createElement(Text, { color: channelColor(log.channel) }, DOT));
2043
2141
  }
2044
- function HeadLine({ log, labelWidth }) {
2142
+ function HeadLine({ log, text, labelWidth }) {
2045
2143
  const wash = WASH[log.level];
2046
2144
  if (wash) return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, { backgroundColor: wash }, /* @__PURE__ */ React.createElement(Prefix, {
2047
2145
  log,
2048
2146
  labelWidth
2049
- })), " ", log.text);
2147
+ })), " ", text);
2050
2148
  return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Prefix, {
2051
2149
  log,
2052
2150
  labelWidth
2053
- }), " ", log.text);
2151
+ }), " ", text);
2152
+ }
2153
+ function HeadTail({ text, labelWidth }) {
2154
+ return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, " ".repeat(messageColumn(labelWidth) - 2), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, GUIDE), " ", text);
2054
2155
  }
2055
- function ContinuationLine({ log, labelWidth }) {
2156
+ function ContinuationLine({ log, text, labelWidth }) {
2056
2157
  if (log.level === "error") return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, {
2057
2158
  color: LEVEL_COLOR.error,
2058
2159
  bold: true
2059
- }, BAR), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, log.text));
2060
- return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, " ".repeat(messageColumn(labelWidth) - 2), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, GUIDE), " ", log.text);
2160
+ }, BAR), /* @__PURE__ */ React.createElement(Text, { dimColor: log.frame }, text));
2161
+ return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, " ".repeat(messageColumn(labelWidth) - 2), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, GUIDE), " ", text);
2061
2162
  }
2062
2163
  function ScrollableLogView({ visible, viewportHeight, measured }) {
2063
2164
  if (!measured) return null;
@@ -2077,15 +2178,25 @@ function ScrollableLogView({ visible, viewportHeight, measured }) {
2077
2178
  flexGrow: 1,
2078
2179
  justifyContent: "flex-end",
2079
2180
  overflow: "hidden"
2080
- }, visible.length === 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Waiting for logs…") : visible.map((row) => row.kind === "rule" ? /* @__PURE__ */ React.createElement(Rule, { key: row.key }) : row.entry.head ? /* @__PURE__ */ React.createElement(HeadLine, {
2081
- key: row.key,
2082
- log: row.entry,
2083
- labelWidth
2084
- }) : /* @__PURE__ */ React.createElement(ContinuationLine, {
2085
- key: row.key,
2086
- log: row.entry,
2087
- labelWidth
2088
- })));
2181
+ }, visible.length === 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Waiting for logs…") : visible.map((row) => {
2182
+ if (row.kind === "rule") return /* @__PURE__ */ React.createElement(Rule, { key: row.key });
2183
+ const props = {
2184
+ log: row.entry,
2185
+ text: row.text,
2186
+ labelWidth
2187
+ };
2188
+ if (!row.entry.head) return /* @__PURE__ */ React.createElement(ContinuationLine, {
2189
+ key: row.key,
2190
+ ...props
2191
+ });
2192
+ return row.first ? /* @__PURE__ */ React.createElement(HeadLine, {
2193
+ key: row.key,
2194
+ ...props
2195
+ }) : /* @__PURE__ */ React.createElement(HeadTail, {
2196
+ key: row.key,
2197
+ ...props
2198
+ });
2199
+ }));
2089
2200
  }
2090
2201
 
2091
2202
  //#endregion
@@ -2116,8 +2227,14 @@ function BlinkDot() {
2116
2227
  function Glyph({ phase, glyph }) {
2117
2228
  const meta = PHASE_META[phase];
2118
2229
  if (glyph === "live") return isStreaming(phase) ? /* @__PURE__ */ React.createElement(BlinkDot, null) : /* @__PURE__ */ React.createElement(Text, null, meta.icon);
2119
- if (meta.kind === "spinner") return /* @__PURE__ */ React.createElement(Spinner, { type: "balloon2" });
2120
- if (meta.kind === "arc") return /* @__PURE__ */ React.createElement(Spinner, { type: "toggle4" });
2230
+ if (meta.kind === "spinner") return /* @__PURE__ */ React.createElement(Spinner, {
2231
+ key: "balloon2",
2232
+ type: "balloon2"
2233
+ });
2234
+ if (meta.kind === "arc") return /* @__PURE__ */ React.createElement(Spinner, {
2235
+ key: "toggle4",
2236
+ type: "toggle4"
2237
+ });
2121
2238
  return /* @__PURE__ */ React.createElement(Text, null, meta.icon);
2122
2239
  }
2123
2240
  function StatusBadge({ phase, glyph = "phase" }) {
@@ -2141,7 +2258,7 @@ function Hotkey({ keyLabel, action, enabled = true, highlight = false }) {
2141
2258
  bold: true
2142
2259
  }, keyLabel), /* @__PURE__ */ React.createElement(Text, { color: actionColor }, " ", action));
2143
2260
  }
2144
- const RESTART_HINT_PHASES = new Set([
2261
+ const RESTART_HINT_PHASES = /* @__PURE__ */ new Set([
2145
2262
  "restart-required",
2146
2263
  "disconnected",
2147
2264
  "error"
@@ -2160,7 +2277,7 @@ function SessionKeys({ phase, interactive, following }) {
2160
2277
  action: "disconnect",
2161
2278
  enabled: interactive && isSessionLive(phase)
2162
2279
  }), /* @__PURE__ */ React.createElement(Hotkey, {
2163
- keyLabel: "l",
2280
+ keyLabel: "c",
2164
2281
  action: "clear",
2165
2282
  enabled: interactive
2166
2283
  }), /* @__PURE__ */ React.createElement(Hotkey, {
@@ -2191,15 +2308,24 @@ function FilterKeys() {
2191
2308
  //#endregion
2192
2309
  //#region src/ui/components/primitives/Sidebar.tsx
2193
2310
  const MAX_RAIL = 40;
2194
- const META_LABEL_WIDTH = 5;
2311
+ const META_LABEL_WIDTH = 7;
2195
2312
  const LOG_DIR = "logs/";
2196
2313
  function Meta({ label, value }) {
2197
2314
  return /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, label.padEnd(META_LABEL_WIDTH)), value);
2198
2315
  }
2316
+ function tunnelValue(status) {
2317
+ return status === "live" || status === "lost" ? status : `${status}…`;
2318
+ }
2199
2319
  function StatusBlock({ state, uptimeMs }) {
2200
2320
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(StatusBadge, { phase: state.phase }), state.status ? /* @__PURE__ */ React.createElement(Text, null, state.status) : null, uptimeMs === null ? null : /* @__PURE__ */ React.createElement(Meta, {
2201
2321
  label: "up",
2202
2322
  value: formatUptime(uptimeMs)
2323
+ }), state.port === null ? null : /* @__PURE__ */ React.createElement(Meta, {
2324
+ label: "port",
2325
+ value: String(state.port)
2326
+ }), state.tunnel === null ? null : /* @__PURE__ */ React.createElement(Meta, {
2327
+ label: "tunnel",
2328
+ value: tunnelValue(state.tunnel)
2203
2329
  }), /* @__PURE__ */ React.createElement(Meta, {
2204
2330
  label: "logs",
2205
2331
  value: LOG_DIR
@@ -2256,7 +2382,8 @@ function StatusLine({ state, notices, columns }) {
2256
2382
  const [top, ...rest] = notices;
2257
2383
  return /* @__PURE__ */ React.createElement(Box, {
2258
2384
  flexShrink: 0,
2259
- justifyContent: "space-between"
2385
+ justifyContent: "space-between",
2386
+ paddingX: 1
2260
2387
  }, /* @__PURE__ */ React.createElement(Box, {
2261
2388
  flexShrink: 1,
2262
2389
  overflow: "hidden"
@@ -2320,48 +2447,6 @@ function DevLayout(props) {
2320
2447
  })), railed ? /* @__PURE__ */ React.createElement(NotificationStack, { notices }) : null));
2321
2448
  }
2322
2449
 
2323
- //#endregion
2324
- //#region src/ui/logRows.ts
2325
- function isBlockLine(entry) {
2326
- return entry !== void 0 && entry.level !== "error";
2327
- }
2328
- function opensBlock(logs, index) {
2329
- const log = logs[index];
2330
- const next = logs[index + 1];
2331
- return log?.head === true && isBlockLine(log) && next !== void 0 && !next.head && isBlockLine(next);
2332
- }
2333
- function closesBlock(logs, index) {
2334
- const log = logs[index];
2335
- const next = logs[index + 1];
2336
- return log !== void 0 && !log.head && isBlockLine(log) && (next === void 0 || next.head);
2337
- }
2338
- function prevClosedBlock(logs, index) {
2339
- const prev = logs[index - 1];
2340
- return prev !== void 0 && !prev.head && isBlockLine(prev);
2341
- }
2342
- function expandRows(logs) {
2343
- const rows = [];
2344
- for (const [index, entry] of logs.entries()) {
2345
- if (opensBlock(logs, index) && !prevClosedBlock(logs, index)) rows.push({
2346
- kind: "rule",
2347
- key: `top-${entry.id}`
2348
- });
2349
- rows.push({
2350
- kind: "entry",
2351
- key: entry.id,
2352
- entry
2353
- });
2354
- if (closesBlock(logs, index)) rows.push({
2355
- kind: "rule",
2356
- key: `bottom-${entry.id}`
2357
- });
2358
- }
2359
- return rows;
2360
- }
2361
- function rowKey(row) {
2362
- return row.key;
2363
- }
2364
-
2365
2450
  //#endregion
2366
2451
  //#region src/ui/components/CommandRefreshPrompt.tsx
2367
2452
  function CommandRefreshPrompt({ files }) {
@@ -2407,6 +2492,23 @@ function ErrorDisplay({ error }) {
2407
2492
  }, "r"), " ", "to restart")));
2408
2493
  }
2409
2494
 
2495
+ //#endregion
2496
+ //#region src/ui/components/QuitConfirmCard.tsx
2497
+ function QuitConfirmCard() {
2498
+ return /* @__PURE__ */ React.createElement(Box, {
2499
+ borderStyle: "round",
2500
+ borderColor: ui.warn,
2501
+ flexDirection: "column",
2502
+ paddingX: 1
2503
+ }, /* @__PURE__ */ React.createElement(Text, {
2504
+ color: ui.warn,
2505
+ bold: true
2506
+ }, "Quit?"), /* @__PURE__ */ React.createElement(Text, null, "press", " ", /* @__PURE__ */ React.createElement(Text, {
2507
+ color: ui.warn,
2508
+ bold: true
2509
+ }, "Ctrl-C"), " ", "again to quit, or any other key to stay"));
2510
+ }
2511
+
2410
2512
  //#endregion
2411
2513
  //#region src/ui/components/RestartRequiredCard.tsx
2412
2514
  function RestartRequiredCard() {
@@ -2424,12 +2526,52 @@ function RestartRequiredCard() {
2424
2526
  }, "r"), " ", "to restart"));
2425
2527
  }
2426
2528
 
2529
+ //#endregion
2530
+ //#region src/ui/components/TunnelOpeningCard.tsx
2531
+ const STEPS = [
2532
+ ["opening", "Opening the tunnel"],
2533
+ ["resolving", "Waiting for it to answer"],
2534
+ ["registering", "Telling Discord where to reach you"]
2535
+ ];
2536
+ function Marker({ done, active }) {
2537
+ if (done) return /* @__PURE__ */ React.createElement(Text, { color: ui.good }, "✔︎");
2538
+ if (active) return /* @__PURE__ */ React.createElement(Spinner, { type: "dots" });
2539
+ return /* @__PURE__ */ React.createElement(Text, { color: ui.faint }, "·");
2540
+ }
2541
+ function TunnelOpeningCard({ phase }) {
2542
+ const at = STEPS.findIndex(([step]) => step === phase);
2543
+ return /* @__PURE__ */ React.createElement(Box, {
2544
+ borderStyle: "round",
2545
+ borderColor: ui.accent,
2546
+ flexDirection: "column",
2547
+ paddingX: 1
2548
+ }, /* @__PURE__ */ React.createElement(Text, {
2549
+ color: ui.accent,
2550
+ bold: true
2551
+ }, "Setting up your interactions endpoint"), STEPS.map(([step, label], index) => /* @__PURE__ */ React.createElement(Text, {
2552
+ key: step,
2553
+ dimColor: index > at
2554
+ }, /* @__PURE__ */ React.createElement(Marker, {
2555
+ done: index < at,
2556
+ active: index === at
2557
+ }), " ", label)));
2558
+ }
2559
+
2427
2560
  //#endregion
2428
2561
  //#region src/ui/notices.tsx
2429
2562
  const RESTART = "press r to restart";
2563
+ function openingPhase(status) {
2564
+ return status === null || status === "live" || status === "lost" ? null : status;
2565
+ }
2430
2566
  /** Every pending notice, ranked with the one that blocks a decision first. */
2431
2567
  function noticesOf(state) {
2432
2568
  const notices = [];
2569
+ if (state.quitArmed) notices.push({
2570
+ key: "quit",
2571
+ summary: "Quit?",
2572
+ action: "press Ctrl-C again",
2573
+ card: /* @__PURE__ */ React.createElement(QuitConfirmCard, null)
2574
+ });
2433
2575
  if (state.commandUpdatePrompt) {
2434
2576
  const count = state.commandUpdatePrompt.length;
2435
2577
  notices.push({
@@ -2451,6 +2593,13 @@ function noticesOf(state) {
2451
2593
  action: RESTART,
2452
2594
  card: /* @__PURE__ */ React.createElement(RestartRequiredCard, null)
2453
2595
  });
2596
+ const phase = openingPhase(state.tunnel);
2597
+ if (phase) notices.push({
2598
+ key: "tunnel",
2599
+ summary: "Setting up your interactions endpoint",
2600
+ action: "hold on",
2601
+ card: /* @__PURE__ */ React.createElement(TunnelOpeningCard, { phase })
2602
+ });
2454
2603
  return notices;
2455
2604
  }
2456
2605
 
@@ -2476,14 +2625,14 @@ function DevApp(props) {
2476
2625
  const [enabledLevels, setEnabledLevels] = useState(() => /* @__PURE__ */ new Set());
2477
2626
  const [cursor, setCursor] = useState(INITIAL_CURSOR);
2478
2627
  const logBoxRef = useRef(null);
2479
- const logBoxHeight = useMeasuredHeight(logBoxRef);
2628
+ const logBox = useMeasuredBox(logBoxRef);
2480
2629
  const railRef = useRef(null);
2481
2630
  const railWidth = useRailWidth(railRef, state.phase, rows, columns);
2482
2631
  const tier = tierFor(rows, columns);
2483
2632
  const notices = noticesOf(state);
2484
2633
  const logs = useLogs(enabled, enabledLevels);
2485
- const logRows = useMemo(() => expandRows(logs), [logs]);
2486
- const viewportHeight = Math.max(1, logBoxHeight);
2634
+ const logRows = useMemo(() => wrapRows(expandRows(logs), logBox.width, Math.max(1, LogStore.instance.getLabelWidth())), [logs, logBox.width]);
2635
+ const viewportHeight = Math.max(1, logBox.height);
2487
2636
  const scroll = useScroll(logRows, viewportHeight, rowKey);
2488
2637
  const uptimeMs = useUptime(state);
2489
2638
  const interactive = !state.isBusy || state.restartRequired;
@@ -2524,7 +2673,7 @@ function DevApp(props) {
2524
2673
  width: columns,
2525
2674
  height: rows,
2526
2675
  overflow: "hidden"
2527
- }, /* @__PURE__ */ React.createElement(DevLayout, {
2676
+ }, /* @__PURE__ */ React.createElement(Rule, null), /* @__PURE__ */ React.createElement(DevLayout, {
2528
2677
  state,
2529
2678
  tier,
2530
2679
  notices,
@@ -2533,7 +2682,7 @@ function DevApp(props) {
2533
2682
  logBoxRef,
2534
2683
  scroll,
2535
2684
  viewportHeight,
2536
- measured: logBoxHeight > 0,
2685
+ measured: logBox.height > 0,
2537
2686
  columns,
2538
2687
  enabled,
2539
2688
  enabledLevels,
@@ -2552,7 +2701,10 @@ const INITIAL = {
2552
2701
  isBusy: true,
2553
2702
  frameworkVersion: null,
2554
2703
  restartRequired: false,
2555
- commandUpdatePrompt: null
2704
+ commandUpdatePrompt: null,
2705
+ port: null,
2706
+ tunnel: null,
2707
+ quitArmed: false
2556
2708
  };
2557
2709
  var DevStore = class extends TypedEventEmitter {
2558
2710
  state = INITIAL;
@@ -2574,6 +2726,12 @@ var DevStore = class extends TypedEventEmitter {
2574
2726
  setFrameworkVersion(frameworkVersion) {
2575
2727
  this.patch({ frameworkVersion });
2576
2728
  }
2729
+ setTunnel(tunnel) {
2730
+ this.patch({ tunnel });
2731
+ }
2732
+ setQuitArmed(quitArmed) {
2733
+ this.patch({ quitArmed });
2734
+ }
2577
2735
  clearPrompt() {
2578
2736
  this.patch({ commandUpdatePrompt: null });
2579
2737
  }
@@ -2583,7 +2741,8 @@ var DevStore = class extends TypedEventEmitter {
2583
2741
  isBusy: true,
2584
2742
  restartRequired: false,
2585
2743
  error: null,
2586
- status: "Restarting…"
2744
+ status: "Restarting…",
2745
+ port: null
2587
2746
  });
2588
2747
  }
2589
2748
  beginDisconnect() {
@@ -2592,7 +2751,8 @@ var DevStore = class extends TypedEventEmitter {
2592
2751
  isBusy: true,
2593
2752
  restartRequired: false,
2594
2753
  error: null,
2595
- status: "Disconnecting…"
2754
+ status: "Disconnecting…",
2755
+ port: null
2596
2756
  });
2597
2757
  }
2598
2758
  beginQuit() {
@@ -2614,6 +2774,9 @@ var DevStore = class extends TypedEventEmitter {
2614
2774
  case "command-update-prompt":
2615
2775
  this.patch({ commandUpdatePrompt: event.files });
2616
2776
  break;
2777
+ case "server-listening":
2778
+ this.patch({ port: event.port });
2779
+ break;
2617
2780
  case "module-loading":
2618
2781
  case "module-loaded":
2619
2782
  case "module-error":
@@ -2632,65 +2795,251 @@ var DevStore = class extends TypedEventEmitter {
2632
2795
  };
2633
2796
 
2634
2797
  //#endregion
2635
- //#region src/commands/dev/runtime/HmrPlugin.ts
2636
- const DEBOUNCE_MS = 250;
2637
- var HmrPlugin = class extends TypedEventEmitter {
2638
- config;
2798
+ //#region src/commands/dev/TscRunner.ts
2799
+ const TSC_GRACEFUL_EXIT_MS = 2e3;
2800
+ var TscRunner = class {
2801
+ tsconfigPath;
2802
+ cwd;
2803
+ process = null;
2639
2804
  logger;
2640
- lastUpdate = /* @__PURE__ */ new Map();
2641
- server = null;
2642
- dynamicRestartPatterns = /* @__PURE__ */ new Set();
2643
- get hot() {
2644
- return this.server?.environments.ssr.hot;
2645
- }
2646
- get dev() {
2647
- return this.hot ? wrapHot(this.hot) : void 0;
2648
- }
2649
- constructor(config) {
2650
- super();
2651
- this.config = config;
2652
- this.logger = new Logger("HMR", { channel: "hmr" });
2653
- }
2654
- get plugin() {
2655
- return {
2656
- name: "seedcord:hmr",
2657
- configureServer: this.configureServer.bind(this),
2658
- hotUpdate: this.hotUpdate.bind(this)
2659
- };
2660
- }
2661
- sendRefreshCommands(shouldRefresh) {
2662
- this.dev?.send("seedcord:refresh-commands", { shouldRefresh });
2805
+ constructor(tsconfigPath, cwd) {
2806
+ this.tsconfigPath = tsconfigPath;
2807
+ this.cwd = cwd;
2808
+ this.logger = new Logger("Typecheck", { channel: "tsc" });
2663
2809
  }
2664
- configureServer(server) {
2665
- this.server = server;
2666
- server.watcher.on("add", (file) => this.handleFileEvent(file, "create"));
2667
- server.watcher.on("unlink", (file) => this.handleFileEvent(file, "delete"));
2668
- server.watcher.on("addDir", (file) => this.handleFileEvent(file, "createDir"));
2669
- server.watcher.on("unlinkDir", (file) => this.handleFileEvent(file, "deleteDir"));
2670
- this.dev?.on("seedcord:commands-update-prompt", (data) => {
2671
- this.emit("event", {
2672
- type: "command-update-prompt",
2673
- files: data.files
2674
- });
2810
+ start() {
2811
+ if (this.process) return;
2812
+ const tscPath = this.resolveProjectTsc();
2813
+ if (!tscPath) {
2814
+ this.logger.error("Unable to resolve \"typescript\". Install it in the project to enable tsc --watch.");
2815
+ return;
2816
+ }
2817
+ const args = [
2818
+ tscPath,
2819
+ "--noEmit",
2820
+ "--watch",
2821
+ "--preserveWatchOutput",
2822
+ "--pretty"
2823
+ ];
2824
+ if (this.tsconfigPath) args.push("--project", this.tsconfigPath);
2825
+ this.logger.debug("Starting tsc --watch...");
2826
+ this.process = spawn(process.execPath, args, {
2827
+ cwd: this.cwd ?? process.cwd(),
2828
+ stdio: [
2829
+ "ignore",
2830
+ "pipe",
2831
+ "pipe"
2832
+ ]
2675
2833
  });
2676
- this.dev?.on("seedcord:register-critical-files", (data) => {
2677
- for (const pattern of data.patterns) this.dynamicRestartPatterns.add(pattern);
2678
- this.logger.utils.list(data.patterns, "Registered critical file patterns:");
2834
+ this.process.stdout?.on("data", (data) => {
2835
+ const lines = data.toString().split("\n");
2836
+ for (const line of lines) if (line.trim()) this.logger.debug(line.trimEnd());
2679
2837
  });
2680
- }
2681
- isDebounced(file, type) {
2682
- const key = `${file}::${type}`;
2683
- const now = Date.now();
2684
- const last = this.lastUpdate.get(key);
2685
- if (last !== void 0 && now - last < DEBOUNCE_MS) return true;
2686
- this.lastUpdate.set(key, now);
2687
- return false;
2688
- }
2689
- handleFileEvent(file, type) {
2690
- if (this.isDebounced(file, type)) return;
2691
- const relPath = relative(process.cwd(), file);
2692
- const typeColor = type === "create" || type === "createDir" ? chalk.green : type === "delete" || type === "deleteDir" ? chalk.red : chalk.blue;
2693
- this.logger.info(`${typeColor(type.toUpperCase())} ${chalk.gray(relPath)}`);
2838
+ this.process.stderr?.on("data", (data) => {
2839
+ const lines = data.toString().split("\n");
2840
+ for (const line of lines) if (line.trim()) this.logger.error(line.trimEnd());
2841
+ });
2842
+ this.process.on("error", (err) => {
2843
+ this.logger.error(`Failed to start tsc: ${err.message}`);
2844
+ });
2845
+ this.process.on("exit", (code) => {
2846
+ if (code !== 0 && code !== null) this.logger.error(`tsc exited with code ${code}`);
2847
+ else this.logger.debug("tsc exited.");
2848
+ this.process = null;
2849
+ });
2850
+ }
2851
+ stop() {
2852
+ const child = this.process;
2853
+ if (!child) return;
2854
+ this.logger.debug("Stopping tsc...");
2855
+ child.kill("SIGTERM");
2856
+ const killTimer = setTimeout(() => child.kill("SIGKILL"), TSC_GRACEFUL_EXIT_MS);
2857
+ killTimer.unref();
2858
+ child.once("exit", () => {
2859
+ clearTimeout(killTimer);
2860
+ });
2861
+ }
2862
+ resolveProjectTsc() {
2863
+ const projectDir = this.cwd ?? process.cwd();
2864
+ const manifest = resolve(projectDir, "package.json");
2865
+ const projectRequire = existsSync(manifest) ? createRequire(manifest) : createRequire(import.meta.url);
2866
+ try {
2867
+ return projectRequire.resolve("typescript/bin/tsc");
2868
+ } catch {
2869
+ return null;
2870
+ }
2871
+ }
2872
+ };
2873
+
2874
+ //#endregion
2875
+ //#region src/commands/dev/DevSession.ts
2876
+ function isSeedcordInstance(candidate) {
2877
+ return typeof candidate === "object" && candidate !== null && candidate[SeedcordBrand] === true;
2878
+ }
2879
+ var DevSession = class {
2880
+ config;
2881
+ runtime;
2882
+ store;
2883
+ onEvent;
2884
+ stopResolve;
2885
+ instance;
2886
+ isStopped = false;
2887
+ startupPromise;
2888
+ tscRunner;
2889
+ stopPromise;
2890
+ constructor(config, runtime, store, onEvent) {
2891
+ this.config = config;
2892
+ this.runtime = runtime;
2893
+ this.store = store;
2894
+ this.onEvent = onEvent;
2895
+ }
2896
+ async loadInstanceModule() {
2897
+ await this.runtime.start({
2898
+ config: this.config,
2899
+ onEvent: (event) => {
2900
+ this.store.apply(event);
2901
+ this.onEvent(event);
2902
+ }
2903
+ });
2904
+ if (!existsSync(this.config.instance)) throw new SeedcordError(SeedcordErrorCode.CliEntryNotFound, [this.config.instance]);
2905
+ try {
2906
+ const { module } = await this.runtime.loadEntry();
2907
+ return module;
2908
+ } catch (error) {
2909
+ const message = Error.isError(error) ? error.message : String(error);
2910
+ throw new SeedcordError(SeedcordErrorCode.CliStartFailed, [this.config.instance, message]);
2911
+ }
2912
+ }
2913
+ async start(onReady) {
2914
+ const cwd = dirname(this.config.configFile);
2915
+ this.tscRunner = new TscRunner(this.config.tsconfig, cwd);
2916
+ this.tscRunner.start();
2917
+ const module = await this.loadInstanceModule();
2918
+ const exported = resolveDefaultExport(module);
2919
+ const instance = await Promise.resolve(exported);
2920
+ if (!isSeedcordInstance(instance)) throw new SeedcordError(SeedcordErrorCode.CliInstanceInvalid);
2921
+ this.instance = instance;
2922
+ this.store.setFrameworkVersion(instance.version);
2923
+ try {
2924
+ this.store.setPhase("starting");
2925
+ this.store.setStatus("Starting Seedcord instance…");
2926
+ this.startupPromise = Promise.resolve(instance.start());
2927
+ await this.startupPromise;
2928
+ if (this.isStopped) return;
2929
+ this.store.setPhase("running");
2930
+ this.store.setStatus(`${chalk.bold(instance.username ?? "Bot")} is ready!`);
2931
+ onReady?.();
2932
+ await new Promise((resolve) => {
2933
+ this.stopResolve = resolve;
2934
+ });
2935
+ } catch (error) {
2936
+ const reason = Error.isError(error) ? error.message : "Unknown error";
2937
+ throw new SeedcordError(SeedcordErrorCode.CliStartFailed, [this.config.instance, reason]);
2938
+ }
2939
+ }
2940
+ async stop() {
2941
+ this.stopPromise ??= this.runStop();
2942
+ return this.stopPromise;
2943
+ }
2944
+ async runStop() {
2945
+ this.isStopped = true;
2946
+ this.tscRunner?.stop();
2947
+ this.instance?.startup.abort();
2948
+ if (this.startupPromise) try {
2949
+ await this.startupPromise;
2950
+ } catch {}
2951
+ await this.instance?.shutdown.run(0, false);
2952
+ this.stopResolve?.();
2953
+ }
2954
+ async dispose() {
2955
+ await this.stop();
2956
+ await this.runtime.dispose();
2957
+ }
2958
+ refreshCommands(shouldRefresh) {
2959
+ this.runtime.refreshCommands?.(shouldRefresh);
2960
+ }
2961
+ };
2962
+
2963
+ //#endregion
2964
+ //#region src/commands/dev/runtime/HmrPlugin.ts
2965
+ const DEBOUNCE_MS = 250;
2966
+ var HmrPlugin = class extends TypedEventEmitter {
2967
+ config;
2968
+ logger;
2969
+ lastUpdate = /* @__PURE__ */ new Map();
2970
+ awaitingCreateEcho = /* @__PURE__ */ new Set();
2971
+ server = null;
2972
+ dynamicRestartPatterns = /* @__PURE__ */ new Set();
2973
+ get hot() {
2974
+ return this.server?.environments.ssr.hot;
2975
+ }
2976
+ get dev() {
2977
+ return this.hot ? wrapHot(this.hot) : void 0;
2978
+ }
2979
+ constructor(config) {
2980
+ super();
2981
+ this.config = config;
2982
+ this.logger = new Logger("HMR", { channel: "hmr" });
2983
+ }
2984
+ get plugin() {
2985
+ return {
2986
+ name: "seedcord:hmr",
2987
+ configureServer: this.configureServer.bind(this),
2988
+ hotUpdate: this.hotUpdate.bind(this)
2989
+ };
2990
+ }
2991
+ sendRefreshCommands(shouldRefresh) {
2992
+ this.dev?.send("seedcord:refresh-commands", { shouldRefresh });
2993
+ }
2994
+ configureServer(server) {
2995
+ this.server = server;
2996
+ server.watcher.on("add", (file) => this.handleFileEvent(file, "create"));
2997
+ server.watcher.on("unlink", (file) => this.handleFileEvent(file, "delete"));
2998
+ server.watcher.on("addDir", (file) => this.handleFileEvent(file, "createDir"));
2999
+ server.watcher.on("unlinkDir", (file) => this.handleFileEvent(file, "deleteDir"));
3000
+ server.watcher.add(this.config.configFile);
3001
+ server.watcher.on("change", (file) => this.handleChange(file));
3002
+ this.dev?.on("seedcord:commands-update-prompt", (data) => {
3003
+ this.emit("event", {
3004
+ type: "command-update-prompt",
3005
+ files: data.files
3006
+ });
3007
+ });
3008
+ this.dev?.on("seedcord:server-listening", (data) => {
3009
+ this.emit("event", {
3010
+ type: "server-listening",
3011
+ port: data.port
3012
+ });
3013
+ });
3014
+ this.dev?.on("seedcord:register-critical-files", (data) => {
3015
+ for (const pattern of data.patterns) this.dynamicRestartPatterns.add(pattern);
3016
+ this.logger.debug(`Registered ${String(data.patterns.length)} critical file patterns`);
3017
+ });
3018
+ }
3019
+ isDebounced(file, type) {
3020
+ if (this.justSaw(file, type)) return true;
3021
+ this.lastUpdate.set(`${file}::${type}`, Date.now());
3022
+ return false;
3023
+ }
3024
+ justSaw(file, type) {
3025
+ const last = this.lastUpdate.get(`${file}::${type}`);
3026
+ return last !== void 0 && Date.now() - last < DEBOUNCE_MS;
3027
+ }
3028
+ handleChange(file) {
3029
+ if (!this.isCriticalFile(file) || this.isDebounced(file, "update")) return;
3030
+ this.reportRestartRequired(file);
3031
+ }
3032
+ reportRestartRequired(file) {
3033
+ const relPath = relative(process.cwd(), file);
3034
+ this.logger.warn(`${paint.coral("Critical file changed:")} ${paint.sky.bold(relPath)}. Restart required.`);
3035
+ this.emit("event", { type: "restart-required" });
3036
+ }
3037
+ handleFileEvent(file, type) {
3038
+ if (this.isDebounced(file, type)) return;
3039
+ if (type === "create") this.awaitingCreateEcho.add(file);
3040
+ const relPath = relative(process.cwd(), file);
3041
+ const typeColor = type === "create" || type === "createDir" ? paint.mint : type === "delete" || type === "deleteDir" ? paint.coral : paint.sky;
3042
+ this.logger.trace(`${typeColor(type.toUpperCase())} ${paint.mute(relPath)}`);
2694
3043
  const payload = {
2695
3044
  file,
2696
3045
  type,
@@ -2701,20 +3050,19 @@ var HmrPlugin = class extends TypedEventEmitter {
2701
3050
  hotUpdate(ctx) {
2702
3051
  const { file, modules, server } = ctx;
2703
3052
  const type = "update";
2704
- if (this.isDebounced(file, type)) return [];
3053
+ if (this.awaitingCreateEcho.delete(file) && this.justSaw(file, "create") || this.isDebounced(file, type)) return [];
2705
3054
  const relPath = relative(process.cwd(), file);
2706
- this.logger.info(`${chalk.blue(type.toUpperCase())} ${chalk.gray(relPath)}`);
3055
+ this.logger.trace(`${paint.sky(type.toUpperCase())} ${paint.mute(relPath)}`);
2707
3056
  if (this.isCriticalFile(file)) {
2708
- this.logger.warn(`${chalk.red("Critical file changed:")} ${chalk.bold(relPath)}. Restart required.`);
2709
- this.emit("event", { type: "restart-required" });
3057
+ this.reportRestartRequired(file);
2710
3058
  return [];
2711
3059
  }
2712
3060
  const moduleGraph = server.moduleGraph;
2713
3061
  const fileModules = moduleGraph.getModulesByFile(file);
2714
3062
  const allModules = fileModules ? [...fileModules] : [];
2715
- const combinedModules = new Set([...modules, ...allModules]);
3063
+ const combinedModules = /* @__PURE__ */ new Set([...modules, ...allModules]);
2716
3064
  const affectedModules = this.getAffectedModules([...combinedModules]);
2717
- const filesToInvalidate = new Set([file, ...affectedModules]);
3065
+ const filesToInvalidate = /* @__PURE__ */ new Set([file, ...affectedModules]);
2718
3066
  for (const fileToInvalidate of filesToInvalidate) {
2719
3067
  const mods = moduleGraph.getModulesByFile(fileToInvalidate);
2720
3068
  if (mods) for (const mod of mods) moduleGraph.invalidateModule(mod);
@@ -2859,7 +3207,6 @@ var ViteDevRuntime = class {
2859
3207
  if (!this.context || !this.viteServer || !this.moduleRunner) throw new SeedcordError(SeedcordErrorCode.CliStartFailed, [this.context?.config.instance ?? "runtime", "ViteDevRuntime.start() must complete before loadEntry()"]);
2860
3208
  const { instance: entryPath } = this.context.config;
2861
3209
  const projectRoot = this.context.config.root;
2862
- const startTime = performance.now();
2863
3210
  this.emit({
2864
3211
  type: "module-loading",
2865
3212
  path: entryPath
@@ -2867,15 +3214,11 @@ var ViteDevRuntime = class {
2867
3214
  try {
2868
3215
  const moduleId = toModuleId(projectRoot, entryPath);
2869
3216
  const module = await this.moduleRunner.import(moduleId);
2870
- const loadTime = performance.now() - startTime;
2871
3217
  this.emit({
2872
3218
  type: "module-loaded",
2873
3219
  path: entryPath
2874
3220
  });
2875
- return {
2876
- module,
2877
- metadata: { loadTime }
2878
- };
3221
+ return { module };
2879
3222
  } catch (error) {
2880
3223
  this.emit({
2881
3224
  type: "module-error",
@@ -2906,191 +3249,376 @@ function toModuleId(projectRoot, file) {
2906
3249
  }
2907
3250
 
2908
3251
  //#endregion
2909
- //#region src/commands/dev/TscRunner.ts
2910
- const TSC_GRACEFUL_EXIT_MS = 2e3;
2911
- var TscRunner = class {
2912
- tsconfigPath;
2913
- cwd;
2914
- process = null;
2915
- logger;
2916
- constructor(tsconfigPath, cwd) {
2917
- this.tsconfigPath = tsconfigPath;
2918
- this.cwd = cwd;
2919
- this.logger = new Logger("Typecheck", { channel: "tsc" });
3252
+ //#region src/commands/dev/tunnel/cloudflared.ts
3253
+ const BINARY = "cloudflared";
3254
+ const DOWNLOADS = "https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/";
3255
+ function systemLookup() {
3256
+ return {
3257
+ pathVar: process.env.PATH,
3258
+ pathExt: process.env.PATHEXT,
3259
+ platform: process.platform,
3260
+ delimiter,
3261
+ exists: existsSync
3262
+ };
3263
+ }
3264
+ function findCloudflared(lookup) {
3265
+ const suffixes = lookup.platform === "win32" ? lookup.pathExt?.split(";") ?? [".EXE"] : [""];
3266
+ for (const dir of lookup.pathVar?.split(lookup.delimiter) ?? []) for (const suffix of suffixes) {
3267
+ const candidate = join(dir, `${BINARY}${suffix}`);
3268
+ if (lookup.exists(candidate)) return candidate;
2920
3269
  }
2921
- start() {
2922
- if (this.process) return;
2923
- const tscPath = this.resolveProjectTsc();
2924
- if (!tscPath) {
2925
- this.logger.error("Unable to resolve \"typescript\". Install it in the project to enable tsc --watch.");
2926
- return;
3270
+ }
3271
+ function installHint(platform) {
3272
+ if (platform === "darwin") return "brew install cloudflared";
3273
+ if (platform === "win32") return "winget install -e --id Cloudflare.cloudflared";
3274
+ return DOWNLOADS;
3275
+ }
3276
+
3277
+ //#endregion
3278
+ //#region src/commands/dev/tunnel/probe.ts
3279
+ const UNAUTHORIZED = 401;
3280
+ const POLL_INTERVAL_MS$1 = 250;
3281
+ const BUDGET_MS = 6e4;
3282
+ const PROBE_SECONDS = BUDGET_MS / 1e3;
3283
+ async function awaitReachable(url, deps, signal) {
3284
+ const budget = AbortSignal.any([signal, AbortSignal.timeout(BUDGET_MS)]);
3285
+ for (let attempt = 0; attempt < BUDGET_MS / POLL_INTERVAL_MS$1; attempt++) {
3286
+ try {
3287
+ if ((await deps.fetch(url, {
3288
+ method: "POST",
3289
+ signal: budget
3290
+ })).status === UNAUTHORIZED) return true;
3291
+ } catch {
3292
+ signal.throwIfAborted();
2927
3293
  }
2928
- const args = [
2929
- tscPath,
2930
- "--noEmit",
2931
- "--watch",
2932
- "--preserveWatchOutput",
2933
- "--pretty"
2934
- ];
2935
- if (this.tsconfigPath) args.push("--project", this.tsconfigPath);
2936
- this.logger.info("Starting tsc --watch...");
2937
- this.process = spawn(process.execPath, args, {
2938
- cwd: this.cwd ?? process.cwd(),
2939
- stdio: [
2940
- "ignore",
2941
- "pipe",
2942
- "pipe"
2943
- ]
2944
- });
2945
- this.process.stdout?.on("data", (data) => {
2946
- const lines = data.toString().split("\n");
2947
- for (const line of lines) if (line.trim()) this.logger.info(line.trimEnd());
3294
+ await deps.wait(POLL_INTERVAL_MS$1);
3295
+ }
3296
+ return false;
3297
+ }
3298
+
3299
+ //#endregion
3300
+ //#region src/commands/dev/tunnel/CloudflaredTunnel.ts
3301
+ const POLL_INTERVAL_MS = 250;
3302
+ const HOSTNAME_ATTEMPTS = 240;
3303
+ const SETTLE_MS = 4e3;
3304
+ const GRACEFUL_EXIT_MS = 2e3;
3305
+ function freePort() {
3306
+ return new Promise((resolve, reject) => {
3307
+ const probe = createServer$1();
3308
+ probe.once("error", reject);
3309
+ probe.listen(0, "127.0.0.1", () => {
3310
+ const { port } = probe.address();
3311
+ probe.close(() => resolve(port));
2948
3312
  });
2949
- this.process.stderr?.on("data", (data) => {
2950
- const lines = data.toString().split("\n");
2951
- for (const line of lines) if (line.trim()) this.logger.error(line.trimEnd());
3313
+ });
3314
+ }
3315
+ function systemTunnelDeps() {
3316
+ return {
3317
+ spawn: (command, args) => spawn(command, args, { stdio: "ignore" }),
3318
+ fetch: (url, init) => fetch(url, init),
3319
+ freePort,
3320
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
3321
+ };
3322
+ }
3323
+ var CloudflaredTunnel = class {
3324
+ deps;
3325
+ binary;
3326
+ onLost;
3327
+ child;
3328
+ failure;
3329
+ constructor(deps, binary, onLost) {
3330
+ this.deps = deps;
3331
+ this.binary = binary;
3332
+ this.onLost = onLost;
3333
+ }
3334
+ async open(signal, targetPort, onResolving) {
3335
+ const metricsPort = await this.deps.freePort();
3336
+ const child = this.deps.spawn(this.binary, [
3337
+ "tunnel",
3338
+ "--url",
3339
+ `http://localhost:${String(targetPort)}`,
3340
+ "--metrics",
3341
+ `127.0.0.1:${String(metricsPort)}`
3342
+ ]);
3343
+ child.on("error", (error) => {
3344
+ this.failure = error;
2952
3345
  });
2953
- this.process.on("error", (err) => {
2954
- this.logger.error(`Failed to start tsc: ${err.message}`);
3346
+ child.on("exit", (code) => {
3347
+ if (this.child !== child) return;
3348
+ this.failure = /* @__PURE__ */ new Error(`cloudflared exited early with code ${String(code)}`);
2955
3349
  });
2956
- this.process.on("exit", (code) => {
2957
- if (code !== 0 && code !== null) this.logger.error(`tsc exited with code ${code}`);
2958
- else this.logger.info("tsc exited.");
2959
- this.process = null;
3350
+ this.child = child;
3351
+ const url = `https://${await this.readHostname(metricsPort, signal)}`;
3352
+ onResolving?.();
3353
+ await this.deps.wait(SETTLE_MS);
3354
+ await awaitReachable(url, this.deps, signal);
3355
+ child.once("exit", () => {
3356
+ if (this.child === child) this.onLost();
2960
3357
  });
3358
+ return url;
2961
3359
  }
2962
3360
  stop() {
2963
- const child = this.process;
3361
+ const child = this.child;
2964
3362
  if (!child) return;
2965
- this.logger.info("Stopping tsc...");
3363
+ this.child = void 0;
3364
+ this.failure = void 0;
2966
3365
  child.kill("SIGTERM");
2967
- const killTimer = setTimeout(() => child.kill("SIGKILL"), TSC_GRACEFUL_EXIT_MS);
2968
- killTimer.unref();
3366
+ const escalate = setTimeout(() => child.kill("SIGKILL"), GRACEFUL_EXIT_MS);
3367
+ escalate.unref();
2969
3368
  child.once("exit", () => {
2970
- clearTimeout(killTimer);
3369
+ clearTimeout(escalate);
2971
3370
  });
2972
3371
  }
2973
- resolveProjectTsc() {
2974
- const manifest = resolve(this.cwd ?? process.cwd(), "package.json");
2975
- const projectRequire = existsSync(manifest) ? createRequire(manifest) : createRequire(import.meta.url);
3372
+ async readHostname(metricsPort, signal) {
3373
+ const budget = AbortSignal.any([signal, AbortSignal.timeout(6e4)]);
3374
+ for (let attempt = 0; attempt < HOSTNAME_ATTEMPTS && !this.failure; attempt++) {
3375
+ signal.throwIfAborted();
3376
+ if (budget.aborted) break;
3377
+ const hostname = await this.pollOnce(metricsPort, budget);
3378
+ if (hostname) return hostname;
3379
+ await this.deps.wait(POLL_INTERVAL_MS);
3380
+ }
3381
+ const cause = this.failure;
3382
+ this.stop();
3383
+ throw new SeedcordError(SeedcordErrorCode.CliTunnelUrlUnavailable, [6e4 / 1e3], { cause });
3384
+ }
3385
+ async pollOnce(metricsPort, signal) {
2976
3386
  try {
2977
- return projectRequire.resolve("typescript/bin/tsc");
3387
+ const { hostname } = await (await this.deps.fetch(`http://127.0.0.1:${String(metricsPort)}/quicktunnel`, { signal })).json();
3388
+ return hostname === "" ? void 0 : hostname;
2978
3389
  } catch {
2979
- return null;
3390
+ return;
2980
3391
  }
2981
3392
  }
2982
3393
  };
2983
3394
 
2984
3395
  //#endregion
2985
- //#region src/commands/dev/DevRunner.ts
2986
- function isSeedcordInstance(candidate) {
2987
- return typeof candidate === "object" && candidate !== null && candidate[SeedcordBrand] === true;
3396
+ //#region src/commands/dev/tunnel/InteractionsEndpoint.ts
3397
+ const FIELD = "interactions_endpoint_url";
3398
+ function unverified(error) {
3399
+ if (!(error instanceof DiscordAPIError) || error.code !== RESTJSONErrorCodes.InvalidFormBodyOrContentType) return false;
3400
+ const { rawError } = error;
3401
+ return "errors" in rawError && containsField(rawError.errors);
2988
3402
  }
2989
- var SeedcordDevSession = class {
2990
- config;
2991
- runtime;
2992
- store;
2993
- stopResolve;
2994
- instance;
2995
- isStopped = false;
2996
- startupPromise;
2997
- tscRunner;
2998
- stopPromise;
2999
- constructor(config, runtime, store) {
3000
- this.config = config;
3001
- this.runtime = runtime;
3002
- this.store = store;
3403
+ function containsField(errors) {
3404
+ return typeof errors === "object" && errors !== null && FIELD in errors;
3405
+ }
3406
+ var InteractionsEndpoint = class InteractionsEndpoint {
3407
+ makeRest;
3408
+ client;
3409
+ constructor(makeRest) {
3410
+ this.makeRest = makeRest;
3003
3411
  }
3004
- async loadInstanceModule() {
3005
- await this.runtime.start({
3006
- config: this.config,
3007
- onEvent: (event) => {
3008
- this.store.apply(event);
3009
- }
3412
+ static create(token) {
3413
+ const rest = new REST({
3414
+ version: "10",
3415
+ rejectOnRateLimit: ["/applications"]
3010
3416
  });
3011
- if (!existsSync(this.config.instance)) throw new SeedcordError(SeedcordErrorCode.CliEntryNotFound, [this.config.instance]);
3417
+ return new InteractionsEndpoint(() => rest.setToken(token()));
3418
+ }
3419
+ async set(url, signal) {
3420
+ signal.throwIfAborted();
3421
+ if ((await this.rest().get(Routes.currentApplication())).interactions_endpoint_url === url) return;
3422
+ signal.throwIfAborted();
3012
3423
  try {
3013
- const { module } = await this.runtime.loadEntry();
3014
- return module;
3424
+ await this.write(url);
3015
3425
  } catch (error) {
3016
- const message = Error.isError(error) ? error.message : String(error);
3017
- throw new SeedcordError(SeedcordErrorCode.CliStartFailed, [this.config.instance, message]);
3426
+ if (!unverified(error)) throw error;
3427
+ throw new SeedcordError(SeedcordErrorCode.CliTunnelNotVerified, [url]);
3018
3428
  }
3019
3429
  }
3020
- async start(onReady) {
3021
- const cwd = dirname(this.config.configFile);
3022
- this.tscRunner = new TscRunner(this.config.tsconfig, cwd);
3023
- this.tscRunner.start();
3024
- const exported = resolveDefaultExport(await this.loadInstanceModule());
3025
- const instance = await Promise.resolve(exported);
3026
- if (!isSeedcordInstance(instance)) throw new SeedcordError(SeedcordErrorCode.CliInstanceInvalid);
3027
- this.instance = instance;
3028
- this.store.setFrameworkVersion(instance.version);
3430
+ rest() {
3431
+ this.client ??= this.makeRest();
3432
+ return this.client;
3433
+ }
3434
+ async clear() {
3435
+ await this.write(null);
3436
+ }
3437
+ async write(endpoint) {
3438
+ await this.rest().patch(Routes.currentApplication(), { body: { interactions_endpoint_url: endpoint } });
3439
+ }
3440
+ };
3441
+
3442
+ //#endregion
3443
+ //#region src/commands/dev/tunnel/TunnelCoordinator.ts
3444
+ const PASTE_HINT = "The tunnel is still up, so try pasting that URL into the dashboard.";
3445
+ const RESTART_HINT = "Restart for a fresh hostname, or set `tunnel` to an https URL you already serve.";
3446
+ function superseded(attempt) {
3447
+ return attempt.signal.aborted;
3448
+ }
3449
+ function opening(kind) {
3450
+ return kind === "quick" ? "Requesting a cloudflared tunnel to port" : "Checking if your tunnel reaches port";
3451
+ }
3452
+ var TunnelCoordinator = class {
3453
+ deps;
3454
+ target;
3455
+ current;
3456
+ constructor(deps) {
3457
+ this.deps = deps;
3458
+ }
3459
+ async onPort(port) {
3460
+ if (this.target === port) return;
3461
+ this.target = port;
3462
+ const attempt = this.begin();
3463
+ const startedAt = Date.now();
3464
+ const since = () => paint.mute(`+${formatUptime(Date.now() - startedAt)}`);
3465
+ const tunnel = this.deps.makeTunnel(() => this.reportLost(attempt));
3466
+ attempt.signal.addEventListener("abort", () => tunnel.stop(), { once: true });
3467
+ let patched = false;
3029
3468
  try {
3030
- this.store.setPhase("starting");
3031
- this.store.setStatus("Starting Seedcord instance…");
3032
- this.startupPromise = Promise.resolve(instance.start());
3033
- await this.startupPromise;
3034
- if (this.isStopped) return;
3035
- this.store.setPhase("running");
3036
- this.store.setStatus(`${chalk.bold(instance.username ?? "Bot")} is ready!`);
3037
- onReady?.();
3038
- await new Promise((resolve) => {
3039
- this.stopResolve = resolve;
3040
- });
3469
+ this.deps.onStatus("opening");
3470
+ this.deps.logger.info(`${opening(this.deps.kind)} ${paint.iris(String(port))}`);
3471
+ const url = await tunnel.open(attempt.signal, port, () => this.deps.onStatus("resolving"));
3472
+ if (superseded(attempt)) return;
3473
+ this.deps.logger.info(`Reachable at ${paint.sky.italic(url)} ${since()}`);
3474
+ this.deps.onStatus("registering");
3475
+ patched = true;
3476
+ await this.deps.endpoint.set(url, attempt.signal);
3477
+ if (superseded(attempt)) return;
3478
+ this.deps.onStatus("live");
3479
+ this.deps.logger.info(`Interactions endpoint set on the dev dashboard ${since()}`);
3041
3480
  } catch (error) {
3042
- const reason = Error.isError(error) ? error.message : "Unknown error";
3043
- throw new SeedcordError(SeedcordErrorCode.CliStartFailed, [this.config.instance, reason]);
3481
+ if (superseded(attempt)) return;
3482
+ if (patched) attempt.abort();
3483
+ this.reportFailure(error, patched);
3044
3484
  }
3045
3485
  }
3486
+ reportLost(attempt) {
3487
+ if (superseded(attempt)) return;
3488
+ this.target = void 0;
3489
+ this.deps.onStatus("lost");
3490
+ this.deps.logger.error("cloudflared exited, so the bot has no public endpoint");
3491
+ }
3492
+ reportFailure(error, patched) {
3493
+ this.target = void 0;
3494
+ this.deps.onStatus(null);
3495
+ this.deps.logger.error("Tunnel setup failed, the bot runs without a public endpoint", error);
3496
+ if (this.deps.kind === "quick") this.deps.logger.info(patched ? RESTART_HINT : PASTE_HINT);
3497
+ }
3046
3498
  async stop() {
3047
- this.stopPromise ??= this.runStop();
3048
- return this.stopPromise;
3499
+ if (!this.current) return;
3500
+ this.target = void 0;
3501
+ this.current.abort();
3502
+ this.current = void 0;
3503
+ this.deps.onStatus(null);
3504
+ if (this.deps.kind === "configured") return;
3505
+ try {
3506
+ await this.deps.endpoint.clear();
3507
+ } catch (error) {
3508
+ this.deps.logger.warn("Could not clear the interactions endpoint", error);
3509
+ }
3049
3510
  }
3050
- async runStop() {
3051
- this.isStopped = true;
3052
- this.tscRunner?.stop();
3053
- this.instance?.startup.abort();
3054
- if (this.startupPromise) try {
3055
- await this.startupPromise;
3056
- } catch {}
3057
- await this.instance?.shutdown.run(0, false);
3058
- this.stopResolve?.();
3511
+ begin() {
3512
+ this.current?.abort();
3513
+ this.current = new AbortController();
3514
+ return this.current;
3059
3515
  }
3060
- async dispose() {
3061
- await this.stop();
3062
- await this.runtime.dispose();
3516
+ };
3517
+
3518
+ //#endregion
3519
+ //#region src/commands/dev/tunnel/createTunnelCoordinator.ts
3520
+ function missingCloudflaredHint(platform) {
3521
+ const arrow = paint.mute("→");
3522
+ return [
3523
+ `${paint.sky.bold("cloudflared")} is missing, so a tunnel can't be set up for you. Discord won't have any url to reach this bot`,
3524
+ `${arrow} install it with ${paint.mint.bold(installHint(platform))}`,
3525
+ `${arrow} or set ${paint.amber("tunnel")} to an https url you already serve, like ${paint.sky.italic("https://bot.example.com")}`
3526
+ ].join("\n");
3527
+ }
3528
+ function createTunnelCoordinator(logger, onStatus, tunnel, findBinary = () => findCloudflared(systemLookup())) {
3529
+ if (tunnel.mode === "off") return void 0;
3530
+ const deps = systemTunnelDeps();
3531
+ const endpoint = InteractionsEndpoint.create(() => validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN")));
3532
+ if (tunnel.mode === "url") return new TunnelCoordinator({
3533
+ makeTunnel: () => ({
3534
+ open: async (signal, _port, onResolving) => {
3535
+ onResolving?.();
3536
+ if (await awaitReachable(tunnel.url, deps, signal)) return tunnel.url;
3537
+ throw new SeedcordError(SeedcordErrorCode.CliTunnelUnreachable, [tunnel.url, 60]);
3538
+ },
3539
+ stop: () => void 0
3540
+ }),
3541
+ kind: "configured",
3542
+ endpoint,
3543
+ onStatus,
3544
+ logger
3545
+ });
3546
+ const binary = findBinary();
3547
+ if (!binary) return void 0;
3548
+ return new TunnelCoordinator({
3549
+ makeTunnel: (onLost) => new CloudflaredTunnel(deps, binary, onLost),
3550
+ kind: "quick",
3551
+ endpoint,
3552
+ onStatus,
3553
+ logger
3554
+ });
3555
+ }
3556
+
3557
+ //#endregion
3558
+ //#region src/commands/dev/tunnel/TunnelRouter.ts
3559
+ function same(a, b) {
3560
+ if (a?.mode !== b.mode) return false;
3561
+ return a.mode === "url" && b.mode === "url" ? a.url === b.url : true;
3562
+ }
3563
+ var TunnelRouter = class {
3564
+ make;
3565
+ logger;
3566
+ coordinator;
3567
+ built;
3568
+ constructor(make, logger) {
3569
+ this.make = make;
3570
+ this.logger = logger;
3063
3571
  }
3064
- refreshCommands(shouldRefresh) {
3065
- this.runtime.refreshCommands?.(shouldRefresh);
3572
+ route(tunnel, event) {
3573
+ if (event.type !== "server-listening") return Promise.resolve();
3574
+ return this.routeTo(tunnel, event.port).catch((error) => {
3575
+ this.logger.error("Tunnel setup failed, the bot runs without a public endpoint", error);
3576
+ });
3577
+ }
3578
+ async routeTo(tunnel, port) {
3579
+ if (!same(this.built, tunnel)) {
3580
+ await this.stop();
3581
+ this.built = tunnel;
3582
+ this.coordinator = tunnel.mode === "off" ? void 0 : this.make(tunnel);
3583
+ if (tunnel.mode !== "off" && !this.coordinator) this.logger.warn(missingCloudflaredHint(process.platform));
3584
+ }
3585
+ await this.coordinator?.onPort(port);
3586
+ }
3587
+ stop() {
3588
+ const running = this.coordinator;
3589
+ this.coordinator = void 0;
3590
+ return running?.stop() ?? Promise.resolve();
3066
3591
  }
3067
3592
  };
3068
- /**
3069
- * Coordinates config discovery, loading, and starting a Seedcord instance.
3070
- */
3593
+
3594
+ //#endregion
3595
+ //#region src/commands/dev/DevRunner.ts
3596
+ const TUNNEL_TEARDOWN_MS = 3e3;
3071
3597
  var DevRunner = class DevRunner {
3072
- locator;
3073
- configLoader;
3074
- store;
3075
- codegen;
3076
- codegenLogger;
3598
+ deps;
3077
3599
  currentSession = null;
3078
3600
  signalResolve;
3079
3601
  shouldQuit = false;
3602
+ quitting;
3080
3603
  isDisconnected = false;
3081
3604
  isRunning = false;
3082
3605
  isRegenerating = false;
3083
- constructor(locator, configLoader, store, codegen, codegenLogger) {
3084
- this.locator = locator;
3085
- this.configLoader = configLoader;
3086
- this.store = store;
3087
- this.codegen = codegen;
3088
- this.codegenLogger = codegenLogger;
3606
+ constructor(deps) {
3607
+ this.deps = deps;
3089
3608
  }
3090
3609
  static create(logger, store) {
3091
3610
  const moduleLoader = new RuntimeModuleLoader();
3092
3611
  const codegenLogger = new Logger("Codegen", { channel: "cli" });
3093
- return new DevRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), store, CodegenRunner.create(codegenLogger), codegenLogger);
3612
+ const tunnelLogger = new Logger("Tunnel", { channel: "cli" });
3613
+ const makeCoordinator = (tunnel) => createTunnelCoordinator(tunnelLogger, (status) => store.setTunnel(status), tunnel);
3614
+ return new DevRunner({
3615
+ locator: new ConfigLocator(logger),
3616
+ configLoader: new ConfigLoader(moduleLoader, logger),
3617
+ store,
3618
+ codegen: CodegenRunner.create(codegenLogger),
3619
+ codegenLogger,
3620
+ tunnel: new TunnelRouter(makeCoordinator, tunnelLogger)
3621
+ });
3094
3622
  }
3095
3623
  async run() {
3096
3624
  if (this.isRunning) return;
@@ -3108,45 +3636,53 @@ var DevRunner = class DevRunner {
3108
3636
  }
3109
3637
  } finally {
3110
3638
  this.isRunning = false;
3639
+ await this.quitting;
3111
3640
  }
3112
3641
  }
3113
3642
  async handleDisconnected() {
3114
- this.store.setPhase("disconnected");
3115
- this.store.setStatus("Disconnected. Press r to restart.");
3116
- this.store.setBusy(false);
3643
+ this.deps.store.setPhase("disconnected");
3644
+ this.deps.store.setStatus("Disconnected. Press r to restart.");
3645
+ this.deps.store.setBusy(false);
3117
3646
  await this.waitForSignal();
3118
- this.store.setBusy(true);
3647
+ this.deps.store.setBusy(true);
3119
3648
  if (!this.shouldQuit) this.isDisconnected = false;
3120
3649
  }
3121
3650
  async runSession() {
3122
3651
  resetChannelColors();
3123
- this.store.setPhase("starting");
3124
- this.store.setBusy(true);
3652
+ this.deps.store.setPhase("starting");
3653
+ this.deps.store.setBusy(true);
3125
3654
  const config = await this.loadConfig();
3126
3655
  const runtime = new ViteDevRuntime();
3127
- this.currentSession = new SeedcordDevSession(config, runtime, this.store);
3656
+ this.currentSession = new DevSession(config, runtime, this.deps.store, (event) => {
3657
+ this.deps.tunnel.route(config.tunnel, event);
3658
+ });
3128
3659
  try {
3129
3660
  await this.currentSession.start(() => {
3130
- this.store.setBusy(false);
3661
+ this.deps.store.setBusy(false);
3131
3662
  });
3132
3663
  } finally {
3133
- this.store.setBusy(true);
3664
+ this.deps.store.setBusy(true);
3134
3665
  await this.currentSession.dispose();
3135
3666
  this.currentSession = null;
3136
3667
  }
3137
3668
  }
3138
3669
  async handleError(error) {
3139
- this.store.setPhase("error");
3140
- this.store.setError(Error.isError(error) ? error : new Error(String(error)));
3141
- this.store.setStatus("Press r to restart.");
3142
- this.store.setBusy(false);
3670
+ this.deps.store.setPhase("error");
3671
+ this.deps.store.setError(Error.isError(error) ? error : new Error(String(error)));
3672
+ this.deps.store.setStatus("Press r to restart.");
3673
+ this.deps.store.setBusy(false);
3143
3674
  await this.waitForSignal();
3144
- this.store.setBusy(true);
3675
+ this.deps.store.setBusy(true);
3145
3676
  }
3146
3677
  async quit() {
3678
+ this.quitting ??= this.runQuit();
3679
+ return this.quitting;
3680
+ }
3681
+ async runQuit() {
3147
3682
  this.shouldQuit = true;
3148
- this.store.setPhase("quitting");
3683
+ this.deps.store.setPhase("quitting");
3149
3684
  await this.currentSession?.stop();
3685
+ await settleWithin(this.deps.tunnel.stop(), TUNNEL_TEARDOWN_MS);
3150
3686
  this.signalResolve?.();
3151
3687
  }
3152
3688
  async restart() {
@@ -3168,9 +3704,9 @@ var DevRunner = class DevRunner {
3168
3704
  if (this.isRegenerating) return;
3169
3705
  this.isRegenerating = true;
3170
3706
  try {
3171
- await this.codegen.run(false);
3707
+ await this.deps.codegen.run(false);
3172
3708
  } catch (error) {
3173
- this.codegenLogger.error("Command registry regeneration failed", error);
3709
+ this.deps.codegenLogger.error("Command registry regeneration failed", error);
3174
3710
  } finally {
3175
3711
  this.isRegenerating = false;
3176
3712
  }
@@ -3181,30 +3717,28 @@ var DevRunner = class DevRunner {
3181
3717
  });
3182
3718
  }
3183
3719
  async loadConfig() {
3184
- const configPath = this.locator.locate();
3185
- return this.configLoader.load(configPath);
3720
+ const configPath = this.deps.locator.locate();
3721
+ return this.deps.configLoader.load(configPath);
3186
3722
  }
3187
3723
  };
3188
3724
 
3189
3725
  //#endregion
3190
3726
  //#region src/commands/dev/DevCommand.ts
3191
3727
  var DevCommand = class extends BaseCommand {
3192
- runner;
3193
- store;
3194
3728
  constructor() {
3195
3729
  super("dev", "Run a Seedcord instance from the config file", "Dev");
3196
- this.store = new DevStore();
3197
- this.runner = DevRunner.create(this.logger, this.store);
3198
3730
  }
3199
3731
  register(program) {
3200
3732
  program.command(this.name).description(this.description).action(async () => {
3733
+ const store = new DevStore();
3734
+ const runner = DevRunner.create(this.logger, store);
3201
3735
  const onSignal = () => {
3202
- this.runner.quit();
3736
+ runner.quit();
3203
3737
  };
3204
3738
  process.once("SIGINT", onSignal);
3205
3739
  process.once("SIGTERM", onSignal);
3206
3740
  try {
3207
- await this.runDevApp();
3741
+ await this.runDevApp(store, runner);
3208
3742
  } catch (error) {
3209
3743
  this.logger.error("Seedcord dev failed", error);
3210
3744
  process.exitCode = 1;
@@ -3219,18 +3753,21 @@ var DevCommand = class extends BaseCommand {
3219
3753
  printLogLocation() {
3220
3754
  process.stdout.write("seedcord dev stopped. logs: logs/\n");
3221
3755
  }
3222
- async runDevApp() {
3756
+ async runDevApp(store, runner) {
3757
+ let failure;
3223
3758
  let runResult = Promise.resolve();
3224
3759
  const { unmount, waitUntilExit } = render(React.createElement(DevApp, {
3225
- store: this.store,
3226
- onQuit: () => this.runner.quit(),
3227
- onDisconnect: () => this.runner.disconnect(),
3228
- onRestart: () => this.runner.restart(),
3229
- onRefreshCommands: (shouldRefresh) => this.runner.refreshCommands(shouldRefresh),
3760
+ store,
3761
+ onQuit: () => runner.quit(),
3762
+ onDisconnect: () => runner.disconnect(),
3763
+ onRestart: () => runner.restart(),
3764
+ onRefreshCommands: (shouldRefresh) => runner.refreshCommands(shouldRefresh),
3230
3765
  onReady: () => {
3231
- runResult = this.runner.run().finally(async () => {
3766
+ runResult = runner.run().finally(async () => {
3232
3767
  await LogStore.instance.flush();
3233
3768
  unmount();
3769
+ }).catch((error) => {
3770
+ failure = { error };
3234
3771
  });
3235
3772
  }
3236
3773
  }), {
@@ -3239,6 +3776,7 @@ var DevCommand = class extends BaseCommand {
3239
3776
  });
3240
3777
  await waitUntilExit();
3241
3778
  await runResult;
3779
+ if (failure) throw failure.error;
3242
3780
  }
3243
3781
  };
3244
3782