oh-my-node-modules 1.3.2 → 1.3.3

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/cli.js CHANGED
@@ -3484,7 +3484,10 @@ async function isNodeModulesInUse(path) {
3484
3484
 
3485
3485
  // src/deletion.ts
3486
3486
  import { promises as fs3 } from "fs";
3487
- import { join as join3 } from "path";
3487
+ import { join as join3, resolve } from "path";
3488
+ import { exec as exec2 } from "child_process";
3489
+ import { promisify as promisify2 } from "util";
3490
+ var execAsync2 = promisify2(exec2);
3488
3491
  async function deleteSelectedNodeModules(nodeModules, options, onProgress) {
3489
3492
  const selected = nodeModules.filter((nm) => nm.selected);
3490
3493
  const result = {
@@ -3547,8 +3550,26 @@ async function deleteNodeModules(nodeModules, options) {
3547
3550
  if (options.dryRun) {
3548
3551
  detail.success = true;
3549
3552
  } else {
3550
- await fs3.rm(nodeModules.path, { recursive: true, force: true });
3551
- detail.success = true;
3553
+ try {
3554
+ if (options.force) {
3555
+ await forceDelete(nodeModules.path);
3556
+ } else {
3557
+ await fs3.rm(nodeModules.path, { recursive: true, force: true });
3558
+ }
3559
+ detail.success = true;
3560
+ } catch (rmError) {
3561
+ const errorCode = rmError.code;
3562
+ const errorMessage = rmError.message;
3563
+ if (errorCode === "EPERM" || errorCode === "EACCES") {
3564
+ detail.error = "Permission denied - run as Administrator or check file permissions";
3565
+ } else if (errorCode === "EBUSY") {
3566
+ detail.error = "Directory in use - close any programs using these files";
3567
+ } else if (errorMessage?.includes("ENOTEMPTY")) {
3568
+ detail.error = "Directory not empty - may contain read-only files. Try using --force";
3569
+ } else {
3570
+ detail.error = errorMessage || "Unknown error during deletion";
3571
+ }
3572
+ }
3552
3573
  }
3553
3574
  detail.durationMs = Date.now() - startTime;
3554
3575
  } catch (error) {
@@ -3557,16 +3578,55 @@ async function deleteNodeModules(nodeModules, options) {
3557
3578
  }
3558
3579
  return detail;
3559
3580
  }
3560
- async function verifyNodeModules(path) {
3581
+ async function forceDelete(dirPath) {
3582
+ const isWindows = process.platform === "win32";
3583
+ try {
3584
+ await fs3.rm(dirPath, { recursive: true, force: true });
3585
+ return;
3586
+ } catch {}
3587
+ try {
3588
+ await makeWritableRecursive(dirPath);
3589
+ await fs3.rm(dirPath, { recursive: true, force: true });
3590
+ return;
3591
+ } catch {}
3592
+ try {
3593
+ if (isWindows) {
3594
+ const windowsPath = dirPath.length > 240 ? `\\\\?\\${resolve(dirPath)}` : dirPath;
3595
+ await execAsync2(`rd /s /q "${windowsPath}"`, { timeout: 30000 });
3596
+ } else {
3597
+ await execAsync2(`rm -rf "${dirPath}"`, { timeout: 30000 });
3598
+ }
3599
+ return;
3600
+ } catch {}
3601
+ const tempPath = `${dirPath}.old.${Date.now()}`;
3602
+ await fs3.rename(dirPath, tempPath);
3603
+ try {
3604
+ await fs3.rm(tempPath, { recursive: true, force: true });
3605
+ } catch {}
3606
+ }
3607
+ async function makeWritableRecursive(dirPath) {
3561
3608
  try {
3562
- const parts = path.split("/");
3563
- if (parts[parts.length - 1] !== "node_modules") {
3609
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true });
3610
+ for (const entry of entries) {
3611
+ const fullPath = join3(dirPath, entry.name);
3612
+ if (entry.isDirectory()) {
3613
+ await makeWritableRecursive(fullPath);
3614
+ }
3615
+ await fs3.chmod(fullPath, 511).catch(() => {});
3616
+ }
3617
+ await fs3.chmod(dirPath, 511).catch(() => {});
3618
+ } catch {}
3619
+ }
3620
+ async function verifyNodeModules(dirPath) {
3621
+ try {
3622
+ const baseName = dirPath.replace(/\\/g, "/").split("/").pop();
3623
+ if (baseName !== "node_modules") {
3564
3624
  return false;
3565
3625
  }
3566
- const entries = await fs3.readdir(path);
3626
+ const entries = await fs3.readdir(dirPath);
3567
3627
  let hasSubdirs = false;
3568
3628
  for (const entry of entries) {
3569
- const entryPath = join3(path, entry);
3629
+ const entryPath = join3(dirPath, entry);
3570
3630
  try {
3571
3631
  const stats = await fs3.stat(entryPath);
3572
3632
  if (stats.isDirectory()) {
@@ -3575,7 +3635,8 @@ async function verifyNodeModules(path) {
3575
3635
  }
3576
3636
  } catch {}
3577
3637
  }
3578
- const parentPath = path.replace(/\/node_modules$/, "").replace(/\\node_modules$/, "");
3638
+ const normalizedPath = dirPath.replace(/\\/g, "/");
3639
+ const parentPath = normalizedPath.replace(/\/node_modules$/, "");
3579
3640
  const hasPackageJson = await fileExists(join3(parentPath, "package.json"));
3580
3641
  return hasSubdirs || hasPackageJson;
3581
3642
  } catch {
@@ -3606,7 +3667,7 @@ function formatItem(item) {
3606
3667
  const warning = !isPending && item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
3607
3668
  return `${size} ${item.projectName}${warning} [${age}]`;
3608
3669
  }
3609
- async function interactiveMode(rootPath) {
3670
+ async function interactiveMode(rootPath, force = false) {
3610
3671
  pe(import_picocolors2.default.cyan("oh-my-node-modules"));
3611
3672
  const s = _2();
3612
3673
  s.start("Scanning for node_modules...");
@@ -3682,10 +3743,22 @@ ${import_picocolors2.default.gray("Total:")} ${import_picocolors2.default.white(
3682
3743
  const result2 = await deleteSelectedNodeModules(items, {
3683
3744
  dryRun: false,
3684
3745
  yes: true,
3746
+ force,
3685
3747
  checkRunningProcesses: false,
3686
3748
  showProgress: false
3687
3749
  });
3688
3750
  ds.stop(`Deleted ${result2.successful}/${result2.totalAttempted} directories`);
3751
+ const failedDeletions = result2.details.filter((d2) => !d2.success && d2.error);
3752
+ if (failedDeletions.length > 0) {
3753
+ console.log(import_picocolors2.default.red(`
3754
+ Failed to delete ${failedDeletions.length} directories:`));
3755
+ for (const detail of failedDeletions.slice(0, 5)) {
3756
+ console.log(import_picocolors2.default.red(` • ${detail.nodeModules.projectName}: ${detail.error}`));
3757
+ }
3758
+ if (failedDeletions.length > 5) {
3759
+ console.log(import_picocolors2.default.red(` ... and ${failedDeletions.length - 5} more`));
3760
+ }
3761
+ }
3689
3762
  ge(import_picocolors2.default.green(`Freed ${result2.formattedBytesFreed}`));
3690
3763
  return;
3691
3764
  }
@@ -3735,7 +3808,7 @@ ${import_picocolors2.default.gray("Total size:")} ${import_picocolors2.default.c
3735
3808
  process.exit(1);
3736
3809
  }
3737
3810
  }
3738
- async function autoDeleteMode(rootPath, minSize, dryRun = false) {
3811
+ async function autoDeleteMode(rootPath, minSize, dryRun = false, force = false) {
3739
3812
  const s = _2();
3740
3813
  s.start("Scanning...");
3741
3814
  try {
@@ -3781,8 +3854,9 @@ Dry run - no files deleted.`));
3781
3854
  const ds = _2();
3782
3855
  ds.start("Deleting...");
3783
3856
  const result2 = await deleteSelectedNodeModules(items, {
3784
- dryRun: false,
3857
+ dryRun,
3785
3858
  yes: true,
3859
+ force,
3786
3860
  checkRunningProcesses: false,
3787
3861
  showProgress: false
3788
3862
  });
@@ -3796,13 +3870,13 @@ Dry run - no files deleted.`));
3796
3870
  }
3797
3871
  }
3798
3872
  var program2 = new Command;
3799
- 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) => {
3873
+ 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("--force", "force delete (handles read-only files and long paths)").option("--json", "output as JSON").action(async (path, options) => {
3800
3874
  if (options.scan) {
3801
3875
  await quickScanMode(path, options.json);
3802
3876
  } else if (options.auto) {
3803
- await autoDeleteMode(path, options.minSize, options.dryRun);
3877
+ await autoDeleteMode(path, options.minSize, options.dryRun, options.force);
3804
3878
  } else {
3805
- await interactiveMode(path);
3879
+ await interactiveMode(path, options.force);
3806
3880
  }
3807
3881
  });
3808
3882
  program2.parse();
package/dist/index.js CHANGED
@@ -765,7 +765,10 @@ async function quickScan(rootPath) {
765
765
  }
766
766
  // src/deletion.ts
767
767
  import { promises as fs3 } from "fs";
768
- import { join as join3 } from "path";
768
+ import { join as join3, resolve } from "path";
769
+ import { exec as exec2 } from "child_process";
770
+ import { promisify as promisify2 } from "util";
771
+ var execAsync2 = promisify2(exec2);
769
772
  async function deleteSelectedNodeModules(nodeModules, options, onProgress) {
770
773
  const selected = nodeModules.filter((nm) => nm.selected);
771
774
  const result = {
@@ -828,8 +831,26 @@ async function deleteNodeModules(nodeModules, options) {
828
831
  if (options.dryRun) {
829
832
  detail.success = true;
830
833
  } else {
831
- await fs3.rm(nodeModules.path, { recursive: true, force: true });
832
- detail.success = true;
834
+ try {
835
+ if (options.force) {
836
+ await forceDelete(nodeModules.path);
837
+ } else {
838
+ await fs3.rm(nodeModules.path, { recursive: true, force: true });
839
+ }
840
+ detail.success = true;
841
+ } catch (rmError) {
842
+ const errorCode = rmError.code;
843
+ const errorMessage = rmError.message;
844
+ if (errorCode === "EPERM" || errorCode === "EACCES") {
845
+ detail.error = "Permission denied - run as Administrator or check file permissions";
846
+ } else if (errorCode === "EBUSY") {
847
+ detail.error = "Directory in use - close any programs using these files";
848
+ } else if (errorMessage?.includes("ENOTEMPTY")) {
849
+ detail.error = "Directory not empty - may contain read-only files. Try using --force";
850
+ } else {
851
+ detail.error = errorMessage || "Unknown error during deletion";
852
+ }
853
+ }
833
854
  }
834
855
  detail.durationMs = Date.now() - startTime;
835
856
  } catch (error) {
@@ -838,16 +859,55 @@ async function deleteNodeModules(nodeModules, options) {
838
859
  }
839
860
  return detail;
840
861
  }
841
- async function verifyNodeModules(path) {
862
+ async function forceDelete(dirPath) {
863
+ const isWindows = process.platform === "win32";
864
+ try {
865
+ await fs3.rm(dirPath, { recursive: true, force: true });
866
+ return;
867
+ } catch {}
868
+ try {
869
+ await makeWritableRecursive(dirPath);
870
+ await fs3.rm(dirPath, { recursive: true, force: true });
871
+ return;
872
+ } catch {}
873
+ try {
874
+ if (isWindows) {
875
+ const windowsPath = dirPath.length > 240 ? `\\\\?\\${resolve(dirPath)}` : dirPath;
876
+ await execAsync2(`rd /s /q "${windowsPath}"`, { timeout: 30000 });
877
+ } else {
878
+ await execAsync2(`rm -rf "${dirPath}"`, { timeout: 30000 });
879
+ }
880
+ return;
881
+ } catch {}
882
+ const tempPath = `${dirPath}.old.${Date.now()}`;
883
+ await fs3.rename(dirPath, tempPath);
884
+ try {
885
+ await fs3.rm(tempPath, { recursive: true, force: true });
886
+ } catch {}
887
+ }
888
+ async function makeWritableRecursive(dirPath) {
889
+ try {
890
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true });
891
+ for (const entry of entries) {
892
+ const fullPath = join3(dirPath, entry.name);
893
+ if (entry.isDirectory()) {
894
+ await makeWritableRecursive(fullPath);
895
+ }
896
+ await fs3.chmod(fullPath, 511).catch(() => {});
897
+ }
898
+ await fs3.chmod(dirPath, 511).catch(() => {});
899
+ } catch {}
900
+ }
901
+ async function verifyNodeModules(dirPath) {
842
902
  try {
843
- const parts = path.split("/");
844
- if (parts[parts.length - 1] !== "node_modules") {
903
+ const baseName = dirPath.replace(/\\/g, "/").split("/").pop();
904
+ if (baseName !== "node_modules") {
845
905
  return false;
846
906
  }
847
- const entries = await fs3.readdir(path);
907
+ const entries = await fs3.readdir(dirPath);
848
908
  let hasSubdirs = false;
849
909
  for (const entry of entries) {
850
- const entryPath = join3(path, entry);
910
+ const entryPath = join3(dirPath, entry);
851
911
  try {
852
912
  const stats = await fs3.stat(entryPath);
853
913
  if (stats.isDirectory()) {
@@ -856,7 +916,8 @@ async function verifyNodeModules(path) {
856
916
  }
857
917
  } catch {}
858
918
  }
859
- const parentPath = path.replace(/\/node_modules$/, "").replace(/\\node_modules$/, "");
919
+ const normalizedPath = dirPath.replace(/\\/g, "/");
920
+ const parentPath = normalizedPath.replace(/\/node_modules$/, "");
860
921
  const hasPackageJson = await fileExists(join3(parentPath, "package.json"));
861
922
  return hasSubdirs || hasPackageJson;
862
923
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-node-modules",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
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",