d4c-pool 0.2.0
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/README.md +116 -0
- package/dist/index.js +1432 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# d4c
|
|
2
|
+
|
|
3
|
+
**Dependency Deduplication Done Dirt Cheap.**
|
|
4
|
+
|
|
5
|
+
Your `node_modules` are scattered across dozens of copies. Pool them.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx d4c-pool dedupe # make identical files share storage — deletes nothing
|
|
9
|
+
npx d4c-pool gc # delete trees nobody has touched in a month
|
|
10
|
+
npx d4c-pool history # what was removed, and how to restore it
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Both are dry-run by default.
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
On the machine this was built for:
|
|
18
|
+
|
|
19
|
+
- 976 `node_modules` trees, holding more than the disk had free
|
|
20
|
+
- `typescript/lib/typescript.js` existed in **89 identical copies**
|
|
21
|
+
- one Next.js SWC binary appeared **38 times**, 4.3 GB of the same bytes
|
|
22
|
+
|
|
23
|
+
Nothing had gone wrong. That is just what happens after a year of
|
|
24
|
+
worktrees, benchmark runs, and abandoned branches.
|
|
25
|
+
|
|
26
|
+
## `dedupe` — reclaim without losing anything
|
|
27
|
+
|
|
28
|
+
Finds files that are byte-identical across trees and makes them share
|
|
29
|
+
storage through APFS clones. Every copy stays where it is and keeps
|
|
30
|
+
working; they simply stop occupying separate blocks until one is written
|
|
31
|
+
to, at which point that one diverges on its own.
|
|
32
|
+
|
|
33
|
+
Matching is by content, not by lockfile. Twenty projects pinning
|
|
34
|
+
`react@18.3.1` have twenty different lockfiles and one identical
|
|
35
|
+
`react/index.js`. Measured on a real tree, content matching found **30.0
|
|
36
|
+
GB** where lockfile grouping found **10.9 GB**.
|
|
37
|
+
|
|
38
|
+
Because nothing is deleted, `dedupe` does not require a lockfile — which
|
|
39
|
+
makes it the only option for trees `gc` will not touch.
|
|
40
|
+
|
|
41
|
+
## `gc` — delete what can be rebuilt
|
|
42
|
+
|
|
43
|
+
Removes trees that have seen no activity past a threshold. Refuses
|
|
44
|
+
anything it cannot restore.
|
|
45
|
+
|
|
46
|
+
## What neither will touch
|
|
47
|
+
|
|
48
|
+
- Trees with recent activity, measured from `node_modules` mtime and git
|
|
49
|
+
index mtime, whichever is more recent
|
|
50
|
+
- Files any process currently has open (checked with `lsof`; if `lsof`
|
|
51
|
+
cannot run, `dedupe` does nothing rather than guess)
|
|
52
|
+
- Symlinked `node_modules` — the real tree lives elsewhere
|
|
53
|
+
- Hardlinked files, where replacing one link frees nothing
|
|
54
|
+
- Files whose permissions differ, where replacing would drop the exec bit
|
|
55
|
+
- Anything crossing a mount boundary, or matched by `--exclude`
|
|
56
|
+
- For `gc` only: trees with no lockfile able to rebuild them. A parent
|
|
57
|
+
lockfile counts only if that directory declares the tree as a workspace
|
|
58
|
+
member
|
|
59
|
+
|
|
60
|
+
Every check is re-run immediately before the destructive step, never
|
|
61
|
+
trusted from the earlier scan.
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
d4c dedupe [options]
|
|
67
|
+
|
|
68
|
+
--days N Idle threshold in days (default 30, minimum 1)
|
|
69
|
+
--min-file SIZE Ignore files smaller than this (default 64K)
|
|
70
|
+
--skip-open-check Proceed without checking for open files
|
|
71
|
+
--exclude PATTERN Protect matching paths. Repeatable, full match
|
|
72
|
+
--yes Apply. Without it nothing changes
|
|
73
|
+
--json Machine-readable output on stdout only
|
|
74
|
+
|
|
75
|
+
d4c gc [options]
|
|
76
|
+
|
|
77
|
+
--days N Idle threshold in days (default 30, minimum 1)
|
|
78
|
+
--min-size SIZE Ignore trees smaller than this (e.g. 10M, 1G)
|
|
79
|
+
--exclude PATTERN Protect matching paths. Repeatable, full match
|
|
80
|
+
--depth N How deep to search (default 8)
|
|
81
|
+
--yes Actually delete
|
|
82
|
+
--json Machine-readable output on stdout only
|
|
83
|
+
|
|
84
|
+
d4c history [--limit N] [--json]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Read the numbers as a lower bound
|
|
88
|
+
|
|
89
|
+
Idle age comes from mtime, which records the last install or git
|
|
90
|
+
operation — not the last read. **A project you still use can look idle,
|
|
91
|
+
and a long-running process leaves no trace at all.** Check the list before
|
|
92
|
+
passing `--yes`.
|
|
93
|
+
|
|
94
|
+
Sizes come from `st_blocks`, tracking what deletion actually frees, within
|
|
95
|
+
about 3% of measured freed space. Files already sharing blocks through
|
|
96
|
+
APFS clones are over-reported, since that sharing is invisible to
|
|
97
|
+
`st_blocks`.
|
|
98
|
+
|
|
99
|
+
## Recovery
|
|
100
|
+
|
|
101
|
+
`gc` appends every deletion to `~/.local/state/d4c/deletions.jsonl` as it
|
|
102
|
+
happens, so an interrupted run is still fully recorded. `d4c history`
|
|
103
|
+
prints each project with the install command that rebuilds it, matched to
|
|
104
|
+
the lockfile that was there.
|
|
105
|
+
|
|
106
|
+
`dedupe` records its replacements to the same log. It has nothing to
|
|
107
|
+
recover from — the files are still there.
|
|
108
|
+
|
|
109
|
+
## Requirements
|
|
110
|
+
|
|
111
|
+
macOS with APFS for `dedupe`, which uses `clonefile(2)` through FFI. `gc`
|
|
112
|
+
runs anywhere. Node 20 or newer.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|