dsh-sessions-manager 3.4.1 → 3.4.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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir3, realpath, rename as rename3, stat, unlink, writeFile as writeFile3 } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join as join3 } from "node:path";
2
+ import { mkdir as mkdir4, realpath, rename as rename4, stat, unlink, writeFile as writeFile4 } from "node:fs/promises";
3
+ import { basename, dirname as dirname2, isAbsolute, join as join4 } from "node:path";
4
4
  import { readFileSync as readFileSync4 } from "node:fs";
5
5
  import { homedir as homedir3 } from "node:os";
6
6
 
@@ -500,12 +500,116 @@ function createSessionMetaCache(opts = {}) {
500
500
  };
501
501
  }
502
502
 
503
+ // src/title-persist-index.js
504
+ import { mkdir as mkdir3, readFile, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
505
+ import { dirname, join as join3 } from "node:path";
506
+ var TITLE_INDEX_SCHEMA_VERSION = 1;
507
+ var MAX_ENTRIES = 2e4;
508
+ function normalizeEntry(raw) {
509
+ if (!raw || typeof raw !== "object") return null;
510
+ const title = typeof raw.title === "string" ? raw.title : null;
511
+ const cwd = typeof raw.cwd === "string" ? raw.cwd : null;
512
+ const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
513
+ const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
514
+ const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
515
+ if (!fingerprint || !title && !cwd) return null;
516
+ return { title, cwd, createdAt, fingerprint, updatedAt };
517
+ }
518
+ function normalizeTitleIndex(raw) {
519
+ const entries = {};
520
+ if (raw && typeof raw === "object" && raw.entries && typeof raw.entries === "object") {
521
+ for (const [id, entry] of Object.entries(raw.entries)) {
522
+ if (typeof id !== "string" || !id || id.length > 200) continue;
523
+ const normalized = normalizeEntry(entry);
524
+ if (normalized) entries[id] = normalized;
525
+ }
526
+ }
527
+ return { schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries };
528
+ }
529
+ function mergeEntries(left, right) {
530
+ const merged = { ...left };
531
+ for (const [id, entry] of Object.entries(right)) merged[id] = entry;
532
+ const ids = Object.keys(merged);
533
+ if (ids.length > MAX_ENTRIES) {
534
+ ids.sort((a, b) => (merged[a].updatedAt || 0) - (merged[b].updatedAt || 0));
535
+ for (const id of ids.slice(0, ids.length - MAX_ENTRIES)) delete merged[id];
536
+ }
537
+ return merged;
538
+ }
539
+ function createTitleIndexStore({ dir, file }) {
540
+ let cache = null;
541
+ let chain = Promise.resolve();
542
+ const path = file || join3(dir, "title-index.json");
543
+ async function readRaw() {
544
+ try {
545
+ return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
546
+ } catch (e) {
547
+ return normalizeTitleIndex(null);
548
+ }
549
+ }
550
+ function enqueue(mutator) {
551
+ const operation = chain.then(async () => {
552
+ const store = cache || (cache = (await readRaw()).entries);
553
+ await mutator(store);
554
+ return store;
555
+ });
556
+ chain = operation.catch(() => {
557
+ });
558
+ return operation;
559
+ }
560
+ return {
561
+ // 只读:内存优先,未加载过才落盘一次。绝不抛错。
562
+ async entries() {
563
+ if (cache) return cache;
564
+ cache = (await readRaw()).entries;
565
+ return cache;
566
+ },
567
+ // 批量合并写入(原子替换)。失败静默:索引只是加速器,坏了下次重解码。
568
+ async merge(batch) {
569
+ const right = {};
570
+ for (const [id, entry] of Object.entries(batch || {})) {
571
+ const normalized = normalizeEntry(entry);
572
+ if (normalized) right[String(id)] = normalized;
573
+ }
574
+ if (!Object.keys(right).length) return false;
575
+ await enqueue(async (store) => {
576
+ const next = mergeEntries(store, right);
577
+ await mkdir3(dirname(path), { recursive: true });
578
+ const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
579
+ await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
580
+ await rename3(tmp, path);
581
+ cache = next;
582
+ });
583
+ return true;
584
+ },
585
+ async remove(ids) {
586
+ const wanted = new Set((ids || []).map(String));
587
+ if (!wanted.size) return false;
588
+ await enqueue(async (store) => {
589
+ let changed = false;
590
+ for (const id of wanted) {
591
+ if (id in store) {
592
+ delete store[id];
593
+ changed = true;
594
+ }
595
+ }
596
+ if (!changed) return;
597
+ await mkdir3(dirname(path), { recursive: true });
598
+ const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
599
+ await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
600
+ await rename3(tmp, path);
601
+ });
602
+ return true;
603
+ }
604
+ };
605
+ }
606
+
503
607
  // src/index.js
504
608
  var name = "dsh-sessions-manager";
505
609
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
506
610
  var MAX_TITLE = 80;
507
- var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join3(homedir3(), ".dsh", "sessions-manager-trash");
508
- var TRASH_INDEX = join3(TRASH_DIR, "index.json");
611
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join4(homedir3(), ".dsh", "sessions-manager-trash");
612
+ var TRASH_INDEX = join4(TRASH_DIR, "index.json");
509
613
  var TRASH_SCHEMA_VERSION = 2;
510
614
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
511
615
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -590,6 +694,40 @@ function apply(ctx) {
590
694
  const dom = () => ctx.storageDomain.get("workspace");
591
695
  const authorityTitleCache = /* @__PURE__ */ new Map();
592
696
  const metaCache = createSessionMetaCache();
697
+ const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join4(TRASH_DIR, "title-index.json") });
698
+ async function hydrateFromPersist(ids, statsById) {
699
+ const hits = /* @__PURE__ */ new Map();
700
+ if (!ids || !ids.length) return hits;
701
+ let store;
702
+ try {
703
+ store = await titleIndex.entries();
704
+ } catch (e) {
705
+ return hits;
706
+ }
707
+ for (const id of ids) {
708
+ const stat2 = statsById.get(id);
709
+ const entry = store && store[id];
710
+ if (!stat2 || !entry) continue;
711
+ const fp = fingerprintOf(stat2);
712
+ if (fp && entry.fingerprint === fp) {
713
+ hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
714
+ }
715
+ }
716
+ return hits;
717
+ }
718
+ function persistDecoded(decoded, statsById) {
719
+ if (!decoded || !decoded.size) return;
720
+ const batch = {};
721
+ const now = Date.now();
722
+ for (const [id, meta] of decoded) {
723
+ const fp = fingerprintOf(statsById.get(id));
724
+ if (!fp) continue;
725
+ batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
726
+ }
727
+ if (!Object.keys(batch).length) return;
728
+ titleIndex.merge(batch).catch(() => {
729
+ });
730
+ }
593
731
  function metaFromSnapshot(o) {
594
732
  let title = null, createdAt = null, cwd = null;
595
733
  if (o) {
@@ -686,6 +824,7 @@ function apply(ctx) {
686
824
  }
687
825
  }
688
826
  metaCache.set(key, statInfo, meta);
827
+ if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
689
828
  return buildItem(key, meta, usage, opts.exposeUsage);
690
829
  }
691
830
  async function collectUsage(preloadedHeaders) {
@@ -746,10 +885,10 @@ function apply(ctx) {
746
885
  return (await readTrashStore()).items;
747
886
  }
748
887
  async function writeTrashStore(store) {
749
- await mkdir3(TRASH_DIR, { recursive: true });
750
- const tmp = join3(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
751
- await writeFile3(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
752
- await rename3(tmp, TRASH_INDEX);
888
+ await mkdir4(TRASH_DIR, { recursive: true });
889
+ const tmp = join4(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
890
+ await writeFile4(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
891
+ await rename4(tmp, TRASH_INDEX);
753
892
  }
754
893
  function mutateTrash(mutator) {
755
894
  const operation = trashMutation.then(async () => {
@@ -855,7 +994,7 @@ function apply(ctx) {
855
994
  } catch (e) {
856
995
  }
857
996
  if (!target && typeof entry.originalPath === "string") target = entry.originalPath;
858
- const targetOwnsSession = target && (basename(dirname(target)) === sid || basename(target).includes(sid));
997
+ const targetOwnsSession = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
859
998
  if (target && !targetOwnsSession) {
860
999
  const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
861
1000
  error.status = 409;
@@ -948,8 +1087,8 @@ function apply(ctx) {
948
1087
  async function moveTargetWorkspace(rawPath) {
949
1088
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
950
1089
  let p = String(rawPath).trim();
951
- if (p.startsWith("~/")) p = join3(homedir3(), p.slice(2));
952
- if (!isAbsolute(p)) p = join3(homedir3(), p);
1090
+ if (p.startsWith("~/")) p = join4(homedir3(), p.slice(2));
1091
+ if (!isAbsolute(p)) p = join4(homedir3(), p);
953
1092
  let canonical = null;
954
1093
  try {
955
1094
  canonical = await realpath(p);
@@ -957,7 +1096,7 @@ function apply(ctx) {
957
1096
  canonical = null;
958
1097
  }
959
1098
  if (canonical === null) {
960
- await mkdir3(p, { recursive: true });
1099
+ await mkdir4(p, { recursive: true });
961
1100
  canonical = await realpath(p);
962
1101
  }
963
1102
  return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
@@ -995,13 +1134,13 @@ function apply(ctx) {
995
1134
  if (!oldPath || !newPath || oldPath === newPath) return false;
996
1135
  const backupPath = `${oldPath}.move-backup-${Date.now()}`;
997
1136
  try {
998
- await mkdir3(dirname(newPath), { recursive: true });
999
- await rename3(oldPath, backupPath);
1137
+ await mkdir4(dirname2(newPath), { recursive: true });
1138
+ await rename4(oldPath, backupPath);
1000
1139
  await rewriteFrame0Cwd(backupPath, canonical);
1001
- await rename3(backupPath, newPath);
1140
+ await rename4(backupPath, newPath);
1002
1141
  } catch (e) {
1003
1142
  try {
1004
- await rename3(backupPath, oldPath);
1143
+ await rename4(backupPath, oldPath);
1005
1144
  } catch (_) {
1006
1145
  }
1007
1146
  if (e && e.code !== "ENOENT") throw e;
@@ -1047,7 +1186,7 @@ function apply(ctx) {
1047
1186
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
1048
1187
  if (backupPath) {
1049
1188
  try {
1050
- await rename3(oldPath, backupPath);
1189
+ await rename4(oldPath, backupPath);
1051
1190
  } catch (e) {
1052
1191
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
1053
1192
  }
@@ -1055,7 +1194,7 @@ function apply(ctx) {
1055
1194
  const restore = async () => {
1056
1195
  if (backupPath) {
1057
1196
  try {
1058
- await rename3(backupPath, oldPath);
1197
+ await rename4(backupPath, oldPath);
1059
1198
  } catch (_) {
1060
1199
  }
1061
1200
  }
@@ -1203,15 +1342,24 @@ function apply(ctx) {
1203
1342
  const usage = await collectUsage(headers);
1204
1343
  const statsById = new Map(visibleIds.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1205
1344
  const { cached, missing } = metaCache.partition(visibleIds, statsById);
1206
- const snapshotById = await projectTitles(missing);
1345
+ const persisted = await hydrateFromPersist(missing, statsById);
1346
+ for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
1347
+ const stillMissing = missing.filter((id) => !persisted.has(id));
1348
+ const snapshotById = await projectTitles(stillMissing);
1349
+ const decoded = /* @__PURE__ */ new Map();
1350
+ const collectDecoded = (id, meta) => {
1351
+ decoded.set(id, meta);
1352
+ };
1207
1353
  const CHUNK = 6;
1208
1354
  for (let i = 0; i < visibleIds.length; i += CHUNK) {
1209
1355
  const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage, {
1210
1356
  exposeUsage: !!(opts && opts.usage),
1211
- preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0
1357
+ preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
1358
+ collectDecoded
1212
1359
  })));
1213
1360
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
1214
1361
  }
1362
+ persistDecoded(decoded, statsById);
1215
1363
  let starredSet = /* @__PURE__ */ new Set();
1216
1364
  try {
1217
1365
  starredSet = new Set((await stars.read()).starredSessionIds);
@@ -1278,17 +1426,26 @@ function apply(ctx) {
1278
1426
  const usage = await collectUsage(headers);
1279
1427
  const statsById = new Map(ids.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1280
1428
  const { cached, missing } = metaCache.partition(ids, statsById);
1281
- const snapshotById = await projectTitles(missing);
1429
+ const persisted = await hydrateFromPersist(missing, statsById);
1430
+ for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
1431
+ const rest = missing.filter((id) => !persisted.has(id));
1432
+ const snapshotById = await projectTitles(rest);
1433
+ const decoded = /* @__PURE__ */ new Map();
1434
+ const collectDecoded = (id, meta) => {
1435
+ decoded.set(id, meta);
1436
+ };
1282
1437
  for (const id of ids) {
1283
- let meta = cached.get(id) || null;
1438
+ let meta = cached.get(id) || persisted.get(id) || null;
1284
1439
  if (!meta) {
1285
1440
  const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
1286
1441
  const next = metaFromSnapshot(snapshot);
1287
1442
  metaCache.set(id, statsById.get(id), next);
1443
+ if (statsById.get(id)) collectDecoded(id, next);
1288
1444
  meta = next;
1289
1445
  }
1290
1446
  if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
1291
1447
  }
1448
+ persistDecoded(decoded, statsById);
1292
1449
  }
1293
1450
  return {
1294
1451
  titles: Object.fromEntries(authorityTitleCache),
@@ -1619,6 +1776,8 @@ function apply(ctx) {
1619
1776
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
1620
1777
  const out = await purgeFromTrash(sid);
1621
1778
  metaCache.invalidate(sid);
1779
+ titleIndex.remove([sid]).catch(() => {
1780
+ });
1622
1781
  json(res, out);
1623
1782
  } catch (e) {
1624
1783
  json(res, { ok: false, error: String(e && e.message || e) }, 500);
@@ -1638,6 +1797,8 @@ function apply(ctx) {
1638
1797
  try {
1639
1798
  results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
1640
1799
  metaCache.invalidate(sid);
1800
+ titleIndex.remove([sid]).catch(() => {
1801
+ });
1641
1802
  } catch (e) {
1642
1803
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
1643
1804
  }
@@ -1819,8 +1980,16 @@ function apply(ctx) {
1819
1980
  const patch = {};
1820
1981
  if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
1821
1982
  if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
1822
- const settings = Object.keys(patch).length ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
1823
- const sweep = await autoArchiveSweep();
1983
+ const isPatch = Object.keys(patch).length > 0;
1984
+ const settings = isPatch ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
1985
+ let sweep;
1986
+ if (isPatch) {
1987
+ sweep = await autoArchiveSweep();
1988
+ } else {
1989
+ void autoArchiveSweep().catch(() => {
1990
+ });
1991
+ sweep = { triggered: true };
1992
+ }
1824
1993
  const store = await autoArchive.read();
1825
1994
  json(res, {
1826
1995
  ok: true,