deveco_hmigbot 0.21.5
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 +26 -0
- package/README.md +50 -0
- package/agents/hmigbot-worker.md +61 -0
- package/agents/hmigbot.md +22 -0
- package/agents/workflow-subagent.md +55 -0
- package/commands/hmigbot.md +17 -0
- package/dist/index.js +1 -0
- package/manifest.json +19 -0
- package/package.json +29 -0
- package/skills/migrate-core/FILES.md +26 -0
- package/skills/migrate-core/SKILL.md +484 -0
- package/skills/migrate-core/references/README.md +64 -0
- package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
- package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
- package/skills/migrate-core/references/flow/conventions-template.md +244 -0
- package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
- package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
- package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
- package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
- package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
- package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
- package/skills/migrate-core/references/host-capabilities.md +24 -0
- package/skills/migrate-core/references/topics/app-identity.md +214 -0
- package/skills/migrate-core/references/topics/env-doctor.md +245 -0
- package/skills/migrate-core/references/topics/i18n/README.md +458 -0
- package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
- package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
- package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
- package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
- package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
- package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
- package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
- package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
- package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
- package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
- package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
- package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
- package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
- package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
- package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
- package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
- package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
- package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
- package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
- package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
- package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
- package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
- package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
- package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
- package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
- package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
- package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
- package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
- package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
- package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
- package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
- package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
- package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
- package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
- package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
- package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
- package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
- package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
- package/skills/migrate-core/scripts/closure/idioms.json +105 -0
- package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
- package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
- package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
- package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
- package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
- package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
- package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
- package/skills/migrate-core/scripts/front.mjs +107 -0
- package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
- package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
- package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
- package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
- package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
- package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
- package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
- package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
- package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
- package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
- package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
- package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
- package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
- package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
- package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
- package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
- package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
- package/skills/migrate-core/templates/acceptance.js +365 -0
- package/skills/migrate-core/templates/explore.js +86 -0
- package/skills/migrate-core/templates/implement.js +211 -0
- package/skills/migrate-core/templates/mig_slices.js +491 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan.mjs — 闭合检查器共享的文本处理层(原三份手拷逐行状态机的替代)。
|
|
3
|
+
*
|
|
4
|
+
* 设计不变量:所有"脱敏"变换都做**同长度空格替换、保留换行**——调用方的
|
|
5
|
+
* 偏移量/行号在变换前后完全一致,可以在脱敏文本上定位、回原文取内容。
|
|
6
|
+
*
|
|
7
|
+
* 修复的审查发现:
|
|
8
|
+
* - 跨行模板串里的 `//` 被旧逐行状态机当行注释砍掉行尾真实代码;
|
|
9
|
+
* - 正则字面量里的 `//`、`/*` 同病;
|
|
10
|
+
* - functionBodies 裸括号计数不识字符串里的 {}(),边界错乱;
|
|
11
|
+
* - walk 无深度上限/符号链接环 → 栈溢出(transpile 有上限,closure 三件没有)。
|
|
12
|
+
*/
|
|
13
|
+
import { readdirSync, lstatSync, readFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const RE_BEFORE = new Set([...'(,=:[!&|?{};+-*%~^<>'.split(''), undefined]);
|
|
17
|
+
const RE_KEYWORDS = /(?:return|typeof|case|in|of|instanceof|new|delete|void|do|else|yield|await)$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 单趟全文状态机。mode 栈支持模板串 ${} 内嵌套代码(含再嵌模板串)。
|
|
21
|
+
* cb(i, ch, state) 逐字符回调,state ∈ code|line|block|str1|str2|tpl|regex|regexClass
|
|
22
|
+
*/
|
|
23
|
+
function scan(code, cb) {
|
|
24
|
+
const stack = ['code']; // 栈顶为当前态;tpl 的 ${ 会压入 code
|
|
25
|
+
let i = 0;
|
|
26
|
+
let prevSig; // 上一个有效代码字符(判断 / 是否正则起点)
|
|
27
|
+
let word = ''; // 追踪紧邻标识符(return 等关键字后可跟正则)
|
|
28
|
+
const n = code.length;
|
|
29
|
+
while (i < n) {
|
|
30
|
+
const st = stack[stack.length - 1];
|
|
31
|
+
const c = code[i], c2 = code[i + 1];
|
|
32
|
+
if (st === 'line') {
|
|
33
|
+
cb(i, c, st);
|
|
34
|
+
if (c === '\n') stack.pop();
|
|
35
|
+
i++; continue;
|
|
36
|
+
}
|
|
37
|
+
if (st === 'block') {
|
|
38
|
+
cb(i, c, st);
|
|
39
|
+
if (c === '*' && c2 === '/') { cb(i + 1, c2, st); i += 2; stack.pop(); continue; }
|
|
40
|
+
i++; continue;
|
|
41
|
+
}
|
|
42
|
+
if (st === 'str1' || st === 'str2') {
|
|
43
|
+
cb(i, c, st);
|
|
44
|
+
if (c === '\\') { if (i + 1 < n) cb(i + 1, code[i + 1], st); i += 2; continue; }
|
|
45
|
+
if (c === (st === 'str1' ? "'" : '"') || c === '\n') stack.pop(); // 未闭合到行尾也退出
|
|
46
|
+
i++; continue;
|
|
47
|
+
}
|
|
48
|
+
if (st === 'tpl') {
|
|
49
|
+
if (c === '\\') { cb(i, c, st); if (i + 1 < n) cb(i + 1, code[i + 1], st); i += 2; continue; }
|
|
50
|
+
if (c === '`') { cb(i, c, st); stack.pop(); i++; continue; }
|
|
51
|
+
if (c === '$' && c2 === '{') { cb(i, c, st); cb(i + 1, c2, st); stack.push('code'); i += 2; continue; }
|
|
52
|
+
cb(i, c, st); i++; continue;
|
|
53
|
+
}
|
|
54
|
+
if (st === 'regex' || st === 'regexClass') {
|
|
55
|
+
cb(i, c, st);
|
|
56
|
+
if (c === '\\') { if (i + 1 < n) cb(i + 1, code[i + 1], st); i += 2; continue; }
|
|
57
|
+
if (st === 'regexClass') { if (c === ']') { stack.pop(); stack.push('regex'); } }
|
|
58
|
+
else if (c === '[') { stack.pop(); stack.push('regexClass'); }
|
|
59
|
+
else if (c === '/' || c === '\n') stack.pop(); // 换行=其实不是正则,放弃(已按原样保留)
|
|
60
|
+
i++; continue;
|
|
61
|
+
}
|
|
62
|
+
// st === 'code'
|
|
63
|
+
if (c === '/' && c2 === '/') { stack.push('line'); cb(i, c, 'line'); cb(i + 1, c2, 'line'); i += 2; continue; }
|
|
64
|
+
if (c === '/' && c2 === '*') { stack.push('block'); cb(i, c, 'block'); cb(i + 1, c2, 'block'); i += 2; continue; }
|
|
65
|
+
if (c === "'") { cb(i, c, 'str1'); stack.push('str1'); i++; continue; }
|
|
66
|
+
if (c === '"') { cb(i, c, 'str2'); stack.push('str2'); i++; continue; }
|
|
67
|
+
if (c === '`') { cb(i, c, 'tpl'); stack.push('tpl'); i++; continue; }
|
|
68
|
+
// 插值闭合 } 是代码结构字符,报 'code'——原报 'tpl' 使 blankStrings 把它空白化而 ${ 却保留,
|
|
69
|
+
// brace 失衡 → try/catch 等 brace 追踪在含模板串插值的块上整体错位(真实工程假阴性实证)。
|
|
70
|
+
if (c === '}' && stack.length > 1 && stack[stack.length - 2] === 'tpl') { cb(i, c, 'code'); stack.pop(); i++; continue; }
|
|
71
|
+
if (c === '/' && (RE_BEFORE.has(prevSig) || RE_KEYWORDS.test(word))) { cb(i, c, 'regex'); stack.push('regex'); i++; continue; }
|
|
72
|
+
cb(i, c, 'code');
|
|
73
|
+
if (!/\s/.test(c)) { prevSig = c; word = /[\w$]/.test(c) ? word + c : ''; }
|
|
74
|
+
i++; continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 枚举工程内全部模块源根。**构建真源优先**:build-profile.json5 的 modules[].srcPath 是模块清单的
|
|
80
|
+
* 唯一权威(多产品/自定义布局都由它显式声明,不靠目录名猜);无法解析时退目录兜底
|
|
81
|
+
* (路径后缀 src/main/ets,任意嵌套——entry、features/x、commons/a/b 通吃)。
|
|
82
|
+
* 修的盲区:检查器原只扫 entry/src/main/ets,多 HAP/HSP/HAR 工程九成源码不被查(实证:218 文件仅 21 在 entry)。
|
|
83
|
+
* 找不到任何模块根时回落 [projectRoot](调用方 walkFiles 自会按扩展名过滤)。
|
|
84
|
+
*/
|
|
85
|
+
export function moduleSrcRoots(projectRoot) {
|
|
86
|
+
// 1) 构建真源:build-profile.json5 modules[].srcPath
|
|
87
|
+
const bp = join(projectRoot, 'build-profile.json5');
|
|
88
|
+
if (existsSync(bp)) {
|
|
89
|
+
try {
|
|
90
|
+
const txt = stripComments(readFileSync(bp, 'utf8'));
|
|
91
|
+
const roots = [];
|
|
92
|
+
for (const m of txt.matchAll(/["']srcPath["']\s*:\s*["']([^"']+)["']/g)) {
|
|
93
|
+
const p = join(projectRoot, m[1], 'src', 'main', 'ets');
|
|
94
|
+
if (existsSync(p)) roots.push(p);
|
|
95
|
+
}
|
|
96
|
+
if (roots.length) return roots;
|
|
97
|
+
} catch { /* 解析失败走目录兜底 */ }
|
|
98
|
+
}
|
|
99
|
+
// 2) 目录兜底(无 build-profile 的裸模块/测试夹具)
|
|
100
|
+
const SKIP = new Set(['build', 'oh_modules', 'node_modules', 'hvigor', 'AppScope']);
|
|
101
|
+
const roots = [];
|
|
102
|
+
const walkDir = (d, depth) => {
|
|
103
|
+
if (depth > 5) return;
|
|
104
|
+
let entries; try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
if (!e.isDirectory() || SKIP.has(e.name) || e.name.startsWith('.')) continue;
|
|
107
|
+
const p = join(d, e.name);
|
|
108
|
+
if (e.name === 'ets' && d.endsWith(join('src', 'main'))) { roots.push(p); continue; }
|
|
109
|
+
walkDir(p, depth + 1);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
walkDir(projectRoot, 0);
|
|
113
|
+
return roots.length ? roots : [projectRoot];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* try/catch 区间追踪(brace 精确;输入应已 stripComments+blankStrings 中和)。
|
|
118
|
+
* 返回 [{start,end,catchStart,catchEnd}](均为 {} 偏移;无 catch 时 -1)。
|
|
119
|
+
* 共享层唯一实现——原 api-guards/permissions 各持一份手拷(本库的存在理由就是反手拷)。
|
|
120
|
+
*/
|
|
121
|
+
export function tryCatchRanges(code) {
|
|
122
|
+
const tries = [];
|
|
123
|
+
const stack = []; let pendingTry = false;
|
|
124
|
+
for (let i = 0; i < code.length; i++) {
|
|
125
|
+
const c = code[i];
|
|
126
|
+
if (c === 't' && code.slice(i, i + 3) === 'try' && !/[\w$]/.test(code[i - 1] || '') && !/[\w$]/.test(code[i + 3] || '')) {
|
|
127
|
+
pendingTry = true; i += 2; continue;
|
|
128
|
+
}
|
|
129
|
+
if (c === '{') { stack.push({ isTry: pendingTry, start: i }); pendingTry = false; }
|
|
130
|
+
else if (c === '}') {
|
|
131
|
+
const top = stack.pop();
|
|
132
|
+
if (top && top.isTry) {
|
|
133
|
+
const entry = { start: top.start, end: i, catchStart: -1, catchEnd: -1 };
|
|
134
|
+
const m = /^\s*catch\s*(\([^)]*\))?\s*\{/.exec(code.slice(i + 1));
|
|
135
|
+
if (m) {
|
|
136
|
+
const cs = i + m[0].length;
|
|
137
|
+
let d = 0;
|
|
138
|
+
for (let j = cs; j < code.length; j++) {
|
|
139
|
+
if (code[j] === '{') d++;
|
|
140
|
+
else if (code[j] === '}') { d--; if (!d) { entry.catchStart = cs; entry.catchEnd = j; break; } }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
tries.push(entry);
|
|
144
|
+
}
|
|
145
|
+
} else if (!/\s/.test(c)) { if (pendingTry && c !== '(') pendingTry = false; }
|
|
146
|
+
}
|
|
147
|
+
return tries;
|
|
148
|
+
}
|
|
149
|
+
/** off 所在的最内层 try 块(无则 null)。 */
|
|
150
|
+
export function enclosingTry(tries, off) {
|
|
151
|
+
let best = null;
|
|
152
|
+
for (const t of tries) if (off > t.start && off < t.end && (!best || t.start > best.start)) best = t;
|
|
153
|
+
return best;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 函数级上下文区间(输入应已 stripComments+blankStrings 中和)——同步抛落在哪种"栈"上决定后果:
|
|
158
|
+
* 'async':async 函数/方法/箭头体——同步抛变成拒绝(模拟器实测:不杀进程、不进 faultlog,静默失败);
|
|
159
|
+
* 'then' :.then/.catch/.finally 的回调(块体箭头按其体、其余按整段实参)——同上;
|
|
160
|
+
* 'sync' :其余函数/方法/块体箭头(事件回调、定时器、生命周期)——未捕获即杀进程。
|
|
161
|
+
* 返回 [{start,end,kind}]({} 或 () 偏移);用 innermostContext 取包含 off 的最内层。
|
|
162
|
+
* 只做"降级"判断:async 体内被同步调用的普通箭头(map/forEach 回调)仍算 'sync',宁可多报不漏报。
|
|
163
|
+
*/
|
|
164
|
+
export function functionContexts(code) {
|
|
165
|
+
const out = [];
|
|
166
|
+
const matchBrace = (open) => { let d = 0; for (let j = open; j < code.length; j++) { if (code[j] === '{') d++; else if (code[j] === '}') { d--; if (!d) return j; } } return -1; };
|
|
167
|
+
const matchParen = (open) => { let d = 0; for (let j = open; j < code.length; j++) { if (code[j] === '(') d++; else if (code[j] === ')') { d--; if (!d) return j; } } return -1; };
|
|
168
|
+
const matchParenBack = (close) => { let d = 0; for (let j = close; j >= 0; j--) { if (code[j] === ')') d++; else if (code[j] === '(') { d--; if (!d) return j; } } return -1; };
|
|
169
|
+
let m;
|
|
170
|
+
// ① 函数/方法:修饰符 (async)? (function)? name(params) (: T)? {
|
|
171
|
+
const fre = /(?:static\s+|private\s+|public\s+|protected\s+|export\s+|async\s+)*(?:function\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
172
|
+
while ((m = fre.exec(code)) !== null) {
|
|
173
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'async', 'await', 'typeof', 'new', 'else'].includes(m[1])) continue;
|
|
174
|
+
const close = matchParen(fre.lastIndex - 1); if (close < 0) continue;
|
|
175
|
+
const bm = /^\s*(?::\s*[^{;=]*?)?\{/.exec(code.slice(close + 1)); if (!bm) continue;
|
|
176
|
+
const start = close + bm[0].length; const end = matchBrace(start); if (end < 0) continue;
|
|
177
|
+
out.push({ start, end, kind: /\basync\s/.test(m[0]) ? 'async' : 'sync' });
|
|
178
|
+
}
|
|
179
|
+
// ② 块体箭头:(params) => { / x => { ;前缀 async → 'async';紧跟 .then( / .catch( / .finally( → 'then'
|
|
180
|
+
const are = /=>\s*\{/g;
|
|
181
|
+
while ((m = are.exec(code)) !== null) {
|
|
182
|
+
const start = m.index + m[0].length - 1; const end = matchBrace(start); if (end < 0) continue;
|
|
183
|
+
const head = code.slice(0, m.index);
|
|
184
|
+
let ps = -1;
|
|
185
|
+
const withParen = /\)\s*(?::\s*[\w$<>\[\]|,.?\s]*)?\s*$/.exec(head);
|
|
186
|
+
if (withParen) ps = matchParenBack(withParen.index);
|
|
187
|
+
else { const ident = /([A-Za-z_$][\w$]*)\s*$/.exec(head); if (ident) ps = ident.index; }
|
|
188
|
+
if (ps < 0) continue;
|
|
189
|
+
const pre = code.slice(0, ps);
|
|
190
|
+
const isAsync = /\basync\s*$/.test(pre);
|
|
191
|
+
const isThen = /\.\s*(?:then|catch|finally)\s*\(\s*(?:async\s*)?$/.test(pre);
|
|
192
|
+
out.push({ start, end, kind: isAsync ? 'async' : isThen ? 'then' : 'sync' });
|
|
193
|
+
}
|
|
194
|
+
// ③ then/catch/finally 的整段实参(表达式体箭头、函数引用也覆盖)
|
|
195
|
+
const tre = /\.\s*(?:then|catch|finally)\s*\(/g;
|
|
196
|
+
while ((m = tre.exec(code)) !== null) {
|
|
197
|
+
const open = m.index + m[0].length - 1; const close = matchParen(open); if (close < 0) continue;
|
|
198
|
+
out.push({ start: open, end: close, kind: 'then' });
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
/** off 所在的最内层函数级上下文(无则 null,视为顶层同步)。 */
|
|
203
|
+
export function innermostContext(ranges, off) {
|
|
204
|
+
let best = null;
|
|
205
|
+
for (const r of ranges) if (off > r.start && off < r.end && (!best || r.start > best.start)) best = r;
|
|
206
|
+
return best;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 注释→同长度空格(换行保留);字符串/模板串/正则原样保留。 */
|
|
210
|
+
export function stripComments(code) {
|
|
211
|
+
const out = code.split('');
|
|
212
|
+
scan(code, (i, c, st) => { if ((st === 'line' || st === 'block') && c !== '\n') out[i] = ' '; });
|
|
213
|
+
return out.join('');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 字符串/模板串**内容**→空格(引号与 ${} 内代码保留)——供调用点扫描防"字符串里的调用样例"。 */
|
|
217
|
+
export function blankStrings(code) {
|
|
218
|
+
const out = code.split('');
|
|
219
|
+
scan(code, (i, c, st) => {
|
|
220
|
+
if ((st === 'str1' || st === 'str2') && c !== "'" && c !== '"' && c !== '\n') out[i] = ' ';
|
|
221
|
+
else if (st === 'tpl' && c !== '`' && c !== '\n' && !(c === '$' && code[i + 1] === '{') && !(code[i - 1] === '$' && c === '{')) out[i] = ' ';
|
|
222
|
+
});
|
|
223
|
+
return out.join('');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 函数体抽取(输入应已 stripComments)。字符串感知的括号平衡;
|
|
228
|
+
* 跳过控制流关键字与**大写开头名**(ArkUI 内置组件/@Builder 构件在表达式位形似函数定义,
|
|
229
|
+
* 曾把 Column() {…} 抽成"函数"造成包装器污染——包装器/方法按惯例小写开头)。
|
|
230
|
+
*/
|
|
231
|
+
export function* functionBodies(code) {
|
|
232
|
+
const blanked = blankStrings(code);
|
|
233
|
+
const re = /(?:static\s+|private\s+|public\s+|export\s+|async\s+)*(?:function\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
234
|
+
let m;
|
|
235
|
+
while ((m = re.exec(blanked)) !== null) {
|
|
236
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'constructor', 'return'].includes(m[1])) continue;
|
|
237
|
+
if (/^[A-Z]/.test(m[1])) continue;
|
|
238
|
+
let depth = 1, i = re.lastIndex;
|
|
239
|
+
while (i < blanked.length && depth > 0) { if (blanked[i] === '(') depth++; else if (blanked[i] === ')') depth--; i++; }
|
|
240
|
+
const paramsRaw = code.slice(re.lastIndex, i - 1);
|
|
241
|
+
const after = blanked.slice(i);
|
|
242
|
+
const bm = after.match(/^\s*(?::\s*[^{;=]*?)?\{/);
|
|
243
|
+
if (!bm) continue;
|
|
244
|
+
const bodyStart = i + bm[0].length;
|
|
245
|
+
let d = 1, j = bodyStart;
|
|
246
|
+
while (j < blanked.length && d > 0) { if (blanked[j] === '{') d++; else if (blanked[j] === '}') d--; j++; }
|
|
247
|
+
const params = [];
|
|
248
|
+
let pd = 0, cur = '';
|
|
249
|
+
for (const c of paramsRaw + ',') {
|
|
250
|
+
if ('({[<'.includes(c)) pd++;
|
|
251
|
+
else if (')}]>'.includes(c)) pd--;
|
|
252
|
+
if (c === ',' && pd === 0) { const nm = cur.trim().split(/[?:]/)[0].trim(); if (/^[A-Za-z_$][\w$]*$/.test(nm)) params.push(nm); cur = ''; }
|
|
253
|
+
else cur += c;
|
|
254
|
+
}
|
|
255
|
+
yield { name: m[1], params, start: bodyStart, end: j, body: code.slice(bodyStart, j) };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 目录遍历:深度上限 + 符号链接目录不进(防环)。 */
|
|
260
|
+
export function walkFiles(dir, { exclude, match, maxDepth = 24 } = {}) {
|
|
261
|
+
const acc = [];
|
|
262
|
+
// spec/ 是套件自己的迁移工作区(台账、转译参考件、走查证据),不是工程产物——所有扫描器一律不进
|
|
263
|
+
const ex = exclude || new Set(['oh_modules', 'node_modules', 'build', '.preview', '.deveco', '.git', 'hvigor', 'spec']);
|
|
264
|
+
(function go(d, depth) {
|
|
265
|
+
if (depth > maxDepth) return;
|
|
266
|
+
let es; try { es = readdirSync(d); } catch { return; }
|
|
267
|
+
for (const e of es) {
|
|
268
|
+
if (ex.has(e)) continue;
|
|
269
|
+
const p = join(d, e);
|
|
270
|
+
let st; try { st = lstatSync(p); } catch { continue; }
|
|
271
|
+
if (st.isSymbolicLink()) continue;
|
|
272
|
+
if (st.isDirectory()) go(p, depth + 1);
|
|
273
|
+
else if (!match || match(p, e)) acc.push(p);
|
|
274
|
+
}
|
|
275
|
+
})(dir, 0);
|
|
276
|
+
return acc;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- 工具链定位(跨平台):DevEco Studio 的 hvigorw/ohpm 在 Windows 上默认不进 PATH,按"工程本地包装器 → PATH → DevEco 安装根"顺序找。
|
|
280
|
+
// 返回可直接 spawn 的命令(绝对路径或裸名);Windows 上 .bat 必须经 shell 起(Node ≥18.20 无 shell 直接 spawn .bat 抛 EINVAL)。
|
|
281
|
+
const DEVECO_ROOTS = () => [
|
|
282
|
+
process.env.DEVECO_HOME,
|
|
283
|
+
'/Applications/DevEco-Studio.app/Contents',
|
|
284
|
+
process.env.PROGRAMFILES && join(process.env.PROGRAMFILES, 'Huawei', 'DevEco Studio'),
|
|
285
|
+
'C:/Program Files/Huawei/DevEco Studio',
|
|
286
|
+
].filter(Boolean);
|
|
287
|
+
function onPath(name) {
|
|
288
|
+
const exts = process.platform === 'win32' ? ['.bat', '.cmd', '.exe', ''] : [''];
|
|
289
|
+
for (const dir of (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean)) {
|
|
290
|
+
for (const ext of exts) if (existsSync(join(dir, name + ext))) return join(dir, name + ext);
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/** hvigorw:<工程>/hvigorw(.bat) → PATH → <DevEco>/tools/hvigor/bin/hvigorw(.bat);找不到返回裸名(spawn 报 ENOENT 由调用方处理) */
|
|
295
|
+
export function resolveHvigorw(projectRoot) {
|
|
296
|
+
const w = process.platform === 'win32';
|
|
297
|
+
const local = projectRoot && join(projectRoot, w ? 'hvigorw.bat' : 'hvigorw');
|
|
298
|
+
if (local && existsSync(local)) return local;
|
|
299
|
+
const p = onPath('hvigorw'); if (p) return p;
|
|
300
|
+
for (const r of DEVECO_ROOTS()) { const c = join(r, 'tools', 'hvigor', 'bin', w ? 'hvigorw.bat' : 'hvigorw'); if (existsSync(c)) return c; }
|
|
301
|
+
return w ? 'hvigorw.bat' : 'hvigorw';
|
|
302
|
+
}
|
|
303
|
+
/** ohpm:PATH → <DevEco>/tools/ohpm/bin/ohpm(.bat) */
|
|
304
|
+
export function resolveOhpm() {
|
|
305
|
+
const w = process.platform === 'win32';
|
|
306
|
+
const p = onPath('ohpm'); if (p) return p;
|
|
307
|
+
for (const r of DEVECO_ROOTS()) { const c = join(r, 'tools', 'ohpm', 'bin', w ? 'ohpm.bat' : 'ohpm'); if (existsSync(c)) return c; }
|
|
308
|
+
return w ? 'ohpm.bat' : 'ohpm';
|
|
309
|
+
}
|
|
310
|
+
/** spawn 选项:Windows 的 .bat/.cmd 必须 shell:true,且含空格路径要加引号 */
|
|
311
|
+
export function spawnOpts(cmd, extra = {}) {
|
|
312
|
+
const needShell = process.platform === 'win32' && /\.(bat|cmd)$/i.test(cmd);
|
|
313
|
+
return { ...extra, shell: needShell || extra.shell === true };
|
|
314
|
+
}
|
|
315
|
+
export const quoteIfSpace = (cmd) => (/\s/.test(cmd) && !/^".*"$/.test(cmd) ? `"${cmd}"` : cmd);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* align-sdk.mjs — 设备就绪 + 工程 SDK 版本与设备对齐(2a 建工程后立即跑,别等装机才发现装不上)。
|
|
4
|
+
*
|
|
5
|
+
* 设备就绪(原 ensure-device,已并入):hdc 有 target → 直接过;无 → 探测 Emulator 二进制 → -list 枚举已创建设备 →
|
|
6
|
+
* 后台 -start(优先 phone 型/--name 指定)→ 轮询 hdc 至 boot 完成。分发环境"拉不起模拟器"的常见因(license 未接受、
|
|
7
|
+
* 设备未创建、镜像未装)都给精确的一条命令指引,不代做大下载与许可决定。
|
|
8
|
+
* 版本对齐:build-profile.json5 的 compatibleSdkVersion(API 数字)≤ 设备 const.ohos.apiversion。
|
|
9
|
+
* 脚本只探测+报因(exit 4 附精确改法),改写 build-profile 归模型(版本串格式需上下文判断)。
|
|
10
|
+
* 仍无设备时 exit 0 跳过(模板默认值直跑,smoke 前会再检)。
|
|
11
|
+
*
|
|
12
|
+
* 用法:align-sdk.mjs --project <鸿蒙工程根> [--name <模拟器名>] [--timeout 180]
|
|
13
|
+
* align-sdk.mjs --device-only [--name <模拟器名>] [--timeout 180] 只做设备就绪(smoke 无设备时调用)
|
|
14
|
+
* 退出码:0 匹配/跳过/设备可用 · 4 版本不匹配(按输出改)· 3 用法错或 --device-only 下无法保障设备
|
|
15
|
+
*/
|
|
16
|
+
import { execSync, spawn } from 'node:child_process';
|
|
17
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
18
|
+
import { resolve, join } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
if (argv.includes('--help')) { console.log(readFileSync(new URL(import.meta.url), 'utf8').split('\n').filter(l => /^ \* /.test(l)).map(l => l.slice(3)).join('\n')); process.exit(0); }
|
|
23
|
+
const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d; };
|
|
24
|
+
const PROJECT = argOf('--project', null);
|
|
25
|
+
const DEVICE_ONLY = argv.includes('--device-only');
|
|
26
|
+
if (!PROJECT && !DEVICE_ONLY) { console.error('usage: align-sdk.mjs --project <root> [--name 模拟器名] [--timeout 秒] | --device-only'); process.exit(3); }
|
|
27
|
+
const TIMEOUT = Number(argOf('--timeout', '180')) * 1000;
|
|
28
|
+
const NAME = argOf('--name', null);
|
|
29
|
+
|
|
30
|
+
// 多设备:hdc 连着多个目标时每条命令都要 -t <connect-key>(否则报 need connect-key)。HMIG_HDC_TARGET 指定,否则取列表首个。
|
|
31
|
+
const hdcTargets = () => { try { return execSync('hdc list targets', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 20000 }).split('\n').map(s => s.trim()).filter(s => s && !/\[Empty\]/i.test(s)); } catch { return []; } };
|
|
32
|
+
const hdcKey = (process.env.HMIG_HDC_TARGET || '').trim() || (() => { const t = hdcTargets(); return t.length > 1 ? t[0] : ''; })();
|
|
33
|
+
const sh = (cmd) => { try { return execSync(hdcKey && /^hdc\s(?!list\s+targets)/.test(cmd) ? cmd.replace(/^hdc\s/, `hdc -t ${hdcKey} `) : cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000 }).trim(); } catch { return null; } };
|
|
34
|
+
const hasTarget = () => hdcTargets().length > 0;
|
|
35
|
+
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
36
|
+
|
|
37
|
+
// ---- 设备就绪:返回 true=可用 / false=无法保障(已打印指引) ----
|
|
38
|
+
function ensureDevice() {
|
|
39
|
+
if (hasTarget()) { console.log('[align-sdk] 设备已在线'); return true; }
|
|
40
|
+
// 探测 Emulator 二进制(env 覆盖 > macOS 默认 > Windows 常见位)
|
|
41
|
+
const CANDIDATES = [
|
|
42
|
+
process.env.DEVECO_EMULATOR,
|
|
43
|
+
'/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator',
|
|
44
|
+
process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Huawei', 'DevEco Studio', 'tools', 'emulator', 'Emulator.exe'),
|
|
45
|
+
'C:/Program Files/Huawei/DevEco Studio/tools/emulator/Emulator.exe',
|
|
46
|
+
].filter(Boolean);
|
|
47
|
+
const EMU = CANDIDATES.find(existsSync);
|
|
48
|
+
if (!EMU) { console.error('[align-sdk] 未找到 DevEco 模拟器程序(设 DEVECO_EMULATOR 环境变量指向 Emulator 二进制,或连真机并开启 USB 调试授权)'); return false; }
|
|
49
|
+
// 枚举已创建设备:-list JSON 为主,deployed 目录名兜底
|
|
50
|
+
let devices = [];
|
|
51
|
+
const listRaw = sh(`"${EMU}" -list`) || '';
|
|
52
|
+
try { const arr = JSON.parse(listRaw.slice(listRaw.indexOf('['))); devices = arr.map(d => ({ name: d.name || d.hvdName || d.deviceName || '', type: d.deviceType || '' })).filter(d => d.name); } catch { /* 走目录兜底 */ }
|
|
53
|
+
if (!devices.length) { const dep = join(homedir(), '.Huawei', 'Emulator', 'deployed'); if (existsSync(dep)) devices = readdirSync(dep).filter(f => f.endsWith('.ini')).map(f => ({ name: f.replace(/\.ini$/, ''), type: /wear/i.test(f) ? 'wearable' : 'phone' })); }
|
|
54
|
+
if (!devices.length) {
|
|
55
|
+
console.error(`[align-sdk] 无已创建的模拟器设备。按序执行(首次需网络与许可):
|
|
56
|
+
1) "${EMU}" -license accept
|
|
57
|
+
2) "${EMU}" -imageList -deviceType phone # 查可用镜像版本
|
|
58
|
+
3) "${EMU}" -install -deviceType phone -osVersion <版本>
|
|
59
|
+
4) "${EMU}" -create MigPhone -deviceType phone -osVersion <版本>
|
|
60
|
+
或打开 DevEco Studio → Device Manager 图形化创建;或连真机开 USB 调试。`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const pick = NAME ? devices.find(d => d.name === NAME) : (devices.find(d => /phone/i.test(d.type)) || devices.find(d => !/wear|watch|tv|pad|book/i.test(d.name)) || devices[0]);
|
|
64
|
+
if (!pick) { console.error(`[align-sdk] 指定设备 ${NAME} 不存在;已有:${devices.map(d => d.name).join(', ')}`); return false; }
|
|
65
|
+
console.log(`[align-sdk] 拉起模拟器:${pick.name}(后台,boot 等待至多 ${TIMEOUT / 1000}s)`);
|
|
66
|
+
const child = spawn(EMU, ['-start', pick.name], { detached: true, stdio: 'ignore' }); child.unref();
|
|
67
|
+
const t0 = Date.now();
|
|
68
|
+
while (Date.now() - t0 <= TIMEOUT) { if (hasTarget()) { console.log(`[align-sdk] 设备就绪(${Math.round((Date.now() - t0) / 1000)}s)`); return true; } sleep(5000); }
|
|
69
|
+
console.error(`[align-sdk] ${TIMEOUT / 1000}s 内未上线。排查:
|
|
70
|
+
· 许可未接受 → "${EMU}" -license accept 后重试
|
|
71
|
+
· macOS 虚拟化授权 → 系统设置-隐私与安全性 里允许 Emulator/qemu;公司管控机可能禁虚拟化
|
|
72
|
+
· 手工前台起看报错 → "${EMU}" -start "${pick.name}"
|
|
73
|
+
· 或连真机:开发者模式+USB 调试,hdc list targets 确认`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const deviceOk = ensureDevice();
|
|
78
|
+
if (DEVICE_ONLY) process.exit(deviceOk ? 0 : 3);
|
|
79
|
+
if (!deviceOk) { console.log('[align-sdk] 无设备,跳过版本对齐(smoke 前会再检)'); process.exit(0); }
|
|
80
|
+
|
|
81
|
+
// ---- 版本对齐 ----
|
|
82
|
+
const devApi = Number((sh('hdc shell param get const.ohos.apiversion') || '').trim()) || null;
|
|
83
|
+
const devVer = (sh('hdc shell param get const.product.software.version') || '').trim();
|
|
84
|
+
const bp = resolve(PROJECT, 'build-profile.json5');
|
|
85
|
+
if (!existsSync(bp)) { console.error(`[align-sdk] 缺 ${bp}`); process.exit(3); }
|
|
86
|
+
const text = readFileSync(bp, 'utf8');
|
|
87
|
+
// json5 键可不带引号、值可用单引号(实证 bikepacking 工程 compatible 读成 null 被跳过)
|
|
88
|
+
const pick = (key) => { const m = text.match(new RegExp(`["']?${key}["']?\\s*:\\s*["']?(?:[\\d.]+\\((\\d+)\\)|(\\d+))["']?`)); return m ? Number(m[1] || m[2]) : null; };
|
|
89
|
+
const compat = pick('compatibleSdkVersion');
|
|
90
|
+
const compile = pick('compileSdkVersion');
|
|
91
|
+
|
|
92
|
+
if (!devApi || !compat) { console.log(`[align-sdk] 信息不全(设备 API=${devApi} 工程 compatible=${compat}),跳过`); process.exit(0); }
|
|
93
|
+
if (compat <= devApi) {
|
|
94
|
+
console.log(`[align-sdk] ✓ 版本对齐:设备 API ${devApi}(${devVer})≥ 工程 compatible ${compat}${compile ? `(compile ${compile})` : ''}`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
// 版本串必须查 hvigor 官方映射(osVersionMapper),禁按设备软件版本号猜——
|
|
98
|
+
// 实证:设备 6.0.0.130 猜成 "6.0.0(22)" 被 hvigor 拒(6.0.0 对应 API 20,22 的版本名是 6.0.2)
|
|
99
|
+
function apiToVersionName(api) {
|
|
100
|
+
const cands = [
|
|
101
|
+
process.env.DEVECO_HOME && join(process.env.DEVECO_HOME, 'tools/hvigor/hvigor-ohos-plugin/node_modules/@ohos/hos-sdkmanager-common/build/res/hos-config.json'),
|
|
102
|
+
'/Applications/DevEco-Studio.app/Contents/tools/hvigor/hvigor-ohos-plugin/node_modules/@ohos/hos-sdkmanager-common/build/res/hos-config.json',
|
|
103
|
+
process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Huawei/DevEco Studio/tools/hvigor/hvigor-ohos-plugin/node_modules/@ohos/hos-sdkmanager-common/build/res/hos-config.json'),
|
|
104
|
+
].filter(Boolean);
|
|
105
|
+
for (const c of cands) {
|
|
106
|
+
try {
|
|
107
|
+
const mapper = JSON.parse(readFileSync(c, 'utf8')).osVersionMapper || {};
|
|
108
|
+
for (const [ver, a] of Object.entries(mapper)) if (Number(a) === api) return ver;
|
|
109
|
+
} catch { /* 下一个候选 */ }
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const verName = apiToVersionName(devApi);
|
|
114
|
+
const exact = verName ? `"${verName}(${devApi})"` : `(映射表未找到——从 hvigor 报错的 osVersionMapper 提示中取 API ${devApi} 对应版本名,禁按设备软件版本号猜)`;
|
|
115
|
+
console.error(`[align-sdk] ✗ 工程 compatibleSdkVersion ${compat} > 设备 API ${devApi}(${devVer})——上机必失败。二选一:
|
|
116
|
+
· 改 ${PROJECT}/build-profile.json5 的 compatibleSdkVersion${compile && compile > devApi ? ' 与 compileSdkVersion' : ''} 为 ${exact},改后重构建
|
|
117
|
+
· 或换高版本镜像:Emulator -imageList -deviceType phone 查版本 → -install/-create 后重试`);
|
|
118
|
+
process.exit(4);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ensure-sign.mjs — 签名保障(幂等三级级联,登录自动化)。
|
|
4
|
+
*
|
|
5
|
+
* 级联:① signingConfigs 已配 → 跳过
|
|
6
|
+
* ② 官签 devecocli signature generate(真机+verify_ui 全通)——需登录时**自动拉起浏览器登录**
|
|
7
|
+
* (auth login 自带浏览器+本地回调,用户点完命令自动返回继续;HMIG_NO_LOGIN=1 禁用,
|
|
8
|
+
* 纯无人值守场景不弹)→ 登录成功重试官签
|
|
9
|
+
* ③ 失败 exit 4 → unsigned+模拟器+hdc uitest 降级链(模拟器面 unsigned 本就全通),提示置顶进最终报告
|
|
10
|
+
* (零登录自签工具在套件仓 devtools/oh-selfsign.mjs,特殊场景备件,不进默认级联)
|
|
11
|
+
*
|
|
12
|
+
* 用法:ensure-sign.mjs --project <鸿蒙工程根>
|
|
13
|
+
* 退出码:0 已签/生成成功(stdout 注明档位)· 4 全部失败(unsigned 降级)· 3 用法错
|
|
14
|
+
*/
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
|
|
21
|
+
const PROJECT = argOf('--project', null);
|
|
22
|
+
if (!PROJECT) { console.error('usage: ensure-sign.mjs --project <root>'); process.exit(3); }
|
|
23
|
+
|
|
24
|
+
const bp = resolve(PROJECT, 'build-profile.json5');
|
|
25
|
+
if (!existsSync(bp)) { console.error(`[ensure-sign] 缺 ${bp}`); process.exit(3); }
|
|
26
|
+
// JSON5 键名可不带引号:devecocli signature generate 实际写成 `signingConfigs: [{`(回归实证),只认带引号会把官签成功误报成失败
|
|
27
|
+
const hasConfig = () => /["']?signingConfigs["']?\s*:\s*\[\s*\{/.test(readFileSync(bp, 'utf8'));
|
|
28
|
+
if (hasConfig()) { console.log('[ensure-sign] ✓ signingConfigs 已配置(官签态),跳过'); process.exit(0); }
|
|
29
|
+
|
|
30
|
+
const sh = (cmd, timeout) => {
|
|
31
|
+
try { return { ok: true, out: execSync(cmd, { cwd: resolve(PROJECT), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout }) }; }
|
|
32
|
+
catch (e) { return { ok: false, out: String((e.stdout || '') + (e.stderr || '') + (e.message || '')) }; }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ② 官签
|
|
36
|
+
let r = sh('devecocli signature generate', 180000);
|
|
37
|
+
if (hasConfig()) { console.log('[ensure-sign] ✓ 官签材料已生成(真机+verify_ui 可用)'); process.exit(0); }
|
|
38
|
+
|
|
39
|
+
// 需登录 → 自动拉浏览器(auth login 自带浏览器+本地回调等待;限时 180s,无人点自动放弃)
|
|
40
|
+
if (/auth login|sign in|登录/i.test(r.out) && process.env.HMIG_NO_LOGIN !== '1') {
|
|
41
|
+
console.log('[ensure-sign] 官签需登录——自动打开浏览器(180s 内完成登录即自动继续;HMIG_NO_LOGIN=1 可禁)');
|
|
42
|
+
const login = sh('devecocli auth login', 180000);
|
|
43
|
+
if (login.ok) {
|
|
44
|
+
r = sh('devecocli signature generate', 180000);
|
|
45
|
+
if (hasConfig()) { console.log('[ensure-sign] ✓ 登录成功,官签材料已生成(真机+verify_ui 可用)'); process.exit(0); }
|
|
46
|
+
} else {
|
|
47
|
+
console.log('[ensure-sign] 登录未完成(超时/取消),走 unsigned 降级链');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.error(`[ensure-sign] ✗ 官签未成(${r.out.trim().slice(-160)})
|
|
52
|
+
本轮走 unsigned+模拟器+hdc uitest 降级链(模拟器面全通);如需 verify_ui/真机请 devecocli auth login 后重跑。此提示须置顶写入最终报告。`);
|
|
53
|
+
process.exit(4);
|