deveco_hmigbot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +50 -0
  3. package/agents/hmigbot-worker.md +61 -0
  4. package/agents/hmigbot.md +22 -0
  5. package/agents/workflow-subagent.md +55 -0
  6. package/commands/hmigbot.md +17 -0
  7. package/dist/index.js +1 -0
  8. package/manifest.json +19 -0
  9. package/package.json +29 -0
  10. package/skills/migrate-core/FILES.md +26 -0
  11. package/skills/migrate-core/SKILL.md +484 -0
  12. package/skills/migrate-core/references/README.md +64 -0
  13. package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
  14. package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
  15. package/skills/migrate-core/references/flow/conventions-template.md +244 -0
  16. package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
  17. package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
  18. package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
  19. package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
  20. package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
  21. package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
  22. package/skills/migrate-core/references/host-capabilities.md +24 -0
  23. package/skills/migrate-core/references/topics/app-identity.md +214 -0
  24. package/skills/migrate-core/references/topics/env-doctor.md +245 -0
  25. package/skills/migrate-core/references/topics/i18n/README.md +458 -0
  26. package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
  27. package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
  28. package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
  29. package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
  30. package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
  31. package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
  32. package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
  33. package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
  34. package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
  35. package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
  36. package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
  37. package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
  38. package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
  39. package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
  40. package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
  41. package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
  42. package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
  43. package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
  44. package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
  45. package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
  46. package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
  47. package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
  48. package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
  49. package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
  50. package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
  51. package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
  52. package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
  53. package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
  54. package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
  55. package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
  56. package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
  57. package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
  58. package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
  59. package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
  60. package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
  61. package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
  62. package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
  63. package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
  64. package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
  65. package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
  66. package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
  67. package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
  68. package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
  69. package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
  70. package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
  71. package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
  72. package/skills/migrate-core/scripts/closure/idioms.json +105 -0
  73. package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
  74. package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
  75. package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
  76. package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
  77. package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
  78. package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
  79. package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
  80. package/skills/migrate-core/scripts/front.mjs +107 -0
  81. package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
  82. package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
  83. package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
  84. package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
  85. package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
  86. package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
  87. package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
  88. package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
  89. package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
  90. package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
  91. package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
  92. package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
  93. package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
  94. package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
  95. package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
  96. package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
  97. package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
  98. package/skills/migrate-core/templates/acceptance.js +365 -0
  99. package/skills/migrate-core/templates/explore.js +86 -0
  100. package/skills/migrate-core/templates/implement.js +211 -0
  101. package/skills/migrate-core/templates/mig_slices.js +491 -0
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * transpile-layout.mjs — 布局静态转译器(静态前场核心件)。
4
+ *
5
+ * Android res/layout/*.xml → ArkUI 骨架 .ets:结构与数值由程序搬运(组件树、尺寸、
6
+ * 边距、颜色、文案引用、权重、对齐),行为留锚点 TODO 给切片。目标不是"翻译全部",
7
+ * 是"结构保真由构造保证 + 覆盖率诚实遥测":转不动的节点降级为带源锚点的占位,
8
+ * 绝不静默丢弃(转译率进报告,作为产线健康度指标)。
9
+ *
10
+ * 映射知识全在 mapping.json(数据即语料);资源名与 convert-resources 的 1:1 命名对齐
11
+ * (@string/x → $r('app.string.x'))。
12
+ *
13
+ * 输出是**参考件**:2a 把它落在 spec/front/layouts(不进编译树),切片按需把片段并进页面/组件——实证 45 工程 244/244 份
14
+ * 骨架从未被页面 import,放进工程只产死代码与悬空立案;<include> 生成的兄弟 import 在整目录一起搬时仍成立。
15
+ *
16
+ * 用法:node transpile-layout.mjs --src <android根> --out <骨架输出目录> [--json report]
17
+ */
18
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from 'node:fs';
19
+ import { join, dirname, basename, resolve, relative, sep } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+ const MAP = JSON.parse(readFileSync(join(HERE, 'mapping.json'), 'utf8'));
24
+ const argv = process.argv.slice(2);
25
+ const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
26
+ const SRC = argOf('--src', null);
27
+ const OUT = argOf('--out', null);
28
+ const JSON_OUT = argOf('--json', null);
29
+ const ONLY = argOf('--file', null);
30
+ // AAR/库 res 目录(逗号分隔):并入 values/styles/shape 解析(布局仍只取应用侧)
31
+ const EXTRA_RES = (argOf('--extra-res', '') || '').split(',').map(s => s.trim()).filter(Boolean);
32
+ if (!SRC || !OUT) { console.error('usage: transpile-layout.mjs --src <android root> --out <dir> [--json report] [--file x.xml] [--extra-res <aar-res,..>]'); process.exit(2); }
33
+
34
+ // ---------- 定位 res 目录与布局文件 ----------
35
+ function walk(dir, acc, depth = 0) {
36
+ if (depth > 14) return acc;
37
+ let es; try { es = readdirSync(dir); } catch { return acc; }
38
+ for (const e of es) {
39
+ if (['build', '.git', 'node_modules', 'test', 'androidTest'].includes(e)) continue;
40
+ const p = join(dir, e);
41
+ let st; try { st = statSync(p); } catch { continue; }
42
+ if (st.isDirectory()) walk(p, acc, depth + 1);
43
+ else acc.push(p);
44
+ }
45
+ return acc;
46
+ }
47
+ const all = walk(resolve(SRC), []);
48
+ for (const er of EXTRA_RES) {
49
+ if (!existsSync(resolve(er))) { console.error(`⚠ --extra-res 目录不存在:${er}(已跳过——若为 AAR 请先解包)`); continue; }
50
+ walk(resolve(er), all);
51
+ }
52
+ const norm = p => p.replace(/\\/g, '/');
53
+ const layouts = ONLY ? [resolve(ONLY)] : all.filter(f => /\/res\/layout\/[^/]+\.xml$/.test(norm(f)) && !EXTRA_RES.some(er => f.startsWith(resolve(er))));
54
+ // 变体目录(layout-land/-v21/-sw600dp…)不转译,但必须进遥测——静默漏采=覆盖率谎报
55
+ const variantLayouts = all.filter(f => /\/res\/layout-[^/]+\/[^/]+\.xml$/.test(norm(f)));
56
+ const valueFiles = all.filter(f => /\/(?:res\/)?values\/[^/]+\.xml$/.test(norm(f)));
57
+
58
+ // ---------- 资源表(strings/colors/dimens 名字 1:1 对齐 convert-resources) ----------
59
+ const dimens = new Map(), colors = new Map();
60
+ for (const f of valueFiles) {
61
+ const t = readFileSync(f, 'utf8');
62
+ for (const m of t.matchAll(/<dimen\s+name="([^"]+)"\s*>\s*(-?[\d.]+)(dp|dip|sp|px)\s*<\/dimen>/g)) dimens.set(m[1], Number(m[2]));
63
+ for (const m of t.matchAll(/<color\s+name="([^"]+)"\s*>\s*(#[0-9a-fA-F]{3,8})\s*<\/color>/g)) colors.set(m[1], m[2]);
64
+ }
65
+ // styles:name → {parent, items{android:attr: value}},支持显式 parent 与点号隐式父链
66
+ const styles = new Map();
67
+ for (const f of valueFiles) {
68
+ const t = readFileSync(f, 'utf8');
69
+ for (const m of t.matchAll(/<style\s+name="([^"]+)"(?:\s+parent="([^"]*)")?\s*>([\s\S]*?)<\/style>/g)) {
70
+ const items = {};
71
+ for (const it of m[3].matchAll(/<item\s+name="([^"]+)"\s*>\s*([^<]+?)\s*<\/item>/g)) items[it[1]] = it[2];
72
+ styles.set(m[1], { parent: m[2] || null, items });
73
+ }
74
+ }
75
+ function resolveStyle(refRaw, depth = 0) {
76
+ if (!refRaw || depth > 8) return {};
77
+ const name = String(refRaw).replace(/^@(?:android:)?style\//, '');
78
+ const s = styles.get(name);
79
+ if (!s) return {};
80
+ const parentName = s.parent !== null ? s.parent : (name.includes('.') ? name.slice(0, name.lastIndexOf('.')) : null);
81
+ return { ...(parentName ? resolveStyle(parentName, depth + 1) : {}), ...s.items };
82
+ }
83
+ // shape drawable:res/drawable/*.xml 根为 <shape> 的 → 背景修饰符(色/圆角/描边)
84
+ const shapes = new Map();
85
+ for (const f of all.filter(x => /\/(?:res\/)?drawable[^/]*\/[^/]+\.xml$/.test(norm(x)))) {
86
+ const t = readFileSync(f, 'utf8');
87
+ if (!/^\s*(?:<\?xml[^>]*\?>)?\s*(?:<!--[\s\S]*?-->\s*)*<shape\b/.test(t)) continue;
88
+ const g = (re) => (t.match(re) || [])[1];
89
+ shapes.set(basename(f, '.xml'), {
90
+ solid: g(/<solid[^>]*android:color="([^"]+)"/),
91
+ radius: g(/<corners[^>]*android:radius="([^"]+)"/) || g(/android:topLeftRadius="([^"]+)"/),
92
+ strokeW: g(/<stroke[^>]*android:width="([^"]+)"/),
93
+ strokeC: g(/<stroke[^>]*android:color="([^"]+)"/),
94
+ });
95
+ }
96
+
97
+ // ---------- 极简 XML 解析(同长注释脱敏 → 标签树) ----------
98
+ // XML 实体解码:属性值/文本进 ArkTS 前必须还原(&amp; 原样进 Text() 是生成物文案错误)
99
+ function decodeEntities(s) {
100
+ return String(s)
101
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
102
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
103
+ .replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
104
+ }
105
+ function parseXml(text) {
106
+ const src = text.replace(/<!--[\s\S]*?-->/g, m => m.replace(/[^\n]/g, ' ')).replace(/<\?xml[\s\S]*?\?>/, '');
107
+ const root = { tag: '#root', attrs: {}, children: [], line: 0 };
108
+ const stack = [root];
109
+ // 属性支持单/双引号(单引号是合法 XML,旧版整节点静默丢弃且关闭标签错配污染树)
110
+ const re = /<\s*(\/)?\s*([\w.$-]+)((?:\s+[\w:.-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/)?\s*>/g;
111
+ let m;
112
+ let parsedTags = 0;
113
+ while ((m = re.exec(src)) !== null) {
114
+ const [, closing, tag, attrStr, selfClose] = m;
115
+ const line = src.slice(0, m.index).split('\n').length;
116
+ if (closing) {
117
+ // 只弹到同名开标签——错配的关闭标签不许把父节点弹栈(污染树)
118
+ for (let k = stack.length - 1; k > 0; k--) if (stack[k].tag === tag) { stack.length = k; break; }
119
+ continue;
120
+ }
121
+ parsedTags++;
122
+ const attrs = {};
123
+ for (const a of attrStr.matchAll(/([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) attrs[a[1]] = decodeEntities(a[2] !== undefined ? a[2] : a[3]);
124
+ const node = { tag, attrs, children: [], line };
125
+ stack[stack.length - 1].children.push(node);
126
+ if (!selfClose) stack.push(node);
127
+ }
128
+ // 诚实遥测:解析器没吃下的开标签数(畸形/不支持写法)——绝不静默蒸发
129
+ const naive = (src.match(/<\s*[\w.$-]/g) || []).length - (src.match(/<\s*\//g) || []).length;
130
+ const skipped = Math.max(0, naive - parsedTags);
131
+ const tree = root.children[0] || null;
132
+ if (tree) tree.parseSkipped = skipped;
133
+ return tree;
134
+ }
135
+
136
+ // ---------- 值解析 ----------
137
+ const stats = { files: 0, nodesTotal: 0, nodesMapped: 0, attrsTotal: 0, attrsMapped: 0, unmapped: {} };
138
+ function dim(v) {
139
+ if (v === 'match_parent' || v === 'fill_parent') return `'100%'`;
140
+ if (v === 'wrap_content') return null;
141
+ const m = String(v).match(/^(-?[\d.]+)(dp|dip|sp|px)$/);
142
+ if (m) return Number(m[1]);
143
+ const r = String(v).match(/^@dimen\/(.+)$/);
144
+ if (r) return dimens.has(r[1]) ? dimens.get(r[1]) : `/* @dimen/${r[1]} */ 0`;
145
+ const pct = String(v).match(/^([\d.]+)%$/);
146
+ if (pct) return `'${v}'`;
147
+ return null;
148
+ }
149
+ function colorOf(v) {
150
+ if (/^#[0-9a-fA-F]{3,8}$/.test(v)) return `'${v}'`;
151
+ const r = String(v).match(/^@color\/(.+)$/);
152
+ if (r) return `$r('app.color.${r[1]}')`;
153
+ const a = String(v).match(/^@android:color\/(.+)$/);
154
+ if (a) return `/* android:${a[1]} */ '#00000000'`;
155
+ return null;
156
+ }
157
+ function textOf(v) {
158
+ const r = String(v).match(/^@string\/(.+)$/);
159
+ if (r) return `$r('app.string.${r[1]}')`;
160
+ // 反斜杠先转义(C:\path 曾变形);属性值里合法的物理换行转 \n(裸换行=生成物语法错误)
161
+ return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\r/g, '').replace(/\n/g, '\\n')}'`;
162
+ }
163
+ function mediaOf(v) {
164
+ const r = String(v).match(/^@(?:drawable|mipmap)\/(.+)$/);
165
+ if (r) return `$r('app.media.${r[1]}')`;
166
+ return null;
167
+ }
168
+
169
+ // ---------- 修饰符装配 ----------
170
+ const A = (attrs, k) => attrs[`android:${k}`];
171
+ function modifiers(node, comp, orientation) {
172
+ const attrs = node.attrs;
173
+ // style / textAppearance 解析:条目作为缺省并入(节点显式属性优先)
174
+ for (const sk of ['style', 'android:textAppearance']) {
175
+ if (attrs[sk]) {
176
+ const items = resolveStyle(attrs[sk]);
177
+ for (const [k, v] of Object.entries(items)) {
178
+ const key = k.startsWith('android:') ? k : `android:${k}`;
179
+ if (attrs[key] === undefined) attrs[key] = v;
180
+ }
181
+ if (Object.keys(items).length) { stats.attrsTotal++; stats.attrsMapped++; }
182
+ delete attrs[sk === 'style' ? 'style' : sk];
183
+ }
184
+ }
185
+ const mods = [];
186
+ const todo = [];
187
+ let mapped = 0, total = 0;
188
+ const take = (k, fn) => {
189
+ const v = A(attrs, k);
190
+ if (v === undefined) return;
191
+ total++;
192
+ const r = fn(v);
193
+ if (r === undefined) { todo.push(`${k}="${v}"`); return; }
194
+ if (r !== null) mods.push(r);
195
+ mapped++;
196
+ };
197
+ take('layout_width', v => { const d = dim(v); return d === null ? null : `.width(${d})`; });
198
+ take('layout_height', v => { const d = dim(v); return d === null ? null : `.height(${d})`; });
199
+ take('layout_weight', v => { const n = Number(v); return `.layoutWeight(${Number.isFinite(n) ? n : 1})`; }); // weight=0 是"显式退出加权",不许 ||1 反转语义
200
+ take('padding', v => { const d = dim(v); return d == null ? undefined : `.padding(${d})`; });
201
+ for (const [ak, side] of [['paddingStart', 'left'], ['paddingLeft', 'left'], ['paddingEnd', 'right'], ['paddingRight', 'right'], ['paddingTop', 'top'], ['paddingBottom', 'bottom']]) {
202
+ take(ak, v => { const d = dim(v); return d == null ? undefined : `.padding({ ${side}: ${d} })`; });
203
+ }
204
+ take('layout_margin', v => { const d = dim(v); return d == null ? undefined : `.margin(${d})`; });
205
+ for (const [ak, side] of [['layout_marginStart', 'left'], ['layout_marginLeft', 'left'], ['layout_marginEnd', 'right'], ['layout_marginRight', 'right'], ['layout_marginTop', 'top'], ['layout_marginBottom', 'bottom']]) {
206
+ take(ak, v => { const d = dim(v); return d == null ? undefined : `.margin({ ${side}: ${d} })`; });
207
+ }
208
+ take('background', v => {
209
+ const c = colorOf(v);
210
+ if (c) return `.backgroundColor(${c})`;
211
+ const sh = String(v).match(/^@drawable\/(.+)$/) && shapes.get(String(v).replace('@drawable/', ''));
212
+ if (sh) {
213
+ const parts = [];
214
+ if (sh.solid) { const sc = colorOf(sh.solid); if (sc) parts.push(`.backgroundColor(${sc})`); }
215
+ if (sh.radius) { const rd = dim(sh.radius); if (rd != null) parts.push(`.borderRadius(${rd})`); }
216
+ if (sh.strokeW) { const bw = dim(sh.strokeW); const bc = sh.strokeC ? colorOf(sh.strokeC) : null; if (bw != null) parts.push(`.border({ width: ${bw}${bc ? `, color: ${bc}` : ''} })`); }
217
+ if (parts.length) return parts.join('\n' + ' '.repeat(0) + '');
218
+ }
219
+ // 认不出的 drawable(ripple/layer-list/selector 等 xml)不发虚 $r 引用——降级 TODO 留给切片
220
+ const dm = String(v).match(/^@drawable\/(.+)$/);
221
+ if (dm && all.some(f => norm(f).includes(`/drawable`) && f.endsWith(`/${dm[1]}.xml`))) return undefined;
222
+ const mimg = mediaOf(v);
223
+ if (mimg) return `.backgroundImage(${mimg})`;
224
+ return undefined;
225
+ });
226
+ take('alpha', v => `.opacity(${v})`);
227
+ take('visibility', v => v === 'gone' ? `.visibility(Visibility.None)` : v === 'invisible' ? `.visibility(Visibility.Hidden)` : null);
228
+ take('minHeight', v => { const d = dim(v); return d == null ? undefined : `.constraintSize({ minHeight: ${d} })`; });
229
+ take('minWidth', v => { const d = dim(v); return d == null ? undefined : `.constraintSize({ minWidth: ${d} })`; });
230
+ take('elevation', v => { const d = dim(v); return d == null ? undefined : `.shadow({ radius: ${d}, color: '#1F000000' })`; });
231
+ if (comp === 'Text' || comp === 'Button' || comp === 'TextInput') {
232
+ take('textSize', v => { const d = dim(v); return d == null ? undefined : `.fontSize(${d})`; });
233
+ take('textColor', v => { const c = colorOf(v); return c ? `.fontColor(${c})` : undefined; });
234
+ take('textStyle', v => v.includes('bold') ? `.fontWeight(FontWeight.Bold)` : null);
235
+ take('maxLines', v => `.maxLines(${v})`);
236
+ take('ellipsize', v => v === 'end' ? `.textOverflow({ overflow: TextOverflow.Ellipsis })` : null);
237
+ take('singleLine', v => v === 'true' ? `.maxLines(1)` : null);
238
+ }
239
+ if (comp === 'Image') {
240
+ take('scaleType', v => ({ centerCrop: `.objectFit(ImageFit.Cover)`, fitCenter: `.objectFit(ImageFit.Contain)`, fitXY: `.objectFit(ImageFit.Fill)`, center: `.objectFit(ImageFit.None)` })[v]);
241
+ take('tint', v => { const c = colorOf(v); return c ? `.fillColor(${c})` : undefined; });
242
+ }
243
+ // gravity(照 ui-layout-semantics:主轴 justifyContent / 交叉轴 alignItems,按容器方向分派)
244
+ const grav = A(attrs, 'gravity');
245
+ if (grav !== undefined && (comp === 'Column' || comp === 'Row')) {
246
+ total++;
247
+ const parts = grav.split('|');
248
+ let ok = false;
249
+ for (const p of parts) {
250
+ const g = MAP.gravity[p.trim()];
251
+ if (!g) continue;
252
+ ok = true;
253
+ if (comp === 'Column') {
254
+ if (g.h) mods.push(`.alignItems(HorizontalAlign.${g.h})`);
255
+ if (g.v) mods.push(`.justifyContent(FlexAlign.${g.v === 'Start' ? 'Start' : g.v === 'End' ? 'End' : 'Center'})`);
256
+ } else {
257
+ if (g.v) mods.push(`.alignItems(VerticalAlign.${g.v === 'Start' ? 'Top' : g.v === 'End' ? 'Bottom' : 'Center'})`);
258
+ if (g.h) mods.push(`.justifyContent(FlexAlign.${g.h === 'Start' ? 'Start' : g.h === 'End' ? 'End' : 'Center'})`);
259
+ }
260
+ }
261
+ if (ok) mapped++; else todo.push(`gravity="${grav}"`);
262
+ }
263
+ // 记账:app:(ConstraintLayout/Material 等库属性)不转译但必须进分母——否则覆盖率虚高
264
+ for (const k of Object.keys(attrs)) {
265
+ if (k.startsWith('app:')) { total++; todo.push(`${k}="${attrs[k]}"`); continue; }
266
+ if (!k.startsWith('android:')) continue;
267
+ const bare = k.slice(8);
268
+ const handled = ['layout_width', 'layout_height', 'layout_weight', 'padding', 'paddingStart', 'paddingLeft', 'paddingEnd', 'paddingRight', 'paddingTop', 'paddingBottom',
269
+ 'layout_margin', 'layout_marginStart', 'layout_marginLeft', 'layout_marginEnd', 'layout_marginRight', 'layout_marginTop', 'layout_marginBottom',
270
+ 'background', 'alpha', 'visibility', 'minHeight', 'minWidth', 'elevation', 'textSize', 'textColor', 'textStyle', 'maxLines', 'ellipsize', 'singleLine',
271
+ 'scaleType', 'tint', 'gravity', 'text', 'src', 'hint', 'id', 'orientation', 'contentDescription', 'checked', 'progress', 'max'];
272
+ if (handled.includes(bare)) continue;
273
+ total++;
274
+ todo.push(`${bare}="${attrs[k]}"`);
275
+ }
276
+ stats.attrsTotal += total;
277
+ stats.attrsMapped += mapped;
278
+ return { mods, todo };
279
+ }
280
+
281
+ // ---------- 递归发射 ----------
282
+ function resolveWidget(tag) {
283
+ const direct = MAP.widgets[tag] || MAP.widgets[tag.split('.').pop()];
284
+ if (direct) return { w: direct, note: null };
285
+ // 后缀回退:自定义子类按标准词条转译(MyTextView/CurrencyTextView → TextView)
286
+ const simple = tag.split('.').pop();
287
+ for (const suf of MAP.fallbackSuffixes?.order || []) {
288
+ if (simple.endsWith(suf) && simple !== suf) {
289
+ const key = MAP.fallbackSuffixes.alias?.[suf] || suf;
290
+ const w = MAP.widgets[key] || MAP.widgets[key.split('.').pop()];
291
+ if (w) return { w, note: `custom subclass of ${suf}(${simple}),样式差异切片按源核对` };
292
+ }
293
+ }
294
+ return { w: null, note: null };
295
+ }
296
+ const includeRefs = new Set();
297
+ function emitNode(node, indent, relFile) {
298
+ const pad = ' '.repeat(indent);
299
+ stats.nodesTotal++;
300
+ const { w, note: subclassNote } = resolveWidget(node.tag);
301
+ const id = A(node.attrs, 'id') ? A(node.attrs, 'id').replace(/^@\+?id\//, '') : null;
302
+ const anchor = `${relFile}:${node.line}${id ? ` #${id}` : ''}`;
303
+ if (w && w.kind === 'include') {
304
+ stats.nodesMapped++;
305
+ const ref = (node.attrs['layout'] || '').replace(/^@layout\//, '');
306
+ const pascal = ref.split(/[_-]/).map(s => s[0] ? s[0].toUpperCase() + s.slice(1) : '').join('');
307
+ if (pascal) includeRefs.add(pascal);
308
+ return `${pad}// ⚓ ${anchor} include @layout/${ref}\n${pad}${pascal ? `${pascal}Layout()` : `// TODO(include 缺 layout 引用)`}`;
309
+ }
310
+ if (!w) {
311
+ stats.unmapped[node.tag] = (stats.unmapped[node.tag] || 0) + 1;
312
+ const kids = node.children.map(c => emitNode(c, indent + 1, relFile)).join('\n');
313
+ return `${pad}// UNMAPPED <${node.tag}> (${anchor}) —— 切片阶段按源实现\n${pad}Column() {\n${kids}\n${pad}}`;
314
+ }
315
+ stats.nodesMapped++;
316
+ let comp = w.component;
317
+ if (w.byOrientation) comp = w.byOrientation[A(node.attrs, 'orientation') || ''] || w.default;
318
+ if (w.kind === 'passthrough') {
319
+ return node.children.map(c => emitNode(c, indent, relFile)).join('\n') || `${pad}// (${node.tag} 透传,无子节点)`;
320
+ }
321
+ if (node.tag === 'View' && w.onlyWhenThin) {
322
+ const h = A(node.attrs, 'layout_height');
323
+ comp = (h && dim(h) !== null && Number(dim(h)) <= 2) ? 'Divider' : 'Blank';
324
+ }
325
+ // 内容参数
326
+ let arg = '';
327
+ if (w.contentAttr && node.attrs[w.contentAttr] !== undefined) {
328
+ const v = node.attrs[w.contentAttr];
329
+ arg = w.contentAttr === 'android:src' ? (mediaOf(v) || `''`) : textOf(v);
330
+ stats.attrsTotal++; stats.attrsMapped++;
331
+ } else if (w.placeholderAttr && node.attrs[w.placeholderAttr] !== undefined) {
332
+ arg = `{ placeholder: ${textOf(node.attrs[w.placeholderAttr])} }`;
333
+ stats.attrsTotal++; stats.attrsMapped++;
334
+ }
335
+ const { mods, todo } = modifiers(node, comp, A(node.attrs, 'orientation'));
336
+ const allMods = [...mods, ...(w.extraMods || [])];
337
+ const kids = (w.kind === 'leaf' || w.kind === 'list') ? [] : node.children;
338
+ const header = `${pad}// ⚓ ${anchor}${subclassNote ? `\n${pad}// NOTE: ${subclassNote}` : ''}${w.todo ? `\n${pad}// TODO(transpile): ${w.todo}` : ''}${todo.length ? `\n${pad}// TODO(attrs): ${todo.join(' · ').slice(0, 160)}` : ''}`;
339
+ const open = `${pad}${comp}(${arg})`;
340
+ const body = kids.length ? ` {\n${kids.map(c => emitNode(c, indent + 1, relFile)).join('\n')}\n${pad}}` : (w.kind === 'container' || w.kind === 'scroll' ? ' {\n' + pad + '}' : '');
341
+ const modStr = allMods.map(m => `\n${pad} ${m}`).join('');
342
+ return `${header}\n${open}${body}${modStr}`;
343
+ }
344
+
345
+ // ---------- 主流程 ----------
346
+ mkdirSync(OUT, { recursive: true });
347
+ const perFile = [];
348
+ const written = new Map(); // 输出名 -> 源文件(多模块同名布局:后写覆盖=静默失真,必须检出)
349
+ for (const f of layouts) {
350
+ const relFile = norm(f).split('/res/layout/')[1] || basename(f);
351
+ const before = { n: stats.nodesTotal, nm: stats.nodesMapped, a: stats.attrsTotal, am: stats.attrsMapped };
352
+ const tree = parseXml(readFileSync(f, 'utf8'));
353
+ if (!tree) { perFile.push({ file: relFile, error: '解析失败' }); continue; }
354
+ const pascal = basename(f, '.xml').split(/[_-]/).map(s => s[0] ? s[0].toUpperCase() + s.slice(1) : '').join('');
355
+ if (written.has(pascal)) {
356
+ perFile.push({ file: relFile, error: `输出名冲突:${pascal}Layout.ets 已由 ${written.get(pascal)} 生成(多模块同名布局),本文件跳过——需人工合并或改名` });
357
+ continue;
358
+ }
359
+ written.set(pascal, relFile);
360
+ includeRefs.clear();
361
+ const body = emitNode(tree, 2, relFile);
362
+ const importLines = [...includeRefs].filter(r => r !== pascal).map(r => `import { ${r}Layout } from './${r}Layout';`).join('\n');
363
+ const ets = `${importLines ? importLines + '\n' : ''}/**
364
+ * ${pascal}Layout —— 由 transpile-layout 生成(源:res/layout/${relFile})。
365
+ * 结构与数值由程序搬运;⚓ 为源锚点;TODO(transpile/attrs) 为未转译项,切片阶段按源补齐。
366
+ * 行为(点击/数据绑定/可见性逻辑)一律由切片实现,本骨架不含任何业务逻辑。
367
+ */
368
+ @ComponentV2
369
+ export struct ${pascal}Layout {
370
+ build() {
371
+ ${body}
372
+ }
373
+ }
374
+ `;
375
+ writeFileSync(join(OUT, `${pascal}Layout.ets`), ets);
376
+ stats.files++;
377
+ perFile.push({
378
+ file: relFile,
379
+ nodes: stats.nodesTotal - before.n,
380
+ nodesMapped: stats.nodesMapped - before.nm,
381
+ attrs: stats.attrsTotal - before.a,
382
+ attrsMapped: stats.attrsMapped - before.am,
383
+ });
384
+ }
385
+ const pct = (a, b) => b ? Math.round(a * 100 / b) : 100;
386
+ const report = {
387
+ src: relative(process.cwd(), resolve(SRC)) || '.', out: relative(process.cwd(), resolve(OUT)) || '.',
388
+ totals: {
389
+ files: stats.files,
390
+ nodeCoverage: `${stats.nodesMapped}/${stats.nodesTotal} (${pct(stats.nodesMapped, stats.nodesTotal)}%)`,
391
+ attrCoverage: `${stats.attrsMapped}/${stats.attrsTotal} (${pct(stats.attrsMapped, stats.attrsTotal)}%)`,
392
+ unmappedTags: stats.unmapped,
393
+ variantLayoutsSkipped: variantLayouts.length ? { count: variantLayouts.length, note: 'layout-land/-v21/-sw600dp 等变体未转译(切片按需处理)', files: variantLayouts.map(f => norm(f).split('/res/')[1] || f).slice(0, 40) } : null,
394
+ parseErrors: perFile.filter(p => p.error).length || undefined,
395
+ },
396
+ perFile,
397
+ };
398
+ if (JSON_OUT) { mkdirSync(dirname(resolve(JSON_OUT)), { recursive: true }); writeFileSync(JSON_OUT, JSON.stringify(report, null, 2)); }
399
+ console.log(`[transpile-layout] ${stats.files} 个布局 → ${OUT}`);
400
+ console.log(` 节点转译率 ${report.totals.nodeCoverage} · 属性转译率 ${report.totals.attrCoverage}`);
401
+ if (variantLayouts.length) console.log(` ⚠ 变体布局 ${variantLayouts.length} 个未转译(layout-land/-v21/…)——已入报告,不算进转译率`);
402
+ for (const p of perFile.filter(x => x.error)) console.log(` ⚠ ${p.file}: ${p.error}`);
403
+ const un = Object.entries(stats.unmapped).sort((x, y) => y[1] - x[1]);
404
+ if (un.length) console.log(` 未映射控件(回流 mapping.json 的候选):${un.slice(0, 8).map(([t, n]) => `${t}×${n}`).join(', ')}`);
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // 纯 ArkTS 黄金向量门:跑 HarmonyOS Local Test(主机侧、不上设备)→ 解析 test_result.txt 裁决。
3
+ // 替代旧的 node run-vectors.mjs(逻辑改 .ets 后 node 跑不了,改由 ArkTS Local Test 执行真实模块)。
4
+ //
5
+ // 用法: node run-arkts-vectors.mjs --project <DST> [--module entry] [--scope Suite[,Suite2]]
6
+ // 退出码: 0=全绿 · 1=有用例失败 · 2=环境/结果缺失(无法裁决)
7
+ //
8
+ // 关键事实(spike 实测):hvigorw test 用例失败仍 BUILD SUCCESSFUL、退出码 0,
9
+ // 故必须解析 <module>/.test/default/intermediates/test/coverage_data/test_result.txt 的
10
+ // "Tests run: N, Failure: F, Error: E, Pass: P" 汇总行 + 逐条 result=Failure。
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { resolveHvigorw, spawnOpts, quoteIfSpace } from '../lib/scan.mjs';
14
+ import { existsSync, readFileSync, writeFileSync, accessSync, constants, statSync } from 'node:fs';
15
+ import { join, resolve } from 'node:path';
16
+
17
+ function arg(name, def) {
18
+ const i = process.argv.indexOf('--' + name);
19
+ return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : def;
20
+ }
21
+ function fail(code, msg) { console.error('[arkts-vectors] ' + msg); process.exit(code); }
22
+
23
+ const project = arg('project');
24
+ const moduleName = arg('module', 'entry');
25
+ const scope = arg('scope');
26
+ if (!project) fail(2, '缺 --project <鸿蒙工程根>');
27
+ const root = resolve(project);
28
+ if (!existsSync(root)) fail(2, '工程不存在: ' + root);
29
+
30
+ // 定位 hvigorw:工程本地 → PATH → DevEco 安装根(Windows 默认不进 PATH);.bat 经 shell 起
31
+ const hvigorw = resolveHvigorw(root);
32
+
33
+ // --no-daemon 必须保留:selfTest 由各切片实现 agent **并行**调用,同工程并发 daemon 会互踩。
34
+ // 已知限制:并发 selfTest 共用同一份 test_result.txt,极端时序下可能读到别的 scope 的结果——
35
+ // 新鲜度闸(防假绿②)挡得住"陈旧"但挡不住"新鲜却是别人的";实测未发生,记录不加机制。
36
+ const cmd = ['test', '-p', 'module=' + moduleName, '-p', 'coverage=false', '--no-daemon'];
37
+ if (scope) cmd.push('-p', 'scope=' + scope);
38
+
39
+ const runStartedAtMs = Date.now();
40
+ const run = spawnSync(quoteIfSpace(hvigorw), cmd, spawnOpts(hvigorw, { cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }));
41
+ if (run.error && run.error.code === 'ENOENT') fail(2, 'hvigorw 未找到(工程无 hvigorw 且不在 PATH):' + hvigorw);
42
+
43
+ // 防假绿 ①:编译失败(COMPILE RESULT:FAIL / 退出码非 0 且无本轮汇总)不得读旧结果文件裁绿。
44
+ // hvigorw test 的"用例失败仍退出 0"不适用编译失败——编译失败时没有本轮测试产物。
45
+ const outText = (run.stdout || '') + '\n' + (run.stderr || '');
46
+ if (/COMPILE RESULT:\s*FAIL/i.test(outText)) {
47
+ const tail = outText.split('\n').slice(-30).join('\n');
48
+ fail(2, 'Local Test 编译失败(COMPILE RESULT:FAIL),不得按旧 test_result.txt 裁决:\n' + tail);
49
+ }
50
+ if (run.status !== null && run.status !== 0 && !/Tests run:/.test(outText)) {
51
+ const tail = outText.split('\n').slice(-30).join('\n');
52
+ fail(2, `hvigorw test 退出码 ${run.status} 且无本轮测试汇总,疑似编译/环境失败:\n` + tail);
53
+ }
54
+
55
+ // 解析结果文件(用例失败时 hvigorw 退出码不可信,见文件头;但编译失败已在上方拦截)
56
+ const resultFile = join(root, moduleName, '.test', 'default', 'intermediates', 'test', 'coverage_data', 'test_result.txt');
57
+ if (!existsSync(resultFile)) {
58
+ const tail = (run.stdout || '').split('\n').slice(-15).join('\n');
59
+ fail(2, '无 test_result.txt(Local Test 可能没跑起来):' + resultFile + '\n最后输出:\n' + tail);
60
+ }
61
+ // 防假绿 ②:结果文件必须比本轮运行新(陈旧文件=本轮根本没产出,按无法裁决失败)。
62
+ try {
63
+ const st = statSync(resultFile);
64
+ if (st.mtimeMs < runStartedAtMs - 2000) {
65
+ fail(2, `test_result.txt 为陈旧文件(mtime ${new Date(st.mtimeMs).toISOString()} 早于本轮运行起点)`
66
+ + '——本轮 Local Test 未产出结果(编译失败或环境问题),拒绝假绿裁决');
67
+ }
68
+ } catch (e) { fail(2, 'test_result.txt 状态读取失败: ' + resultFile); }
69
+ const text = readFileSync(resultFile, 'utf8');
70
+ const summary = /Tests run:\s*(\d+),\s*Failure:\s*(\d+),\s*Error:\s*(\d+),\s*Pass:\s*(\d+)/.exec(text);
71
+ if (!summary) fail(2, 'test_result.txt 无汇总行,无法裁决');
72
+ const runN = +summary[1], failN = +summary[2], errN = +summary[3], passN = +summary[4];
73
+
74
+ const failing = [];
75
+ const lines = text.split('\n');
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const m = /^test=(.+)$/.exec(lines[i].trim());
78
+ if (m) {
79
+ // 该 test 后若干行内出现 result=Failure 或 "Error in"
80
+ for (let j = i + 1; j < Math.min(i + 12, lines.length); j++) {
81
+ if (/^test=/.test(lines[j].trim())) break;
82
+ if (/result=Failure|^Error in/.test(lines[j].trim())) { failing.push(m[1]); break; }
83
+ }
84
+ }
85
+ }
86
+
87
+ console.log(`[arkts-vectors] ${moduleName}: run=${runN} pass=${passN} fail=${failN} error=${errN}`);
88
+ if (runN === 0) fail(2, '0 个用例被执行(scope/脚手架有问题)');
89
+ // 防假绿 ③:向量集缩水检测(部分测试文件编译断裂被静默跳过 → run 数骤降却仍"全绿",实证 46→9)。
90
+ // 基线自记账:每次全绿把本 scope 的 run 数写入 <工程>/.vector-counts.json(只升不降);
91
+ // 低于基线即失败。有意裁剪向量时删除该文件对应键重置基线(错误信息里已写明)。
92
+ const countsFile = join(root, '.vector-counts.json');
93
+ const countsKey = `${moduleName}:${scope || '*'}`;
94
+ let counts = {};
95
+ try { counts = JSON.parse(readFileSync(countsFile, 'utf8')) || {}; } catch { /* 首跑无基线 */ }
96
+ const baseline = Number(counts[countsKey]) || 0;
97
+ if (baseline && runN < baseline) {
98
+ fail(1, `向量集缩水:本轮仅执行 ${runN} 例,低于基线 ${baseline}(${countsKey})——疑似部分测试文件编译断裂被静默跳过;`
99
+ + `如确属有意裁剪向量,删除 ${countsFile} 中 "${countsKey}" 键重置基线后重跑`);
100
+ }
101
+ if (failN > 0 || errN > 0) {
102
+ console.error('[arkts-vectors] 失败用例: ' + (failing.slice(0, 40).join(', ') || '(见 ' + resultFile + ')'));
103
+ process.exit(1);
104
+ }
105
+ try { counts[countsKey] = Math.max(runN, baseline); writeFileSync(countsFile, JSON.stringify(counts, null, 2) + '\n'); } catch { /* 记账失败不阻断裁决 */ }
106
+ console.log('[arkts-vectors] 全绿 ✓');
107
+ process.exit(0);
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // 装 ArkTS Local Test 脚手架:entry/src/test/ + @ohos/hypium 依赖(含离线兜底)。
3
+ // 迁移 stage-2 建空工程后调用;算法域向量门(run-arkts-vectors.mjs)依赖它。
4
+ //
5
+ // 用法: node setup-arkts-test.mjs --project <DST> [--module entry]
6
+ // 退出码: 0=就绪 · 2=无法就绪(hypium 既装不上也找不到缓存)
7
+
8
+ import { spawnSync } from 'node:child_process';
9
+ import { resolveOhpm, spawnOpts, quoteIfSpace } from '../lib/scan.mjs';
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, symlinkSync, readdirSync, statSync } from 'node:fs';
11
+ import { join, resolve } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+
14
+ const arg = (n, d) => { const i = process.argv.indexOf('--' + n); return i >= 0 ? process.argv[i + 1] : d; };
15
+ const project = arg('project'); const moduleName = arg('module', 'entry');
16
+ if (!project) { console.error('[setup-arkts-test] 缺 --project'); process.exit(2); }
17
+ const root = resolve(project);
18
+ const mod = join(root, moduleName);
19
+
20
+ // 1) test 目录 + List.test.ets 骨架(不覆盖已有)
21
+ const testDir = join(mod, 'src', 'test');
22
+ mkdirSync(testDir, { recursive: true });
23
+ const listFile = join(testDir, 'List.test.ets');
24
+ if (!existsSync(listFile)) {
25
+ writeFileSync(listFile, '// 各算法域 harness 在此注册(提取步追加 import + 调用)\nexport default function testsuite() {\n}\n');
26
+ console.log('[setup-arkts-test] 建 ' + listFile);
27
+ }
28
+
29
+ // 2) entry/oh-package.json5 加 hypium devDependency
30
+ const pkgFile = join(mod, 'oh-package.json5');
31
+ if (existsSync(pkgFile)) {
32
+ let t = readFileSync(pkgFile, 'utf8');
33
+ if (!/@ohos\/hypium/.test(t)) {
34
+ if (/"devDependencies"\s*:\s*\{/.test(t)) t = t.replace(/("devDependencies"\s*:\s*\{)/, '$1\n "@ohos/hypium": "1.0.25",');
35
+ else if (/^\s*\{\s*\}\s*$/.test(t)) t = '{\n "devDependencies": { "@ohos/hypium": "1.0.25" }\n}\n';
36
+ // 前一属性若无尾逗号必须补上——直接顶格追加会产出非法 JSON5(构建红,实证)
37
+ else t = t.replace(/(\S)\s*\}\s*$/, (_m, ch) => `${ch === ',' || ch === '{' ? ch : ch + ','}\n "devDependencies": { "@ohos/hypium": "1.0.25" }\n}\n`);
38
+ writeFileSync(pkgFile, t);
39
+ console.log('[setup-arkts-test] oh-package.json5 加 @ohos/hypium');
40
+ }
41
+ }
42
+
43
+ // 3) 装依赖:先 ohpm install,失败则离线链入
44
+ function hypiumReady() {
45
+ return existsSync(join(mod, 'oh_modules', '@ohos', 'hypium')) || existsSync(join(root, 'oh_modules', '@ohos', 'hypium'));
46
+ }
47
+ if (!hypiumReady()) {
48
+ const ohpm = resolveOhpm();
49
+ const r = spawnSync(quoteIfSpace(ohpm), ['install'], spawnOpts(ohpm, { cwd: root, encoding: 'utf8' }));
50
+ if (!hypiumReady()) {
51
+ console.log('[setup-arkts-test] ohpm install 未就绪(' + ((r.stderr || '').includes('ECONNRESET') ? '网络不可达' : '未知') + '),转离线链入');
52
+ // 扫已知缓存位置找 @ohos+hypium@* / @ohos+hamock@*
53
+ const roots = [join(homedir(), 'Downloads'), join(homedir(), 'Repos'), homedir()];
54
+ const found = {};
55
+ for (const base of roots) {
56
+ let entries = [];
57
+ try { entries = readdirSync(base); } catch { continue; }
58
+ for (const e of entries) {
59
+ const store = join(base, e, 'oh_modules', '.ohpm');
60
+ if (!existsSync(store)) continue;
61
+ for (const pkg of (() => { try { return readdirSync(store); } catch { return []; } })()) {
62
+ if (/^@ohos\+hypium@/.test(pkg) && !found.hypium) found.hypium = join(store, pkg);
63
+ if (/^@ohos\+hamock@/.test(pkg) && !found.hamock) found.hamock = join(store, pkg);
64
+ }
65
+ if (found.hypium && found.hamock) break;
66
+ }
67
+ if (found.hypium && found.hamock) break;
68
+ }
69
+ if (!found.hypium) { console.error('[setup-arkts-test] 离线也找不到 @ohos+hypium 缓存——请在联网机器 ohpm install 一次或手动放置'); process.exit(2); }
70
+ const dstStore = join(root, 'oh_modules', '.ohpm');
71
+ const dstAt = join(root, 'oh_modules', '@ohos');
72
+ mkdirSync(dstStore, { recursive: true }); mkdirSync(dstAt, { recursive: true });
73
+ // 根级 + 模块级都要链:hvigorw test 编译 entry 测试时按模块 oh_modules 解析,
74
+ // 只链根级会"装了却解析不到"(实证:需手工补 cp 才编译过)
75
+ const dstAtMod = join(mod, 'oh_modules', '@ohos');
76
+ mkdirSync(dstAtMod, { recursive: true });
77
+ for (const k of ['hypium', 'hamock']) {
78
+ if (!found[k]) continue;
79
+ const name = found[k].split('/').pop();
80
+ cpSync(found[k], join(dstStore, name), { recursive: true });
81
+ const target = join(dstStore, name, 'oh_modules', '@ohos', k);
82
+ try { symlinkSync(target, join(dstAt, k)); } catch { /* 已存在 */ }
83
+ try { symlinkSync(target, join(dstAtMod, k)); } catch { /* 已存在 */ }
84
+ console.log('[setup-arkts-test] 离线链入 @ohos/' + k + '(根级+模块级)← ' + name);
85
+ }
86
+ }
87
+ }
88
+
89
+ if (hypiumReady()) { console.log('[setup-arkts-test] 就绪 ✓'); process.exit(0); }
90
+ console.error('[setup-arkts-test] hypium 仍不可用'); process.exit(2);