@seedcord/services 0.5.1 → 0.7.0-next.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/index.mjs CHANGED
@@ -1,1114 +1,1498 @@
1
- import { Envapter, Envapt } from 'envapt';
2
- import { format, transports, createLogger } from 'winston';
3
- import chalk2 from 'chalk';
4
- import { createServer } from 'http';
5
- import { EventEmitter } from 'events';
1
+ import { a as SeedcordErrorCode, i as isSeedcordError, t as SeedcordError } from "./SeedcordError-xd-Ib_-z.mjs";
2
+ import { Envapter } from "envapt";
3
+ import path from "node:path";
4
+ import winston, { createLogger, format, transports } from "winston";
5
+ import fs from "node:fs";
6
+ import stripAnsi from "strip-ansi";
7
+ import TransportStream from "winston-transport";
8
+ import { formatFilePath } from "@seedcord/utils";
9
+ import chalk from "chalk";
10
+ import { createServer } from "http";
11
+ import { EventEmitter } from "node:events";
6
12
 
7
- var __defProp = Object.defineProperty;
8
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
- var Logger = class _Logger {
10
- static {
11
- __name(this, "Logger");
12
- }
13
- static instances = /* @__PURE__ */ new Map();
14
- static instance(prefix) {
15
- let instance = this.instances.get(prefix);
16
- if (!instance) {
17
- instance = new _Logger(prefix);
18
- this.instances.set(prefix, instance);
19
- }
20
- return instance;
21
- }
22
- constructor(transportName) {
23
- const consoleTransport = this.createConsoleTransport(transportName);
24
- this.initializeLogger(consoleTransport);
25
- }
26
- getFormatCustomizations() {
27
- const padding = 7;
28
- return [
29
- format.errors({
30
- stack: true
31
- }),
32
- format.splat(),
33
- format.colorize({
34
- level: true
35
- }),
36
- format.timestamp({
37
- format: "D MMM, hh:mm:ss a"
38
- }),
39
- format.printf((info) => {
40
- const ts = String(info.timestamp ?? "");
41
- const lvl = String(info.level).padEnd(padding);
42
- const lbl = String(info.label ?? "");
43
- const msg = String(info.message ?? "");
44
- const base = `${ts} [${lvl}]: ${lbl} - ${msg}`;
45
- const splatSym = Symbol.for("splat");
46
- const raw = info[splatSym];
47
- const extras = Array.isArray(raw) ? raw : [];
48
- const cleaned = extras.filter((x) => !(x instanceof Error)).filter((x) => {
49
- if (!x) return false;
50
- if (typeof x !== "object") return true;
51
- return Object.keys(x).length > 0;
52
- });
53
- let rendered = base;
54
- if (typeof info.stack === "string") {
55
- rendered += `
56
- ${String(info.stack)}`;
57
- }
58
- if (cleaned.length) {
59
- const parts = [];
60
- for (const x of cleaned) {
61
- if (typeof x === "string") parts.push(x);
62
- else {
63
- try {
64
- parts.push(JSON.stringify(x, null, 2));
65
- } catch {
66
- parts.push(String(x));
67
- }
68
- }
69
- }
70
- rendered += `
71
- ${parts.join(" ")}`;
72
- }
73
- return rendered;
74
- })
75
- ];
76
- }
77
- createConsoleTransport(transportName) {
78
- return new transports.Console({
79
- format: format.combine(format.label({
80
- label: transportName
81
- }), ...this.getFormatCustomizations()),
82
- level: Envapter.isDevelopment ? "silly" : Envapter.isStaging ? "debug" : "info"
83
- });
84
- }
85
- initializeLogger(consoleTransport) {
86
- const transportsArray = [
87
- consoleTransport
88
- ];
89
- if (Envapter.isDevelopment) {
90
- const maxSizeInMB = 10;
91
- transportsArray.push(new transports.File({
92
- filename: "logs/application.log",
93
- level: "debug",
94
- format: format.combine(format.uncolorize(), format.errors({
95
- stack: true
96
- }), format.timestamp(), format.json({
97
- bigint: true,
98
- space: 2
99
- })),
100
- maxsize: maxSizeInMB * 1024 * 1024,
101
- maxFiles: 5,
102
- tailable: true
103
- }));
104
- }
105
- this.logger = createLogger({
106
- transports: transportsArray
107
- });
108
- }
109
- /**
110
- * Logs an error message with optional additional data.
111
- *
112
- * @param msg - The error message to log
113
- * @param args - Additional data to include in the log entry
114
- */
115
- error(msg, ...args) {
116
- this.logger.error(msg, ...args);
117
- }
118
- /**
119
- * Logs a warning message with optional additional data.
120
- *
121
- * @param msg - The warning message to log
122
- * @param args - Additional data to include in the log entry
123
- */
124
- warn(msg, ...args) {
125
- this.logger.warn(msg, ...args);
126
- }
127
- /**
128
- * Logs an informational message with optional additional data.
129
- *
130
- * @param msg - The informational message to log
131
- * @param args - Additional data to include in the log entry
132
- */
133
- info(msg, ...args) {
134
- this.logger.info(msg, ...args);
135
- }
136
- /**
137
- * Logs an HTTP-related message with optional additional data.
138
- *
139
- * @param msg - The HTTP message to log
140
- * @param args - Additional data to include in the log entry
141
- */
142
- http(msg, ...args) {
143
- this.logger.http(msg, ...args);
144
- }
145
- /**
146
- * Logs a verbose message with optional additional data.
147
- *
148
- * @param msg - The verbose message to log
149
- * @param args - Additional data to include in the log entry
150
- */
151
- verbose(msg, ...args) {
152
- this.logger.verbose(msg, ...args);
153
- }
154
- /**
155
- * Logs a debug message with optional additional data.
156
- *
157
- * @param msg - The debug message to log
158
- * @param args - Additional data to include in the log entry
159
- */
160
- debug(msg, ...args) {
161
- this.logger.debug(msg, ...args);
162
- }
163
- /**
164
- * Logs a silly/trace level message with optional additional data.
165
- *
166
- * @param msg - The silly message to log
167
- * @param args - Additional data to include in the log entry
168
- */
169
- silly(msg, ...args) {
170
- this.logger.silly(msg, ...args);
171
- }
172
- /**
173
- * Static method to log an error message with a specific prefix.
174
- * Creates or retrieves a logger instance for the given prefix.
175
- *
176
- * @param prefix - The logger prefix/label to use
177
- * @param msg - The error message to log
178
- * @param args - Additional data to include in the log entry
179
- */
180
- static Error(prefix, msg, ...args) {
181
- const logger = this.instance(prefix);
182
- logger.error(msg, ...args);
183
- }
184
- /**
185
- * Static method to log an informational message with a specific prefix.
186
- * Creates or retrieves a logger instance for the given prefix.
187
- *
188
- * @param prefix - The logger prefix/label to use
189
- * @param msg - The informational message to log
190
- * @param args - Additional data to include in the log entry
191
- */
192
- static Info(prefix, msg, ...args) {
193
- const logger = this.instance(prefix);
194
- logger.info(msg, ...args);
195
- }
196
- /**
197
- * Static method to log a warning message with a specific prefix.
198
- * Creates or retrieves a logger instance for the given prefix.
199
- *
200
- * @param prefix - The logger prefix/label to use
201
- * @param msg - The warning message to log
202
- * @param args - Additional data to include in the log entry
203
- */
204
- static Warn(prefix, msg, ...args) {
205
- const logger = this.instance(prefix);
206
- logger.warn(msg, ...args);
207
- }
208
- /**
209
- * Static method to log a debug message with a specific prefix.
210
- * Creates or retrieves a logger instance for the given prefix.
211
- *
212
- * @param prefix - The logger prefix/label to use
213
- * @param msg - The debug message to log
214
- * @param args - Additional data to include in the log entry
215
- */
216
- static Debug(prefix, msg, ...args) {
217
- const logger = this.instance(prefix);
218
- logger.debug(msg, ...args);
219
- }
220
- /**
221
- * Static method to log a silly/trace level message with a specific prefix.
222
- * Creates or retrieves a logger instance for the given prefix.
223
- *
224
- * @param prefix - The logger prefix/label to use
225
- * @param msg - The silly message to log
226
- * @param args - Additional data to include in the log entry
227
- */
228
- static Silly(prefix, msg, ...args) {
229
- const logger = this.instance(prefix);
230
- logger.silly(msg, ...args);
231
- }
13
+ //#region src/Logger/LogFormatter.ts
14
+ /**
15
+ * Formats log records for console and file outputs.
16
+ *
17
+ * Supports pretty-printed colored logs for development
18
+ * and JSON logs for production with optional ANSI stripping.
19
+ * @internal
20
+ */
21
+ var LogFormatter = class {
22
+ DEFAULT_PADDING = 7;
23
+ SPLAT = Symbol.for("splat");
24
+ safeString(value) {
25
+ if (typeof value === "string") return value;
26
+ if (value === void 0 || value === null) return "";
27
+ if (typeof value.toString === "function") return String(value.toString());
28
+ if (typeof value === "object") try {
29
+ return JSON.stringify(value);
30
+ } catch {
31
+ return "";
32
+ }
33
+ return "";
34
+ }
35
+ sanitizeAnsi(value) {
36
+ if (typeof value === "string") return stripAnsi(value);
37
+ if (value instanceof Error) {
38
+ const error = value;
39
+ const sanitized = new Error(stripAnsi(error.message));
40
+ sanitized.name = error.name;
41
+ if (typeof error.stack === "string") sanitized.stack = stripAnsi(error.stack);
42
+ return sanitized;
43
+ }
44
+ return value;
45
+ }
46
+ getExtras(info) {
47
+ const raw = info[this.SPLAT];
48
+ return Array.isArray(raw) ? raw : [];
49
+ }
50
+ FORMAT_SPECIFIERS = /%[sdifjoO]/gu;
51
+ HAD_FORMAT_KEY = Symbol.for("hadFormatSpecifiers");
52
+ SAVED_SPLAT_KEY = Symbol.for("savedSplat");
53
+ markFormatSpecifiers() {
54
+ return format((info) => {
55
+ const msg = typeof info.message === "string" ? info.message : "";
56
+ const extras = this.getExtras(info);
57
+ const matches = msg.match(this.FORMAT_SPECIFIERS);
58
+ const formatCount = matches ? matches.length : 0;
59
+ info[this.HAD_FORMAT_KEY] = formatCount;
60
+ info[this.SAVED_SPLAT_KEY] = [...extras];
61
+ return info;
62
+ })();
63
+ }
64
+ createPreFormat() {
65
+ return this.markFormatSpecifiers();
66
+ }
67
+ preserveErrorFormatting() {
68
+ return format((info) => {
69
+ const extras = this.getExtras(info);
70
+ for (const item of extras) if (item instanceof Error && /\u001b/.test(item.name)) {
71
+ const originalName = item.name;
72
+ const plainName = stripAnsi(item.name);
73
+ const formatted = item;
74
+ formatted.__formattedName = originalName;
75
+ formatted.__plainName = plainName;
76
+ }
77
+ return info;
78
+ })();
79
+ }
80
+ restoreErrorFormatting() {
81
+ return format((info) => {
82
+ if (typeof info.stack === "string") {
83
+ const extras = this.getExtras(info);
84
+ for (const item of extras) if (item instanceof Error) {
85
+ const { __formattedName: formattedName, __plainName: plainName } = item;
86
+ if (typeof formattedName === "string" && typeof plainName === "string") info.stack = info.stack.replace(new RegExp(`^${this.escapeRegex(plainName)}`, "m"), formattedName);
87
+ }
88
+ }
89
+ return info;
90
+ })();
91
+ }
92
+ escapeRegex(str) {
93
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
94
+ }
95
+ /**
96
+ * Creates pretty-printed format with colors and timestamps.
97
+ *
98
+ * Ideal for development environments where human readability matters.
99
+ *
100
+ * @param options - Formatting options including padding and ANSI stripping
101
+ * @returns Array of Winston format transformers
102
+ */
103
+ pretty(options = {}) {
104
+ const padding = options.padding ?? this.DEFAULT_PADDING;
105
+ return [
106
+ this.preserveErrorFormatting(),
107
+ format.errors({ stack: true }),
108
+ this.restoreErrorFormatting(),
109
+ format.splat(),
110
+ format.colorize({ level: true }),
111
+ format.timestamp({ format: "D MMM, hh:mm:ss a" }),
112
+ format.printf((info) => {
113
+ let ts = this.safeString(info.timestamp);
114
+ let lvl = this.safeString(info.level).padEnd(padding);
115
+ let lbl = this.safeString(info.label);
116
+ let msg = this.safeString(info.message);
117
+ if (options.stripExtras) {
118
+ ts = stripAnsi(ts);
119
+ lvl = stripAnsi(lvl);
120
+ lbl = stripAnsi(lbl);
121
+ msg = stripAnsi(msg);
122
+ }
123
+ const base = `${ts} [${lvl}]: ${lbl} - ${msg}`;
124
+ const savedExtras = info[this.SAVED_SPLAT_KEY];
125
+ const extras = Array.isArray(savedExtras) ? savedExtras : this.getExtras(info);
126
+ let rendered = base;
127
+ if (typeof info.stack === "string") {
128
+ let stack = this.safeString(info.stack);
129
+ if (options.stripExtras) stack = stripAnsi(stack);
130
+ rendered += `\n${stack}`;
131
+ }
132
+ const cleaned = options.stripExtras ? extras.map((entry) => this.sanitizeAnsi(entry)) : extras;
133
+ const rawFormatCount = info[this.HAD_FORMAT_KEY];
134
+ const formatSpecifierCount = typeof rawFormatCount === "number" ? rawFormatCount : 0;
135
+ const filtered = cleaned.filter((x, index) => {
136
+ if (x === null || x === void 0) return false;
137
+ if (x instanceof Error && typeof info.stack === "string") return false;
138
+ if (typeof x !== "object") return index >= formatSpecifierCount;
139
+ return Object.keys(x).length > 0;
140
+ });
141
+ if (filtered.length) {
142
+ const primitives = [];
143
+ const objects = [];
144
+ for (const x of filtered) if (typeof x === "string" || typeof x === "number" || typeof x === "boolean") primitives.push(String(x));
145
+ else try {
146
+ objects.push(JSON.stringify(x, null, 2));
147
+ } catch {
148
+ objects.push(String(x));
149
+ }
150
+ if (primitives.length) rendered += ` ${primitives.join(" ")}`;
151
+ if (objects.length) rendered += `\n${objects.join("\n")}`;
152
+ }
153
+ return rendered;
154
+ })
155
+ ];
156
+ }
157
+ /**
158
+ * Creates JSON format for structured logging.
159
+ *
160
+ * Best for production environments where logs are parsed by tools.
161
+ *
162
+ * @param options - JSON formatting options including ANSI stripping and minimal mode
163
+ * @returns Array of Winston format transformers
164
+ */
165
+ json(options = {}) {
166
+ const base = [format.timestamp(), format.errors({ stack: true })];
167
+ if (options.stripAnsi) base.push(format((info) => {
168
+ info.message = typeof info.message === "string" ? stripAnsi(info.message) : info.message;
169
+ if (typeof info.stack === "string") info.stack = stripAnsi(info.stack);
170
+ const extras = this.getExtras(info);
171
+ if (extras.length) info.extras = extras.map((entry) => this.sanitizeAnsi(entry));
172
+ return info;
173
+ })());
174
+ base.push(options.minimal ? format.json({}) : format.json({
175
+ bigint: true,
176
+ space: 0
177
+ }));
178
+ return base;
179
+ }
232
180
  };
233
181
 
234
- // src/CooldownManager.ts
235
- var CooldownManager = class {
236
- static {
237
- __name(this, "CooldownManager");
238
- }
239
- window;
240
- Err;
241
- msg;
242
- map = /* @__PURE__ */ new Map();
243
- /**
244
- * Creates a new CooldownManager instance.
245
- *
246
- * @param opts - Configuration options for the cooldown behavior
247
- */
248
- constructor(opts = {}) {
249
- this.window = opts.cooldown ?? 1e3;
250
- this.Err = opts.err ?? Error;
251
- this.msg = opts.message ?? "Cooldown active";
252
- }
253
- /**
254
- * Records usage timestamp for a key without any cooldown checks.
255
- *
256
- * @param key - The unique identifier for the cooldown entry
257
- */
258
- set(key) {
259
- this.map.set(key, Date.now());
260
- }
261
- /**
262
- * Verifies cooldown status for a key and updates timestamp if not active.
263
- *
264
- * If the cooldown is still active, throws the configured error.
265
- * If not active, updates the timestamp and returns successfully.
266
- *
267
- * @param key - The unique identifier to check cooldown for
268
- * @throws An {@link Err} When the cooldown is still active for the given key
269
- */
270
- check(key) {
271
- const now = Date.now();
272
- const last = this.map.get(key);
273
- const remaining = this.window - (now - (last ?? 0));
274
- if (Envapter.isDevelopment && remaining > 0) {
275
- Logger.Debug("CooldownManager", `${key} - ${remaining}ms remaining`);
276
- }
277
- if (last !== void 0 && remaining > 0) {
278
- throw new this.Err(this.msg, remaining);
279
- }
280
- this.map.set(key, now);
281
- }
282
- /**
283
- * Checks if a key is currently cooling down without updating timestamp.
284
- *
285
- * @param key - The unique identifier to check
286
- * @returns True if the key is still cooling down, false otherwise
287
- */
288
- isActive(key) {
289
- const last = this.map.get(key);
290
- return last !== void 0 && Date.now() - last < this.window;
291
- }
292
- /**
293
- * Removes a key from the cooldown map.
294
- *
295
- * @param key - The unique identifier to remove (useful for manual resets)
296
- */
297
- clear(key) {
298
- this.map.delete(key);
299
- }
182
+ //#endregion
183
+ //#region src/Logger/Transports/SinkTransport.ts
184
+ /**
185
+ * Winston transport that forwards logs to custom sinks.
186
+ * @internal
187
+ */
188
+ var SinkTransport = class extends TransportStream {
189
+ channelName;
190
+ sink;
191
+ constructor(options) {
192
+ super(options);
193
+ this.channelName = options.channel;
194
+ this.sink = options.sink;
195
+ }
196
+ log(info, callback) {
197
+ setImmediate(() => this.emit("logged", info));
198
+ const rendered = this.resolveRendered(info);
199
+ const channel = this.resolveChannel(info);
200
+ this.sink.onLog({
201
+ channel,
202
+ rendered,
203
+ info
204
+ });
205
+ callback();
206
+ }
207
+ resolveChannel(info) {
208
+ const channel = info.channel;
209
+ return typeof channel === "string" ? channel : this.channelName;
210
+ }
211
+ resolveRendered(info) {
212
+ const msg = info[Symbol.for("message")];
213
+ if (typeof msg === "string") return msg;
214
+ const fallback = info.message;
215
+ if (typeof fallback === "string") return fallback;
216
+ if (fallback instanceof Error) return fallback.stack ?? fallback.message;
217
+ return String(fallback ?? "");
218
+ }
300
219
  };
301
220
 
302
- // src/Errors/ErrorCodes.ts
303
- var SeedcordErrorCode = /* @__PURE__ */ (function(SeedcordErrorCode2) {
304
- SeedcordErrorCode2[SeedcordErrorCode2["ConfigMissingDiscordToken"] = 1001] = "ConfigMissingDiscordToken";
305
- SeedcordErrorCode2[SeedcordErrorCode2["ConfigUnknownExceptionWebhookMissing"] = 1002] = "ConfigUnknownExceptionWebhookMissing";
306
- SeedcordErrorCode2[SeedcordErrorCode2["ConfigUnknownExceptionWebhookInvalid"] = 1003] = "ConfigUnknownExceptionWebhookInvalid";
307
- SeedcordErrorCode2[SeedcordErrorCode2["LifecycleAddAfterCompletion"] = 1101] = "LifecycleAddAfterCompletion";
308
- SeedcordErrorCode2[SeedcordErrorCode2["LifecycleAddDuringRun"] = 1102] = "LifecycleAddDuringRun";
309
- SeedcordErrorCode2[SeedcordErrorCode2["LifecycleRemoveDuringRun"] = 1103] = "LifecycleRemoveDuringRun";
310
- SeedcordErrorCode2[SeedcordErrorCode2["LifecycleUnknownPhase"] = 1104] = "LifecycleUnknownPhase";
311
- SeedcordErrorCode2[SeedcordErrorCode2["LifecyclePhaseFailures"] = 1105] = "LifecyclePhaseFailures";
312
- SeedcordErrorCode2[SeedcordErrorCode2["LifecycleTaskTimeout"] = 1106] = "LifecycleTaskTimeout";
313
- SeedcordErrorCode2[SeedcordErrorCode2["CoreSingletonViolation"] = 1201] = "CoreSingletonViolation";
314
- SeedcordErrorCode2[SeedcordErrorCode2["CorePluginAfterInit"] = 1202] = "CorePluginAfterInit";
315
- SeedcordErrorCode2[SeedcordErrorCode2["CorePluginKeyExists"] = 1203] = "CorePluginKeyExists";
316
- SeedcordErrorCode2[SeedcordErrorCode2["CoreBotRoleMissing"] = 1204] = "CoreBotRoleMissing";
317
- SeedcordErrorCode2[SeedcordErrorCode2["DecoratorInteractionEventFilter"] = 1301] = "DecoratorInteractionEventFilter";
318
- SeedcordErrorCode2[SeedcordErrorCode2["DecoratorMethodNotFound"] = 1302] = "DecoratorMethodNotFound";
319
- SeedcordErrorCode2[SeedcordErrorCode2["DecoratorCommandAlreadyRegistered"] = 1303] = "DecoratorCommandAlreadyRegistered";
320
- SeedcordErrorCode2[SeedcordErrorCode2["DecoratorCommandGlobalWithGuilds"] = 1304] = "DecoratorCommandGlobalWithGuilds";
321
- SeedcordErrorCode2[SeedcordErrorCode2["DecoratorCommandGuildWithoutGuilds"] = 1305] = "DecoratorCommandGuildWithoutGuilds";
322
- SeedcordErrorCode2[SeedcordErrorCode2["DecoratorInvalidMiddlewarePriority"] = 1306] = "DecoratorInvalidMiddlewarePriority";
323
- SeedcordErrorCode2[SeedcordErrorCode2["UtilHexInputType"] = 1401] = "UtilHexInputType";
324
- SeedcordErrorCode2[SeedcordErrorCode2["UtilHexInvalid"] = 1402] = "UtilHexInvalid";
325
- SeedcordErrorCode2[SeedcordErrorCode2["UtilInvalidSlashRouteArgument"] = 1403] = "UtilInvalidSlashRouteArgument";
326
- SeedcordErrorCode2[SeedcordErrorCode2["PluginMongoServiceDecoratorMissing"] = 2101] = "PluginMongoServiceDecoratorMissing";
327
- SeedcordErrorCode2[SeedcordErrorCode2["PluginMongoModelDecoratorMissing"] = 2102] = "PluginMongoModelDecoratorMissing";
328
- SeedcordErrorCode2[SeedcordErrorCode2["PluginMongoConnectionFailed"] = 2103] = "PluginMongoConnectionFailed";
329
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgServiceDecoratorMissing"] = 2201] = "PluginKpgServiceDecoratorMissing";
330
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgServiceTableMissing"] = 2202] = "PluginKpgServiceTableMissing";
331
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgInvalidStepCount"] = 2203] = "PluginKpgInvalidStepCount";
332
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgUnknownDirection"] = 2204] = "PluginKpgUnknownDirection";
333
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgUnresolvedMigrationsPath"] = 2205] = "PluginKpgUnresolvedMigrationsPath";
334
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgNoMigrationFiles"] = 2206] = "PluginKpgNoMigrationFiles";
335
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgInvalidMigrationModule"] = 2207] = "PluginKpgInvalidMigrationModule";
336
- SeedcordErrorCode2[SeedcordErrorCode2["PluginKpgNonErrorFailure"] = 2208] = "PluginKpgNonErrorFailure";
337
- return SeedcordErrorCode2;
338
- })({});
221
+ //#endregion
222
+ //#region src/Logger/TransportFactory.ts
223
+ /**
224
+ * Creates Winston transports with proper formatting and file path resolution.
225
+ *
226
+ * Builds console and file transports with environment-aware defaults,
227
+ * filename template expansion, and format selection.
228
+ * @internal
229
+ */
230
+ var TransportFactory = class {
231
+ formatter;
232
+ MILLISECOND_PAD = 3;
233
+ cachedSessionTimestamp = null;
234
+ constructor() {
235
+ this.formatter = new LogFormatter();
236
+ }
237
+ ensureDir(filepath) {
238
+ const dir = path.dirname(filepath);
239
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
240
+ }
241
+ withDefaultLabel(label) {
242
+ return format.combine(format((info) => {
243
+ info.label ??= label;
244
+ return info;
245
+ })());
246
+ }
247
+ buildPreFormat() {
248
+ return format.combine(this.formatter.createPreFormat());
249
+ }
250
+ buildConsoleFormat(label) {
251
+ if (Envapter.isProduction) return format.combine(this.withDefaultLabel(label), ...this.formatter.json({ stripAnsi: true }));
252
+ return format.combine(this.withDefaultLabel(label), ...this.formatter.pretty());
253
+ }
254
+ buildFileFormat(label, mode, stripAnsi) {
255
+ const formats = mode === "pretty" ? this.formatter.pretty({ stripExtras: stripAnsi }) : this.formatter.json({ stripAnsi });
256
+ return format.combine(this.withDefaultLabel(label), ...formats);
257
+ }
258
+ buildStreamFormat(label, mode, stripAnsi) {
259
+ const formats = mode === "pretty" ? this.formatter.pretty({ stripExtras: stripAnsi }) : this.formatter.json({ stripAnsi });
260
+ return format.combine(this.withDefaultLabel(label), ...formats);
261
+ }
262
+ pad(value) {
263
+ return value.toString().padStart(2, "0");
264
+ }
265
+ buildTimestamp() {
266
+ if (this.cachedSessionTimestamp) return this.cachedSessionTimestamp;
267
+ const now = /* @__PURE__ */ new Date();
268
+ const yyyy = now.getFullYear();
269
+ const mm = this.pad(now.getMonth() + 1);
270
+ const dd = this.pad(now.getDate());
271
+ const hh = this.pad(now.getHours());
272
+ const min = this.pad(now.getMinutes());
273
+ const ss = this.pad(now.getSeconds());
274
+ const ms = now.getMilliseconds().toString().padStart(this.MILLISECOND_PAD, "0");
275
+ const date = `${yyyy}-${mm}-${dd}`;
276
+ const timestamp = `${date}-${hh}${min}${ss}-${ms}`;
277
+ this.cachedSessionTimestamp = {
278
+ date,
279
+ timestamp
280
+ };
281
+ return this.cachedSessionTimestamp;
282
+ }
283
+ resolveFilename(template, channel) {
284
+ const { date, timestamp } = this.buildTimestamp();
285
+ return template.replaceAll("{channel}", channel).replaceAll("{date}", date).replaceAll("{timestamp}", timestamp);
286
+ }
287
+ /**
288
+ * Builds a Winston transport from configuration.
289
+ *
290
+ * Creates console, file, or stream transport with proper formatting,
291
+ * level filtering, and file rotation settings.
292
+ *
293
+ * @param input - Transport configuration parameters
294
+ * @returns Configured Winston transport instance
295
+ */
296
+ build(input) {
297
+ const effectiveFormat = input.config.format ?? input.defaultFormat;
298
+ const shouldStripAnsi = input.config.stripAnsi ?? input.stripAnsi;
299
+ const level = input.config.level ?? input.level;
300
+ if (input.config.type === "console") return new transports.Console({
301
+ level,
302
+ format: this.buildConsoleFormat(input.label)
303
+ });
304
+ if (input.config.type === "stream") return new transports.Stream({
305
+ level,
306
+ stream: input.config.stream,
307
+ format: this.buildStreamFormat(input.label, effectiveFormat, shouldStripAnsi)
308
+ });
309
+ const filenameTemplate = input.config.filename ?? "logs/application-{timestamp}.log";
310
+ const resolvedFilename = this.resolveFilename(filenameTemplate, input.channel);
311
+ this.ensureDir(resolvedFilename);
312
+ return new transports.File({
313
+ level,
314
+ filename: resolvedFilename,
315
+ ...input.config.maxSize !== void 0 ? { maxsize: input.config.maxSize } : {},
316
+ ...input.config.maxFiles !== void 0 ? { maxFiles: input.config.maxFiles } : {},
317
+ tailable: true,
318
+ format: this.buildFileFormat(input.label, effectiveFormat, shouldStripAnsi)
319
+ });
320
+ }
321
+ /**
322
+ * Builds a sink transport for routing logs to custom destinations.
323
+ *
324
+ * @param input - Transport configuration with channel and level info
325
+ * @param sink - Custom sink to receive log entries
326
+ * @returns Configured sink transport instance
327
+ */
328
+ buildSinkTransport(input, sink) {
329
+ const format = this.buildConsoleFormat(input.label);
330
+ return new SinkTransport({
331
+ level: input.level,
332
+ channel: input.channel,
333
+ sink,
334
+ format
335
+ });
336
+ }
337
+ };
339
338
 
340
- // src/Errors/ErrorMessages.ts
341
- var messages = {
342
- [SeedcordErrorCode.ConfigMissingDiscordToken]: () => "Missing DISCORD_BOT_TOKEN environment variable.",
343
- [SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing]: () => "Missing UNKNOWN_EXCEPTION_WEBHOOK_URL environment variable.",
344
- [SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid]: () => "Invalid UNKNOWN_EXCEPTION_WEBHOOK_URL value.",
345
- [SeedcordErrorCode.LifecycleAddAfterCompletion]: () => "Cannot add tasks after startup sequence has already completed.",
346
- [SeedcordErrorCode.LifecycleAddDuringRun]: () => "Cannot add tasks while startup sequence is in progress.",
347
- [SeedcordErrorCode.LifecycleRemoveDuringRun]: () => "Cannot remove tasks while startup sequence is in progress.",
348
- [SeedcordErrorCode.LifecycleUnknownPhase]: (phase) => `Unknown phase: ${String(phase)}.`,
349
- [SeedcordErrorCode.LifecyclePhaseFailures]: (phase, failures) => `Phase ${phase} completed with ${failures} failed task${failures === 1 ? "" : "s"}.`,
350
- [SeedcordErrorCode.LifecycleTaskTimeout]: (taskName, timeout) => `Task "${taskName}" timed out after ${timeout}ms.`,
351
- [SeedcordErrorCode.CoreSingletonViolation]: () => "Seedcord can only be instantiated once. Use the existing instance instead.",
352
- [SeedcordErrorCode.CorePluginAfterInit]: () => "Cannot attach a plugin after initialization.",
353
- [SeedcordErrorCode.CorePluginKeyExists]: (key) => `Plugin with key "${key}" already exists.`,
354
- [SeedcordErrorCode.CoreBotRoleMissing]: (guildId) => guildId ? `Bot role not found in guild ${guildId}.` : "Bot role not found in guild.",
355
- [SeedcordErrorCode.DecoratorInteractionEventFilter]: () => "Interaction middleware cannot specify event filters.",
356
- [SeedcordErrorCode.DecoratorMethodNotFound]: () => "Decorator could not locate the original method. Ensure the method exists before applying the decorator.",
357
- [SeedcordErrorCode.DecoratorCommandAlreadyRegistered]: (commandName, existingScope, requestedScope) => `Command "${commandName}" is already registered as a "${existingScope}" command and cannot be re-registered as a "${requestedScope}" command.`,
358
- [SeedcordErrorCode.DecoratorCommandGlobalWithGuilds]: () => 'RegisterCommand("global") cannot have guilds specified.',
359
- [SeedcordErrorCode.DecoratorCommandGuildWithoutGuilds]: () => 'RegisterCommand("guild") requires a non-empty guilds array.',
360
- [SeedcordErrorCode.DecoratorInvalidMiddlewarePriority]: () => "Middleware priority must be a finite number.",
361
- [SeedcordErrorCode.UtilHexInputType]: () => "hexToNumber expects a string input.",
362
- [SeedcordErrorCode.UtilHexInvalid]: () => "Invalid hex string.",
363
- [SeedcordErrorCode.UtilInvalidSlashRouteArgument]: () => "Invalid argument passed to buildSlashRoute.",
364
- [SeedcordErrorCode.PluginMongoServiceDecoratorMissing]: (className) => `Missing @RegisterMongoService on ${className}.`,
365
- [SeedcordErrorCode.PluginMongoModelDecoratorMissing]: (className) => `Missing @RegisterMongoModel on ${className}.`,
366
- [SeedcordErrorCode.PluginMongoConnectionFailed]: (databaseName) => databaseName ? `Could not connect to MongoDB (${databaseName}).` : "Could not connect to MongoDB.",
367
- [SeedcordErrorCode.PluginKpgServiceDecoratorMissing]: (className) => `Missing @RegisterKpgService on ${className}.`,
368
- [SeedcordErrorCode.PluginKpgServiceTableMissing]: (className) => `Missing table metadata for ${className}. Provide a table via @RegisterKpgService().`,
369
- [SeedcordErrorCode.PluginKpgInvalidStepCount]: () => "Migration step count must be a non-negative integer.",
370
- [SeedcordErrorCode.PluginKpgUnknownDirection]: (direction) => `Unknown migration direction: ${String(direction)}.`,
371
- [SeedcordErrorCode.PluginKpgUnresolvedMigrationsPath]: (label) => `Unable to resolve migrations at path: ${label}.`,
372
- [SeedcordErrorCode.PluginKpgNoMigrationFiles]: () => "No migration files provided.",
373
- [SeedcordErrorCode.PluginKpgInvalidMigrationModule]: (filePath) => `Migration file ${filePath} must export async functions up and down.`,
374
- [SeedcordErrorCode.PluginKpgNonErrorFailure]: (message) => `Migration failure: ${message}.`
339
+ //#endregion
340
+ //#region src/Logger/LoggerChannelRegistry.ts
341
+ /**
342
+ * Manages Winston logger instances per channel with caching.
343
+ *
344
+ * Stores channel configuration and creates transports for each channel
345
+ * with environment-aware defaults.
346
+ */
347
+ var LoggerChannelRegistry = class LoggerChannelRegistry {
348
+ static _instance = null;
349
+ nextSinkId = 1;
350
+ sinks = /* @__PURE__ */ new Map();
351
+ DEFAULT_LEVEL = Envapter.isDevelopment ? "silly" : Envapter.isStaging ? "debug" : "info";
352
+ config = {
353
+ defaultChannel: "default",
354
+ channels: {},
355
+ files: {
356
+ maxSizeMB: 10,
357
+ maxFiles: 5,
358
+ patterns: {
359
+ dev: "logs/{channel}-{timestamp}.log",
360
+ staging: "logs/staging-{date}-{timestamp}.jsonl",
361
+ prod: "logs/production-{date}.jsonl"
362
+ }
363
+ }
364
+ };
365
+ FORMAT = Envapter.isDevelopment ? "pretty" : "json";
366
+ cache = /* @__PURE__ */ new Map();
367
+ transportFactory;
368
+ constructor() {
369
+ this.transportFactory = new TransportFactory();
370
+ }
371
+ /**
372
+ * Gets the singleton instance of the registry.
373
+ */
374
+ static get instance() {
375
+ return this._instance ??= new LoggerChannelRegistry();
376
+ }
377
+ getDefaultChannelConfig(name) {
378
+ return {
379
+ name,
380
+ level: this.DEFAULT_LEVEL,
381
+ transports: [{
382
+ type: "console",
383
+ level: this.DEFAULT_LEVEL,
384
+ format: this.FORMAT,
385
+ stripAnsi: !Envapter.isDevelopment
386
+ }, {
387
+ type: "file",
388
+ level: this.DEFAULT_LEVEL,
389
+ filename: Envapter.isDevelopment ? this.config.files.patterns.dev : Envapter.isStaging ? this.config.files.patterns.staging : this.config.files.patterns.prod,
390
+ format: this.FORMAT,
391
+ stripAnsi: true,
392
+ maxSize: this.config.files.maxSizeMB * 1024 * 1024,
393
+ maxFiles: this.config.files.maxFiles
394
+ }]
395
+ };
396
+ }
397
+ mergeChannelConfig(base, override) {
398
+ if (!override) return base;
399
+ const level = override.level ?? base.level;
400
+ const stripAnsi = override.stripAnsi ?? base.stripAnsi;
401
+ const format = override.format ?? base.format;
402
+ const transports = override.transports ?? base.transports;
403
+ return {
404
+ name: override.name || base.name,
405
+ ...level !== void 0 ? { level } : {},
406
+ ...stripAnsi !== void 0 ? { stripAnsi } : {},
407
+ ...format !== void 0 ? { format } : {},
408
+ ...transports !== void 0 ? { transports } : {}
409
+ };
410
+ }
411
+ /**
412
+ * Updates global logger configuration and clears cache.
413
+ *
414
+ * @param config - Partial configuration to merge with existing settings
415
+ */
416
+ configure(config) {
417
+ this.disposeCachedLoggers();
418
+ this.config = {
419
+ ...this.config,
420
+ ...config,
421
+ channels: {
422
+ ...this.config.channels,
423
+ ...config.channels ?? {}
424
+ }
425
+ };
426
+ this.cache.clear();
427
+ }
428
+ disposeCachedLoggers() {
429
+ for (const logger of this.cache.values()) for (const transport of [...logger.transports]) {
430
+ logger.remove(transport);
431
+ transport.close?.();
432
+ }
433
+ for (const record of this.sinks.values()) {
434
+ record.transportsByChannel.clear();
435
+ record.removedConsoleByChannel.clear();
436
+ }
437
+ }
438
+ /**
439
+ * Returns the name of the default channel.
440
+ */
441
+ getDefaultChannel() {
442
+ return this.config.defaultChannel;
443
+ }
444
+ /**
445
+ * Returns a list of all known channels (configured or cached).
446
+ */
447
+ getChannels() {
448
+ const configuredChannels = Object.keys(this.config.channels);
449
+ const cachedChannels = Array.from(this.cache.keys());
450
+ const allChannels = new Set([
451
+ ...configuredChannels,
452
+ ...cachedChannels,
453
+ this.config.defaultChannel
454
+ ]);
455
+ return Array.from(allChannels).sort();
456
+ }
457
+ /**
458
+ * Gets the log file path for a specific channel, if one exists.
459
+ *
460
+ * @param channel - Channel name to get log file path for
461
+ * @returns The log file path, or null if no file transport exists
462
+ */
463
+ getLogFilePath(channel) {
464
+ const logger = this.cache.get(channel);
465
+ if (!logger) return null;
466
+ for (const transport of logger.transports) if (transport instanceof winston.transports.File) return path.resolve(transport.dirname, transport.filename);
467
+ return null;
468
+ }
469
+ /**
470
+ * Gets or creates a Winston logger instance for the given channel.
471
+ *
472
+ * @param channel - Channel name to get logger for
473
+ * @returns Configured Winston logger instance
474
+ */
475
+ get(channel) {
476
+ const cached = this.cache.get(channel);
477
+ if (cached) return cached;
478
+ const channelConfig = this.mergeChannelConfig(this.getDefaultChannelConfig(channel), this.config.channels[channel]);
479
+ const effectiveLevel = channelConfig.level ?? this.DEFAULT_LEVEL;
480
+ const consoleMuted = this.isConsoleMuted();
481
+ const transports = (channelConfig.transports ?? []).filter((transportConfig) => !(consoleMuted && transportConfig.type === "console")).map((transportConfig) => this.transportFactory.build({
482
+ channel,
483
+ label: channel,
484
+ level: effectiveLevel,
485
+ config: transportConfig,
486
+ defaultFormat: channelConfig.format ?? "pretty",
487
+ stripAnsi: channelConfig.stripAnsi ?? true
488
+ }));
489
+ const logger = createLogger({
490
+ level: effectiveLevel,
491
+ format: this.transportFactory.buildPreFormat(),
492
+ transports
493
+ });
494
+ for (const entry of this.sinks.values()) this.applySinkToCachedLogger(channel, logger, entry);
495
+ this.cache.set(channel, logger);
496
+ return logger;
497
+ }
498
+ isConsoleMuted() {
499
+ for (const entry of this.sinks.values()) if (entry.muteConsole) return true;
500
+ return false;
501
+ }
502
+ /**
503
+ * Installs a custom sink to capture log output across all channels.
504
+ *
505
+ * Useful for routing logs to custom destinations like TUIs or remote services.
506
+ *
507
+ * @param sink - Custom sink implementation to receive log entries
508
+ * @param options - Optional configuration for console muting behavior
509
+ * @returns Handle to dispose the sink when no longer needed
510
+ */
511
+ installSink(sink, options) {
512
+ const id = this.nextSinkId;
513
+ this.nextSinkId += 1;
514
+ const record = {
515
+ sink,
516
+ muteConsole: options?.muteConsole ?? true,
517
+ transportsByChannel: /* @__PURE__ */ new Map(),
518
+ removedConsoleByChannel: /* @__PURE__ */ new Map()
519
+ };
520
+ this.sinks.set(id, record);
521
+ for (const [channel, logger] of this.cache.entries()) this.applySinkToCachedLogger(channel, logger, record);
522
+ return new class {
523
+ registry;
524
+ key;
525
+ constructor(registry, key) {
526
+ this.registry = registry;
527
+ this.key = key;
528
+ }
529
+ dispose() {
530
+ this.registry.uninstallSink(this.key);
531
+ }
532
+ }(this, id);
533
+ }
534
+ uninstallSink(id) {
535
+ const record = this.sinks.get(id);
536
+ if (!record) return;
537
+ for (const [channel, logger] of this.cache.entries()) {
538
+ const sinkTransport = record.transportsByChannel.get(channel);
539
+ if (sinkTransport) logger.remove(sinkTransport);
540
+ const removed = record.removedConsoleByChannel.get(channel);
541
+ if (removed?.length) for (const t of removed) logger.add(t);
542
+ }
543
+ this.sinks.delete(id);
544
+ }
545
+ applySinkToCachedLogger(channel, logger, record) {
546
+ if (record.muteConsole) {
547
+ const removed = [];
548
+ for (const t of logger.transports) if (t instanceof winston.transports.Console) removed.push(t);
549
+ if (removed.length) {
550
+ for (const t of removed) logger.remove(t);
551
+ record.removedConsoleByChannel.set(channel, removed);
552
+ }
553
+ }
554
+ const sinkTransport = this.transportFactory.buildSinkTransport({
555
+ channel,
556
+ label: channel,
557
+ level: logger.level
558
+ }, record.sink);
559
+ logger.add(sinkTransport);
560
+ record.transportsByChannel.set(channel, sinkTransport);
561
+ }
375
562
  };
376
- function formatSeedcordErrorMessage(code, args) {
377
- const formatter = messages[code];
378
- const resolvedArgs = args ?? [];
379
- return formatter(...resolvedArgs);
380
- }
381
- __name(formatSeedcordErrorMessage, "formatSeedcordErrorMessage");
382
- function resolveIdentifier(code) {
383
- return SeedcordErrorCode[code];
384
- }
385
- __name(resolveIdentifier, "resolveIdentifier");
386
- function resolveMessage(code, args) {
387
- return formatSeedcordErrorMessage(code, args);
388
- }
389
- __name(resolveMessage, "resolveMessage");
390
- function formatErrorName(name, _identifier, code) {
391
- return `${chalk2.bold.red(name)}[${chalk2.gray(code)}]`;
392
- }
393
- __name(formatErrorName, "formatErrorName");
394
- var SeedcordError = class extends Error {
395
- static {
396
- __name(this, "SeedcordError");
397
- }
398
- code;
399
- identifier;
400
- constructor(code, args, options) {
401
- const message = resolveMessage(code, args);
402
- super(message, options);
403
- this.code = code;
404
- this.identifier = resolveIdentifier(code);
405
- this.name = formatErrorName(new.target.name, this.identifier, this.code);
406
- Object.setPrototypeOf(this, new.target.prototype);
407
- if (typeof Error.captureStackTrace === "function") {
408
- Error.captureStackTrace(this, new.target);
409
- }
410
- }
563
+
564
+ //#endregion
565
+ //#region src/Logger/LoggerUtilities.ts
566
+ /**
567
+ * Provides access to common logging utilities.
568
+ */
569
+ var LoggerUtilities = class {
570
+ logger;
571
+ constructor(logger) {
572
+ this.logger = logger;
573
+ }
574
+ arrow(text) {
575
+ return `${chalk.gray("→")} ${text}`;
576
+ }
577
+ /**
578
+ * Logs a single item with an arrow prefix.
579
+ * @param text - The text to log
580
+ */
581
+ item(text, level = "info") {
582
+ this.logger[level](this.arrow(text));
583
+ }
584
+ /**
585
+ * Logs a list of items with optional heading.
586
+ *
587
+ * @param items - Array of items to log as a list
588
+ * @param heading - Optional heading to display above the list
589
+ */
590
+ list(items, heading, level = "info") {
591
+ if (heading) this.logger[level](heading);
592
+ for (const item of items) this.logger[level](this.arrow(item));
593
+ }
594
+ /**
595
+ * Logs a summary with title and key-value pairs.
596
+ * Example: "Loaded: 5 handlers, 3 commands"
597
+ *
598
+ * @param title - The title of the summary
599
+ * @param items - Object with counts/values to display
600
+ */
601
+ summary(title, items, level = "info") {
602
+ const entries = Object.entries(items).map(([key, value]) => `${chalk.magenta.bold(String(value))} ${key}`);
603
+ this.logger[level](`${chalk.bold.green(title)}: ${entries.join(", ")}`);
604
+ }
605
+ /**
606
+ * Logs a component registration message.
607
+ *
608
+ * @param name - Name of the component being registered
609
+ * @param from - File path the component is from
610
+ * @param type - Optional type label (e.g., 'middleware', 'handler')
611
+ */
612
+ registration(name, from, type, level = "info") {
613
+ const scope = type ? `${type} ` : "";
614
+ this.logger[level](`${chalk.italic("Registered")} ${chalk.bold.yellow(scope)}${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(from))}`);
615
+ }
616
+ /**
617
+ * Logs component initialization start/end.
618
+ *
619
+ * @param component - Name of the component
620
+ * @param action - 'start' or 'end' to indicate initialization phase
621
+ */
622
+ initialization(component, action, level = "info") {
623
+ const verb = action === "start" ? "Initializing" : "Initialized";
624
+ this.logger[level](chalk.bold(`${verb} ${component}`));
625
+ }
626
+ /**
627
+ * Logs progress as "[current/total]" with optional item label.
628
+ *
629
+ * @param current - Current progress count
630
+ * @param total - Total count
631
+ * @param item - Optional item label to append
632
+ */
633
+ progress(current, total, item, level = "info") {
634
+ const base = `[${current}/${total}]`;
635
+ const suffix = item ? ` ${item}` : "";
636
+ this.logger[level](`${chalk.cyan(base)}${suffix}`);
637
+ }
638
+ /**
639
+ * Logs content in a decorative box.
640
+ *
641
+ * @param title - Title to display in the box
642
+ * @param content - Lines of content to display in the box
643
+ */
644
+ box(title, content, level = "info") {
645
+ const width = Math.max(title.length, ...content.map((line) => line.length)) + 2;
646
+ const horizontal = "─".repeat(width);
647
+ this.logger[level](`╭${horizontal}╮`);
648
+ this.logger[level](`│ ${title.padEnd(width - 1, " ")}│`);
649
+ for (const line of content) this.logger[level](`│ ${line.padEnd(width - 1, " ")}│`);
650
+ this.logger[level](`╰${horizontal}╯`);
651
+ }
411
652
  };
412
- var SeedcordTypeError = class extends TypeError {
413
- static {
414
- __name(this, "SeedcordTypeError");
415
- }
416
- code;
417
- identifier;
418
- constructor(code, args, options) {
419
- const message = resolveMessage(code, args);
420
- super(message, options);
421
- this.code = code;
422
- this.identifier = resolveIdentifier(code);
423
- this.name = formatErrorName(new.target.name, this.identifier, this.code);
424
- Object.setPrototypeOf(this, new.target.prototype);
425
- if (typeof Error.captureStackTrace === "function") {
426
- Error.captureStackTrace(this, new.target);
427
- }
428
- }
653
+
654
+ //#endregion
655
+ //#region src/Logger/Logger.ts
656
+ /**
657
+ * Public logging service with channel-aware transports and per-run file output.
658
+ *
659
+ * - Channel separation (e.g., bot, cli, hmr)
660
+ * - Production-safe JSON logs with ANSI stripping
661
+ * - Unique log files per run via filename templates
662
+ */
663
+ var Logger = class Logger {
664
+ label;
665
+ channel;
666
+ registry = LoggerChannelRegistry.instance;
667
+ utils;
668
+ static instances = /* @__PURE__ */ new Map();
669
+ static instance(prefix, channel) {
670
+ const key = channel ? `${channel}::${prefix}` : prefix;
671
+ let instance = this.instances.get(key);
672
+ if (!instance) {
673
+ instance = new Logger(prefix, channel ? { channel } : void 0);
674
+ this.instances.set(key, instance);
675
+ }
676
+ return instance;
677
+ }
678
+ /**
679
+ * Configures global logger settings.
680
+ *
681
+ * Applies configuration to all channels and clears instance cache.
682
+ *
683
+ * @param config - Partial configuration to merge with defaults
684
+ */
685
+ static configure(config) {
686
+ LoggerChannelRegistry.instance.configure(config);
687
+ this.instances.clear();
688
+ }
689
+ /**
690
+ * Creates a new Logger instance.
691
+ *
692
+ * @param label - Prefix/label for all log entries from this logger
693
+ * @param options - Optional configuration for channel, format, and ANSI handling
694
+ */
695
+ constructor(label, options) {
696
+ this.label = label;
697
+ this.channel = options?.channel ?? this.registry.getDefaultChannel();
698
+ this.logger = this.registry.get(this.channel).child({
699
+ label: this.label,
700
+ channel: this.channel
701
+ });
702
+ this.utils = new LoggerUtilities(this);
703
+ }
704
+ /**
705
+ * Switches this logger to a different channel.
706
+ *
707
+ * @param channel - Channel name to switch to
708
+ */
709
+ setChannel(channel) {
710
+ this.channel = channel;
711
+ this.logger = this.registry.get(channel).child({
712
+ label: this.label,
713
+ channel
714
+ });
715
+ }
716
+ /**
717
+ * Returns a new Logger instance configured for the specified channel. Loggers are cached per (label, channel) pair.
718
+ *
719
+ * @param channel - Channel name to use
720
+ */
721
+ inChannel(channel) {
722
+ return Logger.instance(this.label, channel);
723
+ }
724
+ /**
725
+ * Logs an error message with optional additional data.
726
+ *
727
+ * @param msg - The error message to log
728
+ * @param args - Additional data to include in the log entry
729
+ */
730
+ error(msg, ...args) {
731
+ this.logger.error(msg, ...args);
732
+ }
733
+ /**
734
+ * Logs a warning message with optional additional data.
735
+ *
736
+ * @param msg - The warning message to log
737
+ * @param args - Additional data to include in the log entry
738
+ */
739
+ warn(msg, ...args) {
740
+ this.logger.warn(msg, ...args);
741
+ }
742
+ /**
743
+ * Logs an informational message with optional additional data.
744
+ *
745
+ * @param msg - The informational message to log
746
+ * @param args - Additional data to include in the log entry
747
+ */
748
+ info(msg, ...args) {
749
+ this.logger.info(msg, ...args);
750
+ }
751
+ /**
752
+ * Logs an HTTP-related message with optional additional data.
753
+ *
754
+ * @param msg - The HTTP message to log
755
+ * @param args - Additional data to include in the log entry
756
+ */
757
+ http(msg, ...args) {
758
+ this.logger.http(msg, ...args);
759
+ }
760
+ /**
761
+ * Logs a verbose message with optional additional data.
762
+ *
763
+ * @param msg - The verbose message to log
764
+ * @param args - Additional data to include in the log entry
765
+ */
766
+ verbose(msg, ...args) {
767
+ this.logger.verbose(msg, ...args);
768
+ }
769
+ /**
770
+ * Logs a debug message with optional additional data.
771
+ *
772
+ * @param msg - The debug message to log
773
+ * @param args - Additional data to include in the log entry
774
+ */
775
+ debug(msg, ...args) {
776
+ this.logger.debug(msg, ...args);
777
+ }
778
+ /**
779
+ * Logs a silly/trace level message with optional additional data.
780
+ *
781
+ * @param msg - The silly message to log
782
+ * @param args - Additional data to include in the log entry
783
+ */
784
+ silly(msg, ...args) {
785
+ this.logger.silly(msg, ...args);
786
+ }
429
787
  };
430
- var SeedcordRangeError = class extends RangeError {
431
- static {
432
- __name(this, "SeedcordRangeError");
433
- }
434
- code;
435
- identifier;
436
- constructor(code, args, options) {
437
- const message = resolveMessage(code, args);
438
- super(message, options);
439
- this.code = code;
440
- this.identifier = resolveIdentifier(code);
441
- this.name = formatErrorName(new.target.name, this.identifier, this.code);
442
- Object.setPrototypeOf(this, new.target.prototype);
443
- if (typeof Error.captureStackTrace === "function") {
444
- Error.captureStackTrace(this, new.target);
445
- }
446
- }
788
+
789
+ //#endregion
790
+ //#region src/CooldownManager.ts
791
+ const logger = new Logger("CooldownManager");
792
+ /**
793
+ * Lightweight utility for per-key cooldowns.
794
+ *
795
+ * Manages time-based restrictions on operations by key,
796
+ * useful for rate limiting, command cooldowns, and spam prevention.
797
+ */
798
+ var CooldownManager = class {
799
+ window;
800
+ Err;
801
+ msg;
802
+ map = /* @__PURE__ */ new Map();
803
+ /**
804
+ * Creates a new CooldownManager instance.
805
+ *
806
+ * @param opts - Configuration options for the cooldown behavior
807
+ */
808
+ constructor(opts = {}) {
809
+ this.window = opts.cooldown ?? 1e3;
810
+ this.Err = opts.err ?? Error;
811
+ this.msg = opts.message ?? "Cooldown active";
812
+ }
813
+ /**
814
+ * Records usage timestamp for a key without any cooldown checks.
815
+ *
816
+ * @param key - The unique identifier for the cooldown entry
817
+ */
818
+ set(key) {
819
+ this.map.set(key, Date.now());
820
+ }
821
+ /**
822
+ * Verifies cooldown status for a key and updates timestamp if not active.
823
+ *
824
+ * If the cooldown is still active, throws the configured error.
825
+ * If not active, updates the timestamp and returns successfully.
826
+ *
827
+ * @param key - The unique identifier to check cooldown for
828
+ * @throws An {@link Err} When the cooldown is still active for the given key
829
+ */
830
+ check(key) {
831
+ const now = Date.now();
832
+ const last = this.map.get(key);
833
+ const remaining = this.window - (now - (last ?? 0));
834
+ if (Envapter.isDevelopment && remaining > 0) logger.debug(`${key} - ${remaining}ms remaining`);
835
+ if (last !== void 0 && remaining > 0) throw new this.Err(this.msg, remaining);
836
+ this.map.set(key, now);
837
+ }
838
+ /**
839
+ * Checks if a key is currently cooling down without updating timestamp.
840
+ *
841
+ * @param key - The unique identifier to check
842
+ * @returns True if the key is still cooling down, false otherwise
843
+ */
844
+ isActive(key) {
845
+ const last = this.map.get(key);
846
+ return last !== void 0 && Date.now() - last < this.window;
847
+ }
848
+ /**
849
+ * Removes a key from the cooldown map.
850
+ *
851
+ * @param key - The unique identifier to remove (useful for manual resets)
852
+ */
853
+ clear(key) {
854
+ this.map.delete(key);
855
+ }
447
856
  };
448
- var SeedcordErrors = {
449
- Error: SeedcordError,
450
- TypeError: SeedcordTypeError,
451
- RangeError: SeedcordRangeError
857
+
858
+ //#endregion
859
+ //#region src/StrictEventEmitter.ts
860
+ /**
861
+ * Typed wrapper around Node.js {@link EventEmitter} enforcing tuple payloads per event name.
862
+ *
863
+ * @typeParam TEvents - Map of event names to readonly tuple payloads
864
+ */
865
+ var StrictEventEmitter = class extends EventEmitter {
866
+ /**
867
+ * Registers a persistent listener with tuple-safe arguments for the given event.
868
+ *
869
+ * @param event - The event name to attach to
870
+ * @param listener - Callback operating on the typed argument tuple for the event
871
+ * @returns This emitter instance for chaining
872
+ */
873
+ on(event, listener) {
874
+ return super.on(event, listener);
875
+ }
876
+ /**
877
+ * Registers a one time listener that is removed after the first invocation.
878
+ *
879
+ * @param event - The event name to attach to
880
+ * @param listener - Callback operating on the typed argument tuple for the event
881
+ * @returns This emitter instance for chaining
882
+ */
883
+ once(event, listener) {
884
+ return super.once(event, listener);
885
+ }
886
+ /**
887
+ * Removes a previously registered listener for the given event.
888
+ *
889
+ * @param event - The event name whose listener should be removed
890
+ * @param listener - Callback originally registered for the event
891
+ * @returns This emitter instance for chaining
892
+ */
893
+ off(event, listener) {
894
+ return super.off(event, listener);
895
+ }
896
+ /**
897
+ * Alias of {@link StrictEventEmitter.on} for compatibility with Node.js EventEmitter APIs.
898
+ *
899
+ * @param event - The event name to attach to
900
+ * @param listener - Callback operating on the typed argument tuple for the event
901
+ * @returns This emitter instance for chaining
902
+ */
903
+ addListener(event, listener) {
904
+ return this.on(event, listener);
905
+ }
906
+ /**
907
+ * Alias of {@link StrictEventEmitter.off} for compatibility with Node.js EventEmitter APIs.
908
+ *
909
+ * @param event - The event name whose listener should be removed
910
+ * @param listener - Callback originally registered for the event
911
+ * @returns This emitter instance for chaining
912
+ */
913
+ removeListener(event, listener) {
914
+ return super.removeListener(event, listener);
915
+ }
916
+ /**
917
+ * Emits an event with the strictly typed argument tuple for the event name.
918
+ *
919
+ * @param event - The event name to emit
920
+ * @param args - Tuple payload for the event
921
+ * @returns True when the event had listeners, false otherwise
922
+ */
923
+ emit(event, ...args) {
924
+ return super.emit(event, ...args);
925
+ }
926
+ /**
927
+ * Retrieves the listener list for a given event with the correct tuple signature.
928
+ *
929
+ * @param event - The event name to inspect
930
+ * @returns Array of listeners registered for the event
931
+ */
932
+ listeners(event) {
933
+ return super.listeners(event);
934
+ }
935
+ /**
936
+ * Counts listeners for an event without widening the return type of {@link EventEmitter.listenerCount}.
937
+ *
938
+ * @param event - The event name to inspect
939
+ * @returns The total number of listeners registered for the event
940
+ */
941
+ listenerCountTyped(event) {
942
+ return super.listenerCount(event);
943
+ }
944
+ /**
945
+ * Returns the list of event names known to the emitter with the mapped key type.
946
+ *
947
+ * @returns Array of event keys supported by the emitter
948
+ */
949
+ eventNamesTyped() {
950
+ return super.eventNames();
951
+ }
952
+ /**
953
+ * Waits for an event to be emitted, resolving with the listener arguments tuple once triggered.
954
+ * Supports optional abort signals and timeouts for cancellation semantics.
955
+ *
956
+ * @param event - The event name to wait for
957
+ * @param opts - Optional abort signal or timeout in milliseconds
958
+ * @returns Promise resolving with the emitted argument tuple; rejects when aborted or timed out
959
+ */
960
+ waitFor(event, opts) {
961
+ return new Promise((resolve, reject) => {
962
+ const onEvent = (...args) => {
963
+ cleanup();
964
+ resolve(args);
965
+ };
966
+ const onAbort = () => {
967
+ cleanup();
968
+ reject(new SeedcordError(1501));
969
+ };
970
+ let timeoutId = null;
971
+ const cleanup = () => {
972
+ this.off(event, onEvent);
973
+ opts?.signal?.removeEventListener("abort", onAbort);
974
+ if (timeoutId) clearTimeout(timeoutId);
975
+ };
976
+ this.once(event, onEvent);
977
+ if (opts?.signal) {
978
+ if (opts.signal.aborted) return onAbort();
979
+ opts.signal.addEventListener("abort", onAbort, { once: true });
980
+ }
981
+ if (opts?.timeoutMs !== void 0) {
982
+ const timeoutMs = opts.timeoutMs;
983
+ timeoutId = setTimeout(() => {
984
+ cleanup();
985
+ reject(new SeedcordError(1502, [timeoutMs]));
986
+ }, timeoutMs);
987
+ }
988
+ });
989
+ }
452
990
  };
453
- function isSeedcordError(error) {
454
- return typeof error === "object" && error !== null && "code" in error && typeof error.code === "number" && "identifier" in error && typeof error.identifier === "string";
455
- }
456
- __name(isSeedcordError, "isSeedcordError");
457
- var CoordinatedLifecycle = class {
458
- static {
459
- __name(this, "CoordinatedLifecycle");
460
- }
461
- phaseOrder;
462
- phaseEnum;
463
- logger;
464
- events = new EventEmitter();
465
- tasksMap = /* @__PURE__ */ new Map();
466
- constructor(loggerName, phaseOrder, phaseEnum) {
467
- this.phaseOrder = phaseOrder;
468
- this.phaseEnum = phaseEnum;
469
- this.logger = new Logger(loggerName);
470
- this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));
471
- }
472
- /**
473
- * Adds a lifecycle task to a specific phase.
474
- *
475
- * Tasks are executed in phase order during lifecycle operations.
476
- * Each task has a timeout to prevent hanging operations.
477
- *
478
- * @param phase - The lifecycle phase to add the task to
479
- * @param taskName - Unique name for the task (used for logging and removal)
480
- * @param task - Async function to execute during the phase
481
- * @param timeoutMs - Maximum time allowed for task execution in milliseconds
482
- * @example
483
- * ```typescript
484
- * lifecycle.addTask(StartupPhase.Services, 'start-database', async () => {
485
- * await database.connect();
486
- * }, 10000);
487
- * ```
488
- */
489
- addTask(phase, taskName, task, timeoutMs) {
490
- if (!this.canAddTask()) return;
491
- const tasks = this.tasksMap.get(phase);
492
- if (!tasks) {
493
- throw new SeedcordError(SeedcordErrorCode.LifecycleUnknownPhase, [
494
- phase
495
- ]);
496
- }
497
- tasks.push({
498
- name: taskName,
499
- task,
500
- timeout: timeoutMs
501
- });
502
- this.logger.debug(`${chalk2.italic("Added")} ${this.getTaskType()} task ${chalk2.bold.cyan(taskName)} to phase ${chalk2.bold.magenta(this.phaseEnum[phase])}`);
503
- }
504
- /**
505
- * Removes a lifecycle task from a specific phase.
506
- *
507
- * @param phase - The lifecycle phase to remove the task from
508
- * @param taskName - Name of the task to remove
509
- * @returns True if the task was found and removed, false otherwise
510
- */
511
- removeTask(phase, taskName) {
512
- if (!this.canRemoveTask()) return false;
513
- const tasks = this.tasksMap.get(phase);
514
- if (!tasks) return false;
515
- const initialLength = tasks.length;
516
- const filteredTasks = tasks.filter((task) => task.name !== taskName);
517
- this.tasksMap.set(phase, filteredTasks);
518
- const removed = initialLength !== filteredTasks.length;
519
- if (removed) {
520
- this.logger.debug(`${chalk2.italic("Removed")} ${this.getTaskType()} task ${chalk2.bold.cyan(taskName)} from phase ${chalk2.bold.magenta(this.phaseEnum[phase])}`);
521
- }
522
- return removed;
523
- }
524
- /**
525
- * Run all tasks in a specific phase
526
- */
527
- async runPhase(phase) {
528
- const tasks = this.tasksMap.get(phase) ?? [];
529
- if (tasks.length === 0) {
530
- this.logger.warn(`No tasks to run in phase ${chalk2.bold.magenta(this.phaseEnum[phase])}`);
531
- return;
532
- }
533
- this.logger.info(`${chalk2.bold.yellow("Running")} ${this.getTaskType()} phase ${chalk2.bold.magenta(this.phaseEnum[phase])} with ${chalk2.bold.cyan(tasks.length)} tasks`);
534
- this.emit(`phase:${phase}:start`);
535
- const results = await this.executeTasksInPhase(phase, tasks);
536
- const failures = results.filter((r) => r.status === "rejected").length;
537
- if (failures > 0) {
538
- throw new SeedcordError(SeedcordErrorCode.LifecyclePhaseFailures, [
539
- chalk2.bold.magenta(this.phaseEnum[phase]),
540
- failures
541
- ]);
542
- } else {
543
- this.logger.info(`Phase ${chalk2.bold.magenta(this.phaseEnum[phase])} ${chalk2.bold.green("completed successfully")}`);
544
- }
545
- this.emit(`phase:${phase}:complete`);
546
- }
547
- /**
548
- * Run a single task with timeout
549
- */
550
- async runTaskWithTimeout(phase, task) {
551
- this.logger.info(`${chalk2.italic("Starting")} task ${chalk2.bold.cyan(task.name)} in phase ${chalk2.bold.magenta(this.phaseEnum[phase])}`);
552
- try {
553
- await Promise.race([
554
- task.task(),
555
- new Promise((_, reject) => {
556
- setTimeout(() => {
557
- reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [
558
- task.name,
559
- task.timeout
560
- ]));
561
- }, task.timeout);
562
- })
563
- ]);
564
- this.logger.info(`${chalk2.italic("Completed")} task ${chalk2.bold.cyan(task.name)} in phase ${chalk2.bold.magenta(this.phaseEnum[phase])}`);
565
- } catch (error) {
566
- this.logger.error(`${chalk2.italic("Failed")} task ${chalk2.bold.cyan(task.name)} in phase ${chalk2.bold.magenta(this.phaseEnum[phase])}:`, error);
567
- throw error;
568
- }
569
- }
570
- /**
571
- * Subscribe to lifecycle events
572
- */
573
- on(event, listener) {
574
- this.events.on(event, listener);
575
- }
576
- /**
577
- * Unsubscribe from lifecycle events
578
- */
579
- off(event, listener) {
580
- this.events.off(event, listener);
581
- }
582
- emit(event, ...args) {
583
- return this.events.emit(event, ...args);
584
- }
991
+
992
+ //#endregion
993
+ //#region src/Lifecycle/CoordinatedLifecycle.ts
994
+ /**
995
+ * Abstract base class for coordinated lifecycle management (startup/shutdown)
996
+ */
997
+ var CoordinatedLifecycle = class extends StrictEventEmitter {
998
+ phaseOrder;
999
+ phaseEnum;
1000
+ logger;
1001
+ tasksMap = /* @__PURE__ */ new Map();
1002
+ constructor(loggerName, phaseOrder, phaseEnum) {
1003
+ super();
1004
+ this.phaseOrder = phaseOrder;
1005
+ this.phaseEnum = phaseEnum;
1006
+ this.logger = new Logger(loggerName);
1007
+ this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));
1008
+ }
1009
+ /**
1010
+ * Adds a lifecycle task to a specific phase.
1011
+ *
1012
+ * Tasks are executed in phase order during lifecycle operations.
1013
+ * Each task has a timeout to prevent hanging operations.
1014
+ *
1015
+ * @param phase - The lifecycle phase to add the task to
1016
+ * @param taskName - Unique name for the task (used for logging and removal)
1017
+ * @param task - Async function to execute during the phase
1018
+ * @param timeoutMs - Maximum time allowed for task execution in milliseconds
1019
+ * @example
1020
+ * ```typescript
1021
+ * lifecycle.addTask(StartupPhase.Services, 'start-database', async () => {
1022
+ * await database.connect();
1023
+ * }, 10000);
1024
+ * ```
1025
+ */
1026
+ addTask(phase, taskName, task, timeoutMs) {
1027
+ if (!this.canAddTask()) return;
1028
+ const tasks = this.tasksMap.get(phase);
1029
+ if (!tasks) throw new SeedcordError(1104, [phase]);
1030
+ tasks.push({
1031
+ name: taskName,
1032
+ task,
1033
+ timeout: timeoutMs
1034
+ });
1035
+ this.logger.debug(`${chalk.italic("Added")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} to phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
1036
+ }
1037
+ /**
1038
+ * Removes a lifecycle task from a specific phase.
1039
+ *
1040
+ * @param phase - The lifecycle phase to remove the task from
1041
+ * @param taskName - Name of the task to remove
1042
+ * @returns True if the task was found and removed, false otherwise
1043
+ */
1044
+ removeTask(phase, taskName) {
1045
+ if (!this.canRemoveTask()) return false;
1046
+ const tasks = this.tasksMap.get(phase);
1047
+ if (!tasks) return false;
1048
+ const initialLength = tasks.length;
1049
+ const filteredTasks = tasks.filter((task) => task.name !== taskName);
1050
+ this.tasksMap.set(phase, filteredTasks);
1051
+ const removed = initialLength !== filteredTasks.length;
1052
+ if (removed) this.logger.debug(`${chalk.italic("Removed")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} from phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
1053
+ return removed;
1054
+ }
1055
+ /**
1056
+ * Run all tasks in a specific phase
1057
+ */
1058
+ async runPhase(phase) {
1059
+ const tasks = this.tasksMap.get(phase) ?? [];
1060
+ if (tasks.length === 0) {
1061
+ this.logger.warn(`No tasks to run in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
1062
+ return;
1063
+ }
1064
+ this.logger.info(`${chalk.bold.yellow("Running")} ${this.getTaskType()} phase ${chalk.bold.magenta(this.phaseEnum[phase])} with ${chalk.bold.cyan(tasks.length)} tasks`);
1065
+ this.emitPhase(phase, "start");
1066
+ const failures = (await this.executeTasksInPhase(phase, tasks)).filter((r) => r.status === "rejected").length;
1067
+ if (failures > 0) throw new SeedcordError(1105, [this.phaseEnum[phase], failures]);
1068
+ else this.logger.info(`Phase ${chalk.bold.magenta(this.phaseEnum[phase])} ${chalk.bold.green("completed successfully")}`);
1069
+ this.emitPhase(phase, "complete");
1070
+ }
1071
+ /**
1072
+ * Run a single task with timeout
1073
+ */
1074
+ async runTaskWithTimeout(phase, task) {
1075
+ this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
1076
+ let timeoutId;
1077
+ try {
1078
+ await Promise.race([task.task(), new Promise((_, reject) => {
1079
+ timeoutId = setTimeout(() => {
1080
+ reject(new SeedcordError(1106, [task.name, task.timeout]));
1081
+ }, task.timeout);
1082
+ })]);
1083
+ this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
1084
+ } catch (error) {
1085
+ this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}:`, error);
1086
+ throw error;
1087
+ } finally {
1088
+ if (timeoutId) clearTimeout(timeoutId);
1089
+ }
1090
+ }
1091
+ emitPhase(phase, action) {
1092
+ EventEmitter.prototype.emit.call(this, `phase:${phase}:${action}`);
1093
+ }
585
1094
  };
586
1095
 
587
- // src/Lifecycle/CoordinatedShutdown.ts
588
- function _ts_decorate(decorators, target, key, desc) {
589
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
590
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
591
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
592
- return c > 3 && r && Object.defineProperty(target, key, r), r;
593
- }
594
- __name(_ts_decorate, "_ts_decorate");
595
- function _ts_metadata(k, v) {
596
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
597
- }
598
- __name(_ts_metadata, "_ts_metadata");
599
- var ShutdownPhase = /* @__PURE__ */ (function(ShutdownPhase2) {
600
- ShutdownPhase2[ShutdownPhase2["StopAcceptingRequests"] = 1] = "StopAcceptingRequests";
601
- ShutdownPhase2[ShutdownPhase2["StopServices"] = 2] = "StopServices";
602
- ShutdownPhase2[ShutdownPhase2["ExternalResources"] = 3] = "ExternalResources";
603
- ShutdownPhase2[ShutdownPhase2["DiscordCleanup"] = 4] = "DiscordCleanup";
604
- ShutdownPhase2[ShutdownPhase2["FinalCleanup"] = 5] = "FinalCleanup";
605
- return ShutdownPhase2;
606
- })({});
607
- var PHASE_ORDER = [
608
- 1,
609
- 2,
610
- 3,
611
- 4,
612
- 5
1096
+ //#endregion
1097
+ //#region src/Lifecycle/CoordinatedShutdown.ts
1098
+ /**
1099
+ * Shutdown phases for coordinated application shutdown.
1100
+ */
1101
+ let ShutdownPhase = /* @__PURE__ */ function(ShutdownPhase) {
1102
+ /** Stop accepting new requests/interactions */
1103
+ ShutdownPhase[ShutdownPhase["StopAcceptingRequests"] = 1] = "StopAcceptingRequests";
1104
+ /** Stop background services (health checks, etc.) */
1105
+ ShutdownPhase[ShutdownPhase["StopServices"] = 2] = "StopServices";
1106
+ /** Disconnect from external resources (database, APIs) */
1107
+ ShutdownPhase[ShutdownPhase["ExternalResources"] = 3] = "ExternalResources";
1108
+ /** Disconnect from Discord */
1109
+ ShutdownPhase[ShutdownPhase["DiscordCleanup"] = 4] = "DiscordCleanup";
1110
+ /** Final cleanup tasks */
1111
+ ShutdownPhase[ShutdownPhase["FinalCleanup"] = 5] = "FinalCleanup";
1112
+ return ShutdownPhase;
1113
+ }({});
1114
+ /** Define the order of phases */
1115
+ const PHASE_ORDER$1 = [
1116
+ 1,
1117
+ 2,
1118
+ 3,
1119
+ 4,
1120
+ 5
613
1121
  ];
614
- var LOG_FLUSH_DELAY_MS = 500;
1122
+ const LOG_FLUSH_DELAY_MS = 500;
1123
+ /**
1124
+ * CoordinatedShutdown manages graceful application shutdown by executing registered tasks across defined phases.
1125
+ *
1126
+ * It listens for termination signals (SIGINT, SIGTERM) and runs tasks in parallel within each phase.
1127
+ * Tasks can be added or removed dynamically, and each task has an associated timeout.
1128
+ */
615
1129
  var CoordinatedShutdown = class extends CoordinatedLifecycle {
616
- static {
617
- __name(this, "CoordinatedShutdown");
618
- }
619
- isShuttingDown = false;
620
- exitCode = 0;
621
- constructor() {
622
- super("CoordinatedShutdown", PHASE_ORDER, ShutdownPhase);
623
- this.registerSignalHandlers();
624
- }
625
- canAddTask() {
626
- return this.isShutdownEnabled;
627
- }
628
- canRemoveTask() {
629
- return true;
630
- }
631
- getTaskType() {
632
- return "shutdown";
633
- }
634
- async executeTasksInPhase(phase, tasks) {
635
- const results = [];
636
- for (const task of tasks) {
637
- results.push(await Promise.resolve().then(() => this.runTaskWithTimeout(phase, task)).then(
638
- () => ({
639
- status: "fulfilled",
640
- value: void 0
641
- }),
642
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
643
- (reason) => ({
644
- status: "rejected",
645
- reason
646
- })
647
- ));
648
- }
649
- return results;
650
- }
651
- registerSignalHandlers() {
652
- if (!this.isShutdownEnabled) return;
653
- process.on("SIGTERM", () => {
654
- this.logger.info(`Received ${chalk2.yellow.bold("SIGTERM")} signal`);
655
- void this.run(0);
656
- });
657
- process.on("SIGINT", () => {
658
- this.logger.info(`Received ${chalk2.yellow.bold("SIGINT")} signal`);
659
- void this.run(0);
660
- });
661
- }
662
- /**
663
- * Adds a task to a specific shutdown phase with timeout.
664
- *
665
- * @param phase - The shutdown phase from {@link ShutdownPhase}
666
- * @param taskName - Unique identifier for the task
667
- * @param task - Async function to execute
668
- * @param timeoutMs - Task timeout in milliseconds (default: 5000)
669
- */
670
- addTask(phase, taskName, task, timeoutMs = 5e3) {
671
- super.addTask(phase, taskName, task, timeoutMs);
672
- }
673
- /**
674
- * Removes a task from a specific shutdown phase.
675
- *
676
- * @param phase - The shutdown phase to remove from
677
- * @param taskName - Name of the task to remove
678
- * @returns True if task was found and removed
679
- */
680
- removeTask(phase, taskName) {
681
- return super.removeTask(phase, taskName);
682
- }
683
- /**
684
- * Executes the coordinated shutdown sequence.
685
- *
686
- * Runs all registered tasks across shutdown phases in reverse order.
687
- * Tasks within each phase are executed in parallel for faster shutdown.
688
- * Process exits with the specified code when complete.
689
- *
690
- * @param exitCode - Process exit code (default: `0`)
691
- * @returns Promise that resolves when shutdown is complete
692
- * @example
693
- * ```typescript
694
- * shutdown.addTask(ShutdownPhase.Services, 'database', () => db.disconnect(), 5000);
695
- * await shutdown.run(0); // Graceful shutdown
696
- * ```
697
- */
698
- async run(exitCode = 0) {
699
- if (this.isShuttingDown) {
700
- this.logger.warn("Shutdown sequence already in progress");
701
- return;
702
- }
703
- this.isShuttingDown = true;
704
- this.exitCode = exitCode;
705
- this.logger.info(`${chalk2.bold.yellow("Starting")} coordinated shutdown with exit code ${chalk2.bold.cyan(exitCode)}`);
706
- this.emit("shutdown:start");
707
- try {
708
- for (const phase of PHASE_ORDER) {
709
- await this.runPhase(phase);
710
- }
711
- this.logger.info(`${chalk2.bold.green("Coordinated shutdown completed")} successfully`);
712
- this.emit("shutdown:complete");
713
- } catch (error) {
714
- this.logger.error(`${chalk2.bold.red("Coordinated shutdown failed")}`);
715
- this.emit("shutdown:error", error);
716
- } finally {
717
- this.logger.info(`${chalk2.bold.red("Exiting")} process with code ${chalk2.bold.cyan(this.exitCode)}`);
718
- setTimeout(() => {
719
- process.exit(this.exitCode);
720
- }, LOG_FLUSH_DELAY_MS);
721
- }
722
- }
723
- /**
724
- * Subscribe to shutdown events
725
- */
726
- on(event, listener) {
727
- super.on(event, listener);
728
- }
729
- /**
730
- * Unsubscribe from shutdown events
731
- */
732
- off(event, listener) {
733
- super.off(event, listener);
734
- }
1130
+ isShutdownEnabled;
1131
+ isShuttingDown = false;
1132
+ exitCode = 0;
1133
+ onSigTerm = null;
1134
+ onSigInt = null;
1135
+ constructor(enabled = true) {
1136
+ super("CoordinatedShutdown", PHASE_ORDER$1, ShutdownPhase);
1137
+ this.isShutdownEnabled = enabled;
1138
+ this.registerSignalHandlers();
1139
+ }
1140
+ canAddTask() {
1141
+ return this.isShutdownEnabled;
1142
+ }
1143
+ canRemoveTask() {
1144
+ return true;
1145
+ }
1146
+ getTaskType() {
1147
+ return "shutdown";
1148
+ }
1149
+ async executeTasksInPhase(phase, tasks) {
1150
+ const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
1151
+ return Promise.allSettled(promises);
1152
+ }
1153
+ registerSignalHandlers() {
1154
+ if (!this.isShutdownEnabled) return;
1155
+ this.onSigTerm = () => {
1156
+ this.logger.info(`Received ${chalk.yellow.bold("SIGTERM")} signal`);
1157
+ this.run(0);
1158
+ };
1159
+ this.onSigInt = () => {
1160
+ this.logger.info(`Received ${chalk.yellow.bold("SIGINT")} signal`);
1161
+ this.run(0);
1162
+ };
1163
+ process.on("SIGTERM", this.onSigTerm);
1164
+ process.on("SIGINT", this.onSigInt);
1165
+ }
1166
+ removeSignalHandlers() {
1167
+ if (this.onSigTerm) {
1168
+ process.off("SIGTERM", this.onSigTerm);
1169
+ this.onSigTerm = null;
1170
+ }
1171
+ if (this.onSigInt) {
1172
+ process.off("SIGINT", this.onSigInt);
1173
+ this.onSigInt = null;
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Adds a task to a specific shutdown phase with timeout.
1178
+ *
1179
+ * @param phase - The shutdown phase from {@link ShutdownPhase}
1180
+ * @param taskName - Unique identifier for the task
1181
+ * @param task - Async function to execute
1182
+ * @param timeoutMs - Task timeout in milliseconds {@default 5000}
1183
+ */
1184
+ addTask(phase, taskName, task, timeoutMs = 5e3) {
1185
+ super.addTask(phase, taskName, task, timeoutMs);
1186
+ }
1187
+ /**
1188
+ * Removes a task from a specific shutdown phase.
1189
+ *
1190
+ * @param phase - The shutdown phase to remove from
1191
+ * @param taskName - Name of the task to remove
1192
+ * @returns True if task was found and removed
1193
+ */
1194
+ removeTask(phase, taskName) {
1195
+ return super.removeTask(phase, taskName);
1196
+ }
1197
+ /**
1198
+ * Executes the coordinated shutdown sequence.
1199
+ *
1200
+ * Runs all registered tasks across shutdown phases in reverse order.
1201
+ * Tasks within each phase are executed in parallel for faster shutdown.
1202
+ * Process exits with the specified code when complete.
1203
+ *
1204
+ * @param exitCode - Process exit code {@default 0}
1205
+ * @param exitProcess - Whether to exit the process after shutdown {@default true}
1206
+ * @returns Promise that resolves when shutdown is complete
1207
+ * @example
1208
+ * ```typescript
1209
+ * shutdown.addTask(ShutdownPhase.Services, 'database', () => db.disconnect(), 5000);
1210
+ * await shutdown.run(0); // Graceful shutdown
1211
+ * ```
1212
+ */
1213
+ async run(exitCode = 0, exitProcess = true) {
1214
+ this.removeSignalHandlers();
1215
+ if (this.isShuttingDown) {
1216
+ this.logger.warn("Shutdown sequence already in progress");
1217
+ return;
1218
+ }
1219
+ this.isShuttingDown = true;
1220
+ this.exitCode = exitCode;
1221
+ this.logger.info(`${chalk.bold.yellow("Starting")} coordinated shutdown with exit code ${chalk.bold.cyan(exitCode)}`);
1222
+ this.emit("shutdown:start");
1223
+ try {
1224
+ for (const phase of PHASE_ORDER$1) await this.runPhase(phase);
1225
+ this.logger.info(`${chalk.bold.green("Coordinated shutdown completed")} successfully`);
1226
+ this.emit("shutdown:complete");
1227
+ } catch (error) {
1228
+ this.logger.error(`${chalk.bold.red("Coordinated shutdown failed")}`);
1229
+ this.emit("shutdown:error", error);
1230
+ } finally {
1231
+ if (exitProcess) {
1232
+ this.logger.info(`${chalk.bold.red("Exiting")} process with code ${chalk.bold.cyan(this.exitCode)}`);
1233
+ setTimeout(() => {
1234
+ process.exit(this.exitCode);
1235
+ }, LOG_FLUSH_DELAY_MS);
1236
+ } else {
1237
+ this.logger.info(`${chalk.bold.yellow("Skipping")} process exit (dev mode)`);
1238
+ this.isShuttingDown = false;
1239
+ }
1240
+ }
1241
+ }
735
1242
  };
736
- _ts_decorate([
737
- Envapt("SHUTDOWN_IS_ENABLED", {
738
- fallback: false
739
- }),
740
- _ts_metadata("design:type", Boolean)
741
- ], CoordinatedShutdown.prototype, "isShutdownEnabled", void 0);
742
1243
 
743
- // src/HealthCheck.ts
744
- function _ts_decorate2(decorators, target, key, desc) {
745
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
746
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
747
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
748
- return c > 3 && r && Object.defineProperty(target, key, r), r;
749
- }
750
- __name(_ts_decorate2, "_ts_decorate");
751
- function _ts_metadata2(k, v) {
752
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
753
- }
754
- __name(_ts_metadata2, "_ts_metadata");
755
- var HTTP_OK = 200;
756
- var HTTP_NOT_FOUND = 404;
1244
+ //#endregion
1245
+ //#region src/HealthCheck.ts
1246
+ const HTTP_OK = 200;
1247
+ const HTTP_NOT_FOUND = 404;
1248
+ const DEFAULT_HEALTH_CHECK_PORT = 6967;
1249
+ const DEFAULT_HEALTH_CHECK_PATH = "/healthcheck";
1250
+ /**
1251
+ * HTTP health check service for monitoring bot status.
1252
+ *
1253
+ * Provides a simple HTTP endpoint that responds with JSON status
1254
+ * information, useful for container orchestration and monitoring.
1255
+ */
757
1256
  var HealthCheck = class {
758
- static {
759
- __name(this, "HealthCheck");
760
- }
761
- logger = new Logger("HealthCheck");
762
- server;
763
- constructor(shutdown) {
764
- shutdown.addTask(ShutdownPhase.StopServices, "stop-healthcheck-server", async () => await this.stop());
765
- }
766
- /**
767
- * Starts the health check server.
768
- * @returns Promise that resolves when the server is listening
769
- */
770
- async init() {
771
- return new Promise((resolve, reject) => {
772
- this.server = createServer((req, res) => {
773
- if (req.method === "GET" && req.url === this.path) {
774
- res.writeHead(HTTP_OK, {
775
- "Content-Type": "application/json"
776
- });
777
- res.end(JSON.stringify({
778
- status: "ok",
779
- timestamp: Date.now()
780
- }));
781
- } else {
782
- res.writeHead(HTTP_NOT_FOUND, {
783
- "Content-Type": "application/json"
784
- });
785
- res.end(JSON.stringify({
786
- status: "not found"
787
- }));
788
- }
789
- });
790
- this.server.on("error", reject);
791
- this.server.once("listening", () => {
792
- const address = this.host ?? "localhost";
793
- this.logger.info(`${chalk2.green.bold("\u2713")} Health check server listening on ${chalk2.cyan(`http://${address}:${this.port}${this.path}`)}`);
794
- resolve();
795
- });
796
- if (this.host) {
797
- this.logger.debug(`Binding health check server to ${this.host}`);
798
- this.server.listen(this.port, this.host);
799
- } else {
800
- this.logger.debug("Binding health check server to all interfaces");
801
- this.server.listen(this.port);
802
- }
803
- });
804
- }
805
- /**
806
- * Stops the health check server.
807
- *
808
- * @returns Promise that resolves when the server is closed
809
- */
810
- stop() {
811
- if (this.server !== void 0) {
812
- const server = this.server;
813
- return new Promise((resolve) => {
814
- server.once("close", () => resolve());
815
- server.close(() => {
816
- this.logger.info(chalk2.bold.red("Health check server stopped"));
817
- });
818
- });
819
- }
820
- return Promise.resolve();
821
- }
822
- };
823
- _ts_decorate2([
824
- Envapt("HEALTH_CHECK_PORT", {
825
- fallback: 6956
826
- }),
827
- _ts_metadata2("design:type", Number)
828
- ], HealthCheck.prototype, "port", void 0);
829
- _ts_decorate2([
830
- Envapt("HEALTH_CHECK_PATH", {
831
- fallback: "/healthcheck"
832
- }),
833
- _ts_metadata2("design:type", String)
834
- ], HealthCheck.prototype, "path", void 0);
835
- _ts_decorate2([
836
- Envapt("HEALTH_CHECK_HOST"),
837
- _ts_metadata2("design:type", Object)
838
- ], HealthCheck.prototype, "host", void 0);
839
- var StrictEventEmitter = class extends EventEmitter {
840
- static {
841
- __name(this, "StrictEventEmitter");
842
- }
843
- /**
844
- * Registers a persistent listener with tuple-safe arguments for the given event.
845
- *
846
- * @param event - The event name to attach to
847
- * @param listener - Callback operating on the typed argument tuple for the event
848
- * @returns This emitter instance for chaining
849
- */
850
- on(event, listener) {
851
- return super.on(event, listener);
852
- }
853
- /**
854
- * Registers a one time listener that is removed after the first invocation.
855
- *
856
- * @param event - The event name to attach to
857
- * @param listener - Callback operating on the typed argument tuple for the event
858
- * @returns This emitter instance for chaining
859
- */
860
- once(event, listener) {
861
- return super.once(event, listener);
862
- }
863
- /**
864
- * Removes a previously registered listener for the given event.
865
- *
866
- * @param event - The event name whose listener should be removed
867
- * @param listener - Callback originally registered for the event
868
- * @returns This emitter instance for chaining
869
- */
870
- off(event, listener) {
871
- return super.off(event, listener);
872
- }
873
- /**
874
- * Alias of {@link StrictEventEmitter.on} for compatibility with Node.js EventEmitter APIs.
875
- *
876
- * @param event - The event name to attach to
877
- * @param listener - Callback operating on the typed argument tuple for the event
878
- * @returns This emitter instance for chaining
879
- */
880
- addListener(event, listener) {
881
- return this.on(event, listener);
882
- }
883
- /**
884
- * Alias of {@link StrictEventEmitter.off} for compatibility with Node.js EventEmitter APIs.
885
- *
886
- * @param event - The event name whose listener should be removed
887
- * @param listener - Callback originally registered for the event
888
- * @returns This emitter instance for chaining
889
- */
890
- removeListener(event, listener) {
891
- return super.removeListener(event, listener);
892
- }
893
- /**
894
- * Emits an event with the strictly typed argument tuple for the event name.
895
- *
896
- * @param event - The event name to emit
897
- * @param args - Tuple payload for the event
898
- * @returns True when the event had listeners, false otherwise
899
- */
900
- emit(event, ...args) {
901
- return super.emit(event, ...args);
902
- }
903
- /**
904
- * Retrieves the listener list for a given event with the correct tuple signature.
905
- *
906
- * @param event - The event name to inspect
907
- * @returns Array of listeners registered for the event
908
- */
909
- listeners(event) {
910
- return super.listeners(event);
911
- }
912
- /**
913
- * Counts listeners for an event without widening the return type of {@link EventEmitter.listenerCount}.
914
- *
915
- * @param event - The event name to inspect
916
- * @returns The total number of listeners registered for the event
917
- */
918
- listenerCountTyped(event) {
919
- return super.listenerCount(event);
920
- }
921
- /**
922
- * Returns the list of event names known to the emitter with the mapped key type.
923
- *
924
- * @returns Array of event keys supported by the emitter
925
- */
926
- eventNamesTyped() {
927
- return super.eventNames();
928
- }
929
- /**
930
- * Waits for an event to be emitted, resolving with the listener arguments tuple once triggered.
931
- * Supports optional abort signals and timeouts for cancellation semantics.
932
- *
933
- * @param event - The event name to wait for
934
- * @param opts - Optional abort signal or timeout in milliseconds
935
- * @returns Promise resolving with the emitted argument tuple; rejects when aborted or timed out
936
- */
937
- waitFor(event, opts) {
938
- return new Promise((resolve, reject) => {
939
- const onEvent = /* @__PURE__ */ __name((...args) => {
940
- cleanup();
941
- resolve(args);
942
- }, "onEvent");
943
- const onAbort = /* @__PURE__ */ __name(() => {
944
- cleanup();
945
- reject(Object.assign(new Error("Aborted"), {
946
- name: "AbortError"
947
- }));
948
- }, "onAbort");
949
- let timeoutId = null;
950
- const cleanup = /* @__PURE__ */ __name(() => {
951
- this.off(event, onEvent);
952
- opts?.signal?.removeEventListener("abort", onAbort);
953
- if (timeoutId) clearTimeout(timeoutId);
954
- }, "cleanup");
955
- this.once(event, onEvent);
956
- if (opts?.signal) {
957
- if (opts.signal.aborted) return onAbort();
958
- opts.signal.addEventListener("abort", onAbort, {
959
- once: true
960
- });
961
- }
962
- if (opts?.timeoutMs !== void 0) {
963
- timeoutId = setTimeout(() => {
964
- cleanup();
965
- reject(new Error("Timed out"));
966
- }, opts.timeoutMs);
967
- }
968
- });
969
- }
1257
+ logger = new Logger("HealthCheck");
1258
+ port;
1259
+ path;
1260
+ host;
1261
+ server;
1262
+ constructor(shutdown, options) {
1263
+ this.port = options?.port ?? DEFAULT_HEALTH_CHECK_PORT;
1264
+ this.path = options?.path ?? DEFAULT_HEALTH_CHECK_PATH;
1265
+ this.host = options?.host;
1266
+ shutdown.addTask(2, "stop-healthcheck-server", async () => await this.stop());
1267
+ }
1268
+ /**
1269
+ * Starts the health check server.
1270
+ * @returns Promise that resolves when the server is listening
1271
+ */
1272
+ async init() {
1273
+ return new Promise((resolve, reject) => {
1274
+ const server = createServer((req, res) => {
1275
+ if (req.method === "GET" && req.url === this.path) {
1276
+ res.writeHead(HTTP_OK, { "Content-Type": "application/json" });
1277
+ res.end(JSON.stringify({
1278
+ status: "ok",
1279
+ timestamp: Date.now()
1280
+ }));
1281
+ } else {
1282
+ res.writeHead(HTTP_NOT_FOUND, { "Content-Type": "application/json" });
1283
+ res.end(JSON.stringify({ status: "not found" }));
1284
+ }
1285
+ });
1286
+ this.server = server;
1287
+ const onListenError = (err) => reject(err);
1288
+ server.on("error", onListenError);
1289
+ server.once("listening", () => {
1290
+ server.removeListener("error", onListenError);
1291
+ server.on("error", (err) => this.logger.error("Health check server error", err));
1292
+ const address = this.host ?? "localhost";
1293
+ this.logger.info(`${chalk.green.bold("✓")} Health check server listening on ${chalk.cyan(`http://${address}:${this.port}${this.path}`)}`);
1294
+ resolve();
1295
+ });
1296
+ if (this.host) {
1297
+ this.logger.debug(`Binding health check server to ${this.host}`);
1298
+ server.listen(this.port, this.host);
1299
+ } else {
1300
+ this.logger.debug("Binding health check server to all interfaces");
1301
+ server.listen(this.port);
1302
+ }
1303
+ });
1304
+ }
1305
+ /**
1306
+ * Stops the health check server.
1307
+ *
1308
+ * @returns Promise that resolves when the server is closed
1309
+ */
1310
+ stop() {
1311
+ const server = this.server;
1312
+ if (!server?.listening) return Promise.resolve();
1313
+ return new Promise((resolve, reject) => {
1314
+ server.once("close", () => resolve());
1315
+ server.close((err) => {
1316
+ if (err) {
1317
+ reject(err);
1318
+ return;
1319
+ }
1320
+ this.logger.info(chalk.bold.red("Health check server stopped"));
1321
+ });
1322
+ });
1323
+ }
970
1324
  };
971
- var StartupPhase = /* @__PURE__ */ (function(StartupPhase2) {
972
- StartupPhase2[StartupPhase2["Validation"] = 1] = "Validation";
973
- StartupPhase2[StartupPhase2["Discovery"] = 2] = "Discovery";
974
- StartupPhase2[StartupPhase2["Registration"] = 3] = "Registration";
975
- StartupPhase2[StartupPhase2["Configuration"] = 4] = "Configuration";
976
- StartupPhase2[StartupPhase2["Instantiation"] = 5] = "Instantiation";
977
- StartupPhase2[StartupPhase2["Activation"] = 6] = "Activation";
978
- StartupPhase2[StartupPhase2["Ready"] = 7] = "Ready";
979
- return StartupPhase2;
980
- })({});
981
- var PHASE_ORDER2 = [
982
- 1,
983
- 2,
984
- 3,
985
- 4,
986
- 5,
987
- 6,
988
- 7
1325
+
1326
+ //#endregion
1327
+ //#region src/Lifecycle/CoordinatedStartup.ts
1328
+ /**
1329
+ * Startup phases for coordinated initialization
1330
+ *
1331
+ * Defines the order in which different components are initialized during bot startup.
1332
+ */
1333
+ let StartupPhase = /* @__PURE__ */ function(StartupPhase) {
1334
+ /** Validate environment variables and config files */
1335
+ StartupPhase[StartupPhase["Validation"] = 1] = "Validation";
1336
+ /** Discover plugin constructors via decorators or registry */
1337
+ StartupPhase[StartupPhase["Discovery"] = 2] = "Discovery";
1338
+ /** Register plugin metadata and declared dependencies */
1339
+ StartupPhase[StartupPhase["Registration"] = 3] = "Registration";
1340
+ /** Inject and validate plugin-specific configuration */
1341
+ StartupPhase[StartupPhase["Configuration"] = 4] = "Configuration";
1342
+ /** Instantiate plugin classes with Core and arguments */
1343
+ StartupPhase[StartupPhase["Instantiation"] = 5] = "Instantiation";
1344
+ /** Activate plugins by calling their init/setup methods */
1345
+ StartupPhase[StartupPhase["Activation"] = 6] = "Activation";
1346
+ /** Mark seedcord as ready and start handling interactions */
1347
+ StartupPhase[StartupPhase["Ready"] = 7] = "Ready";
1348
+ return StartupPhase;
1349
+ }({});
1350
+ /** Define the order of phases */
1351
+ const PHASE_ORDER = [
1352
+ 1,
1353
+ 2,
1354
+ 3,
1355
+ 4,
1356
+ 5,
1357
+ 6,
1358
+ 7
989
1359
  ];
1360
+ /**
1361
+ * Manages bot startup lifecycle with ordered phases
1362
+ *
1363
+ * Coordinates initialization of all bot components in a predictable sequence.
1364
+ * Tasks are executed within their designated phases to ensure proper dependency order.
1365
+ */
990
1366
  var CoordinatedStartup = class extends CoordinatedLifecycle {
991
- static {
992
- __name(this, "CoordinatedStartup");
993
- }
994
- isStartingUp = false;
995
- hasStarted = false;
996
- constructor() {
997
- super("CoordinatedStartup", PHASE_ORDER2, StartupPhase);
998
- }
999
- /**
1000
- * Adds a task to a specific startup phase with timeout.
1001
- *
1002
- * @param phase - The startup phase from {@link StartupPhase}
1003
- * @param taskName - Unique identifier for the task
1004
- * @param task - Async function to execute
1005
- * @param timeoutMs - Task timeout in milliseconds (default: 10000)
1006
- */
1007
- addTask(phase, taskName, task, timeoutMs = 1e4) {
1008
- super.addTask(phase, taskName, task, timeoutMs);
1009
- }
1010
- canAddTask() {
1011
- if (this.hasStarted) {
1012
- throw new SeedcordError(SeedcordErrorCode.LifecycleAddAfterCompletion);
1013
- }
1014
- if (this.isStartingUp) {
1015
- throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);
1016
- }
1017
- return true;
1018
- }
1019
- canRemoveTask() {
1020
- if (this.isStartingUp) {
1021
- throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);
1022
- }
1023
- return true;
1024
- }
1025
- getTaskType() {
1026
- return "startup";
1027
- }
1028
- async executeTasksInPhase(phase, tasks) {
1029
- const results = [];
1030
- for (const task of tasks) {
1031
- results.push(await Promise.resolve().then(() => this.runTaskWithTimeout(phase, task)).then(
1032
- () => ({
1033
- status: "fulfilled",
1034
- value: void 0
1035
- }),
1036
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1037
- (reason) => ({
1038
- status: "rejected",
1039
- reason
1040
- })
1041
- ));
1042
- }
1043
- return results;
1044
- }
1045
- /**
1046
- * Executes the coordinated startup sequence.
1047
- *
1048
- * Runs all registered tasks across startup phases in the correct order.
1049
- * Each phase completes before the next phase begins. Tasks within a phase
1050
- * are executed sequentially to maintain predictable initialization.
1051
- *
1052
- * @returns Promise that resolves when startup is complete
1053
- * @throws An {@link Error} If startup fails or is called multiple times
1054
- * @example
1055
- * ```typescript
1056
- * const startup = new CoordinatedStartup();
1057
- * startup.addTask(StartupPhase.Services, 'database', () => db.connect(), 10000);
1058
- * await startup.run();
1059
- * ```
1060
- */
1061
- async run() {
1062
- if (this.hasStarted) {
1063
- this.logger.warn("Startup sequence has already completed");
1064
- return;
1065
- }
1066
- if (this.isStartingUp) {
1067
- this.logger.warn("Startup sequence already in progress");
1068
- return;
1069
- }
1070
- this.isStartingUp = true;
1071
- this.logger.info(`${chalk2.bold.green("Starting")} coordinated startup sequence`);
1072
- this.emit("startup:start");
1073
- try {
1074
- for (const phase of PHASE_ORDER2) await this.runPhase(phase);
1075
- this.hasStarted = true;
1076
- this.logger.info(`${chalk2.bold.green("Coordinated startup completed")} successfully`);
1077
- this.emit("startup:complete");
1078
- } catch (error) {
1079
- this.logger.error(`${chalk2.bold.red("Coordinated startup failed")}`);
1080
- this.emit("startup:error", error);
1081
- throw error;
1082
- } finally {
1083
- this.isStartingUp = false;
1084
- }
1085
- }
1086
- /**
1087
- * Subscribe to startup events
1088
- */
1089
- on(event, listener) {
1090
- super.on(event, listener);
1091
- }
1092
- /**
1093
- * Unsubscribe from startup events
1094
- */
1095
- off(event, listener) {
1096
- super.off(event, listener);
1097
- }
1098
- /**
1099
- * Check if startup has completed
1100
- */
1101
- get isReady() {
1102
- return this.hasStarted;
1103
- }
1104
- /**
1105
- * Check if startup is currently running
1106
- */
1107
- get isRunning() {
1108
- return this.isStartingUp;
1109
- }
1367
+ isStartingUp = false;
1368
+ hasStarted = false;
1369
+ constructor() {
1370
+ super("CoordinatedStartup", PHASE_ORDER, StartupPhase);
1371
+ }
1372
+ /**
1373
+ * Adds a task to a specific startup phase with timeout.
1374
+ *
1375
+ * @param phase - The startup phase from {@link StartupPhase}
1376
+ * @param taskName - Unique identifier for the task
1377
+ * @param task - Async function to execute
1378
+ * @param timeoutMs - Task timeout in milliseconds {@default 10000}
1379
+ */
1380
+ addTask(phase, taskName, task, timeoutMs = 1e4) {
1381
+ super.addTask(phase, taskName, task, timeoutMs);
1382
+ }
1383
+ canAddTask() {
1384
+ if (this.hasStarted) throw new SeedcordError(1101);
1385
+ if (this.isStartingUp) throw new SeedcordError(1102);
1386
+ return true;
1387
+ }
1388
+ canRemoveTask() {
1389
+ if (this.isStartingUp) throw new SeedcordError(1103);
1390
+ return true;
1391
+ }
1392
+ getTaskType() {
1393
+ return "startup";
1394
+ }
1395
+ async executeTasksInPhase(phase, tasks) {
1396
+ const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
1397
+ return Promise.allSettled(promises);
1398
+ }
1399
+ /**
1400
+ * Executes the coordinated startup sequence.
1401
+ *
1402
+ * Runs all registered tasks across startup phases in the correct order.
1403
+ * Each phase completes before the next phase begins. Tasks within a phase
1404
+ * are executed sequentially to maintain predictable initialization.
1405
+ *
1406
+ * @returns Promise that resolves when startup is complete
1407
+ * @throws An {@link Error} If startup fails or is called multiple times
1408
+ * @example
1409
+ * ```typescript
1410
+ * const startup = new CoordinatedStartup();
1411
+ * startup.addTask(StartupPhase.Services, 'database', () => db.connect(), 10000);
1412
+ * await startup.run();
1413
+ * ```
1414
+ */
1415
+ async run() {
1416
+ if (this.hasStarted) {
1417
+ this.logger.warn("Startup sequence has already completed");
1418
+ return;
1419
+ }
1420
+ if (this.isStartingUp) {
1421
+ this.logger.warn("Startup sequence already in progress");
1422
+ return;
1423
+ }
1424
+ this.isStartingUp = true;
1425
+ this.logger.info(`${chalk.bold.green("Starting")} coordinated startup sequence`);
1426
+ this.emit("startup:start");
1427
+ try {
1428
+ for (const phase of PHASE_ORDER) {
1429
+ if (!this.isStartingUp) {
1430
+ this.logger.warn("Startup sequence aborted");
1431
+ return;
1432
+ }
1433
+ await this.runPhase(phase);
1434
+ }
1435
+ this.hasStarted = true;
1436
+ this.logger.info(`${chalk.bold.green("Coordinated startup completed")} successfully`);
1437
+ this.emit("startup:complete");
1438
+ } catch (error) {
1439
+ if (!this.isStartingUp) {
1440
+ this.logger.warn("Startup sequence aborted during error handling");
1441
+ return;
1442
+ }
1443
+ this.logger.error(`${chalk.bold.red("Coordinated startup failed")}`);
1444
+ this.emit("startup:error", error);
1445
+ throw error;
1446
+ } finally {
1447
+ this.isStartingUp = false;
1448
+ }
1449
+ }
1450
+ async runTaskWithTimeout(phase, task) {
1451
+ this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
1452
+ let timeoutId;
1453
+ try {
1454
+ await Promise.race([task.task(), new Promise((_, reject) => {
1455
+ timeoutId = setTimeout(() => {
1456
+ reject(new SeedcordError(1106, [task.name, task.timeout]));
1457
+ }, task.timeout);
1458
+ })]);
1459
+ this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
1460
+ } catch (error) {
1461
+ if (!this.isStartingUp) return;
1462
+ this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}:`, error);
1463
+ throw error;
1464
+ } finally {
1465
+ if (timeoutId) clearTimeout(timeoutId);
1466
+ }
1467
+ }
1468
+ /**
1469
+ * Aborts the startup sequence if it is currently running.
1470
+ */
1471
+ abort() {
1472
+ if (this.isStartingUp) {
1473
+ this.isStartingUp = false;
1474
+ this.logger.warn("Aborting coordinated startup sequence");
1475
+ }
1476
+ }
1477
+ /**
1478
+ * Check if startup has completed
1479
+ */
1480
+ get isReady() {
1481
+ return this.hasStarted;
1482
+ }
1483
+ /**
1484
+ * Check if startup is currently running
1485
+ */
1486
+ get isRunning() {
1487
+ return this.isStartingUp;
1488
+ }
1110
1489
  };
1111
1490
 
1112
- export { CooldownManager, CoordinatedLifecycle, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Logger, SeedcordError, SeedcordErrorCode, SeedcordErrors, SeedcordRangeError, SeedcordTypeError, ShutdownPhase, StartupPhase, StrictEventEmitter, formatSeedcordErrorMessage, isSeedcordError, messages as seedcordErrorMessages };
1113
- //# sourceMappingURL=index.mjs.map
1491
+ //#endregion
1492
+ //#region src/index.ts
1493
+ /** Package version */
1494
+ const version = "0.7.0-next.0";
1495
+
1496
+ //#endregion
1497
+ export { CooldownManager, CoordinatedLifecycle, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Logger, LoggerChannelRegistry, LoggerUtilities, SeedcordErrorCode, ShutdownPhase, StartupPhase, StrictEventEmitter, isSeedcordError, version };
1114
1498
  //# sourceMappingURL=index.mjs.map