@iasiv5/dsh-skins 1.2.3 → 1.4.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/README.en.md +17 -37
- package/README.i18n.yaml +2 -2
- package/README.md +17 -37
- package/lib/client.js +53 -699
- package/lib/index.js +115 -937
- package/package.json +2 -3
package/lib/index.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { join as join2 } from "node:path";
|
|
6
4
|
|
|
7
|
-
// src/host/
|
|
8
|
-
import {
|
|
5
|
+
// src/host/personalization/store.js
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
copyFileSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync as mkdirSync2,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
renameSync as renameSync2,
|
|
16
|
+
rmdirSync,
|
|
17
|
+
statfsSync,
|
|
18
|
+
statSync,
|
|
19
|
+
unlinkSync,
|
|
20
|
+
writeFileSync as writeFileSync2
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
9
23
|
|
|
10
24
|
// src/host/errors.js
|
|
11
25
|
function codedError(code, message, params) {
|
|
@@ -23,280 +37,6 @@ function publicError(error) {
|
|
|
23
37
|
return value;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
// src/host/restart.js
|
|
27
|
-
var RELAUNCH_HELPER = String.raw`
|
|
28
|
-
const { spawn } = require('node:child_process')
|
|
29
|
-
const [parentPid, executable, cwd, argsJson] = process.argv.slice(1)
|
|
30
|
-
const args = JSON.parse(argsJson)
|
|
31
|
-
const waitForParent = () => {
|
|
32
|
-
try {
|
|
33
|
-
process.kill(Number(parentPid), 0)
|
|
34
|
-
setTimeout(waitForParent, 100)
|
|
35
|
-
} catch {
|
|
36
|
-
setTimeout(() => {
|
|
37
|
-
const child = spawn(executable, args, {
|
|
38
|
-
cwd,
|
|
39
|
-
env: process.env,
|
|
40
|
-
detached: false,
|
|
41
|
-
stdio: 'ignore',
|
|
42
|
-
})
|
|
43
|
-
child.once('error', () => process.exit(1))
|
|
44
|
-
child.once('exit', code => process.exit(code == null ? 1 : code))
|
|
45
|
-
}, 750)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
waitForParent()
|
|
49
|
-
`;
|
|
50
|
-
var MANAGED_RESTART_EXIT_CODE = 75;
|
|
51
|
-
function serviceManagedEnv(env) {
|
|
52
|
-
return typeof env === "object" && env !== null && (env.INVOCATION_ID !== void 0 || env.NOTIFY_SOCKET !== void 0);
|
|
53
|
-
}
|
|
54
|
-
function createRestartScheduler(exit, overrides = {}) {
|
|
55
|
-
if (typeof exit !== "function") return { available: false, schedule() {
|
|
56
|
-
} };
|
|
57
|
-
const runtime = {
|
|
58
|
-
execPath: process.execPath,
|
|
59
|
-
argv: [...process.argv],
|
|
60
|
-
cwd: process.cwd(),
|
|
61
|
-
env: process.env,
|
|
62
|
-
pid: process.pid,
|
|
63
|
-
spawn,
|
|
64
|
-
setTimeout,
|
|
65
|
-
...overrides
|
|
66
|
-
};
|
|
67
|
-
let scheduled = false;
|
|
68
|
-
return {
|
|
69
|
-
available: true,
|
|
70
|
-
schedule() {
|
|
71
|
-
if (scheduled) return;
|
|
72
|
-
scheduled = true;
|
|
73
|
-
if (serviceManagedEnv(runtime.env)) {
|
|
74
|
-
const managed = runtime.setTimeout(() => exit(MANAGED_RESTART_EXIT_CODE), 150);
|
|
75
|
-
managed.unref?.();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const helper = runtime.spawn(runtime.execPath, [
|
|
79
|
-
"-e",
|
|
80
|
-
RELAUNCH_HELPER,
|
|
81
|
-
String(runtime.pid),
|
|
82
|
-
runtime.execPath,
|
|
83
|
-
runtime.cwd,
|
|
84
|
-
JSON.stringify(runtime.argv.slice(1))
|
|
85
|
-
], {
|
|
86
|
-
cwd: runtime.cwd,
|
|
87
|
-
env: runtime.env,
|
|
88
|
-
detached: true,
|
|
89
|
-
stdio: "ignore"
|
|
90
|
-
});
|
|
91
|
-
helper.unref();
|
|
92
|
-
const timer = runtime.setTimeout(() => exit(0), 150);
|
|
93
|
-
timer.unref?.();
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function agentList(agents) {
|
|
98
|
-
if (agents === null || agents === void 0 || typeof agents.list !== "function") return null;
|
|
99
|
-
try {
|
|
100
|
-
const list = agents.list();
|
|
101
|
-
return Array.isArray(list) ? list : null;
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function restartSafety(agents) {
|
|
107
|
-
const list = agentList(agents);
|
|
108
|
-
if (list === null) return { state: "unknown", running: null };
|
|
109
|
-
const running = list.filter((agent) => agent?.status === "running").length;
|
|
110
|
-
return { state: running > 0 ? "blocked" : "safe", running };
|
|
111
|
-
}
|
|
112
|
-
async function waitForRestartSafety(agents, confirmUnknown = false) {
|
|
113
|
-
const list = agentList(agents);
|
|
114
|
-
if (list === null) {
|
|
115
|
-
if (!confirmUnknown) {
|
|
116
|
-
throw codedError("RESTART_SAFETY_UNKNOWN", "无法确认 Agent 运行状态,请再次确认后重启");
|
|
117
|
-
}
|
|
118
|
-
return { state: "unknown", running: null };
|
|
119
|
-
}
|
|
120
|
-
const running = list.filter((agent) => agent?.status === "running").length;
|
|
121
|
-
if (running > 0) {
|
|
122
|
-
throw codedError("AGENTS_RUNNING", `检测到 ${running} 个 Agent 正在运行,请等待任务结束后再重启`, { count: running });
|
|
123
|
-
}
|
|
124
|
-
await Promise.all(list.map((agent) => typeof agent?.whenIdle === "function" ? agent.whenIdle() : void 0));
|
|
125
|
-
const after = agentList(agents);
|
|
126
|
-
if (after === null) {
|
|
127
|
-
if (!confirmUnknown) {
|
|
128
|
-
throw codedError("RESTART_SAFETY_UNKNOWN", "复核 Agent 状态失败,请再次确认后重启");
|
|
129
|
-
}
|
|
130
|
-
return { state: "unknown", running: null };
|
|
131
|
-
}
|
|
132
|
-
const started = after.filter((agent) => agent?.status === "running").length;
|
|
133
|
-
if (started > 0) {
|
|
134
|
-
throw codedError("AGENTS_RUNNING", `检测到 ${started} 个 Agent 刚刚开始运行,请稍后再重启`, { count: started });
|
|
135
|
-
}
|
|
136
|
-
return { state: "safe", running: 0 };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// src/host/routes.js
|
|
140
|
-
function sendJson(response, status, value) {
|
|
141
|
-
response.writeHead(status, {
|
|
142
|
-
"content-type": "application/json; charset=utf-8",
|
|
143
|
-
"cache-control": "no-store"
|
|
144
|
-
});
|
|
145
|
-
response.end(JSON.stringify(value));
|
|
146
|
-
}
|
|
147
|
-
function header(headers, name2) {
|
|
148
|
-
const value = headers?.[name2];
|
|
149
|
-
return typeof value === "string" ? value : Array.isArray(value) ? value[0] : void 0;
|
|
150
|
-
}
|
|
151
|
-
function parseAuthority(authority) {
|
|
152
|
-
try {
|
|
153
|
-
return new URL(`http://${authority}`);
|
|
154
|
-
} catch {
|
|
155
|
-
return void 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function canonicalAuthority(entry, parsed) {
|
|
159
|
-
const port = parsed.port !== "" ? parsed.port : new URL(`https://${entry}`).port;
|
|
160
|
-
return port === "" ? parsed.hostname : `${parsed.hostname}:${port}`;
|
|
161
|
-
}
|
|
162
|
-
function isLoopbackHostname(hostname) {
|
|
163
|
-
if (hostname === "localhost" || hostname === "[::1]") return true;
|
|
164
|
-
const parts = hostname.split(".");
|
|
165
|
-
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
166
|
-
}
|
|
167
|
-
function isTrustedAuthority(hostUrl, trustedHosts) {
|
|
168
|
-
return trustedHosts.some((entry) => {
|
|
169
|
-
const parsed = parseAuthority(entry);
|
|
170
|
-
if (parsed === void 0) return false;
|
|
171
|
-
return canonicalAuthority(entry, parsed) === parsed.hostname ? parsed.hostname === hostUrl.hostname : parsed.host === hostUrl.host;
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
function isTrustedRequest(request, trustedHosts = []) {
|
|
175
|
-
const host = header(request.headers, "host");
|
|
176
|
-
if (host === void 0) return false;
|
|
177
|
-
const hostUrl = parseAuthority(host);
|
|
178
|
-
if (hostUrl === void 0) return false;
|
|
179
|
-
const socketAddress = request.socket?.remoteAddress ?? request.info?.remoteAddress;
|
|
180
|
-
if (isLoopbackHostname(hostUrl.hostname)) {
|
|
181
|
-
if (typeof socketAddress === "string") {
|
|
182
|
-
const loopbackPeer = socketAddress === "127.0.0.1" || socketAddress === "::1" || socketAddress === "::ffff:127.0.0.1";
|
|
183
|
-
if (!loopbackPeer) return false;
|
|
184
|
-
}
|
|
185
|
-
} else if (!isTrustedAuthority(hostUrl, trustedHosts)) {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
|
|
189
|
-
const origin = header(request.headers, "origin");
|
|
190
|
-
if (origin === void 0) return true;
|
|
191
|
-
try {
|
|
192
|
-
return new URL(origin).host === hostUrl.host;
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async function readJsonBody(request, limit = 4096) {
|
|
198
|
-
const type = String(request.headers["content-type"] ?? "").toLowerCase();
|
|
199
|
-
if (!type.startsWith("application/json")) throw new Error("content-type must be application/json");
|
|
200
|
-
const chunks = [];
|
|
201
|
-
let size = 0;
|
|
202
|
-
for await (const chunk of request) {
|
|
203
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
204
|
-
size += buffer.length;
|
|
205
|
-
if (size > limit) throw new Error("request body too large");
|
|
206
|
-
chunks.push(buffer);
|
|
207
|
-
}
|
|
208
|
-
if (size === 0) return {};
|
|
209
|
-
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
210
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid request body");
|
|
211
|
-
return value;
|
|
212
|
-
}
|
|
213
|
-
function method(request, response, expected) {
|
|
214
|
-
if (request.method === expected) return true;
|
|
215
|
-
response.writeHead(405, { allow: expected });
|
|
216
|
-
response.end();
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
function mountUpdateRoutes(host, options) {
|
|
220
|
-
const { updater, restart, agents, trustedHosts = [] } = options;
|
|
221
|
-
const updateRoute = host.webServer.register({
|
|
222
|
-
kind: "exact",
|
|
223
|
-
path: "/dsh-skins/update",
|
|
224
|
-
handler: async (request, response) => {
|
|
225
|
-
if (!isTrustedRequest(request, trustedHosts)) return sendJson(response, 403, { error: "trusted DSH Web request required" });
|
|
226
|
-
if (request.method === "GET") {
|
|
227
|
-
const url = new URL(request.url ?? "/", "http://localhost");
|
|
228
|
-
const force = url.searchParams.get("force") === "1" && request.headers["x-dsh-skins-force"] === "1";
|
|
229
|
-
try {
|
|
230
|
-
const status = await updater.status(force);
|
|
231
|
-
return sendJson(response, 200, {
|
|
232
|
-
...status,
|
|
233
|
-
operation: updater.currentOperation(),
|
|
234
|
-
restartRequired: updater.restartRequired,
|
|
235
|
-
restartAvailable: restart.available === true,
|
|
236
|
-
restartSafety: restartSafety(agents)
|
|
237
|
-
});
|
|
238
|
-
} catch (error) {
|
|
239
|
-
return sendJson(response, 502, {
|
|
240
|
-
...publicError(error),
|
|
241
|
-
operation: updater.currentOperation(),
|
|
242
|
-
restartRequired: updater.restartRequired,
|
|
243
|
-
restartAvailable: restart.available === true,
|
|
244
|
-
restartSafety: restartSafety(agents)
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (!method(request, response, "POST")) return;
|
|
249
|
-
try {
|
|
250
|
-
return sendJson(response, 202, { operation: updater.startUpdate() });
|
|
251
|
-
} catch (error) {
|
|
252
|
-
return sendJson(response, 409, publicError(error));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
const restartRoute = host.webServer.register({
|
|
257
|
-
kind: "exact",
|
|
258
|
-
path: "/dsh-skins/restart",
|
|
259
|
-
handler: async (request, response) => {
|
|
260
|
-
if (!isTrustedRequest(request, trustedHosts)) return sendJson(response, 403, { error: "trusted DSH Web request required" });
|
|
261
|
-
if (!method(request, response, "POST")) return;
|
|
262
|
-
if (restart.available !== true) {
|
|
263
|
-
return sendJson(response, 501, publicError(codedError("RESTART_UNAVAILABLE", "当前 DSH Host 不支持自重启")));
|
|
264
|
-
}
|
|
265
|
-
if (!updater.restartRequired) {
|
|
266
|
-
return sendJson(response, 409, publicError(codedError("NO_PENDING_UPDATE", "当前没有等待重启应用的更新")));
|
|
267
|
-
}
|
|
268
|
-
try {
|
|
269
|
-
const body = await readJsonBody(request);
|
|
270
|
-
const confirmUnknown = body.confirmUnknown === true;
|
|
271
|
-
await waitForRestartSafety(agents, confirmUnknown);
|
|
272
|
-
sendJson(response, 202, { restarting: true });
|
|
273
|
-
restart.schedule();
|
|
274
|
-
} catch (error) {
|
|
275
|
-
sendJson(response, 409, {
|
|
276
|
-
...publicError(error),
|
|
277
|
-
restartSafety: restartSafety(agents)
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
return () => {
|
|
283
|
-
restartRoute();
|
|
284
|
-
updateRoute();
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// src/host/self-update.js
|
|
289
|
-
import { randomUUID } from "node:crypto";
|
|
290
|
-
import {
|
|
291
|
-
existsSync,
|
|
292
|
-
mkdirSync as mkdirSync2,
|
|
293
|
-
readFileSync,
|
|
294
|
-
renameSync as renameSync2,
|
|
295
|
-
unlinkSync,
|
|
296
|
-
writeFileSync as writeFileSync2
|
|
297
|
-
} from "node:fs";
|
|
298
|
-
import { dirname as dirname2, join } from "node:path";
|
|
299
|
-
|
|
300
40
|
// src/host/atomic-write.js
|
|
301
41
|
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
302
42
|
import { dirname } from "node:path";
|
|
@@ -307,535 +47,6 @@ function atomicWriteText(file, content, fs = { mkdirSync, writeFileSync, renameS
|
|
|
307
47
|
fs.renameSync(temporary, file);
|
|
308
48
|
}
|
|
309
49
|
|
|
310
|
-
// src/host/self-update.js
|
|
311
|
-
var PACKAGE_NAME = "@iasiv5/dsh-skins";
|
|
312
|
-
var REPOSITORY = "iasiv5/dsh-skins";
|
|
313
|
-
var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@iasiv5%2Fdsh-skins/latest";
|
|
314
|
-
var NPM_PACKAGE_URL = "https://www.npmjs.com/package/@iasiv5/dsh-skins";
|
|
315
|
-
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
316
|
-
var CACHE_SCHEMA_VERSION = 2;
|
|
317
|
-
var PROFILE_FILES = ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"];
|
|
318
|
-
var STABLE_VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
319
|
-
var NPM_SELF_SPEC_RE = /^npm:@iasiv5\/dsh-skins@/i;
|
|
320
|
-
function isRecord(value) {
|
|
321
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
322
|
-
}
|
|
323
|
-
function parseStableVersion(value) {
|
|
324
|
-
const raw = String(value ?? "").trim();
|
|
325
|
-
const version = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
326
|
-
const match = STABLE_VERSION_RE.exec(version);
|
|
327
|
-
if (match === null) return null;
|
|
328
|
-
return {
|
|
329
|
-
version,
|
|
330
|
-
tag: `v${version}`,
|
|
331
|
-
parts: match.slice(1).map(Number)
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
function compareStableVersions(left, right) {
|
|
335
|
-
const a = parseStableVersion(left);
|
|
336
|
-
const b = parseStableVersion(right);
|
|
337
|
-
if (a === null || b === null) throw new Error("invalid stable semantic version");
|
|
338
|
-
for (let index = 0; index < 3; index += 1) {
|
|
339
|
-
if (a.parts[index] === b.parts[index]) continue;
|
|
340
|
-
return a.parts[index] > b.parts[index] ? 1 : -1;
|
|
341
|
-
}
|
|
342
|
-
return 0;
|
|
343
|
-
}
|
|
344
|
-
function repositoryIdentity(value) {
|
|
345
|
-
const raw = typeof value === "string" ? value : isRecord(value) && typeof value.url === "string" ? value.url : "";
|
|
346
|
-
return raw.trim().replace(/^git\+/, "").replace(/^git@github\.com:/i, "").replace(/^ssh:\/\/git@github\.com\//i, "").replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase();
|
|
347
|
-
}
|
|
348
|
-
function detectInstallSource(spec) {
|
|
349
|
-
const value = String(spec ?? "").trim();
|
|
350
|
-
if (/^link:/i.test(value)) return { kind: "link", spec: value };
|
|
351
|
-
if (/^file:/i.test(value) || /\.tgz(?:$|[?#])/i.test(value)) return { kind: "file", spec: value };
|
|
352
|
-
if (/^(?:git\+|github:|git@|ssh:\/\/|https?:\/\/)/i.test(value)) return { kind: "github", spec: value };
|
|
353
|
-
const range = NPM_SELF_SPEC_RE.test(value) ? value.replace(NPM_SELF_SPEC_RE, "") : value;
|
|
354
|
-
if (range === "*" || range === "latest" || /^[\s~^><=]*\d/.test(range)) {
|
|
355
|
-
return { kind: "npm", spec: value };
|
|
356
|
-
}
|
|
357
|
-
return { kind: "unknown", spec: value };
|
|
358
|
-
}
|
|
359
|
-
function readJson(file, fallback = null) {
|
|
360
|
-
try {
|
|
361
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
362
|
-
} catch {
|
|
363
|
-
return fallback;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function readProfileManifest(profileDir) {
|
|
367
|
-
return readJson(join(profileDir, "package.json"), {});
|
|
368
|
-
}
|
|
369
|
-
function readInstalledManifest(profileDir) {
|
|
370
|
-
return readJson(join(profileDir, "node_modules", PACKAGE_NAME, "package.json"), null);
|
|
371
|
-
}
|
|
372
|
-
function captureProfileSnapshot(profileDir) {
|
|
373
|
-
return PROFILE_FILES.map((name2) => {
|
|
374
|
-
const file = join(profileDir, name2);
|
|
375
|
-
return {
|
|
376
|
-
name: name2,
|
|
377
|
-
exists: existsSync(file),
|
|
378
|
-
content: existsSync(file) ? readFileSync(file) : null
|
|
379
|
-
};
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
function restoreProfileSnapshot(profileDir, snapshot) {
|
|
383
|
-
for (const item of snapshot) {
|
|
384
|
-
const file = join(profileDir, item.name);
|
|
385
|
-
if (item.exists) {
|
|
386
|
-
mkdirSync2(dirname2(file), { recursive: true });
|
|
387
|
-
const temporary = `${file}.${process.pid}.rollback`;
|
|
388
|
-
writeFileSync2(temporary, item.content);
|
|
389
|
-
renameSync2(temporary, file);
|
|
390
|
-
} else if (existsSync(file)) {
|
|
391
|
-
unlinkSync(file);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function preserveProfileBundles(profileDir, snapshot) {
|
|
396
|
-
const packageSnapshot = snapshot.find((item) => item.name === "package.json" && item.exists);
|
|
397
|
-
if (packageSnapshot?.content === null || packageSnapshot?.content === void 0) return;
|
|
398
|
-
let before;
|
|
399
|
-
try {
|
|
400
|
-
before = JSON.parse(packageSnapshot.content.toString("utf8"));
|
|
401
|
-
} catch {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const bundles = before.dsh?.profile?.bundles;
|
|
405
|
-
if (!Array.isArray(bundles) || !bundles.includes(PACKAGE_NAME)) return;
|
|
406
|
-
const current = readProfileManifest(profileDir);
|
|
407
|
-
current.dsh = {
|
|
408
|
-
...current.dsh,
|
|
409
|
-
profile: {
|
|
410
|
-
...current.dsh?.profile,
|
|
411
|
-
bundles: [...bundles]
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
atomicWriteText(join(profileDir, "package.json"), `${JSON.stringify(current, null, 2)}
|
|
415
|
-
`);
|
|
416
|
-
}
|
|
417
|
-
function validatePluginManifest(manifest, expectedVersion) {
|
|
418
|
-
if (!isRecord(manifest)) throw codedError("RELEASE_MANIFEST_MISSING", "发布的包缺少 package.json");
|
|
419
|
-
if (manifest.name !== PACKAGE_NAME) {
|
|
420
|
-
throw codedError("RELEASE_NAME_MISMATCH", `发布的包名必须是 ${PACKAGE_NAME}`, { expected: PACKAGE_NAME });
|
|
421
|
-
}
|
|
422
|
-
if (manifest.version !== expectedVersion) {
|
|
423
|
-
throw codedError(
|
|
424
|
-
"RELEASE_VERSION_MISMATCH",
|
|
425
|
-
`发布版本 v${expectedVersion} 与包版本 ${String(manifest.version ?? "missing")} 不一致`,
|
|
426
|
-
{ tag: expectedVersion, version: String(manifest.version ?? "missing") }
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
if (repositoryIdentity(manifest.repository) !== REPOSITORY.toLowerCase()) {
|
|
430
|
-
throw codedError("RELEASE_REPOSITORY_MISMATCH", `发布的包仓库必须是 ${REPOSITORY}`, { repository: REPOSITORY });
|
|
431
|
-
}
|
|
432
|
-
if (!isRecord(manifest.dsh) || !isRecord(manifest.dsh.client) || manifest.dsh.client.platform !== "web") {
|
|
433
|
-
throw codedError("RELEASE_NOT_WEB_PLUGIN", "发布的包不是 DSH Web 客户端插件");
|
|
434
|
-
}
|
|
435
|
-
if (!isRecord(manifest.dsh.bundle) || typeof manifest.dsh.bundle.patch !== "string") {
|
|
436
|
-
throw codedError("RELEASE_NO_BUNDLE_PATCH", "发布的包未声明 DSH bundle patch");
|
|
437
|
-
}
|
|
438
|
-
return manifest;
|
|
439
|
-
}
|
|
440
|
-
async function fetchRegistryJson(url, options = {}) {
|
|
441
|
-
const response = await fetch(url, {
|
|
442
|
-
headers: {
|
|
443
|
-
accept: "application/json",
|
|
444
|
-
"user-agent": `${PACKAGE_NAME}/${options.currentVersion ?? "unknown"}`
|
|
445
|
-
},
|
|
446
|
-
signal: options.signal
|
|
447
|
-
});
|
|
448
|
-
if (!response.ok) {
|
|
449
|
-
throw codedError(
|
|
450
|
-
"REGISTRY_CHECK_FAILED",
|
|
451
|
-
`npm registry 更新检查失败(HTTP ${response.status})`,
|
|
452
|
-
{ status: response.status }
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
return response.json();
|
|
456
|
-
}
|
|
457
|
-
async function fetchLatestStableRelease(options = {}) {
|
|
458
|
-
const manifest = await (options.fetchJson ?? fetchRegistryJson)(REGISTRY_LATEST_URL, options);
|
|
459
|
-
if (!isRecord(manifest) || manifest.name !== PACKAGE_NAME) {
|
|
460
|
-
throw codedError("REGISTRY_NAME_MISMATCH", "npm registry latest 返回的包名异常", { expected: PACKAGE_NAME });
|
|
461
|
-
}
|
|
462
|
-
const parsed = parseStableVersion(manifest.version);
|
|
463
|
-
if (parsed === null || manifest.version !== parsed.version) {
|
|
464
|
-
throw codedError(
|
|
465
|
-
"REGISTRY_VERSION_INVALID",
|
|
466
|
-
`npm registry latest 不是严格 X.Y.Z 版本:${String(manifest.version ?? "missing")}`,
|
|
467
|
-
{ version: String(manifest.version ?? "missing") }
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
471
|
-
version: parsed.version,
|
|
472
|
-
tag: parsed.tag,
|
|
473
|
-
htmlUrl: `${NPM_PACKAGE_URL}/v/${parsed.version}`,
|
|
474
|
-
name: parsed.tag
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
async function resolveReleaseArtifact(release, options = {}) {
|
|
478
|
-
const fetchJson = options.fetchJson ?? fetchRegistryJson;
|
|
479
|
-
const manifest = await fetchJson(
|
|
480
|
-
`https://registry.npmjs.org/@iasiv5%2Fdsh-skins/${release.version}`,
|
|
481
|
-
options
|
|
482
|
-
);
|
|
483
|
-
if (!isRecord(manifest) || !isRecord(manifest.dist) || typeof manifest.dist.integrity !== "string") {
|
|
484
|
-
throw codedError("REGISTRY_INTEGRITY_MISSING", "npm registry 版本元数据缺少 dist.integrity");
|
|
485
|
-
}
|
|
486
|
-
validatePluginManifest(manifest, release.version);
|
|
487
|
-
return { ...release, manifest, integrity: manifest.dist.integrity };
|
|
488
|
-
}
|
|
489
|
-
function cachePayload(currentVersion, checkedAt, release) {
|
|
490
|
-
return {
|
|
491
|
-
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
492
|
-
currentVersion,
|
|
493
|
-
checkedAt,
|
|
494
|
-
release
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
function validCachedRelease(value, currentVersion, now, ttlMs) {
|
|
498
|
-
if (!isRecord(value) || value.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
499
|
-
if (value.currentVersion !== currentVersion) return null;
|
|
500
|
-
if (!Number.isFinite(value.checkedAt) || now - value.checkedAt < 0 || now - value.checkedAt >= ttlMs) return null;
|
|
501
|
-
const release = value.release;
|
|
502
|
-
if (!isRecord(release) || parseStableVersion(release.version) === null) return null;
|
|
503
|
-
if (release.tag !== `v${release.version}` || typeof release.htmlUrl !== "string") return null;
|
|
504
|
-
return { checkedAt: value.checkedAt, release };
|
|
505
|
-
}
|
|
506
|
-
function publicOperation(operation) {
|
|
507
|
-
if (operation === null) return null;
|
|
508
|
-
return {
|
|
509
|
-
id: operation.id,
|
|
510
|
-
phase: operation.phase,
|
|
511
|
-
message: operation.message,
|
|
512
|
-
startedAt: operation.startedAt,
|
|
513
|
-
...operation.finishedAt === void 0 ? {} : { finishedAt: operation.finishedAt },
|
|
514
|
-
...operation.error === void 0 ? {} : { error: operation.error },
|
|
515
|
-
...operation.rolledBack === void 0 ? {} : { rolledBack: operation.rolledBack },
|
|
516
|
-
...operation.release === void 0 ? {} : { release: operation.release },
|
|
517
|
-
...operation.code === void 0 ? {} : { code: operation.code },
|
|
518
|
-
...operation.params === void 0 ? {} : { params: operation.params },
|
|
519
|
-
...operation.rollbackError === void 0 ? {} : { rollbackError: operation.rollbackError }
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function sourceReason(source) {
|
|
523
|
-
if (source.kind === "link") return "development-link";
|
|
524
|
-
if (source.kind === "file") return "local-file";
|
|
525
|
-
if (source.kind === "github") return "legacy-github";
|
|
526
|
-
if (source.kind !== "npm") return "unsupported-source";
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
function validateInstalledState(profileDir, artifact) {
|
|
530
|
-
const profile = readProfileManifest(profileDir);
|
|
531
|
-
const dependency = profile.dependencies?.[PACKAGE_NAME];
|
|
532
|
-
if (typeof dependency !== "string" || dependency !== artifact.version) {
|
|
533
|
-
throw codedError("PROFILE_NOT_PINNED", "profile 未固定到已验证的精确版本");
|
|
534
|
-
}
|
|
535
|
-
if (!Array.isArray(profile.dsh?.profile?.bundles) || !profile.dsh.profile.bundles.includes(PACKAGE_NAME)) {
|
|
536
|
-
throw codedError("PROFILE_BUNDLE_MISSING", "profile 未注册 dsh-skins bundle");
|
|
537
|
-
}
|
|
538
|
-
const installed = readInstalledManifest(profileDir);
|
|
539
|
-
validatePluginManifest(installed, artifact.version);
|
|
540
|
-
return installed;
|
|
541
|
-
}
|
|
542
|
-
function createSelfUpdater(options, dependencies = {}) {
|
|
543
|
-
const profileDir = options.profileDir;
|
|
544
|
-
const cacheFile = options.cacheFile;
|
|
545
|
-
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
546
|
-
const now = dependencies.now ?? Date.now;
|
|
547
|
-
const latestRelease = dependencies.fetchLatestRelease ?? fetchLatestStableRelease;
|
|
548
|
-
const resolveArtifact = dependencies.resolveReleaseArtifact ?? resolveReleaseArtifact;
|
|
549
|
-
const runner = dependencies.runner;
|
|
550
|
-
if (typeof runner !== "function") throw new Error("self updater requires a profile runner");
|
|
551
|
-
let currentVersion = options.currentVersion;
|
|
552
|
-
if (parseStableVersion(currentVersion) === null) throw new Error(`invalid current version ${currentVersion}`);
|
|
553
|
-
let memoryCache = null;
|
|
554
|
-
let operation = null;
|
|
555
|
-
let updatePromise = null;
|
|
556
|
-
let restartRequired = false;
|
|
557
|
-
const readSource = () => {
|
|
558
|
-
const profile = readProfileManifest(profileDir);
|
|
559
|
-
return detectInstallSource(profile.dependencies?.[PACKAGE_NAME]);
|
|
560
|
-
};
|
|
561
|
-
const readCache = () => {
|
|
562
|
-
if (memoryCache !== null) {
|
|
563
|
-
const valid2 = validCachedRelease(memoryCache, currentVersion, now(), ttlMs);
|
|
564
|
-
if (valid2 !== null) return valid2;
|
|
565
|
-
}
|
|
566
|
-
const disk = readJson(cacheFile, null);
|
|
567
|
-
const valid = validCachedRelease(disk, currentVersion, now(), ttlMs);
|
|
568
|
-
if (valid !== null) memoryCache = disk;
|
|
569
|
-
return valid;
|
|
570
|
-
};
|
|
571
|
-
const writeCache = (checkedAt, release) => {
|
|
572
|
-
memoryCache = cachePayload(currentVersion, checkedAt, release);
|
|
573
|
-
atomicWriteText(cacheFile, `${JSON.stringify(memoryCache, null, 2)}
|
|
574
|
-
`);
|
|
575
|
-
};
|
|
576
|
-
const buildStatus = (release, checkedAt, cached) => {
|
|
577
|
-
const source = readSource();
|
|
578
|
-
const releaseNewer = compareStableVersions(release.version, currentVersion) > 0;
|
|
579
|
-
const disabledReason = sourceReason(source);
|
|
580
|
-
return {
|
|
581
|
-
currentVersion,
|
|
582
|
-
source,
|
|
583
|
-
latest: release,
|
|
584
|
-
checkedAt,
|
|
585
|
-
cached,
|
|
586
|
-
updateAvailable: releaseNewer,
|
|
587
|
-
canUpdate: releaseNewer && disabledReason === null,
|
|
588
|
-
disabledReason,
|
|
589
|
-
restartRequired
|
|
590
|
-
};
|
|
591
|
-
};
|
|
592
|
-
const status = async (force = false) => {
|
|
593
|
-
if (!force) {
|
|
594
|
-
const cached = readCache();
|
|
595
|
-
if (cached !== null) return buildStatus(cached.release, cached.checkedAt, true);
|
|
596
|
-
}
|
|
597
|
-
const signal = AbortSignal.timeout(1e4);
|
|
598
|
-
const release = await latestRelease({ currentVersion, signal });
|
|
599
|
-
const checkedAt = now();
|
|
600
|
-
writeCache(checkedAt, release);
|
|
601
|
-
return buildStatus(release, checkedAt, false);
|
|
602
|
-
};
|
|
603
|
-
const setOperation = (patch) => {
|
|
604
|
-
Object.assign(operation, patch);
|
|
605
|
-
if (operation.phase === "done" || operation.phase === "failed") {
|
|
606
|
-
operation.finishedAt = new Date(now()).toISOString();
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
const executeUpdate = async () => {
|
|
610
|
-
let snapshot = null;
|
|
611
|
-
let previousSpec = null;
|
|
612
|
-
let previousVersion = null;
|
|
613
|
-
try {
|
|
614
|
-
setOperation({ phase: "checking", message: "正在重新检查最新正式版本" });
|
|
615
|
-
const before = await status(true);
|
|
616
|
-
if (!before.canUpdate) {
|
|
617
|
-
if (before.disabledReason === "development-link") {
|
|
618
|
-
throw codedError("UPDATE_LINK_PROTECTED", "本地 link 开发模式不会被在线更新覆盖");
|
|
619
|
-
}
|
|
620
|
-
if (before.updateAvailable) {
|
|
621
|
-
throw codedError("UPDATE_SOURCE_UNSUPPORTED", "当前安装来源不支持一键更新");
|
|
622
|
-
}
|
|
623
|
-
throw codedError("UPDATE_ALREADY_LATEST", "已经是最新正式版本");
|
|
624
|
-
}
|
|
625
|
-
setOperation({ phase: "preparing", message: "正在验证 npm 版本元数据" });
|
|
626
|
-
const artifact = await resolveArtifact(before.latest, {
|
|
627
|
-
currentVersion,
|
|
628
|
-
signal: AbortSignal.timeout(15e3)
|
|
629
|
-
});
|
|
630
|
-
operation.release = {
|
|
631
|
-
version: artifact.version,
|
|
632
|
-
tag: artifact.tag,
|
|
633
|
-
htmlUrl: artifact.htmlUrl
|
|
634
|
-
};
|
|
635
|
-
const profile = readProfileManifest(profileDir);
|
|
636
|
-
previousSpec = profile.dependencies?.[PACKAGE_NAME];
|
|
637
|
-
const previousManifest = readInstalledManifest(profileDir);
|
|
638
|
-
previousVersion = previousManifest?.version ?? currentVersion;
|
|
639
|
-
if (detectInstallSource(previousSpec).kind !== "npm") {
|
|
640
|
-
throw codedError("UPDATE_SOURCE_CHANGED", "更新开始前安装来源已变化,请重新打开主题切换器");
|
|
641
|
-
}
|
|
642
|
-
validatePluginManifest(previousManifest, previousVersion);
|
|
643
|
-
snapshot = captureProfileSnapshot(profileDir);
|
|
644
|
-
setOperation({ phase: "installing", message: `正在安装 ${artifact.tag}` });
|
|
645
|
-
await runner("web", ["add", `${PACKAGE_NAME}@${artifact.version}`], {
|
|
646
|
-
onChunk: (chunk) => {
|
|
647
|
-
if (String(chunk).trim() !== "") operation.message = `正在安装 ${artifact.tag}`;
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
preserveProfileBundles(profileDir, snapshot);
|
|
651
|
-
setOperation({ phase: "validating", message: "正在校验安装结果" });
|
|
652
|
-
validateInstalledState(profileDir, artifact);
|
|
653
|
-
currentVersion = artifact.version;
|
|
654
|
-
restartRequired = true;
|
|
655
|
-
writeCache(now(), {
|
|
656
|
-
version: artifact.version,
|
|
657
|
-
tag: artifact.tag,
|
|
658
|
-
htmlUrl: artifact.htmlUrl,
|
|
659
|
-
name: artifact.name
|
|
660
|
-
});
|
|
661
|
-
setOperation({ phase: "done", message: `已安装 ${artifact.tag},重启后生效`, rolledBack: false });
|
|
662
|
-
} catch (error) {
|
|
663
|
-
const original = error instanceof Error ? error : new Error(String(error));
|
|
664
|
-
let rollbackError = null;
|
|
665
|
-
let rolledBack = false;
|
|
666
|
-
if (snapshot !== null && typeof previousSpec === "string" && parseStableVersion(previousVersion) !== null) {
|
|
667
|
-
setOperation({ phase: "rollback", message: "更新失败,正在恢复原版本" });
|
|
668
|
-
try {
|
|
669
|
-
await runner("web", ["add", `${PACKAGE_NAME}@${previousVersion}`], {});
|
|
670
|
-
restoreProfileSnapshot(profileDir, snapshot);
|
|
671
|
-
const restored = readInstalledManifest(profileDir);
|
|
672
|
-
validatePluginManifest(restored, previousVersion);
|
|
673
|
-
const restoredProfile = readProfileManifest(profileDir);
|
|
674
|
-
if (restoredProfile.dependencies?.[PACKAGE_NAME] !== previousSpec) {
|
|
675
|
-
throw codedError("ROLLBACK_LOCKFILE_MISMATCH", "恢复后的版本锁定校验失败");
|
|
676
|
-
}
|
|
677
|
-
if (!Array.isArray(restoredProfile.dsh?.profile?.bundles) || !restoredProfile.dsh.profile.bundles.includes(PACKAGE_NAME)) {
|
|
678
|
-
throw codedError("ROLLBACK_BUNDLE_MISSING", "恢复后的 bundle 注册校验失败");
|
|
679
|
-
}
|
|
680
|
-
rolledBack = true;
|
|
681
|
-
} catch (rollbackFailure) {
|
|
682
|
-
rollbackError = rollbackFailure instanceof Error ? rollbackFailure : new Error(String(rollbackFailure));
|
|
683
|
-
try {
|
|
684
|
-
restoreProfileSnapshot(profileDir, snapshot);
|
|
685
|
-
} catch {
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
const message = rollbackError === null ? original.message : `${original.message};自动回滚失败:${rollbackError.message}`;
|
|
690
|
-
setOperation({
|
|
691
|
-
phase: "failed",
|
|
692
|
-
message,
|
|
693
|
-
error: message,
|
|
694
|
-
rolledBack,
|
|
695
|
-
...original.code === void 0 ? {} : { code: original.code },
|
|
696
|
-
...original.params === void 0 ? {} : { params: original.params },
|
|
697
|
-
...rollbackError === null ? {} : {
|
|
698
|
-
rollbackError: {
|
|
699
|
-
message: rollbackError.message,
|
|
700
|
-
...rollbackError.code === void 0 ? {} : { code: rollbackError.code },
|
|
701
|
-
...rollbackError.params === void 0 ? {} : { params: rollbackError.params }
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
} finally {
|
|
706
|
-
updatePromise = null;
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
return {
|
|
710
|
-
async status(force = false) {
|
|
711
|
-
return status(force);
|
|
712
|
-
},
|
|
713
|
-
startUpdate() {
|
|
714
|
-
if (updatePromise !== null) return publicOperation(operation);
|
|
715
|
-
operation = {
|
|
716
|
-
id: randomUUID(),
|
|
717
|
-
phase: "queued",
|
|
718
|
-
message: "更新已排队",
|
|
719
|
-
startedAt: new Date(now()).toISOString()
|
|
720
|
-
};
|
|
721
|
-
updatePromise = executeUpdate();
|
|
722
|
-
return publicOperation(operation);
|
|
723
|
-
},
|
|
724
|
-
currentOperation() {
|
|
725
|
-
return publicOperation(operation);
|
|
726
|
-
},
|
|
727
|
-
get restartRequired() {
|
|
728
|
-
return restartRequired;
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// src/host/runner.js
|
|
734
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
735
|
-
import { dirname as dirname3, resolve } from "node:path";
|
|
736
|
-
var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
737
|
-
var WINDOWS_METACHARS = /[\s"&|<>^()%!]/;
|
|
738
|
-
function quoteWindowsArg(value) {
|
|
739
|
-
if (!WINDOWS_METACHARS.test(value)) return value;
|
|
740
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
741
|
-
}
|
|
742
|
-
function spawnPortable(file, args, options) {
|
|
743
|
-
const { viaShell = false, ...spawnOptions } = options;
|
|
744
|
-
if (process.platform !== "win32" || viaShell !== true) {
|
|
745
|
-
return spawn2(file, args, { ...spawnOptions, shell: false });
|
|
746
|
-
}
|
|
747
|
-
const commandLine = [file, ...args].map(quoteWindowsArg).join(" ");
|
|
748
|
-
return spawn2(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${commandLine}"`], {
|
|
749
|
-
...spawnOptions,
|
|
750
|
-
shell: false,
|
|
751
|
-
windowsVerbatimArguments: true
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
function currentDshInvocation() {
|
|
755
|
-
const entry = process.argv[1];
|
|
756
|
-
if (entry !== void 0 && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
|
|
757
|
-
const absolute = resolve(entry);
|
|
758
|
-
return {
|
|
759
|
-
file: process.execPath,
|
|
760
|
-
prefix: [...process.execArgv, absolute],
|
|
761
|
-
cwd: dirname3(absolute),
|
|
762
|
-
viaShell: false
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
return { file: "dsh", prefix: [], cwd: void 0, viaShell: process.platform === "win32" };
|
|
766
|
-
}
|
|
767
|
-
function runDshPlugin(profile, args, options = {}) {
|
|
768
|
-
const invocation = options.invocation ?? currentDshInvocation();
|
|
769
|
-
const argv = [...invocation.prefix, "plugin", "--profile", profile, ...args, "--reporter=ndjson"];
|
|
770
|
-
return new Promise((resolvePromise, reject) => {
|
|
771
|
-
const child = spawnPortable(invocation.file, argv, {
|
|
772
|
-
cwd: invocation.cwd,
|
|
773
|
-
env: { ...process.env, CI: "true" },
|
|
774
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
775
|
-
detached: process.platform !== "win32",
|
|
776
|
-
viaShell: invocation.viaShell
|
|
777
|
-
});
|
|
778
|
-
let output = "";
|
|
779
|
-
let settled = false;
|
|
780
|
-
const finish = (error) => {
|
|
781
|
-
if (settled) return;
|
|
782
|
-
settled = true;
|
|
783
|
-
clearTimeout(timer);
|
|
784
|
-
if (error) reject(error);
|
|
785
|
-
else resolvePromise(output);
|
|
786
|
-
};
|
|
787
|
-
const append = (chunk) => {
|
|
788
|
-
const text = String(chunk);
|
|
789
|
-
output = (output + text).slice(-256 * 1024);
|
|
790
|
-
options.onChunk?.(text);
|
|
791
|
-
};
|
|
792
|
-
child.stdout?.on("data", append);
|
|
793
|
-
child.stderr?.on("data", append);
|
|
794
|
-
child.on("error", finish);
|
|
795
|
-
child.on("close", (code) => {
|
|
796
|
-
if (code === 0) finish();
|
|
797
|
-
else {
|
|
798
|
-
const tail = output.trim().slice(-1200) || "no output";
|
|
799
|
-
finish(codedError(
|
|
800
|
-
"UPDATE_COMMAND_FAILED",
|
|
801
|
-
`DSH 插件更新失败(exit ${String(code)}):${tail}`,
|
|
802
|
-
{ exitCode: String(code), output: tail }
|
|
803
|
-
));
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
const timer = setTimeout(() => {
|
|
807
|
-
if (child.pid !== void 0) {
|
|
808
|
-
try {
|
|
809
|
-
if (process.platform === "win32") child.kill("SIGTERM");
|
|
810
|
-
else process.kill(-child.pid, "SIGTERM");
|
|
811
|
-
} catch {
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
finish(codedError("UPDATE_COMMAND_TIMEOUT", "DSH 插件更新超时"));
|
|
815
|
-
}, options.timeoutMs ?? COMMAND_TIMEOUT_MS);
|
|
816
|
-
timer.unref?.();
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// src/host/personalization/store.js
|
|
821
|
-
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
822
|
-
import {
|
|
823
|
-
closeSync,
|
|
824
|
-
copyFileSync,
|
|
825
|
-
existsSync as existsSync2,
|
|
826
|
-
mkdirSync as mkdirSync3,
|
|
827
|
-
openSync,
|
|
828
|
-
readFileSync as readFileSync2,
|
|
829
|
-
readdirSync,
|
|
830
|
-
renameSync as renameSync3,
|
|
831
|
-
rmdirSync,
|
|
832
|
-
statfsSync,
|
|
833
|
-
statSync,
|
|
834
|
-
unlinkSync as unlinkSync2,
|
|
835
|
-
writeFileSync as writeFileSync3
|
|
836
|
-
} from "node:fs";
|
|
837
|
-
import { join as join2 } from "node:path";
|
|
838
|
-
|
|
839
50
|
// src/host/personalization/image-meta.js
|
|
840
51
|
function ascii(buffer, offset, length) {
|
|
841
52
|
return String.fromCharCode(...buffer.subarray(offset, offset + length));
|
|
@@ -961,49 +172,6 @@ var WALLPAPER_FIELD = {
|
|
|
961
172
|
maxPixels: GLOBAL_MAX_PIXELS
|
|
962
173
|
};
|
|
963
174
|
var SKINS = {
|
|
964
|
-
tgcf: {
|
|
965
|
-
builtinAssets: {
|
|
966
|
-
// Registry mirrors the grid: the factory default (moonlit) leads.
|
|
967
|
-
moonlit: { mime: "image/webp", labelKey: "personalization.tgcf.moonlit" },
|
|
968
|
-
crimson: { mime: "image/webp", labelKey: "personalization.tgcf.crimson" },
|
|
969
|
-
pale: { mime: "image/webp", labelKey: "personalization.tgcf.pale" },
|
|
970
|
-
"seal-favicon": { mime: "image/webp" }
|
|
971
|
-
},
|
|
972
|
-
fields: [
|
|
973
|
-
// Factory default rides moonlit (花怜 · 月下同伞) since the 1.0.0
|
|
974
|
-
// wallpaper addition; it leads the grid (user ruling) and its label
|
|
975
|
-
// carries the (默认壁纸) suffix. crimson/pale remain selectable, and
|
|
976
|
-
// stored overrides are untouched by the default flip.
|
|
977
|
-
{ ...WALLPAPER_FIELD, default: "builtin:tgcf:moonlit", builtinChoices: ["moonlit", "crimson", "pale"] },
|
|
978
|
-
{
|
|
979
|
-
key: "slogan",
|
|
980
|
-
type: "text",
|
|
981
|
-
scope: "locale",
|
|
982
|
-
labelKey: "personalization.slogan",
|
|
983
|
-
maxLength: 40,
|
|
984
|
-
default: { zh: "百无禁忌", en: "No Taboos" }
|
|
985
|
-
},
|
|
986
|
-
{
|
|
987
|
-
// Ruling #14: ONE translucency knob. It drives the panel tint, the
|
|
988
|
-
// wallpaper scrim and the blur as one combined visual (the curve
|
|
989
|
-
// lives in the tgcf projector, calibrated through the historical
|
|
990
|
-
// defaults P=82 → scrim 30 / blur 12); the blur/scrim fields are
|
|
991
|
-
// retired — pre-1.0.0 there are no external users to migrate.
|
|
992
|
-
// Ruling #15: factory default 10; ruling #17 re-tunes it to 30 —
|
|
993
|
-
// a faint veil so menus read while the wallpaper still leads;
|
|
994
|
-
// 1.0.0 re-tunes it to 35 (user) for a touch more panel presence.
|
|
995
|
-
key: "panelOpacity",
|
|
996
|
-
type: "range",
|
|
997
|
-
scope: "single",
|
|
998
|
-
labelKey: "personalization.panelTranslucency",
|
|
999
|
-
min: 0,
|
|
1000
|
-
max: 100,
|
|
1001
|
-
step: 1,
|
|
1002
|
-
unit: "%",
|
|
1003
|
-
default: 35
|
|
1004
|
-
}
|
|
1005
|
-
]
|
|
1006
|
-
},
|
|
1007
175
|
// ADR-0004 (reversing design §9a): every catalog skin declares the same
|
|
1008
176
|
// standard field set. The legacy skins' catalog slogan defaults are
|
|
1009
177
|
// same-source with their factories' static `slogans`, and panelOpacity
|
|
@@ -1062,9 +230,9 @@ var SKINS = {
|
|
|
1062
230
|
}
|
|
1063
231
|
]
|
|
1064
232
|
},
|
|
1065
|
-
// 美人志(v1.0.6
|
|
1066
|
-
// (001
|
|
1067
|
-
//
|
|
233
|
+
// 美人志(v1.0.6 前的出厂皮肤,现注册顺序第三位):6 张内置精选,出厂默认 yuntai
|
|
234
|
+
// (001合照)领头并对齐已移除 tgcf 皮肤的「出厂默认领先」惯例;panelOpacity 锚定
|
|
235
|
+
// 其二次曲线(projector 数学见 skins/meirenzhi/index.js)。
|
|
1068
236
|
meirenzhi: {
|
|
1069
237
|
builtinAssets: {
|
|
1070
238
|
yuntai: { mime: "image/webp", labelKey: "personalization.meirenzhi.yuntai" },
|
|
@@ -1242,12 +410,12 @@ var CORRUPT_BACKUP_LIMIT = 3;
|
|
|
1242
410
|
var ID_ATTEMPTS = 5;
|
|
1243
411
|
function defaultFs() {
|
|
1244
412
|
return {
|
|
1245
|
-
existsSync
|
|
1246
|
-
mkdirSync:
|
|
1247
|
-
readFileSync
|
|
1248
|
-
writeFileSync:
|
|
1249
|
-
renameSync:
|
|
1250
|
-
unlinkSync
|
|
413
|
+
existsSync,
|
|
414
|
+
mkdirSync: mkdirSync2,
|
|
415
|
+
readFileSync,
|
|
416
|
+
writeFileSync: writeFileSync2,
|
|
417
|
+
renameSync: renameSync2,
|
|
418
|
+
unlinkSync,
|
|
1251
419
|
readdirSync,
|
|
1252
420
|
rmdirSync,
|
|
1253
421
|
statSync,
|
|
@@ -1315,9 +483,9 @@ function createPersonalizationStore(options = {}) {
|
|
|
1315
483
|
}
|
|
1316
484
|
const fs = options.fs ?? defaultFs();
|
|
1317
485
|
const now = options.now ?? Date.now;
|
|
1318
|
-
const stateFile =
|
|
1319
|
-
const assetsDir =
|
|
1320
|
-
const quarantineDir =
|
|
486
|
+
const stateFile = join(dataDir, STATE_FILE);
|
|
487
|
+
const assetsDir = join(dataDir, ASSETS_DIR);
|
|
488
|
+
const quarantineDir = join(dataDir, QUARANTINE_DIR);
|
|
1321
489
|
let state = null;
|
|
1322
490
|
let mode = "normal";
|
|
1323
491
|
let recovery = null;
|
|
@@ -1345,7 +513,7 @@ function createPersonalizationStore(options = {}) {
|
|
|
1345
513
|
return fs.readdirSync(assetsDir).filter((name2) => !name2.startsWith("."));
|
|
1346
514
|
}
|
|
1347
515
|
function blobFileFor(id, extension) {
|
|
1348
|
-
return
|
|
516
|
+
return join(assetsDir, `${id}.${extension}`);
|
|
1349
517
|
}
|
|
1350
518
|
function commitState(next) {
|
|
1351
519
|
atomicWriteText(stateFile, `${JSON.stringify(next, null, 2)}
|
|
@@ -1358,7 +526,7 @@ function createPersonalizationStore(options = {}) {
|
|
|
1358
526
|
return run;
|
|
1359
527
|
}
|
|
1360
528
|
function sniffBlobFile(name2) {
|
|
1361
|
-
const file =
|
|
529
|
+
const file = join(assetsDir, name2);
|
|
1362
530
|
let buffer;
|
|
1363
531
|
try {
|
|
1364
532
|
buffer = fs.readFileSync(file);
|
|
@@ -1472,7 +640,7 @@ function createPersonalizationStore(options = {}) {
|
|
|
1472
640
|
const id = name2.split(".")[0];
|
|
1473
641
|
if (!live.has(id)) {
|
|
1474
642
|
try {
|
|
1475
|
-
fs.unlinkSync(
|
|
643
|
+
fs.unlinkSync(join(assetsDir, name2));
|
|
1476
644
|
} catch {
|
|
1477
645
|
}
|
|
1478
646
|
}
|
|
@@ -1593,7 +761,7 @@ function createPersonalizationStore(options = {}) {
|
|
|
1593
761
|
}
|
|
1594
762
|
function writeBlobExclusive(buffer, extension) {
|
|
1595
763
|
for (let attempt = 0; attempt < ID_ATTEMPTS; attempt += 1) {
|
|
1596
|
-
const id = ASSET_ID_PREFIX +
|
|
764
|
+
const id = ASSET_ID_PREFIX + randomUUID().replaceAll("-", "");
|
|
1597
765
|
const target = blobFileFor(id, extension);
|
|
1598
766
|
let descriptor;
|
|
1599
767
|
try {
|
|
@@ -1729,7 +897,7 @@ function createPersonalizationStore(options = {}) {
|
|
|
1729
897
|
fs.mkdirSync(quarantineDir, { recursive: true });
|
|
1730
898
|
for (const name2 of pending.quarantine) {
|
|
1731
899
|
try {
|
|
1732
|
-
fs.renameSync(
|
|
900
|
+
fs.renameSync(join(assetsDir, name2), join(quarantineDir, name2));
|
|
1733
901
|
} catch {
|
|
1734
902
|
}
|
|
1735
903
|
}
|
|
@@ -1738,12 +906,12 @@ function createPersonalizationStore(options = {}) {
|
|
|
1738
906
|
commitState(cleaned);
|
|
1739
907
|
}
|
|
1740
908
|
function pruneCorruptBackups() {
|
|
1741
|
-
const dir =
|
|
909
|
+
const dir = join(dataDir);
|
|
1742
910
|
if (!fs.existsSync(dir)) return;
|
|
1743
911
|
const backups = fs.readdirSync(dir).filter((name2) => name2.startsWith(`${STATE_FILE}.corrupt.`)).sort();
|
|
1744
912
|
while (backups.length > CORRUPT_BACKUP_LIMIT) {
|
|
1745
913
|
try {
|
|
1746
|
-
fs.unlinkSync(
|
|
914
|
+
fs.unlinkSync(join(dir, backups.shift()));
|
|
1747
915
|
} catch {
|
|
1748
916
|
}
|
|
1749
917
|
}
|
|
@@ -1764,6 +932,52 @@ function createPersonalizationStore(options = {}) {
|
|
|
1764
932
|
}
|
|
1765
933
|
|
|
1766
934
|
// src/host/personalization-routes.js
|
|
935
|
+
function parseAuthority(authority) {
|
|
936
|
+
try {
|
|
937
|
+
return new URL(`http://${authority}`);
|
|
938
|
+
} catch {
|
|
939
|
+
return void 0;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function canonicalAuthority(entry, parsed) {
|
|
943
|
+
const port = parsed.port !== "" ? parsed.port : new URL(`https://${entry}`).port;
|
|
944
|
+
return port === "" ? parsed.hostname : `${parsed.hostname}:${port}`;
|
|
945
|
+
}
|
|
946
|
+
function isLoopbackHostname(hostname) {
|
|
947
|
+
if (hostname === "localhost" || hostname === "[::1]") return true;
|
|
948
|
+
const parts = hostname.split(".");
|
|
949
|
+
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
950
|
+
}
|
|
951
|
+
function isTrustedAuthority(hostUrl, trustedHosts) {
|
|
952
|
+
return trustedHosts.some((entry) => {
|
|
953
|
+
const parsed = parseAuthority(entry);
|
|
954
|
+
if (parsed === void 0) return false;
|
|
955
|
+
return canonicalAuthority(entry, parsed) === parsed.hostname ? parsed.hostname === hostUrl.hostname : parsed.host === hostUrl.host;
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
function isTrustedRequest(request, trustedHosts = []) {
|
|
959
|
+
const host = header(request.headers, "host");
|
|
960
|
+
if (host === void 0) return false;
|
|
961
|
+
const hostUrl = parseAuthority(host);
|
|
962
|
+
if (hostUrl === void 0) return false;
|
|
963
|
+
const socketAddress = request.socket?.remoteAddress ?? request.info?.remoteAddress;
|
|
964
|
+
if (isLoopbackHostname(hostUrl.hostname)) {
|
|
965
|
+
if (typeof socketAddress === "string") {
|
|
966
|
+
const loopbackPeer = socketAddress === "127.0.0.1" || socketAddress === "::1" || socketAddress === "::ffff:127.0.0.1";
|
|
967
|
+
if (!loopbackPeer) return false;
|
|
968
|
+
}
|
|
969
|
+
} else if (!isTrustedAuthority(hostUrl, trustedHosts)) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
|
|
973
|
+
const origin = header(request.headers, "origin");
|
|
974
|
+
if (origin === void 0) return true;
|
|
975
|
+
try {
|
|
976
|
+
return new URL(origin).host === hostUrl.host;
|
|
977
|
+
} catch {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
1767
981
|
var CODE_STATUS = {
|
|
1768
982
|
INVALID_CONFIG: 400,
|
|
1769
983
|
INVALID_ASSET_ID: 400,
|
|
@@ -1779,7 +993,7 @@ var CODE_STATUS = {
|
|
|
1779
993
|
STORE_RECOVERY_REQUIRED: 409,
|
|
1780
994
|
STORE_NOT_RECOVERING: 409
|
|
1781
995
|
};
|
|
1782
|
-
function
|
|
996
|
+
function sendJson(response, status, value, extraHeaders = {}) {
|
|
1783
997
|
response.writeHead(status, {
|
|
1784
998
|
"content-type": "application/json; charset=utf-8",
|
|
1785
999
|
"cache-control": "no-store",
|
|
@@ -1789,19 +1003,19 @@ function sendJson2(response, status, value, extraHeaders = {}) {
|
|
|
1789
1003
|
}
|
|
1790
1004
|
function sendError(response, error) {
|
|
1791
1005
|
const status = CODE_STATUS[error?.code] ?? 500;
|
|
1792
|
-
|
|
1006
|
+
sendJson(response, status, publicError(error));
|
|
1793
1007
|
}
|
|
1794
|
-
function
|
|
1008
|
+
function header(headers, name2) {
|
|
1795
1009
|
const value = headers?.[name2];
|
|
1796
1010
|
return typeof value === "string" ? value : Array.isArray(value) ? value[0] : void 0;
|
|
1797
1011
|
}
|
|
1798
|
-
function
|
|
1012
|
+
function method(request, response, expected) {
|
|
1799
1013
|
if (request.method === expected) return true;
|
|
1800
1014
|
response.writeHead(405, { allow: expected });
|
|
1801
1015
|
response.end();
|
|
1802
1016
|
return false;
|
|
1803
1017
|
}
|
|
1804
|
-
async function
|
|
1018
|
+
async function readJsonBody(request, limit = 128 * 1024) {
|
|
1805
1019
|
const type = String(request.headers["content-type"] ?? "").toLowerCase();
|
|
1806
1020
|
if (!type.startsWith("application/json")) throw codedError("INVALID_CONFIG", "content-type must be application/json");
|
|
1807
1021
|
const chunks = [];
|
|
@@ -1836,7 +1050,7 @@ async function readRawBody(request, limit, overflowCode = "UPLOAD_TOO_LARGE") {
|
|
|
1836
1050
|
return Buffer.concat(chunks);
|
|
1837
1051
|
}
|
|
1838
1052
|
function decodeDisplayName(request) {
|
|
1839
|
-
const raw =
|
|
1053
|
+
const raw = header(request.headers, "x-filename");
|
|
1840
1054
|
if (raw === void 0 || raw === "") return "wallpaper";
|
|
1841
1055
|
try {
|
|
1842
1056
|
return decodeURIComponent(raw);
|
|
@@ -1849,7 +1063,7 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1849
1063
|
const disposers = [];
|
|
1850
1064
|
const fence = (request, response) => {
|
|
1851
1065
|
if (isTrustedRequest(request, trustedHosts)) return true;
|
|
1852
|
-
|
|
1066
|
+
sendJson(response, 403, { error: "trusted DSH Web request required" });
|
|
1853
1067
|
return false;
|
|
1854
1068
|
};
|
|
1855
1069
|
try {
|
|
@@ -1858,12 +1072,12 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1858
1072
|
path: "/dsh-skins/config",
|
|
1859
1073
|
handler: async (request, response) => {
|
|
1860
1074
|
if (!fence(request, response)) return;
|
|
1861
|
-
if (request.method === "GET") return
|
|
1862
|
-
if (!
|
|
1075
|
+
if (request.method === "GET") return sendJson(response, 200, store.snapshot());
|
|
1076
|
+
if (!method(request, response, "PATCH")) return;
|
|
1863
1077
|
try {
|
|
1864
|
-
const body = await
|
|
1078
|
+
const body = await readJsonBody(request);
|
|
1865
1079
|
const result = await store.applyOperations(body);
|
|
1866
|
-
return
|
|
1080
|
+
return sendJson(response, 200, result);
|
|
1867
1081
|
} catch (error) {
|
|
1868
1082
|
return sendError(response, error);
|
|
1869
1083
|
}
|
|
@@ -1875,9 +1089,9 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1875
1089
|
path: "/dsh-skins/recovery",
|
|
1876
1090
|
handler: async (request, response) => {
|
|
1877
1091
|
if (!fence(request, response)) return;
|
|
1878
|
-
if (!
|
|
1092
|
+
if (!method(request, response, "POST")) return;
|
|
1879
1093
|
try {
|
|
1880
|
-
return
|
|
1094
|
+
return sendJson(response, 200, await store.confirmRecovery());
|
|
1881
1095
|
} catch (error) {
|
|
1882
1096
|
return sendError(response, error);
|
|
1883
1097
|
}
|
|
@@ -1889,16 +1103,16 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1889
1103
|
path: "/dsh-skins/library",
|
|
1890
1104
|
handler: async (request, response) => {
|
|
1891
1105
|
if (!fence(request, response)) return;
|
|
1892
|
-
if (request.method === "GET") return
|
|
1893
|
-
if (!
|
|
1106
|
+
if (request.method === "GET") return sendJson(response, 200, store.snapshot());
|
|
1107
|
+
if (!method(request, response, "POST")) return;
|
|
1894
1108
|
try {
|
|
1895
|
-
const declaredLength = Number(
|
|
1109
|
+
const declaredLength = Number(header(request.headers, "content-length") ?? "0");
|
|
1896
1110
|
if (declaredLength > GLOBAL_MAX_BYTES) throw codedError("UPLOAD_TOO_LARGE", "图片超过 20MB 上限");
|
|
1897
1111
|
const buffer = await readRawBody(request, GLOBAL_MAX_BYTES + 1024);
|
|
1898
1112
|
const displayName = decodeDisplayName(request);
|
|
1899
|
-
const declaredMime =
|
|
1113
|
+
const declaredMime = header(request.headers, "content-type");
|
|
1900
1114
|
const result = await store.uploadAsset(buffer, { displayName, declaredMime });
|
|
1901
|
-
return
|
|
1115
|
+
return sendJson(response, 201, result);
|
|
1902
1116
|
} catch (error) {
|
|
1903
1117
|
return sendError(response, error);
|
|
1904
1118
|
}
|
|
@@ -1910,15 +1124,15 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1910
1124
|
path: "/dsh-skins/library",
|
|
1911
1125
|
handler: async (request, response) => {
|
|
1912
1126
|
if (!fence(request, response)) return;
|
|
1913
|
-
if (!
|
|
1127
|
+
if (!method(request, response, "DELETE")) return;
|
|
1914
1128
|
const path = String(request.url ?? "").split("?")[0];
|
|
1915
1129
|
const match = /\/dsh-skins\/library\/([^/]+)$/.exec(path);
|
|
1916
1130
|
const suffix = match?.[1] ?? "";
|
|
1917
1131
|
if (!ASSET_ID_PATTERN.test(suffix)) {
|
|
1918
|
-
return
|
|
1132
|
+
return sendJson(response, 400, { error: "invalid asset id", code: "INVALID_ASSET_ID" });
|
|
1919
1133
|
}
|
|
1920
1134
|
try {
|
|
1921
|
-
return
|
|
1135
|
+
return sendJson(response, 200, await store.deleteAsset(suffix));
|
|
1922
1136
|
} catch (error) {
|
|
1923
1137
|
return sendError(response, error);
|
|
1924
1138
|
}
|
|
@@ -1930,11 +1144,11 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1930
1144
|
path: assetsBasePath,
|
|
1931
1145
|
handler: async (request, response) => {
|
|
1932
1146
|
if (!fence(request, response)) return;
|
|
1933
|
-
if (!
|
|
1147
|
+
if (!method(request, response, "GET")) return;
|
|
1934
1148
|
try {
|
|
1935
1149
|
const blob = store.serveAsset(String(request.url ?? ""));
|
|
1936
1150
|
if (blob === null) {
|
|
1937
|
-
return
|
|
1151
|
+
return sendJson(response, 404, { error: "asset not found", code: "ASSET_NOT_FOUND" });
|
|
1938
1152
|
}
|
|
1939
1153
|
response.writeHead(200, {
|
|
1940
1154
|
"content-type": blob.meta.mime,
|
|
@@ -1967,60 +1181,24 @@ function mountPersonalizationRoutes(host, options) {
|
|
|
1967
1181
|
|
|
1968
1182
|
// src/index.js
|
|
1969
1183
|
var name = "@iasiv5/dsh-skins";
|
|
1970
|
-
function packageRoot() {
|
|
1971
|
-
return join3(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
1972
|
-
}
|
|
1973
|
-
function packageVersion() {
|
|
1974
|
-
const manifest = JSON.parse(readFileSync3(join3(packageRoot(), "package.json"), "utf8"));
|
|
1975
|
-
if (typeof manifest.version !== "string") throw new Error("dsh-skins package.json 缺少版本号");
|
|
1976
|
-
return manifest.version;
|
|
1977
|
-
}
|
|
1978
1184
|
function dshHome() {
|
|
1979
|
-
return process.env.DSH_HOME?.trim() ||
|
|
1185
|
+
return process.env.DSH_HOME?.trim() || join2(homedir(), ".dsh");
|
|
1980
1186
|
}
|
|
1981
1187
|
function apply(ctx) {
|
|
1982
|
-
ctx.inject(["webServer", "
|
|
1188
|
+
ctx.inject(["webServer", "webRuntime"], (hostContext) => {
|
|
1983
1189
|
const root = dshHome();
|
|
1984
|
-
const profileDir = join3(root, "profiles", "web");
|
|
1985
|
-
const updater = createSelfUpdater({
|
|
1986
|
-
profileDir,
|
|
1987
|
-
cacheFile: join3(root, "dsh-skins", "update-cache.json"),
|
|
1988
|
-
currentVersion: packageVersion()
|
|
1989
|
-
}, {
|
|
1990
|
-
runner: runDshPlugin
|
|
1991
|
-
});
|
|
1992
|
-
const restart = createRestartScheduler(ctx.get("appExit"));
|
|
1993
1190
|
const personalization = createPersonalizationStore({
|
|
1994
|
-
dataDir:
|
|
1191
|
+
dataDir: join2(root, "dsh-skins")
|
|
1995
1192
|
});
|
|
1996
1193
|
hostContext.effect(() => {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
restart,
|
|
2003
|
-
agents: hostContext.agents,
|
|
2004
|
-
trustedHosts: hostContext.webRuntime.trustedHosts
|
|
2005
|
-
});
|
|
2006
|
-
disposePersonalization = mountPersonalizationRoutes(hostContext, {
|
|
2007
|
-
store: personalization,
|
|
2008
|
-
trustedHosts: hostContext.webRuntime.trustedHosts
|
|
2009
|
-
});
|
|
2010
|
-
} catch (error) {
|
|
2011
|
-
disposePersonalization?.();
|
|
2012
|
-
disposeUpdate?.();
|
|
2013
|
-
throw error;
|
|
2014
|
-
}
|
|
2015
|
-
return () => {
|
|
2016
|
-
disposePersonalization();
|
|
2017
|
-
disposeUpdate();
|
|
2018
|
-
};
|
|
2019
|
-
}, "dsh-skins: self-update routes");
|
|
1194
|
+
return mountPersonalizationRoutes(hostContext, {
|
|
1195
|
+
store: personalization,
|
|
1196
|
+
trustedHosts: hostContext.webRuntime.trustedHosts
|
|
1197
|
+
});
|
|
1198
|
+
}, "dsh-skins: personalization routes");
|
|
2020
1199
|
});
|
|
2021
1200
|
}
|
|
2022
1201
|
export {
|
|
2023
1202
|
apply,
|
|
2024
|
-
createSelfUpdater,
|
|
2025
1203
|
name
|
|
2026
1204
|
};
|