@configjs/cli 1.1.16 → 1.1.18
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/{angular-command-XN26G6L3.js → angular-command-EOREU45Q.js} +8 -8
- package/dist/{angular-installer-FY43HE72.js → angular-installer-TKZDPFLD.js} +9 -1
- package/dist/angular-setup-QDTWXOB4.js +30 -0
- package/dist/check-KAPRT4FM.js +168 -0
- package/dist/{chunk-JYWGJJ4M.js → chunk-D7IWYKUX.js} +476 -28
- package/dist/chunk-EDCNW4UO.js +92 -0
- package/dist/{chunk-TN27AX4L.js → chunk-FJLN62L4.js} +797 -18
- package/dist/{chunk-FIB2J36N.js → chunk-HI7RYD6W.js} +161 -36
- package/dist/{chunk-NYCK4R4K.js → chunk-RIJNUJDC.js} +361 -96
- package/dist/chunk-V2IBYLVH.js +932 -0
- package/dist/chunk-VN4XTFDK.js +315 -0
- package/dist/{chunk-UKEHW2LH.js → chunk-Y4XYC7QV.js} +17 -3
- package/dist/cli.js +31 -21
- package/dist/{installed-D6CUYQM5.js → installed-QMJZIZNC.js} +4 -4
- package/dist/{list-VZDUWV5O.js → list-5T6VDDAO.js} +4 -4
- package/dist/{nextjs-command-WKKOAY7I.js → nextjs-command-C6PM7A5C.js} +8 -9
- package/dist/{nextjs-installer-2ZC5IWJ6.js → nextjs-installer-OFY5BQC4.js} +9 -2
- package/dist/{nextjs-setup-DYOFF72S.js → nextjs-setup-JIKPIJCX.js} +21 -9
- package/dist/{react-command-2T6IOTHB.js → react-command-JMK6VM4Q.js} +8 -9
- package/dist/{remove-ZY3MLPGN.js → remove-4ZNQR6ZR.js} +4 -4
- package/dist/{svelte-command-B2DNNQ5Z.js → svelte-command-YUSD55NO.js} +8 -8
- package/dist/svelte-installer-UP3KDZSY.js +105 -0
- package/dist/{svelte-setup-FWXLXJAC.js → svelte-setup-33E46IBT.js} +16 -5
- package/dist/{vite-installer-Y6VMFHIM.js → vite-installer-EE2LE76G.js} +9 -2
- package/dist/{vite-setup-JRELX6K2.js → vite-setup-VO5BOI46.js} +16 -4
- package/dist/{vue-command-IOTC32AI.js → vue-command-3CYWLLFQ.js} +8 -9
- package/dist/{vue-installer-DGBBVF6F.js → vue-installer-LEGLVD77.js} +9 -2
- package/dist/{vue-setup-G44DLT2U.js → vue-setup-FK5QT7AY.js} +16 -4
- package/package.json +12 -4
- package/dist/angular-setup-Z6TCKNBG.js +0 -18
- package/dist/check-KNGZSCMM.js +0 -131
- package/dist/chunk-6GV4NKUX.js +0 -122
- package/dist/chunk-QPEUT7QG.js +0 -157
- package/dist/svelte-installer-EOSC3EP3.js +0 -65
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getModuleLogger
|
|
3
|
+
} from "./chunk-VN4XTFDK.js";
|
|
4
|
+
|
|
5
|
+
// src/utils/package-manager.ts
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import { resolve, join } from "path";
|
|
9
|
+
|
|
10
|
+
// src/core/package-validator.ts
|
|
11
|
+
var PACKAGE_NAME_REGEX = /^(@[a-z0-9]([a-z0-9._-]*[a-z0-9])?\/)?[a-z0-9]([a-z0-9._-]*[a-z0-9])?(@[~^>=<*\d.+-]+)?$/i;
|
|
12
|
+
var FORBIDDEN_FLAGS = /* @__PURE__ */ new Set([
|
|
13
|
+
"--registry",
|
|
14
|
+
"--proxy",
|
|
15
|
+
"--https-proxy",
|
|
16
|
+
"--ca",
|
|
17
|
+
"--cafile",
|
|
18
|
+
"--cert",
|
|
19
|
+
"--key",
|
|
20
|
+
"--strict-ssl",
|
|
21
|
+
"--save",
|
|
22
|
+
"--save-dev",
|
|
23
|
+
"--save-optional",
|
|
24
|
+
"--save-exact",
|
|
25
|
+
"--save-bundle",
|
|
26
|
+
"--no-save",
|
|
27
|
+
"--no-save-dev",
|
|
28
|
+
"--no-save-optional",
|
|
29
|
+
"--force",
|
|
30
|
+
"--no-optional",
|
|
31
|
+
"--prefer-offline",
|
|
32
|
+
"--no-audit",
|
|
33
|
+
"--no-fund"
|
|
34
|
+
]);
|
|
35
|
+
function validatePackageName(name) {
|
|
36
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (name.startsWith("--")) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (/[;&|`$()[\]{}<>\\]/.test(name)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (FORBIDDEN_FLAGS.has(name.toLowerCase())) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return PACKAGE_NAME_REGEX.test(name);
|
|
49
|
+
}
|
|
50
|
+
function validatePackageNames(names) {
|
|
51
|
+
return names.filter((name) => !validatePackageName(name));
|
|
52
|
+
}
|
|
53
|
+
function getPackageValidationErrorMessage(names) {
|
|
54
|
+
if (names.length === 0) {
|
|
55
|
+
return "No invalid package names";
|
|
56
|
+
}
|
|
57
|
+
const list = names.slice(0, 5).join(", ");
|
|
58
|
+
const suffix = names.length > 5 ? ` and ${names.length - 5} more` : "";
|
|
59
|
+
return `Invalid package name${names.length > 1 ? "s" : ""}: ${list}${suffix}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/rate-limiter.ts
|
|
63
|
+
var RateLimiter = class {
|
|
64
|
+
perUserBuckets = /* @__PURE__ */ new Map();
|
|
65
|
+
globalBucket;
|
|
66
|
+
perUserLimit;
|
|
67
|
+
globalLimit;
|
|
68
|
+
refillInterval;
|
|
69
|
+
burstSize;
|
|
70
|
+
cooldownDuration;
|
|
71
|
+
constructor(config = {}) {
|
|
72
|
+
this.perUserLimit = config.perUserLimit ?? 1;
|
|
73
|
+
this.globalLimit = config.globalLimit ?? 10;
|
|
74
|
+
this.refillInterval = config.refillInterval ?? 1e3;
|
|
75
|
+
this.burstSize = config.burstSize ?? 3;
|
|
76
|
+
this.cooldownDuration = config.cooldownDuration ?? 5e3;
|
|
77
|
+
this.globalBucket = {
|
|
78
|
+
tokens: this.globalLimit * this.burstSize,
|
|
79
|
+
lastRefillTime: Date.now()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if a request is allowed for a specific user
|
|
84
|
+
* @param userId - Unique identifier for the user/client
|
|
85
|
+
* @returns RateLimitResult with status and timing info
|
|
86
|
+
*/
|
|
87
|
+
checkUserLimit(userId) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (!this.perUserBuckets.has(userId)) {
|
|
90
|
+
this.perUserBuckets.set(userId, {
|
|
91
|
+
tokens: this.perUserLimit * this.burstSize,
|
|
92
|
+
lastRefillTime: now
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const userBucket = this.perUserBuckets.get(userId);
|
|
96
|
+
if (!userBucket) {
|
|
97
|
+
throw new Error(`Failed to get or create user bucket for ${userId}`);
|
|
98
|
+
}
|
|
99
|
+
const elapsedMs = now - userBucket.lastRefillTime;
|
|
100
|
+
const tokensToAdd = elapsedMs / this.refillInterval * this.perUserLimit;
|
|
101
|
+
userBucket.tokens = Math.min(
|
|
102
|
+
this.perUserLimit * this.burstSize,
|
|
103
|
+
userBucket.tokens + tokensToAdd
|
|
104
|
+
);
|
|
105
|
+
userBucket.lastRefillTime = now;
|
|
106
|
+
if (userBucket.tokens >= 1) {
|
|
107
|
+
userBucket.tokens -= 1;
|
|
108
|
+
const remainingTokens = Math.floor(userBucket.tokens);
|
|
109
|
+
return {
|
|
110
|
+
allowed: true,
|
|
111
|
+
remainingTokens,
|
|
112
|
+
resetTime: now + this.refillInterval,
|
|
113
|
+
cooldownMs: 0
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const timeToNextToken = this.refillInterval;
|
|
117
|
+
return {
|
|
118
|
+
allowed: false,
|
|
119
|
+
remainingTokens: 0,
|
|
120
|
+
resetTime: now + timeToNextToken,
|
|
121
|
+
cooldownMs: this.cooldownDuration,
|
|
122
|
+
message: `Rate limit exceeded. Please wait ${(this.cooldownDuration / 1e3).toFixed(1)}s before retrying.`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check if global rate limit is exceeded
|
|
127
|
+
* @returns RateLimitResult with status and timing info
|
|
128
|
+
*/
|
|
129
|
+
checkGlobalLimit() {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const elapsedMs = now - this.globalBucket.lastRefillTime;
|
|
132
|
+
const tokensToAdd = elapsedMs / this.refillInterval * this.globalLimit;
|
|
133
|
+
this.globalBucket.tokens = Math.min(
|
|
134
|
+
this.globalLimit * this.burstSize,
|
|
135
|
+
this.globalBucket.tokens + tokensToAdd
|
|
136
|
+
);
|
|
137
|
+
this.globalBucket.lastRefillTime = now;
|
|
138
|
+
if (this.globalBucket.tokens >= 1) {
|
|
139
|
+
this.globalBucket.tokens -= 1;
|
|
140
|
+
const remainingTokens = Math.floor(this.globalBucket.tokens);
|
|
141
|
+
return {
|
|
142
|
+
allowed: true,
|
|
143
|
+
remainingTokens,
|
|
144
|
+
resetTime: now + this.refillInterval,
|
|
145
|
+
cooldownMs: 0
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const timeToNextToken = this.refillInterval;
|
|
149
|
+
return {
|
|
150
|
+
allowed: false,
|
|
151
|
+
remainingTokens: 0,
|
|
152
|
+
resetTime: now + timeToNextToken,
|
|
153
|
+
cooldownMs: this.cooldownDuration,
|
|
154
|
+
message: `Global rate limit exceeded. Service is temporarily throttled. Please wait ${(this.cooldownDuration / 1e3).toFixed(1)}s.`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check both user and global rate limits
|
|
159
|
+
* @param userId - Unique identifier for the user/client
|
|
160
|
+
* @returns RateLimitResult - will be denied if EITHER limit is exceeded
|
|
161
|
+
*/
|
|
162
|
+
checkRequest(userId) {
|
|
163
|
+
const userResult = this.checkUserLimit(userId);
|
|
164
|
+
if (!userResult.allowed) {
|
|
165
|
+
return userResult;
|
|
166
|
+
}
|
|
167
|
+
const globalResult = this.checkGlobalLimit();
|
|
168
|
+
if (!globalResult.allowed) {
|
|
169
|
+
return globalResult;
|
|
170
|
+
}
|
|
171
|
+
const userBucketTokens = this.perUserBuckets.get(userId)?.tokens ?? 0;
|
|
172
|
+
return {
|
|
173
|
+
allowed: true,
|
|
174
|
+
remainingTokens: Math.min(
|
|
175
|
+
Math.floor(userBucketTokens),
|
|
176
|
+
Math.floor(this.globalBucket.tokens)
|
|
177
|
+
),
|
|
178
|
+
resetTime: Math.min(userResult.resetTime, globalResult.resetTime),
|
|
179
|
+
cooldownMs: 0
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Middleware for Express/similar frameworks
|
|
184
|
+
* Adds rate limiting headers to response
|
|
185
|
+
* @param userId - Unique identifier for the user/client
|
|
186
|
+
* @returns Headers to add to HTTP response
|
|
187
|
+
*/
|
|
188
|
+
getHeaders(userId) {
|
|
189
|
+
const result = this.checkUserLimit(userId);
|
|
190
|
+
return {
|
|
191
|
+
"X-RateLimit-Limit": String(this.perUserLimit),
|
|
192
|
+
"X-RateLimit-Remaining": String(result.remainingTokens),
|
|
193
|
+
"X-RateLimit-Reset": String(result.resetTime)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
getStatus(userId) {
|
|
197
|
+
if (userId) {
|
|
198
|
+
const bucket = this.perUserBuckets.get(userId);
|
|
199
|
+
if (!bucket) {
|
|
200
|
+
return {
|
|
201
|
+
userId,
|
|
202
|
+
tokens: this.perUserLimit * this.burstSize,
|
|
203
|
+
lastRefillTime: Date.now()
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
userId,
|
|
208
|
+
tokens: bucket.tokens,
|
|
209
|
+
lastRefillTime: bucket.lastRefillTime
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
global: {
|
|
214
|
+
tokens: this.globalBucket.tokens,
|
|
215
|
+
lastRefillTime: this.globalBucket.lastRefillTime
|
|
216
|
+
},
|
|
217
|
+
users: Array.from(this.perUserBuckets.entries()).map(([id, bucket]) => ({
|
|
218
|
+
userId: id,
|
|
219
|
+
tokens: bucket.tokens,
|
|
220
|
+
lastRefillTime: bucket.lastRefillTime
|
|
221
|
+
}))
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Reset rate limiter (useful for testing)
|
|
226
|
+
*/
|
|
227
|
+
reset() {
|
|
228
|
+
this.perUserBuckets.clear();
|
|
229
|
+
this.globalBucket = {
|
|
230
|
+
tokens: this.globalLimit * this.burstSize,
|
|
231
|
+
lastRefillTime: Date.now()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Clean up old user buckets (older than 1 hour)
|
|
236
|
+
* Prevents memory leaks from abandoned sessions
|
|
237
|
+
*/
|
|
238
|
+
cleanup(maxAge = 36e5) {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const toDelete = [];
|
|
241
|
+
for (const [userId, bucket] of this.perUserBuckets.entries()) {
|
|
242
|
+
if (now - bucket.lastRefillTime > maxAge) {
|
|
243
|
+
toDelete.push(userId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const userId of toDelete) {
|
|
247
|
+
this.perUserBuckets.delete(userId);
|
|
248
|
+
}
|
|
249
|
+
return toDelete.length;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
var globalRateLimiter = null;
|
|
253
|
+
function getGlobalRateLimiter(config) {
|
|
254
|
+
if (!globalRateLimiter) {
|
|
255
|
+
globalRateLimiter = new RateLimiter(config);
|
|
256
|
+
}
|
|
257
|
+
return globalRateLimiter;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/core/timeout-manager.ts
|
|
261
|
+
var OPERATION_TIMEOUTS = {
|
|
262
|
+
/** Package installation maximum duration */
|
|
263
|
+
PACKAGE_INSTALL: 5 * 60 * 1e3,
|
|
264
|
+
// 5 minutes
|
|
265
|
+
/** Framework detection maximum duration */
|
|
266
|
+
FRAMEWORK_DETECT: 30 * 1e3,
|
|
267
|
+
// 30 seconds
|
|
268
|
+
/** Plugin configuration maximum duration */
|
|
269
|
+
PLUGIN_CONFIG: 60 * 1e3,
|
|
270
|
+
// 1 minute
|
|
271
|
+
/** Input validation maximum duration */
|
|
272
|
+
VALIDATION: 30 * 1e3,
|
|
273
|
+
// 30 seconds
|
|
274
|
+
/** General command execution */
|
|
275
|
+
COMMAND_EXEC: 2 * 60 * 1e3
|
|
276
|
+
// 2 minutes
|
|
277
|
+
};
|
|
278
|
+
var RESOURCE_LIMITS = {
|
|
279
|
+
/** Maximum stdout/stderr buffer (10MB) */
|
|
280
|
+
MAX_BUFFER: 10 * 1024 * 1024,
|
|
281
|
+
/** Maximum number of concurrent operations */
|
|
282
|
+
MAX_CONCURRENT: 4,
|
|
283
|
+
/** Maximum child processes per operation */
|
|
284
|
+
MAX_CHILD_PROCESSES: 1
|
|
285
|
+
};
|
|
286
|
+
function createTimeout(duration, operation) {
|
|
287
|
+
return new Promise((_, reject) => {
|
|
288
|
+
const timer = setTimeout(() => {
|
|
289
|
+
const error = new Error(
|
|
290
|
+
`Operation "${operation}" exceeded timeout of ${duration}ms`
|
|
291
|
+
);
|
|
292
|
+
error.code = "TIMEOUT";
|
|
293
|
+
error.operation = operation;
|
|
294
|
+
error.duration = duration;
|
|
295
|
+
error.maxDuration = duration;
|
|
296
|
+
reject(error);
|
|
297
|
+
}, duration);
|
|
298
|
+
timer.unref?.();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async function withTimeout(promise, timeout, operation) {
|
|
302
|
+
return Promise.race([promise, createTimeout(timeout, operation)]);
|
|
303
|
+
}
|
|
304
|
+
function getTimeoutErrorMessage(error, operation) {
|
|
305
|
+
const suggestions = [];
|
|
306
|
+
if (operation.includes("install") || operation.includes("npm")) {
|
|
307
|
+
suggestions.push("\u2022 Check your internet connection");
|
|
308
|
+
suggestions.push(
|
|
309
|
+
"\u2022 Try increasing npm timeout: `npm config set fetch-timeout 60000`"
|
|
310
|
+
);
|
|
311
|
+
suggestions.push(
|
|
312
|
+
"\u2022 Use a different npm registry: `npm config set registry https://registry.npmjs.org/`"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (operation.includes("detect") || operation.includes("framework")) {
|
|
316
|
+
suggestions.push("\u2022 Your project may be very large");
|
|
317
|
+
suggestions.push("\u2022 Check for large node_modules or build artifacts");
|
|
318
|
+
}
|
|
319
|
+
const base = `${operation} timed out after ${error.duration}ms (max: ${error.maxDuration}ms)`;
|
|
320
|
+
const suffix = suggestions.length > 0 ? `
|
|
321
|
+
|
|
322
|
+
Suggestions:
|
|
323
|
+
${suggestions.join("\n")}` : "";
|
|
324
|
+
return base + suffix;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/core/integrity-checker.ts
|
|
328
|
+
import { createHash } from "crypto";
|
|
329
|
+
var IntegrityChecker = class {
|
|
330
|
+
/**
|
|
331
|
+
* Verify package-lock.json integrity
|
|
332
|
+
* Checks for:
|
|
333
|
+
* - Valid JSON structure
|
|
334
|
+
* - Required fields present
|
|
335
|
+
* - Integrity hashes present
|
|
336
|
+
* - No sign of tampering
|
|
337
|
+
*
|
|
338
|
+
* @param lockContent - Raw package-lock.json content
|
|
339
|
+
* @returns Validation result with errors/warnings
|
|
340
|
+
*/
|
|
341
|
+
static verifyLockFile(lockContent) {
|
|
342
|
+
const result = {
|
|
343
|
+
valid: true,
|
|
344
|
+
errors: [],
|
|
345
|
+
warnings: [],
|
|
346
|
+
checksVerified: 0,
|
|
347
|
+
checksSkipped: 0,
|
|
348
|
+
packageCount: 0
|
|
349
|
+
};
|
|
350
|
+
try {
|
|
351
|
+
const lockFile = JSON.parse(lockContent);
|
|
352
|
+
if (!lockFile || typeof lockFile !== "object") {
|
|
353
|
+
result.valid = false;
|
|
354
|
+
result.errors.push("Lock file is not a valid JSON object");
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
if (!lockFile["lockfileVersion"]) {
|
|
358
|
+
result.warnings.push("Missing lockfileVersion field");
|
|
359
|
+
}
|
|
360
|
+
if (!lockFile["dependencies"] && !lockFile["packages"]) {
|
|
361
|
+
result.valid = false;
|
|
362
|
+
result.errors.push("Lock file missing both dependencies and packages");
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
const packages = lockFile["packages"] || lockFile["dependencies"] || {};
|
|
366
|
+
let packageCount = 0;
|
|
367
|
+
for (const [name, pkg] of Object.entries(packages)) {
|
|
368
|
+
if (name === "") continue;
|
|
369
|
+
packageCount++;
|
|
370
|
+
const pkgData = pkg;
|
|
371
|
+
const integrity = pkgData["integrity"] ?? void 0;
|
|
372
|
+
const resolved = pkgData["resolved"] ?? void 0;
|
|
373
|
+
if (!integrity) {
|
|
374
|
+
if (resolved && this.isRegistryPackage(resolved)) {
|
|
375
|
+
result.errors.push(
|
|
376
|
+
`Package ${name} missing integrity hash (registry package)`
|
|
377
|
+
);
|
|
378
|
+
result.valid = false;
|
|
379
|
+
} else {
|
|
380
|
+
result.warnings.push(
|
|
381
|
+
`Package ${name} missing integrity hash (git/file)`
|
|
382
|
+
);
|
|
383
|
+
result.checksSkipped++;
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
if (!this.isValidIntegrityFormat(integrity)) {
|
|
387
|
+
result.errors.push(
|
|
388
|
+
`Package ${name} has invalid integrity format: ${integrity}`
|
|
389
|
+
);
|
|
390
|
+
result.valid = false;
|
|
391
|
+
} else {
|
|
392
|
+
result.checksVerified++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (packageCount === 0) {
|
|
397
|
+
result.warnings.push("Lock file contains no packages");
|
|
398
|
+
}
|
|
399
|
+
result.packageCount = packageCount;
|
|
400
|
+
return result;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
result.valid = false;
|
|
403
|
+
if (error instanceof SyntaxError) {
|
|
404
|
+
result.errors.push(`Invalid JSON in lock file: ${error.message}`);
|
|
405
|
+
} else {
|
|
406
|
+
result.errors.push(`Error parsing lock file: ${String(error)}`);
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Verify integrity hash of a package
|
|
413
|
+
* Compares provided hash with calculated hash
|
|
414
|
+
*
|
|
415
|
+
* @param content - Package content (tarball)
|
|
416
|
+
* @param integrityString - Expected integrity string (e.g., "sha512-ABC...")
|
|
417
|
+
* @returns true if integrity is valid
|
|
418
|
+
*/
|
|
419
|
+
static verifyPackageIntegrity(content, integrityString) {
|
|
420
|
+
try {
|
|
421
|
+
if (!content || !integrityString) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const parts = integrityString.split("-");
|
|
425
|
+
const algorithm = parts[0];
|
|
426
|
+
const expectedHash = parts[1];
|
|
427
|
+
if (!algorithm || !expectedHash) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const hash = createHash(algorithm);
|
|
431
|
+
hash.update(content);
|
|
432
|
+
const calculatedHash = hash.digest("base64");
|
|
433
|
+
return calculatedHash === expectedHash;
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Check if integrity format is valid
|
|
440
|
+
* Valid formats: sha512-ABC..., sha256-ABC..., sha1-ABC...
|
|
441
|
+
*
|
|
442
|
+
* @param integrityString - Integrity string to validate
|
|
443
|
+
* @returns true if format is valid
|
|
444
|
+
*/
|
|
445
|
+
static isValidIntegrityFormat(integrityString) {
|
|
446
|
+
if (!integrityString || typeof integrityString !== "string") {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const parts = integrityString.split("-");
|
|
450
|
+
const algorithm = parts[0];
|
|
451
|
+
const hash = parts[1];
|
|
452
|
+
if (!algorithm || !hash) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const validAlgorithms = ["sha512", "sha256", "sha1"];
|
|
456
|
+
if (!validAlgorithms.includes(algorithm)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
460
|
+
return base64Regex.test(hash);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Check if a package is from npm registry
|
|
464
|
+
* Registry packages have resolved URLs pointing to registry
|
|
465
|
+
*
|
|
466
|
+
* @param resolvedUrl - Resolved URL from lock file
|
|
467
|
+
* @returns true if package is from npm registry
|
|
468
|
+
*/
|
|
469
|
+
static isRegistryPackage(resolvedUrl) {
|
|
470
|
+
if (!resolvedUrl || typeof resolvedUrl !== "string") {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
return resolvedUrl.includes("registry.npmjs.org") || resolvedUrl.includes("npm.pkg.github.com") || resolvedUrl.startsWith("https://") && !resolvedUrl.includes("github.com/");
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Generate integrity hash for content
|
|
477
|
+
*
|
|
478
|
+
* @param content - Content to hash
|
|
479
|
+
* @param algorithm - Hash algorithm (sha512, sha256, sha1)
|
|
480
|
+
* @returns Integrity string (e.g., "sha512-ABC...")
|
|
481
|
+
*/
|
|
482
|
+
static generateIntegrity(content, algorithm = "sha512") {
|
|
483
|
+
const hash = createHash(algorithm);
|
|
484
|
+
hash.update(content);
|
|
485
|
+
return `${algorithm}-${hash.digest("base64")}`;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Compare two integrity strings
|
|
489
|
+
*
|
|
490
|
+
* @param integrity1 - First integrity string
|
|
491
|
+
* @param integrity2 - Second integrity string
|
|
492
|
+
* @returns true if integrities match
|
|
493
|
+
*/
|
|
494
|
+
static compareIntegrity(integrity1, integrity2) {
|
|
495
|
+
if (!integrity1 || !integrity2) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
const parts1 = integrity1.split("-");
|
|
499
|
+
const algo1 = parts1[0];
|
|
500
|
+
const hash1 = parts1[1];
|
|
501
|
+
const parts2 = integrity2.split("-");
|
|
502
|
+
const algo2 = parts2[0];
|
|
503
|
+
const hash2 = parts2[1];
|
|
504
|
+
if (!algo1 || !hash1 || !algo2 || !hash2) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
return algo1.toLowerCase() === algo2.toLowerCase() && hash1 === hash2;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Validate that package has not been modified
|
|
511
|
+
* Compares calculated hash with stored integrity
|
|
512
|
+
*
|
|
513
|
+
* @param content - Package content
|
|
514
|
+
* @param storedIntegrity - Integrity from lock file
|
|
515
|
+
* @returns true if package matches integrity
|
|
516
|
+
*/
|
|
517
|
+
static validatePackageNotModified(content, storedIntegrity) {
|
|
518
|
+
try {
|
|
519
|
+
const parts = storedIntegrity.split("-");
|
|
520
|
+
const algorithm = parts[0];
|
|
521
|
+
const expectedHash = parts[1];
|
|
522
|
+
if (!algorithm || !expectedHash) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
const hash = createHash(algorithm);
|
|
526
|
+
hash.update(content);
|
|
527
|
+
const calculatedHash = hash.digest("base64");
|
|
528
|
+
return calculatedHash === expectedHash;
|
|
529
|
+
} catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Extract algorithm and hash from integrity string
|
|
535
|
+
*
|
|
536
|
+
* @param integrityString - Integrity string to parse
|
|
537
|
+
* @returns Object with algorithm and hash, or null if invalid
|
|
538
|
+
*/
|
|
539
|
+
static extractIntegrityInfo(integrityString) {
|
|
540
|
+
if (!integrityString) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const parts = integrityString.split("-");
|
|
544
|
+
const algorithm = parts[0];
|
|
545
|
+
const hash = parts[1];
|
|
546
|
+
if (!algorithm || !hash) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
return { algorithm, hash };
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Format verification report for display
|
|
553
|
+
*
|
|
554
|
+
* @param result - Verification result
|
|
555
|
+
* @returns Formatted report string
|
|
556
|
+
*/
|
|
557
|
+
static formatReport(result) {
|
|
558
|
+
let report = `Package Integrity Report
|
|
559
|
+
`;
|
|
560
|
+
report += `========================
|
|
561
|
+
|
|
562
|
+
`;
|
|
563
|
+
report += `Status: ${result.valid ? "\u2713 VALID" : "\u2717 INVALID"}
|
|
564
|
+
|
|
565
|
+
`;
|
|
566
|
+
if (result.packageCount !== void 0) {
|
|
567
|
+
report += `Packages Checked: ${result.packageCount}
|
|
568
|
+
`;
|
|
569
|
+
report += `Verified: ${result.checksVerified}
|
|
570
|
+
`;
|
|
571
|
+
report += `Skipped: ${result.checksSkipped}
|
|
572
|
+
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
if (result.errors.length > 0) {
|
|
576
|
+
report += `Errors (${result.errors.length}):
|
|
577
|
+
`;
|
|
578
|
+
result.errors.forEach((error) => {
|
|
579
|
+
report += ` - ${error}
|
|
580
|
+
`;
|
|
581
|
+
});
|
|
582
|
+
report += "\n";
|
|
583
|
+
}
|
|
584
|
+
if (result.warnings.length > 0) {
|
|
585
|
+
report += `\u26A0 Warnings (${result.warnings.length}):
|
|
586
|
+
`;
|
|
587
|
+
result.warnings.forEach((warning) => {
|
|
588
|
+
report += ` - ${warning}
|
|
589
|
+
`;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return report;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/utils/package-manager.ts
|
|
597
|
+
var logger = getModuleLogger();
|
|
598
|
+
var lockIntegrityCache = /* @__PURE__ */ new Map();
|
|
599
|
+
function getLockIntegrityCacheKey(projectRoot, packageManager) {
|
|
600
|
+
return `${projectRoot}:${packageManager}`;
|
|
601
|
+
}
|
|
602
|
+
function invalidateLockFileIntegrityCache(projectRoot) {
|
|
603
|
+
for (const key of lockIntegrityCache.keys()) {
|
|
604
|
+
if (key.startsWith(`${projectRoot}:`)) {
|
|
605
|
+
lockIntegrityCache.delete(key);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
var SAFE_NPM_FLAGS = /* @__PURE__ */ new Set([
|
|
610
|
+
"--prefer-offline",
|
|
611
|
+
"--no-save-exact",
|
|
612
|
+
"--save-exact",
|
|
613
|
+
"--audit",
|
|
614
|
+
"--save-dev",
|
|
615
|
+
"--save",
|
|
616
|
+
"--no-save",
|
|
617
|
+
"--legacy-peer-deps",
|
|
618
|
+
"--no-strict-ssl",
|
|
619
|
+
"--progress",
|
|
620
|
+
"--force",
|
|
621
|
+
"--no-fund",
|
|
622
|
+
"--fund"
|
|
623
|
+
]);
|
|
624
|
+
function hasControlCharacters(str) {
|
|
625
|
+
return /[\x00-\x1f]/.test(str);
|
|
626
|
+
}
|
|
627
|
+
function validateNpmArguments(args) {
|
|
628
|
+
for (const arg of args) {
|
|
629
|
+
if (arg.startsWith("--")) {
|
|
630
|
+
const flagName = arg.split("=")[0] ?? arg;
|
|
631
|
+
if (!SAFE_NPM_FLAGS.has(flagName)) {
|
|
632
|
+
return `Dangerous argument: ${arg}`;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
function validateAdditionalArgs(args) {
|
|
639
|
+
if (!Array.isArray(args)) {
|
|
640
|
+
return "Additional arguments must be an array";
|
|
641
|
+
}
|
|
642
|
+
for (const arg of args) {
|
|
643
|
+
if (typeof arg !== "string") {
|
|
644
|
+
return `Invalid argument type: ${typeof arg} (expected string)`;
|
|
645
|
+
}
|
|
646
|
+
if (arg.length === 0) {
|
|
647
|
+
return "Arguments cannot be empty strings";
|
|
648
|
+
}
|
|
649
|
+
if (!arg.startsWith("--")) {
|
|
650
|
+
return `Invalid argument format: ${arg} (must start with --)`;
|
|
651
|
+
}
|
|
652
|
+
const equalsIndex = arg.indexOf("=");
|
|
653
|
+
const flagName = equalsIndex === -1 ? arg : arg.substring(0, equalsIndex);
|
|
654
|
+
if (/[;&|`$()[\]{}<>\\]/.test(flagName) || hasControlCharacters(flagName)) {
|
|
655
|
+
return `Invalid argument format: ${arg} contains dangerous characters`;
|
|
656
|
+
}
|
|
657
|
+
if (!SAFE_NPM_FLAGS.has(flagName)) {
|
|
658
|
+
return `Unknown or unsafe flag: ${flagName}`;
|
|
659
|
+
}
|
|
660
|
+
if (equalsIndex !== -1) {
|
|
661
|
+
const value = arg.substring(equalsIndex + 1);
|
|
662
|
+
if (value.length === 0) {
|
|
663
|
+
return `Flag ${flagName} has empty value`;
|
|
664
|
+
}
|
|
665
|
+
const hasShellChars = /[;&|`$()[\]{}\\<>'":\x00-\x1f]/.test(value);
|
|
666
|
+
const hasNonAscii = /[^\x20-\x7e\t\n-]/.test(value);
|
|
667
|
+
if (hasShellChars || hasNonAscii) {
|
|
668
|
+
return `Invalid argument format: ${arg} contains dangerous characters`;
|
|
669
|
+
}
|
|
670
|
+
if (value.includes("../") || value.includes("..\\")) {
|
|
671
|
+
return `Invalid argument format: ${arg} contains dangerous characters`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
function createSafeEnvironment() {
|
|
678
|
+
const SAFE_ENV_VARS = [
|
|
679
|
+
"PATH",
|
|
680
|
+
"HOME",
|
|
681
|
+
"NODE_ENV",
|
|
682
|
+
"LANG",
|
|
683
|
+
"LC_ALL",
|
|
684
|
+
"SHELL",
|
|
685
|
+
"USER",
|
|
686
|
+
"TMPDIR",
|
|
687
|
+
"TEMP",
|
|
688
|
+
"TMP"
|
|
689
|
+
];
|
|
690
|
+
const safeEnv = {};
|
|
691
|
+
for (const key of SAFE_ENV_VARS) {
|
|
692
|
+
const value = process.env[key];
|
|
693
|
+
if (value) {
|
|
694
|
+
safeEnv[key] = value;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
safeEnv["npm_config_yes"] = "true";
|
|
698
|
+
safeEnv["YARN_ENABLE_IMMUTABLE_INSTALLS"] = "false";
|
|
699
|
+
return safeEnv;
|
|
700
|
+
}
|
|
701
|
+
async function detectPackageManager(projectRoot, fsAdapter) {
|
|
702
|
+
const root = resolve(projectRoot);
|
|
703
|
+
const lockfiles = [
|
|
704
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
705
|
+
{ file: "yarn.lock", manager: "yarn" },
|
|
706
|
+
{ file: "package-lock.json", manager: "npm" },
|
|
707
|
+
{ file: "bun.lockb", manager: "bun" }
|
|
708
|
+
];
|
|
709
|
+
for (const { file, manager } of lockfiles) {
|
|
710
|
+
const lockfilePath = join(root, file);
|
|
711
|
+
if (fsAdapter) {
|
|
712
|
+
if (await fsAdapter.pathExists(lockfilePath)) {
|
|
713
|
+
logger.debug(`Detected package manager: ${manager} (found ${file})`);
|
|
714
|
+
return manager;
|
|
715
|
+
}
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (await fs.pathExists(lockfilePath)) {
|
|
719
|
+
logger.debug(`Detected package manager: ${manager} (found ${file})`);
|
|
720
|
+
return manager;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
logger.debug("No lockfile found, defaulting to npm");
|
|
724
|
+
return "npm";
|
|
725
|
+
}
|
|
726
|
+
async function verifyLockFileIntegrity(projectRoot, packageManager) {
|
|
727
|
+
const lockFiles = {
|
|
728
|
+
npm: "package-lock.json",
|
|
729
|
+
yarn: "yarn.lock",
|
|
730
|
+
pnpm: "pnpm-lock.yaml",
|
|
731
|
+
bun: "bun.lockb"
|
|
732
|
+
};
|
|
733
|
+
const lockFile = lockFiles[packageManager];
|
|
734
|
+
const lockPath = resolve(projectRoot, lockFile);
|
|
735
|
+
const cacheKey = getLockIntegrityCacheKey(projectRoot, packageManager);
|
|
736
|
+
if (!await fs.pathExists(lockPath)) {
|
|
737
|
+
logger.debug(`Lock file not found: ${lockFile}`);
|
|
738
|
+
lockIntegrityCache.set(cacheKey, {
|
|
739
|
+
result: null,
|
|
740
|
+
exists: false
|
|
741
|
+
});
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const stats = await fs.stat(lockPath);
|
|
746
|
+
const cached = lockIntegrityCache.get(cacheKey);
|
|
747
|
+
if (cached?.exists && cached.mtimeMs === stats.mtimeMs) {
|
|
748
|
+
return cached.result;
|
|
749
|
+
}
|
|
750
|
+
const lockContent = await fs.readFile(lockPath, "utf-8");
|
|
751
|
+
const result = IntegrityChecker.verifyLockFile(lockContent);
|
|
752
|
+
if (!result.valid) {
|
|
753
|
+
const errors = result.errors.join(", ");
|
|
754
|
+
const errorResult = `Lock file integrity check failed: ${errors}`;
|
|
755
|
+
lockIntegrityCache.set(cacheKey, {
|
|
756
|
+
result: errorResult,
|
|
757
|
+
mtimeMs: stats.mtimeMs,
|
|
758
|
+
exists: true
|
|
759
|
+
});
|
|
760
|
+
return errorResult;
|
|
761
|
+
}
|
|
762
|
+
logger.debug(
|
|
763
|
+
`Lock file integrity verified (${result.checksVerified} hashes checked)`
|
|
764
|
+
);
|
|
765
|
+
lockIntegrityCache.set(cacheKey, {
|
|
766
|
+
result: null,
|
|
767
|
+
mtimeMs: stats.mtimeMs,
|
|
768
|
+
exists: true
|
|
769
|
+
});
|
|
770
|
+
return null;
|
|
771
|
+
} catch (error) {
|
|
772
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
773
|
+
const errorResult = `Error reading lock file: ${errorMessage}`;
|
|
774
|
+
lockIntegrityCache.set(cacheKey, {
|
|
775
|
+
result: errorResult,
|
|
776
|
+
exists: true
|
|
777
|
+
});
|
|
778
|
+
return errorResult;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function installPackages(packages, options) {
|
|
782
|
+
if (packages.length === 0) {
|
|
783
|
+
logger.warn("No packages to install");
|
|
784
|
+
return { success: true, packages: [] };
|
|
785
|
+
}
|
|
786
|
+
const invalidPackages = validatePackageNames(packages);
|
|
787
|
+
if (invalidPackages.length > 0) {
|
|
788
|
+
const errorMessage = getPackageValidationErrorMessage(invalidPackages);
|
|
789
|
+
logger.error(errorMessage);
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
packages,
|
|
793
|
+
error: errorMessage
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
const {
|
|
797
|
+
packageManager,
|
|
798
|
+
projectRoot,
|
|
799
|
+
dev = false,
|
|
800
|
+
exact = false,
|
|
801
|
+
silent = false,
|
|
802
|
+
additionalArgs = []
|
|
803
|
+
} = options;
|
|
804
|
+
logger.info(
|
|
805
|
+
`Installing ${packages.length} package(s) with ${packageManager}...`
|
|
806
|
+
);
|
|
807
|
+
const additionalArgsError = validateAdditionalArgs(additionalArgs);
|
|
808
|
+
if (additionalArgsError) {
|
|
809
|
+
logger.error(`Invalid additional arguments: ${additionalArgsError}`);
|
|
810
|
+
return {
|
|
811
|
+
success: false,
|
|
812
|
+
packages,
|
|
813
|
+
error: additionalArgsError
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const rateLimiter = getGlobalRateLimiter();
|
|
818
|
+
const rateLimitResult = rateLimiter.checkRequest(projectRoot);
|
|
819
|
+
if (!rateLimitResult.allowed) {
|
|
820
|
+
const message = rateLimitResult.message ?? "Rate limit exceeded. Please retry after cooldown.";
|
|
821
|
+
logger.warn(message);
|
|
822
|
+
return {
|
|
823
|
+
success: false,
|
|
824
|
+
packages,
|
|
825
|
+
error: message
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
const integrityError = await verifyLockFileIntegrity(
|
|
829
|
+
projectRoot,
|
|
830
|
+
packageManager
|
|
831
|
+
);
|
|
832
|
+
if (integrityError) {
|
|
833
|
+
logger.error(integrityError);
|
|
834
|
+
return {
|
|
835
|
+
success: false,
|
|
836
|
+
packages,
|
|
837
|
+
error: integrityError
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const command = getInstallCommand(packageManager, packages, { dev, exact });
|
|
841
|
+
const cwd = resolve(projectRoot);
|
|
842
|
+
logger.debug(`Executing: ${command.join(" ")} in ${cwd}`);
|
|
843
|
+
const [cmd, ...args] = command;
|
|
844
|
+
if (!cmd) {
|
|
845
|
+
throw new Error("Command is empty");
|
|
846
|
+
}
|
|
847
|
+
const error = validateNpmArguments(args);
|
|
848
|
+
if (error) {
|
|
849
|
+
logger.error(error);
|
|
850
|
+
return {
|
|
851
|
+
success: false,
|
|
852
|
+
packages,
|
|
853
|
+
error
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
const securityArgs = ["--prefer-offline", "--no-save-exact"];
|
|
857
|
+
if (packageManager === "npm") {
|
|
858
|
+
args.push(...securityArgs);
|
|
859
|
+
args.push("--audit");
|
|
860
|
+
}
|
|
861
|
+
if (additionalArgs.length > 0) {
|
|
862
|
+
logger.debug(`Adding ${additionalArgs.length} additional argument(s)`);
|
|
863
|
+
args.push(...additionalArgs);
|
|
864
|
+
}
|
|
865
|
+
const resultPromise = execa(cmd, args, {
|
|
866
|
+
cwd,
|
|
867
|
+
stdio: silent ? "pipe" : "inherit",
|
|
868
|
+
maxBuffer: RESOURCE_LIMITS.MAX_BUFFER,
|
|
869
|
+
env: createSafeEnvironment()
|
|
870
|
+
});
|
|
871
|
+
const result = await withTimeout(
|
|
872
|
+
resultPromise,
|
|
873
|
+
OPERATION_TIMEOUTS.PACKAGE_INSTALL,
|
|
874
|
+
`npm install (${packages.length} packages)`
|
|
875
|
+
);
|
|
876
|
+
if (result.exitCode !== 0) {
|
|
877
|
+
throw new Error(`Installation failed with exit code ${result.exitCode}`);
|
|
878
|
+
}
|
|
879
|
+
logger.success(`Successfully installed ${packages.length} package(s)`);
|
|
880
|
+
invalidateLockFileIntegrityCache(projectRoot);
|
|
881
|
+
return {
|
|
882
|
+
success: true,
|
|
883
|
+
packages
|
|
884
|
+
};
|
|
885
|
+
} catch (error) {
|
|
886
|
+
const isTimeout = error instanceof Error && "code" in error && error.code === "TIMEOUT";
|
|
887
|
+
const errorMessage = isTimeout ? getTimeoutErrorMessage(error, "Package installation") : error instanceof Error ? error.message : String(error);
|
|
888
|
+
logger.error(`Failed to install packages: ${errorMessage}`);
|
|
889
|
+
return {
|
|
890
|
+
success: false,
|
|
891
|
+
packages,
|
|
892
|
+
error: errorMessage
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function getInstallCommand(packageManager, packages, options) {
|
|
897
|
+
const { dev, exact } = options;
|
|
898
|
+
switch (packageManager) {
|
|
899
|
+
case "pnpm":
|
|
900
|
+
return [
|
|
901
|
+
"pnpm",
|
|
902
|
+
"add",
|
|
903
|
+
...dev ? ["-D"] : [],
|
|
904
|
+
...exact ? ["--save-exact"] : [],
|
|
905
|
+
...packages
|
|
906
|
+
];
|
|
907
|
+
case "yarn":
|
|
908
|
+
return [
|
|
909
|
+
"yarn",
|
|
910
|
+
"add",
|
|
911
|
+
...dev ? ["--dev"] : [],
|
|
912
|
+
...exact ? ["--exact"] : [],
|
|
913
|
+
...packages
|
|
914
|
+
];
|
|
915
|
+
case "bun":
|
|
916
|
+
return ["bun", "add", ...dev ? ["--dev"] : [], ...packages];
|
|
917
|
+
case "npm":
|
|
918
|
+
default:
|
|
919
|
+
return [
|
|
920
|
+
"npm",
|
|
921
|
+
"install",
|
|
922
|
+
...dev ? ["--save-dev"] : [],
|
|
923
|
+
...exact ? ["--save-exact"] : [],
|
|
924
|
+
...packages
|
|
925
|
+
];
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export {
|
|
930
|
+
detectPackageManager,
|
|
931
|
+
installPackages
|
|
932
|
+
};
|