dsh-session-manager 0.5.3 → 0.6.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.
@@ -64,6 +64,60 @@ function versionFromArgv() {
64
64
  return undefined;
65
65
  }
66
66
 
67
+ /**
68
+ * Compare two semver strings (e.g. "0.5.4" vs "0.5.5", "0.5.5-beta.10" vs "0.5.5-beta.2").
69
+ * Returns 1 if v1 > v2, -1 if v1 < v2, and 0 if equal.
70
+ */
71
+ export function compareSemver(v1, v2) {
72
+ if (typeof v1 !== "string" || typeof v2 !== "string") return 0;
73
+ const parse = (v) => {
74
+ // Strip build metadata (+...)
75
+ const noBuild = v.trim().replace(/^[vV]/, "").split("+")[0];
76
+ const hyphenIdx = noBuild.indexOf("-");
77
+ const core = hyphenIdx === -1 ? noBuild : noBuild.slice(0, hyphenIdx);
78
+ const prerelease = hyphenIdx === -1 ? undefined : noBuild.slice(hyphenIdx + 1);
79
+ const parts = (core || "").split(".").map(n => parseInt(n, 10) || 0);
80
+ while (parts.length < 3) parts.push(0);
81
+ return { parts, prerelease: prerelease !== undefined ? prerelease.trim() : undefined };
82
+ };
83
+ const p1 = parse(v1);
84
+ const p2 = parse(v2);
85
+ for (let i = 0; i < 3; i++) {
86
+ if (p1.parts[i] > p2.parts[i]) return 1;
87
+ if (p1.parts[i] < p2.parts[i]) return -1;
88
+ }
89
+ // When major, minor, patch match: version without prerelease is higher
90
+ if (p1.prerelease && !p2.prerelease) return -1;
91
+ if (!p1.prerelease && p2.prerelease) return 1;
92
+ if (p1.prerelease && p2.prerelease) {
93
+ if (p1.prerelease === p2.prerelease) return 0;
94
+ const id1 = p1.prerelease.split(".");
95
+ const id2 = p2.prerelease.split(".");
96
+ const maxLen = Math.max(id1.length, id2.length);
97
+ for (let i = 0; i < maxLen; i++) {
98
+ if (i >= id1.length) return -1; // fewer identifiers is lower precedence
99
+ if (i >= id2.length) return 1;
100
+ const part1 = id1[i];
101
+ const part2 = id2[i];
102
+ if (part1 === part2) continue;
103
+ const num1 = /^\d+$/.test(part1) ? parseInt(part1, 10) : null;
104
+ const num2 = /^\d+$/.test(part2) ? parseInt(part2, 10) : null;
105
+ if (num1 !== null && num2 !== null) {
106
+ if (num1 > num2) return 1;
107
+ if (num1 < num2) return -1;
108
+ } else if (num1 !== null && num2 === null) {
109
+ return -1; // numeric has lower precedence than non-numeric
110
+ } else if (num1 === null && num2 !== null) {
111
+ return 1;
112
+ } else {
113
+ if (part1 > part2) return 1;
114
+ if (part1 < part2) return -1;
115
+ }
116
+ }
117
+ }
118
+ return 0;
119
+ }
120
+
67
121
  /** Normalize a persistence list item from both snapshot (header field) and raw header shapes. **/
68
122
  export function normalizeSessionHeader(value) {
69
123
  if (value === undefined || value === null) return undefined;
@@ -95,6 +149,7 @@ export function createDshAdapter(ctx) {
95
149
  get agentPresets() { return getService(ctx, "agentPresets"); },
96
150
  get projectionCache() { return getService(ctx, "sessionProjectionCache"); },
97
151
  get homePath() { return getService(ctx, "dshHomePath"); },
152
+ get pluginManager() { return getService(ctx, "pluginManager"); },
98
153
 
99
154
  getSession(id) {
100
155
  try { return adapter.sessions && adapter.sessions.get && adapter.sessions.get(id); } catch { return undefined; }
package/lib/index.js CHANGED
@@ -26,7 +26,7 @@ import { lstat, mkdir, readdir, realpath, rename, rm } from "node:fs/promises";
26
26
  import { basename, dirname, join } from "node:path";
27
27
  import { randomBytes } from "node:crypto";
28
28
  import { createAnnotationStore } from "./annotation-store.js";
29
- import { createDshAdapter, normalizeSessionHeader } from "./compat/dsh-adapter.js";
29
+ import { createDshAdapter, normalizeSessionHeader, compareSemver } from "./compat/dsh-adapter.js";
30
30
  import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
31
31
 
32
32
  export const name = "dsh-session-manager";
@@ -53,6 +53,7 @@ function safeErrorMessage(error) {
53
53
  }
54
54
  try { return JSON.stringify(error); } catch { return String(error); }
55
55
  }
56
+
56
57
  const API_PREFIX = "/session-manager/api";
57
58
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
58
59
  function liveWriterOf(persistence, sessionId) {
@@ -1239,6 +1240,86 @@ ${rest}`, "utf8");
1239
1240
  const result = await scanPreset({ id: url.searchParams.get("workspaceId") || "", sessionId });
1240
1241
  return send(res, 200, { ok: true, result });
1241
1242
  }
1243
+ if (req.method === "GET" && path === "/update/check") {
1244
+ const currentVersion = adapter.pluginVersion() || "0.6.2";
1245
+ let latestVersion = currentVersion;
1246
+ let hasUpdate = false;
1247
+ try {
1248
+ const requestedRegistry = url.searchParams.get("registry")?.trim();
1249
+ const registry = (requestedRegistry && /^https?:\/\//i.test(requestedRegistry))
1250
+ ? requestedRegistry
1251
+ : (process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org");
1252
+ const base = registry.endsWith("/") ? registry : registry + "/";
1253
+ const pkgUrl = new URL("dsh-session-manager/latest", base).toString();
1254
+ const controller = new AbortController();
1255
+ const timer = setTimeout(() => controller.abort(), 6000);
1256
+ try {
1257
+ const response = await fetch(pkgUrl, {
1258
+ signal: controller.signal,
1259
+ headers: { "accept": "application/json" }
1260
+ });
1261
+ if (response.ok) {
1262
+ const data = await response.json();
1263
+ if (data && typeof data.version === "string") {
1264
+ latestVersion = data.version;
1265
+ }
1266
+ }
1267
+ } finally {
1268
+ clearTimeout(timer);
1269
+ }
1270
+ hasUpdate = compareSemver(latestVersion, currentVersion) > 0;
1271
+ return send(res, 200, {
1272
+ ok: true,
1273
+ result: { currentVersion, latestVersion, hasUpdate }
1274
+ });
1275
+ } catch (err) {
1276
+ return send(res, 200, {
1277
+ ok: false,
1278
+ code: "update-check-failed",
1279
+ error: "检查更新失败: " + safeErrorMessage(err),
1280
+ result: { currentVersion, latestVersion: currentVersion, hasUpdate: false }
1281
+ });
1282
+ }
1283
+ }
1284
+ if (req.method === "POST" && path === "/update/install") {
1285
+ const rawVersion = typeof body?.version === "string" ? body.version.trim() : "";
1286
+ if (rawVersion && !/^[a-zA-Z0-9_.-]+$/.test(rawVersion)) {
1287
+ return send(res, 400, { ok: false, code: "bad-request", error: "Invalid version format" });
1288
+ }
1289
+ const targetVersion = rawVersion || "latest";
1290
+ const spec = "dsh-session-manager@" + targetVersion;
1291
+ const registry = typeof body?.registry === "string" && /^https?:\/\//i.test(body.registry.trim()) ? body.registry.trim() : "";
1292
+ const pm = adapter.pluginManager;
1293
+ if (pm && typeof pm.installBundle === "function") {
1294
+ try {
1295
+ const prevRegistry = process.env.NPM_CONFIG_REGISTRY;
1296
+ if (registry) {
1297
+ process.env.NPM_CONFIG_REGISTRY = registry;
1298
+ }
1299
+ let outcome;
1300
+ try {
1301
+ outcome = await (registry ? pm.installBundle(spec, { registry }) : pm.installBundle(spec));
1302
+ } finally {
1303
+ if (registry) {
1304
+ if (prevRegistry !== undefined) {
1305
+ process.env.NPM_CONFIG_REGISTRY = prevRegistry;
1306
+ } else {
1307
+ delete process.env.NPM_CONFIG_REGISTRY;
1308
+ }
1309
+ }
1310
+ }
1311
+ const application = outcome?.application || "restart-required";
1312
+ return send(res, 200, { ok: true, result: { application, spec } });
1313
+ } catch (err) {
1314
+ return send(res, 500, { ok: false, code: "install-failed", error: safeErrorMessage(err) });
1315
+ }
1316
+ }
1317
+ return send(res, 501, {
1318
+ ok: false,
1319
+ code: "plugin-manager-unavailable",
1320
+ error: "当前宿主环境未提供 PluginManager 安装服务,请通过 DSH 插件市场或命令行更新"
1321
+ });
1322
+ }
1242
1323
  return send(res, 404, { ok: false, error: "not found: " + req.method + " " + path });
1243
1324
  } catch (error) {
1244
1325
  ctx.logger.warn("session-manager: api error: " + safeErrorMessage(error) + "; json=" + safeJson(error));
@@ -1251,4 +1332,4 @@ ${rest}`, "utf8");
1251
1332
  }), "session-manager: http api");
1252
1333
  }
1253
1334
 
1254
- export { listSessionHeaders, scanSessionHeaders, encodeSessionSegment };
1335
+ export { listSessionHeaders, scanSessionHeaders, encodeSessionSegment, compareSemver };
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "dsh-session-manager",
3
- "version": "0.5.3",
4
- "description": "DSH Web 会话管理:删除、归档、跨工作区移动、迁移预设;收藏、待看、搜索、排序、设置优先级,添加标签和备注;批量处理。欢迎至 GitHub 提意见。",
3
+ "version": "0.6.2",
4
+ "description": "DeepSeek Harness session manager: delete, archive, move across workspaces, migrate presets, favorites, review-later, search, filter, sort, priority, tags, notes, and batch actions. | DSH 会话管理:删除、归档、跨工作区移动、预设迁移、收藏、待回看、搜索、筛选、排序、优先级、标签、备注及批量操作。",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "dsh",
8
+ "dsh-plugin",
8
9
  "deepseek-harness",
10
+ "deepseek-harness-plugin",
9
11
  "plugin",
10
12
  "session",
13
+ "session-manager",
14
+ "session-management",
11
15
  "archive",
12
16
  "move",
13
17
  "workspace",
14
18
  "preset",
15
- "migration"
19
+ "agent-preset",
20
+ "migration",
21
+ "cordis"
16
22
  ],
17
23
  "repository": {
18
24
  "type": "git",