@shellraining/dsh-plugin-updates 0.2.0 → 0.2.1
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/README.md +2 -0
- package/lib/client.js +1837 -1996
- package/lib/index.js +207 -130
- package/package.json +3 -4
package/lib/index.js
CHANGED
|
@@ -1,161 +1,238 @@
|
|
|
1
1
|
// Generated by `pnpm run build`; edit src/, not this file.
|
|
2
|
-
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
// src/shared/protocol.ts
|
|
4
6
|
var SELF = "@shellraining/dsh-plugin-updates";
|
|
7
|
+
/**
|
|
8
|
+
* Authenticated Connection route: GET reads the last self-update, POST starts
|
|
9
|
+
* one. Browser-relative, like the host's own `/api/*` routes.
|
|
10
|
+
*/
|
|
5
11
|
var SELF_UPDATE_PATH = "/api/dsh-plugin-updates.self-update";
|
|
6
12
|
function isRunning(status) {
|
|
7
|
-
|
|
13
|
+
return status?.phase === "removing" || status?.phase === "installing" || status?.phase === "restoring";
|
|
8
14
|
}
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
/** pnpm may print successful checks before the actual error; pick the first error line. */
|
|
16
|
+
function errorLine(text) {
|
|
17
|
+
return text.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => /\bERR[_!\s]|\berror\b|\bfatal\b/i.test(line));
|
|
11
18
|
}
|
|
12
|
-
|
|
13
19
|
// src/host/self-update.ts
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
var STALE_MS = 15 * 6e4;
|
|
20
|
+
/** A run whose file still says running after this long was cut short by a host exit. */
|
|
21
|
+
var STALE_MS = 9e5;
|
|
18
22
|
var VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
19
23
|
function record(value) {
|
|
20
|
-
|
|
24
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
21
25
|
}
|
|
22
26
|
function text(error) {
|
|
23
|
-
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
28
|
}
|
|
25
29
|
function failed(result) {
|
|
26
|
-
|
|
30
|
+
return result.application === "failed" || result.application === "cancelled";
|
|
27
31
|
}
|
|
32
|
+
/** The first error line, then the refusal code, then the outcome. */
|
|
28
33
|
function reasonOf(result) {
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
const output = result.error?.diagnostic ?? result.packageResult?.output ?? "";
|
|
35
|
+
return errorLine(output) ?? result.error?.code ?? (output.trim() || result.application);
|
|
31
36
|
}
|
|
37
|
+
/** The full output and log of a failed step, kept for the page to show. */
|
|
32
38
|
function details(result) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const output = (result.packageResult?.output.trim() || result.error?.diagnostic)?.slice(-8e3);
|
|
40
|
+
const logPath = result.packageResult?.logPath;
|
|
41
|
+
return {
|
|
42
|
+
...output ? { output } : {},
|
|
43
|
+
...logPath ? { logPath } : {}
|
|
44
|
+
};
|
|
36
45
|
}
|
|
46
|
+
/** A registry dependency recorded as `name@exact-or-range`; other sources are not ours to replace. */
|
|
37
47
|
function npmVersion(bundle) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
if (bundle === void 0 || !bundle.installed || !bundle.removable) return void 0;
|
|
49
|
+
if (bundle.source?.startsWith(`@shellraining/dsh-plugin-updates@`) !== true || bundle.version === void 0) return void 0;
|
|
50
|
+
return VERSION.test(bundle.version) ? bundle.version : void 0;
|
|
41
51
|
}
|
|
42
52
|
var SelfUpdater = class {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
53
|
+
manager;
|
|
54
|
+
logger;
|
|
55
|
+
file;
|
|
56
|
+
running = false;
|
|
57
|
+
constructor(profileDir, manager, logger) {
|
|
58
|
+
this.manager = manager;
|
|
59
|
+
this.logger = logger;
|
|
60
|
+
this.file = join(profileDir, ".dsh-plugin-updates", "self-update.json");
|
|
61
|
+
}
|
|
62
|
+
async status() {
|
|
63
|
+
let stored;
|
|
64
|
+
try {
|
|
65
|
+
stored = record(JSON.parse(await readFile(this.file, "utf8")));
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const { id, from, to, phase, startedAt } = stored;
|
|
70
|
+
if (typeof id !== "string" || typeof from !== "string" || typeof to !== "string" || typeof phase !== "string") return null;
|
|
71
|
+
const status = stored;
|
|
72
|
+
if (isRunning(status) && !this.running && typeof startedAt === "number" && Date.now() - startedAt > STALE_MS) return {
|
|
73
|
+
...status,
|
|
74
|
+
phase: "failed",
|
|
75
|
+
reason: "interrupted"
|
|
76
|
+
};
|
|
77
|
+
return status;
|
|
78
|
+
}
|
|
79
|
+
async start(version) {
|
|
80
|
+
if (typeof version !== "string" || !VERSION.test(version)) return {
|
|
81
|
+
ok: false,
|
|
82
|
+
code: "invalid-version"
|
|
83
|
+
};
|
|
84
|
+
if (this.running || isRunning(await this.status())) return {
|
|
85
|
+
ok: false,
|
|
86
|
+
code: "busy"
|
|
87
|
+
};
|
|
88
|
+
const from = npmVersion((await this.manager.listBundles()).find((bundle) => bundle.name === SELF));
|
|
89
|
+
if (from === void 0) return {
|
|
90
|
+
ok: false,
|
|
91
|
+
code: "not-npm"
|
|
92
|
+
};
|
|
93
|
+
if (from === version) return {
|
|
94
|
+
ok: false,
|
|
95
|
+
code: "current"
|
|
96
|
+
};
|
|
97
|
+
const id = randomUUID();
|
|
98
|
+
const base = {
|
|
99
|
+
id,
|
|
100
|
+
from,
|
|
101
|
+
to: version,
|
|
102
|
+
startedAt: Date.now()
|
|
103
|
+
};
|
|
104
|
+
this.running = true;
|
|
105
|
+
await this.save({
|
|
106
|
+
...base,
|
|
107
|
+
phase: "removing"
|
|
108
|
+
});
|
|
109
|
+
this.run(base).finally(() => {
|
|
110
|
+
this.running = false;
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
status: {
|
|
115
|
+
id,
|
|
116
|
+
from,
|
|
117
|
+
to: version,
|
|
118
|
+
phase: "removing"
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async run(base) {
|
|
123
|
+
const step = (phase, extra = {}) => this.save({
|
|
124
|
+
...base,
|
|
125
|
+
...extra,
|
|
126
|
+
phase,
|
|
127
|
+
...isRunning({
|
|
128
|
+
...base,
|
|
129
|
+
phase
|
|
130
|
+
}) ? {} : { finishedAt: Date.now() }
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
const removed = await this.manager.removeBundle(SELF);
|
|
134
|
+
if (failed(removed)) {
|
|
135
|
+
await this.reselect();
|
|
136
|
+
await step("failed", {
|
|
137
|
+
reason: reasonOf(removed),
|
|
138
|
+
...details(removed)
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await step("installing");
|
|
143
|
+
const installed = await this.manager.installBundle(`${SELF}@${base.to}`, {
|
|
144
|
+
enabled: true,
|
|
145
|
+
saveExact: true
|
|
146
|
+
});
|
|
147
|
+
if (!failed(installed)) {
|
|
148
|
+
await step(installed.application === "restart-required" ? "restart" : "done", { to: installed.version ?? base.to });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const failure = {
|
|
152
|
+
reason: reasonOf(installed),
|
|
153
|
+
...details(installed)
|
|
154
|
+
};
|
|
155
|
+
await step("restoring", failure);
|
|
156
|
+
const restored = await this.manager.installBundle(`${SELF}@${base.from}`, {
|
|
157
|
+
enabled: true,
|
|
158
|
+
saveExact: true
|
|
159
|
+
});
|
|
160
|
+
if (failed(restored)) {
|
|
161
|
+
this.logger.warn("[plugin-updates] could not restore %s@%s: %s", SELF, base.from, reasonOf(restored));
|
|
162
|
+
await step("failed", {
|
|
163
|
+
...failure,
|
|
164
|
+
reason: `${failure.reason}\n${reasonOf(restored)}`
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
await step("restored", failure);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.logger.warn("[plugin-updates] self-update stopped: %s", text(error));
|
|
171
|
+
await step("failed", { reason: text(error) }).catch(() => void 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async reselect() {
|
|
175
|
+
const bundle = (await this.manager.listBundles()).find((item) => item.name === SELF);
|
|
176
|
+
if (bundle?.installed === true && !bundle.enabled) await this.manager.setBundleEnabled(SELF, true);
|
|
177
|
+
}
|
|
178
|
+
async save(status) {
|
|
179
|
+
const dir = join(this.file, "..");
|
|
180
|
+
await mkdir(dir, {
|
|
181
|
+
recursive: true,
|
|
182
|
+
mode: 448
|
|
183
|
+
});
|
|
184
|
+
const temp = `${this.file}.${process.pid}.tmp`;
|
|
185
|
+
await writeFile(temp, `${JSON.stringify(status, null, 2)}\n`, { mode: 384 });
|
|
186
|
+
await rename(temp, this.file);
|
|
187
|
+
}
|
|
125
188
|
};
|
|
126
|
-
|
|
127
189
|
// src/host/index.ts
|
|
128
190
|
var name = SELF;
|
|
129
|
-
var inject = [
|
|
130
|
-
|
|
191
|
+
var inject = [
|
|
192
|
+
"connection",
|
|
193
|
+
"pluginManager",
|
|
194
|
+
"profileContext"
|
|
195
|
+
];
|
|
196
|
+
var ORIGINAL = Symbol.for("cordis.original");
|
|
197
|
+
/**
|
|
198
|
+
* The plugin manager itself, not the proxy bound to this plugin's context:
|
|
199
|
+
* the job outlives this context, and calls through a disposed context's proxy
|
|
200
|
+
* could resolve services against it.
|
|
201
|
+
*/
|
|
131
202
|
function unwrap(service) {
|
|
132
|
-
|
|
133
|
-
|
|
203
|
+
const original = Reflect.get(service, ORIGINAL);
|
|
204
|
+
return typeof original === "object" && original !== null ? original : service;
|
|
134
205
|
}
|
|
135
206
|
function json(body) {
|
|
136
|
-
|
|
207
|
+
return new Response(JSON.stringify(body), { headers: {
|
|
208
|
+
"content-type": "application/json",
|
|
209
|
+
"cache-control": "no-store"
|
|
210
|
+
} });
|
|
137
211
|
}
|
|
138
212
|
function apply(ctx) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
213
|
+
const updater = new SelfUpdater(ctx.profileContext.dir, unwrap(ctx.pluginManager), ctx.logger);
|
|
214
|
+
ctx.connection.fetch.register({
|
|
215
|
+
path: SELF_UPDATE_PATH,
|
|
216
|
+
methods: ["GET", "POST"],
|
|
217
|
+
requestBody: "buffered",
|
|
218
|
+
fetch: async (request) => {
|
|
219
|
+
try {
|
|
220
|
+
if (request.method === "GET") return json({
|
|
221
|
+
ok: true,
|
|
222
|
+
status: await updater.status()
|
|
223
|
+
});
|
|
224
|
+
const body = await request.json().catch(() => null);
|
|
225
|
+
const version = typeof body === "object" && body !== null ? Reflect.get(body, "version") : void 0;
|
|
226
|
+
return json(await updater.start(version));
|
|
227
|
+
} catch (error) {
|
|
228
|
+
ctx.logger.warn("[plugin-updates] self-update route failed: %s", error instanceof Error ? error.message : String(error));
|
|
229
|
+
return json({
|
|
230
|
+
ok: false,
|
|
231
|
+
code: "internal",
|
|
232
|
+
message: error instanceof Error ? error.message : String(error)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
156
237
|
}
|
|
157
|
-
export {
|
|
158
|
-
apply,
|
|
159
|
-
inject,
|
|
160
|
-
name
|
|
161
|
-
};
|
|
238
|
+
export { apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shellraining/dsh-plugin-updates",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "在插件页检查 npm 来源插件的新版本,一键更新或安装指定版本",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
"@types/react": "18.3.31",
|
|
24
24
|
"@types/react-dom": "18.3.7",
|
|
25
25
|
"@types/semver": "7.8.0",
|
|
26
|
-
"esbuild": "0.28.2",
|
|
27
26
|
"semver": "7.8.5",
|
|
28
|
-
"typescript": "
|
|
27
|
+
"typescript": "7.0.2"
|
|
29
28
|
},
|
|
30
29
|
"dsh": {
|
|
31
30
|
"bundle": {
|
|
@@ -59,6 +58,6 @@
|
|
|
59
58
|
},
|
|
60
59
|
"scripts": {
|
|
61
60
|
"typecheck": "tsc -p tsconfig.host.json && tsc -p tsconfig.client.json",
|
|
62
|
-
"build": "pnpm run typecheck && node build.
|
|
61
|
+
"build": "pnpm run typecheck && node build.ts"
|
|
63
62
|
}
|
|
64
63
|
}
|