@harmonyos-arkts/d2h 0.1.28 → 0.1.29

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "android-to-hmos-migration",
3
3
  "description": "基于确定性 facts、迁移报告与端到端验证的 Android 到 HarmonyOS 迁移插件",
4
- "version": "0.1.28",
4
+ "version": "0.1.29",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "android",
package/README.md CHANGED
@@ -20,7 +20,15 @@
20
20
 
21
21
  ## 安装
22
22
 
23
- 默认安装到 OpenCode:
23
+ 不传 `--agent` 时,在终端用上下键选择安装目标,回车确认,也可输入序号或名称;前两项固定为 OpenCode、VS Code,不显示命令名或路径。检测到工具命令或配置目录时显示“可配置”,否则显示“未检测到”;VS Code 以发现鸿蒙插件为准。检测结果不代替安装校验。未检测到的工具仍可手动选择。
24
+
25
+ ```bash
26
+ npx --yes @harmonyos-arkts/d2h@latest install --force
27
+ ```
28
+
29
+ CI、管道等非交互环境必须显式传入 `--agent`。`upgrade` 同样支持交互选择,`doctor` 保留默认检查 OpenCode 的行为。
30
+
31
+ 直接安装到 OpenCode:
24
32
 
25
33
  ```bash
26
34
  npx --yes @harmonyos-arkts/d2h@latest install --agent opencode --force
@@ -18,6 +18,7 @@ import { inspectVSCodeInstallation, installVSCode } from '../src/install-vscode.
18
18
  import { isSkillHost, skillHosts, skillHostConfigRoot, inspectSkillHostInstallation, installSkillHost } from '../src/install-skills.js';
19
19
  import { installTuiExtension, inspectTuiExtension } from '../src/install-tui-extension.js';
20
20
  import { runWorkflow } from '../src/workflow.js';
21
+ import { selectAgent } from '../src/select-agent.js';
21
22
  const help = `Install this package's skills and agents into supported AI coding tools.
22
23
 
23
24
  Usage: d2h [options]
@@ -34,8 +35,9 @@ Usage: d2h [options]
34
35
  Use d2h --check-env --help for check options
35
36
 
36
37
  --source-dir <path> Source root (defaults to this package)
37
- --agent <name> opencode (default), deveco, vscode, codex, qoder, trae,
38
+ --agent <name> opencode, vscode, deveco, codex, qoder, trae,
38
39
  trae-cn, claude, dsh, zcode, codebuddy or workbuddy
40
+ Omit for interactive selection with local detection hints
39
41
  --extension-dir <path> VS Code HarmonyOS Dev Assistant extension root
40
42
  --config-dir <path> Override the selected agent's config root
41
43
  OPENCODE_CONFIG_DIR / DEVECO_CONFIG_DIR, or
@@ -120,7 +122,6 @@ try {
120
122
  let remote = false;
121
123
  const options = {
122
124
  sourceDir: packageRoot,
123
- agent: 'opencode',
124
125
  };
125
126
  for (let index = 0; index < installArgs.length; index++) {
126
127
  const arg = installArgs[index];
@@ -153,6 +154,7 @@ try {
153
154
  throw new Error(`unknown option: ${arg}`);
154
155
  }
155
156
  }
157
+ options.agent ??= await selectAgent(extensionDir);
156
158
  if (!options.agent || (!['opencode', 'deveco', 'vscode'].includes(options.agent) && !isSkillHost(options.agent))) {
157
159
  throw new Error(`unsupported agent: ${options.agent}; choose opencode, deveco, vscode or ${Object.keys(skillHosts).join(', ')}`);
158
160
  }
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { emitKeypressEvents, moveCursor, cursorTo, clearScreenDown } from 'node:readline';
6
+ import { skillHosts } from './install-skills.js';
7
+ import { locateVSCodeExtension } from './install-vscode.js';
8
+ function executable(name, env, platform) {
9
+ const extensions = platform === 'win32' ? (env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : [''];
10
+ for (const directory of (env.PATH || '').split(platform === 'win32' ? ';' : ':').filter(Boolean)) {
11
+ for (const extension of extensions) {
12
+ const file = path.join(directory, name + extension);
13
+ try {
14
+ if (!fs.statSync(file).isFile())
15
+ continue;
16
+ fs.accessSync(file, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
17
+ return file;
18
+ }
19
+ catch { /* Missing or inaccessible commands are not detection evidence. */ }
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ export function detectAgents({ home = os.homedir(), env = process.env, platform = process.platform, extensionDir } = {}) {
25
+ const configHome = env.XDG_CONFIG_HOME || path.join(home, '.config');
26
+ const hosts = [
27
+ { agent: 'opencode', label: 'OpenCode', root: env.OPENCODE_CONFIG_DIR || path.join(configHome, 'opencode'), commands: ['opencode'] },
28
+ { agent: 'vscode', label: 'VS Code (HarmonyOS Dev Assistant)', root: path.join(home, '.vscode'), commands: ['code'] },
29
+ { agent: 'deveco', label: 'DevEco Code', root: env.DEVECO_CONFIG_DIR || path.join(configHome, 'deveco'), commands: ['deveco'] },
30
+ ...Object.entries(skillHosts).map(([agent, host]) => ({
31
+ agent, label: host.label,
32
+ root: ('environment' in host && env[host.environment]) || path.join(home, host.directory),
33
+ commands: agent === 'qoder' ? ['qodercli', 'qoder'] : agent === 'trae-cn' ? ['trae-cn'] : [agent]
34
+ }))
35
+ ];
36
+ return hosts.map(host => {
37
+ const detected = [];
38
+ for (const command of host.commands) {
39
+ const file = executable(command, env, platform);
40
+ if (file) {
41
+ detected.push(`命令 ${file}`);
42
+ break;
43
+ }
44
+ }
45
+ try {
46
+ if (fs.statSync(host.root).isDirectory())
47
+ detected.push(`配置目录 ${host.root}`);
48
+ }
49
+ catch { /* Not present. */ }
50
+ if (host.agent === 'vscode') {
51
+ try {
52
+ const extension = locateVSCodeExtension(extensionDir, home);
53
+ const metadata = JSON.parse(fs.readFileSync(path.join(extension, 'package.json'), 'utf8'));
54
+ if (String(metadata.name).toLowerCase() === 'harmonyosdevassistantforvscode' &&
55
+ String(metadata.publisher).toLowerCase() === 'huaweideveloperserviceplatform') {
56
+ detected.push(`鸿蒙插件 ${extension}`);
57
+ }
58
+ }
59
+ catch { /* VS Code without the required extension remains selectable. */ }
60
+ }
61
+ return { agent: host.agent, label: host.label, detected };
62
+ });
63
+ }
64
+ export function formatAgentChoice(choice) {
65
+ const detected = choice.agent === 'vscode'
66
+ ? choice.detected.some(item => item.startsWith('鸿蒙插件 '))
67
+ : choice.detected.length > 0;
68
+ const status = detected ? '可配置' : '未检测到';
69
+ const label = ['opencode', 'vscode'].includes(choice.agent) ? choice.label : `${choice.label} (${choice.agent})`;
70
+ return `${label} — ${status}`;
71
+ }
72
+ export async function chooseAgent(choices, ask, write) {
73
+ write('选择安装目标 / Select agent(检测结果仅供参考):\n');
74
+ choices.forEach((choice, index) => write(` ${index + 1}. ${formatAgentChoice(choice)}\n`));
75
+ for (;;) {
76
+ const answer = (await ask('输入序号或 agent 名称,q 取消:')).trim().toLowerCase();
77
+ if (['q', 'quit', 'exit'].includes(answer))
78
+ throw new Error('Installation cancelled before making changes');
79
+ const choice = /^\d+$/.test(answer) ? choices[Number(answer) - 1] : choices.find(item => item.agent === answer);
80
+ if (choice)
81
+ return choice.agent;
82
+ write('请选择列表中的序号或 agent 名称。\n');
83
+ }
84
+ }
85
+ export function chooseAgentWithKeys(choices, input = process.stdin, output = process.stderr) {
86
+ return new Promise((resolve, reject) => {
87
+ let selected = 0;
88
+ let renderedLines = 0;
89
+ let answer = '';
90
+ const colorDepth = process.env.NO_COLOR === undefined ? (output.getColorDepth?.() || 1) : 1;
91
+ const wasRaw = Boolean(input.isRaw);
92
+ const wasFlowing = input.readableFlowing === true;
93
+ const rows = Math.max(1, Math.min(choices.length, (output.rows || 24) - 4));
94
+ function render() {
95
+ if (renderedLines) {
96
+ moveCursor(output, 0, -renderedLines);
97
+ cursorTo(output, 0);
98
+ clearScreenDown(output);
99
+ }
100
+ const start = Math.max(0, Math.min(selected - rows + 1, choices.length - rows));
101
+ const lines = ['选择安装目标 / Select agent(检测结果仅供参考)'];
102
+ for (let i = start; i < start + rows; i++) {
103
+ lines.push(`${i === selected ? '>' : ' '} ${i + 1}. ${formatAgentChoice(choices[i])}`);
104
+ }
105
+ lines.push(`↑↓ 选择,回车确认;也可输入序号或名称,q 取消:${answer}`);
106
+ // Keep every row within the terminal width so cursor movement stays aligned.
107
+ const width = Math.max(1, (output.columns || 80) - 1);
108
+ const clipped = lines.map(line => {
109
+ let cells = 0;
110
+ let result = '';
111
+ for (const char of line) {
112
+ const size = char.codePointAt(0) >= 0x1100 ? 2 : 1;
113
+ if (cells + size > width)
114
+ break;
115
+ result += char;
116
+ cells += size;
117
+ }
118
+ return result;
119
+ });
120
+ const highlighted = clipped.map((line, index) => {
121
+ if (index !== selected - start + 1 || colorDepth < 4)
122
+ return line;
123
+ // HarmonyOS-style blue (#0A59F7), with an ANSI blue fallback.
124
+ const color = colorDepth >= 24 ? '\x1b[1;38;2;255;255;255;48;2;10;89;247m' : '\x1b[1;97;44m';
125
+ return color + line + '\x1b[0m';
126
+ });
127
+ output.write(highlighted.join('\n') + '\n');
128
+ renderedLines = clipped.length;
129
+ }
130
+ function finish(agent) {
131
+ input.removeListener('keypress', onKey);
132
+ input.removeListener('end', onEnd);
133
+ input.removeListener('error', onError);
134
+ input.setRawMode(wasRaw);
135
+ if (!wasFlowing)
136
+ input.pause();
137
+ if (agent) {
138
+ output.write(`已选择:${choices.find(item => item.agent === agent).label}\n`);
139
+ resolve(agent);
140
+ }
141
+ else
142
+ reject(new Error('Installation cancelled before making changes'));
143
+ }
144
+ function onEnd() { finish(); }
145
+ function onError(error) { finish(); }
146
+ function onKey(text, key = {}) {
147
+ if ((key.ctrl && ['c', 'd'].includes(key.name || '')) || key.name === 'escape') {
148
+ finish();
149
+ return;
150
+ }
151
+ if (key.name === 'up' || key.name === 'down') {
152
+ selected = (selected + (key.name === 'up' ? -1 : 1) + choices.length) % choices.length;
153
+ answer = '';
154
+ }
155
+ else if (key.name === 'return' || key.name === 'enter') {
156
+ const value = answer.trim().toLowerCase();
157
+ if (['q', 'quit', 'exit'].includes(value)) {
158
+ finish();
159
+ return;
160
+ }
161
+ const choice = !value ? choices[selected] : /^\d+$/.test(value) ? choices[Number(value) - 1] : choices.find(item => item.agent === value);
162
+ if (choice) {
163
+ finish(choice.agent);
164
+ return;
165
+ }
166
+ answer = '';
167
+ }
168
+ else if (key.name === 'backspace')
169
+ answer = answer.slice(0, -1);
170
+ else if (text && !key.ctrl && !key.meta && /^[a-zA-Z0-9-]+$/.test(text))
171
+ answer = (answer + text).slice(0, 30);
172
+ else
173
+ return;
174
+ render();
175
+ }
176
+ emitKeypressEvents(input);
177
+ input.setRawMode(true);
178
+ input.on('keypress', onKey);
179
+ input.once('end', onEnd);
180
+ input.once('error', onError);
181
+ input.resume();
182
+ render();
183
+ });
184
+ }
185
+ export async function selectAgent(extensionDir) {
186
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
187
+ throw new Error('Agent selection requires an interactive terminal; pass --agent <name> (for example --agent opencode)');
188
+ }
189
+ const choices = detectAgents({ extensionDir });
190
+ if (process.env.TERM !== 'dumb')
191
+ return chooseAgentWithKeys(choices);
192
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
193
+ const controller = new AbortController();
194
+ rl.once('SIGINT', () => controller.abort());
195
+ rl.once('close', () => controller.abort());
196
+ try {
197
+ return await chooseAgent(choices, prompt => rl.question(prompt, { signal: controller.signal }), text => process.stderr.write(text));
198
+ }
199
+ catch (error) {
200
+ if (controller.signal.aborted)
201
+ throw new Error('Installation cancelled before making changes');
202
+ throw error;
203
+ }
204
+ finally {
205
+ rl.close();
206
+ }
207
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harmonyos-arkts/d2h",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "Install Android-to-HarmonyOS migration agents and skills into AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {