@bobfrankston/rmfmail 1.2.324 → 1.2.325
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/.commitmsg +17 -22
- package/.llm/config-cache-crash.md +26 -0
- package/bin/mailx.js +96 -25
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +105 -16
- package/client/app.bundle.js +8 -1
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +15 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +13 -1
- package/npmchanges.md +27 -0
- package/package.json +5 -5
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +7 -2
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +7 -2
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/index.d.ts +15 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +71 -14
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +65 -14
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import * as fs from "node:fs";
|
|
20
20
|
import * as path from "node:path";
|
|
21
|
-
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
|
+
import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
22
22
|
import type { MailxSettings, AccountConfig, AutocompleteSettings, AiKeys, ReminderState } from "@bobfrankston/mailx-types";
|
|
23
23
|
import { REMINDER_STATE_FILE, normalizeReminderState, mergeReminderStates, reminderStatesEqual } from "@bobfrankston/mailx-types";
|
|
24
24
|
import { getCloudProvider, gDriveFindOrCreateFolder, gDriveValidateCachedFolder, type CloudProvider } from "./cloud.js";
|
|
@@ -312,11 +312,22 @@ export async function cloudRead(filename: string): Promise<string | null> {
|
|
|
312
312
|
if (wroteAt >= startedAt) {
|
|
313
313
|
console.log(` [cloud] ${filename} changed locally during the read — keeping the local copy`);
|
|
314
314
|
} else {
|
|
315
|
-
|
|
315
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
316
|
+
// Was an unconditional plain writeFileSync on EVERY cloud read —
|
|
317
|
+
// and pollCloud reads every 3 min — so accounts.jsonc was rewritten
|
|
318
|
+
// all day even when identical (the log showed "fs.watch fired but
|
|
319
|
+
// content unchanged" every 3 minutes). A kernel crash at 16:36
|
|
320
|
+
// today caught one of those rewrites and NTFS left the file
|
|
321
|
+
// zero-filled; the daemon then booted with 0 accounts. Now: skip
|
|
322
|
+
// when identical, and write crash-safe (tmp + fsync + rename).
|
|
316
323
|
stashPrev(filename, content);
|
|
317
|
-
|
|
324
|
+
writeTextIfChanged(path.join(LOCAL_DIR, filename), content);
|
|
318
325
|
}
|
|
319
|
-
} catch
|
|
326
|
+
} catch (e: any) {
|
|
327
|
+
// The cloud content is still returned to the caller; only the local
|
|
328
|
+
// offline cache is stale, and the next read retries the write.
|
|
329
|
+
console.error(` [cloud] ${filename}: local cache write failed: ${e?.message || e}`);
|
|
330
|
+
}
|
|
320
331
|
}
|
|
321
332
|
// Don't set error for missing files — they may not exist yet (e.g., clients.jsonc on first run)
|
|
322
333
|
return content;
|
|
@@ -488,19 +499,31 @@ function readJsonc(filePath: string): any {
|
|
|
488
499
|
if (!fs.existsSync(actual)) return null;
|
|
489
500
|
}
|
|
490
501
|
try {
|
|
491
|
-
|
|
502
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
503
|
+
// jsonc-parser's parse() never throws: on garbage it returns undefined
|
|
504
|
+
// and only reports through the errors array. Without the array, the
|
|
505
|
+
// crash-zeroed accounts.jsonc (1932 NUL bytes) read as "no accounts"
|
|
506
|
+
// with not one log line — the daemon started with zero accounts and
|
|
507
|
+
// the only symptom was the "restart to apply" banner 14 s later.
|
|
508
|
+
const text = fs.readFileSync(actual, "utf-8").replace(/\r/g, "");
|
|
509
|
+
const errors: ParseError[] = [];
|
|
510
|
+
const data = parseJsonc(text, errors);
|
|
511
|
+
if (errors.length > 0) {
|
|
512
|
+
const first = errors[0];
|
|
513
|
+
console.error(`Failed to parse ${actual}: ${printParseErrorCode(first.error)} at offset ${first.offset} (${errors.length} error(s), ${text.length} chars)`);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
return data;
|
|
492
517
|
} catch (e: any) {
|
|
493
518
|
console.error(`Failed to read ${actual}: ${e.message}`);
|
|
494
519
|
return null;
|
|
495
520
|
}
|
|
496
521
|
}
|
|
497
522
|
|
|
523
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1): one write mechanism; the
|
|
524
|
+
// fsync lives in atomicWriteText (see its note).
|
|
498
525
|
function atomicWrite(filePath: string, data: any): void {
|
|
499
|
-
|
|
500
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
501
|
-
const tmp = filePath + ".tmp";
|
|
502
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
503
|
-
fs.renameSync(tmp, filePath);
|
|
526
|
+
atomicWriteText(filePath, JSON.stringify(data, null, 2));
|
|
504
527
|
}
|
|
505
528
|
|
|
506
529
|
/** Read a config file: shared first, local fallback, merge local overrides */
|
|
@@ -850,8 +873,9 @@ export function loadAccounts(): AccountConfig[] {
|
|
|
850
873
|
const norm = (s: string): string =>
|
|
851
874
|
s.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/[ \t\r\n]+$/, "");
|
|
852
875
|
if (norm(sharedContent) !== norm(localContent)) {
|
|
853
|
-
|
|
854
|
-
|
|
876
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1): crash-safe
|
|
877
|
+
// write (tmp + fsync + rename); see atomicWriteText.
|
|
878
|
+
atomicWriteText(localPath, sharedContent);
|
|
855
879
|
}
|
|
856
880
|
} catch { /* ignore */ }
|
|
857
881
|
}
|
|
@@ -1317,14 +1341,41 @@ function buildDictCsv(words: string[]): string {
|
|
|
1317
1341
|
return "# rmfmail user dictionary — one word per line\n" + sorted.join("\n") + "\n";
|
|
1318
1342
|
}
|
|
1319
1343
|
|
|
1320
|
-
|
|
1344
|
+
/** Crash-safe text write: tmp file → fsync → rename over the target.
|
|
1345
|
+
*
|
|
1346
|
+
* 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
1347
|
+
* Added the fsync and exported this for every local-cache writer. Kernel-Power
|
|
1348
|
+
* 41 crash at 16:36 today left `~/.rmfmail/accounts.jsonc` as 1932 NUL bytes:
|
|
1349
|
+
* NTFS journals metadata (the new size) but not data, so a file rewritten
|
|
1350
|
+
* moments before a hard crash comes back zero-filled. A rename alone does not
|
|
1351
|
+
* close that window — the tmp file's data can be unflushed too — hence the
|
|
1352
|
+
* fsync before the rename. The daemon then booted with 0 accounts. */
|
|
1353
|
+
export function atomicWriteText(filePath: string, text: string): void {
|
|
1321
1354
|
const dir = path.dirname(filePath);
|
|
1322
1355
|
fs.mkdirSync(dir, { recursive: true });
|
|
1323
1356
|
const tmp = filePath + ".tmp";
|
|
1324
|
-
fs.
|
|
1357
|
+
const fd = fs.openSync(tmp, "w");
|
|
1358
|
+
try {
|
|
1359
|
+
fs.writeSync(fd, text);
|
|
1360
|
+
fs.fsyncSync(fd);
|
|
1361
|
+
} finally {
|
|
1362
|
+
fs.closeSync(fd);
|
|
1363
|
+
}
|
|
1325
1364
|
fs.renameSync(tmp, filePath);
|
|
1326
1365
|
}
|
|
1327
1366
|
|
|
1367
|
+
/** Write `text` to `filePath` only when it differs from what is already there.
|
|
1368
|
+
* Returns true when a write happened. Identical content is skipped so a
|
|
1369
|
+
* periodic cloud poll doesn't keep the file in a perpetual just-written
|
|
1370
|
+
* window (see atomicWriteText) and doesn't fire fs.watch for nothing. */
|
|
1371
|
+
export function writeTextIfChanged(filePath: string, text: string): boolean {
|
|
1372
|
+
let existing: string = null;
|
|
1373
|
+
try { existing = fs.readFileSync(filePath, "utf-8"); } catch { existing = null; /* missing or unreadable → write it */ }
|
|
1374
|
+
if (existing === text) return false;
|
|
1375
|
+
atomicWriteText(filePath, text);
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1328
1379
|
/** Load user-added dictionary words. Mirrored to GDrive so "Add to dictionary"
|
|
1329
1380
|
* on one machine appears on every machine. Returns a deduped string array.
|
|
1330
1381
|
* Tries the cloud copy first (so a fresh machine sees prior words), then
|