Skip to content

Commit 9bac4a3

Browse files
committed
wip
1 parent 308b0eb commit 9bac4a3

File tree

40 files changed

+1758
-481
lines changed

40 files changed

+1758
-481
lines changed

package-lock.json

Lines changed: 274 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@oclif/plugin-version": "^2.0.17",
2121
"@readme/openapi-parser": "^5.0.1",
2222
"chokidar": "^4.0.3",
23+
"picocolors": "^1.1.1",
2324
"cosmiconfig": "^9.0.0",
2425
"graphology": "^0.26.0",
2526
"inquirer": "^8.2.6",

src/LoggingInterface.ts

Lines changed: 337 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,355 @@
1+
/* eslint-disable no-undef, no-console, security/detect-object-injection */
2+
/**
3+
* Enhanced logging interface with levels, colors, and spinners
4+
*/
5+
import pc from 'picocolors';
6+
7+
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'verbose' | 'debug';
8+
9+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
10+
silent: 0,
11+
error: 1,
12+
warn: 2,
13+
info: 3,
14+
verbose: 4,
15+
debug: 5
16+
};
17+
18+
// Simple spinner implementation using console
19+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
20+
21+
interface SpinnerState {
22+
text: string;
23+
interval: ReturnType<typeof setInterval> | null;
24+
frameIndex: number;
25+
}
26+
127
/**
228
* Logging interface for the model generation library
329
*/
430
export interface LoggingInterface {
5-
debug(message?: any, ...optionalParams: any[]): void;
6-
info(message?: any, ...optionalParams: any[]): void;
7-
warn(message?: any, ...optionalParams: any[]): void;
8-
error(message?: any, ...optionalParams: any[]): void;
31+
debug(message?: unknown, ...optionalParams: unknown[]): void;
32+
info(message?: unknown, ...optionalParams: unknown[]): void;
33+
warn(message?: unknown, ...optionalParams: unknown[]): void;
34+
error(message?: unknown, ...optionalParams: unknown[]): void;
35+
}
36+
37+
/**
38+
* Extended logging interface with additional capabilities
39+
*/
40+
export interface ExtendedLoggingInterface extends LoggingInterface {
41+
// Additional log level
42+
verbose(message?: unknown, ...optionalParams: unknown[]): void;
43+
44+
// Progress helpers
45+
startSpinner(text: string): void;
46+
updateSpinner(text: string): void;
47+
succeedSpinner(text?: string): void;
48+
failSpinner(text?: string): void;
49+
stopSpinner(): void;
50+
51+
// Structured output
52+
json(data: unknown): void;
53+
54+
// Configuration
55+
setLevel(level: LogLevel): void;
56+
setJsonMode(enabled: boolean): void;
57+
setColors(enabled: boolean): void;
58+
59+
// State queries
60+
getLevel(): LogLevel;
61+
isJsonMode(): boolean;
962
}
1063

1164
/**
12-
* Logger class for the model generation library
65+
* Logger class with enhanced capabilities
1366
*
14-
* This class acts as a forefront for any external loggers which is why it also implements the interface itself.
67+
* Supports log levels, colors, spinners, and JSON output mode.
68+
* Acts as a forefront for any external loggers.
1569
*/
16-
export class LoggerClass implements LoggingInterface {
70+
export class LoggerClass implements ExtendedLoggingInterface {
1771
private logger?: LoggingInterface = undefined;
72+
private level: LogLevel = 'info';
73+
private jsonMode = false;
74+
private colorsEnabled = true;
75+
private spinner: SpinnerState | null = null;
76+
77+
/**
78+
* Check if a message at the given level should be logged
79+
*/
80+
private shouldLog(messageLevel: LogLevel): boolean {
81+
return (
82+
LOG_LEVEL_PRIORITY[messageLevel] <= LOG_LEVEL_PRIORITY[this.level] &&
83+
!this.jsonMode
84+
);
85+
}
86+
87+
/**
88+
* Format a message with optional color
89+
*/
90+
private formatMessage(
91+
message: unknown,
92+
colorFn?: (s: string) => string
93+
): string {
94+
const msg = String(message);
95+
if (this.colorsEnabled && colorFn) {
96+
return colorFn(msg);
97+
}
98+
return msg;
99+
}
100+
101+
/**
102+
* Stop spinner before logging to prevent output overlap
103+
*/
104+
private pauseSpinner(): void {
105+
if (this.spinner?.interval) {
106+
clearInterval(this.spinner.interval);
107+
// Clear the current line
108+
if (process.stdout.isTTY) {
109+
process.stdout.clearLine(0);
110+
process.stdout.cursorTo(0);
111+
}
112+
}
113+
}
114+
115+
/**
116+
* Resume spinner after logging
117+
*/
118+
private resumeSpinner(): void {
119+
if (this.spinner && !this.spinner.interval) {
120+
this.renderSpinner();
121+
}
122+
}
123+
124+
/**
125+
* Render the spinner
126+
*/
127+
private renderSpinner(): void {
128+
if (!this.spinner || !process.stdout.isTTY) {
129+
return;
130+
}
131+
132+
this.spinner.interval = setInterval(() => {
133+
if (!this.spinner) {
134+
return;
135+
}
136+
const frame = this.colorsEnabled
137+
? pc.cyan(SPINNER_FRAMES[this.spinner.frameIndex])
138+
: SPINNER_FRAMES[this.spinner.frameIndex];
139+
process.stdout.clearLine(0);
140+
process.stdout.cursorTo(0);
141+
process.stdout.write(`${frame} ${this.spinner.text}`);
142+
this.spinner.frameIndex =
143+
(this.spinner.frameIndex + 1) % SPINNER_FRAMES.length;
144+
}, 80);
145+
}
146+
147+
debug(message?: unknown, ...optionalParams: unknown[]): void {
148+
if (!this.shouldLog('debug')) {
149+
return;
150+
}
151+
152+
this.pauseSpinner();
153+
const prefix = this.formatMessage('[DEBUG] ', pc.gray);
154+
const formattedMessage = this.formatMessage(message, pc.gray);
18155

19-
debug(message?: any, ...optionalParams: any[]): void {
20156
if (this.logger) {
21-
this.logger.debug(message, ...optionalParams);
157+
this.logger.debug(prefix + formattedMessage, ...optionalParams);
158+
} else {
159+
console.debug(prefix + formattedMessage, ...optionalParams);
22160
}
161+
this.resumeSpinner();
23162
}
24163

25-
info(message?: any, ...optionalParams: any[]): void {
164+
verbose(message?: unknown, ...optionalParams: unknown[]): void {
165+
if (!this.shouldLog('verbose')) {
166+
return;
167+
}
168+
169+
this.pauseSpinner();
170+
const formattedMessage = this.formatMessage(message, pc.dim);
171+
26172
if (this.logger) {
27-
this.logger.info(message, ...optionalParams);
173+
this.logger.info(formattedMessage, ...optionalParams);
174+
} else {
175+
console.log(formattedMessage, ...optionalParams);
28176
}
177+
this.resumeSpinner();
29178
}
30179

31-
warn(message?: any, ...optionalParams: any[]): void {
180+
info(message?: unknown, ...optionalParams: unknown[]): void {
181+
if (!this.shouldLog('info')) {
182+
return;
183+
}
184+
185+
this.pauseSpinner();
186+
const msg = String(message);
187+
32188
if (this.logger) {
33-
this.logger.warn(message, ...optionalParams);
189+
this.logger.info(msg, ...optionalParams);
190+
} else {
191+
// Use process.stdout.write for better capture by oclif test utilities
192+
const fullMsg =
193+
optionalParams.length > 0
194+
? `${msg} ${optionalParams.join(' ')}\n`
195+
: `${msg}\n`;
196+
process.stdout.write(fullMsg);
34197
}
198+
this.resumeSpinner();
35199
}
36200

37-
error(message?: any, ...optionalParams: any[]): void {
201+
warn(message?: unknown, ...optionalParams: unknown[]): void {
202+
if (!this.shouldLog('warn')) {
203+
return;
204+
}
205+
206+
this.pauseSpinner();
207+
const formattedMessage = this.formatMessage(message, pc.yellow);
208+
38209
if (this.logger) {
39-
this.logger.error(message, ...optionalParams);
210+
this.logger.warn(formattedMessage, ...optionalParams);
211+
} else {
212+
console.warn(formattedMessage, ...optionalParams);
213+
}
214+
this.resumeSpinner();
215+
}
216+
217+
error(message?: unknown, ...optionalParams: unknown[]): void {
218+
if (!this.shouldLog('error')) {
219+
return;
220+
}
221+
222+
this.pauseSpinner();
223+
const formattedMessage = this.formatMessage(message, pc.red);
224+
225+
if (this.logger) {
226+
this.logger.error(formattedMessage, ...optionalParams);
227+
} else {
228+
console.error(formattedMessage, ...optionalParams);
229+
}
230+
this.resumeSpinner();
231+
}
232+
233+
/**
234+
* Start a spinner with the given text
235+
*/
236+
startSpinner(text: string): void {
237+
if (this.jsonMode) {
238+
return;
239+
}
240+
this.stopSpinner();
241+
242+
this.spinner = {
243+
text,
244+
interval: null,
245+
frameIndex: 0
246+
};
247+
248+
if (process.stdout.isTTY) {
249+
this.renderSpinner();
250+
} else {
251+
// In non-TTY mode, just print the text
252+
console.log(text);
253+
}
254+
}
255+
256+
/**
257+
* Update the spinner text
258+
*/
259+
updateSpinner(text: string): void {
260+
if (this.spinner) {
261+
this.spinner.text = text;
262+
}
263+
}
264+
265+
/**
266+
* Stop the spinner with a success message
267+
*/
268+
succeedSpinner(text?: string): void {
269+
this.stopSpinner();
270+
const displayText = text || this.spinner?.text || '';
271+
if (displayText && this.shouldLog('info')) {
272+
const symbol = this.colorsEnabled ? pc.green('✓') : '[OK]';
273+
console.log(`${symbol} ${displayText}`);
274+
}
275+
}
276+
277+
/**
278+
* Stop the spinner with a failure message
279+
*/
280+
failSpinner(text?: string): void {
281+
this.stopSpinner();
282+
const displayText = text || this.spinner?.text || '';
283+
if (displayText && this.shouldLog('error')) {
284+
const symbol = this.colorsEnabled ? pc.red('✗') : '[FAIL]';
285+
console.log(`${symbol} ${displayText}`);
286+
}
287+
}
288+
289+
/**
290+
* Stop the spinner without a message
291+
*/
292+
stopSpinner(): void {
293+
if (this.spinner) {
294+
if (this.spinner.interval) {
295+
clearInterval(this.spinner.interval);
296+
}
297+
if (process.stdout.isTTY) {
298+
process.stdout.clearLine(0);
299+
process.stdout.cursorTo(0);
300+
}
301+
this.spinner = null;
40302
}
41303
}
42304

305+
/**
306+
* Output structured JSON data
307+
* Only outputs in JSON mode or when explicitly called
308+
*/
309+
json(data: unknown): void {
310+
this.stopSpinner();
311+
console.log(JSON.stringify(data, null, 2));
312+
}
313+
314+
/**
315+
* Set the log level
316+
*/
317+
setLevel(level: LogLevel): void {
318+
this.level = level;
319+
}
320+
321+
/**
322+
* Enable or disable JSON mode
323+
* In JSON mode, only json() output is shown
324+
*/
325+
setJsonMode(enabled: boolean): void {
326+
this.jsonMode = enabled;
327+
if (enabled) {
328+
this.stopSpinner();
329+
}
330+
}
331+
332+
/**
333+
* Enable or disable colored output
334+
*/
335+
setColors(enabled: boolean): void {
336+
this.colorsEnabled = enabled;
337+
}
338+
339+
/**
340+
* Get the current log level
341+
*/
342+
getLevel(): LogLevel {
343+
return this.level;
344+
}
345+
346+
/**
347+
* Check if JSON mode is enabled
348+
*/
349+
isJsonMode(): boolean {
350+
return this.jsonMode;
351+
}
352+
43353
/**
44354
* Sets the logger to use for the model generation library
45355
*
@@ -48,6 +358,18 @@ export class LoggerClass implements LoggingInterface {
48358
setLogger(logger?: LoggingInterface): void {
49359
this.logger = logger;
50360
}
361+
362+
/**
363+
* Reset the logger to default state.
364+
* Useful for testing or when re-initializing the logger.
365+
*/
366+
reset(): void {
367+
this.stopSpinner();
368+
this.level = 'info';
369+
this.jsonMode = false;
370+
this.colorsEnabled = true;
371+
this.logger = undefined;
372+
}
51373
}
52374

53375
export const Logger: LoggerClass = new LoggerClass();

0 commit comments

Comments
 (0)