apkpub-cli 0.0.1-alpha.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/dist/apkpub.js ADDED
@@ -0,0 +1,2527 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/apkpub.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/init.ts
7
+ import { input, confirm, checkbox } from "@inquirer/prompts";
8
+
9
+ // src/config/store.ts
10
+ import { mkdir, readFile, readdir, writeFile, chmod, access } from "fs/promises";
11
+ import path from "path";
12
+ import os from "os";
13
+
14
+ // src/errors/ApkpubError.ts
15
+ var ApkpubError = class extends Error {
16
+ code;
17
+ channel;
18
+ step;
19
+ retryable;
20
+ cause;
21
+ constructor(options) {
22
+ super(options.message);
23
+ this.name = "ApkpubError";
24
+ this.code = options.code;
25
+ this.channel = options.channel;
26
+ this.step = options.step;
27
+ this.retryable = options.retryable ?? isRetryableCode(options.code);
28
+ this.cause = options.cause;
29
+ }
30
+ };
31
+ function isRetryableCode(code) {
32
+ return ["NETWORK_ERROR" /* NETWORK_ERROR */, "TIMEOUT" /* TIMEOUT */, "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */].includes(code);
33
+ }
34
+
35
+ // src/config/schema.ts
36
+ import { z } from "zod";
37
+ var CURRENT_SCHEMA_VERSION = 1;
38
+ var channelParamSchema = z.object({
39
+ name: z.string(),
40
+ value: z.string().default("")
41
+ });
42
+ var marketChannelConfigSchema = z.object({
43
+ name: z.string(),
44
+ type: z.literal("market").default("market"),
45
+ enable: z.boolean().default(true),
46
+ params: z.array(channelParamSchema).default([])
47
+ });
48
+ var ossAuthSchema = z.discriminatedUnion("mode", [
49
+ z.object({
50
+ mode: z.literal("ak"),
51
+ accessKeyId: z.string(),
52
+ accessKeySecret: z.string()
53
+ }),
54
+ z.object({
55
+ mode: z.literal("sts"),
56
+ stsTokenUrl: z.string().url(),
57
+ signKey: z.string(),
58
+ contextB: z.string().default("{}")
59
+ })
60
+ ]);
61
+ var customChannelConfigSchema = z.object({
62
+ name: z.string(),
63
+ type: z.literal("custom"),
64
+ enable: z.boolean().default(true),
65
+ uploadType: z.enum(["oss", "http"]),
66
+ fileNameIdentify: z.string().optional(),
67
+ endpoint: z.string().optional(),
68
+ bucket: z.string().optional(),
69
+ auth: ossAuthSchema.optional(),
70
+ uploadUrl: z.string().optional(),
71
+ method: z.enum(["PUT", "POST"]).optional(),
72
+ headers: z.record(z.string()).optional(),
73
+ formField: z.string().optional(),
74
+ objectKeyTemplate: z.string(),
75
+ downloadUrlTemplate: z.string(),
76
+ params: z.array(channelParamSchema).default([])
77
+ });
78
+ var channelConfigSchema = z.union([marketChannelConfigSchema, customChannelConfigSchema]);
79
+ var extensionSchema = z.object({
80
+ updateDesc: z.string().optional(),
81
+ apkDir: z.string().optional(),
82
+ urls: z.record(z.string()).optional(),
83
+ lastVersionCode: z.record(z.number()).optional(),
84
+ lastVersionName: z.record(z.string()).optional()
85
+ });
86
+ var appConfigSchema = z.object({
87
+ schemaVersion: z.number().default(CURRENT_SCHEMA_VERSION),
88
+ name: z.string(),
89
+ applicationId: z.string(),
90
+ createTime: z.number(),
91
+ enableChannel: z.boolean().default(true),
92
+ channels: z.array(channelConfigSchema).default([]),
93
+ extension: extensionSchema.default({})
94
+ });
95
+ function paramsToRecord(params) {
96
+ const record = {};
97
+ for (const p of params) {
98
+ record[p.name] = p.value;
99
+ }
100
+ return record;
101
+ }
102
+ function migrateConfig(raw) {
103
+ const obj = raw;
104
+ if (!obj.schemaVersion) {
105
+ obj.schemaVersion = 1;
106
+ }
107
+ return appConfigSchema.parse(obj);
108
+ }
109
+ function stripSecrets(config) {
110
+ const sensitiveKeys = [
111
+ "client_secret",
112
+ "privateKey",
113
+ "access_key_secret",
114
+ "accessKeySecret",
115
+ "signKey",
116
+ "password"
117
+ ];
118
+ const stripped = structuredClone(config);
119
+ for (const ch of stripped.channels) {
120
+ if ("params" in ch) {
121
+ for (const p of ch.params) {
122
+ if (sensitiveKeys.some((k) => p.name.toLowerCase().includes(k.toLowerCase()))) {
123
+ p.value = "";
124
+ }
125
+ }
126
+ }
127
+ if (ch.type === "custom" && ch.auth) {
128
+ if (ch.auth.mode === "ak") {
129
+ ch.auth.accessKeyId = "";
130
+ ch.auth.accessKeySecret = "";
131
+ } else {
132
+ ch.auth.signKey = "";
133
+ }
134
+ }
135
+ }
136
+ return stripped;
137
+ }
138
+
139
+ // src/utils/logger.ts
140
+ import pc from "picocolors";
141
+
142
+ // src/utils/redaction.ts
143
+ function redactMessage(message) {
144
+ return message.replace(/(client_secret|access_key_secret|privateKey|password|signKey)=[^&\s]+/gi, "$1=***").replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer ***");
145
+ }
146
+
147
+ // src/utils/logger.ts
148
+ var jsonMode = false;
149
+ var debugMode = false;
150
+ function setJsonMode(enabled) {
151
+ jsonMode = enabled;
152
+ }
153
+ function formatMessage(level, tag, message) {
154
+ const time = (/* @__PURE__ */ new Date()).toISOString();
155
+ const levelColors = {
156
+ debug: pc.gray,
157
+ info: pc.blue,
158
+ warn: pc.yellow,
159
+ error: pc.red
160
+ };
161
+ return `${pc.dim(time)} ${levelColors[level](level.toUpperCase())} [${tag}] ${redactMessage(message)}`;
162
+ }
163
+ function log(level, tag, message) {
164
+ if (jsonMode && level !== "error") return;
165
+ if (level === "debug" && !debugMode) return;
166
+ const stream = level === "error" ? process.stderr : process.stderr;
167
+ stream.write(formatMessage(level, tag, message) + "\n");
168
+ }
169
+ var logger = {
170
+ debug: (tag, message) => log("debug", tag, message),
171
+ info: (tag, message) => log("info", tag, message),
172
+ warn: (tag, message) => log("warn", tag, message),
173
+ error: (tag, message) => log("error", tag, message)
174
+ };
175
+
176
+ // src/config/store.ts
177
+ var CONFIG_DIR_NAME = ".apkpub";
178
+ function getConfigRoot(debug = false) {
179
+ const base = path.join(os.homedir(), CONFIG_DIR_NAME);
180
+ return debug ? path.join(base, "debug") : base;
181
+ }
182
+ function getAppsDir(debug = false) {
183
+ return path.join(getConfigRoot(debug), "apps");
184
+ }
185
+ function getLogsDir(debug = false) {
186
+ return path.join(getConfigRoot(debug), "logs");
187
+ }
188
+ async function ensureConfigDirs(debug = false) {
189
+ const root = getConfigRoot(debug);
190
+ const apps = getAppsDir(debug);
191
+ const logs = getLogsDir(debug);
192
+ await mkdir(apps, { recursive: true, mode: 448 });
193
+ await mkdir(logs, { recursive: true, mode: 448 });
194
+ try {
195
+ await chmod(root, 448);
196
+ } catch {
197
+ }
198
+ await checkPermissions(root);
199
+ }
200
+ async function checkPermissions(dir) {
201
+ try {
202
+ const { statSync } = await import("fs");
203
+ const mode = statSync(dir).mode & 511;
204
+ if (mode > 448) {
205
+ logger.warn("config", `\u914D\u7F6E\u76EE\u5F55\u6743\u9650\u8FC7\u5BBD (${mode.toString(8)})\uFF0C\u5EFA\u8BAE chmod 700 ${dir}`);
206
+ }
207
+ } catch {
208
+ }
209
+ }
210
+ function configPath(applicationId, debug = false) {
211
+ const safe = applicationId.replace(/[^a-zA-Z0-9._-]/g, "_");
212
+ return path.join(getAppsDir(debug), `${safe}.json`);
213
+ }
214
+ async function saveConfig(config, debug = false) {
215
+ await ensureConfigDirs(debug);
216
+ const filePath = configPath(config.applicationId, debug);
217
+ const content = JSON.stringify(config, null, 2);
218
+ await writeFile(filePath, content, { mode: 384 });
219
+ logger.debug("config", `\u5DF2\u4FDD\u5B58\u914D\u7F6E: ${filePath}`);
220
+ }
221
+ async function loadConfig(applicationId, debug = false) {
222
+ const filePath = configPath(applicationId, debug);
223
+ try {
224
+ await access(filePath);
225
+ } catch {
226
+ throw new ApkpubError({
227
+ code: "CONFIG_NOT_FOUND" /* CONFIG_NOT_FOUND */,
228
+ message: `\u672A\u627E\u5230\u5E94\u7528\u914D\u7F6E: ${applicationId}`,
229
+ retryable: false
230
+ });
231
+ }
232
+ const content = await readFile(filePath, "utf8");
233
+ const raw = JSON.parse(content);
234
+ return migrateConfig(raw);
235
+ }
236
+ async function listConfigs(debug = false) {
237
+ await ensureConfigDirs(debug);
238
+ const dir = getAppsDir(debug);
239
+ let files;
240
+ try {
241
+ files = await readdir(dir);
242
+ } catch {
243
+ return [];
244
+ }
245
+ const configs = [];
246
+ for (const file of files) {
247
+ if (!file.endsWith(".json")) continue;
248
+ try {
249
+ const content = await readFile(path.join(dir, file), "utf8");
250
+ configs.push(migrateConfig(JSON.parse(content)));
251
+ } catch (err) {
252
+ logger.warn("config", `\u8DF3\u8FC7\u65E0\u6548\u914D\u7F6E ${file}: ${err instanceof Error ? err.message : String(err)}`);
253
+ }
254
+ }
255
+ return configs;
256
+ }
257
+ async function importConfig(filePath, debug = false) {
258
+ const content = await readFile(filePath, "utf8");
259
+ const raw = JSON.parse(content);
260
+ const config = migrateConfig(raw);
261
+ await saveConfig(config, debug);
262
+ return config;
263
+ }
264
+ async function exportConfig(applicationId, options = {}) {
265
+ const config = await loadConfig(applicationId, options.debug);
266
+ const output = options.includeSecrets ? config : stripSecrets(config);
267
+ return JSON.stringify(output, null, 2);
268
+ }
269
+ async function writeAuditLog(entry, debug = false) {
270
+ await ensureConfigDirs(debug);
271
+ const logFile = path.join(getLogsDir(debug), "audit.log");
272
+ const line = JSON.stringify({ ...entry, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
273
+ const { appendFile } = await import("fs/promises");
274
+ await appendFile(logFile, line, { mode: 384 });
275
+ }
276
+
277
+ // src/channels/huawei/index.ts
278
+ import { z as z2 } from "zod";
279
+ import { createReadStream } from "fs";
280
+ import path2 from "path";
281
+
282
+ // src/utils/http.ts
283
+ import axios from "axios";
284
+ var PRIVATE_IP_PATTERNS = [
285
+ /^localhost$/i,
286
+ /^127\./,
287
+ /^10\./,
288
+ /^172\.(1[6-9]|2\d|3[01])\./,
289
+ /^192\.168\./,
290
+ /^0\.0\.0\.0$/,
291
+ /^\[::1\]$/,
292
+ /^169\.254\./
293
+ ];
294
+ function assertSafeUrl(url, label = "URL") {
295
+ let parsed;
296
+ try {
297
+ parsed = new URL(url);
298
+ } catch {
299
+ throw new ApkpubError({
300
+ code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
301
+ message: `\u65E0\u6548\u7684 ${label}: ${url}`,
302
+ retryable: false
303
+ });
304
+ }
305
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
306
+ throw new ApkpubError({
307
+ code: "SSRF_BLOCKED" /* SSRF_BLOCKED */,
308
+ message: `${label} \u4EC5\u652F\u6301 http/https \u534F\u8BAE`,
309
+ retryable: false
310
+ });
311
+ }
312
+ const host = parsed.hostname;
313
+ if (PRIVATE_IP_PATTERNS.some((p) => p.test(host))) {
314
+ throw new ApkpubError({
315
+ code: "SSRF_BLOCKED" /* SSRF_BLOCKED */,
316
+ message: `${label} \u4E0D\u5141\u8BB8\u6307\u5411\u5185\u7F51\u5730\u5740: ${host}`,
317
+ retryable: false
318
+ });
319
+ }
320
+ }
321
+ function createHttpClient(options) {
322
+ return axios.create({
323
+ timeout: options?.timeout ?? 6e4,
324
+ validateStatus: () => true
325
+ });
326
+ }
327
+ async function withRetry(fn, options = {}) {
328
+ const retries = options.retries ?? 3;
329
+ const delayMs = options.delayMs ?? 1e3;
330
+ let lastError;
331
+ for (let i = 0; i <= retries; i++) {
332
+ if (options.signal?.aborted) {
333
+ throw new ApkpubError({ code: "TIMEOUT" /* TIMEOUT */, message: "\u64CD\u4F5C\u5DF2\u53D6\u6D88", retryable: false });
334
+ }
335
+ try {
336
+ return await fn();
337
+ } catch (err) {
338
+ lastError = err;
339
+ const retryable = err instanceof ApkpubError ? err.retryable : true;
340
+ if (!retryable || i === retries) break;
341
+ await sleep(delayMs * Math.pow(2, i), options.signal);
342
+ }
343
+ }
344
+ throw lastError;
345
+ }
346
+ function sleep(ms, signal) {
347
+ return new Promise((resolve, reject) => {
348
+ const timer = setTimeout(resolve, ms);
349
+ signal?.addEventListener("abort", () => {
350
+ clearTimeout(timer);
351
+ reject(new ApkpubError({ code: "TIMEOUT" /* TIMEOUT */, message: "\u64CD\u4F5C\u5DF2\u53D6\u6D88", retryable: false }));
352
+ });
353
+ });
354
+ }
355
+
356
+ // src/channels/huawei/index.ts
357
+ var BASE_URL = "https://connect-api.cloud.huawei.com";
358
+ var credSchema = z2.object({
359
+ client_id: z2.string().min(1),
360
+ client_secret: z2.string().min(1)
361
+ });
362
+ async function getToken(clientId, clientSecret) {
363
+ const client = createHttpClient();
364
+ const resp = await client.post(`${BASE_URL}/api/oauth2/v1/token`, {
365
+ client_id: clientId,
366
+ client_secret: clientSecret,
367
+ grant_type: "client_credentials"
368
+ });
369
+ if (resp.data?.access_token) return resp.data.access_token;
370
+ throw new ApkpubError({
371
+ code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
372
+ channel: "huawei",
373
+ step: "getToken",
374
+ message: `\u83B7\u53D6 token \u5931\u8D25: ${JSON.stringify(resp.data)}`,
375
+ retryable: false
376
+ });
377
+ }
378
+ async function getAppId(clientId, token, packageName) {
379
+ const client = createHttpClient();
380
+ const resp = await client.get(`${BASE_URL}/api/publish/v2/appid-list`, {
381
+ headers: { client_id: clientId, Authorization: `Bearer ${token}` },
382
+ params: { packageName }
383
+ });
384
+ const list = resp.data?.appids ?? resp.data?.list ?? [];
385
+ if (!list.length) {
386
+ throw new ApkpubError({
387
+ code: "CHANNEL_STATE_FAILED" /* CHANNEL_STATE_FAILED */,
388
+ channel: "huawei",
389
+ step: "getAppId",
390
+ message: `\u672A\u627E\u5230\u5305\u540D ${packageName} \u5BF9\u5E94\u7684\u5E94\u7528`,
391
+ retryable: false
392
+ });
393
+ }
394
+ return list[0].value ?? list[0].id ?? list[0].appId;
395
+ }
396
+ async function getUploadUrl(clientId, token, appId, fileName, contentLength) {
397
+ const client = createHttpClient();
398
+ const resp = await client.get(`${BASE_URL}/api/publish/v2/upload-url/for-obs`, {
399
+ headers: { client_id: clientId, Authorization: `Bearer ${token}` },
400
+ params: { appId, fileName, contentLength }
401
+ });
402
+ const urlInfo = resp.data?.urlInfo ?? resp.data?.url;
403
+ if (!urlInfo?.url) {
404
+ throw new ApkpubError({
405
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
406
+ channel: "huawei",
407
+ step: "getUploadUrl",
408
+ message: `\u83B7\u53D6\u4E0A\u4F20\u5730\u5740\u5931\u8D25: ${JSON.stringify(resp.data)}`,
409
+ retryable: true
410
+ });
411
+ }
412
+ const headers = {};
413
+ if (urlInfo.headers) {
414
+ for (const h of urlInfo.headers) {
415
+ headers[h.key ?? h.name] = h.value;
416
+ }
417
+ }
418
+ return { url: urlInfo.url, objectId: urlInfo.objectId, headers };
419
+ }
420
+ async function uploadFile(uploadUrl, headers, filePath, onProgress) {
421
+ const client = createHttpClient({ timeout: 6e5 });
422
+ const stream = createReadStream(filePath);
423
+ const resp = await client.put(uploadUrl, stream, {
424
+ headers: { ...headers, "Content-Type": "application/octet-stream" },
425
+ maxBodyLength: Infinity,
426
+ onUploadProgress: (e) => {
427
+ if (e.total) onProgress(Math.round(e.loaded / e.total * 100));
428
+ }
429
+ });
430
+ if (resp.status < 200 || resp.status >= 300) {
431
+ throw new ApkpubError({
432
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
433
+ channel: "huawei",
434
+ step: "uploadFile",
435
+ message: `\u4E0A\u4F20\u5931\u8D25 HTTP ${resp.status}`,
436
+ retryable: resp.status >= 500
437
+ });
438
+ }
439
+ }
440
+ async function bindApk(clientId, token, appId, fileName, objectId) {
441
+ const client = createHttpClient();
442
+ const resp = await client.put(
443
+ `${BASE_URL}/api/publish/v2/app-file-info`,
444
+ { fileType: 5, files: [{ fileName, fileDestUrl: objectId }] },
445
+ { headers: { client_id: clientId, Authorization: `Bearer ${token}` }, params: { appId } }
446
+ );
447
+ const pkgId = resp.data?.pkgVersion?.[0]?.pkgId ?? resp.data?.pkgId;
448
+ if (!pkgId) {
449
+ throw new ApkpubError({
450
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
451
+ channel: "huawei",
452
+ step: "bindApk",
453
+ message: `\u7ED1\u5B9A APK \u5931\u8D25: ${JSON.stringify(resp.data)}`,
454
+ retryable: false
455
+ });
456
+ }
457
+ return { pkgId };
458
+ }
459
+ async function waitApkReady(clientId, token, appId, pkgId, signal) {
460
+ const client = createHttpClient();
461
+ const start = Date.now();
462
+ const timeout = 3 * 60 * 1e3;
463
+ while (Date.now() - start < timeout) {
464
+ if (signal.aborted) throw new ApkpubError({ code: "TIMEOUT" /* TIMEOUT */, message: "\u7B49\u5F85\u7F16\u8BD1\u8D85\u65F6", retryable: false });
465
+ await new Promise((r) => setTimeout(r, 1e4));
466
+ const resp = await client.get(`${BASE_URL}/api/publish/v2/package/compile/status`, {
467
+ headers: { client_id: clientId, Authorization: `Bearer ${token}` },
468
+ params: { appId, pkgIds: pkgId }
469
+ });
470
+ const state = resp.data?.pkgStateList?.[0]?.pkgState ?? resp.data?.compileStatus;
471
+ if (state === 0 || state === "COMPILE_SUCCESS") return;
472
+ }
473
+ throw new ApkpubError({
474
+ code: "TIMEOUT" /* TIMEOUT */,
475
+ channel: "huawei",
476
+ step: "waitApkReady",
477
+ message: "\u7B49\u5F85 APK \u7F16\u8BD1\u8D85\u65F6",
478
+ retryable: true
479
+ });
480
+ }
481
+ var huaweiChannel = {
482
+ name: "huawei",
483
+ label: "\u534E\u4E3A",
484
+ type: "market",
485
+ fileNameIdentify: "HUAWEI",
486
+ credentialSchema: credSchema,
487
+ async getMarketState(appId, config) {
488
+ const creds = credSchema.parse(config);
489
+ const token = await getToken(creds.client_id, creds.client_secret);
490
+ const hwAppId = await getAppId(creds.client_id, token, appId);
491
+ const client = createHttpClient();
492
+ const resp = await client.get(`${BASE_URL}/api/publish/v2/app-info`, {
493
+ headers: { client_id: creds.client_id, Authorization: `Bearer ${token}` },
494
+ params: { appId: hwAppId }
495
+ });
496
+ const info = resp.data?.appInfo ?? resp.data;
497
+ const reviewState = mapReviewState(info?.releaseState ?? info?.status);
498
+ return {
499
+ reviewState,
500
+ enableSubmit: reviewState === "online" || reviewState === "rejected",
501
+ lastVersionCode: Number(info?.versionCode ?? 0),
502
+ lastVersionName: String(info?.versionNumber ?? info?.onShelfVersionNumber ?? info?.versionName ?? "0")
503
+ };
504
+ },
505
+ async validateCredentials(config) {
506
+ const creds = credSchema.parse(config);
507
+ await getToken(creds.client_id, creds.client_secret);
508
+ },
509
+ async upload(ctx) {
510
+ const creds = credSchema.parse(ctx.config);
511
+ const fileName = path2.basename(ctx.filePath);
512
+ ctx.onProgress({ step: "getToken" });
513
+ const token = await withRetry(() => getToken(creds.client_id, creds.client_secret));
514
+ ctx.onProgress({ step: "getAppId" });
515
+ const appId = await getAppId(creds.client_id, token, ctx.apkInfo.applicationId);
516
+ ctx.onProgress({ step: "getUploadUrl" });
517
+ const uploadInfo = await getUploadUrl(creds.client_id, token, appId, fileName, ctx.apkInfo.size);
518
+ ctx.onProgress({ step: "uploading", percent: 0 });
519
+ await uploadFile(
520
+ uploadInfo.url,
521
+ uploadInfo.headers,
522
+ ctx.filePath,
523
+ (p) => ctx.onProgress({ step: "uploading", percent: p })
524
+ );
525
+ ctx.onProgress({ step: "bindApk" });
526
+ const bind = await bindApk(creds.client_id, token, appId, fileName, uploadInfo.objectId);
527
+ ctx.onProgress({ step: "waitCompile" });
528
+ await waitApkReady(creds.client_id, token, appId, bind.pkgId, ctx.signal);
529
+ ctx.onProgress({ step: "updateDesc" });
530
+ const client = createHttpClient();
531
+ await client.put(
532
+ `${BASE_URL}/api/publish/v2/app-language-info`,
533
+ { lang: "zh-CN", appDesc: ctx.desc },
534
+ { headers: { client_id: creds.client_id, Authorization: `Bearer ${token}` }, params: { appId } }
535
+ );
536
+ ctx.onProgress({ step: "submit" });
537
+ await client.post(`${BASE_URL}/api/publish/v2/app-submit`, null, {
538
+ headers: { client_id: creds.client_id, Authorization: `Bearer ${token}` },
539
+ params: { appId }
540
+ });
541
+ ctx.onProgress({ step: "done", percent: 100 });
542
+ return { message: "\u63D0\u4EA4\u5BA1\u6838\u6210\u529F" };
543
+ }
544
+ };
545
+ function mapReviewState(state) {
546
+ const s = Number(state);
547
+ if (s === 0 || s === 7) return "online";
548
+ if (s === 4 || s === 5) return "reviewing";
549
+ if (s === 1 || s === 8 || s === 9) return "rejected";
550
+ return "unknown";
551
+ }
552
+
553
+ // src/channels/honor/index.ts
554
+ import { z as z3 } from "zod";
555
+ import { createReadStream as createReadStream3 } from "fs";
556
+ import path4 from "path";
557
+ import FormData from "form-data";
558
+
559
+ // src/utils/files.ts
560
+ import { createHash } from "crypto";
561
+ import { createReadStream as createReadStream2 } from "fs";
562
+ import { readdir as readdir2, stat } from "fs/promises";
563
+ import path3 from "path";
564
+ async function fileMd5(filePath) {
565
+ return hashFile(filePath, "md5");
566
+ }
567
+ async function fileSha256(filePath) {
568
+ return hashFile(filePath, "sha256");
569
+ }
570
+ async function hashFile(filePath, algorithm) {
571
+ return new Promise((resolve, reject) => {
572
+ const hash = createHash(algorithm);
573
+ const stream = createReadStream2(filePath);
574
+ stream.on("data", (chunk) => hash.update(chunk));
575
+ stream.on("end", () => resolve(hash.digest("hex")));
576
+ stream.on("error", reject);
577
+ });
578
+ }
579
+ async function listApkFiles(dir) {
580
+ const entries = await readdir2(dir);
581
+ const apks = [];
582
+ for (const entry of entries) {
583
+ const full = path3.join(dir, entry);
584
+ const info = await stat(full);
585
+ if (info.isFile() && entry.toLowerCase().endsWith(".apk")) {
586
+ apks.push(full);
587
+ }
588
+ }
589
+ return apks;
590
+ }
591
+ async function fileSize(filePath) {
592
+ const info = await stat(filePath);
593
+ return info.size;
594
+ }
595
+
596
+ // src/channels/honor/index.ts
597
+ var BASE_URL2 = "https://appmarket-openapi-drcn.cloud.honor.com";
598
+ var TOKEN_URL = "https://iam.developer.honor.com/auth/token";
599
+ var credSchema2 = z3.object({
600
+ client_id: z3.string().min(1),
601
+ client_secret: z3.string().min(1)
602
+ });
603
+ async function getToken2(clientId, clientSecret) {
604
+ const client = createHttpClient();
605
+ const resp = await client.post(
606
+ TOKEN_URL,
607
+ new URLSearchParams({
608
+ client_id: clientId,
609
+ client_secret: clientSecret,
610
+ grant_type: "client_credentials"
611
+ }).toString(),
612
+ { headers: { "content-type": "application/x-www-form-urlencoded" } }
613
+ );
614
+ if (resp.data?.access_token) return resp.data.access_token;
615
+ throw new ApkpubError({
616
+ code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
617
+ channel: "honor",
618
+ step: "getToken",
619
+ message: `\u83B7\u53D6 token \u5931\u8D25`,
620
+ retryable: false
621
+ });
622
+ }
623
+ async function getAppId2(token, packageName) {
624
+ const client = createHttpClient();
625
+ const resp = await client.get(`${BASE_URL2}/openapi/v1/publish/get-app-id`, {
626
+ headers: { Authorization: `Bearer ${token}` },
627
+ params: { pkgName: packageName }
628
+ });
629
+ checkHonorResult(resp.data, "getAppId");
630
+ const list = resp.data.data ?? [];
631
+ if (!list.length) {
632
+ throw new ApkpubError({
633
+ code: "CHANNEL_STATE_FAILED" /* CHANNEL_STATE_FAILED */,
634
+ channel: "honor",
635
+ message: `\u672A\u627E\u5230\u5305\u540D ${packageName}`,
636
+ retryable: false
637
+ });
638
+ }
639
+ return list[0].appId;
640
+ }
641
+ function checkHonorResult(data, action) {
642
+ if (data.code !== 0) {
643
+ throw new ApkpubError({
644
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
645
+ channel: "honor",
646
+ step: action,
647
+ message: data.message ?? `\u8363\u8000 API \u5931\u8D25: code=${data.code}`,
648
+ retryable: false
649
+ });
650
+ }
651
+ }
652
+ var honorChannel = {
653
+ name: "honor",
654
+ label: "\u8363\u8000",
655
+ type: "market",
656
+ fileNameIdentify: "HONOR",
657
+ credentialSchema: credSchema2,
658
+ async getMarketState(appId, config) {
659
+ const creds = credSchema2.parse(config);
660
+ const token = await getToken2(creds.client_id, creds.client_secret);
661
+ const honorAppId = await getAppId2(token, appId);
662
+ const client = createHttpClient();
663
+ const resp = await client.get(`${BASE_URL2}/openapi/v1/publish/get-app-current-release`, {
664
+ headers: { Authorization: `Bearer ${token}` },
665
+ params: { appId: honorAppId }
666
+ });
667
+ checkHonorResult(resp.data, "getReviewState");
668
+ const data = resp.data.data;
669
+ return {
670
+ reviewState: data?.auditStatus === 1 ? "online" : data?.auditStatus === 2 ? "reviewing" : "unknown",
671
+ enableSubmit: true,
672
+ lastVersionCode: Number(data?.versionCode ?? 0),
673
+ lastVersionName: String(data?.versionName ?? "0")
674
+ };
675
+ },
676
+ async validateCredentials(config) {
677
+ const creds = credSchema2.parse(config);
678
+ await getToken2(creds.client_id, creds.client_secret);
679
+ },
680
+ async upload(ctx) {
681
+ const creds = credSchema2.parse(ctx.config);
682
+ const fileName = path4.basename(ctx.filePath);
683
+ ctx.onProgress({ step: "getToken" });
684
+ const token = await getToken2(creds.client_id, creds.client_secret);
685
+ const auth = `Bearer ${token}`;
686
+ ctx.onProgress({ step: "getAppId" });
687
+ const appId = await getAppId2(token, ctx.apkInfo.applicationId);
688
+ const client = createHttpClient();
689
+ ctx.onProgress({ step: "getAppInfo" });
690
+ const appInfoResp = await client.get(`${BASE_URL2}/openapi/v1/publish/get-app-detail`, {
691
+ headers: { Authorization: auth },
692
+ params: { appId }
693
+ });
694
+ checkHonorResult(appInfoResp.data, "getAppInfo");
695
+ const langInfo = appInfoResp.data.data?.languageInfo?.[0] ?? {};
696
+ ctx.onProgress({ step: "getUploadUrl" });
697
+ const sha256 = await fileSha256(ctx.filePath);
698
+ const uploadUrlResp = await client.post(
699
+ `${BASE_URL2}/openapi/v1/publish/get-file-upload-url`,
700
+ [{ fileName, fileType: 100, fileSize: ctx.apkInfo.size, fileSha256: sha256 }],
701
+ { headers: { Authorization: auth }, params: { appId } }
702
+ );
703
+ checkHonorResult(uploadUrlResp.data, "getUploadUrl");
704
+ const uploadInfo = uploadUrlResp.data.data[0];
705
+ ctx.onProgress({ step: "uploading", percent: 0 });
706
+ const form = new FormData();
707
+ form.append("file", createReadStream3(ctx.filePath), { filename: fileName });
708
+ const uploadResp = await client.post(uploadInfo.url, form, {
709
+ headers: { Authorization: auth, ...form.getHeaders() },
710
+ maxBodyLength: Infinity,
711
+ onUploadProgress: (e) => {
712
+ if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
713
+ }
714
+ });
715
+ if (uploadResp.data?.code !== 0 && uploadResp.status >= 400) {
716
+ throw new ApkpubError({
717
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
718
+ channel: "honor",
719
+ step: "uploadFile",
720
+ message: "\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25",
721
+ retryable: true
722
+ });
723
+ }
724
+ ctx.onProgress({ step: "bindApk" });
725
+ await client.post(
726
+ `${BASE_URL2}/openapi/v1/publish/update-file-info`,
727
+ { fileInfoList: [{ objectId: uploadInfo.objectId }] },
728
+ { headers: { Authorization: auth }, params: { appId } }
729
+ );
730
+ ctx.onProgress({ step: "updateDesc" });
731
+ await client.post(
732
+ `${BASE_URL2}/openapi/v1/publish/update-language-info`,
733
+ {
734
+ languageInfoList: [{
735
+ appName: langInfo.appName,
736
+ intro: langInfo.intro,
737
+ desc: ctx.desc,
738
+ briefIntro: langInfo.briefIntro
739
+ }]
740
+ },
741
+ { headers: { Authorization: auth }, params: { appId } }
742
+ );
743
+ ctx.onProgress({ step: "submit" });
744
+ await client.post(`${BASE_URL2}/openapi/v1/publish/submit-audit`, { releaseType: 1 }, {
745
+ headers: { Authorization: auth },
746
+ params: { appId }
747
+ });
748
+ ctx.onProgress({ step: "done", percent: 100 });
749
+ return { message: "\u63D0\u4EA4\u5BA1\u6838\u6210\u529F" };
750
+ }
751
+ };
752
+
753
+ // src/channels/mi/index.ts
754
+ import { z as z4 } from "zod";
755
+ import { createReadStream as createReadStream4 } from "fs";
756
+ import FormData2 from "form-data";
757
+
758
+ // src/signers/rsa.ts
759
+ import { createPublicKey, publicEncrypt, constants } from "crypto";
760
+ var GROUP_SIZE = 128;
761
+ var ENCRYPT_GROUP_SIZE = GROUP_SIZE - 11;
762
+ function rsaEncrypt(content, publicKeyPem) {
763
+ const pem = normalizePublicKey(publicKeyPem);
764
+ const publicKey = createPublicKey(pem);
765
+ const data = Buffer.from(content, "utf8");
766
+ const chunks = [];
767
+ let offset = 0;
768
+ while (offset < data.length) {
769
+ const remain = data.length - offset;
770
+ const segSize = Math.min(remain, ENCRYPT_GROUP_SIZE);
771
+ const segment = data.subarray(offset, offset + segSize);
772
+ const encrypted = publicEncrypt(
773
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
774
+ segment
775
+ );
776
+ chunks.push(encrypted);
777
+ offset += segSize;
778
+ }
779
+ return Buffer.concat(chunks).toString("hex");
780
+ }
781
+ function normalizePublicKey(key) {
782
+ const trimmed = key.trim();
783
+ if (trimmed.includes("BEGIN CERTIFICATE")) {
784
+ return trimmed;
785
+ }
786
+ if (trimmed.includes("BEGIN PUBLIC KEY")) {
787
+ return trimmed;
788
+ }
789
+ return `-----BEGIN CERTIFICATE-----
790
+ ${trimmed}
791
+ -----END CERTIFICATE-----`;
792
+ }
793
+
794
+ // src/signers/md5.ts
795
+ import { createHash as createHash2 } from "crypto";
796
+ function md5Hex(input2) {
797
+ return createHash2("md5").update(input2, "utf8").digest("hex");
798
+ }
799
+ function md5Sign(data, signKey) {
800
+ return md5Hex(data + signKey);
801
+ }
802
+
803
+ // src/channels/mi/index.ts
804
+ var DOMAIN = "https://api.developer.xiaomi.com/devupload";
805
+ var QUERY_URL = `${DOMAIN}/dev/query`;
806
+ var PUSH_URL = `${DOMAIN}/dev/push`;
807
+ var credSchema3 = z4.object({
808
+ account: z4.string().min(1),
809
+ publicKey: z4.string().min(1),
810
+ privateKey: z4.string().min(1)
811
+ });
812
+ function buildSig(privateKey, hashes) {
813
+ const sig = JSON.stringify({
814
+ password: privateKey,
815
+ sig: hashes.map((h) => ({ name: h.name, hash: h.hash }))
816
+ });
817
+ return sig;
818
+ }
819
+ var miChannel = {
820
+ name: "mi",
821
+ label: "\u5C0F\u7C73",
822
+ type: "market",
823
+ fileNameIdentify: "MI",
824
+ credentialSchema: credSchema3,
825
+ async getMarketState(appId, config) {
826
+ const creds = credSchema3.parse(config);
827
+ const requestData = JSON.stringify({ userName: creds.account, packageName: appId });
828
+ const sigData = buildSig(creds.privateKey, [{ name: "RequestData", hash: md5Hex(requestData) }]);
829
+ const client = createHttpClient();
830
+ const form = new URLSearchParams();
831
+ form.set("RequestData", requestData);
832
+ form.set("SIG", rsaEncrypt(sigData, creds.publicKey));
833
+ const resp = await client.post(QUERY_URL, form.toString(), {
834
+ headers: { "content-type": "application/x-www-form-urlencoded" }
835
+ });
836
+ checkMiResult(resp.data, "\u83B7\u53D6App\u4FE1\u606F");
837
+ const info = resp.data.packageInfo ?? resp.data;
838
+ return {
839
+ reviewState: "online",
840
+ enableSubmit: true,
841
+ lastVersionCode: Number(info?.versionCode ?? 0),
842
+ lastVersionName: String(info?.versionName ?? "0")
843
+ };
844
+ },
845
+ async validateCredentials(config) {
846
+ credSchema3.parse(config);
847
+ },
848
+ async upload(ctx) {
849
+ const creds = credSchema3.parse(ctx.config);
850
+ ctx.onProgress({ step: "getAppInfo" });
851
+ const marketState = await miChannel.getMarketState(ctx.apkInfo.applicationId, ctx.config);
852
+ const requestData = JSON.stringify({
853
+ userName: creds.account,
854
+ synchroType: 1,
855
+ appInfo: {
856
+ appName: marketState?.lastVersionName ?? ctx.apkInfo.versionName,
857
+ packageName: ctx.apkInfo.applicationId,
858
+ updateDesc: ctx.desc
859
+ }
860
+ });
861
+ const apkHash = await fileMd5(ctx.filePath);
862
+ const sigData = buildSig(creds.privateKey, [
863
+ { name: "RequestData", hash: md5Hex(requestData) },
864
+ { name: "apk", hash: apkHash }
865
+ ]);
866
+ ctx.onProgress({ step: "uploading", percent: 0 });
867
+ const form = new FormData2();
868
+ form.append("apk", createReadStream4(ctx.filePath), { filename: "app.apk" });
869
+ form.append("RequestData", requestData);
870
+ form.append("SIG", rsaEncrypt(sigData, creds.publicKey));
871
+ const client = createHttpClient({ timeout: 6e5 });
872
+ const resp = await client.post(PUSH_URL, form, {
873
+ headers: form.getHeaders(),
874
+ maxBodyLength: Infinity,
875
+ onUploadProgress: (e) => {
876
+ if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
877
+ }
878
+ });
879
+ checkMiResult(resp.data, "\u4E0A\u4F20Apk");
880
+ ctx.onProgress({ step: "done", percent: 100 });
881
+ return { message: "\u63D0\u4EA4\u6210\u529F" };
882
+ }
883
+ };
884
+ function checkMiResult(data, action) {
885
+ if (data.result !== 0) {
886
+ throw new ApkpubError({
887
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
888
+ channel: "mi",
889
+ message: `${action}\u5931\u8D25: ${data.message ?? "\u672A\u77E5\u9519\u8BEF"}`,
890
+ retryable: false
891
+ });
892
+ }
893
+ }
894
+
895
+ // src/channels/oppo/index.ts
896
+ import { z as z5 } from "zod";
897
+ import { createReadStream as createReadStream5 } from "fs";
898
+ import FormData3 from "form-data";
899
+
900
+ // src/signers/hmac.ts
901
+ import { createHmac } from "crypto";
902
+ function hmacSha256(data, secret) {
903
+ return createHmac("sha256", secret).update(data, "utf8").digest("hex");
904
+ }
905
+ function signSortedParams(params, secret) {
906
+ const keys = Object.keys(params).sort();
907
+ const pairs = keys.filter((k) => params[k] !== void 0 && params[k] !== null).map((k) => `${k}=${params[k]}`);
908
+ return hmacSha256(pairs.join("&"), secret);
909
+ }
910
+ function vivoSignParams(accessKey, accessSecret, method, originParams) {
911
+ const params = { ...originParams };
912
+ params.access_key = accessKey;
913
+ params.timestamp = String(Date.now());
914
+ params.method = method;
915
+ params.v = "1.0";
916
+ params.sign_method = "HMAC-SHA256";
917
+ params.format = "json";
918
+ params.target_app_key = "developer";
919
+ const keys = Object.keys(params).sort();
920
+ const data = keys.map((k) => `${k}=${params[k]}`).join("&");
921
+ params.sign = hmacSha256(data, accessSecret);
922
+ return params;
923
+ }
924
+
925
+ // src/channels/oppo/index.ts
926
+ var DOMAIN2 = "https://oop-openapi-cn.heytapmobi.com";
927
+ var credSchema4 = z5.object({
928
+ client_id: z5.string().min(1),
929
+ client_secret: z5.string().min(1)
930
+ });
931
+ function buildSignedUrl(originUrl, params, token, clientSecret, appendQuery) {
932
+ const timestamp = String(Math.floor(Date.now() / 1e3));
933
+ const allParams = { ...params, access_token: token, timestamp };
934
+ const url = new URL(originUrl);
935
+ if (appendQuery) {
936
+ for (const [k, v] of Object.entries(allParams)) {
937
+ url.searchParams.set(k, v);
938
+ }
939
+ }
940
+ url.searchParams.set("access_token", token);
941
+ url.searchParams.set("timestamp", timestamp);
942
+ url.searchParams.set("api_sign", signSortedParams(allParams, clientSecret));
943
+ return url.toString();
944
+ }
945
+ var oppoChannel = {
946
+ name: "oppo",
947
+ label: "OPPO",
948
+ type: "market",
949
+ fileNameIdentify: "OPPO",
950
+ credentialSchema: credSchema4,
951
+ async getMarketState(appId, config) {
952
+ const creds = credSchema4.parse(config);
953
+ const token = await getToken3(creds);
954
+ const client = createHttpClient();
955
+ const url = buildSignedUrl(`${DOMAIN2}/resource/v1/app/info`, { pkg_name: appId }, token, creds.client_secret, true);
956
+ const resp = await client.get(url);
957
+ checkOppoResult(resp.data, "\u83B7\u53D6App\u4FE1\u606F");
958
+ const data = resp.data.data;
959
+ return {
960
+ reviewState: "online",
961
+ enableSubmit: true,
962
+ lastVersionCode: Number(data?.version_code ?? 0),
963
+ lastVersionName: String(data?.version_name ?? "0")
964
+ };
965
+ },
966
+ async validateCredentials(config) {
967
+ const creds = credSchema4.parse(config);
968
+ await getToken3(creds);
969
+ },
970
+ async upload(ctx) {
971
+ const creds = credSchema4.parse(ctx.config);
972
+ ctx.onProgress({ step: "getToken" });
973
+ const token = await getToken3(creds);
974
+ const client = createHttpClient();
975
+ ctx.onProgress({ step: "getAppInfo" });
976
+ const appInfoUrl = buildSignedUrl(
977
+ `${DOMAIN2}/resource/v1/app/info`,
978
+ { pkg_name: ctx.apkInfo.applicationId },
979
+ token,
980
+ creds.client_secret,
981
+ true
982
+ );
983
+ const appInfoResp = await client.get(appInfoUrl);
984
+ checkOppoResult(appInfoResp.data, "\u83B7\u53D6App\u4FE1\u606F");
985
+ const appInfo = appInfoResp.data.data;
986
+ ctx.onProgress({ step: "getUploadUrl" });
987
+ const uploadUrlResp = await client.get(
988
+ buildSignedUrl(`${DOMAIN2}/resource/v1/upload/get-upload-url`, {}, token, creds.client_secret, true)
989
+ );
990
+ checkOppoResult(uploadUrlResp.data, "\u83B7\u53D6\u4E0A\u4F20url");
991
+ const { upload_url: rawUploadUrl, sign } = uploadUrlResp.data.data;
992
+ ctx.onProgress({ step: "uploading", percent: 0 });
993
+ const signedUploadUrl = buildSignedUrl(
994
+ rawUploadUrl,
995
+ { type: "apk", sign },
996
+ token,
997
+ creds.client_secret,
998
+ false
999
+ );
1000
+ const form = new FormData3();
1001
+ form.append("file", createReadStream5(ctx.filePath), { filename: "app.apk" });
1002
+ form.append("type", "apk");
1003
+ form.append("sign", sign);
1004
+ const uploadResp = await client.post(signedUploadUrl, form, {
1005
+ headers: form.getHeaders(),
1006
+ maxBodyLength: Infinity,
1007
+ onUploadProgress: (e) => {
1008
+ if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
1009
+ }
1010
+ });
1011
+ checkOppoResult(uploadResp.data, "\u4E0A\u4F20Apk");
1012
+ const apkResult = uploadResp.data.data;
1013
+ ctx.onProgress({ step: "submit" });
1014
+ const apkUrl = JSON.stringify([{ url: apkResult.url, md5: apkResult.md5, cpu_code: 0 }]);
1015
+ const submitParams = {
1016
+ pkg_name: ctx.apkInfo.applicationId,
1017
+ version_code: String(ctx.apkInfo.versionCode),
1018
+ apk_url: apkUrl,
1019
+ update_desc: ctx.desc,
1020
+ online_type: "1",
1021
+ second_category_id: String(appInfo.second_category_id ?? appInfo.secondCategory ?? ""),
1022
+ third_category_id: String(appInfo.third_category_id ?? appInfo.thirdCategory ?? ""),
1023
+ summary: appInfo.summary ?? "",
1024
+ detail_desc: appInfo.detail_desc ?? appInfo.detailDesc ?? "",
1025
+ privacy_source_url: appInfo.privacy_source_url ?? appInfo.privacyUrl ?? "",
1026
+ icon_url: appInfo.icon_url ?? appInfo.iconUrl ?? "",
1027
+ pic_url: appInfo.pic_url ?? appInfo.picUrl ?? "",
1028
+ test_desc: appInfo.test_desc ?? appInfo.testDesc ?? "",
1029
+ business_username: appInfo.business_username ?? appInfo.businessUsername ?? "",
1030
+ business_email: appInfo.business_email ?? appInfo.businessEmail ?? "",
1031
+ business_mobile: appInfo.business_mobile ?? appInfo.businessMobile ?? "",
1032
+ copyright_url: appInfo.copyright_url ?? appInfo.copyrightUrl ?? ""
1033
+ };
1034
+ const submitUrl = buildSignedUrl(`${DOMAIN2}/resource/v1/app/upd`, submitParams, token, creds.client_secret, false);
1035
+ const submitForm = new URLSearchParams(submitParams);
1036
+ const submitResp = await client.post(submitUrl, submitForm.toString(), {
1037
+ headers: { "content-type": "application/x-www-form-urlencoded" }
1038
+ });
1039
+ checkOppoResult(submitResp.data, "\u63D0\u4EA4\u7248\u672C");
1040
+ ctx.onProgress({ step: "done", percent: 100 });
1041
+ return { message: "\u63D0\u4EA4\u6210\u529F" };
1042
+ }
1043
+ };
1044
+ async function getToken3(creds) {
1045
+ const client = createHttpClient();
1046
+ const resp = await client.get(`${DOMAIN2}/developer/v1/token`, {
1047
+ params: { client_id: creds.client_id, client_secret: creds.client_secret }
1048
+ });
1049
+ checkOppoResult(resp.data, "\u83B7\u53D6token");
1050
+ return resp.data.data.access_token;
1051
+ }
1052
+ function checkOppoResult(data, action) {
1053
+ if (data.errno !== 0) {
1054
+ throw new ApkpubError({
1055
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
1056
+ channel: "oppo",
1057
+ message: `${action}\u5931\u8D25: ${data.data?.message ?? "\u672A\u77E5\u9519\u8BEF"}`,
1058
+ retryable: false
1059
+ });
1060
+ }
1061
+ }
1062
+
1063
+ // src/channels/vivo/index.ts
1064
+ import { z as z6 } from "zod";
1065
+ import { createReadStream as createReadStream6 } from "fs";
1066
+ import FormData4 from "form-data";
1067
+ var DOMAIN3 = "https://developer-api.vivo.com.cn/router/rest";
1068
+ var credSchema5 = z6.object({
1069
+ access_key: z6.string().min(1),
1070
+ access_secret: z6.string().min(1)
1071
+ });
1072
+ function buildUrl(method, params, accessKey, accessSecret) {
1073
+ const signed = vivoSignParams(accessKey, accessSecret, method, params);
1074
+ const url = new URL(DOMAIN3);
1075
+ for (const [k, v] of Object.entries(signed)) {
1076
+ url.searchParams.set(k, v);
1077
+ }
1078
+ return url.toString();
1079
+ }
1080
+ var vivoChannel = {
1081
+ name: "vivo",
1082
+ label: "VIVO",
1083
+ type: "market",
1084
+ fileNameIdentify: "VIVO",
1085
+ credentialSchema: credSchema5,
1086
+ async getMarketState(appId, config) {
1087
+ const creds = credSchema5.parse(config);
1088
+ const client = createHttpClient();
1089
+ const url = buildUrl("app.query.details", { packageName: appId }, creds.access_key, creds.access_secret);
1090
+ const resp = await client.get(url);
1091
+ checkVivoResult(resp.data, "\u67E5\u8BE2\u5E94\u7528\u8BE6\u60C5");
1092
+ const data = resp.data.data;
1093
+ return {
1094
+ reviewState: "online",
1095
+ enableSubmit: true,
1096
+ lastVersionCode: Number(data?.versionCode ?? 0),
1097
+ lastVersionName: String(data?.versionName ?? "0")
1098
+ };
1099
+ },
1100
+ async validateCredentials(config) {
1101
+ credSchema5.parse(config);
1102
+ },
1103
+ async upload(ctx) {
1104
+ const creds = credSchema5.parse(ctx.config);
1105
+ const client = createHttpClient({ timeout: 6e5 });
1106
+ ctx.onProgress({ step: "getAppInfo" });
1107
+ const appInfoUrl = buildUrl(
1108
+ "app.query.details",
1109
+ { packageName: ctx.apkInfo.applicationId },
1110
+ creds.access_key,
1111
+ creds.access_secret
1112
+ );
1113
+ const appInfoResp = await client.get(appInfoUrl);
1114
+ checkVivoResult(appInfoResp.data, "\u67E5\u8BE2\u5E94\u7528\u8BE6\u60C5");
1115
+ const appInfo = appInfoResp.data.data;
1116
+ ctx.onProgress({ step: "uploading", percent: 0 });
1117
+ const fileMd5Hash = await fileMd5(ctx.filePath);
1118
+ const uploadUrl = buildUrl(
1119
+ "app.upload.apk.app",
1120
+ { packageName: ctx.apkInfo.applicationId, fileMd5: fileMd5Hash },
1121
+ creds.access_key,
1122
+ creds.access_secret
1123
+ );
1124
+ const form = new FormData4();
1125
+ form.append("file", createReadStream6(ctx.filePath), { filename: "app.apk" });
1126
+ const uploadResp = await client.post(uploadUrl, form, {
1127
+ headers: form.getHeaders(),
1128
+ maxBodyLength: Infinity,
1129
+ onUploadProgress: (e) => {
1130
+ if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
1131
+ }
1132
+ });
1133
+ checkVivoResult(uploadResp.data, "\u4E0A\u4F20apk");
1134
+ const apkResult = uploadResp.data.data;
1135
+ ctx.onProgress({ step: "submit" });
1136
+ const submitUrl = buildUrl(
1137
+ "app.sync.update.app",
1138
+ {
1139
+ packageName: apkResult.packageName,
1140
+ versionCode: String(apkResult.versionCode),
1141
+ apk: apkResult.serialnumber,
1142
+ fileMd5: apkResult.fileMd5,
1143
+ onlineType: String(appInfo.onlineType ?? 1),
1144
+ updateDesc: ctx.desc
1145
+ },
1146
+ creds.access_key,
1147
+ creds.access_secret
1148
+ );
1149
+ const submitResp = await client.get(submitUrl);
1150
+ checkVivoResult(submitResp.data, "\u63D0\u4EA4\u66F4\u65B0");
1151
+ ctx.onProgress({ step: "done", percent: 100 });
1152
+ return { message: "\u63D0\u4EA4\u6210\u529F" };
1153
+ }
1154
+ };
1155
+ function checkVivoResult(data, action) {
1156
+ const subCode = Number(data.subCode ?? -1);
1157
+ if (data.code !== 0 || subCode !== 0) {
1158
+ throw new ApkpubError({
1159
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
1160
+ channel: "vivo",
1161
+ message: `${action}\u5931\u8D25: ${data.msg ?? "\u672A\u77E5\u9519\u8BEF"}`,
1162
+ retryable: false
1163
+ });
1164
+ }
1165
+ }
1166
+
1167
+ // src/channels/custom/oss.ts
1168
+ import OSS from "ali-oss";
1169
+ import path5 from "path";
1170
+
1171
+ // src/utils/template.ts
1172
+ var PLACEHOLDER_RE = /\{(\w+)\}/g;
1173
+ function renderTemplate(template, ctx) {
1174
+ const rendered = template.replace(PLACEHOLDER_RE, (_, key) => {
1175
+ const map = {
1176
+ appId: ctx.appId,
1177
+ versionName: ctx.versionName,
1178
+ versionCode: ctx.versionCode,
1179
+ fileName: ctx.fileName,
1180
+ objectKey: ctx.objectKey ?? ""
1181
+ };
1182
+ const value = map[key];
1183
+ if (value === void 0) return `{${key}}`;
1184
+ return String(value);
1185
+ });
1186
+ return sanitizePath(rendered);
1187
+ }
1188
+ function sanitizePath(path8) {
1189
+ const normalized = path8.replace(/\\/g, "/");
1190
+ if (normalized.includes("..") || normalized.startsWith("/")) {
1191
+ throw new ApkpubError({
1192
+ code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
1193
+ message: `\u8DEF\u5F84\u6A21\u677F\u5305\u542B\u975E\u6CD5\u5B57\u7B26: ${path8}`,
1194
+ retryable: false
1195
+ });
1196
+ }
1197
+ return normalized.replace(/^\/+/, "");
1198
+ }
1199
+
1200
+ // src/channels/custom/oss.ts
1201
+ async function fetchStsToken(stsTokenUrl, signKey, contextB) {
1202
+ assertSafeUrl(stsTokenUrl, "STS Token URL");
1203
+ const sign = md5Sign(contextB, signKey);
1204
+ const client = createHttpClient();
1205
+ const response = await client.post(
1206
+ stsTokenUrl,
1207
+ new URLSearchParams({ c: JSON.stringify({ mode: "text" }), b: contextB, sign }).toString(),
1208
+ { headers: { "content-type": "application/x-www-form-urlencoded" } }
1209
+ );
1210
+ if (response.status !== 200) {
1211
+ throw new ApkpubError({
1212
+ code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
1213
+ message: `\u83B7\u53D6 STS Token \u5931\u8D25: HTTP ${response.status}`,
1214
+ retryable: response.status >= 500
1215
+ });
1216
+ }
1217
+ const data = response.data?.data;
1218
+ if (!data?.AccessKeyId) {
1219
+ throw new ApkpubError({
1220
+ code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
1221
+ message: "STS Token \u54CD\u5E94\u683C\u5F0F\u65E0\u6548",
1222
+ retryable: false
1223
+ });
1224
+ }
1225
+ return {
1226
+ accessKeyId: data.AccessKeyId,
1227
+ accessKeySecret: data.AccessKeySecret,
1228
+ securityToken: data.SecurityToken,
1229
+ expiration: data.Expiration
1230
+ };
1231
+ }
1232
+ async function createOssClient(config) {
1233
+ if (!config.endpoint || !config.bucket) {
1234
+ throw new ApkpubError({
1235
+ code: "CONFIG_INVALID" /* CONFIG_INVALID */,
1236
+ message: "OSS \u6E20\u9053\u9700\u8981\u914D\u7F6E endpoint \u548C bucket",
1237
+ retryable: false
1238
+ });
1239
+ }
1240
+ assertSafeUrl(config.endpoint.startsWith("http") ? config.endpoint : `https://${config.endpoint}`, "OSS endpoint");
1241
+ if (!config.auth) {
1242
+ throw new ApkpubError({
1243
+ code: "CONFIG_INVALID" /* CONFIG_INVALID */,
1244
+ message: "OSS \u6E20\u9053\u9700\u8981\u914D\u7F6E auth",
1245
+ retryable: false
1246
+ });
1247
+ }
1248
+ if (config.auth.mode === "ak") {
1249
+ return new OSS({
1250
+ region: extractRegion(config.endpoint),
1251
+ accessKeyId: config.auth.accessKeyId,
1252
+ accessKeySecret: config.auth.accessKeySecret,
1253
+ bucket: config.bucket,
1254
+ endpoint: config.endpoint
1255
+ });
1256
+ }
1257
+ const sts = await fetchStsToken(config.auth.stsTokenUrl, config.auth.signKey, config.auth.contextB);
1258
+ return new OSS({
1259
+ region: extractRegion(config.endpoint),
1260
+ accessKeyId: sts.accessKeyId,
1261
+ accessKeySecret: sts.accessKeySecret,
1262
+ stsToken: sts.securityToken,
1263
+ bucket: config.bucket,
1264
+ endpoint: config.endpoint,
1265
+ refreshSTSToken: async () => {
1266
+ const auth = config.auth;
1267
+ if (!auth || auth.mode !== "sts") {
1268
+ throw new ApkpubError({ code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */, message: "STS \u914D\u7F6E\u65E0\u6548", retryable: false });
1269
+ }
1270
+ const refreshed = await fetchStsToken(auth.stsTokenUrl, auth.signKey, auth.contextB);
1271
+ return {
1272
+ accessKeyId: refreshed.accessKeyId,
1273
+ accessKeySecret: refreshed.accessKeySecret,
1274
+ stsToken: refreshed.securityToken
1275
+ };
1276
+ }
1277
+ });
1278
+ }
1279
+ function extractRegion(endpoint) {
1280
+ const match = endpoint.match(/oss-([a-z0-9-]+)\./);
1281
+ return match?.[1] ?? "cn-beijing";
1282
+ }
1283
+ async function uploadToOss(ctx, config) {
1284
+ ctx.onProgress({ step: "connecting" });
1285
+ const fileName = path5.basename(ctx.filePath);
1286
+ const objectKey = renderTemplate(config.objectKeyTemplate, {
1287
+ appId: ctx.apkInfo.applicationId,
1288
+ versionName: ctx.apkInfo.versionName,
1289
+ versionCode: ctx.apkInfo.versionCode,
1290
+ fileName
1291
+ });
1292
+ const client = await createOssClient(config);
1293
+ ctx.onProgress({ step: "uploading", percent: 0 });
1294
+ await client.multipartUpload(objectKey, ctx.filePath, {
1295
+ progress: (p) => {
1296
+ ctx.onProgress({ step: "uploading", percent: Math.round(p * 100) });
1297
+ }
1298
+ });
1299
+ const downloadUrl = renderTemplate(config.downloadUrlTemplate, {
1300
+ appId: ctx.apkInfo.applicationId,
1301
+ versionName: ctx.apkInfo.versionName,
1302
+ versionCode: ctx.apkInfo.versionCode,
1303
+ fileName,
1304
+ objectKey
1305
+ });
1306
+ ctx.onProgress({ step: "done", percent: 100 });
1307
+ return { downloadUrl, message: "\u4E0A\u4F20\u6210\u529F" };
1308
+ }
1309
+
1310
+ // src/channels/custom/http.ts
1311
+ import { createReadStream as createReadStream7 } from "fs";
1312
+ import { readFile as readFile2 } from "fs/promises";
1313
+ import path6 from "path";
1314
+ import FormData5 from "form-data";
1315
+ async function uploadToHttp(ctx, config) {
1316
+ if (!config.uploadUrl) {
1317
+ throw new ApkpubError({
1318
+ code: "CONFIG_INVALID" /* CONFIG_INVALID */,
1319
+ message: "HTTP \u6E20\u9053\u9700\u8981\u914D\u7F6E uploadUrl",
1320
+ retryable: false
1321
+ });
1322
+ }
1323
+ const fileName = path6.basename(ctx.filePath);
1324
+ const objectKey = renderTemplate(config.objectKeyTemplate, {
1325
+ appId: ctx.apkInfo.applicationId,
1326
+ versionName: ctx.apkInfo.versionName,
1327
+ versionCode: ctx.apkInfo.versionCode,
1328
+ fileName
1329
+ });
1330
+ const uploadUrl = renderTemplate(config.uploadUrl, {
1331
+ appId: ctx.apkInfo.applicationId,
1332
+ versionName: ctx.apkInfo.versionName,
1333
+ versionCode: ctx.apkInfo.versionCode,
1334
+ fileName,
1335
+ objectKey
1336
+ });
1337
+ assertSafeUrl(uploadUrl, "\u4E0A\u4F20\u5730\u5740");
1338
+ const client = createHttpClient({ timeout: 3e5 });
1339
+ const method = config.method ?? "PUT";
1340
+ ctx.onProgress({ step: "uploading", percent: 0 });
1341
+ if (method === "PUT") {
1342
+ const data = await readFile2(ctx.filePath);
1343
+ const response = await client.request({
1344
+ method: "PUT",
1345
+ url: uploadUrl,
1346
+ data,
1347
+ headers: {
1348
+ "Content-Type": "application/vnd.android.package-archive",
1349
+ ...config.headers
1350
+ },
1351
+ maxBodyLength: Infinity,
1352
+ signal: ctx.signal,
1353
+ onUploadProgress: (e) => {
1354
+ if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
1355
+ }
1356
+ });
1357
+ if (response.status < 200 || response.status >= 300) {
1358
+ throw new ApkpubError({
1359
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
1360
+ message: `HTTP \u4E0A\u4F20\u5931\u8D25: ${response.status}`,
1361
+ retryable: response.status >= 500
1362
+ });
1363
+ }
1364
+ } else {
1365
+ const form = new FormData5();
1366
+ const field = config.formField ?? "file";
1367
+ form.append(field, createReadStream7(ctx.filePath), { filename: fileName });
1368
+ const response = await client.post(uploadUrl, form, {
1369
+ headers: { ...form.getHeaders(), ...config.headers },
1370
+ maxBodyLength: Infinity,
1371
+ signal: ctx.signal,
1372
+ onUploadProgress: (e) => {
1373
+ if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
1374
+ }
1375
+ });
1376
+ if (response.status < 200 || response.status >= 300) {
1377
+ throw new ApkpubError({
1378
+ code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
1379
+ message: `HTTP \u4E0A\u4F20\u5931\u8D25: ${response.status}`,
1380
+ retryable: response.status >= 500
1381
+ });
1382
+ }
1383
+ }
1384
+ const downloadUrl = renderTemplate(config.downloadUrlTemplate, {
1385
+ appId: ctx.apkInfo.applicationId,
1386
+ versionName: ctx.apkInfo.versionName,
1387
+ versionCode: ctx.apkInfo.versionCode,
1388
+ fileName,
1389
+ objectKey
1390
+ });
1391
+ ctx.onProgress({ step: "done", percent: 100 });
1392
+ return { downloadUrl, message: "\u4E0A\u4F20\u6210\u529F" };
1393
+ }
1394
+
1395
+ // src/channels/custom/index.ts
1396
+ function createCustomChannel(config) {
1397
+ return {
1398
+ name: config.name,
1399
+ label: config.name,
1400
+ type: "custom",
1401
+ fileNameIdentify: config.fileNameIdentify ?? config.name,
1402
+ credentialSchema: customChannelConfigSchema,
1403
+ async upload(ctx) {
1404
+ if (config.uploadType === "oss") {
1405
+ return uploadToOss(ctx, config);
1406
+ }
1407
+ return uploadToHttp(ctx, config);
1408
+ },
1409
+ async validateCredentials() {
1410
+ customChannelConfigSchema.parse(config);
1411
+ }
1412
+ };
1413
+ }
1414
+
1415
+ // src/channels/registry.ts
1416
+ var BUILTIN_MARKET_CHANNELS = [
1417
+ huaweiChannel,
1418
+ honorChannel,
1419
+ miChannel,
1420
+ oppoChannel,
1421
+ vivoChannel
1422
+ ];
1423
+ function loadChannelsFromConfig(appConfig) {
1424
+ const channels = [];
1425
+ const builtinNames = new Set(BUILTIN_MARKET_CHANNELS.map((c) => c.name));
1426
+ for (const chConfig of appConfig.channels) {
1427
+ if (!chConfig.enable) continue;
1428
+ if (chConfig.type === "custom") {
1429
+ channels.push(createCustomChannel(chConfig));
1430
+ } else if (builtinNames.has(chConfig.name)) {
1431
+ const builtin = BUILTIN_MARKET_CHANNELS.find((c) => c.name === chConfig.name);
1432
+ if (builtin) channels.push(builtin);
1433
+ }
1434
+ }
1435
+ return channels;
1436
+ }
1437
+ function resolveChannels(appConfig, channelNames) {
1438
+ const all = loadChannelsFromConfig(appConfig);
1439
+ if (!channelNames || channelNames.length === 0) {
1440
+ return all;
1441
+ }
1442
+ const resolved = [];
1443
+ for (const name of channelNames) {
1444
+ const ch = all.find((c) => c.name === name);
1445
+ if (!ch) {
1446
+ throw new ApkpubError({
1447
+ code: "CHANNEL_NOT_FOUND" /* CHANNEL_NOT_FOUND */,
1448
+ message: `\u6E20\u9053 "${name}" \u672A\u5728\u914D\u7F6E\u4E2D\u542F\u7528\u6216\u4E0D\u5B58\u5728`,
1449
+ retryable: false
1450
+ });
1451
+ }
1452
+ resolved.push(ch);
1453
+ }
1454
+ return resolved;
1455
+ }
1456
+ var MARKET_CREDENTIALS = {
1457
+ huawei: [
1458
+ { name: "client_id", required: true, description: "\u534E\u4E3A Connect API \u5BA2\u6237\u7AEF ID" },
1459
+ { name: "client_secret", required: true, description: "\u534E\u4E3A Connect API \u5BC6\u94A5" }
1460
+ ],
1461
+ honor: [
1462
+ { name: "client_id", required: true, description: "\u8363\u8000\u5F00\u53D1\u8005\u51ED\u8BC1 ID" },
1463
+ { name: "client_secret", required: true, description: "\u8363\u8000\u5F00\u53D1\u8005\u51ED\u8BC1\u5BC6\u94A5" }
1464
+ ],
1465
+ mi: [
1466
+ { name: "account", required: true, description: "\u5C0F\u7C73\u5F00\u53D1\u8005\u8D26\u53F7\uFF08\u90AE\u7BB1\uFF09" },
1467
+ { name: "publicKey", required: true, description: "\u516C\u94A5\u8BC1\u4E66\u5185\u5BB9" },
1468
+ { name: "privateKey", required: true, description: "\u79C1\u94A5" }
1469
+ ],
1470
+ oppo: [
1471
+ { name: "client_id", required: true, description: "OPPO \u670D\u52A1\u7AEF\u5E94\u7528 ID" },
1472
+ { name: "client_secret", required: true, description: "OPPO \u670D\u52A1\u7AEF\u5E94\u7528\u5BC6\u94A5" }
1473
+ ],
1474
+ vivo: [
1475
+ { name: "access_key", required: true, description: "VIVO API Access Key" },
1476
+ { name: "access_secret", required: true, description: "VIVO API Access Secret" }
1477
+ ]
1478
+ };
1479
+ function getChannelMetas() {
1480
+ return BUILTIN_MARKET_CHANNELS.map((ch) => ({
1481
+ name: ch.name,
1482
+ label: ch.label,
1483
+ type: ch.type,
1484
+ fileNameIdentify: ch.fileNameIdentify,
1485
+ credentialFields: [
1486
+ ...MARKET_CREDENTIALS[ch.name] ?? [],
1487
+ { name: "fileNameIdentify", required: false, description: "\u591A\u6E20\u9053\u5305\u6587\u4EF6\u540D\u5339\u914D\u6807\u8BC6" }
1488
+ ]
1489
+ }));
1490
+ }
1491
+
1492
+ // src/utils/output.ts
1493
+ import Table from "cli-table3";
1494
+ import pc2 from "picocolors";
1495
+ function printJson(data) {
1496
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
1497
+ }
1498
+ function printPublishResult(result) {
1499
+ const table = new Table({
1500
+ head: ["\u6E20\u9053", "\u72B6\u6001", "\u4E0B\u8F7D\u94FE\u63A5", "\u9519\u8BEF"],
1501
+ colWidths: [12, 10, 40, 30]
1502
+ });
1503
+ for (const r of result.results) {
1504
+ const statusColor = r.status === "success" ? pc2.green : r.status === "failed" ? pc2.red : r.status === "dry_run" ? pc2.yellow : pc2.gray;
1505
+ table.push([
1506
+ r.label,
1507
+ statusColor(r.status),
1508
+ r.downloadUrl ?? "-",
1509
+ r.error?.message ?? "-"
1510
+ ]);
1511
+ }
1512
+ process.stderr.write("\n" + table.toString() + "\n");
1513
+ const { summary } = result;
1514
+ process.stderr.write(
1515
+ pc2.dim(`
1516
+ \u603B\u8BA1: ${summary.total} | \u6210\u529F: ${pc2.green(String(summary.success))} | \u5931\u8D25: ${pc2.red(String(summary.failed))} | \u8DF3\u8FC7: ${summary.skipped}
1517
+ `)
1518
+ );
1519
+ }
1520
+ function printStatusResults(results) {
1521
+ const table = new Table({
1522
+ head: ["\u6E20\u9053", "\u5BA1\u6838\u72B6\u6001", "\u7248\u672C\u53F7", "\u7248\u672C\u540D", "\u5907\u6CE8"]
1523
+ });
1524
+ for (const r of results) {
1525
+ table.push([r.label, r.state ?? "-", r.versionCode ?? "-", r.versionName ?? "-", r.error ?? "-"]);
1526
+ }
1527
+ process.stderr.write("\n" + table.toString() + "\n");
1528
+ }
1529
+ function isInteractive() {
1530
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1531
+ }
1532
+
1533
+ // src/core/result.ts
1534
+ function aggregateResults(results, dryRun = false) {
1535
+ const success = results.filter((r) => r.status === "success" || r.status === "dry_run").length;
1536
+ const failed = results.filter((r) => r.status === "failed").length;
1537
+ const skipped = results.filter((r) => r.status === "skipped").length;
1538
+ const ok = failed === 0;
1539
+ return {
1540
+ ok,
1541
+ dryRun,
1542
+ results,
1543
+ summary: { total: results.length, success, failed, skipped }
1544
+ };
1545
+ }
1546
+ function getExitCode(result) {
1547
+ if (result.ok) return 0 /* SUCCESS */;
1548
+ const { success, failed } = result.summary;
1549
+ if (failed > 0 && success > 0) return 4 /* PARTIAL_FAILURE */;
1550
+ if (failed > 0 && success === 0) {
1551
+ const versionErrors = result.results.some((r) => r.error?.code === "VERSION_TOO_LOW" /* VERSION_TOO_LOW */);
1552
+ if (versionErrors && failed === result.results.length) return 3 /* VERSION_CHECK_FAILED */;
1553
+ return 5 /* ALL_FAILED */;
1554
+ }
1555
+ return 1 /* INTERNAL */;
1556
+ }
1557
+ function channelResultFromError(name, label, err) {
1558
+ const apkErr = err instanceof ApkpubError ? err : new ApkpubError({ code: "INTERNAL" /* INTERNAL */, message: String(err) });
1559
+ return {
1560
+ name,
1561
+ label,
1562
+ status: "failed",
1563
+ retryable: apkErr.retryable,
1564
+ error: {
1565
+ code: apkErr.code,
1566
+ message: apkErr.message,
1567
+ step: apkErr.step,
1568
+ retryable: apkErr.retryable
1569
+ }
1570
+ };
1571
+ }
1572
+
1573
+ // src/cli/init.ts
1574
+ function registerInitCommand(program2) {
1575
+ program2.command("init").description("\u521B\u5EFA\u5E94\u7528\u914D\u7F6E").option("--name <name>", "\u5E94\u7528\u663E\u793A\u540D\u79F0").option("--app <id>", "\u5E94\u7528\u5305\u540D").option("--channels <names>", "\u542F\u7528\u7684\u5E02\u573A\u6E20\u9053\uFF0C\u9017\u53F7\u5206\u9694").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
1576
+ try {
1577
+ await ensureConfigDirs();
1578
+ let name = options.name;
1579
+ let applicationId = options.app;
1580
+ let selectedChannels = options.channels?.split(",").map((s) => s.trim()) ?? [];
1581
+ if (isInteractive() && !options.name) {
1582
+ name = await input({ message: "\u5E94\u7528\u663E\u793A\u540D\u79F0:" });
1583
+ applicationId = await input({ message: "\u5E94\u7528\u5305\u540D (applicationId):" });
1584
+ const metas2 = getChannelMetas();
1585
+ selectedChannels = await checkbox({
1586
+ message: "\u9009\u62E9\u8981\u542F\u7528\u7684\u5E02\u573A\u6E20\u9053:",
1587
+ choices: metas2.map((m) => ({ name: m.label, value: m.name }))
1588
+ });
1589
+ }
1590
+ if (!name || !applicationId) {
1591
+ throw new ApkpubError({
1592
+ code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
1593
+ message: "\u8BF7\u63D0\u4F9B --name \u548C --app\uFF0C\u6216\u5728\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u8FD0\u884C",
1594
+ retryable: false
1595
+ });
1596
+ }
1597
+ const metas = getChannelMetas();
1598
+ const channels = [];
1599
+ for (const meta of metas) {
1600
+ const enabled = selectedChannels.length === 0 || selectedChannels.includes(meta.name);
1601
+ if (!enabled && selectedChannels.length > 0) continue;
1602
+ if (selectedChannels.length > 0 && !selectedChannels.includes(meta.name)) continue;
1603
+ if (isInteractive() && (selectedChannels.length === 0 || selectedChannels.includes(meta.name))) {
1604
+ const enable = selectedChannels.length > 0 ? true : await confirm({ message: `\u542F\u7528 ${meta.label} \u6E20\u9053?`, default: false });
1605
+ if (!enable) continue;
1606
+ const params = [];
1607
+ for (const field of meta.credentialFields) {
1608
+ if (field.name === "fileNameIdentify") continue;
1609
+ const value = await input({
1610
+ message: `${meta.label} - ${field.name}${field.description ? ` (${field.description})` : ""}:`
1611
+ });
1612
+ params.push({ name: field.name, value });
1613
+ }
1614
+ params.push({ name: "fileNameIdentify", value: meta.fileNameIdentify });
1615
+ channels.push({ name: meta.name, type: "market", enable: true, params });
1616
+ } else if (selectedChannels.includes(meta.name)) {
1617
+ channels.push({
1618
+ name: meta.name,
1619
+ type: "market",
1620
+ enable: true,
1621
+ params: meta.credentialFields.filter((f) => f.name !== "fileNameIdentify").map((f) => ({ name: f.name, value: `\${${meta.name.toUpperCase()}_${f.name.toUpperCase()}}` }))
1622
+ });
1623
+ }
1624
+ }
1625
+ const config = {
1626
+ schemaVersion: CURRENT_SCHEMA_VERSION,
1627
+ name,
1628
+ applicationId,
1629
+ createTime: Date.now(),
1630
+ enableChannel: true,
1631
+ channels,
1632
+ extension: {}
1633
+ };
1634
+ await saveConfig(config);
1635
+ if (options.json) {
1636
+ printJson({ ok: true, config: { applicationId, name, channels: channels.map((c) => c.name) } });
1637
+ } else {
1638
+ process.stderr.write(`\u5DF2\u521B\u5EFA\u5E94\u7528\u914D\u7F6E: ${applicationId}
1639
+ `);
1640
+ process.stderr.write(`\u914D\u7F6E\u6587\u4EF6: ~/.apkpub/apps/${applicationId}.json
1641
+ `);
1642
+ }
1643
+ process.exit(0 /* SUCCESS */);
1644
+ } catch (err) {
1645
+ const message = err instanceof ApkpubError ? err.message : String(err);
1646
+ if (options.json) {
1647
+ printJson({ ok: false, error: { message } });
1648
+ } else {
1649
+ process.stderr.write(`\u9519\u8BEF: ${message}
1650
+ `);
1651
+ }
1652
+ process.exit(2 /* INVALID_ARGUMENT */);
1653
+ }
1654
+ });
1655
+ }
1656
+
1657
+ // src/cli/config.ts
1658
+ import { writeFile as writeFile2 } from "fs/promises";
1659
+ function registerConfigCommand(program2) {
1660
+ const config = program2.command("config").description("\u914D\u7F6E\u7BA1\u7406");
1661
+ config.command("list").description("\u5217\u51FA\u6240\u6709\u5E94\u7528\u914D\u7F6E").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
1662
+ const configs = await listConfigs();
1663
+ if (options.json) {
1664
+ printJson({ apps: configs.map((c) => ({ name: c.name, applicationId: c.applicationId })) });
1665
+ } else {
1666
+ for (const c of configs) {
1667
+ process.stderr.write(`${c.applicationId} - ${c.name}
1668
+ `);
1669
+ }
1670
+ }
1671
+ process.exit(0 /* SUCCESS */);
1672
+ });
1673
+ config.command("get <appId>").description("\u83B7\u53D6\u5E94\u7528\u914D\u7F6E").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (appId, options) => {
1674
+ try {
1675
+ const cfg = await loadConfig(appId);
1676
+ if (options.json) {
1677
+ printJson(stripSecrets(cfg));
1678
+ } else {
1679
+ process.stderr.write(JSON.stringify(stripSecrets(cfg), null, 2) + "\n");
1680
+ }
1681
+ process.exit(0 /* SUCCESS */);
1682
+ } catch (err) {
1683
+ handleError(err, options.json);
1684
+ }
1685
+ });
1686
+ config.command("export <appId>").description("\u5BFC\u51FA\u5E94\u7528\u914D\u7F6E\uFF08\u9ED8\u8BA4\u5265\u79BB\u5BC6\u94A5\uFF09").option("--include-secrets", "\u5305\u542B\u5BC6\u94A5\uFF08\u4E0D\u63A8\u8350\uFF09").option("-o, --output <file>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (appId, options) => {
1687
+ try {
1688
+ const content = await exportConfig(appId, { includeSecrets: options.includeSecrets });
1689
+ if (options.output) {
1690
+ await writeFile2(options.output, content, { mode: 384 });
1691
+ process.stderr.write(`\u5DF2\u5BFC\u51FA\u5230 ${options.output}
1692
+ `);
1693
+ } else {
1694
+ process.stdout.write(content + "\n");
1695
+ }
1696
+ process.exit(0 /* SUCCESS */);
1697
+ } catch (err) {
1698
+ handleError(err, false);
1699
+ }
1700
+ });
1701
+ config.command("import <file>").description("\u5BFC\u5165\u5E94\u7528\u914D\u7F6E").action(async (file) => {
1702
+ try {
1703
+ const cfg = await importConfig(file);
1704
+ process.stderr.write(`\u5DF2\u5BFC\u5165: ${cfg.applicationId}
1705
+ `);
1706
+ process.exit(0 /* SUCCESS */);
1707
+ } catch (err) {
1708
+ handleError(err, false);
1709
+ }
1710
+ });
1711
+ }
1712
+ function handleError(err, json) {
1713
+ const message = err instanceof ApkpubError ? err.message : String(err);
1714
+ if (json) {
1715
+ printJson({ ok: false, error: { message } });
1716
+ } else {
1717
+ process.stderr.write(`\u9519\u8BEF: ${message}
1718
+ `);
1719
+ }
1720
+ process.exit(2 /* INVALID_ARGUMENT */);
1721
+ }
1722
+
1723
+ // src/apk/ApkParser.ts
1724
+ import AppInfoParser from "app-info-parser";
1725
+ async function parseApk(filePath) {
1726
+ try {
1727
+ const parser = new AppInfoParser(filePath);
1728
+ const result = await parser.parse();
1729
+ const packageName = result.package;
1730
+ const versionCode = Number(result.versionCode);
1731
+ const versionName = String(result.versionName ?? "");
1732
+ if (!packageName) {
1733
+ throw new ApkpubError({
1734
+ code: "APK_PARSE_FAILED" /* APK_PARSE_FAILED */,
1735
+ message: "\u65E0\u6CD5\u4ECE APK \u4E2D\u8BFB\u53D6\u5305\u540D",
1736
+ retryable: false
1737
+ });
1738
+ }
1739
+ const size = await fileSize(filePath);
1740
+ return {
1741
+ filePath,
1742
+ applicationId: packageName,
1743
+ versionCode,
1744
+ versionName,
1745
+ size
1746
+ };
1747
+ } catch (err) {
1748
+ if (err instanceof ApkpubError) throw err;
1749
+ throw new ApkpubError({
1750
+ code: "APK_PARSE_FAILED" /* APK_PARSE_FAILED */,
1751
+ message: `APK \u89E3\u6790\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
1752
+ retryable: false,
1753
+ cause: err
1754
+ });
1755
+ }
1756
+ }
1757
+
1758
+ // src/cli/info.ts
1759
+ function registerInfoCommand(program2) {
1760
+ program2.command("info <apk>").description("\u89E3\u6790 APK\uFF0C\u8F93\u51FA\u5305\u540D\u3001\u7248\u672C\u53F7\u7B49\u4FE1\u606F").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (apk, options) => {
1761
+ try {
1762
+ const info = await parseApk(apk);
1763
+ const output = {
1764
+ filePath: info.filePath,
1765
+ applicationId: info.applicationId,
1766
+ versionCode: info.versionCode,
1767
+ versionName: info.versionName,
1768
+ size: info.size,
1769
+ sizeMB: (info.size / 1024 / 1024).toFixed(2)
1770
+ };
1771
+ if (options.json) {
1772
+ printJson(output);
1773
+ } else {
1774
+ process.stderr.write(`\u5305\u540D: ${info.applicationId}
1775
+ `);
1776
+ process.stderr.write(`\u7248\u672C\u53F7: ${info.versionCode}
1777
+ `);
1778
+ process.stderr.write(`\u7248\u672C\u540D: ${info.versionName}
1779
+ `);
1780
+ process.stderr.write(`\u5927\u5C0F: ${output.sizeMB} MB
1781
+ `);
1782
+ }
1783
+ process.exit(0 /* SUCCESS */);
1784
+ } catch (err) {
1785
+ const message = err instanceof ApkpubError ? err.message : String(err);
1786
+ if (options.json) {
1787
+ printJson({ ok: false, error: { message, code: err instanceof ApkpubError ? err.code : "INTERNAL" /* INTERNAL */ } });
1788
+ } else {
1789
+ process.stderr.write(`\u9519\u8BEF: ${message}
1790
+ `);
1791
+ }
1792
+ process.exit(2 /* INVALID_ARGUMENT */);
1793
+ }
1794
+ });
1795
+ }
1796
+
1797
+ // src/core/Dispatcher.ts
1798
+ import pLimit from "p-limit";
1799
+
1800
+ // src/secrets/resolver.ts
1801
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync } from "crypto";
1802
+ var ENV_PLACEHOLDER_RE = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
1803
+ async function resolveSecret(value, options = {}) {
1804
+ const envMatch = value.match(ENV_PLACEHOLDER_RE);
1805
+ if (envMatch) {
1806
+ const envVal = process.env[envMatch[1]];
1807
+ if (!envVal) {
1808
+ throw new ApkpubError({
1809
+ code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
1810
+ message: `\u73AF\u5883\u53D8\u91CF ${envMatch[1]} \u672A\u8BBE\u7F6E`,
1811
+ retryable: false
1812
+ });
1813
+ }
1814
+ return { value: envVal, source: "env" };
1815
+ }
1816
+ if (value.startsWith("keychain:")) {
1817
+ const key = value.slice("keychain:".length);
1818
+ const resolved = await resolveFromKeychain(options.service ?? "apkpub-cli", key);
1819
+ return { value: resolved, source: "keychain" };
1820
+ }
1821
+ if (value.startsWith("enc:")) {
1822
+ const resolved = await decryptValue(value.slice("enc:".length));
1823
+ return { value: resolved, source: "encrypted" };
1824
+ }
1825
+ if (looksLikeSecret(value)) {
1826
+ logger.warn("secrets", "\u68C0\u6D4B\u5230\u660E\u6587\u5BC6\u94A5\uFF0C\u5EFA\u8BAE\u8FC1\u79FB\u5230\u73AF\u5883\u53D8\u91CF\u6216 keychain");
1827
+ }
1828
+ return { value, source: "plain" };
1829
+ }
1830
+ async function resolveConfigSecrets(params, options = {}) {
1831
+ const result = {};
1832
+ for (const [key, val] of Object.entries(params)) {
1833
+ if (typeof val === "string" && val.length > 0) {
1834
+ const resolved = await resolveSecret(val, { ...options, account: key });
1835
+ result[key] = resolved.value;
1836
+ } else {
1837
+ result[key] = val;
1838
+ }
1839
+ }
1840
+ return result;
1841
+ }
1842
+ async function resolveFromKeychain(service, account) {
1843
+ try {
1844
+ const keytar = await import("keytar");
1845
+ const password = await keytar.getPassword(service, account);
1846
+ if (!password) {
1847
+ throw new ApkpubError({
1848
+ code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
1849
+ message: `keychain \u4E2D\u672A\u627E\u5230 ${service}/${account}`,
1850
+ retryable: false
1851
+ });
1852
+ }
1853
+ return password;
1854
+ } catch (err) {
1855
+ if (err instanceof ApkpubError) throw err;
1856
+ throw new ApkpubError({
1857
+ code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
1858
+ message: `keychain \u4E0D\u53EF\u7528\uFF0C\u8BF7\u5B89\u88C5 keytar \u6216\u6539\u7528\u73AF\u5883\u53D8\u91CF: ${err instanceof Error ? err.message : String(err)}`,
1859
+ retryable: false,
1860
+ cause: err
1861
+ });
1862
+ }
1863
+ }
1864
+ async function decryptValue(encrypted) {
1865
+ const masterKey = process.env.APKPUB_MASTER_KEY;
1866
+ if (!masterKey) {
1867
+ throw new ApkpubError({
1868
+ code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
1869
+ message: "\u89E3\u5BC6\u9700\u8981\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF APKPUB_MASTER_KEY",
1870
+ retryable: false
1871
+ });
1872
+ }
1873
+ const [ivHex, authTagHex, cipherHex] = encrypted.split(":");
1874
+ if (!ivHex || !authTagHex || !cipherHex) {
1875
+ throw new ApkpubError({
1876
+ code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
1877
+ message: "\u52A0\u5BC6\u503C\u683C\u5F0F\u65E0\u6548",
1878
+ retryable: false
1879
+ });
1880
+ }
1881
+ const key = scryptSync(masterKey, "apkpub-salt", 32);
1882
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
1883
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
1884
+ const decrypted = Buffer.concat([
1885
+ decipher.update(Buffer.from(cipherHex, "hex")),
1886
+ decipher.final()
1887
+ ]);
1888
+ return decrypted.toString("utf8");
1889
+ }
1890
+ function looksLikeSecret(value) {
1891
+ if (value.length < 16) return false;
1892
+ return /secret|key|token|password/i.test(value) === false && /^[A-Za-z0-9+/=_-]{16,}$/.test(value);
1893
+ }
1894
+
1895
+ // src/core/versionCheck.ts
1896
+ function checkVersion(apkInfo, marketInfo, channelName) {
1897
+ if (!marketInfo) return;
1898
+ if (apkInfo.versionCode <= marketInfo.lastVersionCode) {
1899
+ throw new ApkpubError({
1900
+ code: "VERSION_TOO_LOW" /* VERSION_TOO_LOW */,
1901
+ channel: channelName,
1902
+ message: `\u8981\u63D0\u4EA4\u7684 APK \u7248\u672C\u53F7(${apkInfo.versionCode})\u9700\u5927\u4E8E\u7EBF\u4E0A\u6700\u65B0\u7248\u672C\u53F7(${marketInfo.lastVersionCode})`,
1903
+ retryable: false
1904
+ });
1905
+ }
1906
+ }
1907
+ function checkPackageMatch(apkInfo, expectedAppId) {
1908
+ if (apkInfo.applicationId !== expectedAppId) {
1909
+ throw new ApkpubError({
1910
+ code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
1911
+ message: `APK \u5305\u540D(${apkInfo.applicationId})\u4E0E\u914D\u7F6E(${expectedAppId})\u4E0D\u5339\u914D`,
1912
+ retryable: false
1913
+ });
1914
+ }
1915
+ }
1916
+
1917
+ // src/core/TaskLauncher.ts
1918
+ import path7 from "path";
1919
+ import { stat as stat2 } from "fs/promises";
1920
+ var TaskLauncher = class {
1921
+ channel;
1922
+ channelConfig;
1923
+ apkPath;
1924
+ enableChannel;
1925
+ resolvedFile;
1926
+ apkInfo;
1927
+ config = {};
1928
+ constructor(options) {
1929
+ this.channel = options.channel;
1930
+ this.channelConfig = options.channelConfig;
1931
+ this.apkPath = options.apkPath;
1932
+ this.enableChannel = options.enableChannel;
1933
+ }
1934
+ /** 注入渠道配置参数 */
1935
+ injectConfig(resolvedParams) {
1936
+ if (this.channelConfig.type === "custom") {
1937
+ this.config = { ...this.channelConfig, ...resolvedParams };
1938
+ } else {
1939
+ this.config = { ...resolvedParams };
1940
+ const fileId = resolvedParams.fileNameIdentify ?? this.channel.fileNameIdentify;
1941
+ this.config.fileNameIdentify = fileId;
1942
+ }
1943
+ }
1944
+ /** 匹配并选择 APK 文件 */
1945
+ async selectFile() {
1946
+ const info = await stat2(this.apkPath);
1947
+ if (info.isFile()) {
1948
+ this.resolvedFile = this.apkPath;
1949
+ return this.resolvedFile;
1950
+ }
1951
+ if (!this.enableChannel) {
1952
+ throw new ApkpubError({
1953
+ code: "APK_NOT_FOUND" /* APK_NOT_FOUND */,
1954
+ channel: this.channel.name,
1955
+ message: "\u591A\u6E20\u9053\u6A21\u5F0F\u672A\u542F\u7528\uFF0C\u8BF7\u6307\u5B9A\u5355\u4E2A APK \u6587\u4EF6",
1956
+ retryable: false
1957
+ });
1958
+ }
1959
+ const fileId = String(this.config.fileNameIdentify ?? this.channel.fileNameIdentify);
1960
+ const apks = await listApkFiles(this.apkPath);
1961
+ const matches = apks.filter((f) => path7.basename(f).toLowerCase().includes(fileId.toLowerCase()));
1962
+ if (matches.length === 0) {
1963
+ throw new ApkpubError({
1964
+ code: "APK_NOT_FOUND" /* APK_NOT_FOUND */,
1965
+ channel: this.channel.name,
1966
+ message: `\u627E\u4E0D\u5230\u6587\u4EF6\u540D\u4E2D\u5305\u542B "${fileId}" \u7684 APK \u6587\u4EF6`,
1967
+ retryable: false
1968
+ });
1969
+ }
1970
+ if (matches.length > 1) {
1971
+ throw new ApkpubError({
1972
+ code: "APK_AMBIGUOUS" /* APK_AMBIGUOUS */,
1973
+ channel: this.channel.name,
1974
+ message: `\u5339\u914D\u5230\u591A\u4E2A APK \u6587\u4EF6\uFF0C\u8BF7\u786E\u4FDD\u552F\u4E00: ${matches.map((f) => path7.basename(f)).join(", ")}`,
1975
+ retryable: false
1976
+ });
1977
+ }
1978
+ this.resolvedFile = matches[0];
1979
+ return this.resolvedFile;
1980
+ }
1981
+ /** 解析 APK 信息 */
1982
+ async prepare() {
1983
+ const file = this.resolvedFile ?? await this.selectFile();
1984
+ this.apkInfo = await parseApk(file);
1985
+ return this.apkInfo;
1986
+ }
1987
+ getFilePath() {
1988
+ if (!this.resolvedFile) {
1989
+ throw new ApkpubError({
1990
+ code: "APK_NOT_FOUND" /* APK_NOT_FOUND */,
1991
+ message: "\u5C1A\u672A\u9009\u62E9 APK \u6587\u4EF6",
1992
+ retryable: false
1993
+ });
1994
+ }
1995
+ return this.resolvedFile;
1996
+ }
1997
+ getApkInfo() {
1998
+ if (!this.apkInfo) {
1999
+ throw new ApkpubError({
2000
+ code: "APK_PARSE_FAILED" /* APK_PARSE_FAILED */,
2001
+ message: "\u5C1A\u672A\u89E3\u6790 APK",
2002
+ retryable: false
2003
+ });
2004
+ }
2005
+ return this.apkInfo;
2006
+ }
2007
+ getConfig() {
2008
+ return this.config;
2009
+ }
2010
+ /** 从渠道配置提取原始参数 */
2011
+ static getRawParams(channelConfig) {
2012
+ if (channelConfig.type === "custom") {
2013
+ return paramsToRecord(channelConfig.params ?? []);
2014
+ }
2015
+ return paramsToRecord(channelConfig.params);
2016
+ }
2017
+ };
2018
+
2019
+ // src/core/Dispatcher.ts
2020
+ var Dispatcher = class {
2021
+ async dispatch(options) {
2022
+ const { appConfig, channels, apkPath, updateDesc, dryRun, parallel = 1, signal } = options;
2023
+ const limit = pLimit(Math.max(1, parallel));
2024
+ const tasks = channels.map((channel) => {
2025
+ const chConfig = appConfig.channels.find((c) => c.name === channel.name);
2026
+ if (!chConfig || !chConfig.enable) {
2027
+ return async () => ({
2028
+ name: channel.name,
2029
+ label: channel.label,
2030
+ status: "skipped"
2031
+ });
2032
+ }
2033
+ return async () => {
2034
+ const launcher = new TaskLauncher({
2035
+ channel,
2036
+ channelConfig: chConfig,
2037
+ apkPath,
2038
+ enableChannel: appConfig.enableChannel
2039
+ });
2040
+ try {
2041
+ const rawParams = TaskLauncher.getRawParams(chConfig);
2042
+ const resolved = await resolveConfigSecrets(rawParams, {
2043
+ service: "apkpub-cli",
2044
+ account: `${appConfig.applicationId}/${channel.name}`
2045
+ });
2046
+ launcher.injectConfig(resolved);
2047
+ await launcher.selectFile();
2048
+ const apkInfo = await launcher.prepare();
2049
+ checkPackageMatch(apkInfo, appConfig.applicationId);
2050
+ if (channel.getMarketState) {
2051
+ const marketState = await channel.getMarketState(appConfig.applicationId, launcher.getConfig());
2052
+ checkVersion(apkInfo, marketState, channel.name);
2053
+ }
2054
+ if (dryRun) {
2055
+ logger.info(channel.name, `[dry-run] \u9884\u68C0\u901A\u8FC7\uFF0C\u5C06\u4E0A\u4F20 ${apkInfo.versionName}(${apkInfo.versionCode})`);
2056
+ return { name: channel.name, label: channel.label, status: "dry_run" };
2057
+ }
2058
+ const result = await channel.upload({
2059
+ apkInfo,
2060
+ filePath: launcher.getFilePath(),
2061
+ desc: updateDesc,
2062
+ config: launcher.getConfig(),
2063
+ onProgress: ({ step, percent }) => {
2064
+ options.onChannelProgress?.(channel.name, step, percent);
2065
+ },
2066
+ signal: signal ?? new AbortController().signal
2067
+ });
2068
+ await writeAuditLog({
2069
+ action: "publish",
2070
+ appId: appConfig.applicationId,
2071
+ channels: [channel.name],
2072
+ versionCode: apkInfo.versionCode,
2073
+ result: "success"
2074
+ });
2075
+ return {
2076
+ name: channel.name,
2077
+ label: channel.label,
2078
+ status: "success",
2079
+ downloadUrl: result.downloadUrl
2080
+ };
2081
+ } catch (err) {
2082
+ logger.error(channel.name, err instanceof Error ? err.message : String(err));
2083
+ await writeAuditLog({
2084
+ action: "publish",
2085
+ appId: appConfig.applicationId,
2086
+ channels: [channel.name],
2087
+ result: "failed"
2088
+ }).catch(() => {
2089
+ });
2090
+ return channelResultFromError(channel.name, channel.label, err);
2091
+ }
2092
+ };
2093
+ });
2094
+ const results = await Promise.all(tasks.map((task) => limit(task)));
2095
+ return aggregateResults(results, dryRun);
2096
+ }
2097
+ };
2098
+
2099
+ // src/cli/publish.ts
2100
+ function registerPublishCommand(program2) {
2101
+ program2.command("publish").description("\u53D1\u5E03 APK \u5230\u6307\u5B9A\u6E20\u9053").requiredOption("--app <id>", "\u5E94\u7528\u5305\u540D").requiredOption("--apk <path>", "APK \u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84").option("--channels <names>", "\u6E20\u9053\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694", (v) => v.split(",").map((s) => s.trim())).option("--desc <text>", "\u66F4\u65B0\u63CF\u8FF0").option("--parallel [n]", "\u5E76\u884C\u4E0A\u4F20\u6570", "1").option("--dry-run", "\u4EC5\u9884\u68C0\uFF0C\u4E0D\u5B9E\u9645\u4E0A\u4F20").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").option("--no-progress", "\u7981\u7528\u8FDB\u5EA6\u663E\u793A").option("--debug", "\u8C03\u8BD5\u6A21\u5F0F").action(async (options) => {
2102
+ const jsonMode2 = options.json || !isInteractive();
2103
+ setJsonMode(jsonMode2);
2104
+ try {
2105
+ const appConfig = await loadConfig(options.app);
2106
+ const updateDesc = options.desc ?? appConfig.extension.updateDesc ?? "";
2107
+ if (!updateDesc.trim()) {
2108
+ throw new ApkpubError({
2109
+ code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
2110
+ message: "\u8BF7\u901A\u8FC7 --desc \u63D0\u4F9B\u66F4\u65B0\u63CF\u8FF0",
2111
+ retryable: false
2112
+ });
2113
+ }
2114
+ const channelNames = options.channels;
2115
+ const channels = resolveChannels(appConfig, channelNames);
2116
+ if (channels.length === 0) {
2117
+ throw new ApkpubError({
2118
+ code: "CHANNEL_NOT_FOUND" /* CHANNEL_NOT_FOUND */,
2119
+ message: "\u6CA1\u6709\u53EF\u7528\u7684\u53D1\u5E03\u6E20\u9053",
2120
+ retryable: false
2121
+ });
2122
+ }
2123
+ const dispatcher = new Dispatcher();
2124
+ const result = await dispatcher.dispatch({
2125
+ appConfig,
2126
+ channels,
2127
+ apkPath: options.apk,
2128
+ updateDesc: updateDesc.trim(),
2129
+ dryRun: options.dryRun,
2130
+ parallel: parseInt(options.parallel ?? "1", 10),
2131
+ onChannelProgress: (channel, step, percent) => {
2132
+ if (!jsonMode2 && options.progress !== false) {
2133
+ const pct = percent !== void 0 ? ` ${percent}%` : "";
2134
+ process.stderr.write(`[${channel}] ${step}${pct}
2135
+ `);
2136
+ }
2137
+ }
2138
+ });
2139
+ if (jsonMode2) {
2140
+ printJson(result);
2141
+ } else {
2142
+ printPublishResult(result);
2143
+ }
2144
+ process.exit(getExitCode(result));
2145
+ } catch (err) {
2146
+ const apkErr = err instanceof ApkpubError ? err : new ApkpubError({ code: "INTERNAL" /* INTERNAL */, message: String(err) });
2147
+ if (options.json) {
2148
+ printJson({ ok: false, error: { code: apkErr.code, message: apkErr.message, retryable: apkErr.retryable } });
2149
+ } else {
2150
+ process.stderr.write(`\u9519\u8BEF: ${apkErr.message}
2151
+ `);
2152
+ }
2153
+ process.exit(2 /* INVALID_ARGUMENT */);
2154
+ }
2155
+ });
2156
+ }
2157
+
2158
+ // src/cli/status.ts
2159
+ function registerStatusCommand(program2) {
2160
+ program2.command("status").description("\u67E5\u8BE2\u5404\u5E02\u573A\u7EBF\u4E0A\u7248\u672C\u4E0E\u5BA1\u6838\u72B6\u6001").requiredOption("--app <id>", "\u5E94\u7528\u5305\u540D").option("--channels <names>", "\u6E20\u9053\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694", (v) => v.split(",").map((s) => s.trim())).option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
2161
+ setJsonMode(!!options.json);
2162
+ try {
2163
+ const appConfig = await loadConfig(options.app);
2164
+ const channels = resolveChannels(appConfig, options.channels);
2165
+ const results = [];
2166
+ for (const channel of channels) {
2167
+ if (!channel.getMarketState) {
2168
+ results.push({ name: channel.name, label: channel.label, state: "n/a", error: "\u81EA\u5B9A\u4E49\u6E20\u9053\u65E0\u5E02\u573A\u72B6\u6001" });
2169
+ continue;
2170
+ }
2171
+ const chConfig = appConfig.channels.find((c) => c.name === channel.name);
2172
+ try {
2173
+ const rawParams = TaskLauncher.getRawParams(chConfig);
2174
+ const resolved = await resolveConfigSecrets(rawParams);
2175
+ const state = await channel.getMarketState(appConfig.applicationId, resolved);
2176
+ results.push({
2177
+ name: channel.name,
2178
+ label: channel.label,
2179
+ state: state?.reviewState ?? "unknown",
2180
+ versionCode: state?.lastVersionCode,
2181
+ versionName: state?.lastVersionName
2182
+ });
2183
+ } catch (err) {
2184
+ results.push({
2185
+ name: channel.name,
2186
+ label: channel.label,
2187
+ error: err instanceof Error ? err.message : String(err)
2188
+ });
2189
+ }
2190
+ }
2191
+ if (options.json) {
2192
+ printJson({ ok: true, results });
2193
+ } else {
2194
+ printStatusResults(results);
2195
+ }
2196
+ process.exit(0 /* SUCCESS */);
2197
+ } catch (err) {
2198
+ const message = err instanceof ApkpubError ? err.message : String(err);
2199
+ if (options.json) {
2200
+ printJson({ ok: false, error: { message } });
2201
+ } else {
2202
+ process.stderr.write(`\u9519\u8BEF: ${message}
2203
+ `);
2204
+ }
2205
+ process.exit(2 /* INVALID_ARGUMENT */);
2206
+ }
2207
+ });
2208
+ }
2209
+
2210
+ // src/cli/channels.ts
2211
+ function registerChannelsCommand(program2) {
2212
+ program2.command("channels").description("\u5217\u51FA\u652F\u6301\u7684\u5185\u7F6E\u5E02\u573A\u6E20\u9053\u53CA\u6240\u9700\u51ED\u8BC1\u5B57\u6BB5").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
2213
+ const metas = getChannelMetas();
2214
+ if (options.json) {
2215
+ printJson({ channels: metas });
2216
+ } else {
2217
+ for (const ch of metas) {
2218
+ process.stderr.write(`
2219
+ ${ch.label} (${ch.name})
2220
+ `);
2221
+ process.stderr.write(` \u6587\u4EF6\u540D\u6807\u8BC6: ${ch.fileNameIdentify}
2222
+ `);
2223
+ process.stderr.write(` \u51ED\u8BC1\u5B57\u6BB5:
2224
+ `);
2225
+ for (const f of ch.credentialFields) {
2226
+ process.stderr.write(` - ${f.name}${f.required ? " *" : ""}: ${f.description ?? ""}
2227
+ `);
2228
+ }
2229
+ }
2230
+ process.stderr.write("\n");
2231
+ }
2232
+ process.exit(0 /* SUCCESS */);
2233
+ });
2234
+ }
2235
+
2236
+ // src/cli/doctor.ts
2237
+ function registerDoctorCommand(program2) {
2238
+ program2.command("doctor").description("\u4F53\u68C0\uFF1A\u6821\u9A8C\u914D\u7F6E\u5B8C\u6574\u6027\u4E0E\u5404\u6E20\u9053\u51ED\u8BC1\u53EF\u7528\u6027").requiredOption("--app <id>", "\u5E94\u7528\u5305\u540D").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
2239
+ setJsonMode(!!options.json);
2240
+ try {
2241
+ const appConfig = await loadConfig(options.app);
2242
+ const channels = resolveChannels(appConfig);
2243
+ const checks = [];
2244
+ for (const channel of channels) {
2245
+ const chConfig = appConfig.channels.find((c) => c.name === channel.name);
2246
+ try {
2247
+ const rawParams = TaskLauncher.getRawParams(chConfig);
2248
+ const resolved = await resolveConfigSecrets(rawParams);
2249
+ if (channel.validateCredentials) {
2250
+ await channel.validateCredentials(resolved);
2251
+ }
2252
+ checks.push({ channel: channel.name, ok: true, message: "\u51ED\u8BC1\u6709\u6548" });
2253
+ } catch (err) {
2254
+ checks.push({
2255
+ channel: channel.name,
2256
+ ok: false,
2257
+ message: err instanceof Error ? err.message : String(err)
2258
+ });
2259
+ }
2260
+ }
2261
+ const allOk = checks.every((c) => c.ok);
2262
+ const output = { ok: allOk, app: options.app, checks };
2263
+ if (options.json) {
2264
+ printJson(output);
2265
+ } else {
2266
+ for (const c of checks) {
2267
+ const icon = c.ok ? "\u2713" : "\u2717";
2268
+ process.stderr.write(`${icon} [${c.channel}] ${c.message}
2269
+ `);
2270
+ }
2271
+ }
2272
+ process.exit(allOk ? 0 /* SUCCESS */ : 5 /* ALL_FAILED */);
2273
+ } catch (err) {
2274
+ const message = err instanceof ApkpubError ? err.message : String(err);
2275
+ if (options.json) {
2276
+ printJson({ ok: false, error: { message } });
2277
+ } else {
2278
+ process.stderr.write(`\u9519\u8BEF: ${message}
2279
+ `);
2280
+ }
2281
+ process.exit(2 /* INVALID_ARGUMENT */);
2282
+ }
2283
+ });
2284
+ }
2285
+
2286
+ // src/cli/describe.ts
2287
+ var COMMANDS = [
2288
+ {
2289
+ name: "init",
2290
+ description: "\u521B\u5EFA\u5E94\u7528\u914D\u7F6E",
2291
+ options: [
2292
+ { name: "--name", required: false, description: "\u5E94\u7528\u663E\u793A\u540D\u79F0" },
2293
+ { name: "--app", required: false, description: "\u5E94\u7528\u5305\u540D" },
2294
+ { name: "--channels", required: false, description: "\u542F\u7528\u7684\u5E02\u573A\u6E20\u9053" }
2295
+ ]
2296
+ },
2297
+ {
2298
+ name: "publish",
2299
+ description: "\u53D1\u5E03 APK \u5230\u6307\u5B9A\u6E20\u9053",
2300
+ options: [
2301
+ { name: "--app", required: true, description: "\u5E94\u7528\u5305\u540D" },
2302
+ { name: "--apk", required: true, description: "APK \u6587\u4EF6\u6216\u76EE\u5F55" },
2303
+ { name: "--channels", required: false, description: "\u6E20\u9053\u5217\u8868" },
2304
+ { name: "--desc", required: false, description: "\u66F4\u65B0\u63CF\u8FF0" },
2305
+ { name: "--parallel", required: false, description: "\u5E76\u884C\u6570" },
2306
+ { name: "--dry-run", required: false, description: "\u4EC5\u9884\u68C0" },
2307
+ { name: "--json", required: false, description: "JSON \u8F93\u51FA" }
2308
+ ]
2309
+ },
2310
+ {
2311
+ name: "status",
2312
+ description: "\u67E5\u8BE2\u5E02\u573A\u5BA1\u6838\u72B6\u6001",
2313
+ options: [
2314
+ { name: "--app", required: true, description: "\u5E94\u7528\u5305\u540D" },
2315
+ { name: "--channels", required: false, description: "\u6E20\u9053\u5217\u8868" },
2316
+ { name: "--json", required: false, description: "JSON \u8F93\u51FA" }
2317
+ ]
2318
+ },
2319
+ {
2320
+ name: "info",
2321
+ description: "\u89E3\u6790 APK \u4FE1\u606F",
2322
+ options: [
2323
+ { name: "<apk>", required: true, description: "APK \u6587\u4EF6\u8DEF\u5F84" },
2324
+ { name: "--json", required: false, description: "JSON \u8F93\u51FA" }
2325
+ ]
2326
+ },
2327
+ {
2328
+ name: "doctor",
2329
+ description: "\u914D\u7F6E\u4E0E\u51ED\u8BC1\u4F53\u68C0",
2330
+ options: [
2331
+ { name: "--app", required: true, description: "\u5E94\u7528\u5305\u540D" },
2332
+ { name: "--json", required: false, description: "JSON \u8F93\u51FA" }
2333
+ ]
2334
+ },
2335
+ {
2336
+ name: "mcp",
2337
+ description: "\u542F\u52A8 MCP server \u6A21\u5F0F",
2338
+ options: []
2339
+ }
2340
+ ];
2341
+ function registerDescribeCommand(program2) {
2342
+ program2.command("describe").description("\u8F93\u51FA\u547D\u4EE4\u4E0E\u6E20\u9053\u7684\u673A\u5668\u53EF\u8BFB\u6E05\u5355\uFF08\u4F9B Agent \u81EA\u53D1\u73B0\uFF09").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", true).action((options) => {
2343
+ const descriptor = {
2344
+ name: "apkpub-cli",
2345
+ version: "1.0.0",
2346
+ commands: COMMANDS,
2347
+ channels: getChannelMetas(),
2348
+ exitCodes: {
2349
+ 0: "\u5168\u90E8\u6210\u529F",
2350
+ 2: "\u53C2\u6570/\u914D\u7F6E\u9519\u8BEF",
2351
+ 3: "\u7248\u672C\u6821\u9A8C\u5931\u8D25",
2352
+ 4: "\u90E8\u5206\u6E20\u9053\u5931\u8D25",
2353
+ 5: "\u5168\u90E8\u5931\u8D25"
2354
+ },
2355
+ jsonSchema: {
2356
+ publishResult: {
2357
+ ok: "boolean",
2358
+ dryRun: "boolean",
2359
+ results: [{ name: "string", status: "string", downloadUrl: "string?", error: "object?" }],
2360
+ summary: { total: "number", success: "number", failed: "number", skipped: "number" }
2361
+ }
2362
+ }
2363
+ };
2364
+ if (options.json !== false) {
2365
+ printJson(descriptor);
2366
+ }
2367
+ process.exit(0 /* SUCCESS */);
2368
+ });
2369
+ }
2370
+
2371
+ // src/mcp/server.ts
2372
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2373
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2374
+ import {
2375
+ CallToolRequestSchema,
2376
+ ListToolsRequestSchema
2377
+ } from "@modelcontextprotocol/sdk/types.js";
2378
+ async function startMcpServer() {
2379
+ const server = new Server(
2380
+ { name: "apkpub-cli", version: "1.0.0" },
2381
+ { capabilities: { tools: {} } }
2382
+ );
2383
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2384
+ tools: [
2385
+ {
2386
+ name: "apkpub_info",
2387
+ description: "\u89E3\u6790 APK \u6587\u4EF6\uFF0C\u83B7\u53D6\u5305\u540D\u3001\u7248\u672C\u53F7\u7B49\u4FE1\u606F",
2388
+ inputSchema: {
2389
+ type: "object",
2390
+ properties: { apk: { type: "string", description: "APK \u6587\u4EF6\u8DEF\u5F84" } },
2391
+ required: ["apk"]
2392
+ }
2393
+ },
2394
+ {
2395
+ name: "apkpub_status",
2396
+ description: "\u67E5\u8BE2\u5E94\u7528\u5728\u5404\u5E02\u573A\u7684\u5BA1\u6838\u72B6\u6001\u4E0E\u7EBF\u4E0A\u7248\u672C",
2397
+ inputSchema: {
2398
+ type: "object",
2399
+ properties: {
2400
+ app: { type: "string", description: "\u5E94\u7528\u5305\u540D" },
2401
+ channels: { type: "array", items: { type: "string" }, description: "\u6E20\u9053\u5217\u8868" }
2402
+ },
2403
+ required: ["app"]
2404
+ }
2405
+ },
2406
+ {
2407
+ name: "apkpub_publish",
2408
+ description: "\u53D1\u5E03 APK \u5230\u6307\u5B9A\u6E20\u9053",
2409
+ inputSchema: {
2410
+ type: "object",
2411
+ properties: {
2412
+ app: { type: "string", description: "\u5E94\u7528\u5305\u540D" },
2413
+ apk: { type: "string", description: "APK \u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84" },
2414
+ channels: { type: "array", items: { type: "string" }, description: "\u6E20\u9053\u5217\u8868" },
2415
+ desc: { type: "string", description: "\u66F4\u65B0\u63CF\u8FF0" },
2416
+ dryRun: { type: "boolean", description: "\u4EC5\u9884\u68C0" },
2417
+ parallel: { type: "number", description: "\u5E76\u884C\u6570" }
2418
+ },
2419
+ required: ["app", "apk"]
2420
+ }
2421
+ },
2422
+ {
2423
+ name: "apkpub_doctor",
2424
+ description: "\u4F53\u68C0\u5E94\u7528\u914D\u7F6E\u4E0E\u5404\u6E20\u9053\u51ED\u8BC1",
2425
+ inputSchema: {
2426
+ type: "object",
2427
+ properties: { app: { type: "string", description: "\u5E94\u7528\u5305\u540D" } },
2428
+ required: ["app"]
2429
+ }
2430
+ }
2431
+ ]
2432
+ }));
2433
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2434
+ const { name, arguments: args } = request.params;
2435
+ const params = args ?? {};
2436
+ try {
2437
+ switch (name) {
2438
+ case "apkpub_info": {
2439
+ const info = await parseApk(params.apk);
2440
+ return { content: [{ type: "text", text: JSON.stringify(info) }] };
2441
+ }
2442
+ case "apkpub_status": {
2443
+ const appConfig = await loadConfig(params.app);
2444
+ const channels = resolveChannels(appConfig, params.channels);
2445
+ const results = [];
2446
+ for (const channel of channels) {
2447
+ if (!channel.getMarketState) {
2448
+ results.push({ channel: channel.name, state: "n/a" });
2449
+ continue;
2450
+ }
2451
+ const chConfig = appConfig.channels.find((c) => c.name === channel.name);
2452
+ const rawParams = TaskLauncher.getRawParams(chConfig);
2453
+ const resolved = await resolveConfigSecrets(rawParams);
2454
+ const state = await channel.getMarketState(appConfig.applicationId, resolved);
2455
+ results.push({ channel: channel.name, ...state });
2456
+ }
2457
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, results }) }] };
2458
+ }
2459
+ case "apkpub_publish": {
2460
+ const appConfig = await loadConfig(params.app);
2461
+ const channels = resolveChannels(appConfig, params.channels);
2462
+ const dispatcher = new Dispatcher();
2463
+ const result = await dispatcher.dispatch({
2464
+ appConfig,
2465
+ channels,
2466
+ apkPath: params.apk,
2467
+ updateDesc: params.desc ?? appConfig.extension.updateDesc ?? "",
2468
+ dryRun: params.dryRun,
2469
+ parallel: params.parallel ?? 1
2470
+ });
2471
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
2472
+ }
2473
+ case "apkpub_doctor": {
2474
+ const appConfig = await loadConfig(params.app);
2475
+ const channels = resolveChannels(appConfig);
2476
+ const checks = [];
2477
+ for (const channel of channels) {
2478
+ const chConfig = appConfig.channels.find((c) => c.name === channel.name);
2479
+ try {
2480
+ const rawParams = TaskLauncher.getRawParams(chConfig);
2481
+ const resolved = await resolveConfigSecrets(rawParams);
2482
+ if (channel.validateCredentials) await channel.validateCredentials(resolved);
2483
+ checks.push({ channel: channel.name, ok: true });
2484
+ } catch (err) {
2485
+ checks.push({ channel: channel.name, ok: false, error: String(err) });
2486
+ }
2487
+ }
2488
+ return { content: [{ type: "text", text: JSON.stringify({ ok: checks.every((c) => c.ok), checks }) }] };
2489
+ }
2490
+ default:
2491
+ return { content: [{ type: "text", text: JSON.stringify({ error: `\u672A\u77E5\u5DE5\u5177: ${name}` }) }], isError: true };
2492
+ }
2493
+ } catch (err) {
2494
+ return {
2495
+ content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) }],
2496
+ isError: true
2497
+ };
2498
+ }
2499
+ });
2500
+ const transport = new StdioServerTransport();
2501
+ await server.connect(transport);
2502
+ }
2503
+
2504
+ // src/cli/mcp.ts
2505
+ function registerMcpCommand(program2) {
2506
+ program2.command("mcp").description("\u4EE5 MCP server \u6A21\u5F0F\u8FD0\u884C\uFF0C\u66B4\u9732 publish/status/info/doctor \u5DE5\u5177").action(async () => {
2507
+ await startMcpServer();
2508
+ });
2509
+ }
2510
+
2511
+ // src/bin/apkpub.ts
2512
+ var program = new Command();
2513
+ program.name("apkpub").description("APK \u591A\u5E02\u573A\u5206\u53D1 CLI \u5DE5\u5177").version("1.0.0");
2514
+ registerInitCommand(program);
2515
+ registerConfigCommand(program);
2516
+ registerInfoCommand(program);
2517
+ registerPublishCommand(program);
2518
+ registerStatusCommand(program);
2519
+ registerChannelsCommand(program);
2520
+ registerDoctorCommand(program);
2521
+ registerDescribeCommand(program);
2522
+ registerMcpCommand(program);
2523
+ program.parseAsync(process.argv).catch((err) => {
2524
+ process.stderr.write(`\u81F4\u547D\u9519\u8BEF: ${err instanceof Error ? err.message : String(err)}
2525
+ `);
2526
+ process.exit(1);
2527
+ });