@harmonyos-arkts/d2h 0.1.2 → 0.1.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/README.md +78 -44
- package/agents/android-to-hmos-00-orchestrator.md +34 -28
- package/agents/d2h.md +266 -95
- package/dist/bin/install-opencode.js +137 -43
- package/dist/src/install-opencode.js +44 -0
- package/dist/src/install-vscode.js +226 -0
- package/dist/src/toolchain.js +83 -0
- package/docs/environment-check.md +86 -0
- package/opencode/agents.json +1 -1
- package/opencode/vscode-d2h-package.json +7 -0
- package/opencode/vscode-d2h-plugin.mjs +29 -0
- package/package.json +6 -4
- package/schemas/task-envelope.schema.json +58 -4
- package/schemas/task-plan.schema.json +281 -63
- package/scripts/check-environment.mjs +321 -0
- package/skills/android2hmos_resources_convert/SKILL.md +37 -19
- package/skills/appgraph-rule-audit/SKILL.md +24 -5
- package/skills/d2h-hypium-forge/SKILL.md +252 -0
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -0
- package/skills/d2h-hypium-forge/references/official-api.md +135 -0
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -0
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -0
- package/skills/d2h-hypium-forge/scripts/_common_template.py +614 -0
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +156 -0
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -0
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -0
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -0
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -0
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -0
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -0
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -0
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -0
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -0
- package/skills/d2h-hypium-forge/scripts/run_direct.py +392 -0
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -0
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -0
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -0
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -0
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -0
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -0
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -0
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -0
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -0
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -0
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -0
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -0
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -0
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -0
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -0
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -0
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -0
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -0
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -0
- package/skills/d2h-utg-static-generate/SKILL.md +73 -0
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -0
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -0
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -0
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -0
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -0
- package/skills/harmonyos-migration-playbook/SKILL.md +30 -3
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +4 -4
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +4 -3
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +2 -2
- package/skills/maintaining-migration-report/SKILL.md +88 -99
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -0
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -0
- package/skills/managing-d2h-toolchain/SKILL.md +23 -0
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -0
- package/skills/managing-d2h-toolchain/toolchain.json +16 -0
- package/skills/preparing-migration-workspace/SKILL.md +76 -69
- package/skills/preparing-migration-workspace/toolchain.json +0 -43
|
@@ -0,0 +1,321 @@
|
|
|
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 D2H_VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
11
|
+
const VERSION_TOOLS = ['node', 'python', 'git', 'npm', 'java', 'python-json5', 'appgraph', 'droid2hmos', 'hvigor', 'ohpm', 'devecocli'];
|
|
12
|
+
|
|
13
|
+
function versionParts(value) {
|
|
14
|
+
if (typeof value !== 'string' || !/^\d+(?:\.\d+){0,3}$/.test(value)) throw new Error(`无效版本:${value}`);
|
|
15
|
+
const parts = value.split('.').map(Number);
|
|
16
|
+
if (parts.some(part => !Number.isSafeInteger(part))) throw new Error(`无效版本:${value}`);
|
|
17
|
+
return [...parts, ...Array(4 - parts.length).fill(0)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compareVersions(left, right) {
|
|
21
|
+
const a = versionParts(left);
|
|
22
|
+
const b = versionParts(right);
|
|
23
|
+
for (let index = 0; index < a.length; index++) {
|
|
24
|
+
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadRequirements(file = DEFAULT_REQUIREMENTS) {
|
|
30
|
+
const config = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
31
|
+
if (config?.schemaVersion !== 1 || !config.tools || typeof config.tools !== 'object' || Array.isArray(config.tools)) {
|
|
32
|
+
throw new Error('版本配置必须包含 schemaVersion: 1 和 tools 对象');
|
|
33
|
+
}
|
|
34
|
+
for (const key of Object.keys(config)) {
|
|
35
|
+
if (!['schemaVersion', 'tools'].includes(key)) throw new Error(`未知配置字段:${key}`);
|
|
36
|
+
}
|
|
37
|
+
for (const key of Object.keys(config.tools)) {
|
|
38
|
+
if (!VERSION_TOOLS.includes(key)) throw new Error(`未知工具:${key}`);
|
|
39
|
+
}
|
|
40
|
+
for (const key of VERSION_TOOLS) {
|
|
41
|
+
if (!Object.hasOwn(config.tools, key)) throw new Error(`版本配置缺少工具:${key};不约束版本请设为 null`);
|
|
42
|
+
const rule = config.tools[key];
|
|
43
|
+
if (rule === null) continue;
|
|
44
|
+
if (typeof rule !== 'object' || Array.isArray(rule) || !Object.keys(rule).length) throw new Error(`无效工具约束:${key}`);
|
|
45
|
+
for (const bound of Object.keys(rule)) {
|
|
46
|
+
if (!['minimum', 'maximumExclusive'].includes(bound)) throw new Error(`未知版本边界:${key}.${bound}`);
|
|
47
|
+
versionParts(rule[bound]);
|
|
48
|
+
}
|
|
49
|
+
if (rule.minimum && rule.maximumExclusive && compareVersions(rule.minimum, rule.maximumExclusive) >= 0) {
|
|
50
|
+
throw new Error(`版本区间为空:${key}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function constraintLabel(rule) {
|
|
57
|
+
return rule ? [rule.minimum && `>=${rule.minimum}`, rule.maximumExclusive && `<${rule.maximumExclusive}`].filter(Boolean).join(' ') : '仅检查可用性';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function meetsVersionRule(output, rule, id = '') {
|
|
61
|
+
if (!rule) return true;
|
|
62
|
+
const match = output.match(/\d+\.\d+(?:\.\d+){0,2}/);
|
|
63
|
+
if (!match) return false;
|
|
64
|
+
let version = match[0];
|
|
65
|
+
if (id === 'java' && version.startsWith('1.')) version = version.slice(2);
|
|
66
|
+
return (!rule.minimum || compareVersions(version, rule.minimum) >= 0) &&
|
|
67
|
+
(!rule.maximumExclusive || compareVersions(version, rule.maximumExclusive) < 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveExecutable(command, env = process.env, platform = process.platform) {
|
|
71
|
+
// npm ships Unix shims beside .cmd files; Windows cannot spawn the shims.
|
|
72
|
+
const extensions = platform === 'win32' && !path.extname(command)
|
|
73
|
+
? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';') : [''];
|
|
74
|
+
const directories = /[/\\]/.test(command) ? [''] : (env.PATH || '').split(platform === 'win32' ? ';' : ':');
|
|
75
|
+
for (const directory of directories) {
|
|
76
|
+
for (const extension of extensions) {
|
|
77
|
+
const candidate = directory ? path.join(directory, command + extension) : command + extension;
|
|
78
|
+
try {
|
|
79
|
+
if (!fs.statSync(candidate).isFile()) continue;
|
|
80
|
+
if (platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK);
|
|
81
|
+
return path.resolve(candidate);
|
|
82
|
+
} catch { /* Try the next candidate. */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return command;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function invocation(executable, args, platform = process.platform, env = process.env) {
|
|
89
|
+
const resolved = resolveExecutable(executable, env, platform);
|
|
90
|
+
if (/\.m?js$/i.test(resolved)) return [process.execPath, resolved, ...args];
|
|
91
|
+
if (platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
|
|
92
|
+
const quote = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
93
|
+
const source = `& ${[resolved, ...args].map(quote).join(' ')}; exit $LASTEXITCODE`;
|
|
94
|
+
return ['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')];
|
|
95
|
+
}
|
|
96
|
+
return [resolved, ...args];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function probe(executable, args, timeout = 15, options = {}) {
|
|
100
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'd2h-env-'));
|
|
101
|
+
const log = fs.openSync(path.join(directory, 'output.log'), 'w+');
|
|
102
|
+
const errorLog = fs.openSync(path.join(directory, 'error.log'), 'w+');
|
|
103
|
+
let exitCode = null;
|
|
104
|
+
let error = null;
|
|
105
|
+
try {
|
|
106
|
+
const [command, ...commandArgs] = invocation(executable, args);
|
|
107
|
+
await new Promise((resolve) => {
|
|
108
|
+
// Files and the exit event avoid waiting on inherited output handles.
|
|
109
|
+
const child = spawn(command, commandArgs, {
|
|
110
|
+
cwd: options.cwd || directory, windowsHide: true, detached: process.platform !== 'win32',
|
|
111
|
+
env: { ...process.env, DEVECO_CLI_SKIP_VERSION_CHECK: '1' }, stdio: ['ignore', log, errorLog],
|
|
112
|
+
});
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
error = 'timeout';
|
|
115
|
+
if (child.pid) {
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
118
|
+
} else {
|
|
119
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { /* Already exited. */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
resolve();
|
|
124
|
+
}, timeout * 1000);
|
|
125
|
+
child.once('error', (err) => { error ??= err.message; clearTimeout(timer); resolve(); });
|
|
126
|
+
child.once('exit', (code) => { exitCode = code; clearTimeout(timer); resolve(); });
|
|
127
|
+
});
|
|
128
|
+
const read = (descriptor) => {
|
|
129
|
+
const buffer = Buffer.alloc(65536);
|
|
130
|
+
const length = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
131
|
+
return buffer.subarray(0, length).toString('utf8').trim();
|
|
132
|
+
};
|
|
133
|
+
const stdout = read(log);
|
|
134
|
+
const stderr = read(errorLog);
|
|
135
|
+
return { command: [executable, ...args], exitCode, ok: exitCode === 0 && !error, error, stdout, stderr, output: [stdout, stderr].filter(Boolean).join('\n') };
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return { command: [executable, ...args], exitCode, ok: false, error: err.message, output: '' };
|
|
138
|
+
} finally {
|
|
139
|
+
fs.closeSync(log);
|
|
140
|
+
fs.closeSync(errorLog);
|
|
141
|
+
try { fs.rmSync(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); }
|
|
142
|
+
catch { /* Windows scanners may retain a handle briefly; do not hide the probe result. */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isDirectory(value) {
|
|
147
|
+
try { return Boolean(value && fs.statSync(value).isDirectory()); } catch { return false; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isFile(value) {
|
|
151
|
+
try { return Boolean(value && fs.statSync(value).isFile()); } catch { return false; }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function firstDirectory(values) {
|
|
155
|
+
const found = values.find(isDirectory);
|
|
156
|
+
return found ? path.resolve(found) : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function pythonJson5Required(source) {
|
|
160
|
+
function visit(directory) {
|
|
161
|
+
if (!isDirectory(directory)) return false;
|
|
162
|
+
return fs.readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
163
|
+
if (['__pycache__', 'node_modules', '.git', 'tests'].includes(entry.name)) return false;
|
|
164
|
+
const file = path.join(directory, entry.name);
|
|
165
|
+
return entry.isDirectory() ? visit(file) : entry.name.endsWith('.py') &&
|
|
166
|
+
/^\s*(?:import\s+json5\b|from\s+json5\b)/m.test(fs.readFileSync(file, 'utf8'));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return visit(path.join(source, 'skills'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function parseDevEcoJson(output) {
|
|
173
|
+
// DevEco CLI 1.3.3 appends its updater notice to stdout even with --format json.
|
|
174
|
+
// Strip only that known trailing line; unrelated malformed output still fails.
|
|
175
|
+
return JSON.parse(output.replace(/\r?\n\s*New version [\w.+-]+ available, run `devecocli update` to upgrade\s*$/, '').trim());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function collect(options, { runner = probe, env = process.env, platform = process.platform, home = os.homedir(), directoryFinder = firstDirectory, nodeVersion = process.version } = {}) {
|
|
179
|
+
const requirementsFile = path.resolve(options.requirementsFile || DEFAULT_REQUIREMENTS);
|
|
180
|
+
const requirements = loadRequirements(requirementsFile);
|
|
181
|
+
const ruleFor = id => requirements.tools[id];
|
|
182
|
+
const checks = [];
|
|
183
|
+
const add = (id, required, passed, summary, remediation = '', evidence = [], skipped = false) => {
|
|
184
|
+
checks.push({ id, required, status: skipped ? 'skipped' : passed ? 'passed' : required ? 'failed' : 'warning', summary,
|
|
185
|
+
remediation: passed ? '' : remediation, evidence });
|
|
186
|
+
};
|
|
187
|
+
const tool = async (id, executable, params, remediation, predicate = () => true) => {
|
|
188
|
+
const result = await runner(executable, params, options.timeout);
|
|
189
|
+
const rule = ruleFor(id);
|
|
190
|
+
const passed = result.ok && predicate(result.output) && meetsVersionRule(result.output, rule, id);
|
|
191
|
+
const summary = result.output.split(/\r?\n/)[0] || result.error || '命令未成功';
|
|
192
|
+
add(id, true, passed, `${summary}${rule ? `(要求 ${constraintLabel(rule)})` : ''}`,
|
|
193
|
+
`${remediation}${rule ? ` 版本须满足 ${constraintLabel(rule)}。` : ''}`, [result]);
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
add('node', true, meetsVersionRule(nodeVersion, ruleFor('node')), `${nodeVersion} (${process.execPath})(要求 ${constraintLabel(ruleFor('node'))})`,
|
|
197
|
+
`请安装满足 ${constraintLabel(ruleFor('node'))} 的 Node.js,然后重新运行脚本。`);
|
|
198
|
+
await tool('git', 'git', ['--version'], '安装 Git 并将 git 加入 PATH。');
|
|
199
|
+
await tool('npm', 'npm', ['--version'], '使用 Node.js 自带的 npm,检查 PATH。');
|
|
200
|
+
await tool('java', 'java', ['-version'], '安装项目 Gradle/DevEco 所需 JDK,设置 JAVA_HOME 和 PATH;本检查不限定 JDK 主版本。');
|
|
201
|
+
// Python is a migration dependency, no longer a prerequisite to run this doctor.
|
|
202
|
+
const candidates = [...new Set([env.PYTHON_BINARY, platform === 'win32' ? 'python' : 'python3', 'python',
|
|
203
|
+
platform === 'darwin' ? '/opt/homebrew/bin/python3' : null, platform === 'darwin' ? '/usr/local/bin/python3' : null,
|
|
204
|
+
platform === 'darwin' ? '/opt/homebrew/anaconda3/bin/python3' : null].filter(Boolean))];
|
|
205
|
+
const pythonProbes = [];
|
|
206
|
+
let python = null;
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
const result = await runner(candidate, ['--version'], options.timeout);
|
|
209
|
+
pythonProbes.push(result);
|
|
210
|
+
if (result.ok && meetsVersionRule(result.output, ruleFor('python'))) { python = candidate; break; }
|
|
211
|
+
}
|
|
212
|
+
add('python', true, Boolean(python), `${python ? `${pythonProbes.at(-1).output} (${python})` : '未找到符合要求的 Python'}(要求 ${constraintLabel(ruleFor('python'))})`,
|
|
213
|
+
`安装满足 ${constraintLabel(ruleFor('python'))} 的 Python 并加入 PATH,或设置 PYTHON_BINARY 为解释器路径。`, pythonProbes);
|
|
214
|
+
if (pythonJson5Required(path.resolve(options.pluginRoot))) {
|
|
215
|
+
if (python) {
|
|
216
|
+
await tool('python-json5', python, ['-c', 'import json5; from importlib.metadata import version; print("json5 " + version("json5"))'],
|
|
217
|
+
`用检测到的 Python 安装:${platform === 'win32' ? '& ' : ''}'${python.replaceAll("'", platform === 'win32' ? "''" : "'\\''")}' -m pip install json5`);
|
|
218
|
+
} else {
|
|
219
|
+
add('python-json5', true, false, 'Python 不可用,尚未检查 json5', '安装 Python 后重新检查;使用同一解释器执行 -m pip install json5。', [], true);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const name of ['appgraph', 'droid2hmos']) {
|
|
223
|
+
await tool(name, name, ['--version'], `运行 npx --yes @harmonyos-arkts/d2h@${D2H_VERSION} install 安装兼容工具组合;本地 npm link 需由开发者显式管理。`);
|
|
224
|
+
}
|
|
225
|
+
const deveco = directoryFinder([env.DEVECO_STUDIO_HOME, env.DEVECO_HOME,
|
|
226
|
+
platform === 'darwin' ? '/Applications/DevEco-Studio.app/Contents' : null,
|
|
227
|
+
...(platform === 'win32' ? ['C:/Program Files/Huawei/DevEco Studio', 'D:/Program Files/Huawei/DevEco Studio'] : [])]);
|
|
228
|
+
add('deveco', true, Boolean(deveco), deveco || '未找到 DevEco Studio', '安装 DevEco Studio,设置 DEVECO_STUDIO_HOME 为安装根目录(macOS 为 Contents)。');
|
|
229
|
+
for (const [id, variable, relative, fallback] of [
|
|
230
|
+
['hvigor', 'DROID2HMOS_HVIGOR', 'tools/hvigor/bin/hvigorw.js', 'hvigorw'],
|
|
231
|
+
['ohpm', 'DROID2HMOS_OHPM', 'tools/ohpm/bin/pm-cli.js', 'ohpm'],
|
|
232
|
+
]) {
|
|
233
|
+
const bundled = deveco && path.join(deveco, relative);
|
|
234
|
+
await tool(id, env[variable] || (isFile(bundled) ? bundled : fallback), ['--version'], `安装 DevEco 配套工具;设置 ${variable} 为可执行文件或 JS 入口路径。`);
|
|
235
|
+
}
|
|
236
|
+
const sdk = directoryFinder([options.sdkPath, env.HARMONYOS_SDK_HOME, env.OHOS_SDK_HOME, env.DEVECO_SDK_HOME, deveco && path.join(deveco, 'sdk')]);
|
|
237
|
+
add('harmony-sdk-location', false, Boolean(sdk), sdk || '未发现声明 SDK 根目录', '设置 HARMONYOS_SDK_HOME 或 --sdk-path;实际声明有效性由 droid2hmos preflight 检查。');
|
|
238
|
+
const cli = env.DEVECO_CLI_BINARY || 'devecocli';
|
|
239
|
+
await tool('devecocli', cli, ['--version'], '安装 npm install -g @deveco/deveco-cli。');
|
|
240
|
+
const auth = await runner(cli, ['auth', 'status'], options.timeout);
|
|
241
|
+
const loggedIn = auth.ok && /Current user:\s*[^\r\n]+/i.test(auth.output) && !/Not logged in/i.test(auth.output);
|
|
242
|
+
add('devecocli-auth', true, loggedIn, loggedIn ? '已登录' : '未登录或无法确认登录状态',
|
|
243
|
+
'运行 droid2hmos auth login <android-project>,在浏览器完成授权;不要并发登录。',
|
|
244
|
+
[{ command: auth.command, exitCode: auth.exitCode, error: auth.error }]);
|
|
245
|
+
if (options.harmonyProject) {
|
|
246
|
+
const project = path.resolve(options.harmonyProject);
|
|
247
|
+
const versions = await runner(cli, ['check', 'compat', 'versions', '--format', 'json'], options.timeout, { cwd: project });
|
|
248
|
+
let available = [];
|
|
249
|
+
try { const value = parseDevEcoJson(versions.stdout ?? versions.output); available = Array.isArray(value.versions) ? value.versions.filter((v) => typeof v === 'string') : []; } catch { /* Keep diagnostic. */ }
|
|
250
|
+
add('harmony-sdk-compatibility', false, versions.ok && available.length > 0,
|
|
251
|
+
available.length ? `DevEco CLI 可识别 SDK:${available.join(', ')}` : '无法确认工程可用 SDK 版本',
|
|
252
|
+
'在 DevEco 中安装工程所需 SDK;构建与 API 兼容性仍由项目 preflight 验证。', [versions]);
|
|
253
|
+
}
|
|
254
|
+
if (options.androidProject) {
|
|
255
|
+
const root = path.resolve(options.androidProject);
|
|
256
|
+
add('android-source-root', true, ['settings.gradle', 'settings.gradle.kts', 'build.gradle', 'build.gradle.kts'].some((f) => isFile(path.join(root, f))),
|
|
257
|
+
root, '用 --android-project 指向包含 Gradle 配置标记的 Android 源码根目录;不要求 Wrapper 或 Android 编译环境。');
|
|
258
|
+
}
|
|
259
|
+
const result = checks.some((c) => c.required && c.status === 'failed') ? 'failed'
|
|
260
|
+
: checks.some((c) => c.required && c.status === 'skipped') ? 'incomplete' : 'passed';
|
|
261
|
+
return { schemaVersion: '1.0.0', createdAt: new Date().toISOString(), platform: `${platform} ${os.arch()} ${os.release()}`, result, checks,
|
|
262
|
+
requirementsFile, versionRequirements: requirements.tools,
|
|
263
|
+
scope: '主机依赖检查;不证明构建、签名、SDK API 匹配或端到端验收成功。' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function markdown(report) {
|
|
267
|
+
const escape = (value) => String(value).replaceAll('|', '\\|').replaceAll('\r', '').replaceAll('\n', '<br>');
|
|
268
|
+
return ['# Android → HarmonyOS 环境检查', '', `结果:**${report.result}**`, '', report.scope, '', `版本配置:${report.requirementsFile || ''}`, '',
|
|
269
|
+
'| 检查 | 必需 | 状态 | 说明 |', '| --- | --- | --- | --- |',
|
|
270
|
+
...report.checks.map((c) => `| ${c.id} | ${c.required ? '是' : '否'} | ${c.status} | ${escape(c.summary)} |`),
|
|
271
|
+
'', '## 修复指导', '', ...report.checks.filter((c) => c.remediation).map((c) => `- **${c.id}**:${c.remediation}`), '',
|
|
272
|
+
'修复后重新运行本脚本。主机检查通过后,运行 `droid2hmos preflight <android-project>` 验证真实项目门禁;该命令可能初始化工程并执行构建。', ''].join('\n');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function parseOptions(argv) {
|
|
276
|
+
const { values } = parseArgs({ args: argv, options: {
|
|
277
|
+
'plugin-root': { type: 'string' },
|
|
278
|
+
requirements: { type: 'string' },
|
|
279
|
+
'android-project': { type: 'string' }, 'harmony-project': { type: 'string' }, 'sdk-path': { type: 'string' },
|
|
280
|
+
timeout: { type: 'string', default: '15' }, 'output-dir': { type: 'string' }, help: { type: 'boolean', short: 'h' },
|
|
281
|
+
} });
|
|
282
|
+
if (!/^\d+$/.test(values.timeout) || Number(values.timeout) < 1 || Number(values.timeout) > 60) throw new Error('--timeout 必须为 1–60 的整数');
|
|
283
|
+
return { pluginRoot: values['plugin-root'] || fileURLToPath(new URL('../', import.meta.url)),
|
|
284
|
+
requirementsFile: values.requirements ? path.resolve(values.requirements) : DEFAULT_REQUIREMENTS,
|
|
285
|
+
androidProject: values['android-project'], sdkPath: values['sdk-path'],
|
|
286
|
+
harmonyProject: values['harmony-project'],
|
|
287
|
+
timeout: Number(values.timeout),
|
|
288
|
+
outputDir: path.resolve(values['output-dir'] || 'environment-report'), help: values.help };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
292
|
+
let options;
|
|
293
|
+
try { options = parseOptions(argv); } catch (error) { console.error(error.message); return 2; }
|
|
294
|
+
if (options.help) {
|
|
295
|
+
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。只写报告,不自动安装依赖或修改配置。');
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
const report = await collect(options);
|
|
299
|
+
fs.mkdirSync(options.outputDir, { recursive: true });
|
|
300
|
+
fs.writeFileSync(path.join(options.outputDir, 'environment.json'), JSON.stringify(report, null, 2) + '\n');
|
|
301
|
+
fs.writeFileSync(path.join(options.outputDir, 'environment.md'), markdown(report));
|
|
302
|
+
const passed = report.checks.filter((check) => check.status === 'passed').length;
|
|
303
|
+
const failures = report.checks.filter((check) => check.status === 'failed');
|
|
304
|
+
const warnings = report.checks.filter((check) => check.status === 'warning').length;
|
|
305
|
+
const skipped = report.checks.filter((check) => check.status === 'skipped').length;
|
|
306
|
+
const resultLabel = { passed: '通过', failed: '未通过', incomplete: '未完成' }[report.result];
|
|
307
|
+
console.log(`环境检查:${resultLabel}(通过 ${passed},失败 ${failures.length}${warnings ? `,提醒 ${warnings}` : ''}${skipped ? `,未检查 ${skipped}` : ''})`);
|
|
308
|
+
const statusLabels = { passed: '通过', failed: '未通过', warning: '提醒', skipped: '未检查' };
|
|
309
|
+
for (const check of report.checks) {
|
|
310
|
+
console.log(`[${statusLabels[check.status] || check.status}] ${check.id}:${check.summary}`);
|
|
311
|
+
if (check.status !== 'passed' && check.remediation) {
|
|
312
|
+
console.log(` 修复:${check.remediation}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
console.log(`详细报告:${path.join(options.outputDir, 'environment.md')}`);
|
|
316
|
+
return report.result === 'failed' ? 1 : report.result === 'incomplete' ? 2 : 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
320
|
+
try { process.exitCode = await main(); } catch (error) { console.error(`环境检查失败:${error.message}`); process.exitCode = 1; }
|
|
321
|
+
}
|
|
@@ -27,17 +27,40 @@ values 资源(strings/colors/dimens 的迁移)、布局、Lottie、字体明
|
|
|
27
27
|
| Android XML Drawable(vector/shape/layer-list/selector)转 SVG | `references/xml-drawable-to-svg-rules.md` |
|
|
28
28
|
| 修复 SVG 显示问题(空白/不可见/尺寸错误/残留 Android 语法) | `references/svg-fix-patterns.md` |
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## 输入与统一领域结果
|
|
31
31
|
|
|
32
32
|
1. **Android 工程路径** — Android Gradle 工程根目录(含 `build.gradle` 或 `build.gradle.kts`)。
|
|
33
|
-
2. **HarmonyOS 工程路径** — 由 Preflight 创建或复用的工程(见 `preparing-migration-workspace`),通常是迁移工作区下的 `ohos/`,或 `.migration/config.json` 中 `--harmony-project` 配置的路径。图片写入 `<harmony>/entry/src/main/resources/`。本 skill 不创建工程壳;`ohos/`
|
|
33
|
+
2. **HarmonyOS 工程路径** — 由 Preflight 创建或复用的工程(见 `preparing-migration-workspace`),通常是迁移工作区下的 `ohos/`,或 `.migration/config.json` 中 `--harmony-project` 配置的路径。图片写入 `<harmony>/entry/src/main/resources/`。本 skill 不创建工程壳;`ohos/` 尚不存在时由调用方先完成 Preflight。
|
|
34
|
+
|
|
35
|
+
始终返回统一领域结果:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"status": "completed | partial | failed",
|
|
40
|
+
"changedFiles": [],
|
|
41
|
+
"sourceFiles": [],
|
|
42
|
+
"evidence": [],
|
|
43
|
+
"domainChecks": [],
|
|
44
|
+
"unresolved": [],
|
|
45
|
+
"verificationRequests": [],
|
|
46
|
+
"suggestedRemediation": [],
|
|
47
|
+
"details": {
|
|
48
|
+
"convertedResources": [],
|
|
49
|
+
"qualifierMappings": [],
|
|
50
|
+
"unmappedResources": [],
|
|
51
|
+
"unresolvedReferences": []
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`status` 只表示图片资源转换和静态完整性检查的领域状态,不代表工程已构建、设备显示已验证或迁移已完成。`changedFiles`、`sourceFiles`、`unmappedResources` 和 `unresolvedReferences` 必须列出真实路径或资源标识。构建、设备显示和交互影响核对写入 `verificationRequests`,由调用方选择执行方式。
|
|
34
57
|
|
|
35
58
|
## 工作流
|
|
36
59
|
|
|
37
60
|
### 第 1 步:校验输入
|
|
38
61
|
|
|
39
62
|
1. 确认 Android 工程路径存在且含 `build.gradle` 或 `build.gradle.kts`。
|
|
40
|
-
2. 通过 `droid2hmos status <ANDROID_PROJECT>` 或 `.migration/config.json` 确认 HarmonyOS 工程位置,并确认 `entry/src/main/resources/`
|
|
63
|
+
2. 通过 `droid2hmos status <ANDROID_PROJECT>` 或 `.migration/config.json` 确认 HarmonyOS 工程位置,并确认 `entry/src/main/resources/` 存在;不存在时返回 `failed`,把缺少工程壳写入 `unresolved` 和 `suggestedRemediation`,不要猜测工程结构或自行调度 Preflight。
|
|
41
64
|
|
|
42
65
|
### 第 2 步:确定资源来源
|
|
43
66
|
|
|
@@ -47,7 +70,7 @@ values 资源(strings/colors/dimens 的迁移)、布局、Lottie、字体明
|
|
|
47
70
|
- 多模块工程同时检查其它模块目录
|
|
48
71
|
- 使用 flavor source set 时一并纳入(如 `app/src/debug/res/`、`app/src/flavor/res/`)
|
|
49
72
|
|
|
50
|
-
恒定从源码 `res/` 转换:仅由库依赖(AAR)提供的图片不在源码 `res/`
|
|
73
|
+
恒定从源码 `res/` 转换:仅由库依赖(AAR)提供的图片不在源码 `res/` 中,这类引用在统一结果中记为 unresolved,不尝试回收。
|
|
51
74
|
|
|
52
75
|
### 第 3 步:清点与分类
|
|
53
76
|
|
|
@@ -60,7 +83,7 @@ values 资源(strings/colors/dimens 的迁移)、布局、Lottie、字体明
|
|
|
60
83
|
| XML drawable:vector/shape/layer-list | `.xml`,根 `<vector>` / `<shape>` / `<layer-list>` | 转 SVG(第 4.2 步) |
|
|
61
84
|
| XML drawable:selector | `.xml`,根 `<selector>` | 提取默认状态 → SVG(第 4.2 步) |
|
|
62
85
|
| 自适应图标 | `.xml`,根 `<adaptive-icon>` | 转 layered-image JSON(第 4.3 步) |
|
|
63
|
-
| ripple/animated-vector/animated-selector/transition/level-list 等 | `.xml`,对应根元素 | 不可映射 —
|
|
86
|
+
| ripple/animated-vector/animated-selector/transition/level-list 等 | `.xml`,对应根元素 | 不可映射 — 记入统一结果 |
|
|
64
87
|
|
|
65
88
|
忽略 `values*/`、`layout*/`、`menu/`、`raw/`、`font/`、`xml/`、`anim*/`、`color/` — 超出范围。只有 `values*/colors.xml` 和 `values*/dimens.xml` 被读取,用于解析 drawable 内的 `@color:`/`@dimen:` 引用(第 4.4 步)。
|
|
66
89
|
|
|
@@ -70,7 +93,7 @@ values 资源(strings/colors/dimens 的迁移)、布局、Lottie、字体明
|
|
|
70
93
|
|
|
71
94
|
**4.1 位图与 nine-patch → `media/`**
|
|
72
95
|
- 拷贝到限定符映射后的 `media/` 目录(完整限定符算法见 image-conversion-rules.md)
|
|
73
|
-
- `.9.png` → 重命名为 `<name>_9.png`(HarmonyOS 禁止资源文件名中含 `.`,仅允许最后扩展名分隔符)。nine-patch 拉伸语义丢失 —
|
|
96
|
+
- `.9.png` → 重命名为 `<name>_9.png`(HarmonyOS 禁止资源文件名中含 `.`,仅允许最后扩展名分隔符)。nine-patch 拉伸语义丢失 — 在统一结果中记录
|
|
74
97
|
|
|
75
98
|
**4.2 XML drawable → `media/` 中的 `.svg`**
|
|
76
99
|
- 严格按 `references/xml-drawable-to-svg-rules.md` 执行 — 尤其是强制保留 `clip-path`、三分支 `fill` 规则和渐变处理
|
|
@@ -84,19 +107,19 @@ values 资源(strings/colors/dimens 的迁移)、布局、Lottie、字体明
|
|
|
84
107
|
**4.4 解析 drawable 内引用**
|
|
85
108
|
- SVG 填充/描边中的 `@color/name` → 从 `values*/colors.xml` 解析(沿引用链;兜底 `#000000` + 记录)
|
|
86
109
|
- SVG 尺寸中的 `@dimen/name` → 从 `values*/dimens.xml` 解析(兜底:合理默认值 + 记录)
|
|
87
|
-
- 未解析引用(库专属资源不在源码 `res
|
|
110
|
+
- 未解析引用(库专属资源不在源码 `res/`)记入统一结果,绝不静默丢弃
|
|
88
111
|
|
|
89
112
|
### 第 5 步:完整性校验
|
|
90
113
|
|
|
91
|
-
1. 第 3
|
|
114
|
+
1. 第 3 步发现的每个文件都有且只有一个结局:已转换,或在统一结果中列为 unmappable/unmapped
|
|
92
115
|
2. 每个转换产物都实际存在于 HarmonyOS 工程
|
|
93
116
|
3. 生成的 layered-image JSON 中每个 `$media:xxx` 引用都指向 `media/` 中存在的文件
|
|
94
117
|
4. 每个转换后的 SVG 通过 `references/xml-drawable-to-svg-rules.md` 的自检
|
|
95
118
|
5. HarmonyOS 工程已有 `.ets` 文件时,扫描 `$r('app.media.xxx')` 并核对每个引用的图片存在(已有 media 文件**不覆盖** — 合并,冲突记录)
|
|
96
119
|
|
|
97
|
-
### 第 6
|
|
120
|
+
### 第 6 步:组装领域结果
|
|
98
121
|
|
|
99
|
-
|
|
122
|
+
把转换清单和静态校验结果写入统一领域结果;如需可读审计附件,可生成以下结构并把路径列入 `evidence`:
|
|
100
123
|
|
|
101
124
|
```
|
|
102
125
|
# Android to HarmonyOS Image Conversion Report
|
|
@@ -134,15 +157,9 @@ values 资源(strings/colors/dimens 的迁移)、布局、Lottie、字体明
|
|
|
134
157
|
- SVG self-checks passed: Yes/No
|
|
135
158
|
```
|
|
136
159
|
|
|
137
|
-
影响行为的结论(不可映射资源、丢失的 nine-patch
|
|
138
|
-
|
|
139
|
-
### 第 7 步:Git 检查点
|
|
160
|
+
影响行为的结论(不可映射资源、丢失的 nine-patch 语义、未解析引用)必须进入 `unresolved`,并标明 `blocking`、来源、用户影响和建议补救。需要回流报告 ref 或 discoveries 的项通过结果交给调用方,本 skill 不直接更新迁移报告。
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
```text
|
|
144
|
-
droid2hmos checkpoint create <ANDROID_PROJECT> --message <MESSAGE> --include <harmony>/entry/src/main/resources
|
|
145
|
-
```
|
|
162
|
+
转换完成后,将实际生成或修改的 HarmonyOS 资源写入 `changedFiles`,把对应 Android 源资源写入 `sourceFiles`;不要只返回计数。静态校验写入 `domainChecks`,待构建和设备核对写入 `verificationRequests`。
|
|
146
163
|
|
|
147
164
|
## 重要说明
|
|
148
165
|
|
|
@@ -152,6 +169,7 @@ droid2hmos checkpoint create <ANDROID_PROJECT> --message <MESSAGE> --include <ha
|
|
|
152
169
|
- **转换恒定从源码 `res/` 出发**:仅库依赖(AAR)提供的图片不在其中;对它们的引用按 unresolved 报告,不做恢复。
|
|
153
170
|
- 资源名保持 Android 原拼写 — 它们是标识符。
|
|
154
171
|
- 解析 Android XML 时正确处理命名空间、注释和 CDATA;按 local name 匹配属性。
|
|
172
|
+
- 本 skill 不更新迁移报告、不暂存或提交文件、不创建 checkpoint、不调用 `complete`,也不选择仓库级构建、测试或设备命令。调用方消费统一领域结果并决定外层动作。
|
|
155
173
|
|
|
156
174
|
## 与其他 skill 的关系
|
|
157
175
|
|
|
@@ -159,4 +177,4 @@ droid2hmos checkpoint create <ANDROID_PROJECT> --message <MESSAGE> --include <ha
|
|
|
159
177
|
- **arkts-app-identity**:它处理 App 级身份资源(app.json5 图标字段、版本号、包名);本 skill 负责 res/ 全量图片资产进 `media/`。启动器图标的位图/分层资产由本 skill 转换,app.json5 的接线归它。
|
|
160
178
|
- **arkts-icon-sizing**:图片资产就位后,它修 `.ets` 消费侧丢失的 `.width()/.height()`;本 skill 只管资源侧。
|
|
161
179
|
- **arkts-i18n**:values 资源(string.json 等)的迁移归它;本 skill 只把 colors.xml/dimens.xml 当查找源。
|
|
162
|
-
- **maintaining-migration-report
|
|
180
|
+
- **maintaining-migration-report**:调用方可把本 skill 返回的行为影响项、源码和证据映射到报告分片;本 skill 不直接写报告。
|
|
@@ -7,13 +7,13 @@ description: Review fresh AppGraph and SPEC results after Android analysis, iden
|
|
|
7
7
|
|
|
8
8
|
Review the latest deterministic Android analysis and produce maintainable rule-improvement candidates. This is a read-only diagnostic step. Do not edit AppGraph, SPEC facts, Android source, rule packs, or analyzers.
|
|
9
9
|
|
|
10
|
-
All `.migration/...` paths below live in the sibling migration workspace `<android-project>-migration/.migration/`.
|
|
10
|
+
All `.migration/...` paths below live in the sibling migration workspace `<android-project>-migration/.migration/`. Never fall back to the legacy `<android-project>/.migration/`; if the sibling workspace or required artifacts are absent, return a structured unavailable/stale result.
|
|
11
11
|
|
|
12
12
|
## Preconditions
|
|
13
13
|
|
|
14
|
-
1.
|
|
14
|
+
1. The caller must supply artifacts from a successful `droid2hmos analyze android <android-project>` run; this Skill does not run analysis itself.
|
|
15
15
|
2. Locate `.migration/spec/index.json`, `.migration/spec/facts/`, and `.migration/appgraph/app-graph.json`.
|
|
16
|
-
3. Check that the SPEC `generation.sourceRevision`, when present, matches the Android Git HEAD. Treat a mismatch or missing required artifact as stale analysis: write no recommendations and
|
|
16
|
+
3. Check that the SPEC `generation.sourceRevision`, when present, matches the Android Git HEAD. Treat a mismatch or missing required artifact as stale analysis: write no recommendations and return the structured result below. Do not run analysis or ask the user whether to run it.
|
|
17
17
|
4. Keep all source and generated data local. Network access is unnecessary.
|
|
18
18
|
|
|
19
19
|
## Review workflow
|
|
@@ -34,7 +34,25 @@ All `.migration/...` paths below live in the sibling migration workspace `<andro
|
|
|
34
34
|
|
|
35
35
|
## Outputs
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
When analysis is missing or stale, return without creating or replacing opportunity reports:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"status": "stale",
|
|
42
|
+
"workspace": "<android-project>-migration/.migration/",
|
|
43
|
+
"staleReasons": [],
|
|
44
|
+
"requiredAction": "reanalyze-android",
|
|
45
|
+
"expectedArtifacts": [
|
|
46
|
+
".migration/spec/index.json",
|
|
47
|
+
".migration/spec/facts/",
|
|
48
|
+
".migration/appgraph/app-graph.json"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The caller consumes `requiredAction` and decides the workflow transition. This Skill does not invoke `droid2hmos analyze android`, pause for a user choice, or continue against legacy/stale artifacts.
|
|
54
|
+
|
|
55
|
+
When inputs are fresh, create or replace:
|
|
38
56
|
|
|
39
57
|
```text
|
|
40
58
|
.migration/rule-audit/rule-opportunities.json
|
|
@@ -55,4 +73,5 @@ When no evidence-backed rule improvement exists, write valid outputs with an emp
|
|
|
55
73
|
- Do not turn host relationships into navigation edges.
|
|
56
74
|
- Do not mark multiple targets as resolved.
|
|
57
75
|
- Do not implement recommendations. A maintainer may later pass an accepted opportunity to the separate `appgraph-navigation-rules` Skill.
|
|
58
|
-
-
|
|
76
|
+
- With fresh inputs, an audit execution failure is advisory: record the limitation and return it to the caller. Missing or stale analysis is not an audit failure; return `requiredAction: reanalyze-android` as defined above.
|
|
77
|
+
- Do not advance an outer state, create a checkpoint, update the migration report, or invoke completion. Return findings or the stale result to the caller.
|