@harmonyos-arkts/d2h 0.1.3 → 0.1.6
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 +87 -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/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/references/ui-automation.md +4 -3
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +1 -1
- 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 +9 -7
- package/agents/d2h_v2.md +0 -351
- package/skills/preparing-migration-workspace/toolchain.json +0 -19
|
@@ -2,57 +2,151 @@
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { installOpenCode } from '../src/install-opencode.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
--
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
import { inspectInstallation, installOpenCode } from '../src/install-opencode.js';
|
|
6
|
+
import { inspectToolchain, installToolchain, loadToolchain } from '../src/toolchain.js';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { inspectVSCodeInstallation, installVSCode } from '../src/install-vscode.js';
|
|
9
|
+
const help = `Install this package's skills and agents into OpenCode or DevEco Code.
|
|
10
|
+
|
|
11
|
+
Usage: d2h [options]
|
|
12
|
+
|
|
13
|
+
d2h install Install compatible CLIs and agent/skill definitions
|
|
14
|
+
d2h upgrade Reapply this package's tested toolchain and definitions
|
|
15
|
+
d2h upgrade --check Preview version and installation status only
|
|
16
|
+
d2h doctor [--agent opencode|deveco|vscode] [--config-dir <path>]
|
|
17
|
+
Check definitions, host environment and CLI capabilities
|
|
18
|
+
|
|
19
|
+
--check-env Check host tools and write reports without installing skills
|
|
20
|
+
Example: d2h --check-env --output-dir ./environment-report
|
|
21
|
+
Use d2h --check-env --help for check options
|
|
22
|
+
|
|
23
|
+
--source-dir <path> Source root (defaults to this package)
|
|
24
|
+
--agent <name> opencode (default), deveco or vscode
|
|
25
|
+
--extension-dir <path> VS Code HarmonyOS Dev Assistant extension root
|
|
26
|
+
--config-dir <path> Override the selected agent's config root
|
|
27
|
+
OPENCODE_CONFIG_DIR / DEVECO_CONFIG_DIR, or
|
|
28
|
+
$XDG_CONFIG_HOME/<agent>, or ~/.config/<agent>
|
|
29
|
+
--link Create absolute symbolic links (development only)
|
|
30
|
+
--force Replace conflicting definitions and managed CLI npm links
|
|
31
|
+
--dry-run Print operations without writing files
|
|
32
|
+
--skip-tools Install definitions only (local development)
|
|
33
|
+
-h, --help Show this help
|
|
34
|
+
|
|
35
|
+
Managed AppGraph and droid2hmos CLIs plus skills/*/SKILL.md trees and agents/*.md are installed.
|
|
36
|
+
Existing application configuration and AGENTS.md are not modified.
|
|
37
|
+
With npx, prefer the default copy mode: its cache can be removed.
|
|
23
38
|
`;
|
|
24
39
|
try {
|
|
25
|
-
const options = {
|
|
26
|
-
sourceDir: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'),
|
|
27
|
-
agent: 'opencode',
|
|
28
|
-
};
|
|
29
40
|
const args = process.argv.slice(2);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
42
|
+
if (args[0] === 'doctor') {
|
|
43
|
+
let agent = 'opencode';
|
|
44
|
+
let configDir;
|
|
45
|
+
let extensionDir;
|
|
46
|
+
for (let index = 1; index < args.length; index++) {
|
|
47
|
+
const arg = args[index];
|
|
48
|
+
if (arg === '--agent' || arg === '--config-dir' || arg === '--extension-dir') {
|
|
49
|
+
const value = args[++index];
|
|
50
|
+
if (!value || value.startsWith('--'))
|
|
51
|
+
throw new Error(`${arg} requires a value`);
|
|
52
|
+
if (arg === '--agent')
|
|
53
|
+
agent = value;
|
|
54
|
+
else if (arg === '--config-dir')
|
|
55
|
+
configDir = value;
|
|
56
|
+
else
|
|
57
|
+
extensionDir = value;
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
throw new Error(`unknown doctor option: ${arg}`);
|
|
35
61
|
}
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
if (!['opencode', 'deveco', 'vscode'].includes(agent))
|
|
63
|
+
throw new Error(`unsupported agent: ${agent}`);
|
|
64
|
+
configDir ??= process.env[agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), agent);
|
|
65
|
+
const installation = agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : inspectInstallation(configDir);
|
|
66
|
+
const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
|
|
67
|
+
const definitionsMatch = installation.status === 'passed' && installation.packageVersion === packageVersion &&
|
|
68
|
+
(agent === 'vscode' || ('agent' in installation && installation.agent === agent));
|
|
69
|
+
console.log(`definitions ${definitionsMatch ? 'passed' : 'failed'}: ${installation.summary}${definitionsMatch ? '' : `; reinstall ${agent} definitions from D2H ${packageVersion}`}`);
|
|
70
|
+
const results = inspectToolchain(packageRoot);
|
|
71
|
+
for (const item of results)
|
|
72
|
+
console.log(`${item.status}: ${item.package} expected ${item.expectedVersion}, installed ${item.installedVersion ?? 'missing'}; capabilities ${JSON.stringify(item.capabilities)}`);
|
|
73
|
+
const checkerUrl = new URL('../../scripts/check-environment.mjs', import.meta.url);
|
|
74
|
+
const { main } = await import(checkerUrl.href);
|
|
75
|
+
const environmentExit = await main([]);
|
|
76
|
+
if (environmentExit !== 0 || !definitionsMatch || results.some(item => item.status !== 'passed'))
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
else if (args.includes('--check-env')) {
|
|
80
|
+
if (args.filter(arg => arg === '--check-env').length !== 1)
|
|
81
|
+
throw new Error('--check-env may only be specified once');
|
|
82
|
+
const checkerUrl = new URL('../../scripts/check-environment.mjs', import.meta.url);
|
|
83
|
+
const { main } = await import(checkerUrl.href);
|
|
84
|
+
process.exitCode = await main(args.filter(arg => arg !== '--check-env'));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const explicitSource = args.includes('--source-dir');
|
|
88
|
+
const upgrade = args[0] === 'upgrade';
|
|
89
|
+
const installArgs = ['install', 'upgrade'].includes(args[0]) ? args.slice(1) : args;
|
|
90
|
+
const checkOnly = upgrade && installArgs.includes('--check');
|
|
91
|
+
const skipTools = installArgs.includes('--skip-tools') || explicitSource;
|
|
92
|
+
let extensionDir;
|
|
93
|
+
const options = {
|
|
94
|
+
sourceDir: packageRoot,
|
|
95
|
+
agent: 'opencode',
|
|
96
|
+
};
|
|
97
|
+
for (let index = 0; index < installArgs.length; index++) {
|
|
98
|
+
const arg = installArgs[index];
|
|
99
|
+
if (arg === '-h' || arg === '--help') {
|
|
100
|
+
console.log(help);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
if (arg === '--source-dir' || arg === '--config-dir' || arg === '--agent' || arg === '--extension-dir') {
|
|
104
|
+
const value = installArgs[++index];
|
|
105
|
+
if (!value || value.startsWith('--'))
|
|
106
|
+
throw new Error(`${arg} requires a value`);
|
|
107
|
+
if (arg === '--extension-dir')
|
|
108
|
+
extensionDir = value;
|
|
109
|
+
else {
|
|
110
|
+
const key = { '--source-dir': 'sourceDir', '--config-dir': 'configDir', '--agent': 'agent' }[arg];
|
|
111
|
+
options[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (arg === '--link' || arg === '--force' || arg === '--dry-run') {
|
|
115
|
+
const key = { '--link': 'link', '--force': 'force', '--dry-run': 'dryRun' }[arg];
|
|
116
|
+
options[key] = true;
|
|
117
|
+
}
|
|
118
|
+
else if (arg === '--skip-tools' || (upgrade && arg === '--check')) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new Error(`unknown option: ${arg}`);
|
|
123
|
+
}
|
|
42
124
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
options[key] = true;
|
|
125
|
+
if (!options.agent || !['opencode', 'deveco', 'vscode'].includes(options.agent)) {
|
|
126
|
+
throw new Error(`unsupported agent: ${options.agent}; choose opencode, deveco or vscode`);
|
|
46
127
|
}
|
|
47
|
-
|
|
48
|
-
throw new Error(
|
|
128
|
+
if (options.agent !== 'vscode' && extensionDir)
|
|
129
|
+
throw new Error('--extension-dir requires --agent vscode');
|
|
130
|
+
if (options.agent === 'vscode' && options.configDir)
|
|
131
|
+
throw new Error('--config-dir is not used for VS Code; use --extension-dir');
|
|
132
|
+
options.configDir ??= process.env[options.agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), options.agent);
|
|
133
|
+
if (checkOnly) {
|
|
134
|
+
const installation = options.agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : inspectInstallation(options.configDir);
|
|
135
|
+
console.log(`definitions ${installation.status}: ${installation.summary}`);
|
|
136
|
+
for (const item of inspectToolchain(packageRoot))
|
|
137
|
+
console.log(`${item.status}: ${item.package} expected ${item.expectedVersion}, installed ${item.installedVersion ?? 'missing'}`);
|
|
138
|
+
}
|
|
139
|
+
else if (!skipTools)
|
|
140
|
+
installToolchain(packageRoot, { dryRun: options.dryRun, force: options.force });
|
|
141
|
+
else if (!explicitSource && !options.dryRun)
|
|
142
|
+
console.log(`Skipped managed CLI installation (${Object.keys(loadToolchain(packageRoot).tools).join(', ')}).`);
|
|
143
|
+
if (!checkOnly) {
|
|
144
|
+
if (options.agent === 'vscode')
|
|
145
|
+
installVSCode({ sourceDir: options.sourceDir, extensionDir, dryRun: options.dryRun });
|
|
146
|
+
else
|
|
147
|
+
installOpenCode(options);
|
|
49
148
|
}
|
|
50
149
|
}
|
|
51
|
-
if (!options.agent || !['opencode', 'deveco'].includes(options.agent)) {
|
|
52
|
-
throw new Error(`unsupported agent: ${options.agent}; choose opencode or deveco`);
|
|
53
|
-
}
|
|
54
|
-
options.configDir ??= process.env[options.agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), options.agent);
|
|
55
|
-
installOpenCode(options);
|
|
56
150
|
}
|
|
57
151
|
catch (error) {
|
|
58
152
|
console.error(`install-opencode: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
4
5
|
function stat(file) {
|
|
5
6
|
try {
|
|
6
7
|
return fs.lstatSync(file);
|
|
@@ -28,6 +29,34 @@ function sameTree(source, destination) {
|
|
|
28
29
|
}
|
|
29
30
|
return left.isFile() && right.isFile() && fs.readFileSync(source).equals(fs.readFileSync(destination));
|
|
30
31
|
}
|
|
32
|
+
function digest(file) {
|
|
33
|
+
const info = fs.lstatSync(file);
|
|
34
|
+
const hash = crypto.createHash('sha256');
|
|
35
|
+
if (info.isSymbolicLink())
|
|
36
|
+
return hash.update(`link:${fs.readlinkSync(file)}`).digest('hex');
|
|
37
|
+
if (info.isFile())
|
|
38
|
+
return hash.update(fs.readFileSync(file)).digest('hex');
|
|
39
|
+
for (const name of fs.readdirSync(file).sort())
|
|
40
|
+
hash.update(name).update(digest(path.join(file, name)));
|
|
41
|
+
return hash.digest('hex');
|
|
42
|
+
}
|
|
43
|
+
export function inspectInstallation(configDir) {
|
|
44
|
+
const root = path.resolve(configDir);
|
|
45
|
+
const receipt = path.join(root, '.d2h-installation.json');
|
|
46
|
+
if (!fs.existsSync(receipt))
|
|
47
|
+
return { status: 'missing', summary: `installation record missing: ${receipt}` };
|
|
48
|
+
const value = JSON.parse(fs.readFileSync(receipt, 'utf8'));
|
|
49
|
+
if (value.schemaVersion !== 1 || !Array.isArray(value.entries))
|
|
50
|
+
throw new Error(`invalid installation record: ${receipt}`);
|
|
51
|
+
const changed = value.entries.filter(entry => {
|
|
52
|
+
if (!['skills', 'agents'].includes(entry.kind) || !/^[a-zA-Z0-9_.-]+$/.test(entry.name))
|
|
53
|
+
throw new Error(`invalid installation entry: ${entry.name}`);
|
|
54
|
+
const target = path.join(root, entry.kind, entry.name);
|
|
55
|
+
return !stat(target) || digest(target) !== entry.digest;
|
|
56
|
+
}).map(entry => `${entry.kind}/${entry.name}`);
|
|
57
|
+
return { status: changed.length ? 'failed' : 'passed', packageVersion: value.packageVersion, agent: value.agent,
|
|
58
|
+
summary: changed.length ? `changed or missing: ${changed.join(', ')}` : `${value.entries.length} installed definitions verified` };
|
|
59
|
+
}
|
|
31
60
|
function contains(parent, child) {
|
|
32
61
|
const relative = path.relative(parent, child);
|
|
33
62
|
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
@@ -127,6 +156,21 @@ export function installOpenCode({ sourceDir, configDir, agent = 'opencode', link
|
|
|
127
156
|
}
|
|
128
157
|
const skills = entries.filter(entry => entry.kind === 'skills').length;
|
|
129
158
|
const agents = entries.filter(entry => entry.kind === 'agents').length;
|
|
159
|
+
if (!dryRun) {
|
|
160
|
+
const receipt = path.join(configRoot, '.d2h-installation.json');
|
|
161
|
+
const temporary = path.join(configRoot, `.d2h-installation-${process.pid}.tmp`);
|
|
162
|
+
const packageFile = path.join(sourceRoot, 'package.json');
|
|
163
|
+
const packageVersion = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')).version : 'source';
|
|
164
|
+
const record = { schemaVersion: 1, packageVersion, agent, entries: entries.map(entry => ({ kind: entry.kind, name: entry.name, digest: digest(entry.destination) })) };
|
|
165
|
+
try {
|
|
166
|
+
fs.writeFileSync(temporary, JSON.stringify(record, null, 2) + '\n');
|
|
167
|
+
fs.renameSync(temporary, receipt);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (fs.existsSync(temporary))
|
|
171
|
+
fs.rmSync(temporary);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
130
174
|
log(`${application} installation ${dryRun ? 'preview' : 'complete'}: ${skills} skill(s), ${agents} agent(s).`);
|
|
131
175
|
log(`Config directory: ${configRoot}`);
|
|
132
176
|
if (!dryRun)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
const extensionPrefix = 'huaweideveloperserviceplatform.harmonyosdevassistantforvscode-';
|
|
6
|
+
const modeLabel = '安卓转鸿蒙';
|
|
7
|
+
function digest(file) {
|
|
8
|
+
const hash = crypto.createHash('sha256');
|
|
9
|
+
const info = fs.lstatSync(file);
|
|
10
|
+
if (info.isFile())
|
|
11
|
+
return hash.update(fs.readFileSync(file)).digest('hex');
|
|
12
|
+
for (const name of fs.readdirSync(file).sort())
|
|
13
|
+
hash.update(name).update(digest(path.join(file, name)));
|
|
14
|
+
return hash.digest('hex');
|
|
15
|
+
}
|
|
16
|
+
function inside(root, target) {
|
|
17
|
+
const relative = path.relative(root, target);
|
|
18
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
19
|
+
}
|
|
20
|
+
function assertSafeDestination(root, target) {
|
|
21
|
+
if (!inside(root, target))
|
|
22
|
+
throw new Error(`VS Code extension path escaped its root: ${target}`);
|
|
23
|
+
let parent = path.dirname(target);
|
|
24
|
+
while (inside(root, parent)) {
|
|
25
|
+
if (fs.existsSync(parent) && fs.lstatSync(parent).isSymbolicLink())
|
|
26
|
+
throw new Error(`refusing linked extension parent: ${parent}`);
|
|
27
|
+
parent = path.dirname(parent);
|
|
28
|
+
}
|
|
29
|
+
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink())
|
|
30
|
+
throw new Error(`refusing linked extension target: ${target}`);
|
|
31
|
+
}
|
|
32
|
+
export function locateVSCodeExtension(extensionDir, home = os.homedir()) {
|
|
33
|
+
if (extensionDir)
|
|
34
|
+
return path.resolve(extensionDir);
|
|
35
|
+
const extensions = path.join(home, '.vscode', 'extensions');
|
|
36
|
+
if (!fs.existsSync(extensions))
|
|
37
|
+
throw new Error(`VS Code extensions directory not found: ${extensions}`);
|
|
38
|
+
const candidates = fs.readdirSync(extensions).filter(name => name.toLowerCase().startsWith(extensionPrefix))
|
|
39
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
40
|
+
if (!candidates.length)
|
|
41
|
+
throw new Error('HarmonyOS Dev Assistant VS Code extension is not installed');
|
|
42
|
+
return path.join(extensions, candidates[0]);
|
|
43
|
+
}
|
|
44
|
+
function validatedRoot(extensionDir) {
|
|
45
|
+
const root = fs.realpathSync(extensionDir);
|
|
46
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
47
|
+
if (String(metadata.name).toLowerCase() !== 'harmonyosdevassistantforvscode' ||
|
|
48
|
+
String(metadata.publisher).toLowerCase() !== 'huaweideveloperserviceplatform') {
|
|
49
|
+
throw new Error(`not the HarmonyOS Dev Assistant VS Code extension: ${root}`);
|
|
50
|
+
}
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
function activeBundle(root, bundleCacheRoot = path.join(os.homedir(), '.harmonybot', 'opencode-bundle', 'vscode')) {
|
|
54
|
+
const hashFile = path.join(root, 'resources', 'opencode', '.bundle-hash');
|
|
55
|
+
const hash = fs.readFileSync(hashFile, 'utf8').trim();
|
|
56
|
+
if (!/^[a-f0-9]{16}$/i.test(hash))
|
|
57
|
+
throw new Error(`invalid VS Code OpenCode bundle hash: ${hashFile}`);
|
|
58
|
+
const bundle = path.join(bundleCacheRoot, hash);
|
|
59
|
+
if (!fs.existsSync(path.join(bundle, '.bundle-complete')) || !fs.existsSync(path.join(bundle, 'opencode.json'))) {
|
|
60
|
+
throw new Error(`VS Code OpenCode bundle is not extracted: ${bundle}; start the extension once before installing D2H`);
|
|
61
|
+
}
|
|
62
|
+
return fs.realpathSync(bundle);
|
|
63
|
+
}
|
|
64
|
+
export function updateD2HModeLabel(source) {
|
|
65
|
+
const map = /\{"harmonyos-multi-device"[^{}]{0,1000}"hmos-library-adapter-fast"[^{}]{0,300}\}/g;
|
|
66
|
+
const matches = [...source.matchAll(map)];
|
|
67
|
+
if (matches.length !== 1)
|
|
68
|
+
throw new Error(`expected one VS Code mode map, found ${matches.length}`);
|
|
69
|
+
const original = matches[0][0];
|
|
70
|
+
const next = /(?:^|,)d2h:"[^"]*"/.test(original)
|
|
71
|
+
? original.replace(/((?:^|,)d2h:)"[^"]*"/, `$1${JSON.stringify(modeLabel)}`)
|
|
72
|
+
: original.slice(0, -1) + `,d2h:${JSON.stringify(modeLabel)}}`;
|
|
73
|
+
return source.slice(0, matches[0].index) + next + source.slice(matches[0].index + original.length);
|
|
74
|
+
}
|
|
75
|
+
function updateConfig(source) {
|
|
76
|
+
const value = JSON.parse(source);
|
|
77
|
+
value.agent ??= {};
|
|
78
|
+
value.agent.d2h = { ...value.agent.d2h, description: modeLabel,
|
|
79
|
+
tools: { ...value.agent.d2h?.tools, read: true, write: true, edit: true },
|
|
80
|
+
permission: { ...value.agent.d2h?.permission, read: 'allow', edit: 'allow' } };
|
|
81
|
+
delete value.agent.d2h.permission.write;
|
|
82
|
+
value.plugin ??= [];
|
|
83
|
+
if (!Array.isArray(value.plugin))
|
|
84
|
+
throw new Error('opencode plugin list must be an array');
|
|
85
|
+
if (!value.plugin.includes('./plugin/d2h'))
|
|
86
|
+
value.plugin.push('./plugin/d2h');
|
|
87
|
+
return JSON.stringify(value, null, 2) + '\n';
|
|
88
|
+
}
|
|
89
|
+
function copyWithBackup(source, destination, backupRoot, root) {
|
|
90
|
+
if (!inside(root, destination) || !inside(root, backupRoot))
|
|
91
|
+
throw new Error('VS Code extension path escaped its root');
|
|
92
|
+
if (fs.existsSync(destination) && digest(source) === digest(destination))
|
|
93
|
+
return false;
|
|
94
|
+
if (fs.existsSync(destination)) {
|
|
95
|
+
if (fs.lstatSync(destination).isSymbolicLink())
|
|
96
|
+
throw new Error(`refusing linked extension path: ${destination}`);
|
|
97
|
+
const backup = path.join(backupRoot, path.relative(root, destination));
|
|
98
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
99
|
+
fs.renameSync(destination, backup);
|
|
100
|
+
try {
|
|
101
|
+
fs.cpSync(source, destination, { recursive: fs.statSync(source).isDirectory() });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
fs.renameSync(backup, destination);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
110
|
+
fs.cpSync(source, destination, { recursive: fs.statSync(source).isDirectory() });
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
export function installVSCode({ sourceDir, extensionDir, bundleCacheRoot, dryRun = false, log = console.log }) {
|
|
115
|
+
const sourceRoot = fs.realpathSync(sourceDir);
|
|
116
|
+
const root = validatedRoot(locateVSCodeExtension(extensionDir));
|
|
117
|
+
const bundle = activeBundle(root, bundleCacheRoot);
|
|
118
|
+
const pluginRoot = path.join(root, 'resources', 'opencode', 'plugin', 'd2h');
|
|
119
|
+
const configFile = path.join(root, 'resources', 'opencode', 'opencode.json');
|
|
120
|
+
const bundleConfigFile = path.join(bundle, 'opencode.json');
|
|
121
|
+
const distFiles = ['dist/extension.js', 'dist/webview/main.js'];
|
|
122
|
+
const patches = [
|
|
123
|
+
{ file: configFile, content: updateConfig(fs.readFileSync(configFile, 'utf8')) },
|
|
124
|
+
...distFiles.map(name => ({ file: path.join(root, name), content: updateD2HModeLabel(fs.readFileSync(path.join(root, name), 'utf8')) })),
|
|
125
|
+
];
|
|
126
|
+
const copies = [
|
|
127
|
+
{ source: path.join(sourceRoot, 'skills'), destination: path.join(pluginRoot, 'skills') },
|
|
128
|
+
{ source: path.join(sourceRoot, 'agents'), destination: path.join(pluginRoot, 'agents') },
|
|
129
|
+
{ source: path.join(sourceRoot, 'AGENTS.md'), destination: path.join(pluginRoot, 'AGENTS.md') },
|
|
130
|
+
{ source: path.join(sourceRoot, 'opencode', 'vscode-d2h-plugin.mjs'), destination: path.join(pluginRoot, 'src', 'index.js') },
|
|
131
|
+
{ source: path.join(sourceRoot, 'opencode', 'vscode-d2h-package.json'), destination: path.join(pluginRoot, 'package.json') },
|
|
132
|
+
];
|
|
133
|
+
const bundleCopies = copies.map(entry => ({ source: entry.source,
|
|
134
|
+
destination: path.join(bundle, 'plugin', 'd2h', path.relative(pluginRoot, entry.destination)) }));
|
|
135
|
+
const bundleConfig = updateConfig(fs.readFileSync(bundleConfigFile, 'utf8'));
|
|
136
|
+
for (const entry of copies)
|
|
137
|
+
if (!fs.existsSync(entry.source))
|
|
138
|
+
throw new Error(`missing D2H source: ${entry.source}`);
|
|
139
|
+
for (const target of [...copies.map(entry => entry.destination), ...patches.map(entry => entry.file)])
|
|
140
|
+
assertSafeDestination(root, target);
|
|
141
|
+
for (const target of [...bundleCopies.map(entry => entry.destination), bundleConfigFile])
|
|
142
|
+
assertSafeDestination(bundle, target);
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
for (const entry of copies)
|
|
145
|
+
log(`would copy ${entry.source} -> ${entry.destination}`);
|
|
146
|
+
for (const entry of patches)
|
|
147
|
+
log(`would update ${entry.file}`);
|
|
148
|
+
for (const entry of bundleCopies)
|
|
149
|
+
log(`would copy ${entry.source} -> ${entry.destination}`);
|
|
150
|
+
log(`would update ${bundleConfigFile}`);
|
|
151
|
+
return { extensionRoot: root, changed: false };
|
|
152
|
+
}
|
|
153
|
+
if (!/^name: d2h\r?$/m.test(fs.readFileSync(path.join(sourceRoot, 'agents', 'd2h.md'), 'utf8'))) {
|
|
154
|
+
throw new Error('d2h.md frontmatter name is missing');
|
|
155
|
+
}
|
|
156
|
+
const backupRoot = fs.mkdtempSync(path.join(root, '.d2h-backup-'));
|
|
157
|
+
let changed = false;
|
|
158
|
+
for (const entry of copies)
|
|
159
|
+
changed = copyWithBackup(entry.source, entry.destination, backupRoot, root) || changed;
|
|
160
|
+
for (const entry of patches) {
|
|
161
|
+
if (fs.readFileSync(entry.file, 'utf8') === entry.content)
|
|
162
|
+
continue;
|
|
163
|
+
if (fs.lstatSync(entry.file).isSymbolicLink())
|
|
164
|
+
throw new Error(`refusing linked extension file: ${entry.file}`);
|
|
165
|
+
const backup = path.join(backupRoot, path.relative(root, entry.file));
|
|
166
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
167
|
+
fs.copyFileSync(entry.file, backup);
|
|
168
|
+
const temporary = `${entry.file}.d2h-${process.pid}.tmp`;
|
|
169
|
+
fs.writeFileSync(temporary, entry.content);
|
|
170
|
+
fs.renameSync(temporary, entry.file);
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
const bundleBackupRoot = fs.mkdtempSync(path.join(bundle, '.d2h-backup-'));
|
|
174
|
+
for (const entry of bundleCopies)
|
|
175
|
+
changed = copyWithBackup(entry.source, entry.destination, bundleBackupRoot, bundle) || changed;
|
|
176
|
+
if (fs.readFileSync(bundleConfigFile, 'utf8') !== bundleConfig) {
|
|
177
|
+
const backup = path.join(bundleBackupRoot, 'opencode.json');
|
|
178
|
+
fs.copyFileSync(bundleConfigFile, backup);
|
|
179
|
+
const temporary = `${bundleConfigFile}.d2h-${process.pid}.tmp`;
|
|
180
|
+
fs.writeFileSync(temporary, bundleConfig);
|
|
181
|
+
fs.renameSync(temporary, bundleConfigFile);
|
|
182
|
+
changed = true;
|
|
183
|
+
}
|
|
184
|
+
if (fs.readdirSync(bundleBackupRoot).length === 0)
|
|
185
|
+
fs.rmdirSync(bundleBackupRoot);
|
|
186
|
+
const tracked = [...copies.map(entry => entry.destination), ...patches.map(entry => entry.file)];
|
|
187
|
+
const receipt = { schemaVersion: 1, packageVersion: JSON.parse(fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8')).version,
|
|
188
|
+
files: tracked.map(file => ({ path: path.relative(root, file).replaceAll('\\', '/'), digest: digest(file) })) };
|
|
189
|
+
fs.writeFileSync(path.join(pluginRoot, '.d2h-installation.json'), JSON.stringify(receipt, null, 2) + '\n');
|
|
190
|
+
if (!changed)
|
|
191
|
+
fs.rmdirSync(backupRoot);
|
|
192
|
+
log(`VS Code D2H ${changed ? 'installed' : 'unchanged'} at ${root}; active bundle: ${bundle}${changed ? `; backup: ${backupRoot}` : ''}`);
|
|
193
|
+
return { extensionRoot: root, changed, backupRoot: changed ? backupRoot : undefined };
|
|
194
|
+
}
|
|
195
|
+
export function inspectVSCodeInstallation(extensionDir, bundleCacheRoot) {
|
|
196
|
+
const root = validatedRoot(locateVSCodeExtension(extensionDir));
|
|
197
|
+
const file = path.join(root, 'resources', 'opencode', 'plugin', 'd2h', '.d2h-installation.json');
|
|
198
|
+
if (!fs.existsSync(file))
|
|
199
|
+
return { status: 'missing', summary: `installation record missing: ${file}` };
|
|
200
|
+
const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
201
|
+
if (receipt.schemaVersion !== 1 || !Array.isArray(receipt.files))
|
|
202
|
+
throw new Error(`invalid VS Code installation record: ${file}`);
|
|
203
|
+
const changed = receipt.files.filter(entry => {
|
|
204
|
+
const target = path.resolve(root, entry.path);
|
|
205
|
+
return !inside(root, target) || !fs.existsSync(target) || digest(target) !== entry.digest;
|
|
206
|
+
}).map(entry => entry.path);
|
|
207
|
+
try {
|
|
208
|
+
const bundle = activeBundle(root, bundleCacheRoot);
|
|
209
|
+
const pluginRoot = path.join(root, 'resources', 'opencode', 'plugin', 'd2h');
|
|
210
|
+
for (const name of ['skills', 'agents', 'AGENTS.md', 'src/index.js', 'package.json']) {
|
|
211
|
+
const source = path.join(pluginRoot, name);
|
|
212
|
+
const target = path.join(bundle, 'plugin', 'd2h', name);
|
|
213
|
+
if (!fs.existsSync(target) || digest(source) !== digest(target))
|
|
214
|
+
changed.push(`bundle/plugin/d2h/${name}`);
|
|
215
|
+
}
|
|
216
|
+
const config = JSON.parse(fs.readFileSync(path.join(bundle, 'opencode.json'), 'utf8'));
|
|
217
|
+
if (!config.plugin?.includes('./plugin/d2h') || config.agent?.d2h?.description !== modeLabel) {
|
|
218
|
+
changed.push('bundle/opencode.json');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
changed.push(`active bundle: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
224
|
+
return { status: changed.length ? 'failed' : 'passed', packageVersion: receipt.packageVersion,
|
|
225
|
+
summary: changed.length ? `changed: ${changed.join(', ')}` : `${receipt.files.length} VS Code resources and active bundle verified` };
|
|
226
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
export function loadToolchain(root) {
|
|
5
|
+
const value = JSON.parse(fs.readFileSync(path.join(root, 'skills', 'managing-d2h-toolchain', 'toolchain.json'), 'utf8'));
|
|
6
|
+
if (value?.schemaVersion !== 1 || !value.tools || Array.isArray(value.tools) || !Object.keys(value.tools).length ||
|
|
7
|
+
!value.node || !/^\d+\.\d+\.\d+$/.test(value.node.minimum) ||
|
|
8
|
+
!/^\d+\.\d+\.\d+$/.test(value.node.maximumExclusive))
|
|
9
|
+
throw new Error('invalid toolchain manifest');
|
|
10
|
+
for (const [name, tool] of Object.entries(value.tools)) {
|
|
11
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name) || tool?.package !== `@harmonyos-arkts/${name}` ||
|
|
12
|
+
!/^\d+\.\d+\.\d+$/.test(tool.version) || !tool.capabilities ||
|
|
13
|
+
typeof tool.capabilities !== 'object' || Array.isArray(tool.capabilities) ||
|
|
14
|
+
!Object.keys(tool.capabilities).length || Object.entries(tool.capabilities).some(([capability, args]) => !/^[a-z][a-z0-9-]*$/.test(capability) || !Array.isArray(args) || !args.length ||
|
|
15
|
+
args.some(arg => typeof arg !== 'string' || !arg))) {
|
|
16
|
+
throw new Error(`invalid toolchain entry: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export const run = (command, args) => {
|
|
22
|
+
let executable = command;
|
|
23
|
+
let parameters = args;
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
const quote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
26
|
+
const binary = /^(npm|appgraph|droid2hmos)$/.test(command) ? `${command}.cmd` : command;
|
|
27
|
+
const script = `& ${[binary, ...args].map(quote).join(' ')}; exit $LASTEXITCODE`;
|
|
28
|
+
executable = 'powershell.exe';
|
|
29
|
+
parameters = ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')];
|
|
30
|
+
}
|
|
31
|
+
const result = spawnSync(executable, parameters, { encoding: 'utf8', windowsHide: true, timeout: 300000 });
|
|
32
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '', error: result.error };
|
|
33
|
+
};
|
|
34
|
+
function version(output) { return output.match(/\b\d+\.\d+\.\d+\b/)?.[0] ?? null; }
|
|
35
|
+
export function inspectTool(name, tool, runner = run) {
|
|
36
|
+
const current = runner(name, ['--version']);
|
|
37
|
+
const installedVersion = current.status === 0 ? version(`${current.stdout}\n${current.stderr}`) : null;
|
|
38
|
+
const capabilities = Object.fromEntries(Object.entries(tool.capabilities).map(([capability, args]) => {
|
|
39
|
+
const result = installedVersion ? runner(name, args) : null;
|
|
40
|
+
const output = `${result?.stdout ?? ''}\n${result?.stderr ?? ''}`;
|
|
41
|
+
const expectedCommand = capability === 'analyze-android' ? 'analyze' : capability === 'build-android' ? 'build' : capability;
|
|
42
|
+
return [capability, result?.status === 0 && output.includes(expectedCommand)];
|
|
43
|
+
}));
|
|
44
|
+
return { name, package: tool.package, expectedVersion: tool.version, installedVersion,
|
|
45
|
+
status: installedVersion === tool.version && Object.values(capabilities).every(Boolean) ? 'passed' : 'failed', capabilities };
|
|
46
|
+
}
|
|
47
|
+
export function inspectToolchain(root, runner = run) {
|
|
48
|
+
const manifest = loadToolchain(root);
|
|
49
|
+
return Object.entries(manifest.tools).map(([name, tool]) => inspectTool(name, tool, runner));
|
|
50
|
+
}
|
|
51
|
+
export function installToolchain(root, { dryRun = false, force = false, runner = run, log = console.log } = {}) {
|
|
52
|
+
const manifest = loadToolchain(root);
|
|
53
|
+
const before = Object.entries(manifest.tools).map(([name, tool]) => inspectTool(name, tool, runner));
|
|
54
|
+
const targets = force ? before : before.filter(item => item.status !== 'passed');
|
|
55
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
56
|
+
const globalRoot = targets.length ? runner(npm, ['root', '--global']) : null;
|
|
57
|
+
if (targets.length && globalRoot?.status !== 0)
|
|
58
|
+
throw new Error(`cannot inspect global npm installation: ${globalRoot?.stderr ?? 'unknown error'}`);
|
|
59
|
+
for (const item of targets) {
|
|
60
|
+
const tool = manifest.tools[item.name];
|
|
61
|
+
const coordinate = `${tool.package}@${tool.version}`;
|
|
62
|
+
const linkedPath = path.join(globalRoot.stdout.trim(), ...tool.package.split('/'));
|
|
63
|
+
const linked = fs.existsSync(linkedPath) && fs.lstatSync(linkedPath).isSymbolicLink();
|
|
64
|
+
if (linked && !force) {
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
log(`would preserve npm link ${tool.package} at ${linkedPath}; rerun with --force to replace it`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`${tool.package} is an npm link at ${linkedPath}; rerun with --force to replace it or use --skip-tools.`);
|
|
70
|
+
}
|
|
71
|
+
if (dryRun) {
|
|
72
|
+
log(`would ${linked ? 'replace npm link with' : 'install'} ${coordinate}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const result = runner(npm, ['install', '--global', coordinate]);
|
|
76
|
+
if (result.status !== 0)
|
|
77
|
+
throw new Error(`failed to install ${coordinate}: ${result.stderr || result.error?.message}`);
|
|
78
|
+
if (linked && fs.existsSync(linkedPath) && fs.lstatSync(linkedPath).isSymbolicLink()) {
|
|
79
|
+
throw new Error(`${coordinate} installation completed but npm link remains at ${linkedPath}`);
|
|
80
|
+
}
|
|
81
|
+
const verified = inspectTool(item.name, tool, runner);
|
|
82
|
+
if (verified.status !== 'passed')
|
|
83
|
+
throw new Error(`${coordinate} installed but CLI version or capabilities failed verification`);
|
|
84
|
+
log(`installed and verified ${coordinate}`);
|
|
85
|
+
}
|
|
86
|
+
return before;
|
|
87
|
+
}
|