dbx-plugin-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/bin/dbx-plugin-skill.mjs +330 -0
- package/lib/installer.mjs +256 -0
- package/package.json +49 -0
- package/skill/SKILL.md +268 -0
- package/skill/references/cli.md +225 -0
- package/skill/references/contributions.md +229 -0
- package/skill/references/debugging.md +210 -0
- package/skill/references/host-api.md +195 -0
- package/skill/references/manifest.md +229 -0
- package/skill/references/packaging.md +183 -0
- package/skill/references/publishing.md +374 -0
- package/skill/references/sidecar-protocol.md +242 -0
- package/skill/references/troubleshooting.md +171 -0
- package/skill/scripts/check-project.mjs +915 -0
- package/skill/scripts/dev-logs.mjs +191 -0
- package/skill/scripts/inspect-dbxp.mjs +400 -0
- package/skill/scripts/make-candidate.mjs +413 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* make-candidate.mjs — 从 dist/*.artifact.json 生成上架所需的两份 JSON(零依赖,Node.js 18+)
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node make-candidate.mjs [项目目录] [--dist DIR] [--out DIR] [--repo owner/name] [--tag TAG]
|
|
7
|
+
* [--release-notes TEXT] [--json] [--quiet] [--no-verify]
|
|
8
|
+
*
|
|
9
|
+
* 产出:
|
|
10
|
+
* <out>/candidates/<plugin-id>.json 提交到 t8y2/dbx-store 的候选(targets[].url 为 HTTPS)
|
|
11
|
+
* <out>/release-candidates.json 插件 Release 的资产(artifacts[].url 为纯文件名)
|
|
12
|
+
*
|
|
13
|
+
* 关键规则(来自 dbx-store 脚本,写错必然被拒):
|
|
14
|
+
* - release-candidates.json 的 artifacts[].url 必须是**纯 .dbxp 文件名**,不是 URL
|
|
15
|
+
* - 候选的 targets[].url 必须是 **HTTPS**
|
|
16
|
+
* - 候选不能含 signingKeyId / verified
|
|
17
|
+
* - sha256 / size 必须与未签名包的**确切字节**一致
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync, existsSync, statSync, readdirSync, mkdirSync } from "node:fs";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { join, resolve, basename } from "node:path";
|
|
24
|
+
|
|
25
|
+
const ALLOWED_STORE_FIELDS = [
|
|
26
|
+
"name",
|
|
27
|
+
"description",
|
|
28
|
+
"icon",
|
|
29
|
+
"tags",
|
|
30
|
+
"permissions",
|
|
31
|
+
"source",
|
|
32
|
+
"homepage",
|
|
33
|
+
"license",
|
|
34
|
+
"releaseNotes",
|
|
35
|
+
"localizations",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const CANDIDATE_FIELDS = [
|
|
39
|
+
"schemaVersion",
|
|
40
|
+
"id",
|
|
41
|
+
"publisher",
|
|
42
|
+
"version",
|
|
43
|
+
"name",
|
|
44
|
+
"description",
|
|
45
|
+
"icon",
|
|
46
|
+
"tags",
|
|
47
|
+
"permissions",
|
|
48
|
+
"source",
|
|
49
|
+
"homepage",
|
|
50
|
+
"license",
|
|
51
|
+
"releaseNotes",
|
|
52
|
+
"localizations",
|
|
53
|
+
"targets",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const IDENTIFIER = /^[a-z0-9][a-z0-9._-]*$/;
|
|
57
|
+
const TARGET = /^[a-z0-9-]{1,64}$/;
|
|
58
|
+
const SHA256 = /^[a-f0-9]{64}$/i;
|
|
59
|
+
const SEMVER = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
60
|
+
const MAX_SIZE = 512 * 1024 * 1024;
|
|
61
|
+
|
|
62
|
+
const errors = [];
|
|
63
|
+
const warnings = [];
|
|
64
|
+
let jsonMode = false;
|
|
65
|
+
let quiet = false;
|
|
66
|
+
|
|
67
|
+
const error = (m, hint) => errors.push(hint ? `${m}\n → ${hint}` : m);
|
|
68
|
+
const warn = (m, hint) => warnings.push(hint ? `${m}\n → ${hint}` : m);
|
|
69
|
+
function note(message) {
|
|
70
|
+
if (!jsonMode && !quiet) process.stdout.write(`${message}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sha256File = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
74
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
75
|
+
|
|
76
|
+
function parseArgs(args) {
|
|
77
|
+
const options = {
|
|
78
|
+
project: ".",
|
|
79
|
+
dist: null,
|
|
80
|
+
out: null,
|
|
81
|
+
repo: null,
|
|
82
|
+
tag: null,
|
|
83
|
+
releaseNotes: null,
|
|
84
|
+
verify: true,
|
|
85
|
+
};
|
|
86
|
+
const positional = [];
|
|
87
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
88
|
+
const arg = args[i];
|
|
89
|
+
const take = () => {
|
|
90
|
+
i += 1;
|
|
91
|
+
const value = args[i];
|
|
92
|
+
if (value === undefined) {
|
|
93
|
+
process.stderr.write(`${arg} 需要一个值\n`);
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
98
|
+
if (arg === "--dist") options.dist = take();
|
|
99
|
+
else if (arg === "--out") options.out = take();
|
|
100
|
+
else if (arg === "--repo") options.repo = take();
|
|
101
|
+
else if (arg === "--tag") options.tag = take();
|
|
102
|
+
else if (arg === "--release-notes") options.releaseNotes = take();
|
|
103
|
+
else if (arg === "--json") jsonMode = true;
|
|
104
|
+
else if (arg === "--quiet") quiet = true;
|
|
105
|
+
else if (arg === "--no-verify") options.verify = false;
|
|
106
|
+
else if (arg === "-h" || arg === "--help") {
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
"用法: node make-candidate.mjs [项目目录] [--dist DIR] [--out DIR] [--repo owner/name] [--tag TAG]\n" +
|
|
109
|
+
" [--release-notes TEXT] [--json] [--quiet] [--no-verify]\n\n" +
|
|
110
|
+
"从 dist/*.artifact.json 生成 candidates/<id>.json 与 release-candidates.json,\n" +
|
|
111
|
+
"并复核每个 .dbxp 的 sha256/size 与 artifact.json 是否一致。\n",
|
|
112
|
+
);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} else if (arg.startsWith("-")) {
|
|
115
|
+
process.stderr.write(`未知选项: ${arg}\n`);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
} else positional.push(arg);
|
|
118
|
+
}
|
|
119
|
+
if (positional.length) options.project = positional[0];
|
|
120
|
+
return options;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function detectFromGit(project) {
|
|
124
|
+
const run = (args) => {
|
|
125
|
+
try {
|
|
126
|
+
return execFileSync("git", args, { cwd: project, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
let repo = null;
|
|
132
|
+
const remote = run(["remote", "get-url", "origin"]);
|
|
133
|
+
if (remote) {
|
|
134
|
+
const https = /^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(remote);
|
|
135
|
+
const ssh = /^[^@]+@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(remote);
|
|
136
|
+
const match = https ?? ssh;
|
|
137
|
+
if (match) repo = `${match[1]}/${match[2]}`;
|
|
138
|
+
}
|
|
139
|
+
const tag = run(["describe", "--tags", "--exact-match"]) ?? run(["tag", "--points-at", "HEAD"])?.split("\n")[0] ?? null;
|
|
140
|
+
return { repo, tag: tag || null };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function main() {
|
|
144
|
+
const options = parseArgs(process.argv.slice(2));
|
|
145
|
+
const project = resolve(options.project);
|
|
146
|
+
if (!existsSync(project)) {
|
|
147
|
+
process.stderr.write(`项目目录不存在: ${project}\n`);
|
|
148
|
+
process.exit(2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const manifestPath = join(project, "manifest.json");
|
|
152
|
+
if (!existsSync(manifestPath)) {
|
|
153
|
+
process.stderr.write(`缺少 manifest.json: ${manifestPath}\n`);
|
|
154
|
+
process.exit(2);
|
|
155
|
+
}
|
|
156
|
+
let manifest;
|
|
157
|
+
try {
|
|
158
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
process.stderr.write(`manifest.json 解析失败: ${err.message}\n`);
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const id = manifest.id;
|
|
165
|
+
const version = manifest.version;
|
|
166
|
+
const publisher = manifest.publisher;
|
|
167
|
+
if (typeof id !== "string" || !IDENTIFIER.test(id)) error(`manifest.id 非法: ${JSON.stringify(id)}`);
|
|
168
|
+
if (typeof version !== "string" || !SEMVER.test(version)) error(`manifest.version 非法: ${JSON.stringify(version)}`);
|
|
169
|
+
if (typeof publisher !== "string" || !IDENTIFIER.test(publisher)) {
|
|
170
|
+
error(`manifest.publisher 非法(商店要求小写标识符): ${JSON.stringify(publisher)}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const distDir = resolve(options.dist ? options.dist : join(project, "dist"));
|
|
174
|
+
const outDir = resolve(options.out ? options.out : distDir);
|
|
175
|
+
|
|
176
|
+
if (!existsSync(distDir)) {
|
|
177
|
+
process.stderr.write(`未找到构建输出目录: ${distDir}\n请先运行: dbx-plugin package .\n`);
|
|
178
|
+
process.exit(2);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// .dbx-store.json(可选,展示字段)
|
|
182
|
+
const storePath = join(project, ".dbx-store.json");
|
|
183
|
+
let storeFields = {};
|
|
184
|
+
if (existsSync(storePath)) {
|
|
185
|
+
try {
|
|
186
|
+
const raw = JSON.parse(readFileSync(storePath, "utf8"));
|
|
187
|
+
if (!isPlainObject(raw)) {
|
|
188
|
+
error(".dbx-store.json 顶层必须是对象");
|
|
189
|
+
} else {
|
|
190
|
+
const unknown = Object.keys(raw).filter((k) => !ALLOWED_STORE_FIELDS.includes(k));
|
|
191
|
+
if (unknown.length) error(`.dbx-store.json 含白名单外字段: ${unknown.join(", ")}(商店会报 Unsupported store metadata field)`);
|
|
192
|
+
storeFields = Object.fromEntries(Object.entries(raw).filter(([k]) => ALLOWED_STORE_FIELDS.includes(k)));
|
|
193
|
+
for (const [key, value] of Object.entries(storeFields)) {
|
|
194
|
+
if (typeof value === "string" && value === "") {
|
|
195
|
+
error(`.dbx-store.json 的 "${key}" 是空字符串(商店会报 must be a non-empty string when provided)`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
error(`.dbx-store.json 解析失败: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
warn(`未找到 .dbx-store.json(首次上架需要 name 与 license)`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 收集 artifact.json
|
|
207
|
+
const artifactFiles = readdirSync(distDir)
|
|
208
|
+
.filter((name) => name.endsWith(".artifact.json"))
|
|
209
|
+
.sort();
|
|
210
|
+
if (artifactFiles.length === 0) {
|
|
211
|
+
process.stderr.write(`未在 ${distDir} 找到 *.artifact.json\n请先运行: dbx-plugin package .\n`);
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const git = detectFromGit(project);
|
|
216
|
+
const repo = options.repo ?? git.repo;
|
|
217
|
+
const tag = options.tag ?? git.tag;
|
|
218
|
+
if (!repo) warn("未指定 --repo 且无法从 git remote 推断;候选 targets[].url 可能无法生成 HTTPS 地址");
|
|
219
|
+
if (!tag) warn("未指定 --tag 且无法从 git tag 推断;候选 targets[].url 可能无法生成 HTTPS 地址");
|
|
220
|
+
|
|
221
|
+
const targets = [];
|
|
222
|
+
const releaseArtifacts = [];
|
|
223
|
+
const seenTargets = new Set();
|
|
224
|
+
|
|
225
|
+
for (const artifactFile of artifactFiles) {
|
|
226
|
+
const artifactPath = join(distDir, artifactFile);
|
|
227
|
+
let artifact;
|
|
228
|
+
try {
|
|
229
|
+
artifact = JSON.parse(readFileSync(artifactPath, "utf8"));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
error(`${artifactFile} 解析失败: ${err.message}`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const at = artifactFile;
|
|
235
|
+
if (!isPlainObject(artifact)) {
|
|
236
|
+
error(`${at} 顶层必须是对象`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (artifact.signingKeyId !== undefined) {
|
|
240
|
+
error(`${at} 含 signingKeyId —— 候选包必须未签名,不能携带 signingKeyId`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const target = artifact.target;
|
|
244
|
+
if (typeof target !== "string" || !TARGET.test(target)) {
|
|
245
|
+
error(`${at} 的 target 非法: ${JSON.stringify(target)}(只允许 [a-z0-9-],长度 ≤64)`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (seenTargets.has(target)) error(`${at} 的 target 重复: ${target}`);
|
|
249
|
+
seenTargets.add(target);
|
|
250
|
+
|
|
251
|
+
if (typeof artifact.sha256 !== "string" || !SHA256.test(artifact.sha256)) {
|
|
252
|
+
error(`${at} 的 sha256 非法: ${JSON.stringify(artifact.sha256)}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!Number.isSafeInteger(artifact.size) || artifact.size < 1 || artifact.size > MAX_SIZE) {
|
|
256
|
+
error(`${at} 的 size 非法: ${JSON.stringify(artifact.size)}(要求 1 … 512 MiB 的安全整数)`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const expectedName = `${id}-${version}-${target}.dbxp`;
|
|
261
|
+
const packagePath = join(distDir, expectedName);
|
|
262
|
+
if (!existsSync(packagePath)) {
|
|
263
|
+
error(`缺少包文件 ${expectedName}(${at} 指向该 target)`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (options.verify) {
|
|
268
|
+
const actualHash = sha256File(packagePath);
|
|
269
|
+
const actualSize = statSync(packagePath).size;
|
|
270
|
+
if (actualHash !== artifact.sha256.toLowerCase()) {
|
|
271
|
+
error(`${expectedName} 的 SHA-256 与 ${at} 不一致:artifact=${artifact.sha256} 实际=${actualHash}`);
|
|
272
|
+
}
|
|
273
|
+
if (actualSize !== artifact.size) {
|
|
274
|
+
error(`${expectedName} 的 size 与 ${at} 不一致:artifact=${artifact.size} 实际=${actualSize}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 候选 URL 必须是 HTTPS
|
|
279
|
+
let candidateUrl = null;
|
|
280
|
+
const rawUrl = typeof artifact.url === "string" ? artifact.url : expectedName;
|
|
281
|
+
if (/^https:\/\//i.test(rawUrl)) candidateUrl = rawUrl;
|
|
282
|
+
else if (/^http:\/\//i.test(rawUrl)) error(`${at} 的 url 是 http,商店要求 HTTPS`);
|
|
283
|
+
else if (repo && tag) candidateUrl = `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${rawUrl}`;
|
|
284
|
+
else candidateUrl = rawUrl;
|
|
285
|
+
|
|
286
|
+
targets.push({
|
|
287
|
+
target,
|
|
288
|
+
url: candidateUrl,
|
|
289
|
+
sha256: artifact.sha256.toLowerCase(),
|
|
290
|
+
size: artifact.size,
|
|
291
|
+
});
|
|
292
|
+
releaseArtifacts.push({
|
|
293
|
+
target,
|
|
294
|
+
url: rawUrl.startsWith("http") ? basename(new URL(rawUrl).pathname) : rawUrl,
|
|
295
|
+
sha256: artifact.sha256.toLowerCase(),
|
|
296
|
+
size: artifact.size,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (targets.length === 0) {
|
|
301
|
+
error("没有收集到任何有效 target");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 组装候选
|
|
305
|
+
const candidate = {
|
|
306
|
+
schemaVersion: 1,
|
|
307
|
+
id,
|
|
308
|
+
publisher,
|
|
309
|
+
version,
|
|
310
|
+
...storeFields,
|
|
311
|
+
targets,
|
|
312
|
+
};
|
|
313
|
+
if (options.releaseNotes !== null) candidate.releaseNotes = options.releaseNotes;
|
|
314
|
+
else if (candidate.releaseNotes === undefined && storeFields.releaseNotes === undefined) {
|
|
315
|
+
warn("候选没有 releaseNotes(商店目录的版本说明来自这里,不是 GitHub Release 正文)");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const unknownCandidate = Object.keys(candidate).filter((k) => !CANDIDATE_FIELDS.includes(k));
|
|
319
|
+
if (unknownCandidate.length) error(`候选含未知字段: ${unknownCandidate.join(", ")}`);
|
|
320
|
+
if (candidate.name === undefined) warn("候选缺少 name —— 首次上架时商店要求非空 name");
|
|
321
|
+
if (candidate.license === undefined) warn("候选缺少 license —— 首次上架时商店要求非空 license");
|
|
322
|
+
if (candidate.source === undefined) warn("候选缺少 source —— 首次上架时商店要求 https:// 的 source");
|
|
323
|
+
if (candidate.source !== undefined && !/^https:\/\//.test(String(candidate.source))) {
|
|
324
|
+
error(`候选 source 必须是 https://(当前 ${JSON.stringify(candidate.source)})`);
|
|
325
|
+
}
|
|
326
|
+
for (const key of ["description", "icon", "homepage", "name", "license"]) {
|
|
327
|
+
if (candidate[key] !== undefined && (typeof candidate[key] !== "string" || candidate[key] === "")) {
|
|
328
|
+
error(`候选的 "${key}" 存在但为空(商店会拒绝)`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (candidate.homepage !== undefined && !/^https?:\/\//.test(String(candidate.homepage))) {
|
|
332
|
+
error(`候选 homepage 必须是 http(s)://(当前 ${JSON.stringify(candidate.homepage)})`);
|
|
333
|
+
}
|
|
334
|
+
// .dbx-store.json 允许相对 icon;同步脚本会把它改写成该 tag 下的 raw.githubusercontent URL。
|
|
335
|
+
// 手工提交的候选则必须是 http(s),所以这里做同样的改写。
|
|
336
|
+
if (candidate.icon !== undefined && !/^https?:\/\//.test(String(candidate.icon))) {
|
|
337
|
+
const iconPath = String(candidate.icon).replace(/^\.\//, "");
|
|
338
|
+
if (repo && tag) {
|
|
339
|
+
candidate.icon = `https://raw.githubusercontent.com/${repo}/${encodeURIComponent(tag)}/${iconPath}`;
|
|
340
|
+
note(`已将相对 icon 改写为: ${candidate.icon}`);
|
|
341
|
+
} else {
|
|
342
|
+
error(
|
|
343
|
+
`候选 icon 必须是 http(s)://(当前 ${JSON.stringify(candidate.icon)})`,
|
|
344
|
+
"相对路径只对 .dbx-store.json 的自动同步路径有效;请提供 --repo/--tag 以便改写,或直接写绝对 HTTPS 地址。",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (candidate.icon !== undefined && !/\.(svg|png)$/i.test(String(candidate.icon))) {
|
|
349
|
+
error(`候选 icon 必须以 .svg 或 .png 结尾(商店只接受 SVG/PNG,当前 ${JSON.stringify(candidate.icon)})`);
|
|
350
|
+
}
|
|
351
|
+
if (candidate.targets.some((t) => !/^https:\/\//.test(t.url))) {
|
|
352
|
+
error("候选存在非 HTTPS 的 target url(商店要求 HTTPS,且不能指向 t8y2/dbx-store 的 Release)");
|
|
353
|
+
}
|
|
354
|
+
if (candidate.targets.some((t) => t.url.startsWith("https://github.com/t8y2/dbx-store/releases/"))) {
|
|
355
|
+
error("候选 URL 指向了 DBX Store 的 Release —— 必须提交自己仓库的未签名包");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const releaseCandidates = {
|
|
359
|
+
plugin: {
|
|
360
|
+
id,
|
|
361
|
+
publisher,
|
|
362
|
+
version,
|
|
363
|
+
...(candidate.name !== undefined ? { name: candidate.name } : {}),
|
|
364
|
+
...(candidate.description !== undefined ? { description: candidate.description } : {}),
|
|
365
|
+
},
|
|
366
|
+
artifacts: releaseArtifacts,
|
|
367
|
+
};
|
|
368
|
+
for (const artifact of releaseCandidates.artifacts) {
|
|
369
|
+
if (!/^[A-Za-z0-9._-]+\.dbxp$/.test(artifact.url)) {
|
|
370
|
+
error(
|
|
371
|
+
`release-candidates 的 artifacts[].url 必须是纯 .dbxp 文件名(当前 ${JSON.stringify(artifact.url)})` +
|
|
372
|
+
" —— 商店脚本用 basename + 正则校验后自行拼接 GitHub Release URL",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (errors.length > 0) {
|
|
378
|
+
if (jsonMode) {
|
|
379
|
+
process.stdout.write(`${JSON.stringify({ ok: false, errors, warnings }, null, 2)}\n`);
|
|
380
|
+
} else {
|
|
381
|
+
for (const message of errors) process.stderr.write(`[ERROR] ${message}\n`);
|
|
382
|
+
for (const message of warnings) process.stderr.write(`[WARN ] ${message}\n`);
|
|
383
|
+
process.stderr.write(`\n生成中止:${errors.length} 个错误。\n`);
|
|
384
|
+
}
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const candidatesDir = join(outDir, "candidates");
|
|
389
|
+
mkdirSync(candidatesDir, { recursive: true });
|
|
390
|
+
const candidatePath = join(candidatesDir, `${id}.json`);
|
|
391
|
+
const releasePath = join(outDir, "release-candidates.json");
|
|
392
|
+
writeFileSync(candidatePath, `${JSON.stringify(candidate, null, 2)}\n`);
|
|
393
|
+
writeFileSync(releasePath, `${JSON.stringify(releaseCandidates, null, 2)}\n`);
|
|
394
|
+
|
|
395
|
+
if (jsonMode) {
|
|
396
|
+
process.stdout.write(
|
|
397
|
+
`${JSON.stringify({ ok: true, candidatePath, releasePath, candidate, releaseCandidates, warnings }, null, 2)}\n`,
|
|
398
|
+
);
|
|
399
|
+
} else if (!quiet) {
|
|
400
|
+
for (const message of warnings) process.stderr.write(`[WARN ] ${message}\n`);
|
|
401
|
+
note(`\n已生成:`);
|
|
402
|
+
note(` 候选(提交到 t8y2/dbx-store): ${candidatePath}`);
|
|
403
|
+
note(` Release 资产(上传到 GitHub Release): ${releasePath}`);
|
|
404
|
+
note(` targets: ${targets.map((t) => t.target).join(", ")}`);
|
|
405
|
+
note(`\n下一步:`);
|
|
406
|
+
note(` 1) 把 ${basename(releasePath)} 与各 .dbxp / .artifact.json 上传到同一个 GitHub Release`);
|
|
407
|
+
note(` 2) 把 candidates/${id}.json 放到 dbx-store 的 PR 中(首次还需 publishers/${publisher}.json,且需先合并进 main)`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
process.exit(0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
main();
|