dsh-plugin-update 0.1.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/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/client.js +25 -0
- package/dist/commands.js +80 -0
- package/dist/config.js +97 -0
- package/dist/gate.js +115 -0
- package/dist/host.js +279 -0
- package/dist/ports.js +5 -0
- package/dist/reader.js +193 -0
- package/dist/service.js +355 -0
- package/dist/store.js +346 -0
- package/event-list.template.json +6 -0
- package/package.json +33 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// AUTO-GENERATED by node packages/dsh-plugin-update/build.mjs — DO NOT EDIT. Source: packages/dsh-plugin-update/src/service.ts
|
|
2
|
+
const PACKAGE_NAME = "dsh-mattpocock-skills-deck";
|
|
3
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
4
|
+
const CHECK_TIMEOUT_MS = 1e4;
|
|
5
|
+
const CONFIRMATION_TTL_MS = 10 * 6e4;
|
|
6
|
+
const RECHECK_WINDOW_MS = 2e3;
|
|
7
|
+
const MAX_METADATA_BYTES = 256 * 1024;
|
|
8
|
+
const INTEGRITY_PATTERN = "^sha512-[A-Za-z0-9+/]{86}==$";
|
|
9
|
+
function validRequestId(v) {
|
|
10
|
+
return typeof v === "string" && v.trim().length >= 1 && v.trim().length <= 128;
|
|
11
|
+
}
|
|
12
|
+
function updateError(code) {
|
|
13
|
+
return Object.assign(new Error(code), { code });
|
|
14
|
+
}
|
|
15
|
+
function validVersion(v) {
|
|
16
|
+
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
|
|
17
|
+
}
|
|
18
|
+
function parseTriple(v) {
|
|
19
|
+
const parts = String(v).split(".");
|
|
20
|
+
if (parts.length > 3) return null;
|
|
21
|
+
const nums = [];
|
|
22
|
+
for (const p of parts) {
|
|
23
|
+
if (!/^\d+$/.test(p)) return null;
|
|
24
|
+
const n = Number(p);
|
|
25
|
+
if (!Number.isSafeInteger(n)) return null;
|
|
26
|
+
nums.push(n);
|
|
27
|
+
}
|
|
28
|
+
while (nums.length < 3) nums.push(0);
|
|
29
|
+
return [nums[0], nums[1], nums[2]];
|
|
30
|
+
}
|
|
31
|
+
function compareVersions(a, b) {
|
|
32
|
+
const pa = parseTriple(a);
|
|
33
|
+
const pb = parseTriple(b);
|
|
34
|
+
if (!pa || !pb) throw updateError("invalid-release");
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
if (pa[i] < pb[i]) return -1;
|
|
37
|
+
if (pa[i] > pb[i]) return 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
function compareOne(node, op, target) {
|
|
42
|
+
const order = compareTriple(node, target);
|
|
43
|
+
if (op === "=" || op === "") return order === 0;
|
|
44
|
+
if (op === ">") return order === 1;
|
|
45
|
+
if (op === ">=") return order >= 0;
|
|
46
|
+
if (op === "<") return order === -1;
|
|
47
|
+
if (op === "<=") return order <= 0;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
function compareTriple(a, b) {
|
|
51
|
+
for (let i = 0; i < 3; i++) {
|
|
52
|
+
if (a[i] < b[i]) return -1;
|
|
53
|
+
if (a[i] > b[i]) return 1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
function upperBound(t, kind, kept) {
|
|
58
|
+
if (kind === "^") {
|
|
59
|
+
if (kept >= 1) return [t[0] + 1, 0, 0];
|
|
60
|
+
return [0, t[1] + 1, 0];
|
|
61
|
+
}
|
|
62
|
+
if (kept >= 2) return [t[0], t[1] + 1, 0];
|
|
63
|
+
return [t[0] + 1, 0, 0];
|
|
64
|
+
}
|
|
65
|
+
function satisfiesNodeRange(nodeVersion, range) {
|
|
66
|
+
if (range === void 0 || range === null) return true;
|
|
67
|
+
const text = String(range).trim();
|
|
68
|
+
if (text === "" || text === "*") return true;
|
|
69
|
+
const node = parseTriple(String(nodeVersion).replace(/^v/, ""));
|
|
70
|
+
if (!node) return true;
|
|
71
|
+
if (text.includes("||")) return true;
|
|
72
|
+
const parts = text.split(/\s+/).filter(Boolean);
|
|
73
|
+
if (parts.length === 0) return true;
|
|
74
|
+
for (const part of parts) {
|
|
75
|
+
const m = part.match(/^(\^|~|>=|<=|>|<|=)?(.+)$/);
|
|
76
|
+
if (!m) return true;
|
|
77
|
+
const op = m[1] || "";
|
|
78
|
+
const target = parseTriple(m[2]);
|
|
79
|
+
if (!target) return true;
|
|
80
|
+
if (op === "^" || op === "~") {
|
|
81
|
+
const kept = m[2].split(".").length;
|
|
82
|
+
if (compareTriple(node, target) < 0) return false;
|
|
83
|
+
if (compareTriple(node, upperBound(target, op, kept)) >= 0) return false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!compareOne(node, op, target)) return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function byteLength(text) {
|
|
91
|
+
try {
|
|
92
|
+
return new TextEncoder().encode(text).length;
|
|
93
|
+
} catch {
|
|
94
|
+
return text.length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function timeoutSignal(ms) {
|
|
98
|
+
try {
|
|
99
|
+
const ctor = globalThis.AbortSignal;
|
|
100
|
+
if (ctor && typeof ctor.timeout === "function") return ctor.timeout(ms);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
106
|
+
const targetName = opts?.targetPackageName ?? PACKAGE_NAME;
|
|
107
|
+
const registry = opts?.registryUrl ?? NPM_REGISTRY;
|
|
108
|
+
let response = null;
|
|
109
|
+
try {
|
|
110
|
+
response = await fetchImpl(`${registry}${encodeURIComponent(targetName)}/latest`, {
|
|
111
|
+
headers: { accept: "application/json" },
|
|
112
|
+
redirect: "error",
|
|
113
|
+
signal: timeoutSignal(timeoutMs)
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
throw updateError("check-failed");
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
if (!response.ok) throw updateError("check-failed");
|
|
120
|
+
const declared = Number(response.headers.get("content-length"));
|
|
121
|
+
if (Number.isFinite(declared) && declared > MAX_METADATA_BYTES) throw updateError("invalid-release");
|
|
122
|
+
const text = await response.text();
|
|
123
|
+
if (byteLength(text) > MAX_METADATA_BYTES) throw updateError("invalid-release");
|
|
124
|
+
const value = JSON.parse(text);
|
|
125
|
+
if (value.name !== targetName || !validVersion(value.version)) throw updateError("invalid-release");
|
|
126
|
+
const version = value.version;
|
|
127
|
+
const nodeRange = value.engines?.node;
|
|
128
|
+
if (nodeRange !== void 0 && typeof nodeRange !== "string") throw updateError("invalid-release");
|
|
129
|
+
const tarballText = value.dist?.tarball;
|
|
130
|
+
const integrity = value.dist?.integrity;
|
|
131
|
+
let tarball;
|
|
132
|
+
try {
|
|
133
|
+
tarball = new URL(String(tarballText));
|
|
134
|
+
} catch {
|
|
135
|
+
throw updateError("invalid-release");
|
|
136
|
+
}
|
|
137
|
+
const registryOrigin = new URL(registry).origin;
|
|
138
|
+
const shapeOk = tarball.origin === registryOrigin && !tarball.username && !tarball.password && !tarball.search && !tarball.hash && tarball.pathname === `/${targetName}/-/${targetName}-${version}.tgz` && typeof integrity === "string" && new RegExp(INTEGRITY_PATTERN).test(integrity);
|
|
139
|
+
if (!shapeOk) throw updateError("invalid-release");
|
|
140
|
+
return { version, nodeRange: typeof nodeRange === "string" ? nodeRange : "*", integrity, tarball: tarball.href };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error?.code === "check-failed") throw error;
|
|
143
|
+
if (error?.code === "invalid-release") throw error;
|
|
144
|
+
if (response !== null && response.ok) throw updateError("invalid-release");
|
|
145
|
+
throw updateError("check-failed");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function createUpdateCore(ports) {
|
|
149
|
+
const checkTimeoutMs = ports.checkTimeoutMs ?? CHECK_TIMEOUT_MS;
|
|
150
|
+
const confirmationTtlMs = ports.confirmationTtlMs ?? CONFIRMATION_TTL_MS;
|
|
151
|
+
const targetPackageName = ports.targetPackageName ?? PACKAGE_NAME;
|
|
152
|
+
const registryUrl = ports.registryUrl ?? NPM_REGISTRY;
|
|
153
|
+
function fetchRelease() {
|
|
154
|
+
return fetchNpmRelease(ports.fetchImpl, checkTimeoutMs, { targetPackageName, registryUrl });
|
|
155
|
+
}
|
|
156
|
+
let checked = null;
|
|
157
|
+
let checking = null;
|
|
158
|
+
let lastCheckAt = -Infinity;
|
|
159
|
+
let activeJobId = null;
|
|
160
|
+
let memJob = null;
|
|
161
|
+
let memLock = null;
|
|
162
|
+
async function loadJob() {
|
|
163
|
+
try {
|
|
164
|
+
return ports.readJob ? await ports.readJob() ?? null : memJob;
|
|
165
|
+
} catch {
|
|
166
|
+
throw updateError("install-failed");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function saveJob(job) {
|
|
170
|
+
try {
|
|
171
|
+
if (ports.writeJob) await ports.writeJob(job);
|
|
172
|
+
else memJob = job;
|
|
173
|
+
} catch {
|
|
174
|
+
throw updateError("install-failed");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function acquire(lockId) {
|
|
178
|
+
try {
|
|
179
|
+
if (ports.tryAcquireLock) return await ports.tryAcquireLock(lockId);
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (memLock !== null) return false;
|
|
184
|
+
memLock = lockId;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async function release(lockId) {
|
|
188
|
+
try {
|
|
189
|
+
if (ports.releaseLock) await ports.releaseLock(lockId);
|
|
190
|
+
else if (memLock === lockId) memLock = null;
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function healJob(job, env) {
|
|
195
|
+
if (!job) return null;
|
|
196
|
+
if (job.state === "installing" || job.state === "verifying") {
|
|
197
|
+
return job.id === activeJobId ? job : { ...job, state: "interrupted", message: "recovery-required", requestId: job.requestId ?? null };
|
|
198
|
+
}
|
|
199
|
+
if (job.state === "restart-required" || job.state === "completed") {
|
|
200
|
+
if (!job.targetVersion || env.installedVersion !== job.targetVersion) {
|
|
201
|
+
return { ...job, state: "interrupted", message: "installation-changed", requestId: job.requestId ?? null };
|
|
202
|
+
}
|
|
203
|
+
const runningVersion = ports.readRunningVersion();
|
|
204
|
+
return { ...job, state: runningVersion === job.targetVersion ? "completed" : "restart-required", requestId: job.requestId ?? null };
|
|
205
|
+
}
|
|
206
|
+
return job;
|
|
207
|
+
}
|
|
208
|
+
function buildSnapshot(env, job) {
|
|
209
|
+
let blockedReason = env.blockedReason ?? checked?.blockedReason ?? null;
|
|
210
|
+
if (job?.state === "interrupted" || job?.state === "failed") {
|
|
211
|
+
if (env.installedVersion !== ports.readRunningVersion()) blockedReason = "recovery-required";
|
|
212
|
+
} else if (job?.state === "restart-required" || env.installedVersion && env.installedVersion !== ports.readRunningVersion()) {
|
|
213
|
+
blockedReason = "pending-restart";
|
|
214
|
+
}
|
|
215
|
+
const busy = job?.state === "installing" || job?.state === "verifying";
|
|
216
|
+
const fresh = checked !== null && checked.checkId !== null && ports.now() < checked.expiresAt;
|
|
217
|
+
const runningVersion = ports.readRunningVersion();
|
|
218
|
+
const canInstall = Boolean(
|
|
219
|
+
env.eligible && !blockedReason && !busy && fresh && checked?.installationKey === env.installationKey && validVersion(runningVersion) && checked?.release && compareVersions(checked.release.version, runningVersion) === 1
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
runningVersion,
|
|
223
|
+
installedVersion: env.installedVersion ?? null,
|
|
224
|
+
latestVersion: checked?.release.version ?? null,
|
|
225
|
+
canInstall,
|
|
226
|
+
blockedReason,
|
|
227
|
+
job
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function status() {
|
|
231
|
+
const env = await ports.readInstalled();
|
|
232
|
+
const job = healJob(await loadJob(), env);
|
|
233
|
+
return buildSnapshot(env, job);
|
|
234
|
+
}
|
|
235
|
+
async function check() {
|
|
236
|
+
if (checking) return checking;
|
|
237
|
+
if (checked?.checkId && ports.now() - lastCheckAt < RECHECK_WINDOW_MS) {
|
|
238
|
+
const env = await ports.readInstalled();
|
|
239
|
+
const snapshot = buildSnapshot(env, healJob(await loadJob(), env));
|
|
240
|
+
return { snapshot, receipt: snapshot.canInstall ? toReceipt() : null };
|
|
241
|
+
}
|
|
242
|
+
lastCheckAt = ports.now();
|
|
243
|
+
checking = (async () => {
|
|
244
|
+
try {
|
|
245
|
+
const env = await ports.readInstalled();
|
|
246
|
+
const release2 = await fetchRelease();
|
|
247
|
+
checked = {
|
|
248
|
+
release: release2,
|
|
249
|
+
checkId: ports.randomId(),
|
|
250
|
+
checkedAt: ports.now(),
|
|
251
|
+
expiresAt: ports.now() + confirmationTtlMs,
|
|
252
|
+
installationKey: env.installationKey,
|
|
253
|
+
blockedReason: satisfiesNodeRange(ports.nodeVersion, release2.nodeRange) ? null : "incompatible-node"
|
|
254
|
+
};
|
|
255
|
+
const snapshot = buildSnapshot(env, healJob(await loadJob(), env));
|
|
256
|
+
return { snapshot, receipt: snapshot.canInstall ? toReceipt() : null };
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (checked) checked = { ...checked, checkId: null, expiresAt: 0 };
|
|
259
|
+
throw error;
|
|
260
|
+
} finally {
|
|
261
|
+
checking = null;
|
|
262
|
+
}
|
|
263
|
+
})();
|
|
264
|
+
return checking;
|
|
265
|
+
}
|
|
266
|
+
function toReceipt() {
|
|
267
|
+
if (!checked?.checkId) return null;
|
|
268
|
+
return { checkId: checked.checkId, checkedAt: checked.checkedAt, expiresAt: checked.expiresAt };
|
|
269
|
+
}
|
|
270
|
+
async function runBackground(job, envAtStart) {
|
|
271
|
+
try {
|
|
272
|
+
if (ports.runInstall) await ports.runInstall({ version: job.targetVersion, profileName: envAtStart.profileName, environmentKind: envAtStart.environmentKind });
|
|
273
|
+
else throw updateError("unsupported");
|
|
274
|
+
const doing = { ...job, state: "verifying", message: null };
|
|
275
|
+
activeJobId = doing.id;
|
|
276
|
+
await saveJob(doing);
|
|
277
|
+
const env = await ports.readInstalled();
|
|
278
|
+
if (env.installedVersion !== job.targetVersion) throw updateError("install-failed");
|
|
279
|
+
if (env.blockedReason && env.blockedReason !== "pending-restart") throw updateError("install-failed");
|
|
280
|
+
const done = { ...doing, state: "restart-required", message: null };
|
|
281
|
+
await saveJob(done);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const code = error?.code;
|
|
284
|
+
const message = code === "installation-changed" || code === "registry-conflict" || code === "install-failed" ? String(code) : "install-failed";
|
|
285
|
+
const failedJob = { ...job, state: "failed", message, requestId: job.requestId ?? null };
|
|
286
|
+
try {
|
|
287
|
+
await saveJob(failedJob);
|
|
288
|
+
} catch {
|
|
289
|
+
memJob = failedJob;
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
await release(job.id);
|
|
293
|
+
if (activeJobId === job.id) activeJobId = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function install(args) {
|
|
297
|
+
const checkId = typeof args?.checkId === "string" ? args.checkId : "";
|
|
298
|
+
const requestId = typeof args?.requestId === "string" ? args.requestId : "";
|
|
299
|
+
if (!checkId || !validRequestId(requestId)) throw updateError("check-expired");
|
|
300
|
+
let env = await ports.readInstalled();
|
|
301
|
+
let previous = healJob(await loadJob(), env);
|
|
302
|
+
if (previous?.requestId === requestId) return buildSnapshot(env, previous);
|
|
303
|
+
if (previous?.state === "installing" || previous?.state === "verifying" || previous?.state === "restart-required") {
|
|
304
|
+
throw updateError("update-busy");
|
|
305
|
+
}
|
|
306
|
+
if (!checked || !checked.checkId || checked.checkId !== checkId || ports.now() >= checked.expiresAt) {
|
|
307
|
+
throw updateError("check-expired");
|
|
308
|
+
}
|
|
309
|
+
if (env.installationKey !== checked.installationKey) throw updateError("installation-changed");
|
|
310
|
+
if (!buildSnapshot(env, previous).canInstall) {
|
|
311
|
+
throw updateError(env.blockedReason ?? checked.blockedReason ?? "update-busy");
|
|
312
|
+
}
|
|
313
|
+
const job = {
|
|
314
|
+
id: ports.randomId(),
|
|
315
|
+
state: "installing",
|
|
316
|
+
targetVersion: checked.release.version,
|
|
317
|
+
message: null,
|
|
318
|
+
requestId
|
|
319
|
+
};
|
|
320
|
+
if (!await acquire(job.id)) throw updateError("update-busy");
|
|
321
|
+
try {
|
|
322
|
+
const current = await fetchRelease();
|
|
323
|
+
if (JSON.stringify(current) !== JSON.stringify(checked.release)) throw updateError("check-expired");
|
|
324
|
+
env = await ports.readInstalled();
|
|
325
|
+
if (env.installationKey !== checked.installationKey) throw updateError("installation-changed");
|
|
326
|
+
if (!env.eligible || env.blockedReason) throw updateError(env.blockedReason ?? "update-busy");
|
|
327
|
+
if (ports.backupJob) await ports.backupJob(job);
|
|
328
|
+
await saveJob(job);
|
|
329
|
+
activeJobId = job.id;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
await release(job.id);
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
void runBackground(job, env).catch(() => {
|
|
335
|
+
});
|
|
336
|
+
return buildSnapshot(env, job);
|
|
337
|
+
}
|
|
338
|
+
return { status, check, install };
|
|
339
|
+
}
|
|
340
|
+
export {
|
|
341
|
+
CHECK_TIMEOUT_MS,
|
|
342
|
+
CONFIRMATION_TTL_MS,
|
|
343
|
+
INTEGRITY_PATTERN,
|
|
344
|
+
MAX_METADATA_BYTES,
|
|
345
|
+
NPM_REGISTRY,
|
|
346
|
+
PACKAGE_NAME,
|
|
347
|
+
RECHECK_WINDOW_MS,
|
|
348
|
+
compareVersions,
|
|
349
|
+
createUpdateCore,
|
|
350
|
+
fetchNpmRelease,
|
|
351
|
+
satisfiesNodeRange,
|
|
352
|
+
updateError,
|
|
353
|
+
validRequestId,
|
|
354
|
+
validVersion
|
|
355
|
+
};
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// AUTO-GENERATED by node packages/dsh-plugin-update/build.mjs — DO NOT EDIT. Source: packages/dsh-plugin-update/src/store.ts
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, open, readFile, rename, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { installRecipe } from "./commands.js";
|
|
6
|
+
import { BACKUP_FILE, LEGACY_PLUGIN_ID, LOCK_FILE, STATE_FILE } from "./config.js";
|
|
7
|
+
const LEGACY_SEGMENT = "updates";
|
|
8
|
+
const LEGACY_DIR_PLUGIN = LEGACY_PLUGIN_ID;
|
|
9
|
+
const JOB_STATES = ["installing", "verifying", "restart-required", "completed", "failed", "interrupted"];
|
|
10
|
+
const SNAPSHOT_FILES = ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"];
|
|
11
|
+
function fail(code) {
|
|
12
|
+
return Object.assign(new Error(code), { code });
|
|
13
|
+
}
|
|
14
|
+
function shortHash(text) {
|
|
15
|
+
try {
|
|
16
|
+
return createHash("sha256").update(String(text)).digest("hex").slice(0, 24);
|
|
17
|
+
} catch {
|
|
18
|
+
return "00000000";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function pathsForUpdate(homeDir, pluginId, profileDir) {
|
|
22
|
+
if (typeof homeDir !== "string" || !homeDir || typeof profileDir !== "string" || !profileDir) return null;
|
|
23
|
+
if (typeof pluginId !== "string" || !pluginId) return null;
|
|
24
|
+
const directory = join(homeDir, LEGACY_SEGMENT, pluginId, shortHash(profileDir));
|
|
25
|
+
return { directory, state: join(directory, STATE_FILE), lock: join(directory, LOCK_FILE), backup: join(directory, BACKUP_FILE) };
|
|
26
|
+
}
|
|
27
|
+
function legacyPathsForUpdate(homeDir, profileDir) {
|
|
28
|
+
if (typeof homeDir !== "string" || !homeDir || typeof profileDir !== "string" || !profileDir) return null;
|
|
29
|
+
const directory = join(homeDir, LEGACY_SEGMENT, LEGACY_DIR_PLUGIN, shortHash(profileDir));
|
|
30
|
+
return { directory, state: join(directory, STATE_FILE), lock: join(directory, LOCK_FILE), backup: join(directory, BACKUP_FILE) };
|
|
31
|
+
}
|
|
32
|
+
async function readJsonGuarded(filename, missing) {
|
|
33
|
+
try {
|
|
34
|
+
if ((await stat(filename)).size > 10 * 1024 * 1024) throw fail("install-failed");
|
|
35
|
+
return JSON.parse(await readFile(filename, "utf8"));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error && error.code === "ENOENT") return missing;
|
|
38
|
+
throw error && error.code ? error : fail("install-failed");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function writeJsonAtomic(filename, value) {
|
|
42
|
+
const temporary = `${filename}.${randomUUID()}.tmp`;
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}
|
|
45
|
+
`, { mode: 384, flag: "wx" });
|
|
46
|
+
await rename(temporary, filename);
|
|
47
|
+
} catch {
|
|
48
|
+
throw fail("install-failed");
|
|
49
|
+
} finally {
|
|
50
|
+
await unlink(temporary).catch(() => {
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function normalizeJob(job) {
|
|
55
|
+
return {
|
|
56
|
+
id: typeof job.id === "string" ? job.id : "corrupt",
|
|
57
|
+
state: JOB_STATES.includes(String(job.state)) ? job.state : "interrupted",
|
|
58
|
+
targetVersion: typeof job.targetVersion === "string" ? job.targetVersion : null,
|
|
59
|
+
message: typeof job.message === "string" ? job.message : null,
|
|
60
|
+
requestId: typeof job.requestId === "string" ? job.requestId : null
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function createUpdateDiskPorts(homeDir, pluginId, profileDir, deps = {}) {
|
|
64
|
+
const paths = pathsForUpdate(homeDir, pluginId, profileDir);
|
|
65
|
+
const legacyPaths = pluginId === LEGACY_DIR_PLUGIN ? null : legacyPathsForUpdate(homeDir, profileDir);
|
|
66
|
+
const readText = deps.readFileImpl ?? ((filename, encoding) => readFile(filename, encoding));
|
|
67
|
+
const statFile = deps.statImpl ?? ((filename) => stat(filename));
|
|
68
|
+
async function readAt(p) {
|
|
69
|
+
if (!p) return null;
|
|
70
|
+
try {
|
|
71
|
+
if ((await statFile(p.state)).size > 10 * 1024 * 1024) throw fail("install-failed");
|
|
72
|
+
return JSON.parse(await readText(p.state, "utf8"));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error && error.code === "ENOENT") return null;
|
|
75
|
+
throw error && error.code ? error : fail("install-failed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function readJob() {
|
|
79
|
+
if (!paths) return null;
|
|
80
|
+
const job = await readAt(paths);
|
|
81
|
+
if (job) {
|
|
82
|
+
if (!JOB_STATES.includes(String(job.state)) || typeof job.id !== "string") {
|
|
83
|
+
return { id: "corrupt", state: "interrupted", message: "recovery-required", targetVersion: null, requestId: null };
|
|
84
|
+
}
|
|
85
|
+
return { requestId: null, message: null, ...job };
|
|
86
|
+
}
|
|
87
|
+
if (legacyPaths) {
|
|
88
|
+
const legacyJob = await readAt(legacyPaths);
|
|
89
|
+
if (legacyJob) {
|
|
90
|
+
if (!JOB_STATES.includes(String(legacyJob.state)) || typeof legacyJob.id !== "string") {
|
|
91
|
+
return { id: "corrupt", state: "interrupted", message: "recovery-required", targetVersion: null, requestId: null };
|
|
92
|
+
}
|
|
93
|
+
return { requestId: null, message: null, ...legacyJob };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const lock = await readJsonGuarded(paths.lock, null);
|
|
97
|
+
if (lock) return { id: "locked", state: "interrupted", message: "recovery-required", targetVersion: null, requestId: null };
|
|
98
|
+
if (legacyPaths) {
|
|
99
|
+
const legacyLock = await readJsonGuarded(legacyPaths.lock, null).catch(() => null);
|
|
100
|
+
if (legacyLock) return { id: "locked", state: "interrupted", message: "recovery-required", targetVersion: null, requestId: null };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
async function writeJob(job) {
|
|
105
|
+
if (!paths) throw fail("install-failed");
|
|
106
|
+
if (job === null) {
|
|
107
|
+
await unlink(paths.state).catch((error) => {
|
|
108
|
+
if (!error || error.code !== "ENOENT") throw fail("install-failed");
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
113
|
+
await writeJsonAtomic(paths.state, job);
|
|
114
|
+
}
|
|
115
|
+
async function tryAcquireLock(lockId) {
|
|
116
|
+
if (!paths) return false;
|
|
117
|
+
try {
|
|
118
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
119
|
+
const handle = await open(paths.lock, "wx", 384);
|
|
120
|
+
try {
|
|
121
|
+
await handle.writeFile(JSON.stringify({ id: lockId, pid: process.pid, startedAt: Date.now() }));
|
|
122
|
+
} finally {
|
|
123
|
+
await handle.close();
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!error || error.code !== "EEXIST") return false;
|
|
128
|
+
try {
|
|
129
|
+
const current = await readJsonGuarded(paths.lock, null);
|
|
130
|
+
const job = await readJsonGuarded(paths.state, null);
|
|
131
|
+
if (!current || job && job.id === current.id) return false;
|
|
132
|
+
await unlink(paths.lock).catch(() => {
|
|
133
|
+
});
|
|
134
|
+
const handle = await open(paths.lock, "wx", 384);
|
|
135
|
+
try {
|
|
136
|
+
await handle.writeFile(JSON.stringify({ id: lockId, pid: process.pid, startedAt: Date.now() }));
|
|
137
|
+
} finally {
|
|
138
|
+
await handle.close();
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function releaseLock(lockId) {
|
|
147
|
+
if (!paths) return;
|
|
148
|
+
try {
|
|
149
|
+
const current = await readJsonGuarded(paths.lock, null);
|
|
150
|
+
if (current && current.id === lockId) await unlink(paths.lock).catch(() => {
|
|
151
|
+
});
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function backupJob(job) {
|
|
156
|
+
if (!paths) throw fail("install-failed");
|
|
157
|
+
const files = {};
|
|
158
|
+
for (const name of SNAPSHOT_FILES) {
|
|
159
|
+
try {
|
|
160
|
+
const filename = join(profileDir, name);
|
|
161
|
+
if ((await stat(filename)).size > 3 * 1024 * 1024) throw fail("install-failed");
|
|
162
|
+
files[name] = await readFile(filename, "utf8");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!error || error.code !== "ENOENT") throw fail("install-failed");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await mkdir(paths.directory, { recursive: true, mode: 448 });
|
|
168
|
+
await writeJsonAtomic(paths.backup, {
|
|
169
|
+
jobId: job.id,
|
|
170
|
+
previousVersion: job.previousVersion ?? null,
|
|
171
|
+
files
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return { paths, legacyPaths, readJob, writeJob, tryAcquireLock, releaseLock, backupJob };
|
|
175
|
+
}
|
|
176
|
+
const CLI_PACKAGE_NAME = "@deepseek-ai/dsh";
|
|
177
|
+
const INSTALL_STDIO = { stdin: "ignore", stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } };
|
|
178
|
+
const TERMINATION_GRACE_MS = 3e3;
|
|
179
|
+
function part(value) {
|
|
180
|
+
return typeof value === "function" ? value() : value;
|
|
181
|
+
}
|
|
182
|
+
function withExit(error, exitCode) {
|
|
183
|
+
if (typeof exitCode === "number") error.exitCode = exitCode;
|
|
184
|
+
return error;
|
|
185
|
+
}
|
|
186
|
+
async function sameDir(a, b) {
|
|
187
|
+
if (typeof a !== "string" || typeof b !== "string" || !a || !b) return false;
|
|
188
|
+
try {
|
|
189
|
+
if (resolve(a) === resolve(b)) return true;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const pair = await Promise.all([realpath(a), realpath(b)]);
|
|
195
|
+
return pair[0] === pair[1];
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function readManifestAt(directory) {
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function resolveCliEntry(argv1, deps = {}) {
|
|
208
|
+
const readAt = typeof deps.readManifest === "function" ? deps.readManifest : readManifestAt;
|
|
209
|
+
const realpathImpl = typeof deps.realpath === "function" ? deps.realpath : realpath;
|
|
210
|
+
if (typeof argv1 !== "string" || !argv1 || argv1.includes("\0")) return null;
|
|
211
|
+
let entry = "";
|
|
212
|
+
try {
|
|
213
|
+
entry = await realpathImpl(argv1);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (typeof entry !== "string" || !entry) return null;
|
|
218
|
+
let directory = dirname(entry);
|
|
219
|
+
while (true) {
|
|
220
|
+
let manifest = null;
|
|
221
|
+
try {
|
|
222
|
+
manifest = await Promise.resolve().then(() => readAt(directory)).catch(() => null);
|
|
223
|
+
} catch {
|
|
224
|
+
manifest = null;
|
|
225
|
+
}
|
|
226
|
+
if (manifest && manifest.name === CLI_PACKAGE_NAME) {
|
|
227
|
+
const bin = manifest.bin;
|
|
228
|
+
const declared = typeof bin === "string" ? bin : bin && bin.dsh;
|
|
229
|
+
if (typeof declared !== "string" || !declared || isAbsolute(declared) || declared.includes("\0")) return null;
|
|
230
|
+
try {
|
|
231
|
+
return await realpathImpl(resolve(directory, declared)) === entry ? entry : null;
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const parent = dirname(directory);
|
|
237
|
+
if (parent === directory) return null;
|
|
238
|
+
directory = parent;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function awaitOutcome(handle, timeoutMs) {
|
|
242
|
+
const settled = Promise.resolve(handle.done).then(
|
|
243
|
+
(value) => ({ exitCode: value && typeof value.exitCode === "number" ? value.exitCode : null, timedOut: false }),
|
|
244
|
+
() => ({ exitCode: null, timedOut: true })
|
|
245
|
+
);
|
|
246
|
+
if (!(typeof timeoutMs === "number" && timeoutMs > 0)) return await settled;
|
|
247
|
+
let timer = null;
|
|
248
|
+
const deadline = new Promise((settle) => {
|
|
249
|
+
timer = setTimeout(() => {
|
|
250
|
+
try {
|
|
251
|
+
if (handle && typeof handle.terminate === "function") handle.terminate();
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
settle({ exitCode: null, timedOut: true });
|
|
255
|
+
}, timeoutMs);
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
return await Promise.race([settled, deadline]);
|
|
259
|
+
} finally {
|
|
260
|
+
if (timer !== null) {
|
|
261
|
+
try {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function emitInstall(parts, recipe, ok, exitCode, durationMs) {
|
|
269
|
+
const log = parts.log;
|
|
270
|
+
if (typeof log !== "function") return;
|
|
271
|
+
try {
|
|
272
|
+
const pluginId = part(parts.pluginId ?? null);
|
|
273
|
+
log("info", "update.install.exec", {
|
|
274
|
+
route: recipe ? recipe.route : "none",
|
|
275
|
+
ok: ok === true,
|
|
276
|
+
exitCode: typeof exitCode === "number" ? exitCode : null,
|
|
277
|
+
durationMs,
|
|
278
|
+
pluginId: typeof pluginId === "string" ? pluginId : null
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function runDesktopService(recipe, parts) {
|
|
284
|
+
const service = part(parts.desktopPnpm);
|
|
285
|
+
if (!service || typeof service.runPlugin !== "function") throw fail("install-failed");
|
|
286
|
+
const profiles = part(parts.desktopProfiles);
|
|
287
|
+
const active = profiles && profiles.current ? profiles.current : null;
|
|
288
|
+
const profileDir = part(parts.profileDir);
|
|
289
|
+
if (!active || !active.dir || !profileDir || !await sameDir(active.dir, profileDir)) throw fail("install-failed");
|
|
290
|
+
const handle = service.runPlugin(recipe.pluginArgs, profileDir, void 0);
|
|
291
|
+
if (!handle || !handle.done || typeof handle.done.then !== "function") throw fail("install-failed");
|
|
292
|
+
const outcome = await handle.done;
|
|
293
|
+
const code = outcome && typeof outcome.exitCode === "number" ? outcome.exitCode : null;
|
|
294
|
+
if (code !== 0) throw withExit(fail("install-failed"), code);
|
|
295
|
+
return code;
|
|
296
|
+
}
|
|
297
|
+
async function runCliProcess(recipe, parts) {
|
|
298
|
+
const executable = part(parts.runtimeExecutable) || (typeof process !== "undefined" ? process.execPath : "");
|
|
299
|
+
const execArgs = part(parts.runtimeExecArgs) ?? (typeof process !== "undefined" && Array.isArray(process.execArgv) ? process.execArgv : []);
|
|
300
|
+
const entry = part(parts.cliEntry) ?? await resolveCliEntry(typeof process !== "undefined" && process.argv ? process.argv[1] : "");
|
|
301
|
+
if (!executable || !entry) throw fail("install-failed");
|
|
302
|
+
const argv = [executable, ...execArgs, entry, "plugin", "--profile", recipe.profileName, ...recipe.pluginArgs];
|
|
303
|
+
const subprocess = part(parts.subprocess);
|
|
304
|
+
if (!subprocess || typeof subprocess.spawn !== "function") throw fail("install-failed");
|
|
305
|
+
const handle = subprocess.spawn({
|
|
306
|
+
argv,
|
|
307
|
+
cwd: part(parts.profileDir) || void 0,
|
|
308
|
+
stdio: INSTALL_STDIO,
|
|
309
|
+
graceMs: TERMINATION_GRACE_MS
|
|
310
|
+
});
|
|
311
|
+
if (!handle || !handle.done || typeof handle.done.then !== "function") throw fail("install-failed");
|
|
312
|
+
const outcome = await awaitOutcome(handle, recipe.timeoutMs);
|
|
313
|
+
if (outcome.timedOut || outcome.exitCode !== 0) throw withExit(fail("install-failed"), outcome.exitCode);
|
|
314
|
+
return outcome.exitCode;
|
|
315
|
+
}
|
|
316
|
+
function createUpdateExecutor(parts = {}) {
|
|
317
|
+
return async function runInstall(args = {}) {
|
|
318
|
+
const recipe = installRecipe({
|
|
319
|
+
profileName: args.profileName ?? part(parts.profileName ?? null),
|
|
320
|
+
version: String(args.version ?? ""),
|
|
321
|
+
environmentKind: args.environmentKind ?? part(parts.environmentKind ?? "cli"),
|
|
322
|
+
targetPackageName: parts.targetPackageName === void 0 ? void 0 : part(parts.targetPackageName),
|
|
323
|
+
registryUrl: parts.registryUrl === void 0 ? void 0 : part(parts.registryUrl),
|
|
324
|
+
timeoutMs: parts.installTimeoutMs === void 0 ? void 0 : part(parts.installTimeoutMs)
|
|
325
|
+
});
|
|
326
|
+
const startedAt = Date.now();
|
|
327
|
+
let exitCode = null;
|
|
328
|
+
try {
|
|
329
|
+
if (!recipe) throw fail("install-failed");
|
|
330
|
+
exitCode = recipe.route === "desktop-service" ? await runDesktopService(recipe, parts) : await runCliProcess(recipe, parts);
|
|
331
|
+
emitInstall(parts, recipe, true, exitCode, Date.now() - startedAt);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
emitInstall(parts, recipe, false, error?.exitCode ?? exitCode, Date.now() - startedAt);
|
|
334
|
+
throw Object.assign(fail("install-failed"), { debug: String(error?.stack || error) });
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const __executorInternals = { INSTALL_STDIO, TERMINATION_GRACE_MS, CLI_PACKAGE_NAME };
|
|
339
|
+
export {
|
|
340
|
+
__executorInternals,
|
|
341
|
+
createUpdateDiskPorts,
|
|
342
|
+
createUpdateExecutor,
|
|
343
|
+
legacyPathsForUpdate,
|
|
344
|
+
pathsForUpdate,
|
|
345
|
+
resolveCliEntry
|
|
346
|
+
};
|