claude-mem-lite 6.25.0 → 6.25.2
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/.claude-plugin/plugin.json +1 -1
- package/hook-shared.mjs +10 -7
- package/hook-update.mjs +243 -37
- package/hook.mjs +8 -5
- package/install.mjs +393 -87
- package/lib/doctor-stale-temp.mjs +129 -16
- package/lib/proc-lock.mjs +49 -15
- package/lib/schema-skew.mjs +5 -2
- package/lib/wal-open-blocked.mjs +69 -0
- package/mem-cli.mjs +16 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/source-files.mjs +3 -0
package/install.mjs
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
readdirSync,
|
|
17
17
|
statSync,
|
|
18
18
|
lstatSync,
|
|
19
|
-
accessSync,
|
|
20
19
|
constants as fsConstants,
|
|
21
20
|
} from 'fs';
|
|
22
21
|
import { join, resolve, dirname, basename, sep } from 'path';
|
|
@@ -73,10 +72,12 @@ import { doctorDbModeHint } from './lib/doctor-modes.mjs';
|
|
|
73
72
|
import { checkHookInterpreter } from './lib/doctor-hook-interpreter.mjs';
|
|
74
73
|
import {
|
|
75
74
|
classifyEpisodeFile,
|
|
75
|
+
classifyUpdateResidue,
|
|
76
76
|
EPISODE_AGE_LABEL,
|
|
77
77
|
isEpisodeResidue,
|
|
78
78
|
isUpdateResidue,
|
|
79
79
|
scanStaleTempFiles,
|
|
80
|
+
SWAP_RESOLVED_NAME,
|
|
80
81
|
} from './lib/doctor-stale-temp.mjs';
|
|
81
82
|
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
82
83
|
|
|
@@ -96,9 +97,10 @@ import {
|
|
|
96
97
|
import { detectInstallShape, probeRuntimeRoots, hasAnyManagedCode } from './lib/install-shape.mjs';
|
|
97
98
|
import { probeSchemaCompat, schemaSkewRemedy } from './lib/schema-skew.mjs';
|
|
98
99
|
import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
|
|
99
|
-
import { acquireLock } from './lib/proc-lock.mjs';
|
|
100
|
+
import { acquireLock, lockHeld } from './lib/proc-lock.mjs';
|
|
100
101
|
import { atomicWriteFileSync, atomicCopyFileSync } from './lib/atomic-write.mjs';
|
|
101
102
|
import { shellWord } from './cli-path.mjs';
|
|
103
|
+
import { dirAccessError, walOpenBlocked, dataDirRemedy, WAL_BLOCKED_WHY } from './lib/wal-open-blocked.mjs';
|
|
102
104
|
import {
|
|
103
105
|
claudeConfigDir,
|
|
104
106
|
claudeConfigDirFor,
|
|
@@ -1298,6 +1300,123 @@ function offerCleanOldVectorDb() {
|
|
|
1298
1300
|
}
|
|
1299
1301
|
}
|
|
1300
1302
|
|
|
1303
|
+
// A path, a dangling symlink included (existsSync follows the link).
|
|
1304
|
+
function pathPresent(p) {
|
|
1305
|
+
try {
|
|
1306
|
+
lstatSync(p);
|
|
1307
|
+
return true;
|
|
1308
|
+
} catch {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Update residue in the code dir, each entry with what it is (lib/doctor-stale-temp.mjs).
|
|
1314
|
+
function listUpdateResidue() {
|
|
1315
|
+
return readdirSync(INSTALL_DIR)
|
|
1316
|
+
.map((f) => ({ f, kind: classifyUpdateResidue(INSTALL_DIR, f) }))
|
|
1317
|
+
.filter((r) => r.kind);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Finish what interrupted updates left in the code dir, the way the next update entry would:
|
|
1322
|
+
* hook-update's recoverInterruptedSwaps replays each journal, then removes the residue. Reports
|
|
1323
|
+
* every entry by what became of it. The caller holds install.lock.
|
|
1324
|
+
*
|
|
1325
|
+
* Never throws: install calls it before writing the tree, and a recovery that threw would stop
|
|
1326
|
+
* the install it exists to protect. If hook-update cannot load, the residue of finished swaps is
|
|
1327
|
+
* still removed and an unfinished swap is left whole (for install, which retires it once the
|
|
1328
|
+
* tree is written, or for a later entry that can replay it).
|
|
1329
|
+
* @param {{f: string, kind: string}[]} found entries from listUpdateResidue
|
|
1330
|
+
* @returns {Promise<{finished: string[], removed: string[], kept: string[], deferred: string[],
|
|
1331
|
+
* unfinished: string[], left: string[]}>} kept: left on purpose because its journal cannot be
|
|
1332
|
+
* read; deferred: an unfinished swap left because hook-update could not load; unfinished: one the
|
|
1333
|
+
* replay did not finish, because a file could not be put back from it, or a later one that must be
|
|
1334
|
+
* finished first could not be finished or read (D#293); left: anything else
|
|
1335
|
+
*/
|
|
1336
|
+
async function recoverUpdateResidue(found) {
|
|
1337
|
+
let replayed = true;
|
|
1338
|
+
try {
|
|
1339
|
+
const { recoverInterruptedSwaps } = await import('./hook-update.mjs');
|
|
1340
|
+
recoverInterruptedSwaps(INSTALL_DIR);
|
|
1341
|
+
} catch (e) {
|
|
1342
|
+
replayed = false;
|
|
1343
|
+
warn(`Could not finish an interrupted update here (${e.message}) — it is left in place`);
|
|
1344
|
+
}
|
|
1345
|
+
// Leftovers recovery did not take: a file or a symlink with the name (it replays only real
|
|
1346
|
+
// directories), or everything when hook-update could not load. Only 'stale' ones go.
|
|
1347
|
+
for (const { f, kind } of found) {
|
|
1348
|
+
if (kind !== 'stale' || !pathPresent(join(INSTALL_DIR, f))) continue;
|
|
1349
|
+
try {
|
|
1350
|
+
rmSync(join(INSTALL_DIR, f), { recursive: true, force: true });
|
|
1351
|
+
} catch {
|
|
1352
|
+
/* reported below as left */
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const out = { finished: [], removed: [], kept: [], deferred: [], unfinished: [], left: [] };
|
|
1356
|
+
for (const { f, kind } of found) {
|
|
1357
|
+
if (!pathPresent(join(INSTALL_DIR, f))) (kind === 'unfinished-swap' ? out.finished : out.removed).push(f);
|
|
1358
|
+
else if (kind === 'unreadable-journal') out.kept.push(f);
|
|
1359
|
+
else if (kind === 'unfinished-swap' && !replayed) out.deferred.push(f);
|
|
1360
|
+
else if (kind === 'unfinished-swap' && classifyUpdateResidue(INSTALL_DIR, f) === 'unfinished-swap')
|
|
1361
|
+
out.unfinished.push(f);
|
|
1362
|
+
else out.left.push(f);
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// A swap an updater was killed in the middle of is finished BEFORE this install writes the tree
|
|
1368
|
+
// (D#289). Its journal names an older release's paths; left for the next update entry, it was
|
|
1369
|
+
// replayed over the tree written here: it deleted the files the killed swap had added and put the
|
|
1370
|
+
// older ones back. Runs before the swap barrier, because the recovery arms and clears that marker.
|
|
1371
|
+
async function finishInterruptedSwaps() {
|
|
1372
|
+
let found;
|
|
1373
|
+
try {
|
|
1374
|
+
found = listUpdateResidue();
|
|
1375
|
+
} catch {
|
|
1376
|
+
return; // no code dir yet, so nothing to finish
|
|
1377
|
+
}
|
|
1378
|
+
if (!found.some((r) => r.kind === 'unfinished-swap')) return;
|
|
1379
|
+
const r = await recoverUpdateResidue(found);
|
|
1380
|
+
if (r.finished.length > 0)
|
|
1381
|
+
ok(`Finished ${r.finished.length} interrupted update(s) before installing: ${r.finished.join(', ')}`);
|
|
1382
|
+
// A swap left unfinished here (deferred, kept, unfinished) is retired once the tree is written.
|
|
1383
|
+
for (const f of r.unfinished)
|
|
1384
|
+
warn(
|
|
1385
|
+
`Could not finish the interrupted update ${f}: a file it moved aside could not be put back, or a later one that must be finished first could not be. This install writes over it and then retires it.`,
|
|
1386
|
+
);
|
|
1387
|
+
for (const f of r.left) warn(`Could not remove ${f} from ${INSTALL_DIR}`);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const isUnresolvedSwap = (f) => {
|
|
1391
|
+
const kind = classifyUpdateResidue(INSTALL_DIR, f);
|
|
1392
|
+
return kind === 'unfinished-swap' || kind === 'unreadable-journal';
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// A journal still unresolved once deployCodeTree has written the code tree names paths of a
|
|
1396
|
+
// release this install has just replaced, and a later update entry would replay it over this tree:
|
|
1397
|
+
// delete the files the install wrote and put that release's back (v6.25.1 review; reached when
|
|
1398
|
+
// hook-update cannot load, or a journal cannot be read or removed). Marked resolved, it is never
|
|
1399
|
+
// replayed, and cleanup removes it as leftover. node_modules is written after this, by
|
|
1400
|
+
// installDependencies: if that fails, a node_modules the retired dir held is not put back (D#293).
|
|
1401
|
+
function retireLeftoverJournals() {
|
|
1402
|
+
let names;
|
|
1403
|
+
try {
|
|
1404
|
+
names = readdirSync(INSTALL_DIR).filter(isUnresolvedSwap);
|
|
1405
|
+
} catch {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
for (const f of names) {
|
|
1409
|
+
try {
|
|
1410
|
+
writeFileSync(join(INSTALL_DIR, f, SWAP_RESOLVED_NAME), '');
|
|
1411
|
+
ok(`Retired ${f}: this install replaced the release it belongs to (cleanup removes it)`);
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
warn(
|
|
1414
|
+
`Could not retire ${f} (${e.code || e.message}): delete it, or the next update puts back the files it holds`,
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1301
1420
|
async function install() {
|
|
1302
1421
|
console.log('\nclaude-mem-lite installer\n');
|
|
1303
1422
|
// Refuse an unparseable settings.json BEFORE the first side effect (throws
|
|
@@ -1310,12 +1429,14 @@ async function install() {
|
|
|
1310
1429
|
const IS_DEV = flags.has('--dev');
|
|
1311
1430
|
|
|
1312
1431
|
prepareInstallDirs();
|
|
1432
|
+
await finishInterruptedSwaps();
|
|
1313
1433
|
// Both steps rewrite the tree hooks import — the code, then node_modules — so one swap
|
|
1314
1434
|
// barrier spans them (D#223), for its first two minutes: the launcher ignores an older marker,
|
|
1315
1435
|
// and npm install has no timeout. installDependencies may process.exit(1): the marker it leaves
|
|
1316
1436
|
// names a dead pid, which the launcher ignores.
|
|
1317
1437
|
await withSwapBarrier(async () => {
|
|
1318
1438
|
deployCodeTree(IS_DEV);
|
|
1439
|
+
retireLeftoverJournals();
|
|
1319
1440
|
await installDependencies(IS_DEV);
|
|
1320
1441
|
});
|
|
1321
1442
|
createCliSymlink();
|
|
@@ -1742,14 +1863,22 @@ async function status() {
|
|
|
1742
1863
|
}
|
|
1743
1864
|
|
|
1744
1865
|
// Database. A data dir this process cannot enter reads as an empty one (D#199), and
|
|
1745
|
-
// `exists: false` would be a guess — null says nobody could look.
|
|
1866
|
+
// `exists: false` would be a guess — null says nobody could look. One it can read but not write
|
|
1867
|
+
// (the 755 a `sudo` run leaves owned by root) is read where SQLite can open the store there: its
|
|
1868
|
+
// counts are real, under a ✗ (D#284). A closed WAL store there cannot be opened; see the catch.
|
|
1746
1869
|
const dataDirDenied = dataDirAccessError();
|
|
1747
|
-
|
|
1870
|
+
const dataDirUnreadable = dataDirDenied
|
|
1871
|
+
? dataDirAccessError(MEM_DATA_DIR, fsConstants.R_OK | fsConstants.X_OK)
|
|
1872
|
+
: null;
|
|
1873
|
+
const unwritable = dataDirDenied
|
|
1874
|
+
? ` — ${MEM_DATA_DIR} can be read but not written (${dataDirDenied}). Fix: ${dataDirAccessRemedy(dataDirDenied)}`
|
|
1875
|
+
: '';
|
|
1876
|
+
if (dataDirUnreadable) {
|
|
1748
1877
|
push(
|
|
1749
1878
|
'fail',
|
|
1750
1879
|
'database',
|
|
1751
|
-
`Database: ${MEM_DATA_DIR} is not accessible (${
|
|
1752
|
-
{ exists: null, error:
|
|
1880
|
+
`Database: ${MEM_DATA_DIR} is not accessible (${dataDirUnreadable}) — Fix: ${dataDirAccessRemedy(dataDirUnreadable)}`,
|
|
1881
|
+
{ exists: null, error: dataDirUnreadable },
|
|
1753
1882
|
);
|
|
1754
1883
|
} else if (existsSync(DB_PATH)) {
|
|
1755
1884
|
try {
|
|
@@ -1759,19 +1888,38 @@ async function status() {
|
|
|
1759
1888
|
// DISTINCT, like stats: a session can own several summary rows (legacy duplicates).
|
|
1760
1889
|
const sess = db.prepare('SELECT COUNT(DISTINCT memory_session_id) as c FROM session_summaries').get();
|
|
1761
1890
|
db.close();
|
|
1762
|
-
push(
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1891
|
+
push(
|
|
1892
|
+
dataDirDenied ? 'fail' : 'ok',
|
|
1893
|
+
'database',
|
|
1894
|
+
`Database: ${obs.c} observations, ${sess.c} sessions${unwritable}`,
|
|
1895
|
+
{
|
|
1896
|
+
exists: true,
|
|
1897
|
+
observations: obs.c,
|
|
1898
|
+
sessions: sess.c,
|
|
1899
|
+
...(dataDirDenied && { error: dataDirDenied }),
|
|
1900
|
+
},
|
|
1901
|
+
);
|
|
1767
1902
|
} catch (e) {
|
|
1768
|
-
|
|
1903
|
+
// The store is WAL (schema.mjs), and even a read-only open needs its -wal and -shm files, which
|
|
1904
|
+
// SQLite creates beside the DB: in a directory it cannot write, a closed store cannot be opened
|
|
1905
|
+
// in place (a copy elsewhere could be read) — 6.24.0 could not open it either. Say so rather than relay "attempt to write a readonly database".
|
|
1906
|
+
// The errno stands in for SQLite's message only where the directory is the cause.
|
|
1907
|
+
const walBlocked = walOpenBlocked(e.code, dataDirDenied, DB_PATH);
|
|
1908
|
+
const why = walBlocked
|
|
1909
|
+
? `exists, but ${WAL_BLOCKED_WHY}`
|
|
1910
|
+
: bindingMissing(e)
|
|
1911
|
+
? `exists, but not checked — ${BINDING_MISSING_WHY}`
|
|
1912
|
+
: 'exists but check failed — ' + e.message;
|
|
1913
|
+
push(dataDirDenied ? 'fail' : 'warn', 'database', `Database: ${why}${unwritable}`, {
|
|
1769
1914
|
exists: true,
|
|
1770
|
-
error: e.message,
|
|
1915
|
+
error: walBlocked ? dataDirDenied : e.message,
|
|
1771
1916
|
});
|
|
1772
1917
|
}
|
|
1773
1918
|
} else {
|
|
1774
|
-
push('warn', 'database', 'Database: not found', {
|
|
1919
|
+
push(dataDirDenied ? 'fail' : 'warn', 'database', 'Database: not found' + unwritable, {
|
|
1920
|
+
exists: false,
|
|
1921
|
+
...(dataDirDenied && { error: dataDirDenied }),
|
|
1922
|
+
});
|
|
1775
1923
|
}
|
|
1776
1924
|
|
|
1777
1925
|
// CLI.
|
|
@@ -1990,12 +2138,21 @@ async function doctor() {
|
|
|
1990
2138
|
fail(
|
|
1991
2139
|
`Data directory: ${MEM_DATA_DIR} is not accessible (${dataDirDenied}) — ` +
|
|
1992
2140
|
(dataDirUnreadable ? 'checks that read it say "not checked"' : 'it can be read but not written') +
|
|
1993
|
-
`. Fix: ${dataDirAccessRemedy()}`,
|
|
2141
|
+
`. Fix: ${dataDirAccessRemedy(dataDirDenied)}`,
|
|
1994
2142
|
);
|
|
1995
2143
|
issues++;
|
|
1996
2144
|
}
|
|
1997
2145
|
const notCheckedDenied = (what, dir = MEM_DATA_DIR) =>
|
|
1998
2146
|
dwarn(`${what}: not checked — ${dir} is not accessible`);
|
|
2147
|
+
// A closed WAL store in a data dir that can be read but not written: even a read-only open has to
|
|
2148
|
+
// create the -wal/-shm files beside it, so every check that opens the DB fails, for the fault the
|
|
2149
|
+
// Data directory line names and counts. Those checks say they did not look, in the words status
|
|
2150
|
+
// uses (D#287, the D#199 shape). Keyed on SQLite's code: its message is shared with other faults.
|
|
2151
|
+
const walBlocked = (code) => walOpenBlocked(code, dataDirDenied, DB_PATH);
|
|
2152
|
+
const notCheckedWal = (what, scope = '') =>
|
|
2153
|
+
dwarn(
|
|
2154
|
+
`${what}: not checked${scope} — ${WAL_BLOCKED_WHY}, and ${MEM_DATA_DIR} cannot be written (see Data directory above)`,
|
|
2155
|
+
);
|
|
1999
2156
|
// The code dir is the data dir in the default shape; under CLAUDE_MEM_DIR it is checked apart.
|
|
2000
2157
|
const codeIsDataDir = sameDir(INSTALL_DIR, MEM_DATA_DIR);
|
|
2001
2158
|
const codeDirDenied = codeIsDataDir ? dataDirUnreadable : dataDirAccessError(INSTALL_DIR, readMode);
|
|
@@ -2016,7 +2173,10 @@ async function doctor() {
|
|
|
2016
2173
|
// process.
|
|
2017
2174
|
const rootProbes = probeRuntimeRoots(shape.runtimeRoots);
|
|
2018
2175
|
const brokenRoots = rootProbes.filter((r) => !r.ok);
|
|
2019
|
-
if (rootProbes.length === 0) {
|
|
2176
|
+
if (rootProbes.length === 0 && codeDirDenied) {
|
|
2177
|
+
// The code home is locked, so it may own a binding nobody could look at (D#284 review P3-4).
|
|
2178
|
+
notCheckedDenied('better-sqlite3', INSTALL_DIR);
|
|
2179
|
+
} else if (rootProbes.length === 0) {
|
|
2020
2180
|
fail('better-sqlite3: no install on this machine owns a native binding — nothing here can open the DB');
|
|
2021
2181
|
issues++;
|
|
2022
2182
|
} else if (brokenRoots.length === 0) {
|
|
@@ -2064,6 +2224,8 @@ async function doctor() {
|
|
|
2064
2224
|
notCheckedDenied('DB schema');
|
|
2065
2225
|
} else if (!existsSync(DB_PATH)) {
|
|
2066
2226
|
ok('DB schema: no database yet — nothing to compare');
|
|
2227
|
+
} else if (rootProbes.length === 0 && codeDirDenied) {
|
|
2228
|
+
notCheckedDenied('DB schema', INSTALL_DIR); // the locked code home may own one (D#284 review)
|
|
2067
2229
|
} else if (rootProbes.length === 0) {
|
|
2068
2230
|
// The fourth outcome the first cut had and did not print. The `fail` above already tells
|
|
2069
2231
|
// the reader no install owns a binding, but a block whose stated design point is "three
|
|
@@ -2135,7 +2297,8 @@ async function doctor() {
|
|
|
2135
2297
|
// printed as a green line is the defect the v6.2.0 round wrote and its pre-ship review
|
|
2136
2298
|
// caught before the tag — a check that says "nothing to check" and "I could not look"
|
|
2137
2299
|
// in the same voice ends the reader's search instead of directing it.
|
|
2138
|
-
|
|
2300
|
+
if (walBlocked(u.errorCode)) notCheckedWal('DB schema', ` for ${u.label}`);
|
|
2301
|
+
else dwarn(`DB schema: could not determine compatibility for ${u.label} (${u.error})`);
|
|
2139
2302
|
}
|
|
2140
2303
|
}
|
|
2141
2304
|
|
|
@@ -2220,7 +2383,11 @@ async function doctor() {
|
|
|
2220
2383
|
// is 6h-rate-limited stderr nobody reads), the live probe says "is it broken
|
|
2221
2384
|
// right now". A Node upgrade breaks every DB-touching path at once, so this is
|
|
2222
2385
|
// the single highest-value line in doctor when it fires.
|
|
2223
|
-
|
|
2386
|
+
// The marker sits in the runtime dir: in a locked data dir it reads as absent (D#284).
|
|
2387
|
+
const breakageUnread = dataDirUnreadable && runtimeInDataDir;
|
|
2388
|
+
const breakage = breakageUnread ? null : readNativeBindingBreakage(MEM_RUNTIME_DIR);
|
|
2389
|
+
const breakageSeen = (b) =>
|
|
2390
|
+
`~${Math.round((Date.now() - (b.ts || 0)) / 3600000)}h ago (${b.reason || 'unknown'})`;
|
|
2224
2391
|
// Reuses the per-root probes above — same trees, same question, and doctor
|
|
2225
2392
|
// should not pay for another round of child spawns to ask it twice.
|
|
2226
2393
|
if (brokenRoots.length > 0) {
|
|
@@ -2228,10 +2395,20 @@ async function doctor() {
|
|
|
2228
2395
|
`Native DB binding: unusable in ${brokenRoots.map((b) => b.label).join(', ')} — run \`node ${shellWord(join(PROJECT_DIR, 'cli.mjs'))} rebuild-binding\` (repairs every broken install, not just this one)`,
|
|
2229
2396
|
);
|
|
2230
2397
|
issues++;
|
|
2398
|
+
} else if (rootProbes.length === 0) {
|
|
2399
|
+
// Nothing was probed: a ✓ here sat under the better-sqlite3 ✗ for the same machine. Said, not
|
|
2400
|
+
// left silent, like DB schema's fourth outcome above; a marker it could read still shows.
|
|
2401
|
+
const why = codeDirDenied
|
|
2402
|
+
? `${INSTALL_DIR} is not accessible`
|
|
2403
|
+
: 'no install on this machine owns a native binding to probe';
|
|
2404
|
+
dwarn(
|
|
2405
|
+
`Native DB binding: not checked — ${why}${breakage ? `; a fire failed ${breakageSeen(breakage)}` : ''}`,
|
|
2406
|
+
);
|
|
2407
|
+
} else if (breakageUnread) {
|
|
2408
|
+
notCheckedDenied(`Native DB binding: loadable on Node ${process.version}, past failures`);
|
|
2231
2409
|
} else if (breakage) {
|
|
2232
|
-
const ageH = Math.round((Date.now() - (breakage.ts || 0)) / 3600000);
|
|
2233
2410
|
dwarn(
|
|
2234
|
-
`Native DB binding: healthy now, but a fire failed
|
|
2411
|
+
`Native DB binding: healthy now, but a fire failed ${breakageSeen(breakage)} — stale marker clears on the next successful rebuild-binding`,
|
|
2235
2412
|
);
|
|
2236
2413
|
} else {
|
|
2237
2414
|
ok(`Native DB binding: loadable on Node ${process.version}`);
|
|
@@ -2334,9 +2511,17 @@ async function doctor() {
|
|
|
2334
2511
|
// with require-error noise every session. README's Uninstall section warns
|
|
2335
2512
|
// about the right ordering; this check flags the broken state so it surfaces
|
|
2336
2513
|
// even when the user skipped the README.
|
|
2337
|
-
const
|
|
2514
|
+
const unreadableHookPaths = [];
|
|
2515
|
+
const orphanPaths =
|
|
2516
|
+
settings === null ? null : collectOrphanHookPaths(settings, INSTALL_DIR, unreadableHookPaths);
|
|
2338
2517
|
if (orphanPaths === null) {
|
|
2339
2518
|
dwarn('Orphan hooks: not checked (settings.json unreadable)');
|
|
2519
|
+
} else if (orphanPaths.length === 0 && unreadableHookPaths.length > 0) {
|
|
2520
|
+
// A hook that points into a code home this user cannot enter is not an orphan: the file may
|
|
2521
|
+
// well be there, and the Entry points line names and counts the locked dir (D#297).
|
|
2522
|
+
dwarn(
|
|
2523
|
+
`Orphan hooks: not checked — ${unreadableHookPaths.length} hook target(s) cannot be read (${unreadableHookPaths[0]}${unreadableHookPaths.length > 1 ? ', …' : ''})`,
|
|
2524
|
+
);
|
|
2340
2525
|
} else if (orphanPaths.length > 0) {
|
|
2341
2526
|
fail(
|
|
2342
2527
|
`Orphan hooks: ${orphanPaths.length} settings.json entr${orphanPaths.length === 1 ? 'y references a missing file' : 'ies reference missing files'}`,
|
|
@@ -2474,24 +2659,39 @@ async function doctor() {
|
|
|
2474
2659
|
dwarn('FTS5 index: missing (will be created on server start)');
|
|
2475
2660
|
}
|
|
2476
2661
|
} catch (e) {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2662
|
+
if (walBlocked(e.code)) {
|
|
2663
|
+
notCheckedWal('Database');
|
|
2664
|
+
} else if (bindingMissing(e)) {
|
|
2665
|
+
dwarn(`Database: not checked — ${BINDING_MISSING_WHY}`);
|
|
2666
|
+
} else {
|
|
2667
|
+
fail('Database: ' + e.message);
|
|
2668
|
+
// Every other ✗ on this screen carries a remedy; this one used to be the exception,
|
|
2669
|
+
// and a corrupt store is the failure a user is least able to diagnose unaided.
|
|
2670
|
+
// dbCheckRemedy returns null rather than invent one for an error it cannot classify.
|
|
2671
|
+
// Pre-ship review 2026-09-09: dbCheckRemedy became async and lazy (P1-1), which moved
|
|
2672
|
+
// its failure mode from load time into THIS catch — and an unhandled rejection here
|
|
2673
|
+
// aborts doctor exactly as the static import did, so `--json` still emitted zero bytes
|
|
2674
|
+
// on the compound shape "a file is missing AND the database will not open". A remedy
|
|
2675
|
+
// is an extra sentence on a check that has already failed; never let it take the run.
|
|
2676
|
+
let remedy = null;
|
|
2677
|
+
try {
|
|
2678
|
+
remedy = await dbCheckRemedy(DB_PATH, e);
|
|
2679
|
+
} catch (remedyErr) {
|
|
2680
|
+
log(` (could not build a repair hint: ${remedyErr.message})`);
|
|
2681
|
+
}
|
|
2682
|
+
if (remedy) log(` ${remedy}`);
|
|
2683
|
+
issues++;
|
|
2491
2684
|
}
|
|
2492
|
-
if (remedy) log(` ${remedy}`);
|
|
2493
|
-
issues++;
|
|
2494
2685
|
}
|
|
2686
|
+
} else if (rootProbes.length === 0) {
|
|
2687
|
+
// "will be created" beside the better-sqlite3 line's "nothing here can open the DB" was a
|
|
2688
|
+
// promise this screen had just contradicted (D#287). With the code home locked the binding
|
|
2689
|
+
// was not looked for, so neither half is claimed.
|
|
2690
|
+
dwarn(
|
|
2691
|
+
codeDirDenied
|
|
2692
|
+
? 'Database: not found'
|
|
2693
|
+
: 'Database: not found — and nothing here can create it until an install has a native binding (see better-sqlite3 above)',
|
|
2694
|
+
);
|
|
2495
2695
|
} else {
|
|
2496
2696
|
dwarn('Database: not found (will be created)');
|
|
2497
2697
|
}
|
|
@@ -2822,18 +3022,45 @@ async function doctor() {
|
|
|
2822
3022
|
// Stale temp files. The rules live in lib/doctor-stale-temp.mjs because this scanner and
|
|
2823
3023
|
// cleanup's deleter are the same question asked twice and had drifted twice — see that
|
|
2824
3024
|
// file. Counting is all that differs here; the classification is shared, so "what doctor
|
|
2825
|
-
// calls stale" and "what cleanup removes" agree on the age gate
|
|
2826
|
-
//
|
|
2827
|
-
//
|
|
2828
|
-
//
|
|
2829
|
-
//
|
|
3025
|
+
// calls stale" and "what cleanup removes" agree on the age gate and on the directory, the
|
|
3026
|
+
// axes they diverged on (the third time, D#289: both looked for update residue in the data
|
|
3027
|
+
// dir, which CLAUDE_MEM_DIR moves away from the code dir it lives in). And on the lock: while
|
|
3028
|
+
// install.lock is held, cleanup skips update residue (a running update's own staging and backup
|
|
3029
|
+
// dirs among it), so doctor does not count it either, and says it did not look (D#297). The holder
|
|
3030
|
+
// may be any installer: an update, install, repair, a binding rebuild. It asks without taking it.
|
|
2830
3031
|
try {
|
|
2831
|
-
const
|
|
2832
|
-
|
|
3032
|
+
const lockHeldNow = lockHeld(join(MEM_DATA_DIR, 'runtime', 'install.lock')); // runtime-dir:stays-put — install lock serialises real installers
|
|
3033
|
+
const { stale, inFlight, unfinishedSwaps, unreadableJournals, notChecked } = scanStaleTempFiles({
|
|
3034
|
+
codeDir: INSTALL_DIR,
|
|
2833
3035
|
runtimeDir: MEM_RUNTIME_DIR,
|
|
3036
|
+
legacyDir: codeIsDataDir ? undefined : MEM_DATA_DIR,
|
|
3037
|
+
installLockHeld: lockHeldNow,
|
|
2834
3038
|
});
|
|
2835
|
-
|
|
2836
|
-
|
|
3039
|
+
const installer = `node ${shellWord(join(PROJECT_DIR, 'install.mjs'))}`;
|
|
3040
|
+
if (unfinishedSwaps > 0) {
|
|
3041
|
+
dwarn(
|
|
3042
|
+
`Unfinished update: ${unfinishedSwaps} backup dir(s) in ${INSTALL_DIR} still hold a swap journal — an update was interrupted, or could not put back every file it moved aside (run: ${installer} cleanup — it finishes them newest first, and stops at one it cannot read or put back)`,
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
if (unreadableJournals > 0) {
|
|
3046
|
+
dwarn(
|
|
3047
|
+
`Unfinished update: ${unreadableJournals} backup dir(s) in ${INSTALL_DIR} whose journal cannot be read, so nothing can finish that update; they hold the files it moved aside (run: ${installer} repair — it reinstalls over them; cleanup then removes them)`,
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
const skipped = lockHeldNow
|
|
3051
|
+
? `update residue not checked — install.lock is held (by a running install, update, repair or binding rebuild, or as a lock file this user cannot read), and cleanup skips it too`
|
|
3052
|
+
: '';
|
|
3053
|
+
if (notChecked.length > 0) {
|
|
3054
|
+
const where = notChecked.map((n) => `${n.dir} is not accessible (${n.code})`).join('; ');
|
|
3055
|
+
dwarn(
|
|
3056
|
+
`Stale temp files: not checked — ${where}` +
|
|
3057
|
+
(stale > 0 ? `; ${stale} found elsewhere (run: ${installer} cleanup)` : '') +
|
|
3058
|
+
(skipped ? `; ${skipped}` : ''),
|
|
3059
|
+
);
|
|
3060
|
+
} else if (stale > 0) {
|
|
3061
|
+
dwarn(`Stale temp files: ${stale} found (run: ${installer} cleanup)${skipped ? `; ${skipped}` : ''}`);
|
|
3062
|
+
} else if (skipped) {
|
|
3063
|
+
dwarn(`Stale temp files: ${skipped}`);
|
|
2837
3064
|
} else {
|
|
2838
3065
|
ok('Stale temp files: none');
|
|
2839
3066
|
}
|
|
@@ -2875,7 +3102,9 @@ async function doctor() {
|
|
|
2875
3102
|
dwarn(`${stats} — but the install you are running cannot use this database (see DB schema above)`);
|
|
2876
3103
|
else ok(stats);
|
|
2877
3104
|
} catch (e) {
|
|
2878
|
-
|
|
3105
|
+
if (walBlocked(e.code)) notCheckedWal('DB stats');
|
|
3106
|
+
else if (bindingMissing(e)) dwarn(`DB stats: not checked — ${BINDING_MISSING_WHY}`);
|
|
3107
|
+
else dwarn('DB stats: ' + e.message);
|
|
2879
3108
|
}
|
|
2880
3109
|
}
|
|
2881
3110
|
|
|
@@ -3000,9 +3229,31 @@ function looksLikeHookPath(p) {
|
|
|
3000
3229
|
return HOOK_PATH_EXTS.some((ext) => p.endsWith(ext));
|
|
3001
3230
|
}
|
|
3002
3231
|
|
|
3003
|
-
|
|
3232
|
+
// 'missing' when the path is not there, 'unreadable' when it cannot be looked at (a directory on the
|
|
3233
|
+
// way this user cannot enter). existsSync answers false to both, so hooks pointing into a locked code
|
|
3234
|
+
// home were orphans, a second ✗ beside the Entry points line that names the lock (D#297).
|
|
3235
|
+
function hookTargetState(p) {
|
|
3236
|
+
try {
|
|
3237
|
+
statSync(p);
|
|
3238
|
+
return 'present';
|
|
3239
|
+
} catch (e) {
|
|
3240
|
+
return e.code === 'ENOENT' || e.code === 'ENOTDIR' ? 'missing' : 'unreadable';
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
/**
|
|
3245
|
+
* @param {string[]|null} [unreadable] collects the targets that could not be looked at, which are
|
|
3246
|
+
* not returned as missing
|
|
3247
|
+
*/
|
|
3248
|
+
export function collectOrphanHookPaths(settings, installDir = INSTALL_DIR, unreadable = null) {
|
|
3004
3249
|
if (!settings?.hooks) return [];
|
|
3005
3250
|
const out = [];
|
|
3251
|
+
const judge = (p) => {
|
|
3252
|
+
if (out.includes(p) || unreadable?.includes(p)) return;
|
|
3253
|
+
const state = hookTargetState(p);
|
|
3254
|
+
if (state === 'missing') out.push(p);
|
|
3255
|
+
else if (state === 'unreadable') unreadable?.push(p);
|
|
3256
|
+
};
|
|
3006
3257
|
for (const configs of Object.values(settings.hooks)) {
|
|
3007
3258
|
if (!Array.isArray(configs)) continue;
|
|
3008
3259
|
for (const cfg of configs) {
|
|
@@ -3019,7 +3270,7 @@ export function collectOrphanHookPaths(settings, installDir = INSTALL_DIR) {
|
|
|
3019
3270
|
// ERR_MODULE_NOT_FOUND — and doctor went back to printing "Orphan hooks: none".
|
|
3020
3271
|
// Both files have to exist for the hook to run, so both are reportable.
|
|
3021
3272
|
const entry = launcherEntryPath(cmd, installDir);
|
|
3022
|
-
if (entry
|
|
3273
|
+
if (entry) judge(entry);
|
|
3023
3274
|
// v2.80: scan ALL quoted tokens (was: only the first), prefer ones
|
|
3024
3275
|
// that look like a hook path. Fixes a footgun where a wrapper command
|
|
3025
3276
|
// like `bash -c "some inline" "/real/path.sh"` would pick "some inline"
|
|
@@ -3038,7 +3289,7 @@ export function collectOrphanHookPaths(settings, installDir = INSTALL_DIR) {
|
|
|
3038
3289
|
path = parts.find((p) => looksLikeHookPath(p)) || null;
|
|
3039
3290
|
}
|
|
3040
3291
|
if (!path) continue;
|
|
3041
|
-
|
|
3292
|
+
judge(path);
|
|
3042
3293
|
}
|
|
3043
3294
|
}
|
|
3044
3295
|
}
|
|
@@ -3229,7 +3480,7 @@ function writeSettings(settings) {
|
|
|
3229
3480
|
|
|
3230
3481
|
// ─── Cleanup Stale Files ─────────────────────────────────────────────────────
|
|
3231
3482
|
|
|
3232
|
-
function cleanup() {
|
|
3483
|
+
async function cleanup() {
|
|
3233
3484
|
// Dogfood-7 addition: --dry-run lists which files would be removed without
|
|
3234
3485
|
// touching disk. Useful before running cleanup on a remote/CI machine where
|
|
3235
3486
|
// accidentally pruning the wrong file would be costly. Doctor reports stale
|
|
@@ -3238,39 +3489,87 @@ function cleanup() {
|
|
|
3238
3489
|
console.log(`\nclaude-mem-lite cleanup${dryRun ? ' (--dry-run)' : ''}\n`);
|
|
3239
3490
|
let removed = 0;
|
|
3240
3491
|
|
|
3241
|
-
//
|
|
3242
|
-
//
|
|
3492
|
+
// Update residue: `.update-staging-*` / `.update-backup-*` in the CODE dir, where hook-update's
|
|
3493
|
+
// swap writes them. Under CLAUDE_MEM_DIR this scanned only the data dir, where nothing has written
|
|
3494
|
+
// them since v2.90.0; residue an older updater left there is still removed, below. A backup dir
|
|
3495
|
+
// that holds its journal and no resolved marker is a swap whose updater was killed, holding the
|
|
3496
|
+
// only copy of every file the swap had moved out: cleanup finishes it, by replaying the journal
|
|
3497
|
+
// the way the next update entry would, instead of deleting it (D#289).
|
|
3243
3498
|
//
|
|
3244
|
-
// R10 P2-10: take install.lock first.
|
|
3245
|
-
//
|
|
3246
|
-
//
|
|
3247
|
-
// and the
|
|
3248
|
-
//
|
|
3249
|
-
|
|
3250
|
-
|
|
3499
|
+
// R10 P2-10: take install.lock first. A backup dir of a swap running now holds a journal too,
|
|
3500
|
+
// and the staging dir is the tree being swapped in; touching either mid-update leaves the install
|
|
3501
|
+
// unrecoverable. The window is long (it spans the source-compile fallback, up to five minutes),
|
|
3502
|
+
// and doctor tells the user to run cleanup. Non-blocking: if an installer holds the lock we skip
|
|
3503
|
+
// only the update residue, not the rest of cleanup.
|
|
3504
|
+
let finished = 0;
|
|
3505
|
+
// --dry-run writes nothing, so it asks whether the lock is held instead of taking it: taking it
|
|
3506
|
+
// created the data dir, its runtime dir and the lock on a machine with nothing installed (D#297).
|
|
3507
|
+
const lockPath = join(MEM_DATA_DIR, 'runtime', 'install.lock'); // runtime-dir:stays-put — install lock serialises real installers
|
|
3508
|
+
const updateLock = dryRun ? (lockHeld(lockPath) ? null : () => {}) : acquireLock(lockPath);
|
|
3251
3509
|
if (!updateLock) {
|
|
3252
3510
|
warn('Update residue skipped: install in progress (install.lock held)');
|
|
3253
|
-
} else
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3511
|
+
} else {
|
|
3512
|
+
try {
|
|
3513
|
+
let found = [];
|
|
3514
|
+
try {
|
|
3515
|
+
found = existsSync(INSTALL_DIR) ? listUpdateResidue() : [];
|
|
3516
|
+
} catch (e) {
|
|
3517
|
+
warn(`Update residue: could not list ${INSTALL_DIR} (${e.code || e.message})`);
|
|
3518
|
+
}
|
|
3519
|
+
if (dryRun) {
|
|
3520
|
+
for (const { f, kind } of found) {
|
|
3521
|
+
if (kind === 'unfinished-swap') {
|
|
3522
|
+
ok(`Would finish an interrupted update: ${f} (puts back the files its journal lists)`);
|
|
3523
|
+
finished++;
|
|
3524
|
+
} else if (kind === 'unreadable-journal') {
|
|
3525
|
+
warn(`Would leave in place: ${f} (its journal cannot be read)`);
|
|
3526
|
+
} else {
|
|
3527
|
+
ok(`Would remove: ${f}`);
|
|
3528
|
+
removed++;
|
|
3529
|
+
}
|
|
3260
3530
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3531
|
+
} else if (found.length > 0) {
|
|
3532
|
+
const r = await recoverUpdateResidue(found);
|
|
3533
|
+
for (const f of r.finished)
|
|
3534
|
+
ok(`Finished an interrupted update: ${f} (put back the files it had moved out)`);
|
|
3535
|
+
for (const f of r.removed) ok(`Removed: ${f}`);
|
|
3536
|
+
for (const f of r.kept)
|
|
3537
|
+
warn(
|
|
3538
|
+
`Left in place: ${f} — its journal cannot be read, so nothing can finish that update, and it holds the files the update moved aside. repair reinstalls over it; cleanup removes it after that.`,
|
|
3539
|
+
);
|
|
3540
|
+
for (const f of r.unfinished)
|
|
3541
|
+
warn(
|
|
3542
|
+
`Could not finish the interrupted update ${f}: a file it moved aside could not be put back (held open, or in a directory that cannot be emptied), or a later one that must be finished first could not be. It stays in place holding what it moved aside; cleanup tries again, and repair reinstalls over it instead.`,
|
|
3543
|
+
);
|
|
3544
|
+
for (const f of r.left) warn(`Failed to remove ${f}`);
|
|
3545
|
+
finished += r.finished.length;
|
|
3546
|
+
removed += r.removed.length;
|
|
3547
|
+
}
|
|
3548
|
+
// A relocated data dir that an updater older than v2.90.0 swapped into: its residue is stale,
|
|
3549
|
+
// whatever it holds, and is never replayed (lib/doctor-stale-temp.mjs, legacyDir).
|
|
3550
|
+
if (!sameDir(INSTALL_DIR, MEM_DATA_DIR) && existsSync(MEM_DATA_DIR)) {
|
|
3551
|
+
for (const f of readdirSync(MEM_DATA_DIR)) {
|
|
3552
|
+
if (!isUpdateResidue(f)) continue;
|
|
3553
|
+
if (dryRun) {
|
|
3554
|
+
ok(`Would remove: ${join(MEM_DATA_DIR, f)}`);
|
|
3555
|
+
removed++;
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
try {
|
|
3559
|
+
rmSync(join(MEM_DATA_DIR, f), { recursive: true, force: true });
|
|
3560
|
+
ok(`Removed: ${join(MEM_DATA_DIR, f)}`);
|
|
3561
|
+
removed++;
|
|
3562
|
+
} catch (e) {
|
|
3563
|
+
warn(`Failed to remove ${join(MEM_DATA_DIR, f)}: ${e.message}`);
|
|
3564
|
+
}
|
|
3267
3565
|
}
|
|
3268
3566
|
}
|
|
3567
|
+
} finally {
|
|
3568
|
+
// Released here: the rest of cleanup touches runtime scratch that no installer owns, and
|
|
3569
|
+
// holding install.lock across it would block a self-heal for no reason.
|
|
3570
|
+
updateLock();
|
|
3269
3571
|
}
|
|
3270
3572
|
}
|
|
3271
|
-
// Release immediately: the rest of cleanup touches runtime scratch that no installer owns,
|
|
3272
|
-
// and holding install.lock across it would block a self-heal for no reason.
|
|
3273
|
-
if (updateLock) updateLock();
|
|
3274
3573
|
|
|
3275
3574
|
// Clean pending-* / ep-flush-* in runtime/ (env-aware, and honouring the runtime override).
|
|
3276
3575
|
//
|
|
@@ -3322,7 +3621,11 @@ function cleanup() {
|
|
|
3322
3621
|
// start of every run (tests/global-setup.mjs → lib/tmp-fixture-sweep.mjs).
|
|
3323
3622
|
|
|
3324
3623
|
const verb = dryRun ? 'would be removed' : 'removed';
|
|
3325
|
-
|
|
3624
|
+
const done = [];
|
|
3625
|
+
if (removed > 0) done.push(`${removed} stale file(s) ${verb}`);
|
|
3626
|
+
if (finished > 0)
|
|
3627
|
+
done.push(`${finished} interrupted update(s) ${dryRun ? 'would be finished' : 'finished'}`);
|
|
3628
|
+
console.log(`\n ${done.length === 0 ? 'No stale files found.' : `${done.join('; ')}.`}\n`);
|
|
3326
3629
|
}
|
|
3327
3630
|
|
|
3328
3631
|
// ─── Manual Update ───────────────────────────────────────────────────────────
|
|
@@ -3690,12 +3993,7 @@ function dataDirAccessError(
|
|
|
3690
3993
|
dir = MEM_DATA_DIR,
|
|
3691
3994
|
mode = fsConstants.R_OK | fsConstants.W_OK | fsConstants.X_OK,
|
|
3692
3995
|
) {
|
|
3693
|
-
|
|
3694
|
-
accessSync(dir, mode);
|
|
3695
|
-
return null;
|
|
3696
|
-
} catch (e) {
|
|
3697
|
-
return e.code === 'ENOENT' ? null : e.code || 'EACCES';
|
|
3698
|
-
}
|
|
3996
|
+
return dirAccessError(dir, mode);
|
|
3699
3997
|
}
|
|
3700
3998
|
|
|
3701
3999
|
// Same directory however it is spelled (a trailing slash, a symlinked HOME). stat works on a
|
|
@@ -3710,14 +4008,22 @@ function sameDir(a, b) {
|
|
|
3710
4008
|
}
|
|
3711
4009
|
}
|
|
3712
4010
|
|
|
3713
|
-
|
|
3714
|
-
|
|
4011
|
+
// The fix for a data dir this user cannot use, chosen by the error code (lib/wal-open-blocked.mjs).
|
|
4012
|
+
export const dataDirAccessRemedy = (code) => dataDirRemedy(code, MEM_DATA_DIR);
|
|
4013
|
+
|
|
4014
|
+
// This process cannot load better-sqlite3: none installed where it runs (a checkout, or a plugin
|
|
4015
|
+
// cache before its first launch installed node_modules), or one that will not load (a stale ABI, a
|
|
4016
|
+
// damaged prebuild). The better-sqlite3 line names and counts that; a line that would open the DB
|
|
4017
|
+
// says it did not look, rather than count the same fault again (D#297, pre-ship review P3-4).
|
|
4018
|
+
const bindingMissing = (e) =>
|
|
4019
|
+
(e?.code === 'ERR_MODULE_NOT_FOUND' && /'better-sqlite3'/.test(e.message)) || isNativeBindingError(e);
|
|
4020
|
+
const BINDING_MISSING_WHY = `better-sqlite3 cannot be loaded from ${PROJECT_DIR}`;
|
|
3715
4021
|
|
|
3716
4022
|
async function runLockedInstall() {
|
|
3717
4023
|
const denied = dataDirAccessError();
|
|
3718
4024
|
if (denied) {
|
|
3719
4025
|
console.error(
|
|
3720
|
-
`[install] ${MEM_DATA_DIR} is not accessible (${denied}) — nothing was done. Fix: ${dataDirAccessRemedy()}`,
|
|
4026
|
+
`[install] ${MEM_DATA_DIR} is not accessible (${denied}) — nothing was done. Fix: ${dataDirAccessRemedy(denied)}`,
|
|
3721
4027
|
);
|
|
3722
4028
|
process.exitCode = 1;
|
|
3723
4029
|
return;
|
|
@@ -3824,7 +4130,7 @@ async function dispatch(cmd) {
|
|
|
3824
4130
|
await cleanupHooks();
|
|
3825
4131
|
break;
|
|
3826
4132
|
case 'cleanup':
|
|
3827
|
-
cleanup();
|
|
4133
|
+
await cleanup();
|
|
3828
4134
|
break;
|
|
3829
4135
|
case 'self-update':
|
|
3830
4136
|
case 'update':
|