grepmax 0.26.12 → 0.26.14
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/.claude-plugin/marketplace.json +1 -1
- package/dist/commands/doctor.js +54 -8
- package/dist/config.js +14 -1
- package/dist/lib/daemon/daemon.js +5 -1
- package/dist/lib/store/vector-db.js +78 -0
- package/dist/lib/utils/root-availability.js +105 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "grepmax",
|
|
11
11
|
"source": "./plugins/grepmax",
|
|
12
12
|
"description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
|
|
13
|
-
"version": "0.26.
|
|
13
|
+
"version": "0.26.14",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
package/dist/commands/doctor.js
CHANGED
|
@@ -56,6 +56,7 @@ const lock_1 = require("../lib/utils/lock");
|
|
|
56
56
|
const mlx_hf_cache_1 = require("../lib/utils/mlx-hf-cache");
|
|
57
57
|
const project_registry_1 = require("../lib/utils/project-registry");
|
|
58
58
|
const project_root_1 = require("../lib/utils/project-root");
|
|
59
|
+
const root_availability_1 = require("../lib/utils/root-availability");
|
|
59
60
|
function formatSize(bytes) {
|
|
60
61
|
if (bytes < 1024)
|
|
61
62
|
return `${bytes} B`;
|
|
@@ -197,8 +198,9 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
197
198
|
let needsOptimize = false;
|
|
198
199
|
let staleLock = false;
|
|
199
200
|
const orphanedProjects = [];
|
|
201
|
+
const unavailableProjects = [];
|
|
200
202
|
try {
|
|
201
|
-
const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
|
|
203
|
+
const { VectorDB, findStaleLanceTempFiles, sweepStaleLanceTempFiles } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
|
|
202
204
|
const db = new VectorDB(config_1.PATHS.lancedbDir);
|
|
203
205
|
const table = yield db.ensureTable();
|
|
204
206
|
const totalChunks = yield table.countRows();
|
|
@@ -250,15 +252,33 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
250
252
|
// Daemon status
|
|
251
253
|
const { isDaemonRunning } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
252
254
|
const daemonUp = yield isDaemonRunning();
|
|
253
|
-
// Project registry health
|
|
255
|
+
// Project registry health.
|
|
256
|
+
//
|
|
257
|
+
// A root can be unreadable for two very different reasons, and only one of
|
|
258
|
+
// them is the project's fault. An unplugged external drive or a downed
|
|
259
|
+
// network share leaves the project perfectly intact — deregistering it
|
|
260
|
+
// would strand its vectors in the shared table with no owning project and
|
|
261
|
+
// would not come back when the volume returns. Only a root whose volume is
|
|
262
|
+
// mounted, and whose directory is still gone, is actually orphaned.
|
|
254
263
|
const projects = (0, project_registry_1.listProjects)();
|
|
255
264
|
for (const p of projects) {
|
|
256
|
-
|
|
265
|
+
const availability = (0, root_availability_1.classifyRoot)(p.root);
|
|
266
|
+
if (availability === "missing")
|
|
257
267
|
orphanedProjects.push(p.root);
|
|
258
|
-
|
|
268
|
+
else if (availability === "unavailable")
|
|
269
|
+
unavailableProjects.push(p.root);
|
|
259
270
|
}
|
|
260
271
|
// Compute warning flags
|
|
261
|
-
|
|
272
|
+
//
|
|
273
|
+
// Abandoned `.tmp*` scratch files are disk that optimize provably cannot
|
|
274
|
+
// reclaim (no manifest entry, so `deleteUnverified` will not touch them).
|
|
275
|
+
// They get their own remediation — a sweep — and are excluded from the
|
|
276
|
+
// ratio that decides whether to run a whole-table optimize, because
|
|
277
|
+
// counting them there prescribes a slow rewrite for bytes it cannot free.
|
|
278
|
+
const staleTempFiles = findStaleLanceTempFiles(config_1.PATHS.lancedbDir);
|
|
279
|
+
const staleTempBytes = staleTempFiles.reduce((n, f) => n + f.size, 0);
|
|
280
|
+
const reclaimableDiskSize = Math.max(0, diskSize - staleTempBytes);
|
|
281
|
+
const bloatRatio = logicalSize > 0 ? reclaimableDiskSize / logicalSize : 0;
|
|
262
282
|
if (bloatRatio > 2.0)
|
|
263
283
|
needsOptimize = true;
|
|
264
284
|
// Track the compactor's own threshold. Warning (and worse, --fix forcing a
|
|
@@ -304,6 +324,9 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
304
324
|
`lock=${lockStatus.split(" ")[0]}`,
|
|
305
325
|
`daemon=${daemonUp ? "running" : "stopped"}`,
|
|
306
326
|
`orphaned=${orphanedProjects.length}`,
|
|
327
|
+
`offline=${unavailableProjects.length}`,
|
|
328
|
+
`stale_temp=${staleTempFiles.length}`,
|
|
329
|
+
`stale_temp_bytes=${staleTempBytes}`,
|
|
307
330
|
`stale_chunker=${staleChunkerProjects.length}`,
|
|
308
331
|
`stale_embedding=${staleEmbeddingProjects.length}`,
|
|
309
332
|
`legacy_embedding=${legacyEmbeddingCount}`,
|
|
@@ -380,11 +403,15 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
380
403
|
console.log(`ok Disk: ${formatSize(availBytes)} available`);
|
|
381
404
|
}
|
|
382
405
|
// Storage
|
|
406
|
+
const storageLine = `${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk`;
|
|
383
407
|
if (bloatRatio > 2.0) {
|
|
384
|
-
console.log(`WARN Storage: ${
|
|
408
|
+
console.log(`WARN Storage: ${storageLine} (${bloatRatio.toFixed(1)}x — orphaned files)`);
|
|
385
409
|
}
|
|
386
410
|
else {
|
|
387
|
-
console.log(`ok Storage: ${
|
|
411
|
+
console.log(`ok Storage: ${storageLine}`);
|
|
412
|
+
}
|
|
413
|
+
if (staleTempFiles.length > 0) {
|
|
414
|
+
console.log(`WARN Abandoned temp files: ${staleTempFiles.length} (${formatSize(staleTempBytes)}) — leftover from interrupted compaction, run with --fix`);
|
|
388
415
|
}
|
|
389
416
|
// Fragments
|
|
390
417
|
if (numSmallFragments > config_1.FRAGMENT_COMPACT_THRESHOLD) {
|
|
@@ -440,13 +467,19 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
440
467
|
if (legacyNotice)
|
|
441
468
|
console.log(legacyNotice);
|
|
442
469
|
// Projects
|
|
470
|
+
if (unavailableProjects.length > 0) {
|
|
471
|
+
console.log(`INFO Offline projects: ${unavailableProjects.length} (volume not mounted — kept, reconnect to resume)`);
|
|
472
|
+
for (const up of unavailableProjects) {
|
|
473
|
+
console.log(` - ${up}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
443
476
|
if (orphanedProjects.length > 0) {
|
|
444
477
|
console.log(`WARN Orphaned projects: ${orphanedProjects.length} (directories no longer exist)`);
|
|
445
478
|
for (const op of orphanedProjects) {
|
|
446
479
|
console.log(` - ${op}`);
|
|
447
480
|
}
|
|
448
481
|
}
|
|
449
|
-
else if (projects.length > 0) {
|
|
482
|
+
else if (projects.length > 0 && unavailableProjects.length === 0) {
|
|
450
483
|
console.log(`ok Projects: ${projects.length} registered, all directories exist`);
|
|
451
484
|
}
|
|
452
485
|
// Cache Coherence
|
|
@@ -500,6 +533,16 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
500
533
|
console.log("ok Removed stale lock");
|
|
501
534
|
fixed++;
|
|
502
535
|
}
|
|
536
|
+
// Sweep before optimize: these bytes are unreachable by optimize, and
|
|
537
|
+
// clearing them first means any bloat still visible afterwards is real.
|
|
538
|
+
// Unlike optimize this needs no single-writer routing — the age gate in
|
|
539
|
+
// findStaleLanceTempFiles already excludes anything a writer could hold.
|
|
540
|
+
if (staleTempFiles.length > 0) {
|
|
541
|
+
const swept = sweepStaleLanceTempFiles(config_1.PATHS.lancedbDir);
|
|
542
|
+
if (!opts.agent)
|
|
543
|
+
console.log(`ok Removed ${swept.filesRemoved} abandoned temp file(s), freed ${formatSize(swept.bytesFreed)}`);
|
|
544
|
+
fixed++;
|
|
545
|
+
}
|
|
503
546
|
if (needsOptimize) {
|
|
504
547
|
if (!opts.agent)
|
|
505
548
|
console.log("... Running optimize (compact + prune)...");
|
|
@@ -538,6 +581,9 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
538
581
|
console.log(`ok Optimize complete (${via})`);
|
|
539
582
|
fixed++;
|
|
540
583
|
}
|
|
584
|
+
// Only roots whose volume is mounted and whose directory is still gone
|
|
585
|
+
// reach this list — projects on an unplugged drive are classified
|
|
586
|
+
// `unavailable` and never enter it, so --fix cannot deregister them.
|
|
541
587
|
if (orphanedProjects.length > 0) {
|
|
542
588
|
for (const op of orphanedProjects) {
|
|
543
589
|
(0, project_registry_1.removeProject)(op);
|
package/dist/config.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.INDEXABLE_EXTENSIONS = exports.VERSION_PRUNE_THRESHOLD = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.REBUILD_COMMAND = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
|
|
36
|
+
exports.INDEXABLE_EXTENSIONS = exports.STALE_TEMP_FILE_AGE_MS = exports.VERSION_PRUNE_THRESHOLD = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.REBUILD_COMMAND = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
|
|
37
37
|
exports.describeChunkerGap = describeChunkerGap;
|
|
38
38
|
exports.describeEmbeddingGap = describeEmbeddingGap;
|
|
39
39
|
exports.describeSchemaDimGap = describeSchemaDimGap;
|
|
@@ -281,6 +281,19 @@ exports.FRAGMENT_COMPACT_THRESHOLD = 400;
|
|
|
281
281
|
// fire on every healthy store — and make `doctor --fix` force the whole-table
|
|
282
282
|
// rewrite that the fragment threshold exists to avoid.
|
|
283
283
|
exports.VERSION_PRUNE_THRESHOLD = 2000;
|
|
284
|
+
// Age past which a Lance `.tmp*` scratch file in a `*.lance/data` directory is
|
|
285
|
+
// considered abandoned rather than in flight. Lance writes a new fragment to a
|
|
286
|
+
// dot-prefixed temp file and renames it on success, so a killed optimize leaves
|
|
287
|
+
// the temp behind forever: it is in no manifest, so `optimize({deleteUnverified})`
|
|
288
|
+
// never collects it, and the dot prefix hides it from a plain `ls`. Three such
|
|
289
|
+
// files (7.3 GB, up to 2.5 months old) were what kept this store pinned at a 2.1x
|
|
290
|
+
// disk/logical ratio that repeated optimize passes could not move.
|
|
291
|
+
//
|
|
292
|
+
// The age gate is the whole safety argument: an in-flight write keeps bumping its
|
|
293
|
+
// temp file's mtime, so anything this stale has no writer, and deleting it is safe
|
|
294
|
+
// without knowing how many processes are on the store. Observed optimize runs on a
|
|
295
|
+
// 325k-row store peak at ~1.4 min, so an hour is ~40x the worst case seen.
|
|
296
|
+
exports.STALE_TEMP_FILE_AGE_MS = 60 * 60 * 1000;
|
|
284
297
|
// Extensions we consider for indexing to avoid binary noise and improve relevance.
|
|
285
298
|
exports.INDEXABLE_EXTENSIONS = new Set([
|
|
286
299
|
".ts",
|
|
@@ -67,6 +67,7 @@ const logger_1 = require("../utils/logger");
|
|
|
67
67
|
const operation_coordinator_1 = require("../utils/operation-coordinator");
|
|
68
68
|
const process_1 = require("../utils/process");
|
|
69
69
|
const project_registry_1 = require("../utils/project-registry");
|
|
70
|
+
const root_availability_1 = require("../utils/root-availability");
|
|
70
71
|
const watcher_store_1 = require("../utils/watcher-store");
|
|
71
72
|
const pool_1 = require("../workers/pool");
|
|
72
73
|
const ipc_handler_1 = require("./ipc-handler");
|
|
@@ -352,7 +353,10 @@ class Daemon {
|
|
|
352
353
|
const indexed = allProjects.filter((p) => p.status === "indexed");
|
|
353
354
|
for (const p of indexed) {
|
|
354
355
|
if (!fs.existsSync(p.root)) {
|
|
355
|
-
|
|
356
|
+
// Skipping is already the safe behavior — the project keeps its registry
|
|
357
|
+
// entry and its vectors either way. Naming the reason keeps an unplugged
|
|
358
|
+
// drive from reading like data loss in the log.
|
|
359
|
+
console.log(`[daemon] Skipping ${path.basename(p.root)} — ${(0, root_availability_1.describeRoot)(p.root)}`);
|
|
356
360
|
continue;
|
|
357
361
|
}
|
|
358
362
|
try {
|
|
@@ -43,6 +43,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.VectorDB = exports.DiskPressureError = void 0;
|
|
46
|
+
exports.findStaleLanceTempFiles = findStaleLanceTempFiles;
|
|
47
|
+
exports.sweepStaleLanceTempFiles = sweepStaleLanceTempFiles;
|
|
46
48
|
exports.isLanceCorruptionError = isLanceCorruptionError;
|
|
47
49
|
exports.isMissingTableError = isMissingTableError;
|
|
48
50
|
const fs = __importStar(require("node:fs"));
|
|
@@ -55,6 +57,73 @@ const filter_builder_1 = require("../utils/filter-builder");
|
|
|
55
57
|
const logger_1 = require("../utils/logger");
|
|
56
58
|
const ann_config_1 = require("./ann-config");
|
|
57
59
|
const store_lease_1 = require("./store-lease");
|
|
60
|
+
/**
|
|
61
|
+
* Find abandoned Lance scratch files under `<lancedbDir>/ *.lance/data`.
|
|
62
|
+
*
|
|
63
|
+
* Lance stages a new fragment in a dot-prefixed `.tmp*` file and renames it into
|
|
64
|
+
* place on success. A killed or panicking optimize leaves the temp behind, and
|
|
65
|
+
* nothing ever collects it: it appears in no manifest, so `deleteUnverified`
|
|
66
|
+
* cannot prove it is garbage, and the leading dot hides it from `ls`. The result
|
|
67
|
+
* is permanent disk bloat that no amount of optimizing reclaims.
|
|
68
|
+
*
|
|
69
|
+
* Only files older than `minAgeMs` are reported. An in-flight write keeps
|
|
70
|
+
* bumping its temp file's mtime, so the age gate — not a writer count — is what
|
|
71
|
+
* makes deleting the result safe from any process.
|
|
72
|
+
*/
|
|
73
|
+
function findStaleLanceTempFiles(lancedbDir, minAgeMs = config_1.STALE_TEMP_FILE_AGE_MS) {
|
|
74
|
+
const found = [];
|
|
75
|
+
const cutoff = Date.now() - minAgeMs;
|
|
76
|
+
let tables;
|
|
77
|
+
try {
|
|
78
|
+
tables = fs.readdirSync(lancedbDir);
|
|
79
|
+
}
|
|
80
|
+
catch (_a) {
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
for (const tableDir of tables) {
|
|
84
|
+
if (!tableDir.endsWith(".lance"))
|
|
85
|
+
continue;
|
|
86
|
+
const dataDir = `${lancedbDir}/${tableDir}/data`;
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = fs.readdirSync(dataDir);
|
|
90
|
+
}
|
|
91
|
+
catch (_b) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.startsWith(".tmp"))
|
|
96
|
+
continue;
|
|
97
|
+
const full = `${dataDir}/${entry}`;
|
|
98
|
+
try {
|
|
99
|
+
const s = fs.statSync(full);
|
|
100
|
+
if (!s.isFile() || s.mtimeMs > cutoff)
|
|
101
|
+
continue;
|
|
102
|
+
found.push({ path: full, size: s.size, mtimeMs: s.mtimeMs });
|
|
103
|
+
}
|
|
104
|
+
catch (_c) { }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Delete abandoned Lance scratch files. Returns bytes actually reclaimed —
|
|
111
|
+
* a file that vanishes between the scan and the unlink is not counted, so the
|
|
112
|
+
* number reported is never larger than what was freed.
|
|
113
|
+
*/
|
|
114
|
+
function sweepStaleLanceTempFiles(lancedbDir, minAgeMs = config_1.STALE_TEMP_FILE_AGE_MS) {
|
|
115
|
+
let filesRemoved = 0;
|
|
116
|
+
let bytesFreed = 0;
|
|
117
|
+
for (const f of findStaleLanceTempFiles(lancedbDir, minAgeMs)) {
|
|
118
|
+
try {
|
|
119
|
+
fs.unlinkSync(f.path);
|
|
120
|
+
filesRemoved++;
|
|
121
|
+
bytesFreed += f.size;
|
|
122
|
+
}
|
|
123
|
+
catch (_a) { }
|
|
124
|
+
}
|
|
125
|
+
return { filesRemoved, bytesFreed };
|
|
126
|
+
}
|
|
58
127
|
class DiskPressureError extends Error {
|
|
59
128
|
constructor(message = "Disk critically low — writes suspended") {
|
|
60
129
|
super(message);
|
|
@@ -1093,6 +1162,15 @@ class VectorDB {
|
|
|
1093
1162
|
if (this.lastOptimizeDidWork) {
|
|
1094
1163
|
const table = yield this.openExistingTableUnsafe();
|
|
1095
1164
|
if (table) {
|
|
1165
|
+
// Collect abandoned scratch files before measuring. They are pure
|
|
1166
|
+
// disk with no manifest entry, so they inflate the ratio while being
|
|
1167
|
+
// invisible to optimize — measuring first would blame the compactor
|
|
1168
|
+
// for bytes it has no way to reach and send us into a second
|
|
1169
|
+
// full-table pass that reclaims nothing.
|
|
1170
|
+
const swept = sweepStaleLanceTempFiles(this.lancedbDir);
|
|
1171
|
+
if (swept.filesRemoved > 0) {
|
|
1172
|
+
(0, logger_1.log)("vectordb", `Swept ${swept.filesRemoved} abandoned temp file(s), freed ${(swept.bytesFreed / 1024 / 1024).toFixed(1)}MB`);
|
|
1173
|
+
}
|
|
1096
1174
|
const stats = yield table.stats();
|
|
1097
1175
|
const diskSize = this.getDirectorySize(this.lancedbDir);
|
|
1098
1176
|
const logicalSize = stats.totalBytes;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.classifyRoot = classifyRoot;
|
|
37
|
+
exports.isRootUnavailable = isRootUnavailable;
|
|
38
|
+
exports.describeRoot = describeRoot;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
/**
|
|
41
|
+
* Directories that hold mount points rather than user data, with how many path
|
|
42
|
+
* segments below them the mount point itself sits.
|
|
43
|
+
*
|
|
44
|
+
* `mountDepth` errs high where a platform is ambiguous (Linux uses both
|
|
45
|
+
* `/media/<volume>` and `/media/<user>/<volume>`). Overshooting biases the
|
|
46
|
+
* classifier toward `unavailable`, which is the safe direction: a wrong
|
|
47
|
+
* `unavailable` only means we decline to auto-remove a genuinely dead project,
|
|
48
|
+
* while a wrong `missing` silently deregisters a live one.
|
|
49
|
+
*/
|
|
50
|
+
const MOUNT_CONTAINERS = [
|
|
51
|
+
{ prefix: "/Volumes", mountDepth: 1 }, // macOS
|
|
52
|
+
{ prefix: "/run/media", mountDepth: 2 }, // Linux, udisks2
|
|
53
|
+
{ prefix: "/media", mountDepth: 2 },
|
|
54
|
+
{ prefix: "/mnt", mountDepth: 1 },
|
|
55
|
+
{ prefix: "/net", mountDepth: 1 }, // autofs network mounts
|
|
56
|
+
];
|
|
57
|
+
function exists(p) {
|
|
58
|
+
try {
|
|
59
|
+
fs.statSync(p);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (_a) {
|
|
63
|
+
// A disconnected network mount can throw EIO/ETIMEDOUT rather than ENOENT.
|
|
64
|
+
// Either way the path is not readable, and the caller's container check
|
|
65
|
+
// decides whether that means unavailable or missing.
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Classify a project root that may live on removable or network storage.
|
|
71
|
+
*
|
|
72
|
+
* Longest matching container wins, so `/run/media` is not shadowed by `/media`.
|
|
73
|
+
*/
|
|
74
|
+
function classifyRoot(root) {
|
|
75
|
+
if (exists(root))
|
|
76
|
+
return "present";
|
|
77
|
+
const container = MOUNT_CONTAINERS.filter((c) => root === c.prefix || root.startsWith(`${c.prefix}/`)).sort((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
78
|
+
if (!container)
|
|
79
|
+
return "missing";
|
|
80
|
+
// The mount point is the container plus `mountDepth` segments. If it is
|
|
81
|
+
// absent, nothing is mounted there and the project is merely out of reach.
|
|
82
|
+
const rest = root.slice(container.prefix.length).split("/").filter(Boolean);
|
|
83
|
+
if (rest.length < container.mountDepth)
|
|
84
|
+
return "missing";
|
|
85
|
+
const mountPoint = `${container.prefix}/${rest.slice(0, container.mountDepth).join("/")}`;
|
|
86
|
+
return exists(mountPoint) ? "missing" : "unavailable";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* True when the root is absent only because its volume is not mounted.
|
|
90
|
+
* Callers that delete or rewrite persistent state must check this first.
|
|
91
|
+
*/
|
|
92
|
+
function isRootUnavailable(root) {
|
|
93
|
+
return classifyRoot(root) === "unavailable";
|
|
94
|
+
}
|
|
95
|
+
/** Human-readable reason for a non-present root, for logs and doctor output. */
|
|
96
|
+
function describeRoot(root) {
|
|
97
|
+
switch (classifyRoot(root)) {
|
|
98
|
+
case "present":
|
|
99
|
+
return "available";
|
|
100
|
+
case "unavailable":
|
|
101
|
+
return "volume not mounted";
|
|
102
|
+
case "missing":
|
|
103
|
+
return "directory not found";
|
|
104
|
+
}
|
|
105
|
+
}
|
package/package.json
CHANGED