@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.
Files changed (34) hide show
  1. package/dist/{angular-command-XN26G6L3.js → angular-command-EOREU45Q.js} +8 -8
  2. package/dist/{angular-installer-FY43HE72.js → angular-installer-TKZDPFLD.js} +9 -1
  3. package/dist/angular-setup-QDTWXOB4.js +30 -0
  4. package/dist/check-KAPRT4FM.js +168 -0
  5. package/dist/{chunk-JYWGJJ4M.js → chunk-D7IWYKUX.js} +476 -28
  6. package/dist/chunk-EDCNW4UO.js +92 -0
  7. package/dist/{chunk-TN27AX4L.js → chunk-FJLN62L4.js} +797 -18
  8. package/dist/{chunk-FIB2J36N.js → chunk-HI7RYD6W.js} +161 -36
  9. package/dist/{chunk-NYCK4R4K.js → chunk-RIJNUJDC.js} +361 -96
  10. package/dist/chunk-V2IBYLVH.js +932 -0
  11. package/dist/chunk-VN4XTFDK.js +315 -0
  12. package/dist/{chunk-UKEHW2LH.js → chunk-Y4XYC7QV.js} +17 -3
  13. package/dist/cli.js +31 -21
  14. package/dist/{installed-D6CUYQM5.js → installed-QMJZIZNC.js} +4 -4
  15. package/dist/{list-VZDUWV5O.js → list-5T6VDDAO.js} +4 -4
  16. package/dist/{nextjs-command-WKKOAY7I.js → nextjs-command-C6PM7A5C.js} +8 -9
  17. package/dist/{nextjs-installer-2ZC5IWJ6.js → nextjs-installer-OFY5BQC4.js} +9 -2
  18. package/dist/{nextjs-setup-DYOFF72S.js → nextjs-setup-JIKPIJCX.js} +21 -9
  19. package/dist/{react-command-2T6IOTHB.js → react-command-JMK6VM4Q.js} +8 -9
  20. package/dist/{remove-ZY3MLPGN.js → remove-4ZNQR6ZR.js} +4 -4
  21. package/dist/{svelte-command-B2DNNQ5Z.js → svelte-command-YUSD55NO.js} +8 -8
  22. package/dist/svelte-installer-UP3KDZSY.js +105 -0
  23. package/dist/{svelte-setup-FWXLXJAC.js → svelte-setup-33E46IBT.js} +16 -5
  24. package/dist/{vite-installer-Y6VMFHIM.js → vite-installer-EE2LE76G.js} +9 -2
  25. package/dist/{vite-setup-JRELX6K2.js → vite-setup-VO5BOI46.js} +16 -4
  26. package/dist/{vue-command-IOTC32AI.js → vue-command-3CYWLLFQ.js} +8 -9
  27. package/dist/{vue-installer-DGBBVF6F.js → vue-installer-LEGLVD77.js} +9 -2
  28. package/dist/{vue-setup-G44DLT2U.js → vue-setup-FK5QT7AY.js} +16 -4
  29. package/package.json +12 -4
  30. package/dist/angular-setup-Z6TCKNBG.js +0 -18
  31. package/dist/check-KNGZSCMM.js +0 -131
  32. package/dist/chunk-6GV4NKUX.js +0 -122
  33. package/dist/chunk-QPEUT7QG.js +0 -157
  34. 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
+ };