d4c-pool 0.2.0 → 0.3.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/dist/index.js +199 -19
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
|
|
5
5
|
// ../core/src/reap/index.ts
|
|
6
6
|
import { lstat as lstat2, stat as stat2, rm } from "node:fs/promises";
|
|
7
|
-
import { basename, join as
|
|
7
|
+
import { basename, join as join4 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// ../core/src/fs/trees.ts
|
|
10
10
|
import { readdir, lstat, stat, readFile } from "node:fs/promises";
|
|
@@ -352,6 +352,80 @@ function restoreCommand(lockfile) {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// ../core/src/fs/native.ts
|
|
356
|
+
import { execFile } from "node:child_process";
|
|
357
|
+
import { existsSync } from "node:fs";
|
|
358
|
+
import { join as join3, dirname as dirname3 } from "node:path";
|
|
359
|
+
import { fileURLToPath } from "node:url";
|
|
360
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
361
|
+
function findBinary(explicit) {
|
|
362
|
+
if (explicit !== undefined)
|
|
363
|
+
return existsSync(explicit) ? explicit : null;
|
|
364
|
+
if (process.env.D4C_FS_BIN !== undefined) {
|
|
365
|
+
return existsSync(process.env.D4C_FS_BIN) ? process.env.D4C_FS_BIN : null;
|
|
366
|
+
}
|
|
367
|
+
const pkg = `@voidmatcha/d4c-fs-${process.platform}-${process.arch}`;
|
|
368
|
+
try {
|
|
369
|
+
const req = createRequire2(import.meta.url);
|
|
370
|
+
return req.resolve(`${pkg}/d4c-fs`);
|
|
371
|
+
} catch {}
|
|
372
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
373
|
+
const candidates = [
|
|
374
|
+
join3(here, "../../../../crates/d4c-fs/target/release/d4c-fs"),
|
|
375
|
+
join3(here, `../../../fs-${process.platform}-${process.arch}/d4c-fs`)
|
|
376
|
+
];
|
|
377
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
378
|
+
}
|
|
379
|
+
async function nativeDiskUsage(paths, opts = {}) {
|
|
380
|
+
const bin = findBinary(opts.binPath);
|
|
381
|
+
if (bin === null || paths.length === 0)
|
|
382
|
+
return null;
|
|
383
|
+
const args = ["disk-usage"];
|
|
384
|
+
for (const p of paths)
|
|
385
|
+
args.push("--path", p);
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
execFile(bin, args, { maxBuffer: 64 * 1024 * 1024, timeout: opts.timeoutMs ?? 300000 }, (err, stdout) => {
|
|
388
|
+
if (err !== null || stdout === "") {
|
|
389
|
+
resolve(null);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
resolve(JSON.parse(stdout));
|
|
394
|
+
} catch {
|
|
395
|
+
resolve(null);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
var shareBin;
|
|
401
|
+
async function fileShareInfoBatch(paths, opts = {}) {
|
|
402
|
+
if (paths.length === 0)
|
|
403
|
+
return new Map;
|
|
404
|
+
if (opts.binPath !== undefined)
|
|
405
|
+
shareBin = findBinary(opts.binPath);
|
|
406
|
+
else if (shareBin === undefined)
|
|
407
|
+
shareBin = findBinary();
|
|
408
|
+
if (shareBin === null)
|
|
409
|
+
return null;
|
|
410
|
+
const args = ["file-info"];
|
|
411
|
+
for (const p of paths)
|
|
412
|
+
args.push("--path", p);
|
|
413
|
+
return new Promise((resolve) => {
|
|
414
|
+
execFile(shareBin, args, { maxBuffer: 64 * 1024 * 1024, timeout: opts.timeoutMs ?? 120000 }, (err, stdout) => {
|
|
415
|
+
if (err !== null || stdout === "") {
|
|
416
|
+
resolve(null);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const j = JSON.parse(stdout);
|
|
421
|
+
resolve(new Map(j.files.map((f) => [f.path, f])));
|
|
422
|
+
} catch {
|
|
423
|
+
resolve(null);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
355
429
|
// ../core/src/reap/index.ts
|
|
356
430
|
function relativeTo(path, root) {
|
|
357
431
|
if (path === root)
|
|
@@ -414,6 +488,21 @@ async function planReap(root, opts = {}) {
|
|
|
414
488
|
totalBytes += c.tree.sizeBytes ?? 0;
|
|
415
489
|
opts.onMeasured?.(c.tree);
|
|
416
490
|
}
|
|
491
|
+
let measurementSource = "approximate";
|
|
492
|
+
let nativeGroupBytes = null;
|
|
493
|
+
if (candidates.length > 0 && !interrupted) {
|
|
494
|
+
const native = await nativeDiskUsage(candidates.map((c) => c.tree.path), { binPath: opts.nativeBinPath });
|
|
495
|
+
if (native !== null && native.measured) {
|
|
496
|
+
measurementSource = "native";
|
|
497
|
+
const byPath = new Map(native.paths.map((p) => [p.path, p]));
|
|
498
|
+
for (const c of candidates) {
|
|
499
|
+
const m = byPath.get(c.tree.path);
|
|
500
|
+
if (m !== undefined)
|
|
501
|
+
c.tree.sizeBytes = m.privateBytes;
|
|
502
|
+
}
|
|
503
|
+
nativeGroupBytes = native.group.occupancyBytes;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
417
506
|
const tooSmall = candidates.filter((c) => (c.tree.sizeBytes ?? 0) < minSizeBytes);
|
|
418
507
|
for (const c of tooSmall)
|
|
419
508
|
skipped.push({ tree: c.tree, reason: "below-min-size" });
|
|
@@ -423,12 +512,13 @@ async function planReap(root, opts = {}) {
|
|
|
423
512
|
candidates.sort((a, b) => (b.tree.sizeBytes ?? 0) - (a.tree.sizeBytes ?? 0));
|
|
424
513
|
return {
|
|
425
514
|
root,
|
|
515
|
+
measurementSource,
|
|
426
516
|
idleThresholdDays,
|
|
427
517
|
exclude,
|
|
428
518
|
minSizeBytes,
|
|
429
519
|
candidates,
|
|
430
520
|
skipped,
|
|
431
|
-
reclaimableBytes: candidates.reduce((s, c) => s + (c.tree.sizeBytes ?? 0), 0),
|
|
521
|
+
reclaimableBytes: nativeGroupBytes ?? candidates.reduce((s, c) => s + (c.tree.sizeBytes ?? 0), 0),
|
|
432
522
|
totalBytes,
|
|
433
523
|
treeCount: trees.length,
|
|
434
524
|
diagnostics,
|
|
@@ -457,7 +547,7 @@ async function verifyStillReapable(c) {
|
|
|
457
547
|
return "lockfile-vanished";
|
|
458
548
|
for (const f of [tree.lockfile, "package.json"]) {
|
|
459
549
|
try {
|
|
460
|
-
const st2 = await stat2(
|
|
550
|
+
const st2 = await stat2(join4(tree.lockfileDir, f));
|
|
461
551
|
if (!st2.isFile())
|
|
462
552
|
return "lockfile-vanished";
|
|
463
553
|
} catch {
|
|
@@ -581,6 +671,7 @@ async function gc(args) {
|
|
|
581
671
|
schemaVersion: 1,
|
|
582
672
|
root: args.cwd,
|
|
583
673
|
idleThresholdDays: plan.idleThresholdDays,
|
|
674
|
+
measurementSource: plan.measurementSource,
|
|
584
675
|
exclude: plan.exclude,
|
|
585
676
|
minSizeBytes: plan.minSizeBytes,
|
|
586
677
|
dryRun: result.dryRun,
|
|
@@ -666,6 +757,13 @@ async function gc(args) {
|
|
|
666
757
|
out.push("Interrupted. The trees listed above are what was handled before stopping;");
|
|
667
758
|
out.push("anything else was left untouched.");
|
|
668
759
|
}
|
|
760
|
+
if (plan.measurementSource === "approximate") {
|
|
761
|
+
out.push("");
|
|
762
|
+
out.push("Sizes are approximate. Files already sharing blocks through APFS");
|
|
763
|
+
out.push("clones are counted at full size, so the figure above can be much");
|
|
764
|
+
out.push("larger than what deleting would actually free. Build crates/d4c-fs");
|
|
765
|
+
out.push("for exact numbers.");
|
|
766
|
+
}
|
|
669
767
|
out.push("");
|
|
670
768
|
out.push("Idle age is measured from the last install or modification, not the");
|
|
671
769
|
out.push("last time the tree was read. A project you still use can look idle.");
|
|
@@ -685,11 +783,11 @@ var DEFAULT_DAYS = DEFAULT_IDLE_THRESHOLD_DAYS;
|
|
|
685
783
|
|
|
686
784
|
// ../core/src/dedupe/index.ts
|
|
687
785
|
import { lstat as lstat5, mkdir as mkdir2, appendFile as appendFile2, realpath as realpath2 } from "node:fs/promises";
|
|
688
|
-
import { dirname as
|
|
786
|
+
import { dirname as dirname5 } from "node:path";
|
|
689
787
|
|
|
690
788
|
// ../core/src/dedupe/index-builder.ts
|
|
691
789
|
import { readdir as readdir2, lstat as lstat3, open } from "node:fs/promises";
|
|
692
|
-
import { join as
|
|
790
|
+
import { join as join5 } from "node:path";
|
|
693
791
|
import { createHash } from "node:crypto";
|
|
694
792
|
var DEFAULT_MIN_FILE_BYTES = 65536;
|
|
695
793
|
async function hashFile(path) {
|
|
@@ -727,7 +825,7 @@ async function buildDuplicateIndex(treeRoots, opts = {}) {
|
|
|
727
825
|
continue;
|
|
728
826
|
}
|
|
729
827
|
for (const e of entries) {
|
|
730
|
-
const p =
|
|
828
|
+
const p = join5(dir, e.name);
|
|
731
829
|
if (e.isDirectory()) {
|
|
732
830
|
stack.push(p);
|
|
733
831
|
continue;
|
|
@@ -767,7 +865,8 @@ async function buildDuplicateIndex(treeRoots, opts = {}) {
|
|
|
767
865
|
ino: st.ino,
|
|
768
866
|
mode: st.mode & 4095,
|
|
769
867
|
mtimeMs: st.mtimeMs,
|
|
770
|
-
cloneRefs: 0
|
|
868
|
+
cloneRefs: 0,
|
|
869
|
+
cloneId: null
|
|
771
870
|
};
|
|
772
871
|
const list = byHash.get(hash) ?? [];
|
|
773
872
|
list.push(entry);
|
|
@@ -775,6 +874,26 @@ async function buildDuplicateIndex(treeRoots, opts = {}) {
|
|
|
775
874
|
}
|
|
776
875
|
}
|
|
777
876
|
}
|
|
877
|
+
const candidatePaths = [];
|
|
878
|
+
for (const all of byHash.values()) {
|
|
879
|
+
if (all.length >= 2)
|
|
880
|
+
for (const f of all)
|
|
881
|
+
candidatePaths.push(f.path);
|
|
882
|
+
}
|
|
883
|
+
const share = await fileShareInfoBatch(candidatePaths, { binPath: opts.nativeBinPath });
|
|
884
|
+
if (share !== null) {
|
|
885
|
+
for (const all of byHash.values()) {
|
|
886
|
+
for (const f of all) {
|
|
887
|
+
const s = share.get(f.path);
|
|
888
|
+
if (s === undefined || !s.measured)
|
|
889
|
+
continue;
|
|
890
|
+
if (s.privateBytes !== null)
|
|
891
|
+
f.sizeBytes = s.privateBytes;
|
|
892
|
+
f.cloneRefs = s.cloneRefcnt ?? 0;
|
|
893
|
+
f.cloneId = s.cloneId;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
778
897
|
const groups = [];
|
|
779
898
|
for (const [hash, all] of byHash) {
|
|
780
899
|
const byMode = new Map;
|
|
@@ -796,8 +915,11 @@ async function buildDuplicateIndex(treeRoots, opts = {}) {
|
|
|
796
915
|
if (same.length < 2)
|
|
797
916
|
continue;
|
|
798
917
|
const canonical = pickCanonical(same);
|
|
799
|
-
const
|
|
800
|
-
|
|
918
|
+
const targets = same.filter((f) => f !== canonical && !(f.cloneId !== null && f.cloneId === canonical.cloneId));
|
|
919
|
+
if (targets.length === 0)
|
|
920
|
+
continue;
|
|
921
|
+
const reclaimableBytes = targets.reduce((s, f) => s + f.sizeBytes, 0);
|
|
922
|
+
groups.push({ hash, files: [canonical, ...targets], canonical, reclaimableBytes });
|
|
801
923
|
}
|
|
802
924
|
}
|
|
803
925
|
}
|
|
@@ -809,11 +931,14 @@ function pickCanonical(files) {
|
|
|
809
931
|
}
|
|
810
932
|
|
|
811
933
|
// ../core/src/dedupe/replace.ts
|
|
812
|
-
import { lstat as lstat4, open as open2, rm as
|
|
813
|
-
import { dirname as
|
|
814
|
-
import { randomBytes } from "node:crypto";
|
|
934
|
+
import { lstat as lstat4, open as open2, rm as rm3, rename, chmod, utimes } from "node:fs/promises";
|
|
935
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
936
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
815
937
|
|
|
816
938
|
// ../core/src/clone/clonefile.ts
|
|
939
|
+
import { rm as rm2, stat as stat3 } from "node:fs/promises";
|
|
940
|
+
import { join as join6 } from "node:path";
|
|
941
|
+
import { randomBytes } from "node:crypto";
|
|
817
942
|
var cachedFn;
|
|
818
943
|
function loadClonefile() {
|
|
819
944
|
if (cachedFn !== undefined)
|
|
@@ -847,6 +972,35 @@ async function cloneFile(src, dst) {
|
|
|
847
972
|
throw new CloneFailedError(`clonefile failed: ${src} -> ${dst}`);
|
|
848
973
|
}
|
|
849
974
|
}
|
|
975
|
+
async function cloneSupported(dir) {
|
|
976
|
+
const tag = randomBytes(6).toString("hex");
|
|
977
|
+
const src = join6(dir, `.d4c-probe-${tag}`);
|
|
978
|
+
const dst = join6(dir, `.d4c-probe-${tag}-clone`);
|
|
979
|
+
try {
|
|
980
|
+
await Bun.write(src, "probe");
|
|
981
|
+
await cloneFile(src, dst);
|
|
982
|
+
return true;
|
|
983
|
+
} catch {
|
|
984
|
+
return false;
|
|
985
|
+
} finally {
|
|
986
|
+
await rm2(src, { force: true });
|
|
987
|
+
await rm2(dst, { force: true });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function cloneDiagnostics(dir) {
|
|
991
|
+
if (process.platform !== "darwin") {
|
|
992
|
+
return { supported: false, reason: `clonefile is macOS-only (this is ${process.platform})` };
|
|
993
|
+
}
|
|
994
|
+
if (loadClonefile() === null) {
|
|
995
|
+
return { supported: false, reason: "koffi is not installed; run `npm i koffi` to enable dedupe" };
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
await stat3(dir);
|
|
999
|
+
} catch {
|
|
1000
|
+
return { supported: false, reason: `cannot stat ${dir}` };
|
|
1001
|
+
}
|
|
1002
|
+
return await cloneSupported(dir) ? { supported: true, reason: "clonefile works here" } : { supported: false, reason: "clonefile refused on this filesystem" };
|
|
1003
|
+
}
|
|
850
1004
|
|
|
851
1005
|
// ../core/src/dedupe/replace.ts
|
|
852
1006
|
async function sameContent(a, b) {
|
|
@@ -897,11 +1051,11 @@ async function replaceWithClone(canonical, target) {
|
|
|
897
1051
|
if (!await sameContent(canonical.path, target.path)) {
|
|
898
1052
|
return { ok: false, reason: "content-changed" };
|
|
899
1053
|
}
|
|
900
|
-
const tmp =
|
|
1054
|
+
const tmp = join7(dirname4(target.path), `.d4c-tmp-${randomBytes2(6).toString("hex")}`);
|
|
901
1055
|
try {
|
|
902
1056
|
await cloneFile(canonical.path, tmp);
|
|
903
1057
|
} catch (e) {
|
|
904
|
-
await
|
|
1058
|
+
await rm3(tmp, { force: true });
|
|
905
1059
|
return { ok: false, reason: "clone-failed", detail: String(e) };
|
|
906
1060
|
}
|
|
907
1061
|
try {
|
|
@@ -909,7 +1063,7 @@ async function replaceWithClone(canonical, target) {
|
|
|
909
1063
|
await utimes(tmp, ts.atime, ts.mtime);
|
|
910
1064
|
await rename(tmp, target.path);
|
|
911
1065
|
} catch (e) {
|
|
912
|
-
await
|
|
1066
|
+
await rm3(tmp, { force: true });
|
|
913
1067
|
return { ok: false, reason: "clone-failed", detail: String(e) };
|
|
914
1068
|
}
|
|
915
1069
|
try {
|
|
@@ -923,7 +1077,7 @@ async function replaceWithClone(canonical, target) {
|
|
|
923
1077
|
}
|
|
924
1078
|
|
|
925
1079
|
// ../core/src/dedupe/open-files.ts
|
|
926
|
-
import { execFile } from "node:child_process";
|
|
1080
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
927
1081
|
import { realpath } from "node:fs/promises";
|
|
928
1082
|
async function openFilesUnder(roots, opts = {}) {
|
|
929
1083
|
if (roots.length === 0)
|
|
@@ -931,7 +1085,7 @@ async function openFilesUnder(roots, opts = {}) {
|
|
|
931
1085
|
const bin = opts.lsofPath ?? "lsof";
|
|
932
1086
|
const realRoots = await Promise.all(roots.map((r) => realpath(r).catch(() => r)));
|
|
933
1087
|
return new Promise((resolve) => {
|
|
934
|
-
|
|
1088
|
+
execFile2(bin, ["-Fn", "-w"], { maxBuffer: 64 * 1024 * 1024, timeout: opts.timeoutMs ?? 60000 }, (err, stdout) => {
|
|
935
1089
|
if (stdout === undefined || stdout === "") {
|
|
936
1090
|
resolve(err ? null : new Set);
|
|
937
1091
|
return;
|
|
@@ -970,17 +1124,24 @@ async function planDedupe(inputRoot, opts = {}) {
|
|
|
970
1124
|
treeMtimes[t.path] = (await lstat5(t.path)).mtimeMs;
|
|
971
1125
|
} catch {}
|
|
972
1126
|
}
|
|
973
|
-
const empty = (check2) => ({
|
|
1127
|
+
const empty = (check2, support = "supported", reason = "") => ({
|
|
974
1128
|
root,
|
|
975
1129
|
idleThresholdDays,
|
|
976
1130
|
trees,
|
|
977
1131
|
groups: [],
|
|
978
1132
|
reclaimableBytes: 0,
|
|
979
1133
|
openFileCheck: check2,
|
|
1134
|
+
cloneSupport: support,
|
|
1135
|
+
cloneReason: reason,
|
|
980
1136
|
excludedOpenFiles: 0,
|
|
981
1137
|
treeMtimes,
|
|
982
1138
|
interrupted: false
|
|
983
1139
|
});
|
|
1140
|
+
const probe = opts.cloneProbe ?? cloneSupported;
|
|
1141
|
+
if (!await probe(root)) {
|
|
1142
|
+
const d = await cloneDiagnostics(root);
|
|
1143
|
+
return empty(opts.skipOpenFileCheck === true ? "skipped" : "clean", "unsupported", d.reason);
|
|
1144
|
+
}
|
|
984
1145
|
if (trees.length === 0)
|
|
985
1146
|
return empty(opts.skipOpenFileCheck === true ? "skipped" : "clean");
|
|
986
1147
|
let held = new Set;
|
|
@@ -1023,6 +1184,8 @@ async function planDedupe(inputRoot, opts = {}) {
|
|
|
1023
1184
|
groups: filtered,
|
|
1024
1185
|
reclaimableBytes: filtered.reduce((s, g) => s + g.reclaimableBytes, 0),
|
|
1025
1186
|
openFileCheck: check,
|
|
1187
|
+
cloneSupport: "supported",
|
|
1188
|
+
cloneReason: "",
|
|
1026
1189
|
excludedOpenFiles,
|
|
1027
1190
|
treeMtimes,
|
|
1028
1191
|
interrupted: opts.signal?.aborted === true
|
|
@@ -1081,7 +1244,7 @@ async function executeDedupe(plan, opts = {}) {
|
|
|
1081
1244
|
freedBytes += r.freedBytes;
|
|
1082
1245
|
if (logPath !== null) {
|
|
1083
1246
|
try {
|
|
1084
|
-
await mkdir2(
|
|
1247
|
+
await mkdir2(dirname5(logPath), { recursive: true });
|
|
1085
1248
|
await appendFile2(logPath, JSON.stringify({
|
|
1086
1249
|
event: "dedupe",
|
|
1087
1250
|
at: new Date().toISOString(),
|
|
@@ -1149,6 +1312,8 @@ async function dedupe(args) {
|
|
|
1149
1312
|
dryRun: result.dryRun,
|
|
1150
1313
|
interrupted: plan.interrupted || result.interrupted,
|
|
1151
1314
|
openFileCheck: plan.openFileCheck,
|
|
1315
|
+
cloneSupport: plan.cloneSupport,
|
|
1316
|
+
cloneReason: plan.cloneReason,
|
|
1152
1317
|
excludedOpenFiles: plan.excludedOpenFiles,
|
|
1153
1318
|
treeCount: plan.trees.length,
|
|
1154
1319
|
groupCount: plan.groups.length,
|
|
@@ -1169,6 +1334,21 @@ async function dedupe(args) {
|
|
|
1169
1334
|
return 0;
|
|
1170
1335
|
}
|
|
1171
1336
|
const out = ["D4C — deduplicate identical dependency files", ""];
|
|
1337
|
+
if (plan.cloneSupport === "unsupported") {
|
|
1338
|
+
out.push("Block cloning is not available here, so nothing was examined.");
|
|
1339
|
+
out.push(` ${plan.cloneReason}`);
|
|
1340
|
+
out.push("");
|
|
1341
|
+
out.push("dedupe needs a filesystem that can share blocks between files:");
|
|
1342
|
+
out.push(" macOS APFS");
|
|
1343
|
+
out.push(" Linux btrfs or XFS (ext4 cannot)");
|
|
1344
|
+
out.push(" Windows ReFS, including a Dev Drive (NTFS cannot)");
|
|
1345
|
+
out.push("");
|
|
1346
|
+
out.push("`d4c gc` works anywhere and does not need this.");
|
|
1347
|
+
process.stdout.write(out.join(`
|
|
1348
|
+
`) + `
|
|
1349
|
+
`);
|
|
1350
|
+
return 0;
|
|
1351
|
+
}
|
|
1172
1352
|
if (plan.openFileCheck === "unavailable") {
|
|
1173
1353
|
out.push("Could not run lsof, so it is unknown whether anything has these files open.");
|
|
1174
1354
|
out.push("Replacing a file a process is writing to would lose that write silently,");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "d4c-pool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pool your scattered node_modules. Deduplicate identical files across project copies, and remove trees nothing has touched.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node_modules",
|
|
@@ -35,5 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"koffi": "^3.1.6"
|
|
38
|
+
},
|
|
39
|
+
"optionalDependencies": {
|
|
40
|
+
"@voidmatcha/d4c-fs-darwin-arm64": "^0.1.0"
|
|
38
41
|
}
|
|
39
42
|
}
|