dsh-plugin-update 0.1.2 → 0.3.0
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/CHANGELOG.md +49 -0
- package/README.md +483 -255
- package/dist/batch-run.d.ts +80 -0
- package/dist/batch-run.js +86 -0
- package/dist/batch.d.ts +111 -0
- package/dist/batch.js +183 -0
- package/dist/changelog-io.d.ts +56 -0
- package/dist/changelog-io.js +258 -0
- package/dist/changelog.d.ts +96 -0
- package/dist/changelog.js +287 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +75 -1
- package/dist/commands.d.ts +59 -0
- package/dist/commands.js +60 -4
- package/dist/config.d.ts +48 -0
- package/dist/config.js +9 -1
- package/dist/diag.d.ts +125 -0
- package/dist/diag.js +243 -0
- package/dist/entry.d.ts +60 -0
- package/dist/entry.js +305 -0
- package/dist/gate.d.ts +35 -0
- package/dist/host-batch.d.ts +113 -0
- package/dist/host-batch.js +778 -0
- package/dist/host.d.ts +129 -0
- package/dist/host.js +506 -52
- package/dist/panel-batch.d.ts +151 -0
- package/dist/panel-batch.js +927 -0
- package/dist/panel.d.ts +256 -0
- package/dist/panel.js +1300 -0
- package/dist/ports.d.ts +208 -0
- package/dist/queue.d.ts +109 -0
- package/dist/queue.js +182 -0
- package/dist/reader.d.ts +84 -0
- package/dist/reader.js +65 -6
- package/dist/redaction.d.ts +120 -0
- package/dist/redaction.js +205 -0
- package/dist/service.d.ts +72 -0
- package/dist/service.js +100 -6
- package/dist/store.d.ts +229 -0
- package/dist/store.js +251 -12
- package/package.json +27 -2
package/dist/store.js
CHANGED
|
@@ -3,7 +3,11 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { mkdir, open, readFile, rename, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { installRecipe } from "./commands.js";
|
|
6
|
-
import { BACKUP_FILE, LEGACY_PLUGIN_ID, LOCK_FILE, STATE_FILE } from "./config.js";
|
|
6
|
+
import { BACKUP_FILE, LEGACY_PLUGIN_ID, LOCK_FILE, SKIPPED_FILE, STATE_FILE } from "./config.js";
|
|
7
|
+
import { emptyBatchSession, normalizeBatchSession } from "./batch.js";
|
|
8
|
+
import { normalizeQueueState } from "./queue.js";
|
|
9
|
+
import { validReleaseVersion } from "./service.js";
|
|
10
|
+
import { sanitizeDetail } from "./redaction.js";
|
|
7
11
|
const LEGACY_SEGMENT = "updates";
|
|
8
12
|
const LEGACY_DIR_PLUGIN = LEGACY_PLUGIN_ID;
|
|
9
13
|
const JOB_STATES = ["installing", "verifying", "restart-required", "completed", "failed", "interrupted"];
|
|
@@ -173,6 +177,228 @@ function createUpdateDiskPorts(homeDir, pluginId, profileDir, deps = {}) {
|
|
|
173
177
|
}
|
|
174
178
|
return { paths, legacyPaths, readJob, writeJob, tryAcquireLock, releaseLock, backupJob };
|
|
175
179
|
}
|
|
180
|
+
const QUEUE_DIR_SEGMENT = "update-queue";
|
|
181
|
+
const QUEUE_FILE = "queue.json";
|
|
182
|
+
const QUEUE_LOCK_FILE = "global.lock";
|
|
183
|
+
const QUEUE_LOCK_STALE_MS = 15 * 6e4;
|
|
184
|
+
function queuePathsForUpdate(homeDir, profileDir) {
|
|
185
|
+
if (typeof homeDir !== "string" || !homeDir || typeof profileDir !== "string" || !profileDir) return null;
|
|
186
|
+
const directory = join(homeDir, QUEUE_DIR_SEGMENT, shortHash(profileDir));
|
|
187
|
+
return { directory, file: join(directory, QUEUE_FILE), lock: join(directory, QUEUE_LOCK_FILE) };
|
|
188
|
+
}
|
|
189
|
+
function queueFail() {
|
|
190
|
+
return Object.assign(new Error("queue-unavailable"), { code: "queue-unavailable" });
|
|
191
|
+
}
|
|
192
|
+
function createUpdateQueuePorts(homeDir, profileDir, deps = {}) {
|
|
193
|
+
const paths = queuePathsForUpdate(homeDir, profileDir);
|
|
194
|
+
const readText = deps.readFileImpl ?? ((filename, encoding) => readFile(filename, encoding));
|
|
195
|
+
const statFile = deps.statImpl ?? ((filename) => stat(filename));
|
|
196
|
+
const now = deps.nowImpl ?? Date.now;
|
|
197
|
+
async function readQueue() {
|
|
198
|
+
if (!paths) throw queueFail();
|
|
199
|
+
try {
|
|
200
|
+
if ((await statFile(paths.file)).size > 10 * 1024 * 1024) throw queueFail();
|
|
201
|
+
return normalizeQueueState(JSON.parse(await readText(paths.file, "utf8")));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error && error.code === "ENOENT") {
|
|
204
|
+
return normalizeQueueState(null);
|
|
205
|
+
}
|
|
206
|
+
throw error && error.code ? error : queueFail();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function writeQueue(state) {
|
|
210
|
+
if (!paths) throw queueFail();
|
|
211
|
+
try {
|
|
212
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
213
|
+
const temporary = `${paths.file}.${randomUUID()}.tmp`;
|
|
214
|
+
try {
|
|
215
|
+
await writeFile(temporary, `${JSON.stringify(normalizeQueueState(state), null, 2)}
|
|
216
|
+
`, { mode: 384, flag: "wx" });
|
|
217
|
+
await rename(temporary, paths.file);
|
|
218
|
+
} catch {
|
|
219
|
+
throw queueFail();
|
|
220
|
+
} finally {
|
|
221
|
+
await unlink(temporary).catch(() => {
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw error && error.code ? error : queueFail();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function readLockRaw() {
|
|
229
|
+
if (!paths) return null;
|
|
230
|
+
try {
|
|
231
|
+
if ((await statFile(paths.lock)).size > 1024 * 1024) return null;
|
|
232
|
+
return JSON.parse(await readText(paths.lock, "utf8"));
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function tryAcquireGlobalLock(lockId, pluginId, opts = {}) {
|
|
238
|
+
if (!paths) return false;
|
|
239
|
+
if (typeof lockId !== "string" || !lockId || typeof pluginId !== "string" || !pluginId) return false;
|
|
240
|
+
const timeoutMs = typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : QUEUE_LOCK_STALE_MS;
|
|
241
|
+
try {
|
|
242
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
243
|
+
const handle = await open(paths.lock, "wx", 384);
|
|
244
|
+
try {
|
|
245
|
+
await handle.writeFile(JSON.stringify({ id: lockId, pluginId, pid: process.pid, startedAt: now() }));
|
|
246
|
+
} finally {
|
|
247
|
+
await handle.close();
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (!error || error.code !== "EEXIST") return false;
|
|
252
|
+
try {
|
|
253
|
+
const current = await readLockRaw();
|
|
254
|
+
const startedAt = current && typeof current["startedAt"] === "number" ? current["startedAt"] : 0;
|
|
255
|
+
if (!current || now() - startedAt < timeoutMs) return false;
|
|
256
|
+
await unlink(paths.lock).catch(() => {
|
|
257
|
+
});
|
|
258
|
+
const handle = await open(paths.lock, "wx", 384);
|
|
259
|
+
try {
|
|
260
|
+
await handle.writeFile(JSON.stringify({ id: lockId, pluginId, pid: process.pid, startedAt: now() }));
|
|
261
|
+
} finally {
|
|
262
|
+
await handle.close();
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function releaseGlobalLock(lockId) {
|
|
271
|
+
if (!paths) return;
|
|
272
|
+
try {
|
|
273
|
+
const current = await readLockRaw();
|
|
274
|
+
if (current && current.id === lockId) await unlink(paths.lock).catch(() => {
|
|
275
|
+
});
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { paths, readQueue, writeQueue, tryAcquireGlobalLock, releaseGlobalLock };
|
|
280
|
+
}
|
|
281
|
+
const BATCH_FILE = "batch.json";
|
|
282
|
+
const BATCH_FILE_MAX_BYTES = 1024 * 1024;
|
|
283
|
+
function batchPathsForUpdate(homeDir, profileDir) {
|
|
284
|
+
const queue = queuePathsForUpdate(homeDir, profileDir);
|
|
285
|
+
if (!queue) return null;
|
|
286
|
+
return { directory: queue.directory, file: join(queue.directory, BATCH_FILE) };
|
|
287
|
+
}
|
|
288
|
+
function batchFail() {
|
|
289
|
+
return Object.assign(new Error("batch-unavailable"), { code: "batch-unavailable" });
|
|
290
|
+
}
|
|
291
|
+
function createBatchDiskPorts(homeDir, profileDir, deps = {}) {
|
|
292
|
+
const paths = batchPathsForUpdate(homeDir, profileDir);
|
|
293
|
+
const readText = deps.readFileImpl ?? ((filename, encoding) => readFile(filename, encoding));
|
|
294
|
+
const statFile = deps.statImpl ?? ((filename) => stat(filename));
|
|
295
|
+
async function readBatch() {
|
|
296
|
+
if (!paths) return emptyBatchSession();
|
|
297
|
+
try {
|
|
298
|
+
if ((await statFile(paths.file)).size > BATCH_FILE_MAX_BYTES) return emptyBatchSession();
|
|
299
|
+
return normalizeBatchSession(JSON.parse(await readText(paths.file, "utf8")));
|
|
300
|
+
} catch {
|
|
301
|
+
return emptyBatchSession();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function writeBatch(session) {
|
|
305
|
+
if (!paths) throw batchFail();
|
|
306
|
+
const normalized = normalizeBatchSession(session);
|
|
307
|
+
try {
|
|
308
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
309
|
+
const temporary = `${paths.file}.${randomUUID()}.tmp`;
|
|
310
|
+
try {
|
|
311
|
+
await writeFile(temporary, `${JSON.stringify(normalized, null, 2)}
|
|
312
|
+
`, { mode: 384, flag: "wx" });
|
|
313
|
+
await rename(temporary, paths.file);
|
|
314
|
+
} catch {
|
|
315
|
+
throw batchFail();
|
|
316
|
+
} finally {
|
|
317
|
+
await unlink(temporary).catch(() => {
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw error && error.code ? error : batchFail();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function clearBatch() {
|
|
325
|
+
if (!paths) return;
|
|
326
|
+
await writeBatch(emptyBatchSession());
|
|
327
|
+
}
|
|
328
|
+
return { paths, readBatch, writeBatch, clearBatch };
|
|
329
|
+
}
|
|
330
|
+
const MAX_SKIPPED_ENTRIES = 50;
|
|
331
|
+
function skipKey(pluginId, version) {
|
|
332
|
+
return `${String(pluginId)}@${String(version)}`;
|
|
333
|
+
}
|
|
334
|
+
function normalizeSkipped(raw) {
|
|
335
|
+
if (!raw || typeof raw !== "object") return [];
|
|
336
|
+
const list = raw.skipped;
|
|
337
|
+
if (!Array.isArray(list)) return [];
|
|
338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
339
|
+
const out = [];
|
|
340
|
+
for (const item of list) {
|
|
341
|
+
if (!item || typeof item !== "object") continue;
|
|
342
|
+
const version = item.version;
|
|
343
|
+
const skippedAt = item.skippedAt;
|
|
344
|
+
if (!validReleaseVersion(version) || seen.has(version)) continue;
|
|
345
|
+
seen.add(version);
|
|
346
|
+
out.push({ version, skippedAt: typeof skippedAt === "number" && Number.isFinite(skippedAt) ? skippedAt : 0 });
|
|
347
|
+
}
|
|
348
|
+
return out.slice(0, MAX_SKIPPED_ENTRIES);
|
|
349
|
+
}
|
|
350
|
+
function isVersionSkipped(skipped, version) {
|
|
351
|
+
if (!validReleaseVersion(version)) return false;
|
|
352
|
+
return normalizeSkipped({ skipped }).some((entry) => entry.version === version);
|
|
353
|
+
}
|
|
354
|
+
function addSkipped(skipped, version, now) {
|
|
355
|
+
if (!validReleaseVersion(version)) throw fail("install-failed");
|
|
356
|
+
const at = typeof now === "number" && Number.isFinite(now) ? now : Date.now();
|
|
357
|
+
const rest = normalizeSkipped({ skipped }).filter((entry) => entry.version !== version);
|
|
358
|
+
return [{ version, skippedAt: at }, ...rest].slice(0, MAX_SKIPPED_ENTRIES);
|
|
359
|
+
}
|
|
360
|
+
function clearSkipped(skipped, version) {
|
|
361
|
+
const base = normalizeSkipped({ skipped });
|
|
362
|
+
if (version === void 0) return [];
|
|
363
|
+
return base.filter((entry) => entry.version !== version);
|
|
364
|
+
}
|
|
365
|
+
function createSkipDiskPorts(homeDir, pluginId, profileDir, deps = {}) {
|
|
366
|
+
const paths = pathsForUpdate(homeDir, pluginId, profileDir);
|
|
367
|
+
const file = paths ? join(paths.directory, SKIPPED_FILE) : null;
|
|
368
|
+
const readText = deps.readFileImpl ?? ((filename, encoding) => readFile(filename, encoding));
|
|
369
|
+
const statFile = deps.statImpl ?? ((filename) => stat(filename));
|
|
370
|
+
async function readSkipped() {
|
|
371
|
+
if (!file) return [];
|
|
372
|
+
try {
|
|
373
|
+
if ((await statFile(file)).size > 64 * 1024) return [];
|
|
374
|
+
return normalizeSkipped(JSON.parse(await readText(file, "utf8")));
|
|
375
|
+
} catch {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function writeSkipped(skipped) {
|
|
380
|
+
if (!file || !paths) throw fail("install-failed");
|
|
381
|
+
const normalized = normalizeSkipped({ skipped });
|
|
382
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
383
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
384
|
+
try {
|
|
385
|
+
await writeFile(temporary, `${JSON.stringify({ skipped: normalized }, null, 2)}
|
|
386
|
+
`, { mode: 384, flag: "wx" });
|
|
387
|
+
await rename(temporary, file);
|
|
388
|
+
} catch {
|
|
389
|
+
throw fail("install-failed");
|
|
390
|
+
} finally {
|
|
391
|
+
await unlink(temporary).catch(() => {
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function clearSkippedVersion(version) {
|
|
396
|
+
const next = clearSkipped(await readSkipped(), version);
|
|
397
|
+
await writeSkipped(next);
|
|
398
|
+
return next;
|
|
399
|
+
}
|
|
400
|
+
return { file, readSkipped, writeSkipped, clearSkippedVersion };
|
|
401
|
+
}
|
|
176
402
|
const CLI_PACKAGE_NAME = "@deepseek-ai/dsh";
|
|
177
403
|
const INSTALL_STDIO = { stdin: "ignore", stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } };
|
|
178
404
|
const TERMINATION_GRACE_MS = 3e3;
|
|
@@ -294,19 +520,11 @@ async function runDesktopService(recipe, parts) {
|
|
|
294
520
|
if (code !== 0) throw withExit(fail("install-failed"), code);
|
|
295
521
|
return code;
|
|
296
522
|
}
|
|
297
|
-
const DETAIL_MAX = 300;
|
|
298
523
|
const MANAGER_OK = ["applied", "restart-required", "overridden"];
|
|
299
524
|
function withDetail(error, detail) {
|
|
300
525
|
if (detail) error.detail = detail;
|
|
301
526
|
return error;
|
|
302
527
|
}
|
|
303
|
-
const ABSOLUTE_PATH_RE = /[A-Za-z]:\\[^\s"']*|(^|\s)\/[^\s"']+/g;
|
|
304
|
-
function sanitizeDetail(text) {
|
|
305
|
-
const flat = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
306
|
-
if (!flat) return "";
|
|
307
|
-
const noPath = flat.replace(ABSOLUTE_PATH_RE, (_whole, lead) => `${lead || ""}<\u8DEF\u5F84>`);
|
|
308
|
-
return noPath.length > DETAIL_MAX ? `${noPath.slice(0, DETAIL_MAX)}\u2026` : noPath;
|
|
309
|
-
}
|
|
310
528
|
function detailText(error) {
|
|
311
529
|
if (typeof error === "string") return sanitizeDetail(error);
|
|
312
530
|
if (error instanceof Error) return sanitizeDetail(error.message);
|
|
@@ -315,7 +533,12 @@ function detailText(error) {
|
|
|
315
533
|
const code = typeof shape.code === "string" ? shape.code : "";
|
|
316
534
|
const said = typeof shape.diagnostic === "string" ? shape.diagnostic : typeof shape.message === "string" ? shape.message : "";
|
|
317
535
|
const line = [code, said].filter(Boolean).join(": ");
|
|
318
|
-
if (line)
|
|
536
|
+
if (line) {
|
|
537
|
+
const clean = sanitizeDetail(line);
|
|
538
|
+
if (clean) return clean;
|
|
539
|
+
if (code) return sanitizeDetail(code);
|
|
540
|
+
return "";
|
|
541
|
+
}
|
|
319
542
|
try {
|
|
320
543
|
return sanitizeDetail(JSON.stringify(error));
|
|
321
544
|
} catch {
|
|
@@ -460,7 +683,6 @@ function createUpdateExecutor(parts = {}) {
|
|
|
460
683
|
emitInstall(parts, recipe, false, error?.exitCode ?? exitCode, Date.now() - startedAt);
|
|
461
684
|
const detail = error?.detail;
|
|
462
685
|
throw Object.assign(fail("install-failed"), {
|
|
463
|
-
debug: String(error?.stack || error),
|
|
464
686
|
...typeof detail === "string" && detail ? { detail } : {}
|
|
465
687
|
});
|
|
466
688
|
}
|
|
@@ -468,10 +690,27 @@ function createUpdateExecutor(parts = {}) {
|
|
|
468
690
|
}
|
|
469
691
|
const __executorInternals = { INSTALL_STDIO, TERMINATION_GRACE_MS, CLI_PACKAGE_NAME };
|
|
470
692
|
export {
|
|
693
|
+
BATCH_FILE,
|
|
694
|
+
BATCH_FILE_MAX_BYTES,
|
|
695
|
+
MAX_SKIPPED_ENTRIES,
|
|
696
|
+
QUEUE_DIR_SEGMENT,
|
|
697
|
+
QUEUE_FILE,
|
|
698
|
+
QUEUE_LOCK_FILE,
|
|
699
|
+
QUEUE_LOCK_STALE_MS,
|
|
471
700
|
__executorInternals,
|
|
701
|
+
addSkipped,
|
|
702
|
+
batchPathsForUpdate,
|
|
703
|
+
clearSkipped,
|
|
704
|
+
createBatchDiskPorts,
|
|
705
|
+
createSkipDiskPorts,
|
|
472
706
|
createUpdateDiskPorts,
|
|
473
707
|
createUpdateExecutor,
|
|
708
|
+
createUpdateQueuePorts,
|
|
709
|
+
isVersionSkipped,
|
|
474
710
|
legacyPathsForUpdate,
|
|
711
|
+
normalizeSkipped,
|
|
475
712
|
pathsForUpdate,
|
|
476
|
-
|
|
713
|
+
queuePathsForUpdate,
|
|
714
|
+
resolveCliEntry,
|
|
715
|
+
skipKey
|
|
477
716
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-update",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "王辰浩",
|
|
@@ -12,8 +12,32 @@
|
|
|
12
12
|
"node": ">=22"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
|
+
"types": "./dist/host.d.ts",
|
|
15
16
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/host.d.ts",
|
|
19
|
+
"default": "./dist/host.js"
|
|
20
|
+
},
|
|
21
|
+
"./panel": {
|
|
22
|
+
"types": "./dist/panel.d.ts",
|
|
23
|
+
"default": "./dist/panel.js"
|
|
24
|
+
},
|
|
25
|
+
"./client": {
|
|
26
|
+
"types": "./dist/client.d.ts",
|
|
27
|
+
"default": "./dist/client.js"
|
|
28
|
+
},
|
|
29
|
+
"./batch": {
|
|
30
|
+
"types": "./dist/host-batch.d.ts",
|
|
31
|
+
"default": "./dist/host-batch.js"
|
|
32
|
+
},
|
|
33
|
+
"./panel-batch": {
|
|
34
|
+
"types": "./dist/panel-batch.d.ts",
|
|
35
|
+
"default": "./dist/panel-batch.js"
|
|
36
|
+
},
|
|
37
|
+
"./entry": {
|
|
38
|
+
"types": "./dist/entry.d.ts",
|
|
39
|
+
"default": "./dist/entry.js"
|
|
40
|
+
},
|
|
17
41
|
"./package.json": "./package.json"
|
|
18
42
|
},
|
|
19
43
|
"files": [
|
|
@@ -21,6 +45,7 @@
|
|
|
21
45
|
"derive-client-values.mjs",
|
|
22
46
|
"event-list.template.json",
|
|
23
47
|
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
24
49
|
"LICENSE"
|
|
25
50
|
],
|
|
26
51
|
"publishConfig": {
|