1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
/**
* Checks if the current environment supports interactive terminal features
* like carriage return (\r) for progress updates.
*
* Returns false for:
* - Non-TTY environments (CI/CD logs, file redirects)
* - Dumb terminals
* - Environments without cursor control support
*/
function isInteractiveTerminal(): boolean {
// Check if stdout is a TTY (interactive terminal)
if (!process.stdout.isTTY) return false;
// Check for dumb terminal
if (process.env.TERM === 'dumb') return false;
// Check for CI environments (most set CI=true)
if (process.env.CI === 'true' || process.env.CI === '1') return false;
// Check for common CI environment variables
const ciEnvVars = [
'GITHUB_ACTIONS',
'GITLAB_CI',
'CIRCLECI',
'TRAVIS',
'JENKINS_HOME',
'BUILDKITE',
'DRONE',
'RENDER', // Render.com
'CF_PAGES', // Cloudflare Pages
'VERCEL' // Vercel
];
for (const envVar of ciEnvVars) {
if (process.env[envVar]) return false;
}
return true;
}
/**
* Formats transfer statistics (size and speed).
*/
function formatTransferStats(bytes: number, elapsedSeconds: number): string {
const sizeMB = (bytes / (1024 * 1024)).toFixed(2);
const speedMBps = (bytes / (1024 * 1024) / elapsedSeconds).toFixed(2);
return `${sizeMB} MB (${speedMBps} MB/s)`;
}
class TimerLogger {
private isInteractive: boolean;
private startTime: number;
private lastLogTime: number;
private prefix: string;
constructor(prefix: string) {
this.isInteractive = isInteractiveTerminal();
this.startTime = Date.now();
this.lastLogTime = this.startTime;
this.prefix = prefix;
}
/**
* Logs timer progress at regular intervals (throttled to 1 second).
*/
progress(): this {
const now = Date.now();
if (now - this.lastLogTime >= 1000) {
const elapsed = (now - this.startTime) / 1000;
const message = `${this.prefix} (${elapsed.toFixed(0)}s)...`;
if (this.isInteractive) process.stdout.write(`\r${message}`);
else console.log(message);
this.lastLogTime = now;
}
return this;
}
/**
* Logs final timer completion message.
*/
complete(): void {
const elapsed = (Date.now() - this.startTime) / 1000;
const message = `${this.prefix} (${elapsed.toFixed(0)}s)`;
if (this.isInteractive) process.stdout.write(`\r\x1b[K${message}\n`);
else console.log(message);
}
}
class TransferLogger {
private isInteractive: boolean;
private startTime: number;
private lastLogTime: number;
private prefix: string;
constructor(prefix: string) {
this.isInteractive = isInteractiveTerminal();
this.startTime = Date.now();
this.lastLogTime = this.startTime;
this.prefix = prefix;
}
/**
* Logs transfer progress at regular intervals (throttled to 1 second).
*/
progress(bytes: number): this {
const now = Date.now();
if (now - this.lastLogTime >= 1000) {
const elapsed = (now - this.startTime) / 1000;
const message = `${this.prefix} ${formatTransferStats(bytes, elapsed)}...`;
if (this.isInteractive) process.stdout.write(`\r${message}`);
else console.log(message);
this.lastLogTime = now;
}
return this;
}
/**
* Logs final transfer completion message.
*/
complete(bytes: number): void {
if (bytes > 0) {
const elapsed = (Date.now() - this.startTime) / 1000;
const message = `${this.prefix} ${formatTransferStats(bytes, elapsed)}`;
if (this.isInteractive) process.stdout.write(`\r\x1b[K${message}\n`);
else console.log(message);
}
}
}
class Logger {
/**
* Creates a timer progress logger.
* Usage: log.timer('Uploading cache').progress().complete()
*/
timer(prefix: string): TimerLogger {
return new TimerLogger(prefix);
}
/**
* Creates a transfer progress logger.
* Usage: log.transfer('Received').progress(bytes).complete(bytes)
*/
transfer(prefix: string): TransferLogger {
return new TransferLogger(prefix);
}
}
export const log = new Logger();
|