@applift/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { n as program } from "./src-CoYlWjDO.mjs";
3
+ //#region src/bin.ts
4
+ program.parseAsync().catch((e) => {
5
+ process.stderr.write(`✗ ${e.message}\n`);
6
+ process.exit(1);
7
+ });
8
+ //#endregion
9
+ export {};
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { t as makeContext, n as program } from "./src-CoYlWjDO.mjs";
@@ -0,0 +1,917 @@
1
+ import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { AgcHarmonyChannel, AscChannel, GooglePlayChannel, HonorChannel, HuaweiAndroidChannel, OppoChannel, PgyerChannel, PluginHost, VivoChannel, XiaomiChannel, YybChannel, appendEntry, assertListingFiles, detectBundleIds, detectFramework, findArtifacts, humanizeError, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, readArtifactVersion, readEntries, registry, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, runStage, runSubmit, saveCredentials, setProjectProfile, sha256File } from "@applift/core";
7
+ //#region src/target.ts
8
+ function resolveChannelTarget(id, config, detected) {
9
+ const store = config.android?.stores?.[id] ?? {};
10
+ if (id === "agc-harmony") return {
11
+ ...detected.harmonyBundleName ? { bundleName: detected.harmonyBundleName } : {},
12
+ ...config.harmony,
13
+ ...store
14
+ };
15
+ if (id === "asc") return {
16
+ ...detected.iosBundleId ? { bundleId: detected.iosBundleId } : {},
17
+ ...config.ios,
18
+ ...store
19
+ };
20
+ return {
21
+ ...detected.androidPkgName ? { pkgName: detected.androidPkgName } : {},
22
+ ...config.android?.pkgName ? { pkgName: config.android.pkgName } : {},
23
+ ...store
24
+ };
25
+ }
26
+ //#endregion
27
+ //#region src/lifecycle.ts
28
+ /**
29
+ * init 向导(PM 视角:发版小白一次配置完成)、use 个人 profile 切换与 status 审核跟踪。
30
+ */
31
+ /** 各渠道在 init/status 场景需要的凭据键与获取指引 */
32
+ const CREDENTIAL_FIELDS = [
33
+ {
34
+ key: "PGYER_API_KEY",
35
+ label: "蒲公英(内测)",
36
+ where: "pgyer.com → 账号设置 → API"
37
+ },
38
+ {
39
+ key: "AGC_CLIENT_ID",
40
+ label: "华为 AGC Client ID",
41
+ where: "AGC → 项目设置 → 常规 → API Client"
42
+ },
43
+ {
44
+ key: "AGC_CLIENT_SECRET",
45
+ label: "华为 AGC Secret",
46
+ where: "同上"
47
+ },
48
+ {
49
+ key: "HW_ANDROID_APP_ID",
50
+ label: "华为安卓 App ID",
51
+ where: "AGC 我的应用(安卓)"
52
+ },
53
+ {
54
+ key: "MI_USER_NAME",
55
+ label: "小米账号",
56
+ where: "dev.mi.com → 管理中心"
57
+ },
58
+ {
59
+ key: "MI_PASSWORD",
60
+ label: "小米上传密码",
61
+ where: "同上"
62
+ },
63
+ {
64
+ key: "MI_CERT_PATH",
65
+ label: "小米证书路径",
66
+ where: "dev.api.public.cer 绝对路径"
67
+ },
68
+ {
69
+ key: "OPPO_CLIENT_ID",
70
+ label: "OPPO Client ID",
71
+ where: "open.oppomobile.com → API 传包"
72
+ },
73
+ {
74
+ key: "OPPO_CLIENT_SECRET",
75
+ label: "OPPO Secret",
76
+ where: "同上"
77
+ },
78
+ {
79
+ key: "VIVO_ACCESS_KEY",
80
+ label: "vivo Access Key",
81
+ where: "dev.vivo.com.cn → API 传包"
82
+ },
83
+ {
84
+ key: "VIVO_ACCESS_SECRET",
85
+ label: "vivo Secret",
86
+ where: "同上"
87
+ },
88
+ {
89
+ key: "HONOR_CLIENT_ID",
90
+ label: "荣耀 Client ID",
91
+ where: "developer.honor.com → 设置 → API"
92
+ },
93
+ {
94
+ key: "HONOR_CLIENT_SECRET",
95
+ label: "荣耀 Secret",
96
+ where: "同上"
97
+ },
98
+ {
99
+ key: "HONOR_APP_ID",
100
+ label: "荣耀 App ID",
101
+ where: "荣耀开发者后台"
102
+ },
103
+ {
104
+ key: "YYB_USER_ID",
105
+ label: "应用宝 UserID",
106
+ where: "app.open.qq.com 管理中心"
107
+ },
108
+ {
109
+ key: "YYB_ACCESS_SECRET",
110
+ label: "应用宝 AccessSecret",
111
+ where: "同上"
112
+ },
113
+ {
114
+ key: "YYB_APP_ID",
115
+ label: "应用宝 App ID",
116
+ where: "同上"
117
+ },
118
+ {
119
+ key: "ASC_KEY_ID",
120
+ label: "ASC Key ID",
121
+ where: "App Store Connect → 集成 → 团队密钥"
122
+ },
123
+ {
124
+ key: "ASC_ISSUER_ID",
125
+ label: "ASC Issuer ID",
126
+ where: "同上"
127
+ }
128
+ ];
129
+ function registerLifecycleCommands(program, registryRef) {
130
+ program.command("init").description("向导:检测项目 → 生成 applift.config.ts → 引导配置凭据").option("--cwd <dir>", "项目目录", process.cwd()).action(async (cmdOpts) => {
131
+ const cwd = path.resolve(cmdOpts.cwd);
132
+ p.intro(pc.bgCyan(pc.black(" applift init ")));
133
+ const framework = detectFramework(cwd);
134
+ if (framework) p.log.success(`框架识别: ${pc.bold(framework.label)}`);
135
+ else p.log.warn("未识别框架(可继续,产物路径用 --artifact 指定)");
136
+ const configPath = path.join(cwd, "applift.config.ts");
137
+ if (fs.existsSync(configPath)) {
138
+ const overwrite = await p.confirm({ message: `已存在 ${configPath},覆盖重生成?` });
139
+ if (p.isCancel(overwrite) || !overwrite) {
140
+ p.outro("保留现有配置,跳过生成");
141
+ return;
142
+ }
143
+ }
144
+ const androidPkg = await p.text({
145
+ message: "安卓包名(如 com.thclouds.driver,可空跳过安卓渠道)",
146
+ placeholder: "com.example.app"
147
+ });
148
+ const harmonyBundle = await p.text({
149
+ message: "鸿蒙 bundleName(可空跳过鸿蒙)",
150
+ placeholder: "com.example.app.hm"
151
+ });
152
+ const iosScheme = await p.text({
153
+ message: "iOS scheme(可空跳过 iOS)",
154
+ placeholder: "MyApp"
155
+ });
156
+ const profileName = await p.text({
157
+ message: "凭据 profile 名",
158
+ initialValue: "default"
159
+ });
160
+ if (p.isCancel(androidPkg) || p.isCancel(harmonyBundle) || p.isCancel(iosScheme) || p.isCancel(profileName)) {
161
+ p.cancel("已取消");
162
+ return;
163
+ }
164
+ const stores = androidPkg ? `
165
+ android: {
166
+ pkgName: ${JSON.stringify(String(androidPkg))},
167
+ // 按需启用渠道(appId/证书路径等渠道侧配置)
168
+ stores: {
169
+ // huawei: { appId: 'xxx' },
170
+ // honor: { appId: 'xxx' },
171
+ // oppo: {}, vivo: {}, yyb: { appId: 'xxx' },
172
+ },
173
+ },` : "";
174
+ const harmony = harmonyBundle ? `
175
+ harmony: {
176
+ bundleName: ${JSON.stringify(String(harmonyBundle))},
177
+ // appId: 'xxx', // 缺省按 bundleName 自动查询
178
+ },` : "";
179
+ const ios = iosScheme ? `
180
+ ios: {
181
+ scheme: ${JSON.stringify(String(iosScheme))},
182
+ },` : "";
183
+ const content = `import { defineConfig } from '@applift/core';
184
+
185
+ export default defineConfig({
186
+ profile: ${JSON.stringify(String(profileName || "default"))},${stores}${harmony}${ios}
187
+ // plugins: [], // vite 风格钩子:拦截提审/改文案/自定义渠道,见 README
188
+ });
189
+ `;
190
+ fs.writeFileSync(configPath, content);
191
+ p.log.success(`已生成 ${pc.bold("applift.config.ts")}`);
192
+ const doCreds = await p.confirm({
193
+ message: "现在配置凭据?(写入 ~/.applift/credentials.toml,0600)",
194
+ initialValue: false
195
+ });
196
+ if (p.isCancel(doCreds)) return;
197
+ if (doCreds) {
198
+ const creds = loadCredentialsLayered(cmdOpts.cwd);
199
+ const profile = String(profileName || "default");
200
+ creds.profiles[profile] ??= {};
201
+ const group = await p.multiselect({
202
+ message: "选择要配置的凭据组(空格选择,回车确认)",
203
+ options: [
204
+ {
205
+ value: "beta",
206
+ label: "内测(蒲公英)"
207
+ },
208
+ {
209
+ value: "huawei",
210
+ label: "华为(安卓+鸿蒙)"
211
+ },
212
+ {
213
+ value: "xiaomi",
214
+ label: "小米"
215
+ },
216
+ {
217
+ value: "oppo",
218
+ label: "OPPO"
219
+ },
220
+ {
221
+ value: "vivo",
222
+ label: "vivo"
223
+ },
224
+ {
225
+ value: "honor",
226
+ label: "荣耀"
227
+ },
228
+ {
229
+ value: "yyb",
230
+ label: "应用宝"
231
+ },
232
+ {
233
+ value: "asc",
234
+ label: "iOS App Store Connect"
235
+ }
236
+ ],
237
+ required: false
238
+ });
239
+ if (!p.isCancel(group)) {
240
+ const groups = {
241
+ beta: ["PGYER_API_KEY"],
242
+ huawei: [
243
+ "AGC_CLIENT_ID",
244
+ "AGC_CLIENT_SECRET",
245
+ "HW_ANDROID_APP_ID"
246
+ ],
247
+ xiaomi: [
248
+ "MI_USER_NAME",
249
+ "MI_PASSWORD",
250
+ "MI_CERT_PATH"
251
+ ],
252
+ oppo: ["OPPO_CLIENT_ID", "OPPO_CLIENT_SECRET"],
253
+ vivo: ["VIVO_ACCESS_KEY", "VIVO_ACCESS_SECRET"],
254
+ honor: [
255
+ "HONOR_CLIENT_ID",
256
+ "HONOR_CLIENT_SECRET",
257
+ "HONOR_APP_ID"
258
+ ],
259
+ yyb: [
260
+ "YYB_USER_ID",
261
+ "YYB_ACCESS_SECRET",
262
+ "YYB_APP_ID"
263
+ ],
264
+ asc: ["ASC_KEY_ID", "ASC_ISSUER_ID"]
265
+ };
266
+ for (const g of group) for (const key of groups[String(g)] ?? []) {
267
+ const meta = CREDENTIAL_FIELDS.find((f) => f.key === key);
268
+ const v = await p.text({
269
+ message: `${meta.label}(${pc.dim(meta.where)})`,
270
+ defaultValue: ""
271
+ });
272
+ if (!p.isCancel(v) && String(v).trim()) creds.profiles[profile][key] = String(v).trim();
273
+ }
274
+ saveCredentials(creds);
275
+ p.log.success(`凭据已写入 ~/.applift/credentials.toml(profile: ${pc.bold(profile)},0600)`);
276
+ }
277
+ }
278
+ p.outro(`下一步: ${pc.cyan("applift doctor")} 体检 → ${pc.cyan("applift beta android")} 发内测`);
279
+ });
280
+ program.command("use").description("查看/切换本项目凭据 profile(个人选择写 ~/.applift/projects.json,不进项目/git;项目配置 profile 是团队默认)").argument("[profile]", "目标 profile 名;缺省=查看当前生效与可用列表").option("--cwd <dir>", "项目目录", process.cwd()).action(async (profile, cmdOpts) => {
281
+ const cwd = path.resolve(cmdOpts.cwd);
282
+ const available = Object.keys(loadCredentialsLayered(cmdOpts.cwd).profiles);
283
+ p.intro(pc.bgCyan(pc.black(" applift use ")));
284
+ const target = profile?.trim();
285
+ if (!target) {
286
+ const { config } = await loadProjectConfig(cwd);
287
+ const source = loadProjectProfileMap()[cwd] ? "个人选择 ~/.applift/projects.json" : config.profile !== "default" ? `项目默认 applift.config.ts → profile: ${config.profile}` : "全局默认";
288
+ p.log.message(`当前生效 profile: ${pc.bold(resolveProfileFor(cwd, config.profile))} ${pc.dim(`(来源: ${source})`)}`);
289
+ p.log.message(available.length > 0 ? `可用 profiles: ${available.join(", ")}` : "可用 profiles: (无 — 先在 ~/.applift/credentials.toml 配置)");
290
+ p.outro(`个人切换: ${pc.cyan("applift use <profile>")}(改团队默认 → applift.config.ts 的 profile 字段)`);
291
+ return;
292
+ }
293
+ setProjectProfile(cwd, target);
294
+ p.log.success(`本项目已切换到 profile ${pc.bold(target)}(写 ~/.applift/projects.json,不进项目/git)`);
295
+ if (!available.includes(target)) p.log.warn(`~/.applift/credentials.toml 还没有 profile "${target}"(可用: ${available.join(", ") || "无"}),发版前记得补凭据`);
296
+ });
297
+ program.command("status").description("查询各渠道审核状态").option("--stores <list>", "渠道列表(逗号分隔),缺省=项目配置全部").option("--cwd <dir>", "项目目录", process.cwd()).option("--json", "结构化输出(stdout 给机器)").action(async (cmdOpts) => {
298
+ registryRef.setup();
299
+ const { config } = await loadProjectConfig(cmdOpts.cwd);
300
+ const profile = loadCredentialsLayered(cmdOpts.cwd).profiles[resolveProfileFor(cmdOpts.cwd, config.profile)] ?? {};
301
+ const storeIds = (cmdOpts.stores ? cmdOpts.stores.split(",") : Object.keys(config.android?.stores ?? {})).filter(Boolean);
302
+ if (storeIds.length === 0) throw new Error("未指定渠道(--stores 或项目配置 android.stores)");
303
+ const detected = detectBundleIds(cmdOpts.cwd);
304
+ const isJson = Boolean(cmdOpts.json);
305
+ const results = [];
306
+ let hasError = false;
307
+ if (!isJson) p.intro(pc.bgBlue(pc.black(" applift status ")));
308
+ for (const id of storeIds) {
309
+ const channel = registryRef.registry.get(id);
310
+ if (!channel) {
311
+ results.push({
312
+ channel: id,
313
+ state: "unknown",
314
+ error: "未知渠道"
315
+ });
316
+ hasError = true;
317
+ if (!isJson) p.log.error(`${id}: 未知渠道`);
318
+ continue;
319
+ }
320
+ const target = resolveChannelTarget(id, config, detected);
321
+ if (!channel.capabilities.status) {
322
+ results.push({
323
+ channel: id,
324
+ state: "unknown",
325
+ error: "该渠道状态查询暂未接入"
326
+ });
327
+ continue;
328
+ }
329
+ try {
330
+ const st = await channel.status({
331
+ credentials: profile,
332
+ target
333
+ });
334
+ results.push({
335
+ channel: id,
336
+ ...st
337
+ });
338
+ if (!isJson) {
339
+ const icon = st.state === "approved" ? pc.green("●") : st.state === "rejected" ? pc.red("✗") : st.state === "reviewing" ? pc.yellow("●") : pc.dim("◌");
340
+ const text = st.state === "approved" ? `审核通过${st.live ? pc.green("(已上架)") : pc.dim("(未上架)")}` : st.state === "rejected" ? `被驳回${st.reason ? ` — ${st.reason}` : ""}` : st.state === "reviewing" ? "审核中" : st.raw ?? "未知";
341
+ p.log.message(` ${icon} ${pc.bold(channel.label)} ${text}`);
342
+ }
343
+ } catch (e) {
344
+ results.push({
345
+ channel: id,
346
+ state: "unknown",
347
+ error: e.message
348
+ });
349
+ hasError = true;
350
+ if (!isJson) p.log.error(`${channel.label}: ${e.message}`);
351
+ }
352
+ }
353
+ if (isJson) process.stdout.write(JSON.stringify({ status: results }, null, 2) + "\n");
354
+ else p.outro("状态查询完成");
355
+ if (hasError) process.exitCode = 1;
356
+ });
357
+ program.command("history").description("发版历史(stage/submit 记录,~/.applift/history.jsonl)").option("--channel <id>", "按渠道过滤").option("--sha256 <hash>", "按产物 sha256 过滤").option("--limit <n>", "最近 n 条(缺省 20)", "20").option("--json", "JSON 输出(AI agent 友好)").action(async (cmdOpts) => {
358
+ const entries = readEntries({
359
+ channel: cmdOpts.channel,
360
+ sha256: cmdOpts.sha256
361
+ });
362
+ const limit = Number(cmdOpts.limit);
363
+ const recent = (Number.isFinite(limit) && limit > 0 ? entries.slice(-limit) : entries).reverse();
364
+ if (cmdOpts.json) {
365
+ console.log(JSON.stringify(recent, null, 2));
366
+ return;
367
+ }
368
+ p.intro(pc.bgBlue(pc.black(" applift history ")));
369
+ if (recent.length === 0) p.log.message(pc.dim("暂无记录(发版执行 release 后自动落一条)"));
370
+ for (const e of recent) {
371
+ const time = new Date(e.ts).toLocaleString();
372
+ const icon = e.ok ? pc.green("✓") : pc.red("✗");
373
+ p.log.message(` ${icon} ${pc.dim(time)} ${pc.bold(e.channel.padEnd(10))} ${e.action.padEnd(6)} ${e.artifactVersion ? pc.dim(e.artifactVersion) : ""}${e.ref ? pc.dim(` ref=${e.ref}`) : ""}`);
374
+ }
375
+ p.outro(`共 ${recent.length} 条`);
376
+ });
377
+ }
378
+ //#endregion
379
+ //#region src/listing.ts
380
+ /**
381
+ * listing 命令:更新商店应用信息(简介/描述/图标/截图,按渠道覆盖)。
382
+ * --dry-run 只打印合并后的 per-store 生效字段;--json 结构化输出(AI agent 友好)。
383
+ */
384
+ function registerListingCommand(program, registryRef) {
385
+ program.command("listing").description("更新商店应用信息(--dry-run 预览,--json 结构化输出)").requiredOption("--stores <list>", "渠道列表(逗号分隔),如 huawei").option("--file <path>", "listing 配置文件(TS:export default { brief, description, stores })").option("--brief <text>", "应用简介(直传,覆盖 --file 中的值)").option("--description <text>", "应用描述(直传,覆盖 --file 中的值)").option("--cwd <dir>", "项目目录", process.cwd()).option("--json", "JSON 输出").option("--dry-run", "只预览各渠道生效字段,不调用接口").action(async (opts) => {
386
+ registryRef.setup();
387
+ if (!opts.file && opts.brief === void 0 && opts.description === void 0) throw new Error("未提供 listing 内容(--file 或 --brief/--description)");
388
+ let listing = {};
389
+ if (opts.file) listing = await loadListingFile(opts.file);
390
+ if (opts.brief !== void 0) listing.brief = opts.brief;
391
+ if (opts.description !== void 0) listing.description = opts.description;
392
+ assertListingFiles(listing);
393
+ const storeIds = (opts.stores ?? "").split(",").map((s) => s.trim()).filter(Boolean);
394
+ if (storeIds.length === 0) throw new Error("未指定渠道(--stores huawei[,xiaomi])");
395
+ const { config } = await loadProjectConfig(opts.cwd ?? process.cwd());
396
+ const profile = loadCredentials().profiles[config.profile] ?? {};
397
+ if (opts.dryRun) {
398
+ const preview = storeIds.map((id) => ({
399
+ store: id,
400
+ ...resolveListing(listing, id)
401
+ }));
402
+ if (opts.json) console.log(JSON.stringify({
403
+ dryRun: true,
404
+ stores: preview
405
+ }));
406
+ else {
407
+ p.intro(pc.bgBlue(pc.black(" applift listing (dry-run) ")));
408
+ for (const item of preview) {
409
+ const fields = {
410
+ brief: item.brief,
411
+ description: item.description,
412
+ iconPath: item.iconPath,
413
+ screenshots: item.screenshots
414
+ };
415
+ p.log.message(` ${pc.bold(item.store)} ${pc.dim(JSON.stringify(fields))}`);
416
+ }
417
+ p.outro("预览完成(未调用任何接口)");
418
+ }
419
+ return;
420
+ }
421
+ const results = [];
422
+ if (!opts.json) p.intro(pc.bgBlue(pc.black(" applift listing ")));
423
+ for (const id of storeIds) {
424
+ const channel = registryRef.registry.get(id);
425
+ if (!channel) {
426
+ results.push({
427
+ ok: false,
428
+ store: id,
429
+ updated: [],
430
+ skipped: [],
431
+ error: "未知渠道"
432
+ });
433
+ if (!opts.json) p.log.error(`${id}: 未知渠道`);
434
+ continue;
435
+ }
436
+ const listingChannel = channel;
437
+ if (typeof listingChannel.updateListing !== "function") {
438
+ results.push({
439
+ ok: false,
440
+ store: id,
441
+ updated: [],
442
+ skipped: [],
443
+ error: "该渠道暂不支持 listing 更新"
444
+ });
445
+ if (!opts.json) p.log.message(` ${pc.yellow("◌")} ${pc.bold(channel.label)} ${pc.dim("listing 更新暂未接入")}`);
446
+ continue;
447
+ }
448
+ try {
449
+ const res = await listingChannel.updateListing({
450
+ credentials: profile,
451
+ target: config.android?.stores?.[id] ?? {},
452
+ emit: (e) => {
453
+ if (!opts.json && e.type === "warn") p.log.warn(e.message);
454
+ }
455
+ }, listing);
456
+ results.push({
457
+ ...res,
458
+ label: channel.label
459
+ });
460
+ if (!opts.json) {
461
+ const up = res.updated.length ? pc.green(`已更新: ${res.updated.join(", ")}`) : pc.dim("无可写字段");
462
+ const skip = res.skipped.length ? pc.yellow(` 跳过: ${res.skipped.join(", ")}`) : "";
463
+ p.log.message(` ${pc.green("●")} ${pc.bold(channel.label)} ${up}${skip}`);
464
+ }
465
+ } catch (e) {
466
+ results.push({
467
+ ok: false,
468
+ store: id,
469
+ updated: [],
470
+ skipped: [],
471
+ error: e.message,
472
+ label: channel.label
473
+ });
474
+ if (!opts.json) p.log.error(`${channel.label}: ${e.message}`);
475
+ }
476
+ }
477
+ if (opts.json) console.log(JSON.stringify({ stores: results }));
478
+ else p.outro("listing 更新完成");
479
+ });
480
+ }
481
+ //#endregion
482
+ //#region src/channel-meta.ts
483
+ /**
484
+ * 渠道配置元数据:所需凭据键与获取指引(channels 命令消费,AI agent 自发现的依据)。
485
+ * 与 lifecycle.ts 的 CREDENTIAL_FIELDS 保持单一来源:此处为 channel → keys 映射。
486
+ */
487
+ const REQUIRED_KEYS_BY_CHANNEL = {
488
+ pgyer: ["PGYER_API_KEY"],
489
+ huawei: [
490
+ "HW_CLIENT_ID",
491
+ "HW_CLIENT_SECRET",
492
+ "HW_ANDROID_APP_ID"
493
+ ],
494
+ "agc-harmony": ["AGC_CLIENT_ID", "AGC_CLIENT_SECRET"],
495
+ xiaomi: [
496
+ "MI_USER_NAME",
497
+ "MI_PASSWORD",
498
+ "MI_CERT_PATH"
499
+ ],
500
+ oppo: ["OPPO_CLIENT_ID", "OPPO_CLIENT_SECRET"],
501
+ vivo: ["VIVO_ACCESS_KEY", "VIVO_ACCESS_SECRET"],
502
+ honor: [
503
+ "HONOR_CLIENT_ID",
504
+ "HONOR_CLIENT_SECRET",
505
+ "HONOR_APP_ID"
506
+ ],
507
+ yyb: [
508
+ "YYB_USER_ID",
509
+ "YYB_ACCESS_SECRET",
510
+ "YYB_APP_ID"
511
+ ],
512
+ asc: ["ASC_KEY_ID", "ASC_ISSUER_ID"],
513
+ googleplay: ["GOOGLEPLAY_SA_JSON 或 GOOGLEPLAY_SA_KEY"]
514
+ };
515
+ const CHANNEL_HINTS = {
516
+ pgyer: "pgyer.com → 账号设置 → API(免费,内测分发三端)",
517
+ huawei: "AGC → 项目设置 → API Client(安卓应用 App ID 独立)",
518
+ "agc-harmony": "AGC → 项目设置 → API Client(与华为安卓同源凭据,appId 可按 bundleName 自动查询)",
519
+ xiaomi: "dev.mi.com → 管理中心 → API(需 dev.api.public.cer 证书路径)",
520
+ oppo: "open.oppomobile.com → 管理中心 → API 传包",
521
+ vivo: "dev.vivo.com.cn → 应用分发 → API 传包(有沙箱环境可免费真测)",
522
+ honor: "developer.honor.com → 设置 → API",
523
+ yyb: "app.open.qq.com 管理中心(UserID + AccessSecret)",
524
+ asc: "App Store Connect → 用户和访问 → 集成 → 团队密钥;p8 放 ~/.applift/private_keys/",
525
+ googleplay: "Google Cloud Console → Service Account(JSON 密钥)+ Play Console 关联"
526
+ };
527
+ //#endregion
528
+ //#region src/index.ts
529
+ /**
530
+ * CLI:commander 参数解析 + @clack/prompts 交互 + picocolors 美化。
531
+ * 薄壳——渲染 core 事件流与插件钩子结果,业务全在 core。
532
+ */
533
+ /** 框架探测(含插件注册的自定义适配器) */
534
+ function detectFrameworkWith(cwd, extra) {
535
+ return detectFramework(cwd, extra);
536
+ }
537
+ const program = new Command("applift");
538
+ program.name("applift").description("三端 App 发版工具(内测 / 多商店提审 / 插件扩展)").version("0.2.0");
539
+ function makeContext(channel, creds, target, artifactPath, note, extra = {}) {
540
+ const label = pc.bold(channel.label);
541
+ const emit = (event) => {
542
+ if (event.type === "phase") {
543
+ const phaseText = {
544
+ auth: pc.blue("鉴权"),
545
+ upload: pc.blue("上传"),
546
+ bind: pc.blue("绑定"),
547
+ wait: pc.yellow("等待"),
548
+ submit: pc.magenta("提审"),
549
+ done: pc.green("完成")
550
+ }[event.phase] ?? event.phase;
551
+ process.stderr.write(` ${label} ${phaseText}${event.message ? pc.dim(` — ${event.message}`) : ""}\n`);
552
+ } else if (event.type === "warn") process.stderr.write(` ${label} ⚠ ${event.message}\n`);
553
+ };
554
+ return {
555
+ credentials: creds,
556
+ target,
557
+ artifactPath,
558
+ updateDescription: note,
559
+ releaseTime: extra.releaseTime,
560
+ remark: extra.remark,
561
+ installPassword: extra.installPassword,
562
+ emit
563
+ };
564
+ }
565
+ /** clack 确认:计划表 → confirm(非 TTY/CI 环境自动拒绝,需 --yes) */
566
+ async function confirmWithClack(plans) {
567
+ const rows = plans.map((plan) => ` ${pc.cyan(plan.platform.padEnd(7))} ${pc.bold(plan.channel.padEnd(12))} ${plan.action.padEnd(13)} ${plan.afterApproval === "manual" ? pc.green("审核通过后手动发布") : plan.afterApproval === "scheduled" ? pc.yellow("定时上架") : pc.red("审核通过后自动上架")}`);
568
+ p.log.message(`${pc.bold("┌ 发布计划")} ${pc.dim("────────────────────────────┐")}`);
569
+ for (const r of rows) p.log.message(r);
570
+ p.log.message(`${pc.bold("└───────────────────────────────────┘")}`);
571
+ if (plans.some((x) => x.afterApproval === "auto-publish")) p.log.warn(`${pc.red("⚠")} 提审后审核通过即自动上架,无法撤回(iOS manual 除外)`);
572
+ const yes = await p.confirm({ message: `确认提交以上 ${plans.length} 项?` });
573
+ if (p.isCancel(yes)) return false;
574
+ return Boolean(yes);
575
+ }
576
+ let channelsRegistered = false;
577
+ function setupChannels() {
578
+ if (channelsRegistered) return;
579
+ channelsRegistered = true;
580
+ registry.register(new PgyerChannel());
581
+ registry.register(new HuaweiAndroidChannel());
582
+ registry.register(new XiaomiChannel());
583
+ registry.register(new OppoChannel());
584
+ registry.register(new VivoChannel());
585
+ registry.register(new HonorChannel());
586
+ registry.register(new YybChannel());
587
+ registry.register(new AgcHarmonyChannel());
588
+ registry.register(new AscChannel());
589
+ registry.register(new GooglePlayChannel());
590
+ }
591
+ /** 装载插件宿主与生效配置:c12 只 load 一次;config:resolved 钩子返回值即生效配置(P2-5) */
592
+ async function loadPluginHost(cwd) {
593
+ const { config, raw } = await loadProjectConfig(cwd);
594
+ const plugins = resolvePlugins(raw);
595
+ if (plugins.length === 0) return {
596
+ config,
597
+ extraFrameworks: []
598
+ };
599
+ const host = new PluginHost(plugins);
600
+ await host.broadcast("channel:register", { cwd }, registry);
601
+ const extraFrameworks = [];
602
+ await host.broadcast("framework:register", { cwd }, extraFrameworks);
603
+ return {
604
+ host,
605
+ config: (await host.apply("config:resolved", config, { cwd })).value,
606
+ extraFrameworks
607
+ };
608
+ }
609
+ program.command("beta").description("内测分发(android/ios → 蒲公英扫码即装;harmony 默认 AGC 测试发布,--via pgyer 改走蒲公英)").argument("<platform>", "android | harmony | ios").option("--artifact <path>", "产物路径(缺省自动探测最新构建产物)").option("--note <text>", "更新说明").option("--via <target>", "harmony 分发通道:agc(默认,AGC 指定设备测试发布)| pgyer", "agc").option("--install-password <pwd>", "安装密码(蒲公英密码安装)").option("--cwd <dir>", "项目目录", process.cwd()).action(async (platform, cmdOpts) => {
610
+ setupChannels();
611
+ const { extraFrameworks: pluginFrameworks } = await loadPluginHost(cmdOpts.cwd);
612
+ const { config: projCfg } = await loadProjectConfig(cmdOpts.cwd);
613
+ const creds = resolveProfile(loadCredentialsLayered(cmdOpts.cwd), resolveProfileFor(cmdOpts.cwd, projCfg.profile), KNOWN_CREDENTIAL_KEYS);
614
+ const detected = detectBundleIds(cmdOpts.cwd);
615
+ let artifactPath = cmdOpts.artifact ? path.resolve(cmdOpts.artifact) : void 0;
616
+ if (!artifactPath) {
617
+ const framework = detectFrameworkWith(cmdOpts.cwd, pluginFrameworks);
618
+ const patterns = projCfg.artifacts?.[platform] ?? framework?.artifactPatterns(platform) ?? [];
619
+ artifactPath = findArtifacts(cmdOpts.cwd, patterns)[0];
620
+ }
621
+ if (!artifactPath) throw new Error("未找到构建产物:先构建或用 --artifact 指定");
622
+ if (!fs.existsSync(artifactPath)) throw new Error(`产物不存在: ${artifactPath}`);
623
+ const s = p.spinner();
624
+ if (platform === "harmony" && cmdOpts.via !== "pgyer") {
625
+ const agc = registry.get("agc-harmony");
626
+ if (!agc) throw new Error("agc-harmony 渠道未注册");
627
+ const target = resolveChannelTarget("agc-harmony", projCfg, detected);
628
+ s.start(`AGC 测试发布 ${path.basename(artifactPath)}`);
629
+ const ctx = makeContext(agc, creds, {
630
+ ...target,
631
+ releaseType: "6"
632
+ }, artifactPath, cmdOpts.note);
633
+ const r = await agc.stage({
634
+ ...ctx,
635
+ emit: () => {}
636
+ });
637
+ if (r.ok) s.stop(`AGC 测试包已上传 ${pc.green("✓")} 到控制台「我的应用 → 测试」指定测试设备`);
638
+ else {
639
+ s.stop(`AGC 测试发布失败: ${r.error}`);
640
+ process.exitCode = 1;
641
+ }
642
+ return;
643
+ }
644
+ const pgyer = registry.get("pgyer");
645
+ if (!pgyer) throw new Error("pgyer 渠道未注册");
646
+ s.start(`蒲公英上传 ${path.basename(artifactPath)}`);
647
+ const ctx = makeContext(pgyer, creds, resolveChannelTarget("pgyer", projCfg, detected), artifactPath, cmdOpts.note, { installPassword: cmdOpts.installPassword });
648
+ const r = await pgyer.stage({
649
+ ...ctx,
650
+ emit: () => {}
651
+ });
652
+ if (r.ok) s.stop(`蒲公英发布成功 ${pc.green("✓")} 扫码即装`);
653
+ else {
654
+ s.stop(`蒲公英上传失败: ${r.error}`);
655
+ process.exitCode = 1;
656
+ }
657
+ });
658
+ program.command("release").description("正式发版两段式:不带 --submit 只传包到各家草稿(安全);--submit = 自动完成传包+确认+提审一条龙(审核通过即上架)").argument("<platform>", "android | harmony | ios").option("--stores <list>", "渠道列表(逗号分隔);缺省进入交互多选").option("--submit", "提审(L3 不可逆:审核通过自动上架)").option("--skip-stage", "与 --submit 同用:复用已 stage 的包直接提审").option("--artifact <path>", "产物路径;缺省自动探测框架最新构建产物").option("--note <text>", "更新说明").option("--release-time <rfc3339>", "定时上架时间(RFC3339,仅 scheduled 渠道生效)").option("--remark <text>", "提审备注(给审核员)").option("--profile <name>", "凭据 profile(最高优先级;缺省走 applift use 个人选择 > 项目配置团队默认)").option("--cwd <dir>", "项目目录", process.cwd()).option("--yes", "CI 无人值守豁免确认").option("--dry-run", "只打印计划").option("--json", "结构化输出(stdout 给机器,渲染抑制)").action(async (platform, raw) => {
659
+ const opts = raw;
660
+ const KNOWN_PLATFORMS = [
661
+ "android",
662
+ "harmony",
663
+ "ios",
664
+ "windows",
665
+ "macos",
666
+ "linux"
667
+ ];
668
+ if (!KNOWN_PLATFORMS.includes(platform)) process.stderr.write(`⚠ 平台 "${platform}" 非内置(${KNOWN_PLATFORMS.join(" | ")});若你注册了自定义框架适配器则继续\n`);
669
+ setupChannels();
670
+ const { host: plugins, config, extraFrameworks: pluginFrameworks } = await loadPluginHost(opts.cwd);
671
+ const global = loadGlobalConfig();
672
+ const creds = opts.dryRun ? {} : resolveProfile(loadCredentials(), opts.profile ?? resolveProfileFor(opts.cwd, config.profile), KNOWN_CREDENTIAL_KEYS);
673
+ const isJson = Boolean(opts.json);
674
+ const framework = detectFrameworkWith(opts.cwd, pluginFrameworks);
675
+ if (!isJson) {
676
+ if (framework) p.log.info(`框架: ${pc.bold(framework.expoAwareLabel?.(opts.cwd) ?? framework.label)}${pc.dim(` (${framework.id})`)}`);
677
+ else p.log.warn("未识别框架,产物路径请用 --artifact 指定");
678
+ }
679
+ let artifactPath = opts.artifact ? path.resolve(opts.artifact) : void 0;
680
+ if (!artifactPath) {
681
+ const patterns = config.artifacts?.[platform] ?? framework?.artifactPatterns(platform) ?? [];
682
+ artifactPath = findArtifacts(opts.cwd, patterns)[0];
683
+ if (artifactPath && !isJson) {
684
+ const via = config.artifacts?.[platform] ? "项目配置 artifacts" : `${framework?.label} 内置 pattern`;
685
+ p.log.info(`自动探测产物(${via}): ${pc.bold(path.basename(artifactPath))} ${pc.dim(artifactPath)}`);
686
+ }
687
+ }
688
+ if (!artifactPath && !opts.dryRun) throw new Error("未找到构建产物:先构建或用 --artifact 指定");
689
+ if (artifactPath && !fs.existsSync(artifactPath) && !opts.dryRun) throw new Error(artifactPath ? `产物不存在: ${artifactPath}` : "未找到构建产物:先构建或用 --artifact 指定");
690
+ let storeIds;
691
+ if (opts.stores) {
692
+ storeIds = opts.stores.split(",").map((s) => s.trim()).filter(Boolean);
693
+ const known = new Set(registry.all().map((c) => c.id));
694
+ for (const id of storeIds) if (!known.has(id)) process.stderr.write(`⚠ 未知渠道 ${id}(可用: ${[...known].join(", ")})\n`);
695
+ } else {
696
+ const configured = Object.keys(config.android?.stores ?? {});
697
+ if (configured.length > 0) storeIds = configured;
698
+ else if (opts.dryRun || opts.yes || isJson || global.assumeYes) storeIds = registry.all().map((c) => c.id);
699
+ else {
700
+ const selected = await p.multiselect({
701
+ message: "选择提审渠道",
702
+ options: registry.all().filter((c) => c.id !== "pgyer").map((c) => ({
703
+ value: c.id,
704
+ label: c.label
705
+ })),
706
+ required: true
707
+ });
708
+ if (p.isCancel(selected)) throw new Error("已取消");
709
+ storeIds = selected;
710
+ }
711
+ }
712
+ const channels = storeIds.map((id) => registry.get(id)).filter((c) => c !== void 0);
713
+ if (channels.length === 0) throw new Error(`未找到渠道(${storeIds.join(", ")})。已注册: ${registry.all().map((c) => c.id).join(", ")}`);
714
+ const detected = detectBundleIds(opts.cwd);
715
+ const contexts = /* @__PURE__ */ new Map();
716
+ for (const c of channels) contexts.set(c.id, makeContext(c, creds, resolveChannelTarget(c.id, config, detected), artifactPath ?? "", opts.note, {
717
+ releaseTime: opts.releaseTime,
718
+ remark: opts.remark
719
+ }));
720
+ if (opts.dryRun) {
721
+ const plans = [];
722
+ const errors = {};
723
+ for (const c of channels) try {
724
+ const plan = await c.plan(contexts.get(c.id));
725
+ plans.push(plan);
726
+ if (!isJson) p.log.message(`${pc.cyan(plan.platform.padEnd(8))} ${pc.bold(plan.channel.padEnd(12))} → ${plan.action} (${plan.afterApproval})`);
727
+ } catch (e) {
728
+ errors[c.id] = e.message;
729
+ if (!isJson) p.log.warn(`${pc.bold(c.label)}: ${e.message}`);
730
+ }
731
+ if (isJson) process.stdout.write(JSON.stringify({
732
+ dryRun: true,
733
+ plans,
734
+ errors
735
+ }, null, 2) + "\n");
736
+ return;
737
+ }
738
+ const confirmFn = opts.yes || global.assumeYes ? async (plans) => {
739
+ p.log.warn(`[CI 模式] assumeYes,自动确认 ${plans.length} 项提审`);
740
+ return true;
741
+ } : confirmWithClack;
742
+ let cachedSha;
743
+ const artifactSha256 = () => {
744
+ if (!artifactPath || !fs.existsSync(artifactPath)) return "";
745
+ cachedSha ??= sha256File(artifactPath);
746
+ return cachedSha;
747
+ };
748
+ const historySink = (e) => {
749
+ appendEntry({
750
+ ts: Date.now(),
751
+ cwd: opts.cwd,
752
+ artifactSha256: artifactSha256(),
753
+ ...e
754
+ });
755
+ };
756
+ const pipelineInput = {
757
+ channels,
758
+ contexts,
759
+ concurrency: global.concurrency,
760
+ cwd: opts.cwd,
761
+ confirm: confirmFn,
762
+ plugins,
763
+ historySink
764
+ };
765
+ if (!opts.submit) {
766
+ if (!isJson) p.intro(pc.bgCyan(pc.black(" applift release · stage ")));
767
+ const interactive = process.stdout.isTTY && !isJson && !opts.yes && !global.assumeYes;
768
+ if (interactive) {
769
+ const list = channels.map((c) => c.label).join("、");
770
+ const ok = await p.confirm({ message: `将上传到 ${pc.bold(list)} 的草稿区(不提审),继续?` });
771
+ if (p.isCancel(ok) || !ok) throw new Error("已取消(未做任何变更)");
772
+ }
773
+ const summary = await runStage(pipelineInput);
774
+ if (isJson) process.stdout.write(JSON.stringify({ stage: summary.stage.map((e) => ({
775
+ channel: e.channel,
776
+ ok: e.ok,
777
+ ref: void 0,
778
+ error: e.stageError
779
+ })) }, null, 2) + "\n");
780
+ else renderSummary(summary.stage);
781
+ if (summary.stage.some((e) => !e.ok)) {
782
+ process.exitCode = 1;
783
+ if (!isJson) p.outro("部分渠道传包失败,未提审;修复后重跑");
784
+ return;
785
+ }
786
+ if (interactive) {
787
+ const goSubmit = await p.confirm({ message: `草稿已就绪。${pc.red("现在提审?")}(审核通过即自动上架,不可撤回)` });
788
+ if (p.isCancel(goSubmit) || !goSubmit) {
789
+ p.outro(`已保留草稿。之后提审: ${pc.cyan(`applift release ${platform} --submit --skip-stage`)}`);
790
+ return;
791
+ }
792
+ const submitSummary = await runSubmit(pipelineInput, { skipStage: true });
793
+ renderSummary(submitSummary.submit);
794
+ if (submitSummary.submit.some((s) => !s.ok)) process.exitCode = 1;
795
+ p.outro("完成");
796
+ return;
797
+ }
798
+ if (!isJson) p.outro(`已传包到草稿(${pc.bold("未提审")})。提审执行: ${pc.cyan(`applift release ${platform} --submit`)}`);
799
+ return;
800
+ }
801
+ if (!isJson) p.intro(pc.bgMagenta(pc.black(" applift release · submit ")));
802
+ if (!opts.skipStage && artifactPath && fs.existsSync(artifactPath)) {
803
+ const sha = artifactSha256();
804
+ const stagedChannels = new Set(readEntries({ sha256: sha }).filter((h) => h.action === "stage" && h.ok).map((h) => h.channel));
805
+ const reusable = channels.filter((c) => stagedChannels.has(c.id)).map((c) => c.id);
806
+ if (reusable.length > 0) p.log.warn(`以下渠道已有同包(sha256 ${sha.slice(0, 8)}…)成功 stage 记录:${pc.bold(reusable.join(", "))}。如草稿未被改动,可加 ${pc.cyan("--skip-stage")} 复用免重传`);
807
+ }
808
+ if (opts.skipStage && artifactPath && fs.existsSync(artifactPath)) {
809
+ const sha = artifactSha256();
810
+ for (const ch of channels) {
811
+ const last = readEntries({ channel: ch.id }).find((h) => h.action === "stage" && h.ok);
812
+ if (last && last.artifactSha256 && last.artifactSha256 !== sha) p.log.warn(`${pc.bold(ch.label)} 的最近草稿是另一个包(sha ${last.artifactSha256.slice(0, 8)}… ≠ 当前 ${sha.slice(0, 8)}…)——${pc.red("提审的将是商店后台当前草稿,请确认")}(去掉 --skip-stage 重传当前包)`);
813
+ }
814
+ }
815
+ const summary = await runSubmit(pipelineInput, { skipStage: Boolean(opts.skipStage) });
816
+ if (isJson) process.stdout.write(JSON.stringify({ submit: summary.submit.map((e) => ({
817
+ channel: e.channel,
818
+ ok: e.ok,
819
+ error: e.submitError ?? e.stageError
820
+ })) }, null, 2) + "\n");
821
+ else {
822
+ renderSummary(summary.submit);
823
+ p.outro("完成");
824
+ }
825
+ if (summary.submit.some((s) => !s.ok)) process.exitCode = 1;
826
+ });
827
+ function renderSummary(entries) {
828
+ for (const e of entries) {
829
+ const raw = e.stageError ?? e.submitError;
830
+ const hints = e.channel ? registry.get(e.channel)?.errorHints : void 0;
831
+ const err = raw ? humanizeError(hints, raw) : raw;
832
+ p.log.message(` ${e.ok ? pc.green("✓") : pc.red("✗")} ${pc.bold(e.label)}${err ? pc.red(` — ${err}`) : ""}`);
833
+ }
834
+ }
835
+ program.command("doctor").description("体检:框架/凭据/渠道配置").option("--cwd <dir>", "项目目录", process.cwd()).action(async (cmdOpts) => {
836
+ setupChannels();
837
+ const framework = detectFramework(cmdOpts.cwd);
838
+ p.intro(pc.bgBlue(pc.black(" applift doctor ")));
839
+ if (framework) p.log.success(`框架识别: ${pc.bold(framework.label)}`);
840
+ else p.log.error("框架识别失败(目录结构不符合任何已知框架)");
841
+ const ids = detectBundleIds(cmdOpts.cwd);
842
+ const idParts = [
843
+ ids.androidPkgName ? `android ${pc.bold(ids.androidPkgName)}` : "",
844
+ ids.harmonyBundleName ? `harmony ${pc.bold(ids.harmonyBundleName)}` : "",
845
+ ids.iosBundleId ? `ios ${pc.bold(ids.iosBundleId)}${ids.iosScheme ? pc.dim(`(scheme ${ids.iosScheme})`) : ""}` : ""
846
+ ].filter(Boolean);
847
+ if (idParts.length > 0) p.log.success(`自动推导: ${idParts.join(" · ")} ${pc.dim("(配置缺省时兜底)")}`);
848
+ else p.log.warn("自动推导: 未识别到三端标识(app.json / build.gradle / AppScope/app.json5 / pbxproj 均未命中)");
849
+ let healthy = true;
850
+ const creds = loadCredentials();
851
+ const profileNames = Object.keys(creds.profiles);
852
+ if (profileNames.length > 0) p.log.success(`凭据 profiles: ${pc.bold(profileNames.join(", "))}`);
853
+ else p.log.error("无凭据(~/.applift/credentials.toml)");
854
+ if (profileNames.length === 0) healthy = false;
855
+ const { config } = await loadProjectConfig(cmdOpts.cwd);
856
+ const profile = resolveProfileFor(cmdOpts.cwd, config.profile);
857
+ const stores = Object.keys(config.android?.stores ?? {});
858
+ if (stores.length > 0) p.log.success(`项目渠道: ${stores.join(", ")}(profile=${profile})`);
859
+ else p.log.info("项目未配置渠道(applift.config.ts → android.stores)");
860
+ const activeProfile = creds.profiles[profile];
861
+ if (stores.length > 0) {
862
+ if (!activeProfile) {
863
+ p.log.error(`凭据 profile "${profile}" 不存在(可用: ${profileNames.join(", ") || "无"};个人可 applift use 切换)`);
864
+ healthy = false;
865
+ } else for (const id of stores) {
866
+ const need = REQUIRED_KEYS_BY_CHANNEL[id];
867
+ if (!need) continue;
868
+ const missing = need.filter((k) => !k.includes(" 或 ") && !activeProfile[k]);
869
+ if (missing.length > 0) {
870
+ p.log.error(`${pc.bold(id)}: 缺凭据 ${missing.join(", ")} — ${CHANNEL_HINTS[id] ?? ""}`);
871
+ healthy = false;
872
+ } else p.log.success(`${id}: 凭据齐全`);
873
+ }
874
+ }
875
+ if (framework) for (const plat of [
876
+ "android",
877
+ "harmony",
878
+ "ios"
879
+ ]) {
880
+ const patterns = config.artifacts?.[plat] ?? framework.artifactPatterns(plat);
881
+ const found = findArtifacts(cmdOpts.cwd, patterns)[0];
882
+ if (found) {
883
+ const v = readArtifactVersion(found);
884
+ p.log.info(`产物 ${plat}: ${pc.bold(path.basename(found))}${v ? `(${v.versionName ?? "?"} build ${v.versionCode ?? "?"},读自 ${v.source})` : pc.dim("(版本自动读取不支持该格式)")}`);
885
+ break;
886
+ }
887
+ }
888
+ p.outro(healthy ? "体检完成:一切就绪" : "体检完成:存在待修复项(见上)");
889
+ if (!healthy) process.exitCode = 1;
890
+ });
891
+ registerListingCommand(program, {
892
+ setup: setupChannels,
893
+ registry
894
+ });
895
+ /** 环境变量兼容的裸键名全集(渠道凭据键展开) */
896
+ const KNOWN_CREDENTIAL_KEYS = [...new Set(Object.values(REQUIRED_KEYS_BY_CHANNEL).flat().flatMap((k) => k.split(" 或 ")))];
897
+ registerLifecycleCommands(program, {
898
+ setup: setupChannels,
899
+ registry
900
+ });
901
+ program.command("channels").description("列出全部渠道及所需配置(--json 供 AI agent 消费)").option("--json", "JSON 输出").action(async (cmdOpts) => {
902
+ setupChannels();
903
+ const rows = registry.all().map((c) => ({
904
+ id: c.id,
905
+ label: c.label,
906
+ capabilities: c.capabilities,
907
+ credentials: REQUIRED_KEYS_BY_CHANNEL[c.id] ?? [],
908
+ hint: CHANNEL_HINTS[c.id] ?? ""
909
+ }));
910
+ if (cmdOpts.json) {
911
+ process.stdout.write(JSON.stringify({ channels: rows }, null, 2) + "\n");
912
+ return;
913
+ }
914
+ for (const r of rows) process.stdout.write(` ${pc.bold(r.id.padEnd(14))} ${r.label} ${pc.dim(r.credentials.join("/") || "无需凭据")}\n`);
915
+ });
916
+ //#endregion
917
+ export { program as n, makeContext as t };
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@applift/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/hcg1023/applift.git",
7
+ "directory": "packages/cli"
8
+ },
9
+ "bin": {
10
+ "applift": "./dist/bin.mjs"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@applift/core": "0.1.1",
21
+ "@clack/prompts": "^1.8.1",
22
+ "commander": "^15.0.0",
23
+ "picocolors": "^1.1.1"
24
+ },
25
+ "devDependencies": {
26
+ "tsdown": "^0.23.1",
27
+ "tsx": "^4.23.15"
28
+ },
29
+ "scripts": {
30
+ "build": "tsdown src/bin.ts src/index.ts --format esm --clean",
31
+ "typecheck": "tsc --noEmit",
32
+ "dev": "tsx src/bin.ts"
33
+ }
6
34
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.