pm2 save when the process list changes, and a check that does not lie about symlinks

the owner asked whether anything runs `pm2 save` when a sidecar is added or
removed. nothing did.

PM2 resurrects from ~/.pm2/dump.pm2, written only by `pm2 save`, so the dump was
whatever a human last happened to save. today it was FIVE DAYS old: 13 processes
saved against 26 running. a reboot would have silently lost every sidecar
installed since — bitcoin-wallet, desktop, gitea, invoices, iris, jellyfin,
minesweeper, music, photos, soulseek, transcribe, transmission, vault. thirteen
features gone, machine healthy, nothing saying why.

saved after start AND after delete. removal matters as much: a dump that keeps
resurrecting a deleted sidecar brings it back after a reboot as a crash loop for
code no longer on the machine. a RESTART does not save — the same processes are
running and the dump is already right.

best effort and never fatal: a failed save leaves the estate correct and only
the dump stale, which is where things already were. failing an install over
bookkeeping would be the worse trade.

separately, plugins:check filtered directories on the DIRENT's isDirectory(),
which answers false for a symlink to a directory. platform/plugins is symlinked
now, so the check would have reported "0 plugin repositories, all well-formed" —
a green check over nothing, which is worse than a red one. it stats the path
instead, the way discover.ts already does.

dump saved once by hand as well, since five days of drift was already there:
26 saved, 26 running, no difference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-27 07:52:46 +00:00
co-authored by Claude Opus 5
parent 68d6dd942e
commit c14b686782
2 changed files with 41 additions and 2 deletions
+14 -2
View File
@@ -25,7 +25,7 @@
that running the check can never be the thing that changes your tree.
*/
import { readdirSync, existsSync } from 'node:fs';
import { readdirSync, existsSync, statSync } from 'node:fs';
import { join } from 'node:path';
/*
@@ -52,7 +52,19 @@ type Problem = { plugin: string; what: string; fix?: () => void };
const problems: Problem[] = [];
const dirs = readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
// `statSync().isDirectory()`, not the dirent's — a Dirent for a SYMLINK to a directory answers false to
// `isDirectory()` and true to `isSymbolicLink()`, so filtering on the dirent silently skips every
// symlinked plugin. `platform/plugins/` is symlinks into the workspace clones since 2026-08-27, which
// would have made this report "0 plugin repositories, all well-formed" — a green check over nothing.
// `discover.ts` already does it this way, at line 239, for the same reason.
.filter((e) => !e.name.startsWith('.') && e.name !== 'node_modules')
.filter((e) => {
try {
return statSync(join(PLUGINS_DIR, e.name)).isDirectory();
} catch {
return false; // a broken symlink, which the checks below have nothing useful to say about
}
})
.map((e) => e.name)
.filter((name) => existsSync(join(PLUGINS_DIR, name, '.git')))
.sort();