publish-apk 0.0.1-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/package.json +40 -0
- package/src/apk.js +694 -0
- package/src/cert.js +1468 -0
- package/src/cli.js +185 -0
- package/src/google.js +1884 -0
- package/src/hap.js +939 -0
- package/src/harmony-material.js +174 -0
- package/src/help.js +50 -0
- package/src/huawei-signing.js +1279 -0
- package/src/huawei.js +3295 -0
- package/src/jwt.js +161 -0
- package/src/profile.js +753 -0
- package/src/publish-apk.js +161 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令行通用设施:程序名、退出代码、输出、帮助标记与选项解析。
|
|
3
|
+
*
|
|
4
|
+
* 本模块不含任何厂商信息——各厂商的命令与帮助文档放在各自的模块里
|
|
5
|
+
* (如 src/huawei.js),只复用这里的工具函数。
|
|
6
|
+
*/
|
|
7
|
+
import process from 'node:process'
|
|
8
|
+
|
|
9
|
+
/** 程序名,帮助文档的「用法」「示例」里使用 */
|
|
10
|
+
export const PROGRAM = 'publish-apk'
|
|
11
|
+
|
|
12
|
+
/** 退出代码:成功 */
|
|
13
|
+
export const EXIT_OK = 0
|
|
14
|
+
/** 退出代码:参数缺失、参数错误或调用失败 */
|
|
15
|
+
export const EXIT_FAILURE = 1
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 参数错误:统一「stderr 记录错误日志 + stdout 输出对应帮助文档 + 退出代码 1」。
|
|
19
|
+
*
|
|
20
|
+
* help 传的是渲染好的帮助文档文本,因此本模块不需要知道任何厂商的帮助内容。
|
|
21
|
+
*/
|
|
22
|
+
export class UsageError extends Error {
|
|
23
|
+
constructor(message, help) {
|
|
24
|
+
super(message)
|
|
25
|
+
this.name = 'UsageError'
|
|
26
|
+
this.help = help
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const stdout = (text) => process.stdout.write(`${text}\n`)
|
|
31
|
+
export const stderr = (text) => process.stderr.write(`${text}\n`)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 写入 stdout 并在数据写完(或冲刷进管道)后 resolve,供顺序敏感的输出使用。
|
|
35
|
+
*
|
|
36
|
+
* 用法错误要「先用法、后错误」:帮助文档写 stdout、错误日志写 stderr,两个流各自独立,
|
|
37
|
+
* 直接顺序调用时实际落地顺序取决于各流的缓冲,终端里常常先看到错误。因此这里提供可 await
|
|
38
|
+
* 的写入,让调用方能在写完帮助文档之后再写错误日志。EPIPE 等写入错误不在这里处理
|
|
39
|
+
* (由 installOutputGuards 决定忽略还是抛出),所以回调一律 resolve。
|
|
40
|
+
*/
|
|
41
|
+
export function writeStdout(text) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
process.stdout.write(`${text}\n`, () => resolve())
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 下游提前关闭管道时(例如 `... --help | head`、`| less` 后按 q、被工具截断输出),
|
|
49
|
+
* Node 默认把 EPIPE 抛成未捕获异常,于是「看帮助文档」会变成打印堆栈 + 退出代码 1。
|
|
50
|
+
* 帮助文档不该这样失败,所以静默忽略 EPIPE;其他写入错误照旧抛出。
|
|
51
|
+
*/
|
|
52
|
+
export function installOutputGuards() {
|
|
53
|
+
const ignoreEpipe = (error) => {
|
|
54
|
+
if (error?.code !== 'EPIPE') {
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
process.stdout.on('error', ignoreEpipe)
|
|
59
|
+
process.stderr.on('error', ignoreEpipe)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 帮助标记:子命令 help,或 -h / -help / --h / --help 等写法(忽略大小写)。
|
|
64
|
+
*
|
|
65
|
+
* 容错识别是为了保证「要帮助」永远不会被当成参数错误:
|
|
66
|
+
* help、-h、-help、--h、--help、--HELP、--help=true
|
|
67
|
+
*/
|
|
68
|
+
const HELP_REQUEST = /^(?:help|-{1,2}h(?:elp)?)$/i
|
|
69
|
+
|
|
70
|
+
/** 是否在请求帮助文档:去掉 `=value` 部分后按 HELP_REQUEST 判断 */
|
|
71
|
+
export function isHelpToken(token) {
|
|
72
|
+
const index = token.indexOf('=')
|
|
73
|
+
const name = index === -1 ? token : token.slice(0, index)
|
|
74
|
+
return HELP_REQUEST.test(name)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 判断某个 token 是否形如选项(--help、--private-key、--nope)。
|
|
79
|
+
*
|
|
80
|
+
* 注意:私钥 PEM 数据以 `-----BEGIN` 开头,即 `--` 后面紧跟 `-`,
|
|
81
|
+
* 因此这里要求 `--` 后面必须是字母,避免把 PEM 数据误判成选项。
|
|
82
|
+
*/
|
|
83
|
+
const looksLikeOption = (token) => /^--[A-Za-z]/.test(token)
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 回显用户输入前的脱敏处理。
|
|
87
|
+
*
|
|
88
|
+
* 私钥 PEM 数据被误当成「未知选项 / 未知参数」时(例如把私钥当裸参数传进来),
|
|
89
|
+
* 原样回显就会把整份密钥写进 stderr、CI 日志和工单里。这类值不逐字符掩码——
|
|
90
|
+
* 几千个星号同样刷屏——而是整体替换为占位符,只保留字符数用于排查。
|
|
91
|
+
* 判断依据:含 PEM 头、含换行,或长度明显超出选项名与常见路径。
|
|
92
|
+
*/
|
|
93
|
+
const SECRET_LIKE_TOKEN = /-----BEGIN|[\r\n]/
|
|
94
|
+
const SECRET_LIKE_TOKEN_LENGTH = 80
|
|
95
|
+
|
|
96
|
+
function redactToken(token) {
|
|
97
|
+
if (SECRET_LIKE_TOKEN.test(token) || token.length > SECRET_LIKE_TOKEN_LENGTH) {
|
|
98
|
+
return `(已省略,疑似敏感数据,共 ${token.length} 字符)`
|
|
99
|
+
}
|
|
100
|
+
return token
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 拆分 `--private-key=./private.pem` 形式的选项,返回 [选项名, 内联值] */
|
|
104
|
+
function splitInlineOption(token) {
|
|
105
|
+
if (!token.startsWith('--')) {
|
|
106
|
+
return [token, undefined]
|
|
107
|
+
}
|
|
108
|
+
const index = token.indexOf('=')
|
|
109
|
+
if (index === -1) {
|
|
110
|
+
return [token, undefined]
|
|
111
|
+
}
|
|
112
|
+
return [token.slice(0, index), token.slice(index + 1)]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 读取环境变量:未设置或为空串时返回 undefined,避免空串被当成「已提供」 */
|
|
116
|
+
export function readEnv(env, name) {
|
|
117
|
+
const value = env[name]
|
|
118
|
+
return value === undefined || value === '' ? undefined : value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 解析 `--name value` 与 `--name=value` 两种写法,各厂商命令共用同一套规则:
|
|
123
|
+
* - 只接受 allowed 里的选项名,其余报「未知选项」或「未知参数」
|
|
124
|
+
* - 同名选项重复出现报错、缺少参数值报错、值为空串报错
|
|
125
|
+
* - flags 里的开关型选项不带值,出现即为 true
|
|
126
|
+
*
|
|
127
|
+
* @param {string[]} argv 命令名之后的参数
|
|
128
|
+
* @param {Set<string>} allowed 该命令允许的全部选项名(含开关)
|
|
129
|
+
* @param {() => string} help 出错时附带的帮助文档(传函数是为了不在正常路径上白渲染)
|
|
130
|
+
* @param {Set<string>} [flags] 开关型选项名,不接收参数值
|
|
131
|
+
* @param {Set<string>} [multi] 可重复、可跟多个连续值的选项名(如 --file a b c)
|
|
132
|
+
* @returns {Map<string, string | string[] | true>} 选项名 → 值(开关为 true;multi 为数组)
|
|
133
|
+
*/
|
|
134
|
+
export function parseOptions(argv, allowed, help, flags = new Set(), multi = new Set()) {
|
|
135
|
+
const options = new Map()
|
|
136
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
137
|
+
const token = argv[index]
|
|
138
|
+
const [name, inlineValue] = splitInlineOption(token)
|
|
139
|
+
|
|
140
|
+
if (!allowed.has(name)) {
|
|
141
|
+
const kind = token.startsWith('-') ? '选项' : '参数'
|
|
142
|
+
throw new UsageError(`未知${kind} "${redactToken(token)}"`, help())
|
|
143
|
+
}
|
|
144
|
+
if (options.has(name) && !multi.has(name)) {
|
|
145
|
+
throw new UsageError(`选项 "${name}" 重复指定`, help())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 开关型选项:只认 `--verbose` 这种裸写法,写成 `--verbose=true` 属于误用
|
|
149
|
+
if (flags.has(name)) {
|
|
150
|
+
if (inlineValue !== undefined) {
|
|
151
|
+
throw new UsageError(`选项 "${name}" 是开关,不接受参数值`, help())
|
|
152
|
+
}
|
|
153
|
+
options.set(name, true)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const values = []
|
|
158
|
+
if (inlineValue !== undefined) {
|
|
159
|
+
values.push(inlineValue)
|
|
160
|
+
} else {
|
|
161
|
+
const value = argv[index + 1]
|
|
162
|
+
if (value === undefined || looksLikeOption(value)) {
|
|
163
|
+
throw new UsageError(`选项 "${name}" 缺少参数值`, help())
|
|
164
|
+
}
|
|
165
|
+
values.push(value)
|
|
166
|
+
index += 1
|
|
167
|
+
}
|
|
168
|
+
if (multi.has(name)) {
|
|
169
|
+
while (index + 1 < argv.length && !looksLikeOption(argv[index + 1])) {
|
|
170
|
+
values.push(argv[index + 1])
|
|
171
|
+
index += 1
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (values.some((value) => value === '')) {
|
|
175
|
+
throw new UsageError(`选项 "${name}" 的值不能为空`, help())
|
|
176
|
+
}
|
|
177
|
+
if (multi.has(name)) {
|
|
178
|
+
const previous = options.get(name)
|
|
179
|
+
options.set(name, previous === undefined ? values : [...previous, ...values])
|
|
180
|
+
} else {
|
|
181
|
+
options.set(name, values[0])
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return options
|
|
185
|
+
}
|