@northflare/runner 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/utils/config.d.ts +1 -0
  2. package/dist/utils/config.d.ts.map +1 -1
  3. package/dist/utils/config.js +13 -2
  4. package/dist/utils/config.js.map +1 -1
  5. package/package.json +1 -2
  6. package/coverage/base.css +0 -224
  7. package/coverage/block-navigation.js +0 -87
  8. package/coverage/coverage-final.json +0 -12
  9. package/coverage/favicon.png +0 -0
  10. package/coverage/index.html +0 -176
  11. package/coverage/lib/index.html +0 -116
  12. package/coverage/lib/preload-script.js.html +0 -964
  13. package/coverage/prettify.css +0 -1
  14. package/coverage/prettify.js +0 -2
  15. package/coverage/sort-arrow-sprite.png +0 -0
  16. package/coverage/sorter.js +0 -196
  17. package/coverage/src/collections/index.html +0 -116
  18. package/coverage/src/collections/runner-messages.ts.html +0 -312
  19. package/coverage/src/components/claude-manager.ts.html +0 -1290
  20. package/coverage/src/components/index.html +0 -146
  21. package/coverage/src/components/message-handler.ts.html +0 -730
  22. package/coverage/src/components/repository-manager.ts.html +0 -841
  23. package/coverage/src/index.html +0 -131
  24. package/coverage/src/index.ts.html +0 -448
  25. package/coverage/src/runner.ts.html +0 -1239
  26. package/coverage/src/utils/config.ts.html +0 -780
  27. package/coverage/src/utils/console.ts.html +0 -121
  28. package/coverage/src/utils/index.html +0 -161
  29. package/coverage/src/utils/logger.ts.html +0 -475
  30. package/coverage/src/utils/status-line.ts.html +0 -445
  31. package/exceptions.log +0 -24
  32. package/lib/codex-sdk/src/codex.ts +0 -38
  33. package/lib/codex-sdk/src/codexOptions.ts +0 -10
  34. package/lib/codex-sdk/src/events.ts +0 -80
  35. package/lib/codex-sdk/src/exec.ts +0 -336
  36. package/lib/codex-sdk/src/index.ts +0 -39
  37. package/lib/codex-sdk/src/items.ts +0 -127
  38. package/lib/codex-sdk/src/outputSchemaFile.ts +0 -40
  39. package/lib/codex-sdk/src/thread.ts +0 -155
  40. package/lib/codex-sdk/src/threadOptions.ts +0 -18
  41. package/lib/codex-sdk/src/turnOptions.ts +0 -6
  42. package/lib/codex-sdk/tests/abort.test.ts +0 -165
  43. package/lib/codex-sdk/tests/codexExecSpy.ts +0 -37
  44. package/lib/codex-sdk/tests/responsesProxy.ts +0 -225
  45. package/lib/codex-sdk/tests/run.test.ts +0 -687
  46. package/lib/codex-sdk/tests/runStreamed.test.ts +0 -211
  47. package/lib/codex-sdk/tsconfig.json +0 -24
  48. package/rejections.log +0 -68
  49. package/runner.log +0 -488
  50. package/src/components/claude-sdk-manager.ts +0 -1425
  51. package/src/components/codex-sdk-manager.ts +0 -1358
  52. package/src/components/enhanced-repository-manager.ts +0 -823
  53. package/src/components/message-handler-sse.ts +0 -1097
  54. package/src/components/repository-manager.ts +0 -337
  55. package/src/index.ts +0 -168
  56. package/src/runner-sse.ts +0 -917
  57. package/src/services/RunnerAPIClient.ts +0 -175
  58. package/src/services/SSEClient.ts +0 -258
  59. package/src/types/claude.ts +0 -66
  60. package/src/types/computer-name.d.ts +0 -4
  61. package/src/types/index.ts +0 -64
  62. package/src/types/messages.ts +0 -39
  63. package/src/types/runner-interface.ts +0 -36
  64. package/src/utils/StateManager.ts +0 -187
  65. package/src/utils/config.ts +0 -316
  66. package/src/utils/console.ts +0 -15
  67. package/src/utils/debug.ts +0 -18
  68. package/src/utils/expand-env.ts +0 -22
  69. package/src/utils/logger.ts +0 -134
  70. package/src/utils/model.ts +0 -29
  71. package/src/utils/status-line.ts +0 -122
  72. package/src/utils/tool-response-sanitizer.ts +0 -160
  73. package/test-debug.sh +0 -26
  74. package/tests/retry-strategies.test.ts +0 -410
  75. package/tests/sdk-integration.test.ts +0 -329
  76. package/tests/sdk-streaming.test.ts +0 -1180
  77. package/tests/setup.ts +0 -5
  78. package/tests/test-claude-manager.ts +0 -120
  79. package/tests/tool-response-sanitizer.test.ts +0 -63
  80. package/tsconfig.json +0 -36
  81. package/vitest.config.ts +0 -27
@@ -1,187 +0,0 @@
1
- /**
2
- * State Manager for persisting runner state
3
- *
4
- * Manages persistent state including lastProcessedAt timestamp
5
- * to enable proper catch-up on restart.
6
- */
7
-
8
- import fs from 'fs/promises';
9
- import path from 'path';
10
- import { createLogger } from './logger';
11
-
12
- const logger = createLogger('StateManager');
13
-
14
- export interface RunnerState {
15
- runnerId: string;
16
- runnerUid: string | null;
17
- lastProcessedAt: string | null; // ISO string
18
- isActiveRunner: boolean;
19
- updatedAt: string; // ISO string
20
- }
21
-
22
- export class StateManager {
23
- private statePath: string;
24
- private state: RunnerState | null = null;
25
- private saveTimer: NodeJS.Timeout | null = null;
26
-
27
- constructor(dataDir: string, runnerId: string) {
28
- // Namespace state file by runnerId (stable external ID) to separate different users
29
- const stateFile = `runner-state-${runnerId}.json`;
30
- this.statePath = path.join(dataDir, stateFile);
31
- }
32
-
33
- /**
34
- * Load state from disk
35
- */
36
- async loadState(): Promise<RunnerState | null> {
37
- try {
38
- const content = await fs.readFile(this.statePath, 'utf-8');
39
- this.state = JSON.parse(content);
40
- logger.info('Loaded runner state', {
41
- runnerId: this.state?.runnerId,
42
- runnerUid: this.state?.runnerUid,
43
- lastProcessedAt: this.state?.lastProcessedAt,
44
- isActiveRunner: this.state?.isActiveRunner,
45
- });
46
- return this.state;
47
- } catch (error) {
48
- if ((error as any).code === 'ENOENT') {
49
- logger.debug('No state file found, starting fresh');
50
- return null;
51
- }
52
- logger.error('Failed to load state file:', error);
53
- return null;
54
- }
55
- }
56
-
57
- /**
58
- * Save state to disk
59
- */
60
- async saveState(state: RunnerState): Promise<void> {
61
- this.state = state;
62
-
63
- // Cancel any pending save
64
- if (this.saveTimer) {
65
- clearTimeout(this.saveTimer);
66
- }
67
-
68
- // Debounce saves to avoid excessive disk writes
69
- this.saveTimer = setTimeout(async () => {
70
- try {
71
- // Ensure directory exists
72
- const dir = path.dirname(this.statePath);
73
- await fs.mkdir(dir, { recursive: true });
74
-
75
- // Update timestamp
76
- state.updatedAt = new Date().toISOString();
77
-
78
- // Write atomically by writing to temp file first
79
- const tempPath = `${this.statePath}.tmp`;
80
- await fs.writeFile(tempPath, JSON.stringify(state, null, 2), 'utf-8');
81
- await fs.rename(tempPath, this.statePath);
82
-
83
- logger.debug('Saved runner state', {
84
- runnerId: state.runnerId,
85
- lastProcessedAt: state.lastProcessedAt,
86
- isActiveRunner: state.isActiveRunner,
87
- });
88
- } catch (error) {
89
- logger.error('Failed to save state file:', error);
90
- throw error;
91
- }
92
- }, 1000); // 1 second debounce
93
- }
94
-
95
- /**
96
- * Update lastProcessedAt timestamp
97
- */
98
- async updateLastProcessedAt(timestamp: Date | string): Promise<void> {
99
- if (!this.state) {
100
- throw new Error('State not loaded');
101
- }
102
-
103
- const isoString = timestamp instanceof Date ? timestamp.toISOString() : timestamp;
104
-
105
- const updatedState: RunnerState = {
106
- ...this.state,
107
- lastProcessedAt: isoString,
108
- };
109
-
110
- await this.saveState(updatedState);
111
- }
112
-
113
- /**
114
- * Update runner registration details
115
- */
116
- async updateRunnerRegistration(
117
- runnerId: string,
118
- runnerUid: string | null,
119
- lastProcessedAt: Date | null
120
- ): Promise<void> {
121
- const state: RunnerState = this.state || {
122
- runnerId: '',
123
- runnerUid: null,
124
- lastProcessedAt: null,
125
- isActiveRunner: false,
126
- updatedAt: new Date().toISOString(),
127
- };
128
-
129
- const updatedState: RunnerState = {
130
- ...state,
131
- runnerId,
132
- runnerUid,
133
- lastProcessedAt: lastProcessedAt ? lastProcessedAt.toISOString() : null,
134
- };
135
-
136
- await this.saveState(updatedState);
137
- }
138
-
139
- /**
140
- * Update active runner status
141
- */
142
- async updateActiveStatus(isActive: boolean): Promise<void> {
143
- if (!this.state) {
144
- throw new Error('State not loaded');
145
- }
146
-
147
- const updatedState: RunnerState = {
148
- ...this.state,
149
- isActiveRunner: isActive,
150
- };
151
-
152
- await this.saveState(updatedState);
153
- }
154
-
155
- /**
156
- * Get current state
157
- */
158
- getState(): RunnerState | null {
159
- return this.state;
160
- }
161
-
162
- /**
163
- * Get lastProcessedAt as Date
164
- */
165
- getLastProcessedAt(): Date | null {
166
- if (!this.state || !this.state.lastProcessedAt) {
167
- return null;
168
- }
169
- return new Date(this.state.lastProcessedAt);
170
- }
171
-
172
- /**
173
- * Clear state (for testing or reset)
174
- */
175
- async clearState(): Promise<void> {
176
- this.state = null;
177
- try {
178
- await fs.unlink(this.statePath);
179
- logger.info('Cleared runner state');
180
- } catch (error) {
181
- if ((error as any).code !== 'ENOENT') {
182
- logger.error('Failed to clear state file:', error);
183
- throw error;
184
- }
185
- }
186
- }
187
- }
@@ -1,316 +0,0 @@
1
- /**
2
- * Configuration management utilities
3
- */
4
-
5
- import { RunnerConfig, EnvironmentConfig, RetryStrategy } from "../types";
6
- import fs from "fs/promises";
7
- import path from "path";
8
- import { createLogger } from "./logger";
9
- import { Command } from "commander";
10
-
11
- const logger = createLogger("ConfigManager");
12
-
13
- export class ConfigManager {
14
- private static DEFAULT_CONFIG: Partial<RunnerConfig> = {
15
- dataDir: "./data",
16
- heartbeatInterval: 120000, // 2 minutes
17
- retryStrategy: "exponential" as RetryStrategy,
18
- retryIntervalSecs: 60,
19
- retryDurationSecs: 900,
20
- };
21
-
22
- /**
23
- * Load configuration without parsing command line arguments
24
- * Used when arguments have already been parsed by the CLI
25
- */
26
- static async loadConfig(configPath?: string): Promise<RunnerConfig> {
27
- // Validate required environment variables
28
- this.validateEnvironment();
29
-
30
- // Start with defaults
31
- let config: Partial<RunnerConfig> = { ...this.DEFAULT_CONFIG };
32
-
33
- // Determine config path - use provided path or default location
34
- let effectiveConfigPath = configPath;
35
- if (!effectiveConfigPath) {
36
- // Try to use default config location
37
- try {
38
- const envPaths = require("env-paths").default || require("env-paths");
39
- const paths = envPaths("northflare-runner", { suffix: "" });
40
- const defaultConfigPath = path.join(paths.config, "config.json");
41
-
42
- // Check if default config exists
43
- const fs = require("fs");
44
- if (fs.existsSync(defaultConfigPath)) {
45
- effectiveConfigPath = defaultConfigPath;
46
- logger.info(`Using default config file: ${defaultConfigPath}`);
47
- }
48
- } catch (error) {
49
- // env-paths not available or error accessing default location
50
- logger.debug("Could not check default config location:", error);
51
- }
52
- }
53
-
54
- // Load from config file if we have a path
55
- if (effectiveConfigPath) {
56
- const fileConfig = await this.loadConfigFile(effectiveConfigPath);
57
- config = { ...config, ...fileConfig };
58
- }
59
-
60
- // Override with environment variables
61
- const envConfig = this.loadFromEnvironment();
62
- config = { ...config, ...envConfig };
63
-
64
- // Validate final configuration
65
- this.validateConfig(config);
66
-
67
- return config as RunnerConfig;
68
- }
69
-
70
- /**
71
- * Parse command line arguments and load configuration
72
- */
73
- static async parseArgsAndLoadConfig(argv: string[]): Promise<RunnerConfig> {
74
- const program = new Command();
75
-
76
- program
77
- .name("northflare-runner")
78
- .description(
79
- "Northflare Runner - Executes Claude agents for task processing"
80
- )
81
- .version("1.0.0")
82
- .option("-c, --config <path>", "Path to configuration file")
83
- .option(
84
- "--retry-strategy <strategy>",
85
- "Registration retry strategy (none, interval, exponential)",
86
- "exponential"
87
- )
88
- .option(
89
- "--retry-interval-secs <seconds>",
90
- "Retry interval in seconds for interval strategy",
91
- "60"
92
- )
93
- .option(
94
- "--retry-duration-secs <seconds>",
95
- "Max retry duration in seconds for exponential strategy",
96
- "900"
97
- )
98
- .option("--data-dir <path>", "Data directory path", "./data")
99
- .option(
100
- "--heartbeat-interval <ms>",
101
- "Heartbeat interval in milliseconds",
102
- "120000"
103
- );
104
-
105
- program.parse(argv);
106
- const options = program.opts();
107
-
108
- // Validate required environment variables
109
- this.validateEnvironment();
110
-
111
- // Start with defaults
112
- let config: Partial<RunnerConfig> = { ...this.DEFAULT_CONFIG };
113
-
114
- // Load from config file if provided
115
- if (options["config"]) {
116
- const fileConfig = await this.loadConfigFile(options["config"]);
117
- config = { ...config, ...fileConfig };
118
- }
119
-
120
- // Override with environment variables
121
- const envConfig = this.loadFromEnvironment();
122
- config = { ...config, ...envConfig };
123
-
124
- // Override with CLI arguments (highest priority)
125
- if (options["retryStrategy"]) {
126
- config.retryStrategy = options["retryStrategy"] as RetryStrategy;
127
- }
128
- if (options["retryIntervalSecs"]) {
129
- config.retryIntervalSecs = parseInt(options["retryIntervalSecs"]);
130
- }
131
- if (options["retryDurationSecs"]) {
132
- config.retryDurationSecs = parseInt(options["retryDurationSecs"]);
133
- }
134
- if (options["dataDir"]) {
135
- config.dataDir = options["dataDir"];
136
- }
137
- if (options["heartbeatInterval"]) {
138
- config.heartbeatInterval = parseInt(options["heartbeatInterval"]);
139
- }
140
-
141
- // Validate final configuration
142
- this.validateConfig(config);
143
-
144
- return config as RunnerConfig;
145
- }
146
-
147
- /**
148
- * Validate required environment variables
149
- */
150
- private static validateEnvironment(): void {
151
- // Set default for NORTHFLARE_WORKSPACE_DIR if not provided
152
- if (!process.env["NORTHFLARE_WORKSPACE_DIR"]) {
153
- try {
154
- const envPaths = require("env-paths").default || require("env-paths");
155
- const paths = envPaths("northflare-runner", { suffix: "" });
156
- process.env["NORTHFLARE_WORKSPACE_DIR"] = paths.data;
157
- } catch (error) {
158
- // Fallback to original default if env-paths is not available
159
- process.env["NORTHFLARE_WORKSPACE_DIR"] = "/workspace";
160
- }
161
- }
162
-
163
- // Set default for NORTHFLARE_ORCHESTRATOR_URL if not provided
164
- if (!process.env["NORTHFLARE_ORCHESTRATOR_URL"]) {
165
- process.env["NORTHFLARE_ORCHESTRATOR_URL"] = "https://api.northflare.app";
166
- }
167
-
168
- const required = [
169
- "NORTHFLARE_RUNNER_TOKEN",
170
- "NORTHFLARE_WORKSPACE_DIR",
171
- "NORTHFLARE_ORCHESTRATOR_URL",
172
- ];
173
-
174
- const missing = required.filter((key) => !process.env[key]);
175
-
176
- if (missing.length > 0) {
177
- throw new Error(
178
- `Missing required environment variables: ${missing.join(", ")}\n` +
179
- "Please set these environment variables before starting the runner."
180
- );
181
- }
182
- }
183
-
184
- /**
185
- * Load configuration from environment variables
186
- */
187
- private static loadFromEnvironment(): Partial<RunnerConfig> {
188
- const config: Partial<RunnerConfig> = {
189
- orchestratorUrl: process.env["NORTHFLARE_ORCHESTRATOR_URL"]!,
190
- };
191
-
192
- // Optional environment overrides
193
- if (process.env["NORTHFLARE_DATA_DIR"]) {
194
- config.dataDir = process.env["NORTHFLARE_DATA_DIR"];
195
- }
196
-
197
- return config;
198
- }
199
-
200
- /**
201
- * Load configuration from file
202
- */
203
- private static async loadConfigFile(
204
- configPath: string
205
- ): Promise<Partial<RunnerConfig>> {
206
- try {
207
- const absolutePath = path.resolve(configPath);
208
- const content = await fs.readFile(absolutePath, "utf-8");
209
-
210
- // Support both JSON and YAML formats
211
- if (configPath.endsWith(".json")) {
212
- const config = JSON.parse(content);
213
-
214
- // Log if runnerRepos are found in config
215
- if (config.runnerRepos && Array.isArray(config.runnerRepos)) {
216
- logger.info(
217
- `Found ${config.runnerRepos.length} runner repos in config file`
218
- );
219
- }
220
-
221
- return config;
222
- } else if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
223
- // For YAML support, we'd need to add a yaml parser dependency
224
- throw new Error("YAML configuration files are not yet supported");
225
- } else {
226
- throw new Error("Configuration file must be .json format");
227
- }
228
- } catch (error) {
229
- if ((error as any).code === "ENOENT") {
230
- logger.warn(`Configuration file not found: ${configPath}`);
231
- return {};
232
- }
233
- throw error;
234
- }
235
- }
236
-
237
- /**
238
- * Validate final configuration
239
- */
240
- private static validateConfig(config: Partial<RunnerConfig>): void {
241
- // Note: Runner ID will be generated by server during registration
242
-
243
- if (!config.orchestratorUrl) {
244
- throw new Error("orchestratorUrl is required");
245
- }
246
-
247
- if (!config.dataDir) {
248
- throw new Error("dataDir is required");
249
- }
250
-
251
- if (!config.heartbeatInterval || config.heartbeatInterval < 1000) {
252
- throw new Error("heartbeatInterval must be at least 1000ms");
253
- }
254
-
255
- // Validate retry strategy
256
- const validStrategies: RetryStrategy[] = [
257
- "none",
258
- "interval",
259
- "exponential",
260
- ];
261
- if (
262
- !config.retryStrategy ||
263
- !validStrategies.includes(config.retryStrategy)
264
- ) {
265
- throw new Error(
266
- `retryStrategy must be one of: ${validStrategies.join(", ")}`
267
- );
268
- }
269
-
270
- if (
271
- config.retryStrategy === "interval" &&
272
- (!config.retryIntervalSecs || config.retryIntervalSecs < 1)
273
- ) {
274
- throw new Error(
275
- "retryIntervalSecs must be at least 1 when using interval strategy"
276
- );
277
- }
278
-
279
- if (
280
- config.retryStrategy === "exponential" &&
281
- (!config.retryDurationSecs || config.retryDurationSecs < 1)
282
- ) {
283
- throw new Error(
284
- "retryDurationSecs must be at least 1 when using exponential strategy"
285
- );
286
- }
287
- }
288
-
289
- /**
290
- * Get all environment configuration
291
- */
292
- static getEnvironmentConfig(): EnvironmentConfig {
293
- return {
294
- NORTHFLARE_RUNNER_TOKEN: process.env["NORTHFLARE_RUNNER_TOKEN"]!,
295
- NORTHFLARE_WORKSPACE_DIR: process.env["NORTHFLARE_WORKSPACE_DIR"]!,
296
- NORTHFLARE_ORCHESTRATOR_URL: process.env["NORTHFLARE_ORCHESTRATOR_URL"]!,
297
- NORTHFLARE_RUNNER_DEBUG: process.env["NORTHFLARE_RUNNER_DEBUG"],
298
- DEBUG: process.env["DEBUG"],
299
- };
300
- }
301
-
302
- /**
303
- * Save configuration file with updated data
304
- */
305
- static async saveConfigFile(configPath: string, config: any): Promise<void> {
306
- try {
307
- const absolutePath = path.resolve(configPath);
308
- const content = JSON.stringify(config, null, 2);
309
- await fs.writeFile(absolutePath, content, "utf-8");
310
- logger.info(`Updated configuration file: ${absolutePath}`);
311
- } catch (error) {
312
- logger.error("Failed to save configuration file", error);
313
- throw error;
314
- }
315
- }
316
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Console wrapper that suppresses output when not in debug mode
3
- */
4
-
5
- import { isRunnerDebugEnabled } from "./debug";
6
-
7
- const isDebug = isRunnerDebugEnabled();
8
-
9
- export const console = {
10
- log: isDebug ? global.console.log.bind(global.console) : () => {},
11
- warn: isDebug ? global.console.warn.bind(global.console) : () => {},
12
- error: global.console.error.bind(global.console), // Always show errors
13
- info: isDebug ? global.console.info.bind(global.console) : () => {},
14
- debug: isDebug ? global.console.debug.bind(global.console) : () => {},
15
- };
@@ -1,18 +0,0 @@
1
- export function isRunnerDebugEnabled(): boolean {
2
- const explicitFlag = process.env["NORTHFLARE_RUNNER_DEBUG"];
3
- if (typeof explicitFlag === "string") {
4
- return isTruthy(explicitFlag);
5
- }
6
-
7
- const legacyFlag = process.env["DEBUG"];
8
- if (typeof legacyFlag === "string") {
9
- return isTruthy(legacyFlag);
10
- }
11
-
12
- return false;
13
- }
14
-
15
- function isTruthy(value: string): boolean {
16
- const normalized = value.trim().toLowerCase();
17
- return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
18
- }
@@ -1,22 +0,0 @@
1
- export function expandEnv(
2
- obj: any,
3
- env: Record<string, string | undefined> = process.env
4
- ): any {
5
- if (typeof obj === "string") {
6
- return obj.replace(/\$\{([^}]+)\}/g, (match, key) => env[key] || match);
7
- }
8
-
9
- if (Array.isArray(obj)) {
10
- return obj.map((item) => expandEnv(item, env));
11
- }
12
-
13
- if (obj && typeof obj === "object") {
14
- const result: Record<string, any> = {};
15
- for (const [key, value] of Object.entries(obj)) {
16
- result[key] = expandEnv(value, env);
17
- }
18
- return result;
19
- }
20
-
21
- return obj;
22
- }
@@ -1,134 +0,0 @@
1
- /**
2
- * Logger configuration using Winston
3
- */
4
-
5
- import winston from "winston";
6
- import path from "path";
7
- import { isRunnerDebugEnabled } from "./debug";
8
-
9
- // Custom log levels
10
- const levels = {
11
- error: 0,
12
- warn: 1,
13
- info: 2,
14
- http: 3,
15
- verbose: 4,
16
- debug: 5,
17
- silly: 6,
18
- };
19
-
20
- // Log colors
21
- const colors = {
22
- error: "red",
23
- warn: "yellow",
24
- info: "green",
25
- http: "magenta",
26
- verbose: "cyan",
27
- debug: "blue",
28
- silly: "gray",
29
- };
30
-
31
- winston.addColors(colors);
32
-
33
- const debugEnabled = isRunnerDebugEnabled();
34
-
35
- // Format for console output
36
- const consoleFormat = winston.format.combine(
37
- winston.format.colorize({ all: true }),
38
- winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
39
- winston.format.printf(({ timestamp, level, message, stack, ...metadata }) => {
40
- let msg = `${timestamp} [${level}]: ${message}`;
41
- // Only show stack traces and metadata in debug mode
42
- if (debugEnabled) {
43
- if (stack) {
44
- msg += `\n${stack}`;
45
- }
46
- const filteredMetadata = { ...metadata };
47
- delete filteredMetadata["component"]; // Keep component info
48
- if (Object.keys(filteredMetadata).length > 0) {
49
- msg += ` ${JSON.stringify(filteredMetadata)}`;
50
- }
51
- }
52
- return msg;
53
- })
54
- );
55
-
56
- // Format for file output
57
- const fileFormat = winston.format.combine(
58
- winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
59
- winston.format.errors({ stack: true }),
60
- winston.format.json()
61
- );
62
-
63
- // Create logger instance
64
- export const logger = winston.createLogger({
65
- level: debugEnabled ? "debug" : "info",
66
- levels,
67
- transports: [
68
- // Always include console transport for errors
69
- new winston.transports.Console({
70
- format: consoleFormat,
71
- level: debugEnabled ? "debug" : "error",
72
- }),
73
- ],
74
- });
75
-
76
- // Add file transports if log directory is configured
77
- export function configureFileLogging(logDir: string): void {
78
- // Error log file
79
- logger.add(
80
- new winston.transports.File({
81
- filename: path.join(logDir, "error.log"),
82
- level: "error",
83
- format: fileFormat,
84
- maxsize: 10 * 1024 * 1024, // 10MB
85
- maxFiles: 5,
86
- })
87
- );
88
-
89
- // Combined log file
90
- logger.add(
91
- new winston.transports.File({
92
- filename: path.join(logDir, "combined.log"),
93
- format: fileFormat,
94
- maxsize: 50 * 1024 * 1024, // 50MB
95
- maxFiles: 10,
96
- })
97
- );
98
-
99
- // Debug log file (only in debug mode)
100
- if (debugEnabled) {
101
- logger.add(
102
- new winston.transports.File({
103
- filename: path.join(logDir, "debug.log"),
104
- level: "debug",
105
- format: fileFormat,
106
- maxsize: 100 * 1024 * 1024, // 100MB
107
- maxFiles: 3,
108
- })
109
- );
110
- }
111
- }
112
-
113
- // Create child logger for specific components
114
- export function createLogger(component: string): winston.Logger {
115
- return logger.child({ component });
116
- }
117
-
118
- // Log unhandled errors
119
- logger.exceptions.handle(
120
- new winston.transports.File({
121
- filename: "exceptions.log",
122
- format: fileFormat,
123
- })
124
- );
125
-
126
- logger.rejections.handle(
127
- new winston.transports.File({
128
- filename: "rejections.log",
129
- format: fileFormat,
130
- })
131
- );
132
-
133
- // Export default logger
134
- export default logger;