oh-my-node-modules 1.2.7 → 1.2.9

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 (3) hide show
  1. package/dist/cli.js +450 -83
  2. package/dist/index.js +435 -80
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -2809,6 +2809,162 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
2809
2809
  import { promises as fs2 } from "fs";
2810
2810
  import { join as join2, basename, dirname } from "path";
2811
2811
 
2812
+ // node_modules/yocto-queue/index.js
2813
+ class Node {
2814
+ value;
2815
+ next;
2816
+ constructor(value) {
2817
+ this.value = value;
2818
+ }
2819
+ }
2820
+
2821
+ class Queue {
2822
+ #head;
2823
+ #tail;
2824
+ #size;
2825
+ constructor() {
2826
+ this.clear();
2827
+ }
2828
+ enqueue(value) {
2829
+ const node = new Node(value);
2830
+ if (this.#head) {
2831
+ this.#tail.next = node;
2832
+ this.#tail = node;
2833
+ } else {
2834
+ this.#head = node;
2835
+ this.#tail = node;
2836
+ }
2837
+ this.#size++;
2838
+ }
2839
+ dequeue() {
2840
+ const current = this.#head;
2841
+ if (!current) {
2842
+ return;
2843
+ }
2844
+ this.#head = this.#head.next;
2845
+ this.#size--;
2846
+ if (!this.#head) {
2847
+ this.#tail = undefined;
2848
+ }
2849
+ return current.value;
2850
+ }
2851
+ peek() {
2852
+ if (!this.#head) {
2853
+ return;
2854
+ }
2855
+ return this.#head.value;
2856
+ }
2857
+ clear() {
2858
+ this.#head = undefined;
2859
+ this.#tail = undefined;
2860
+ this.#size = 0;
2861
+ }
2862
+ get size() {
2863
+ return this.#size;
2864
+ }
2865
+ *[Symbol.iterator]() {
2866
+ let current = this.#head;
2867
+ while (current) {
2868
+ yield current.value;
2869
+ current = current.next;
2870
+ }
2871
+ }
2872
+ *drain() {
2873
+ while (this.#head) {
2874
+ yield this.dequeue();
2875
+ }
2876
+ }
2877
+ }
2878
+
2879
+ // node_modules/p-limit/index.js
2880
+ function pLimit(concurrency) {
2881
+ let rejectOnClear = false;
2882
+ if (typeof concurrency === "object") {
2883
+ ({ concurrency, rejectOnClear = false } = concurrency);
2884
+ }
2885
+ validateConcurrency(concurrency);
2886
+ if (typeof rejectOnClear !== "boolean") {
2887
+ throw new TypeError("Expected `rejectOnClear` to be a boolean");
2888
+ }
2889
+ const queue = new Queue;
2890
+ let activeCount = 0;
2891
+ const resumeNext = () => {
2892
+ if (activeCount < concurrency && queue.size > 0) {
2893
+ activeCount++;
2894
+ queue.dequeue().run();
2895
+ }
2896
+ };
2897
+ const next = () => {
2898
+ activeCount--;
2899
+ resumeNext();
2900
+ };
2901
+ const run = async (function_, resolve, arguments_) => {
2902
+ const result = (async () => function_(...arguments_))();
2903
+ resolve(result);
2904
+ try {
2905
+ await result;
2906
+ } catch {}
2907
+ next();
2908
+ };
2909
+ const enqueue = (function_, resolve, reject, arguments_) => {
2910
+ const queueItem = { reject };
2911
+ new Promise((internalResolve) => {
2912
+ queueItem.run = internalResolve;
2913
+ queue.enqueue(queueItem);
2914
+ }).then(run.bind(undefined, function_, resolve, arguments_));
2915
+ if (activeCount < concurrency) {
2916
+ resumeNext();
2917
+ }
2918
+ };
2919
+ const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
2920
+ enqueue(function_, resolve, reject, arguments_);
2921
+ });
2922
+ Object.defineProperties(generator, {
2923
+ activeCount: {
2924
+ get: () => activeCount
2925
+ },
2926
+ pendingCount: {
2927
+ get: () => queue.size
2928
+ },
2929
+ clearQueue: {
2930
+ value() {
2931
+ if (!rejectOnClear) {
2932
+ queue.clear();
2933
+ return;
2934
+ }
2935
+ const abortError = AbortSignal.abort().reason;
2936
+ while (queue.size > 0) {
2937
+ queue.dequeue().reject(abortError);
2938
+ }
2939
+ }
2940
+ },
2941
+ concurrency: {
2942
+ get: () => concurrency,
2943
+ set(newConcurrency) {
2944
+ validateConcurrency(newConcurrency);
2945
+ concurrency = newConcurrency;
2946
+ queueMicrotask(() => {
2947
+ while (activeCount < concurrency && queue.size > 0) {
2948
+ resumeNext();
2949
+ }
2950
+ });
2951
+ }
2952
+ },
2953
+ map: {
2954
+ async value(iterable, function_) {
2955
+ const promises = Array.from(iterable, (value, index) => this(function_, value, index));
2956
+ return Promise.all(promises);
2957
+ }
2958
+ }
2959
+ });
2960
+ return generator;
2961
+ }
2962
+ function validateConcurrency(concurrency) {
2963
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
2964
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
2965
+ }
2966
+ }
2967
+
2812
2968
  // src/utils.ts
2813
2969
  import { promises as fs } from "fs";
2814
2970
  import { join } from "path";
@@ -2948,89 +3104,112 @@ async function readPackageJson(projectPath) {
2948
3104
  }
2949
3105
  }
2950
3106
 
2951
- // src/scanner.ts
2952
- async function scanForNodeModules(options, onProgress) {
2953
- const result = {
2954
- nodeModules: [],
2955
- directoriesScanned: 0,
2956
- errors: []
2957
- };
2958
- const visitedPaths = new Set;
2959
- const pathsToScan = [
2960
- { path: options.rootPath, depth: 0 }
2961
- ];
2962
- let processedCount = 0;
2963
- let totalEstimate = 1;
2964
- while (pathsToScan.length > 0) {
2965
- const { path: currentPath, depth } = pathsToScan.shift();
2966
- if (visitedPaths.has(currentPath))
2967
- continue;
2968
- if (options.maxDepth !== undefined && depth > options.maxDepth)
2969
- continue;
2970
- if (shouldExcludePath(currentPath, options.excludePatterns))
2971
- continue;
2972
- visitedPaths.add(currentPath);
2973
- result.directoriesScanned++;
2974
- try {
2975
- const entries = await fs2.readdir(currentPath, { withFileTypes: true });
2976
- const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
2977
- if (hasNodeModules) {
2978
- const nodeModulesPath = join2(currentPath, "node_modules");
2979
- const info = await analyzeNodeModules(nodeModulesPath, currentPath);
2980
- if (options.minSizeBytes && info.sizeBytes < options.minSizeBytes) {} else if (options.olderThanDays && getAgeInDays2(info.lastModified) < options.olderThanDays) {} else {
2981
- result.nodeModules.push(info);
2982
- }
2983
- }
2984
- for (const entry of entries) {
2985
- if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
2986
- const subPath = join2(currentPath, entry.name);
2987
- if (!shouldExcludePath(subPath, options.excludePatterns)) {
2988
- pathsToScan.push({ path: subPath, depth: depth + 1 });
2989
- totalEstimate++;
2990
- }
2991
- }
2992
- }
2993
- } catch (error) {
2994
- const errorMessage = error instanceof Error ? error.message : String(error);
2995
- result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
3107
+ // src/native-size.ts
3108
+ import { exec } from "child_process";
3109
+ import { promisify } from "util";
3110
+ import { platform } from "os";
3111
+ var execAsync = promisify(exec);
3112
+ async function getSizeWithDu(dirPath) {
3113
+ try {
3114
+ const { stdout } = await execAsync(`du -sb "${dirPath}"`, {
3115
+ timeout: 30000,
3116
+ maxBuffer: 1024 * 1024
3117
+ });
3118
+ const match = stdout.trim().match(/^(\d+)/);
3119
+ if (match) {
3120
+ return parseInt(match[1], 10);
2996
3121
  }
2997
- processedCount++;
2998
- if (onProgress) {
2999
- const progress = Math.min(100, Math.round(processedCount / totalEstimate * 100));
3000
- onProgress(progress);
3122
+ return null;
3123
+ } catch {
3124
+ return null;
3125
+ }
3126
+ }
3127
+ async function getSizeWithDir(dirPath) {
3128
+ try {
3129
+ const { stdout } = await execAsync(`dir /s "${dirPath}"`, {
3130
+ timeout: 30000,
3131
+ maxBuffer: 1024 * 1024
3132
+ });
3133
+ const lines = stdout.split(`
3134
+ `);
3135
+ for (let i = lines.length - 1;i >= 0; i--) {
3136
+ const line = lines[i];
3137
+ const match = line.match(/File\(s\)\s+([\d,]+)\s+bytes?/i);
3138
+ if (match) {
3139
+ const bytesStr = match[1].replace(/,/g, "");
3140
+ return parseInt(bytesStr, 10);
3141
+ }
3001
3142
  }
3143
+ return null;
3144
+ } catch {
3145
+ return null;
3002
3146
  }
3003
- if (onProgress) {
3004
- onProgress(100);
3147
+ }
3148
+ async function countPackagesNative(dirPath) {
3149
+ try {
3150
+ const isWindows = platform() === "win32";
3151
+ if (isWindows) {
3152
+ const { stdout } = await execAsync(`dir /b /ad "${dirPath}" | find /c /v ""`, { timeout: 1e4 });
3153
+ const topLevel = parseInt(stdout.trim(), 10) || 0;
3154
+ return { topLevel, total: topLevel };
3155
+ } else {
3156
+ const { stdout: topLevelOut } = await execAsync(`find "${dirPath}" -maxdepth 1 -type d ! -name ".*" ! -name "node_modules" | wc -l`, { timeout: 1e4 });
3157
+ const topLevel = Math.max(0, parseInt(topLevelOut.trim(), 10) - 1);
3158
+ const { stdout: totalOut } = await execAsync(`find "${dirPath}" -type d ! -path "${dirPath}" ! -name ".*" | wc -l`, { timeout: 1e4 });
3159
+ const total = parseInt(totalOut.trim(), 10);
3160
+ return { topLevel, total };
3161
+ }
3162
+ } catch {
3163
+ return null;
3005
3164
  }
3006
- return result;
3007
3165
  }
3008
- async function analyzeNodeModules(nodeModulesPath, projectPath) {
3009
- const stats = await fs2.stat(nodeModulesPath);
3010
- const { totalSize, packageCount, totalPackageCount } = await calculateDirectorySize(nodeModulesPath);
3011
- const packageJson = await readPackageJson(projectPath);
3012
- const projectName = packageJson?.name || basename(projectPath);
3013
- const projectVersion = packageJson?.version;
3014
- const sizeCategory = getSizeCategory(totalSize);
3015
- const ageCategory = getAgeCategory(stats.mtime);
3166
+ async function getFastDirectorySize(dirPath) {
3167
+ const isWindows = platform() === "win32";
3168
+ let sizeBytes = null;
3169
+ if (isWindows) {
3170
+ sizeBytes = await getSizeWithDir(dirPath);
3171
+ } else {
3172
+ sizeBytes = await getSizeWithDu(dirPath);
3173
+ }
3174
+ const packageCounts = await countPackagesNative(dirPath);
3175
+ if (sizeBytes !== null && packageCounts !== null) {
3176
+ return {
3177
+ bytes: sizeBytes,
3178
+ packageCount: packageCounts.topLevel,
3179
+ totalPackageCount: packageCounts.total,
3180
+ isNative: true
3181
+ };
3182
+ }
3016
3183
  return {
3017
- path: nodeModulesPath,
3018
- projectPath,
3019
- projectName,
3020
- projectVersion,
3021
- sizeBytes: totalSize,
3022
- sizeFormatted: formatBytes(totalSize),
3023
- packageCount,
3024
- totalPackageCount,
3025
- lastModified: stats.mtime,
3026
- lastModifiedFormatted: formatRelativeTime(stats.mtime),
3027
- selected: false,
3028
- isFavorite: false,
3029
- ageCategory,
3030
- sizeCategory
3184
+ bytes: 0,
3185
+ packageCount: 0,
3186
+ totalPackageCount: 0,
3187
+ isNative: false
3031
3188
  };
3032
3189
  }
3033
- async function calculateDirectorySize(dirPath) {
3190
+
3191
+ // src/scanner.ts
3192
+ var DEFAULT_CONCURRENCY = 5;
3193
+ var SIZE_CALCULATION_CONCURRENCY = 3;
3194
+ async function findRepoRoot(startPath) {
3195
+ let currentPath = startPath;
3196
+ const root = process.platform === "win32" ? "C:\\" : "/";
3197
+ while (currentPath !== root && currentPath !== dirname(currentPath)) {
3198
+ const gitPath = join2(currentPath, ".git");
3199
+ try {
3200
+ if (await fileExists(gitPath)) {
3201
+ return currentPath;
3202
+ }
3203
+ } catch {}
3204
+ currentPath = dirname(currentPath);
3205
+ }
3206
+ return startPath;
3207
+ }
3208
+ function getAgeInDays2(date) {
3209
+ const now = new Date;
3210
+ return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
3211
+ }
3212
+ async function calculateDirectorySizeFallback(dirPath) {
3034
3213
  let totalSize = 0;
3035
3214
  let packageCount = 0;
3036
3215
  let totalPackageCount = 0;
@@ -3067,7 +3246,7 @@ async function calculateDirectorySize(dirPath) {
3067
3246
  pathsToProcess.push(entryPath);
3068
3247
  }
3069
3248
  } catch {}
3070
- } else if (stats.isSymbolicLink()) {}
3249
+ }
3071
3250
  } catch {}
3072
3251
  if (currentPath === dirPath) {
3073
3252
  isTopLevel = false;
@@ -3075,9 +3254,181 @@ async function calculateDirectorySize(dirPath) {
3075
3254
  }
3076
3255
  return { totalSize, packageCount, totalPackageCount };
3077
3256
  }
3078
- function getAgeInDays2(date) {
3079
- const now = new Date;
3080
- return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
3257
+ async function calculateSizeWithFallback(dirPath) {
3258
+ const nativeResult = await getFastDirectorySize(dirPath);
3259
+ if (nativeResult.isNative && nativeResult.bytes > 0) {
3260
+ return {
3261
+ totalSize: nativeResult.bytes,
3262
+ packageCount: nativeResult.packageCount,
3263
+ totalPackageCount: nativeResult.totalPackageCount,
3264
+ isNative: true
3265
+ };
3266
+ }
3267
+ const fallbackResult = await calculateDirectorySizeFallback(dirPath);
3268
+ return {
3269
+ ...fallbackResult,
3270
+ isNative: false
3271
+ };
3272
+ }
3273
+ async function analyzeNodeModules(nodeModulesPath, projectPath, lazy = false) {
3274
+ const stats = await fs2.stat(nodeModulesPath);
3275
+ if (lazy) {
3276
+ const packageJson2 = await readPackageJson(projectPath);
3277
+ const projectName2 = packageJson2?.name || basename(projectPath);
3278
+ const repoPath2 = await findRepoRoot(projectPath);
3279
+ return {
3280
+ path: nodeModulesPath,
3281
+ projectPath,
3282
+ projectName: projectName2,
3283
+ projectVersion: packageJson2?.version,
3284
+ repoPath: repoPath2,
3285
+ sizeBytes: 0,
3286
+ sizeFormatted: "calculating...",
3287
+ packageCount: 0,
3288
+ totalPackageCount: 0,
3289
+ lastModified: stats.mtime,
3290
+ lastModifiedFormatted: formatRelativeTime(stats.mtime),
3291
+ selected: false,
3292
+ isFavorite: false,
3293
+ ageCategory: getAgeCategory(stats.mtime),
3294
+ sizeCategory: "small",
3295
+ isPending: true
3296
+ };
3297
+ }
3298
+ const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(nodeModulesPath);
3299
+ const packageJson = await readPackageJson(projectPath);
3300
+ const projectName = packageJson?.name || basename(projectPath);
3301
+ const repoPath = await findRepoRoot(projectPath);
3302
+ const sizeCategory = getSizeCategory(totalSize);
3303
+ const ageCategory = getAgeCategory(stats.mtime);
3304
+ return {
3305
+ path: nodeModulesPath,
3306
+ projectPath,
3307
+ projectName,
3308
+ projectVersion: packageJson?.version,
3309
+ repoPath,
3310
+ sizeBytes: totalSize,
3311
+ sizeFormatted: formatBytes(totalSize),
3312
+ packageCount,
3313
+ totalPackageCount,
3314
+ lastModified: stats.mtime,
3315
+ lastModifiedFormatted: formatRelativeTime(stats.mtime),
3316
+ selected: false,
3317
+ isFavorite: false,
3318
+ ageCategory,
3319
+ sizeCategory,
3320
+ isNativeCalculation: isNative
3321
+ };
3322
+ }
3323
+ async function updateNodeModulesSize(info) {
3324
+ const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(info.path);
3325
+ return {
3326
+ ...info,
3327
+ sizeBytes: totalSize,
3328
+ sizeFormatted: formatBytes(totalSize),
3329
+ packageCount,
3330
+ totalPackageCount,
3331
+ sizeCategory: getSizeCategory(totalSize),
3332
+ isPending: false,
3333
+ isNativeCalculation: isNative
3334
+ };
3335
+ }
3336
+ async function scanForNodeModules(options, onProgress, lazy = false) {
3337
+ const result = {
3338
+ nodeModules: [],
3339
+ directoriesScanned: 0,
3340
+ errors: []
3341
+ };
3342
+ const visitedPaths = new Set;
3343
+ const pathsToScan = [
3344
+ { path: options.rootPath, depth: 0 }
3345
+ ];
3346
+ const scanLimit = pLimit(DEFAULT_CONCURRENCY);
3347
+ const sizeLimit = pLimit(SIZE_CALCULATION_CONCURRENCY);
3348
+ let processedCount = 0;
3349
+ let totalEstimate = 1;
3350
+ let foundCount = 0;
3351
+ while (pathsToScan.length > 0) {
3352
+ const batch = pathsToScan.splice(0, Math.min(pathsToScan.length, DEFAULT_CONCURRENCY * 2));
3353
+ const scanPromises = batch.map(({ path: currentPath, depth }) => scanLimit(async () => {
3354
+ if (visitedPaths.has(currentPath))
3355
+ return;
3356
+ if (options.maxDepth !== undefined && depth > options.maxDepth)
3357
+ return;
3358
+ if (shouldExcludePath(currentPath, options.excludePatterns))
3359
+ return;
3360
+ visitedPaths.add(currentPath);
3361
+ result.directoriesScanned++;
3362
+ try {
3363
+ const entries = await fs2.readdir(currentPath, { withFileTypes: true });
3364
+ const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
3365
+ if (hasNodeModules) {
3366
+ const nodeModulesPath = join2(currentPath, "node_modules");
3367
+ const info = await sizeLimit(() => analyzeNodeModules(nodeModulesPath, currentPath, lazy));
3368
+ const passesFilter = (!options.minSizeBytes || info.sizeBytes >= options.minSizeBytes) && (!options.olderThanDays || getAgeInDays2(info.lastModified) >= options.olderThanDays);
3369
+ if (passesFilter || lazy) {
3370
+ result.nodeModules.push(info);
3371
+ foundCount++;
3372
+ if (onProgress) {
3373
+ onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
3374
+ }
3375
+ }
3376
+ }
3377
+ for (const entry of entries) {
3378
+ if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
3379
+ const subPath = join2(currentPath, entry.name);
3380
+ if (!shouldExcludePath(subPath, options.excludePatterns)) {
3381
+ pathsToScan.push({ path: subPath, depth: depth + 1 });
3382
+ totalEstimate++;
3383
+ }
3384
+ }
3385
+ }
3386
+ } catch (error) {
3387
+ const errorMessage = error instanceof Error ? error.message : String(error);
3388
+ result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
3389
+ }
3390
+ processedCount++;
3391
+ if (onProgress) {
3392
+ onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
3393
+ }
3394
+ }));
3395
+ await Promise.all(scanPromises);
3396
+ }
3397
+ if (onProgress) {
3398
+ onProgress(100, foundCount);
3399
+ }
3400
+ return result;
3401
+ }
3402
+ async function calculatePendingSizes(nodeModules, onProgress) {
3403
+ const pending = nodeModules.filter((nm) => nm.isPending);
3404
+ if (pending.length === 0)
3405
+ return nodeModules;
3406
+ const limit = pLimit(SIZE_CALCULATION_CONCURRENCY);
3407
+ let completed = 0;
3408
+ const total = pending.length;
3409
+ const updatePromises = pending.map((pendingItem) => limit(async () => {
3410
+ try {
3411
+ const updated = await updateNodeModulesSize(pendingItem);
3412
+ completed++;
3413
+ if (onProgress) {
3414
+ onProgress(completed, total);
3415
+ }
3416
+ return updated;
3417
+ } catch (error) {
3418
+ completed++;
3419
+ if (onProgress) {
3420
+ onProgress(completed, total);
3421
+ }
3422
+ return {
3423
+ ...pendingItem,
3424
+ sizeFormatted: "error",
3425
+ isPending: false
3426
+ };
3427
+ }
3428
+ }));
3429
+ const updatedItems = await Promise.all(updatePromises);
3430
+ const updatedMap = new Map(updatedItems.map((item) => [item.path, item]));
3431
+ return nodeModules.map((item) => updatedMap.get(item.path) || item);
3081
3432
  }
3082
3433
  async function loadIgnorePatterns() {
3083
3434
  const patterns = [
@@ -3237,9 +3588,10 @@ function getVersion() {
3237
3588
  }
3238
3589
  }
3239
3590
  function formatItem(item) {
3240
- const size = import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
3591
+ const isPending = item.isPending;
3592
+ const size = isPending ? import_picocolors2.default.yellow(" (...) ") : import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
3241
3593
  const age = import_picocolors2.default.gray(item.lastModifiedFormatted);
3242
- const warning = item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
3594
+ const warning = !isPending && item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
3243
3595
  return `${size} ${item.projectName}${warning} [${age}]`;
3244
3596
  }
3245
3597
  async function interactiveMode(rootPath) {
@@ -3252,13 +3604,28 @@ async function interactiveMode(rootPath) {
3252
3604
  rootPath,
3253
3605
  excludePatterns,
3254
3606
  followSymlinks: false
3255
- });
3607
+ }, (_progress, found) => {
3608
+ if (found > 0) {
3609
+ s.message(`Scanning... found ${found} node_modules`);
3610
+ }
3611
+ }, true);
3256
3612
  s.stop(`Found ${result.nodeModules.length} node_modules directories`);
3257
3613
  if (result.nodeModules.length === 0) {
3258
3614
  ge(import_picocolors2.default.yellow("No node_modules found."));
3259
3615
  return;
3260
3616
  }
3261
3617
  let items = sortNodeModules(result.nodeModules, "size-desc");
3618
+ console.log(`
3619
+ ${import_picocolors2.default.gray("Calculating sizes...")}`);
3620
+ console.log(`${import_picocolors2.default.gray("Projects sorted by size:")}
3621
+ `);
3622
+ const sizeSpinner = _2();
3623
+ sizeSpinner.start("Calculating directory sizes...");
3624
+ items = await calculatePendingSizes(items, (completed, total) => {
3625
+ sizeSpinner.message(`Calculating sizes... ${completed}/${total}`);
3626
+ });
3627
+ sizeSpinner.stop(`Calculated sizes for ${items.length} directories`);
3628
+ items = sortNodeModules(items, "size-desc");
3262
3629
  const stats = calculateStatistics(items);
3263
3630
  console.log(`
3264
3631
  ${import_picocolors2.default.gray("Total:")} ${import_picocolors2.default.white(stats.totalSizeFormatted)} across ${import_picocolors2.default.white(String(stats.totalProjects))} projects`);
@@ -3417,7 +3784,7 @@ Dry run - no files deleted.`));
3417
3784
  }
3418
3785
  }
3419
3786
  var program2 = new Command;
3420
- program2.name("onm").description("Find and clean up node_modules directories").version(getVersion()).argument("[path]", "Directory to scan", process.cwd()).option("--scan", "quick scan mode (no interactive UI)").option("--auto", "auto-delete mode with filters").option("--min-size <size>", "minimum size in bytes for auto mode").option("--dry-run", "simulate deletion without actually deleting").option("--json", "output as JSON").action(async (path, options) => {
3787
+ program2.name("onm").description("Find and clean up node_modules directories").version(getVersion()).argument("<path>", "Directory to scan").option("--scan", "quick scan mode (no interactive UI)").option("--auto", "auto-delete mode with filters").option("--min-size <size>", "minimum size in bytes for auto mode").option("--dry-run", "simulate deletion without actually deleting").option("--json", "output as JSON").action(async (path, options) => {
3421
3788
  if (options.scan) {
3422
3789
  await quickScanMode(path, options.json);
3423
3790
  } else if (options.auto) {
package/dist/index.js CHANGED
@@ -2,6 +2,162 @@
2
2
  import { promises as fs2 } from "fs";
3
3
  import { join as join2, basename, dirname } from "path";
4
4
 
5
+ // node_modules/yocto-queue/index.js
6
+ class Node {
7
+ value;
8
+ next;
9
+ constructor(value) {
10
+ this.value = value;
11
+ }
12
+ }
13
+
14
+ class Queue {
15
+ #head;
16
+ #tail;
17
+ #size;
18
+ constructor() {
19
+ this.clear();
20
+ }
21
+ enqueue(value) {
22
+ const node = new Node(value);
23
+ if (this.#head) {
24
+ this.#tail.next = node;
25
+ this.#tail = node;
26
+ } else {
27
+ this.#head = node;
28
+ this.#tail = node;
29
+ }
30
+ this.#size++;
31
+ }
32
+ dequeue() {
33
+ const current = this.#head;
34
+ if (!current) {
35
+ return;
36
+ }
37
+ this.#head = this.#head.next;
38
+ this.#size--;
39
+ if (!this.#head) {
40
+ this.#tail = undefined;
41
+ }
42
+ return current.value;
43
+ }
44
+ peek() {
45
+ if (!this.#head) {
46
+ return;
47
+ }
48
+ return this.#head.value;
49
+ }
50
+ clear() {
51
+ this.#head = undefined;
52
+ this.#tail = undefined;
53
+ this.#size = 0;
54
+ }
55
+ get size() {
56
+ return this.#size;
57
+ }
58
+ *[Symbol.iterator]() {
59
+ let current = this.#head;
60
+ while (current) {
61
+ yield current.value;
62
+ current = current.next;
63
+ }
64
+ }
65
+ *drain() {
66
+ while (this.#head) {
67
+ yield this.dequeue();
68
+ }
69
+ }
70
+ }
71
+
72
+ // node_modules/p-limit/index.js
73
+ function pLimit(concurrency) {
74
+ let rejectOnClear = false;
75
+ if (typeof concurrency === "object") {
76
+ ({ concurrency, rejectOnClear = false } = concurrency);
77
+ }
78
+ validateConcurrency(concurrency);
79
+ if (typeof rejectOnClear !== "boolean") {
80
+ throw new TypeError("Expected `rejectOnClear` to be a boolean");
81
+ }
82
+ const queue = new Queue;
83
+ let activeCount = 0;
84
+ const resumeNext = () => {
85
+ if (activeCount < concurrency && queue.size > 0) {
86
+ activeCount++;
87
+ queue.dequeue().run();
88
+ }
89
+ };
90
+ const next = () => {
91
+ activeCount--;
92
+ resumeNext();
93
+ };
94
+ const run = async (function_, resolve, arguments_) => {
95
+ const result = (async () => function_(...arguments_))();
96
+ resolve(result);
97
+ try {
98
+ await result;
99
+ } catch {}
100
+ next();
101
+ };
102
+ const enqueue = (function_, resolve, reject, arguments_) => {
103
+ const queueItem = { reject };
104
+ new Promise((internalResolve) => {
105
+ queueItem.run = internalResolve;
106
+ queue.enqueue(queueItem);
107
+ }).then(run.bind(undefined, function_, resolve, arguments_));
108
+ if (activeCount < concurrency) {
109
+ resumeNext();
110
+ }
111
+ };
112
+ const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
113
+ enqueue(function_, resolve, reject, arguments_);
114
+ });
115
+ Object.defineProperties(generator, {
116
+ activeCount: {
117
+ get: () => activeCount
118
+ },
119
+ pendingCount: {
120
+ get: () => queue.size
121
+ },
122
+ clearQueue: {
123
+ value() {
124
+ if (!rejectOnClear) {
125
+ queue.clear();
126
+ return;
127
+ }
128
+ const abortError = AbortSignal.abort().reason;
129
+ while (queue.size > 0) {
130
+ queue.dequeue().reject(abortError);
131
+ }
132
+ }
133
+ },
134
+ concurrency: {
135
+ get: () => concurrency,
136
+ set(newConcurrency) {
137
+ validateConcurrency(newConcurrency);
138
+ concurrency = newConcurrency;
139
+ queueMicrotask(() => {
140
+ while (activeCount < concurrency && queue.size > 0) {
141
+ resumeNext();
142
+ }
143
+ });
144
+ }
145
+ },
146
+ map: {
147
+ async value(iterable, function_) {
148
+ const promises = Array.from(iterable, (value, index) => this(function_, value, index));
149
+ return Promise.all(promises);
150
+ }
151
+ }
152
+ });
153
+ return generator;
154
+ }
155
+ function validateConcurrency(concurrency) {
156
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
157
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
158
+ }
159
+ }
160
+
5
161
  // src/utils.ts
6
162
  import { promises as fs } from "fs";
7
163
  import { join } from "path";
@@ -182,89 +338,112 @@ async function readPackageJson(projectPath) {
182
338
  }
183
339
  }
184
340
 
185
- // src/scanner.ts
186
- async function scanForNodeModules(options, onProgress) {
187
- const result = {
188
- nodeModules: [],
189
- directoriesScanned: 0,
190
- errors: []
191
- };
192
- const visitedPaths = new Set;
193
- const pathsToScan = [
194
- { path: options.rootPath, depth: 0 }
195
- ];
196
- let processedCount = 0;
197
- let totalEstimate = 1;
198
- while (pathsToScan.length > 0) {
199
- const { path: currentPath, depth } = pathsToScan.shift();
200
- if (visitedPaths.has(currentPath))
201
- continue;
202
- if (options.maxDepth !== undefined && depth > options.maxDepth)
203
- continue;
204
- if (shouldExcludePath(currentPath, options.excludePatterns))
205
- continue;
206
- visitedPaths.add(currentPath);
207
- result.directoriesScanned++;
208
- try {
209
- const entries = await fs2.readdir(currentPath, { withFileTypes: true });
210
- const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
211
- if (hasNodeModules) {
212
- const nodeModulesPath = join2(currentPath, "node_modules");
213
- const info = await analyzeNodeModules(nodeModulesPath, currentPath);
214
- if (options.minSizeBytes && info.sizeBytes < options.minSizeBytes) {} else if (options.olderThanDays && getAgeInDays2(info.lastModified) < options.olderThanDays) {} else {
215
- result.nodeModules.push(info);
216
- }
217
- }
218
- for (const entry of entries) {
219
- if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
220
- const subPath = join2(currentPath, entry.name);
221
- if (!shouldExcludePath(subPath, options.excludePatterns)) {
222
- pathsToScan.push({ path: subPath, depth: depth + 1 });
223
- totalEstimate++;
224
- }
225
- }
226
- }
227
- } catch (error) {
228
- const errorMessage = error instanceof Error ? error.message : String(error);
229
- result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
341
+ // src/native-size.ts
342
+ import { exec } from "child_process";
343
+ import { promisify } from "util";
344
+ import { platform } from "os";
345
+ var execAsync = promisify(exec);
346
+ async function getSizeWithDu(dirPath) {
347
+ try {
348
+ const { stdout } = await execAsync(`du -sb "${dirPath}"`, {
349
+ timeout: 30000,
350
+ maxBuffer: 1024 * 1024
351
+ });
352
+ const match = stdout.trim().match(/^(\d+)/);
353
+ if (match) {
354
+ return parseInt(match[1], 10);
230
355
  }
231
- processedCount++;
232
- if (onProgress) {
233
- const progress = Math.min(100, Math.round(processedCount / totalEstimate * 100));
234
- onProgress(progress);
356
+ return null;
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ async function getSizeWithDir(dirPath) {
362
+ try {
363
+ const { stdout } = await execAsync(`dir /s "${dirPath}"`, {
364
+ timeout: 30000,
365
+ maxBuffer: 1024 * 1024
366
+ });
367
+ const lines = stdout.split(`
368
+ `);
369
+ for (let i = lines.length - 1;i >= 0; i--) {
370
+ const line = lines[i];
371
+ const match = line.match(/File\(s\)\s+([\d,]+)\s+bytes?/i);
372
+ if (match) {
373
+ const bytesStr = match[1].replace(/,/g, "");
374
+ return parseInt(bytesStr, 10);
375
+ }
235
376
  }
377
+ return null;
378
+ } catch {
379
+ return null;
236
380
  }
237
- if (onProgress) {
238
- onProgress(100);
381
+ }
382
+ async function countPackagesNative(dirPath) {
383
+ try {
384
+ const isWindows = platform() === "win32";
385
+ if (isWindows) {
386
+ const { stdout } = await execAsync(`dir /b /ad "${dirPath}" | find /c /v ""`, { timeout: 1e4 });
387
+ const topLevel = parseInt(stdout.trim(), 10) || 0;
388
+ return { topLevel, total: topLevel };
389
+ } else {
390
+ const { stdout: topLevelOut } = await execAsync(`find "${dirPath}" -maxdepth 1 -type d ! -name ".*" ! -name "node_modules" | wc -l`, { timeout: 1e4 });
391
+ const topLevel = Math.max(0, parseInt(topLevelOut.trim(), 10) - 1);
392
+ const { stdout: totalOut } = await execAsync(`find "${dirPath}" -type d ! -path "${dirPath}" ! -name ".*" | wc -l`, { timeout: 1e4 });
393
+ const total = parseInt(totalOut.trim(), 10);
394
+ return { topLevel, total };
395
+ }
396
+ } catch {
397
+ return null;
239
398
  }
240
- return result;
241
399
  }
242
- async function analyzeNodeModules(nodeModulesPath, projectPath) {
243
- const stats = await fs2.stat(nodeModulesPath);
244
- const { totalSize, packageCount, totalPackageCount } = await calculateDirectorySize(nodeModulesPath);
245
- const packageJson = await readPackageJson(projectPath);
246
- const projectName = packageJson?.name || basename(projectPath);
247
- const projectVersion = packageJson?.version;
248
- const sizeCategory = getSizeCategory(totalSize);
249
- const ageCategory = getAgeCategory(stats.mtime);
400
+ async function getFastDirectorySize(dirPath) {
401
+ const isWindows = platform() === "win32";
402
+ let sizeBytes = null;
403
+ if (isWindows) {
404
+ sizeBytes = await getSizeWithDir(dirPath);
405
+ } else {
406
+ sizeBytes = await getSizeWithDu(dirPath);
407
+ }
408
+ const packageCounts = await countPackagesNative(dirPath);
409
+ if (sizeBytes !== null && packageCounts !== null) {
410
+ return {
411
+ bytes: sizeBytes,
412
+ packageCount: packageCounts.topLevel,
413
+ totalPackageCount: packageCounts.total,
414
+ isNative: true
415
+ };
416
+ }
250
417
  return {
251
- path: nodeModulesPath,
252
- projectPath,
253
- projectName,
254
- projectVersion,
255
- sizeBytes: totalSize,
256
- sizeFormatted: formatBytes(totalSize),
257
- packageCount,
258
- totalPackageCount,
259
- lastModified: stats.mtime,
260
- lastModifiedFormatted: formatRelativeTime(stats.mtime),
261
- selected: false,
262
- isFavorite: false,
263
- ageCategory,
264
- sizeCategory
418
+ bytes: 0,
419
+ packageCount: 0,
420
+ totalPackageCount: 0,
421
+ isNative: false
265
422
  };
266
423
  }
267
- async function calculateDirectorySize(dirPath) {
424
+
425
+ // src/scanner.ts
426
+ var DEFAULT_CONCURRENCY = 5;
427
+ var SIZE_CALCULATION_CONCURRENCY = 3;
428
+ async function findRepoRoot(startPath) {
429
+ let currentPath = startPath;
430
+ const root = process.platform === "win32" ? "C:\\" : "/";
431
+ while (currentPath !== root && currentPath !== dirname(currentPath)) {
432
+ const gitPath = join2(currentPath, ".git");
433
+ try {
434
+ if (await fileExists(gitPath)) {
435
+ return currentPath;
436
+ }
437
+ } catch {}
438
+ currentPath = dirname(currentPath);
439
+ }
440
+ return startPath;
441
+ }
442
+ function getAgeInDays2(date) {
443
+ const now = new Date;
444
+ return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
445
+ }
446
+ async function calculateDirectorySizeFallback(dirPath) {
268
447
  let totalSize = 0;
269
448
  let packageCount = 0;
270
449
  let totalPackageCount = 0;
@@ -301,7 +480,7 @@ async function calculateDirectorySize(dirPath) {
301
480
  pathsToProcess.push(entryPath);
302
481
  }
303
482
  } catch {}
304
- } else if (stats.isSymbolicLink()) {}
483
+ }
305
484
  } catch {}
306
485
  if (currentPath === dirPath) {
307
486
  isTopLevel = false;
@@ -309,9 +488,181 @@ async function calculateDirectorySize(dirPath) {
309
488
  }
310
489
  return { totalSize, packageCount, totalPackageCount };
311
490
  }
312
- function getAgeInDays2(date) {
313
- const now = new Date;
314
- return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
491
+ async function calculateSizeWithFallback(dirPath) {
492
+ const nativeResult = await getFastDirectorySize(dirPath);
493
+ if (nativeResult.isNative && nativeResult.bytes > 0) {
494
+ return {
495
+ totalSize: nativeResult.bytes,
496
+ packageCount: nativeResult.packageCount,
497
+ totalPackageCount: nativeResult.totalPackageCount,
498
+ isNative: true
499
+ };
500
+ }
501
+ const fallbackResult = await calculateDirectorySizeFallback(dirPath);
502
+ return {
503
+ ...fallbackResult,
504
+ isNative: false
505
+ };
506
+ }
507
+ async function analyzeNodeModules(nodeModulesPath, projectPath, lazy = false) {
508
+ const stats = await fs2.stat(nodeModulesPath);
509
+ if (lazy) {
510
+ const packageJson2 = await readPackageJson(projectPath);
511
+ const projectName2 = packageJson2?.name || basename(projectPath);
512
+ const repoPath2 = await findRepoRoot(projectPath);
513
+ return {
514
+ path: nodeModulesPath,
515
+ projectPath,
516
+ projectName: projectName2,
517
+ projectVersion: packageJson2?.version,
518
+ repoPath: repoPath2,
519
+ sizeBytes: 0,
520
+ sizeFormatted: "calculating...",
521
+ packageCount: 0,
522
+ totalPackageCount: 0,
523
+ lastModified: stats.mtime,
524
+ lastModifiedFormatted: formatRelativeTime(stats.mtime),
525
+ selected: false,
526
+ isFavorite: false,
527
+ ageCategory: getAgeCategory(stats.mtime),
528
+ sizeCategory: "small",
529
+ isPending: true
530
+ };
531
+ }
532
+ const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(nodeModulesPath);
533
+ const packageJson = await readPackageJson(projectPath);
534
+ const projectName = packageJson?.name || basename(projectPath);
535
+ const repoPath = await findRepoRoot(projectPath);
536
+ const sizeCategory = getSizeCategory(totalSize);
537
+ const ageCategory = getAgeCategory(stats.mtime);
538
+ return {
539
+ path: nodeModulesPath,
540
+ projectPath,
541
+ projectName,
542
+ projectVersion: packageJson?.version,
543
+ repoPath,
544
+ sizeBytes: totalSize,
545
+ sizeFormatted: formatBytes(totalSize),
546
+ packageCount,
547
+ totalPackageCount,
548
+ lastModified: stats.mtime,
549
+ lastModifiedFormatted: formatRelativeTime(stats.mtime),
550
+ selected: false,
551
+ isFavorite: false,
552
+ ageCategory,
553
+ sizeCategory,
554
+ isNativeCalculation: isNative
555
+ };
556
+ }
557
+ async function updateNodeModulesSize(info) {
558
+ const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(info.path);
559
+ return {
560
+ ...info,
561
+ sizeBytes: totalSize,
562
+ sizeFormatted: formatBytes(totalSize),
563
+ packageCount,
564
+ totalPackageCount,
565
+ sizeCategory: getSizeCategory(totalSize),
566
+ isPending: false,
567
+ isNativeCalculation: isNative
568
+ };
569
+ }
570
+ async function scanForNodeModules(options, onProgress, lazy = false) {
571
+ const result = {
572
+ nodeModules: [],
573
+ directoriesScanned: 0,
574
+ errors: []
575
+ };
576
+ const visitedPaths = new Set;
577
+ const pathsToScan = [
578
+ { path: options.rootPath, depth: 0 }
579
+ ];
580
+ const scanLimit = pLimit(DEFAULT_CONCURRENCY);
581
+ const sizeLimit = pLimit(SIZE_CALCULATION_CONCURRENCY);
582
+ let processedCount = 0;
583
+ let totalEstimate = 1;
584
+ let foundCount = 0;
585
+ while (pathsToScan.length > 0) {
586
+ const batch = pathsToScan.splice(0, Math.min(pathsToScan.length, DEFAULT_CONCURRENCY * 2));
587
+ const scanPromises = batch.map(({ path: currentPath, depth }) => scanLimit(async () => {
588
+ if (visitedPaths.has(currentPath))
589
+ return;
590
+ if (options.maxDepth !== undefined && depth > options.maxDepth)
591
+ return;
592
+ if (shouldExcludePath(currentPath, options.excludePatterns))
593
+ return;
594
+ visitedPaths.add(currentPath);
595
+ result.directoriesScanned++;
596
+ try {
597
+ const entries = await fs2.readdir(currentPath, { withFileTypes: true });
598
+ const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
599
+ if (hasNodeModules) {
600
+ const nodeModulesPath = join2(currentPath, "node_modules");
601
+ const info = await sizeLimit(() => analyzeNodeModules(nodeModulesPath, currentPath, lazy));
602
+ const passesFilter = (!options.minSizeBytes || info.sizeBytes >= options.minSizeBytes) && (!options.olderThanDays || getAgeInDays2(info.lastModified) >= options.olderThanDays);
603
+ if (passesFilter || lazy) {
604
+ result.nodeModules.push(info);
605
+ foundCount++;
606
+ if (onProgress) {
607
+ onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
608
+ }
609
+ }
610
+ }
611
+ for (const entry of entries) {
612
+ if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
613
+ const subPath = join2(currentPath, entry.name);
614
+ if (!shouldExcludePath(subPath, options.excludePatterns)) {
615
+ pathsToScan.push({ path: subPath, depth: depth + 1 });
616
+ totalEstimate++;
617
+ }
618
+ }
619
+ }
620
+ } catch (error) {
621
+ const errorMessage = error instanceof Error ? error.message : String(error);
622
+ result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
623
+ }
624
+ processedCount++;
625
+ if (onProgress) {
626
+ onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
627
+ }
628
+ }));
629
+ await Promise.all(scanPromises);
630
+ }
631
+ if (onProgress) {
632
+ onProgress(100, foundCount);
633
+ }
634
+ return result;
635
+ }
636
+ async function calculatePendingSizes(nodeModules, onProgress) {
637
+ const pending = nodeModules.filter((nm) => nm.isPending);
638
+ if (pending.length === 0)
639
+ return nodeModules;
640
+ const limit = pLimit(SIZE_CALCULATION_CONCURRENCY);
641
+ let completed = 0;
642
+ const total = pending.length;
643
+ const updatePromises = pending.map((pendingItem) => limit(async () => {
644
+ try {
645
+ const updated = await updateNodeModulesSize(pendingItem);
646
+ completed++;
647
+ if (onProgress) {
648
+ onProgress(completed, total);
649
+ }
650
+ return updated;
651
+ } catch (error) {
652
+ completed++;
653
+ if (onProgress) {
654
+ onProgress(completed, total);
655
+ }
656
+ return {
657
+ ...pendingItem,
658
+ sizeFormatted: "error",
659
+ isPending: false
660
+ };
661
+ }
662
+ }));
663
+ const updatedItems = await Promise.all(updatePromises);
664
+ const updatedMap = new Map(updatedItems.map((item) => [item.path, item]));
665
+ return nodeModules.map((item) => updatedMap.get(item.path) || item);
315
666
  }
316
667
  async function loadIgnorePatterns() {
317
668
  const patterns = [
@@ -383,10 +734,12 @@ async function quickScan(rootPath) {
383
734
  const projectPath = currentPath;
384
735
  const nodeModulesPath = join2(currentPath, "node_modules");
385
736
  const packageJson = await readPackageJson(projectPath);
737
+ const repoPath = await findRepoRoot(projectPath);
386
738
  results.push({
387
739
  path: nodeModulesPath,
388
740
  projectPath,
389
- projectName: packageJson?.name || basename(projectPath)
741
+ projectName: packageJson?.name || basename(projectPath),
742
+ repoPath
390
743
  });
391
744
  }
392
745
  for (const entry of entries) {
@@ -558,6 +911,7 @@ function invertSelection(nodeModules) {
558
911
  // src/index.ts
559
912
  var VERSION = "1.0.0";
560
913
  export {
914
+ updateNodeModulesSize,
561
915
  toggleSelection,
562
916
  sortNodeModules,
563
917
  selectBySize,
@@ -580,6 +934,7 @@ export {
580
934
  filterNodeModules,
581
935
  deleteSelectedNodeModules,
582
936
  calculateStatistics,
937
+ calculatePendingSizes,
583
938
  analyzeNodeModules,
584
939
  VERSION,
585
940
  SIZE_THRESHOLDS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-node-modules",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Visualize, analyze, and clean up node_modules directories to reclaim disk space",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -65,6 +65,7 @@
65
65
  "dependencies": {
66
66
  "@clack/prompts": "^0.8.2",
67
67
  "commander": "^14.0.3",
68
+ "p-limit": "^7.3.0",
68
69
  "picocolors": "^1.1.1",
69
70
  "zod": "^3.23.8"
70
71
  },