@androidsup/dsh-plugin-updates 0.0.0-stage → 0.1.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.en.md +67 -0
- package/README.md +127 -2
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +538 -0
- package/lib/client.js.map +7 -0
- package/lib/host-v0.1.1.js +641 -0
- package/lib/index.js +641 -0
- package/lib/index.js.map +7 -0
- package/package.json +72 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// src/index.mjs
|
|
2
|
+
import { join as join2 } from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/plugin-updates.mjs
|
|
5
|
+
import { readFile, stat, rm } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { parse as parseYaml, parseDocument, isMap, isSeq, isScalar } from "yaml";
|
|
11
|
+
import { withFileLock, writeFileAtomic as writeFileAtomic2 } from "@deepseek-ai/dsh-atomic-write";
|
|
12
|
+
|
|
13
|
+
// src/storage.mjs
|
|
14
|
+
import { mkdir } from "node:fs/promises";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
17
|
+
async function writePrivateJson(file, value) {
|
|
18
|
+
await mkdir(dirname(file), { recursive: true, mode: 448 });
|
|
19
|
+
await writeFileAtomic(file, `${JSON.stringify(value, null, 2)}
|
|
20
|
+
`, { mode: 384 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/activation.mjs
|
|
24
|
+
function outsideActivation(hmr, operation) {
|
|
25
|
+
const scope = hmr?.executing;
|
|
26
|
+
return typeof scope?.exit === "function" ? scope.exit(operation) : operation();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/version.mjs
|
|
30
|
+
function parseVersion(raw) {
|
|
31
|
+
if (typeof raw !== "string") return void 0;
|
|
32
|
+
const normalized = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
33
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(normalized);
|
|
34
|
+
if (match === null) return void 0;
|
|
35
|
+
const rawPrerelease = match[4] === void 0 ? [] : match[4].split(".");
|
|
36
|
+
if (rawPrerelease.some((identifier) => /^\d+$/u.test(identifier) && !/^(0|[1-9]\d*)$/u.test(identifier))) return void 0;
|
|
37
|
+
const prerelease = rawPrerelease.map((identifier) => /^(0|[1-9]\d*)$/u.test(identifier) ? Number(identifier) : identifier);
|
|
38
|
+
if (prerelease.some((identifier) => typeof identifier === "number" && !Number.isSafeInteger(identifier))) return void 0;
|
|
39
|
+
const parsed = {
|
|
40
|
+
major: Number(match[1]),
|
|
41
|
+
minor: Number(match[2]),
|
|
42
|
+
patch: Number(match[3]),
|
|
43
|
+
prerelease
|
|
44
|
+
};
|
|
45
|
+
return [parsed.major, parsed.minor, parsed.patch].every(Number.isSafeInteger) ? parsed : void 0;
|
|
46
|
+
}
|
|
47
|
+
function compareIdentifiers(left, right) {
|
|
48
|
+
if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
|
|
49
|
+
if (typeof left === "number") return -1;
|
|
50
|
+
if (typeof right === "number") return 1;
|
|
51
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
52
|
+
}
|
|
53
|
+
function compareVersions(left, right) {
|
|
54
|
+
const a = parseVersion(left);
|
|
55
|
+
const b = parseVersion(right);
|
|
56
|
+
if (a === void 0 || b === void 0) throw new TypeError("invalid plugin version");
|
|
57
|
+
for (const [aPart, bPart] of [[a.major, b.major], [a.minor, b.minor], [a.patch, b.patch]]) {
|
|
58
|
+
if (aPart !== bPart) return aPart < bPart ? -1 : 1;
|
|
59
|
+
}
|
|
60
|
+
if (a.prerelease.length === 0 && b.prerelease.length !== 0) return 1;
|
|
61
|
+
if (a.prerelease.length !== 0 && b.prerelease.length === 0) return -1;
|
|
62
|
+
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
|
|
63
|
+
const aPart = a.prerelease[index];
|
|
64
|
+
const bPart = b.prerelease[index];
|
|
65
|
+
if (aPart === void 0) return -1;
|
|
66
|
+
if (bPart === void 0) return 1;
|
|
67
|
+
const comparison = compareIdentifiers(aPart, bPart);
|
|
68
|
+
if (comparison !== 0) return comparison;
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/plugin-updates.mjs
|
|
74
|
+
var packageName = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
75
|
+
var safeRef = /^[a-zA-Z0-9._/-]{1,200}$/;
|
|
76
|
+
function parsePluginSource(name2, spec) {
|
|
77
|
+
if (!packageName.test(name2) || typeof spec !== "string" || spec.length > 600 || spec.startsWith("-") || spec.trim() !== spec || /[\x00-\x1f]/.test(spec)) return { kind: "unsupported" };
|
|
78
|
+
const git = /^(?:github:|git\+https:\/\/github\.com\/|https:\/\/github\.com\/|git\+ssh:\/\/git@github\.com\/|git@github\.com:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:#(.+))?$/.exec(spec);
|
|
79
|
+
if (git && !spec.startsWith("@") && !spec.startsWith("file:") && !spec.startsWith("link:")) {
|
|
80
|
+
const [, owner, repo, ref] = git;
|
|
81
|
+
if (owner === "." || owner === ".." || repo === "." || repo === ".." || ref && (!safeRef.test(ref) || ref.includes(".."))) return { kind: "unsupported" };
|
|
82
|
+
return { kind: "github", owner, repo, ref, source: `github:${owner}/${repo}${ref ? "#" + ref : ""}` };
|
|
83
|
+
}
|
|
84
|
+
if (/^(?:file:|link:|workspace:|https?:|git|\/|\.)/.test(spec)) return { kind: "unsupported" };
|
|
85
|
+
const alias = /^npm:((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
|
|
86
|
+
const actual = alias?.[1] ?? name2, range = alias?.[2] ?? spec;
|
|
87
|
+
if (!packageName.test(actual) || !/^[a-zA-Z0-9.*^~<>=|+ -]{1,160}$/.test(range)) return { kind: "unsupported" };
|
|
88
|
+
const channel = /^[a-zA-Z][\w.-]*$/.test(range);
|
|
89
|
+
return { kind: "npm", package: actual, alias: !!alias, channel, tag: channel ? range : "latest", source: spec };
|
|
90
|
+
}
|
|
91
|
+
function githubCommit(value) {
|
|
92
|
+
if (typeof value !== "string") return void 0;
|
|
93
|
+
return /(?:\/tar\.gz\/|\/tarball\/|#)([a-f0-9]{40})(?:\b|\()/i.exec(value)?.[1]?.toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
async function jsonFile(file) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
98
|
+
} catch {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function locks(dir) {
|
|
103
|
+
let pnpm;
|
|
104
|
+
try {
|
|
105
|
+
pnpm = parseYaml(await readFile(join(dir, "pnpm-lock.yaml"), "utf8"), { maxAliasCount: 20 });
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
const npm = await jsonFile(join(dir, "package-lock.json"));
|
|
109
|
+
const times = await Promise.all(["pnpm-lock.yaml", "package-lock.json"].map((name2) => stat(join(dir, name2)).then((s) => s.mtimeMs).catch(() => 0)));
|
|
110
|
+
return { pnpm, npm, preferNpm: times[1] > times[0] };
|
|
111
|
+
}
|
|
112
|
+
async function publicJson(url, fetchImpl, signal) {
|
|
113
|
+
const res = await fetchImpl(url, {
|
|
114
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(12e3)]),
|
|
115
|
+
// Request only the SHA: the default JSON includes file patches and can be megabytes.
|
|
116
|
+
headers: { Accept: "application/vnd.github.sha", "User-Agent": "dsh-plugin-updates" },
|
|
117
|
+
redirect: "error"
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) throw Error(res.status === 403 || res.status === 429 ? "\u8FDC\u7AEF\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : res.status === 404 ? "\u4ED3\u5E93 / \u7248\u672C\u4E0D\u5B58\u5728\uFF0C\u6216\u9700\u8981\u79C1\u6709\u4ED3\u5E93\u6388\u6743" : `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09`);
|
|
120
|
+
let size = 0, chunks = [];
|
|
121
|
+
for await (const chunk of res.body) {
|
|
122
|
+
size += chunk.byteLength;
|
|
123
|
+
if (size > 256 * 1024) throw Error("\u66F4\u65B0\u5143\u6570\u636E\u8FC7\u5927");
|
|
124
|
+
chunks.push(Buffer.from(chunk));
|
|
125
|
+
}
|
|
126
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
127
|
+
return /^[a-f0-9]{40}$/i.test(text) ? { sha: text } : JSON.parse(text);
|
|
128
|
+
}
|
|
129
|
+
var short = (sha) => sha?.slice(0, 8);
|
|
130
|
+
var reviewedVersion = (row) => row.source.kind === "npm" && parseVersion(row.latestVersion) ? `${row.source.package}@${row.latestVersion}` : void 0;
|
|
131
|
+
async function allowReviewedVersion(profile, row) {
|
|
132
|
+
const file = join(profile.dir, "pnpm-workspace.yaml"), prefix = `${row.source.package}@`;
|
|
133
|
+
try {
|
|
134
|
+
await withFileLock(file, async () => {
|
|
135
|
+
let text;
|
|
136
|
+
try {
|
|
137
|
+
text = await readFile(file, "utf8");
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e.code !== "ENOENT") throw e;
|
|
140
|
+
text = "{}\n";
|
|
141
|
+
}
|
|
142
|
+
const doc = parseDocument(text), entries = doc.get("minimumReleaseAgeExclude");
|
|
143
|
+
if (doc.errors.length || !isMap(doc.contents) || entries !== void 0 && (!isSeq(entries) || entries.anchor || entries.items.some((x) => !isScalar(x) || typeof x.value !== "string" || x.anchor))) throw Error("invalid-policy");
|
|
144
|
+
const values = entries?.items.map((x) => x.value) ?? [];
|
|
145
|
+
const existing = values.filter((x) => x.startsWith(prefix));
|
|
146
|
+
const versions = [.../* @__PURE__ */ new Set([...existing.flatMap((x) => x.slice(prefix.length).split(/\s*\|\|\s*/)), row.latestVersion])];
|
|
147
|
+
const merged = prefix + versions.join(" || ");
|
|
148
|
+
if (existing.length === 1 && existing[0] === merged) return;
|
|
149
|
+
if (!entries) doc.set("minimumReleaseAgeExclude", [merged]);
|
|
150
|
+
else {
|
|
151
|
+
const first = entries.items.findIndex((x) => x.value.startsWith(prefix));
|
|
152
|
+
if (first < 0) entries.add(merged);
|
|
153
|
+
else {
|
|
154
|
+
entries.items[first].value = merged;
|
|
155
|
+
for (let i = entries.items.length - 1; i > first; i--) if (entries.items[i].value.startsWith(prefix)) entries.delete(i);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await writeFileAtomic2(file, String(doc), { mode: 384 });
|
|
159
|
+
}, { waitMs: 12e4 });
|
|
160
|
+
} catch {
|
|
161
|
+
throw Error("\u65E0\u6CD5\u4FDD\u5B58\u6B64\u7248\u672C\u7684\u7B49\u5F85\u671F\u4F8B\u5916\u914D\u7F6E\uFF1B\u8BF7\u68C0\u67E5\u5F53\u524D DSH \u914D\u7F6E\u76EE\u5F55\u6743\u9650\u53CA pnpm-workspace.yaml \u683C\u5F0F\u3002\u672A\u5173\u95ED\u7B49\u5F85\u671F\u4FDD\u62A4\u3002");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
var installFailed = (r) => !!(r.error || ["failed", "cancelled"].includes(r.application) || r.packageResult && r.packageResult.exitCode !== 0);
|
|
165
|
+
function installFailure(result) {
|
|
166
|
+
const text = [result.error?.diagnostic, result.packageResult?.output, result.packageResult?.kind].filter((v) => typeof v === "string").join("\n");
|
|
167
|
+
let reason = "unknown", message = "\u5B89\u88C5\u672A\u5B8C\u6210\uFF1B\u8BF7\u5728 DSH \u539F\u751F\u63D2\u4EF6\u9875\u67E5\u770B\u5B89\u88C5\u65E5\u5FD7\uFF0C\u6216\u590D\u5236\u4E0B\u65B9\u8BCA\u65AD\u4FE1\u606F\u3002";
|
|
168
|
+
if (result.application === "cancelled") {
|
|
169
|
+
reason = "cancelled";
|
|
170
|
+
message = "\u5B89\u88C5\u5DF2\u53D6\u6D88\uFF0C\u53EF\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\u3002";
|
|
171
|
+
} else if (/ERR_PNPM_(?:MINIMUM_RELEASE_AGE_VIOLATION|NO_MATURE_MATCHING_VERSION)|minimumReleaseAge/i.test(text)) {
|
|
172
|
+
reason = "release-age";
|
|
173
|
+
message = "\u65B0\u5305\u5C1A\u5728\u5B89\u88C5\u5668\u7B49\u5F85\u671F\u5185\u3002\u53EF\u5728\u300C\u67E5\u770B\u5E76\u66F4\u65B0\u300D\u4E2D\u52FE\u9009\u300C\u5141\u8BB8\u672C\u6B21\u6307\u5B9A\u7248\u672C\u7ACB\u5373\u5B89\u88C5\u300D\u540E\u91CD\u8BD5\uFF1B\u53EA\u653E\u884C\u786E\u8BA4\u7684\u63D2\u4EF6\u7248\u672C\uFF0C\u4F9D\u8D56\u4ECD\u9700\u6EE1\u8DB3\u7B49\u5F85\u671F\u3002\u4E5F\u53EF\u7B49\u5F85\u5230\u671F\u518D\u5B89\u88C5\u3002";
|
|
174
|
+
} else if (result.pendingBuilds?.length) {
|
|
175
|
+
reason = "build";
|
|
176
|
+
message = "\u4F9D\u8D56\u6784\u5EFA\u811A\u672C\u9700\u8981\u6388\u6743\uFF0C\u8BF7\u5230 DSH \u63D2\u4EF6\u5B89\u88C5\u9875\u5141\u8BB8\u8FD9\u4E9B\u811A\u672C\u540E\u91CD\u8BD5\u3002";
|
|
177
|
+
} else if (result.error?.code === "incompatible-version") {
|
|
178
|
+
reason = "compatibility";
|
|
179
|
+
message = "\u63D2\u4EF6\u4E0E\u5F53\u524D DSH \u4E0D\u517C\u5BB9\uFF0C\u8BF7\u5148\u5347\u7EA7 DSH \u6216\u9009\u62E9\u517C\u5BB9\u7248\u672C\u3002";
|
|
180
|
+
} else if (/\b(?:EPERM|EBUSY|EACCES)\b|resource busy|file.*(?:locked|in use)/i.test(text)) {
|
|
181
|
+
reason = "files";
|
|
182
|
+
message = "\u63D2\u4EF6\u6587\u4EF6\u88AB\u5360\u7528\u6216\u76EE\u5F55\u6743\u9650\u4E0D\u8DB3\uFF0C\u5B89\u88C5\u672A\u5B8C\u6210\u3002\u8BF7\u5148\u5728 DSH \u505C\u7528\u8BE5\u63D2\u4EF6\u540E\u91CD\u8BD5\uFF0C\u5E76\u68C0\u67E5\u5B89\u88C5\u76EE\u5F55\u6743\u9650\uFF1B\u4ECD\u88AB\u5360\u7528\u65F6\u9700\u9000\u51FA\u6258\u76D8\u5E94\u7528\u3002";
|
|
183
|
+
} else if (/HMR transactions cannot be nested|activation|did not activate|entry.*activate/i.test(text)) {
|
|
184
|
+
reason = "activation";
|
|
185
|
+
message = "\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\u3002\u8BF7\u68C0\u67E5 DSH \u63D2\u4EF6\u9875\u7684\u7EC4\u4EF6\u9519\u8BEF\uFF0C\u518D\u5173\u95ED\u5E76\u91CD\u65B0\u542F\u7528\uFF1B\u5B89\u88C5\u4E0E\u52A0\u8F7D\u662F\u4E24\u4E2A\u72EC\u7ACB\u6B65\u9AA4\u3002";
|
|
186
|
+
} else if (result.packageResult?.timedOut || /\b(?:ETIMEDOUT|ECONNRESET|ENOTFOUND|EAI_AGAIN|ECONNREFUSED)\b|network|fetch|download/i.test(text)) {
|
|
187
|
+
reason = "network";
|
|
188
|
+
message = "\u63D2\u4EF6\u4E0B\u8F7D\u6216\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u5B89\u88C5\u672A\u5B8C\u6210\u3002npm \u6765\u6E90\u53EF\u5207\u6362\u5230\u300Cnpm \u5B98\u65B9\u300D\u540E\u91CD\u8BD5\uFF1BGitHub \u6765\u6E90\u8BF7\u68C0\u67E5 GitHub \u7F51\u7EDC\u8FDE\u63A5\u3002";
|
|
189
|
+
}
|
|
190
|
+
const code = /\b(?:ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|EPERM|EBUSY|EACCES|ETIMEDOUT|ECONNRESET|ENOTFOUND|EAI_AGAIN|ECONNREFUSED)\b/.exec(text)?.[0] ?? (["operation-error", "incompatible-version"].includes(result.error?.code) ? result.error.code : "unknown");
|
|
191
|
+
return { status: "failed", application: result.application, message, diagnostic: {
|
|
192
|
+
reason,
|
|
193
|
+
code,
|
|
194
|
+
platform: process.platform,
|
|
195
|
+
stage: reason === "activation" ? "activation" : "install",
|
|
196
|
+
...Number.isInteger(result.packageResult?.exitCode) ? { exitCode: result.packageResult.exitCode } : {}
|
|
197
|
+
} };
|
|
198
|
+
}
|
|
199
|
+
async function registryVersion(service, profile, spec, signal, requestedRegistry) {
|
|
200
|
+
const require2 = createRequire(profile.installAnchor);
|
|
201
|
+
const { viewProfilePackage } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
|
|
202
|
+
const { registryPlan } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
|
|
203
|
+
for (const registry of registryPlan(requestedRegistry, await service.registries())) {
|
|
204
|
+
const answer = await viewProfilePackage(profile.dir, spec, { ...profile.packageManager, registry, timeoutMs: 12e3, signal });
|
|
205
|
+
if (signal.aborted) throw Error("\u68C0\u67E5\u5DF2\u53D6\u6D88");
|
|
206
|
+
if (answer.exitCode !== 0) continue;
|
|
207
|
+
const parsed = JSON.parse(answer.stdout), metadata = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
|
208
|
+
if (metadata?.version) return metadata;
|
|
209
|
+
}
|
|
210
|
+
throw Error("npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7F51\u7EDC\u3001registry \u8BBE\u7F6E\u6216\u5305\u7684\u8BBF\u95EE\u6743\u9650");
|
|
211
|
+
}
|
|
212
|
+
async function refreshProfilePlugin(service, profile, row, options, signal) {
|
|
213
|
+
const require2 = createRequire(profile.installAnchor);
|
|
214
|
+
const { runProfilePnpm } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
|
|
215
|
+
const { registryPlan, attributeFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
|
|
216
|
+
const { classifyInstallFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager")).href);
|
|
217
|
+
const plan = registryPlan(options.registry, await service.registries());
|
|
218
|
+
return withFileLock(join(profile.dir, "package.json"), async () => {
|
|
219
|
+
const files = await Promise.all(["package.json", "pnpm-lock.yaml"].map(async (name2) => {
|
|
220
|
+
const path = join(profile.dir, name2);
|
|
221
|
+
try {
|
|
222
|
+
return { path, content: await readFile(path, "utf8") };
|
|
223
|
+
} catch (e) {
|
|
224
|
+
if (e.code !== "ENOENT") throw e;
|
|
225
|
+
return { path };
|
|
226
|
+
}
|
|
227
|
+
}));
|
|
228
|
+
const restore = () => Promise.all(files.map((f) => f.content === void 0 ? rm(f.path, { force: true }) : writeFileAtomic2(f.path, f.content, { mode: 384 })));
|
|
229
|
+
if (JSON.parse(files[0].content).dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
230
|
+
let result;
|
|
231
|
+
try {
|
|
232
|
+
for (const registry of plan) {
|
|
233
|
+
signal.throwIfAborted();
|
|
234
|
+
const run = await runProfilePnpm({ ...profile, profile: profile.name }, ["update", row.source.kind === "github" ? row.name : row.target, ...registry ? [`--registry=${registry}`] : []], {
|
|
235
|
+
execution: "service",
|
|
236
|
+
...profile.packageManager,
|
|
237
|
+
signal,
|
|
238
|
+
outputBytes: 16384,
|
|
239
|
+
idleTimeoutMs: 6e5,
|
|
240
|
+
lookupTimeoutMs: 2e4,
|
|
241
|
+
activateNewBundles: false
|
|
242
|
+
});
|
|
243
|
+
if (run.exitCode === 0 && !run.timedOut && !signal.aborted) return { packageResult: run, application: "restart-required" };
|
|
244
|
+
const kind = classifyInstallFailure({ log: run.output, timedOut: run.timedOut });
|
|
245
|
+
result = {
|
|
246
|
+
packageResult: { ...run, kind },
|
|
247
|
+
application: signal.aborted ? "cancelled" : "failed",
|
|
248
|
+
...run.incompatible?.length ? { error: { code: "incompatible-version" } } : {}
|
|
249
|
+
};
|
|
250
|
+
await restore();
|
|
251
|
+
if (signal.aborted || attributeFailure(kind, run.output, row.source.kind === "github" ? { kind: "git", host: "github.com" } : { kind: "npm" }) !== "registry") break;
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
} catch (e) {
|
|
255
|
+
await restore();
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
}, { waitMs: 12e4 });
|
|
259
|
+
}
|
|
260
|
+
var PluginUpdates = class {
|
|
261
|
+
constructor({ service = () => void 0, profile = () => void 0, hmr = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshPackage = refreshProfilePlugin, onInstalled, stateFile, selfName = "@androidsup/dsh-plugin-updates", now = Date.now } = {}) {
|
|
262
|
+
this.selfName = selfName;
|
|
263
|
+
this.stateFile = stateFile;
|
|
264
|
+
this.now = now;
|
|
265
|
+
this.dismissed = {};
|
|
266
|
+
this.ignored = /* @__PURE__ */ new Set();
|
|
267
|
+
this.lastAttempt = 0;
|
|
268
|
+
this.saveQueue = Promise.resolve();
|
|
269
|
+
this.service = service;
|
|
270
|
+
this.profile = profile;
|
|
271
|
+
this.hmr = hmr;
|
|
272
|
+
this.fetchImpl = fetchImpl;
|
|
273
|
+
this.rows = [];
|
|
274
|
+
this.abort = new AbortController();
|
|
275
|
+
this.lookup = lookup;
|
|
276
|
+
this.refreshPackage = refreshPackage;
|
|
277
|
+
this.onInstalled = onInstalled;
|
|
278
|
+
}
|
|
279
|
+
async load() {
|
|
280
|
+
const saved = this.stateFile && await jsonFile(this.stateFile);
|
|
281
|
+
if (saved?.version === 1) {
|
|
282
|
+
this.lastAttempt = Number.isFinite(saved.lastAttempt) ? saved.lastAttempt : 0;
|
|
283
|
+
this.checkedAt = saved.checkedAt;
|
|
284
|
+
this.dismissed = saved.dismissed && typeof saved.dismissed === "object" && !Array.isArray(saved.dismissed) ? saved.dismissed : {};
|
|
285
|
+
this.rows = Array.isArray(saved.rows) ? saved.rows.filter((r) => ["github", "npm"].includes(r.source?.kind)) : [];
|
|
286
|
+
}
|
|
287
|
+
if (saved?.version === 1) this.ignored = new Set(Array.isArray(saved.ignored) ? saved.ignored.filter((key) => typeof key === "string") : []);
|
|
288
|
+
}
|
|
289
|
+
persist() {
|
|
290
|
+
if (!this.stateFile) return Promise.resolve();
|
|
291
|
+
const saved = structuredClone({ version: 1, lastAttempt: this.lastAttempt, checkedAt: this.checkedAt, dismissed: this.dismissed, ignored: [...this.ignored], rows: this.rows.filter((r) => ["github", "npm"].includes(r.source.kind)) });
|
|
292
|
+
this.saveQueue = this.saveQueue.catch(() => {
|
|
293
|
+
}).then(() => writePrivateJson(this.stateFile, saved));
|
|
294
|
+
return this.saveQueue;
|
|
295
|
+
}
|
|
296
|
+
reminderKey(row) {
|
|
297
|
+
return JSON.stringify([row.name, row.source, row.version, row.latestVersion ?? row.latestCommit]);
|
|
298
|
+
}
|
|
299
|
+
targetKey(row) {
|
|
300
|
+
const source = row.source;
|
|
301
|
+
return JSON.stringify(source.kind === "npm" ? [row.name, "npm", source.package, source.tag, row.latestVersion] : source.kind === "github" ? [row.name, "github", source.owner, source.repo, source.ref ?? "HEAD", row.latestCommit] : [row.name, "unsupported"]);
|
|
302
|
+
}
|
|
303
|
+
isIgnored(row) {
|
|
304
|
+
return this.ignored.has(this.targetKey(row));
|
|
305
|
+
}
|
|
306
|
+
reminders() {
|
|
307
|
+
return this.rows.filter((r) => r.status === "available" && !this.isIgnored(r) && (!Object.hasOwn(this.dismissed, r.name) || this.dismissed[r.name] !== this.reminderKey(r))).map((r) => ({ key: this.reminderKey(r), targetKey: this.targetKey(r), name: r.name, version: r.version, latestVersion: r.latestVersion, latestCommit: r.latestCommit }));
|
|
308
|
+
}
|
|
309
|
+
async ignore(keys, restore = false) {
|
|
310
|
+
if (this.flight) throw Error("\u5DF2\u6709\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210");
|
|
311
|
+
if (!Array.isArray(keys) || !keys.length || keys.length > 200 || keys.some((k) => typeof k !== "string") || new Set(keys).size !== keys.length) throw Error("\u65E0\u6548\u7684\u7248\u672C\u5217\u8868");
|
|
312
|
+
const rows = keys.map((key) => this.rows.find((row) => row.target && ["available", "failed"].includes(row.status) && this.targetKey(row) === key));
|
|
313
|
+
if (rows.some((row) => !row)) throw Error("\u76EE\u6807\u7248\u672C\u5DF2\u6539\u53D8\uFF0C\u8BF7\u5237\u65B0\u6E05\u5355\u540E\u91CD\u8BD5");
|
|
314
|
+
const previous = new Set(this.ignored), dismissed = { ...this.dismissed };
|
|
315
|
+
for (const row of rows) {
|
|
316
|
+
if (restore) {
|
|
317
|
+
this.ignored.delete(this.targetKey(row));
|
|
318
|
+
delete this.dismissed[row.name];
|
|
319
|
+
} else this.ignored.add(this.targetKey(row));
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
await this.persist();
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.ignored = previous;
|
|
325
|
+
this.dismissed = dismissed;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async dismiss(keys) {
|
|
330
|
+
if (!Array.isArray(keys) || keys.length > 200 || keys.some((k) => typeof k !== "string")) throw Error("\u65E0\u6548\u7684\u63D0\u9192\u5217\u8868");
|
|
331
|
+
for (const row of this.reminders()) if (keys.includes(row.key)) Object.defineProperty(this.dismissed, row.name, { value: row.key, enumerable: true, configurable: true, writable: true });
|
|
332
|
+
await this.persist();
|
|
333
|
+
}
|
|
334
|
+
async tick() {
|
|
335
|
+
if (this.flight || !this.state().available) return;
|
|
336
|
+
await this.list();
|
|
337
|
+
if (this.abort.signal.aborted) return;
|
|
338
|
+
if (this.flight || this.now() >= this.lastAttempt && this.now() - this.lastAttempt < 864e5) return;
|
|
339
|
+
this.lastAttempt = this.now();
|
|
340
|
+
await this.persist();
|
|
341
|
+
if (!this.flight && !this.abort.signal.aborted) this.check();
|
|
342
|
+
}
|
|
343
|
+
state() {
|
|
344
|
+
return {
|
|
345
|
+
available: !!this.service()?.listBundles && !!this.profile()?.dir,
|
|
346
|
+
checkedAt: this.checkedAt,
|
|
347
|
+
reminders: this.reminders(),
|
|
348
|
+
autoCheckHours: 24,
|
|
349
|
+
rows: this.rows.map(({ spec, ...row }) => ({ ...row, ignored: this.isIgnored(row), targetKey: this.targetKey(row), source: row.source.kind === "unsupported" ? { kind: "unsupported" } : row.source })),
|
|
350
|
+
job: this.job,
|
|
351
|
+
error: this.error
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async list() {
|
|
355
|
+
const service = this.service(), profile = this.profile();
|
|
356
|
+
if (!service?.listBundles || !profile?.dir) {
|
|
357
|
+
this.rows = [];
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const [manifest, bundles, lock] = await Promise.all([jsonFile(join(profile.dir, "package.json")), service.listBundles(), locks(profile.dir)]);
|
|
361
|
+
if (!manifest) throw Error("\u5F53\u524D DSH \u914D\u7F6E\u7684\u63D2\u4EF6\u6E05\u5355\u65E0\u6CD5\u8BFB\u53D6");
|
|
362
|
+
this.rows = bundles.filter((b) => (b.installed || b.version) && manifest.dependencies?.[b.name] && b.removable !== false).map((b) => {
|
|
363
|
+
const spec = manifest.dependencies?.[b.name];
|
|
364
|
+
const source = parsePluginSource(b.name, spec);
|
|
365
|
+
const npm = lock.npm?.packages?.[`node_modules/${b.name}`]?.resolved;
|
|
366
|
+
const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[b.name]?.version;
|
|
367
|
+
const installed = lock.preferNpm ? npm ?? pnpm : pnpm ?? npm;
|
|
368
|
+
const previous = this.rows.find((r) => r.name === b.name && r.spec === spec && r.version === b.version && r.commit === githubCommit(installed));
|
|
369
|
+
return {
|
|
370
|
+
...previous,
|
|
371
|
+
name: b.name,
|
|
372
|
+
version: b.version,
|
|
373
|
+
enabled: b.enabled,
|
|
374
|
+
spec,
|
|
375
|
+
source,
|
|
376
|
+
commit: githubCommit(installed),
|
|
377
|
+
status: previous?.status ?? (source.kind === "unsupported" ? "unsupported" : "unchecked"),
|
|
378
|
+
...source.kind === "unsupported" ? { message: "\u672C\u5730\u6587\u4EF6 / \u94FE\u63A5\u6765\u6E90\uFF0C\u8BF7\u901A\u8FC7\u539F\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0" } : {}
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async checkRow(row) {
|
|
383
|
+
try {
|
|
384
|
+
if (row.source.kind === "npm") {
|
|
385
|
+
const spec = `${row.source.package}@${row.source.tag}`;
|
|
386
|
+
const found = await this.lookup(this.service(), this.profile(), spec, this.abort.signal);
|
|
387
|
+
if (!found.version || found.problem && found.problem !== "already-installed") throw Error(found.problem === "not-found" ? "npm \u4E2D\u672A\u627E\u5230\u8BE5\u5305\u6216\u9891\u9053" : "npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7684\u7F51\u7EDC\u4E0E registry \u8BBE\u7F6E");
|
|
388
|
+
if (!parseVersion(found.version) || row.version && !parseVersion(row.version)) throw Error("\u65E0\u6CD5\u6BD4\u8F83\u7248\u672C\u53F7");
|
|
389
|
+
row.latestVersion = found.version;
|
|
390
|
+
row.target = `${row.name}@${row.source.alias ? `npm:${row.source.package}@` : ""}${row.source.channel ? row.source.tag : found.version}`;
|
|
391
|
+
row.status = !row.version || compareVersions(found.version, row.version) > 0 ? "available" : "current";
|
|
392
|
+
row.message = !row.version ? "\u5B89\u88C5\u6587\u4EF6\u4E0D\u53EF\u8BFB\uFF0C\u53EF\u91CD\u65B0\u5B89\u88C5\u8BE5\u6765\u6E90\u4FEE\u590D" : row.status === "current" ? "\u5F53\u524D\u7248\u672C\u5DF2\u662F\u8BE5\u9891\u9053\u6700\u65B0\uFF0C\u6216\u9AD8\u4E8E\u8BE5\u9891\u9053" : void 0;
|
|
393
|
+
} else if (row.source.kind === "github") {
|
|
394
|
+
const { owner, repo, ref } = row.source;
|
|
395
|
+
const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref ?? "HEAD")}`;
|
|
396
|
+
const found = await publicJson(url, this.fetchImpl, this.abort.signal);
|
|
397
|
+
if (!/^[a-f0-9]{40}$/i.test(found.sha)) throw Error("GitHub \u8FD4\u56DE\u65E0\u6548\u63D0\u4EA4\u4FE1\u606F");
|
|
398
|
+
row.latestCommit = found.sha.toLowerCase();
|
|
399
|
+
row.target = `${row.name}@${row.spec}`;
|
|
400
|
+
row.status = row.commit === row.latestCommit ? "current" : "available";
|
|
401
|
+
row.message = row.commit ? row.status === "available" ? `\u63D0\u4EA4 ${short(row.commit)} \u2192 ${short(row.latestCommit)}` : "\u8BE5\u5206\u652F / \u6807\u7B7E\u7684\u63D0\u4EA4\u5DF2\u540C\u6B65" : "\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730 Git \u63D0\u4EA4\uFF0C\u53EF\u91CD\u65B0\u5B89\u88C5\u5DF2\u68C0\u67E5\u7684\u6765\u6E90";
|
|
402
|
+
if (ref && /^[a-f0-9]{7,40}$/i.test(ref)) {
|
|
403
|
+
row.status = "pinned";
|
|
404
|
+
row.message = "\u5B89\u88C5\u6765\u6E90\u56FA\u5B9A\u5728 Git \u63D0\u4EA4\uFF1B\u4FDD\u7559\u6B64\u63D0\u4EA4\uFF0C\u4E0D\u81EA\u52A8\u5207\u6362\u5206\u652F";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
row.checkedAt = Date.now();
|
|
408
|
+
row.checkedEnabled = row.enabled;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
row.status = "failed";
|
|
411
|
+
row.message = error.message;
|
|
412
|
+
delete row.target;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
start(kind, task) {
|
|
416
|
+
if (this.flight) throw Error("\u5DF2\u6709\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210");
|
|
417
|
+
this.job = { id: randomUUID(), kind, state: "running", startedAt: Date.now(), results: [], total: 0 };
|
|
418
|
+
this.error = void 0;
|
|
419
|
+
this.flight = Promise.resolve().then(() => task(this.job)).then(() => {
|
|
420
|
+
this.job.state = this.job.cancelRequested ? "cancelled" : "done";
|
|
421
|
+
}, (e) => {
|
|
422
|
+
this.job.state = this.job.cancelRequested ? "cancelled" : "failed";
|
|
423
|
+
this.job.message = e.message;
|
|
424
|
+
}).finally(async () => {
|
|
425
|
+
this.job.finishedAt = Date.now();
|
|
426
|
+
try {
|
|
427
|
+
await this.persist();
|
|
428
|
+
} catch {
|
|
429
|
+
this.error = "\u66F4\u65B0\u68C0\u67E5\u8BB0\u5F55\u672A\u80FD\u4FDD\u5B58\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u76EE\u5F55\u6743\u9650";
|
|
430
|
+
}
|
|
431
|
+
this.flight = void 0;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
check() {
|
|
435
|
+
this.start("check", async (job) => {
|
|
436
|
+
this.lastAttempt = this.now();
|
|
437
|
+
await this.persist();
|
|
438
|
+
await this.list();
|
|
439
|
+
const rows = this.rows.filter((r) => ["npm", "github"].includes(r.source.kind));
|
|
440
|
+
job.total = rows.length;
|
|
441
|
+
let next = 0;
|
|
442
|
+
await Promise.all(Array.from({ length: Math.min(3, rows.length) }, async () => {
|
|
443
|
+
while (next < rows.length && !job.cancelRequested) {
|
|
444
|
+
const row = rows[next++];
|
|
445
|
+
await this.checkRow(row);
|
|
446
|
+
job.results.push({ name: row.name, status: row.status, message: row.message });
|
|
447
|
+
}
|
|
448
|
+
}));
|
|
449
|
+
this.checkedAt = Date.now();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async installRow(row, registry, allowFreshVersions = []) {
|
|
453
|
+
const original = await jsonFile(join(this.profile().dir, "package.json"));
|
|
454
|
+
if (original?.dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
455
|
+
const current = (await this.service().listBundles()).find((b) => b.name === row.name);
|
|
456
|
+
if (!current || current.removable === false || current.enabled !== row.checkedEnabled || current.version !== row.version) throw Error("\u63D2\u4EF6\u7248\u672C\u6216\u542F\u7528\u72B6\u6001\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
457
|
+
if (allowFreshVersions.includes(reviewedVersion(row))) await allowReviewedVersion(this.profile(), row);
|
|
458
|
+
const options = { enabled: row.enabled, requestId: this.job.id, ...registry ? { registry } : {} };
|
|
459
|
+
let result;
|
|
460
|
+
const refresh = async () => {
|
|
461
|
+
this.gitAbort = new AbortController();
|
|
462
|
+
try {
|
|
463
|
+
return await this.refreshPackage(this.service(), this.profile(), row, options, AbortSignal.any([this.abort.signal, this.gitAbort.signal]));
|
|
464
|
+
} finally {
|
|
465
|
+
this.gitAbort = void 0;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
if (row.source.kind === "github" || row.name === this.selfName) result = await refresh();
|
|
469
|
+
else {
|
|
470
|
+
result = await outsideActivation(this.hmr(), () => this.service().installBundle(row.target, options));
|
|
471
|
+
if (result.error?.code === "already-installed") result = await refresh();
|
|
472
|
+
}
|
|
473
|
+
if (installFailed(result)) return { name: row.name, ...installFailure(result), expectedVersion: row.latestVersion };
|
|
474
|
+
const installed = (await this.service().listBundles()).find((b) => b.name === row.name);
|
|
475
|
+
if (row.source.kind === "npm" && installed?.version !== row.latestVersion) return {
|
|
476
|
+
name: row.name,
|
|
477
|
+
status: "failed",
|
|
478
|
+
message: `\u5B9E\u9645\u5B89\u88C5\u7248\u672C ${installed?.version ?? "\u672A\u77E5"} \u4E0E\u68C0\u67E5\u7248\u672C ${row.latestVersion} \u4E0D\u540C\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u68C0\u67E5\uFF0C\u53EF\u80FD\u662F\u955C\u50CF\u5C1A\u672A\u540C\u6B65\u6216\u53D1\u884C\u65F6\u95F4\u9650\u5236`
|
|
479
|
+
};
|
|
480
|
+
if (row.source.kind === "github") {
|
|
481
|
+
const lock = await locks(this.profile().dir);
|
|
482
|
+
const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[row.name]?.version, npm = lock.npm?.packages?.[`node_modules/${row.name}`]?.resolved;
|
|
483
|
+
if (githubCommit(lock.preferNpm ? npm ?? pnpm : pnpm ?? npm) !== row.latestCommit) return {
|
|
484
|
+
name: row.name,
|
|
485
|
+
status: "failed",
|
|
486
|
+
restartRequired: true,
|
|
487
|
+
message: "\u5B9E\u9645 Git \u63D0\u4EA4\u4E0E\u68C0\u67E5\u7ED3\u679C\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\uFF1B\u4E0D\u628A\u4ECD\u505C\u7559\u5728\u65E7\u9501\u6587\u4EF6\u7684\u5B89\u88C5\u62A5\u544A\u4E3A\u6210\u529F"
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const reloadPending = result.application === "restart-required" && this.onInstalled?.(row.name) === true;
|
|
491
|
+
return { name: row.name, status: "installed", application: reloadPending ? "reload-pending" : result.application, message: reloadPending ? "\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u52A0\u8F7D\uFF1B\u8FD0\u884C\u7248\u672C\u89C1\u9762\u677F\u9876\u90E8" : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u65B0\u6253\u5F00 DSH \u540E\u751F\u6548" : "\u5B89\u88C5\u5B8C\u6210", restartRequired: !reloadPending && result.application === "restart-required" };
|
|
492
|
+
}
|
|
493
|
+
update(names, registry, allowFreshVersions = []) {
|
|
494
|
+
if (!Array.isArray(names) || !names.length || names.length > 200 || names.some((n) => typeof n !== "string") || new Set(names).size !== names.length) throw Error("\u8BF7\u9009\u62E9\u8981\u66F4\u65B0\u7684\u63D2\u4EF6");
|
|
495
|
+
const selected = names.map((name2) => this.rows.find((r) => r.name === name2));
|
|
496
|
+
if (selected.some((r) => !r || !["available", "failed"].includes(r.status) || !r.target || Date.now() - r.checkedAt > 6e5)) throw Error("\u8BF7\u5148\u68C0\u67E5\u66F4\u65B0\uFF0C\u518D\u9009\u62E9\u53EF\u66F4\u65B0\u7684\u63D2\u4EF6\uFF08\u68C0\u67E5\u7ED3\u679C\u6709\u6548\u671F 10 \u5206\u949F\uFF09");
|
|
497
|
+
const reviewed = selected.map(reviewedVersion).filter(Boolean);
|
|
498
|
+
if (!Array.isArray(allowFreshVersions) || allowFreshVersions.length > selected.length || new Set(allowFreshVersions).size !== allowFreshVersions.length || allowFreshVersions.some((v) => typeof v !== "string" || !reviewed.includes(v))) throw Error("\u786E\u8BA4\u7684\u63D2\u4EF6\u7248\u672C\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u770B\u5E76\u786E\u8BA4\u66F4\u65B0");
|
|
499
|
+
selected.sort((a, b) => Number(a.name === this.selfName) - Number(b.name === this.selfName));
|
|
500
|
+
this.start("update", async (job) => {
|
|
501
|
+
job.total = selected.length;
|
|
502
|
+
for (const row of selected) {
|
|
503
|
+
if (job.cancelRequested) break;
|
|
504
|
+
job.current = row.name;
|
|
505
|
+
let result;
|
|
506
|
+
try {
|
|
507
|
+
result = await this.installRow(row, registry, allowFreshVersions);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
result = { name: row.name, status: "failed", message: e.message };
|
|
510
|
+
}
|
|
511
|
+
job.results.push(result);
|
|
512
|
+
row.status = result.status;
|
|
513
|
+
row.message = result.message;
|
|
514
|
+
}
|
|
515
|
+
delete job.current;
|
|
516
|
+
await this.list();
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async action(input) {
|
|
520
|
+
if (!this.state().available) throw Error("\u5F53\u524D DSH \u672A\u63D0\u4F9B\u63D2\u4EF6\u7BA1\u7406\u63A5\u53E3\uFF0C\u8BF7\u5347\u7EA7 DSH \u540E\u91CD\u8BD5");
|
|
521
|
+
if (input.registry && !["https://registry.npmjs.org/", "https://registry.npmmirror.com/"].includes(input.registry)) throw Error("\u65E0\u6548\u7684 npm \u6765\u6E90");
|
|
522
|
+
if (input.action === "plugins.list") {
|
|
523
|
+
if (!this.flight) await this.list();
|
|
524
|
+
} else if (input.action === "plugins.dismiss") await this.dismiss(input.keys);
|
|
525
|
+
else if (input.action === "plugins.ignore") await this.ignore(input.keys);
|
|
526
|
+
else if (input.action === "plugins.unignore") await this.ignore(input.keys, true);
|
|
527
|
+
else if (input.action === "plugins.check") this.check();
|
|
528
|
+
else if (input.action === "plugins.update") this.update(input.names, input.registry, input.allowFreshVersions);
|
|
529
|
+
else if (input.action === "plugins.cancel") {
|
|
530
|
+
if (this.job?.state === "running") {
|
|
531
|
+
this.job.cancelRequested = true;
|
|
532
|
+
if (this.gitAbort) this.gitAbort.abort();
|
|
533
|
+
else if (this.job.kind !== "check") await this.service().cancelInstall?.(this.job.id);
|
|
534
|
+
}
|
|
535
|
+
} else throw Error("\u672A\u77E5\u63D2\u4EF6\u64CD\u4F5C");
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/http.mjs
|
|
540
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
541
|
+
var ENDPOINT = "/.well-known/dsh-plugin-updates";
|
|
542
|
+
var HttpError = class extends Error {
|
|
543
|
+
constructor(status, message) {
|
|
544
|
+
super(message);
|
|
545
|
+
this.status = status;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var loopback = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
549
|
+
function localRequest(req) {
|
|
550
|
+
try {
|
|
551
|
+
const host = req.headers.host;
|
|
552
|
+
const url = new URL(`http://${host}`);
|
|
553
|
+
if (url.host !== host || !loopback.has(url.hostname)) return false;
|
|
554
|
+
const remote = req.socket?.remoteAddress;
|
|
555
|
+
return (!remote || ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) && (!req.headers.origin || [`http://${host}`, `https://${host}`].includes(req.headers.origin)) && req.headers["sec-fetch-site"] !== "cross-site";
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function updatesHandler(updater, pluginVersion) {
|
|
561
|
+
const key = randomBytes(32).toString("base64url");
|
|
562
|
+
const state = () => ({ pluginVersion, pluginUpdates: updater.state(), key });
|
|
563
|
+
return async (req, res) => {
|
|
564
|
+
const json = (status, value) => {
|
|
565
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
566
|
+
res.end(JSON.stringify(value));
|
|
567
|
+
};
|
|
568
|
+
try {
|
|
569
|
+
if (!localRequest(req)) throw new HttpError(403, "\u4EC5\u5141\u8BB8\u672C\u673A\u540C\u6E90\u8BF7\u6C42");
|
|
570
|
+
if (req.method === "GET") {
|
|
571
|
+
json(200, state());
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (req.method !== "POST") throw new HttpError(405, "\u65B9\u6CD5\u65E0\u6548");
|
|
575
|
+
const actual = Buffer.from(req.headers["x-plugin-updates-key"] ?? ""), expected = Buffer.from(key);
|
|
576
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new HttpError(401, "\u8BF7\u5237\u65B0\u66F4\u65B0\u7BA1\u7406\u9875\u9762");
|
|
577
|
+
if (!/^application\/json(?:\s*;|$)/i.test(req.headers["content-type"] ?? "")) throw new HttpError(415, "\u9700\u8981 JSON");
|
|
578
|
+
let size = 0;
|
|
579
|
+
const chunks = [];
|
|
580
|
+
for await (const chunk of req) {
|
|
581
|
+
size += chunk.length;
|
|
582
|
+
if (size > 65536) throw new HttpError(413, "\u8BF7\u6C42\u8FC7\u5927");
|
|
583
|
+
chunks.push(Buffer.from(chunk));
|
|
584
|
+
}
|
|
585
|
+
let input;
|
|
586
|
+
try {
|
|
587
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
588
|
+
} catch {
|
|
589
|
+
throw new HttpError(400, "JSON \u65E0\u6548");
|
|
590
|
+
}
|
|
591
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new HttpError(400, "\u64CD\u4F5C\u65E0\u6548");
|
|
592
|
+
try {
|
|
593
|
+
await updater.action(input);
|
|
594
|
+
} catch {
|
|
595
|
+
throw new HttpError(400, "\u64CD\u4F5C\u672A\u6267\u884C\uFF0C\u8BF7\u5237\u65B0\u6E05\u5355\u5E76\u91CD\u65B0\u68C0\u67E5\uFF1B\u4EC5\u652F\u6301\u5DF2\u5B89\u88C5\u63D2\u4EF6\u7684\u68C0\u67E5\u3001\u66F4\u65B0\u3001\u63D0\u9192\u548C\u53D6\u6D88\u64CD\u4F5C");
|
|
596
|
+
}
|
|
597
|
+
json(200, state());
|
|
598
|
+
} catch (error) {
|
|
599
|
+
json(error instanceof HttpError ? error.status : 500, { error: error instanceof HttpError ? error.message : "\u66F4\u65B0\u7BA1\u7406\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5 DSH \u63D2\u4EF6\u9875" });
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// package.json
|
|
605
|
+
var version = "0.1.1";
|
|
606
|
+
|
|
607
|
+
// src/index.mjs
|
|
608
|
+
var name = "@androidsup/dsh-plugin-updates";
|
|
609
|
+
var inject = ["pluginManager", "profileContext"];
|
|
610
|
+
async function apply(ctx) {
|
|
611
|
+
const updater = new PluginUpdates({
|
|
612
|
+
service: () => ctx.pluginManager,
|
|
613
|
+
profile: () => ctx.profileContext,
|
|
614
|
+
hmr: () => ctx.get("hmr"),
|
|
615
|
+
stateFile: join2(ctx.profileContext.dir, ".dsh-plugin-updates", "state.json")
|
|
616
|
+
});
|
|
617
|
+
let closed = false, timer;
|
|
618
|
+
ctx.effect(() => () => {
|
|
619
|
+
closed = true;
|
|
620
|
+
clearInterval(timer);
|
|
621
|
+
updater.abort.abort();
|
|
622
|
+
return Promise.allSettled([updater.flight, updater.saveQueue]);
|
|
623
|
+
});
|
|
624
|
+
await updater.load();
|
|
625
|
+
if (closed) return;
|
|
626
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
627
|
+
webCtx.effect(() => webCtx.webServer.register({ kind: "exact", path: ENDPOINT, handler: updatesHandler(updater, version) }));
|
|
628
|
+
});
|
|
629
|
+
const tick = () => {
|
|
630
|
+
void updater.tick().catch(() => ctx.logger.warn("plugin-updates: automatic check unavailable"));
|
|
631
|
+
};
|
|
632
|
+
tick();
|
|
633
|
+
timer = setInterval(tick, 6e4);
|
|
634
|
+
timer.unref();
|
|
635
|
+
}
|
|
636
|
+
export {
|
|
637
|
+
apply,
|
|
638
|
+
inject,
|
|
639
|
+
name
|
|
640
|
+
};
|
|
641
|
+
//# sourceMappingURL=index.js.map
|