@harmonyos-arkts/d2h 0.1.11 → 0.1.13

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 (126) hide show
  1. package/.claude-plugin/marketplace.json +15 -13
  2. package/.claude-plugin/plugin.json +13 -7
  3. package/AGENTS.md +5 -5
  4. package/README.md +212 -212
  5. package/agents/android-to-hmos-00-orchestrator.md +318 -318
  6. package/agents/d2h.md +157 -157
  7. package/dist/bin/install-opencode.js +30 -30
  8. package/docs/environment-check.md +86 -86
  9. package/install-opencode.sh +160 -160
  10. package/opencode/agents.json +16 -16
  11. package/opencode/vscode-d2h-package.json +7 -7
  12. package/opencode/vscode-d2h-plugin.mjs +29 -29
  13. package/package.json +54 -54
  14. package/schemas/android-source-manifest.schema.json +25 -25
  15. package/schemas/checkpoint-provenance.schema.json +67 -67
  16. package/schemas/final-acceptance.schema.json +29 -29
  17. package/schemas/managed-evidence-index.schema.json +15 -15
  18. package/schemas/managed-evidence.schema.json +114 -114
  19. package/schemas/migration-config.schema.json +51 -51
  20. package/schemas/migration-report-index.schema.json +39 -39
  21. package/schemas/migration-report-item.schema.json +51 -51
  22. package/schemas/migration-report-summary.schema.json +25 -25
  23. package/schemas/migration-status.schema.json +202 -202
  24. package/schemas/preflight.schema.json +60 -60
  25. package/schemas/source-order-audit.schema.json +16 -16
  26. package/schemas/source-provenance-event.schema.json +19 -19
  27. package/schemas/spec-app-shard.schema.json +45 -45
  28. package/schemas/spec-fact-corrections-shard.schema.json +13 -13
  29. package/schemas/spec-features-shard.schema.json +43 -43
  30. package/schemas/spec-index.schema.json +26 -26
  31. package/schemas/spec-interactions-shard.schema.json +40 -40
  32. package/schemas/spec-page.schema.json +98 -98
  33. package/schemas/spec-pages-index.schema.json +1 -1
  34. package/schemas/spec-unresolved-shard.schema.json +1 -1
  35. package/schemas/task-envelope.schema.json +109 -109
  36. package/schemas/task-plan.schema.json +341 -341
  37. package/scripts/check-environment.mjs +320 -320
  38. package/skills/android2hmos_resources_convert/SKILL.md +180 -180
  39. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
  40. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
  41. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
  42. package/skills/arkts-app-identity/SKILL.md +238 -238
  43. package/skills/arkts-i18n/SKILL.md +496 -496
  44. package/skills/arkts-i18n/evals/evals.json +84 -84
  45. package/skills/arkts-i18n/references/code-examples.md +302 -302
  46. package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
  47. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
  48. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
  49. package/skills/arkts-i18n/references/language-codes.md +104 -104
  50. package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
  51. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
  52. package/skills/arkts-i18n/references/v1-compat.md +244 -244
  53. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
  54. package/skills/arkts-icon-sizing/SKILL.md +211 -211
  55. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
  56. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
  57. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
  58. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
  59. package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
  60. package/skills/d2h-hypium-forge/SKILL.md +309 -309
  61. package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
  62. package/skills/d2h-hypium-forge/references/official-api.md +135 -135
  63. package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
  64. package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
  65. package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
  66. package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
  67. package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
  68. package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
  69. package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
  70. package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
  71. package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
  72. package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
  73. package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
  74. package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
  75. package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
  76. package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
  77. package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
  78. package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
  79. package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
  80. package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
  81. package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
  82. package/skills/d2h-page-develop/SKILL.md +112 -112
  83. package/skills/d2h-page-develop/agents/openai.yaml +7 -7
  84. package/skills/d2h-page-spec-generate/SKILL.md +206 -206
  85. package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
  86. package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
  87. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
  88. package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
  89. package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
  90. package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
  91. package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
  92. package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
  93. package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
  94. package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
  95. package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
  96. package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
  97. package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
  98. package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
  99. package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
  100. package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
  101. package/skills/d2h-utg-static-generate/SKILL.md +73 -73
  102. package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
  103. package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
  104. package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
  105. package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
  106. package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
  107. package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
  108. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
  109. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
  110. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
  111. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
  112. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
  113. package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
  114. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
  115. package/skills/maintaining-migration-report/SKILL.md +144 -144
  116. package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
  117. package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
  118. package/skills/managing-d2h-toolchain/SKILL.md +24 -24
  119. package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
  120. package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
  121. package/skills/managing-d2h-toolchain/toolchain.json +16 -16
  122. package/skills/native-library-substitution/SKILL.md +384 -384
  123. package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
  124. package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
  125. package/skills/preparing-migration-workspace/SKILL.md +131 -131
  126. package/skills/reviewing-migration-process/SKILL.md +62 -62
@@ -1,320 +1,320 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { spawn, spawnSync } from 'node:child_process';
6
- import { fileURLToPath, pathToFileURL } from 'node:url';
7
- import { parseArgs } from 'node:util';
8
-
9
- const DEFAULT_REQUIREMENTS = fileURLToPath(new URL('../skills/managing-d2h-toolchain/environment-requirements.json', import.meta.url));
10
- const VERSION_TOOLS = ['node', 'python', 'git', 'npm', 'java', 'python-json5', 'appgraph', 'droid2hmos', 'hvigor', 'ohpm', 'devecocli'];
11
-
12
- function versionParts(value) {
13
- if (typeof value !== 'string' || !/^\d+(?:\.\d+){0,3}$/.test(value)) throw new Error(`无效版本:${value}`);
14
- const parts = value.split('.').map(Number);
15
- if (parts.some(part => !Number.isSafeInteger(part))) throw new Error(`无效版本:${value}`);
16
- return [...parts, ...Array(4 - parts.length).fill(0)];
17
- }
18
-
19
- function compareVersions(left, right) {
20
- const a = versionParts(left);
21
- const b = versionParts(right);
22
- for (let index = 0; index < a.length; index++) {
23
- if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
24
- }
25
- return 0;
26
- }
27
-
28
- export function loadRequirements(file = DEFAULT_REQUIREMENTS) {
29
- const config = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
30
- if (config?.schemaVersion !== 1 || !config.tools || typeof config.tools !== 'object' || Array.isArray(config.tools)) {
31
- throw new Error('版本配置必须包含 schemaVersion: 1 和 tools 对象');
32
- }
33
- for (const key of Object.keys(config)) {
34
- if (!['schemaVersion', 'tools'].includes(key)) throw new Error(`未知配置字段:${key}`);
35
- }
36
- for (const key of Object.keys(config.tools)) {
37
- if (!VERSION_TOOLS.includes(key)) throw new Error(`未知工具:${key}`);
38
- }
39
- for (const key of VERSION_TOOLS) {
40
- if (!Object.hasOwn(config.tools, key)) throw new Error(`版本配置缺少工具:${key};不约束版本请设为 null`);
41
- const rule = config.tools[key];
42
- if (rule === null) continue;
43
- if (typeof rule !== 'object' || Array.isArray(rule) || !Object.keys(rule).length) throw new Error(`无效工具约束:${key}`);
44
- for (const bound of Object.keys(rule)) {
45
- if (!['minimum', 'maximumExclusive'].includes(bound)) throw new Error(`未知版本边界:${key}.${bound}`);
46
- versionParts(rule[bound]);
47
- }
48
- if (rule.minimum && rule.maximumExclusive && compareVersions(rule.minimum, rule.maximumExclusive) >= 0) {
49
- throw new Error(`版本区间为空:${key}`);
50
- }
51
- }
52
- return config;
53
- }
54
-
55
- function constraintLabel(rule) {
56
- return rule ? [rule.minimum && `>=${rule.minimum}`, rule.maximumExclusive && `<${rule.maximumExclusive}`].filter(Boolean).join(' ') : '仅检查可用性';
57
- }
58
-
59
- export function meetsVersionRule(output, rule, id = '') {
60
- if (!rule) return true;
61
- const match = output.match(/\d+\.\d+(?:\.\d+){0,2}/);
62
- if (!match) return false;
63
- let version = match[0];
64
- if (id === 'java' && version.startsWith('1.')) version = version.slice(2);
65
- return (!rule.minimum || compareVersions(version, rule.minimum) >= 0) &&
66
- (!rule.maximumExclusive || compareVersions(version, rule.maximumExclusive) < 0);
67
- }
68
-
69
- export function resolveExecutable(command, env = process.env, platform = process.platform) {
70
- // npm ships Unix shims beside .cmd files; Windows cannot spawn the shims.
71
- const extensions = platform === 'win32' && !path.extname(command)
72
- ? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';') : [''];
73
- const directories = /[/\\]/.test(command) ? [''] : (env.PATH || '').split(platform === 'win32' ? ';' : ':');
74
- for (const directory of directories) {
75
- for (const extension of extensions) {
76
- const candidate = directory ? path.join(directory, command + extension) : command + extension;
77
- try {
78
- if (!fs.statSync(candidate).isFile()) continue;
79
- if (platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK);
80
- return path.resolve(candidate);
81
- } catch { /* Try the next candidate. */ }
82
- }
83
- }
84
- return command;
85
- }
86
-
87
- export function invocation(executable, args, platform = process.platform, env = process.env) {
88
- const resolved = resolveExecutable(executable, env, platform);
89
- if (/\.m?js$/i.test(resolved)) return [process.execPath, resolved, ...args];
90
- if (platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
91
- const quote = (value) => `'${String(value).replaceAll("'", "''")}'`;
92
- const source = `& ${[resolved, ...args].map(quote).join(' ')}; exit $LASTEXITCODE`;
93
- return ['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')];
94
- }
95
- return [resolved, ...args];
96
- }
97
-
98
- export async function probe(executable, args, timeout = 15, options = {}) {
99
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'd2h-env-'));
100
- const log = fs.openSync(path.join(directory, 'output.log'), 'w+');
101
- const errorLog = fs.openSync(path.join(directory, 'error.log'), 'w+');
102
- let exitCode = null;
103
- let error = null;
104
- try {
105
- const [command, ...commandArgs] = invocation(executable, args);
106
- await new Promise((resolve) => {
107
- // Files and the exit event avoid waiting on inherited output handles.
108
- const child = spawn(command, commandArgs, {
109
- cwd: options.cwd || directory, windowsHide: true, detached: process.platform !== 'win32',
110
- env: { ...process.env, DEVECO_CLI_SKIP_VERSION_CHECK: '1' }, stdio: ['ignore', log, errorLog],
111
- });
112
- const timer = setTimeout(() => {
113
- error = 'timeout';
114
- if (child.pid) {
115
- if (process.platform === 'win32') {
116
- spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
117
- } else {
118
- try { process.kill(-child.pid, 'SIGKILL'); } catch { /* Already exited. */ }
119
- }
120
- }
121
- clearTimeout(timer);
122
- resolve();
123
- }, timeout * 1000);
124
- child.once('error', (err) => { error ??= err.message; clearTimeout(timer); resolve(); });
125
- child.once('exit', (code) => { exitCode = code; clearTimeout(timer); resolve(); });
126
- });
127
- const read = (descriptor) => {
128
- const buffer = Buffer.alloc(65536);
129
- const length = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
130
- return buffer.subarray(0, length).toString('utf8').trim();
131
- };
132
- const stdout = read(log);
133
- const stderr = read(errorLog);
134
- return { command: [executable, ...args], exitCode, ok: exitCode === 0 && !error, error, stdout, stderr, output: [stdout, stderr].filter(Boolean).join('\n') };
135
- } catch (err) {
136
- return { command: [executable, ...args], exitCode, ok: false, error: err.message, output: '' };
137
- } finally {
138
- fs.closeSync(log);
139
- fs.closeSync(errorLog);
140
- try { fs.rmSync(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); }
141
- catch { /* Windows scanners may retain a handle briefly; do not hide the probe result. */ }
142
- }
143
- }
144
-
145
- function isDirectory(value) {
146
- try { return Boolean(value && fs.statSync(value).isDirectory()); } catch { return false; }
147
- }
148
-
149
- function isFile(value) {
150
- try { return Boolean(value && fs.statSync(value).isFile()); } catch { return false; }
151
- }
152
-
153
- export function firstDirectory(values) {
154
- const found = values.find(isDirectory);
155
- return found ? path.resolve(found) : null;
156
- }
157
-
158
- export function pythonJson5Required(source) {
159
- function visit(directory) {
160
- if (!isDirectory(directory)) return false;
161
- return fs.readdirSync(directory, { withFileTypes: true }).some((entry) => {
162
- if (['__pycache__', 'node_modules', '.git', 'tests'].includes(entry.name)) return false;
163
- const file = path.join(directory, entry.name);
164
- return entry.isDirectory() ? visit(file) : entry.name.endsWith('.py') &&
165
- /^\s*(?:import\s+json5\b|from\s+json5\b)/m.test(fs.readFileSync(file, 'utf8'));
166
- });
167
- }
168
- return visit(path.join(source, 'skills'));
169
- }
170
-
171
- export function parseDevEcoJson(output) {
172
- // DevEco CLI 1.3.3 appends its updater notice to stdout even with --format json.
173
- // Strip only that known trailing line; unrelated malformed output still fails.
174
- return JSON.parse(output.replace(/\r?\n\s*New version [\w.+-]+ available, run `devecocli update` to upgrade\s*$/, '').trim());
175
- }
176
-
177
- export async function collect(options, { runner = probe, env = process.env, platform = process.platform, home = os.homedir(), directoryFinder = firstDirectory, nodeVersion = process.version } = {}) {
178
- const requirementsFile = path.resolve(options.requirementsFile || DEFAULT_REQUIREMENTS);
179
- const requirements = loadRequirements(requirementsFile);
180
- const ruleFor = id => requirements.tools[id];
181
- const checks = [];
182
- const add = (id, required, passed, summary, remediation = '', evidence = [], skipped = false) => {
183
- checks.push({ id, required, status: skipped ? 'skipped' : passed ? 'passed' : required ? 'failed' : 'warning', summary,
184
- remediation: passed ? '' : remediation, evidence });
185
- };
186
- const tool = async (id, executable, params, remediation, predicate = () => true) => {
187
- const result = await runner(executable, params, options.timeout);
188
- const rule = ruleFor(id);
189
- const passed = result.ok && predicate(result.output) && meetsVersionRule(result.output, rule, id);
190
- const summary = result.output.split(/\r?\n/)[0] || result.error || '命令未成功';
191
- add(id, true, passed, `${summary}${rule ? `(要求 ${constraintLabel(rule)})` : ''}`,
192
- `${remediation}${rule ? ` 版本须满足 ${constraintLabel(rule)}。` : ''}`, [result]);
193
- return result;
194
- };
195
- add('node', true, meetsVersionRule(nodeVersion, ruleFor('node')), `${nodeVersion} (${process.execPath})(要求 ${constraintLabel(ruleFor('node'))})`,
196
- `请安装满足 ${constraintLabel(ruleFor('node'))} 的 Node.js,然后重新运行脚本。`);
197
- await tool('git', 'git', ['--version'], '安装 Git 并将 git 加入 PATH。');
198
- await tool('npm', 'npm', ['--version'], '使用 Node.js 自带的 npm,检查 PATH。');
199
- await tool('java', 'java', ['-version'], '安装项目 Gradle/DevEco 所需 JDK,设置 JAVA_HOME 和 PATH;本检查不限定 JDK 主版本。');
200
- // Python is a migration dependency, no longer a prerequisite to run this doctor.
201
- const candidates = [...new Set([env.PYTHON_BINARY, platform === 'win32' ? 'python' : 'python3', 'python',
202
- platform === 'darwin' ? '/opt/homebrew/bin/python3' : null, platform === 'darwin' ? '/usr/local/bin/python3' : null,
203
- platform === 'darwin' ? '/opt/homebrew/anaconda3/bin/python3' : null].filter(Boolean))];
204
- const pythonProbes = [];
205
- let python = null;
206
- for (const candidate of candidates) {
207
- const result = await runner(candidate, ['--version'], options.timeout);
208
- pythonProbes.push(result);
209
- if (result.ok && meetsVersionRule(result.output, ruleFor('python'))) { python = candidate; break; }
210
- }
211
- add('python', true, Boolean(python), `${python ? `${pythonProbes.at(-1).output} (${python})` : '未找到符合要求的 Python'}(要求 ${constraintLabel(ruleFor('python'))})`,
212
- `安装满足 ${constraintLabel(ruleFor('python'))} 的 Python 并加入 PATH,或设置 PYTHON_BINARY 为解释器路径。`, pythonProbes);
213
- if (pythonJson5Required(path.resolve(options.pluginRoot))) {
214
- if (python) {
215
- await tool('python-json5', python, ['-c', 'import json5; from importlib.metadata import version; print("json5 " + version("json5"))'],
216
- `用检测到的 Python 安装:${platform === 'win32' ? '& ' : ''}'${python.replaceAll("'", platform === 'win32' ? "''" : "'\\''")}' -m pip install json5`);
217
- } else {
218
- add('python-json5', true, false, 'Python 不可用,尚未检查 json5', '安装 Python 后重新检查;使用同一解释器执行 -m pip install json5。', [], true);
219
- }
220
- }
221
- for (const name of ['appgraph', 'droid2hmos']) {
222
- await tool(name, name, ['--version'], '运行 managing-d2h-toolchain Skill 中的 node scripts/manage-toolchain.mjs install 安装精确版本;本地 npm link 需由开发者显式管理。');
223
- }
224
- const deveco = directoryFinder([env.DEVECO_STUDIO_HOME, env.DEVECO_HOME,
225
- platform === 'darwin' ? '/Applications/DevEco-Studio.app/Contents' : null,
226
- ...(platform === 'win32' ? ['C:/Program Files/Huawei/DevEco Studio', 'D:/Program Files/Huawei/DevEco Studio'] : [])]);
227
- add('deveco', true, Boolean(deveco), deveco || '未找到 DevEco Studio', '安装 DevEco Studio,设置 DEVECO_STUDIO_HOME 为安装根目录(macOS 为 Contents)。');
228
- for (const [id, variable, relative, fallback] of [
229
- ['hvigor', 'DROID2HMOS_HVIGOR', 'tools/hvigor/bin/hvigorw.js', 'hvigorw'],
230
- ['ohpm', 'DROID2HMOS_OHPM', 'tools/ohpm/bin/pm-cli.js', 'ohpm'],
231
- ]) {
232
- const bundled = deveco && path.join(deveco, relative);
233
- await tool(id, env[variable] || (isFile(bundled) ? bundled : fallback), ['--version'], `安装 DevEco 配套工具;设置 ${variable} 为可执行文件或 JS 入口路径。`);
234
- }
235
- const sdk = directoryFinder([options.sdkPath, env.HARMONYOS_SDK_HOME, env.OHOS_SDK_HOME, env.DEVECO_SDK_HOME, deveco && path.join(deveco, 'sdk')]);
236
- add('harmony-sdk-location', false, Boolean(sdk), sdk || '未发现声明 SDK 根目录', '设置 HARMONYOS_SDK_HOME 或 --sdk-path;实际声明有效性由 droid2hmos preflight 检查。');
237
- const cli = env.DEVECO_CLI_BINARY || 'devecocli';
238
- await tool('devecocli', cli, ['--version'], '安装 npm install -g @deveco/deveco-cli。');
239
- const auth = await runner(cli, ['auth', 'status'], options.timeout);
240
- const loggedIn = auth.ok && /Current user:\s*[^\r\n]+/i.test(auth.output) && !/Not logged in/i.test(auth.output);
241
- add('devecocli-auth', true, loggedIn, loggedIn ? '已登录' : '未登录或无法确认登录状态',
242
- '运行 droid2hmos auth login <android-project>,在浏览器完成授权;不要并发登录。',
243
- [{ command: auth.command, exitCode: auth.exitCode, error: auth.error }]);
244
- if (options.harmonyProject) {
245
- const project = path.resolve(options.harmonyProject);
246
- const versions = await runner(cli, ['check', 'compat', 'versions', '--format', 'json'], options.timeout, { cwd: project });
247
- let available = [];
248
- try { const value = parseDevEcoJson(versions.stdout ?? versions.output); available = Array.isArray(value.versions) ? value.versions.filter((v) => typeof v === 'string') : []; } catch { /* Keep diagnostic. */ }
249
- add('harmony-sdk-compatibility', false, versions.ok && available.length > 0,
250
- available.length ? `DevEco CLI 可识别 SDK:${available.join(', ')}` : '无法确认工程可用 SDK 版本',
251
- '在 DevEco 中安装工程所需 SDK;构建与 API 兼容性仍由项目 preflight 验证。', [versions]);
252
- }
253
- if (options.androidProject) {
254
- const root = path.resolve(options.androidProject);
255
- add('android-source-root', true, ['settings.gradle', 'settings.gradle.kts', 'build.gradle', 'build.gradle.kts'].some((f) => isFile(path.join(root, f))),
256
- root, '用 --android-project 指向包含 Gradle 配置标记的 Android 源码根目录;不要求 Wrapper 或 Android 编译环境。');
257
- }
258
- const result = checks.some((c) => c.required && c.status === 'failed') ? 'failed'
259
- : checks.some((c) => c.required && c.status === 'skipped') ? 'incomplete' : 'passed';
260
- return { schemaVersion: '1.0.0', createdAt: new Date().toISOString(), platform: `${platform} ${os.arch()} ${os.release()}`, result, checks,
261
- requirementsFile, versionRequirements: requirements.tools,
262
- scope: '主机依赖检查;不证明构建、签名、SDK API 匹配或端到端验收成功。' };
263
- }
264
-
265
- export function markdown(report) {
266
- const escape = (value) => String(value).replaceAll('|', '\\|').replaceAll('\r', '').replaceAll('\n', '<br>');
267
- return ['# Android → HarmonyOS 环境检查', '', `结果:**${report.result}**`, '', report.scope, '', `版本配置:${report.requirementsFile || ''}`, '',
268
- '| 检查 | 必需 | 状态 | 说明 |', '| --- | --- | --- | --- |',
269
- ...report.checks.map((c) => `| ${c.id} | ${c.required ? '是' : '否'} | ${c.status} | ${escape(c.summary)} |`),
270
- '', '## 修复指导', '', ...report.checks.filter((c) => c.remediation).map((c) => `- **${c.id}**:${c.remediation}`), '',
271
- '修复后重新运行本脚本。主机检查通过后,运行 `droid2hmos preflight <android-project>` 验证真实项目门禁;该命令可能初始化工程并执行构建。', ''].join('\n');
272
- }
273
-
274
- export function parseOptions(argv) {
275
- const { values } = parseArgs({ args: argv, options: {
276
- 'plugin-root': { type: 'string' },
277
- requirements: { type: 'string' },
278
- 'android-project': { type: 'string' }, 'harmony-project': { type: 'string' }, 'sdk-path': { type: 'string' },
279
- timeout: { type: 'string', default: '15' }, 'output-dir': { type: 'string' }, help: { type: 'boolean', short: 'h' },
280
- } });
281
- if (!/^\d+$/.test(values.timeout) || Number(values.timeout) < 1 || Number(values.timeout) > 60) throw new Error('--timeout 必须为 1–60 的整数');
282
- return { pluginRoot: values['plugin-root'] || fileURLToPath(new URL('../', import.meta.url)),
283
- requirementsFile: values.requirements ? path.resolve(values.requirements) : DEFAULT_REQUIREMENTS,
284
- androidProject: values['android-project'], sdkPath: values['sdk-path'],
285
- harmonyProject: values['harmony-project'],
286
- timeout: Number(values.timeout),
287
- outputDir: path.resolve(values['output-dir'] || 'environment-report'), help: values.help };
288
- }
289
-
290
- export async function main(argv = process.argv.slice(2)) {
291
- let options;
292
- try { options = parseOptions(argv); } catch (error) { console.error(error.message); return 2; }
293
- if (options.help) {
294
- console.log('用法:node scripts/check-environment.mjs [--plugin-root <path>] [--android-project <path>]\n [--harmony-project <path>] [--sdk-path <path>] [--requirements <json>]\n [--timeout <1–60 秒>] [--output-dir <path>]\n默认版本约束来自 skills/managing-d2h-toolchain/environment-requirements.json。只写报告,不自动安装依赖或修改配置。');
295
- return 0;
296
- }
297
- const report = await collect(options);
298
- fs.mkdirSync(options.outputDir, { recursive: true });
299
- fs.writeFileSync(path.join(options.outputDir, 'environment.json'), JSON.stringify(report, null, 2) + '\n');
300
- fs.writeFileSync(path.join(options.outputDir, 'environment.md'), markdown(report));
301
- const passed = report.checks.filter((check) => check.status === 'passed').length;
302
- const failures = report.checks.filter((check) => check.status === 'failed');
303
- const warnings = report.checks.filter((check) => check.status === 'warning').length;
304
- const skipped = report.checks.filter((check) => check.status === 'skipped').length;
305
- const resultLabel = { passed: '通过', failed: '未通过', incomplete: '未完成' }[report.result];
306
- console.log(`环境检查:${resultLabel}(通过 ${passed},失败 ${failures.length}${warnings ? `,提醒 ${warnings}` : ''}${skipped ? `,未检查 ${skipped}` : ''})`);
307
- const statusLabels = { passed: '通过', failed: '未通过', warning: '提醒', skipped: '未检查' };
308
- for (const check of report.checks) {
309
- console.log(`[${statusLabels[check.status] || check.status}] ${check.id}:${check.summary}`);
310
- if (check.status !== 'passed' && check.remediation) {
311
- console.log(` 修复:${check.remediation}`);
312
- }
313
- }
314
- console.log(`详细报告:${path.join(options.outputDir, 'environment.md')}`);
315
- return report.result === 'failed' ? 1 : report.result === 'incomplete' ? 2 : 0;
316
- }
317
-
318
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
319
- try { process.exitCode = await main(); } catch (error) { console.error(`环境检查失败:${error.message}`); process.exitCode = 1; }
320
- }
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn, spawnSync } from 'node:child_process';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import { parseArgs } from 'node:util';
8
+
9
+ const DEFAULT_REQUIREMENTS = fileURLToPath(new URL('../skills/managing-d2h-toolchain/environment-requirements.json', import.meta.url));
10
+ const VERSION_TOOLS = ['node', 'python', 'git', 'npm', 'java', 'python-json5', 'appgraph', 'droid2hmos', 'hvigor', 'ohpm', 'devecocli'];
11
+
12
+ function versionParts(value) {
13
+ if (typeof value !== 'string' || !/^\d+(?:\.\d+){0,3}$/.test(value)) throw new Error(`无效版本:${value}`);
14
+ const parts = value.split('.').map(Number);
15
+ if (parts.some(part => !Number.isSafeInteger(part))) throw new Error(`无效版本:${value}`);
16
+ return [...parts, ...Array(4 - parts.length).fill(0)];
17
+ }
18
+
19
+ function compareVersions(left, right) {
20
+ const a = versionParts(left);
21
+ const b = versionParts(right);
22
+ for (let index = 0; index < a.length; index++) {
23
+ if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
24
+ }
25
+ return 0;
26
+ }
27
+
28
+ export function loadRequirements(file = DEFAULT_REQUIREMENTS) {
29
+ const config = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
30
+ if (config?.schemaVersion !== 1 || !config.tools || typeof config.tools !== 'object' || Array.isArray(config.tools)) {
31
+ throw new Error('版本配置必须包含 schemaVersion: 1 和 tools 对象');
32
+ }
33
+ for (const key of Object.keys(config)) {
34
+ if (!['schemaVersion', 'tools'].includes(key)) throw new Error(`未知配置字段:${key}`);
35
+ }
36
+ for (const key of Object.keys(config.tools)) {
37
+ if (!VERSION_TOOLS.includes(key)) throw new Error(`未知工具:${key}`);
38
+ }
39
+ for (const key of VERSION_TOOLS) {
40
+ if (!Object.hasOwn(config.tools, key)) throw new Error(`版本配置缺少工具:${key};不约束版本请设为 null`);
41
+ const rule = config.tools[key];
42
+ if (rule === null) continue;
43
+ if (typeof rule !== 'object' || Array.isArray(rule) || !Object.keys(rule).length) throw new Error(`无效工具约束:${key}`);
44
+ for (const bound of Object.keys(rule)) {
45
+ if (!['minimum', 'maximumExclusive'].includes(bound)) throw new Error(`未知版本边界:${key}.${bound}`);
46
+ versionParts(rule[bound]);
47
+ }
48
+ if (rule.minimum && rule.maximumExclusive && compareVersions(rule.minimum, rule.maximumExclusive) >= 0) {
49
+ throw new Error(`版本区间为空:${key}`);
50
+ }
51
+ }
52
+ return config;
53
+ }
54
+
55
+ function constraintLabel(rule) {
56
+ return rule ? [rule.minimum && `>=${rule.minimum}`, rule.maximumExclusive && `<${rule.maximumExclusive}`].filter(Boolean).join(' ') : '仅检查可用性';
57
+ }
58
+
59
+ export function meetsVersionRule(output, rule, id = '') {
60
+ if (!rule) return true;
61
+ const match = output.match(/\d+\.\d+(?:\.\d+){0,2}/);
62
+ if (!match) return false;
63
+ let version = match[0];
64
+ if (id === 'java' && version.startsWith('1.')) version = version.slice(2);
65
+ return (!rule.minimum || compareVersions(version, rule.minimum) >= 0) &&
66
+ (!rule.maximumExclusive || compareVersions(version, rule.maximumExclusive) < 0);
67
+ }
68
+
69
+ export function resolveExecutable(command, env = process.env, platform = process.platform) {
70
+ // npm ships Unix shims beside .cmd files; Windows cannot spawn the shims.
71
+ const extensions = platform === 'win32' && !path.extname(command)
72
+ ? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';') : [''];
73
+ const directories = /[/\\]/.test(command) ? [''] : (env.PATH || '').split(platform === 'win32' ? ';' : ':');
74
+ for (const directory of directories) {
75
+ for (const extension of extensions) {
76
+ const candidate = directory ? path.join(directory, command + extension) : command + extension;
77
+ try {
78
+ if (!fs.statSync(candidate).isFile()) continue;
79
+ if (platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK);
80
+ return path.resolve(candidate);
81
+ } catch { /* Try the next candidate. */ }
82
+ }
83
+ }
84
+ return command;
85
+ }
86
+
87
+ export function invocation(executable, args, platform = process.platform, env = process.env) {
88
+ const resolved = resolveExecutable(executable, env, platform);
89
+ if (/\.m?js$/i.test(resolved)) return [process.execPath, resolved, ...args];
90
+ if (platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
91
+ const quote = (value) => `'${String(value).replaceAll("'", "''")}'`;
92
+ const source = `& ${[resolved, ...args].map(quote).join(' ')}; exit $LASTEXITCODE`;
93
+ return ['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')];
94
+ }
95
+ return [resolved, ...args];
96
+ }
97
+
98
+ export async function probe(executable, args, timeout = 15, options = {}) {
99
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'd2h-env-'));
100
+ const log = fs.openSync(path.join(directory, 'output.log'), 'w+');
101
+ const errorLog = fs.openSync(path.join(directory, 'error.log'), 'w+');
102
+ let exitCode = null;
103
+ let error = null;
104
+ try {
105
+ const [command, ...commandArgs] = invocation(executable, args);
106
+ await new Promise((resolve) => {
107
+ // Files and the exit event avoid waiting on inherited output handles.
108
+ const child = spawn(command, commandArgs, {
109
+ cwd: options.cwd || directory, windowsHide: true, detached: process.platform !== 'win32',
110
+ env: { ...process.env, DEVECO_CLI_SKIP_VERSION_CHECK: '1' }, stdio: ['ignore', log, errorLog],
111
+ });
112
+ const timer = setTimeout(() => {
113
+ error = 'timeout';
114
+ if (child.pid) {
115
+ if (process.platform === 'win32') {
116
+ spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
117
+ } else {
118
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { /* Already exited. */ }
119
+ }
120
+ }
121
+ clearTimeout(timer);
122
+ resolve();
123
+ }, timeout * 1000);
124
+ child.once('error', (err) => { error ??= err.message; clearTimeout(timer); resolve(); });
125
+ child.once('exit', (code) => { exitCode = code; clearTimeout(timer); resolve(); });
126
+ });
127
+ const read = (descriptor) => {
128
+ const buffer = Buffer.alloc(65536);
129
+ const length = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
130
+ return buffer.subarray(0, length).toString('utf8').trim();
131
+ };
132
+ const stdout = read(log);
133
+ const stderr = read(errorLog);
134
+ return { command: [executable, ...args], exitCode, ok: exitCode === 0 && !error, error, stdout, stderr, output: [stdout, stderr].filter(Boolean).join('\n') };
135
+ } catch (err) {
136
+ return { command: [executable, ...args], exitCode, ok: false, error: err.message, output: '' };
137
+ } finally {
138
+ fs.closeSync(log);
139
+ fs.closeSync(errorLog);
140
+ try { fs.rmSync(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); }
141
+ catch { /* Windows scanners may retain a handle briefly; do not hide the probe result. */ }
142
+ }
143
+ }
144
+
145
+ function isDirectory(value) {
146
+ try { return Boolean(value && fs.statSync(value).isDirectory()); } catch { return false; }
147
+ }
148
+
149
+ function isFile(value) {
150
+ try { return Boolean(value && fs.statSync(value).isFile()); } catch { return false; }
151
+ }
152
+
153
+ export function firstDirectory(values) {
154
+ const found = values.find(isDirectory);
155
+ return found ? path.resolve(found) : null;
156
+ }
157
+
158
+ export function pythonJson5Required(source) {
159
+ function visit(directory) {
160
+ if (!isDirectory(directory)) return false;
161
+ return fs.readdirSync(directory, { withFileTypes: true }).some((entry) => {
162
+ if (['__pycache__', 'node_modules', '.git', 'tests'].includes(entry.name)) return false;
163
+ const file = path.join(directory, entry.name);
164
+ return entry.isDirectory() ? visit(file) : entry.name.endsWith('.py') &&
165
+ /^\s*(?:import\s+json5\b|from\s+json5\b)/m.test(fs.readFileSync(file, 'utf8'));
166
+ });
167
+ }
168
+ return visit(path.join(source, 'skills'));
169
+ }
170
+
171
+ export function parseDevEcoJson(output) {
172
+ // DevEco CLI 1.3.3 appends its updater notice to stdout even with --format json.
173
+ // Strip only that known trailing line; unrelated malformed output still fails.
174
+ return JSON.parse(output.replace(/\r?\n\s*New version [\w.+-]+ available, run `devecocli update` to upgrade\s*$/, '').trim());
175
+ }
176
+
177
+ export async function collect(options, { runner = probe, env = process.env, platform = process.platform, home = os.homedir(), directoryFinder = firstDirectory, nodeVersion = process.version } = {}) {
178
+ const requirementsFile = path.resolve(options.requirementsFile || DEFAULT_REQUIREMENTS);
179
+ const requirements = loadRequirements(requirementsFile);
180
+ const ruleFor = id => requirements.tools[id];
181
+ const checks = [];
182
+ const add = (id, required, passed, summary, remediation = '', evidence = [], skipped = false) => {
183
+ checks.push({ id, required, status: skipped ? 'skipped' : passed ? 'passed' : required ? 'failed' : 'warning', summary,
184
+ remediation: passed ? '' : remediation, evidence });
185
+ };
186
+ const tool = async (id, executable, params, remediation, predicate = () => true) => {
187
+ const result = await runner(executable, params, options.timeout);
188
+ const rule = ruleFor(id);
189
+ const passed = result.ok && predicate(result.output) && meetsVersionRule(result.output, rule, id);
190
+ const summary = result.output.split(/\r?\n/)[0] || result.error || '命令未成功';
191
+ add(id, true, passed, `${summary}${rule ? `(要求 ${constraintLabel(rule)})` : ''}`,
192
+ `${remediation}${rule ? ` 版本须满足 ${constraintLabel(rule)}。` : ''}`, [result]);
193
+ return result;
194
+ };
195
+ add('node', true, meetsVersionRule(nodeVersion, ruleFor('node')), `${nodeVersion} (${process.execPath})(要求 ${constraintLabel(ruleFor('node'))})`,
196
+ `请安装满足 ${constraintLabel(ruleFor('node'))} 的 Node.js,然后重新运行脚本。`);
197
+ await tool('git', 'git', ['--version'], '安装 Git 并将 git 加入 PATH。');
198
+ await tool('npm', 'npm', ['--version'], '使用 Node.js 自带的 npm,检查 PATH。');
199
+ await tool('java', 'java', ['-version'], '安装项目 Gradle/DevEco 所需 JDK,设置 JAVA_HOME 和 PATH;本检查不限定 JDK 主版本。');
200
+ // Python is a migration dependency, no longer a prerequisite to run this doctor.
201
+ const candidates = [...new Set([env.PYTHON_BINARY, platform === 'win32' ? 'python' : 'python3', 'python',
202
+ platform === 'darwin' ? '/opt/homebrew/bin/python3' : null, platform === 'darwin' ? '/usr/local/bin/python3' : null,
203
+ platform === 'darwin' ? '/opt/homebrew/anaconda3/bin/python3' : null].filter(Boolean))];
204
+ const pythonProbes = [];
205
+ let python = null;
206
+ for (const candidate of candidates) {
207
+ const result = await runner(candidate, ['--version'], options.timeout);
208
+ pythonProbes.push(result);
209
+ if (result.ok && meetsVersionRule(result.output, ruleFor('python'))) { python = candidate; break; }
210
+ }
211
+ add('python', true, Boolean(python), `${python ? `${pythonProbes.at(-1).output} (${python})` : '未找到符合要求的 Python'}(要求 ${constraintLabel(ruleFor('python'))})`,
212
+ `安装满足 ${constraintLabel(ruleFor('python'))} 的 Python 并加入 PATH,或设置 PYTHON_BINARY 为解释器路径。`, pythonProbes);
213
+ if (pythonJson5Required(path.resolve(options.pluginRoot))) {
214
+ if (python) {
215
+ await tool('python-json5', python, ['-c', 'import json5; from importlib.metadata import version; print("json5 " + version("json5"))'],
216
+ `用检测到的 Python 安装:${platform === 'win32' ? '& ' : ''}'${python.replaceAll("'", platform === 'win32' ? "''" : "'\\''")}' -m pip install json5`);
217
+ } else {
218
+ add('python-json5', true, false, 'Python 不可用,尚未检查 json5', '安装 Python 后重新检查;使用同一解释器执行 -m pip install json5。', [], true);
219
+ }
220
+ }
221
+ for (const name of ['appgraph', 'droid2hmos']) {
222
+ await tool(name, name, ['--version'], '运行 managing-d2h-toolchain Skill 中的 node scripts/manage-toolchain.mjs install 安装精确版本;本地 npm link 需由开发者显式管理。');
223
+ }
224
+ const deveco = directoryFinder([env.DEVECO_STUDIO_HOME, env.DEVECO_HOME,
225
+ platform === 'darwin' ? '/Applications/DevEco-Studio.app/Contents' : null,
226
+ ...(platform === 'win32' ? ['C:/Program Files/Huawei/DevEco Studio', 'D:/Program Files/Huawei/DevEco Studio'] : [])]);
227
+ add('deveco', true, Boolean(deveco), deveco || '未找到 DevEco Studio', '安装 DevEco Studio,设置 DEVECO_STUDIO_HOME 为安装根目录(macOS 为 Contents)。');
228
+ for (const [id, variable, relative, fallback] of [
229
+ ['hvigor', 'DROID2HMOS_HVIGOR', 'tools/hvigor/bin/hvigorw.js', 'hvigorw'],
230
+ ['ohpm', 'DROID2HMOS_OHPM', 'tools/ohpm/bin/pm-cli.js', 'ohpm'],
231
+ ]) {
232
+ const bundled = deveco && path.join(deveco, relative);
233
+ await tool(id, env[variable] || (isFile(bundled) ? bundled : fallback), ['--version'], `安装 DevEco 配套工具;设置 ${variable} 为可执行文件或 JS 入口路径。`);
234
+ }
235
+ const sdk = directoryFinder([options.sdkPath, env.HARMONYOS_SDK_HOME, env.OHOS_SDK_HOME, env.DEVECO_SDK_HOME, deveco && path.join(deveco, 'sdk')]);
236
+ add('harmony-sdk-location', false, Boolean(sdk), sdk || '未发现声明 SDK 根目录', '设置 HARMONYOS_SDK_HOME 或 --sdk-path;实际声明有效性由 droid2hmos preflight 检查。');
237
+ const cli = env.DEVECO_CLI_BINARY || 'devecocli';
238
+ await tool('devecocli', cli, ['--version'], '安装 npm install -g @deveco/deveco-cli。');
239
+ const auth = await runner(cli, ['auth', 'status'], options.timeout);
240
+ const loggedIn = auth.ok && /Current user:\s*[^\r\n]+/i.test(auth.output) && !/Not logged in/i.test(auth.output);
241
+ add('devecocli-auth', true, loggedIn, loggedIn ? '已登录' : '未登录或无法确认登录状态',
242
+ '运行 droid2hmos auth login <android-project>,在浏览器完成授权;不要并发登录。',
243
+ [{ command: auth.command, exitCode: auth.exitCode, error: auth.error }]);
244
+ if (options.harmonyProject) {
245
+ const project = path.resolve(options.harmonyProject);
246
+ const versions = await runner(cli, ['check', 'compat', 'versions', '--format', 'json'], options.timeout, { cwd: project });
247
+ let available = [];
248
+ try { const value = parseDevEcoJson(versions.stdout ?? versions.output); available = Array.isArray(value.versions) ? value.versions.filter((v) => typeof v === 'string') : []; } catch { /* Keep diagnostic. */ }
249
+ add('harmony-sdk-compatibility', false, versions.ok && available.length > 0,
250
+ available.length ? `DevEco CLI 可识别 SDK:${available.join(', ')}` : '无法确认工程可用 SDK 版本',
251
+ '在 DevEco 中安装工程所需 SDK;构建与 API 兼容性仍由项目 preflight 验证。', [versions]);
252
+ }
253
+ if (options.androidProject) {
254
+ const root = path.resolve(options.androidProject);
255
+ add('android-source-root', true, ['settings.gradle', 'settings.gradle.kts', 'build.gradle', 'build.gradle.kts'].some((f) => isFile(path.join(root, f))),
256
+ root, '用 --android-project 指向包含 Gradle 配置标记的 Android 源码根目录;不要求 Wrapper 或 Android 编译环境。');
257
+ }
258
+ const result = checks.some((c) => c.required && c.status === 'failed') ? 'failed'
259
+ : checks.some((c) => c.required && c.status === 'skipped') ? 'incomplete' : 'passed';
260
+ return { schemaVersion: '1.0.0', createdAt: new Date().toISOString(), platform: `${platform} ${os.arch()} ${os.release()}`, result, checks,
261
+ requirementsFile, versionRequirements: requirements.tools,
262
+ scope: '主机依赖检查;不证明构建、签名、SDK API 匹配或端到端验收成功。' };
263
+ }
264
+
265
+ export function markdown(report) {
266
+ const escape = (value) => String(value).replaceAll('|', '\\|').replaceAll('\r', '').replaceAll('\n', '<br>');
267
+ return ['# Android → HarmonyOS 环境检查', '', `结果:**${report.result}**`, '', report.scope, '', `版本配置:${report.requirementsFile || ''}`, '',
268
+ '| 检查 | 必需 | 状态 | 说明 |', '| --- | --- | --- | --- |',
269
+ ...report.checks.map((c) => `| ${c.id} | ${c.required ? '是' : '否'} | ${c.status} | ${escape(c.summary)} |`),
270
+ '', '## 修复指导', '', ...report.checks.filter((c) => c.remediation).map((c) => `- **${c.id}**:${c.remediation}`), '',
271
+ '修复后重新运行本脚本。主机检查通过后,运行 `droid2hmos preflight <android-project>` 验证真实项目门禁;该命令可能初始化工程并执行构建。', ''].join('\n');
272
+ }
273
+
274
+ export function parseOptions(argv) {
275
+ const { values } = parseArgs({ args: argv, options: {
276
+ 'plugin-root': { type: 'string' },
277
+ requirements: { type: 'string' },
278
+ 'android-project': { type: 'string' }, 'harmony-project': { type: 'string' }, 'sdk-path': { type: 'string' },
279
+ timeout: { type: 'string', default: '15' }, 'output-dir': { type: 'string' }, help: { type: 'boolean', short: 'h' },
280
+ } });
281
+ if (!/^\d+$/.test(values.timeout) || Number(values.timeout) < 1 || Number(values.timeout) > 60) throw new Error('--timeout 必须为 1–60 的整数');
282
+ return { pluginRoot: values['plugin-root'] || fileURLToPath(new URL('../', import.meta.url)),
283
+ requirementsFile: values.requirements ? path.resolve(values.requirements) : DEFAULT_REQUIREMENTS,
284
+ androidProject: values['android-project'], sdkPath: values['sdk-path'],
285
+ harmonyProject: values['harmony-project'],
286
+ timeout: Number(values.timeout),
287
+ outputDir: path.resolve(values['output-dir'] || 'environment-report'), help: values.help };
288
+ }
289
+
290
+ export async function main(argv = process.argv.slice(2)) {
291
+ let options;
292
+ try { options = parseOptions(argv); } catch (error) { console.error(error.message); return 2; }
293
+ if (options.help) {
294
+ console.log('用法:node scripts/check-environment.mjs [--plugin-root <path>] [--android-project <path>]\n [--harmony-project <path>] [--sdk-path <path>] [--requirements <json>]\n [--timeout <1–60 秒>] [--output-dir <path>]\n默认版本约束来自 skills/managing-d2h-toolchain/environment-requirements.json。只写报告,不自动安装依赖或修改配置。');
295
+ return 0;
296
+ }
297
+ const report = await collect(options);
298
+ fs.mkdirSync(options.outputDir, { recursive: true });
299
+ fs.writeFileSync(path.join(options.outputDir, 'environment.json'), JSON.stringify(report, null, 2) + '\n');
300
+ fs.writeFileSync(path.join(options.outputDir, 'environment.md'), markdown(report));
301
+ const passed = report.checks.filter((check) => check.status === 'passed').length;
302
+ const failures = report.checks.filter((check) => check.status === 'failed');
303
+ const warnings = report.checks.filter((check) => check.status === 'warning').length;
304
+ const skipped = report.checks.filter((check) => check.status === 'skipped').length;
305
+ const resultLabel = { passed: '通过', failed: '未通过', incomplete: '未完成' }[report.result];
306
+ console.log(`环境检查:${resultLabel}(通过 ${passed},失败 ${failures.length}${warnings ? `,提醒 ${warnings}` : ''}${skipped ? `,未检查 ${skipped}` : ''})`);
307
+ const statusLabels = { passed: '通过', failed: '未通过', warning: '提醒', skipped: '未检查' };
308
+ for (const check of report.checks) {
309
+ console.log(`[${statusLabels[check.status] || check.status}] ${check.id}:${check.summary}`);
310
+ if (check.status !== 'passed' && check.remediation) {
311
+ console.log(` 修复:${check.remediation}`);
312
+ }
313
+ }
314
+ console.log(`详细报告:${path.join(options.outputDir, 'environment.md')}`);
315
+ return report.result === 'failed' ? 1 : report.result === 'incomplete' ? 2 : 0;
316
+ }
317
+
318
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
319
+ try { process.exitCode = await main(); } catch (error) { console.error(`环境检查失败:${error.message}`); process.exitCode = 1; }
320
+ }