create-dev-to 1.3.0 → 1.3.1
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.js +3 -3
- package/dist/installLogger.js +232 -0
- package/dist/outputParsers.js +237 -0
- package/dist/visualComponents.js +251 -0
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -11,10 +11,10 @@ import { InstallLogger } from "./installLogger.js";
|
|
|
11
11
|
import { displayInstallSummary } from "./visualComponents.js";
|
|
12
12
|
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
13
13
|
const __BUILD_INFO__ = {
|
|
14
|
-
commit: "
|
|
14
|
+
commit: "399e4ab",
|
|
15
15
|
branch: "main",
|
|
16
|
-
buildTime: "2026-01-10 14:
|
|
17
|
-
version: "1.3.
|
|
16
|
+
buildTime: "2026-01-10 14:27",
|
|
17
|
+
version: "1.3.1"
|
|
18
18
|
};
|
|
19
19
|
const FRAMEWORKS = [
|
|
20
20
|
{
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { InstallRenderer } from "./visualComponents.js";
|
|
5
|
+
import { createParser } from "./outputParsers.js";
|
|
6
|
+
class InstallLogger {
|
|
7
|
+
packageManager;
|
|
8
|
+
parser;
|
|
9
|
+
renderer;
|
|
10
|
+
stats;
|
|
11
|
+
phases;
|
|
12
|
+
renderScheduled = false;
|
|
13
|
+
lastRenderTime = 0;
|
|
14
|
+
RENDER_INTERVAL = 50;
|
|
15
|
+
smoothProgressTimer = null;
|
|
16
|
+
constructor(packageManager) {
|
|
17
|
+
this.packageManager = packageManager;
|
|
18
|
+
this.parser = createParser(packageManager);
|
|
19
|
+
this.renderer = new InstallRenderer();
|
|
20
|
+
this.stats = {
|
|
21
|
+
packagesAdded: 0,
|
|
22
|
+
packagesUpdated: 0,
|
|
23
|
+
packagesRemoved: 0,
|
|
24
|
+
packagesTotal: 0,
|
|
25
|
+
downloadSpeed: 0,
|
|
26
|
+
downloadedBytes: 0,
|
|
27
|
+
nodeModulesSize: 0,
|
|
28
|
+
warnings: []
|
|
29
|
+
};
|
|
30
|
+
this.phases = [
|
|
31
|
+
{ name: "resolving", progress: 0, active: true },
|
|
32
|
+
{ name: "downloading", progress: 0, active: false },
|
|
33
|
+
{ name: "installing", progress: 0, active: false }
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
this.stats.startTime = performance.now();
|
|
38
|
+
this.render();
|
|
39
|
+
this.startSmoothProgress();
|
|
40
|
+
}
|
|
41
|
+
processLine(line, stream) {
|
|
42
|
+
if (!line || line.trim() === "") return;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = this.parser.parse(line, stream);
|
|
45
|
+
this.handleParsedOutput(parsed);
|
|
46
|
+
this.scheduleRender();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (process.env.DEBUG) {
|
|
49
|
+
console.error("Parse error:", error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async finish(projectDir) {
|
|
54
|
+
this.stopSmoothProgress();
|
|
55
|
+
this.stats.endTime = performance.now();
|
|
56
|
+
this.stats.duration = this.stats.endTime - (this.stats.startTime || 0);
|
|
57
|
+
this.updatePhase("installing", 100);
|
|
58
|
+
this.render();
|
|
59
|
+
this.renderer.clear();
|
|
60
|
+
try {
|
|
61
|
+
this.stats.nodeModulesSize = await this.calculateNodeModulesSize(
|
|
62
|
+
projectDir
|
|
63
|
+
);
|
|
64
|
+
} catch {
|
|
65
|
+
this.stats.nodeModulesSize = 0;
|
|
66
|
+
}
|
|
67
|
+
return this.stats;
|
|
68
|
+
}
|
|
69
|
+
error() {
|
|
70
|
+
this.stopSmoothProgress();
|
|
71
|
+
this.renderer.clear();
|
|
72
|
+
}
|
|
73
|
+
startSmoothProgress() {
|
|
74
|
+
this.smoothProgressTimer = setInterval(() => {
|
|
75
|
+
const activePhase = this.phases.find((p) => p.active);
|
|
76
|
+
if (activePhase && activePhase.progress < 95) {
|
|
77
|
+
const increment = Math.max(0.5, (95 - activePhase.progress) / 20);
|
|
78
|
+
activePhase.progress = Math.min(95, activePhase.progress + increment);
|
|
79
|
+
this.render();
|
|
80
|
+
}
|
|
81
|
+
}, 200);
|
|
82
|
+
}
|
|
83
|
+
stopSmoothProgress() {
|
|
84
|
+
if (this.smoothProgressTimer) {
|
|
85
|
+
clearInterval(this.smoothProgressTimer);
|
|
86
|
+
this.smoothProgressTimer = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
handleParsedOutput(parsed) {
|
|
90
|
+
switch (parsed.type) {
|
|
91
|
+
case "phase_change":
|
|
92
|
+
if (parsed.phase) {
|
|
93
|
+
this.activatePhase(parsed.phase);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "progress":
|
|
97
|
+
if (parsed.phase && parsed.progress !== void 0) {
|
|
98
|
+
this.updatePhase(parsed.phase, parsed.progress);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case "package_info":
|
|
102
|
+
if (parsed.packageCount !== void 0) {
|
|
103
|
+
this.stats.packagesAdded = parsed.packageCount;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case "warning":
|
|
107
|
+
if (parsed.message) {
|
|
108
|
+
this.stats.warnings = this.stats.warnings || [];
|
|
109
|
+
this.stats.warnings.push(parsed.message);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case "error":
|
|
113
|
+
break;
|
|
114
|
+
case "ignore":
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
activatePhase(phaseName) {
|
|
119
|
+
const phase = this.phases.find((p) => p.name === phaseName);
|
|
120
|
+
if (phase) {
|
|
121
|
+
if (phase.progress >= 100) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.phases.forEach((p) => p.active = false);
|
|
125
|
+
phase.active = true;
|
|
126
|
+
if (phase.progress === 0) {
|
|
127
|
+
phase.progress = 1;
|
|
128
|
+
}
|
|
129
|
+
const phaseIndex = this.phases.findIndex((p) => p.name === phaseName);
|
|
130
|
+
for (let i = 0; i < phaseIndex; i++) {
|
|
131
|
+
const prevPhase = this.phases[i];
|
|
132
|
+
if (prevPhase.progress < 100) {
|
|
133
|
+
prevPhase.progress = 100;
|
|
134
|
+
}
|
|
135
|
+
prevPhase.active = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
updatePhase(phaseName, progress) {
|
|
140
|
+
const phase = this.phases.find((p) => p.name === phaseName);
|
|
141
|
+
if (phase) {
|
|
142
|
+
phase.progress = Math.max(phase.progress, Math.min(100, progress));
|
|
143
|
+
const phaseIndex = this.phases.findIndex((p) => p.name === phaseName);
|
|
144
|
+
for (let i = 0; i < phaseIndex; i++) {
|
|
145
|
+
if (this.phases[i].progress < 100) {
|
|
146
|
+
this.phases[i].progress = 100;
|
|
147
|
+
this.phases[i].active = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (phase.progress < 100) {
|
|
151
|
+
phase.active = true;
|
|
152
|
+
}
|
|
153
|
+
if (progress >= 100) {
|
|
154
|
+
phase.active = false;
|
|
155
|
+
if (phaseIndex < this.phases.length - 1) {
|
|
156
|
+
const nextPhase = this.phases[phaseIndex + 1];
|
|
157
|
+
if (nextPhase && nextPhase.progress === 0) {
|
|
158
|
+
nextPhase.active = true;
|
|
159
|
+
nextPhase.progress = 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
scheduleRender() {
|
|
166
|
+
if (this.renderScheduled) return;
|
|
167
|
+
const now = performance.now();
|
|
168
|
+
const timeSinceLastRender = now - this.lastRenderTime;
|
|
169
|
+
if (timeSinceLastRender >= this.RENDER_INTERVAL) {
|
|
170
|
+
this.render();
|
|
171
|
+
this.lastRenderTime = now;
|
|
172
|
+
} else {
|
|
173
|
+
this.renderScheduled = true;
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
this.render();
|
|
176
|
+
this.renderScheduled = false;
|
|
177
|
+
this.lastRenderTime = performance.now();
|
|
178
|
+
}, this.RENDER_INTERVAL - timeSinceLastRender);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
render() {
|
|
182
|
+
try {
|
|
183
|
+
this.renderer.render(this.phases, this.stats);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (process.env.DEBUG) {
|
|
186
|
+
console.error("Render error:", error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async calculateNodeModulesSize(projectDir) {
|
|
191
|
+
const nodeModulesPath = path.join(projectDir, "node_modules");
|
|
192
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
if (process.platform !== "win32") {
|
|
196
|
+
try {
|
|
197
|
+
const result = execSync(`du -sk "${nodeModulesPath}"`, {
|
|
198
|
+
encoding: "utf-8",
|
|
199
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
200
|
+
});
|
|
201
|
+
const sizeKB = parseInt(result.split(" ")[0]);
|
|
202
|
+
return sizeKB * 1024;
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return this.getDirectorySize(nodeModulesPath);
|
|
207
|
+
}
|
|
208
|
+
getDirectorySize(dir) {
|
|
209
|
+
let size = 0;
|
|
210
|
+
try {
|
|
211
|
+
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
const filePath = path.join(dir, file.name);
|
|
214
|
+
try {
|
|
215
|
+
if (file.isDirectory()) {
|
|
216
|
+
size += this.getDirectorySize(filePath);
|
|
217
|
+
} else {
|
|
218
|
+
const stats = fs.statSync(filePath);
|
|
219
|
+
size += stats.size;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
return size;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
InstallLogger
|
|
232
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
class OutputParser {
|
|
2
|
+
}
|
|
3
|
+
class PnpmParser extends OutputParser {
|
|
4
|
+
totalPackages = 0;
|
|
5
|
+
maxResolved = 0;
|
|
6
|
+
lastPhase = "resolving";
|
|
7
|
+
parse(line, stream) {
|
|
8
|
+
if (line.includes("Lockfile is up to date") || line.includes("Already up to date")) {
|
|
9
|
+
return {
|
|
10
|
+
type: "progress",
|
|
11
|
+
phase: "installing",
|
|
12
|
+
progress: 100,
|
|
13
|
+
packageCount: 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const progressMatch = line.match(
|
|
17
|
+
/Progress: resolved (\d+)(?:, reused (\d+))?(?:, downloaded (\d+))?(?:, added (\d+))?/
|
|
18
|
+
);
|
|
19
|
+
if (progressMatch) {
|
|
20
|
+
const resolved = parseInt(progressMatch[1]);
|
|
21
|
+
const reused = parseInt(progressMatch[2] || "0");
|
|
22
|
+
const downloaded = parseInt(progressMatch[3] || "0");
|
|
23
|
+
const added = parseInt(progressMatch[4] || "0");
|
|
24
|
+
this.maxResolved = Math.max(this.maxResolved, resolved);
|
|
25
|
+
if (added > 0 && added >= resolved) {
|
|
26
|
+
this.lastPhase = "installing";
|
|
27
|
+
return {
|
|
28
|
+
type: "progress",
|
|
29
|
+
phase: "installing",
|
|
30
|
+
packageCount: added,
|
|
31
|
+
progress: 100
|
|
32
|
+
};
|
|
33
|
+
} else if (added > 0) {
|
|
34
|
+
this.lastPhase = "installing";
|
|
35
|
+
const installProgress = Math.min(99, Math.max(10, added / resolved * 100));
|
|
36
|
+
return {
|
|
37
|
+
type: "progress",
|
|
38
|
+
phase: "installing",
|
|
39
|
+
progress: installProgress
|
|
40
|
+
};
|
|
41
|
+
} else if (downloaded > 0 || reused > 0) {
|
|
42
|
+
const total = downloaded + reused;
|
|
43
|
+
if (this.lastPhase !== "downloading") {
|
|
44
|
+
this.lastPhase = "downloading";
|
|
45
|
+
}
|
|
46
|
+
const downloadProgress = Math.min(99, Math.max(10, total / resolved * 100));
|
|
47
|
+
return {
|
|
48
|
+
type: "progress",
|
|
49
|
+
phase: "downloading",
|
|
50
|
+
progress: downloadProgress
|
|
51
|
+
};
|
|
52
|
+
} else if (resolved > 0) {
|
|
53
|
+
if (this.lastPhase !== "resolving") {
|
|
54
|
+
this.lastPhase = "resolving";
|
|
55
|
+
}
|
|
56
|
+
const resolveProgress = Math.min(95, Math.max(10, resolved / Math.max(this.maxResolved, 200) * 100));
|
|
57
|
+
return {
|
|
58
|
+
type: "progress",
|
|
59
|
+
phase: "resolving",
|
|
60
|
+
progress: resolveProgress
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const packagesMatch = line.match(/Packages:\s*\+(\d+)/);
|
|
65
|
+
if (packagesMatch) {
|
|
66
|
+
const count = parseInt(packagesMatch[1]);
|
|
67
|
+
this.totalPackages = count;
|
|
68
|
+
return {
|
|
69
|
+
type: "package_info",
|
|
70
|
+
packageCount: count
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (line.includes("warn") && stream === "stderr") {
|
|
74
|
+
return {
|
|
75
|
+
type: "warning",
|
|
76
|
+
message: line
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if ((line.includes("error") || line.includes("Error")) && stream === "stderr") {
|
|
80
|
+
return {
|
|
81
|
+
type: "error",
|
|
82
|
+
message: line
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { type: "ignore" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class NpmParser extends OutputParser {
|
|
89
|
+
isInstalling = false;
|
|
90
|
+
parse(line, stream) {
|
|
91
|
+
const completionMatch = line.match(
|
|
92
|
+
/added (\d+) packages?(?:, (?:and|)?(?:audited|updated) (\d+) packages)?/
|
|
93
|
+
);
|
|
94
|
+
if (completionMatch) {
|
|
95
|
+
this.isInstalling = true;
|
|
96
|
+
return {
|
|
97
|
+
type: "progress",
|
|
98
|
+
packageCount: parseInt(completionMatch[1]),
|
|
99
|
+
progress: 100,
|
|
100
|
+
phase: "installing"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (line.includes("up to date")) {
|
|
104
|
+
this.isInstalling = true;
|
|
105
|
+
return {
|
|
106
|
+
type: "progress",
|
|
107
|
+
packageCount: 0,
|
|
108
|
+
progress: 100,
|
|
109
|
+
phase: "installing"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const progressMatch = line.match(/\|(.+?)\|/);
|
|
113
|
+
if (progressMatch) {
|
|
114
|
+
const progress = progressMatch[1].length / 50 * 100;
|
|
115
|
+
return {
|
|
116
|
+
type: "progress",
|
|
117
|
+
progress: Math.min(95, Math.max(1, progress)),
|
|
118
|
+
phase: "downloading"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (line.includes("vulnerabilities") || line.includes("vulnerability")) {
|
|
122
|
+
return {
|
|
123
|
+
type: "warning",
|
|
124
|
+
message: line
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (line.includes("ERR!") || line.includes("error") && stream === "stderr") {
|
|
128
|
+
return {
|
|
129
|
+
type: "error",
|
|
130
|
+
message: line
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { type: "ignore" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
class YarnParser extends OutputParser {
|
|
137
|
+
parse(line, stream) {
|
|
138
|
+
const phaseMatch = line.match(/\[(\d+)\/4\]\s+(.+?)\.{3}/);
|
|
139
|
+
if (phaseMatch) {
|
|
140
|
+
const stepNum = parseInt(phaseMatch[1]);
|
|
141
|
+
const phaseNames = [
|
|
142
|
+
"resolving",
|
|
143
|
+
"fetching",
|
|
144
|
+
"linking",
|
|
145
|
+
"building"
|
|
146
|
+
];
|
|
147
|
+
const phase = phaseNames[stepNum - 1] || "resolving";
|
|
148
|
+
const mappedPhase = phase === "fetching" ? "downloading" : phase;
|
|
149
|
+
return {
|
|
150
|
+
type: "phase_change",
|
|
151
|
+
phase: mappedPhase
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const successMatch = line.match(/Done in ([\d.]+)s/);
|
|
155
|
+
if (successMatch) {
|
|
156
|
+
return {
|
|
157
|
+
type: "progress",
|
|
158
|
+
progress: 100,
|
|
159
|
+
phase: "installing"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const addedMatch = line.match(/(?:Yarn )?added (\d+) packages?/);
|
|
163
|
+
if (addedMatch) {
|
|
164
|
+
return {
|
|
165
|
+
type: "package_info",
|
|
166
|
+
packageCount: parseInt(addedMatch[1])
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (line.includes("warning") && stream === "stderr") {
|
|
170
|
+
return {
|
|
171
|
+
type: "warning",
|
|
172
|
+
message: line
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (line.includes("error") && stream === "stderr") {
|
|
176
|
+
return {
|
|
177
|
+
type: "error",
|
|
178
|
+
message: line
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { type: "ignore" };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
class BunParser extends OutputParser {
|
|
185
|
+
parse(line, stream) {
|
|
186
|
+
const completionMatch = line.match(/^\s*\+\s*(\d+)\s+packages installed\s*\[([\d.]+)s\]/);
|
|
187
|
+
if (completionMatch) {
|
|
188
|
+
return {
|
|
189
|
+
type: "progress",
|
|
190
|
+
packageCount: parseInt(completionMatch[1]),
|
|
191
|
+
progress: 100,
|
|
192
|
+
phase: "installing"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const alreadyMatch = line.match(/^\s*(\d+)\s+packages already installed/);
|
|
196
|
+
if (alreadyMatch) {
|
|
197
|
+
return {
|
|
198
|
+
type: "progress",
|
|
199
|
+
packageCount: 0,
|
|
200
|
+
progress: 100,
|
|
201
|
+
phase: "installing"
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (line.includes("Downloading") || line.includes("downloading")) {
|
|
205
|
+
return { type: "phase_change", phase: "downloading" };
|
|
206
|
+
}
|
|
207
|
+
if (line.includes("error") || line.includes("Error") || line.includes("failed") && stream === "stderr") {
|
|
208
|
+
return {
|
|
209
|
+
type: "error",
|
|
210
|
+
message: line
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return { type: "ignore" };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function createParser(pm) {
|
|
217
|
+
switch (pm) {
|
|
218
|
+
case "pnpm":
|
|
219
|
+
return new PnpmParser();
|
|
220
|
+
case "npm":
|
|
221
|
+
return new NpmParser();
|
|
222
|
+
case "yarn":
|
|
223
|
+
return new YarnParser();
|
|
224
|
+
case "bun":
|
|
225
|
+
return new BunParser();
|
|
226
|
+
default:
|
|
227
|
+
return new NpmParser();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
BunParser,
|
|
232
|
+
NpmParser,
|
|
233
|
+
OutputParser,
|
|
234
|
+
PnpmParser,
|
|
235
|
+
YarnParser,
|
|
236
|
+
createParser
|
|
237
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { green, yellow, red, dim, bold } from "kolorist";
|
|
2
|
+
const ANSI = {
|
|
3
|
+
cursorUp: (n) => `\x1B[${n}A`,
|
|
4
|
+
cursorDown: (n) => `\x1B[${n}B`,
|
|
5
|
+
cursorTo: (x, y) => y !== void 0 ? `\x1B[${y};${x}H` : `\x1B[${x}G`,
|
|
6
|
+
clearLine: () => "\x1B[2K",
|
|
7
|
+
clearScreen: () => "\x1B[2J",
|
|
8
|
+
hideCursor: () => "\x1B[?25l",
|
|
9
|
+
showCursor: () => "\x1B[?25h"
|
|
10
|
+
};
|
|
11
|
+
function trueColor(r, g, b, text) {
|
|
12
|
+
return `\x1B[38;2;${r};${g};${b}m${text}\x1B[0m`;
|
|
13
|
+
}
|
|
14
|
+
function createGradient(text, startColor, endColor) {
|
|
15
|
+
const chars = text.split("");
|
|
16
|
+
const length = chars.length;
|
|
17
|
+
if (length === 0) return text;
|
|
18
|
+
if (length === 1) return trueColor(startColor.r, startColor.g, startColor.b, text);
|
|
19
|
+
let result = "";
|
|
20
|
+
for (let i = 0; i < length; i++) {
|
|
21
|
+
const ratio = i / (length - 1);
|
|
22
|
+
const r = Math.round(startColor.r + (endColor.r - startColor.r) * ratio);
|
|
23
|
+
const g = Math.round(startColor.g + (endColor.g - startColor.g) * ratio);
|
|
24
|
+
const b = Math.round(startColor.b + (endColor.b - startColor.b) * ratio);
|
|
25
|
+
result += trueColor(r, g, b, chars[i]);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function renderProgressBar(options) {
|
|
30
|
+
const { width, progress, label, gradient, colors, showPercentage } = options;
|
|
31
|
+
const filledWidth = Math.floor(width * progress / 100);
|
|
32
|
+
const emptyWidth = width - filledWidth;
|
|
33
|
+
let bar = "";
|
|
34
|
+
if (gradient && colors && colors.length > 0) {
|
|
35
|
+
for (let i = 0; i < filledWidth; i++) {
|
|
36
|
+
const ratio = filledWidth > 1 ? i / (filledWidth - 1) : 0;
|
|
37
|
+
const colorIndex = Math.min(
|
|
38
|
+
Math.floor(ratio * (colors.length - 1)),
|
|
39
|
+
colors.length - 1
|
|
40
|
+
);
|
|
41
|
+
const nextColorIndex = Math.min(colorIndex + 1, colors.length - 1);
|
|
42
|
+
const color = colors[colorIndex];
|
|
43
|
+
const nextColor = colors[nextColorIndex];
|
|
44
|
+
const localRatio = ratio * (colors.length - 1) - Math.floor(ratio * (colors.length - 1));
|
|
45
|
+
const r = Math.round(color.r + (nextColor.r - color.r) * localRatio);
|
|
46
|
+
const g = Math.round(color.g + (nextColor.g - color.g) * localRatio);
|
|
47
|
+
const b = Math.round(color.b + (nextColor.b - color.b) * localRatio);
|
|
48
|
+
bar += trueColor(r, g, b, "\u2593");
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
bar = "\u2593".repeat(filledWidth);
|
|
52
|
+
}
|
|
53
|
+
bar += dim("\u2591".repeat(emptyWidth));
|
|
54
|
+
const percentage = showPercentage ? ` ${Math.round(progress)}%` : "";
|
|
55
|
+
return `${bar} ${label}${percentage}`;
|
|
56
|
+
}
|
|
57
|
+
function getPhaseIcon(phase) {
|
|
58
|
+
const icons = {
|
|
59
|
+
resolving: "\u{1F50D}",
|
|
60
|
+
downloading: "\u2B07\uFE0F",
|
|
61
|
+
installing: "\u{1F4E6}",
|
|
62
|
+
building: "\u{1F528}"
|
|
63
|
+
};
|
|
64
|
+
return icons[phase] || "\u25CF";
|
|
65
|
+
}
|
|
66
|
+
function getPhaseColors(phase) {
|
|
67
|
+
const colorSets = {
|
|
68
|
+
resolving: [
|
|
69
|
+
{ r: 59, g: 130, b: 246 },
|
|
70
|
+
{ r: 37, g: 99, b: 235 }
|
|
71
|
+
],
|
|
72
|
+
downloading: [
|
|
73
|
+
{ r: 34, g: 197, b: 94 },
|
|
74
|
+
{ r: 22, g: 163, b: 74 }
|
|
75
|
+
],
|
|
76
|
+
installing: [
|
|
77
|
+
{ r: 168, g: 85, b: 247 },
|
|
78
|
+
{ r: 147, g: 51, b: 234 }
|
|
79
|
+
],
|
|
80
|
+
building: [
|
|
81
|
+
{ r: 251, g: 146, b: 60 },
|
|
82
|
+
{ r: 249, g: 115, b: 22 }
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
return colorSets[phase] || [{ r: 156, g: 163, b: 175 }];
|
|
86
|
+
}
|
|
87
|
+
function formatSpeed(bytesPerSecond) {
|
|
88
|
+
if (bytesPerSecond < 1024) return `${bytesPerSecond.toFixed(0)} B/s`;
|
|
89
|
+
if (bytesPerSecond < 1024 * 1024)
|
|
90
|
+
return `${(bytesPerSecond / 1024).toFixed(1)} KB/s`;
|
|
91
|
+
return `${(bytesPerSecond / 1024 / 1024).toFixed(1)} MB/s`;
|
|
92
|
+
}
|
|
93
|
+
function formatSize(bytes) {
|
|
94
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
95
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
96
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
97
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
98
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
99
|
+
}
|
|
100
|
+
function formatDuration(ms) {
|
|
101
|
+
const seconds = ms / 1e3;
|
|
102
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
103
|
+
const minutes = Math.floor(seconds / 60);
|
|
104
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
105
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
106
|
+
}
|
|
107
|
+
function detectColorSupport() {
|
|
108
|
+
if (process.env.NO_COLOR || process.env.TERM === "dumb") {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
class InstallRenderer {
|
|
114
|
+
isTTY;
|
|
115
|
+
supportsColor;
|
|
116
|
+
lineCount = 0;
|
|
117
|
+
constructor() {
|
|
118
|
+
this.isTTY = process.stdout.isTTY ?? false;
|
|
119
|
+
this.supportsColor = detectColorSupport();
|
|
120
|
+
}
|
|
121
|
+
render(phases, stats) {
|
|
122
|
+
if (!this.isTTY) {
|
|
123
|
+
this.renderSimple(phases);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const lines = this.buildEnhancedUI(phases, stats);
|
|
127
|
+
this.updateTerminal(lines);
|
|
128
|
+
}
|
|
129
|
+
buildEnhancedUI(phases, stats) {
|
|
130
|
+
const lines = [];
|
|
131
|
+
lines.push("");
|
|
132
|
+
if (this.supportsColor) {
|
|
133
|
+
const title = createGradient(
|
|
134
|
+
"\u25C6 Installing Dependencies",
|
|
135
|
+
{ r: 99, g: 102, b: 241 },
|
|
136
|
+
{ r: 236, g: 72, b: 153 }
|
|
137
|
+
);
|
|
138
|
+
lines.push(bold(title));
|
|
139
|
+
} else {
|
|
140
|
+
lines.push(bold("\u25C6 Installing Dependencies"));
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
phases.forEach((phase) => {
|
|
144
|
+
const icon = getPhaseIcon(phase.name);
|
|
145
|
+
const bar = renderProgressBar({
|
|
146
|
+
width: 40,
|
|
147
|
+
progress: phase.progress,
|
|
148
|
+
label: this.getPhaseLabel(phase.name),
|
|
149
|
+
gradient: this.supportsColor,
|
|
150
|
+
colors: getPhaseColors(phase.name),
|
|
151
|
+
showPercentage: true
|
|
152
|
+
});
|
|
153
|
+
lines.push(`${icon} ${bar}`);
|
|
154
|
+
});
|
|
155
|
+
lines.push("");
|
|
156
|
+
const statsLine = this.buildStatsLine(stats);
|
|
157
|
+
if (statsLine) {
|
|
158
|
+
lines.push(dim(statsLine));
|
|
159
|
+
lines.push("");
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
getPhaseLabel(phase) {
|
|
164
|
+
const labels = {
|
|
165
|
+
resolving: "Resolving packages ",
|
|
166
|
+
downloading: "Downloading ",
|
|
167
|
+
installing: "Installing ",
|
|
168
|
+
building: "Building "
|
|
169
|
+
};
|
|
170
|
+
return labels[phase] || phase;
|
|
171
|
+
}
|
|
172
|
+
buildStatsLine(stats) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
if (stats.packagesAdded) {
|
|
175
|
+
parts.push(`Packages: ${green(`+${stats.packagesAdded}`)}`);
|
|
176
|
+
}
|
|
177
|
+
if (stats.packagesUpdated) {
|
|
178
|
+
parts.push(`Updated: ${yellow(stats.packagesUpdated.toString())}`);
|
|
179
|
+
}
|
|
180
|
+
if (stats.packagesRemoved) {
|
|
181
|
+
parts.push(`Removed: ${red(stats.packagesRemoved.toString())}`);
|
|
182
|
+
}
|
|
183
|
+
if (stats.startTime) {
|
|
184
|
+
const duration = performance.now() - stats.startTime;
|
|
185
|
+
parts.push(`Time: ${formatDuration(duration)}`);
|
|
186
|
+
}
|
|
187
|
+
if (stats.downloadSpeed && stats.downloadSpeed > 0) {
|
|
188
|
+
parts.push(`Speed: ${formatSpeed(stats.downloadSpeed)}`);
|
|
189
|
+
}
|
|
190
|
+
return parts.join(" ");
|
|
191
|
+
}
|
|
192
|
+
updateTerminal(lines) {
|
|
193
|
+
if (this.lineCount > 0) {
|
|
194
|
+
for (let i = 0; i < this.lineCount; i++) {
|
|
195
|
+
process.stdout.write(ANSI.cursorUp(1) + ANSI.clearLine());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
lines.forEach((line) => process.stdout.write(line + "\n"));
|
|
199
|
+
this.lineCount = lines.length;
|
|
200
|
+
}
|
|
201
|
+
renderSimple(phases) {
|
|
202
|
+
const activePhase = phases.find((p) => p.active);
|
|
203
|
+
if (activePhase) {
|
|
204
|
+
const label = this.getPhaseLabel(activePhase.name).trim();
|
|
205
|
+
console.log(
|
|
206
|
+
`[${label.toUpperCase()}] ${Math.round(activePhase.progress)}%`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
clear() {
|
|
211
|
+
if (this.isTTY && this.lineCount > 0) {
|
|
212
|
+
for (let i = 0; i < this.lineCount; i++) {
|
|
213
|
+
process.stdout.write(ANSI.cursorUp(1) + ANSI.clearLine());
|
|
214
|
+
}
|
|
215
|
+
this.lineCount = 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function displayInstallSummary(stats) {
|
|
220
|
+
const lines = [];
|
|
221
|
+
lines.push("");
|
|
222
|
+
lines.push(green("\u2728 Installation Complete!"));
|
|
223
|
+
lines.push("");
|
|
224
|
+
const pkgStats = [];
|
|
225
|
+
if (stats.packagesAdded > 0)
|
|
226
|
+
pkgStats.push(`${green(`+${stats.packagesAdded}`)} added`);
|
|
227
|
+
if (stats.packagesUpdated > 0)
|
|
228
|
+
pkgStats.push(`${yellow(`~${stats.packagesUpdated}`)} updated`);
|
|
229
|
+
if (stats.packagesRemoved > 0)
|
|
230
|
+
pkgStats.push(`${red(`-${stats.packagesRemoved}`)} removed`);
|
|
231
|
+
if (pkgStats.length > 0) {
|
|
232
|
+
lines.push(` ${bold("Packages:")} ${pkgStats.join(", ")}`);
|
|
233
|
+
}
|
|
234
|
+
lines.push(` ${bold("Duration:")} ${formatDuration(stats.duration)}`);
|
|
235
|
+
if (stats.downloadSpeed > 0) {
|
|
236
|
+
lines.push(` ${bold("Avg Speed:")} ${formatSpeed(stats.downloadSpeed)}`);
|
|
237
|
+
}
|
|
238
|
+
if (stats.nodeModulesSize > 0) {
|
|
239
|
+
lines.push(` ${bold("Disk Usage:")} ${formatSize(stats.nodeModulesSize)}`);
|
|
240
|
+
}
|
|
241
|
+
if (stats.warnings.length > 0) {
|
|
242
|
+
lines.push("");
|
|
243
|
+
lines.push(yellow(` \u26A0\uFE0F ${stats.warnings.length} warning(s)`));
|
|
244
|
+
}
|
|
245
|
+
lines.push("");
|
|
246
|
+
lines.forEach((line) => console.log(line));
|
|
247
|
+
}
|
|
248
|
+
export {
|
|
249
|
+
InstallRenderer,
|
|
250
|
+
displayInstallSummary
|
|
251
|
+
};
|