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.
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dev-logs.mjs — 读取 dbx-plugin dev 的脱敏诊断日志(零依赖,Node.js 18+)
4
+ *
5
+ * 用法:
6
+ * node dev-logs.mjs [--port 5190] [--url URL] [--level debug|info|error]
7
+ * [--limit 100] [--follow] [--interval 1000]
8
+ * [--json] [--details] [--no-details] [--quiet]
9
+ *
10
+ * 对应接口: GET http://127.0.0.1:<port>/api/diagnostics
11
+ * 参数: after(排他游标)、limit(1-500)、level(精确匹配)、instanceId
12
+ *
13
+ * 条目形状: { id, time, level, category, message, details }
14
+ * 响应形状: { instanceId, entries, nextAfter, hasMore, reset, truncated, oldestId, latestId, plugin, backendState, port }
15
+ *
16
+ * 轮询语义:带上一页的 nextAfter 作为 after、并带上 instanceId;reset 表示实例/游标重置;
17
+ * truncated 表示旧日志已被 500 条环形缓冲覆盖。改变筛选条件需从 after=0 重新开始。
18
+ */
19
+
20
+ const LEVELS = ["debug", "info", "error"];
21
+
22
+ function parseArgs(args) {
23
+ const options = {
24
+ port: 5190,
25
+ url: null,
26
+ level: null,
27
+ limit: 100,
28
+ follow: false,
29
+ interval: 1000,
30
+ json: false,
31
+ details: true,
32
+ quiet: false,
33
+ };
34
+ for (let i = 0; i < args.length; i += 1) {
35
+ const arg = args[i];
36
+ const take = () => {
37
+ i += 1;
38
+ const value = args[i];
39
+ if (value === undefined) {
40
+ process.stderr.write(`${arg} 需要一个值\n`);
41
+ process.exit(2);
42
+ }
43
+ return value;
44
+ };
45
+ if (arg === "--port") options.port = Number(take());
46
+ else if (arg === "--url") options.url = take();
47
+ else if (arg === "--level") options.level = take();
48
+ else if (arg === "--limit") options.limit = Number(take());
49
+ else if (arg === "--interval") options.interval = Number(take());
50
+ else if (arg === "--follow" || arg === "-f") options.follow = true;
51
+ else if (arg === "--json") options.json = true;
52
+ else if (arg === "--details") options.details = true;
53
+ else if (arg === "--no-details") options.details = false;
54
+ else if (arg === "--quiet") options.quiet = true;
55
+ else if (arg === "-h" || arg === "--help") {
56
+ process.stdout.write(
57
+ "用法: node dev-logs.mjs [--port 5190] [--url URL] [--level debug|info|error]\n" +
58
+ " [--limit 100] [--follow] [--interval 1000]\n" +
59
+ " [--json] [--no-details] [--quiet]\n\n" +
60
+ "读取 dbx-plugin dev 的脱敏诊断日志。--follow 会持续轮询并按游标增量输出。\n",
61
+ );
62
+ process.exit(0);
63
+ } else {
64
+ process.stderr.write(`未知参数: ${arg}\n`);
65
+ process.exit(2);
66
+ }
67
+ }
68
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535) {
69
+ process.stderr.write("--port 必须是 0-65535 的整数\n");
70
+ process.exit(2);
71
+ }
72
+ if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 500) {
73
+ process.stderr.write("--limit 必须在 1-500 之间\n");
74
+ process.exit(2);
75
+ }
76
+ if (options.level !== null && !LEVELS.includes(options.level)) {
77
+ process.stderr.write(`--level 必须是 ${LEVELS.join(" / ")} 之一\n`);
78
+ process.exit(2);
79
+ }
80
+ if (!Number.isInteger(options.interval) || options.interval < 100) {
81
+ process.stderr.write("--interval 必须是不小于 100 的整数(毫秒)\n");
82
+ process.exit(2);
83
+ }
84
+ return options;
85
+ }
86
+
87
+ const baseUrl = (options) => options.url ?? `http://127.0.0.1:${options.port}`;
88
+
89
+ async function fetchPage(options, state) {
90
+ const query = new URLSearchParams();
91
+ query.set("after", String(state.after));
92
+ query.set("limit", String(options.limit));
93
+ if (options.level) query.set("level", options.level);
94
+ if (state.instanceId) query.set("instanceId", state.instanceId);
95
+
96
+ const endpoint = `${baseUrl(options)}/api/diagnostics?${query.toString()}`;
97
+ let response;
98
+ try {
99
+ response = await fetch(endpoint);
100
+ } catch (err) {
101
+ process.stderr.write(`无法连接 ${endpoint}: ${err.message}\n`);
102
+ process.stderr.write("请确认 dbx-plugin dev 正在运行,并用它实际打印的端口。\n");
103
+ process.exit(1);
104
+ }
105
+ if (!response.ok) {
106
+ process.stderr.write(`请求失败: HTTP ${response.status} ${response.statusText}(${endpoint})\n`);
107
+ process.exit(1);
108
+ }
109
+ return response.json();
110
+ }
111
+
112
+ const LEVEL_TAG = { debug: "DEBUG", info: "INFO ", error: "ERROR" };
113
+
114
+ function formatEntry(entry, showDetails) {
115
+ const time = typeof entry.time === "string" ? entry.time.slice(11, 23) : " ";
116
+ const tag = LEVEL_TAG[entry.level] ?? String(entry.level ?? "?").toUpperCase();
117
+ const category = entry.category ? `[${entry.category}] ` : "";
118
+ let line = `${time} ${tag} ${category}${entry.message ?? ""}`;
119
+ if (showDetails && entry.details && Object.keys(entry.details).length > 0) {
120
+ line += `\n ${JSON.stringify(entry.details)}`;
121
+ }
122
+ return line;
123
+ }
124
+
125
+ function emit(entries, options) {
126
+ for (const entry of entries) {
127
+ if (options.json) process.stdout.write(`${JSON.stringify(entry)}\n`);
128
+ else process.stdout.write(`${formatEntry(entry, options.details)}\n`);
129
+ }
130
+ }
131
+
132
+ async function main() {
133
+ const options = parseArgs(process.argv.slice(2));
134
+ const state = { after: 0, instanceId: undefined, first: true };
135
+
136
+ if (!options.follow) {
137
+ const page = await fetchPage(options, state);
138
+ if (!options.json && !options.quiet) {
139
+ const describe = (value) => (typeof value === "string" ? value : JSON.stringify(value));
140
+ const bits = [
141
+ page.plugin ? `plugin=${describe(page.plugin)}` : null,
142
+ page.backendState ? `backend=${describe(page.backendState)}` : null,
143
+ page.port ? `port=${describe(page.port)}` : null,
144
+ ].filter(Boolean);
145
+ process.stderr.write(`${bits.join(" ")}${bits.length ? "\n" : ""}`);
146
+ if (page.truncated) process.stderr.write("(旧日志已被 500 条环形缓冲覆盖)\n");
147
+ }
148
+ emit(page.entries ?? [], options);
149
+ if (!options.json && !options.quiet) {
150
+ const count = (page.entries ?? []).length;
151
+ process.stderr.write(
152
+ `${count} 条${page.hasMore ? "(还有更多,请用更大的 --limit 或 --follow)" : ""}\n`,
153
+ );
154
+ }
155
+ return;
156
+ }
157
+
158
+ let running = true;
159
+ process.on("SIGINT", () => {
160
+ running = false;
161
+ process.stderr.write("\n已停止。\n");
162
+ process.exit(0);
163
+ });
164
+
165
+ while (running) {
166
+ const page = await fetchPage(options, state);
167
+ if (page.reset && !state.first) {
168
+ process.stderr.write("(服务实例或游标重置,从可用历史重新开始)\n");
169
+ state.after = 0;
170
+ }
171
+ if (page.truncated) process.stderr.write("(部分旧日志已被覆盖)\n");
172
+ state.instanceId = page.instanceId;
173
+ state.first = false;
174
+
175
+ const entries = page.entries ?? [];
176
+ emit(entries, options);
177
+
178
+ if (page.hasMore) {
179
+ // 立即取下一页
180
+ state.after = page.nextAfter ?? (entries.at(-1)?.id ?? state.after);
181
+ continue;
182
+ }
183
+ state.after = page.nextAfter ?? state.after;
184
+ await new Promise((resolve) => setTimeout(resolve, options.interval));
185
+ }
186
+ }
187
+
188
+ main().catch((err) => {
189
+ process.stderr.write(`未预期的错误: ${err.stack ?? err.message}\n`);
190
+ process.exit(1);
191
+ });
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * inspect-dbxp.mjs — 检查 .dbxp 包内容(零依赖,Node.js 18+)
4
+ *
5
+ * 用法:
6
+ * node inspect-dbxp.mjs <file.dbxp> [--json] [--extract DIR] [--no-verify] [--quiet]
7
+ *
8
+ * .dbxp 是 ZIP 容器,通常包含:
9
+ * manifest.json, checksums.json, [signature.json], bin/<target>/<binary>, <include 目录...>
10
+ *
11
+ * 会做:列出条目、解析 manifest、逐条重算 checksums.json 里的 SHA-256、
12
+ * 判断签名状态、检查 bin/ 下的 target 与文件名是否一致、发现不该出现的文件。
13
+ *
14
+ * 退出码: 0 = 检查通过,1 = 发现问题,2 = 用法/IO 错误
15
+ */
16
+
17
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from "node:fs";
18
+ import { createHash } from "node:crypto";
19
+ import { inflateRawSync } from "node:zlib";
20
+ import { join, resolve, dirname, posix } from "node:path";
21
+
22
+ const EOCD_SIGNATURE = 0x06054b50;
23
+ const CENTRAL_SIGNATURE = 0x02014b50;
24
+ const LOCAL_SIGNATURE = 0x04034b50;
25
+ const ZIP64_ENTRY_SENTINEL = 0xffffffff;
26
+ const ZIP64_COUNT_SENTINEL = 0xffff;
27
+
28
+ /** 商店目录实际使用的 target 词表;长 target 需先匹配(darwin-arm64 先于 arm64)。 */
29
+ const KNOWN_TARGETS = [
30
+ "darwin-arm64",
31
+ "darwin-x64",
32
+ "linux-arm64",
33
+ "linux-x64",
34
+ "windows-arm64",
35
+ "windows-x64",
36
+ "universal",
37
+ ];
38
+
39
+ /**
40
+ * 解析 `<id>-<version>-<target>.dbxp`。
41
+ * target 在末尾且可能含 `-`,因此先按已知词表从右匹配,再回退到最后一个 `-`。
42
+ */
43
+ function parseDbxpName(fileName) {
44
+ if (!fileName.endsWith(".dbxp")) return null;
45
+ const stem = fileName.slice(0, -".dbxp".length);
46
+ let target = null;
47
+ let canonical = false;
48
+ for (const candidate of KNOWN_TARGETS) {
49
+ if (stem.endsWith(`-${candidate}`)) {
50
+ target = candidate;
51
+ canonical = true;
52
+ break;
53
+ }
54
+ }
55
+ if (target === null) {
56
+ const index = stem.lastIndexOf("-");
57
+ if (index < 0) return null;
58
+ target = stem.slice(index + 1);
59
+ }
60
+ const rest = stem.slice(0, stem.length - target.length - 1);
61
+ const match = /^(.*)-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/.exec(rest);
62
+ if (!match) return { id: rest, version: null, target, canonical };
63
+ return { id: match[1], version: match[2], target, canonical };
64
+ }
65
+
66
+ const problems = [];
67
+ let jsonMode = false;
68
+ let quiet = false;
69
+
70
+ function problem(message, hint) {
71
+ problems.push({ level: "error", message, hint });
72
+ if (!jsonMode && !quiet) process.stderr.write(`[ERROR] ${message}${hint ? `\n → ${hint}` : ""}\n`);
73
+ }
74
+ function note(message) {
75
+ if (!jsonMode && !quiet) process.stdout.write(`${message}\n`);
76
+ }
77
+
78
+ // ---------------------------------------------------------------- ZIP 读取
79
+
80
+ function findEocd(buffer) {
81
+ const minOffset = Math.max(0, buffer.length - 66000);
82
+ for (let i = buffer.length - 22; i >= minOffset; i -= 1) {
83
+ if (buffer.readUInt32LE(i) === EOCD_SIGNATURE) return i;
84
+ }
85
+ return -1;
86
+ }
87
+
88
+ function readEntries(buffer) {
89
+ const eocd = findEocd(buffer);
90
+ if (eocd < 0) throw new Error("未找到 ZIP 中央目录(文件可能不是合法的 .dbxp)");
91
+ const totalEntries = buffer.readUInt16LE(eocd + 10);
92
+ const cdSize = buffer.readUInt32LE(eocd + 12);
93
+ const cdOffset = buffer.readUInt32LE(eocd + 16);
94
+ if (totalEntries === ZIP64_COUNT_SENTINEL || cdOffset === ZIP64_ENTRY_SENTINEL || cdSize === ZIP64_ENTRY_SENTINEL) {
95
+ throw new Error("暂不支持 ZIP64(条目数或偏移超出常规 ZIP 上限)");
96
+ }
97
+ if (cdOffset + cdSize > buffer.length) throw new Error("中央目录偏移越界,文件可能被截断");
98
+
99
+ const entries = [];
100
+ let cursor = cdOffset;
101
+ for (let i = 0; i < totalEntries; i += 1) {
102
+ if (cursor + 46 > buffer.length) throw new Error("中央目录条目越界");
103
+ if (buffer.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) throw new Error(`第 ${i} 个中央目录条目签名非法`);
104
+ const method = buffer.readUInt16LE(cursor + 10);
105
+ const crc32 = buffer.readUInt32LE(cursor + 16);
106
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
107
+ const uncompressedSize = buffer.readUInt32LE(cursor + 24);
108
+ const nameLength = buffer.readUInt16LE(cursor + 28);
109
+ const extraLength = buffer.readUInt16LE(cursor + 30);
110
+ const commentLength = buffer.readUInt16LE(cursor + 32);
111
+ const externalAttributes = buffer.readUInt32LE(cursor + 38);
112
+ const localOffset = buffer.readUInt32LE(cursor + 42);
113
+ const name = buffer.toString("utf8", cursor + 46, cursor + 46 + nameLength);
114
+ entries.push({
115
+ name,
116
+ method,
117
+ crc32,
118
+ compressedSize,
119
+ uncompressedSize,
120
+ unixMode: (externalAttributes >>> 16) & 0xffff,
121
+ localOffset,
122
+ });
123
+ cursor += 46 + nameLength + extraLength + commentLength;
124
+ }
125
+ return entries;
126
+ }
127
+
128
+ function readEntry(buffer, entry) {
129
+ const offset = entry.localOffset;
130
+ if (offset + 30 > buffer.length) throw new Error(`条目 ${entry.name}: 本地头越界`);
131
+ if (buffer.readUInt32LE(offset) !== LOCAL_SIGNATURE) throw new Error(`条目 ${entry.name}: 本地文件头签名非法`);
132
+ const nameLength = buffer.readUInt16LE(offset + 26);
133
+ const extraLength = buffer.readUInt16LE(offset + 28);
134
+ const start = offset + 30 + nameLength + extraLength;
135
+ const end = start + entry.compressedSize;
136
+ if (end > buffer.length) throw new Error(`条目 ${entry.name}: 数据越界`);
137
+ const raw = buffer.subarray(start, end);
138
+ if (entry.method === 0) return Buffer.from(raw);
139
+ if (entry.method === 8) return inflateRawSync(raw);
140
+ throw new Error(`条目 ${entry.name}: 不支持的压缩方法 ${entry.method}`);
141
+ }
142
+
143
+ const sha256 = (data) => createHash("sha256").update(data).digest("hex");
144
+ const formatSize = (bytes) => {
145
+ if (bytes < 1024) return `${bytes} B`;
146
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
147
+ return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
148
+ };
149
+
150
+ // ---------------------------------------------------------------- 检查
151
+
152
+ function main() {
153
+ const args = process.argv.slice(2);
154
+ const positional = [];
155
+ let extractDir = null;
156
+ let verify = true;
157
+
158
+ for (let i = 0; i < args.length; i += 1) {
159
+ const arg = args[i];
160
+ if (arg === "--json") {
161
+ jsonMode = true;
162
+ } else if (arg === "--quiet") quiet = true;
163
+ else if (arg === "--no-verify") verify = false;
164
+ else if (arg === "--extract") {
165
+ extractDir = args[i + 1];
166
+ i += 1;
167
+ if (!extractDir) {
168
+ process.stderr.write("--extract 需要一个目录参数\n");
169
+ process.exit(2);
170
+ }
171
+ } else if (arg === "-h" || arg === "--help") {
172
+ process.stdout.write(
173
+ "用法: node inspect-dbxp.mjs <file.dbxp> [--json] [--extract DIR] [--no-verify] [--quiet]\n\n" +
174
+ "列出 .dbxp 条目、解析 manifest、校验 checksums.json、判断签名与 bin target。\n",
175
+ );
176
+ return;
177
+ } else if (arg.startsWith("-")) {
178
+ process.stderr.write(`未知选项: ${arg}\n`);
179
+ process.exit(2);
180
+ } else positional.push(arg);
181
+ }
182
+
183
+ if (positional.length === 0) {
184
+ process.stderr.write("用法: node inspect-dbxp.mjs <file.dbxp> [--json] [--extract DIR] [--no-verify]\n");
185
+ process.exit(2);
186
+ }
187
+
188
+ const filePath = resolve(positional[0]);
189
+ if (!existsSync(filePath)) {
190
+ process.stderr.write(`文件不存在: ${filePath}\n`);
191
+ process.exit(2);
192
+ }
193
+
194
+ const buffer = readFileSync(filePath);
195
+ const fileName = posix.basename(filePath.split("\\").join("/"));
196
+
197
+ let entries;
198
+ try {
199
+ entries = readEntries(buffer);
200
+ } catch (err) {
201
+ process.stderr.write(`读取失败: ${err.message}\n`);
202
+ process.exit(2);
203
+ }
204
+
205
+ // 文件名 → <id>-<version>-<target>.dbxp
206
+ const parsedName = parseDbxpName(fileName);
207
+
208
+ // manifest
209
+ let manifest = null;
210
+ const manifestEntry = entries.find((e) => e.name === "manifest.json");
211
+ if (!manifestEntry) {
212
+ problem("包内缺少 manifest.json", "DBX 安装会失败;请用 dbx-plugin package 重新打包。");
213
+ } else {
214
+ try {
215
+ manifest = JSON.parse(readEntry(buffer, manifestEntry).toString("utf8"));
216
+ } catch (err) {
217
+ problem(`包内 manifest.json 无法解析: ${err.message}`);
218
+ }
219
+ }
220
+
221
+ // signature
222
+ const signatureEntry = entries.find((e) => e.name === "signature.json");
223
+ let signature = null;
224
+ if (signatureEntry) {
225
+ try {
226
+ signature = JSON.parse(readEntry(buffer, signatureEntry).toString("utf8"));
227
+ } catch (err) {
228
+ problem(`signature.json 无法解析: ${err.message}`);
229
+ }
230
+ }
231
+
232
+ // checksums
233
+ const checksumsEntry = entries.find((e) => e.name === "checksums.json");
234
+ let checksums = null;
235
+ if (!checksumsEntry) {
236
+ problem("包内缺少 checksums.json", "DBX 会拒绝安装:除 checksums/signature 外每个文件都必须有一条 SHA-256。");
237
+ } else {
238
+ try {
239
+ checksums = JSON.parse(readEntry(buffer, checksumsEntry).toString("utf8"));
240
+ } catch (err) {
241
+ problem(`checksums.json 无法解析: ${err.message}`);
242
+ }
243
+ }
244
+
245
+ const verification = { checked: 0, mismatched: [], missing: [], extra: [], algorithm: null };
246
+ if (checksums && verify) {
247
+ if (checksums.algorithm !== "sha256") {
248
+ problem(`checksums.json algorithm 期望 "sha256",实际 ${JSON.stringify(checksums.algorithm)}`);
249
+ }
250
+ verification.algorithm = checksums.algorithm ?? null;
251
+ const files = checksums.files && typeof checksums.files === "object" ? checksums.files : {};
252
+ const payloadEntries = entries.filter((e) => !e.name.endsWith("/") && e.name !== "checksums.json" && e.name !== "signature.json");
253
+ for (const entry of payloadEntries) {
254
+ const expected = files[entry.name];
255
+ if (expected === undefined) {
256
+ verification.missing.push(entry.name);
257
+ problem(`checksums.json 缺少条目: ${entry.name}`, "每个非 checksums/signature 文件都必须被登记。");
258
+ continue;
259
+ }
260
+ let actual;
261
+ try {
262
+ actual = sha256(readEntry(buffer, entry));
263
+ } catch (err) {
264
+ problem(`无法读取条目 ${entry.name}: ${err.message}`);
265
+ continue;
266
+ }
267
+ verification.checked += 1;
268
+ if (actual !== String(expected).toLowerCase()) {
269
+ verification.mismatched.push({ name: entry.name, expected, actual });
270
+ problem(`SHA-256 不匹配: ${entry.name}`, `期望 ${expected},实际 ${actual}`);
271
+ }
272
+ }
273
+ for (const name of Object.keys(files)) {
274
+ if (!entries.some((e) => e.name === name)) {
275
+ verification.extra.push(name);
276
+ problem(`checksums.json 登记了不存在的文件: ${name}`);
277
+ }
278
+ }
279
+ }
280
+
281
+ // 内容层面的问题
282
+ const dbxDev = entries.filter((e) => e.name === ".dbx-dev" || e.name.startsWith(".dbx-dev/"));
283
+ if (dbxDev.length) {
284
+ problem(`包内含开发数据: ${dbxDev.slice(0, 5).map((e) => e.name).join(", ")}`, "可能泄露明文凭据;检查 [package].include 与 .gitignore。");
285
+ }
286
+ const binTargets = new Set();
287
+ for (const entry of entries) {
288
+ const match = /^bin\/([^/]+)\/(.+)$/.exec(entry.name);
289
+ if (match) binTargets.add(match[1]);
290
+ }
291
+
292
+ // 包内 manifest 的 backend.executable 形如 bin/<target>/<binary>
293
+ const executable = manifest?.entrypoints?.backend?.executable;
294
+ let executableTarget = null;
295
+ if (typeof executable === "string") {
296
+ const match = /^bin\/([^/]+)\/(.+)$/.exec(executable);
297
+ if (match) executableTarget = match[1];
298
+ }
299
+
300
+ if (parsedName && parsedName.canonical && binTargets.size > 0 && !binTargets.has(parsedName.target)) {
301
+ problem(
302
+ `bin/ 下的 target (${[...binTargets].join(", ")}) 与文件名 target (${parsedName.target}) 不一致`,
303
+ "包内 manifest 的 backend.executable 应指向 bin/<target>/<binary>。",
304
+ );
305
+ }
306
+ if (parsedName && parsedName.canonical && executableTarget && executableTarget !== parsedName.target) {
307
+ problem(
308
+ `包内 manifest 的 backend.executable target "${executableTarget}" 与文件名 target "${parsedName.target}" 不一致`,
309
+ "dbx-plugin package 会把 executable 改写为 bin/<target>/<binary>;不一致说明包被手工改过。",
310
+ );
311
+ }
312
+ if (manifest && parsedName?.canonical && parsedName.version && manifest.version !== parsedName.version) {
313
+ problem(`包内 manifest version "${manifest.version}" 与文件名 version "${parsedName.version}" 不一致`);
314
+ }
315
+ if (manifest && parsedName?.canonical && parsedName.id && manifest.id !== parsedName.id) {
316
+ problem(`包内 manifest id "${manifest.id}" 与文件名 id "${parsedName.id}" 不一致`);
317
+ }
318
+ if (parsedName && !binTargets.size && manifest?.entrypoints?.backend) {
319
+ problem("manifest 声明了 backend,但包内没有 bin/<target>/ 可执行文件");
320
+ }
321
+ if (manifest && manifest.entrypoints?.ui?.entry) {
322
+ const uiEntry = manifest.entrypoints.ui.entry;
323
+ if (!entries.some((e) => e.name === uiEntry)) {
324
+ problem(`manifest.entrypoints.ui.entry "${uiEntry}" 不在包内`);
325
+ }
326
+ }
327
+ if (manifest && typeof manifest.icon === "string" && manifest.icon !== "") {
328
+ if (!entries.some((e) => e.name === manifest.icon)) problem(`manifest.icon "${manifest.icon}" 不在包内`);
329
+ }
330
+ if (!signatureEntry) {
331
+ note("签名状态: 未签名(候选包,仅可用于本地开发安装)");
332
+ } else {
333
+ note(`签名状态: 已签名 (key_id=${signature?.key_id ?? "?"}, algorithm=${signature?.algorithm ?? "?"})`);
334
+ }
335
+
336
+ // 可选解包
337
+ if (extractDir) {
338
+ const target = resolve(extractDir);
339
+ for (const entry of entries) {
340
+ const destination = join(target, ...entry.name.split("/"));
341
+ if (entry.name.endsWith("/")) {
342
+ mkdirSync(destination, { recursive: true });
343
+ continue;
344
+ }
345
+ mkdirSync(dirname(destination), { recursive: true });
346
+ writeFileSync(destination, readEntry(buffer, entry));
347
+ }
348
+ note(`已解包到: ${target}`);
349
+ }
350
+
351
+ if (jsonMode) {
352
+ process.stdout.write(
353
+ `${JSON.stringify(
354
+ {
355
+ file: filePath,
356
+ size: statSync(filePath).size,
357
+ parsedName,
358
+ entryCount: entries.length,
359
+ signed: Boolean(signatureEntry),
360
+ signature,
361
+ manifest,
362
+ manifestId: manifest?.id ?? null,
363
+ manifestVersion: manifest?.version ?? null,
364
+ manifestPublisher: manifest?.publisher ?? null,
365
+ backendExecutable: manifest?.entrypoints?.backend?.executable ?? null,
366
+ binTargets: [...binTargets].sort(),
367
+ verification,
368
+ entries: entries
369
+ .map((e) => ({
370
+ name: e.name,
371
+ method: e.method === 0 ? "store" : e.method === 8 ? "deflate" : `method-${e.method}`,
372
+ size: e.uncompressedSize,
373
+ compressedSize: e.compressedSize,
374
+ unixMode: `0${(e.unixMode & 0o7777).toString(8)}`,
375
+ }))
376
+ .sort((a, b) => a.name.localeCompare(b.name)),
377
+ problems,
378
+ },
379
+ null,
380
+ 2,
381
+ )}\n`,
382
+ );
383
+ } else if (!quiet) {
384
+ process.stdout.write(
385
+ `\n文件: ${fileName} (${formatSize(buffer.length)})\n` +
386
+ `条目: ${entries.length} 个\n` +
387
+ (manifest
388
+ ? `身份: id=${manifest.id} version=${manifest.version} publisher=${manifest.publisher}\n`
389
+ : "身份: 无法读取\n") +
390
+ (parsedName ? `文件名解析: id=${parsedName.id} version=${parsedName.version} target=${parsedName.target}\n` : "") +
391
+ (binTargets.size ? `bin targets: ${[...binTargets].sort().join(", ")}\n` : "") +
392
+ (verification.checked ? `checksums: 已校验 ${verification.checked} 个文件\n` : "") +
393
+ `\n结论: ${problems.length === 0 ? "OK" : `${problems.length} 个问题`}\n`,
394
+ );
395
+ }
396
+
397
+ process.exit(problems.length === 0 ? 0 : 1);
398
+ }
399
+
400
+ main();