@buaa_smat/hometrans 0.1.9 → 0.1.11
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 +229 -213
- package/agents/build-fixer.md +17 -10
- package/agents/code-reviewer.md +4 -2
- package/dist/cli/config-store.js +81 -1
- package/dist/cli/config.js +9 -10
- package/dist/cli/index.js +24 -0
- package/dist/cli/init.js +343 -22
- package/dist/cli/mcp-setup.js +2 -6
- package/dist/cli/uninstall.js +20 -9
- package/dist/context/index.js +72 -0
- package/package.json +1 -1
- package/resource/choose_editor.png +0 -0
- package/resource/hometrans_config.png +0 -0
- package/skills/hmos-convert-pipeline/SKILL.md +19 -11
- package/skills/hmos-fix-build-errors/SKILL.md +13 -5
- package/skills/hmos-incremental-ui-align/SKILL.md +15 -1
- package/tools/test-tools/autotest/uv.lock +3156 -3156
- package/resource/finish_init.png +0 -0
package/dist/cli/config-store.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 全局配置存储:~/.hometrans/config.json
|
|
3
3
|
*
|
|
4
|
-
* 当前包含 editors 配置(Claude Code / Cursor / OpenCode / Codex),后续可扩展
|
|
4
|
+
* 当前包含 editors 配置(Claude Code / Cursor / OpenCode / Codex / CodeBuddy),后续可扩展
|
|
5
5
|
* 其它顶层字段。首次 setup 时若文件不存在则写入默认值;之后用户可手动编辑。
|
|
6
6
|
* `ht config` 只读不改。
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import os from 'node:os';
|
|
11
|
+
/** 由 DEVECO_SDK_HOME 派生其余三个 SDK 相关路径。输入为空时全部返回空串。 */
|
|
12
|
+
export function deriveSdkPaths(devecoSdkHome) {
|
|
13
|
+
const home = devecoSdkHome.trim().replace(/[\\/]+$/, '');
|
|
14
|
+
if (!home) {
|
|
15
|
+
return { DEVECO_SDK_HOME: '', DEVECO_PATH: '', OHOS_SDK_PATH: '', HMS_SDK_PATH: '' };
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
DEVECO_SDK_HOME: home,
|
|
19
|
+
DEVECO_PATH: path.dirname(home),
|
|
20
|
+
OHOS_SDK_PATH: path.join(home, 'default', 'openharmony', 'ets'),
|
|
21
|
+
HMS_SDK_PATH: path.join(home, 'default', 'hms', 'ets'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 旧配置迁移:从 OHOS_SDK_PATH(…/sdk/default/openharmony/ets)反推
|
|
26
|
+
* DEVECO_SDK_HOME(…/sdk)。HMS 同理。匹配不上返回 ''。
|
|
27
|
+
*/
|
|
28
|
+
export function sdkHomeFromLegacyPaths(env) {
|
|
29
|
+
const tryStrip = (p, suffix) => {
|
|
30
|
+
if (!p)
|
|
31
|
+
return '';
|
|
32
|
+
const norm = p.replace(/[\\/]+$/, '').split(/[\\/]/);
|
|
33
|
+
if (norm.length <= suffix.length)
|
|
34
|
+
return '';
|
|
35
|
+
const tail = norm.slice(-suffix.length).map((s) => s.toLowerCase());
|
|
36
|
+
if (tail.join('/') !== suffix.join('/'))
|
|
37
|
+
return '';
|
|
38
|
+
return norm.slice(0, -suffix.length).join(path.sep);
|
|
39
|
+
};
|
|
40
|
+
return (tryStrip(env.OHOS_SDK_PATH, ['default', 'openharmony', 'ets']) ||
|
|
41
|
+
tryStrip(env.HMS_SDK_PATH, ['default', 'hms', 'ets']));
|
|
42
|
+
}
|
|
11
43
|
export function getConfigDir() {
|
|
12
44
|
return path.join(os.homedir(), '.hometrans');
|
|
13
45
|
}
|
|
@@ -76,6 +108,19 @@ export function defaultEditors() {
|
|
|
76
108
|
section: 'mcp_servers.hometrans',
|
|
77
109
|
},
|
|
78
110
|
},
|
|
111
|
+
{
|
|
112
|
+
// CodeBuddy(腾讯):与 Claude Code 同构的 .codebuddy/ 目录,
|
|
113
|
+
// 全局安装映射到 ~/.codebuddy/{skills,agents},MCP 走 mcpServers JSON。
|
|
114
|
+
name: 'CodeBuddy',
|
|
115
|
+
markerDir: '~/.codebuddy',
|
|
116
|
+
skillsDir: '~/.codebuddy/skills',
|
|
117
|
+
agentsDir: '~/.codebuddy/agents',
|
|
118
|
+
mcp: {
|
|
119
|
+
format: 'jsonc-object',
|
|
120
|
+
path: '~/.codebuddy/mcp.json',
|
|
121
|
+
keyPath: ['mcpServers', 'hometrans'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
79
124
|
];
|
|
80
125
|
}
|
|
81
126
|
async function fileExists(p) {
|
|
@@ -93,9 +138,12 @@ async function fileExists(p) {
|
|
|
93
138
|
*/
|
|
94
139
|
export function defaultEnv() {
|
|
95
140
|
return {
|
|
141
|
+
DEVECO_SDK_HOME: '',
|
|
142
|
+
DEVECO_PATH: '',
|
|
96
143
|
OHOS_SDK_PATH: '',
|
|
97
144
|
HMS_SDK_PATH: '',
|
|
98
145
|
TEST_API_KEY: '',
|
|
146
|
+
GLM_API_KEY: '',
|
|
99
147
|
TOOL_PATH: '',
|
|
100
148
|
};
|
|
101
149
|
}
|
|
@@ -139,6 +187,38 @@ export async function loadHomeTransConfig() {
|
|
|
139
187
|
delete anyParsed.sdkPaths;
|
|
140
188
|
delete anyParsed.params;
|
|
141
189
|
delete anyParsed.tool_path;
|
|
190
|
+
// SDK 路径统一迁移:老配置只有 OHOS_SDK_PATH/HMS_SDK_PATH 时反推
|
|
191
|
+
// DEVECO_SDK_HOME,再用它补齐缺失的派生路径(不覆盖已有非空值)。
|
|
192
|
+
let envDirty = false;
|
|
193
|
+
if (!config.env.DEVECO_SDK_HOME) {
|
|
194
|
+
const migrated = sdkHomeFromLegacyPaths(config.env);
|
|
195
|
+
if (migrated) {
|
|
196
|
+
config.env.DEVECO_SDK_HOME = migrated;
|
|
197
|
+
envDirty = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (config.env.DEVECO_SDK_HOME) {
|
|
201
|
+
const derived = deriveSdkPaths(config.env.DEVECO_SDK_HOME);
|
|
202
|
+
for (const key of ['DEVECO_PATH', 'OHOS_SDK_PATH', 'HMS_SDK_PATH']) {
|
|
203
|
+
if (!config.env[key]) {
|
|
204
|
+
config.env[key] = derived[key];
|
|
205
|
+
envDirty = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (envDirty) {
|
|
210
|
+
await saveHomeTransConfig(config);
|
|
211
|
+
}
|
|
212
|
+
// Append any default editors that this (older) config predates, matched by
|
|
213
|
+
// name. Existing entries are never modified, so manual edits are preserved;
|
|
214
|
+
// we only add editors the user's file is missing (e.g. CodeBuddy shipped
|
|
215
|
+
// after they first ran `ht init`). Persist so the new editor shows up.
|
|
216
|
+
const existingNames = new Set(config.editors.map((e) => e.name));
|
|
217
|
+
const missingEditors = defaultEditors().filter((e) => !existingNames.has(e.name));
|
|
218
|
+
if (missingEditors.length > 0) {
|
|
219
|
+
config.editors.push(...missingEditors);
|
|
220
|
+
await saveHomeTransConfig(config);
|
|
221
|
+
}
|
|
142
222
|
return config;
|
|
143
223
|
}
|
|
144
224
|
export async function saveHomeTransConfig(config) {
|
package/dist/cli/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `ht config` — 打印 editors.json 路径与内容。
|
|
3
3
|
*
|
|
4
4
|
* 该命令只读:用户若要修改 editor 配置,直接编辑这个 JSON 文件即可。
|
|
5
|
-
* 文件不存在时自动写入默认
|
|
5
|
+
* 文件不存在时自动写入默认 5 个 editor。
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import { getConfigPath, loadHomeTransConfig } from './config-store.js';
|
|
@@ -17,16 +17,15 @@ export async function configCommand() {
|
|
|
17
17
|
console.log(` Path: ${configPath}`);
|
|
18
18
|
console.log('');
|
|
19
19
|
// User parameters
|
|
20
|
-
const
|
|
21
|
-
? config.env.TEST_API_KEY.slice(0, 4) +
|
|
22
|
-
'***' +
|
|
23
|
-
config.env.TEST_API_KEY.slice(-4)
|
|
24
|
-
: '(not set)';
|
|
20
|
+
const mask = (key) => key ? key.slice(0, 4) + '***' + key.slice(-4) : '(not set)';
|
|
25
21
|
console.log(' Parameters:');
|
|
26
|
-
console.log(`
|
|
27
|
-
console.log(`
|
|
28
|
-
console.log(`
|
|
29
|
-
console.log(`
|
|
22
|
+
console.log(` DEVECO_SDK_HOME : ${config.env.DEVECO_SDK_HOME || '(not set)'}`);
|
|
23
|
+
console.log(` DEVECO_PATH : ${config.env.DEVECO_PATH || '(not set)'} (derived)`);
|
|
24
|
+
console.log(` OHOS_SDK_PATH : ${config.env.OHOS_SDK_PATH || '(not set)'} (derived)`);
|
|
25
|
+
console.log(` HMS_SDK_PATH : ${config.env.HMS_SDK_PATH || '(not set)'} (derived)`);
|
|
26
|
+
console.log(` TEST_API_KEY : ${mask(config.env.TEST_API_KEY)}`);
|
|
27
|
+
console.log(` GLM_API_KEY : ${mask(config.env.GLM_API_KEY)}`);
|
|
28
|
+
console.log(` TOOL_PATH : ${config.env.TOOL_PATH || '(not set)'}`);
|
|
30
29
|
console.log('');
|
|
31
30
|
// Full config content
|
|
32
31
|
console.log(' Full Content:');
|
package/dist/cli/index.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
import { initCommand } from './init.js';
|
|
5
6
|
import { runMcpServer } from './mcp.js';
|
|
6
7
|
import { configCommand } from './config.js';
|
|
7
8
|
import { uninstallCommand } from './uninstall.js';
|
|
8
9
|
const _require = createRequire(import.meta.url);
|
|
9
10
|
const pkg = _require('../../package.json');
|
|
11
|
+
/**
|
|
12
|
+
* Global Ctrl-C safety net.
|
|
13
|
+
*
|
|
14
|
+
* inquirer intercepts Ctrl-C as a keypress *inside* a prompt (raw mode) and the
|
|
15
|
+
* per-command catches print a tailored "Cancelled" notice there. But Ctrl-C
|
|
16
|
+
* pressed *outside* a prompt — e.g. while `init` is copying files or `uninstall`
|
|
17
|
+
* is deleting — arrives as an OS SIGINT, which Node would otherwise terminate on
|
|
18
|
+
* silently. This handler guarantees a cancel notice in that case too.
|
|
19
|
+
*
|
|
20
|
+
* The MCP server (`ht mcp`) runs over stdio and must keep its own shutdown
|
|
21
|
+
* semantics, so it removes this handler on startup.
|
|
22
|
+
*/
|
|
23
|
+
let interrupted = false;
|
|
24
|
+
function onSigint() {
|
|
25
|
+
if (interrupted)
|
|
26
|
+
process.exit(130);
|
|
27
|
+
interrupted = true;
|
|
28
|
+
console.log(chalk.yellow('\n Cancelled by Ctrl-C. No further changes will be made.\n'));
|
|
29
|
+
process.exit(130);
|
|
30
|
+
}
|
|
31
|
+
process.on('SIGINT', onSigint);
|
|
10
32
|
const program = new Command();
|
|
11
33
|
program
|
|
12
34
|
.name('hometrans')
|
|
@@ -23,6 +45,8 @@ program
|
|
|
23
45
|
.command('mcp')
|
|
24
46
|
.description('Run the HomeTrans MCP server (stdio)')
|
|
25
47
|
.action(async () => {
|
|
48
|
+
// The MCP server owns its own SIGINT/shutdown handling; drop the CLI net.
|
|
49
|
+
process.off('SIGINT', onSigint);
|
|
26
50
|
await runMcpServer();
|
|
27
51
|
});
|
|
28
52
|
program
|
package/dist/cli/init.js
CHANGED
|
@@ -7,18 +7,33 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import figlet from 'figlet';
|
|
14
14
|
import inquirer from 'inquirer';
|
|
15
|
+
import { parseTree, modify, applyEdits } from 'jsonc-parser';
|
|
15
16
|
import { setupMcpForAllEditors } from './mcp-setup.js';
|
|
16
|
-
import { expandHome, getConfigPath, getToolsDir, loadHomeTransConfig, saveHomeTransConfig, } from './config-store.js';
|
|
17
|
+
import { deriveSdkPaths, expandHome, getConfigPath, getToolsDir, loadHomeTransConfig, saveHomeTransConfig, } from './config-store.js';
|
|
17
18
|
function ensureChalkColor() {
|
|
18
19
|
if (process.stdout.isTTY && chalk.level === 0) {
|
|
19
20
|
chalk.level = 1;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Did the user abort an inquirer prompt with Ctrl-C / Esc?
|
|
25
|
+
* inquirer v14 (@inquirer/core) rejects with an `ExitPromptError` in that case.
|
|
26
|
+
* We must distinguish this from a genuine non-interactive failure (no TTY),
|
|
27
|
+
* otherwise Ctrl-C silently gets swallowed and the install proceeds anyway.
|
|
28
|
+
*/
|
|
29
|
+
export function isPromptAbort(err) {
|
|
30
|
+
return err instanceof Error && err.name === 'ExitPromptError';
|
|
31
|
+
}
|
|
32
|
+
/** Print a cancellation notice and exit with the conventional SIGINT code. */
|
|
33
|
+
function abortInit() {
|
|
34
|
+
console.log(chalk.yellow('\n Cancelled. No changes were made beyond this point.\n'));
|
|
35
|
+
process.exit(130);
|
|
36
|
+
}
|
|
22
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
38
|
const __dirname = path.dirname(__filename);
|
|
24
39
|
export async function dirExists(dirPath) {
|
|
@@ -116,6 +131,70 @@ async function refreshAutotestConfig(autotestDir, apiKey) {
|
|
|
116
131
|
? `autotest config.yaml seeded + api_key filled`
|
|
117
132
|
: `autotest config.yaml api_key refreshed`;
|
|
118
133
|
}
|
|
134
|
+
const UI_ALIGN_SKILL = 'hmos-incremental-ui-align';
|
|
135
|
+
/**
|
|
136
|
+
* Initialize / refresh `<skillsDir>/hmos-incremental-ui-align/config.json`
|
|
137
|
+
* in an editor's installed skills dir — same semantics as the autotest
|
|
138
|
+
* config.yaml handling:
|
|
139
|
+
* - If config.json does not exist, seed it from config-example.json.
|
|
140
|
+
* - If it exists, surgically overwrite ONLY the `glm_api_key` and
|
|
141
|
+
* `hmos_sdk_dir` values (via jsonc edits), preserving every other
|
|
142
|
+
* field the user has set. `hmos_sdk_dir` = `<DEVECO_SDK_HOME>/default`.
|
|
143
|
+
*
|
|
144
|
+
* Returns a status string for the result summary, or null if nothing was
|
|
145
|
+
* done (skill not installed there, or no values to write into an existing file).
|
|
146
|
+
*/
|
|
147
|
+
export async function refreshUiAlignConfig(skillsDir, glmApiKey, hmosSdkDir) {
|
|
148
|
+
const skillDir = path.join(skillsDir, UI_ALIGN_SKILL);
|
|
149
|
+
const examplePath = path.join(skillDir, 'config-example.json');
|
|
150
|
+
const configPath = path.join(skillDir, 'config.json');
|
|
151
|
+
const hasExample = await fs
|
|
152
|
+
.access(examplePath)
|
|
153
|
+
.then(() => true)
|
|
154
|
+
.catch(() => false);
|
|
155
|
+
if (!hasExample)
|
|
156
|
+
return null;
|
|
157
|
+
let seeded = false;
|
|
158
|
+
const hasConfig = await fs
|
|
159
|
+
.access(configPath)
|
|
160
|
+
.then(() => true)
|
|
161
|
+
.catch(() => false);
|
|
162
|
+
if (!hasConfig) {
|
|
163
|
+
await fs.copyFile(examplePath, configPath);
|
|
164
|
+
seeded = true;
|
|
165
|
+
}
|
|
166
|
+
const updates = [];
|
|
167
|
+
if (glmApiKey)
|
|
168
|
+
updates.push(['glm_api_key', glmApiKey]);
|
|
169
|
+
if (hmosSdkDir)
|
|
170
|
+
updates.push(['hmos_sdk_dir', hmosSdkDir]);
|
|
171
|
+
if (updates.length === 0) {
|
|
172
|
+
return seeded
|
|
173
|
+
? `${UI_ALIGN_SKILL}/config.json seeded (glm_api_key / hmos_sdk_dir left empty)`
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
176
|
+
let raw = await fs.readFile(configPath, 'utf-8');
|
|
177
|
+
const parseErrors = [];
|
|
178
|
+
const tree = parseTree(raw, parseErrors);
|
|
179
|
+
if (!tree || tree.type !== 'object' || parseErrors.length > 0) {
|
|
180
|
+
// Corrupt config — never regenerate over user content; just report.
|
|
181
|
+
return `${UI_ALIGN_SKILL}/config.json NOT updated (file is not valid JSON — fix it manually)`;
|
|
182
|
+
}
|
|
183
|
+
const original = raw;
|
|
184
|
+
for (const [key, value] of updates) {
|
|
185
|
+
const edits = modify(raw, [key], value, {
|
|
186
|
+
formattingOptions: { tabSize: 2, insertSpaces: true },
|
|
187
|
+
});
|
|
188
|
+
raw = applyEdits(raw, edits);
|
|
189
|
+
}
|
|
190
|
+
if (raw !== original) {
|
|
191
|
+
await fs.writeFile(configPath, raw, 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
const what = updates.map(([k]) => k).join(' + ');
|
|
194
|
+
return seeded
|
|
195
|
+
? `${UI_ALIGN_SKILL}/config.json seeded + ${what} filled`
|
|
196
|
+
: `${UI_ALIGN_SKILL}/config.json ${what} refreshed`;
|
|
197
|
+
}
|
|
119
198
|
async function installSkillsTo(skillsRoot, targetDir) {
|
|
120
199
|
let entries;
|
|
121
200
|
try {
|
|
@@ -166,7 +245,7 @@ async function installAgentsTo(agentsRoot, targetDir) {
|
|
|
166
245
|
}
|
|
167
246
|
return installed;
|
|
168
247
|
}
|
|
169
|
-
async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
248
|
+
async function installForEditor(editor, skillsRoot, agentsRoot, glmApiKey, hmosSdkDir, result) {
|
|
170
249
|
const marker = expandHome(editor.markerDir);
|
|
171
250
|
if (marker && !(await dirExists(marker))) {
|
|
172
251
|
result.skipped.push(`${editor.name} (not installed)`);
|
|
@@ -183,6 +262,17 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
|
183
262
|
catch (err) {
|
|
184
263
|
result.errors.push(`${editor.name} skills: ${err.message}`);
|
|
185
264
|
}
|
|
265
|
+
// Seed / refresh the incremental-ui-align per-skill config.json with the
|
|
266
|
+
// GLM key + SDK dir from ~/.hometrans/config.json (existing files: key-only update).
|
|
267
|
+
try {
|
|
268
|
+
const status = await refreshUiAlignConfig(skillsDir, glmApiKey, hmosSdkDir);
|
|
269
|
+
if (status) {
|
|
270
|
+
result.configured.push(`${editor.name} ${status}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
result.errors.push(`${editor.name} ${UI_ALIGN_SKILL}/config.json: ${err.message}`);
|
|
275
|
+
}
|
|
186
276
|
try {
|
|
187
277
|
const agents = await installAgentsTo(agentsRoot, agentsDir);
|
|
188
278
|
if (agents.length > 0) {
|
|
@@ -193,12 +283,185 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
|
193
283
|
result.errors.push(`${editor.name} agents: ${err.message}`);
|
|
194
284
|
}
|
|
195
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Display paths as full absolute OS-native paths (e.g. on Windows:
|
|
288
|
+
* `C:\Users\<you>\.claude\skills`) — no `~` abbreviation, no separator
|
|
289
|
+
* rewriting, so output matches what the user can copy into their shell.
|
|
290
|
+
*/
|
|
196
291
|
export function prettyHome(p) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
292
|
+
return p;
|
|
293
|
+
}
|
|
294
|
+
/** Locate a command on PATH (`where` on Windows, `which` elsewhere). */
|
|
295
|
+
function whichSync(cmd) {
|
|
296
|
+
const isWin = process.platform === 'win32';
|
|
297
|
+
try {
|
|
298
|
+
const out = execFileSync(isWin ? 'where' : 'which', [cmd], {
|
|
299
|
+
encoding: 'utf-8',
|
|
300
|
+
timeout: 5000,
|
|
301
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
302
|
+
});
|
|
303
|
+
const lines = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
304
|
+
return lines[0] || null;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function captureVersion(cmd, args) {
|
|
311
|
+
try {
|
|
312
|
+
const out = execFileSync(cmd, args, {
|
|
313
|
+
encoding: 'utf-8',
|
|
314
|
+
timeout: 5000,
|
|
315
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
316
|
+
});
|
|
317
|
+
return out.split('\n')[0].trim() || null;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function fileExists(p) {
|
|
324
|
+
try {
|
|
325
|
+
await fs.access(p);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** Install hints shown when a tool is missing. */
|
|
333
|
+
const TOOL_HINTS = {
|
|
334
|
+
deveco: 'set DEVECO_SDK_HOME via `ht init` (DevEco Studio install)',
|
|
335
|
+
adb: 'Android SDK platform-tools (needed for Android-device capture)',
|
|
336
|
+
hdc: 'ships with DevEco: <sdk>/default/openharmony/toolchains',
|
|
337
|
+
python: 'install Python >= 3.10 and add to PATH (UI-align capture/parse scripts)',
|
|
338
|
+
uv: 'https://docs.astral.sh/uv (AutoTest runs under uv)',
|
|
339
|
+
java: 'any JDK 17+, or reuse DevEco jbr: <deveco>/jbr/bin',
|
|
340
|
+
gitnexus: 'npm i -g gitnexus && gitnexus setup',
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Detect external tools. `hdc` falls back to the DevEco toolchains dir and
|
|
344
|
+
* `java` to DevEco's bundled jbr when not on PATH, mirroring how the
|
|
345
|
+
* skills/agents themselves resolve them.
|
|
346
|
+
*/
|
|
347
|
+
async function detectEnvironment(env) {
|
|
348
|
+
const status = new Map();
|
|
349
|
+
const isWin = process.platform === 'win32';
|
|
350
|
+
const sdkOk = env.DEVECO_SDK_HOME ? await dirExists(env.DEVECO_SDK_HOME) : false;
|
|
351
|
+
status.set('deveco', {
|
|
352
|
+
found: sdkOk,
|
|
353
|
+
path: sdkOk ? env.DEVECO_SDK_HOME : undefined,
|
|
354
|
+
});
|
|
355
|
+
for (const cmd of ['adb', 'python', 'uv', 'gitnexus']) {
|
|
356
|
+
const p = whichSync(cmd);
|
|
357
|
+
const st = { found: !!p, path: p ?? undefined };
|
|
358
|
+
if (cmd === 'python' && p) {
|
|
359
|
+
const v = captureVersion('python', ['--version']);
|
|
360
|
+
if (v) {
|
|
361
|
+
st.note = v;
|
|
362
|
+
const m = v.match(/(\d+)\.(\d+)/);
|
|
363
|
+
if (m && (Number(m[1]) < 3 || (Number(m[1]) === 3 && Number(m[2]) < 10))) {
|
|
364
|
+
st.note += ' (UI-align scripts require >= 3.10)';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
status.set(cmd, st);
|
|
369
|
+
}
|
|
370
|
+
// hdc: PATH first, then DevEco toolchains.
|
|
371
|
+
let hdcPath = whichSync('hdc');
|
|
372
|
+
if (!hdcPath && sdkOk) {
|
|
373
|
+
const cand = path.join(env.DEVECO_SDK_HOME, 'default', 'openharmony', 'toolchains', isWin ? 'hdc.exe' : 'hdc');
|
|
374
|
+
if (await fileExists(cand))
|
|
375
|
+
hdcPath = cand;
|
|
376
|
+
}
|
|
377
|
+
status.set('hdc', { found: !!hdcPath, path: hdcPath ?? undefined });
|
|
378
|
+
// java: PATH first, then DevEco jbr.
|
|
379
|
+
let javaPath = whichSync('java');
|
|
380
|
+
let javaNote;
|
|
381
|
+
if (!javaPath && env.DEVECO_PATH) {
|
|
382
|
+
const cand = path.join(env.DEVECO_PATH, 'jbr', 'bin', isWin ? 'java.exe' : 'java');
|
|
383
|
+
if (await fileExists(cand)) {
|
|
384
|
+
javaPath = cand;
|
|
385
|
+
javaNote = 'via DevEco jbr';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
status.set('java', { found: !!javaPath, path: javaPath ?? undefined, note: javaNote });
|
|
389
|
+
return status;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Which tools each skill/agent needs. `required` missing → BLOCKED;
|
|
393
|
+
* only `optional` missing → LIMITED. Kept in sync with the README
|
|
394
|
+
* "Per-skill environment requirements" matrix.
|
|
395
|
+
*/
|
|
396
|
+
const IMPACT_MATRIX = [
|
|
397
|
+
{ name: 'hmos-spec-generate', kind: 'skill', required: ['gitnexus'], optional: [] },
|
|
398
|
+
{ name: 'hmos-resources-convert', kind: 'skill', required: [], optional: ['java'], note: 'no java -> falls back to source res/' },
|
|
399
|
+
{ name: 'hmos-batch-ui-align', kind: 'skill', required: ['python'], optional: ['adb'], note: 'adb only for page exploration' },
|
|
400
|
+
{ name: 'hmos-incremental-ui-align', kind: 'skill', required: ['deveco', 'python', 'hdc', 'adb'], optional: [] },
|
|
401
|
+
{ name: 'hmos-integration-test', kind: 'skill', required: ['deveco', 'uv', 'hdc'], optional: [] },
|
|
402
|
+
{ name: 'hmos-fix-build-errors', kind: 'skill', required: ['deveco'], optional: [] },
|
|
403
|
+
{ name: 'hmos-convert-pipeline', kind: 'skill', required: ['deveco'], optional: ['uv', 'hdc'], note: 'test stage skippable via skip-test' },
|
|
404
|
+
{ name: 'logic-context-builder', kind: 'agent', required: [], optional: ['python'] },
|
|
405
|
+
{ name: 'logic-coder', kind: 'agent', required: [], optional: ['python'] },
|
|
406
|
+
{ name: 'spec-generator', kind: 'agent', required: ['gitnexus'], optional: [] },
|
|
407
|
+
{ name: 'build-fixer', kind: 'agent', required: ['deveco'], optional: [] },
|
|
408
|
+
{ name: 'code-reviewer', kind: 'agent', required: [], optional: ['deveco'] },
|
|
409
|
+
{ name: 'review-fixer', kind: 'agent', required: [], optional: [] },
|
|
410
|
+
{ name: 'self-tester', kind: 'agent', required: ['uv', 'hdc'], optional: [] },
|
|
411
|
+
{ name: 'self-test-fixer', kind: 'agent', required: [], optional: [] },
|
|
412
|
+
];
|
|
413
|
+
/** Print detected tools + the per-skill/agent impact table. */
|
|
414
|
+
export async function runEnvironmentCheck(env) {
|
|
415
|
+
console.log('');
|
|
416
|
+
console.log(chalk.blue(' Environment Check'));
|
|
417
|
+
const tools = await detectEnvironment(env);
|
|
418
|
+
const order = ['deveco', 'adb', 'hdc', 'python', 'uv', 'java', 'gitnexus'];
|
|
419
|
+
for (const name of order) {
|
|
420
|
+
const st = tools.get(name);
|
|
421
|
+
if (st.found) {
|
|
422
|
+
const note = st.note ? chalk.gray(` (${st.note})`) : '';
|
|
423
|
+
console.log(` ${chalk.green('+')} ${name.padEnd(9)} ${st.path ?? ''}${note}`);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
console.log(` ${chalk.yellow('!')} ${name.padEnd(9)} ${chalk.yellow('not found')} ${chalk.gray(TOOL_HINTS[name] ?? '')}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const missing = order.filter((n) => !tools.get(n).found);
|
|
430
|
+
console.log('');
|
|
431
|
+
console.log(chalk.blue(' Impact on skills/agents:'));
|
|
432
|
+
const nameW = 28;
|
|
433
|
+
console.log(chalk.gray(` ${'Name'.padEnd(nameW)} ${'Kind'.padEnd(6)} ${'Status'.padEnd(8)} Missing`));
|
|
434
|
+
for (const row of IMPACT_MATRIX) {
|
|
435
|
+
const missReq = row.required.filter((t) => missing.includes(t));
|
|
436
|
+
const missOpt = row.optional.filter((t) => missing.includes(t));
|
|
437
|
+
let plain;
|
|
438
|
+
let color;
|
|
439
|
+
let parts;
|
|
440
|
+
if (missReq.length > 0) {
|
|
441
|
+
plain = 'BLOCKED';
|
|
442
|
+
color = chalk.red;
|
|
443
|
+
parts = [...missReq, ...missOpt.map((t) => `${t}(optional)`)];
|
|
444
|
+
}
|
|
445
|
+
else if (missOpt.length > 0) {
|
|
446
|
+
plain = 'LIMITED';
|
|
447
|
+
color = chalk.yellow;
|
|
448
|
+
parts = missOpt.map((t) => `${t}(optional)`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
plain = 'OK';
|
|
452
|
+
color = chalk.green;
|
|
453
|
+
parts = [];
|
|
454
|
+
}
|
|
455
|
+
const noteStr = parts.length > 0 && row.note ? chalk.gray(` — ${row.note}`) : '';
|
|
456
|
+
console.log(` ${row.name.padEnd(nameW)} ${row.kind.padEnd(6)} ${color(plain.padEnd(8))} ${parts.join(', ')}${noteStr}`);
|
|
457
|
+
}
|
|
458
|
+
console.log('');
|
|
459
|
+
if (missing.length === 0) {
|
|
460
|
+
console.log(chalk.green(' All environment dependencies found — every skill/agent is fully usable.'));
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
console.log(chalk.gray(' Install the missing tools above and re-run `ht init` to clear the impact list.'));
|
|
200
464
|
}
|
|
201
|
-
return p.replace(/\\/g, '/');
|
|
202
465
|
}
|
|
203
466
|
async function detectInstalledEditors(editors) {
|
|
204
467
|
const status = new Map();
|
|
@@ -256,13 +519,26 @@ export async function initCommand(options = {}) {
|
|
|
256
519
|
{
|
|
257
520
|
type: 'checkbox',
|
|
258
521
|
name: 'selectedEditors',
|
|
259
|
-
message: 'Select editors to configure
|
|
522
|
+
message: 'Select editors to configure:',
|
|
260
523
|
choices,
|
|
524
|
+
// Append a "ctrl-c cancel" entry to the built-in key help line, reusing
|
|
525
|
+
// @inquirer/checkbox's default formatting (bold key · dim action · dim
|
|
526
|
+
// " • " separator):
|
|
527
|
+
// ↑↓ navigate • space select • a all • i invert • ⏎ submit • ctrl-c cancel
|
|
528
|
+
theme: {
|
|
529
|
+
style: {
|
|
530
|
+
keysHelpTip: (keys) => [...keys, ['ctrl-c', 'cancel']]
|
|
531
|
+
.map(([key, action]) => `${chalk.bold(key)} ${chalk.dim(action)}`)
|
|
532
|
+
.join(chalk.dim(' • ')),
|
|
533
|
+
},
|
|
534
|
+
},
|
|
261
535
|
},
|
|
262
536
|
]);
|
|
263
537
|
selectedEditors = answers.selectedEditors;
|
|
264
538
|
}
|
|
265
|
-
catch {
|
|
539
|
+
catch (err) {
|
|
540
|
+
if (isPromptAbort(err))
|
|
541
|
+
abortInit();
|
|
266
542
|
console.log(chalk.yellow('\n Interactive selection failed. Auto-selecting all detected editors.'));
|
|
267
543
|
console.log(chalk.gray(' Tip: use --all to skip interactive selection.\n'));
|
|
268
544
|
selectedEditors = editors
|
|
@@ -282,31 +558,72 @@ export async function initCommand(options = {}) {
|
|
|
282
558
|
const answers = await inquirer.prompt([
|
|
283
559
|
{
|
|
284
560
|
type: 'input',
|
|
285
|
-
name: '
|
|
286
|
-
message: '
|
|
287
|
-
default: config.env.
|
|
561
|
+
name: 'DEVECO_SDK_HOME',
|
|
562
|
+
message: 'DEVECO_SDK_HOME (DevEco Studio SDK dir):',
|
|
563
|
+
default: config.env.DEVECO_SDK_HOME || undefined,
|
|
288
564
|
},
|
|
289
565
|
{
|
|
290
566
|
type: 'input',
|
|
291
|
-
name: '
|
|
292
|
-
message: '
|
|
293
|
-
default: config.env.
|
|
567
|
+
name: 'TEST_API_KEY',
|
|
568
|
+
message: 'TEST_API_KEY (LLM API key used by the integration-test agent to run test cases):',
|
|
569
|
+
default: config.env.TEST_API_KEY || undefined,
|
|
294
570
|
},
|
|
295
571
|
{
|
|
296
572
|
type: 'input',
|
|
297
|
-
name: '
|
|
298
|
-
message: '
|
|
299
|
-
default: config.env.
|
|
573
|
+
name: 'GLM_API_KEY',
|
|
574
|
+
message: 'GLM_API_KEY (LLM API key for the GLM phone-agent used in UI alignment):',
|
|
575
|
+
default: config.env.GLM_API_KEY || undefined,
|
|
300
576
|
},
|
|
301
577
|
]);
|
|
302
|
-
|
|
303
|
-
config.env.
|
|
578
|
+
const sdk = deriveSdkPaths(answers.DEVECO_SDK_HOME);
|
|
579
|
+
config.env.DEVECO_SDK_HOME = sdk.DEVECO_SDK_HOME;
|
|
580
|
+
config.env.DEVECO_PATH = sdk.DEVECO_PATH;
|
|
581
|
+
config.env.OHOS_SDK_PATH = sdk.OHOS_SDK_PATH;
|
|
582
|
+
config.env.HMS_SDK_PATH = sdk.HMS_SDK_PATH;
|
|
304
583
|
config.env.TEST_API_KEY = answers.TEST_API_KEY.trim();
|
|
584
|
+
config.env.GLM_API_KEY = answers.GLM_API_KEY.trim();
|
|
305
585
|
await saveHomeTransConfig(config);
|
|
586
|
+
// Echo the derived paths and validate each exists on disk; missing ones are
|
|
587
|
+
// a strong signal of a typo'd DEVECO_SDK_HOME or a broken DevEco install.
|
|
588
|
+
if (sdk.DEVECO_SDK_HOME) {
|
|
589
|
+
const checks = [
|
|
590
|
+
['DEVECO_SDK_HOME', sdk.DEVECO_SDK_HOME],
|
|
591
|
+
['DEVECO_PATH', sdk.DEVECO_PATH],
|
|
592
|
+
['OHOS_SDK_PATH', sdk.OHOS_SDK_PATH],
|
|
593
|
+
['HMS_SDK_PATH', sdk.HMS_SDK_PATH],
|
|
594
|
+
];
|
|
595
|
+
console.log('');
|
|
596
|
+
console.log(chalk.blue(' Derived from DEVECO_SDK_HOME:'));
|
|
597
|
+
let missingCount = 0;
|
|
598
|
+
for (const [name, p] of checks) {
|
|
599
|
+
const exists = await dirExists(p);
|
|
600
|
+
if (!exists)
|
|
601
|
+
missingCount++;
|
|
602
|
+
const mark = exists ? chalk.green('+') : chalk.yellow('!');
|
|
603
|
+
const note = exists ? '' : chalk.yellow(' (not found on disk)');
|
|
604
|
+
console.log(` ${mark} ${name.padEnd(15)} : ${p}${note}`);
|
|
605
|
+
}
|
|
606
|
+
if (missingCount > 0) {
|
|
607
|
+
console.log('');
|
|
608
|
+
console.log(chalk.yellow(' ! Check DEVECO_SDK_HOME (it should be the "sdk" folder inside your DevEco Studio install)'));
|
|
609
|
+
console.log(chalk.yellow(' and re-run `ht init` after fixing it. Skills that build or'));
|
|
610
|
+
console.log(chalk.yellow(' review code will not work until these paths resolve.'));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
306
613
|
}
|
|
307
|
-
catch {
|
|
614
|
+
catch (err) {
|
|
615
|
+
if (isPromptAbort(err))
|
|
616
|
+
abortInit();
|
|
308
617
|
console.log(chalk.yellow(' Parameter prompts skipped (non-interactive mode).'));
|
|
309
618
|
}
|
|
619
|
+
// Detect external tools (adb / hdc / python / uv / java / gitnexus + DevEco)
|
|
620
|
+
// and report which skills/agents are impacted by anything missing.
|
|
621
|
+
try {
|
|
622
|
+
await runEnvironmentCheck(config.env);
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
console.log(chalk.yellow(` ! environment check skipped: ${err.message}`));
|
|
626
|
+
}
|
|
310
627
|
// Copy bundled tools/ into ~/.hometrans/tools and record env.TOOL_PATH.
|
|
311
628
|
// Must run before the autotest config step below, which seeds config.yaml
|
|
312
629
|
// into the installed tools dir (the location agents read via env.TOOL_PATH).
|
|
@@ -340,9 +657,13 @@ export async function initCommand(options = {}) {
|
|
|
340
657
|
console.log('');
|
|
341
658
|
const editorsToSetup = editors.filter((e) => selectedEditors.includes(e.name));
|
|
342
659
|
const result = { configured: [], skipped: [], errors: [] };
|
|
660
|
+
// hmos_sdk_dir consumed by incremental-ui-align = <DEVECO_SDK_HOME>/default.
|
|
661
|
+
const hmosSdkDir = config.env.DEVECO_SDK_HOME
|
|
662
|
+
? path.join(config.env.DEVECO_SDK_HOME, 'default')
|
|
663
|
+
: '';
|
|
343
664
|
for (const editor of editorsToSetup) {
|
|
344
665
|
console.log(chalk.blue(` Configuring ${editor.name}...`));
|
|
345
|
-
await installForEditor(editor, skillsRoot, agentsRoot, result);
|
|
666
|
+
await installForEditor(editor, skillsRoot, agentsRoot, config.env.GLM_API_KEY, hmosSdkDir, result);
|
|
346
667
|
}
|
|
347
668
|
await setupMcpForAllEditors(editorsToSetup, result);
|
|
348
669
|
console.log('');
|
package/dist/cli/mcp-setup.js
CHANGED
|
@@ -227,12 +227,8 @@ async function writeTomlSection(editor, result) {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
function prettyConfigPath(p) {
|
|
230
|
-
// 与 init.ts 中 prettyHome
|
|
231
|
-
|
|
232
|
-
if (home && p.startsWith(home)) {
|
|
233
|
-
return '~' + p.slice(home.length).replace(/\\/g, '/');
|
|
234
|
-
}
|
|
235
|
-
return p.replace(/\\/g, '/');
|
|
230
|
+
// 与 init.ts 中 prettyHome 行为一致:完整绝对路径、OS 原生分隔符,不做 ~ 缩写。
|
|
231
|
+
return p;
|
|
236
232
|
}
|
|
237
233
|
async function setupOneEditor(editor, result) {
|
|
238
234
|
const marker = expandHome(editor.markerDir);
|