@sayknow-cli/utils 0.3.11 → 0.3.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/utils",
4
- "version": "0.3.11",
4
+ "version": "0.3.13",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -21,17 +21,17 @@
21
21
  "streams"
22
22
  ],
23
23
  "main": "./src/index.ts",
24
- "types": "./dist/types/index.d.ts",
24
+ "types": "./src/index.ts",
25
25
  "scripts": {
26
26
  "check": "biome check . && bun run check:types",
27
- "check:types": "tsgo -p tsconfig.json --noEmit",
27
+ "check:types": "tsc -p tsconfig.json --noEmit",
28
28
  "lint": "biome lint .",
29
29
  "test": "bun test",
30
30
  "fix": "biome check --write --unsafe .",
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@sayknow-cli/natives": "0.3.11",
34
+ "@sayknow-cli/natives": "0.3.13",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
@@ -44,16 +44,15 @@
44
44
  "bun": ">=1.3.14"
45
45
  },
46
46
  "files": [
47
- "src",
48
- "dist/types"
47
+ "src"
49
48
  ],
50
49
  "exports": {
51
50
  ".": {
52
- "types": "./dist/types/index.d.ts",
51
+ "types": "./src/index.ts",
53
52
  "import": "./src/index.ts"
54
53
  },
55
54
  "./*": {
56
- "types": "./dist/types/*.d.ts",
55
+ "types": "./src/*.ts",
57
56
  "import": "./src/*.ts"
58
57
  },
59
58
  "./*.js": "./src/*.ts"
package/src/logger.ts CHANGED
@@ -11,8 +11,7 @@
11
11
  */
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
13
  import * as fs from "node:fs";
14
- import winston from "winston";
15
- import DailyRotateFile from "winston-daily-rotate-file";
14
+ import type * as winston from "winston";
16
15
  import { getLogsDir } from "./dirs";
17
16
 
18
17
  /** Ensure a logs directory exists; return the resolved path. */
@@ -23,28 +22,59 @@ function ensureDir(dir: string): string {
23
22
  return dir;
24
23
  }
25
24
 
26
- /** Custom format that includes pid and flattens metadata */
27
- const logFormat = winston.format.combine(
28
- winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
29
- winston.format.printf(({ timestamp, level, message, ...meta }) => {
30
- const entry: Record<string, unknown> = {
31
- timestamp,
32
- level,
33
- pid: process.pid,
34
- message,
35
- };
36
- // Flatten metadata into entry
37
- for (const [key, value] of Object.entries(meta)) {
38
- if (key !== "level" && key !== "timestamp" && key !== "message") {
39
- entry[key] = value;
25
+ type WinstonModule = typeof import("winston");
26
+ type DailyRotateFileCtor = typeof import("winston-daily-rotate-file");
27
+ type Logger = winston.Logger;
28
+ type Transport = winston.transport;
29
+ type LogLevel = "error" | "warn" | "info" | "debug";
30
+ type LogRecord = { level: LogLevel; message: string; context?: Record<string, unknown> };
31
+
32
+ let winstonModule: WinstonModule | undefined;
33
+ let dailyRotateFileCtor: DailyRotateFileCtor | undefined;
34
+ let winstonLogger: Logger | undefined;
35
+ let loggerInit: Promise<Logger> | undefined;
36
+ let flushingBufferedLogs = false;
37
+ const bufferedLogs: LogRecord[] = [];
38
+ /** Cap pre-init buffering so a hung/failed winston import cannot grow heap unboundedly. */
39
+ const MAX_BUFFERED_LOGS = 10_000;
40
+ let transportOptions: { console?: boolean; file?: boolean | string } = { file: true };
41
+
42
+ async function loadLoggingModules(): Promise<{ winston: WinstonModule; DailyRotateFile: DailyRotateFileCtor }> {
43
+ if (!winstonModule || !dailyRotateFileCtor) {
44
+ const [winstonImport, dailyRotateFileImport] = await Promise.all([
45
+ import("winston"),
46
+ import("winston-daily-rotate-file"),
47
+ ]);
48
+ winstonModule = (winstonImport.default ?? winstonImport) as WinstonModule;
49
+ dailyRotateFileCtor = dailyRotateFileImport.default;
50
+ }
51
+ return { winston: winstonModule, DailyRotateFile: dailyRotateFileCtor };
52
+ }
53
+
54
+ /** Build the JSON log formatter after winston is loaded. */
55
+ function makeLogFormat(winston: WinstonModule): winston.Logform.Format {
56
+ return winston.format.combine(
57
+ winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
58
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
59
+ const entry: Record<string, unknown> = {
60
+ timestamp,
61
+ level,
62
+ pid: process.pid,
63
+ message,
64
+ };
65
+ // Flatten metadata into entry
66
+ for (const [key, value] of Object.entries(meta)) {
67
+ if (key !== "level" && key !== "timestamp" && key !== "message") {
68
+ entry[key] = value;
69
+ }
40
70
  }
41
- }
42
- return JSON.stringify(entry);
43
- }),
44
- );
71
+ return JSON.stringify(entry);
72
+ }),
73
+ );
74
+ }
45
75
 
46
76
  /** Build a rotating file transport, materializing the target directory lazily. */
47
- function makeFileTransport(dir?: string): winston.transport {
77
+ function makeFileTransport(DailyRotateFile: DailyRotateFileCtor, dir?: string): Transport {
48
78
  return new DailyRotateFile({
49
79
  dirname: ensureDir(dir ?? getLogsDir()),
50
80
  filename: "skc.%DATE%.log",
@@ -55,18 +85,83 @@ function makeFileTransport(dir?: string): winston.transport {
55
85
  });
56
86
  }
57
87
 
58
- function makeConsoleTransport(): winston.transport {
59
- return new winston.transports.Console({ format: logFormat });
88
+ function makeConsoleTransport(winston: WinstonModule): Transport {
89
+ return new winston.transports.Console({ format: makeLogFormat(winston) });
90
+ }
91
+
92
+ function applyTransports(
93
+ logger: Logger,
94
+ modules: { winston: WinstonModule; DailyRotateFile: DailyRotateFileCtor },
95
+ ): void {
96
+ logger.clear();
97
+ if (transportOptions.file) {
98
+ logger.add(
99
+ makeFileTransport(
100
+ modules.DailyRotateFile,
101
+ typeof transportOptions.file === "string" ? transportOptions.file : undefined,
102
+ ),
103
+ );
104
+ }
105
+ if (transportOptions.console) logger.add(makeConsoleTransport(modules.winston));
60
106
  }
61
107
 
62
108
  /** The winston logger instance. Default: file ON (TUI-safe), console OFF. */
63
- const winstonLogger = winston.createLogger({
64
- level: "debug",
65
- format: logFormat,
66
- transports: [makeFileTransport()],
67
- // Don't exit on error - logging failures shouldn't crash the app
68
- exitOnError: false,
69
- });
109
+ async function getWinstonLogger(): Promise<Logger> {
110
+ if (winstonLogger) return winstonLogger;
111
+ loggerInit ??= (async () => {
112
+ const modules = await loadLoggingModules();
113
+ const logger = modules.winston.createLogger({
114
+ level: "debug",
115
+ format: makeLogFormat(modules.winston),
116
+ transports: [],
117
+ // Don't exit on error - logging failures shouldn't crash the app
118
+ exitOnError: false,
119
+ });
120
+ applyTransports(logger, modules);
121
+ winstonLogger = logger;
122
+ flushBufferedLogs();
123
+ return logger;
124
+ })();
125
+ return loggerInit;
126
+ }
127
+
128
+ function flushBufferedLogs(): void {
129
+ if (!winstonLogger || flushingBufferedLogs) return;
130
+ flushingBufferedLogs = true;
131
+ try {
132
+ // Loop: records buffered by reentrant writes during a flush pass (e.g.
133
+ // a transport callback that logs) are drained by the next pass instead
134
+ // of being stranded forever.
135
+ while (bufferedLogs.length > 0) {
136
+ for (const record of bufferedLogs.splice(0)) {
137
+ winstonLogger[record.level](record.message, record.context);
138
+ }
139
+ }
140
+ } finally {
141
+ flushingBufferedLogs = false;
142
+ }
143
+ }
144
+
145
+ function writeLog(level: LogLevel, message: string, context?: Record<string, unknown>): void {
146
+ try {
147
+ if (winstonLogger && !flushingBufferedLogs) {
148
+ winstonLogger[level](message, context);
149
+ return;
150
+ }
151
+ bufferedLogs.push({ level, message, context });
152
+ if (bufferedLogs.length > MAX_BUFFERED_LOGS) {
153
+ bufferedLogs.splice(0, bufferedLogs.length - MAX_BUFFERED_LOGS);
154
+ }
155
+ void getWinstonLogger().catch(() => {
156
+ bufferedLogs.length = 0;
157
+ // Allow a later write to retry initialization instead of pinning a
158
+ // rejected promise forever.
159
+ loggerInit = undefined;
160
+ });
161
+ } catch {
162
+ // Silently ignore logging failures
163
+ }
164
+ }
70
165
 
71
166
  /**
72
167
  * Replace the active log transports. Pass `console: true, file: false` for
@@ -74,11 +169,20 @@ const winstonLogger = winston.createLogger({
74
169
  * logs piped into a process supervisor instead of the rotating file.
75
170
  */
76
171
  export function setTransports(opts: { console?: boolean; file?: boolean | string }): void {
77
- winstonLogger.clear();
78
- if (opts.file) {
79
- winstonLogger.add(makeFileTransport(typeof opts.file === "string" ? opts.file : undefined));
172
+ transportOptions = opts;
173
+ if (winstonLogger && winstonModule && dailyRotateFileCtor) {
174
+ applyTransports(winstonLogger, { winston: winstonModule, DailyRotateFile: dailyRotateFileCtor });
175
+ } else if (loggerInit) {
176
+ // Init in flight: re-apply once it settles so the new options can never
177
+ // be silently skipped by an ordering race inside the init closure.
178
+ void loggerInit
179
+ .then(logger => {
180
+ if (winstonModule && dailyRotateFileCtor) {
181
+ applyTransports(logger, { winston: winstonModule, DailyRotateFile: dailyRotateFileCtor });
182
+ }
183
+ })
184
+ .catch(() => {});
80
185
  }
81
- if (opts.console) winstonLogger.add(makeConsoleTransport());
82
186
  }
83
187
 
84
188
  /**
@@ -87,11 +191,7 @@ export function setTransports(opts: { console?: boolean; file?: boolean | string
87
191
  * @param context - The context to log.
88
192
  */
89
193
  export function error(message: string, context?: Record<string, unknown>): void {
90
- try {
91
- winstonLogger.error(message, context);
92
- } catch {
93
- // Silently ignore logging failures
94
- }
194
+ writeLog("error", message, context);
95
195
  }
96
196
 
97
197
  /**
@@ -100,11 +200,7 @@ export function error(message: string, context?: Record<string, unknown>): void
100
200
  * @param context - The context to log.
101
201
  */
102
202
  export function warn(message: string, context?: Record<string, unknown>): void {
103
- try {
104
- winstonLogger.warn(message, context);
105
- } catch {
106
- // Silently ignore logging failures
107
- }
203
+ writeLog("warn", message, context);
108
204
  }
109
205
 
110
206
  /**
@@ -113,11 +209,7 @@ export function warn(message: string, context?: Record<string, unknown>): void {
113
209
  * @param context - The context to log.
114
210
  */
115
211
  export function info(message: string, context?: Record<string, unknown>): void {
116
- try {
117
- winstonLogger.info(message, context);
118
- } catch {
119
- // Silently ignore logging failures
120
- }
212
+ writeLog("info", message, context);
121
213
  }
122
214
 
123
215
  /**
@@ -126,11 +218,7 @@ export function info(message: string, context?: Record<string, unknown>): void {
126
218
  * @param context - The context to log.
127
219
  */
128
220
  export function debug(message: string, context?: Record<string, unknown>): void {
129
- try {
130
- winstonLogger.debug(message, context);
131
- } catch {
132
- // Silently ignore logging failures
133
- }
221
+ writeLog("debug", message, context);
134
222
  }
135
223
 
136
224
  const LOGGED_TIMING_THRESHOLD_MS = 0.5;